Merge pull request #1741 from master3395/v2.5.5-dev

V2.5.5 dev
This commit is contained in:
Master3395
2026-03-24 20:33:27 +01:00
committed by GitHub
16 changed files with 1223 additions and 222 deletions

View File

@@ -0,0 +1,82 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Idempotent schema repair: add custom_quota_enabled and custom_quota_size to the
Django ftp.Users table (Meta.db_table = 'users') if missing.
Fixes MySQL 1054: Unknown column 'custom_quota_enabled' in 'INSERT INTO'
when creating FTP accounts after upgrading CyberPanel without a matching migration.
Usage:
CP_DIR=/usr/local/CyberCP python3 ensure_ftp_users_quota_columns.py
python3 ensure_ftp_users_quota_columns.py /usr/local/CyberCP
"""
from __future__ import annotations
import os
import sys
def main() -> int:
try:
if len(sys.argv) > 1 and sys.argv[1].strip():
cp_dir = os.path.abspath(sys.argv[1].strip())
else:
cp_dir = os.path.abspath(os.environ.get("CP_DIR", "/usr/local/CyberCP"))
if not os.path.isdir(cp_dir):
sys.stderr.write("ensure_ftp_users_quota_columns: CP directory not found: %s\n" % cp_dir)
return 1
sys.path.insert(0, cp_dir)
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "CyberCP.settings")
import django
django.setup()
from django.db import connection
table = "users"
alters = (
("custom_quota_enabled", "TINYINT(1) NOT NULL DEFAULT 0"),
("custom_quota_size", "INT NOT NULL DEFAULT 0"),
)
with connection.cursor() as cursor:
cursor.execute("SELECT DATABASE()")
row = cursor.fetchone()
dbname = row[0] if row else None
if not dbname:
sys.stderr.write(
"ensure_ftp_users_quota_columns: could not resolve current database name.\n"
)
return 1
for col, definition in alters:
cursor.execute(
"""
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s AND COLUMN_NAME = %s
""",
[dbname, table, col],
)
exists = cursor.fetchone()[0] > 0
if exists:
print("ensure_ftp_users_quota_columns: column %s already on %s; skipped." % (col, table))
continue
# identifiers are fixed literals; definition is controlled (no user input)
cursor.execute(
"ALTER TABLE `%s` ADD COLUMN `%s` %s" % (table, col, definition)
)
print("ensure_ftp_users_quota_columns: added column %s to %s." % (col, table))
print("ensure_ftp_users_quota_columns: done.")
return 0
except Exception as exc:
sys.stderr.write("ensure_ftp_users_quota_columns: error: %s\n" % (exc,))
return 1
if __name__ == "__main__":
sys.exit(main())

View File

@@ -0,0 +1,66 @@
#!/bin/bash
# Add missing custom_quota_enabled / custom_quota_size columns to ftp Users table (users).
# Fixes: (1054, "Unknown column 'custom_quota_enabled' in 'INSERT INTO'") on FTP account creation.
#
# Usage:
# sudo bash /home/cyberpanel-repo/deploy-ftp-users-custom-quota-columns.sh
# sudo bash deploy-ftp-users-custom-quota-columns.sh [REPO_DIR] [CP_DIR]
set -e
log() { echo "[$(date +%Y-%m-%d\ %H:%M:%S)] $*"; }
err() { log "ERROR: $*" >&2; }
# Resolve REPO_DIR
if [[ -n "$1" && -f "$1/CPScripts/ensure_ftp_users_quota_columns.py" ]]; then
REPO_DIR="$1"
shift
elif [[ -f "$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)/CPScripts/ensure_ftp_users_quota_columns.py" ]]; then
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
elif [[ -f "/home/cyberpanel-repo/CPScripts/ensure_ftp_users_quota_columns.py" ]]; then
REPO_DIR="/home/cyberpanel-repo"
elif [[ -f "./CPScripts/ensure_ftp_users_quota_columns.py" ]]; then
REPO_DIR="$(pwd)"
else
err "CPScripts/ensure_ftp_users_quota_columns.py not found."
exit 1
fi
CP_DIR="${1:-/usr/local/CyberCP}"
RESTART_LSCPD="${RESTART_LSCPD:-1}"
if [[ ! -d "$CP_DIR" ]]; then
err "CyberPanel directory not found: $CP_DIR"
exit 1
fi
log "REPO_DIR=$REPO_DIR"
log "CP_DIR=$CP_DIR"
mkdir -p "$CP_DIR/CPScripts"
cp -f "$REPO_DIR/CPScripts/ensure_ftp_users_quota_columns.py" "$CP_DIR/CPScripts/ensure_ftp_users_quota_columns.py"
chmod 644 "$CP_DIR/CPScripts/ensure_ftp_users_quota_columns.py"
log "Copied ensure_ftp_users_quota_columns.py to $CP_DIR/CPScripts/"
log "Ensuring FTP users table has custom quota columns..."
export CP_DIR
PY="$CP_DIR/bin/python"
if [[ -x "$PY" ]]; then
"$PY" "$CP_DIR/CPScripts/ensure_ftp_users_quota_columns.py" "$CP_DIR" || { err "Python repair failed"; exit 1; }
else
python3 "$CP_DIR/CPScripts/ensure_ftp_users_quota_columns.py" "$CP_DIR" || { err "Python repair failed"; exit 1; }
fi
if [[ "$RESTART_LSCPD" =~ ^(1|yes|true)$ ]]; then
if systemctl is-active --quiet lscpd 2>/dev/null; then
log "Restarting lscpd..."
systemctl restart lscpd || { err "lscpd restart failed"; exit 1; }
log "lscpd restarted."
else
log "lscpd not running or not a systemd service; skip restart."
fi
else
log "Skipping restart (set RESTART_LSCPD=1 to restart lscpd)."
fi
log "Deploy complete. Test: Websites → FTP → Create FTP Account."

View File

@@ -283,6 +283,7 @@ class FTPManager:
'custom_quota_enabled': items.custom_quota_enabled,
'custom_quota_size': items.custom_quota_size,
'package_quota': items.domain.package.diskSpace,
'acct_enabled': (str(items.status) == '1'),
}
if checker == 0:
@@ -330,6 +331,75 @@ class FTPManager:
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
def changeFTPDirectory(self):
"""
Change FTP account home directory after creation.
Uses listFTPAccounts permission (same as password / quota).
"""
try:
userID = self.request.session['userID']
currentACL = ACLManager.loadedACL(userID)
if ACLManager.currentContextPermission(currentACL, 'listFTPAccounts') == 0:
return ACLManager.loadErrorJson('changeDirectoryStatus', 0)
data = json.loads(self.request.body)
userName = data['ftpUserName']
selectedDomain = data['selectedDomain']
newPath = data.get('path', '')
if newPath is None:
newPath = ''
admin = Administrator.objects.get(pk=userID)
if ACLManager.checkOwnership(selectedDomain, admin, currentACL) != 1:
return ACLManager.loadErrorJson()
ftp = Users.objects.get(user=userName)
if currentACL['admin'] != 1 and ftp.domain.admin != admin:
return ACLManager.loadErrorJson()
result = FTPUtilities.changeFTPDirectory(userName, newPath, selectedDomain)
if result[0] == 1:
data_ret = {'status': 1, 'changeDirectoryStatus': 1, 'error_message': 'None'}
else:
data_ret = {'status': 0, 'changeDirectoryStatus': 0, 'error_message': result[1]}
return HttpResponse(json.dumps(data_ret), content_type='application/json')
except BaseException as msg:
data_ret = {'status': 0, 'changeDirectoryStatus': 0, 'error_message': str(msg)}
return HttpResponse(json.dumps(data_ret), content_type='application/json')
def setFTPAccountStatus(self):
"""Enable (enabled=true) or disable (enabled=false) FTP login for a user."""
try:
userID = self.request.session['userID']
currentACL = ACLManager.loadedACL(userID)
if ACLManager.currentContextPermission(currentACL, 'listFTPAccounts') == 0:
return ACLManager.loadErrorJson('setFTPStatusResult', 0)
data = json.loads(self.request.body)
userName = data['ftpUserName']
selectedDomain = data['selectedDomain']
enabled = bool(data.get('enabled', True))
admin = Administrator.objects.get(pk=userID)
if ACLManager.checkOwnership(selectedDomain, admin, currentACL) != 1:
return ACLManager.loadErrorJson()
ftp = Users.objects.get(user=userName)
if currentACL['admin'] != 1 and ftp.domain.admin != admin:
return ACLManager.loadErrorJson()
result = FTPUtilities.setFTPAccountStatus(userName, enabled, selectedDomain)
if result[0] == 1:
data_ret = {'status': 1, 'setFTPStatusResult': 1, 'error_message': 'None'}
else:
data_ret = {'status': 0, 'setFTPStatusResult': 0, 'error_message': result[1]}
return HttpResponse(json.dumps(data_ret), content_type='application/json')
except BaseException as msg:
data_ret = {'status': 0, 'setFTPStatusResult': 0, 'error_message': str(msg)}
return HttpResponse(json.dumps(data_ret), content_type='application/json')
def updateFTPQuota(self):
try:
userID = self.request.session['userID']

View File

@@ -33,4 +33,20 @@ class pluginManager:
@staticmethod
def postChangePassword(request, response):
return pluginManagerGlobal.globalPlug(request, postChangePassword, response)
return pluginManagerGlobal.globalPlug(request, postChangePassword, response)
@staticmethod
def preChangeFTPDirectory(request):
return pluginManagerGlobal.globalPlug(request, preChangeFTPDirectory)
@staticmethod
def postChangeFTPDirectory(request, response):
return pluginManagerGlobal.globalPlug(request, postChangeFTPDirectory, response)
@staticmethod
def preSetFTPAccountStatus(request):
return pluginManagerGlobal.globalPlug(request, preSetFTPAccountStatus)
@staticmethod
def postSetFTPAccountStatus(request, response):
return pluginManagerGlobal.globalPlug(request, postSetFTPAccountStatus, response)

View File

@@ -26,4 +26,11 @@ postSubmitFTPDelete = Signal()
preChangePassword = Signal()
## This event is fired after CyberPanel core finished deletion of child-domain
postChangePassword = Signal()
postChangePassword = Signal()
## Before / after changing FTP account home directory (list FTP page)
preChangeFTPDirectory = Signal()
postChangeFTPDirectory = Signal()
preSetFTPAccountStatus = Signal()
postSetFTPAccountStatus = Signal()

View File

@@ -6,14 +6,20 @@
/* Java script code to create account */
app.controller('createFTPAccount', function ($scope, $http) {
// Initialize all ng-hide variables to hide alerts on page load
$scope.ftpLoading = false;
$scope.ftpDetails = true;
$scope.canNotCreateFTP = true;
$scope.successfullyCreatedFTP = true;
$scope.couldNotConnect = true;
// Positive flags + ng-show: stay hidden until create flow sets them (avoids pre-bind ng-hide/undefined flash)
$scope.alertFtpCreateError = false;
$scope.alertFtpCreateSuccess = false;
$scope.alertFtpConnectFailed = false;
$scope.generatedPasswordView = true;
function resetFtpCreateAlerts() {
$scope.alertFtpCreateError = false;
$scope.alertFtpCreateSuccess = false;
$scope.alertFtpConnectFailed = false;
}
$(document).ready(function () {
$( ".ftpDetails, .account-details" ).hide();
$( ".ftpPasswordView" ).hide();
@@ -65,11 +71,11 @@ app.controller('createFTPAccount', function ($scope, $http) {
$scope.createFTPAccount = function () {
var submissionCompleted = false;
$scope.ftpLoading = true; // Show loading while creating
$scope.ftpDetails = false;
$scope.canNotCreateFTP = true;
$scope.successfullyCreatedFTP = true;
$scope.couldNotConnect = true;
resetFtpCreateAlerts();
var ftpDomain = $scope.ftpDomain;
var ftpUserName = $scope.ftpUserName;
@@ -89,9 +95,8 @@ app.controller('createFTPAccount', function ($scope, $http) {
var dangerousChars = /[;&|$`'"<>*?~]/;
if (dangerousChars.test(path)) {
$scope.ftpLoading = false;
$scope.canNotCreateFTP = false;
$scope.successfullyCreatedFTP = true;
$scope.couldNotConnect = true;
resetFtpCreateAlerts();
$scope.alertFtpCreateError = true;
$scope.errorMessage = "Invalid path: Path contains dangerous characters";
return;
}
@@ -99,22 +104,12 @@ app.controller('createFTPAccount', function ($scope, $http) {
// Check for path traversal attempts
if (path.indexOf("..") !== -1 || path.indexOf("~") !== -1) {
$scope.ftpLoading = false;
$scope.canNotCreateFTP = false;
$scope.successfullyCreatedFTP = true;
$scope.couldNotConnect = true;
resetFtpCreateAlerts();
$scope.alertFtpCreateError = true;
$scope.errorMessage = "Invalid path: Path cannot contain '..' or '~'";
return;
}
// Check if path starts with slash (should be relative)
if (path.startsWith("/")) {
$scope.ftpLoading = false;
$scope.canNotCreateFTP = false;
$scope.successfullyCreatedFTP = true;
$scope.couldNotConnect = true;
$scope.errorMessage = "Invalid path: Path must be relative (not starting with '/')";
return;
}
// Absolute paths under /home/... are allowed; server validates they stay inside the site home
}
var url = "/ftp/submitFTPCreation";
@@ -140,20 +135,20 @@ app.controller('createFTPAccount', function ($scope, $http) {
function ListInitialDatas(response) {
if (submissionCompleted) {
return;
}
submissionCompleted = true;
$scope.ftpLoading = false;
resetFtpCreateAlerts();
if (response.data && response.data.creatFTPStatus === 1) {
$scope.ftpLoading = false; // Hide loading on success
$scope.successfullyCreatedFTP = false;
$scope.canNotCreateFTP = true;
$scope.couldNotConnect = true;
$scope.alertFtpCreateSuccess = true;
$scope.createdFTPUsername = (response.data.createdFTPUsername != null && response.data.createdFTPUsername !== '') ? response.data.createdFTPUsername : (ftpDomain + '_' + ftpUserName);
if (typeof PNotify !== 'undefined') {
new PNotify({ title: 'Success!', text: 'FTP account successfully created.', type: 'success' });
}
} else {
$scope.ftpLoading = false;
$scope.canNotCreateFTP = false;
$scope.successfullyCreatedFTP = true;
$scope.couldNotConnect = true;
$scope.alertFtpCreateError = true;
$scope.errorMessage = (response.data && response.data.error_message) ? response.data.error_message : 'Unknown error';
if (typeof PNotify !== 'undefined') {
new PNotify({ title: 'Operation Failed!', text: $scope.errorMessage, type: 'error' });
@@ -162,14 +157,15 @@ app.controller('createFTPAccount', function ($scope, $http) {
}
function cantLoadInitialDatas(response) {
if (submissionCompleted) {
return;
}
submissionCompleted = true;
$scope.ftpLoading = false;
if ($scope.successfullyCreatedFTP !== false) {
$scope.couldNotConnect = false;
$scope.canNotCreateFTP = true;
$scope.successfullyCreatedFTP = true;
if (typeof PNotify !== 'undefined') {
new PNotify({ title: 'Operation Failed!', text: 'Could not connect to server, please refresh this page', type: 'error' });
}
resetFtpCreateAlerts();
$scope.alertFtpConnectFailed = true;
if (typeof PNotify !== 'undefined') {
new PNotify({ title: 'Operation Failed!', text: 'Could not connect to server, please refresh this page', type: 'error' });
}
}
@@ -177,9 +173,7 @@ app.controller('createFTPAccount', function ($scope, $http) {
};
$scope.hideFewDetails = function () {
$scope.successfullyCreatedFTP = true;
$scope.canNotCreateFTP = true;
$scope.couldNotConnect = true;
resetFtpCreateAlerts();
};
///
@@ -373,6 +367,7 @@ app.controller('listFTPAccounts', function ($scope, $http) {
$scope.ftpAccounts = true;
$scope.changePasswordBox = true;
$scope.quotaManagementBox = true;
$scope.directoryManagementBox = true;
$scope.notificationsBox = true;
var globalFTPUsername = "";
@@ -388,6 +383,8 @@ app.controller('listFTPAccounts', function ($scope, $http) {
$scope.couldNotConnect = true;
$scope.ftpLoading = false; // Don't show loading when opening password dialog
$scope.changePasswordBox = false;
$scope.quotaManagementBox = true;
$scope.directoryManagementBox = true;
$scope.notificationsBox = true;
$scope.ftpUsername = ftpUsername;
globalFTPUsername = ftpUsername;
@@ -452,6 +449,8 @@ app.controller('listFTPAccounts', function ($scope, $http) {
$scope.ftpLoading = true; // Show loading while fetching
$scope.ftpAccounts = true;
$scope.changePasswordBox = true;
$scope.quotaManagementBox = true;
$scope.directoryManagementBox = true;
var selectedDomain = $scope.selectedDomain;
@@ -477,7 +476,11 @@ app.controller('listFTPAccounts', function ($scope, $http) {
if (response.data.fetchStatus == 1) {
$scope.records = JSON.parse(response.data.data);
angular.forEach($scope.records, function (r) {
if (typeof r.acct_enabled === 'undefined') {
r.acct_enabled = true;
}
});
$scope.notificationsBox = false;
$scope.recordsFetched = false;
@@ -487,6 +490,8 @@ app.controller('listFTPAccounts', function ($scope, $http) {
$scope.ftpLoading = false; // Hide loading when done
$scope.ftpAccounts = false;
$scope.changePasswordBox = true;
$scope.quotaManagementBox = true;
$scope.directoryManagementBox = true;
$scope.domainFeteched = $scope.selectedDomain;
@@ -499,6 +504,8 @@ app.controller('listFTPAccounts', function ($scope, $http) {
$scope.ftpLoading = false; // Hide loading on error
$scope.ftpAccounts = true;
$scope.changePasswordBox = true;
$scope.quotaManagementBox = true;
$scope.directoryManagementBox = true;
$scope.errorMessage = response.data.error_message;
}
@@ -514,6 +521,8 @@ app.controller('listFTPAccounts', function ($scope, $http) {
$scope.ftpLoading = false; // Hide loading on connection error
$scope.ftpAccounts = true;
$scope.changePasswordBox = true;
$scope.quotaManagementBox = true;
$scope.directoryManagementBox = true;
}
@@ -540,6 +549,8 @@ app.controller('listFTPAccounts', function ($scope, $http) {
$scope.canNotChangePassword = true;
$scope.couldNotConnect = true;
$scope.ftpLoading = false;
$scope.changePasswordBox = true;
$scope.directoryManagementBox = true;
$scope.quotaManagementBox = false;
$scope.notificationsBox = true;
$scope.ftpUsername = record.user;
@@ -628,6 +639,128 @@ app.controller('listFTPAccounts', function ($scope, $http) {
}
};
$scope.manageDirectory = function (record) {
$scope.recordsFetched = true;
$scope.passwordChanged = true;
$scope.canNotChangePassword = true;
$scope.couldNotConnect = true;
$scope.ftpLoading = false;
$scope.changePasswordBox = true;
$scope.quotaManagementBox = true;
$scope.directoryManagementBox = false;
$scope.notificationsBox = true;
$scope.ftpUsername = record.user;
globalFTPUsername = record.user;
$scope.ftpPathEdit = record.dir || '';
};
$scope.changeDirectoryBtn = function () {
$scope.ftpLoading = true;
var url = "/ftp/changeFTPDirectory";
var pathVal = $scope.ftpPathEdit;
if (typeof pathVal === 'undefined' || pathVal === null) {
pathVal = '';
} else {
pathVal = String(pathVal).trim();
}
var data = {
ftpUserName: globalFTPUsername,
selectedDomain: $scope.selectedDomain,
path: pathVal
};
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, data, config).then(ListInitialDatas, cantLoadInitialDatas);
function ListInitialDatas(response) {
if (response.data.changeDirectoryStatus === 1) {
$scope.ftpLoading = false;
$scope.directoryManagementBox = true;
if (typeof PNotify !== 'undefined') {
new PNotify({
title: 'Success!',
text: 'FTP directory updated successfully.',
type: 'success'
});
}
populateCurrentRecords();
} else {
$scope.ftpLoading = false;
$scope.errorMessage = response.data.error_message;
if (typeof PNotify !== 'undefined') {
new PNotify({
title: 'Error!',
text: response.data.error_message,
type: 'error'
});
}
}
}
function cantLoadInitialDatas() {
$scope.ftpLoading = false;
$scope.couldNotConnect = false;
if (typeof PNotify !== 'undefined') {
new PNotify({
title: 'Error!',
text: 'Could not connect to server.',
type: 'error'
});
}
}
};
$scope.setFtpAccountStatus = function (record, enabled) {
$scope.ftpLoading = true;
var url = "/ftp/setFTPAccountStatus";
var data = {
ftpUserName: record.user,
selectedDomain: $scope.selectedDomain,
enabled: !!enabled
};
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, data, config).then(function (response) {
$scope.ftpLoading = false;
if (response.data.setFTPStatusResult === 1) {
record.acct_enabled = !!enabled;
if (typeof PNotify !== 'undefined') {
new PNotify({
title: 'Success!',
text: enabled ? 'FTP account enabled.' : 'FTP account disabled.',
type: 'success'
});
}
populateCurrentRecords();
} else {
$scope.errorMessage = response.data.error_message;
if (typeof PNotify !== 'undefined') {
new PNotify({
title: 'Error!',
text: response.data.error_message,
type: 'error'
});
}
}
}, function () {
$scope.ftpLoading = false;
$scope.couldNotConnect = false;
if (typeof PNotify !== 'undefined') {
new PNotify({
title: 'Error!',
text: 'Could not connect to server.',
type: 'error'
});
}
});
};
});

View File

@@ -415,9 +415,14 @@
.package-quota-info {
margin-top: 1rem;
}
/* Hide Angular root until first digest (avoids inverted ng-hide flashing all alerts) */
[ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak {
display: none !important;
}
</style>
<div class="modern-container" ng-controller="createFTPAccount">
<div class="modern-container" ng-controller="createFTPAccount" ng-cloak>
<div class="page-header">
<h1 class="page-title">
<i class="fas fa-folder-plus"></i>
@@ -564,26 +569,26 @@
</div>
<div ng-hide="ftpDetails" class="form-group">
<label class="form-label">{% trans "Path (Relative)" %}</label>
<label class="form-label">{% trans "FTP home path" %}</label>
<div class="path-info">
<i class="fas fa-folder"></i>
{% trans "Leave empty to use the website's home directory, or specify a subdirectory" %}
{% trans "Leave empty for the website's home directory. Use a subdirectory, or the full path under that home." %}
<br>
<small style="margin-top: 0.5rem; display: block; color: var(--text-secondary, #64748b);">
<i class="fas fa-info-circle"></i>
<strong>{% trans "Examples:" %}</strong> {% trans "docs, public_html, uploads, api" %}
<strong>{% trans "Examples:" %}</strong> {% trans "public_html, uploads, or /home/yourdomain.com/public_html" %}
<br>
<i class="fas fa-shield-alt"></i>
{% trans "Security: Path will be restricted to this subdirectory only" %}
{% trans "Security: FTP will be restricted to this directory only (must stay inside the site home)" %}
</small>
</div>
<input placeholder="{% trans 'e.g., docs or public_html (leave empty for home directory)' %}"
<input placeholder="{% trans 'e.g. public_html or full path under site home (optional)' %}"
type="text" class="form-control" ng-model="ftpPath"
pattern="^[a-zA-Z0-9._/-]+$"
title="{% trans 'Only letters, numbers, dots, underscores, hyphens, and forward slashes allowed' %}">
<small style="color: var(--text-secondary, #64748b); margin-top: 0.5rem; display: block;">
<i class="fas fa-exclamation-triangle"></i>
{% trans "Do not use: .. ~ / or special characters like ; | & $ ` ' \" < > * ?" %}
{% trans "Do not use: .. ~ or special characters like ; | & $ ` ' \" < > * ?" %}
</small>
</div>
@@ -631,19 +636,19 @@
</div>
</div>
<!-- Alert Messages -->
<!-- Alert Messages (ng-show + false defaults: only after submit / API outcome) -->
<div class="form-section">
<div ng-hide="canNotCreateFTP" class="alert alert-danger">
<div ng-show="alertFtpCreateError" class="alert alert-danger">
<i class="fas fa-exclamation-circle"></i>
{% trans "Cannot create FTP account. Error message:" %} {$ errorMessage $}
</div>
<div ng-hide="successfullyCreatedFTP" class="alert alert-success">
<div ng-show="alertFtpCreateSuccess" class="alert alert-success">
<i class="fas fa-check-circle"></i>
{% trans "FTP account successfully created with username:" %} <strong>{$ createdFTPUsername $}</strong>
</div>
<div ng-hide="couldNotConnect" class="alert alert-danger">
<div ng-show="alertFtpConnectFailed" class="alert alert-danger">
<i class="fas fa-times-circle"></i>
{% trans "Could not connect to server. Please refresh this page." %}
</div>

View File

@@ -301,6 +301,10 @@
font-size: 0.875rem;
font-weight: 500;
}
.ftp-row-disabled td {
opacity: 0.75;
}
@keyframes spin {
to { transform: rotate(360deg); }
@@ -541,25 +545,58 @@
</div>
</div>
<!-- Change FTP home directory -->
<div ng-hide="directoryManagementBox" class="password-section">
<h3><i class="fas fa-folder-open"></i> {% trans "FTP home directory for" %} {$ ftpUsername $}</h3>
<div class="row">
<div class="col-md-8">
<div class="form-group">
<label class="form-label">{% trans "Path" %}</label>
<div class="path-info" style="margin-bottom: 0.75rem;">
<i class="fas fa-info-circle"></i>
{% trans "Leave empty to use this site's home directory. You may enter a subdirectory or the full path under that home (no path traversal)." %}
</div>
<input type="text" class="form-control" ng-model="ftpPathEdit"
placeholder="{% trans 'e.g. public_html/sub or full path under site home' %}">
</div>
<div style="margin-top: 1rem;">
<button type="button" ng-click="changeDirectoryBtn()" class="btn-primary">
<i class="fas fa-save"></i>
{% trans "Update Directory" %}
</button>
</div>
</div>
</div>
</div>
<!-- FTP Accounts Table -->
<div ng-hide="ftpAccounts">
<table class="ftp-table">
<thead>
<tr>
<th style="width: 8%;">{% trans "ID" %}</th>
<th style="width: 20%;">{% trans "User Name" %}</th>
<th style="width: 30%;">{% trans "Directory" %}</th>
<th style="width: 15%;">{% trans "Quota" %}</th>
<th style="width: 27%; text-align: center;">{% trans "Actions" %}</th>
<th style="width: 7%;">{% trans "ID" %}</th>
<th style="width: 16%;">{% trans "User Name" %}</th>
<th style="width: 10%;">{% trans "Status" %}</th>
<th style="width: 24%;">{% trans "Directory" %}</th>
<th style="width: 12%;">{% trans "Quota" %}</th>
<th style="width: 31%; text-align: center;">{% trans "Actions" %}</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="record in records track by $index">
<tr ng-repeat="record in records track by $index" ng-class="{'ftp-row-disabled': !record.acct_enabled}">
<td><strong ng-bind="record.id"></strong></td>
<td>
<i class="fas fa-user-circle" style="color: var(--accent-color, #5b5fcf); margin-right: 0.5rem;"></i>
<span ng-bind="record.user"></span>
</td>
<td>
<span class="quota-badge" ng-show="record.acct_enabled" style="background: rgba(34,197,94,0.15); color: #16a34a;">
<i class="fas fa-check-circle"></i> {% trans "Enabled" %}
</span>
<span class="quota-badge" ng-hide="record.acct_enabled" style="background: rgba(239,68,68,0.12); color: #dc2626;">
<i class="fas fa-ban"></i> {% trans "Disabled" %}
</span>
</td>
<td>
<span class="directory-badge">
<i class="fas fa-folder"></i>
@@ -579,6 +616,18 @@
<i class="fas fa-hdd"></i>
{% trans "Quota" %}
</button>
<button type="button" ng-click="manageDirectory(record)" class="btn-action" title="{% trans 'Change which directory this FTP user is locked to' %}">
<i class="fas fa-folder-open"></i>
{% trans "Directory" %}
</button>
<button type="button" ng-click="setFtpAccountStatus(record, false)" ng-show="record.acct_enabled" class="btn-action" style="opacity: 0.95;" title="{% trans 'Block FTP login without deleting the account' %}">
<i class="fas fa-user-slash"></i>
{% trans "Disable" %}
</button>
<button type="button" ng-click="setFtpAccountStatus(record, true)" ng-hide="record.acct_enabled" class="btn-action" title="{% trans 'Allow FTP login again' %}">
<i class="fas fa-user-check"></i>
{% trans "Enable" %}
</button>
</div>
</td>
</tr>

View File

@@ -15,6 +15,8 @@ urlpatterns = [
path('listFTPAccounts', views.listFTPAccounts, name='listFTPAccounts'),
path('getAllFTPAccounts', views.getAllFTPAccounts, name='getAllFTPAccounts'),
path('changePassword', views.changePassword, name='changePassword'),
path('changeFTPDirectory', views.changeFTPDirectory, name='changeFTPDirectory'),
path('setFTPAccountStatus', views.setFTPAccountStatus, name='setFTPAccountStatus'),
path('updateFTPQuota', views.updateFTPQuota, name='updateFTPQuota'),
path('getFTPQuotaUsage', views.getFTPQuotaUsage, name='getFTPQuotaUsage'),
path('migrateFTPQuotas', views.migrateFTPQuotas, name='migrateFTPQuotas'),

View File

@@ -255,6 +255,42 @@ def changePassword(request):
except KeyError:
return redirect(loadLoginPage)
def changeFTPDirectory(request):
try:
result = pluginManager.preChangeFTPDirectory(request)
if result != 200:
return result
fm = FTPManager(request)
coreResult = fm.changeFTPDirectory()
result = pluginManager.postChangeFTPDirectory(request, coreResult)
if result != 200:
return result
return coreResult
except KeyError:
return redirect(loadLoginPage)
def setFTPAccountStatus(request):
try:
result = pluginManager.preSetFTPAccountStatus(request)
if result != 200:
return result
fm = FTPManager(request)
coreResult = fm.setFTPAccountStatus()
result = pluginManager.postSetFTPAccountStatus(request, coreResult)
if result != 200:
return result
return coreResult
except KeyError:
return redirect(loadLoginPage)
def updateFTPQuota(request):
try:
fm = FTPManager(request)

View File

@@ -3,12 +3,12 @@ MYSQLPort 3306
MYSQLSocket /var/lib/mysql/mysql.sock
MYSQLDatabase cyberpanel
MYSQLCrypt md5
MYSQLGetDir SELECT Dir FROM users WHERE User='\L'
MYSQLGetGID SELECT Gid FROM users WHERE User='\L'
MYSQLGetPW SELECT Password FROM users WHERE User='\L'
MYSQLGetUID SELECT Uid FROM users WHERE User='\L'
# Quota enforcement queries
MYSQLGetQTAFS SELECT QuotaSize FROM users WHERE User='\L'
MYSQLGetQTAUS SELECT 0 FROM users WHERE User='\L'
# Only accounts with Status='1' can authenticate (disabled = Status '0')
MYSQLGetDir SELECT Dir FROM users WHERE User='\L' AND Status='1'
MYSQLGetGID SELECT Gid FROM users WHERE User='\L' AND Status='1'
MYSQLGetPW SELECT Password FROM users WHERE User='\L' AND Status='1'
MYSQLGetUID SELECT Uid FROM users WHERE User='\L' AND Status='1'
MYSQLGetQTAFS SELECT QuotaSize FROM users WHERE User='\L' AND Status='1'
MYSQLGetQTAUS SELECT 0 FROM users WHERE User='\L' AND Status='1'
MYSQLPassword 1qaz@9xvps
MYSQLUser cyberpanel

View File

@@ -3,12 +3,13 @@ MYSQLPort 3307
MYSQLSocket /var/lib/mysql1/mysql.sock
MYSQLDatabase cyberpanel
MYSQLCrypt md5
MYSQLGetDir SELECT Dir FROM users WHERE User='\L'
MYSQLGetGID SELECT Gid FROM users WHERE User='\L'
MYSQLGetPW SELECT Password FROM users WHERE User='\L'
MYSQLGetUID SELECT Uid FROM users WHERE User='\L'
# Only accounts with Status='1' can authenticate (disabled = Status '0')
MYSQLGetDir SELECT Dir FROM users WHERE User='\L' AND Status='1'
MYSQLGetGID SELECT Gid FROM users WHERE User='\L' AND Status='1'
MYSQLGetPW SELECT Password FROM users WHERE User='\L' AND Status='1'
MYSQLGetUID SELECT Uid FROM users WHERE User='\L' AND Status='1'
# Quota enforcement queries
MYSQLGetQTAFS SELECT QuotaSize FROM users WHERE User='\L'
MYSQLGetQTAUS SELECT 0 FROM users WHERE User='\L'
MYSQLGetQTAFS SELECT QuotaSize FROM users WHERE User='\L' AND Status='1'
MYSQLGetQTAUS SELECT 0 FROM users WHERE User='\L' AND Status='1'
MYSQLPassword 1qaz@9xvps
MYSQLUser cyberpanel

View File

@@ -22,6 +22,57 @@ from plogical.processUtilities import ProcessUtilities
class FTPUtilities:
@staticmethod
def get_domain_home_directory(domain_name):
"""
Filesystem root for the selected site: primary domain /home/<domain>,
or child domain /home/<master>/<child.path> per CyberPanel vhost layout.
"""
try:
child = ChildDomains.objects.select_related('master').get(domain=domain_name)
master_dom = child.master.domain
rel = (child.path or '').strip().strip('/')
if rel:
return os.path.abspath('/home/%s/%s' % (master_dom, rel))
except ChildDomains.DoesNotExist:
pass
return os.path.abspath('/home/' + domain_name)
@staticmethod
def assert_ftp_raw_path_safe(raw):
"""Reject shell metacharacters and obvious traversal markers in user input."""
if raw is None or not str(raw).strip():
return
s = str(raw)
dangerous_chars = [';', '|', '&', '$', '`', '\'', '"', '<', '>', '*', '?']
if any(char in s for char in dangerous_chars):
raise BaseException("Invalid path: Path contains dangerous characters")
if '..' in s or '~' in s:
raise BaseException("Invalid path: Path cannot contain '..' or '~'")
@staticmethod
def resolve_ftp_home_path(domain_name, raw_path):
"""
Resolve FTP home directory under domain_name.
Empty / None / 'None' -> domain document root only.
Absolute paths are allowed if they resolve under that root (no /home duplication).
"""
domain_home = FTPUtilities.get_domain_home_directory(domain_name)
if raw_path is None:
return domain_home
raw = str(raw_path).strip()
if raw == '' or raw == 'None':
return domain_home
FTPUtilities.assert_ftp_raw_path_safe(raw)
if raw.startswith('/'):
candidate = os.path.abspath(raw)
else:
candidate = os.path.abspath(os.path.join(domain_home, raw))
dh = domain_home
if candidate != dh and not candidate.startswith(dh + os.sep):
raise BaseException("Security violation: Path must be within domain home directory")
return candidate
@staticmethod
def createNewFTPAccount(udb,upass,username,password,path):
try:
@@ -143,37 +194,14 @@ class FTPUtilities:
## gid , uid ends
# Enhanced path validation and handling
if path and path.strip() and path != 'None':
# Clean the path
path = path.strip().lstrip("/")
# Additional security checks
if path.find("..") > -1 or path.find("~") > -1 or path.startswith("/"):
raise BaseException("Invalid path: Path must be relative and not contain '..' or '~' or start with '/'")
# Check for dangerous characters
dangerous_chars = [';', '|', '&', '$', '`', '\'', '"', '<', '>', '*', '?']
if any(char in path for char in dangerous_chars):
raise BaseException("Invalid path: Path contains dangerous characters")
# Construct full path
full_path = "/home/" + domainName + "/" + path
# Additional security: ensure path is within domain directory
domain_home = "/home/" + domainName
if not os.path.abspath(full_path).startswith(os.path.abspath(domain_home)):
raise BaseException("Security violation: Path must be within domain directory")
result = FTPUtilities.ftpFunctions(full_path, externalApp)
if result[0] == 1:
path = full_path
else:
# Path: empty -> domain home; relative or absolute under domain home (no duplicate /home/... prefix)
if path and str(path).strip() and str(path).strip() != 'None':
path = FTPUtilities.resolve_ftp_home_path(domainName, path)
result = FTPUtilities.ftpFunctions(path, externalApp)
if result[0] != 1:
raise BaseException("Path validation failed: " + result[1])
else:
path = "/home/" + domainName
path = FTPUtilities.get_domain_home_directory(domainName)
# Enhanced symlink handling
if os.path.islink(path):
@@ -251,6 +279,63 @@ class FTPUtilities:
except BaseException as msg:
return 0, str(msg)
@staticmethod
def changeFTPDirectory(userName, raw_path, selected_domain):
"""
Update FTP user home directory after creation. selected_domain must match
the master website domain for this account (same as list FTP dropdown).
"""
try:
website = Websites.objects.get(domain=selected_domain)
ftp = Users.objects.get(user=userName)
if ftp.domain_id != website.id:
raise BaseException("FTP user does not belong to the selected domain")
externalApp = website.externalApp
resolved = FTPUtilities.resolve_ftp_home_path(selected_domain, raw_path)
if os.path.islink(resolved):
logging.CyberCPLogFileWriter.writeToFile(
"FTP path is symlinked: %s" % resolved)
raise BaseException("Cannot set FTP directory: Path is a symbolic link")
result = FTPUtilities.ftpFunctions(resolved, externalApp)
if result[0] != 1:
raise BaseException("Path validation failed: " + result[1])
ftp.dir = resolved
ftp.save()
return 1, None
except Users.DoesNotExist:
return 0, "FTP user not found"
except Websites.DoesNotExist:
return 0, "Domain not found"
except BaseException as msg:
logging.CyberCPLogFileWriter.writeToFile(str(msg) + " [changeFTPDirectory]")
return 0, str(msg)
@staticmethod
def setFTPAccountStatus(userName, enabled, selected_domain):
"""
Enable or disable FTP login (Status '1' / '0'). Pure-FTPd must use
MySQL queries that include AND Status='1' for authentication.
"""
try:
website = Websites.objects.get(domain=selected_domain)
ftp = Users.objects.get(user=userName)
if ftp.domain_id != website.id:
raise BaseException("FTP user does not belong to the selected domain")
ftp.status = '1' if enabled else '0'
ftp.save()
return 1, None
except Users.DoesNotExist:
return 0, "FTP user not found"
except Websites.DoesNotExist:
return 0, "Domain not found"
except BaseException as msg:
logging.CyberCPLogFileWriter.writeToFile(str(msg) + " [setFTPAccountStatus]")
return 0, str(msg)
@staticmethod
def changeFTPPassword(userName, password):
try:

View File

@@ -6,14 +6,25 @@
/* Java script code to create account */
app.controller('createFTPAccount', function ($scope, $http) {
$scope.ftpLoading = false;
$scope.ftpDetails = true;
// Positive flags + ng-show: stay hidden until create flow sets them (avoids pre-bind ng-hide/undefined flash)
$scope.alertFtpCreateError = false;
$scope.alertFtpCreateSuccess = false;
$scope.alertFtpConnectFailed = false;
$scope.generatedPasswordView = true;
function resetFtpCreateAlerts() {
$scope.alertFtpCreateError = false;
$scope.alertFtpCreateSuccess = false;
$scope.alertFtpConnectFailed = false;
}
$(document).ready(function () {
$( ".ftpDetails, .account-details" ).hide();
$( ".ftpPasswordView" ).hide();
// Only use select2 if it's actually a function (avoids errors when Rocket Loader defers scripts)
if (typeof $ !== 'undefined' && $ && typeof $.fn !== 'undefined' && typeof $.fn.select2 === 'function') {
try {
var $sel = $('.create-ftp-acct-select');
@@ -21,22 +32,33 @@ app.controller('createFTPAccount', function ($scope, $http) {
$sel.select2();
$sel.on('select2:select', function (e) {
var data = e.params.data;
$scope.ftpDomain = data.text;
$scope.ftpDetails = false;
$scope.$apply();
$scope.$evalAsync(function () {
$scope.ftpDomain = data.text;
$scope.ftpDetails = false;
});
$(".ftpDetails, .account-details").show();
});
} else {
initNativeSelect();
}
} catch (err) {}
} catch (err) {
initNativeSelect();
}
} else {
initNativeSelect();
}
function initNativeSelect() {
$('.create-ftp-acct-select').off('select2:select').on('change', function () {
var val = $(this).val();
$scope.$evalAsync(function () {
$scope.ftpDomain = val;
$scope.ftpDetails = (val && val !== '') ? false : true;
});
$(".ftpDetails, .account-details").show();
});
}
$('.create-ftp-acct-select').off('select2:select').on('change', function () {
$scope.ftpDomain = $(this).val();
$scope.ftpDetails = ($scope.ftpDomain && $scope.ftpDomain !== '') ? false : true;
$scope.$apply();
$(".ftpDetails, .account-details").show();
});
});
$scope.showFTPDetails = function() {
if ($scope.ftpDomain && $scope.ftpDomain !== "") {
$scope.ftpDetails = false;
@@ -49,19 +71,45 @@ app.controller('createFTPAccount', function ($scope, $http) {
$scope.createFTPAccount = function () {
$scope.ftpLoading = false;
var submissionCompleted = false;
$scope.ftpLoading = true; // Show loading while creating
$scope.ftpDetails = false;
$scope.canNotCreate = true;
$scope.successfullyCreated = true;
$scope.couldNotConnect = true;
resetFtpCreateAlerts();
var ftpDomain = $scope.ftpDomain;
var ftpUserName = $scope.ftpUserName;
var ftpPassword = $scope.ftpPassword;
var path = $scope.ftpPath;
if (typeof path === 'undefined') {
// Enhanced path validation
if (typeof path === 'undefined' || path === null) {
path = "";
} else {
path = path.trim();
}
// Client-side path validation
if (path && path !== "") {
// Check for dangerous characters
var dangerousChars = /[;&|$`'"<>*?~]/;
if (dangerousChars.test(path)) {
$scope.ftpLoading = false;
resetFtpCreateAlerts();
$scope.alertFtpCreateError = true;
$scope.errorMessage = "Invalid path: Path contains dangerous characters";
return;
}
// Check for path traversal attempts
if (path.indexOf("..") !== -1 || path.indexOf("~") !== -1) {
$scope.ftpLoading = false;
resetFtpCreateAlerts();
$scope.alertFtpCreateError = true;
$scope.errorMessage = "Invalid path: Path cannot contain '..' or '~'";
return;
}
// Absolute paths under /home/... are allowed; server validates they stay inside the site home
}
var url = "/ftp/submitFTPCreation";
@@ -71,7 +119,10 @@ app.controller('createFTPAccount', function ($scope, $http) {
ftpDomain: ftpDomain,
ftpUserName: ftpUserName,
passwordByPass: ftpPassword,
path: path,
path: path || '',
api: '0',
enableCustomQuota: $scope.enableCustomQuota || false,
customQuotaSize: $scope.customQuotaSize || 0,
};
var config = {
@@ -84,60 +135,65 @@ app.controller('createFTPAccount', function ($scope, $http) {
function ListInitialDatas(response) {
if (response.data.creatFTPStatus === 1) {
$scope.ftpLoading = true;
new PNotify({
title: 'Success!',
text: 'FTP account successfully created.',
type: 'success'
});
} else {
$scope.ftpLoading = true;
new PNotify({
title: 'Operation Failed!',
text: response.data.error_message,
type: 'error'
});
if (submissionCompleted) {
return;
}
submissionCompleted = true;
$scope.ftpLoading = false;
resetFtpCreateAlerts();
if (response.data && response.data.creatFTPStatus === 1) {
$scope.alertFtpCreateSuccess = true;
$scope.createdFTPUsername = (response.data.createdFTPUsername != null && response.data.createdFTPUsername !== '') ? response.data.createdFTPUsername : (ftpDomain + '_' + ftpUserName);
if (typeof PNotify !== 'undefined') {
new PNotify({ title: 'Success!', text: 'FTP account successfully created.', type: 'success' });
}
} else {
$scope.alertFtpCreateError = true;
$scope.errorMessage = (response.data && response.data.error_message) ? response.data.error_message : 'Unknown error';
if (typeof PNotify !== 'undefined') {
new PNotify({ title: 'Operation Failed!', text: $scope.errorMessage, type: 'error' });
}
}
}
function cantLoadInitialDatas(response) {
$scope.ftpLoading = true;
new PNotify({
title: 'Operation Failed!',
text: 'Could not connect to server, please refresh this page',
type: 'error'
});
if (submissionCompleted) {
return;
}
submissionCompleted = true;
$scope.ftpLoading = false;
resetFtpCreateAlerts();
$scope.alertFtpConnectFailed = true;
if (typeof PNotify !== 'undefined') {
new PNotify({ title: 'Operation Failed!', text: 'Could not connect to server, please refresh this page', type: 'error' });
}
}
};
$scope.hideFewDetails = function () {
$scope.successfullyCreated = true;
resetFtpCreateAlerts();
};
///
$scope.generatePassword = function () {
$( ".ftpPasswordView" ).show();
$(".ftpPasswordView").show();
$scope.generatedPasswordView = false;
$scope.ftpPassword = randomPassword(16);
};
$scope.usePassword = function () {
$(".ftpPasswordView" ).hide();
$(".ftpPasswordView").hide();
$scope.generatedPasswordView = true;
};
// Quota management functions
$scope.toggleCustomQuota = function() {
if (!$scope.enableCustomQuota) {
$scope.customQuotaSize = 0;
}
};
});
@@ -199,32 +255,24 @@ app.controller('deleteFTPAccount', function ($scope, $http) {
} else {
$scope.ftpAccountsOfDomain = true;
$scope.deleteFTPButton = true;
$scope.deleteFailure = true;
$scope.deleteFailure = false;
$scope.deleteSuccess = true;
$scope.couldNotConnect = false;
$scope.couldNotConnect = true;
$scope.deleteFTPButtonInit = true;
$scope.errorMessage = (response.data && (response.data.error_message || response.data.errorMessage)) || 'Unknown error';
}
}
function cantLoadInitialDatas(response) {
$scope.ftpAccountsOfDomain = true;
$scope.deleteFTPButton = true;
$scope.deleteFailure = true;
$scope.deleteSuccess = true;
$scope.couldNotConnect = false;
$scope.deleteFTPButtonInit = true;
}
};
$scope.deleteFTPAccount = function () {
@@ -315,9 +363,11 @@ app.controller('listFTPAccounts', function ($scope, $http) {
$scope.passwordChanged = true;
$scope.canNotChangePassword = true;
$scope.couldNotConnect = true;
$scope.ftpLoading = true;
$scope.ftpLoading = false;
$scope.ftpAccounts = true;
$scope.changePasswordBox = true;
$scope.quotaManagementBox = true;
$scope.directoryManagementBox = true;
$scope.notificationsBox = true;
var globalFTPUsername = "";
@@ -331,8 +381,10 @@ app.controller('listFTPAccounts', function ($scope, $http) {
$scope.passwordChanged = true;
$scope.canNotChangePassword = true;
$scope.couldNotConnect = true;
$scope.ftpLoading = true;
$scope.ftpLoading = false; // Don't show loading when opening password dialog
$scope.changePasswordBox = false;
$scope.quotaManagementBox = true;
$scope.directoryManagementBox = true;
$scope.notificationsBox = true;
$scope.ftpUsername = ftpUsername;
globalFTPUsername = ftpUsername;
@@ -341,7 +393,7 @@ app.controller('listFTPAccounts', function ($scope, $http) {
$scope.changePasswordBtn = function () {
$scope.ftpLoading = false;
$scope.ftpLoading = true; // Show loading while changing password
url = "/ftp/changePassword";
@@ -367,13 +419,13 @@ app.controller('listFTPAccounts', function ($scope, $http) {
if (response.data.changePasswordStatus == 1) {
$scope.notificationsBox = false;
$scope.passwordChanged = false;
$scope.ftpLoading = true;
$scope.ftpLoading = false; // Hide loading when done
$scope.domainFeteched = $scope.selectedDomain;
} else {
$scope.notificationsBox = false;
$scope.canNotChangePassword = false;
$scope.ftpLoading = true;
$scope.ftpLoading = false; // Hide loading on error
$scope.canNotChangePassword = false;
$scope.errorMessage = response.data.error_message;
}
@@ -383,7 +435,7 @@ app.controller('listFTPAccounts', function ($scope, $http) {
function cantLoadInitialDatas(response) {
$scope.notificationsBox = false;
$scope.couldNotConnect = false;
$scope.ftpLoading = true;
$scope.ftpLoading = false; // Hide loading on connection error
}
@@ -394,9 +446,11 @@ app.controller('listFTPAccounts', function ($scope, $http) {
$scope.passwordChanged = true;
$scope.canNotChangePassword = true;
$scope.couldNotConnect = true;
$scope.ftpLoading = false;
$scope.ftpLoading = true; // Show loading while fetching
$scope.ftpAccounts = true;
$scope.changePasswordBox = true;
$scope.quotaManagementBox = true;
$scope.directoryManagementBox = true;
var selectedDomain = $scope.selectedDomain;
@@ -422,16 +476,22 @@ app.controller('listFTPAccounts', function ($scope, $http) {
if (response.data.fetchStatus == 1) {
$scope.records = JSON.parse(response.data.data);
angular.forEach($scope.records, function (r) {
if (typeof r.acct_enabled === 'undefined') {
r.acct_enabled = true;
}
});
$scope.notificationsBox = false;
$scope.recordsFetched = false;
$scope.passwordChanged = true;
$scope.canNotChangePassword = true;
$scope.couldNotConnect = true;
$scope.ftpLoading = true;
$scope.ftpLoading = false; // Hide loading when done
$scope.ftpAccounts = false;
$scope.changePasswordBox = true;
$scope.quotaManagementBox = true;
$scope.directoryManagementBox = true;
$scope.domainFeteched = $scope.selectedDomain;
@@ -441,9 +501,11 @@ app.controller('listFTPAccounts', function ($scope, $http) {
$scope.passwordChanged = true;
$scope.canNotChangePassword = true;
$scope.couldNotConnect = true;
$scope.ftpLoading = true;
$scope.ftpLoading = false; // Hide loading on error
$scope.ftpAccounts = true;
$scope.changePasswordBox = true;
$scope.quotaManagementBox = true;
$scope.directoryManagementBox = true;
$scope.errorMessage = response.data.error_message;
}
@@ -456,9 +518,11 @@ app.controller('listFTPAccounts', function ($scope, $http) {
$scope.passwordChanged = true;
$scope.canNotChangePassword = true;
$scope.couldNotConnect = false;
$scope.ftpLoading = true;
$scope.ftpLoading = false; // Hide loading on connection error
$scope.ftpAccounts = true;
$scope.changePasswordBox = true;
$scope.quotaManagementBox = true;
$scope.directoryManagementBox = true;
}
@@ -478,4 +542,292 @@ app.controller('listFTPAccounts', function ($scope, $http) {
$scope.generatedPasswordView = true;
};
// Quota management functions
$scope.manageQuota = function (record) {
$scope.recordsFetched = true;
$scope.passwordChanged = true;
$scope.canNotChangePassword = true;
$scope.couldNotConnect = true;
$scope.ftpLoading = false;
$scope.changePasswordBox = true;
$scope.directoryManagementBox = true;
$scope.quotaManagementBox = false;
$scope.notificationsBox = true;
$scope.ftpUsername = record.user;
globalFTPUsername = record.user;
// Set current quota info
$scope.currentQuotaInfo = record.quotasize;
$scope.packageQuota = record.package_quota;
$scope.enableCustomQuotaEdit = record.custom_quota_enabled;
$scope.customQuotaSizeEdit = record.custom_quota_size || 0;
};
$scope.toggleCustomQuotaEdit = function() {
if (!$scope.enableCustomQuotaEdit) {
$scope.customQuotaSizeEdit = 0;
}
};
$scope.updateQuotaBtn = function () {
$scope.ftpLoading = true;
url = "/ftp/updateFTPQuota";
var data = {
ftpUserName: globalFTPUsername,
customQuotaSize: parseInt($scope.customQuotaSizeEdit) || 0,
enableCustomQuota: $scope.enableCustomQuotaEdit || false,
};
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, data, config).then(ListInitialDatas, cantLoadInitialDatas);
function ListInitialDatas(response) {
if (response.data.updateQuotaStatus == 1) {
$scope.notificationsBox = false;
$scope.quotaUpdated = false;
$scope.ftpLoading = false;
$scope.domainFeteched = $scope.selectedDomain;
// Refresh the records to show updated quota
populateCurrentRecords();
// Show success notification
if (typeof PNotify !== 'undefined') {
new PNotify({
title: 'Success!',
text: 'FTP quota updated successfully.',
type: 'success'
});
}
} else {
$scope.notificationsBox = false;
$scope.quotaUpdateFailed = false;
$scope.ftpLoading = false;
$scope.errorMessage = response.data.error_message;
// Show error notification
if (typeof PNotify !== 'undefined') {
new PNotify({
title: 'Error!',
text: response.data.error_message,
type: 'error'
});
}
}
}
function cantLoadInitialDatas(response) {
$scope.notificationsBox = false;
$scope.couldNotConnect = false;
$scope.ftpLoading = false;
// Show error notification
if (typeof PNotify !== 'undefined') {
new PNotify({
title: 'Error!',
text: 'Could not connect to server.',
type: 'error'
});
}
}
};
$scope.manageDirectory = function (record) {
$scope.recordsFetched = true;
$scope.passwordChanged = true;
$scope.canNotChangePassword = true;
$scope.couldNotConnect = true;
$scope.ftpLoading = false;
$scope.changePasswordBox = true;
$scope.quotaManagementBox = true;
$scope.directoryManagementBox = false;
$scope.notificationsBox = true;
$scope.ftpUsername = record.user;
globalFTPUsername = record.user;
$scope.ftpPathEdit = record.dir || '';
};
$scope.changeDirectoryBtn = function () {
$scope.ftpLoading = true;
var url = "/ftp/changeFTPDirectory";
var pathVal = $scope.ftpPathEdit;
if (typeof pathVal === 'undefined' || pathVal === null) {
pathVal = '';
} else {
pathVal = String(pathVal).trim();
}
var data = {
ftpUserName: globalFTPUsername,
selectedDomain: $scope.selectedDomain,
path: pathVal
};
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, data, config).then(ListInitialDatas, cantLoadInitialDatas);
function ListInitialDatas(response) {
if (response.data.changeDirectoryStatus === 1) {
$scope.ftpLoading = false;
$scope.directoryManagementBox = true;
if (typeof PNotify !== 'undefined') {
new PNotify({
title: 'Success!',
text: 'FTP directory updated successfully.',
type: 'success'
});
}
populateCurrentRecords();
} else {
$scope.ftpLoading = false;
$scope.errorMessage = response.data.error_message;
if (typeof PNotify !== 'undefined') {
new PNotify({
title: 'Error!',
text: response.data.error_message,
type: 'error'
});
}
}
}
function cantLoadInitialDatas() {
$scope.ftpLoading = false;
$scope.couldNotConnect = false;
if (typeof PNotify !== 'undefined') {
new PNotify({
title: 'Error!',
text: 'Could not connect to server.',
type: 'error'
});
}
}
};
});
app.controller('Resetftpconf', function ($scope, $http, $timeout, $window){
$scope.Loading = true;
$scope.NotifyBox = true;
$scope.InstallBox = true;
$scope.installationDetailsForm = false;
$scope.alertType = '';
$scope.errorMessage = '';
$scope.resetftp = function () {
$scope.Loading = false;
$scope.installationDetailsForm = true;
$scope.InstallBox = false;
$scope.alertType = '';
$scope.NotifyBox = true;
var url = "/ftp/resetftpnow";
var data = {};
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken'),
'Content-Type': 'application/json'
}
};
$http.post(url, data, config).then(ListInitialData, cantLoadInitialData);
function ListInitialData(response) {
if (response.data && response.data.status === 1) {
$scope.NotifyBox = true;
$scope.InstallBox = false;
$scope.Loading = false;
$scope.alertType = '';
$scope.statusfile = response.data.tempStatusPath;
$timeout(getRequestStatus, 1000);
} else {
$scope.errorMessage = (response.data && (response.data.error_message || response.data.errorMessage)) || 'Unknown error';
$scope.alertType = 'failedToStart';
$scope.NotifyBox = false;
$scope.InstallBox = true;
$scope.Loading = false;
}
}
function cantLoadInitialData(response) {
$scope.errorMessage = (response && response.data && (response.data.error_message || response.data.errorMessage)) || 'Could not connect to server. Please refresh this page.';
$scope.alertType = 'couldNotConnect';
$scope.NotifyBox = false;
$scope.InstallBox = true;
$scope.Loading = false;
try {
new PNotify({ title: 'Error!', text: $scope.errorMessage, type: 'error' });
} catch (e) {}
}
}
var statusPollPromise = null;
function getRequestStatus() {
$scope.NotifyBox = true;
$scope.InstallBox = false;
$scope.Loading = false;
$scope.alertType = '';
var url = "/ftp/getresetstatus";
var data = { statusfile: $scope.statusfile };
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken'),
'Content-Type': 'application/json'
}
};
$http.post(url, data, config).then(ListInitialDatas, cantLoadInitialDatas);
function ListInitialDatas(response) {
if (!response.data) return;
if (response.data.abort === 0) {
$scope.alertType = '';
$scope.requestData = response.data.requestStatus || '';
statusPollPromise = $timeout(getRequestStatus, 1000);
} else {
if (statusPollPromise) {
$timeout.cancel(statusPollPromise);
statusPollPromise = null;
}
$scope.NotifyBox = false;
$scope.InstallBox = false;
$scope.Loading = false;
$scope.requestData = response.data.requestStatus || '';
if (response.data.installed === 0) {
$scope.alertType = 'resetFailed';
$scope.errorMessage = response.data.error_message || 'Reset failed';
} else {
$scope.alertType = 'success';
$timeout(function () { $window.location.reload(); }, 3000);
}
}
}
function cantLoadInitialDatas(response) {
if (statusPollPromise) {
$timeout.cancel(statusPollPromise);
statusPollPromise = null;
}
$scope.alertType = 'couldNotConnect';
$scope.errorMessage = (response && response.data && (response.data.error_message || response.data.errorMessage)) || 'Could not connect to server. Please refresh this page.';
$scope.NotifyBox = false;
$scope.InstallBox = true;
$scope.Loading = false;
}
}
});

View File

@@ -6,14 +6,20 @@
/* Java script code to create account */
app.controller('createFTPAccount', function ($scope, $http) {
// Initialize all ng-hide variables to hide alerts on page load
$scope.ftpLoading = false;
$scope.ftpDetails = true;
$scope.canNotCreateFTP = true;
$scope.successfullyCreatedFTP = true;
$scope.couldNotConnect = true;
// Positive flags + ng-show: stay hidden until create flow sets them (avoids pre-bind ng-hide/undefined flash)
$scope.alertFtpCreateError = false;
$scope.alertFtpCreateSuccess = false;
$scope.alertFtpConnectFailed = false;
$scope.generatedPasswordView = true;
function resetFtpCreateAlerts() {
$scope.alertFtpCreateError = false;
$scope.alertFtpCreateSuccess = false;
$scope.alertFtpConnectFailed = false;
}
$(document).ready(function () {
$( ".ftpDetails, .account-details" ).hide();
$( ".ftpPasswordView" ).hide();
@@ -65,11 +71,11 @@ app.controller('createFTPAccount', function ($scope, $http) {
$scope.createFTPAccount = function () {
var submissionCompleted = false;
$scope.ftpLoading = true; // Show loading while creating
$scope.ftpDetails = false;
$scope.canNotCreateFTP = true;
$scope.successfullyCreatedFTP = true;
$scope.couldNotConnect = true;
resetFtpCreateAlerts();
var ftpDomain = $scope.ftpDomain;
var ftpUserName = $scope.ftpUserName;
@@ -89,9 +95,8 @@ app.controller('createFTPAccount', function ($scope, $http) {
var dangerousChars = /[;&|$`'"<>*?~]/;
if (dangerousChars.test(path)) {
$scope.ftpLoading = false;
$scope.canNotCreateFTP = false;
$scope.successfullyCreatedFTP = true;
$scope.couldNotConnect = true;
resetFtpCreateAlerts();
$scope.alertFtpCreateError = true;
$scope.errorMessage = "Invalid path: Path contains dangerous characters";
return;
}
@@ -99,22 +104,12 @@ app.controller('createFTPAccount', function ($scope, $http) {
// Check for path traversal attempts
if (path.indexOf("..") !== -1 || path.indexOf("~") !== -1) {
$scope.ftpLoading = false;
$scope.canNotCreateFTP = false;
$scope.successfullyCreatedFTP = true;
$scope.couldNotConnect = true;
resetFtpCreateAlerts();
$scope.alertFtpCreateError = true;
$scope.errorMessage = "Invalid path: Path cannot contain '..' or '~'";
return;
}
// Check if path starts with slash (should be relative)
if (path.startsWith("/")) {
$scope.ftpLoading = false;
$scope.canNotCreateFTP = false;
$scope.successfullyCreatedFTP = true;
$scope.couldNotConnect = true;
$scope.errorMessage = "Invalid path: Path must be relative (not starting with '/')";
return;
}
// Absolute paths under /home/... are allowed; server validates they stay inside the site home
}
var url = "/ftp/submitFTPCreation";
@@ -140,20 +135,20 @@ app.controller('createFTPAccount', function ($scope, $http) {
function ListInitialDatas(response) {
if (submissionCompleted) {
return;
}
submissionCompleted = true;
$scope.ftpLoading = false;
resetFtpCreateAlerts();
if (response.data && response.data.creatFTPStatus === 1) {
$scope.ftpLoading = false; // Hide loading on success
$scope.successfullyCreatedFTP = false;
$scope.canNotCreateFTP = true;
$scope.couldNotConnect = true;
$scope.alertFtpCreateSuccess = true;
$scope.createdFTPUsername = (response.data.createdFTPUsername != null && response.data.createdFTPUsername !== '') ? response.data.createdFTPUsername : (ftpDomain + '_' + ftpUserName);
if (typeof PNotify !== 'undefined') {
new PNotify({ title: 'Success!', text: 'FTP account successfully created.', type: 'success' });
}
} else {
$scope.ftpLoading = false;
$scope.canNotCreateFTP = false;
$scope.successfullyCreatedFTP = true;
$scope.couldNotConnect = true;
$scope.alertFtpCreateError = true;
$scope.errorMessage = (response.data && response.data.error_message) ? response.data.error_message : 'Unknown error';
if (typeof PNotify !== 'undefined') {
new PNotify({ title: 'Operation Failed!', text: $scope.errorMessage, type: 'error' });
@@ -162,14 +157,15 @@ app.controller('createFTPAccount', function ($scope, $http) {
}
function cantLoadInitialDatas(response) {
if (submissionCompleted) {
return;
}
submissionCompleted = true;
$scope.ftpLoading = false;
if ($scope.successfullyCreatedFTP !== false) {
$scope.couldNotConnect = false;
$scope.canNotCreateFTP = true;
$scope.successfullyCreatedFTP = true;
if (typeof PNotify !== 'undefined') {
new PNotify({ title: 'Operation Failed!', text: 'Could not connect to server, please refresh this page', type: 'error' });
}
resetFtpCreateAlerts();
$scope.alertFtpConnectFailed = true;
if (typeof PNotify !== 'undefined') {
new PNotify({ title: 'Operation Failed!', text: 'Could not connect to server, please refresh this page', type: 'error' });
}
}
@@ -177,9 +173,7 @@ app.controller('createFTPAccount', function ($scope, $http) {
};
$scope.hideFewDetails = function () {
$scope.successfullyCreatedFTP = true;
$scope.canNotCreateFTP = true;
$scope.couldNotConnect = true;
resetFtpCreateAlerts();
};
///
@@ -373,6 +367,7 @@ app.controller('listFTPAccounts', function ($scope, $http) {
$scope.ftpAccounts = true;
$scope.changePasswordBox = true;
$scope.quotaManagementBox = true;
$scope.directoryManagementBox = true;
$scope.notificationsBox = true;
var globalFTPUsername = "";
@@ -388,6 +383,8 @@ app.controller('listFTPAccounts', function ($scope, $http) {
$scope.couldNotConnect = true;
$scope.ftpLoading = false; // Don't show loading when opening password dialog
$scope.changePasswordBox = false;
$scope.quotaManagementBox = true;
$scope.directoryManagementBox = true;
$scope.notificationsBox = true;
$scope.ftpUsername = ftpUsername;
globalFTPUsername = ftpUsername;
@@ -452,6 +449,8 @@ app.controller('listFTPAccounts', function ($scope, $http) {
$scope.ftpLoading = true; // Show loading while fetching
$scope.ftpAccounts = true;
$scope.changePasswordBox = true;
$scope.quotaManagementBox = true;
$scope.directoryManagementBox = true;
var selectedDomain = $scope.selectedDomain;
@@ -477,7 +476,11 @@ app.controller('listFTPAccounts', function ($scope, $http) {
if (response.data.fetchStatus == 1) {
$scope.records = JSON.parse(response.data.data);
angular.forEach($scope.records, function (r) {
if (typeof r.acct_enabled === 'undefined') {
r.acct_enabled = true;
}
});
$scope.notificationsBox = false;
$scope.recordsFetched = false;
@@ -487,6 +490,8 @@ app.controller('listFTPAccounts', function ($scope, $http) {
$scope.ftpLoading = false; // Hide loading when done
$scope.ftpAccounts = false;
$scope.changePasswordBox = true;
$scope.quotaManagementBox = true;
$scope.directoryManagementBox = true;
$scope.domainFeteched = $scope.selectedDomain;
@@ -499,6 +504,8 @@ app.controller('listFTPAccounts', function ($scope, $http) {
$scope.ftpLoading = false; // Hide loading on error
$scope.ftpAccounts = true;
$scope.changePasswordBox = true;
$scope.quotaManagementBox = true;
$scope.directoryManagementBox = true;
$scope.errorMessage = response.data.error_message;
}
@@ -514,6 +521,8 @@ app.controller('listFTPAccounts', function ($scope, $http) {
$scope.ftpLoading = false; // Hide loading on connection error
$scope.ftpAccounts = true;
$scope.changePasswordBox = true;
$scope.quotaManagementBox = true;
$scope.directoryManagementBox = true;
}
@@ -540,6 +549,8 @@ app.controller('listFTPAccounts', function ($scope, $http) {
$scope.canNotChangePassword = true;
$scope.couldNotConnect = true;
$scope.ftpLoading = false;
$scope.changePasswordBox = true;
$scope.directoryManagementBox = true;
$scope.quotaManagementBox = false;
$scope.notificationsBox = true;
$scope.ftpUsername = record.user;
@@ -628,6 +639,80 @@ app.controller('listFTPAccounts', function ($scope, $http) {
}
};
$scope.manageDirectory = function (record) {
$scope.recordsFetched = true;
$scope.passwordChanged = true;
$scope.canNotChangePassword = true;
$scope.couldNotConnect = true;
$scope.ftpLoading = false;
$scope.changePasswordBox = true;
$scope.quotaManagementBox = true;
$scope.directoryManagementBox = false;
$scope.notificationsBox = true;
$scope.ftpUsername = record.user;
globalFTPUsername = record.user;
$scope.ftpPathEdit = record.dir || '';
};
$scope.changeDirectoryBtn = function () {
$scope.ftpLoading = true;
var url = "/ftp/changeFTPDirectory";
var pathVal = $scope.ftpPathEdit;
if (typeof pathVal === 'undefined' || pathVal === null) {
pathVal = '';
} else {
pathVal = String(pathVal).trim();
}
var data = {
ftpUserName: globalFTPUsername,
selectedDomain: $scope.selectedDomain,
path: pathVal
};
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, data, config).then(ListInitialDatas, cantLoadInitialDatas);
function ListInitialDatas(response) {
if (response.data.changeDirectoryStatus === 1) {
$scope.ftpLoading = false;
$scope.directoryManagementBox = true;
if (typeof PNotify !== 'undefined') {
new PNotify({
title: 'Success!',
text: 'FTP directory updated successfully.',
type: 'success'
});
}
populateCurrentRecords();
} else {
$scope.ftpLoading = false;
$scope.errorMessage = response.data.error_message;
if (typeof PNotify !== 'undefined') {
new PNotify({
title: 'Error!',
text: response.data.error_message,
type: 'error'
});
}
}
}
function cantLoadInitialDatas() {
$scope.ftpLoading = false;
$scope.couldNotConnect = false;
if (typeof PNotify !== 'undefined') {
new PNotify({
title: 'Error!',
text: 'Could not connect to server.',
type: 'error'
});
}
}
};
});

View File

@@ -368,6 +368,18 @@ if [ -f /usr/local/CyberCP/public/phpmyadmin/phpmyadminsignin.php ]; then
grep -q "127.0.0.1" /usr/local/CyberCP/public/phpmyadmin/phpmyadminsignin.php && echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] phpMyAdmin signon default host set to 127.0.0.1" | tee -a /var/log/cyberpanel_upgrade_debug.log
fi
# FTP users table: custom quota columns (fixes 1054 on Create FTP Account if schema predates model fields)
if [[ -f /usr/local/CyberCP/CPScripts/ensure_ftp_users_quota_columns.py ]]; then
if [[ -x /usr/local/CyberCP/bin/python ]]; then
CP_DIR=/usr/local/CyberCP /usr/local/CyberCP/bin/python /usr/local/CyberCP/CPScripts/ensure_ftp_users_quota_columns.py /usr/local/CyberCP 2>&1 | tee -a /var/log/cyberpanel_upgrade_debug.log || true
else
CP_DIR=/usr/local/CyberCP python3 /usr/local/CyberCP/CPScripts/ensure_ftp_users_quota_columns.py /usr/local/CyberCP 2>&1 | tee -a /var/log/cyberpanel_upgrade_debug.log || true
fi
echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Ran ensure_ftp_users_quota_columns (FTP users table custom quota columns)" | tee -a /var/log/cyberpanel_upgrade_debug.log
else
echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] INFO: ensure_ftp_users_quota_columns.py not in CyberCP yet; run deploy-ftp-users-custom-quota-columns.sh after sync" | tee -a /var/log/cyberpanel_upgrade_debug.log
fi
systemctl restart lscpd
}