diff --git a/ftp/ftpManager.py b/ftp/ftpManager.py
index 596bae449..e3fc3404c 100644
--- a/ftp/ftpManager.py
+++ b/ftp/ftpManager.py
@@ -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']
diff --git a/ftp/pluginManager.py b/ftp/pluginManager.py
index baacfe757..6b2130361 100644
--- a/ftp/pluginManager.py
+++ b/ftp/pluginManager.py
@@ -33,4 +33,20 @@ class pluginManager:
@staticmethod
def postChangePassword(request, response):
- return pluginManagerGlobal.globalPlug(request, postChangePassword, response)
\ No newline at end of file
+ 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)
\ No newline at end of file
diff --git a/ftp/signals.py b/ftp/signals.py
index 874ee0421..6aa74c237 100644
--- a/ftp/signals.py
+++ b/ftp/signals.py
@@ -26,4 +26,11 @@ postSubmitFTPDelete = Signal()
preChangePassword = Signal()
## This event is fired after CyberPanel core finished deletion of child-domain
-postChangePassword = Signal()
\ No newline at end of file
+postChangePassword = Signal()
+
+## Before / after changing FTP account home directory (list FTP page)
+preChangeFTPDirectory = Signal()
+postChangeFTPDirectory = Signal()
+
+preSetFTPAccountStatus = Signal()
+postSetFTPAccountStatus = Signal()
\ No newline at end of file
diff --git a/ftp/static/ftp/ftp.js b/ftp/static/ftp/ftp.js
index 47bcfe1ec..83f715a08 100644
--- a/ftp/static/ftp/ftp.js
+++ b/ftp/static/ftp/ftp.js
@@ -109,15 +109,7 @@ app.controller('createFTPAccount', function ($scope, $http) {
$scope.errorMessage = "Invalid path: Path cannot contain '..' or '~'";
return;
}
-
- // Check if path starts with slash (should be relative)
- if (path.startsWith("/")) {
- $scope.ftpLoading = false;
- resetFtpCreateAlerts();
- $scope.alertFtpCreateError = 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";
@@ -375,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 = "";
@@ -390,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;
@@ -454,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;
@@ -479,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;
@@ -489,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;
@@ -501,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;
}
@@ -516,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;
}
@@ -542,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;
@@ -630,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'
+ });
+ }
+ });
+ };
+
});
diff --git a/ftp/templates/ftp/createFTPAccount.html b/ftp/templates/ftp/createFTPAccount.html
index dd494125f..0850528c7 100644
--- a/ftp/templates/ftp/createFTPAccount.html
+++ b/ftp/templates/ftp/createFTPAccount.html
@@ -569,26 +569,26 @@
diff --git a/ftp/templates/ftp/listFTPAccounts.html b/ftp/templates/ftp/listFTPAccounts.html
index 97a7b25b2..6f7dffaae 100644
--- a/ftp/templates/ftp/listFTPAccounts.html
+++ b/ftp/templates/ftp/listFTPAccounts.html
@@ -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 @@
+
+
+
{% trans "FTP home directory for" %} {$ ftpUsername $}
+
+
+
+
+
+
+ {% trans "Update Directory" %}
+
+
+
+
+
+
- {% trans "ID" %}
- {% trans "User Name" %}
- {% trans "Directory" %}
- {% trans "Quota" %}
- {% trans "Actions" %}
+ {% trans "ID" %}
+ {% trans "User Name" %}
+ {% trans "Status" %}
+ {% trans "Directory" %}
+ {% trans "Quota" %}
+ {% trans "Actions" %}
-
+
+
+
+ {% trans "Enabled" %}
+
+
+ {% trans "Disabled" %}
+
+
@@ -579,6 +616,18 @@
{% trans "Quota" %}
+
+
+ {% trans "Directory" %}
+
+
+
+ {% trans "Disable" %}
+
+
+
+ {% trans "Enable" %}
+
diff --git a/ftp/urls.py b/ftp/urls.py
index 19adb1764..e4cdebfb0 100644
--- a/ftp/urls.py
+++ b/ftp/urls.py
@@ -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'),
diff --git a/ftp/views.py b/ftp/views.py
index f7f2b088b..f42fc6468 100644
--- a/ftp/views.py
+++ b/ftp/views.py
@@ -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)
diff --git a/install/pure-ftpd-one/pureftpd-mysql.conf b/install/pure-ftpd-one/pureftpd-mysql.conf
index f882c9600..befb00bba 100644
--- a/install/pure-ftpd-one/pureftpd-mysql.conf
+++ b/install/pure-ftpd-one/pureftpd-mysql.conf
@@ -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
diff --git a/install/pure-ftpd/pureftpd-mysql.conf b/install/pure-ftpd/pureftpd-mysql.conf
index f2e5548f0..2dd1ad2f8 100644
--- a/install/pure-ftpd/pureftpd-mysql.conf
+++ b/install/pure-ftpd/pureftpd-mysql.conf
@@ -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
diff --git a/plogical/ftpUtilities.py b/plogical/ftpUtilities.py
index d2be496a5..e4f3693fa 100644
--- a/plogical/ftpUtilities.py
+++ b/plogical/ftpUtilities.py
@@ -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/,
+ or child domain /home// 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:
diff --git a/public/static/ftp/ftp.js b/public/static/ftp/ftp.js
index 47bcfe1ec..1071d554a 100644
--- a/public/static/ftp/ftp.js
+++ b/public/static/ftp/ftp.js
@@ -109,15 +109,7 @@ app.controller('createFTPAccount', function ($scope, $http) {
$scope.errorMessage = "Invalid path: Path cannot contain '..' or '~'";
return;
}
-
- // Check if path starts with slash (should be relative)
- if (path.startsWith("/")) {
- $scope.ftpLoading = false;
- resetFtpCreateAlerts();
- $scope.alertFtpCreateError = 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";
@@ -375,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 = "";
@@ -390,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;
@@ -454,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;
@@ -479,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;
@@ -489,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;
@@ -501,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;
}
@@ -516,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;
}
@@ -542,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;
@@ -630,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'
+ });
+ }
+ }
+ };
+
});
diff --git a/static/ftp/ftp.js b/static/ftp/ftp.js
index 47bcfe1ec..1071d554a 100644
--- a/static/ftp/ftp.js
+++ b/static/ftp/ftp.js
@@ -109,15 +109,7 @@ app.controller('createFTPAccount', function ($scope, $http) {
$scope.errorMessage = "Invalid path: Path cannot contain '..' or '~'";
return;
}
-
- // Check if path starts with slash (should be relative)
- if (path.startsWith("/")) {
- $scope.ftpLoading = false;
- resetFtpCreateAlerts();
- $scope.alertFtpCreateError = 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";
@@ -375,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 = "";
@@ -390,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;
@@ -454,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;
@@ -479,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;
@@ -489,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;
@@ -501,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;
}
@@ -516,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;
}
@@ -542,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;
@@ -630,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'
+ });
+ }
+ }
+ };
+
});