diff --git a/CPScripts/mailscannerinstaller.sh b/CPScripts/mailscannerinstaller.sh index 63925c14d..bd127609f 100644 --- a/CPScripts/mailscannerinstaller.sh +++ b/CPScripts/mailscannerinstaller.sh @@ -47,9 +47,15 @@ fi ### OS Detection Server_OS="" Server_OS_Version="" -if grep -q -E "CentOS Linux 7|CentOS Linux 8" /etc/os-release ; then +if grep -q -E "CentOS Linux 7|CentOS Linux 8|CentOS Stream" /etc/os-release ; then Server_OS="CentOS" -elif grep -q -E "AlmaLinux-8|AlmaLinux-9|AlmaLinux-10" /etc/os-release ; then +elif grep -q "Red Hat Enterprise Linux" /etc/os-release ; then + Server_OS="RedHat" +elif grep -q "AlmaLinux-8" /etc/os-release ; then + Server_OS="AlmaLinux" +elif grep -q "AlmaLinux-9" /etc/os-release ; then + Server_OS="AlmaLinux" +elif grep -q "AlmaLinux-10" /etc/os-release ; then Server_OS="AlmaLinux" elif grep -q -E "CloudLinux 7|CloudLinux 8" /etc/os-release ; then Server_OS="CloudLinux" @@ -57,11 +63,13 @@ elif grep -q -E "Rocky Linux" /etc/os-release ; then Server_OS="RockyLinux" elif grep -q -E "Ubuntu 18.04|Ubuntu 20.04|Ubuntu 20.10|Ubuntu 22.04|Ubuntu 24.04" /etc/os-release ; then Server_OS="Ubuntu" +elif grep -q -E "Debian GNU/Linux 11|Debian GNU/Linux 12|Debian GNU/Linux 13" /etc/os-release ; then + Server_OS="Debian" elif grep -q -E "openEuler 20.03|openEuler 22.03" /etc/os-release ; then Server_OS="openEuler" else echo -e "Unable to detect your system..." - echo -e "\nCyberPanel is supported on x86_64 based Ubuntu 18.04, Ubuntu 20.04, Ubuntu 20.10, Ubuntu 22.04, Ubuntu 24.04, CentOS 7, CentOS 8, AlmaLinux 8, AlmaLinux 9, AlmaLinux 10, RockyLinux 8, CloudLinux 7, CloudLinux 8, openEuler 20.03, openEuler 22.03...\n" + echo -e "\nCyberPanel is supported on x86_64 based Ubuntu 18.04, Ubuntu 20.04, Ubuntu 20.10, Ubuntu 22.04, Ubuntu 24.04, Ubuntu 24.04.3, Debian 11, Debian 12, Debian 13, CentOS 7, CentOS 8, CentOS 9, RHEL 8, RHEL 9, AlmaLinux 8, AlmaLinux 9, AlmaLinux 10, RockyLinux 8, RockyLinux 9, CloudLinux 7, CloudLinux 8, openEuler 20.03, openEuler 22.03...\n" exit fi @@ -69,10 +77,13 @@ Server_OS_Version=$(grep VERSION_ID /etc/os-release | awk -F[=,] '{print $2}' | echo -e "System: $Server_OS $Server_OS_Version detected...\n" -if [[ $Server_OS = "CloudLinux" ]] || [[ "$Server_OS" = "AlmaLinux" ]] || [[ "$Server_OS" = "RockyLinux" ]] ; then +if [[ $Server_OS = "CloudLinux" ]] || [[ "$Server_OS" = "AlmaLinux" ]] || [[ "$Server_OS" = "RockyLinux" ]] || [[ "$Server_OS" = "RedHat" ]] ; then Server_OS="CentOS" #CloudLinux gives version id like 7.8, 7.9, so cut it to show first number only - #treat CloudLinux, Rocky and Alma as CentOS + #treat CloudLinux, Rocky, Alma and RedHat as CentOS +elif [[ "$Server_OS" = "Debian" ]] ; then + Server_OS="Ubuntu" + #Treat Debian as Ubuntu for package management (both use apt-get) fi if [[ $Server_OS = "CentOS" ]] && [[ "$Server_OS_Version" = "7" ]] ; then @@ -114,6 +125,29 @@ elif [[ $Server_OS = "CentOS" ]] && [[ "$Server_OS_Version" = "8" ]] ; then freshclam -v +elif [[ $Server_OS = "CentOS" ]] && [[ "$Server_OS_Version" = "9" ]] ; then + + setenforce 0 + dnf install -y perl dnf-utils perl-CPAN + dnf --enablerepo=crb install -y perl-IO-stringy + dnf install -y gcc cpp perl bzip2 zip make patch automake rpm-build perl-Archive-Zip perl-Filesys-Df perl-OLE-Storage_Lite perl-Net-CIDR perl-DBI perl-MIME-tools perl-DBD-SQLite binutils glibc-devel perl-Filesys-Df zlib unzip zlib-devel wget mlocate clamav clamav-update "perl(DBD::mysql)" + + # Install unrar for AlmaLinux 9 (using EPEL) + dnf install -y unrar + + export PERL_MM_USE_DEFAULT=1 + curl -L https://cpanmin.us | perl - App::cpanminus + + perl -MCPAN -e 'install Encoding::FixLatin' + perl -MCPAN -e 'install Digest::SHA1' + perl -MCPAN -e 'install Geo::IP' + perl -MCPAN -e 'install Razor2::Client::Agent' + perl -MCPAN -e 'install Sys::Hostname::Long' + perl -MCPAN -e 'install Sys::SigAction' + perl -MCPAN -e 'install Net::Patricia' + + freshclam -v + elif [ "$CLNVERSION" = "ID=\"cloudlinux\"" ]; then setenforce 0 diff --git a/CPScripts/mailscanneruninstaller.sh b/CPScripts/mailscanneruninstaller.sh index 347deae60..eed23ddbb 100644 --- a/CPScripts/mailscanneruninstaller.sh +++ b/CPScripts/mailscanneruninstaller.sh @@ -4,9 +4,15 @@ ### OS Detection Server_OS="" Server_OS_Version="" -if grep -q -E "CentOS Linux 7|CentOS Linux 8" /etc/os-release ; then +if grep -q -E "CentOS Linux 7|CentOS Linux 8|CentOS Stream" /etc/os-release ; then Server_OS="CentOS" -elif grep -q -E "AlmaLinux-8|AlmaLinux-9|AlmaLinux-10" /etc/os-release ; then +elif grep -q "Red Hat Enterprise Linux" /etc/os-release ; then + Server_OS="RedHat" +elif grep -q "AlmaLinux-8" /etc/os-release ; then + Server_OS="AlmaLinux" +elif grep -q "AlmaLinux-9" /etc/os-release ; then + Server_OS="AlmaLinux" +elif grep -q "AlmaLinux-10" /etc/os-release ; then Server_OS="AlmaLinux" elif grep -q -E "CloudLinux 7|CloudLinux 8" /etc/os-release ; then Server_OS="CloudLinux" @@ -14,11 +20,13 @@ elif grep -q -E "Rocky Linux" /etc/os-release ; then Server_OS="RockyLinux" elif grep -q -E "Ubuntu 18.04|Ubuntu 20.04|Ubuntu 20.10|Ubuntu 22.04|Ubuntu 24.04" /etc/os-release ; then Server_OS="Ubuntu" +elif grep -q -E "Debian GNU/Linux 11|Debian GNU/Linux 12|Debian GNU/Linux 13" /etc/os-release ; then + Server_OS="Debian" elif grep -q -E "openEuler 20.03|openEuler 22.03" /etc/os-release ; then Server_OS="openEuler" else echo -e "Unable to detect your system..." - echo -e "\nCyberPanel is supported on x86_64 based Ubuntu 18.04, Ubuntu 20.04, Ubuntu 20.10, Ubuntu 22.04, Ubuntu 24.04, CentOS 7, CentOS 8, AlmaLinux 8, AlmaLinux 9, AlmaLinux 10, RockyLinux 8, CloudLinux 7, CloudLinux 8, openEuler 20.03, openEuler 22.03...\n" + echo -e "\nCyberPanel is supported on x86_64 based Ubuntu 18.04, Ubuntu 20.04, Ubuntu 20.10, Ubuntu 22.04, Ubuntu 24.04, Ubuntu 24.04.3, Debian 11, Debian 12, Debian 13, CentOS 7, CentOS 8, CentOS 9, RHEL 8, RHEL 9, AlmaLinux 8, AlmaLinux 9, AlmaLinux 10, RockyLinux 8, RockyLinux 9, CloudLinux 7, CloudLinux 8, openEuler 20.03, openEuler 22.03...\n" exit fi @@ -26,10 +34,13 @@ Server_OS_Version=$(grep VERSION_ID /etc/os-release | awk -F[=,] '{print $2}' | echo -e "System: $Server_OS $Server_OS_Version detected...\n" -if [[ $Server_OS = "CloudLinux" ]] || [[ "$Server_OS" = "AlmaLinux" ]] || [[ "$Server_OS" = "RockyLinux" ]] ; then +if [[ $Server_OS = "CloudLinux" ]] || [[ "$Server_OS" = "AlmaLinux" ]] || [[ "$Server_OS" = "RockyLinux" ]] || [[ "$Server_OS" = "RedHat" ]] ; then Server_OS="CentOS" #CloudLinux gives version id like 7.8, 7.9, so cut it to show first number only - #treat CloudLinux, Rocky and Alma as CentOS + #treat CloudLinux, Rocky, Alma and RedHat as CentOS +elif [[ "$Server_OS" = "Debian" ]] ; then + Server_OS="Ubuntu" + #Treat Debian as Ubuntu for package management (both use apt-get) fi systemctl stop mailscanner diff --git a/databases/databaseManager.py b/databases/databaseManager.py index 0b0c7617a..60c817a3b 100644 --- a/databases/databaseManager.py +++ b/databases/databaseManager.py @@ -31,6 +31,11 @@ class DatabaseManager: return proc.render() def phpMyAdmin(self, request = None, userID = None): + try: + from plogical.phpmyadmin_utils import ensure_phpmyadmin_signin_bridge + ensure_phpmyadmin_signin_bridge() + except BaseException: + pass template = 'databases/phpMyAdmin.html' proc = httpProc(request, template, None, 'createDatabase') return proc.render() diff --git a/databases/views.py b/databases/views.py index e32f96371..37ec05ff9 100644 --- a/databases/views.py +++ b/databases/views.py @@ -257,6 +257,11 @@ def generateAccess(request): @csrf_exempt def fetchDetailsPHPMYAdmin(request): try: + try: + from plogical.phpmyadmin_utils import ensure_phpmyadmin_signin_bridge + ensure_phpmyadmin_signin_bridge() + except BaseException: + pass userID = request.session['userID'] admin = Administrator.objects.get(id=userID) diff --git a/install.sh b/install.sh index 560d22dbf..b9d348da4 100644 --- a/install.sh +++ b/install.sh @@ -118,8 +118,8 @@ rm -f "$SCRIPT_PATH" "$TEMP_DIR/cyberpanel.sh" "$TEMP_DIR/install.tar.gz" # Ensure temp directory exists and is writable mkdir -p "$TEMP_DIR" 2>/dev/null || true -# For v2.5.5-dev, try to get the cyberpanel.sh from the branch -if [ "$BRANCH_NAME" = "v2.5.5-dev" ] || [ "$BRANCH_NAME" = "stable" ]; then +# Prefer master3395/cyberpanel raw cyberpanel.sh for known branches (includes AlmaLinux 10 etc.) +if [ "$BRANCH_NAME" = "v2.5.5-dev" ] || [ "$BRANCH_NAME" = "stable" ] || [ "$BRANCH_NAME" = "v2.4.5" ]; then # Try to download from the branch-specific URL if curl --silent -o "$SCRIPT_PATH" "https://raw.githubusercontent.com/master3395/cyberpanel/$BRANCH_NAME/cyberpanel.sh" 2>/dev/null; then if [ -f "$SCRIPT_PATH" ] && [ -s "$SCRIPT_PATH" ]; then diff --git a/install/install.py b/install/install.py index 765f8b5a3..f66fc189b 100644 --- a/install/install.py +++ b/install/install.py @@ -236,6 +236,11 @@ class preFlightsChecks: os_info = self.detect_os_info() return os_info['name'] == 'almalinux' and os_info['major_version'] == 9 + def is_almalinux10(self): + """Check if running on AlmaLinux 10 (GH usmannasir/cyberpanel#1736)""" + os_info = self.detect_os_info() + return os_info['name'] == 'almalinux' and os_info['major_version'] == 10 + def is_ubuntu(self): """Check if running on Ubuntu""" os_info = self.detect_os_info() @@ -651,6 +656,35 @@ class preFlightsChecks: except Exception as e: self.stdOut(f"Error applying AlmaLinux 9 MariaDB fixes: {str(e)}", 0) + def fix_almalinux10_mariadb(self): + """EPEL/CRB + MariaDB official repo for AlmaLinux 10 (installer prereqs, GH #1736).""" + if not self.is_almalinux10(): + return + try: + self.stdOut("Applying AlmaLinux 10 MariaDB / repo fixes...", 1) + for cmd, desc in ( + ("dnf install -y epel-release", "EPEL"), + ("dnf config-manager --set-enabled crb 2>/dev/null || dnf config-manager --set-enabled powertools 2>/dev/null || true", "CRB/PowerTools"), + ("dnf install -y htop 2>/dev/null || true", "htop"), + ("dnf install -y libxcrypt-compat 2>/dev/null || true", "libxcrypt-compat for lscpd"), + ): + self.call(cmd, self.distro, desc, desc, 1, 0, os.EX_OSERR) + for cmd, desc in ( + ("dnf config-manager --disable mariadb-maxscale 2>/dev/null || true", "disable maxscale"), + ("rm -f /etc/yum.repos.d/mariadb-maxscale.repo /etc/yum.repos.d/mariadb-maxscale.repo.rpmnew 2>/dev/null || true", "remove maxscale repo files"), + ): + self.call(cmd, self.distro, desc, desc, 1, 0, os.EX_OSERR) + self.stdOut("Setting up MariaDB official repository (11.8 LTS, EL10)...", 1) + cmd = "curl -sS https://downloads.mariadb.com/MariaDB/mariadb_repo_setup | bash -s -- --mariadb-server-version='11.8'" + self.call(cmd, self.distro, cmd, cmd, 1, 0, os.EX_OSERR) + self.call("dnf config-manager --disable mariadb-maxscale 2>/dev/null || true", self.distro, "disable maxscale after setup", "disable maxscale after setup", 1, 0, os.EX_OSERR) + self.stdOut("Installing MariaDB packages from MariaDB.org repo...", 1) + pkgs = "MariaDB-server MariaDB-client MariaDB-backup MariaDB-devel" + self.call(f"dnf install -y --nobest {pkgs}", self.distro, "MariaDB packages", "MariaDB packages", 1, 0, os.EX_OSERR) + self.stdOut("AlmaLinux 10 MariaDB fixes applied successfully", 1) + except Exception as e: + self.stdOut(f"Error applying AlmaLinux 10 MariaDB fixes: {str(e)}", 0) + def install_package_with_fallbacks(self, package_name, dev_package_name=None): """Install package with comprehensive fallback methods for AlmaLinux 9.6+""" try: @@ -826,7 +860,11 @@ class preFlightsChecks: universal_fixes = UniversalOSFixes() if universal_fixes.run_comprehensive_setup(): self.stdOut("Universal OS fixes applied successfully", 1) - return True + os_i = self.detect_os_info() + if os_i.get('name') == 'almalinux' and os_i.get('major_version') == 10: + self.stdOut("AlmaLinux 10: running legacy RHEL integration steps after universal fixes...", 1) + else: + return True else: self.stdOut("Universal OS fixes failed, falling back to legacy fixes...", 1) except ImportError: @@ -842,6 +880,8 @@ class preFlightsChecks: for fix in fixes_needed: if fix == 'mariadb' and self.is_almalinux9(): self.fix_almalinux9_mariadb() + elif fix == 'mariadb' and self.is_almalinux10(): + self.fix_almalinux10_mariadb() elif fix == 'ubuntu_specific' and self.is_ubuntu(): self.fix_ubuntu_specific() elif fix == 'debian_specific' and self.is_debian(): @@ -889,6 +929,11 @@ class preFlightsChecks: if any(distro in content for distro in ['red hat', 'almalinux', 'rocky', 'cloudlinux', 'centos']): return 'rhel9' + # EL10: use rhel9 OLS/custom binaries until el10-specific builds ship (GLIBC-compatible) + if 'version="10.' in content or 'version_id="10.' in content or 'version_id="10"' in content: + if any(distro in content for distro in ['red hat', 'almalinux', 'rocky', 'cloudlinux', 'centos']): + return 'rhel9' + # Default to rhel8 if can't detect (safer default - rhel9 binaries may require GLIBC 2.35) self.stdOut("WARNING: Could not detect platform, defaulting to rhel8", 1) return 'rhel8' @@ -1711,6 +1756,11 @@ module cyberpanel_ols { if result.returncode != 0: logging.InstallLog.writeToFile(f"Failed to setup MariaDB repository: {result.stderr}") return False + try: + import install_utils + install_utils.strip_mariadb_maxscale_apt_repos() + except Exception: + pass command = 'DEBIAN_FRONTEND=noninteractive apt-get update -y' result = subprocess.run(command, shell=True, capture_output=True, universal_newlines=True) @@ -3936,7 +3986,7 @@ $cfg['Servers'][$i]['LogoutURL'] = 'phpmyadminsignin.php?logout'; writeToFile.close() - os.mkdir('/usr/local/CyberCP/public/phpmyadmin/tmp') + os.makedirs('/usr/local/CyberCP/public/phpmyadmin/tmp', exist_ok=True) command = 'chown -R lscpd:lscpd /usr/local/CyberCP/public/phpmyadmin' preFlightsChecks.call(command, self.distro, '[chown -R lscpd:lscpd /usr/local/CyberCP/public/phpmyadmin]', @@ -4946,6 +4996,21 @@ user_query = SELECT email as user, password, 'vmail' as uid, 'vmail' as gid, '/h result = open('/etc/lsb-release', 'r').read() if result.find('22.04') > -1 or result.find('24.04') > -1: lscpdSelection = 'lscpd.0.4.0' + # AlmaLinux/RHEL 9 and 10: lscpd.0.4.0 (el9 binary on el10; origin/v2.4.5) + try: + cl_al_ver = FetchCloudLinuxAlmaVersionVersion() + if cl_al_ver in ('al-93', 'al-100'): + lscpdSelection = 'lscpd.0.4.0' + except Exception: + pass + if os.path.exists('/etc/os-release'): + with open('/etc/os-release', 'r') as f: + osrel = f.read() + if (('VERSION_ID="9"' in osrel or 'VERSION_ID="10"' in osrel or + 'VERSION_ID="9.' in osrel or 'VERSION_ID="10.' in osrel) and + ('AlmaLinux' in osrel or 'Rocky' in osrel or 'Red Hat' in osrel or + 'CentOS' in osrel)): + lscpdSelection = 'lscpd.0.4.0' else: lscpdSelection = 'lscpd.aarch64' @@ -4956,6 +5021,12 @@ user_query = SELECT email as user, password, 'vmail' as uid, 'vmail' as gid, '/h result = open('/etc/lsb-release', 'r').read() if result.find('22.04') > -1 or result.find('24.04') > -1: lscpdSelection = 'lscpd.0.4.0' + try: + cl_al_ver = FetchCloudLinuxAlmaVersionVersion() + if cl_al_ver in ('al-93', 'al-100'): + lscpdSelection = 'lscpd.0.4.0' + except Exception: + pass command = f'cp -f /usr/local/CyberCP/{lscpdSelection} /usr/local/lscp/bin/{lscpdSelection}' @@ -5352,8 +5423,18 @@ user_query = SELECT email as user, password, 'vmail' as uid, 'vmail' as gid, '/h ## + command = 'systemctl daemon-reload' + preFlightsChecks.call(command, self.distro, command, command, 1, 0, os.EX_OSERR) command = 'systemctl start lscpd' - # preFlightsChecks.call(command, self.distro, command, command, 1, 0, os.EX_OSERR) + ret = preFlightsChecks.call(command, self.distro, command, command, 0, 0, os.EX_OSERR) + if ret != 0: + preFlightsChecks.stdOut("LSCPD start failed, reloading systemd and retrying...") + logging.InstallLog.writeToFile("LSCPD first start failed, retrying after daemon-reload") + preFlightsChecks.call('systemctl daemon-reload', self.distro, 'daemon-reload', 'daemon-reload', 1, 0, os.EX_OSERR) + ret = preFlightsChecks.call('systemctl start lscpd', self.distro, 'systemctl start lscpd', 'systemctl start lscpd', 0, 0, os.EX_OSERR) + if ret != 0: + preFlightsChecks.stdOut("[WARNING] LSCPD may not have started. Run: systemctl status lscpd") + logging.InstallLog.writeToFile("[WARNING] LSCPD start failed after retry - run systemctl status lscpd") preFlightsChecks.stdOut("LSCPD Daemon Set!") diff --git a/install/installCyberPanel.py b/install/installCyberPanel.py index 0e8b3e38a..8f04b70a4 100644 --- a/install/installCyberPanel.py +++ b/install/installCyberPanel.py @@ -38,6 +38,7 @@ def get_Ubuntu_code_name(): return "xenial" + # Using shared function from install_utils FetchCloudLinuxAlmaVersionVersion = install_utils.FetchCloudLinuxAlmaVersionVersion @@ -996,9 +997,11 @@ deb [arch=amd64,arm64,ppc64el,s390x signed-by=/usr/share/keyrings/mariadb-keyrin install_utils.writeToFile("Manual MariaDB repository configuration completed.") + # GH #1740: strip broken MaxScale apt entries after mariadb_repo_setup (noble/jammy+) + if get_Ubuntu_release() > 21.00: + install_utils.strip_mariadb_maxscale_apt_repos() - - command = 'DEBIAN_FRONTEND=noninteractive apt-get update -y' + command = 'DEBIAN_FRONTEND=noninteractive apt-get update -y' install_utils.call(command, self.distro, command, command, 1, 1, os.EX_OSERR, True) diff --git a/install/install_utils.py b/install/install_utils.py index 5e856a503..53285a8d8 100644 --- a/install/install_utils.py +++ b/install/install_utils.py @@ -5,6 +5,7 @@ This module contains shared functions used by both install.py and installCyberPa """ import os +import glob import sys import time import logging @@ -676,6 +677,61 @@ def generate_random_string(length=32, include_special=False): return ''.join(secrets.choice(alphabet) for _ in range(length)) + +def strip_mariadb_maxscale_apt_repos(): + """ + MariaDB mariadb_repo_setup adds MaxScale apt repo; Ubuntu noble has no Release (GH usmannasir/cyberpanel#1740). + """ + slist = '/etc/apt/sources.list.d' + try: + if not os.path.isdir(slist): + return + for pattern in ( + 'mariadb-maxscale*.list', 'mariadb-maxscale*.sources', + '*maxscale*.list', '*maxscale*.sources', + ): + for fp in glob.glob(os.path.join(slist, pattern)): + try: + os.remove(fp) + except OSError: + pass + for fp in glob.glob(os.path.join(slist, 'mariadb*.list')): + try: + with open(fp, 'r', encoding='utf-8', errors='replace') as handle: + lines = handle.readlines() + new_lines = [ + ln for ln in lines + if 'maxscale' not in ln.lower() + and 'dlm.mariadb.com/repo/maxscale' not in ln + ] + if new_lines != lines: + with open(fp, 'w', encoding='utf-8') as handle: + handle.writelines(new_lines) + except OSError: + pass + for fp in glob.glob(os.path.join(slist, 'mariadb*.sources')): + try: + with open(fp, 'r', encoding='utf-8', errors='replace') as handle: + content = handle.read() + if 'maxscale' not in content.lower() and 'dlm.mariadb.com/repo/maxscale' not in content: + continue + blocks = content.split('\n\n') + kept = [] + for block in blocks: + bl = block.lower() + if 'maxscale' in bl or 'dlm.mariadb.com/repo/maxscale' in block: + continue + kept.append(block) + new_content = '\n\n'.join(kept) + if new_content.strip() != content.strip(): + with open(fp, 'w', encoding='utf-8') as handle: + handle.write(new_content) + except OSError: + pass + except Exception: + pass + + def writeToFile(message): """ Write a message to the installation log file diff --git a/install/universal_os_fixes.py b/install/universal_os_fixes.py index 1e29d4a04..4384ec4b6 100644 --- a/install/universal_os_fixes.py +++ b/install/universal_os_fixes.py @@ -487,6 +487,12 @@ class UniversalOSFixes: ] subprocess.run(' '.join(cmd), shell=True, check=True) + if os_id in ['ubuntu', 'debian']: + try: + import install_utils + install_utils.strip_mariadb_maxscale_apt_repos() + except Exception: + pass self.logger.info("MariaDB repository setup completed") return True diff --git a/install/venvsetup_modules/03_main_run_pip.sh b/install/venvsetup_modules/03_main_run_pip.sh index bfd89c509..f4ec3c607 100644 --- a/install/venvsetup_modules/03_main_run_pip.sh +++ b/install/venvsetup_modules/03_main_run_pip.sh @@ -68,6 +68,8 @@ rm -rf requirements.txt wget -O requirements.txt https://raw.githubusercontent.com/usmannasir/cyberpanel/1.8.0/requirments.txt # Install packages with robust error handling to prevent broken pipe errors safe_pip_install "pip" "requirements.txt" "--ignore-installed" +# python-dotenv for Django .env loading (upstream f3437739; critical on some AlmaLinux 8 venvs) +pip install python-dotenv 2>/dev/null || echo "⚠️ python-dotenv install skipped or failed" fi if [[ $DEV == "ON" ]] ; then @@ -100,6 +102,7 @@ EOF fi safe_pip_install "pip3.6" "requirements.txt" "--ignore-installed" + pip3.6 install python-dotenv 2>/dev/null || echo "⚠️ python-dotenv (pip3.6) install skipped or failed" fi if [ -f requirements.txt ] && [ -d cyberpanel ] ; then diff --git a/install/venvsetup_modules/04_after_install.sh b/install/venvsetup_modules/04_after_install.sh index fd9bd3e50..b14eb0e5d 100644 --- a/install/venvsetup_modules/04_after_install.sh +++ b/install/venvsetup_modules/04_after_install.sh @@ -2,6 +2,17 @@ # install/venvsetup part 4 – after_install after_install() { +# Robust lscpd restart (origin/v2.4.5 e49ed16f; EL9/10) +_restart_lscpd_safe() { + systemctl daemon-reload 2>/dev/null || true + systemctl restart lscpd 2>/dev/null || true + if ! systemctl is-active --quiet lscpd 2>/dev/null; then + systemctl daemon-reload + systemctl restart lscpd + fi + systemctl restart fastapi_ssh_server 2>/dev/null || true +} + if [ ! -d "/var/lib/php" ]; then mkdir /var/lib/php fi @@ -50,7 +61,8 @@ EOF fi safe_pip_install "pip3.6" "requirements.txt" "--ignore-installed" -systemctl restart lscpd +pip3.6 install python-dotenv 2>/dev/null || echo "⚠️ python-dotenv (after_install) skipped or failed" +_restart_lscpd_safe fi for version in $(ls /usr/local/lsws | grep lsphp); @@ -112,7 +124,7 @@ ELAPSED="$(($SECONDS / 3600)) hrs $((($SECONDS / 60) % 60)) min $(($SECONDS % 60 MYSQLPASSWD=$(cat /etc/cyberpanel/mysqlPassword) echo "$ADMIN_PASS" > /etc/cyberpanel/adminPass /usr/local/CyberPanel/bin/python2 /usr/local/CyberCP/plogical/adminPass.py --password $ADMIN_PASS -systemctl restart lscpd +_restart_lscpd_safe systemctl restart lsws echo "/usr/local/CyberPanel/bin/python2 /usr/local/CyberCP/plogical/adminPass.py --password \"\$@\"" > /usr/bin/adminPass echo "systemctl restart lscpd" >> /usr/bin/adminPass diff --git a/plogical/DockerSites.py b/plogical/DockerSites.py index 47d1307a5..111b23b02 100644 --- a/plogical/DockerSites.py +++ b/plogical/DockerSites.py @@ -308,9 +308,8 @@ extprocessor docker{port} {{ logging.writeToFile("Context already exists, skipping...") return True - # Add proxy context with proper headers for n8n - # NOTE: Do NOT include "RequestHeader set Origin" - OpenLiteSpeed cannot override - # browser Origin headers, which is why NODE_ENV=development is required + # Add proxy context with proper headers for n8n (OLS adds X-Forwarded-*; Origin set for n8n) + # NOTE: OpenLiteSpeed cannot override browser Origin headers; NODE_ENV=development may be required proxy_context = f''' # N8N Proxy Configuration @@ -321,10 +320,7 @@ context / {{ websocket 1 extraHeaders << bool: + """ + Copy plogical/phpmyadminsignin.php into the public phpMyAdmin tree if missing, + ensure tmp/ exists, and fix ownership for lscpd. + Returns True if the sign-in file is present afterward. + """ + dst = os.path.join(PMA_DIR, SIGNIN_NAME) + try: + if not os.path.isdir(PMA_DIR): + return False + if not os.path.isfile(SIGNIN_SRC): + logging.writeToFile('phpmyadmin_utils: source signin missing at ' + SIGNIN_SRC) + return os.path.isfile(dst) + need_copy = (not os.path.isfile(dst)) or os.path.getsize(dst) < 32 + if need_copy: + shutil.copy2(SIGNIN_SRC, dst) + tmp_dir = os.path.join(PMA_DIR, 'tmp') + os.makedirs(tmp_dir, exist_ok=True) + try: + from plogical.processUtilities import ProcessUtilities + ProcessUtilities.executioner('chown -R lscpd:lscpd ' + PMA_DIR) + except Exception as ch_ex: + logging.writeToFile('phpmyadmin_utils: chown skipped or failed (non-fatal): ' + str(ch_ex)) + return os.path.isfile(dst) + except Exception as ex: + logging.writeToFile('phpmyadmin_utils: ensure_phpmyadmin_signin_bridge failed: ' + str(ex)) + return os.path.isfile(dst) diff --git a/plogical/upgrade.py b/plogical/upgrade.py index eae86d2cb..f728108ef 100644 --- a/plogical/upgrade.py +++ b/plogical/upgrade.py @@ -1388,7 +1388,7 @@ $cfg['Servers'][$i]['port'] = '3306'; writeToFile.writelines("$cfg['TempDir'] = '/usr/local/CyberCP/public/phpmyadmin/tmp';\n") writeToFile.close() - os.mkdir('/usr/local/CyberCP/public/phpmyadmin/tmp') + os.makedirs('/usr/local/CyberCP/public/phpmyadmin/tmp', exist_ok=True) if saved_signon and os.path.isfile(tmp_signon): shutil.copy2(tmp_signon, os.path.join(pma_dir, 'phpmyadminsignin.php')) @@ -1417,6 +1417,12 @@ $cfg['Servers'][$i]['port'] = '3306'; command = 'chown -R lscpd:lscpd /usr/local/CyberCP/public/phpmyadmin/tmp' Upgrade.executioner_silent(command, 'chown phpMyAdmin tmp') + try: + from plogical.phpmyadmin_utils import ensure_phpmyadmin_signin_bridge + ensure_phpmyadmin_signin_bridge() + except Exception: + pass + os.chdir(cwd) except Exception as e: diff --git a/pluginHolder/management/__init__.py b/pluginHolder/management/__init__.py new file mode 100644 index 000000000..792d60054 --- /dev/null +++ b/pluginHolder/management/__init__.py @@ -0,0 +1 @@ +# diff --git a/pluginHolder/management/commands/__init__.py b/pluginHolder/management/commands/__init__.py new file mode 100644 index 000000000..792d60054 --- /dev/null +++ b/pluginHolder/management/commands/__init__.py @@ -0,0 +1 @@ +# diff --git a/pluginHolder/management/commands/refresh_plugin_store_cache.py b/pluginHolder/management/commands/refresh_plugin_store_cache.py new file mode 100644 index 000000000..e2a0fa2bd --- /dev/null +++ b/pluginHolder/management/commands/refresh_plugin_store_cache.py @@ -0,0 +1,102 @@ +# -*- coding: utf-8 -*- +import os +import time + +from django.core.management.base import BaseCommand + + +class Command(BaseCommand): + help = "Refresh CyberPanel plugin store cache (hourly scheduler)." + + def add_arguments(self, parser): + parser.add_argument( + "--force", + action="store_true", + help="Refresh even if cache is not expired.", + ) + parser.add_argument( + "--stale-lock-seconds", + type=int, + default=900, + help="Remove the cache-refresh lock if it is older than this many seconds.", + ) + + def handle(self, *args, **options): + force = bool(options.get("force", False)) + stale_lock_seconds = int(options.get("stale_lock_seconds", 900)) + + try: + from pluginHolder import views as plugin_views + except Exception as e: + # Avoid printing secrets; just show a minimal message. + self.stderr.write("Failed to import pluginHolder views for cache refresh.") + return 1 + + # Only refresh when needed (unless --force is used). + try: + cache_expiry_timestamp, _ = plugin_views._get_cache_expiry_time() + cache_expired = plugin_views._is_cache_expired(cache_expiry_timestamp) + except Exception: + cache_expired = True + + if not force and cache_expiry_timestamp and not cache_expired: + self.stdout.write("Plugin store cache is still fresh; no refresh needed.") + return 0 + + lock_path = plugin_views.PLUGIN_STORE_REFRESH_LOCK_FILE + try: + plugin_views._ensure_cache_dir() + except Exception: + pass + + # Remove stale lock left behind by a crashed/aborted refresh. + if os.path.exists(lock_path): + try: + age_s = time.time() - os.path.getmtime(lock_path) + if age_s > stale_lock_seconds: + os.remove(lock_path) + try: + plugin_views.logging.writeToFile( + f"Management refresh: removed stale plugin store refresh lock (age: {age_s:.0f}s)" + ) + except Exception: + pass + except Exception: + pass + + # Acquire lock to avoid stampedes when multiple instances refresh. + try: + fd = os.open(lock_path, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o644) + with os.fdopen(fd, "w") as f: + f.write(str(os.getpid())) + except FileExistsError: + self.stdout.write("Plugin store refresh skipped: lock already exists.") + return 0 + except Exception: + self.stderr.write("Plugin store refresh failed: could not acquire refresh lock.") + return 1 + + try: + plugins = plugin_views._fetch_plugins_from_github() + if not plugins: + self.stdout.write("Plugin store refresh fetched 0 plugins; cache not updated.") + return 0 + + plugin_views._save_plugins_cache(plugins) + self.stdout.write(f"Plugin store cache refreshed successfully. plugins={len(plugins)}") + return 0 + except Exception as e: + # Log error summary server-side; don't leak internal exception details to stdout. + try: + plugin_views.logging.writeToFile(f"Plugin store cache refresh failed: {str(e)}") + except Exception: + pass + self.stderr.write("Plugin store cache refresh failed. Check error logs.") + return 1 + finally: + try: + if os.path.exists(lock_path): + os.remove(lock_path) + except Exception: + pass + diff --git a/pluginHolder/models.py b/pluginHolder/models.py index 4e6a8e76d..0043b69ca 100644 --- a/pluginHolder/models.py +++ b/pluginHolder/models.py @@ -3,4 +3,22 @@ from django.db import models -# Create your models here. + +class PluginActivationKey(models.Model): + """ + Optional ORM mirror for activation keys persisted in MariaDB. + Runtime code uses raw SQL CREATE TABLE IF NOT EXISTS for migration safety. + """ + plugin_name = models.CharField(max_length=191) + user_identity = models.CharField(max_length=191) + activation_key_hash = models.CharField(max_length=64) + key_last4 = models.CharField(max_length=4, blank=True, default='') + source = models.CharField(max_length=50, blank=True, default='manual') + is_active = models.BooleanField(default=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + managed = False + db_table = 'plugin_activation_keys' + unique_together = (('plugin_name', 'user_identity'),) diff --git a/pluginHolder/plugin_access.py b/pluginHolder/plugin_access.py index f460e0768..9dbacef12 100644 --- a/pluginHolder/plugin_access.py +++ b/pluginHolder/plugin_access.py @@ -5,7 +5,170 @@ Checks if user has access to paid plugins """ from .patreon_verifier import PatreonVerifier -import logging +import hashlib +from django.db import connection +from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter as logging + + +def _normalize_identity(value): + if not value: + return '' + return str(value).strip().lower() + + +def _hash_activation_key(raw_key): + return hashlib.sha256(raw_key.encode('utf-8')).hexdigest() + + +def _ensure_activation_table(): + """ + Create table on-demand so upgrade paths without Django migrations are safe. + """ + sql = """ + CREATE TABLE IF NOT EXISTS plugin_activation_keys ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + plugin_name VARCHAR(191) NOT NULL, + user_identity VARCHAR(191) NOT NULL, + activation_key_hash CHAR(64) NOT NULL, + key_last4 VARCHAR(4) NOT NULL DEFAULT '', + source VARCHAR(50) NOT NULL DEFAULT 'manual', + is_active TINYINT(1) NOT NULL DEFAULT 1, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id), + UNIQUE KEY uniq_plugin_identity (plugin_name, user_identity), + KEY idx_identity (user_identity), + KEY idx_plugin (plugin_name) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + """ + with connection.cursor() as cursor: + cursor.execute(sql) + + +def save_activation_key(plugin_name, user_identity, activation_key, source='manual'): + """ + Persist activation key hash in MariaDB (upsert by plugin_name + user_identity). + """ + plugin_name = _normalize_identity(plugin_name) + user_identity = _normalize_identity(user_identity) + activation_key = str(activation_key or '').strip() + if not plugin_name or not user_identity or not activation_key: + return False + + try: + _ensure_activation_table() + key_hash = _hash_activation_key(activation_key) + key_last4 = activation_key[-4:] if len(activation_key) >= 4 else activation_key + with connection.cursor() as cursor: + cursor.execute( + """ + INSERT INTO plugin_activation_keys + (plugin_name, user_identity, activation_key_hash, key_last4, source, is_active) + VALUES (%s, %s, %s, %s, %s, 1) + ON DUPLICATE KEY UPDATE + activation_key_hash = VALUES(activation_key_hash), + key_last4 = VALUES(key_last4), + source = VALUES(source), + is_active = 1 + """, + [plugin_name, user_identity, key_hash, key_last4, source] + ) + return True + except Exception as e: + logging.writeToFile('plugin_access.save_activation_key failed: %s' % str(e)) + return False + + +def has_saved_activation(plugin_name, user_identity): + plugin_name = _normalize_identity(plugin_name) + user_identity = _normalize_identity(user_identity) + if not plugin_name or not user_identity: + return False + + try: + _ensure_activation_table() + with connection.cursor() as cursor: + cursor.execute( + """ + SELECT 1 + FROM plugin_activation_keys + WHERE plugin_name = %s + AND user_identity = %s + AND is_active = 1 + LIMIT 1 + """, + [plugin_name, user_identity] + ) + return cursor.fetchone() is not None + except Exception as e: + logging.writeToFile('plugin_access.has_saved_activation failed: %s' % str(e)) + return False + + +def verify_saved_activation_key(plugin_name, user_identity, activation_key): + plugin_name = _normalize_identity(plugin_name) + user_identity = _normalize_identity(user_identity) + activation_key = str(activation_key or '').strip() + if not plugin_name or not user_identity or not activation_key: + return False + + try: + _ensure_activation_table() + key_hash = _hash_activation_key(activation_key) + with connection.cursor() as cursor: + cursor.execute( + """ + SELECT 1 + FROM plugin_activation_keys + WHERE plugin_name = %s + AND user_identity = %s + AND activation_key_hash = %s + AND is_active = 1 + LIMIT 1 + """, + [plugin_name, user_identity, key_hash] + ) + return cursor.fetchone() is not None + except Exception as e: + logging.writeToFile('plugin_access.verify_saved_activation_key failed: %s' % str(e)) + return False + + +def _resolve_identity_for_request(request): + """ + CyberPanel often authenticates via session userID (not Django auth user). + Prefer Administrator email when available, otherwise username. + """ + candidates = [] + try: + if getattr(request, 'user', None) and request.user.is_authenticated: + u = request.user + email = getattr(u, 'email', None) or '' + if email: + candidates.append(email) + uname = getattr(u, 'username', None) or '' + if uname: + candidates.append(uname) + except Exception: + pass + try: + uid = request.session.get('userID') if hasattr(request, 'session') else None + if uid: + from loginSystem.models import Administrator + admin = Administrator.objects.filter(pk=uid).only('email', 'userName').first() + if admin: + if getattr(admin, 'email', '') and str(admin.email).lower() != 'none': + candidates.append(str(admin.email)) + if getattr(admin, 'userName', ''): + candidates.append(str(admin.userName)) + except Exception: + pass + for item in candidates: + item = (item or '').strip() + if item: + return item.lower() + return '' + def check_plugin_access(request, plugin_name, plugin_meta=None): """ @@ -40,21 +203,7 @@ def check_plugin_access(request, plugin_name, plugin_meta=None): if not plugin_meta or not plugin_meta.get('is_paid', False): return default_response - # Plugin is paid - check Patreon membership - if not request.user or not request.user.is_authenticated: - return { - 'has_access': False, - 'is_paid': True, - 'message': 'Please log in to access this plugin', - 'patreon_url': plugin_meta.get('patreon_url') - } - - # Get user email - user_email = getattr(request.user, 'email', None) - if not user_email: - # Try to get from username or other fields - user_email = getattr(request.user, 'username', '') - + user_email = _resolve_identity_for_request(request) if not user_email: return { 'has_access': False, @@ -63,7 +212,16 @@ def check_plugin_access(request, plugin_name, plugin_meta=None): 'patreon_url': plugin_meta.get('patreon_url') } - # Check Patreon membership + # First allow DB-backed activation keys (survives upgrades) + if has_saved_activation(plugin_name, user_email): + return { + 'has_access': True, + 'is_paid': True, + 'message': 'Access granted', + 'patreon_url': None + } + + # Fallback to Patreon membership verifier = PatreonVerifier() has_membership = verifier.check_membership_cached(user_email) diff --git a/pluginHolder/templates/pluginHolder/plugins.html b/pluginHolder/templates/pluginHolder/plugins.html index ee7d05484..c10982332 100644 --- a/pluginHolder/templates/pluginHolder/plugins.html +++ b/pluginHolder/templates/pluginHolder/plugins.html @@ -1695,8 +1695,8 @@ {% trans "Plugin Name" %} - {% trans "New Version" %} {% trans "Your Version" %} + {% trans "New Version" %} {% trans "Date" %} {% trans "Status / Action" %} @@ -1737,7 +1737,7 @@ {% trans "Cache Information:" %} {% trans "Plugin store data is cached for 1 hour to improve performance and reduce GitHub API rate limits. New plugins may take up to 1 hour to appear after being published." %} {% if cache_expiry_timestamp %} -
{% trans "Next cache update:" %} {% trans "Calculating..." %} +
{% trans "Next cache update:" %} {% trans "Calculating..." %} {% endif %}

