diff --git a/baseTemplate/static/baseTemplate/custom-js/system-status.js b/baseTemplate/static/baseTemplate/custom-js/system-status.js index fab9a6274..76353b1e4 100644 --- a/baseTemplate/static/baseTemplate/custom-js/system-status.js +++ b/baseTemplate/static/baseTemplate/custom-js/system-status.js @@ -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); diff --git a/baseTemplate/templates/baseTemplate/homePage.html b/baseTemplate/templates/baseTemplate/homePage.html index 2373f10ae..3c078bf0f 100644 --- a/baseTemplate/templates/baseTemplate/homePage.html +++ b/baseTemplate/templates/baseTemplate/homePage.html @@ -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() { diff --git a/baseTemplate/templates/baseTemplate/index.html b/baseTemplate/templates/baseTemplate/index.html index ab433a6e5..0802be080 100644 --- a/baseTemplate/templates/baseTemplate/index.html +++ b/baseTemplate/templates/baseTemplate/index.html @@ -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" %} diff --git a/baseTemplate/views.py b/baseTemplate/views.py index 1c1c637c2..77ccc5509 100644 --- a/baseTemplate/views.py +++ b/baseTemplate/views.py @@ -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: diff --git a/deploy-email-limits-fix.sh b/deploy-email-limits-fix.sh new file mode 100755 index 000000000..51b0f9b1e --- /dev/null +++ b/deploy-email-limits-fix.sh @@ -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)." diff --git a/deploy-ftp-create-account-fix.sh b/deploy-ftp-create-account-fix.sh new file mode 100644 index 000000000..09ddab347 --- /dev/null +++ b/deploy-ftp-create-account-fix.sh @@ -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)." diff --git a/deploy-ftp-quotas-table.sh b/deploy-ftp-quotas-table.sh new file mode 100644 index 000000000..390acfe6d --- /dev/null +++ b/deploy-ftp-quotas-table.sh @@ -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" diff --git a/firewall/firewallManager.py b/firewall/firewallManager.py index 097e5c0f3..0a50250e4 100644 --- a/firewall/firewallManager.py +++ b/firewall/firewallManager.py @@ -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) diff --git a/ftp/static/ftp/ftp.js b/ftp/static/ftp/ftp.js index 6a7cb75da..ef6cd4a4e 100644 --- a/ftp/static/ftp/ftp.js +++ b/ftp/static/ftp/ftp.js @@ -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(); } }; diff --git a/ftp/templates/ftp/createFTPAccount.html b/ftp/templates/ftp/createFTPAccount.html index 35e796608..6fc8e8a1c 100644 --- a/ftp/templates/ftp/createFTPAccount.html +++ b/ftp/templates/ftp/createFTPAccount.html @@ -452,18 +452,65 @@
- {% for items in websiteList %} {% endfor %} +
-
+

{% trans "FTP Account Details" %}

