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

V2.5.5 dev
This commit is contained in:
Master3395
2026-02-04 19:27:30 +01:00
committed by GitHub
37 changed files with 3091 additions and 392 deletions

View File

@@ -1130,19 +1130,12 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
console.log('Parsed responseData from string:', responseData);
} catch (e) {
console.error('Failed to parse response as JSON:', e);
console.error('Raw response string:', responseData);
// Try to extract error from string
if (responseData.includes('error')) {
if (typeof PNotify !== 'undefined') {
new PNotify({
title: 'Error',
text: 'Failed to block IP address: ' + responseData,
type: 'error',
delay: 5000
});
}
return;
var errorMsg = responseData && responseData.length ? responseData : 'Failed to block IP address';
if (typeof PNotify !== 'undefined') {
new PNotify({ title: 'Error', text: errorMsg, type: 'error', delay: 5000 });
}
$scope.blockingIP = null;
return;
}
}
@@ -1217,28 +1210,20 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
$scope.lastErrorTime = Date.now();
var errorMessage = 'Failed to block IP address';
if (err.data) {
var errData = err.data;
if (typeof errData === 'string') {
try {
errData = JSON.parse(errData);
} catch (e) {
errorMessage = errData || errorMessage;
var errData = err.data;
if (typeof errData === 'string') {
try {
errData = JSON.parse(errData);
} catch (e) {
if (errData && errData.length) {
errorMessage = errData.length > 200 ? errData.substring(0, 200) + '...' : errData;
}
}
if (errData && typeof errData === 'object') {
if (errData.error_message) {
errorMessage = errData.error_message;
} else if (errData.error) {
errorMessage = errData.error;
} else if (errData.message) {
errorMessage = errData.message;
}
}
} else if (err.statusText) {
errorMessage = err.statusText;
}
if (errData && typeof errData === 'object') {
errorMessage = errData.error_message || errData.error || errData.message || errorMessage;
} else if (err.status) {
errorMessage = `HTTP ${err.status}: ${err.statusText || 'Unknown error'}`;
errorMessage = 'HTTP ' + err.status + ': ' + (errorMessage);
}
console.error('Final error message:', errorMessage);

View File

@@ -1380,56 +1380,38 @@
}
$.ajax({
url: '/base/blockIPAddress',
url: '/firewall/addBannedIP',
type: 'POST',
contentType: 'application/json',
headers: {
'X-CSRFToken': getCookie('csrftoken')
},
data: JSON.stringify({
'ip_address': ipAddress,
'reason': 'Security alert detected from dashboard'
'ip': ipAddress,
'reason': 'Brute force attack detected from SSH Security Analysis',
'duration': 'permanent'
}),
success: function(data) {
// Handle both success and error responses
if (data.status === 1) {
showNotification('success', data.message || 'IP address blocked successfully');
if (data && data.status === 1) {
showNotification('success', data.message || 'IP address blocked successfully. Manage in Firewall > Banned IPs.');
// Refresh the page to update the blocked IPs list
setTimeout(() => {
location.reload();
}, 1000);
setTimeout(function() { location.reload(); }, 1000);
} else {
// Handle error response - check for both 'error' and 'error_message' fields
var errorMsg = data.error || data.error_message || data.message || 'Failed to block IP address';
var errorMsg = (data && (data.error_message || data.error || data.message)) || 'Failed to block IP address';
showNotification('error', errorMsg);
}
},
error: function(xhr, status, error) {
// Handle network errors and parse JSON errors
console.error('Ban IP error:', xhr, status, error);
console.error('Ban IP error:', xhr.status, xhr.responseText);
var errorMsg = 'Failed to block IP address. Please try again.';
// Log full response for debugging
console.log('Response status:', xhr.status);
console.log('Response text:', xhr.responseText);
if (xhr.responseJSON) {
errorMsg = xhr.responseJSON.error || xhr.responseJSON.error_message || xhr.responseJSON.message || errorMsg;
console.log('Parsed error from JSON:', errorMsg);
} else if (xhr.responseText) {
try {
var errorData = JSON.parse(xhr.responseText);
errorMsg = errorData.error || errorData.error_message || errorData.message || errorMsg;
console.log('Parsed error from text:', errorMsg);
} catch(e) {
console.error('Failed to parse error response:', e);
// If parsing fails, try to extract error from response text
if (xhr.responseText.includes('error')) {
errorMsg = xhr.responseText.substring(0, 200);
}
}
var data = xhr.responseJSON;
if (!data && xhr.responseText) {
try { data = JSON.parse(xhr.responseText); } catch(e) {}
}
if (data && (data.error_message || data.error || data.message)) {
errorMsg = data.error_message || data.error || data.message;
}
showNotification('error', errorMsg);
},
complete: function() {

View File

@@ -1,6 +1,6 @@
{% load i18n %}
{% get_current_language as LANGUAGE_CODE %}
{% with CP_VERSION="2.4.4.1" %}
{% with CP_VERSION=CYBERPANEL_FULL_VERSION|default:"2.5.5.dev" %}
<!DOCTYPE html>
<html lang="en" ng-app="CyberCP">
<head>

View File

@@ -16,6 +16,7 @@ from plogical.acl import ACLManager
from manageServices.models import PDNSStatus
from django.views.decorators.csrf import ensure_csrf_cookie, csrf_exempt
from plogical.processUtilities import ProcessUtilities
from plogical.firewallUtilities import FirewallUtilities
from plogical.httpProc import httpProc
from websiteFunctions.models import Websites, WPSites
from databases.models import Databases
@@ -1318,54 +1319,14 @@ def blockIPAddress(request):
'error': 'Invalid IP address'
}), content_type='application/json', status=400)
# Use firewalld (CSF has been discontinued)
# Use FirewallUtilities so firewall-cmd runs with proper privileges (root/lscpd)
firewall_cmd = 'firewalld'
reason = data.get('reason', 'Security alert detected from dashboard')
try:
# Verify firewalld is active using subprocess for better security
import subprocess
firewalld_check = subprocess.run(['systemctl', 'is-active', 'firewalld'],
capture_output=True, text=True, timeout=10)
if not (firewalld_check.returncode == 0 and 'active' in firewalld_check.stdout):
return HttpResponse(json.dumps({
'status': 0,
'error': 'Firewalld is not active. Please enable firewalld service.'
}), content_type='application/json', status=500)
except subprocess.TimeoutExpired:
return HttpResponse(json.dumps({
'status': 0,
'error': 'Timeout checking firewalld status'
}), content_type='application/json', status=500)
success, msg = FirewallUtilities.blockIP(ip_address, reason)
except Exception as e:
return HttpResponse(json.dumps({
'status': 0,
'error': f'Cannot check firewalld status: {str(e)}'
}), content_type='application/json', status=500)
# Block the IP address using firewalld with subprocess for better security
success = False
error_message = ''
try:
# Use subprocess with explicit argument lists to prevent injection
rich_rule = f'rule family=ipv4 source address={ip_address} drop'
add_rule_cmd = ['firewall-cmd', '--permanent', '--add-rich-rule', rich_rule]
# Execute the add rule command
result = subprocess.run(add_rule_cmd, capture_output=True, text=True, timeout=30)
if result.returncode == 0:
# Reload firewall rules
reload_cmd = ['firewall-cmd', '--reload']
reload_result = subprocess.run(reload_cmd, capture_output=True, text=True, timeout=30)
if reload_result.returncode == 0:
success = True
else:
error_message = f'Failed to reload firewall rules: {reload_result.stderr}'
else:
error_message = f'Failed to add firewall rule: {result.stderr}'
except subprocess.TimeoutExpired:
error_message = 'Firewall command timed out'
except Exception as e:
error_message = f'Firewall command failed: {str(e)}'
success = False
msg = str(e)
if success:
# Add to banned IPs JSON file for consistency with firewall page
@@ -1430,7 +1391,7 @@ def blockIPAddress(request):
else:
return HttpResponse(json.dumps({
'status': 0,
'error': error_message or 'Failed to block IP address'
'error': msg or 'Failed to block IP address'
}), content_type='application/json', status=500)
except json.JSONDecodeError as e:

78
deploy-email-limits-fix.sh Executable file
View File

@@ -0,0 +1,78 @@
#!/bin/bash
# Deploy Email Limits fix to a CyberPanel installation.
# Copies recommended mailServer files and optionally restarts lscpd.
#
# Usage (run from anywhere):
# sudo bash /home/cyberpanel-repo/deploy-email-limits-fix.sh
# sudo bash deploy-email-limits-fix.sh [REPO_DIR] [CP_DIR]
#
# Or from repo root: cd /home/cyberpanel-repo && sudo bash deploy-email-limits-fix.sh
set -e
log() { echo "[$(date +%Y-%m-%d\ %H:%M:%S)] $*"; }
err() { log "ERROR: $*" >&2; }
# Resolve REPO_DIR: explicit arg, then script dir, then common locations
if [[ -n "$1" && -d "$1/mailServer" ]]; then
REPO_DIR="$1"
shift
elif [[ -d "$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)/mailServer" ]]; then
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
elif [[ -d "/home/cyberpanel-repo/mailServer" ]]; then
REPO_DIR="/home/cyberpanel-repo"
elif [[ -d "./mailServer" ]]; then
REPO_DIR="$(pwd)"
else
err "Repo not found. Use: sudo bash /home/cyberpanel-repo/deploy-email-limits-fix.sh"
err "Or: cd /path/to/cyberpanel-repo && sudo bash deploy-email-limits-fix.sh"
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
if [[ ! -d "$REPO_DIR/mailServer" ]]; then
err "Repo mailServer not found in: $REPO_DIR"
exit 1
fi
log "REPO_DIR=$REPO_DIR"
log "CP_DIR=$CP_DIR"
FILES=(
"mailServer/mailserverManager.py"
"mailServer/templates/mailServer/EmailLimits.html"
"mailServer/static/mailServer/mailServer.js"
"mailServer/static/mailServer/emailLimitsController.js"
)
for rel in "${FILES[@]}"; do
src="$REPO_DIR/$rel"
dst="$CP_DIR/$rel"
if [[ ! -f "$src" ]]; then
err "Source missing: $src"
exit 1
fi
mkdir -p "$(dirname "$dst")"
cp -f "$src" "$dst"
log "Copied: $rel"
done
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. Hard-refresh /email/EmailLimits in the browser (Ctrl+Shift+R)."

View File

@@ -0,0 +1,64 @@
#!/bin/bash
# Deploy FTP Create Account template fix to a CyberPanel installation.
# Copies the updated createFTPAccount.html and optionally restarts lscpd.
#
# Usage (run from anywhere):
# sudo bash /home/cyberpanel-repo/deploy-ftp-create-account-fix.sh
# sudo bash deploy-ftp-create-account-fix.sh [REPO_DIR] [CP_DIR]
#
# Or from repo root: cd /home/cyberpanel-repo && sudo bash deploy-ftp-create-account-fix.sh
set -e
log() { echo "[$(date +%Y-%m-%d\ %H:%M:%S)] $*"; }
err() { log "ERROR: $*" >&2; }
# Resolve REPO_DIR
if [[ -n "$1" && -d "$1/ftp" ]]; then
REPO_DIR="$1"
shift
elif [[ -d "$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)/ftp" ]]; then
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
elif [[ -d "/home/cyberpanel-repo/ftp" ]]; then
REPO_DIR="/home/cyberpanel-repo"
elif [[ -d "./ftp" ]]; then
REPO_DIR="$(pwd)"
else
err "Repo not found. Use: sudo bash /home/cyberpanel-repo/deploy-ftp-create-account-fix.sh"
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
if [[ ! -f "$REPO_DIR/ftp/templates/ftp/createFTPAccount.html" ]]; then
err "Source template not found in: $REPO_DIR"
exit 1
fi
log "REPO_DIR=$REPO_DIR"
log "CP_DIR=$CP_DIR"
SRC="$REPO_DIR/ftp/templates/ftp/createFTPAccount.html"
DST="$CP_DIR/ftp/templates/ftp/createFTPAccount.html"
mkdir -p "$(dirname "$DST")"
cp -f "$SRC" "$DST"
log "Copied: ftp/templates/ftp/createFTPAccount.html"
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. Hard-refresh /ftp/createFTPAccount in the browser (Ctrl+Shift+R)."

View File

@@ -0,0 +1,65 @@
#!/bin/bash
# Create the missing ftp_quotas table in the CyberPanel database.
# Fixes: (1146, "Table 'cyberpanel.ftp_quotas' doesn't exist") on /ftp/quotaManagement
#
# Usage:
# sudo bash /home/cyberpanel-repo/deploy-ftp-quotas-table.sh
# sudo bash deploy-ftp-quotas-table.sh [REPO_DIR] [CP_DIR]
set -e
log() { echo "[$(date +%Y-%m-%d\ %H:%M:%S)] $*"; }
err() { log "ERROR: $*" >&2; }
if [[ -n "$1" && -f "$1/sql/create_ftp_quotas.sql" ]]; then
REPO_DIR="$1"
shift
elif [[ -f "$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)/sql/create_ftp_quotas.sql" ]]; then
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
elif [[ -f "/home/cyberpanel-repo/sql/create_ftp_quotas.sql" ]]; then
REPO_DIR="/home/cyberpanel-repo"
else
err "sql/create_ftp_quotas.sql not found."
exit 1
fi
CP_DIR="${1:-/usr/local/CyberCP}"
SQL_FILE="$REPO_DIR/sql/create_ftp_quotas.sql"
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/sql"
cp -f "$SQL_FILE" "$CP_DIR/sql/create_ftp_quotas.sql"
log "Copied create_ftp_quotas.sql to $CP_DIR/sql/"
# Run SQL using Django DB connection (no password on command line)
log "Creating ftp_quotas table..."
export CP_DIR
python3 << 'PYEOF'
import os
import sys
cp_dir = os.environ.get('CP_DIR', '/usr/local/CyberCP')
sys.path.insert(0, cp_dir)
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'CyberCP.settings')
import django
django.setup()
from django.db import connection
with open(os.path.join(cp_dir, 'sql', 'create_ftp_quotas.sql'), 'r') as f:
sql = f.read()
with connection.cursor() as cursor:
cursor.execute(sql)
print('Executed CREATE TABLE IF NOT EXISTS ftp_quotas.')
PYEOF
log "Done. Reload https://207.180.193.210:2087/ftp/quotaManagement"

View File

@@ -1935,14 +1935,15 @@ class FirewallManager:
try:
admin = Administrator.objects.get(pk=userID)
if admin.acl.adminStatus != 1:
return ACLManager.loadError()
final_dic = {'status': 0, 'error_message': 'You are not authorized to access this resource.', 'error': 'You are not authorized to access this resource.'}
return HttpResponse(json.dumps(final_dic), content_type='application/json', status=403)
ip = data.get('ip', '').strip()
reason = data.get('reason', '').strip()
duration = data.get('duration', '24h')
if not ip or not reason:
final_dic = {'status': 0, 'error_message': 'IP address and reason are required'}
final_dic = {'status': 0, 'error_message': 'IP address and reason are required', 'error': 'IP address and reason are required'}
return HttpResponse(json.dumps(final_dic), content_type='application/json')
# Validate IP address format
@@ -1950,7 +1951,7 @@ class FirewallManager:
try:
ipaddress.ip_address(ip.split('/')[0]) # Handle CIDR notation
except ValueError:
final_dic = {'status': 0, 'error_message': 'Invalid IP address format'}
final_dic = {'status': 0, 'error_message': 'Invalid IP address format', 'error': 'Invalid IP address format'}
return HttpResponse(json.dumps(final_dic), content_type='application/json')
# Calculate expiration time
@@ -1973,7 +1974,8 @@ class FirewallManager:
# Check if IP is already banned
for banned_ip in banned_ips:
if banned_ip.get('ip') == ip and banned_ip.get('active', True):
final_dic = {'status': 0, 'error_message': 'IP address %s is already banned' % ip}
msg = 'IP address %s is already banned' % ip
final_dic = {'status': 0, 'error_message': msg, 'error': msg}
return HttpResponse(json.dumps(final_dic), content_type='application/json')
# Add new banned IP
@@ -1991,54 +1993,38 @@ class FirewallManager:
# Save to file
self._save_banned_ips_store(banned_ips)
# Apply firewall rule to block the IP using firewalld
# Apply firewall rule using FirewallUtilities (runs with proper privileges via ProcessUtilities/lscpd)
try:
import subprocess
# Verify firewalld is active
firewalld_check = subprocess.run(['systemctl', 'is-active', 'firewalld'],
capture_output=True, text=True, timeout=10)
if not (firewalld_check.returncode == 0 and 'active' in firewalld_check.stdout):
final_dic = {'status': 0, 'error_message': 'Firewalld is not active. Please enable firewalld service.'}
block_ok, block_msg = FirewallUtilities.blockIP(ip, reason)
if not block_ok:
# Rollback: remove the IP we just added from the store
banned_ips_rollback = [b for b in banned_ips if b.get('ip') != ip or not b.get('active', True)]
if len(banned_ips_rollback) < len(banned_ips):
self._save_banned_ips_store(banned_ips_rollback)
logging.CyberCPLogFileWriter.writeToFile('Failed to add firewalld rule for %s: %s' % (ip, block_msg))
err_msg = block_msg or 'Failed to add firewall rule'
final_dic = {'status': 0, 'error_message': err_msg, 'error': err_msg}
return HttpResponse(json.dumps(final_dic), content_type='application/json')
# Add firewalld rich rule to block the IP
rich_rule = f'rule family=ipv4 source address={ip} drop'
add_rule_cmd = ['firewall-cmd', '--permanent', '--add-rich-rule', rich_rule]
# Execute the add rule command
result = subprocess.run(add_rule_cmd, capture_output=True, text=True, timeout=30)
if result.returncode == 0:
# Reload firewall rules
reload_cmd = ['firewall-cmd', '--reload']
reload_result = subprocess.run(reload_cmd, capture_output=True, text=True, timeout=30)
if reload_result.returncode == 0:
logging.CyberCPLogFileWriter.writeToFile(f'Banned IP {ip} with reason: {reason}')
else:
logging.CyberCPLogFileWriter.writeToFile('Failed to reload firewalld for %s: %s' % (ip, reload_result.stderr))
final_dic = {'status': 0, 'error_message': 'Failed to reload firewall rules: %s' % reload_result.stderr}
return HttpResponse(json.dumps(final_dic), content_type='application/json')
else:
# Check if rule already exists (this is not an error)
if result.stderr and ('ALREADY_ENABLED' in result.stderr or 'already exists' in result.stderr.lower()):
logging.CyberCPLogFileWriter.writeToFile('IP %s already blocked in firewalld' % ip)
else:
logging.CyberCPLogFileWriter.writeToFile('Failed to add firewalld rule for %s: %s' % (ip, result.stderr or ''))
final_dic = {'status': 0, 'error_message': 'Failed to add firewall rule: %s' % (result.stderr or 'Unknown error')}
return HttpResponse(json.dumps(final_dic), content_type='application/json')
except subprocess.TimeoutExpired:
logging.CyberCPLogFileWriter.writeToFile('Timeout adding firewalld rule for %s' % ip)
final_dic = {'status': 0, 'error_message': 'Firewall command timed out'}
return HttpResponse(json.dumps(final_dic), content_type='application/json')
logging.CyberCPLogFileWriter.writeToFile(f'Banned IP {ip} with reason: {reason}')
except Exception as e:
# Rollback store on any exception
try:
banned_ips_rollback = [b for b in banned_ips if b.get('ip') != ip or not b.get('active', True)]
if len(banned_ips_rollback) < len(banned_ips):
self._save_banned_ips_store(banned_ips_rollback)
except Exception:
pass
logging.CyberCPLogFileWriter.writeToFile('Failed to add firewalld rule for %s: %s' % (ip, str(e)))
final_dic = {'status': 0, 'error_message': 'Firewall command failed: %s' % str(e)}
err_msg = 'Firewall command failed: %s' % str(e)
final_dic = {'status': 0, 'error_message': err_msg, 'error': err_msg}
return HttpResponse(json.dumps(final_dic), content_type='application/json')
final_dic = {'status': 1, 'message': 'IP address %s has been banned successfully' % ip}
return HttpResponse(json.dumps(final_dic), content_type='application/json')
except BaseException as msg:
final_dic = {'status': 0, 'error_message': str(msg)}
err_msg = str(msg)
final_dic = {'status': 0, 'error_message': err_msg, 'error': err_msg}
return HttpResponse(json.dumps(final_dic), content_type='application/json')
def removeBannedIP(self, userID=None, data=None):
@@ -2085,16 +2071,9 @@ class FirewallManager:
ip_to_unban = banned_ip.ip_address
banned_ip.active = False
banned_ip.save()
# Remove firewalld rule
# Remove firewalld rule using FirewallUtilities (runs with proper privileges)
try:
import subprocess
firewalld_check = subprocess.run(['systemctl', 'is-active', 'firewalld'],
capture_output=True, text=True, timeout=10)
if firewalld_check.returncode == 0 and 'active' in firewalld_check.stdout:
rich_rule = f'rule family=ipv4 source address={ip_to_unban} drop'
subprocess.run(['firewall-cmd', '--permanent', '--remove-rich-rule', rich_rule],
capture_output=True, text=True, timeout=30)
subprocess.run(['firewall-cmd', '--reload'], capture_output=True, text=True, timeout=30)
FirewallUtilities.unblockIP(ip_to_unban)
except Exception as e:
logging.CyberCPLogFileWriter.writeToFile(f'Warning removing firewalld rule: {str(e)}')
final_dic = {'status': 1, 'message': f'IP address {ip_to_unban} has been unbanned successfully'}
@@ -2124,40 +2103,12 @@ class FirewallManager:
# Save updated banned IPs
self._save_banned_ips_store(banned_ips)
# Remove firewalld rule to unblock the IP
# Remove firewalld rule using FirewallUtilities (runs with proper privileges)
try:
import subprocess
# Verify firewalld is active
firewalld_check = subprocess.run(['systemctl', 'is-active', 'firewalld'],
capture_output=True, text=True, timeout=10)
if not (firewalld_check.returncode == 0 and 'active' in firewalld_check.stdout):
# Firewalld not active, but still mark as unbanned in JSON
logging.CyberCPLogFileWriter.writeToFile(f'Warning: Firewalld not active when unbanned IP {ip_to_unban}')
else:
# Remove firewalld rich rule
rich_rule = f'rule family=ipv4 source address={ip_to_unban} drop'
remove_rule_cmd = ['firewall-cmd', '--permanent', '--remove-rich-rule', rich_rule]
# Execute the remove rule command
result = subprocess.run(remove_rule_cmd, capture_output=True, text=True, timeout=30)
if result.returncode == 0:
# Reload firewall rules
reload_cmd = ['firewall-cmd', '--reload']
reload_result = subprocess.run(reload_cmd, capture_output=True, text=True, timeout=30)
if reload_result.returncode == 0:
logging.CyberCPLogFileWriter.writeToFile(f'Unbanned IP {ip_to_unban}')
else:
logging.CyberCPLogFileWriter.writeToFile(f'Warning: Failed to reload firewalld after unbanning {ip_to_unban}: {reload_result.stderr}')
else:
# Rule might not exist, which is okay
if 'NOT_ENABLED' in result.stderr or 'not found' in result.stderr.lower():
logging.CyberCPLogFileWriter.writeToFile(f'IP {ip_to_unban} rule not found in firewalld (may have been removed already)')
else:
logging.CyberCPLogFileWriter.writeToFile(f'Warning: Failed to remove firewalld rule for {ip_to_unban}: {result.stderr}')
except subprocess.TimeoutExpired:
logging.CyberCPLogFileWriter.writeToFile(f'Timeout removing firewalld rule for {ip_to_unban}')
FirewallUtilities.unblockIP(ip_to_unban)
logging.CyberCPLogFileWriter.writeToFile(f'Unbanned IP {ip_to_unban}')
except Exception as e:
logging.CyberCPLogFileWriter.writeToFile(f'Failed to remove firewalld rule for {ip_to_unban}: {str(e)}')
logging.CyberCPLogFileWriter.writeToFile(f'Warning removing firewalld rule for {ip_to_unban}: {str(e)}')
final_dic = {'status': 1, 'message': f'IP address {ip_to_unban} has been unbanned successfully'}
final_json = json.dumps(final_dic)

View File

@@ -15,7 +15,7 @@ app.controller('createFTPAccount', function ($scope, $http) {
$scope.generatedPasswordView = true;
$(document).ready(function () {
$( ".ftpDetails" ).hide();
$( ".ftpDetails, .account-details" ).hide();
$( ".ftpPasswordView" ).hide();
// Only use select2 if it's actually a function (avoids errors when Rocket Loader defers scripts)
@@ -26,9 +26,11 @@ app.controller('createFTPAccount', function ($scope, $http) {
$sel.select2();
$sel.on('select2:select', function (e) {
var data = e.params.data;
$scope.ftpDomain = data.text;
$scope.$apply();
$(".ftpDetails").show();
$scope.$evalAsync(function () {
$scope.ftpDomain = data.text;
$scope.ftpDetails = false;
});
$(".ftpDetails, .account-details").show();
});
} else {
initNativeSelect();
@@ -41,20 +43,23 @@ app.controller('createFTPAccount', function ($scope, $http) {
}
function initNativeSelect() {
$('.create-ftp-acct-select').off('select2:select').on('change', function () {
$scope.ftpDomain = $(this).val();
$scope.$apply();
$(".ftpDetails").show();
var val = $(this).val();
$scope.$evalAsync(function () {
$scope.ftpDomain = val;
$scope.ftpDetails = (val && val !== '') ? false : true;
});
$(".ftpDetails, .account-details").show();
});
}
});
$scope.showFTPDetails = function() {
if ($scope.ftpDomain && $scope.ftpDomain !== "") {
$(".ftpDetails").show();
$scope.ftpDetails = false;
$(".ftpDetails, .account-details").show();
} else {
$(".ftpDetails").hide();
$scope.ftpDetails = true;
$(".ftpDetails, .account-details").hide();
}
};

View File

@@ -452,18 +452,65 @@
<div class="col-md-8">
<div class="form-group">
<label class="form-label">{% trans "Select Website" %}</label>
<select ng-model="ftpDomain" ng-change="showFTPDetails()" class="form-control create-ftp-acct-select">
<select id="create-ftp-website-select" ng-model="ftpDomain" ng-change="showFTPDetails()" class="form-control create-ftp-acct-select">
<option value="">{% trans "Choose a website..." %}</option>
{% for items in websiteList %}
<option value="{{ items }}">{{ items }}</option>
{% endfor %}
</select>
<script data-cfasync="false">
(function() {
function hasWebsiteSelected() {
var sel = document.getElementById('create-ftp-website-select');
if (!sel) return false;
if (sel.value && sel.value !== '') return true;
var rendered = document.querySelector('.select2-selection__rendered');
var text = rendered ? (rendered.title || rendered.textContent || '').trim() : '';
return text !== '' && text.indexOf('Choose') === -1;
}
function showFTPFormSection() {
var sel = document.getElementById('create-ftp-website-select');
var section = document.querySelector('.account-details.ftpDetails');
var container = document.querySelector('.modern-container[ng-controller="createFTPAccount"]');
if (section && hasWebsiteSelected()) {
section.classList.remove('ng-hide');
section.style.setProperty('display', 'block', 'important');
try { section.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } catch (e) {}
if (typeof angular !== 'undefined' && container) {
try {
var scope = angular.element(container).scope();
if (scope) {
scope.ftpDetails = false;
scope.$evalAsync(function() { scope.ftpDetails = false; });
}
} catch (e) {}
}
}
}
document.addEventListener('DOMContentLoaded', function() {
var sel = document.getElementById('create-ftp-website-select');
if (!sel) return;
sel.addEventListener('change', showFTPFormSection);
sel.addEventListener('input', showFTPFormSection);
if (sel.value) showFTPFormSection();
var delays = [50, 150, 350, 600, 1000, 1500, 2200, 3000];
for (var i = 0; i < delays.length; i++) {
setTimeout(showFTPFormSection, delays[i]);
}
var pollCount = 0;
var pollId = setInterval(function() {
showFTPFormSection();
if (++pollCount >= 20) clearInterval(pollId);
}, 200);
});
})();
</script>
</div>
</div>
</div>
</div>
<div ng-hide="ftpDetails" class="account-details">
<div ng-hide="ftpDetails" class="account-details ftpDetails">
<h3><i class="fas fa-cog"></i> {% trans "FTP Account Details" %}</h3>
<div class="row">

251
install/env_generator.py Normal file
View File

@@ -0,0 +1,251 @@
#!/usr/bin/env python3
"""
CyberPanel Environment Configuration Generator
Generates secure .env file with random passwords during installation
"""
import os
import sys
import secrets
import string
import socket
import urllib.request
import re
from pathlib import Path
def generate_secure_password(length=24):
"""
Generate a cryptographically secure password
Args:
length: Length of the password to generate (default 24)
Returns:
str: Random password containing uppercase, lowercase, digits and safe special chars
"""
# Use safe characters that don't require escaping in most contexts
safe_chars = string.ascii_letters + string.digits + '!@#$%^&*'
return ''.join(secrets.choice(safe_chars) for _ in range(length))
def generate_secret_key(length=64):
"""
Generate a cryptographically secure Django secret key
Args:
length: Length of the secret key to generate (default 64)
Returns:
str: Random secret key
"""
chars = string.ascii_letters + string.digits + '!@#$%^&*(-_=+)'
return ''.join(secrets.choice(chars) for _ in range(length))
def get_public_ip():
"""Get the public IP address of the server using multiple methods"""
methods = [
'https://ipv4.icanhazip.com',
'https://api.ipify.org',
'https://checkip.amazonaws.com',
'https://ipecho.net/plain'
]
for url in methods:
try:
with urllib.request.urlopen(url, timeout=10) as response:
ip = response.read().decode('utf-8').strip()
# Validate IP format
if re.match(r'^(\d{1,3}\.){3}\d{1,3}$', ip):
print(f"✓ Detected public IP: {ip}")
return ip
except Exception as e:
print(f"Failed to get IP from {url}: {e}")
continue
print("⚠️ Could not detect public IP address")
return None
def get_local_ip():
"""Get the local IP address of the server"""
try:
# Connect to a remote address to determine the local IP
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
s.connect(("8.8.8.8", 80))
local_ip = s.getsockname()[0]
print(f"✓ Detected local IP: {local_ip}")
return local_ip
except Exception as e:
print(f"Failed to detect local IP: {e}")
return None
def create_env_file(cyberpanel_path, mysql_root_password=None, cyberpanel_db_password=None):
"""
Create .env file with generated secure credentials
Args:
cyberpanel_path: Path to CyberPanel installation directory
mysql_root_password: Optional MySQL root password (will generate if None)
cyberpanel_db_password: Optional CyberPanel DB password (will generate if None)
"""
# Generate secure passwords if not provided
if not mysql_root_password:
mysql_root_password = generate_secure_password(24)
if not cyberpanel_db_password:
cyberpanel_db_password = generate_secure_password(24)
secret_key = generate_secret_key(64)
# Auto-detect IP addresses for ALLOWED_HOSTS
print("🔍 Auto-detecting server IP addresses...")
# Get hostname and local hostname resolution
try:
hostname = socket.gethostname()
hostname_ip = socket.gethostbyname(hostname)
except:
hostname = 'localhost'
hostname_ip = '127.0.0.1'
# Get actual local IP address
local_ip = get_local_ip()
# Get public IP address
public_ip = get_public_ip()
# Build ALLOWED_HOSTS list with all detected IPs
allowed_hosts = ['localhost', '127.0.0.1']
# Add hostname if different from localhost
if hostname and hostname != 'localhost':
allowed_hosts.append(hostname)
# Add hostname IP if different from localhost
if hostname_ip and hostname_ip not in allowed_hosts:
allowed_hosts.append(hostname_ip)
# Add local IP if detected and different
if local_ip and local_ip not in allowed_hosts:
allowed_hosts.append(local_ip)
# Add public IP if detected and different
if public_ip and public_ip not in allowed_hosts:
allowed_hosts.append(public_ip)
# Add wildcard for maximum compatibility (allows any host)
# This ensures CyberPanel works regardless of how the server is accessed
allowed_hosts.append('*')
allowed_hosts_str = ','.join(allowed_hosts)
print(f"✓ ALLOWED_HOSTS configured: {allowed_hosts_str}")
# Create .env content
env_content = f"""# CyberPanel Environment Configuration
# Generated automatically during installation - DO NOT EDIT MANUALLY
# Generated on: {__import__('datetime').datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
# Django Configuration
SECRET_KEY={secret_key}
DEBUG=False
ALLOWED_HOSTS={allowed_hosts_str}
# Database Configuration - CyberPanel Database
DB_NAME=cyberpanel
DB_USER=cyberpanel
DB_PASSWORD={cyberpanel_db_password}
DB_HOST=localhost
DB_PORT=3306
# Root Database Configuration - MySQL Root Access
ROOT_DB_NAME=mysql
ROOT_DB_USER=root
ROOT_DB_PASSWORD={mysql_root_password}
ROOT_DB_HOST=localhost
ROOT_DB_PORT=3306
# Security Settings
SECURE_SSL_REDIRECT=False
SECURE_HSTS_SECONDS=0
SECURE_HSTS_INCLUDE_SUBDOMAINS=False
SECURE_HSTS_PRELOAD=False
SESSION_COOKIE_SECURE=False
CSRF_COOKIE_SECURE=False
# File Upload Settings
DATA_UPLOAD_MAX_MEMORY_SIZE=2147483648
# Logging Configuration
LOG_LEVEL=INFO
"""
# Write .env file
env_file_path = os.path.join(cyberpanel_path, '.env')
with open(env_file_path, 'w') as f:
f.write(env_content)
# Set secure permissions (owner read/write only)
os.chmod(env_file_path, 0o600)
print(f"✓ Generated secure .env file at: {env_file_path}")
print(f"✓ MySQL Root Password: {mysql_root_password}")
print(f"✓ CyberPanel DB Password: {cyberpanel_db_password}")
print(f"✓ Django Secret Key: {secret_key[:20]}...")
return {
'mysql_root_password': mysql_root_password,
'cyberpanel_db_password': cyberpanel_db_password,
'secret_key': secret_key
}
def create_env_backup(cyberpanel_path, credentials):
"""
Create a secure backup of credentials for recovery purposes
Args:
cyberpanel_path: Path to CyberPanel installation directory
credentials: Dictionary containing generated credentials
"""
backup_content = f"""# CyberPanel Credentials Backup
# Generated: {__import__('datetime').datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
#
# IMPORTANT: Store this file securely and delete it after recording credentials
# These are your database passwords and should be kept confidential
MySQL Root Password: {credentials['mysql_root_password']}
CyberPanel Database Password: {credentials['cyberpanel_db_password']}
Django Secret Key: {credentials['secret_key']}
# To restore these credentials, copy them to your .env file
"""
backup_file_path = os.path.join(cyberpanel_path, '.env.backup')
with open(backup_file_path, 'w') as f:
f.write(backup_content)
# Set secure permissions (owner read/write only)
os.chmod(backup_file_path, 0o600)
print(f"✓ Created credentials backup at: {backup_file_path}")
print("⚠️ IMPORTANT: Record these credentials and delete the backup file for security")
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: python env_generator.py <cyberpanel_path> [mysql_root_password] [cyberpanel_db_password]")
sys.exit(1)
cyberpanel_path = sys.argv[1]
mysql_root_password = sys.argv[2] if len(sys.argv) > 2 else None
cyberpanel_db_password = sys.argv[3] if len(sys.argv) > 3 else None
if not os.path.exists(cyberpanel_path):
print(f"Error: CyberPanel path does not exist: {cyberpanel_path}")
sys.exit(1)
try:
credentials = create_env_file(cyberpanel_path, mysql_root_password, cyberpanel_db_password)
create_env_backup(cyberpanel_path, credentials)
print("\n✓ Environment configuration generated successfully!")
print("✓ Remember to delete .env.backup after recording credentials")
except Exception as e:
print(f"Error generating environment configuration: {e}")
sys.exit(1)

1449
install/installCyberPanel.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -31,6 +31,6 @@ MaxDiskUsage 99
CustomerProof yes
TLS 1
PassivePortRange 40110 40210
# Quota enforcement
Quota yes
# Quota enforcement (maxfiles:maxsizeMB; enables MySQL per-user quotas)
Quota 100000:100000

View File

@@ -31,6 +31,6 @@ MaxDiskUsage 99
CustomerProof yes
TLS 1
PassivePortRange 40110 40210
# Quota enforcement
Quota yes
# Quota enforcement (maxfiles:maxsizeMB; enables MySQL per-user quotas)
Quota 100000:100000

View File

@@ -43,6 +43,27 @@ import bcrypt
import threading as multi
import argparse
def _get_email_limits_controller_js():
"""Return EmailLimitsNew controller JS: from file or hardcoded fallback so it always works."""
try:
from django.conf import settings
for base in (os.path.dirname(__file__), getattr(settings, 'BASE_DIR', None)):
if not base:
continue
for script_path in (
os.path.join(base, 'static', 'mailServer', 'emailLimitsController.js'),
os.path.join(base, 'mailServer', 'static', 'mailServer', 'emailLimitsController.js'),
):
if os.path.isfile(script_path):
with open(script_path, 'r') as f:
return f.read()
except Exception:
pass
# Hardcoded fallback so page works even when static file is missing or path wrong
return r"""(function(){'use strict';var app=typeof window.app!=='undefined'?window.app:angular.module('CyberCP');if(!app)return;app.controller('EmailLimitsNew',function($scope,$http){$scope.creationBox=true;$scope.emailDetails=true;$scope.forwardLoading=false;$scope.forwardError=true;$scope.forwardSuccess=true;$scope.couldNotConnect=true;$scope.notifyBox=true;$scope.showEmailDetails=function(){$scope.creationBox=true;$scope.emailDetails=true;$scope.forwardLoading=true;$scope.forwardError=true;$scope.forwardSuccess=true;$scope.couldNotConnect=true;$scope.notifyBox=true;var url="/email/getEmailsForDomain",data={domain:$scope.emailDomain},config={headers:{'X-CSRFToken':getCookie('csrftoken')}};$http.post(url,data,config).then(function(r){if(r.data.fetchStatus===1){$scope.emails=JSON.parse(r.data.data);$scope.creationBox=true;$scope.emailDetails=false;$scope.forwardLoading=false;$scope.notifyBox=false;}else{$scope.creationBox=true;$scope.emailDetails=true;$scope.forwardLoading=false;$scope.forwardError=false;$scope.errorMessage=r.data.error_message;}},function(){$scope.creationBox=true;$scope.emailDetails=true;$scope.couldNotConnect=false;$scope.notifyBox=false;});};$scope.selectForwardingEmail=function(){$scope.creationBox=false;$scope.emailDetails=false;$scope.forwardLoading=true;$scope.notifyBox=true;var g=$scope.selectedEmail;if($scope.emails)for(var i=0;i<$scope.emails.length;i++)if($scope.emails[i].email===g){$scope.numberofEmails=$scope.emails[i].numberofEmails;$scope.duration=$scope.emails[i].duration;break;}};$scope.SaveChanges=function(){$scope.forwardLoading=true;var url="/email/SaveEmailLimitsNew",data={numberofEmails:$scope.numberofEmails,source:$scope.selectedEmail,duration:$scope.duration},config={headers:{'X-CSRFToken':getCookie('csrftoken')}};$http.post(url,data,config).then(function(r){if(r.data.status===1){$scope.forwardLoading=false;if(typeof PNotify!=='undefined')new PNotify({title:'Success!',text:'Changes applied.',type:'success'});$scope.showEmailDetails();}else{$scope.forwardError=false;$scope.notifyBox=false;if(typeof PNotify!=='undefined')new PNotify({title:'Error!',text:r.data.error_message||'Error',type:'error'});}},function(){$scope.creationBox=true;$scope.couldNotConnect=false;});};});})();"""
class MailServerManager(multi.Thread):
def __init__(self, request = None, function = None, extraArgs = None):
@@ -177,7 +198,9 @@ class MailServerManager(multi.Thread):
userID = self.request.session['userID']
currentACL = ACLManager.loadedACL(userID)
if ACLManager.currentContextPermission(currentACL, 'deleteEmail') == 0:
# Allow fetch for List Emails (deleteEmail) or Email Limits (emailForwarding)
if (ACLManager.currentContextPermission(currentACL, 'deleteEmail') == 0 and
ACLManager.currentContextPermission(currentACL, 'emailForwarding') == 0):
return ACLManager.loadErrorJson('fetchStatus', 0)
data = json.loads(self.request.body)
@@ -1971,9 +1994,13 @@ protocol sieve {
except BaseException as msg:
template = 'mailServer/EmailLimits.html'
# Embed controller script inline so page works (no 404, no file path issues)
email_limits_controller_js = _get_email_limits_controller_js()
# Prevent </script> in JS from closing the HTML script tag
email_limits_controller_js = email_limits_controller_js.replace('</script>', '<\\/script>')
proc = httpProc(self.request, template,
{'websiteList': websitesName, "status": 1}, 'emailForwarding')
{'websiteList': websitesName, "status": 1, 'email_limits_controller_js': email_limits_controller_js}, 'emailForwarding')
return proc.render()
def SaveEmailLimitsNew(self):

View File

@@ -0,0 +1,146 @@
/**
* Email Limits page controller - ensures EmailLimitsNew is registered on CyberCP
* even if main mailServer.js loads late or fails. Load this script in the
* EmailLimits template so the page works reliably.
*/
(function () {
'use strict';
var app = (typeof window.app !== 'undefined') ? window.app : angular.module('CyberCP');
if (!app) return;
app.controller('EmailLimitsNew', function ($scope, $http) {
$scope.creationBox = true;
$scope.emailDetails = true;
$scope.forwardLoading = false;
$scope.forwardError = true;
$scope.forwardSuccess = true;
$scope.couldNotConnect = true;
$scope.notifyBox = true;
$scope.showEmailDetails = function () {
$scope.creationBox = true;
$scope.emailDetails = true;
$scope.forwardLoading = true;
$scope.forwardError = true;
$scope.forwardSuccess = true;
$scope.couldNotConnect = true;
$scope.notifyBox = true;
var url = "/email/getEmailsForDomain";
var data = { domain: $scope.emailDomain };
var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') } };
$http.post(url, data, config).then(ListInitialDatas, cantLoadInitialDatas);
function ListInitialDatas(response) {
if (response.data.fetchStatus === 1) {
$scope.emails = JSON.parse(response.data.data);
$scope.creationBox = true;
$scope.emailDetails = false;
$scope.forwardLoading = false;
$scope.forwardError = true;
$scope.forwardSuccess = true;
$scope.couldNotConnect = true;
$scope.notifyBox = false;
} else {
$scope.creationBox = true;
$scope.emailDetails = true;
$scope.forwardLoading = false;
$scope.forwardError = false;
$scope.forwardSuccess = true;
$scope.couldNotConnect = true;
$scope.notifyBox = false;
$scope.errorMessage = response.data.error_message;
}
}
function cantLoadInitialDatas() {
$scope.creationBox = true;
$scope.emailDetails = true;
$scope.forwardLoading = false;
$scope.forwardError = true;
$scope.forwardSuccess = true;
$scope.couldNotConnect = false;
$scope.notifyBox = false;
}
};
$scope.selectForwardingEmail = function () {
$scope.creationBox = false;
$scope.emailDetails = false;
$scope.forwardLoading = true;
$scope.forwardError = true;
$scope.forwardSuccess = true;
$scope.couldNotConnect = true;
$scope.notifyBox = true;
var givenEmail = $scope.selectedEmail;
if ($scope.emails) {
for (var i = 0; i < $scope.emails.length; i++) {
if ($scope.emails[i].email === givenEmail) {
$scope.numberofEmails = $scope.emails[i].numberofEmails;
$scope.duration = $scope.emails[i].duration;
break;
}
}
}
};
$scope.SaveChanges = function () {
$scope.creationBox = false;
$scope.emailDetails = false;
$scope.forwardLoading = true;
$scope.forwardError = true;
$scope.forwardSuccess = true;
$scope.couldNotConnect = true;
$scope.notifyBox = true;
var url = "/email/SaveEmailLimitsNew";
var data = {
numberofEmails: $scope.numberofEmails,
source: $scope.selectedEmail,
duration: $scope.duration
};
var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') } };
$http.post(url, data, config).then(SaveListInitialDatas, SaveCantLoadInitialDatas);
function SaveListInitialDatas(response) {
if (response.data.status === 1) {
$scope.creationBox = false;
$scope.emailDetails = false;
$scope.forwardLoading = false;
$scope.forwardError = true;
$scope.forwardSuccess = true;
$scope.couldNotConnect = true;
$scope.notifyBox = true;
if (typeof PNotify === 'function') {
new PNotify({ title: 'Success!', text: 'Changes applied.', type: 'success' });
}
$scope.showEmailDetails();
} else {
$scope.creationBox = false;
$scope.emailDetails = false;
$scope.forwardLoading = false;
$scope.forwardError = false;
$scope.forwardSuccess = true;
$scope.couldNotConnect = true;
$scope.notifyBox = false;
if (typeof PNotify === 'function') {
new PNotify({ title: 'Error!', text: response.data.error_message || 'Error', type: 'error' });
}
}
}
function SaveCantLoadInitialDatas() {
$scope.creationBox = true;
$scope.emailDetails = true;
$scope.forwardLoading = false;
$scope.forwardError = true;
$scope.forwardSuccess = true;
$scope.couldNotConnect = false;
$scope.notifyBox = false;
}
};
});
})();

View File

@@ -2,6 +2,14 @@
* Created by usman on 8/15/17.
*/
// Ensure app is available (get existing CyberCP module so controllers register correctly)
if (typeof app === 'undefined') {
if (typeof window !== 'undefined' && typeof window.app !== 'undefined') {
app = window.app;
} else {
app = angular.module('CyberCP');
}
}
/* Java script code to create account */
app.controller('createEmailAccount', function ($scope, $http) {
@@ -1506,6 +1514,7 @@ app.controller('EmailLimitsNew', function ($scope, $http) {
// Given email to search for
var givenEmail = $scope.selectedEmail;
if ($scope.emails) {
for (var i = 0; i < $scope.emails.length; i++) {
if ($scope.emails[i].email === givenEmail) {
// Extract numberofEmails and duration
@@ -1515,14 +1524,11 @@ app.controller('EmailLimitsNew', function ($scope, $http) {
$scope.numberofEmails = numberofEmails;
$scope.duration = duration;
// Use numberofEmails and duration as needed
console.log("Number of emails:", numberofEmails);
console.log("Duration:", duration);
// Break out of the loop since the email is found
break;
}
}
}
};

View File

@@ -1,5 +1,6 @@
{% extends "baseTemplate/index.html" %}
{% load i18n %}
{% load static %}
{% block title %}{% trans "Email Limits - CyberPanel" %}{% endblock %}
{% block content %}
@@ -387,4 +388,13 @@
</div>
</div>
{% endblock %}
{% block footer_scripts %}
{# EmailLimitsNew controller: inline from view (preferred) or fallback to static file #}
{% if email_limits_controller_js %}
<script data-cfasync="false">{{ email_limits_controller_js|safe }}</script>
{% else %}
<script src="{% static 'mailServer/emailLimitsController.js' %}?v=2.4.4.1" data-cfasync="false"></script>
{% endif %}
{% endblock %}

View File

@@ -118,22 +118,23 @@ class FirewallUtilities:
logging.CyberCPLogFileWriter.writeToFile(f"Blocking IP address: {ip_address} - Reason: {reason}")
# executioner returns 1 on success, 0 on failure
result = ProcessUtilities.executioner(command)
if result == 0:
if result == 1:
logging.CyberCPLogFileWriter.writeToFile(f"Successfully blocked IP: {ip_address}")
# Reload firewall to apply changes
ProcessUtilities.executioner('firewall-cmd --reload')
# Log the block in a dedicated file
block_log_path = "/usr/local/lscp/logs/blocked_ips.log"
with open(block_log_path, "a") as f:
from datetime import datetime
f.write(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} - {ip_address} - {reason}\n")
block_log_path = "/usr/local/CyberCP/data/blocked_ips.log"
try:
import os
os.makedirs(os.path.dirname(block_log_path), exist_ok=True)
with open(block_log_path, "a") as f:
from datetime import datetime
f.write(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} - {ip_address} - {reason}\n")
except Exception as log_err:
logging.CyberCPLogFileWriter.writeToFile(f"Warning: could not write blocked_ips.log: {log_err}")
return True, f"IP {ip_address} blocked successfully"
else:
logging.CyberCPLogFileWriter.writeToFile(f"Failed to block IP: {ip_address}")
logging.CyberCPLogFileWriter.writeToFile(f"Failed to block IP: {ip_address} (executioner returned %s)" % result)
return False, f"Failed to block IP: {ip_address}"
except Exception as e:
@@ -154,16 +155,14 @@ class FirewallUtilities:
logging.CyberCPLogFileWriter.writeToFile(f"Unblocking IP address: {ip_address}")
# executioner returns 1 on success, 0 on failure
result = ProcessUtilities.executioner(command)
if result == 0:
if result == 1:
logging.CyberCPLogFileWriter.writeToFile(f"Successfully unblocked IP: {ip_address}")
# Reload firewall to apply changes
ProcessUtilities.executioner('firewall-cmd --reload')
return True, f"IP {ip_address} unblocked successfully"
else:
logging.CyberCPLogFileWriter.writeToFile(f"Failed to unblock IP: {ip_address}")
logging.CyberCPLogFileWriter.writeToFile(f"Failed to unblock IP: {ip_address} (executioner returned %s)" % result)
return False, f"Failed to unblock IP: {ip_address}"
except Exception as e:

View File

@@ -1682,69 +1682,69 @@ LogFile /var/log/clamav/clamav.log
# Query each DNS server
for url in urls:
try:
response = requests.get(f'{url}/index.php?ip={ip_address}', timeout=5)
try:
response = requests.get(f'{url}/index.php?ip={ip_address}', timeout=5)
if os.path.exists(ProcessUtilities.debugPath):
logging.CyberCPLogFileWriter.writeToFile(f'url to call {ip_address} is {url}')
if os.path.exists(ProcessUtilities.debugPath):
logging.CyberCPLogFileWriter.writeToFile(f'url to call {ip_address} is {url}')
if response.status_code == 200:
try:
data = response.json()
if response.status_code == 200:
try:
data = response.json()
if os.path.exists(ProcessUtilities.debugPath):
logging.CyberCPLogFileWriter.writeToFile(f'response from dns system {str(data)}')
if os.path.exists(ProcessUtilities.debugPath):
logging.CyberCPLogFileWriter.writeToFile(f'response from dns system {str(data)}')
# Validate response structure
if not isinstance(data, dict):
logging.CyberCPLogFileWriter.writeToFile(f'Invalid response format from {url}: not a dictionary')
continue
# Validate response structure
if not isinstance(data, dict):
logging.CyberCPLogFileWriter.writeToFile(f'Invalid response format from {url}: not a dictionary')
continue
if 'status' not in data:
logging.CyberCPLogFileWriter.writeToFile(f'Response from {url} missing "status" key')
continue
if 'status' not in data:
logging.CyberCPLogFileWriter.writeToFile(f'Response from {url} missing "status" key')
continue
if data['status'] == 1:
# Validate results structure
if 'results' not in data or not isinstance(data['results'], dict):
logging.CyberCPLogFileWriter.writeToFile(f'Response from {url} missing or invalid "results" key')
if data['status'] == 1:
# Validate results structure
if 'results' not in data or not isinstance(data['results'], dict):
logging.CyberCPLogFileWriter.writeToFile(f'Response from {url} missing or invalid "results" key')
continue
results_dict = data['results']
# Safely extract results from different DNS servers
dns_servers = ['8.8.8.8', '1.1.1.1', '9.9.9.9']
for dns_server in dns_servers:
if dns_server in results_dict:
result_value = results_dict[dns_server]
if result_value and result_value not in results:
results.append(result_value)
successful_queries += 1
else:
if os.path.exists(ProcessUtilities.debugPath):
logging.CyberCPLogFileWriter.writeToFile(f'DNS server {url} returned status != 1: {data.get("status", "unknown")}')
except ValueError as e:
logging.CyberCPLogFileWriter.writeToFile(f'Failed to parse JSON response from {url}: {str(e)}')
continue
except KeyError as e:
logging.CyberCPLogFileWriter.writeToFile(f'Missing key in response from {url}: {str(e)}')
continue
else:
if os.path.exists(ProcessUtilities.debugPath):
logging.CyberCPLogFileWriter.writeToFile(f'DNS server {url} returned HTTP {response.status_code}')
except Timeout as e:
logging.CyberCPLogFileWriter.writeToFile(f'Timeout while querying DNS server {url}: {str(e)}')
continue
except ConnectionError as e:
logging.CyberCPLogFileWriter.writeToFile(f'Connection error while querying DNS server {url}: {str(e)}')
continue
except RequestException as e:
logging.CyberCPLogFileWriter.writeToFile(f'Request error while querying DNS server {url}: {str(e)}')
continue
except Exception as e:
logging.CyberCPLogFileWriter.writeToFile(f'Unexpected error while querying DNS server {url}: {str(e)}')
continue
results_dict = data['results']
# Safely extract results from different DNS servers
dns_servers = ['8.8.8.8', '1.1.1.1', '9.9.9.9']
for dns_server in dns_servers:
if dns_server in results_dict:
result_value = results_dict[dns_server]
if result_value and result_value not in results:
results.append(result_value)
successful_queries += 1
else:
if os.path.exists(ProcessUtilities.debugPath):
logging.CyberCPLogFileWriter.writeToFile(f'DNS server {url} returned status != 1: {data.get("status", "unknown")}')
except ValueError as e:
logging.CyberCPLogFileWriter.writeToFile(f'Failed to parse JSON response from {url}: {str(e)}')
continue
except KeyError as e:
logging.CyberCPLogFileWriter.writeToFile(f'Missing key in response from {url}: {str(e)}')
continue
else:
if os.path.exists(ProcessUtilities.debugPath):
logging.CyberCPLogFileWriter.writeToFile(f'DNS server {url} returned HTTP {response.status_code}')
except Timeout as e:
logging.CyberCPLogFileWriter.writeToFile(f'Timeout while querying DNS server {url}: {str(e)}')
continue
except ConnectionError as e:
logging.CyberCPLogFileWriter.writeToFile(f'Connection error while querying DNS server {url}: {str(e)}')
continue
except RequestException as e:
logging.CyberCPLogFileWriter.writeToFile(f'Request error while querying DNS server {url}: {str(e)}')
continue
except Exception as e:
logging.CyberCPLogFileWriter.writeToFile(f'Unexpected error while querying DNS server {url}: {str(e)}')
continue
if os.path.exists(ProcessUtilities.debugPath):
logging.CyberCPLogFileWriter.writeToFile(f'rDNS result of {ip_address} is {str(results)} (successful queries: {successful_queries}/{len(urls)})')

View File

@@ -378,8 +378,8 @@ class ProcessUtilities(multi.Thread):
if getpass.getuser() == 'root':
if os.path.exists(ProcessUtilities.debugPath):
logging.writeToFile(f"[executioner] Running as root, using normalExecutioner")
ProcessUtilities.normalExecutioner(command, shell, user)
return 1
res = ProcessUtilities.normalExecutioner(command, shell, user)
return 1 if res == 1 else 0
if os.path.exists(ProcessUtilities.debugPath):
logging.writeToFile(f"[executioner] Not root, using sendCommand via lscpd")

View File

@@ -8,20 +8,44 @@ app.controller('createFTPAccount', function ($scope, $http) {
$scope.ftpLoading = false;
$scope.ftpDetails = true;
$(document).ready(function () {
$( ".ftpDetails" ).hide();
$( ".ftpDetails, .account-details" ).hide();
$( ".ftpPasswordView" ).hide();
$('.create-ftp-acct-select').select2();
if (typeof $ !== 'undefined' && $ && typeof $.fn !== 'undefined' && typeof $.fn.select2 === 'function') {
try {
var $sel = $('.create-ftp-acct-select');
if ($sel.length) {
$sel.select2();
$sel.on('select2:select', function (e) {
var data = e.params.data;
$scope.ftpDomain = data.text;
$scope.ftpDetails = false;
$scope.$apply();
$(".ftpDetails, .account-details").show();
});
}
} catch (err) {}
}
$('.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();
});
});
$('.create-ftp-acct-select').on('select2:select', function (e) {
var data = e.params.data;
$scope.ftpDomain = data.text;
$( ".ftpDetails" ).show();
});
$scope.ftpLoading = true;
$scope.showFTPDetails = function() {
if ($scope.ftpDomain && $scope.ftpDomain !== "") {
$scope.ftpDetails = false;
$(".ftpDetails, .account-details").show();
} else {
$scope.ftpDetails = true;
$(".ftpDetails, .account-details").hide();
}
};
$scope.createFTPAccount = function () {

View File

@@ -641,11 +641,15 @@ app.controller('servicesManager', function ($scope, $http) {
getServiceStatus();
$scope.ActionSuccessfull = true;
$scope.ActionFailed = false;
$scope.actionErrorMsg = '';
$scope.couldNotConnect = false;
$scope.actionLoader = false;
$scope.btnDisable = false;
}, 3000);
} else {
var errMsg = (response.data && response.data.error_message) ? response.data.error_message : 'Action failed';
if (errMsg === 0) errMsg = 'Action failed';
$scope.actionErrorMsg = errMsg;
setTimeout(function () {
getServiceStatus();
$scope.ActionSuccessfull = false;
@@ -654,7 +658,6 @@ app.controller('servicesManager', function ($scope, $http) {
$scope.actionLoader = false;
$scope.btnDisable = false;
}, 5000);
}
}

View File

@@ -622,6 +622,7 @@
<div ng-show="ActionFailed" class="alert alert-danger">
<i class="fas fa-exclamation-circle alert-icon"></i>
<span>{% trans "Action Failed" %}</span>
<span ng-show="actionErrorMsg" class="d-block mt-2"><strong>{% trans "Details:" %}</strong> <span ng-bind="actionErrorMsg"></span></span>
</div>
<div ng-show="ActionSuccessfull" class="alert alert-success">
<i class="fas fa-check-circle alert-icon"></i>

View File

@@ -319,18 +319,36 @@ def servicesAction(request):
final_json = json.dumps(final_dic)
return HttpResponse(final_json)
else:
if service == 'pure-ftpd':
if os.path.exists("/etc/lsb-release"):
service = 'pure-ftpd-mysql'
else:
service = 'pure-ftpd'
if service == 'pure-ftpd':
if os.path.exists("/etc/lsb-release"):
service = 'pure-ftpd-mysql'
else:
service = 'pure-ftpd'
command = 'sudo systemctl %s %s' % (action, service)
ProcessUtilities.executioner(command)
final_dic = {'serviceAction': 1, "error_message": 0}
final_json = json.dumps(final_dic)
return HttpResponse(final_json)
# Run as root with shell so systemctl has permission (panel may run as lscpd)
command = 'systemctl %s %s' % (action, service)
ProcessUtilities.executioner(command, 'root', True)
time.sleep(1)
# For start action, verify service actually came up; return error if not
if action == 'start':
try:
out = ProcessUtilities.outputExecutioner('systemctl is-active %s' % service, 'root', True)
if not (out and out.strip() == 'active'):
status_out = ProcessUtilities.outputExecutioner(
'systemctl status %s --no-pager -l 2>&1 | head -15' % service, 'root', True)
err_msg = (status_out or '').strip().replace('\n', ' ')[:400]
final_dic = {'serviceAction': 0, 'error_message': 'Service did not start. ' + err_msg}
final_json = json.dumps(final_dic)
return HttpResponse(final_json)
except Exception as e:
final_dic = {'serviceAction': 0, 'error_message': 'Service did not start: %s' % str(e)}
final_json = json.dumps(final_dic)
return HttpResponse(final_json)
final_dic = {'serviceAction': 1, "error_message": 0}
final_json = json.dumps(final_dic)
return HttpResponse(final_json)
except BaseException as msg:

21
sql/create_ftp_quotas.sql Normal file
View File

@@ -0,0 +1,21 @@
-- Create ftp_quotas table for FTP Quota Management (websiteFunctions.models.FTPQuota)
-- Run once per CyberPanel database. Safe to run: uses IF NOT EXISTS.
CREATE TABLE IF NOT EXISTS `ftp_quotas` (
`id` INT AUTO_INCREMENT NOT NULL PRIMARY KEY,
`user_id` INT NOT NULL,
`ftp_user` VARCHAR(255) NOT NULL,
`domain_id` INT NULL,
`quota_size_mb` INT NOT NULL DEFAULT 0,
`quota_used_mb` INT NOT NULL DEFAULT 0,
`quota_files` INT NOT NULL DEFAULT 0,
`quota_files_used` INT NOT NULL DEFAULT 0,
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
`created_at` DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
`updated_at` DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
UNIQUE KEY `ftp_quotas_ftp_user_unique` (`ftp_user`),
KEY `ftp_quotas_user_id` (`user_id`),
KEY `ftp_quotas_domain_id` (`domain_id`),
CONSTRAINT `ftp_quotas_user_id_fk` FOREIGN KEY (`user_id`) REFERENCES `loginSystem_administrator` (`id`) ON DELETE CASCADE,
CONSTRAINT `ftp_quotas_domain_id_fk` FOREIGN KEY (`domain_id`) REFERENCES `websiteFunctions_websites` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

View File

@@ -15,7 +15,7 @@ app.controller('createFTPAccount', function ($scope, $http) {
$scope.generatedPasswordView = true;
$(document).ready(function () {
$( ".ftpDetails" ).hide();
$( ".ftpDetails, .account-details" ).hide();
$( ".ftpPasswordView" ).hide();
// Only use select2 if it's actually a function (avoids errors when Rocket Loader defers scripts)
@@ -27,8 +27,9 @@ app.controller('createFTPAccount', function ($scope, $http) {
$sel.on('select2:select', function (e) {
var data = e.params.data;
$scope.ftpDomain = data.text;
$scope.ftpDetails = false;
$scope.$apply();
$(".ftpDetails").show();
$(".ftpDetails, .account-details").show();
});
} else {
initNativeSelect();
@@ -42,19 +43,20 @@ app.controller('createFTPAccount', function ($scope, $http) {
function initNativeSelect() {
$('.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").show();
$(".ftpDetails, .account-details").show();
});
}
});
$scope.showFTPDetails = function() {
if ($scope.ftpDomain && $scope.ftpDomain !== "") {
$(".ftpDetails").show();
$scope.ftpDetails = false;
$(".ftpDetails, .account-details").show();
} else {
$(".ftpDetails").hide();
$scope.ftpDetails = true;
$(".ftpDetails, .account-details").hide();
}
};

View File

@@ -0,0 +1,146 @@
/**
* Email Limits page controller - ensures EmailLimitsNew is registered on CyberCP
* even if main mailServer.js loads late or fails. Load this script in the
* EmailLimits template so the page works reliably.
*/
(function () {
'use strict';
var app = (typeof window.app !== 'undefined') ? window.app : angular.module('CyberCP');
if (!app) return;
app.controller('EmailLimitsNew', function ($scope, $http) {
$scope.creationBox = true;
$scope.emailDetails = true;
$scope.forwardLoading = false;
$scope.forwardError = true;
$scope.forwardSuccess = true;
$scope.couldNotConnect = true;
$scope.notifyBox = true;
$scope.showEmailDetails = function () {
$scope.creationBox = true;
$scope.emailDetails = true;
$scope.forwardLoading = true;
$scope.forwardError = true;
$scope.forwardSuccess = true;
$scope.couldNotConnect = true;
$scope.notifyBox = true;
var url = "/email/getEmailsForDomain";
var data = { domain: $scope.emailDomain };
var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') } };
$http.post(url, data, config).then(ListInitialDatas, cantLoadInitialDatas);
function ListInitialDatas(response) {
if (response.data.fetchStatus === 1) {
$scope.emails = JSON.parse(response.data.data);
$scope.creationBox = true;
$scope.emailDetails = false;
$scope.forwardLoading = false;
$scope.forwardError = true;
$scope.forwardSuccess = true;
$scope.couldNotConnect = true;
$scope.notifyBox = false;
} else {
$scope.creationBox = true;
$scope.emailDetails = true;
$scope.forwardLoading = false;
$scope.forwardError = false;
$scope.forwardSuccess = true;
$scope.couldNotConnect = true;
$scope.notifyBox = false;
$scope.errorMessage = response.data.error_message;
}
}
function cantLoadInitialDatas() {
$scope.creationBox = true;
$scope.emailDetails = true;
$scope.forwardLoading = false;
$scope.forwardError = true;
$scope.forwardSuccess = true;
$scope.couldNotConnect = false;
$scope.notifyBox = false;
}
};
$scope.selectForwardingEmail = function () {
$scope.creationBox = false;
$scope.emailDetails = false;
$scope.forwardLoading = true;
$scope.forwardError = true;
$scope.forwardSuccess = true;
$scope.couldNotConnect = true;
$scope.notifyBox = true;
var givenEmail = $scope.selectedEmail;
if ($scope.emails) {
for (var i = 0; i < $scope.emails.length; i++) {
if ($scope.emails[i].email === givenEmail) {
$scope.numberofEmails = $scope.emails[i].numberofEmails;
$scope.duration = $scope.emails[i].duration;
break;
}
}
}
};
$scope.SaveChanges = function () {
$scope.creationBox = false;
$scope.emailDetails = false;
$scope.forwardLoading = true;
$scope.forwardError = true;
$scope.forwardSuccess = true;
$scope.couldNotConnect = true;
$scope.notifyBox = true;
var url = "/email/SaveEmailLimitsNew";
var data = {
numberofEmails: $scope.numberofEmails,
source: $scope.selectedEmail,
duration: $scope.duration
};
var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') } };
$http.post(url, data, config).then(SaveListInitialDatas, SaveCantLoadInitialDatas);
function SaveListInitialDatas(response) {
if (response.data.status === 1) {
$scope.creationBox = false;
$scope.emailDetails = false;
$scope.forwardLoading = false;
$scope.forwardError = true;
$scope.forwardSuccess = true;
$scope.couldNotConnect = true;
$scope.notifyBox = true;
if (typeof new PNotify === 'function') {
new PNotify({ title: 'Success!', text: 'Changes applied.', type: 'success' });
}
$scope.showEmailDetails();
} else {
$scope.creationBox = false;
$scope.emailDetails = false;
$scope.forwardLoading = false;
$scope.forwardError = false;
$scope.forwardSuccess = true;
$scope.couldNotConnect = true;
$scope.notifyBox = false;
if (typeof new PNotify === 'function') {
new PNotify({ title: 'Error!', text: response.data.error_message || 'Error', type: 'error' });
}
}
}
function SaveCantLoadInitialDatas() {
$scope.creationBox = true;
$scope.emailDetails = true;
$scope.forwardLoading = false;
$scope.forwardError = true;
$scope.forwardSuccess = true;
$scope.couldNotConnect = false;
$scope.notifyBox = false;
}
};
});
})();

View File

@@ -2,6 +2,14 @@
* Created by usman on 8/15/17.
*/
// Ensure app is available (get existing CyberCP module so controllers register correctly)
if (typeof app === 'undefined') {
if (typeof window !== 'undefined' && typeof window.app !== 'undefined') {
app = window.app;
} else {
app = angular.module('CyberCP');
}
}
/* Java script code to create account */
app.controller('createEmailAccount', function ($scope, $http) {

View File

@@ -0,0 +1,57 @@
# Email Limits Fix Deploy Checklist
Use this after pulling the Email Limits fixes in this repo so that https://your-panel/email/EmailLimits works (controller registers, email list loads, configure section works).
## Files that are part of the fix
| File | Purpose |
|------|--------|
| `mailServer/mailserverManager.py` | Passes controller JS to template; allows getEmailsForDomain for emailForwarding |
| `mailServer/templates/mailServer/EmailLimits.html` | Inline controller in footer_scripts (no static file dependency) |
| `mailServer/static/mailServer/mailServer.js` | EmailLimitsNew controller + guard for `$scope.emails` |
| `mailServer/static/mailServer/emailLimitsController.js` | Standalone controller + PNotify check fix |
## Option A: Deploy script (recommended)
**Run from anywhere** (use the full path to the script so the shell can find it):
```bash
sudo bash /home/cyberpanel-repo/deploy-email-limits-fix.sh
```
Or from repo root:
```bash
cd /home/cyberpanel-repo && sudo bash deploy-email-limits-fix.sh
```
- Script auto-detects repo at `/home/cyberpanel-repo` if run from another directory.
- Default CyberPanel path: `/usr/local/CyberCP`.
- Override: `sudo bash /home/cyberpanel-repo/deploy-email-limits-fix.sh /path/to/repo /usr/local/CyberCP`.
- Skip restart: `sudo RESTART_LSCPD=0 bash /home/cyberpanel-repo/deploy-email-limits-fix.sh`.
## Option B: Manual copy + restart
On the server, from the repo root (e.g. `/home/cyberpanel-repo`):
```bash
CP_DIR=/usr/local/CyberCP
cp -f mailServer/mailserverManager.py "$CP_DIR/mailServer/"
cp -f mailServer/templates/mailServer/EmailLimits.html "$CP_DIR/mailServer/templates/mailServer/"
cp -f mailServer/static/mailServer/mailServer.js "$CP_DIR/mailServer/static/mailServer/"
cp -f mailServer/static/mailServer/emailLimitsController.js "$CP_DIR/mailServer/static/mailServer/"
sudo systemctl restart lscpd
```
## After deploy
1. Hard refresh the Email Limits page: **Ctrl+Shift+R** (or Cmd+Shift+R).
2. Open **Email Limits**, choose a **website**, then check that **email account** dropdown fills and **Configure Email Limits** appears and works.
## If it still fails
- Confirm the four files above are present under `$CP_DIR` and were updated (check timestamps).
- Check panel/Python logs and browser console for `[$controller:ctrlreg]` or JS errors.
- Ensure `lscpd` (or the process serving the panel) was restarted after copying.

View File

@@ -0,0 +1,108 @@
# Email Limits Live Server Checklist (vs upstream v2.4.4)
## Upstream v2.4.4 behaviour
In [usmannasir/cyberpanel at v2.4.4](https://github.com/usmannasir/cyberpanel/tree/v2.4.4):
- **Template**: `mailServer/templates/mailServer/EmailLimits.html` exists and uses `ng-controller="EmailLimitsNew"` and `{$ … $}` bindings.
- **Routes**: `mailServer/urls.py` has `EmailLimits` and `SaveEmailLimitsNew`.
- **Controller**: The **`EmailLimitsNew` controller is not present** in `static/mailServer/mailServer.js`. Upstream `mailServer.js` ends at “List Emails” and has no `EmailLimitsNew` block.
So on a stock v2.4.4 install, the Email Limits page will show raw `{$ selectedEmail $}` and “Could not connect to server” because the Angular controller is never registered.
---
## How it is loaded in v2.4.4
1. **Base template** (`baseTemplate/templates/baseTemplate/index.html`) loads one script bundle:
- `{% static 'mailServer/mailServer.js' %}?v={{ CP_VERSION }}`
(in the “Additional Scripts” block at the bottom of the body.)
2. **Email Limits template** only provides content; it does **not** load any extra script in upstream. It expects `EmailLimitsNew` to come from `mailServer.js`, but that controller is missing in v2.4.4.
3. **Backend**: `mailServer/views.py``EmailLimits`, `SaveEmailLimitsNew`; `mailServer/mailserverManager.py``EmailLimits()`, `SaveEmailLimitsNew()`.
---
## Files that must be on the live server
Use the paths below relative to the CyberPanel app root (e.g. `/usr/local/CyberCP/` or your repo root). Django static files may be served from `STATIC_ROOT` after `collectstatic`; templates and Python files must be in the app directories.
### 1. Python / URLs / views (same as upstream + your tweaks)
| Path | Purpose |
|------|--------|
| `mailServer/urls.py` | Must include `EmailLimits` and `SaveEmailLimitsNew` routes. |
| `mailServer/views.py` | Must define `EmailLimits` and `SaveEmailLimitsNew` and call manager. |
| `mailServer/mailserverManager.py` | Must implement `EmailLimits()` and `SaveEmailLimitsNew()` and render `mailServer/EmailLimits.html` with `websiteList` and `status`. |
### 2. Template (must load the controller script)
| Path | Purpose |
|------|--------|
| `mailServer/templates/mailServer/EmailLimits.html` | Must extend `baseTemplate/index.html`, contain `ng-controller="EmailLimitsNew"`, and **include the script tag** that loads `emailLimitsController.js` at the top of `{% block content %}`. |
### 3. Base template (unchanged from upstream for Email Limits)
| Path | Purpose |
|------|--------|
| `baseTemplate/templates/baseTemplate/index.html` | Must load `{% static 'mailServer/mailServer.js' %}` in the same script block as other app JS (no `load_email_limits_controller` needed). |
### 4. Static files (at least one of the two options)
**Option A Use main bundle (repos `mailServer.js` with controller)**
| Path | Purpose |
|------|--------|
| `static/mailServer/mailServer.js` | Must define `app` (e.g. `window.app` or `angular.module('CyberCP')`) at the top and register `app.controller('EmailLimitsNew', ...)`. |
| `mailServer/static/mailServer/mailServer.js` | Same as above if you use app static dirs. |
**Option B Use standalone controller (recommended so it works even if `mailServer.js` is old)**
| Path | Purpose |
|------|--------|
| `static/mailServer/emailLimitsController.js` | Standalone script that registers `EmailLimitsNew` on the CyberCP module. |
| `mailServer/static/mailServer/emailLimitsController.js` | Same file under the apps `static` dir. |
The Email Limits template in this repo loads `emailLimitsController.js` at the top of the content block, so the controller is registered on the Email Limits page even if the live server still has an older `mailServer.js` without `EmailLimitsNew`.
---
## Quick verification on the live server
Run from the CyberPanel app root (e.g. `/usr/local/CyberCP/`):
```bash
# 1. Template must contain the controller script and ng-controller
grep -l "emailLimitsController.js" mailServer/templates/mailServer/EmailLimits.html && \
grep -l "EmailLimitsNew" mailServer/templates/mailServer/EmailLimits.html && \
echo "Template OK" || echo "Template MISSING or WRONG"
# 2. Standalone controller script must exist (at least one location)
([ -f static/mailServer/emailLimitsController.js ] || [ -f mailServer/static/mailServer/emailLimitsController.js ]) && \
echo "emailLimitsController.js OK" || echo "emailLimitsController.js MISSING"
# 3. mailServer.js (if you rely on it for Email Limits) must define EmailLimitsNew
grep -q "EmailLimitsNew" static/mailServer/mailServer.js 2>/dev/null || grep -q "EmailLimitsNew" mailServer/static/mailServer/mailServer.js 2>/dev/null && \
echo "mailServer.js has EmailLimitsNew" || echo "mailServer.js has NO EmailLimitsNew (use emailLimitsController.js)"
# 4. Routes
grep -q "EmailLimits" mailServer/urls.py && echo "URLs OK" || echo "URLs MISSING"
```
After deploying, run:
```bash
python3 manage.py collectstatic --noinput
# Restart your app server (e.g. LiteSpeed / Gunicorn)
```
Then hard-refresh the Email Limits page (Ctrl+Shift+R).
---
## Summary
- **Upstream v2.4.4**: Email Limits template and routes exist; **controller is missing** from `mailServer.js`, so the page is broken by default.
- **This repo**: Adds `EmailLimitsNew` in `mailServer.js` and a standalone `emailLimitsController.js`, and the Email Limits template loads `emailLimitsController.js` so the page works even with an old `mailServer.js`.
- **Live server**: Ensure the template, URLs, views, manager, base template, and either the updated `mailServer.js` or `emailLimitsController.js` (or both) are present as in this checklist, then run `collectstatic` and restart the app.

View File

@@ -0,0 +1,25 @@
# FTP Quotas Table Fix
## Problem
- **URL:** https://207.180.193.210:2087/ftp/quotaManagement
- **Error:** `(1146, "Table 'cyberpanel.ftp_quotas' doesn't exist")`
The `FTPQuota` model in `websiteFunctions/models.py` uses `db_table = 'ftp_quotas'`, but the table had never been created in the database.
## Solution
1. **SQL:** `sql/create_ftp_quotas.sql` `CREATE TABLE IF NOT EXISTS ftp_quotas` with columns and FKs to `loginSystem_administrator` and `websiteFunctions_websites`.
2. **Deploy script:** `deploy-ftp-quotas-table.sh` Copies the SQL to `/usr/local/CyberCP/sql/` and runs it using Djangos DB connection (no password on command line).
## Deploy (already run)
```bash
sudo bash /home/cyberpanel-repo/deploy-ftp-quotas-table.sh
```
## Manual run (if needed)
From repo root:
```bash
sudo bash deploy-ftp-quotas-table.sh [REPO_DIR] [CP_DIR]
```
Default `CP_DIR` is `/usr/local/CyberCP`.
After deployment, reload `/ftp/quotaManagement` in the browser.

View File

@@ -0,0 +1,24 @@
# Pure-FTPd Quota Syntax Fix (2026-02-04)
## Problem
Pure-FTPd failed to start with:
```
/etc/pure-ftpd/pure-ftpd.conf:35:1: syntax error line 35: [Quota ...].
```
## Cause
The config used `Quota yes`, but Pure-FTPd expects **`Quota maxfiles:maxsize`** (e.g. `Quota 1000:10` for 1000 files and 10 MB). The value is not a boolean.
## Fix applied
### On the server
- `/etc/pure-ftpd/pure-ftpd.conf`: line 35 set to `Quota 100000:100000` (high default so MySQL per-user quotas apply).
- Service started successfully: `systemctl start pure-ftpd`.
### In the repo
- **install/pure-ftpd/pure-ftpd.conf** and **install/pure-ftpd-one/pure-ftpd.conf**: `Quota yes``Quota 100000:100000`.
- **websiteFunctions/website.py** (`enableFTPQuota`): sed/echo now write `Quota 100000:100000` instead of `Quota yes` (or tabs).
## Reference
- Upstream: https://github.com/jedisct1/pure-ftpd/blob/master/pure-ftpd.conf.in (comment: "Quota 1000:10").
- `pure-ftpd --help`: `-n --quota <opt>`.

View File

@@ -0,0 +1,83 @@
# Runtime vs Repo: What Belongs in cyberpanel-repo for 2.5.5-dev
## Goal
When users upgrade to **our** (master3395) 2.5.5-dev, the panel should look and behave the same. That means **default** look-and-feel and behavior must be defined in the repo, not only “generated” on the server.
---
## What is “runtime generated”?
On the live server, after install/upgrade you have:
1. **From the repo (clone/copy)**
All app code, templates, static sources, migrations, `version.txt`, default `settings.py`, etc.
→ This **should** be in the repo (and already is).
2. **Generated at install/upgrade**
- Python venv under `/usr/local/CyberCP/bin`, `lib`, `lib64`
- `collectstatic` output under `/usr/local/CyberCP/public/static`
- `version` table and `baseTemplate_cyberpanelcosmetic` row (if created by code/migrations)
- `lscpd` binary copy, symlinks, etc.
→ The **sources** that produce these (e.g. static sources, migrations) **should** be in the repo.
3. **Per-server / preserved**
- `CyberCP/settings.py` — upgrade **merges** only the `DATABASES` section from the old server; the rest (e.g. `INSTALLED_APPS`) comes from the **new** clone.
- `baseTemplate/static/baseTemplate/custom/` (custom CSS files)
- DB row `baseTemplate_cyberpanelcosmetic.MainDashboardCSS` (custom dashboard CSS)
- `.git/`, phpMyAdmin config, SnappyMail data, etc.
**Defaults** that define “how 2.5.5-dev looks” should be in the repo; **per-server overrides** stay on the server.
---
## What we need in the repo so 2.5.5-dev “looks the same”
- **Templates, static sources, JS/CSS**
Already in repo (e.g. `baseTemplate/`, `static/`). No change needed for “same look” unless you change the design.
- **Default `settings.py`**
Already in repo. Upgrade keeps DB credentials from the server and uses repo for everything else (e.g. `INSTALLED_APPS`).
So 2.5.5-dev behavior is driven by the repos `settings.py`.
- **Version**
`baseTemplate/views.py` has `VERSION = '2.5.5'`, `BUILD = 'dev'`. Repos `version.txt` is `{"version":"2.5.5","build":"dev"}`.
Upgrade also writes version into the DB. So version “same as 2.5.5-dev” is already defined in the repo.
- **Default “look” (cosmetic)**
- Code already creates a default `CyberPanelCosmetic` row with **empty** `MainDashboardCSS` if none exists (`baseTemplate/context_processors.py`, `plogical/httpProc.py`, `loginSystem/views.py`).
- If **your live server** has custom dashboard CSS (in DB or in `baseTemplate/static/baseTemplate/custom/`), that is **your** customization.
- To make “our 2.5.5-dev” ship with that same look as default, you have two options:
1. **Data migration**
Add a baseTemplate data migration that does:
- `CyberPanelCosmetic.objects.get_or_create(pk=1, defaults={'MainDashboardCSS': '<your default CSS>'})`
so every new/upgraded install gets that default look.
2. **Static default**
Put the CSS in a static file under `baseTemplate/static/` and include it in the base template so the default theme matches your live server.
- **Migrations**
All schema (and optional data) migrations must be in the repo so every 2.5.5-dev install/upgrade runs the same schema and, if you add it, the same default cosmetic data.
---
## What should **not** be in the repo
- **Secrets**: DB password, `SECRET_KEY`, API keys.
Keep in `settings.py` only placeholders or env reads; real values stay on the server (or in config.php / env per your rules).
- **User data**: sites, users, mail, backups.
These are per-server.
- **Generated artifacts**: venv, `collectstatic` output, compiled binaries.
Repo holds the **source**; install/upgrade generates these on the server.
---
## Summary
- **Yes:** “Runtime generated” **defaults** that define how 2.5.5-dev looks and behaves **should** be reflected in the repo (templates, static sources, migrations, default cosmetic logic or data).
- **Already in repo:** App code, default settings structure, version, static sources, migrations. So 2.5.5-dev upgrades already get the same **code** and **default look** (empty custom CSS).
- **Optional:** If your live server has a **specific** custom look (e.g. custom dashboard CSS), and you want that to be the **default** for everyone on 2.5.5-dev, add it to the repo via a data migration or default static CSS as above.
No change is **required** for “same look” unless you want to ship a non-empty default cosmetic (e.g. your current dashboard CSS) as part of 2.5.5-dev.

View File

@@ -0,0 +1,89 @@
# v2.5.5-dev: Fixes and Deploy Guide
This document lists fixes included in **v2.5.5-dev** and how to deploy them on a CyberPanel server.
---
## Version and cache busting
- **baseTemplate:** `CP_VERSION` in `baseTemplate/templates/baseTemplate/index.html` now uses `CYBERPANEL_FULL_VERSION` from context (from `baseTemplate/views.py`: `VERSION = '2.5.5'`, `BUILD = 'dev'`), so static URLs use `?v=2.5.5.dev` and cache busting matches the branch.
---
## 1. Email Limits page
- **Issue:** Raw Angular bindings (`{$ selectedEmail $}`) and `EmailLimitsNew` controller not registered.
- **Files:** `mailServer/mailserverManager.py`, `mailServer/templates/mailServer/EmailLimits.html`, `mailServer/static/mailServer/mailServer.js`, `mailServer/static/mailServer/emailLimitsController.js`, and mirrored under `static/mailServer/`.
- **Deploy:**
```bash
sudo bash /home/cyberpanel-repo/deploy-email-limits-fix.sh
```
- **Details:** See `to-do/EMAIL-LIMITS-DEPLOY-CHECKLIST.md`.
---
## 2. FTP Create Account page
- **Issue:** After selecting a website, the “FTP Account Details” form (username, password, path, quota) did not appear.
- **Files:** `ftp/templates/ftp/createFTPAccount.html` (inline script + polling + Angular scope sync), `ftp/static/ftp/ftp.js`, `static/ftp/ftp.js`, `public/static/ftp/ftp.js`.
- **Deploy:**
```bash
sudo bash /home/cyberpanel-repo/deploy-ftp-create-account-fix.sh
```
- **After deploy:** Hard-refresh `/ftp/createFTPAccount` (Ctrl+Shift+R).
---
## 3. FTP Quota Management page
- **Issue:** `(1146, "Table 'cyberpanel.ftp_quotas' doesn't exist")` on `/ftp/quotaManagement`.
- **Files:** `sql/create_ftp_quotas.sql`, `websiteFunctions/models.py` (FTPQuota model already present).
- **Deploy:**
```bash
sudo bash /home/cyberpanel-repo/deploy-ftp-quotas-table.sh
```
- **Details:** See `to-do/FTP-QUOTAS-TABLE-FIX.md`.
---
## 4. mailUtilities indentation fix
- **File:** `plogical/mailUtilities.py` (indentation fix in DNS query try/except block).
- **Deploy:** Copy to `/usr/local/CyberCP/plogical/mailUtilities.py` and restart lscpd if needed.
---
## Deploy all fixes (in order)
Run on the server (e.g. from repo root):
```bash
sudo bash deploy-email-limits-fix.sh
sudo bash deploy-ftp-create-account-fix.sh
sudo bash deploy-ftp-quotas-table.sh
```
Then hard-refresh the FTP Create Account page in the browser. No need to restart lscpd after the FTP quotas table script (it only runs SQL).
---
## Files changed / added in v2.5.5-dev (fixes)
| Path | Description |
|------|-------------|
| `baseTemplate/templates/baseTemplate/index.html` | CP_VERSION from CYBERPANEL_FULL_VERSION |
| `mailServer/` (Email Limits) | Controller, template, getEmailsForDomain permission |
| `ftp/templates/ftp/createFTPAccount.html` | Inline fallback + polling for details form |
| `ftp/static/ftp/ftp.js`, `static/ftp/ftp.js`, `public/static/ftp/ftp.js` | showFTPDetails, select2/change handlers |
| `websiteFunctions/models.py` | FTPQuota model (table created via sql script) |
| `sql/create_ftp_quotas.sql` | CREATE TABLE ftp_quotas |
| `plogical/mailUtilities.py` | DNS block indentation fix |
| `deploy-email-limits-fix.sh` | Deploy Email Limits fix |
| `deploy-ftp-create-account-fix.sh` | Deploy FTP Create Account template |
| `deploy-ftp-quotas-table.sh` | Create ftp_quotas table |
---
## Optional (not committed by default)
- **CyberCP/urls.py:** If `emailMarketing` is commented out for local runserver, leave it uncommitted or revert before pushing so production keeps the route.

View File

@@ -41,8 +41,8 @@ FTP Quota Management - CyberPanel
<div class="alert alert-info">
<h5><i class="fas fa-info-circle"></i> FTP Quota System</h5>
<p>Enable and manage individual FTP user quotas. This allows you to set storage limits for each FTP user.</p>
<button class="btn btn-primary" onclick="enableFTPQuota()">
<i class="fas fa-play"></i> Enable FTP Quota System
<button type="button" id="btnEnableFTPQuota" class="btn btn-primary" onclick="enableFTPQuota()">
<i class="fas fa-play"></i> <span id="btnEnableFTPQuotaText">Enable FTP Quota System</span>
</button>
</div>
</div>
@@ -125,15 +125,79 @@ FTP Quota Management - CyberPanel
</div>
<script>
function getCsrfToken() {
var name = 'csrftoken';
var cookies = document.cookie.split(';');
for (var i = 0; i < cookies.length; i++) {
var c = cookies[i].trim();
if (c.indexOf(name + '=') === 0) return c.substring(name.length + 1);
}
return '{{ csrf_token }}';
}
function showNotification(type, message) {
if (typeof PNotify !== 'undefined') {
try {
new PNotify({ type: type === 'success' ? 'success' : 'error', text: message });
return;
} catch (e) {}
}
var alertClass = type === 'success' ? 'alert-success' : 'alert-danger';
var icon = type === 'success' ? 'fa-check-circle' : 'fa-exclamation-circle';
var notification = '<div class="alert ' + alertClass + ' alert-dismissible fade show" role="alert">' +
'<i class="fas ' + icon + '"></i> ' + message +
'<button type="button" class="close" data-dismiss="alert"><span>&times;</span></button></div>';
var target = document.querySelector('.ftp-quota-info .alert-info');
if (target && target.parentNode) {
var wrap = document.createElement('div');
wrap.innerHTML = notification;
target.parentNode.insertBefore(wrap.firstChild, target);
setTimeout(function() {
var al = target.parentNode.querySelector('.alert');
if (al && al.remove) al.remove();
}, 6000);
}
}
function enableFTPQuota() {
$.post('{% url "enableFTPQuota" %}', {
'csrfmiddlewaretoken': '{{ csrf_token }}'
}, function(data) {
if (data.status === 1) {
showNotification('success', (data && (data.message || data.error_message)) || 'Success');
refreshQuotas();
} else {
showNotification('error', (data && (data.error_message || data.message)) || 'Unknown error');
var btn = document.getElementById('btnEnableFTPQuota');
var btnText = document.getElementById('btnEnableFTPQuotaText');
if (!btn || !btnText) return;
var originalHtml = btnText.innerHTML;
btn.disabled = true;
btnText.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Enabling...';
$.ajax({
url: '{% url "enableFTPQuota" %}',
type: 'POST',
data: { 'csrfmiddlewaretoken': getCsrfToken() },
headers: { 'X-CSRFToken': getCsrfToken() },
dataType: 'json',
success: function(data) {
if (data && data.status === 1) {
showNotification('success', data.message || 'FTP quota system enabled successfully');
refreshQuotas();
} else {
showNotification('error', (data && (data.message || data.error_message)) || 'Enable failed');
}
},
error: function(xhr) {
var msg = 'Request failed';
if (xhr.responseJSON && (xhr.responseJSON.message || xhr.responseJSON.error_message)) {
msg = xhr.responseJSON.message || xhr.responseJSON.error_message;
} else if (xhr.responseText && xhr.responseText.length < 500) {
try {
var j = JSON.parse(xhr.responseText);
msg = j.message || j.error_message || msg;
} catch (e) {}
}
if (xhr.status === 403 || xhr.status === 302) {
msg = 'Session may have expired. Please refresh the page and try again.';
}
showNotification('error', msg);
},
complete: function() {
btn.disabled = false;
btnText.innerHTML = originalHtml;
}
});
}
@@ -230,26 +294,6 @@ function saveQuota() {
});
}
function showNotification(type, message) {
const alertClass = type === 'success' ? 'alert-success' : 'alert-danger';
const icon = type === 'success' ? 'fa-check-circle' : 'fa-exclamation-circle';
const notification = `
<div class="alert ${alertClass} alert-dismissible fade show" role="alert">
<i class="fas ${icon}"></i> ${message}
<button type="button" class="close" data-dismiss="alert">
<span>&times;</span>
</button>
</div>
`;
$('.card-body').prepend(notification);
setTimeout(() => {
$('.alert').fadeOut();
}, 5000);
}
// Load quotas on page load
$(document).ready(function() {
refreshQuotas();

View File

@@ -8801,7 +8801,8 @@ StrictHostKeyChecking no
def enableFTPQuota(self, userID=None, data=None):
"""
Enable FTP quota system
Enable FTP quota: ensure Quota yes in existing config (do not overwrite), restart Pure-FTPd.
Uses correct service name (pure-ftpd-mysql on Debian/Ubuntu, pure-ftpd on RHEL/Alma).
"""
try:
currentACL = ACLManager.loadedACL(userID)
@@ -8811,60 +8812,79 @@ StrictHostKeyChecking no
if not (currentACL.get('admin', 0) == 1):
return ACLManager.loadErrorJson('status', 0)
# Backup existing configurations
logging.CyberCPLogFileWriter.writeToFile("Backing up existing Pure-FTPd configurations...")
# Resolve Pure-FTPd service name (Debian/Ubuntu use pure-ftpd-mysql)
if os.path.exists('/etc/lsb-release'):
ftp_service = 'pure-ftpd-mysql'
else:
ftp_service = 'pure-ftpd'
import shutil
from datetime import datetime
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
# Backup pure-ftpd.conf
if os.path.exists('/etc/pure-ftpd/pure-ftpd.conf'):
shutil.copy('/etc/pure-ftpd/pure-ftpd.conf', f'/etc/pure-ftpd/pure-ftpd.conf.backup.{timestamp}')
# Backup pureftpd-mysql.conf
if os.path.exists('/etc/pure-ftpd/pureftpd-mysql.conf'):
shutil.copy('/etc/pure-ftpd/pureftpd-mysql.conf', f'/etc/pure-ftpd/pureftpd-mysql.conf.backup.{timestamp}')
# Apply new configurations
logging.CyberCPLogFileWriter.writeToFile("Applying FTP quota configurations...")
# Copy updated configurations
if os.path.exists('/usr/local/CyberCP/install/pure-ftpd/pure-ftpd.conf'):
shutil.copy('/usr/local/CyberCP/install/pure-ftpd/pure-ftpd.conf', '/etc/pure-ftpd/pure-ftpd.conf')
if os.path.exists('/usr/local/CyberCP/install/pure-ftpd/pureftpd-mysql.conf'):
shutil.copy('/usr/local/CyberCP/install/pure-ftpd/pureftpd-mysql.conf', '/etc/pure-ftpd/pureftpd-mysql.conf')
conf_path = '/etc/pure-ftpd/pure-ftpd.conf'
# Only ensure Quota is enabled; do not overwrite existing config (preserves DB credentials, paths)
if os.path.exists(conf_path):
# If service is not running, try restoring latest backup (in case a previous run overwrote working config)
try:
out = ProcessUtilities.outputExecutioner(
"systemctl is-active %s 2>/dev/null || true" % ftp_service, 'root', True)
if not (out and out.strip() == 'active'):
# Restore latest backups if present
ProcessUtilities.executioner(
"ls -t /etc/pure-ftpd/pure-ftpd.conf.backup.* 2>/dev/null | head -1 | xargs -r -I {} cp {} /etc/pure-ftpd/pure-ftpd.conf",
'root', True)
ProcessUtilities.executioner(
"ls -t /etc/pure-ftpd/pureftpd-mysql.conf.backup.* 2>/dev/null | head -1 | xargs -r -I {} cp {} /etc/pure-ftpd/pureftpd-mysql.conf",
'root', True)
except Exception:
pass
# Add or replace Quota line via root (Pure-FTPd expects maxfiles:maxsizeMB, not "yes")
ProcessUtilities.executioner(
"grep -q '^Quota' %s && sed -i 's/^Quota.*/Quota 100000:100000/' %s || echo 'Quota 100000:100000' >> %s" % (conf_path, conf_path, conf_path),
'root', True)
logging.CyberCPLogFileWriter.writeToFile("Set Quota yes in existing pure-ftpd.conf")
else:
# First-time: copy from repo
from datetime import datetime
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
if os.path.exists('/usr/local/CyberCP/install/pure-ftpd/pure-ftpd.conf'):
ProcessUtilities.executioner(
'cp /usr/local/CyberCP/install/pure-ftpd/pure-ftpd.conf /etc/pure-ftpd/pure-ftpd.conf', 'root', True)
if os.path.exists('/usr/local/CyberCP/install/pure-ftpd/pureftpd-mysql.conf'):
ProcessUtilities.executioner(
'cp /usr/local/CyberCP/install/pure-ftpd/pureftpd-mysql.conf /etc/pure-ftpd/pureftpd-mysql.conf', 'root', True)
# Restart Pure-FTPd
logging.CyberCPLogFileWriter.writeToFile("Restarting Pure-FTPd service...")
ProcessUtilities.executioner('systemctl restart pure-ftpd')
logging.CyberCPLogFileWriter.writeToFile("Restarting Pure-FTPd service (%s)..." % ftp_service)
ProcessUtilities.executioner('systemctl restart %s' % ftp_service, 'root', True)
time.sleep(1)
# Verify configuration
if ProcessUtilities.executioner('systemctl is-active --quiet pure-ftpd'):
try:
output = ProcessUtilities.outputExecutioner('systemctl is-active %s' % ftp_service, 'root', True)
is_active = (output and output.strip() == 'active')
except Exception:
is_active = False
if is_active:
logging.CyberCPLogFileWriter.writeToFile("FTP quota system enabled successfully")
data_ret = {
'status': 1,
'message': 'FTP quota system enabled successfully'
}
data_ret = {'status': 1, 'message': 'FTP quota system enabled successfully'}
else:
data_ret = {
'status': 0,
'message': 'Failed to restart Pure-FTPd service'
}
# Capture failure reason for the user
try:
status_out = ProcessUtilities.outputExecutioner(
'systemctl status %s --no-pager -l 2>&1 | head -20' % ftp_service, 'root', True)
status_preview = (status_out or '').strip().replace('\n', ' ')[:300]
except Exception:
status_preview = ''
logging.CyberCPLogFileWriter.writeToFile("Pure-FTPd service not active after restart")
msg = 'Pure-FTPd did not start. Run: systemctl status %s' % ftp_service
if status_preview:
msg += '. ' + status_preview
data_ret = {'status': 0, 'message': msg}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
return HttpResponse(json.dumps(data_ret), content_type='application/json')
except Exception as e:
data_ret = {
'status': 0,
'message': f'Error enabling FTP quota: {str(e)}'
}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
logging.CyberCPLogFileWriter.writeToFile("Error enabling FTP quota: %s" % str(e))
data_ret = {'status': 0, 'message': 'Error enabling FTP quota: %s' % str(e)}
return HttpResponse(json.dumps(data_ret), content_type='application/json')
def getFTPQuotas(self, userID=None, data=None):
"""