@@ -1850,7 +1850,7 @@ +""" % json.dumps(plugin_name) + if '' in body: + body = body.replace('', hook_script + '') + else: + body += hook_script + response.content = body.encode('utf-8') + return response + except Exception: + return response + + def plugin_help(request, plugin_name): """Plugin-specific help page - shows plugin information, version history, and help content""" mailUtilities.checkHome() @@ -2322,10 +2602,11 @@ def plugin_help(request, plugin_name): return proc.render() @csrf_exempt -@require_http_methods(["GET"]) +@require_http_methods(["GET", "POST"]) def check_plugin_subscription(request, plugin_name): """ - API endpoint to check if user has Patreon subscription for a paid plugin + API endpoint to check plugin premium access. + Supports optional activation key save/verify to persist entitlement in MariaDB. Args: request: Django request object @@ -2342,21 +2623,56 @@ def check_plugin_subscription(request, plugin_name): try: if not user_can_manage_plugins(request): return deny_plugin_manage_json_response(request) - # Check if user is authenticated - if not request.user or not request.user.is_authenticated: + + # Load plugin metadata + from .plugin_access import ( + check_plugin_access, + _load_plugin_meta, + save_activation_key, + verify_saved_activation_key + ) + + plugin_meta = _load_plugin_meta(plugin_name) + + user_email = _resolve_logged_in_plugin_identity(request) + if not user_email: return JsonResponse({ 'success': False, 'has_access': False, 'is_paid': False, - 'message': 'Please log in to check subscription status', + 'message': 'Unable to determine user identity', 'patreon_url': None - }, status=401) - - # Load plugin metadata - from .plugin_access import check_plugin_access, _load_plugin_meta - - plugin_meta = _load_plugin_meta(plugin_name) - + }, status=400) + activation_key = '' + if request.method == 'POST': + try: + payload = json.loads(request.body.decode('utf-8') or '{}') + except Exception: + payload = {} + activation_key = str(payload.get('activation_key', '')).strip() + if activation_key and user_email: + # If key is already known for this user/plugin -> immediate access + if verify_saved_activation_key(plugin_name, user_email, activation_key): + return JsonResponse({ + 'success': True, + 'has_access': True, + 'is_paid': bool(plugin_meta and plugin_meta.get('is_paid', False)), + 'message': 'Access granted', + 'patreon_url': None, + 'activation_saved': True + }) + # Save submitted key as persistent entitlement (admin-managed workflow) + saved = save_activation_key(plugin_name, user_email, activation_key, source='plugin_settings') + if saved: + return JsonResponse({ + 'success': True, + 'has_access': True, + 'is_paid': bool(plugin_meta and plugin_meta.get('is_paid', False)), + 'message': 'Activation key saved', + 'patreon_url': None, + 'activation_saved': True + }) + # Check access access_result = check_plugin_access(request, plugin_name, plugin_meta) @@ -2365,7 +2681,8 @@ def check_plugin_subscription(request, plugin_name): 'has_access': access_result['has_access'], 'is_paid': access_result['is_paid'], 'message': access_result['message'], - 'patreon_url': access_result.get('patreon_url') + 'patreon_url': access_result.get('patreon_url'), + 'activation_saved': access_result['has_access'] and access_result['is_paid'] }) except Exception as e: @@ -2374,6 +2691,40 @@ def check_plugin_subscription(request, plugin_name): 'success': False, 'has_access': False, 'is_paid': False, - 'message': f'Error checking subscription: {str(e)}', + 'message': 'Error checking subscription', 'patreon_url': None }, status=500) + + +@csrf_exempt +@require_http_methods(["POST"]) +def store_plugin_activation_key(request, plugin_name): + """ + Store activation key in MariaDB so upgrades do not lose premium entitlement. + """ + try: + if not user_can_manage_plugins(request): + return deny_plugin_manage_json_response(request) + + try: + payload = json.loads(request.body.decode('utf-8') or '{}') + except Exception: + payload = {} + + activation_key = str(payload.get('activation_key', '')).strip() + if not activation_key: + return JsonResponse({'success': False, 'message': 'activation_key is required'}, status=400) + + user_email = _resolve_logged_in_plugin_identity(request) + if not user_email: + return JsonResponse({'success': False, 'message': 'Unable to determine user identity'}, status=400) + + from .plugin_access import save_activation_key + ok = save_activation_key(plugin_name, user_email, activation_key, source='api') + if not ok: + return JsonResponse({'success': False, 'message': 'Failed to persist activation key'}, status=500) + + return JsonResponse({'success': True, 'message': 'Activation key saved'}) + except Exception as e: + logging.writeToFile('store_plugin_activation_key failed for %s: %s' % (plugin_name, str(e))) + return JsonResponse({'success': False, 'message': 'Internal server error'}, status=500) diff --git a/pluginInstaller/pluginInstaller.py b/pluginInstaller/pluginInstaller.py index 9b0b98b09..0041fe070 100644 --- a/pluginInstaller/pluginInstaller.py +++ b/pluginInstaller/pluginInstaller.py @@ -58,6 +58,35 @@ class pluginInstaller: pluginHome = '/usr/local/CyberCP/' + pluginName return os.path.exists(pluginHome + '/enable_migrations') + @staticmethod + def shouldApplyPluginDatabaseMigrations(pluginName: str) -> bool: + """ + Run Django migrations when the plugin opts in (enable_migrations file) + or when a migrations/ package with real migration modules is shipped. + """ + if pluginInstaller.migrationsEnabled(pluginName): + return True + mig_dir = '/usr/local/CyberCP/' + pluginName + '/migrations' + if not os.path.isdir(mig_dir): + return False + try: + for fn in os.listdir(mig_dir): + if fn.endswith('.py') and fn != '__init__.py': + return True + except OSError: + return False + return False + + @staticmethod + def _manage_python_executable(): + for candidate in ('/usr/local/CyberCP/bin/python', '/usr/local/CyberCP/bin/python3'): + try: + if os.path.isfile(candidate) and os.access(candidate, os.X_OK): + return candidate + except OSError: + continue + return 'python3' + @staticmethod def _write_lines_to_protected_file(target_path, lines): """ @@ -338,12 +367,31 @@ class pluginInstaller: @staticmethod def installMigrations(pluginName): currentDir = os.getcwd() - os.chdir('/usr/local/CyberCP') - command = "python3 /usr/local/CyberCP/manage.py makemigrations %s" % pluginName - subprocess.call(shlex.split(command)) - command = "python3 /usr/local/CyberCP/manage.py migrate %s" % pluginName - subprocess.call(shlex.split(command)) - os.chdir(currentDir) + manage_py = '/usr/local/CyberCP/manage.py' + py = pluginInstaller._manage_python_executable() + try: + os.chdir('/usr/local/CyberCP') + mk = subprocess.call( + [py, manage_py, 'makemigrations', pluginName], + stdin=subprocess.DEVNULL, + ) + if mk != 0: + pluginInstaller.stdOut( + 'makemigrations %s exited %s (ok if no model changes)' % (pluginName, mk) + ) + mig = subprocess.call( + [py, manage_py, 'migrate', pluginName, '--noinput'], + stdin=subprocess.DEVNULL, + ) + if mig != 0: + pluginInstaller.stdOut( + 'migrate %s exited %s — check CyberPanel logs and DB permissions' % (pluginName, mig) + ) + finally: + try: + os.chdir(currentDir) + except OSError: + pass @staticmethod @@ -427,12 +475,14 @@ class pluginInstaller: ## - if pluginInstaller.migrationsEnabled(pluginName): - pluginInstaller.stdOut('Running Migrations..') + if pluginInstaller.shouldApplyPluginDatabaseMigrations(pluginName): + pluginInstaller.stdOut('Running database migrations for %s..' % pluginName) pluginInstaller.installMigrations(pluginName) - pluginInstaller.stdOut('Migrations Completed..') + pluginInstaller.stdOut('Database migrations step finished for %s.' % pluginName) else: - pluginInstaller.stdOut('Migrations not enabled, add file \'enable_migrations\' to plugin to enable') + pluginInstaller.stdOut( + 'No plugin migrations to apply (no migrations/ package and no enable_migrations marker).' + ) ## @@ -625,8 +675,11 @@ class pluginInstaller: def removeMigrations(pluginName): currentDir = os.getcwd() os.chdir('/usr/local/CyberCP') - command = "python3 /usr/local/CyberCP/manage.py migrate %s zero" % pluginName - subprocess.call(shlex.split(command)) + py = pluginInstaller._manage_python_executable() + subprocess.call( + [py, '/usr/local/CyberCP/manage.py', 'migrate', pluginName, 'zero', '--noinput'], + stdin=subprocess.DEVNULL, + ) os.chdir(currentDir) @staticmethod @@ -640,12 +693,12 @@ class pluginInstaller: ## - if pluginInstaller.migrationsEnabled(pluginName): - pluginInstaller.stdOut('Removing migrations..') + if pluginInstaller.shouldApplyPluginDatabaseMigrations(pluginName): + pluginInstaller.stdOut('Reverting database migrations for %s..' % pluginName) pluginInstaller.removeMigrations(pluginName) - pluginInstaller.stdOut('Migrations removed..') + pluginInstaller.stdOut('Database migrations reverted for %s.' % pluginName) else: - pluginInstaller.stdOut('Migrations not enabled, add file \'enable_migrations\' to plugin to enable') + pluginInstaller.stdOut('Skipping migrate zero (no migrations package / marker).') ## @@ -711,4 +764,4 @@ def main(): pluginInstaller.removePlugin(args.pluginName) if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/public/phpmyadmin/phpmyadminsignin.php b/public/phpmyadmin/phpmyadminsignin.php new file mode 100644 index 000000000..1ac2461e9 --- /dev/null +++ b/public/phpmyadmin/phpmyadminsignin.php @@ -0,0 +1,68 @@ +'; + echo ''; + echo ''; + echo ''; + echo ''; + + } else if (isset($_POST['logout']) || isset($_GET['logout'])) { + session_name(PMA_SIGNON_SESSIONNAME); + @session_start(); + $_SESSION = array(); + $params = session_get_cookie_params(); + setcookie(session_name(), '', time() - 86400, $params["path"], $params["domain"], $params["secure"], $params["httponly"]); + session_destroy(); + header('Location: /base/'); + exit; + } else if (isset($_POST['password'])) { + + session_name(PMA_SIGNON_SESSIONNAME); + @session_start(); + + $username = htmlspecialchars($_POST['username'], ENT_QUOTES, 'UTF-8'); + $password = $_POST['password']; + $host = isset($_POST['host']) ? trim($_POST['host']) : '127.0.0.1'; + if ($host === 'localhost') { $host = '127.0.0.1'; } + + $_SESSION['PMA_single_signon_user'] = $username; + $_SESSION['PMA_single_signon_password'] = $password; + $_SESSION['PMA_single_signon_host'] = $host; + + @session_write_close(); + + header('Location: /phpmyadmin/index.php?server=' . PMA_SIGNON_INDEX); + } +} catch (Exception $e) { + echo 'Caught exception: ', $e->getMessage(), "\n"; + $params = session_get_cookie_params(); + setcookie(session_name(), '', time() - 86400, $params["path"], $params["domain"], $params["secure"], $params["httponly"]); + session_destroy(); + header('Location: /dataBases/phpMyAdmin'); + return; +}