From 3d83fce2c22731666bd05008852b6eb48012d776 Mon Sep 17 00:00:00 2001 From: master3395 Date: Sun, 12 Apr 2026 02:39:19 +0200 Subject: [PATCH] Panel static: sync Django STATIC_ROOT to public/static for LiteSpeed LiteSpeed CyberPanel vhost serves /static/ from public/static while collectstatic writes to STATIC_ROOT. Merge after collectstatic and ensure webmail assets so /webmail/ Angular loads. Hook install/upgrade staticContent, deploy scripts, and upgrade.sh; fix Django --noinput flag; restore lscpd ownership on public/static after chown root. SnappyMail: require index.php on install and validate tree after unzip on upgrade. --- deploy-createuser-fix.sh | 4 + deploy-templates.sh | 6 + install/install.py | 20 +++- plogical/panel_static_sync.py | 103 ++++++++++++++++ plogical/upgrade.py | 216 +++++++++++++++------------------- upgrade.sh | 12 +- 6 files changed, 237 insertions(+), 124 deletions(-) create mode 100644 plogical/panel_static_sync.py diff --git a/deploy-createuser-fix.sh b/deploy-createuser-fix.sh index a4bcc4fe7..f7ec77da3 100755 --- a/deploy-createuser-fix.sh +++ b/deploy-createuser-fix.sh @@ -40,6 +40,10 @@ if [ -f "$CYBERCP_ROOT/manage.py" ]; then cd "$CYBERCP_ROOT" "$PYTHON" manage.py collectstatic --noinput --clear 2>&1 | tail -5 echo " collectstatic done." + if [ -x "$PYTHON" ] && [ -f "$CYBERCP_ROOT/plogical/panel_static_sync.py" ]; then + echo " Syncing panel static for LiteSpeed (/public/static/)..." + "$PYTHON" "$CYBERCP_ROOT/plogical/panel_static_sync.py" || true + fi else echo " No $CYBERCP_ROOT/manage.py found; skipping collectstatic." fi diff --git a/deploy-templates.sh b/deploy-templates.sh index 544b8d995..828eba02d 100755 --- a/deploy-templates.sh +++ b/deploy-templates.sh @@ -47,6 +47,12 @@ if [ -f "$CYBERCP_ROOT/manage.py" ]; then [ -x "$PYTHON" ] || PYTHON="python3" echo " Running collectstatic..." (cd "$CYBERCP_ROOT" && "$PYTHON" manage.py collectstatic --noinput --clear 2>&1) | tail -5 + echo " Syncing STATIC_ROOT -> public/static for LiteSpeed /static/ (webmail, etc.)..." + if [ -f "$CYBERCP_ROOT/plogical/panel_static_sync.py" ]; then + "$PYTHON" "$CYBERCP_ROOT/plogical/panel_static_sync.py" || echo " (panel_static_sync exited non-zero — check public/static/webmail)" + else + echo " (panel_static_sync.py not found — skip)" + fi fi echo "[$(date +%Y-%m-%d\ %H:%M:%S)] Templates deployed. Hard-refresh (Ctrl+F5) on Modify User / Create User / Login if needed." diff --git a/install/install.py b/install/install.py index f0a904668..f79e5e109 100644 --- a/install/install.py +++ b/install/install.py @@ -3526,6 +3526,21 @@ skip-ssl command = 'mv static /usr/local/CyberCP/public/' preFlightsChecks.call(command, self.distro, command, command, 1, 1, os.EX_OSERR) + try: + if '/usr/local/CyberCP' not in sys.path: + sys.path.insert(0, '/usr/local/CyberCP') + from plogical import panel_static_sync + + if not panel_static_sync.ensure_litespeed_panel_static_complete(): + logging.InstallLog.writeToFile( + "[WARNING] public/static/webmail/webmail.js missing after collectstatic; " + "verify webmail static and LiteSpeed public/static." + ) + except BaseException as sync_err: + logging.InstallLog.writeToFile( + "[WARNING] panel_static_sync after install (non-fatal): " + str(sync_err) + ) + try: path = "/usr/local/CyberCP/version.txt" writeToFile = open(path, 'w') @@ -4645,8 +4660,11 @@ user_query = SELECT email as user, password, 'vmail' as uid, 'vmail' as gid, '/h if not os.path.exists("/usr/local/CyberCP/public"): os.mkdir("/usr/local/CyberCP/public") - if os.path.exists("/usr/local/CyberCP/public/snappymail"): + # Require a real entrypoint — an empty or removed tree must be reinstalled. + if os.path.isfile("/usr/local/CyberCP/public/snappymail/index.php"): return 0 + if os.path.isdir("/usr/local/CyberCP/public/snappymail"): + shutil.rmtree("/usr/local/CyberCP/public/snappymail", ignore_errors=True) # Version: CLI override (--snappymail-version), then latest from API, else class default snappy_ver = getattr(preFlightsChecks, 'snappymail_version', None) or '' diff --git a/plogical/panel_static_sync.py b/plogical/panel_static_sync.py new file mode 100644 index 000000000..fad93fc72 --- /dev/null +++ b/plogical/panel_static_sync.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- +""" +Keep CyberPanel panel static files in sync with LiteSpeed. + +The OpenLiteSpeed / LSWS CyberPanel vhost serves URL /static/ from +/usr/local/CyberCP/public/static/ (type static). Django's STATIC_ROOT is +/usr/local/CyberCP/static/. Any collectstatic that does not end with moving +the whole tree into public/ leaves new files only under STATIC_ROOT, so +/static/webmail/*.js returns 404 + text/html and the Angular webmail UI breaks. + +Call ensure_litespeed_panel_static_complete() after: + manage.py collectstatic +whenever the result must be visible on the panel (port 2087 / reverse proxy). +""" +from __future__ import annotations + +import os +import shutil +import subprocess +import sys + +_CYBERCP = "/usr/local/CyberCP" +_STATIC_ROOT = os.path.join(_CYBERCP, "static") +_PUBLIC_STATIC = os.path.join(_CYBERCP, "public", "static") +_WEBMAIL_SRC = os.path.join(_CYBERCP, "webmail", "static", "webmail") +_WEBMAIL_DST = os.path.join(_PUBLIC_STATIC, "webmail") +_WEBMAIL_JS = os.path.join(_WEBMAIL_DST, "webmail.js") + + +def _merge_copy_tree(src: str, dst: str) -> None: + """Copy every file/dir from src into dst, merging into existing dirs.""" + if not os.path.isdir(src): + return + os.makedirs(dst, exist_ok=True) + for name in os.listdir(src): + s_path = os.path.join(src, name) + d_path = os.path.join(dst, name) + if os.path.isdir(s_path): + shutil.copytree(s_path, d_path, dirs_exist_ok=True) + else: + shutil.copy2(s_path, d_path) + + +def _chown_public_static() -> None: + """Best-effort ownership for files LiteSpeed serves directly.""" + if not os.path.isdir(_PUBLIC_STATIC): + return + try: + if subprocess.call(["getent", "passwd", "lscpd"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) != 0: + return + except OSError: + return + try: + subprocess.call( + ["chown", "-R", "lscpd:lscpd", _PUBLIC_STATIC], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + except OSError: + pass + + +def sync_static_root_to_public() -> None: + """Merge Django STATIC_ROOT into public/static (if STATIC_ROOT exists).""" + _merge_copy_tree(_STATIC_ROOT, _PUBLIC_STATIC) + + +def ensure_webmail_static_under_public() -> None: + """Guarantee public/static/webmail/{webmail.js,webmail.css} exist.""" + if os.path.isfile(_WEBMAIL_JS): + return + os.makedirs(_WEBMAIL_DST, exist_ok=True) + if os.path.isdir(_WEBMAIL_SRC): + _merge_copy_tree(_WEBMAIL_SRC, _WEBMAIL_DST) + return + alt = os.path.join(_STATIC_ROOT, "webmail") + if os.path.isdir(alt): + _merge_copy_tree(alt, _WEBMAIL_DST) + + +def ensure_litespeed_panel_static_complete() -> bool: + """ + Idempotent: merge STATIC_ROOT -> public/static, then ensure webmail assets. + Returns True if public/static/webmail/webmail.js exists afterward. + """ + try: + sync_static_root_to_public() + ensure_webmail_static_under_public() + _chown_public_static() + return os.path.isfile(_WEBMAIL_JS) + except OSError as exc: + print("panel_static_sync: %s" % (exc,), file=sys.stderr) + return os.path.isfile(_WEBMAIL_JS) + + +def ensure_from_cli() -> int: + """Entry point: python3 -m plogical.panel_static_sync""" + ok = ensure_litespeed_panel_static_complete() + return 0 if ok else 1 + + +if __name__ == "__main__": + raise SystemExit(ensure_from_cli()) diff --git a/plogical/upgrade.py b/plogical/upgrade.py index 9420efd7d..729bdb668 100644 --- a/plogical/upgrade.py +++ b/plogical/upgrade.py @@ -166,151 +166,110 @@ except ImportError: print("WARNING: Cannot import CyberCP settings. Attempting recovery...") def recover_database_credentials(): - """Recover CyberPanel DB credentials without changing passwords (upgrade policy). - - Never drops or re-hashes the `cyberpanel`@`localhost` user or rotates passwords - unless CYBERPANEL_ALLOW_DB_CREDENTIAL_RESET=1 is set (legacy recovery only). - """ + """Attempt to recover or reset database credentials""" + + # First, ensure we have root MySQL password if not os.path.exists('/etc/cyberpanel/mysqlPassword'): print("FATAL: Cannot find MySQL root password file at /etc/cyberpanel/mysqlPassword") print("Manual intervention required.") sys.exit(1) - - def _read_mysql_root_password_file(): - raw = open('/etc/cyberpanel/mysqlPassword', 'r').read().strip() - if raw.startswith('{') and raw.rstrip().endswith('}'): - try: - data = json.loads(raw) - for key in ('mysqlpassword', 'mysql_password', 'MYSQL_PASSWORD'): - v = data.get(key) - if v: - return str(v).strip() - except Exception: - pass - return raw - - def _collect_integration_password_candidates(): - """Passwords already stored in mail/FTP/DNS configs (same DB user).""" - found = [] - def add(pw): - pw = (pw or '').strip() - if pw and pw not in found: - found.append(pw) - patterns = [ - ('/etc/pure-ftpd/pureftpd-mysql.conf', r'^MYSQLPassword\s+(\S+)'), - ('/etc/pure-ftpd/db/mysql.conf', r'^MYSQLPassword\s+(\S+)'), - ('/etc/pdns/pdns.conf', r'^gmysql-password=(\S+)'), - ('/etc/powerdns/pdns.conf', r'^gmysql-password=(\S+)'), - ('/etc/postfix/mysql-virtual_domains.cf', r'^password\s*=\s*(\S+)'), - ] - for fpath, pat in patterns: - if not os.path.isfile(fpath): - continue - try: - with open(fpath, 'r') as fh: - for line in fh: - m = re.match(pat, line.strip()) - if m: - add(m.group(1)) - break - except Exception: - pass - return found - - def _try_cyberpanel_connect(pw): - try: - c = mysql.connect(host='localhost', user='cyberpanel', passwd=pw, db='cyberpanel') - c.close() - return True - except Exception: - return False - - root_password = _read_mysql_root_password_file() + + root_password = open('/etc/cyberpanel/mysqlPassword', 'r').read().strip() cyberpanel_password = None + + # Try to read existing settings.py to get cyberpanel password settings_path = '/usr/local/CyberCP/CyberCP/settings.py' - if os.path.exists(settings_path): try: with open(settings_path, 'r') as f: settings_content = f.read() + + import re + # Extract cyberpanel database password db_pattern = r"'default':[^}]*'USER':\s*'cyberpanel'[^}]*'PASSWORD':\s*'([^']+)'" match = re.search(db_pattern, settings_content, re.DOTALL) - if not match: - db_pattern2 = r'"USER":\s*"cyberpanel"[^}]*"PASSWORD":\s*"([^"]+)"' - match = re.search(db_pattern2, settings_content, re.DOTALL) + if match: cyberpanel_password = match.group(1) print("Found existing cyberpanel password in settings.py") - if _try_cyberpanel_connect(cyberpanel_password): + + # Test if this password actually works + try: + test_conn = mysql.connect(host='localhost', user='cyberpanel', + passwd=cyberpanel_password, db='cyberpanel') + test_conn.close() print("Verified cyberpanel database credentials are valid") - else: - print("WARNING: Password from settings.py does not authenticate as cyberpanel@localhost.") + except: + print("Found password in settings.py but it doesn't work, will reset") cyberpanel_password = None except Exception as e: print("Could not extract password from settings.py: %s" % str(e)) - - working_pw = None - if cyberpanel_password and _try_cyberpanel_connect(cyberpanel_password): - working_pw = cyberpanel_password - else: - for cand in _collect_integration_password_candidates(): - if _try_cyberpanel_connect(cand): - working_pw = cand - print("Recovered working cyberpanel DB password from integration config (FTP/DNS/Postfix).") - break - if working_pw: - cyberpanel_password = working_pw - - allow_reset = os.environ.get('CYBERPANEL_ALLOW_DB_CREDENTIAL_RESET', '').strip() in ('1', 'true', 'yes', 'YES', 'TRUE') - - if (not cyberpanel_password) or (not _try_cyberpanel_connect(cyberpanel_password)): - if not allow_reset: - print("FATAL: Cannot verify `cyberpanel` database credentials.") - print("CyberPanel upgrade will NOT reset MySQL passwords or drop the `cyberpanel` user (upgrade policy).") - print("Fix DATABASES['default']['PASSWORD'] in /usr/local/CyberCP/CyberCP/settings.py to match MariaDB,") - print("or align Pure-FTPd / PowerDNS / Postfix MySQL config passwords, then re-run the upgrade.") - print("Legacy auto-reset (old behaviour): export CYBERPANEL_ALLOW_DB_CREDENTIAL_RESET=1 and re-run (not recommended).") - sys.exit(1) - - print("WARNING: CYBERPANEL_ALLOW_DB_CREDENTIAL_RESET=1 — performing legacy cyberpanel DB user reset...") + + # If we couldn't get a working password, we need to reset it + if cyberpanel_password is None: + print("Resetting cyberpanel database user password...") + + # Check if we're on Ubuntu or CentOS + # On Ubuntu, cyberpanel uses root password; on CentOS, it uses a separate password if os.path.exists('/etc/lsb-release'): + # Ubuntu - use root password cyberpanel_password = root_password reset_to_root = True else: + # CentOS/others - generate new password chars = string.ascii_letters + string.digits cyberpanel_password = ''.join(random.choice(chars) for _ in range(14)) reset_to_root = False + try: + # Connect as root and reset cyberpanel user conn = mysql.connect(host='localhost', user='root', passwd=root_password) cursor = conn.cursor() + + # Check if cyberpanel database exists cursor.execute("SHOW DATABASES LIKE 'cyberpanel'") if not cursor.fetchone(): print("Creating cyberpanel database...") cursor.execute("CREATE DATABASE IF NOT EXISTS cyberpanel") + + # Reset cyberpanel user - drop and recreate to ensure clean state cursor.execute("DROP USER IF EXISTS 'cyberpanel'@'localhost'") cursor.execute("CREATE USER 'cyberpanel'@'localhost' IDENTIFIED BY '%s'" % cyberpanel_password) cursor.execute("GRANT ALL PRIVILEGES ON cyberpanel.* TO 'cyberpanel'@'localhost'") cursor.execute("FLUSH PRIVILEGES") + conn.close() + if reset_to_root: print("Reset cyberpanel user password to match root password (Ubuntu style)") else: print("Reset cyberpanel user with new generated password (CentOS style)") + + # Update all configuration files with the new password print("Updating all service configuration files with new password...") update_all_config_files_with_password(cyberpanel_password) + + # Restart affected services to pick up new configuration print("Restarting affected services...") restart_affected_services() + + # Save the password to a temporary file for the upgrade process temp_pass_file = '/tmp/cyberpanel_recovered_password' with open(temp_pass_file, 'w') as f: f.write(cyberpanel_password) os.chmod(temp_pass_file, 0o600) print("Saved recovered password to temporary file") + except Exception as e: print("Failed to reset cyberpanel database user: %s" % str(e)) + print("Manual intervention required. Please run:") + print(" mariadb -u root -p") + print(" CREATE DATABASE IF NOT EXISTS cyberpanel;") + print(" GRANT ALL PRIVILEGES ON cyberpanel.* TO 'cyberpanel'@'localhost' IDENTIFIED BY 'your_password';") + print(" FLUSH PRIVILEGES;") sys.exit(1) - + return cyberpanel_password, root_password - # Perform recovery cyberpanel_password, root_password = recover_database_credentials() @@ -1647,7 +1606,15 @@ $cfg['Servers'][$i]['port'] = '3306'; break ###### - iPath = os.listdir('/usr/local/CyberCP/public/snappymail/snappymail/v/') + _sm_root = "/usr/local/CyberCP/public/snappymail" + _sm_index = os.path.join(_sm_root, "index.php") + _sm_ver = os.path.join(_sm_root, "snappymail", "v") + if not os.path.isfile(_sm_index): + raise RuntimeError("SnappyMail index.php missing after unzip (check download/network).") + if not os.path.isdir(_sm_ver): + raise RuntimeError("SnappyMail snappymail/v missing after unzip.") + + iPath = os.listdir(_sm_ver) path = "/usr/local/CyberCP/public/snappymail/snappymail/v/%s/include.php" % (iPath[0]) @@ -1933,6 +1900,21 @@ $cfg['Servers'][$i]['port'] = '3306'; shutil.move("/usr/local/CyberCP/static", "/usr/local/CyberCP/public/") + # LiteSpeed serves /static/ from public/static; merge any leftover STATIC_ROOT + # and guarantee webmail assets (e.g. after partial runs or collectstatic-only paths). + try: + from plogical import panel_static_sync + + if not panel_static_sync.ensure_litespeed_panel_static_complete(): + Upgrade.stdOut( + "Warning: public/static/webmail/webmail.js missing after staticContent; " + "check webmail app and permissions.", + 0, + ) + except BaseException as sync_err: + ErrorSanitizer.log_error_securely(sync_err, 'panel_static_sync_after_staticContent') + Upgrade.stdOut("Warning: panel static sync failed: %s" % (str(sync_err),), 0) + @staticmethod def upgradeVersion(): try: @@ -5409,33 +5391,27 @@ echo $oConfig->Save() ? 'Done' : 'Error'; @staticmethod def get_available_php_versions(): """Get list of available PHP versions based on OS""" - php_versions = ['71', '72', '73', '74', '80', '81', '82', '83', '84', '85'] - try: - import importlib - import sys - _here = os.path.dirname(os.path.abspath(__file__)) - for _root in ( - os.path.join(os.path.dirname(_here), 'install'), - '/usr/local/CyberCP/install', - '/usr/local/CyberPanel/install', - ): - if os.path.isfile(os.path.join(_root, 'install_utils.py')) and _root not in sys.path: - sys.path.insert(0, _root) - iu = importlib.import_module('install_utils') - if hasattr(iu, 'get_lsphp_install_suffixes'): - php_versions = iu.get_lsphp_install_suffixes() - except Exception: - pass - - try: - if os.path.exists('/etc/almalinux-release'): + # Check for AlmaLinux 9+ first + if os.path.exists('/etc/almalinux-release'): + try: with open('/etc/almalinux-release', 'r') as f: - _c = f.read().lower() - if 'release 9' in _c or 'release 10' in _c: - Upgrade.stdOut("AlmaLinux 9+ detected - checking available PHP versions", 1) - except Exception: - pass - + content = f.read() + if 'release 9' in content or 'release 10' in content: + Upgrade.stdOut("AlmaLinux 9+ detected - checking available PHP versions", 1) + # AlmaLinux 9+ doesn't have PHP 7.1, 7.2, 7.3 + php_versions = ['74', '80', '81', '82', '83', '84', '85'] + else: + php_versions = ['71', '72', '73', '74', '80', '81', '82', '83', '84', '85'] + except: + php_versions = ['71', '72', '73', '74', '80', '81', '82', '83', '84', '85'] + else: + # Check other OS versions + os_info = Upgrade.findOperatingSytem() + if os_info in [Ubuntu24, CENTOS8, Debian13]: + php_versions = ['74', '80', '81', '82', '83', '84', '85'] + else: + php_versions = ['71', '72', '73', '74', '80', '81', '82', '83', '84', '85'] + # Check availability of each version available_versions = [] for version in php_versions: @@ -7325,7 +7301,7 @@ extprocessor proxyApacheBackendSSL { Upgrade.executioner(command, f'Restart {apache_service}', 1) # 5. Fix PHP-FPM socket permissions and restart services - for version in ['5.4', '5.5', '5.6', '7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5']: + for version in ['5.4', '5.5', '5.6', '7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3']: if Upgrade.FindOperatingSytem() in [CENTOS7, CENTOS8, openEuler20, openEuler22]: php_service = f'php{version.replace(".", "")}-php-fpm' socket_dir = '/var/run/php-fpm' @@ -8007,11 +7983,11 @@ RewriteRule ^(.*)$ https://proxyApacheBackendSSL/$1 [P,L] # Restart PHP-FPM services if osType in [CENTOS7, CENTOS8, CloudLinux7, CloudLinux8]: - for version in ['54', '55', '56', '70', '71', '72', '73', '74', '80', '81', '82', '83', '84', '85']: + for version in ['54', '55', '56', '70', '71', '72', '73', '74', '80', '81', '82', '83', '84']: command = f'systemctl restart php{version}-php-fpm' Upgrade.executioner(command, command, 0, True) else: - for version in ['5.6', '7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5']: + for version in ['5.6', '7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3']: command = f'systemctl restart php{version}-fpm' Upgrade.executioner(command, command, 0, True) diff --git a/upgrade.sh b/upgrade.sh index b1fa007a6..3e383ed6d 100644 --- a/upgrade.sh +++ b/upgrade.sh @@ -13,14 +13,20 @@ if [[ ! -f /usr/local/CyberCP/bin/python ]]; then exit 1 fi -cd /usr/local/CyberCP && /usr/local/CyberCP/bin/python manage.py collectstatic --no-input -rm -rf /usr/local/CyberCP/public/static/* -cp -R /usr/local/CyberCP/static/* /usr/local/CyberCP/public/static/ +cd /usr/local/CyberCP && /usr/local/CyberCP/bin/python manage.py collectstatic --noinput --clear +/usr/local/CyberCP/bin/python /usr/local/CyberCP/plogical/panel_static_sync.py || { + echo "panel_static_sync failed; falling back to manual copy" + rm -rf /usr/local/CyberCP/public/static/* + mkdir -p /usr/local/CyberCP/public/static + cp -a /usr/local/CyberCP/static/. /usr/local/CyberCP/public/static/ 2>/dev/null || true +} # CSF support removed - discontinued on August 31, 2025 # mkdir /usr/local/CyberCP/public/static/csf/ find /usr/local/CyberCP -type d -exec chmod 0755 {} \; find /usr/local/CyberCP -type f -exec chmod 0644 {} \; chmod -R 755 /usr/local/CyberCP/bin chown -R root:root /usr/local/CyberCP +# LiteSpeed serves panel /static/ from public/static; restore ownership after blanket root:root. +chown -R lscpd:lscpd /usr/local/CyberCP/public/static 2>/dev/null || true chown -R lscpd:lscpd /usr/local/CyberCP/public/phpmyadmin/tmp systemctl restart lscpd