diff --git a/install/env_generator.py b/install/env_generator.py new file mode 100644 index 000000000..0edb1d7f4 --- /dev/null +++ b/install/env_generator.py @@ -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 [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) diff --git a/install/installCyberPanel.py b/install/installCyberPanel.py new file mode 100644 index 000000000..1ff6cb0d6 --- /dev/null +++ b/install/installCyberPanel.py @@ -0,0 +1,1449 @@ +import shutil +import subprocess +import os +from mysqlUtilities import mysqlUtilities +import installLog as logging +import errno +import MySQLdb as mariadb +import install +from os.path import exists +import time +import install_utils +import urllib.request +import re + +# distros - using from install_utils +centos = install_utils.centos +ubuntu = install_utils.ubuntu +cent8 = install_utils.cent8 +openeuler = install_utils.openeuler + + +def get_Ubuntu_release(): + return install_utils.get_Ubuntu_release(use_print=True, exit_on_error=True) + + +def get_Ubuntu_code_name(): + """Get Ubuntu codename based on version""" + release = get_Ubuntu_release() + if release >= 24.04: + return "noble" + elif release >= 22.04: + return "jammy" + elif release >= 20.04: + return "focal" + elif release >= 18.04: + return "bionic" + else: + return "xenial" + + +# Using shared function from install_utils +FetchCloudLinuxAlmaVersionVersion = install_utils.FetchCloudLinuxAlmaVersionVersion + +class InstallCyberPanel: + mysql_Root_password = "" + mysqlPassword = "" + + def is_almalinux9(self): + """Check if running on AlmaLinux 9""" + if os.path.exists('/etc/almalinux-release'): + try: + with open('/etc/almalinux-release', 'r') as f: + content = f.read() + return 'release 9' in content + except: + return False + return False + + def fix_almalinux9_mariadb(self): + """Fix AlmaLinux 9 MariaDB installation issues""" + if not self.is_almalinux9(): + return + + self.stdOut("Applying AlmaLinux 9 MariaDB fixes...", 1) + + try: + # Disable problematic MariaDB MaxScale repository + self.stdOut("Disabling problematic MariaDB MaxScale repository...", 1) + command = "dnf config-manager --disable mariadb-maxscale 2>/dev/null || true" + install_utils.call(command, self.distro, command, command, 1, 0, os.EX_OSERR) + + # Remove problematic repository files + self.stdOut("Removing problematic repository files...", 1) + problematic_repos = [ + '/etc/yum.repos.d/mariadb-maxscale.repo', + '/etc/yum.repos.d/mariadb-maxscale.repo.rpmnew' + ] + for repo_file in problematic_repos: + if os.path.exists(repo_file): + os.remove(repo_file) + self.stdOut(f"Removed {repo_file}", 1) + + # Clean DNF cache + self.stdOut("Cleaning DNF cache...", 1) + command = "dnf clean all" + install_utils.call(command, self.distro, command, command, 1, 0, os.EX_OSERR) + + # Install MariaDB from official repository + self.stdOut("Setting up official MariaDB repository...", 1) + command = "curl -sS https://downloads.mariadb.com/MariaDB/mariadb_repo_setup | bash -s -- --mariadb-server-version='10.11'" + install_utils.call(command, self.distro, command, command, 1, 0, os.EX_OSERR) + + # Install MariaDB packages + self.stdOut("Installing MariaDB packages...", 1) + mariadb_packages = "MariaDB-server MariaDB-client MariaDB-backup MariaDB-devel" + command = f"dnf install -y {mariadb_packages}" + install_utils.call(command, self.distro, command, command, 1, 0, os.EX_OSERR) + + self.stdOut("AlmaLinux 9 MariaDB fixes completed", 1) + + except Exception as e: + self.stdOut(f"Error applying AlmaLinux 9 MariaDB fixes: {str(e)}", 0) + CloudLinux8 = 0 + + def install_package(self, package_name, options=""): + """Unified package installation across distributions""" + command, shell = install_utils.get_package_install_command(self.distro, package_name, options) + + # InstallCyberPanel always uses verbose mode (no silent option) + if self.distro == ubuntu: + return install_utils.call(command, self.distro, command, command, 1, 1, os.EX_OSERR, shell) + else: + # For non-Ubuntu, original code didn't pass shell parameter + return install_utils.call(command, self.distro, command, command, 1, 1, os.EX_OSERR) + + def manage_service(self, service_name, action="start"): + """Unified service management""" + service_map = { + 'mariadb': 'mariadb', + 'pureftpd': 'pure-ftpd-mysql' if self.distro == ubuntu else 'pure-ftpd', + 'pdns': 'pdns' + } + + actual_service = service_map.get(service_name, service_name) + + # For AlmaLinux 9, try both mariadb and mysqld services + if service_name == 'mariadb' and (self.distro == cent8 or self.distro == openeuler): + # Try mariadb first, then mysqld if mariadb fails + command = f'systemctl {action} {actual_service}' + result = install_utils.call(command, self.distro, command, command, 1, 0, os.EX_OSERR) + if result != 0: + # If mariadb service fails, try mysqld + command = f'systemctl {action} mysqld' + return install_utils.call(command, self.distro, command, command, 1, 1, os.EX_OSERR) + return result + else: + command = f'systemctl {action} {actual_service}' + return install_utils.call(command, self.distro, command, command, 1, 1, os.EX_OSERR) + + def modify_file_content(self, file_path, replacements): + """Generic file content modification""" + try: + with open(file_path, 'r') as f: + data = f.readlines() + + with open(file_path, 'w') as f: + for line in data: + modified_line = line + for old, new in replacements.items(): + if old in line: + modified_line = line.replace(old, new) + break + f.write(modified_line) + return True + except IOError as e: + logging.InstallLog.writeToFile(f'[ERROR] {str(e)} [modify_file_content]') + return False + + def copy_config_file(self, source_dir, dest_path, mysql_mode='One'): + """Handle configuration file copying with mode selection""" + # For directories like 'dns' vs 'dns-one', 'pure-ftpd' vs 'pure-ftpd-one' + # Default mode is 'One' which uses the -one directories + if mysql_mode == 'Two': + source_path = source_dir + else: + # Default mode 'One' uses directories with -one suffix + source_path = f"{source_dir}-one" + + # Ensure we're working with absolute paths + if not os.path.isabs(source_path): + source_path = os.path.join(self.cwd, source_path) + + # Determine the actual file to copy + if os.path.isdir(source_path): + # If dest_path is a file (like pdns.conf), copy the specific file + if dest_path.endswith('.conf'): + # Look for the specific config file + source_file = os.path.join(source_path, os.path.basename(dest_path)) + if os.path.exists(source_file): + if os.path.exists(dest_path): + os.remove(dest_path) + shutil.copy(source_file, dest_path) + else: + raise IOError(f"Source file {source_file} not found") + else: + # If it's a directory, copy the whole directory + if os.path.exists(dest_path): + if os.path.isdir(dest_path): + shutil.rmtree(dest_path) + shutil.copytree(source_path, dest_path) + else: + raise IOError(f"Source path {source_path} not found") + + @staticmethod + def ISARM(): + + try: + command = 'uname -a' + try: + result = subprocess.run(command, capture_output=True, universal_newlines=True, shell=True) + except: + result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, shell=True) + + if 'aarch64' in result.stdout: + return True + else: + return False + except: + return False + + @staticmethod + def OSFlags(): + if os.path.exists("/etc/redhat-release"): + data = open('/etc/redhat-release', 'r').read() + + if data.find('CloudLinux 8') > -1 or data.find('cloudlinux 8') > -1: + InstallCyberPanel.CloudLinux8 = 1 + + def __init__(self, rootPath, cwd, distro, ent, serial=None, port=None, ftp=None, dns=None, publicip=None, + remotemysql=None, mysqlhost=None, mysqldb=None, mysqluser=None, mysqlpassword=None, mysqlport=None): + self.server_root_path = rootPath + self.cwd = cwd + self.distro = distro + self.ent = ent + self.serial = serial + self.port = port + self.ftp = None + self.dns = dns + self.publicip = publicip + self.remotemysql = remotemysql + self.mysqlhost = mysqlhost + self.mysqluser = mysqluser + self.mysqlpassword = mysqlpassword + self.mysqlport = mysqlport + self.mysqldb = mysqldb + + ## TURN ON OS FLAGS FOR SPECIFIC NEEDS LATER + + InstallCyberPanel.OSFlags() + + @staticmethod + def stdOut(message, log=0, exit=0, code=os.EX_OK): + install_utils.stdOut(message, log, exit, code) + + @staticmethod + def getLatestLSWSVersion(): + """Fetch the latest LSWS Enterprise version from LiteSpeed's website""" + try: + # Try to fetch from the download page + url = "https://www.litespeedtech.com/products/litespeed-web-server/download" + req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'}) + with urllib.request.urlopen(req, timeout=10) as response: + html = response.read().decode('utf-8') + + # Look for the latest version pattern: lsws-X.Y.Z-ent + version_pattern = r'lsws-(\d+\.\d+\.\d+)-ent' + versions = re.findall(version_pattern, html) + + if versions: + # Get the latest version + latest_version = sorted(versions, key=lambda v: [int(x) for x in v.split('.')])[-1] + InstallCyberPanel.stdOut(f"Found latest LSWS Enterprise version: {latest_version}", 1) + return latest_version + else: + InstallCyberPanel.stdOut("Could not find version pattern in HTML, using fallback", 1) + + except Exception as e: + InstallCyberPanel.stdOut(f"Failed to fetch latest LSWS version: {str(e)}, using fallback", 1) + + # Fallback to known latest version + return "6.3.4" + + def detectArchitecture(self): + """Detect system architecture - custom binaries only for x86_64""" + try: + import platform + arch = platform.machine() + return arch == "x86_64" + except Exception as msg: + logging.InstallLog.writeToFile(str(msg) + " [detectArchitecture]") + return False + + def detectBinarySuffix(self): + """Detect which binary suffix to use based on OS distribution + Returns 'ubuntu' for Ubuntu/Debian systems + Returns 'rhel8' for RHEL/AlmaLinux/Rocky 8.x systems + Returns 'rhel9' for RHEL/AlmaLinux/Rocky 9.x systems + """ + try: + # Check /etc/os-release first for more accurate detection + if os.path.exists('/etc/os-release'): + with open('/etc/os-release', 'r') as f: + os_release = f.read().lower() + + # Check for Ubuntu/Debian FIRST + if 'ubuntu' in os_release or 'debian' in os_release: + return 'ubuntu' + + # Check for RHEL-based distributions and extract version + if any(x in os_release for x in ['almalinux', 'rocky', 'rhel', 'centos stream']): + # Extract version number + for line in os_release.split('\n'): + if 'version_id' in line: + version = line.split('=')[1].strip('"').split('.')[0] + if version == '9': + return 'rhel9' + elif version == '8': + return 'rhel8' + # Default to rhel9 if version extraction fails + return 'rhel9' + + # Fallback: Use distro variable + # Ubuntu/Debian → ubuntu suffix + if self.distro == ubuntu: + return 'ubuntu' + + # CentOS 8+/AlmaLinux/Rocky/OpenEuler → rhel9 by default + elif self.distro == cent8 or self.distro == openeuler: + return 'rhel9' + + # CentOS 7 → ubuntu suffix (uses libcrypt.so.1) + elif self.distro == centos: + return 'ubuntu' + + # Default to ubuntu for unknown distros + else: + InstallCyberPanel.stdOut("Unknown OS distribution, defaulting to Ubuntu binaries", 1) + return 'ubuntu' + + except Exception as msg: + logging.InstallLog.writeToFile(str(msg) + " [detectBinarySuffix]") + InstallCyberPanel.stdOut("Error detecting OS, defaulting to Ubuntu binaries", 1) + return 'ubuntu' + + def downloadCustomBinary(self, url, destination): + """Download custom binary file""" + try: + InstallCyberPanel.stdOut(f"Downloading {os.path.basename(destination)}...", 1) + + # Use wget for better progress display + command = f'wget -q --show-progress {url} -O {destination}' + install_utils.call(command, self.distro, command, command, 1, 0, os.EX_OSERR) + + # Check if file was downloaded successfully by verifying it exists and has reasonable size + if os.path.exists(destination): + file_size = os.path.getsize(destination) + # Verify file size is reasonable (at least 10KB to avoid error pages/empty files) + if file_size > 10240: # 10KB + if file_size > 1048576: # 1MB + InstallCyberPanel.stdOut(f"Downloaded successfully ({file_size / (1024*1024):.2f} MB)", 1) + else: + InstallCyberPanel.stdOut(f"Downloaded successfully ({file_size / 1024:.2f} KB)", 1) + return True + else: + InstallCyberPanel.stdOut(f"ERROR: Downloaded file too small ({file_size} bytes)", 1) + return False + else: + InstallCyberPanel.stdOut("ERROR: Download failed - file not found", 1) + return False + + except Exception as msg: + logging.InstallLog.writeToFile(str(msg) + " [downloadCustomBinary]") + InstallCyberPanel.stdOut(f"ERROR: {msg}", 1) + return False + + def verifyCustomBinary(self, binary_path): + """Verify custom binary has correct dependencies and can run""" + try: + InstallCyberPanel.stdOut("Verifying custom binary compatibility...", 1) + + # Check library dependencies + command = f'ldd {binary_path}' + result = subprocess.run(command, shell=True, capture_output=True, text=True) + + if result.returncode != 0: + InstallCyberPanel.stdOut("ERROR: Failed to check binary dependencies", 1) + return False + + # Check for missing libraries + if 'not found' in result.stdout: + InstallCyberPanel.stdOut("ERROR: Binary has missing library dependencies:", 1) + for line in result.stdout.split('\n'): + if 'not found' in line: + InstallCyberPanel.stdOut(f" {line.strip()}", 1) + return False + + # Try to run the binary with -v to check if it can execute + command = f'{binary_path} -v' + result = subprocess.run(command, shell=True, capture_output=True, text=True, timeout=5) + + if result.returncode != 0: + InstallCyberPanel.stdOut("ERROR: Binary failed to execute", 1) + if result.stderr: + InstallCyberPanel.stdOut(f" Error: {result.stderr.strip()}", 1) + return False + + InstallCyberPanel.stdOut("Binary verification successful", 1) + return True + + except subprocess.TimeoutExpired: + InstallCyberPanel.stdOut("ERROR: Binary verification timed out", 1) + return False + except Exception as msg: + logging.InstallLog.writeToFile(str(msg) + " [verifyCustomBinary]") + InstallCyberPanel.stdOut(f"ERROR: Verification failed: {msg}", 1) + return False + + def rollbackCustomBinary(self, backup_dir, binary_path, module_path): + """Rollback to original binary if custom binary fails""" + try: + InstallCyberPanel.stdOut("Rolling back to original binary...", 1) + + backup_binary = f"{backup_dir}/openlitespeed.backup" + + # Restore original binary if backup exists + if os.path.exists(backup_binary): + shutil.copy2(backup_binary, binary_path) + os.chmod(binary_path, 0o755) + InstallCyberPanel.stdOut("Original binary restored successfully", 1) + else: + InstallCyberPanel.stdOut("WARNING: No backup found, cannot restore", 1) + + # Remove failed custom module + if os.path.exists(module_path): + os.remove(module_path) + InstallCyberPanel.stdOut("Custom module removed", 1) + + InstallCyberPanel.stdOut("Rollback completed", 1) + return True + + except Exception as msg: + logging.InstallLog.writeToFile(str(msg) + " [rollbackCustomBinary]") + InstallCyberPanel.stdOut(f"ERROR: Rollback failed: {msg}", 1) + return False + + def installCustomOLSBinaries(self): + """Install custom OpenLiteSpeed binaries with PHP config support""" + try: + InstallCyberPanel.stdOut("Installing Custom OpenLiteSpeed Binaries", 1) + InstallCyberPanel.stdOut("=" * 50, 1) + + # Check architecture + if not self.detectArchitecture(): + InstallCyberPanel.stdOut("WARNING: Custom binaries only available for x86_64", 1) + InstallCyberPanel.stdOut("Skipping custom binary installation", 1) + InstallCyberPanel.stdOut("Standard OLS will be used", 1) + return True # Not a failure, just skip + + # Detect OS and select appropriate binary suffix + binary_suffix = self.detectBinarySuffix() + InstallCyberPanel.stdOut(f"Detected OS type: using '{binary_suffix}' binaries", 1) + + # URLs for custom binaries with OS-specific paths + BASE_URL = "https://cyberpanel.net/binaries" + + # Set URLs based on OS type + if binary_suffix == 'rhel8': + OLS_BINARY_URL = f"{BASE_URL}/rhel8/openlitespeed-phpconfig-x86_64-rhel8" + MODULE_URL = f"{BASE_URL}/rhel8/cyberpanel_ols_x86_64_rhel8.so" + elif binary_suffix == 'rhel9': + OLS_BINARY_URL = f"{BASE_URL}/rhel9/openlitespeed-phpconfig-x86_64-rhel" + MODULE_URL = f"{BASE_URL}/rhel9/cyberpanel_ols_x86_64_rhel.so" + else: # ubuntu + OLS_BINARY_URL = f"{BASE_URL}/ubuntu/openlitespeed-phpconfig-x86_64-ubuntu" + MODULE_URL = f"{BASE_URL}/ubuntu/cyberpanel_ols_x86_64_ubuntu.so" + + OLS_BINARY_PATH = "/usr/local/lsws/bin/openlitespeed" + MODULE_PATH = "/usr/local/lsws/modules/cyberpanel_ols.so" + + # Create backup + from datetime import datetime + timestamp = datetime.now().strftime("%Y%m%d-%H%M%S") + backup_dir = f"/usr/local/lsws/backup-{timestamp}" + + try: + os.makedirs(backup_dir, exist_ok=True) + if os.path.exists(OLS_BINARY_PATH): + shutil.copy2(OLS_BINARY_PATH, f"{backup_dir}/openlitespeed.backup") + InstallCyberPanel.stdOut(f"Backup created at: {backup_dir}", 1) + except Exception as e: + InstallCyberPanel.stdOut(f"WARNING: Could not create backup: {e}", 1) + + # Download binaries to temp location + tmp_binary = "/tmp/openlitespeed-custom" + tmp_module = "/tmp/cyberpanel_ols.so" + + InstallCyberPanel.stdOut("Downloading custom binaries...", 1) + + # Download OpenLiteSpeed binary + if not self.downloadCustomBinary(OLS_BINARY_URL, tmp_binary): + InstallCyberPanel.stdOut("ERROR: Failed to download OLS binary", 1) + InstallCyberPanel.stdOut("Continuing with standard OLS", 1) + return True # Not fatal, continue with standard OLS + + # Download module + if not self.downloadCustomBinary(MODULE_URL, tmp_module): + InstallCyberPanel.stdOut("ERROR: Failed to download module", 1) + InstallCyberPanel.stdOut("Continuing with standard OLS", 1) + return True # Not fatal, continue with standard OLS + + # Install OpenLiteSpeed binary + InstallCyberPanel.stdOut("Installing custom binaries...", 1) + + try: + shutil.move(tmp_binary, OLS_BINARY_PATH) + os.chmod(OLS_BINARY_PATH, 0o755) + InstallCyberPanel.stdOut("Installed OpenLiteSpeed binary", 1) + except Exception as e: + InstallCyberPanel.stdOut(f"ERROR: Failed to install binary: {e}", 1) + logging.InstallLog.writeToFile(str(e) + " [installCustomOLSBinaries - binary install]") + return False + + # Install module + try: + os.makedirs(os.path.dirname(MODULE_PATH), exist_ok=True) + shutil.move(tmp_module, MODULE_PATH) + os.chmod(MODULE_PATH, 0o644) + InstallCyberPanel.stdOut("Installed CyberPanel module", 1) + except Exception as e: + InstallCyberPanel.stdOut(f"ERROR: Failed to install module: {e}", 1) + logging.InstallLog.writeToFile(str(e) + " [installCustomOLSBinaries - module install]") + return False + + # Verify installation files exist + if not (os.path.exists(OLS_BINARY_PATH) and os.path.exists(MODULE_PATH)): + InstallCyberPanel.stdOut("ERROR: Installation verification failed - files not found", 1) + return False + + # Verify binary compatibility + if not self.verifyCustomBinary(OLS_BINARY_PATH): + InstallCyberPanel.stdOut("ERROR: Custom binary verification failed", 1) + InstallCyberPanel.stdOut("This usually means wrong binary type for your OS", 1) + + # Rollback to original binary + if os.path.exists(backup_dir): + self.rollbackCustomBinary(backup_dir, OLS_BINARY_PATH, MODULE_PATH) + InstallCyberPanel.stdOut("Continuing with standard OLS", 1) + else: + InstallCyberPanel.stdOut("WARNING: Cannot rollback, no backup found", 1) + + return True # Non-fatal, continue with standard OLS + + # Success! + InstallCyberPanel.stdOut("=" * 50, 1) + InstallCyberPanel.stdOut("Custom Binaries Installed Successfully", 1) + InstallCyberPanel.stdOut("Features enabled:", 1) + InstallCyberPanel.stdOut(" - Apache-style .htaccess support", 1) + InstallCyberPanel.stdOut(" - php_value/php_flag directives", 1) + InstallCyberPanel.stdOut(" - Enhanced header control", 1) + InstallCyberPanel.stdOut(f"Backup: {backup_dir}", 1) + InstallCyberPanel.stdOut("=" * 50, 1) + return True + + except Exception as msg: + logging.InstallLog.writeToFile(str(msg) + " [installCustomOLSBinaries]") + InstallCyberPanel.stdOut(f"ERROR: {msg}", 1) + InstallCyberPanel.stdOut("Continuing with standard OLS", 1) + return True # Non-fatal error, continue + + def configureCustomModule(self): + """Configure CyberPanel module in OpenLiteSpeed config""" + try: + InstallCyberPanel.stdOut("Configuring CyberPanel module...", 1) + + CONFIG_FILE = "/usr/local/lsws/conf/httpd_config.conf" + + if not os.path.exists(CONFIG_FILE): + InstallCyberPanel.stdOut("WARNING: Config file not found", 1) + InstallCyberPanel.stdOut("Module will be auto-loaded", 1) + return True + + # Check if module is already configured + with open(CONFIG_FILE, 'r') as f: + content = f.read() + if 'cyberpanel_ols' in content: + InstallCyberPanel.stdOut("Module already configured", 1) + return True + + # Add module configuration + module_config = """ +module cyberpanel_ols { + ls_enabled 1 +} +""" + # Backup config + shutil.copy2(CONFIG_FILE, f"{CONFIG_FILE}.backup") + + # Append module config + with open(CONFIG_FILE, 'a') as f: + f.write(module_config) + + InstallCyberPanel.stdOut("Module configured successfully", 1) + return True + + except Exception as msg: + logging.InstallLog.writeToFile(str(msg) + " [configureCustomModule]") + InstallCyberPanel.stdOut(f"WARNING: Module configuration failed: {msg}", 1) + InstallCyberPanel.stdOut("Module may still work via auto-load", 1) + return True # Non-fatal + + def installLiteSpeed(self): + if self.ent == 0: + # Install standard OpenLiteSpeed package + self.install_package('openlitespeed') + + # Install custom binaries with PHP config support + # This replaces the standard binary with enhanced version + self.installCustomOLSBinaries() + + # Configure the custom module + self.configureCustomModule() + + else: + try: + try: + command = 'groupadd nobody' + install_utils.call(command, self.distro, command, command, 1, 0, os.EX_OSERR) + except: + pass + + try: + command = 'usermod -a -G nobody nobody' + install_utils.call(command, self.distro, command, command, 1, 0, os.EX_OSERR) + except: + pass + + # Get the latest LSWS Enterprise version dynamically + lsws_version = InstallCyberPanel.getLatestLSWSVersion() + + if InstallCyberPanel.ISARM(): + command = f'wget https://www.litespeedtech.com/packages/6.0/lsws-{lsws_version}-ent-aarch64-linux.tar.gz' + else: + command = f'wget https://www.litespeedtech.com/packages/6.0/lsws-{lsws_version}-ent-x86_64-linux.tar.gz' + + install_utils.call(command, self.distro, command, command, 1, 1, os.EX_OSERR) + + if InstallCyberPanel.ISARM(): + command = f'tar zxf lsws-{lsws_version}-ent-aarch64-linux.tar.gz' + else: + command = f'tar zxf lsws-{lsws_version}-ent-x86_64-linux.tar.gz' + + install_utils.call(command, self.distro, command, command, 1, 1, os.EX_OSERR) + + if str.lower(self.serial) == 'trial': + command = f'wget -q --output-document=lsws-{lsws_version}/trial.key http://license.litespeedtech.com/reseller/trial.key' + if self.serial == '1111-2222-3333-4444': + command = f'wget -q --output-document=/root/cyberpanel/install/lsws-{lsws_version}/trial.key http://license.litespeedtech.com/reseller/trial.key' + install_utils.call(command, self.distro, command, command, 1, 1, os.EX_OSERR) + else: + writeSerial = open(f'lsws-{lsws_version}/serial.no', 'w') + writeSerial.writelines(self.serial) + writeSerial.close() + + shutil.copy('litespeed/install.sh', f'lsws-{lsws_version}/') + shutil.copy('litespeed/functions.sh', f'lsws-{lsws_version}/') + + os.chdir(f'lsws-{lsws_version}') + + command = 'chmod +x install.sh' + install_utils.call(command, self.distro, command, command, 1, 1, os.EX_OSERR) + + command = 'chmod +x functions.sh' + install_utils.call(command, self.distro, command, command, 1, 1, os.EX_OSERR) + + command = './install.sh' + install_utils.call(command, self.distro, command, command, 1, 1, os.EX_OSERR) + + os.chdir(self.cwd) + confPath = '/usr/local/lsws/conf/' + shutil.copy('litespeed/httpd_config.xml', confPath) + shutil.copy('litespeed/modsec.conf', confPath) + shutil.copy('litespeed/httpd.conf', confPath) + + command = 'chown -R lsadm:lsadm ' + confPath + install_utils.call(command, self.distro, command, command, 1, 0, os.EX_OSERR) + + except BaseException as msg: + logging.InstallLog.writeToFile('[ERROR] ' + str(msg) + " [installLiteSpeed]") + return 0 + + return 1 + + def reStartLiteSpeed(self): + command = install_utils.format_restart_litespeed_command(self.server_root_path) + install_utils.call(command, self.distro, command, command, 1, 0, os.EX_OSERR) + + def fix_ols_configs(self): + try: + + InstallCyberPanel.stdOut("Fixing OpenLiteSpeed configurations!", 1) + + ## remove example virtual host + + data = open(self.server_root_path + "conf/httpd_config.conf", 'r').readlines() + + writeDataToFile = open(self.server_root_path + "conf/httpd_config.conf", 'w') + + for items in data: + if items.find("map") > -1 and items.find("Example") > -1: + continue + else: + writeDataToFile.writelines(items) + + writeDataToFile.close() + + InstallCyberPanel.stdOut("OpenLiteSpeed Configurations fixed!", 1) + except IOError as msg: + logging.InstallLog.writeToFile('[ERROR] ' + str(msg) + " [fix_ols_configs]") + return 0 + + return self.reStartLiteSpeed() + + def changePortTo80(self): + try: + InstallCyberPanel.stdOut("Changing default port to 80..", 1) + + file_path = self.server_root_path + "conf/httpd_config.conf" + if self.modify_file_content(file_path, {"*:8088": "*:80"}): + InstallCyberPanel.stdOut("Default port is now 80 for OpenLiteSpeed!", 1) + else: + return 0 + + except Exception as msg: + logging.InstallLog.writeToFile('[ERROR] ' + str(msg) + " [changePortTo80]") + return 0 + + return self.reStartLiteSpeed() + + def installAllPHPVersions(self): + php_versions = ['71', '72', '73', '74', '80', '81', '82', '83', '84', '85'] + + if self.distro == ubuntu: + # Install base PHP 7.x packages + command = 'DEBIAN_FRONTEND=noninteractive apt-get -y install ' \ + 'lsphp7? lsphp7?-common lsphp7?-curl lsphp7?-dev lsphp7?-imap lsphp7?-intl lsphp7?-json ' \ + 'lsphp7?-ldap lsphp7?-mysql lsphp7?-opcache lsphp7?-pspell lsphp7?-recode ' \ + 'lsphp7?-sqlite3 lsphp7?-tidy' + os.system(command) + + # Install PHP 8.x versions + for version in php_versions[4:]: # 80, 81, 82, 83 + self.install_package(f'lsphp{version}*') + + elif self.distro == centos: + # First install the group + command = 'yum -y groupinstall lsphp-all' + install_utils.call(command, self.distro, command, command, 1, 1, os.EX_OSERR) + + InstallCyberPanel.stdOut("LiteSpeed PHPs successfully installed!", 1) + + # Install individual PHP versions + for version in php_versions: + self.install_package(f'lsphp{version}*', '--skip-broken') + + elif self.distro == cent8: + # Install PHP versions in batches with exclusions + exclude_flags = "--exclude lsphp73-pecl-zip --exclude *imagick*" + + # First batch: PHP 7.x and 8.0 + versions_batch1 = ' '.join([f'lsphp{v}*' for v in php_versions[:5]]) + self.install_package(versions_batch1, f'{exclude_flags} --skip-broken') + + # Second batch: PHP 8.1+ + versions_batch2 = ' '.join([f'lsphp{v}*' for v in php_versions[5:]]) + self.install_package(versions_batch2, f'{exclude_flags} --skip-broken') + + elif self.distro == openeuler: + # Install all PHP versions at once + all_versions = ' '.join([f'lsphp{v}*' for v in php_versions]) + self.install_package(all_versions) + + if self.distro != ubuntu: + InstallCyberPanel.stdOut("LiteSpeed PHPs successfully installed!", 1) + + def installSieve(self): + """Install Sieve (Dovecot Sieve) for email filtering on all OS variants""" + try: + InstallCyberPanel.stdOut("Installing Sieve (Dovecot Sieve) for email filtering...", 1) + + if self.distro == ubuntu: + # Install dovecot-sieve and dovecot-managesieved + self.install_package('dovecot-sieve dovecot-managesieved') + else: + # For CentOS/AlmaLinux/OpenEuler + self.install_package('dovecot-pigeonhole') + + # Add Sieve port 4190 to firewall + from plogical.firewallUtilities import FirewallUtilities + FirewallUtilities.addSieveFirewallRule() + + InstallCyberPanel.stdOut("Sieve successfully installed and configured!", 1) + return 1 + + except BaseException as msg: + logging.InstallLog.writeToFile('[ERROR] ' + str(msg) + " [installSieve]") + return 0 + + def installMySQL(self, mysql): + + ############## Install mariadb ###################### + + if self.distro == ubuntu: + + command = 'DEBIAN_FRONTEND=noninteractive apt-get install software-properties-common apt-transport-https curl -y' + install_utils.call(command, self.distro, command, command, 1, 1, os.EX_OSERR, True) + + command = "mkdir -p /etc/apt/keyrings" + install_utils.call(command, self.distro, command, command, 1, 1, os.EX_OSERR) + + command = "curl -o /etc/apt/keyrings/mariadb-keyring.pgp 'https://mariadb.org/mariadb_release_signing_key.pgp'" + install_utils.call(command, self.distro, command, command, 1, 1, os.EX_OSERR) + RepoPath = '/etc/apt/sources.list.d/mariadb.sources' + RepoContent = f""" +# MariaDB 10.11 repository list - created 2023-12-11 07:53 UTC +# https://mariadb.org/download/ +X-Repolib-Name: MariaDB +Types: deb +# deb.mariadb.org is a dynamic mirror if your preferred mirror goes offline. See https://mariadb.org/mirrorbits/ for details. +# URIs: https://deb.mariadb.org/10.11/ubuntu +URIs: https://mirrors.gigenet.com/mariadb/repo/10.11/ubuntu +Suites: jammy +Components: main main/debug +Signed-By: /etc/apt/keyrings/mariadb-keyring.pgp +""" + + if get_Ubuntu_release() > 21.00: + command = 'curl -LsS https://downloads.mariadb.com/MariaDB/mariadb_repo_setup | sudo bash -s -- --mariadb-server-version=10.11' + result = install_utils.call(command, self.distro, command, command, 1, 0, os.EX_OSERR, True) + + # If the download fails, use manual repo configuration as fallback + if result != 1: + install_utils.writeToFile("MariaDB repo setup script failed, using manual configuration...") + + # First, ensure directories exist + command = 'mkdir -p /usr/share/keyrings /etc/apt/sources.list.d' + install_utils.call(command, self.distro, command, command, 1, 1, os.EX_OSERR, True) + + # Download and add MariaDB signing key + command = 'curl -fsSL https://mariadb.org/mariadb_release_signing_key.pgp | gpg --dearmor -o /usr/share/keyrings/mariadb-keyring.pgp' + install_utils.call(command, self.distro, command, command, 1, 1, os.EX_OSERR, True) + + # Use multiple mirror options for better reliability + RepoPath = '/etc/apt/sources.list.d/mariadb.list' + codename = get_Ubuntu_code_name() + RepoContent = f"""# MariaDB 10.11 repository list - manual fallback +# Primary mirror +deb [arch=amd64,arm64,ppc64el,s390x signed-by=/usr/share/keyrings/mariadb-keyring.pgp] https://mirror.mariadb.org/repo/10.11/ubuntu {codename} main + +# Alternative mirrors (uncomment if primary fails) +# deb [arch=amd64,arm64,ppc64el,s390x signed-by=/usr/share/keyrings/mariadb-keyring.pgp] https://mirrors.gigenet.com/mariadb/repo/10.11/ubuntu {codename} main +# deb [arch=amd64,arm64,ppc64el,s390x signed-by=/usr/share/keyrings/mariadb-keyring.pgp] https://ftp.osuosl.org/pub/mariadb/repo/10.11/ubuntu {codename} main +""" + + WriteToFile = open(RepoPath, 'w') + WriteToFile.write(RepoContent) + WriteToFile.close() + + install_utils.writeToFile("Manual MariaDB repository configuration completed.") + + + + command = 'DEBIAN_FRONTEND=noninteractive apt-get update -y' + install_utils.call(command, self.distro, command, command, 1, 1, os.EX_OSERR, True) + + + command = "DEBIAN_FRONTEND=noninteractive apt-get install mariadb-server -y" + elif self.distro == centos: + + RepoPath = '/etc/yum.repos.d/mariadb.repo' + RepoContent = f""" +[mariadb] +name = MariaDB +baseurl = http://yum.mariadb.org/10.11/rhel8-amd64 +module_hotfixes=1 +gpgkey=https://yum.mariadb.org/RPM-GPG-KEY-MariaDB +gpgcheck=1 +""" + WriteToFile = open(RepoPath, 'w') + WriteToFile.write(RepoContent) + WriteToFile.close() + + command = 'dnf install mariadb-server -y' + elif self.distro == cent8 or self.distro == openeuler: + + clAPVersion = FetchCloudLinuxAlmaVersionVersion() + type = clAPVersion.split('-')[0] + version = int(clAPVersion.split('-')[1]) + + + if type == 'cl' and version >= 88: + + command = 'yum remove db-governor db-governor-mysql -y' + install_utils.call(command, self.distro, command, command, 1, 1, os.EX_OSERR, True) + + command = 'yum install governor-mysql -y' + install_utils.call(command, self.distro, command, command, 1, 1, os.EX_OSERR, True) + + command = '/usr/share/lve/dbgovernor/mysqlgovernor.py --mysql-version=mariadb106' + install_utils.call(command, self.distro, command, command, 1, 1, os.EX_OSERR, True) + + command = '/usr/share/lve/dbgovernor/mysqlgovernor.py --install --yes' + + else: + + command = 'curl -LsS https://downloads.mariadb.com/MariaDB/mariadb_repo_setup | sudo bash -s -- --mariadb-server-version=10.11' + install_utils.call(command, self.distro, command, command, 1, 1, os.EX_OSERR, True) + + command = 'yum remove mariadb* -y' + install_utils.call(command, self.distro, command, command, 1, 1, os.EX_OSERR, True) + + command = 'sudo dnf -qy module disable mariadb' + install_utils.call(command, self.distro, command, command, 1, 1, os.EX_OSERR, True) + + command = 'sudo dnf module reset mariadb -y' + install_utils.call(command, self.distro, command, command, 1, 1, os.EX_OSERR, True) + + # Disable problematic mariadb-maxscale repository to avoid 404 errors + command = 'dnf config-manager --disable mariadb-maxscale' + install_utils.call(command, self.distro, command, command, 1, 0, os.EX_OSERR, True) + + # Clear dnf cache to avoid repository issues + command = 'dnf clean all' + install_utils.call(command, self.distro, command, command, 1, 1, os.EX_OSERR, True) + + command = 'dnf install MariaDB-server MariaDB-client MariaDB-backup -y' + + install_utils.call(command, self.distro, command, command, 1, 1, os.EX_OSERR, True) + + ############## Start mariadb ###################### + + self.startMariaDB() + + def changeMYSQLRootPassword(self): + if self.remotemysql == 'OFF': + if self.distro == ubuntu: + passwordCMD = "use mysql;DROP DATABASE IF EXISTS test;DELETE FROM mysql.db WHERE Db='test' OR Db='test\\_%%';GRANT ALL PRIVILEGES ON *.* TO 'root'@'localhost' IDENTIFIED BY '%s';UPDATE user SET plugin='' WHERE User='root';flush privileges;" % ( + InstallCyberPanel.mysql_Root_password) + else: + passwordCMD = "use mysql;DROP DATABASE IF EXISTS test;DELETE FROM mysql.db WHERE Db='test' OR Db='test\\_%%';GRANT ALL PRIVILEGES ON *.* TO 'root'@'localhost' IDENTIFIED BY '%s';flush privileges;" % ( + InstallCyberPanel.mysql_Root_password) + + # For AlmaLinux 9, try mysql command first, then mariadb + if self.distro == cent8 or self.distro == openeuler: + command = 'mysql -u root -e "' + passwordCMD + '"' + result = install_utils.call(command, self.distro, command, command, 0, 0, os.EX_OSERR) + if result != 0: + # If mysql command fails, try mariadb + command = 'mariadb -u root -e "' + passwordCMD + '"' + install_utils.call(command, self.distro, command, command, 0, 0, os.EX_OSERR) + else: + command = 'mariadb -u root -e "' + passwordCMD + '"' + install_utils.call(command, self.distro, command, command, 0, 0, os.EX_OSERR) + + def startMariaDB(self): + + if self.remotemysql == 'OFF': + ############## Start mariadb ###################### + + # Check if AlmaLinux 9 and apply fixes + if self.is_almalinux9(): + self.stdOut("AlmaLinux 9 detected - applying MariaDB fixes", 1) + self.fix_almalinux9_mariadb() + + self.manage_service('mariadb', 'start') + + ############## Enable mariadb at system startup ###################### + + if os.path.exists('/etc/systemd/system/mysqld.service'): + os.remove('/etc/systemd/system/mysqld.service') + if os.path.exists('/etc/systemd/system/mariadb.service'): + os.remove('/etc/systemd/system/mariadb.service') + + self.manage_service('mariadb', 'enable') + + def fixMariaDB(self): + self.stdOut("Setup MariaDB so it can support Cyberpanel's needs") + + conn = mariadb.connect(user='root', passwd=self.mysql_Root_password) + cursor = conn.cursor() + cursor.execute('set global innodb_file_per_table = on;') + try: + cursor.execute('set global innodb_file_format = Barracuda;') + cursor.execute('set global innodb_large_prefix = on;') + except BaseException as msg: + self.stdOut('%s. [ERROR:335]' % (str(msg))) + cursor.close() + conn.close() + + try: + fileName = '/etc/mysql/mariadb.conf.d/50-server.cnf' + data = open(fileName, 'r').readlines() + + writeDataToFile = open(fileName, 'w') + for line in data: + writeDataToFile.write(line.replace('utf8mb4', 'utf8')) + writeDataToFile.close() + except IOError as err: + self.stdOut("[ERROR] Error in setting: " + fileName + ": " + str(err), 1, 1, os.EX_OSERR) + + # Use the manage_service method for consistent service management + if self.distro == cent8 or self.distro == openeuler: + # Try mariadb first, then mysqld + result = os.system('systemctl restart mariadb') + if result != 0: + os.system('systemctl restart mysqld') + else: + os.system('systemctl restart mariadb') + + self.stdOut("MariaDB is now setup so it can support Cyberpanel's needs") + + def installPureFTPD(self): + if self.distro == ubuntu: + self.install_package('pure-ftpd-mysql') + + if get_Ubuntu_release() == 18.10: + # Special handling for Ubuntu 18.10 + packages = [ + ('pure-ftpd-common_1.0.47-3_all.deb', 'wget https://rep.cyberpanel.net/pure-ftpd-common_1.0.47-3_all.deb'), + ('pure-ftpd-mysql_1.0.47-3_amd64.deb', 'wget https://rep.cyberpanel.net/pure-ftpd-mysql_1.0.47-3_amd64.deb') + ] + + for filename, wget_cmd in packages: + install_utils.call(wget_cmd, self.distro, wget_cmd, wget_cmd, 1, 1, os.EX_OSERR) + command = f'dpkg --install --force-confold {filename}' + install_utils.call(command, self.distro, command, command, 1, 1, os.EX_OSERR) + else: + self.install_package('pure-ftpd') + + ####### Install pureftpd to system startup + + command = "systemctl enable " + install.preFlightsChecks.pureFTPDServiceName(self.distro) + install_utils.call(command, self.distro, command, command, 1, 1, os.EX_OSERR) + + ###### FTP Groups and user settings settings + + command = 'groupadd -g 2001 ftpgroup' + install_utils.call(command, self.distro, command, command, 1, 1, os.EX_OSERR) + + command = 'useradd -u 2001 -s /bin/false -d /bin/null -c "pureftpd user" -g ftpgroup ftpuser' + install_utils.call(command, self.distro, command, command, 1, 1, os.EX_OSERR) + + def startPureFTPD(self): + ############## Start pureftpd ###################### + serviceName = install.preFlightsChecks.pureFTPDServiceName(self.distro) + + # During fresh installation, don't start Pure-FTPd yet + # It will be started after Django migrations create the required tables + InstallCyberPanel.stdOut("Pure-FTPd enabled for startup.", 1) + InstallCyberPanel.stdOut("Note: Pure-FTPd will start after database setup is complete.", 1) + logging.InstallLog.writeToFile("Pure-FTPd enabled but not started - waiting for Django migrations") + + def installPureFTPDConfigurations(self, mysql): + try: + ## setup ssl for ftp + + InstallCyberPanel.stdOut("Configuring PureFTPD..", 1) + + try: + if not os.path.exists("/etc/ssl/private"): + os.makedirs("/etc/ssl/private", mode=0o755) + except OSError as e: + if e.errno != errno.EEXIST: + logging.InstallLog.writeToFile("[ERROR] Could not create directory for FTP SSL: " + str(e)) + raise + + if (self.distro == centos or self.distro == cent8 or self.distro == openeuler) or ( + self.distro == ubuntu and get_Ubuntu_release() == 18.14): + command = 'openssl req -newkey rsa:1024 -new -nodes -x509 -days 3650 -subj "/C=US/ST=Denial/L=Springfield/O=Dis/CN=www.example.com" -keyout /etc/ssl/private/pure-ftpd.pem -out /etc/ssl/private/pure-ftpd.pem' + else: + command = 'openssl req -x509 -nodes -days 7300 -newkey rsa:2048 -subj "/C=US/ST=Denial/L=Sprinal-ield/O=Dis/CN=www.example.com" -keyout /etc/ssl/private/pure-ftpd.pem -out /etc/ssl/private/pure-ftpd.pem' + + install_utils.call(command, self.distro, command, command, 1, 0, os.EX_OSERR) + + os.chdir(self.cwd) + ftpdPath = "/etc/pure-ftpd" + + self.copy_config_file("pure-ftpd", ftpdPath, mysql) + + if self.distro == ubuntu: + try: + os.mkdir('/etc/pure-ftpd/conf') + os.mkdir('/etc/pure-ftpd/auth') + os.mkdir('/etc/pure-ftpd/db') + except OSError as err: + self.stdOut("[ERROR] Error creating extra pure-ftpd directories: " + str(err), ". Should be ok", 1) + + data = open(ftpdPath + "/pureftpd-mysql.conf", "r").readlines() + + writeDataToFile = open(ftpdPath + "/pureftpd-mysql.conf", "w") + + dataWritten = "MYSQLPassword " + InstallCyberPanel.mysqlPassword + '\n' + for items in data: + if items.find("MYSQLPassword") > -1: + writeDataToFile.writelines(dataWritten) + else: + writeDataToFile.writelines(items) + + writeDataToFile.close() + + ftpConfPath = '/etc/pure-ftpd/pureftpd-mysql.conf' + + if self.remotemysql == 'ON': + command = "sed -i 's|localhost|%s|g' %s" % (self.mysqlhost, ftpConfPath) + install_utils.call(command, self.distro, command, command, 1, 1, os.EX_OSERR) + + command = "sed -i 's|3306|%s|g' %s" % (self.mysqlport, ftpConfPath) + install_utils.call(command, self.distro, command, command, 1, 1, os.EX_OSERR) + + command = "sed -i 's|MYSQLSocket /var/lib/mysql/mysql.sock||g' %s" % (ftpConfPath) + install_utils.call(command, self.distro, command, command, 1, 1, os.EX_OSERR) + + if self.distro == ubuntu: + + if os.path.exists('/etc/pure-ftpd/db/mysql.conf'): + os.remove('/etc/pure-ftpd/db/mysql.conf') + shutil.copy(ftpdPath + "/pureftpd-mysql.conf", '/etc/pure-ftpd/db/mysql.conf') + else: + shutil.copy(ftpdPath + "/pureftpd-mysql.conf", '/etc/pure-ftpd/db/mysql.conf') + + command = 'echo 1 > /etc/pure-ftpd/conf/TLS' + subprocess.call(command, shell=True) + + command = 'echo %s > /etc/pure-ftpd/conf/ForcePassiveIP' % (self.publicip) + subprocess.call(command, shell=True) + + command = 'echo "40110 40210" > /etc/pure-ftpd/conf/PassivePortRange' + subprocess.call(command, shell=True) + + command = 'echo "no" > /etc/pure-ftpd/conf/UnixAuthentication' + subprocess.call(command, shell=True) + + command = 'echo "/etc/pure-ftpd/db/mysql.conf" > /etc/pure-ftpd/conf/MySQLConfigFile' + subprocess.call(command, shell=True) + + command = 'ln -s /etc/pure-ftpd/conf/MySQLConfigFile /etc/pure-ftpd/auth/30mysql' + install_utils.call(command, self.distro, command, command, 1, 1, os.EX_OSERR) + + command = 'ln -s /etc/pure-ftpd/conf/UnixAuthentication /etc/pure-ftpd/auth/65unix' + install_utils.call(command, self.distro, command, command, 1, 1, os.EX_OSERR) + + command = 'systemctl restart pure-ftpd-mysql.service' + install_utils.call(command, self.distro, command, command, 1, 1, os.EX_OSERR) + + + + + if get_Ubuntu_release() > 21.00: + ### change mysql md5 to crypt + + command = "sed -i 's/MYSQLCrypt md5/MYSQLCrypt crypt/g' /etc/pure-ftpd/db/mysql.conf" + install_utils.call(command, self.distro, command, command, 1, 1, os.EX_OSERR) + + command = "systemctl restart pure-ftpd-mysql.service" + install_utils.call(command, self.distro, command, command, 1, 1, os.EX_OSERR) + else: + + try: + clAPVersion = FetchCloudLinuxAlmaVersionVersion() + type = clAPVersion.split('-')[0] + version = int(clAPVersion.split('-')[1]) + + if type == 'al' and version >= 90: + command = "sed -i 's/MYSQLCrypt md5/MYSQLCrypt crypt/g' /etc/pure-ftpd/pureftpd-mysql.conf" + install_utils.call(command, self.distro, command, command, 1, 1, os.EX_OSERR) + except: + pass + + + + InstallCyberPanel.stdOut("PureFTPD configured!", 1) + + except IOError as msg: + logging.InstallLog.writeToFile('[ERROR] ' + str(msg) + " [installPureFTPDConfigurations]") + return 0 + + def installPowerDNS(self): + try: + if self.distro == ubuntu or self.distro == cent8 or self.distro == openeuler: + # Stop and disable systemd-resolved + self.manage_service('systemd-resolved', 'stop') + self.manage_service('systemd-resolved.service', 'disable') + + try: + os.rename('/etc/resolv.conf', '/etc/resolv.conf.bak') + except OSError as e: + if e.errno != errno.EEXIST and e.errno != errno.ENOENT: + InstallCyberPanel.stdOut("[ERROR] Unable to rename /etc/resolv.conf to install PowerDNS: " + + str(e), 1, 1, os.EX_OSERR) + + # Create a temporary resolv.conf with Google DNS for package installation + try: + with open('/etc/resolv.conf', 'w') as f: + f.write('nameserver 8.8.8.8\n') + f.write('nameserver 8.8.4.4\n') + InstallCyberPanel.stdOut("Created temporary /etc/resolv.conf with Google DNS", 1) + except IOError as e: + InstallCyberPanel.stdOut("[ERROR] Unable to create /etc/resolv.conf: " + str(e), 1, 1, os.EX_OSERR) + + # Install PowerDNS packages + if self.distro == ubuntu: + # Update package list first + command = "DEBIAN_FRONTEND=noninteractive apt-get update" + install_utils.call(command, self.distro, command, command, 1, 1, os.EX_OSERR, True) + + # Install PowerDNS packages + command = "DEBIAN_FRONTEND=noninteractive apt-get -y install pdns-server pdns-backend-mysql" + install_utils.call(command, self.distro, command, command, 1, 1, os.EX_OSERR, True) + + # Ensure service is stopped after installation for configuration + command = 'systemctl stop pdns || true' + install_utils.call(command, self.distro, command, command, 1, 0, os.EX_OSERR, True) + return 1 + else: + self.install_package('pdns pdns-backend-mysql') + + except BaseException as msg: + logging.InstallLog.writeToFile('[ERROR] ' + str(msg) + " [powerDNS]") + + def installPowerDNSConfigurations(self, mysqlPassword, mysql): + try: + + InstallCyberPanel.stdOut("Configuring PowerDNS..", 1) + + os.chdir(self.cwd) + if self.distro == centos or self.distro == cent8 or self.distro == openeuler: + dnsPath = "/etc/pdns/pdns.conf" + else: + dnsPath = "/etc/powerdns/pdns.conf" + # Ensure directory exists for Ubuntu + dnsDir = os.path.dirname(dnsPath) + if not os.path.exists(dnsDir): + try: + os.makedirs(dnsDir, mode=0o755) + except OSError as e: + if e.errno != errno.EEXIST: + raise + + try: + self.copy_config_file("dns", dnsPath, mysql) + except Exception as e: + InstallCyberPanel.stdOut("[ERROR] Failed to copy PowerDNS config: " + str(e), 1) + logging.InstallLog.writeToFile('[ERROR] Failed to copy PowerDNS config: ' + str(e)) + raise + + # Verify the file was copied and has content + if not os.path.exists(dnsPath): + raise IOError(f"PowerDNS config file not found at {dnsPath} after copy") + + # Check if file has content + with open(dnsPath, "r") as f: + content = f.read() + if not content or "launch=gmysql" not in content: + InstallCyberPanel.stdOut("[WARNING] PowerDNS config appears empty or incomplete, attempting to fix...", 1) + + # First try to re-copy + try: + if os.path.exists(dnsPath): + os.remove(dnsPath) + source_file = os.path.join(self.cwd, "dns-one", "pdns.conf") + shutil.copy2(source_file, dnsPath) + except Exception as copy_error: + InstallCyberPanel.stdOut("[WARNING] Failed to re-copy config: " + str(copy_error), 1) + + # Fallback: directly write the essential MySQL configuration + InstallCyberPanel.stdOut("[INFO] Directly writing MySQL backend configuration...", 1) + try: + mysql_config = f"""# PowerDNS MySQL Backend Configuration +launch=gmysql +gmysql-host=localhost +gmysql-port=3306 +gmysql-user=cyberpanel +gmysql-password={mysqlPassword} +gmysql-dbname=cyberpanel + +# Basic PowerDNS settings +daemon=no +guardian=no +setgid=pdns +setuid=pdns +""" + # If file exists and has some content, append our config + if os.path.exists(dnsPath) and content.strip(): + # Check if it's just missing the MySQL part + with open(dnsPath, "a") as f: + f.write("\n\n" + mysql_config) + else: + # Write a complete minimal config + with open(dnsPath, "w") as f: + f.write(mysql_config) + + InstallCyberPanel.stdOut("[SUCCESS] MySQL backend configuration written directly", 1) + except Exception as write_error: + InstallCyberPanel.stdOut("[ERROR] Failed to write MySQL config: " + str(write_error), 1) + raise + + InstallCyberPanel.stdOut("PowerDNS config file prepared at: " + dnsPath, 1) + + data = open(dnsPath, "r").readlines() + + writeDataToFile = open(dnsPath, "w") + + dataWritten = "gmysql-password=" + mysqlPassword + "\n" + + for items in data: + if items.find("gmysql-password") > -1: + writeDataToFile.writelines(dataWritten) + else: + writeDataToFile.writelines(items) + + # if self.distro == ubuntu: + # os.fchmod(writeDataToFile.fileno(), stat.S_IRUSR | stat.S_IWUSR) + + writeDataToFile.close() + + if self.remotemysql == 'ON': + command = "sed -i 's|gmysql-host=localhost|gmysql-host=%s|g' %s" % (self.mysqlhost, dnsPath) + install_utils.call(command, self.distro, command, command, 1, 1, os.EX_OSERR) + + command = "sed -i 's|gmysql-port=3306|gmysql-port=%s|g' %s" % (self.mysqlport, dnsPath) + install_utils.call(command, self.distro, command, command, 1, 1, os.EX_OSERR) + + # Set proper permissions for PowerDNS config + if self.distro == ubuntu: + # Ensure pdns user/group exists + command = 'id -u pdns &>/dev/null || useradd -r -s /usr/sbin/nologin pdns' + install_utils.call(command, self.distro, command, command, 1, 0, os.EX_OSERR) + + command = 'chown root:pdns %s' % dnsPath + install_utils.call(command, self.distro, command, command, 1, 0, os.EX_OSERR) + + command = 'chmod 640 %s' % dnsPath + install_utils.call(command, self.distro, command, command, 1, 0, os.EX_OSERR) + + InstallCyberPanel.stdOut("PowerDNS configured!", 1) + + except IOError as msg: + logging.InstallLog.writeToFile('[ERROR] ' + str(msg) + " [installPowerDNSConfigurations]") + return 0 + return 1 + + def startPowerDNS(self): + + ############## Start PowerDNS ###################### + + self.manage_service('pdns', 'enable') + + # During fresh installation, don't start PowerDNS yet + # It will be started after Django migrations create the required tables + InstallCyberPanel.stdOut("PowerDNS enabled for startup.", 1) + InstallCyberPanel.stdOut("Note: PowerDNS will start after database setup is complete.", 1) + logging.InstallLog.writeToFile("PowerDNS enabled but not started - waiting for Django migrations") + + # The service will be started later after migrations run + # or manually by the admin after installation completes + + +def Main(cwd, mysql, distro, ent, serial=None, port="8090", ftp=None, dns=None, publicip=None, remotemysql=None, + mysqlhost=None, mysqldb=None, mysqluser=None, mysqlpassword=None, mysqlport=None): + InstallCyberPanel.mysqlPassword = install_utils.generate_pass() + InstallCyberPanel.mysql_Root_password = install_utils.generate_pass() + + file_name = '/etc/cyberpanel/mysqlPassword' + + if remotemysql == 'OFF': + if os.access(file_name, os.F_OK): + password = open(file_name, 'r') + InstallCyberPanel.mysql_Root_password = password.readline() + password.close() + else: + password = open(file_name, "w") + password.writelines(InstallCyberPanel.mysql_Root_password) + password.close() + else: + mysqlData = {'remotemysql': remotemysql, 'mysqlhost': mysqlhost, 'mysqldb': mysqldb, 'mysqluser': mysqluser, + 'mysqlpassword': mysqlpassword, 'mysqlport': mysqlport} + from json import dumps + writeToFile = open(file_name, 'w') + writeToFile.write(dumps(mysqlData)) + writeToFile.close() + + if install.preFlightsChecks.debug: + print(open(file_name, 'r').read()) + time.sleep(10) + + try: + command = 'chmod 640 %s' % (file_name) + install_utils.call(command, distro, '[chmod]', + '', + 1, 0, os.EX_OSERR) + command = 'chown root:cyberpanel %s' % (file_name) + install_utils.call(command, distro, '[chmod]', + '', + 1, 0, os.EX_OSERR) + except: + pass + + # For RHEL-based systems (CentOS, AlmaLinux, Rocky, etc.), generate a separate password + if distro in [centos, cent8, openeuler]: + InstallCyberPanel.mysqlPassword = install_utils.generate_pass() + else: + # For Ubuntu/Debian, use the same password as root + InstallCyberPanel.mysqlPassword = InstallCyberPanel.mysql_Root_password + + installer = InstallCyberPanel("/usr/local/lsws/", cwd, distro, ent, serial, port, ftp, dns, publicip, remotemysql, + mysqlhost, mysqldb, mysqluser, mysqlpassword, mysqlport) + + logging.InstallLog.writeToFile('Installing LiteSpeed Web server,40') + installer.installLiteSpeed() + if ent == 0: + installer.changePortTo80() + logging.InstallLog.writeToFile('Installing Optimized PHPs..,50') + installer.installAllPHPVersions() + if ent == 0: + installer.fix_ols_configs() + + logging.InstallLog.writeToFile('Installing Sieve for email filtering..,55') + installer.installSieve() + + logging.InstallLog.writeToFile('Installing MySQL,60') + installer.installMySQL(mysql) + installer.changeMYSQLRootPassword() + + installer.startMariaDB() + + if remotemysql == 'OFF': + if distro == ubuntu: + installer.fixMariaDB() + + mysqlUtilities.createDatabase("cyberpanel", "cyberpanel", InstallCyberPanel.mysqlPassword, publicip) + + if ftp is None: + installer.installPureFTPD() + installer.installPureFTPDConfigurations(mysql) + installer.startPureFTPD() + else: + if ftp == 'ON': + installer.installPureFTPD() + installer.installPureFTPDConfigurations(mysql) + installer.startPureFTPD() + + if dns is None: + installer.installPowerDNS() + installer.installPowerDNSConfigurations(InstallCyberPanel.mysqlPassword, mysql) + installer.startPowerDNS() + else: + if dns == 'ON': + installer.installPowerDNS() + installer.installPowerDNSConfigurations(InstallCyberPanel.mysqlPassword, mysql) + installer.startPowerDNS() diff --git a/install/pure-ftpd-one/pure-ftpd.conf b/install/pure-ftpd-one/pure-ftpd.conf index 27f4d1544..9298bb32d 100644 --- a/install/pure-ftpd-one/pure-ftpd.conf +++ b/install/pure-ftpd-one/pure-ftpd.conf @@ -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 diff --git a/install/pure-ftpd/pure-ftpd.conf b/install/pure-ftpd/pure-ftpd.conf index 27f4d1544..9298bb32d 100644 --- a/install/pure-ftpd/pure-ftpd.conf +++ b/install/pure-ftpd/pure-ftpd.conf @@ -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 diff --git a/mailServer/mailserverManager.py b/mailServer/mailserverManager.py index 1fd984d07..758b26904 100644 --- a/mailServer/mailserverManager.py +++ b/mailServer/mailserverManager.py @@ -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 in JS from closing the HTML script tag + email_limits_controller_js = email_limits_controller_js.replace('', '<\\/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): diff --git a/mailServer/static/mailServer/emailLimitsController.js b/mailServer/static/mailServer/emailLimitsController.js new file mode 100644 index 000000000..21d504cc7 --- /dev/null +++ b/mailServer/static/mailServer/emailLimitsController.js @@ -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; + } + }; + }); +})(); diff --git a/mailServer/static/mailServer/mailServer.js b/mailServer/static/mailServer/mailServer.js index 6b6695052..a546a8cec 100644 --- a/mailServer/static/mailServer/mailServer.js +++ b/mailServer/static/mailServer/mailServer.js @@ -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; } } + } }; diff --git a/mailServer/templates/mailServer/EmailLimits.html b/mailServer/templates/mailServer/EmailLimits.html index 6a7364f76..1c1961c7f 100644 --- a/mailServer/templates/mailServer/EmailLimits.html +++ b/mailServer/templates/mailServer/EmailLimits.html @@ -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 @@
+{% endblock %} + +{% block footer_scripts %} + {# EmailLimitsNew controller: inline from view (preferred) or fallback to static file #} + {% if email_limits_controller_js %} + + {% else %} + + {% endif %} {% endblock %} \ No newline at end of file diff --git a/plogical/firewallUtilities.py b/plogical/firewallUtilities.py index fe4295554..78c8995c0 100644 --- a/plogical/firewallUtilities.py +++ b/plogical/firewallUtilities.py @@ -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: diff --git a/plogical/mailUtilities.py b/plogical/mailUtilities.py index 59701151a..67ad2790b 100644 --- a/plogical/mailUtilities.py +++ b/plogical/mailUtilities.py @@ -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)})') diff --git a/plogical/processUtilities.py b/plogical/processUtilities.py index ff1322e8f..e9557f5a1 100644 --- a/plogical/processUtilities.py +++ b/plogical/processUtilities.py @@ -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") diff --git a/public/static/ftp/ftp.js b/public/static/ftp/ftp.js index 113845c8f..875bf72cd 100644 --- a/public/static/ftp/ftp.js +++ b/public/static/ftp/ftp.js @@ -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 () { diff --git a/serverStatus/static/serverStatus/serverStatus.js b/serverStatus/static/serverStatus/serverStatus.js index f16d66701..733d26a96 100644 --- a/serverStatus/static/serverStatus/serverStatus.js +++ b/serverStatus/static/serverStatus/serverStatus.js @@ -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); - } } diff --git a/serverStatus/templates/serverStatus/services.html b/serverStatus/templates/serverStatus/services.html index 11fdcd624..db6b6084d 100644 --- a/serverStatus/templates/serverStatus/services.html +++ b/serverStatus/templates/serverStatus/services.html @@ -622,6 +622,7 @@
{% trans "Action Failed" %} + {% trans "Details:" %}
diff --git a/serverStatus/views.py b/serverStatus/views.py index 9d5d54bd2..8f051b86f 100644 --- a/serverStatus/views.py +++ b/serverStatus/views.py @@ -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: diff --git a/sql/create_ftp_quotas.sql b/sql/create_ftp_quotas.sql new file mode 100644 index 000000000..65af044a8 --- /dev/null +++ b/sql/create_ftp_quotas.sql @@ -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; diff --git a/static/ftp/ftp.js b/static/ftp/ftp.js index 6a7cb75da..3035a8c7c 100644 --- a/static/ftp/ftp.js +++ b/static/ftp/ftp.js @@ -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(); } }; diff --git a/static/mailServer/emailLimitsController.js b/static/mailServer/emailLimitsController.js new file mode 100644 index 000000000..45f6bc77b --- /dev/null +++ b/static/mailServer/emailLimitsController.js @@ -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; + } + }; + }); +})(); diff --git a/static/mailServer/mailServer.js b/static/mailServer/mailServer.js index 6b6695052..62be0aefe 100644 --- a/static/mailServer/mailServer.js +++ b/static/mailServer/mailServer.js @@ -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) { diff --git a/to-do/EMAIL-LIMITS-DEPLOY-CHECKLIST.md b/to-do/EMAIL-LIMITS-DEPLOY-CHECKLIST.md new file mode 100644 index 000000000..779548dab --- /dev/null +++ b/to-do/EMAIL-LIMITS-DEPLOY-CHECKLIST.md @@ -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. diff --git a/to-do/EMAIL-LIMITS-LIVE-SERVER-CHECKLIST.md b/to-do/EMAIL-LIMITS-LIVE-SERVER-CHECKLIST.md new file mode 100644 index 000000000..70fc18670 --- /dev/null +++ b/to-do/EMAIL-LIMITS-LIVE-SERVER-CHECKLIST.md @@ -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 (repo’s `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 app’s `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. diff --git a/to-do/FTP-QUOTAS-TABLE-FIX.md b/to-do/FTP-QUOTAS-TABLE-FIX.md new file mode 100644 index 000000000..99f3160ea --- /dev/null +++ b/to-do/FTP-QUOTAS-TABLE-FIX.md @@ -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 Django’s 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. diff --git a/to-do/PURE-FTPD-QUOTA-SYNTAX-FIX.md b/to-do/PURE-FTPD-QUOTA-SYNTAX-FIX.md new file mode 100644 index 000000000..917206ec1 --- /dev/null +++ b/to-do/PURE-FTPD-QUOTA-SYNTAX-FIX.md @@ -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 `. diff --git a/to-do/RUNTIME-VS-REPO-2.5.5-DEV.md b/to-do/RUNTIME-VS-REPO-2.5.5-DEV.md new file mode 100644 index 000000000..d272848f7 --- /dev/null +++ b/to-do/RUNTIME-VS-REPO-2.5.5-DEV.md @@ -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 repo’s `settings.py`. + +- **Version** + `baseTemplate/views.py` has `VERSION = '2.5.5'`, `BUILD = 'dev'`. Repo’s `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': ''})` + 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. diff --git a/to-do/V2.5.5-DEV-FIXES-AND-DEPLOY.md b/to-do/V2.5.5-DEV-FIXES-AND-DEPLOY.md new file mode 100644 index 000000000..b58f4721a --- /dev/null +++ b/to-do/V2.5.5-DEV-FIXES-AND-DEPLOY.md @@ -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. diff --git a/websiteFunctions/templates/websiteFunctions/ftpQuotaManagement.html b/websiteFunctions/templates/websiteFunctions/ftpQuotaManagement.html index 8f2545a5c..b075aa0fd 100644 --- a/websiteFunctions/templates/websiteFunctions/ftpQuotaManagement.html +++ b/websiteFunctions/templates/websiteFunctions/ftpQuotaManagement.html @@ -41,8 +41,8 @@ FTP Quota Management - CyberPanel
FTP Quota System

Enable and manage individual FTP user quotas. This allows you to set storage limits for each FTP user.

-
@@ -125,15 +125,79 @@ FTP Quota Management - CyberPanel