diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 497829761..000000000 Binary files a/.DS_Store and /dev/null differ diff --git a/.github/scripts/ci-validate-upgrade.sh b/.github/scripts/ci-validate-upgrade.sh new file mode 100755 index 000000000..e92d89157 --- /dev/null +++ b/.github/scripts/ci-validate-upgrade.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +# Run inside CI Docker container (or repo root). Validates upgrade loader + modules and key files. +set -e + +echo "=== Shell syntax check ===" +for f in cyberpanel_upgrade.sh preUpgrade.sh fix-phpmyadmin.sh cyberpanel.sh cyberpanel_utility.sh; do + [ ! -f "$f" ] && continue + bash -n "$f" || { echo "FAIL (syntax): $f"; exit 1; } + echo "OK $f" +done +test -f preUpgrade.sh && test -f cyberpanel_upgrade.sh || { echo "Missing required scripts"; exit 1; } + +echo "=== Key files ===" +for f in preUpgrade.sh cyberpanel_upgrade.sh plogical/upgrade.py install/install.py; do + test -f "$f" || { echo "Missing: $f"; exit 1; } +done +grep -q 'BRANCH_NAME' preUpgrade.sh || exit 1 + +if [ -d upgrade_modules ]; then + for n in 00_common 01_variables 02_checks 03_mariadb 04_git_url 05_repository 06_components 07_branch_input 08_main_upgrade 09_sync 10_post_tweak 11_display_final; do + test -f "upgrade_modules/${n}.sh" || { echo "Missing: upgrade_modules/${n}.sh"; exit 1; } + done + grep -q 'Branch_Check\|Branch_Name' upgrade_modules/00_common.sh upgrade_modules/02_checks.sh || exit 1 + grep -q 'upgrade_modules\|Pre_Upgrade_Branch_Input\|Set_Default_Variables' cyberpanel_upgrade.sh || exit 1 + for f in upgrade_modules/*.sh; do + [ -f "$f" ] || continue + bash -n "$f" || { echo "FAIL (syntax): $f"; exit 1; } + done +else + grep -q 'Branch_Name\|download_install_phpmyadmin\|Branch_Check' cyberpanel_upgrade.sh || exit 1 +fi + +echo "Done." diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..89b392867 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,99 @@ +# Lightweight CI for v2.5.5-dev (free for public repos). +# Validates shell syntax, Python version fetcher, key files, and upgrade script. +# Pinned to ubuntu-22.04. Multi-OS validation: run locally with +# for img in almalinux:8 centos:7 debian:12 ubuntu:24.04; do docker run --rm -v "$PWD:/repo:ro" -w /repo $img bash /repo/.github/scripts/ci-validate-upgrade.sh; done +name: CI + +on: + push: + branches: [v2.5.5-dev, stable, master] + pull_request: + branches: [v2.5.5-dev, stable, master] + +jobs: + validate-shell: + name: Validate shell scripts + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + - name: Syntax check shell scripts + run: | + for f in cyberpanel_upgrade.sh preUpgrade.sh fix-phpmyadmin.sh cyberpanel.sh cyberpanel_utility.sh; do + [ ! -f "$f" ] && continue + bash -n "$f" || { echo "FAIL (syntax): $f"; exit 1; } + echo "OK $f" + done + test -f preUpgrade.sh && test -f cyberpanel_upgrade.sh || { echo "Missing required scripts"; exit 1; } + if [ -d upgrade_modules ]; then + echo "=== Upgrade modules syntax ===" + for f in upgrade_modules/*.sh; do + [ -f "$f" ] || continue + bash -n "$f" || { echo "FAIL (syntax): $f"; exit 1; } + echo "OK $f" + done + fi + echo "All shell scripts passed syntax check" + + validate-python: + name: Validate Python (version fetcher) + runs-on: ubuntu-22.04 + timeout-minutes: 2 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.10' + - name: Install requests + run: pip install requests + - name: Run version fetcher (phpMyAdmin / SnappyMail) + run: | + if [ ! -f plogical/versionFetcher.py ]; then + echo "Skipping (plogical/versionFetcher.py not present on this branch)" + exit 0 + fi + PYTHONPATH=. python3 -c " + from plogical.versionFetcher import get_latest_phpmyadmin_version, get_latest_snappymail_version + pma = get_latest_phpmyadmin_version() + snappy = get_latest_snappymail_version() + assert pma and len(pma) >= 5, 'phpMyAdmin version invalid' + assert snappy and len(snappy) >= 3, 'SnappyMail version invalid' + print('phpMyAdmin:', pma, 'SnappyMail:', snappy) + " + + smoke-key-files: + name: Key files present + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + - name: Check key install/upgrade files exist + run: | + for f in preUpgrade.sh cyberpanel_upgrade.sh plogical/upgrade.py install/install.py; do + test -f "$f" || { echo "Missing: $f"; exit 1; } + done + test -f plogical/versionFetcher.py && echo "versionFetcher.py present" || echo "versionFetcher.py optional (not on all branches)" + grep -q 'BRANCH_NAME' preUpgrade.sh || exit 1 + if [ -d upgrade_modules ]; then + for n in 00_common 01_variables 02_checks 03_mariadb 04_git_url 05_repository 06_components 07_branch_input 08_main_upgrade 09_sync 10_post_tweak 11_display_final; do + test -f "upgrade_modules/${n}.sh" || { echo "Missing: upgrade_modules/${n}.sh"; exit 1; } + done + grep -q 'Branch_Check\|Branch_Name' upgrade_modules/00_common.sh upgrade_modules/02_checks.sh || exit 1 + grep -q 'upgrade_modules\|Pre_Upgrade_Branch_Input\|Set_Default_Variables' cyberpanel_upgrade.sh || exit 1 + else + grep -q 'Branch_Name\|download_install_phpmyadmin\|Branch_Check' cyberpanel_upgrade.sh || exit 1 + fi + echo "Key files OK" + + # Run upgrade validation script (same checks as other jobs; no Docker to avoid runner issues). + validate-upgrade-script: + name: Validate upgrade script + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + - name: Run ci-validate-upgrade.sh + run: | + set -e + if [ ! -f .github/scripts/ci-validate-upgrade.sh ]; then + echo "Missing .github/scripts/ci-validate-upgrade.sh" + exit 1 + fi + bash .github/scripts/ci-validate-upgrade.sh diff --git a/CPScripts/mailscannerinstaller.sh b/CPScripts/mailscannerinstaller.sh index 76a20e02d..63925c14d 100644 --- a/CPScripts/mailscannerinstaller.sh +++ b/CPScripts/mailscannerinstaller.sh @@ -55,13 +55,13 @@ elif grep -q -E "CloudLinux 7|CloudLinux 8" /etc/os-release ; then Server_OS="CloudLinux" 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" /etc/os-release ; then +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 "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, CentOS 7, CentOS 8, AlmaLinux 8, 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, CentOS 7, CentOS 8, AlmaLinux 8, AlmaLinux 9, AlmaLinux 10, RockyLinux 8, CloudLinux 7, CloudLinux 8, openEuler 20.03, openEuler 22.03...\n" exit fi diff --git a/CPScripts/mailscanneruninstaller.sh b/CPScripts/mailscanneruninstaller.sh index 9eebad670..347deae60 100644 --- a/CPScripts/mailscanneruninstaller.sh +++ b/CPScripts/mailscanneruninstaller.sh @@ -12,13 +12,13 @@ elif grep -q -E "CloudLinux 7|CloudLinux 8" /etc/os-release ; then Server_OS="CloudLinux" 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" /etc/os-release ; then +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 "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, CentOS 7, CentOS 8, AlmaLinux 8, 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, CentOS 7, CentOS 8, AlmaLinux 8, AlmaLinux 9, AlmaLinux 10, RockyLinux 8, CloudLinux 7, CloudLinux 8, openEuler 20.03, openEuler 22.03...\n" exit fi diff --git a/CPScripts/migrate_rainloop_to_snappymail.sh b/CPScripts/migrate_rainloop_to_snappymail.sh new file mode 100644 index 000000000..1edb5cd3f --- /dev/null +++ b/CPScripts/migrate_rainloop_to_snappymail.sh @@ -0,0 +1,89 @@ +#!/bin/bash +# Migrate RainLoop to SnappyMail on live server: rename public folder and/or install SnappyMail if missing, migrate data, fix paths, restart, test. +# Run as root: bash /usr/local/CyberCP/CPScripts/migrate_rainloop_to_snappymail.sh +set -e +LOG="/var/log/cyberpanel_upgrade_debug.log" +log() { echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] $*" | tee -a "$LOG"; } +SNAPPY_VERSION="${SNAPPY_VERSION:-2.38.2}" + +log "=== RainLoop -> SnappyMail migration ===" + +# 1) Ensure public/snappymail exists: rename rainloop -> snappymail OR download and install SnappyMail +if [ -d "/usr/local/CyberCP/public/rainloop" ] && [ ! -d "/usr/local/CyberCP/public/snappymail" ]; then + log "Renaming public/rainloop to public/snappymail..." + mv /usr/local/CyberCP/public/rainloop /usr/local/CyberCP/public/snappymail + log "Renamed public/rainloop -> public/snappymail" + if [ -f "/usr/local/CyberCP/public/snappymail/include.php" ]; then + sed -i 's|/usr/local/lscp/cyberpanel/rainloop/data|/usr/local/lscp/cyberpanel/snappymail/data|g' /usr/local/CyberCP/public/snappymail/include.php + log "Updated include.php data path" + fi + for inc in /usr/local/CyberCP/public/snappymail/snappymail/v/*/include.php /usr/local/CyberCP/public/snappymail/rainloop/v/*/include.php; do + [ -f "$inc" ] && sed -i 's|/usr/local/lscp/cyberpanel/rainloop/data|/usr/local/lscp/cyberpanel/snappymail/data|g' "$inc" && log "Updated $inc" && break + done 2>/dev/null || true +elif [ ! -d "/usr/local/CyberCP/public/snappymail" ]; then + log "public/snappymail missing - installing SnappyMail v${SNAPPY_VERSION}..." + cd /usr/local/CyberCP/public || exit 1 + if ! wget -q "https://github.com/the-djmaze/snappymail/releases/download/v${SNAPPY_VERSION}/snappymail-${SNAPPY_VERSION}.zip" -O "snappymail-${SNAPPY_VERSION}.zip"; then + log "ERROR: wget SnappyMail failed" + exit 1 + fi + unzip -q "snappymail-${SNAPPY_VERSION}.zip" -d /usr/local/CyberCP/public/snappymail + rm -f "snappymail-${SNAPPY_VERSION}.zip" + find /usr/local/CyberCP/public/snappymail -type d -exec chmod 755 {} \; + find /usr/local/CyberCP/public/snappymail -type f -exec chmod 644 {} \; + log "SnappyMail installed" + # Configure data path and run CyberPanel integration + wget -q -O /usr/local/CyberCP/snappymail_cyberpanel.php "https://raw.githubusercontent.com/the-djmaze/snappymail/master/integrations/cyberpanel/install.php" 2>/dev/null || true + if [ -f /usr/local/CyberCP/snappymail_cyberpanel.php ]; then + for php in /usr/local/lsws/lsphp83/bin/php /usr/local/lsws/lsphp82/bin/php /usr/local/lsws/lsphp81/bin/php /usr/local/lsws/lsphp80/bin/php; do + [ -x "$php" ] && $php /usr/local/CyberCP/snappymail_cyberpanel.php 2>/dev/null && break + done + fi +else + log "public/snappymail already exists" +fi + +# 2) Data migration: lscp/cyberpanel/rainloop/data -> snappymail/data +mkdir -p /usr/local/lscp/cyberpanel/snappymail/data/_data_/_default_/configs/ +mkdir -p /usr/local/lscp/cyberpanel/snappymail/data/_data_/_default_/domains/ +mkdir -p /usr/local/lscp/cyberpanel/snappymail/data/_data_/_default_/storage/ +mkdir -p /usr/local/lscp/cyberpanel/snappymail/data/_data_/_default_/temp/ +mkdir -p /usr/local/lscp/cyberpanel/snappymail/data/_data_/_default_/cache/ + +if [ -d "/usr/local/lscp/cyberpanel/rainloop/data" ] && [ "$(ls -A /usr/local/lscp/cyberpanel/rainloop/data 2>/dev/null)" ]; then + log "Migrating rainloop data to snappymail..." + rsync -av --ignore-existing /usr/local/lscp/cyberpanel/rainloop/data/ /usr/local/lscp/cyberpanel/snappymail/data/ 2>&1 | tee -a "$LOG" + if [ -f "/usr/local/CyberCP/public/snappymail/include.php" ]; then + sed -i 's|/usr/local/lscp/cyberpanel/rainloop/data|/usr/local/lscp/cyberpanel/snappymail/data|g' /usr/local/CyberCP/public/snappymail/include.php + fi + find /usr/local/lscp/cyberpanel/snappymail/data -type f \( -name "*.ini" -o -name "*.json" -o -name "*.php" -o -name "*.cfg" \) -exec grep -l "rainloop" {} \; 2>/dev/null | while read -r f; do + sed -i 's|/usr/local/lscp/cyberpanel/rainloop/data|/usr/local/lscp/cyberpanel/snappymail/data|g; s|/rainloop/|/snappymail/|g; s|rainloop/data|snappymail/data|g' "$f" + done + log "Data migration done" +fi + +# 3) Ownership and permissions +if id -u lscpd >/dev/null 2>&1; then + chown -R lscpd:lscpd /usr/local/lscp/cyberpanel/snappymail/ + chown -R lscpd:lscpd /usr/local/CyberCP/public/snappymail/ 2>/dev/null || true + log "Set ownership lscpd:lscpd" +fi +chmod -R 775 /usr/local/lscp/cyberpanel/snappymail/data/ 2>/dev/null || true + +# 4) Restart panel +log "Restarting lscpd..." +systemctl restart lscpd +sleep 2 + +# 5) Test +PORT=$(cat /usr/local/lscp/conf/bind.conf 2>/dev/null | grep -oE '[0-9]+' | head -1) +PORT=${PORT:-8090} +log "Testing https://127.0.0.1:${PORT}/snappymail/index.php ..." +CODE=$(curl -sk -o /dev/null -w "%{http_code}" "https://127.0.0.1:${PORT}/snappymail/index.php" 2>/dev/null || echo "000") +if [ "$CODE" = "200" ] || [ "$CODE" = "302" ]; then + log "OK: SnappyMail URL returned HTTP $CODE" +else + log "WARNING: SnappyMail URL returned HTTP $CODE (expected 200 or 302)" +fi + +log "=== Migration script finished ===" diff --git a/CyberCP/secMiddleware.py b/CyberCP/secMiddleware.py index 29061ce9e..dcf498c22 100644 --- a/CyberCP/secMiddleware.py +++ b/CyberCP/secMiddleware.py @@ -267,7 +267,7 @@ class secMiddleware: response['X-XSS-Protection'] = "1; mode=block" response['X-Frame-Options'] = "sameorigin" - response['Content-Security-Policy'] = "script-src 'self' https://www.jsdelivr.com" + response['Content-Security-Policy'] = "script-src 'self' 'unsafe-inline' https://www.jsdelivr.com" response['Content-Security-Policy'] = "connect-src *;" response['Content-Security-Policy'] = "font-src 'self' 'unsafe-inline' https://www.jsdelivr.com https://fonts.googleapis.com" response[ diff --git a/CyberCP/settings.py b/CyberCP/settings.py index 1edb42b25..c4d987dd8 100644 --- a/CyberCP/settings.py +++ b/CyberCP/settings.py @@ -13,15 +13,15 @@ https://docs.djangoproject.com/en/1.11/ref/settings/ import os from django.utils.translation import gettext_lazy as _ -# Patreon OAuth Configuration for Paid Plugins -# SECURITY: Environment variables take precedence. Hardcoded values are fallback for this server only. -# For repository version, use empty defaults and set via environment variables. -PATREON_CLIENT_ID = os.environ.get('PATREON_CLIENT_ID', 'LFXeXUcfrM8MeVbUcmGbB7BgeJ9RzZi2v_H9wL4d9vG6t1dV4SUnQ4ibn9IYzvt7') -PATREON_CLIENT_SECRET = os.environ.get('PATREON_CLIENT_SECRET', 'APuJ5qoL3TLFmNnGDVkgl-qr3sCzp2CQsKfslBbp32hhnhlD0y6-ZcSCkb_FaUJv') +# Patreon OAuth (optional): for paid-plugin verification via Patreon membership. +# Set these only if you use Patreon-gated plugins; leave unset otherwise. +# Use environment variables; no defaults so the repo stays generic and safe to push to GitHub. +PATREON_CLIENT_ID = os.environ.get('PATREON_CLIENT_ID', '') +PATREON_CLIENT_SECRET = os.environ.get('PATREON_CLIENT_SECRET', '') PATREON_CREATOR_ID = os.environ.get('PATREON_CREATOR_ID', '') -PATREON_MEMBERSHIP_TIER_ID = os.environ.get('PATREON_MEMBERSHIP_TIER_ID', '27789984') # CyberPanel Paid Plugin tier -PATREON_CREATOR_ACCESS_TOKEN = os.environ.get('PATREON_CREATOR_ACCESS_TOKEN', 'niAHRiI9SgrRCMmaf5exoXXphy3RWXWsX4kO5Yv9SQI') -PATREON_CREATOR_REFRESH_TOKEN = os.environ.get('PATREON_CREATOR_REFRESH_TOKEN', 'VZlCQoPwJUr4NLni1N82-K_CpJHTAOYUOCx2PujdjQg') +PATREON_MEMBERSHIP_TIER_ID = os.environ.get('PATREON_MEMBERSHIP_TIER_ID', '') +PATREON_CREATOR_ACCESS_TOKEN = os.environ.get('PATREON_CREATOR_ACCESS_TOKEN', '') +PATREON_CREATOR_REFRESH_TOKEN = os.environ.get('PATREON_CREATOR_REFRESH_TOKEN', '') # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -37,6 +37,22 @@ DEBUG = False ALLOWED_HOSTS = ['*'] +# When the panel is behind a reverse proxy (e.g. https://panel.example.com -> http://backend:port), +# the browser sends Origin/Referer with the public domain while the proxy may send Host as the +# backend address. Django then fails CSRF (Referer vs Host mismatch) and POSTs get 403. +# Set CSRF_TRUSTED_ORIGINS to your public origin(s) so CSRF passes. Optional; leave unset if +# you access the panel by IP:port only. +# Example: export CSRF_TRUSTED_ORIGINS="https://panel.example.com,http://panel.example.com" +_csrf_origins_env = os.environ.get('CSRF_TRUSTED_ORIGINS', '') +_csrf_origins_list = [o.strip() for o in _csrf_origins_env.split(',') if o.strip()] +# Add default trusted origins for common CyberPanel domains +_default_origins = [ + 'https://cyberpanel.newstargeted.com', + 'http://cyberpanel.newstargeted.com', +] +# Merge environment and default origins, avoiding duplicates +CSRF_TRUSTED_ORIGINS = list(dict.fromkeys(_csrf_origins_list + _default_origins)) + # Application definition INSTALLED_APPS = [ @@ -65,7 +81,8 @@ INSTALLED_APPS = [ # Apps with multiple or complex dependencies 'emailPremium', - 'emailMarketing', # Depends on websiteFunctions and loginSystem + # Optional plugins (e.g. emailMarketing, discordWebhooks) - install via Plugin Store + # from https://github.com/master3395/cyberpanel-plugins - plugin installer adds them 'cloudAPI', # Depends on websiteFunctions 'containerization', # Depends on websiteFunctions 'IncBackups', # Depends on websiteFunctions and loginSystem @@ -123,6 +140,8 @@ TEMPLATES = [ 'baseTemplate.context_processors.version_context', 'baseTemplate.context_processors.cosmetic_context', 'baseTemplate.context_processors.notification_preferences_context', + 'baseTemplate.context_processors.firewall_static_context', + 'baseTemplate.context_processors.dns_static_context', ], }, }, @@ -192,6 +211,9 @@ STATIC_ROOT = os.path.join(BASE_DIR, "static/") STATIC_URL = '/static/' +# Panel public directory (SnappyMail, phpMyAdmin, etc.) – served so /snappymail/ and /phpmyadmin/ work when panel is behind Django +PUBLIC_ROOT = os.path.join(BASE_DIR, 'public') + LOCALE_PATHS = ( os.path.join(BASE_DIR, 'locale'), ) @@ -230,4 +252,25 @@ LOGIN_REDIRECT_URL = '/' # Default primary key field type # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field -DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' \ No newline at end of file +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +# Sync INSTALLED_APPS with plugins on disk so /plugins// and /plugins//settings/ work. +# Plugins installed under /usr/local/CyberCP/ (or BASE_DIR) are added here if they have meta.xml + urls.py. +_cybercp_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +if os.path.isdir(_cybercp_root): + try: + _existing_apps = set(INSTALLED_APPS) + for _name in os.listdir(_cybercp_root): + if _name.startswith('.'): + continue + _plugin_dir = os.path.join(_cybercp_root, _name) + if not os.path.isdir(_plugin_dir): + continue + if _name in _existing_apps: + continue + if (os.path.exists(os.path.join(_plugin_dir, 'meta.xml')) and + os.path.exists(os.path.join(_plugin_dir, 'urls.py'))): + INSTALLED_APPS.append(_name) + _existing_apps.add(_name) + except (OSError, IOError): + pass \ No newline at end of file diff --git a/CyberCP/urls.py b/CyberCP/urls.py index ea5ccf382..69692fa4c 100644 --- a/CyberCP/urls.py +++ b/CyberCP/urls.py @@ -13,16 +13,34 @@ Including another URLconf 1. Import the include() function: from django.urls import path, include 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ +import os from django.urls import path, re_path, include from django.contrib import admin from django.conf import settings from django.conf.urls.static import static from django.views.static import serve +from django.views.generic import RedirectView from firewall import views as firewall_views +# Plugin routes are no longer hardcoded here; pluginHolder.urls dynamically +# includes each installed plugin (under /plugins//) so settings and +# other plugin pages work for any installed plugin. + +# Optional app: may be missing after clean clone or git clean -fd (not in all repo trees) +_optional_email_marketing = [] +try: + _optional_email_marketing.append(path('emailMarketing/', include('emailMarketing.urls'))) +except ModuleNotFoundError: + pass + urlpatterns = [ # Serve static files first (before catch-all routes) re_path(r'^static/(?P.*)$', serve, {'document_root': settings.STATIC_ROOT}), + # Serve SnappyMail and phpMyAdmin from public directory (fixes 404 when panel is served by Django/lscpd on :2087/:8090) + re_path(r'^snappymail/?$', RedirectView.as_view(url='/snappymail/index.php', permanent=False)), + re_path(r'^snappymail/(?P.*)$', serve, {'document_root': os.path.join(settings.PUBLIC_ROOT, 'snappymail')}), + re_path(r'^phpmyadmin/?$', RedirectView.as_view(url='/phpmyadmin/index.php', permanent=False)), + re_path(r'^phpmyadmin/(?P.*)$', serve, {'document_root': os.path.join(settings.PUBLIC_ROOT, 'phpmyadmin')}), path('base/', include('baseTemplate.urls')), path('imunifyav/', firewall_views.imunifyAV, name='imunifyav_root'), path('ImunifyAV/', firewall_views.imunifyAV, name='imunifyav_root_legacy'), @@ -43,6 +61,7 @@ urlpatterns = [ path('api/', include('api.urls')), path('filemanager/', include('filemanager.urls')), path('emailPremium/', include('emailPremium.urls')), + *_optional_email_marketing, path('manageservices/', include('manageServices.urls')), path('plugins/', include('pluginHolder.urls')), path('cloudAPI/', include('cloudAPI.urls')), @@ -51,7 +70,6 @@ urlpatterns = [ path('CloudLinux/', include('CLManager.urls')), path('IncrementalBackups/', include('IncBackups.urls')), path('aiscanner/', include('aiScanner.urls')), - path('emailMarketing/', include('emailMarketing.urls')), # path('Terminal/', include('WebTerminal.urls')), path('', include('loginSystem.urls')), ] diff --git a/CyberCP/wsgi.py b/CyberCP/wsgi.py index c9cc8c835..03015f999 100644 --- a/CyberCP/wsgi.py +++ b/CyberCP/wsgi.py @@ -8,7 +8,13 @@ https://docs.djangoproject.com/en/1.11/howto/deployment/wsgi/ """ import os +import sys +# Ensure CyberPanel app path takes precedence over system 'firewall' package +PROJECT_ROOT = '/usr/local/CyberCP' +while PROJECT_ROOT in sys.path: + sys.path.remove(PROJECT_ROOT) +sys.path.insert(0, PROJECT_ROOT) from django.core.wsgi import get_wsgi_application diff --git a/README.md b/README.md index 6dcd87f83..f1777f50b 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ **Web Hosting Control Panel powered by OpenLiteSpeed** Fast • Secure • Scalable — Simplify hosting management with style. -**Version**: 2.5.5-dev • **Updated**: November 15, 2025 +**Version**: 2.5.5-dev • **Updated**: January 15, 2026 [![GitHub](https://img.shields.io/badge/GitHub-Repo-000?style=flat-square\&logo=github)](https://github.com/usmannasir/cyberpanel) [![Docs](https://img.shields.io/badge/Docs-Read-green?style=flat-square\&logo=gitbook)](https://cyberpanel.net/KnowledgeBase/) @@ -70,25 +70,25 @@ Fast • Secure • Scalable — Simplify hosting management with style. | OS family | Recommended / Supported | | -------------------------- | ----------------------: | -| Ubuntu 24.04, 22.04, 20.04 | ✅ Recommended | -| Debian 13, 12, 11 | ✅ Supported | -| AlmaLinux 10, 9, 8 | ✅ Supported | -| RockyLinux 9, 8 | ✅ Supported | -| RHEL 9, 8 | ✅ Supported | -| CloudLinux 9, 8 | ✅ Supported | +| AlmaLinux 10, 9, 8 | ✅ Recommended | | CentOS 7 | ⚠️ Legacy — EOL | +| CloudLinux 9, 8 | ✅ Supported | +| Debian 13, 12, 11 | ✅ Supported | +| RHEL 9, 8 | ✅ Supported | +| RockyLinux 9, 8 | ✅ Supported | +| Ubuntu 24.04, 22.04, 20.04 | ✅ Recommended | -> CyberPanel targets x86\_64 only. Test the unsupported OS in staging first. +> **Architectures:** x86_64 (primary), aarch64/ARM64 (supported). AlmaLinux is the recommended RHEL-compatible distribution. Test unsupported OS in staging first. --- ## PHP support (short) -* ✅ **Recommended**: PHP 8.5 (beta), 8.4, 8.3, 8.2, 8.1 -* ⚠️ **Legacy**: PHP 8.0, PHP 7.4 (security-only) -* ❌ **Deprecated**: PHP 7.1, 7.2, 7.3 (no longer installed) +* ✅ **Recommended**: PHP 8.5, 8.4 +* ⚠️ **Security fixes only**: PHP 8.3, 8.2, 8.1 +* ❌ **EOL / Deprecated**: PHP 8.0, 7.4, 7.1, 7.2, 7.3 (no longer supported) -Third-party repositories (Remi, Ondrej) may provide older or niche versions; verify compatibility before use. +Third-party repositories may provide older or niche versions; verify compatibility before use. RHEL/Alma/Rocky: [Remi RPM](https://rpms.remirepo.net/). Ubuntu/Debian: [Ondrej PPA](https://launchpad.net/~ondrej/+archive/ubuntu/php). See [php.net/supported-versions](https://www.php.net/supported-versions.php). --- @@ -112,6 +112,34 @@ sh <(curl -s https://raw.githubusercontent.com/usmannasir/cyberpanel/stable/preU --- +## Upgrade to v2.5.5-dev (non-interactive) + +Upgrade to v2.5.5-dev without branch or MariaDB prompts. + +**MariaDB version options:** `10.11`, `11.8` (LTS default), `12.1` (latest). Use `--mariadb` for 10.11, or `--mariadb-version X` to choose explicitly. If you want to **default to 11.8** and skip the prompt, use `--mariadb-version 11.8`. + +```bash +# Upgrade to v2.5.5-dev without prompts (script will prompt for MariaDB unless you pass a flag) +sh <(curl -s https://raw.githubusercontent.com/usmannasir/cyberpanel/v2.5.5-dev/preUpgrade.sh || wget -O - https://raw.githubusercontent.com/usmannasir/cyberpanel/v2.5.5-dev/preUpgrade.sh) -b v2.5.5-dev + +# Default to MariaDB 11.8 (LTS) — recommended, non-interactive +sh <(curl -s https://raw.githubusercontent.com/usmannasir/cyberpanel/v2.5.5-dev/preUpgrade.sh || wget -O - https://raw.githubusercontent.com/usmannasir/cyberpanel/v2.5.5-dev/preUpgrade.sh) -b v2.5.5-dev --mariadb-version 11.8 + +# MariaDB 10.11 (non-interactive) +sh <(curl -s https://raw.githubusercontent.com/usmannasir/cyberpanel/v2.5.5-dev/preUpgrade.sh || wget -O - https://raw.githubusercontent.com/usmannasir/cyberpanel/v2.5.5-dev/preUpgrade.sh) -b v2.5.5-dev --mariadb + +# MariaDB 12.1 (latest) +sh <(curl -s https://raw.githubusercontent.com/usmannasir/cyberpanel/v2.5.5-dev/preUpgrade.sh || wget -O - https://raw.githubusercontent.com/usmannasir/cyberpanel/v2.5.5-dev/preUpgrade.sh) -b v2.5.5-dev --mariadb-version 12.1 +``` + +**Full non-interactive (v2.5.5-dev + MariaDB 11.8):** + +```bash +sh <(curl -s https://raw.githubusercontent.com/usmannasir/cyberpanel/v2.5.5-dev/preUpgrade.sh || wget -O - https://raw.githubusercontent.com/usmannasir/cyberpanel/v2.5.5-dev/preUpgrade.sh) -b v2.5.5-dev --mariadb-version 11.8 +``` + +--- + ## Troubleshooting (common) **Command not found** — install curl/wget/git/python3 @@ -144,10 +172,27 @@ journalctl -u lscpd -f ## Recent fixes +* **02.02.2026** — Plugin updates: premiumPlugin & paypalPremiumPlugin unified verification (Plugin Grants, activation key, Patreon, PayPal, AES-256-CBC encryption). Installed Plugins UI: bulk activate/deactivate, freshness badges, removed Patreon messaging from front. * **15.11.2025** — Hardened MySQL password rotation: `mysqlUtilities.changePassword` now auto-resolves the backing MySQL account (user + host) even when `DBUsers` metadata is missing, preventing the historical `[mysqlUtilities.changePassword] can only concatenate str (not "int")` error. Regression tests live under `Test/mysqlUtilities/`, and you should restart `lscpd` after deploying the patch so the helper reloads. --- +## Testing + +### OLS Feature Test Suite + +The OpenLiteSpeed feature test suite (128 tests) validates binary integrity, CyberPanel module, Auto-SSL config, SSL listener auto-mapping, .htaccess processing, ReadApacheConf directives, and more. + +```bash +# Run from CyberPanel repo root +./tests/ols_test_setup.sh # One-time setup +./tests/ols_feature_tests.sh +``` + +Requires a live CyberPanel + OLS installation. + +--- + ## Resources * Official site: [https://cyberpanel.net](https://cyberpanel.net) diff --git a/baseTemplate/context_processors.py b/baseTemplate/context_processors.py index 696f9d840..83215a38a 100644 --- a/baseTemplate/context_processors.py +++ b/baseTemplate/context_processors.py @@ -1,4 +1,7 @@ # -*- coding: utf-8 -*- +import os +import time + from .views import VERSION, BUILD def version_context(request): @@ -49,4 +52,54 @@ def notification_preferences_context(request): return { 'backup_notification_dismissed': False, 'ai_scanner_notification_dismissed': False + } + +def firewall_static_context(request): + """Expose a cache-busting token for firewall static assets (bumps when firewall.js changes).""" + try: + from django.conf import settings + base = settings.BASE_DIR + # Check both app static and repo static so version updates when either is updated + paths = [ + os.path.join(base, 'firewall', 'static', 'firewall', 'firewall.js'), + os.path.join(base, 'static', 'firewall', 'firewall.js'), + os.path.join(base, 'public', 'static', 'firewall', 'firewall.js'), + ] + version = 0 + for p in paths: + try: + version = max(version, int(os.path.getmtime(p))) + except (OSError, TypeError): + pass + if version <= 0: + version = int(time.time()) + except (OSError, AttributeError): + version = int(time.time()) + return { + 'FIREWALL_STATIC_VERSION': version + } + + +def dns_static_context(request): + """Cache-busting for DNS static assets (bumps when dns.js changes). Avoids stale JS/layout.""" + try: + from django.conf import settings + base = settings.BASE_DIR + paths = [ + os.path.join(base, 'dns', 'static', 'dns', 'dns.js'), + os.path.join(base, 'static', 'dns', 'dns.js'), + os.path.join(base, 'public', 'static', 'dns', 'dns.js'), + ] + version = 0 + for p in paths: + try: + version = max(version, int(os.path.getmtime(p))) + except (OSError, TypeError): + pass + if version <= 0: + version = int(time.time()) + except (OSError, AttributeError): + version = int(time.time()) + return { + 'DNS_STATIC_VERSION': version } \ No newline at end of file diff --git a/baseTemplate/static/baseTemplate/assets/mobile-responsive.css b/baseTemplate/static/baseTemplate/assets/mobile-responsive.css new file mode 100644 index 000000000..35e2674e5 --- /dev/null +++ b/baseTemplate/static/baseTemplate/assets/mobile-responsive.css @@ -0,0 +1,589 @@ +/* CyberPanel Mobile Responsive & Readability Fixes */ +/* This file ensures all pages are mobile-friendly with proper font sizes and readable text */ + +/* Base font size and mobile-first approach */ +html { + font-size: 16px; /* Base font size for better readability */ + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; + text-size-adjust: 100%; +} + +body { + font-size: 16px; + line-height: 1.6; + color: #2f3640; /* Dark text for better readability on white backgrounds */ + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; +} + +/* Ensure all text is readable with proper contrast */ +* { + color: inherit; +} + +/* Override any light text that might be hard to read */ +.text-muted, .text-secondary, .text-light { + color: #2f3640 !important; /* Dark text for better readability on white backgrounds */ +} + +/* Fix small font sizes that are hard to read */ +small, .small, .text-small { + font-size: 14px !important; /* Minimum readable size */ +} + +/* Table improvements for mobile */ +.table { + font-size: 16px !important; /* Larger table text */ + width: 100%; + border-collapse: collapse; + margin-bottom: 20px; +} + +.table th, .table td { + padding: 12px 8px !important; /* More padding for touch targets */ + border: 1px solid #e8e9ff; + text-align: left; + vertical-align: middle; + font-size: 14px !important; + line-height: 1.4; +} + +.table th { + background-color: #f8f9fa; + font-weight: 600; + color: #2f3640 !important; + font-size: 15px !important; +} + +/* Button improvements for mobile */ +.btn { + font-size: 16px !important; + padding: 12px 20px !important; + border-radius: 8px; + min-height: 44px; /* Minimum touch target size */ + display: inline-flex; + align-items: center; + justify-content: center; + text-decoration: none; + border: none; + cursor: pointer; + transition: all 0.2s ease; +} + +.btn-sm { + font-size: 14px !important; + padding: 8px 16px !important; + min-height: 36px; +} + +.btn-xs { + font-size: 13px !important; + padding: 6px 12px !important; + min-height: 32px; +} + +/* Form elements */ +.form-control, input, textarea, select { + font-size: 16px !important; /* Prevents zoom on iOS */ + padding: 12px 16px !important; + border: 2px solid #e8e9ff; + border-radius: 8px; + min-height: 44px; + line-height: 1.4; + color: #2f3640 !important; + background-color: #ffffff; +} + +.form-control:focus, input:focus, textarea:focus, select:focus { + border-color: #5856d6; + box-shadow: 0 0 0 3px rgba(88, 86, 214, 0.1); + outline: none; +} + +/* Labels and form text */ +label, .control-label { + font-size: 16px !important; + font-weight: 600; + color: #2f3640 !important; + margin-bottom: 8px; + display: block; +} + +/* Headings with proper hierarchy */ +h1 { + font-size: 2.5rem !important; /* 40px */ + font-weight: 700; + color: #1e293b !important; + line-height: 1.2; + margin-bottom: 1rem; +} + +h2 { + font-size: 2rem !important; /* 32px */ + font-weight: 600; + color: #1e293b !important; + line-height: 1.3; + margin-bottom: 0.875rem; +} + +h3 { + font-size: 1.5rem !important; /* 24px */ + font-weight: 600; + color: #2f3640 !important; + line-height: 1.4; + margin-bottom: 0.75rem; +} + +h4 { + font-size: 1.25rem !important; /* 20px */ + font-weight: 600; + color: #2f3640 !important; + line-height: 1.4; + margin-bottom: 0.5rem; +} + +h5 { + font-size: 1.125rem !important; /* 18px */ + font-weight: 600; + color: #2f3640 !important; + line-height: 1.4; + margin-bottom: 0.5rem; +} + +h6 { + font-size: 1rem !important; /* 16px */ + font-weight: 600; + color: #2f3640 !important; + line-height: 1.4; + margin-bottom: 0.5rem; +} + +/* Paragraph and body text */ +p { + font-size: 16px !important; + line-height: 1.6; + color: #2f3640 !important; + margin-bottom: 1rem; +} + +/* Sidebar improvements */ +#page-sidebar { + font-size: 16px !important; +} + +#page-sidebar ul li a { + font-size: 16px !important; + padding: 12px 20px !important; + color: #2f3640 !important; + min-height: 44px; + display: flex; + align-items: center; + text-decoration: none; +} + +#page-sidebar ul li a:hover { + background-color: #f8f9fa; + color: #5856d6 !important; +} + +/* Content area improvements */ +.content-box, .panel, .card { + font-size: 16px !important; + color: #2f3640 !important; + background-color: #ffffff; + border: 1px solid #e8e9ff; + border-radius: 12px; + padding: 20px; + margin-bottom: 20px; +} + +/* Modal improvements */ +.modal-content { + font-size: 16px !important; + color: #2f3640 !important; +} + +.modal-title { + font-size: 1.5rem !important; + font-weight: 600; + color: #1e293b !important; +} + +/* Alert and notification improvements */ +.alert { + font-size: 16px !important; + padding: 16px 20px !important; + border-radius: 8px; + margin-bottom: 20px; +} + +.alert-success { + background-color: #f0fdf4; + border-color: #bbf7d0; + color: #166534 !important; +} + +.alert-danger { + background-color: #fef2f2; + border-color: #fecaca; + color: #dc2626 !important; +} + +.alert-warning { + background-color: #fffbeb; + border-color: #fed7aa; + color: #d97706 !important; +} + +.alert-info { + background-color: #eff6ff; + border-color: #bfdbfe; + color: #2563eb !important; +} + +/* Navigation improvements */ +.navbar-nav .nav-link { + font-size: 16px !important; + padding: 12px 16px !important; + color: #2f3640 !important; +} + +/* Breadcrumb improvements */ +.breadcrumb { + font-size: 16px !important; + background-color: transparent; + padding: 0; + margin-bottom: 20px; +} + +.breadcrumb-item { + color: #64748b !important; +} + +.breadcrumb-item.active { + color: #2f3640 !important; +} + +/* Mobile-first responsive breakpoints */ +@media (max-width: 1200px) { + .container, .container-fluid { + padding-left: 15px; + padding-right: 15px; + } + + .table-responsive { + border: none; + margin-bottom: 20px; + } +} + +@media (max-width: 992px) { + /* Stack columns on tablets */ + .col-md-3, .col-md-4, .col-md-6, .col-md-8, .col-md-9 { + flex: 0 0 100%; + max-width: 100%; + margin-bottom: 20px; + } + + /* Adjust sidebar for tablets */ + #page-sidebar { + width: 100%; + position: static; + height: auto; + } + + /* Make tables horizontally scrollable */ + .table-responsive { + overflow-x: auto; + } + + .table { + min-width: 600px; + } +} + +@media (max-width: 768px) { + /* Mobile-specific adjustments */ + html { + font-size: 14px; + } + + body { + font-size: 14px; + padding: 0; + } + + .container, .container-fluid { + padding-left: 10px; + padding-right: 10px; + } + + /* Stack all columns on mobile */ + .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-6, .col-sm-8, .col-sm-9, .col-sm-12, + .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-6, .col-md-8, .col-md-9, .col-md-12 { + flex: 0 0 100%; + max-width: 100%; + margin-bottom: 15px; + } + + /* Adjust headings for mobile */ + h1 { + font-size: 2rem !important; /* 32px */ + } + + h2 { + font-size: 1.75rem !important; /* 28px */ + } + + h3 { + font-size: 1.5rem !important; /* 24px */ + } + + h4 { + font-size: 1.25rem !important; /* 20px */ + } + + /* Button adjustments for mobile */ + .btn { + font-size: 16px !important; + padding: 14px 20px !important; + width: 100%; + margin-bottom: 10px; + } + + .btn-group .btn { + width: auto; + margin-bottom: 0; + } + + /* Form adjustments for mobile */ + .form-control, input, textarea, select { + font-size: 16px !important; /* Prevents zoom on iOS */ + padding: 14px 16px !important; + width: 100%; + } + + /* Table adjustments for mobile */ + .table { + font-size: 14px !important; + } + + .table th, .table td { + padding: 8px 6px !important; + font-size: 13px !important; + } + + /* Hide less important columns on mobile */ + .table .d-none-mobile { + display: none !important; + } + + /* Modal adjustments for mobile */ + .modal-dialog { + margin: 10px; + width: calc(100% - 20px); + } + + .modal-content { + padding: 20px 15px; + } + + /* Content box adjustments */ + .content-box, .panel, .card { + padding: 15px; + margin-bottom: 15px; + } + + /* Sidebar adjustments for mobile */ + #page-sidebar { + position: fixed; + top: 0; + left: -100%; + width: 280px; + height: 100vh; + z-index: 1000; + transition: left 0.3s ease; + background-color: #ffffff; + box-shadow: 2px 0 10px rgba(0,0,0,0.1); + } + + #page-sidebar.show { + left: 0; + } + + /* Main content adjustments when sidebar is open */ + #main-content { + transition: margin-left 0.3s ease; + } + + #main-content.sidebar-open { + margin-left: 280px; + } + + /* Mobile menu toggle */ + .mobile-menu-toggle { + display: block; + position: fixed; + top: 20px; + left: 20px; + z-index: 1001; + background-color: #5856d6; + color: white; + border: none; + padding: 12px; + border-radius: 8px; + font-size: 18px; + cursor: pointer; + } +} + +@media (max-width: 576px) { + /* Extra small devices */ + html { + font-size: 14px; + } + + .container, .container-fluid { + padding-left: 8px; + padding-right: 8px; + } + + /* Even smaller buttons and forms for very small screens */ + .btn { + font-size: 14px !important; + padding: 12px 16px !important; + } + + .form-control, input, textarea, select { + font-size: 16px !important; /* Still 16px to prevent zoom */ + padding: 12px 14px !important; + } + + /* Compact table for very small screens */ + .table th, .table td { + padding: 6px 4px !important; + font-size: 12px !important; + } + + /* Hide even more columns on very small screens */ + .table .d-none-mobile-sm { + display: none !important; + } +} + +/* Utility classes for mobile */ +.d-none-mobile { + display: block; +} + +.d-none-mobile-sm { + display: block; +} + +@media (max-width: 768px) { + .d-none-mobile { + display: none !important; + } +} + +@media (max-width: 576px) { + .d-none-mobile-sm { + display: none !important; + } +} + +/* Ensure all text has proper contrast */ +.text-white { + color: #ffffff !important; +} + +.text-dark { + color: #2f3640 !important; +} + +.text-muted { + color: #2f3640 !important; /* Dark text for better readability */ +} + +/* Fix any light text on light backgrounds */ +.bg-light .text-muted, +.bg-white .text-muted, +.panel .text-muted { + color: #2f3640 !important; /* Dark text for better readability */ +} + +/* Ensure proper spacing for touch targets */ +a, button, input, select, textarea { + min-height: 44px; + min-width: 44px; +} + +/* Additional text readability improvements */ +/* Fix any green text issues */ +.ng-binding { + color: #2f3640 !important; /* Normal dark text instead of green */ +} + +/* Ensure all text elements have proper contrast */ +span, div, p, label, td, th { + color: inherit; +} + +/* Fix specific text color issues */ +.text-success { + color: #059669 !important; /* Darker green for better readability */ +} + +.text-info { + color: #0284c7 !important; /* Darker blue for better readability */ +} + +.text-warning { + color: #d97706 !important; /* Darker orange for better readability */ +} + +/* Override Bootstrap's muted text */ +.text-muted { + color: #2f3640 !important; /* Dark text instead of grey */ +} + +/* Fix any remaining light text on light backgrounds */ +.bg-white .text-light, +.bg-light .text-light, +.panel .text-light, +.card .text-light { + color: #2f3640 !important; +} + +/* Fix for small clickable elements */ +.glyph-icon, .icon { + min-width: 44px; + min-height: 44px; + display: flex; + align-items: center; + justify-content: center; +} + +/* Loading and spinner improvements */ +.spinner, .loading { + font-size: 16px !important; + color: #5856d6 !important; +} + +/* Print styles */ +@media print { + body { + font-size: 12pt; + color: #000000 !important; + background: #ffffff !important; + } + + .table th, .table td { + font-size: 10pt !important; + color: #000000 !important; + } + + .btn, .alert, .modal { + display: none !important; + } +} diff --git a/baseTemplate/static/baseTemplate/assets/readability-fixes.css b/baseTemplate/static/baseTemplate/assets/readability-fixes.css new file mode 100644 index 000000000..1911482b7 --- /dev/null +++ b/baseTemplate/static/baseTemplate/assets/readability-fixes.css @@ -0,0 +1,265 @@ +/* CyberPanel Readability & Design Fixes */ +/* This file fixes the core design issues with grey text and color inconsistencies */ + +/* Override CSS Variables for Better Text Contrast */ +:root { + /* Ensure all text uses proper dark colors for readability */ + --text-primary: #2f3640; + --text-secondary: #2f3640; /* Changed from grey to dark for better readability */ + --text-heading: #1e293b; +} + +/* Dark theme also uses proper contrast */ +[data-theme="dark"] { + --text-primary: #e4e4e7; + --text-secondary: #e4e4e7; /* Changed from grey to light for better readability */ + --text-heading: #f3f4f6; +} + +/* Fix Green Text Issues */ +/* Override Angular binding colors that might be green */ +.ng-binding { + color: var(--text-secondary) !important; +} + +/* Specific fix for uptime display */ +#sidebar .server-info .info-line span, +#sidebar .server-info .info-line .ng-binding, +.server-info .ng-binding { + color: var(--text-secondary) !important; +} + +/* Fix Grey Text on White Background */ +/* Override all muted and secondary text classes */ +.text-muted, +.text-secondary, +.text-light, +small, +.small, +.text-small { + color: var(--text-secondary) !important; +} + +/* Fix specific Bootstrap classes */ +.text-muted { + color: #2f3640 !important; /* Dark text for better readability */ +} + +/* Fix text on white/light backgrounds */ +.bg-white .text-muted, +.bg-light .text-muted, +.panel .text-muted, +.card .text-muted, +.content-box .text-muted { + color: #2f3640 !important; +} + +/* Fix menu items and navigation */ +#sidebar .menu-item, +#sidebar .menu-item span, +#sidebar .menu-item i, +.sidebar .menu-item, +.sidebar .menu-item span, +.sidebar .menu-item i { + color: var(--text-secondary) !important; +} + +#sidebar .menu-item:hover, +.sidebar .menu-item:hover { + color: var(--accent-color) !important; +} + +#sidebar .menu-item.active, +.sidebar .menu-item.active { + color: white !important; +} + +/* Fix server info and details */ +.server-info, +.server-info *, +.server-details, +.server-details *, +.info-line, +.info-line span, +.info-line strong, +.tagline, +.brand { + color: inherit !important; +} + +/* Fix form elements */ +label, +.control-label, +.form-label { + color: var(--text-primary) !important; + font-weight: 600; +} + +/* Fix table text */ +.table th, +.table td { + color: var(--text-primary) !important; +} + +.table th { + font-weight: 600; +} + +/* Fix alert text */ +.alert { + color: var(--text-primary) !important; +} + +.alert-success { + color: #059669 !important; /* Darker green for better readability */ +} + +.alert-info { + color: #0284c7 !important; /* Darker blue for better readability */ +} + +.alert-warning { + color: #d97706 !important; /* Darker orange for better readability */ +} + +.alert-danger { + color: #dc2626 !important; /* Darker red for better readability */ +} + +/* Fix breadcrumb text */ +.breadcrumb-item { + color: var(--text-secondary) !important; +} + +.breadcrumb-item.active { + color: var(--text-primary) !important; +} + +/* Fix modal text */ +.modal-content { + color: var(--text-primary) !important; +} + +.modal-title { + color: var(--text-heading) !important; +} + +/* Fix button text */ +.btn { + color: inherit; +} + +/* Fix any remaining light text issues */ +.bg-light .text-light, +.bg-white .text-light, +.panel .text-light, +.card .text-light { + color: #2f3640 !important; +} + +/* Ensure proper contrast for all text elements */ +span, div, p, label, td, th, a, li { + color: inherit; +} + +/* Fix specific color classes */ +.text-success { + color: #059669 !important; /* Darker green for better readability */ +} + +.text-info { + color: #0284c7 !important; /* Darker blue for better readability */ +} + +.text-warning { + color: #d97706 !important; /* Darker orange for better readability */ +} + +.text-danger { + color: #dc2626 !important; /* Darker red for better readability */ +} + +/* Fix any Angular-specific styling */ +[ng-controller] { + color: inherit; +} + +[ng-show], +[ng-hide], +[ng-if] { + color: inherit; +} + +/* Ensure all content areas have proper text color */ +.content-box, +.panel, +.card, +.main-content, +.page-content { + color: var(--text-primary) !important; +} + +/* Fix any remaining Bootstrap classes */ +.text-dark { + color: #2f3640 !important; +} + +.text-body { + color: var(--text-primary) !important; +} + +/* Mobile-specific fixes */ +@media (max-width: 768px) { + /* Ensure mobile text is also readable */ + body, + .container, + .container-fluid { + color: var(--text-primary) !important; + } + + /* Fix mobile menu text */ + .mobile-menu .menu-item, + .mobile-menu .menu-item span { + color: var(--text-secondary) !important; + } +} + +/* Print styles */ +@media print { + body, + .content-box, + .panel, + .card { + color: #000000 !important; + background: #ffffff !important; + } + + .text-muted, + .text-secondary { + color: #000000 !important; + } +} + +/* High contrast mode support */ +@media (prefers-contrast: high) { + :root { + --text-primary: #000000; + --text-secondary: #000000; + --text-heading: #000000; + } + + [data-theme="dark"] { + --text-primary: #ffffff; + --text-secondary: #ffffff; + --text-heading: #ffffff; + } +} + +/* Reduced motion support */ +@media (prefers-reduced-motion: reduce) { + * { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } +} diff --git a/baseTemplate/static/baseTemplate/custom-js/system-status.js b/baseTemplate/static/baseTemplate/custom-js/system-status.js index fab9a6274..37720ec3e 100644 --- a/baseTemplate/static/baseTemplate/custom-js/system-status.js +++ b/baseTemplate/static/baseTemplate/custom-js/system-status.js @@ -10,7 +10,7 @@ function getCookie(name) { if (document.cookie && document.cookie !== '') { var cookies = document.cookie.split(';'); for (var i = 0; i < cookies.length; i++) { - var cookie = jQuery.trim(cookies[i]); + var cookie = (cookies[i] || '').replace(/^\s+|\s+$/g, ''); // Does this cookie string begin with the name we want? if (cookie.substring(0, name.length + 1) === (name + '=')) { cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); @@ -39,6 +39,77 @@ function randomPassword(length) { window.app = angular.module('CyberCP', []); var app = window.app; // Local reference for this file +// MUST be first: register dashboard controller before any other setup (avoids ctrlreg when CDN/Tracking Prevention blocks scripts) +app.controller('dashboardStatsController', ['$scope', '$http', '$timeout', function ($scope, $http, $timeout) { + $scope.cpuUsage = 0; $scope.ramUsage = 0; $scope.diskUsage = 0; $scope.cpuCores = 0; + $scope.ramTotalMB = 0; $scope.diskTotalGB = 0; $scope.diskFreeGB = 0; + $scope.totalUsers = 0; $scope.totalSites = 0; $scope.totalWPSites = 0; + $scope.totalDBs = 0; $scope.totalEmails = 0; $scope.totalFTPUsers = 0; + $scope.topProcesses = []; $scope.sshLogins = []; $scope.sshLogs = []; + $scope.loadingTopProcesses = true; $scope.loadingSSHLogins = true; $scope.loadingSSHLogs = true; + $scope.blockedIPs = {}; $scope.blockingIP = null; $scope.securityAlerts = []; + var opts = { headers: { 'X-CSRFToken': (typeof getCookie === 'function') ? getCookie('csrftoken') : '' } }; + try { + $http.get('/base/getSystemStatus', opts).then(function (r) { + if (r && r.data && r.data.status === 1) { + $scope.cpuUsage = r.data.cpuUsage || 0; $scope.ramUsage = r.data.ramUsage || 0; + $scope.diskUsage = r.data.diskUsage || 0; $scope.cpuCores = r.data.cpuCores || 0; + $scope.ramTotalMB = r.data.ramTotalMB || 0; $scope.diskTotalGB = r.data.diskTotalGB || 0; + $scope.diskFreeGB = r.data.diskFreeGB || 0; + } + }); + $http.get('/base/getDashboardStats', opts).then(function (r) { + if (r && r.data && r.data.status === 1) { + $scope.totalUsers = r.data.total_users || 0; $scope.totalSites = r.data.total_sites || 0; + $scope.totalWPSites = r.data.total_wp_sites || 0; $scope.totalDBs = r.data.total_dbs || 0; + $scope.totalEmails = r.data.total_emails || 0; $scope.totalFTPUsers = r.data.total_ftp_users || 0; + } + }); + $http.get('/base/getRecentSSHLogins', opts).then(function (r) { + $scope.loadingSSHLogins = false; + $scope.sshLogins = (r && r.data && r.data.logins) ? r.data.logins : []; + }, function () { $scope.loadingSSHLogins = false; $scope.sshLogins = []; }); + $http.get('/base/getRecentSSHLogs', opts).then(function (r) { + $scope.loadingSSHLogs = false; + $scope.sshLogs = (r && r.data && r.data.logs) ? r.data.logs : []; + }, function () { $scope.loadingSSHLogs = false; $scope.sshLogs = []; }); + $http.get('/base/getTopProcesses', opts).then(function (r) { + $scope.loadingTopProcesses = false; + $scope.topProcesses = (r && r.data && r.data.status === 1 && r.data.processes) ? r.data.processes : []; + }, function () { $scope.loadingTopProcesses = false; $scope.topProcesses = []; }); + if (typeof $timeout === 'function') { $timeout(function() { /* refresh */ }, 10000); } + } catch (e) { /* ignore */ } +}]); + +// Overview CPU/RAM/Disk cards use systemStatusInfo – register early so data loads even if later script fails +app.controller('systemStatusInfo', ['$scope', '$http', '$timeout', function ($scope, $http, $timeout) { + $scope.uptimeLoaded = false; + $scope.uptime = 'Loading...'; + $scope.cpuUsage = 0; $scope.ramUsage = 0; $scope.diskUsage = 0; + $scope.cpuCores = 0; $scope.ramTotalMB = 0; $scope.diskTotalGB = 0; $scope.diskFreeGB = 0; + $scope.getSystemStatus = function() { fetchStatus(); }; + function fetchStatus() { + try { + var csrf = (typeof getCookie === 'function') ? getCookie('csrftoken') : ''; + $http.get('/base/getSystemStatus', { headers: { 'X-CSRFToken': csrf } }).then(function (r) { + if (r && r.data && r.data.status === 1) { + $scope.cpuUsage = r.data.cpuUsage != null ? r.data.cpuUsage : 0; + $scope.ramUsage = r.data.ramUsage != null ? r.data.ramUsage : 0; + $scope.diskUsage = r.data.diskUsage != null ? r.data.diskUsage : 0; + $scope.cpuCores = r.data.cpuCores != null ? r.data.cpuCores : 0; + $scope.ramTotalMB = r.data.ramTotalMB != null ? r.data.ramTotalMB : 0; + $scope.diskTotalGB = r.data.diskTotalGB != null ? r.data.diskTotalGB : 0; + $scope.diskFreeGB = r.data.diskFreeGB != null ? r.data.diskFreeGB : 0; + $scope.uptime = r.data.uptime || 'N/A'; + } + $scope.uptimeLoaded = true; + }, function() { $scope.uptime = 'Unavailable'; $scope.uptimeLoaded = true; }); + if (typeof $timeout === 'function') { $timeout(fetchStatus, 60000); } + } catch (e) { $scope.uptimeLoaded = true; } + } + fetchStatus(); +}]); + var globalScope; function GlobalRespSuccess(response) { @@ -122,9 +193,17 @@ app.controller('systemStatusInfo', function ($scope, $http, $timeout) { $scope.uptimeLoaded = false; $scope.uptime = 'Loading...'; - + // Defaults so template never shows undefined (avoids raw {$ cpuUsage $} when API is slow or fails) + $scope.cpuUsage = 0; + $scope.ramUsage = 0; + $scope.diskUsage = 0; + $scope.cpuCores = 0; + $scope.ramTotalMB = 0; + $scope.diskTotalGB = 0; + $scope.diskFreeGB = 0; + getStuff(); - + $scope.getSystemStatus = function() { getStuff(); }; @@ -138,17 +217,15 @@ app.controller('systemStatusInfo', function ($scope, $http, $timeout) { function ListInitialData(response) { - $scope.cpuUsage = response.data.cpuUsage; - $scope.ramUsage = response.data.ramUsage; - $scope.diskUsage = response.data.diskUsage; - - // Total system information - $scope.cpuCores = response.data.cpuCores; - $scope.ramTotalMB = response.data.ramTotalMB; - $scope.diskTotalGB = response.data.diskTotalGB; - $scope.diskFreeGB = response.data.diskFreeGB; - - // Get uptime if available + $scope.cpuUsage = response.data.cpuUsage != null ? response.data.cpuUsage : 0; + $scope.ramUsage = response.data.ramUsage != null ? response.data.ramUsage : 0; + $scope.diskUsage = response.data.diskUsage != null ? response.data.diskUsage : 0; + + $scope.cpuCores = response.data.cpuCores != null ? response.data.cpuCores : 0; + $scope.ramTotalMB = response.data.ramTotalMB != null ? response.data.ramTotalMB : 0; + $scope.diskTotalGB = response.data.diskTotalGB != null ? response.data.diskTotalGB : 0; + $scope.diskFreeGB = response.data.diskFreeGB != null ? response.data.diskFreeGB : 0; + if (response.data.uptime) { $scope.uptime = response.data.uptime; $scope.uptimeLoaded = true; @@ -162,6 +239,9 @@ app.controller('systemStatusInfo', function ($scope, $http, $timeout) { function cantLoadInitialData(response) { $scope.uptime = 'Unavailable'; $scope.uptimeLoaded = true; + $scope.cpuUsage = 0; + $scope.ramUsage = 0; + $scope.diskUsage = 0; } $timeout(getStuff, 60000); // Update every minute @@ -273,11 +353,11 @@ app.controller('adminController', function ($scope, $http, $timeout) { } if (!Boolean(response.data.deleteZone)) { - $('.addDeleteRecords').hide(); + $('.deleteZone').hide(); } if (!Boolean(response.data.addDeleteRecords)) { - $('.deleteDatabase').hide(); + $('.addDeleteRecords').hide(); } // Email Management @@ -557,15 +637,18 @@ app.controller('homePageStatus', function ($scope, $http, $timeout) { //////////// function increment() { - $('.box').hide(); + var boxes = document.querySelectorAll ? document.querySelectorAll('.box') : []; + for (var i = 0; i < boxes.length; i++) boxes[i].style.display = 'none'; setTimeout(function () { - $('.box').show(); + for (var j = 0; j < boxes.length; j++) boxes[j].style.display = ''; }, 100); - - } -increment(); +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', increment); +} else { + increment(); +} //////////// @@ -579,6 +662,7 @@ app.controller('versionManagment', function ($scope, $http, $timeout) { $scope.updateFinish = true; $scope.couldNotConnect = true; + var upgradeStatusTimer = null; $scope.upgrade = function () { @@ -660,7 +744,8 @@ app.controller('versionManagment', function ($scope, $http, $timeout) { if (response.data.upgradeStatus === 1) { if (response.data.finished === 1) { - $timeout.cancel(); + if (upgradeStatusTimer) $timeout.cancel(upgradeStatusTimer); + upgradeStatusTimer = null; $scope.upgradelogBox = false; $scope.upgradeLog = response.data.upgradeLog; $scope.upgradeLoading = true; @@ -672,7 +757,7 @@ app.controller('versionManagment', function ($scope, $http, $timeout) { } else { $scope.upgradelogBox = false; $scope.upgradeLog = response.data.upgradeLog; - timeout(getUpgradeStatus, 2000); + upgradeStatusTimer = $timeout(getUpgradeStatus, 2000); } } @@ -900,7 +985,8 @@ app.controller('OnboardingCP', function ($scope, $http, $timeout, $window) { }); -app.controller('dashboardStatsController', function ($scope, $http, $timeout) { +// Single implementation registered under both names for compatibility (some templates/caches use newDashboardStat) +var dashboardStatsControllerFn = function ($scope, $http, $timeout) { console.log('dashboardStatsController initialized'); // Card values @@ -920,7 +1006,8 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { $scope.errorTopProcesses = ''; $scope.refreshTopProcesses = function() { $scope.loadingTopProcesses = true; - $http.get('/base/getTopProcesses').then(function (response) { + var h = { headers: { 'X-CSRFToken': (typeof getCookie === 'function') ? getCookie('csrftoken') : '' } }; + $http.get('/base/getTopProcesses', h).then(function (response) { $scope.loadingTopProcesses = false; if (response.data && response.data.status === 1 && response.data.processes) { $scope.topProcesses = response.data.processes; @@ -939,7 +1026,8 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { $scope.errorSSHLogins = ''; $scope.refreshSSHLogins = function() { $scope.loadingSSHLogins = true; - $http.get('/base/getRecentSSHLogins').then(function (response) { + var h = { headers: { 'X-CSRFToken': (typeof getCookie === 'function') ? getCookie('csrftoken') : '' } }; + $http.get('/base/getRecentSSHLogins', h).then(function (response) { $scope.loadingSSHLogins = false; if (response.data && response.data.logins) { $scope.sshLogins = response.data.logins; @@ -967,7 +1055,8 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { $scope.loadingSecurityAnalysis = false; $scope.refreshSSHLogs = function() { $scope.loadingSSHLogs = true; - $http.get('/base/getRecentSSHLogs').then(function (response) { + var h = { headers: { 'X-CSRFToken': (typeof getCookie === 'function') ? getCookie('csrftoken') : '' } }; + $http.get('/base/getRecentSSHLogs', h).then(function (response) { $scope.loadingSSHLogs = false; if (response.data && response.data.logs) { $scope.sshLogs = response.data.logs; @@ -1073,19 +1162,8 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { return; // Already processing this IP } - // Check if already blocked - if ($scope.blockedIPs && $scope.blockedIPs[ipAddress]) { - console.log('IP already blocked:', ipAddress); - if (typeof PNotify !== 'undefined') { - new PNotify({ - title: 'Info', - text: `IP address ${ipAddress} is already banned`, - type: 'info', - delay: 3000 - }); - } - return; - } + // Do not early-return when IP is already in blockedIPs: still call the API so the + // backend can close any active connections from this IP (already-banned path). // Set blocking flag to prevent duplicate requests $scope.blockingIP = ipAddress; @@ -1130,19 +1208,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; } } @@ -1159,11 +1230,12 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { } $scope.blockedIPs[ipAddress] = true; - // Show success notification + // Show success notification (use server message when present, e.g. already-banned + connections closed) if (typeof PNotify !== 'undefined') { + var successText = (responseData.message && responseData.message.length) ? responseData.message : `IP address ${ipAddress} has been permanently banned and added to the firewall. You can manage it in the Firewall > Banned IPs section.`; new PNotify({ title: 'IP Address Banned', - text: `IP address ${ipAddress} has been permanently banned and added to the firewall. You can manage it in the Firewall > Banned IPs section.`, + text: successText, type: 'success', delay: 5000 }); @@ -1217,28 +1289,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); @@ -1287,14 +1351,9 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { return; // Already processing } - if ($scope.blockedIPs[ipAddress]) { - new PNotify({ - title: 'Info', - text: `IP address ${ipAddress} is already banned`, - type: 'info', - delay: 3000 - }); - return; + // Still call API when already in blockedIPs so backend can close active connections + if (!$scope.blockedIPs) { + $scope.blockedIPs = {}; } $scope.blockingIP = ipAddress; @@ -1380,14 +1439,9 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { return; // Already processing } - if ($scope.blockedIPs[ipAddress]) { - new PNotify({ - title: 'Info', - text: `IP address ${ipAddress} is already banned`, - type: 'info', - delay: 3000 - }); - return; + // Still call API when already in blockedIPs so backend can close active connections + if (!$scope.blockedIPs) { + $scope.blockedIPs = {}; } $scope.blockingIP = ipAddress; @@ -1474,6 +1528,12 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { var pollInterval = 2000; // ms var maxPoints = 30; + // Expose so switchTab can create charts on first tab click if they weren't created at load + window.cyberPanelSetupChartsIfNeeded = function() { + if (window.trafficChart && window.diskIOChart && window.cpuChart) return; + try { setupCharts(); } catch (e) { console.error('cyberPanelSetupChartsIfNeeded:', e); } + }; + function pollDashboardStats() { console.log('[dashboardStatsController] pollDashboardStats() called'); console.log('[dashboardStatsController] Fetching dashboard stats from /base/getDashboardStats'); @@ -1532,8 +1592,8 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { } function pollTraffic() { - console.log('pollTraffic called'); $http.get('/base/getTrafficStats').then(function(response) { + if (!response || !response.data) return; if (response.data.admin_only) { // Hide chart for non-admin users $scope.hideSystemCharts = true; @@ -1581,13 +1641,16 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { } lastRx = rx; lastTx = tx; } else { - console.log('pollTraffic error or no data:', response); + console.warn('pollTraffic: no data or status', response.data); } + }).catch(function(err) { + console.warn('pollTraffic failed:', err); }); } function pollDiskIO() { $http.get('/base/getDiskIOStats').then(function(response) { + if (!response || !response.data) return; if (response.data.admin_only) { // Hide chart for non-admin users $scope.hideSystemCharts = true; @@ -1626,11 +1689,14 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { } lastDiskRead = read; lastDiskWrite = write; } + }).catch(function(err) { + console.warn('pollDiskIO failed:', err); }); } function pollCPU() { $http.get('/base/getCPULoadGraph').then(function(response) { + if (!response || !response.data) return; if (response.data.admin_only) { // Hide chart for non-admin users $scope.hideSystemCharts = true; @@ -1669,13 +1735,34 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { } lastCPUTimes = cpuTimes; } + }).catch(function(err) { + console.warn('pollCPU failed:', err); }); } - function setupCharts() { - console.log('setupCharts called, initializing charts...'); - var trafficCtx = document.getElementById('trafficChart').getContext('2d'); - trafficChart = new Chart(trafficCtx, { + function setupCharts(retryCount) { + retryCount = retryCount || 0; + if (typeof Chart === 'undefined') { + if (retryCount < 3) { + $timeout(function() { setupCharts(retryCount + 1); }, 400); + } + return; + } + var trafficEl = document.getElementById('trafficChart'); + if (!trafficEl) { + if (retryCount < 5) { + $timeout(function() { setupCharts(retryCount + 1); }, 300); + } + return; + } + try { + var trafficCtx = trafficEl.getContext('2d'); + } catch (e) { + console.error('trafficChart getContext failed:', e); + return; + } + try { + trafficChart = new Chart(trafficCtx, { type: 'line', data: { labels: [], @@ -1767,7 +1854,9 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { console.log('trafficChart resized and updated after setup.'); } }, 500); - var diskCtx = document.getElementById('diskIOChart').getContext('2d'); + var diskEl = document.getElementById('diskIOChart'); + if (!diskEl) { console.warn('diskIOChart canvas not found'); return; } + var diskCtx = diskEl.getContext('2d'); diskIOChart = new Chart(diskCtx, { type: 'line', data: { @@ -1852,7 +1941,10 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { layout: { padding: { top: 10, bottom: 10, left: 10, right: 10 } } } }); - var cpuCtx = document.getElementById('cpuChart').getContext('2d'); + window.diskIOChart = diskIOChart; + var cpuEl = document.getElementById('cpuChart'); + if (!cpuEl) { console.warn('cpuChart canvas not found'); return; } + var cpuCtx = cpuEl.getContext('2d'); cpuChart = new Chart(cpuCtx, { type: 'line', data: { @@ -1925,6 +2017,10 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { layout: { padding: { top: 10, bottom: 10, left: 10, right: 10 } } } }); + window.cpuChart = cpuChart; + } catch (e) { + console.error('setupCharts error:', e); + } // Redraw charts on tab shown $("a[data-toggle='tab']").on('shown.bs.tab', function (e) { @@ -1957,19 +2053,20 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { $scope.refreshSSHLogs(); $timeout(function() { - // Check if user is admin before setting up charts + // Always create charts so Traffic/Disk IO/CPU tabs have something to show; admin check only affects hideSystemCharts + setupCharts(); $http.get('/base/getAdminStatus').then(function(response) { - if (response.data && response.data.admin === 1) { - setupCharts(); + if (response.data && (response.data.admin === 1 || response.data.admin === true)) { + $scope.hideSystemCharts = false; } else { $scope.hideSystemCharts = true; } - }).catch(function() { - // If error, assume non-admin and hide charts + }).catch(function(err) { + console.warn('getAdminStatus failed:', err); $scope.hideSystemCharts = true; }); - // Start polling for all stats + // Start polling for all stats (data feeds charts) function pollAll() { pollDashboardStats(); pollTraffic(); @@ -1979,7 +2076,7 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { $timeout(pollAll, pollInterval); } pollAll(); - }, 500); + }, 800); // SSH User Activity Modal $scope.showSSHActivityModal = false; @@ -2425,4 +2522,6 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { $scope.closeSSHActivityModal(); } }; -}); \ No newline at end of file +}; +app.controller('dashboardStatsController', dashboardStatsControllerFn); +app.controller('newDashboardStat', dashboardStatsControllerFn); \ No newline at end of file diff --git a/baseTemplate/static/baseTemplate/vendor/select2/select2.full.min.js b/baseTemplate/static/baseTemplate/vendor/select2/select2.full.min.js new file mode 100644 index 000000000..683301daf --- /dev/null +++ b/baseTemplate/static/baseTemplate/vendor/select2/select2.full.min.js @@ -0,0 +1,2 @@ +/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ +!function(n){"function"==typeof define&&define.amd?define(["jquery"],n):"object"==typeof module&&module.exports?module.exports=function(e,t){return void 0===t&&(t="undefined"!=typeof window?require("jquery"):require("jquery")(e)),n(t),t}:n(jQuery)}(function(d){var e=function(){if(d&&d.fn&&d.fn.select2&&d.fn.select2.amd)var e=d.fn.select2.amd;var t,n,i,h,o,s,f,g,m,v,y,_,r,a,w,l;function b(e,t){return r.call(e,t)}function c(e,t){var n,i,r,o,s,a,l,c,u,d,p,h=t&&t.split("/"),f=y.map,g=f&&f["*"]||{};if(e){for(s=(e=e.split("/")).length-1,y.nodeIdCompat&&w.test(e[s])&&(e[s]=e[s].replace(w,"")),"."===e[0].charAt(0)&&h&&(e=h.slice(0,h.length-1).concat(e)),u=0;u":">",'"':""","'":"'","/":"/"};return"string"!=typeof e?e:String(e).replace(/[&<>"'\/\\]/g,function(e){return t[e]})},r.appendMany=function(e,t){if("1.7"===o.fn.jquery.substr(0,3)){var n=o();o.map(t,function(e){n=n.add(e)}),t=n}e.append(t)},r.__cache={};var n=0;return r.GetUniqueElementId=function(e){var t=e.getAttribute("data-select2-id");return null==t&&(e.id?(t=e.id,e.setAttribute("data-select2-id",t)):(e.setAttribute("data-select2-id",++n),t=n.toString())),t},r.StoreData=function(e,t,n){var i=r.GetUniqueElementId(e);r.__cache[i]||(r.__cache[i]={}),r.__cache[i][t]=n},r.GetData=function(e,t){var n=r.GetUniqueElementId(e);return t?r.__cache[n]&&null!=r.__cache[n][t]?r.__cache[n][t]:o(e).data(t):r.__cache[n]},r.RemoveData=function(e){var t=r.GetUniqueElementId(e);null!=r.__cache[t]&&delete r.__cache[t],e.removeAttribute("data-select2-id")},r}),e.define("select2/results",["jquery","./utils"],function(h,f){function i(e,t,n){this.$element=e,this.data=n,this.options=t,i.__super__.constructor.call(this)}return f.Extend(i,f.Observable),i.prototype.render=function(){var e=h('
    ');return this.options.get("multiple")&&e.attr("aria-multiselectable","true"),this.$results=e},i.prototype.clear=function(){this.$results.empty()},i.prototype.displayMessage=function(e){var t=this.options.get("escapeMarkup");this.clear(),this.hideLoading();var n=h(''),i=this.options.get("translations").get(e.message);n.append(t(i(e.args))),n[0].className+=" select2-results__message",this.$results.append(n)},i.prototype.hideMessages=function(){this.$results.find(".select2-results__message").remove()},i.prototype.append=function(e){this.hideLoading();var t=[];if(null!=e.results&&0!==e.results.length){e.results=this.sort(e.results);for(var n=0;n",{class:"select2-results__options select2-results__options--nested"});p.append(l),s.append(a),s.append(p)}else this.template(e,t);return f.StoreData(t,"data",e),t},i.prototype.bind=function(t,e){var l=this,n=t.id+"-results";this.$results.attr("id",n),t.on("results:all",function(e){l.clear(),l.append(e.data),t.isOpen()&&(l.setClasses(),l.highlightFirstItem())}),t.on("results:append",function(e){l.append(e.data),t.isOpen()&&l.setClasses()}),t.on("query",function(e){l.hideMessages(),l.showLoading(e)}),t.on("select",function(){t.isOpen()&&(l.setClasses(),l.options.get("scrollAfterSelect")&&l.highlightFirstItem())}),t.on("unselect",function(){t.isOpen()&&(l.setClasses(),l.options.get("scrollAfterSelect")&&l.highlightFirstItem())}),t.on("open",function(){l.$results.attr("aria-expanded","true"),l.$results.attr("aria-hidden","false"),l.setClasses(),l.ensureHighlightVisible()}),t.on("close",function(){l.$results.attr("aria-expanded","false"),l.$results.attr("aria-hidden","true"),l.$results.removeAttr("aria-activedescendant")}),t.on("results:toggle",function(){var e=l.getHighlightedResults();0!==e.length&&e.trigger("mouseup")}),t.on("results:select",function(){var e=l.getHighlightedResults();if(0!==e.length){var t=f.GetData(e[0],"data");"true"==e.attr("aria-selected")?l.trigger("close",{}):l.trigger("select",{data:t})}}),t.on("results:previous",function(){var e=l.getHighlightedResults(),t=l.$results.find("[aria-selected]"),n=t.index(e);if(!(n<=0)){var i=n-1;0===e.length&&(i=0);var r=t.eq(i);r.trigger("mouseenter");var o=l.$results.offset().top,s=r.offset().top,a=l.$results.scrollTop()+(s-o);0===i?l.$results.scrollTop(0):s-o<0&&l.$results.scrollTop(a)}}),t.on("results:next",function(){var e=l.getHighlightedResults(),t=l.$results.find("[aria-selected]"),n=t.index(e)+1;if(!(n>=t.length)){var i=t.eq(n);i.trigger("mouseenter");var r=l.$results.offset().top+l.$results.outerHeight(!1),o=i.offset().top+i.outerHeight(!1),s=l.$results.scrollTop()+o-r;0===n?l.$results.scrollTop(0):rthis.$results.outerHeight()||o<0)&&this.$results.scrollTop(r)}},i.prototype.template=function(e,t){var n=this.options.get("templateResult"),i=this.options.get("escapeMarkup"),r=n(e,t);null==r?t.style.display="none":"string"==typeof r?t.innerHTML=i(r):h(t).append(r)},i}),e.define("select2/keys",[],function(){return{BACKSPACE:8,TAB:9,ENTER:13,SHIFT:16,CTRL:17,ALT:18,ESC:27,SPACE:32,PAGE_UP:33,PAGE_DOWN:34,END:35,HOME:36,LEFT:37,UP:38,RIGHT:39,DOWN:40,DELETE:46}}),e.define("select2/selection/base",["jquery","../utils","../keys"],function(n,i,r){function o(e,t){this.$element=e,this.options=t,o.__super__.constructor.call(this)}return i.Extend(o,i.Observable),o.prototype.render=function(){var e=n('');return this._tabindex=0,null!=i.GetData(this.$element[0],"old-tabindex")?this._tabindex=i.GetData(this.$element[0],"old-tabindex"):null!=this.$element.attr("tabindex")&&(this._tabindex=this.$element.attr("tabindex")),e.attr("title",this.$element.attr("title")),e.attr("tabindex",this._tabindex),e.attr("aria-disabled","false"),this.$selection=e},o.prototype.bind=function(e,t){var n=this,i=e.id+"-results";this.container=e,this.$selection.on("focus",function(e){n.trigger("focus",e)}),this.$selection.on("blur",function(e){n._handleBlur(e)}),this.$selection.on("keydown",function(e){n.trigger("keypress",e),e.which===r.SPACE&&e.preventDefault()}),e.on("results:focus",function(e){n.$selection.attr("aria-activedescendant",e.data._resultId)}),e.on("selection:update",function(e){n.update(e.data)}),e.on("open",function(){n.$selection.attr("aria-expanded","true"),n.$selection.attr("aria-owns",i),n._attachCloseHandler(e)}),e.on("close",function(){n.$selection.attr("aria-expanded","false"),n.$selection.removeAttr("aria-activedescendant"),n.$selection.removeAttr("aria-owns"),n.$selection.trigger("focus"),n._detachCloseHandler(e)}),e.on("enable",function(){n.$selection.attr("tabindex",n._tabindex),n.$selection.attr("aria-disabled","false")}),e.on("disable",function(){n.$selection.attr("tabindex","-1"),n.$selection.attr("aria-disabled","true")})},o.prototype._handleBlur=function(e){var t=this;window.setTimeout(function(){document.activeElement==t.$selection[0]||n.contains(t.$selection[0],document.activeElement)||t.trigger("blur",e)},1)},o.prototype._attachCloseHandler=function(e){n(document.body).on("mousedown.select2."+e.id,function(e){var t=n(e.target).closest(".select2");n(".select2.select2-container--open").each(function(){this!=t[0]&&i.GetData(this,"element").select2("close")})})},o.prototype._detachCloseHandler=function(e){n(document.body).off("mousedown.select2."+e.id)},o.prototype.position=function(e,t){t.find(".selection").append(e)},o.prototype.destroy=function(){this._detachCloseHandler(this.container)},o.prototype.update=function(e){throw new Error("The `update` method must be defined in child classes.")},o.prototype.isEnabled=function(){return!this.isDisabled()},o.prototype.isDisabled=function(){return this.options.get("disabled")},o}),e.define("select2/selection/single",["jquery","./base","../utils","../keys"],function(e,t,n,i){function r(){r.__super__.constructor.apply(this,arguments)}return n.Extend(r,t),r.prototype.render=function(){var e=r.__super__.render.call(this);return e.addClass("select2-selection--single"),e.html(''),e},r.prototype.bind=function(t,e){var n=this;r.__super__.bind.apply(this,arguments);var i=t.id+"-container";this.$selection.find(".select2-selection__rendered").attr("id",i).attr("role","textbox").attr("aria-readonly","true"),this.$selection.attr("aria-labelledby",i),this.$selection.on("mousedown",function(e){1===e.which&&n.trigger("toggle",{originalEvent:e})}),this.$selection.on("focus",function(e){}),this.$selection.on("blur",function(e){}),t.on("focus",function(e){t.isOpen()||n.$selection.trigger("focus")})},r.prototype.clear=function(){var e=this.$selection.find(".select2-selection__rendered");e.empty(),e.removeAttr("title")},r.prototype.display=function(e,t){var n=this.options.get("templateSelection");return this.options.get("escapeMarkup")(n(e,t))},r.prototype.selectionContainer=function(){return e("")},r.prototype.update=function(e){if(0!==e.length){var t=e[0],n=this.$selection.find(".select2-selection__rendered"),i=this.display(t,n);n.empty().append(i);var r=t.title||t.text;r?n.attr("title",r):n.removeAttr("title")}else this.clear()},r}),e.define("select2/selection/multiple",["jquery","./base","../utils"],function(r,e,l){function n(e,t){n.__super__.constructor.apply(this,arguments)}return l.Extend(n,e),n.prototype.render=function(){var e=n.__super__.render.call(this);return e.addClass("select2-selection--multiple"),e.html('
      '),e},n.prototype.bind=function(e,t){var i=this;n.__super__.bind.apply(this,arguments),this.$selection.on("click",function(e){i.trigger("toggle",{originalEvent:e})}),this.$selection.on("click",".select2-selection__choice__remove",function(e){if(!i.isDisabled()){var t=r(this).parent(),n=l.GetData(t[0],"data");i.trigger("unselect",{originalEvent:e,data:n})}})},n.prototype.clear=function(){var e=this.$selection.find(".select2-selection__rendered");e.empty(),e.removeAttr("title")},n.prototype.display=function(e,t){var n=this.options.get("templateSelection");return this.options.get("escapeMarkup")(n(e,t))},n.prototype.selectionContainer=function(){return r('
    • ×
    • ')},n.prototype.update=function(e){if(this.clear(),0!==e.length){for(var t=[],n=0;n×');a.StoreData(i[0],"data",t),this.$selection.find(".select2-selection__rendered").prepend(i)}},e}),e.define("select2/selection/search",["jquery","../utils","../keys"],function(i,a,l){function e(e,t,n){e.call(this,t,n)}return e.prototype.render=function(e){var t=i('');this.$searchContainer=t,this.$search=t.find("input");var n=e.call(this);return this._transferTabIndex(),n},e.prototype.bind=function(e,t,n){var i=this,r=t.id+"-results";e.call(this,t,n),t.on("open",function(){i.$search.attr("aria-controls",r),i.$search.trigger("focus")}),t.on("close",function(){i.$search.val(""),i.$search.removeAttr("aria-controls"),i.$search.removeAttr("aria-activedescendant"),i.$search.trigger("focus")}),t.on("enable",function(){i.$search.prop("disabled",!1),i._transferTabIndex()}),t.on("disable",function(){i.$search.prop("disabled",!0)}),t.on("focus",function(e){i.$search.trigger("focus")}),t.on("results:focus",function(e){e.data._resultId?i.$search.attr("aria-activedescendant",e.data._resultId):i.$search.removeAttr("aria-activedescendant")}),this.$selection.on("focusin",".select2-search--inline",function(e){i.trigger("focus",e)}),this.$selection.on("focusout",".select2-search--inline",function(e){i._handleBlur(e)}),this.$selection.on("keydown",".select2-search--inline",function(e){if(e.stopPropagation(),i.trigger("keypress",e),i._keyUpPrevented=e.isDefaultPrevented(),e.which===l.BACKSPACE&&""===i.$search.val()){var t=i.$searchContainer.prev(".select2-selection__choice");if(0this.maximumInputLength?this.trigger("results:message",{message:"inputTooLong",args:{maximum:this.maximumInputLength,input:t.term,params:t}}):e.call(this,t,n)},e}),e.define("select2/data/maximumSelectionLength",[],function(){function e(e,t,n){this.maximumSelectionLength=n.get("maximumSelectionLength"),e.call(this,t,n)}return e.prototype.bind=function(e,t,n){var i=this;e.call(this,t,n),t.on("select",function(){i._checkIfMaximumSelected()})},e.prototype.query=function(e,t,n){var i=this;this._checkIfMaximumSelected(function(){e.call(i,t,n)})},e.prototype._checkIfMaximumSelected=function(e,n){var i=this;this.current(function(e){var t=null!=e?e.length:0;0=i.maximumSelectionLength?i.trigger("results:message",{message:"maximumSelected",args:{maximum:i.maximumSelectionLength}}):n&&n()})},e}),e.define("select2/dropdown",["jquery","./utils"],function(t,e){function n(e,t){this.$element=e,this.options=t,n.__super__.constructor.call(this)}return e.Extend(n,e.Observable),n.prototype.render=function(){var e=t('');return e.attr("dir",this.options.get("dir")),this.$dropdown=e},n.prototype.bind=function(){},n.prototype.position=function(e,t){},n.prototype.destroy=function(){this.$dropdown.remove()},n}),e.define("select2/dropdown/search",["jquery","../utils"],function(o,e){function t(){}return t.prototype.render=function(e){var t=e.call(this),n=o('');return this.$searchContainer=n,this.$search=n.find("input"),t.prepend(n),t},t.prototype.bind=function(e,t,n){var i=this,r=t.id+"-results";e.call(this,t,n),this.$search.on("keydown",function(e){i.trigger("keypress",e),i._keyUpPrevented=e.isDefaultPrevented()}),this.$search.on("input",function(e){o(this).off("keyup")}),this.$search.on("keyup input",function(e){i.handleSearch(e)}),t.on("open",function(){i.$search.attr("tabindex",0),i.$search.attr("aria-controls",r),i.$search.trigger("focus"),window.setTimeout(function(){i.$search.trigger("focus")},0)}),t.on("close",function(){i.$search.attr("tabindex",-1),i.$search.removeAttr("aria-controls"),i.$search.removeAttr("aria-activedescendant"),i.$search.val(""),i.$search.trigger("blur")}),t.on("focus",function(){t.isOpen()||i.$search.trigger("focus")}),t.on("results:all",function(e){null!=e.query.term&&""!==e.query.term||(i.showSearch(e)?i.$searchContainer.removeClass("select2-search--hide"):i.$searchContainer.addClass("select2-search--hide"))}),t.on("results:focus",function(e){e.data._resultId?i.$search.attr("aria-activedescendant",e.data._resultId):i.$search.removeAttr("aria-activedescendant")})},t.prototype.handleSearch=function(e){if(!this._keyUpPrevented){var t=this.$search.val();this.trigger("query",{term:t})}this._keyUpPrevented=!1},t.prototype.showSearch=function(e,t){return!0},t}),e.define("select2/dropdown/hidePlaceholder",[],function(){function e(e,t,n,i){this.placeholder=this.normalizePlaceholder(n.get("placeholder")),e.call(this,t,n,i)}return e.prototype.append=function(e,t){t.results=this.removePlaceholder(t.results),e.call(this,t)},e.prototype.normalizePlaceholder=function(e,t){return"string"==typeof t&&(t={id:"",text:t}),t},e.prototype.removePlaceholder=function(e,t){for(var n=t.slice(0),i=t.length-1;0<=i;i--){var r=t[i];this.placeholder.id===r.id&&n.splice(i,1)}return n},e}),e.define("select2/dropdown/infiniteScroll",["jquery"],function(n){function e(e,t,n,i){this.lastParams={},e.call(this,t,n,i),this.$loadingMore=this.createLoadingMore(),this.loading=!1}return e.prototype.append=function(e,t){this.$loadingMore.remove(),this.loading=!1,e.call(this,t),this.showLoadingMore(t)&&(this.$results.append(this.$loadingMore),this.loadMoreIfNeeded())},e.prototype.bind=function(e,t,n){var i=this;e.call(this,t,n),t.on("query",function(e){i.lastParams=e,i.loading=!0}),t.on("query:append",function(e){i.lastParams=e,i.loading=!0}),this.$results.on("scroll",this.loadMoreIfNeeded.bind(this))},e.prototype.loadMoreIfNeeded=function(){var e=n.contains(document.documentElement,this.$loadingMore[0]);if(!this.loading&&e){var t=this.$results.offset().top+this.$results.outerHeight(!1);this.$loadingMore.offset().top+this.$loadingMore.outerHeight(!1)<=t+50&&this.loadMore()}},e.prototype.loadMore=function(){this.loading=!0;var e=n.extend({},{page:1},this.lastParams);e.page++,this.trigger("query:append",e)},e.prototype.showLoadingMore=function(e,t){return t.pagination&&t.pagination.more},e.prototype.createLoadingMore=function(){var e=n('
    • '),t=this.options.get("translations").get("loadingMore");return e.html(t(this.lastParams)),e},e}),e.define("select2/dropdown/attachBody",["jquery","../utils"],function(f,a){function e(e,t,n){this.$dropdownParent=f(n.get("dropdownParent")||document.body),e.call(this,t,n)}return e.prototype.bind=function(e,t,n){var i=this;e.call(this,t,n),t.on("open",function(){i._showDropdown(),i._attachPositioningHandler(t),i._bindContainerResultHandlers(t)}),t.on("close",function(){i._hideDropdown(),i._detachPositioningHandler(t)}),this.$dropdownContainer.on("mousedown",function(e){e.stopPropagation()})},e.prototype.destroy=function(e){e.call(this),this.$dropdownContainer.remove()},e.prototype.position=function(e,t,n){t.attr("class",n.attr("class")),t.removeClass("select2"),t.addClass("select2-container--open"),t.css({position:"absolute",top:-999999}),this.$container=n},e.prototype.render=function(e){var t=f(""),n=e.call(this);return t.append(n),this.$dropdownContainer=t},e.prototype._hideDropdown=function(e){this.$dropdownContainer.detach()},e.prototype._bindContainerResultHandlers=function(e,t){if(!this._containerResultsHandlersBound){var n=this;t.on("results:all",function(){n._positionDropdown(),n._resizeDropdown()}),t.on("results:append",function(){n._positionDropdown(),n._resizeDropdown()}),t.on("results:message",function(){n._positionDropdown(),n._resizeDropdown()}),t.on("select",function(){n._positionDropdown(),n._resizeDropdown()}),t.on("unselect",function(){n._positionDropdown(),n._resizeDropdown()}),this._containerResultsHandlersBound=!0}},e.prototype._attachPositioningHandler=function(e,t){var n=this,i="scroll.select2."+t.id,r="resize.select2."+t.id,o="orientationchange.select2."+t.id,s=this.$container.parents().filter(a.hasScroll);s.each(function(){a.StoreData(this,"select2-scroll-position",{x:f(this).scrollLeft(),y:f(this).scrollTop()})}),s.on(i,function(e){var t=a.GetData(this,"select2-scroll-position");f(this).scrollTop(t.y)}),f(window).on(i+" "+r+" "+o,function(e){n._positionDropdown(),n._resizeDropdown()})},e.prototype._detachPositioningHandler=function(e,t){var n="scroll.select2."+t.id,i="resize.select2."+t.id,r="orientationchange.select2."+t.id;this.$container.parents().filter(a.hasScroll).off(n),f(window).off(n+" "+i+" "+r)},e.prototype._positionDropdown=function(){var e=f(window),t=this.$dropdown.hasClass("select2-dropdown--above"),n=this.$dropdown.hasClass("select2-dropdown--below"),i=null,r=this.$container.offset();r.bottom=r.top+this.$container.outerHeight(!1);var o={height:this.$container.outerHeight(!1)};o.top=r.top,o.bottom=r.top+o.height;var s=this.$dropdown.outerHeight(!1),a=e.scrollTop(),l=e.scrollTop()+e.height(),c=ar.bottom+s,d={left:r.left,top:o.bottom},p=this.$dropdownParent;"static"===p.css("position")&&(p=p.offsetParent());var h={top:0,left:0};(f.contains(document.body,p[0])||p[0].isConnected)&&(h=p.offset()),d.top-=h.top,d.left-=h.left,t||n||(i="below"),u||!c||t?!c&&u&&t&&(i="below"):i="above",("above"==i||t&&"below"!==i)&&(d.top=o.top-h.top-s),null!=i&&(this.$dropdown.removeClass("select2-dropdown--below select2-dropdown--above").addClass("select2-dropdown--"+i),this.$container.removeClass("select2-container--below select2-container--above").addClass("select2-container--"+i)),this.$dropdownContainer.css(d)},e.prototype._resizeDropdown=function(){var e={width:this.$container.outerWidth(!1)+"px"};this.options.get("dropdownAutoWidth")&&(e.minWidth=e.width,e.position="relative",e.width="auto"),this.$dropdown.css(e)},e.prototype._showDropdown=function(e){this.$dropdownContainer.appendTo(this.$dropdownParent),this._positionDropdown(),this._resizeDropdown()},e}),e.define("select2/dropdown/minimumResultsForSearch",[],function(){function e(e,t,n,i){this.minimumResultsForSearch=n.get("minimumResultsForSearch"),this.minimumResultsForSearch<0&&(this.minimumResultsForSearch=1/0),e.call(this,t,n,i)}return e.prototype.showSearch=function(e,t){return!(function e(t){for(var n=0,i=0;i');return e.attr("dir",this.options.get("dir")),this.$container=e,this.$container.addClass("select2-container--"+this.options.get("theme")),u.StoreData(e[0],"element",this.$element),e},d}),e.define("select2/compat/utils",["jquery"],function(s){return{syncCssClasses:function(e,t,n){var i,r,o=[];(i=s.trim(e.attr("class")))&&s((i=""+i).split(/\s+/)).each(function(){0===this.indexOf("select2-")&&o.push(this)}),(i=s.trim(t.attr("class")))&&s((i=""+i).split(/\s+/)).each(function(){0!==this.indexOf("select2-")&&null!=(r=n(this))&&o.push(r)}),e.attr("class",o.join(" "))}}}),e.define("select2/compat/containerCss",["jquery","./utils"],function(s,a){function l(e){return null}function e(){}return e.prototype.render=function(e){var t=e.call(this),n=this.options.get("containerCssClass")||"";s.isFunction(n)&&(n=n(this.$element));var i=this.options.get("adaptContainerCssClass");if(i=i||l,-1!==n.indexOf(":all:")){n=n.replace(":all:","");var r=i;i=function(e){var t=r(e);return null!=t?t+" "+e:e}}var o=this.options.get("containerCss")||{};return s.isFunction(o)&&(o=o(this.$element)),a.syncCssClasses(t,this.$element,i),t.css(o),t.addClass(n),t},e}),e.define("select2/compat/dropdownCss",["jquery","./utils"],function(s,a){function l(e){return null}function e(){}return e.prototype.render=function(e){var t=e.call(this),n=this.options.get("dropdownCssClass")||"";s.isFunction(n)&&(n=n(this.$element));var i=this.options.get("adaptDropdownCssClass");if(i=i||l,-1!==n.indexOf(":all:")){n=n.replace(":all:","");var r=i;i=function(e){var t=r(e);return null!=t?t+" "+e:e}}var o=this.options.get("dropdownCss")||{};return s.isFunction(o)&&(o=o(this.$element)),a.syncCssClasses(t,this.$element,i),t.css(o),t.addClass(n),t},e}),e.define("select2/compat/initSelection",["jquery"],function(i){function e(e,t,n){n.get("debug")&&window.console&&console.warn&&console.warn("Select2: The `initSelection` option has been deprecated in favor of a custom data adapter that overrides the `current` method. This method is now called multiple times instead of a single time when the instance is initialized. Support will be removed for the `initSelection` option in future versions of Select2"),this.initSelection=n.get("initSelection"),this._isInitialized=!1,e.call(this,t,n)}return e.prototype.current=function(e,t){var n=this;this._isInitialized?e.call(this,t):this.initSelection.call(null,this.$element,function(e){n._isInitialized=!0,i.isArray(e)||(e=[e]),t(e)})},e}),e.define("select2/compat/inputData",["jquery","../utils"],function(s,i){function e(e,t,n){this._currentData=[],this._valueSeparator=n.get("valueSeparator")||",","hidden"===t.prop("type")&&n.get("debug")&&console&&console.warn&&console.warn("Select2: Using a hidden input with Select2 is no longer supported and may stop working in the future. It is recommended to use a ` + + + + + + per page + +
      + {$ (sshLoginsPage - 1) * sshLoginsPerPage + 1 $}-{$ (sshLoginsPage * sshLoginsPerPage > sshLoginsTotal ? sshLoginsTotal : sshLoginsPage * sshLoginsPerPage) $} of {$ sshLoginsTotal $} + + +
      + + @@ -1065,6 +1088,29 @@
      + + +
      +
      + Show + + per page +
      +
      + {$ (sshLogsPage - 1) * sshLogsPerPage + 1 $}-{$ (sshLogsPage * sshLogsPerPage > sshLogsTotal ? sshLogsTotal : sshLogsPage * sshLogsPerPage) $} of {$ sshLogsTotal $} + + +
      +
      @@ -1311,11 +1357,25 @@ // Add active class to clicked tab tabButton.classList.add('active'); - // Trigger chart resize if switching to chart tabs + // Chart tabs: ensure charts exist (lazy init on first click), then resize/update if (tabId === 'traffic' || tabId === 'diskio' || tabId === 'cpu-usage') { + if (typeof window.cyberPanelSetupChartsIfNeeded === 'function') { + window.cyberPanelSetupChartsIfNeeded(); + } setTimeout(() => { + var ch; + if (tabId === 'traffic' && (ch = window.trafficChart) && typeof ch.resize === 'function') { + ch.resize(); + if (typeof ch.update === 'function') ch.update(); + } else if (tabId === 'diskio' && (ch = window.diskIOChart) && typeof ch.resize === 'function') { + ch.resize(); + if (typeof ch.update === 'function') ch.update(); + } else if (tabId === 'cpu-usage' && (ch = window.cpuChart) && typeof ch.resize === 'function') { + ch.resize(); + if (typeof ch.update === 'function') ch.update(); + } window.dispatchEvent(new Event('resize')); - }, 100); + }, 200); } } @@ -1334,56 +1394,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 ec310faaf..b075692b2 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" %} @@ -26,29 +26,25 @@ - + + - - - + - - - - - - + + +
      @@ -860,6 +959,12 @@ {% trans "DNS Records" %} +
      + + + +
      +
      Loading DNS records...
      @@ -868,29 +973,63 @@ No DNS records found.
      +
      + {% trans "No records match your search." %} +
      + +
      - - - - - - + + + + + + - - - + + + + + - - -
      {% trans "Name" %}{% trans "Type" %}{% trans "TTL" %}{% trans "Value" %}{% trans "Priority" %}{% trans "Proxy" %} + {% trans "Name" %} + + {% trans "Type" %} + + {% trans "TTL" %} + + {% trans "Value" %} + + {% trans "Priority" %} + + {% trans "Proxy" %} + {% trans "Actions" %}
      - - {{ record.type }} - +
      + + + + + + + + + + +
      +
      + + +
      +
      +

      + {% trans "Edit DNS Record" %} +

      +
      + + +
      +
      + + +
      +
      + + +
      +
      + + +
      +
      + + +
      +
      + + +
      +
      + + +
      +
      +
      diff --git a/dns/urls.py b/dns/urls.py index 00f019f68..2ca5850c2 100644 --- a/dns/urls.py +++ b/dns/urls.py @@ -27,6 +27,7 @@ urlpatterns = [ re_path(r'^getCurrentRecordsForDomainCloudFlare$', views.getCurrentRecordsForDomainCloudFlare, name='getCurrentRecordsForDomainCloudFlare'), re_path(r'^deleteDNSRecordCloudFlare$', views.deleteDNSRecordCloudFlare, name='deleteDNSRecordCloudFlare'), re_path(r'^addDNSRecordCloudFlare$', views.addDNSRecordCloudFlare, name='addDNSRecordCloudFlare'), + re_path(r'^updateDNSRecordCloudFlare$', views.updateDNSRecordCloudFlare, name='updateDNSRecordCloudFlare'), re_path(r'^syncCF$', views.syncCF, name='syncCF'), re_path(r'^enableProxy$', views.enableProxy, name='enableProxy'), ] diff --git a/dns/views.py b/dns/views.py index de628f894..ba07d9bdd 100644 --- a/dns/views.py +++ b/dns/views.py @@ -199,7 +199,12 @@ def addDeleteDNSRecordsCloudFlare(request): try: userID = request.session['userID'] dm = DNSManager() - return dm.addDeleteDNSRecordsCloudFlare(request, userID) + response = dm.addDeleteDNSRecordsCloudFlare(request, userID) + if hasattr(response, 'headers'): + response['Cache-Control'] = 'no-cache, no-store, must-revalidate, max-age=0' + response['Pragma'] = 'no-cache' + response['Expires'] = '0' + return response except KeyError: return redirect(loadLoginPage) @@ -339,6 +344,15 @@ def addDNSRecordCloudFlare(request): return redirect(loadLoginPage) +def updateDNSRecordCloudFlare(request): + try: + userID = request.session['userID'] + dm = DNSManager() + return dm.updateDNSRecordCloudFlare(userID, json.loads(request.body)) + except KeyError: + return redirect(loadLoginPage) + + def syncCF(request): try: userID = request.session['userID'] @@ -355,10 +369,11 @@ def syncCF(request): def enableProxy(request): try: userID = request.session['userID'] - + body = json.loads(request.body or '{}') dm = DNSManager() - coreResult = dm.enableProxy(userID, json.loads(request.body)) - + coreResult = dm.enableProxy(userID, body) return coreResult except KeyError: return redirect(loadLoginPage) + except (ValueError, TypeError): + return HttpResponse(json.dumps({'status': 0, 'error_message': 'Invalid request'}), status=400, content_type='application/json') diff --git a/emailMarketing/.DS_Store b/emailMarketing/.DS_Store deleted file mode 100644 index 742482470..000000000 Binary files a/emailMarketing/.DS_Store and /dev/null differ diff --git a/emailMarketing/__init__.py b/emailMarketing/__init__.py deleted file mode 100644 index d33aad86a..000000000 --- a/emailMarketing/__init__.py +++ /dev/null @@ -1 +0,0 @@ -default_app_config = 'emailMarketing.apps.EmailmarketingConfig' \ No newline at end of file diff --git a/emailMarketing/admin.py b/emailMarketing/admin.py deleted file mode 100644 index 4c33e0ec3..000000000 --- a/emailMarketing/admin.py +++ /dev/null @@ -1,6 +0,0 @@ -# -*- coding: utf-8 -*- - - -from django.contrib import admin - -# Register your models here. diff --git a/emailMarketing/apps.py b/emailMarketing/apps.py deleted file mode 100644 index 6de896393..000000000 --- a/emailMarketing/apps.py +++ /dev/null @@ -1,10 +0,0 @@ -# -*- coding: utf-8 -*- - - -from django.apps import AppConfig - - -class EmailmarketingConfig(AppConfig): - name = 'emailMarketing' - def ready(self): - from . import signals diff --git a/emailMarketing/emACL.py b/emailMarketing/emACL.py deleted file mode 100644 index e68571c8f..000000000 --- a/emailMarketing/emACL.py +++ /dev/null @@ -1,66 +0,0 @@ -from .models import EmailMarketing, EmailTemplate, SMTPHosts, EmailLists, EmailJobs -from websiteFunctions.models import Websites - -class emACL: - - @staticmethod - def checkIfEMEnabled(userName): - try: - user = EmailMarketing.objects.get(userName=userName) - return 0 - except: - return 1 - - @staticmethod - def getEmailsLists(domain): - website = Websites.objects.get(domain=domain) - emailLists = website.emaillists_set.all() - listNames = [] - - for items in emailLists: - listNames.append(items.listName) - - return listNames - - @staticmethod - def allTemplates(currentACL, admin): - if currentACL['admin'] == 1: - allTemplates = EmailTemplate.objects.all() - else: - allTemplates = admin.emailtemplate_set.all() - - templateNames = [] - for items in allTemplates: - templateNames.append(items.name) - return templateNames - - @staticmethod - def allSMTPHosts(currentACL, admin): - if currentACL['admin'] == 1: - allHosts = SMTPHosts.objects.all() - else: - allHosts = admin.smtphosts_set.all() - hostNames = [] - - for items in allHosts: - hostNames.append(items.host) - - return hostNames - - @staticmethod - def allEmailsLists(currentACL, admin): - listNames = [] - emailLists = EmailLists.objects.all() - - if currentACL['admin'] == 1: - for items in emailLists: - listNames.append(items.listName) - else: - for items in emailLists: - if items.owner.admin == admin: - listNames.append(items.listName) - - return listNames - - - diff --git a/emailMarketing/emailMarketing.py b/emailMarketing/emailMarketing.py deleted file mode 100644 index 3ac749ad6..000000000 --- a/emailMarketing/emailMarketing.py +++ /dev/null @@ -1,427 +0,0 @@ -#!/usr/local/CyberCP/bin/python - -import os -import time -import csv -import re -import plogical.CyberCPLogFileWriter as logging -from .models import EmailLists, EmailsInList, EmailTemplate, EmailJobs, SMTPHosts, ValidationLog -from plogical.backupSchedule import backupSchedule -from websiteFunctions.models import Websites -import threading as multi -import socket, smtplib -import DNS -from random import randint -from plogical.processUtilities import ProcessUtilities - -class emailMarketing(multi.Thread): - def __init__(self, function, extraArgs): - multi.Thread.__init__(self) - self.function = function - self.extraArgs = extraArgs - - def run(self): - try: - if self.function == 'createEmailList': - self.createEmailList() - elif self.function == 'verificationJob': - self.verificationJob() - elif self.function == 'startEmailJob': - self.startEmailJob() - except BaseException as msg: - logging.CyberCPLogFileWriter.writeToFile(str(msg) + ' [emailMarketing.run]') - - def createEmailList(self): - try: - website = Websites.objects.get(domain=self.extraArgs['domain']) - try: - newList = EmailLists(owner=website, listName=self.extraArgs['listName'], dateCreated=time.strftime("%I-%M-%S-%a-%b-%Y")) - newList.save() - except: - newList = EmailLists.objects.get(listName=self.extraArgs['listName']) - - counter = 0 - - if self.extraArgs['path'].endswith('.csv'): - with open(self.extraArgs['path'], 'r') as emailsList: - data = csv.reader(emailsList, delimiter=',') - for items in data: - try: - for value in items: - if re.match('^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,4})$', value) != None: - try: - getEmail = EmailsInList.objects.get(owner=newList, email=value) - except: - try: - newEmail = EmailsInList(owner=newList, email=value, - verificationStatus='NOT CHECKED', - dateCreated=time.strftime("%I-%M-%S-%a-%b-%Y")) - newEmail.save() - except: - pass - logging.CyberCPLogFileWriter.statusWriter(self.extraArgs['tempStatusPath'], str(counter) + ' emails read.') - counter = counter + 1 - except BaseException as msg: - logging.CyberCPLogFileWriter.writeToFile('%s. [createEmailList]' % (str(msg))) - continue - elif self.extraArgs['path'].endswith('.txt'): - with open(self.extraArgs['path'], 'r') as emailsList: - emails = emailsList.readline() - while emails: - email = emails.strip('\n') - if re.match('^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,4})$', email) != None: - try: - getEmail = EmailsInList.objects.get(owner=newList, email=email) - except BaseException as msg: - newEmail = EmailsInList(owner=newList, email=email, verificationStatus='NOT CHECKED', - dateCreated=time.strftime("%I-%M-%S-%a-%b-%Y")) - newEmail.save() - logging.CyberCPLogFileWriter.statusWriter(self.extraArgs['tempStatusPath'],str(counter) + ' emails read.') - counter = counter + 1 - emails = emailsList.readline() - - logging.CyberCPLogFileWriter.statusWriter(self.extraArgs['tempStatusPath'], str(counter) + 'Successfully read all emails. [200]') - except BaseException as msg: - logging.CyberCPLogFileWriter.statusWriter(self.extraArgs['tempStatusPath'], str(msg) +'. [404]') - return 0 - - def findNextIP(self): - try: - if self.delayData['rotation'] == 'Disable': - return None - elif self.delayData['rotation'] == 'IPv4': - if self.delayData['ipv4'].find(',') == -1: - return self.delayData['ipv4'] - else: - ipv4s = self.delayData['ipv4'].split(',') - - if self.currentIP == '': - return ipv4s[0] - else: - returnCheck = 0 - - for items in ipv4s: - if returnCheck == 1: - return items - if items == self.currentIP: - returnCheck = 1 - - return ipv4s[0] - else: - if self.delayData['ipv6'].find(',') == -1: - return self.delayData['ipv6'] - else: - ipv6 = self.delayData['ipv6'].split(',') - - if self.currentIP == '': - return ipv6[0] - else: - returnCheck = 0 - - for items in ipv6: - if returnCheck == 1: - return items - if items == self.currentIP: - returnCheck = 1 - return ipv6[0] - except BaseException as msg: - logging.CyberCPLogFileWriter.writeToFile(str(msg)) - return None - - def verificationJob(self): - try: - - verificationList = EmailLists.objects.get(listName=self.extraArgs['listName']) - domain = verificationList.owner.domain - - if not os.path.exists('/home/cyberpanel/' + domain): - os.mkdir('/home/cyberpanel/' + domain) - - tempStatusPath = '/home/cyberpanel/' + domain + "/" + self.extraArgs['listName'] - logging.CyberCPLogFileWriter.statusWriter(tempStatusPath, 'Starting verification job..') - - counter = 1 - counterGlobal = 0 - - allEmailsInList = verificationList.emailsinlist_set.all() - - configureVerifyPath = '/home/cyberpanel/configureVerify' - finalPath = '%s/%s' % (configureVerifyPath, domain) - - - import json - if os.path.exists(finalPath): - self.delayData = json.loads(open(finalPath, 'r').read()) - - self.currentIP = '' - - ValidationLog(owner=verificationList, status=backupSchedule.INFO, message='Starting email verification..').save() - - for items in allEmailsInList: - if items.verificationStatus != 'Verified': - try: - - email = items.email - self.currentEmail = email - domainName = email.split('@')[1] - records = DNS.dnslookup(domainName, 'MX', 15) - - counterGlobal = counterGlobal + 1 - - for mxRecord in records: - - # Get local server hostname - host = socket.gethostname() - - ## Only fetching smtp object - - if os.path.exists(finalPath): - try: - delay = self.delayData['delay'] - if delay == 'Enable': - if counterGlobal == int(self.delayData['delayAfter']): - ValidationLog(owner=verificationList, status=backupSchedule.INFO, - message='Sleeping for %s seconds...' % (self.delayData['delayTime'])).save() - - time.sleep(int(self.delayData['delayTime'])) - counterGlobal = 0 - self.currentIP = self.findNextIP() - - ValidationLog(owner=verificationList, status=backupSchedule.INFO, - message='IP being used for validation until next sleep: %s.' % (str(self.currentIP))).save() - - if self.currentIP == None: - server = smtplib.SMTP(timeout=10) - else: - server = smtplib.SMTP(self.currentIP, timeout=10) - else: - - if self.currentIP == '': - self.currentIP = self.findNextIP() - ValidationLog(owner=verificationList, status=backupSchedule.INFO, - message='IP being used for validation until next sleep: %s.' % ( - str(self.currentIP))).save() - - if self.currentIP == None: - server = smtplib.SMTP(timeout=10) - else: - server = smtplib.SMTP(self.currentIP, timeout=10) - else: - logging.CyberCPLogFileWriter.writeToFile( - 'Delay not configured..') - - ValidationLog(owner=verificationList, status=backupSchedule.INFO, - message='Delay not configured..').save() - - server = smtplib.SMTP(timeout=10) - except BaseException as msg: - - ValidationLog(owner=verificationList, status=backupSchedule.ERROR, - message='Delay not configured. Error message: %s' % (str(msg))).save() - - server = smtplib.SMTP(timeout=10) - else: - server = smtplib.SMTP(timeout=10) - - ### - - server.set_debuglevel(0) - - # SMTP Conversation - server.connect(mxRecord[1]) - server.helo(host) - server.mail('host' + "@" + host) - code, message = server.rcpt(str(email)) - server.quit() - - # Assume 250 as Success - if code == 250: - items.verificationStatus = 'Verified' - items.save() - break - else: - ValidationLog(owner=verificationList, status=backupSchedule.ERROR, - message='Failed to verify %s. Error message %s' % (email, message.decode())).save() - items.verificationStatus = 'Verification Failed' - items.save() - - logging.CyberCPLogFileWriter.statusWriter(tempStatusPath, str(counter) + ' emails verified so far..') - counter = counter + 1 - except BaseException as msg: - items.verificationStatus = 'Verification Failed' - items.save() - counter = counter + 1 - ValidationLog(owner=verificationList, status=backupSchedule.ERROR, - message='Failed to verify %s. Error message %s' % ( - self.currentEmail , str(msg))).save() - - - verificationList.notVerified = verificationList.emailsinlist_set.filter(verificationStatus='Verification Failed').count() - verificationList.verified = verificationList.emailsinlist_set.filter(verificationStatus='Verified').count() - verificationList.save() - - ValidationLog(owner=verificationList, status=backupSchedule.ERROR, message=str(counter) + ' emails successfully verified. [200]').save() - - logging.CyberCPLogFileWriter.statusWriter(tempStatusPath, str(counter) + ' emails successfully verified. [200]') - except BaseException as msg: - verificationList = EmailLists.objects.get(listName=self.extraArgs['listName']) - domain = verificationList.owner.domain - tempStatusPath = '/home/cyberpanel/' + domain + "/" + self.extraArgs['listName'] - logging.CyberCPLogFileWriter.statusWriter(tempStatusPath, str(msg) +'. [404]') - logging.CyberCPLogFileWriter.writeToFile(str(msg)) - return 0 - - def setupSMTPConnection(self): - try: - if self.extraArgs['host'] == 'localhost': - self.smtpServer = smtplib.SMTP('127.0.0.1') - return 1 - else: - self.verifyHost = SMTPHosts.objects.get(host=self.extraArgs['host']) - self.smtpServer = smtplib.SMTP(str(self.verifyHost.host), int(self.verifyHost.port)) - - if int(self.verifyHost.port) == 587: - self.smtpServer.starttls() - - self.smtpServer.login(str(self.verifyHost.userName), str(self.verifyHost.password)) - return 1 - except smtplib.SMTPHeloError: - logging.CyberCPLogFileWriter.statusWriter(self.extraArgs['tempStatusPath'], - 'The server didnt reply properly to the HELO greeting.') - return 0 - except smtplib.SMTPAuthenticationError: - logging.CyberCPLogFileWriter.statusWriter(self.extraArgs['tempStatusPath'], - 'Username and password combination not accepted.') - return 0 - except smtplib.SMTPException: - logging.CyberCPLogFileWriter.statusWriter(self.extraArgs['tempStatusPath'], - 'No suitable authentication method was found.') - return 0 - - def startEmailJob(self): - try: - - if self.setupSMTPConnection() == 0: - logging.CyberCPLogFileWriter.writeToFile('SMTP Connection failed. [301]') - return 0 - - emailList = EmailLists.objects.get(listName=self.extraArgs['listName']) - allEmails = emailList.emailsinlist_set.all() - emailMessage = EmailTemplate.objects.get(name=self.extraArgs['selectedTemplate']) - - totalEmails = allEmails.count() - sent = 0 - failed = 0 - - ipFile = "/etc/cyberpanel/machineIP" - f = open(ipFile) - ipData = f.read() - ipAddress = ipData.split('\n', 1)[0] - - ## Compose Message - from email.mime.multipart import MIMEMultipart - from email.mime.text import MIMEText - import re - - tempPath = "/home/cyberpanel/" + str(randint(1000, 9999)) - - emailJob = EmailJobs(owner=emailMessage, date=time.strftime("%I-%M-%S-%a-%b-%Y"), - host=self.extraArgs['host'], totalEmails=totalEmails, - sent=sent, failed=failed - ) - emailJob.save() - - for items in allEmails: - try: - message = MIMEMultipart('alternative') - message['Subject'] = emailMessage.subject - message['From'] = emailMessage.fromEmail - message['reply-to'] = emailMessage.replyTo - - if (items.verificationStatus == 'Verified' or self.extraArgs[ - 'verificationCheck']) and not items.verificationStatus == 'REMOVED': - try: - port = ProcessUtilities.fetchCurrentPort() - removalLink = "https:\/\/" + ipAddress + ":%s\/emailMarketing\/remove\/" % (port) + self.extraArgs[ - 'listName'] + "\/" + items.email - messageText = emailMessage.emailMessage.encode('utf-8', 'replace') - message['To'] = items.email - - if re.search(b' - - Email Marketing - plugin - Email Marketing plugin for CyberPanel. - 1.0.0 - \ No newline at end of file diff --git a/emailMarketing/migrations/__init__.py b/emailMarketing/migrations/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/emailMarketing/models.py b/emailMarketing/models.py deleted file mode 100644 index b352f2549..000000000 --- a/emailMarketing/models.py +++ /dev/null @@ -1,56 +0,0 @@ -# -*- coding: utf-8 -*- - - -from django.db import models -from websiteFunctions.models import Websites -from loginSystem.models import Administrator - -# Create your models here. - -class EmailMarketing(models.Model): - userName = models.CharField(max_length=50, unique=True) - -class EmailLists(models.Model): - owner = models.ForeignKey(Websites, on_delete=models.PROTECT) - listName = models.CharField(max_length=50, unique=True) - dateCreated = models.CharField(max_length=200) - verified = models.IntegerField(default=0) - notVerified = models.IntegerField(default=0) - -class EmailsInList(models.Model): - owner = models.ForeignKey(EmailLists, on_delete=models.CASCADE) - email = models.CharField(max_length=50) - firstName = models.CharField(max_length=20, default='') - lastName = models.CharField(max_length=20, default='') - verificationStatus = models.CharField(max_length=100) - dateCreated = models.CharField(max_length=200) - -class SMTPHosts(models.Model): - owner = models.ForeignKey(Administrator, on_delete=models.CASCADE) - host = models.CharField(max_length=150, unique= True) - port = models.CharField(max_length=10) - userName = models.CharField(max_length=200) - password = models.CharField(max_length=200) - -class EmailTemplate(models.Model): - owner = models.ForeignKey(Administrator, on_delete=models.CASCADE) - name = models.CharField(unique=True, max_length=100) - subject = models.CharField(max_length=1000) - fromName = models.CharField(max_length=100) - fromEmail = models.CharField(max_length=150) - replyTo = models.CharField(max_length=150) - emailMessage = models.TextField(max_length=65532) - -class EmailJobs(models.Model): - owner = models.ForeignKey(EmailTemplate, on_delete=models.CASCADE) - date = models.CharField(max_length=200) - host = models.CharField(max_length=1000) - totalEmails = models.IntegerField() - sent = models.IntegerField() - failed = models.IntegerField() - -class ValidationLog(models.Model): - owner = models.ForeignKey(EmailLists, on_delete=models.CASCADE) - status = models.IntegerField() - message = models.TextField() - diff --git a/emailMarketing/signals.py b/emailMarketing/signals.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/emailMarketing/static/emailMarketing/checklist.png b/emailMarketing/static/emailMarketing/checklist.png deleted file mode 100644 index 5236aa0a8..000000000 Binary files a/emailMarketing/static/emailMarketing/checklist.png and /dev/null differ diff --git a/emailMarketing/static/emailMarketing/compose.png b/emailMarketing/static/emailMarketing/compose.png deleted file mode 100644 index 4e786da4d..000000000 Binary files a/emailMarketing/static/emailMarketing/compose.png and /dev/null differ diff --git a/emailMarketing/static/emailMarketing/emailMarketing.js b/emailMarketing/static/emailMarketing/emailMarketing.js deleted file mode 100644 index 69ed07493..000000000 --- a/emailMarketing/static/emailMarketing/emailMarketing.js +++ /dev/null @@ -1,1468 +0,0 @@ -/** - * Created by usman on 8/1/17. - */ - -var emailListURL = "/emailMarketing/" + $("#domainNamePage").text() + "/emailLists"; -$("#emailLists").attr("href", emailListURL); -$("#emailListsChild").attr("href", emailListURL); - -var manageListsURL = "/emailMarketing/" + $("#domainNamePage").text() + "/manageLists"; -$("#manageLists").attr("href", manageListsURL); -$("#manageListsChild").attr("href", manageListsURL); - -var sendEmailsURL = "/emailMarketing/sendEmails"; -$("#sendEmailsPage").attr("href", sendEmailsURL); -$("#sendEmailsPageChild").attr("href", sendEmailsURL); - -var composeEmailURL = "/emailMarketing/composeEmailMessage"; -$("#composeEmails").attr("href", composeEmailURL); -$("#composeEmailsChild").attr("href", composeEmailURL); - -var smtpHostsURL = "/emailMarketing/" + $("#domainNamePage").text() + "/manageSMTP"; -$("#manageSMTPHosts").attr("href", smtpHostsURL); -$("#manageSMTPHostsChild").attr("href", smtpHostsURL); - - -app.controller('emailMarketing', function ($scope, $http) { - - $scope.cyberPanelLoading = true; - $scope.fetchUsers = function () { - $scope.cyberPanelLoading = false; - - url = "/emailMarketing/fetchUsers"; - - var data = {}; - - var config = { - headers: { - 'X-CSRFToken': getCookie('csrftoken') - } - }; - - $http.post(url, data, config).then(ListInitialDatas, cantLoadInitialDatas); - - - function ListInitialDatas(response) { - $scope.cyberPanelLoading = true; - - if (response.data.status === 1) { - $scope.users = JSON.parse(response.data.data); - } else { - new PNotify({ - title: 'Operation Failed!', - text: response.data.error_message, - type: 'error' - }); - } - } - - function cantLoadInitialDatas(response) { - $scope.cyberPanelLoading = false; - new PNotify({ - title: 'Operation Failed!', - text: 'Could not connect to server, please refresh this page', - type: 'error' - }); - - } - - }; - $scope.fetchUsers(); - $scope.enableDisableMarketing = function (status, userName) { - $scope.cyberPanelLoading = false; - - url = "/emailMarketing/enableDisableMarketing"; - - var data = {userName: userName}; - - var config = { - headers: { - 'X-CSRFToken': getCookie('csrftoken') - } - }; - - $http.post(url, data, config).then(ListInitialDatas, cantLoadInitialDatas); - - - function ListInitialDatas(response) { - $scope.cyberPanelLoading = true; - $scope.fetchUsers(); - - if (response.data.status === 1) { - new PNotify({ - title: 'Success!', - text: 'Changes successfully saved.', - type: 'success' - }); - } else { - new PNotify({ - title: 'Operation Failed!', - text: response.data.error_message, - type: 'error' - }); - } - } - - function cantLoadInitialDatas(response) { - $scope.cyberPanelLoading = false; - new PNotify({ - title: 'Operation Failed!', - text: 'Could not connect to server, please refresh this page', - type: 'error' - }); - - } - - }; -}); - -app.controller('createEmailList', function ($scope, $http, $timeout) { - - $scope.installationDetailsForm = false; - $scope.installationProgress = true; - $scope.cyberPanelLoading = true; - $scope.goBackDisable = true; - - var statusFile; - var path; - - - $scope.goBack = function () { - $scope.installationDetailsForm = false; - $scope.installationProgress = true; - $scope.cyberPanelLoading = true; - $scope.goBackDisable = true; - $("#installProgress").css("width", "0%"); - }; - - $scope.createEmailList = function () { - - $scope.installationDetailsForm = true; - $scope.installationProgress = false; - $scope.cyberPanelLoading = false; - $scope.goBackDisable = true; - $scope.currentStatus = "Starting to load email addresses.."; - - - url = "/emailMarketing/submitEmailList"; - - var data = { - domain: $("#domainNamePage").text(), - path: $scope.path, - listName: $scope.listName - }; - - var config = { - headers: { - 'X-CSRFToken': getCookie('csrftoken') - } - }; - - $http.post(url, data, config).then(ListInitialDatas, cantLoadInitialDatas); - - - function ListInitialDatas(response) { - - if (response.data.status === 1) { - statusFile = response.data.tempStatusPath; - getInstallStatus(); - } else { - - $scope.installationDetailsForm = true; - $scope.cyberPanelLoading = true; - $scope.goBackDisable = false; - - new PNotify({ - title: 'Operation Failed!', - text: response.data.error_message, - type: 'error' - }); - - } - - } - - function cantLoadInitialDatas(response) { - $scope.cyberPanelLoading = true; - new PNotify({ - title: 'Operation Failed!', - text: 'Could not connect to server, please refresh this page', - type: 'error' - }); - - } - - }; - - function getInstallStatus() { - - url = "/websites/installWordpressStatus"; - - var data = { - statusFile: statusFile, - domainName: $("#domainNamePage").text() - }; - - var config = { - headers: { - 'X-CSRFToken': getCookie('csrftoken') - } - }; - - - $http.post(url, data, config).then(ListInitialDatas, cantLoadInitialDatas); - - - function ListInitialDatas(response) { - - - if (response.data.abort === 1) { - - if (response.data.installStatus === 1) { - - $scope.installationDetailsForm = true; - $scope.installationProgress = false; - $scope.cyberPanelLoading = true; - $scope.goBackDisable = false; - $scope.currentStatus = 'Emails successfully loaded.'; - $timeout.cancel(); - - } else { - - $scope.installationDetailsForm = true; - $scope.installationProgress = false; - $scope.cyberPanelLoading = true; - $scope.goBackDisable = false; - $scope.currentStatus = response.data.error_message; - - - } - - } else { - $scope.installPercentage = response.data.installationProgress; - $scope.currentStatus = response.data.currentStatus; - - $timeout(getInstallStatus, 1000); - } - - } - - function cantLoadInitialDatas(response) { - $scope.cyberPanelLoading = true; - new PNotify({ - title: 'Operation Failed!', - text: 'Could not connect to server, please refresh this page', - type: 'error' - }); - - - } - - - } - - $scope.fetchEmails = function () { - $scope.cyberPanelLoading = false; - - url = "/emailMarketing/fetchEmails"; - - var data = {'listName': $scope.listName}; - - var config = { - headers: { - 'X-CSRFToken': getCookie('csrftoken') - } - }; - - $http.post(url, data, config).then(ListInitialDatas, cantLoadInitialDatas); - - - function ListInitialDatas(response) { - $scope.cyberPanelLoading = true; - - if (response.data.status === 1) { - $scope.records = JSON.parse(response.data.data); - } else { - new PNotify({ - title: 'Operation Failed!', - text: response.data.error_message, - type: 'error' - }); - } - } - - function cantLoadInitialDatas(response) { - $scope.cyberPanelLoading = false; - new PNotify({ - title: 'Operation Failed!', - text: 'Could not connect to server, please refresh this page', - type: 'error' - }); - - } - - }; - - -}); - -app.controller('manageEmailLists', function ($scope, $http, $timeout) { - - $scope.installationDetailsForm = true; - $scope.installationProgress = true; - $scope.cyberPanelLoading = true; - $scope.goBackDisable = true; - $scope.verificationStatus = true; - - var statusFile; - var path; - - - $scope.goBack = function () { - $scope.installationDetailsForm = false; - $scope.installationProgress = true; - $scope.cyberPanelLoading = true; - $scope.goBackDisable = true; - $("#installProgress").css("width", "0%"); - }; - - $scope.createEmailList = function () { - - $scope.installationDetailsForm = true; - $scope.installationProgress = false; - $scope.cyberPanelLoading = false; - $scope.goBackDisable = true; - $scope.currentStatus = "Starting to load email addresses.."; - - - url = "/emailMarketing/submitEmailList"; - - var data = { - domain: $("#domainNamePage").text(), - path: $scope.path, - listName: $scope.listName - }; - - var config = { - headers: { - 'X-CSRFToken': getCookie('csrftoken') - } - }; - - $http.post(url, data, config).then(ListInitialDatas, cantLoadInitialDatas); - - - function ListInitialDatas(response) { - - if (response.data.status === 1) { - statusFile = response.data.tempStatusPath; - getInstallStatus(); - } else { - - $scope.installationDetailsForm = true; - $scope.cyberPanelLoading = true; - $scope.goBackDisable = false; - - new PNotify({ - title: 'Operation Failed!', - text: response.data.error_message, - type: 'error' - }); - - } - - } - - function cantLoadInitialDatas(response) { - $scope.cyberPanelLoading = true; - new PNotify({ - title: 'Operation Failed!', - text: 'Could not connect to server, please refresh this page', - type: 'error' - }); - - } - - }; - - function getInstallStatus() { - - url = "/websites/installWordpressStatus"; - - var data = { - statusFile: statusFile, - domainName: $("#domainNamePage").text() - }; - - var config = { - headers: { - 'X-CSRFToken': getCookie('csrftoken') - } - }; - - - $http.post(url, data, config).then(ListInitialDatas, cantLoadInitialDatas); - - - function ListInitialDatas(response) { - - - if (response.data.abort === 1) { - - if (response.data.installStatus === 1) { - - $scope.installationDetailsForm = true; - $scope.installationProgress = false; - $scope.cyberPanelLoading = true; - $scope.goBackDisable = false; - $scope.currentStatus = 'Emails successfully loaded.'; - $timeout.cancel(); - - } else { - - $scope.installationDetailsForm = true; - $scope.installationProgress = false; - $scope.cyberPanelLoading = true; - $scope.goBackDisable = false; - $scope.currentStatus = response.data.error_message; - - - } - - } else { - $scope.installPercentage = response.data.installationProgress; - $scope.currentStatus = response.data.currentStatus; - - $timeout(getInstallStatus, 1000); - } - - } - - function cantLoadInitialDatas(response) { - $scope.cyberPanelLoading = true; - new PNotify({ - title: 'Operation Failed!', - text: 'Could not connect to server, please refresh this page', - type: 'error' - }); - - - } - - - } - - - /// - - $scope.currentRecords = true; - - $scope.recordstoShow = 50; - var globalPage; - - $scope.fetchRecords = function () { - $scope.fetchEmails(globalPage); - }; - - $scope.fetchEmails = function (page) { - globalPage = page; - listVerificationStatus(); - - $scope.cyberPanelLoading = false; - - url = "/emailMarketing/fetchEmails"; - - var data = { - 'listName': $scope.listName, - 'recordstoShow': $scope.recordstoShow, - 'page': page - - }; - - var config = { - headers: { - 'X-CSRFToken': getCookie('csrftoken') - } - }; - - $http.post(url, data, config).then(ListInitialDatas, cantLoadInitialDatas); - - - function ListInitialDatas(response) { - $scope.cyberPanelLoading = true; - - if (response.data.status === 1) { - $scope.currentRecords = false; - $scope.records = JSON.parse(response.data.data); - $scope.pagination = response.data.pagination; - $scope.verificationButton = false; - } else { - new PNotify({ - title: 'Operation Failed!', - text: response.data.error_message, - type: 'error' - }); - } - } - - function cantLoadInitialDatas(response) { - $scope.cyberPanelLoading = false; - new PNotify({ - title: 'Operation Failed!', - text: 'Could not connect to server, please refresh this page', - type: 'error' - }); - - } - - }; - - $scope.deleteList = function () { - - $scope.cyberPanelLoading = false; - - url = "/emailMarketing/deleteList"; - - var data = { - listName: $scope.listName - }; - - var config = { - headers: { - 'X-CSRFToken': getCookie('csrftoken') - } - }; - - $http.post(url, data, config).then(ListInitialDatas, cantLoadInitialDatas); - - - function ListInitialDatas(response) { - $scope.cyberPanelLoading = true; - if (response.data.status === 1) { - new PNotify({ - title: 'Success!', - text: 'Emails Successfully Deleted.', - type: 'success' - }); - } else { - - $scope.cyberPanelLoading = false; - - new PNotify({ - title: 'Operation Failed!', - text: response.data.error_message, - type: 'error' - }); - - } - - } - - function cantLoadInitialDatas(response) { - $scope.cyberPanelLoading = true; - new PNotify({ - title: 'Operation Failed!', - text: 'Could not connect to server, please refresh this page', - type: 'error' - }); - - } - - }; - - $scope.showAddEmails = function () { - $scope.installationDetailsForm = false; - $scope.verificationStatus = true; - }; - - // List Verification - - $scope.startVerification = function () { - - $scope.currentStatusVerification = 'Email verification job started..'; - $scope.installationDetailsForm = true; - $scope.verificationStatus = false; - $scope.verificationButton = true; - $scope.cyberPanelLoading = false; - - url = "/emailMarketing/emailVerificationJob"; - - var data = { - listName: $scope.listName - }; - - var config = { - headers: { - 'X-CSRFToken': getCookie('csrftoken') - } - }; - - $http.post(url, data, config).then(ListInitialDatas, cantLoadInitialDatas); - - - function ListInitialDatas(response) { - - if (response.data.status === 1) { - listVerificationStatus(); - $scope.verificationButton = true; - } else { - - $scope.cyberPanelLoading = true; - $scope.verificationButton = false; - - new PNotify({ - title: 'Operation Failed!', - text: response.data.error_message, - type: 'error' - }); - - } - - } - - function cantLoadInitialDatas(response) { - $scope.cyberPanelLoading = true; - $scope.verificationButton = false; - new PNotify({ - title: 'Operation Failed!', - text: 'Could not connect to server, please refresh this page', - type: 'error' - }); - - } - - - }; - - var globalCounter = 0; - - function listVerificationStatus() { - - $scope.verificationButton = true; - $scope.cyberPanelLoading = false; - - url = "/websites/installWordpressStatus"; - - var data = { - domain: $("#domainNamePage").text(), - statusFile: "/home/cyberpanel/" + $("#domainNamePage").text() + "/" + $scope.listName - }; - - var config = { - headers: { - 'X-CSRFToken': getCookie('csrftoken') - } - }; - - - $http.post(url, data, config).then(ListInitialDatas, cantLoadInitialDatas); - - - function ListInitialDatas(response) { - - - if (response.data.abort === 1) { - - if (response.data.installStatus === 1) { - $scope.cyberPanelLoading = true; - $scope.verificationButton = false; - $scope.currentStatusVerification = 'Emails successfully verified.'; - $timeout.cancel(); - - } else { - - if (response.data.error_message.search('No such file') > -1) { - $scope.verificationButton = false; - return; - } - $scope.verificationButton = true; - $scope.cyberPanelLoading = false; - $scope.verificationStatus = false; - $scope.currentStatusVerification = response.data.error_message; - - } - - } else { - - if (response.data.currentStatus.search('No such file') > -1) { - $scope.cyberPanelLoading = true; - $scope.deleteTemplateBTN = false; - $scope.sendEmailBTN = false; - $scope.sendEmailsView = true; - $scope.jobStatus = true; - $scope.goBackDisable = false; - $timeout.cancel(); - return; - } - - $scope.currentStatusVerification = response.data.currentStatus; - $timeout(listVerificationStatus, 1000); - $scope.verificationStatus = false; - } - - } - - function cantLoadInitialDatas(response) { - $scope.cyberPanelLoading = true; - new PNotify({ - title: 'Operation Failed!', - text: 'Could not connect to server, please refresh this page', - type: 'error' - }); - - - } - - - } - - // Delete Email from list - - $scope.deleteEmail = function (id) { - - $scope.cyberPanelLoading = false; - - url = "/emailMarketing/deleteEmail"; - - var data = { - id: id - }; - - var config = { - headers: { - 'X-CSRFToken': getCookie('csrftoken') - } - }; - - $http.post(url, data, config).then(ListInitialDatas, cantLoadInitialDatas); - - - function ListInitialDatas(response) { - $scope.cyberPanelLoading = true; - $scope.fetchEmails(globalPage); - - if (response.data.status === 1) { - $scope.fetchEmails(globalPage); - new PNotify({ - title: 'Success.', - text: 'Email Successfully deleted.', - type: 'success' - }); - - } else { - - new PNotify({ - title: 'Operation Failed!', - text: response.data.error_message, - type: 'error' - }); - - } - - } - - function cantLoadInitialDatas(response) { - $scope.cyberPanelLoading = true; - new PNotify({ - title: 'Operation Failed!', - text: 'Could not connect to server, please refresh this page', - type: 'error' - }); - - } - - }; - - - $scope.currentPageLogs = 1; - $scope.recordsToShowLogs = 10; - - $scope.fetchLogs = function () { - - $scope.cyberPanelLoading = false; - - var config = { - headers: { - 'X-CSRFToken': getCookie('csrftoken') - } - }; - - var data = { - listName: $scope.listName, - page: $scope.currentPageLogs, - recordsToShow: $scope.recordsToShowLogs - }; - - url = "/emailMarketing/fetchVerifyLogs"; - - $http.post(url, data, config).then(ListInitialData, cantLoadInitialData); - - function ListInitialData(response) { - $scope.cyberPanelLoading = true; - if (response.data.status === 1) { - $scope.recordsLogs = JSON.parse(response.data.logs); - $scope.paginationLogs = response.data.pagination; - $scope.totalEmails = response.data.totalEmails; - $scope.verified = response.data.verified; - $scope.notVerified = response.data.notVerified; - } else { - new PNotify({ - title: 'Error!', - text: response.data.error_message, - type: 'error' - }); - } - } - function cantLoadInitialData(response) { - $scope.cyberPanelLoading = true; - new PNotify({ - title: 'Operation Failed!', - text: 'Could not connect to server, please refresh this page', - type: 'error' - }); - } - - - }; - - -}); - -app.controller('manageSMTPHostsCTRL', function ($scope, $http) { - - $scope.cyberPanelLoading = true; - $scope.fetchSMTPHosts = function () { - $scope.cyberPanelLoading = false; - - url = "/emailMarketing/fetchSMTPHosts"; - - var data = {}; - - var config = { - headers: { - 'X-CSRFToken': getCookie('csrftoken') - } - }; - - $http.post(url, data, config).then(ListInitialDatas, cantLoadInitialDatas); - - - function ListInitialDatas(response) { - $scope.cyberPanelLoading = true; - - if (response.data.status === 1) { - $scope.records = JSON.parse(response.data.data); - } else { - new PNotify({ - title: 'Operation Failed!', - text: response.data.error_message, - type: 'error' - }); - } - } - - function cantLoadInitialDatas(response) { - $scope.cyberPanelLoading = false; - new PNotify({ - title: 'Operation Failed!', - text: 'Could not connect to server, please refresh this page', - type: 'error' - }); - - } - - }; - $scope.fetchSMTPHosts(); - $scope.saveSMTPHost = function (status, userName) { - $scope.cyberPanelLoading = false; - - url = "/emailMarketing/saveSMTPHost"; - - var data = { - smtpHost: $scope.smtpHost, - smtpPort: $scope.smtpPort, - smtpUserName: $scope.smtpUserName, - smtpPassword: $scope.smtpPassword - }; - - var config = { - headers: { - 'X-CSRFToken': getCookie('csrftoken') - } - }; - - $http.post(url, data, config).then(ListInitialDatas, cantLoadInitialDatas); - - - function ListInitialDatas(response) { - $scope.cyberPanelLoading = true; - - if (response.data.status === 1) { - $scope.fetchSMTPHosts(); - new PNotify({ - title: 'Success!', - text: 'Successfully saved new SMTP host.', - type: 'success' - }); - } else { - new PNotify({ - title: 'Operation Failed!', - text: response.data.error_message, - type: 'error' - }); - } - } - - function cantLoadInitialDatas(response) { - $scope.cyberPanelLoading = false; - new PNotify({ - title: 'Operation Failed!', - text: 'Could not connect to server, please refresh this page', - type: 'error' - }); - - } - - }; - $scope.smtpHostOperations = function (operation, id) { - $scope.cyberPanelLoading = false; - - url = "/emailMarketing/smtpHostOperations"; - - var data = { - id: id, - operation: operation - }; - - var config = { - headers: { - 'X-CSRFToken': getCookie('csrftoken') - } - }; - - $http.post(url, data, config).then(ListInitialDatas, cantLoadInitialDatas); - - - function ListInitialDatas(response) { - $scope.cyberPanelLoading = true; - $scope.fetchSMTPHosts(); - - if (response.data.status === 1) { - new PNotify({ - title: 'Success!', - text: response.data.message, - type: 'success' - }); - } else { - new PNotify({ - title: 'Operation Failed!', - text: response.data.error_message, - type: 'error' - }); - } - } - - function cantLoadInitialDatas(response) { - $scope.cyberPanelLoading = false; - new PNotify({ - title: 'Operation Failed!', - text: 'Could not connect to server, please refresh this page', - type: 'error' - }); - - } - - }; -}); - -app.controller('composeMessageCTRL', function ($scope, $http) { - - $scope.cyberPanelLoading = true; - $scope.saveTemplate = function (status, userName) { - $scope.cyberPanelLoading = false; - - url = "/emailMarketing/saveEmailTemplate"; - - var data = { - name: $scope.name, - subject: $scope.subject, - fromName: $scope.fromName, - fromEmail: $scope.fromEmail, - replyTo: $scope.replyTo, - emailMessage: $scope.emailMessage - }; - - var config = { - headers: { - 'X-CSRFToken': getCookie('csrftoken') - } - }; - - $http.post(url, data, config).then(ListInitialDatas, cantLoadInitialDatas); - - - function ListInitialDatas(response) { - $scope.cyberPanelLoading = true; - - if (response.data.status === 1) { - new PNotify({ - title: 'Success!', - text: 'Template successfully saved.', - type: 'success' - }); - } else { - new PNotify({ - title: 'Operation Failed!', - text: response.data.error_message, - type: 'error' - }); - } - } - - function cantLoadInitialDatas(response) { - $scope.cyberPanelLoading = false; - new PNotify({ - title: 'Operation Failed!', - text: 'Could not connect to server, please refresh this page', - type: 'error' - }); - - } - - }; -}); - -app.controller('sendEmailsCTRL', function ($scope, $http, $timeout) { - - $scope.cyberPanelLoading = true; - $scope.availableFunctions = true; - $scope.sendEmailsView = true; - $scope.jobStatus = true; - - // Button - - $scope.deleteTemplateBTN = false; - $scope.sendEmailBTN = false; - - $scope.templateSelected = function () { - $scope.availableFunctions = false; - $scope.sendEmailsView = true; - $scope.previewLink = '/emailMarketing/preview/' + $scope.selectedTemplate; - $scope.jobStatus = true; - emailJobStatus(); - - }; - - $scope.sendEmails = function () { - $scope.sendEmailsView = false; - $scope.fetchJobs(); - }; - - $scope.fetchJobs = function () { - - $scope.cyberPanelLoading = false; - - url = "/emailMarketing/fetchJobs"; - - var data = { - 'selectedTemplate': $scope.selectedTemplate - }; - - var config = { - headers: { - 'X-CSRFToken': getCookie('csrftoken') - } - }; - - $http.post(url, data, config).then(ListInitialDatas, cantLoadInitialDatas); - - - function ListInitialDatas(response) { - $scope.cyberPanelLoading = true; - - if (response.data.status === 1) { - $scope.currentRecords = false; - $scope.records = JSON.parse(response.data.data); - } else { - new PNotify({ - title: 'Operation Failed!', - text: response.data.error_message, - type: 'error' - }); - } - } - - function cantLoadInitialDatas(response) { - $scope.cyberPanelLoading = false; - new PNotify({ - title: 'Operation Failed!', - text: 'Could not connect to server, please refresh this page', - type: 'error' - }); - - } - - }; - - $scope.startEmailJob = function () { - $scope.cyberPanelLoading = false; - $scope.deleteTemplateBTN = true; - $scope.sendEmailBTN = true; - $scope.sendEmailsView = true; - $scope.goBackDisable = true; - - url = "/emailMarketing/startEmailJob"; - - - var data = { - 'selectedTemplate': $scope.selectedTemplate, - 'listName': $scope.listName, - 'host': $scope.host, - 'verificationCheck': $scope.verificationCheck, - 'unsubscribeCheck': $scope.unsubscribeCheck - }; - - var config = { - headers: { - 'X-CSRFToken': getCookie('csrftoken') - } - }; - - $http.post(url, data, config).then(ListInitialDatas, cantLoadInitialDatas); - - - function ListInitialDatas(response) { - $scope.cyberPanelLoading = true; - - if (response.data.status === 1) { - emailJobStatus(); - } else { - $scope.cyberPanelLoading = true; - $scope.deleteTemplateBTN = false; - $scope.sendEmailBTN = false; - $scope.sendEmailsView = false; - $scope.jobStatus = true; - $scope.goBackDisable = false; - new PNotify({ - title: 'Operation Failed!', - text: response.data.error_message, - type: 'error' - }); - } - } - - function cantLoadInitialDatas(response) { - $scope.cyberPanelLoading = false; - new PNotify({ - title: 'Operation Failed!', - text: 'Could not connect to server, please refresh this page', - type: 'error' - }); - - } - - - }; - - function emailJobStatus() { - - $scope.cyberPanelLoading = false; - $scope.deleteTemplateBTN = true; - $scope.sendEmailBTN = true; - $scope.sendEmailsView = true; - $scope.jobStatus = false; - $scope.goBackDisable = true; - - url = "/websites/installWordpressStatus"; - - var data = { - domain: 'example.com', - statusFile: "/home/cyberpanel/" + $scope.selectedTemplate + "_pendingJob" - }; - - var config = { - headers: { - 'X-CSRFToken': getCookie('csrftoken') - } - }; - - - $http.post(url, data, config).then(ListInitialDatas, cantLoadInitialDatas); - - - function ListInitialDatas(response) { - - - if (response.data.abort === 1) { - - if (response.data.installStatus === 1) { - $scope.cyberPanelLoading = true; - $scope.deleteTemplateBTN = false; - $scope.sendEmailBTN = false; - $scope.sendEmailsView = true; - $scope.jobStatus = false; - $scope.goBackDisable = false; - $scope.currentStatus = 'Emails successfully sent.'; - $scope.fetchJobs(); - $timeout.cancel(); - - } else { - - if (response.data.error_message.search('No such file') > -1) { - $scope.cyberPanelLoading = true; - $scope.deleteTemplateBTN = false; - $scope.sendEmailBTN = false; - $scope.sendEmailsView = true; - $scope.jobStatus = true; - $scope.goBackDisable = false; - return; - } - $scope.cyberPanelLoading = true; - $scope.deleteTemplateBTN = false; - $scope.sendEmailBTN = false; - $scope.sendEmailsView = true; - $scope.jobStatus = false; - $scope.goBackDisable = false; - $scope.currentStatus = response.data.error_message; - - } - - } else { - - if (response.data.currentStatus.search('No such file') > -1) { - $scope.cyberPanelLoading = true; - $scope.deleteTemplateBTN = false; - $scope.sendEmailBTN = false; - $scope.sendEmailsView = true; - $scope.jobStatus = true; - $scope.goBackDisable = false; - $timeout.cancel(); - return; - } - - $scope.currentStatus = response.data.currentStatus; - $timeout(emailJobStatus, 1000); - $scope.cyberPanelLoading = false; - $scope.deleteTemplateBTN = true; - $scope.sendEmailBTN = true; - $scope.sendEmailsView = true; - $scope.jobStatus = false; - $scope.goBackDisable = true; - } - - } - - function cantLoadInitialDatas(response) { - $scope.cyberPanelLoading = true; - new PNotify({ - title: 'Operation Failed!', - text: 'Could not connect to server, please refresh this page.', - type: 'error' - }); - - - } - - - } - - $scope.goBack = function () { - $scope.cyberPanelLoading = true; - $scope.deleteTemplateBTN = false; - $scope.sendEmailBTN = false; - $scope.sendEmailsView = false; - $scope.jobStatus = true; - - }; - - $scope.deleteTemplate = function () { - - $scope.cyberPanelLoading = false; - - url = "/emailMarketing/deleteTemplate"; - - var data = { - selectedTemplate: $scope.selectedTemplate - }; - - var config = { - headers: { - 'X-CSRFToken': getCookie('csrftoken') - } - }; - - $http.post(url, data, config).then(ListInitialDatas, cantLoadInitialDatas); - - - function ListInitialDatas(response) { - $scope.cyberPanelLoading = true; - - if (response.data.status === 1) { - new PNotify({ - title: 'Success.', - text: 'Template Successfully deleted.', - type: 'success' - }); - - } else { - - new PNotify({ - title: 'Operation Failed!', - text: response.data.error_message, - type: 'error' - }); - - } - - } - - function cantLoadInitialDatas(response) { - $scope.cyberPanelLoading = true; - new PNotify({ - title: 'Operation Failed!', - text: 'Could not connect to server, please refresh this page', - type: 'error' - }); - - } - - }; - $scope.deleteJob = function (id) { - - $scope.cyberPanelLoading = false; - - url = "/emailMarketing/deleteJob"; - - var data = { - id: id - }; - - var config = { - headers: { - 'X-CSRFToken': getCookie('csrftoken') - } - }; - - $http.post(url, data, config).then(ListInitialDatas, cantLoadInitialDatas); - - - function ListInitialDatas(response) { - $scope.cyberPanelLoading = true; - $scope.fetchJobs(); - - if (response.data.status === 1) { - new PNotify({ - title: 'Success.', - text: 'Template Successfully deleted.', - type: 'success' - }); - - } else { - - new PNotify({ - title: 'Operation Failed!', - text: response.data.error_message, - type: 'error' - }); - - } - - } - - function cantLoadInitialDatas(response) { - $scope.cyberPanelLoading = true; - new PNotify({ - title: 'Operation Failed!', - text: 'Could not connect to server, please refresh this page', - type: 'error' - }); - - } - - }; -}); - -app.controller('configureVerify', function ($scope, $http) { - - $scope.cyberPanelLoading = true; - $scope.ipv4Hidden = true; - $scope.ipv6Hidden = true; - $scope.delayHidden = true; - - $scope.delayInitial = function () { - if ($scope.delay === 'Disable') { - $scope.delayHidden = true; - } else { - $scope.delayHidden = false; - } - }; - $scope.rotateInitial = function () { - if ($scope.rotation === 'Disable') { - $scope.rotationHidden = true; - } else if ($scope.rotation === 'IPv4') { - $scope.ipv4Hidden = false; - $scope.ipv6Hidden = true; - } else { - $scope.ipv4Hidden = true; - $scope.ipv6Hidden = false; - } - }; - - $scope.saveChanges = function () { - - $scope.cyberPanelLoading = false; - - url = "/emailMarketing/saveConfigureVerify"; - - var data = { - domain: $("#domainName").text(), - rotation: $scope.rotation, - delay: $scope.delay, - delayAfter: $scope.delayAfter, - delayTime: $scope.delayTime, - ipv4: $scope.ipv4, - ipv6: $scope.ipv6 - }; - - var config = { - headers: { - 'X-CSRFToken': getCookie('csrftoken') - } - }; - - $http.post(url, data, config).then(ListInitialDatas, cantLoadInitialDatas); - - - function ListInitialDatas(response) { - $scope.cyberPanelLoading = true; - - if (response.data.status === 1) { - - new PNotify({ - title: 'Success!', - text: 'Successfully saved verification settings.', - type: 'success' - }); - } else { - new PNotify({ - title: 'Operation Failed!', - text: response.data.error_message, - type: 'error' - }); - } - } - - function cantLoadInitialDatas(response) { - $scope.cyberPanelLoading = false; - new PNotify({ - title: 'Operation Failed!', - text: 'Could not connect to server, please refresh this page', - type: 'error' - }); - - } - - }; -}); - - diff --git a/emailMarketing/static/emailMarketing/mailing.png b/emailMarketing/static/emailMarketing/mailing.png deleted file mode 100644 index da227aa32..000000000 Binary files a/emailMarketing/static/emailMarketing/mailing.png and /dev/null differ diff --git a/emailMarketing/static/emailMarketing/paper-plane.png b/emailMarketing/static/emailMarketing/paper-plane.png deleted file mode 100644 index 79998aa8d..000000000 Binary files a/emailMarketing/static/emailMarketing/paper-plane.png and /dev/null differ diff --git a/emailMarketing/static/emailMarketing/post-office.png b/emailMarketing/static/emailMarketing/post-office.png deleted file mode 100644 index 7419b2b29..000000000 Binary files a/emailMarketing/static/emailMarketing/post-office.png and /dev/null differ diff --git a/emailMarketing/templates/emailMarketing/composeMessages.html b/emailMarketing/templates/emailMarketing/composeMessages.html deleted file mode 100644 index 8c6fae014..000000000 --- a/emailMarketing/templates/emailMarketing/composeMessages.html +++ /dev/null @@ -1,90 +0,0 @@ -{% extends "baseTemplate/index.html" %} -{% load i18n %} -{% block title %}{% trans "Compose Email Message - CyberPanel" %}{% endblock %} -{% block content %} - -{% load static %} -{% get_current_language as LANGUAGE_CODE %} - - - -
      -
      -

      {% trans "Compose Email Message" %}

      -

      {% trans "On this page you can compose email message to be sent out later." %}

      -
      -
      -
      -

      - {% trans "Compose Email Message" %} -

      -
      - - -
      - - - -
      - -
      - -
      -
      - -
      - -
      - -
      -
      - -
      - -
      - -
      -
      - -
      - -
      - -
      -
      - -
      - -
      - -
      -
      - - -
      -
      - -
      -
      - -
      - -
      - -
      -
      - - - -
      - - - - -
      -
      -
      -
      - - -{% endblock %} \ No newline at end of file diff --git a/emailMarketing/templates/emailMarketing/configureVerify.html b/emailMarketing/templates/emailMarketing/configureVerify.html deleted file mode 100644 index 3c33bc0d3..000000000 --- a/emailMarketing/templates/emailMarketing/configureVerify.html +++ /dev/null @@ -1,99 +0,0 @@ -{% extends "baseTemplate/index.html" %} -{% load i18n %} -{% block title %}{% trans "Configure Email Verification - CyberPanel" %}{% endblock %} -{% block content %} - - {% load static %} - {% get_current_language as LANGUAGE_CODE %} - - - -
      -
      -

      {% trans "Configure Email Verification" %}

      -

      {% trans "On this page you can configure parameters regarding how email verification is performed for " %}{{ domain }}

      -
      -
      -
      -

      - {% trans "Compose Email Message" %} -

      -
      - - -
      - - - -
      - -
      - -
      -
      - -
      - -
      - -
      -
      - -
      - -
      - -
      -
      - -
      - -
      - -
      -
      - -
      - -
      - -
      -
      - -
      - -
      - -
      -
      - - - -
      - -
      - -
      -
      - - - -
      - - -
      -
      -
      -
      - - -{% endblock %} \ No newline at end of file diff --git a/emailMarketing/templates/emailMarketing/createEmailList.html b/emailMarketing/templates/emailMarketing/createEmailList.html deleted file mode 100644 index 17e6f6d61..000000000 --- a/emailMarketing/templates/emailMarketing/createEmailList.html +++ /dev/null @@ -1,75 +0,0 @@ -{% extends "baseTemplate/index.html" %} -{% load i18n %} -{% block title %}{% trans "Create Email List - CyberPanel" %}{% endblock %} -{% block content %} - -{% load static %} -{% get_current_language as LANGUAGE_CODE %} - - - -
      -
      -

      {% trans "Create Email List" %} - {{ domain }}

      -

      {% trans "Create email list, to send out news letters and marketing emails." %}

      -
      -
      -
      -

      - {% trans "Create Email List" %} -

      -
      - - -
      -
      - -
      - -
      -
      - -
      - -
      - -
      -
      - -
      - -
      - - -
      -
      - -
      - -
      -
      -

      {$ currentStatus $}

      -
      -
      -
      - -
      - -
      - -
      -
      - -
      - - - - -
      -
      -
      - - -
      -{% endblock %} - diff --git a/emailMarketing/templates/emailMarketing/emailMarketing.html b/emailMarketing/templates/emailMarketing/emailMarketing.html deleted file mode 100644 index e2ba1970b..000000000 --- a/emailMarketing/templates/emailMarketing/emailMarketing.html +++ /dev/null @@ -1,72 +0,0 @@ -{% extends "baseTemplate/index.html" %} -{% load i18n %} -{% block title %}{% trans "Email Marketing - CyberPanel" %}{% endblock %} -{% block content %} - -{% load static %} -{% get_current_language as LANGUAGE_CODE %} - - - -
      - -
      -

      {% trans "Email Marketing" %}

      -

      {% trans "Select users to Enable/Disable Email Marketing feature!" %}

      -
      - -
      -
      - -

      - {% trans "Email Marketing" %} -

      - - {% if installCheck == 0 %} - -
      -
      -

      {% trans "Email Policy Server is not enabled " %} - -

      -
      -
      - {% else %} -
      - - - - - - - - - - - - - - - - - - -
      {% trans 'ID' %}{% trans 'Username' %}{% trans 'Status' %}
      - - - - -
      - -
      - - {% endif %} -
      -
      - - - -{% endblock %} diff --git a/emailMarketing/templates/emailMarketing/manageLists.html b/emailMarketing/templates/emailMarketing/manageLists.html deleted file mode 100644 index 541e0e301..000000000 --- a/emailMarketing/templates/emailMarketing/manageLists.html +++ /dev/null @@ -1,315 +0,0 @@ -{% extends "baseTemplate/index.html" %} -{% load i18n %} -{% block title %}{% trans "Manage Email Lists - CyberPanel" %}{% endblock %} -{% block content %} - - {% load static %} - {% get_current_language as LANGUAGE_CODE %} - - - -
      -
      -

      {% trans "Manage Email Lists" %} - {{ domain }}

      -

      {% trans "On this page you can manage your email lists (Delete, Verify, Add More Emails)." %}

      -
      -
      -
      -

      - {% trans "Manage Email Lists" %} -

      -
      - - -
      - - -
      - -
      - -
      -
      - -
      -
      -
      - - - - -
      -
      - -
      -
      - -
      -
      - - - - -
      -
      - -
      -
      -
      - - - - -
      - -
      - -
      -
      - -
      - -
      - - -
      -
      - -
      - -
      -
      -

      {$ currentStatus $}

      -
      -
      -
      - -
      - -
      - -
      -
      - - - - - -
      - -
      -
      -

      {$ currentStatusVerification $}

      -
      -
      -
      - - - - - - -
      - -
      - -
      - -
      - -
      - -
      - - - - - - - - - - - - - - - - - - - - - -
      {% trans "ID" %}{% trans "email" %}{% trans "Verification Status" %}{% trans "Date Created" %}{% trans "Actions" %}
      - -
      - -
      -
      - - -
      -
      -
      -
      - - - -
      - - -
      -
      -
      - - -
      - - -{% endblock %} \ No newline at end of file diff --git a/emailMarketing/templates/emailMarketing/manageSMTPHosts.html b/emailMarketing/templates/emailMarketing/manageSMTPHosts.html deleted file mode 100644 index 2b94ddf05..000000000 --- a/emailMarketing/templates/emailMarketing/manageSMTPHosts.html +++ /dev/null @@ -1,126 +0,0 @@ -{% extends "baseTemplate/index.html" %} -{% load i18n %} -{% block title %}{% trans "Manage SMTP Hosts - CyberPanel" %}{% endblock %} -{% block content %} - -{% load static %} -{% get_current_language as LANGUAGE_CODE %} - - - -
      -
      -

      {% trans "Manage SMTP Hosts" %}

      -

      {% trans "On this page you can manage STMP Host. (SMTP hosts are used to send emails)" %}

      -
      -
      -
      -

      - {% trans "Manage SMTP Hosts" %} -

      -
      - - -
      - - - -
      - -
      - -
      -
      - -
      - -
      - -
      -
      - -
      - -
      - -
      -
      - -
      - -
      - -
      -
      - -
      - -
      - -
      -
      - - - - - -
      - -
      - - - - - - - - - - - - - - - - - - - - - - - -
      {% trans "ID" %}{% trans "Owner" %}{% trans "Host" %}{% trans "Port" %}{% trans "Username" %}{% trans "Actions" %}
      - - -
      - -
      -
      - - -
      -
      -
      -
      - - - -
      - - - - -
      -
      -
      - - -
      - - -{% endblock %} \ No newline at end of file diff --git a/emailMarketing/templates/emailMarketing/sendEmails.html b/emailMarketing/templates/emailMarketing/sendEmails.html deleted file mode 100644 index dee7c06dc..000000000 --- a/emailMarketing/templates/emailMarketing/sendEmails.html +++ /dev/null @@ -1,204 +0,0 @@ -{% extends "baseTemplate/index.html" %} -{% load i18n %} -{% block title %}{% trans "Send Emails - CyberPanel" %}{% endblock %} -{% block content %} - -{% load static %} -{% get_current_language as LANGUAGE_CODE %} - - - -
      -
      -

      {% trans "Send Emails" %}

      -

      {% trans "On this page you can send emails to the lists you created using SMTP Hosts." %}

      -
      -
      -
      -

      - {% trans "Send Emails" %} -

      -
      - - -
      - - -
      - -
      - -
      -
      - -
      - -
      - - - - -
      - -
      - -
      -
      - -
      - -
      - -
      -
      - -
      - -
      - -
      -
      - -
      - -
      -
      - -
      -
      - -
      -
      - -
      -
      -
      - -
      - -
      - - -
      -
      - - - -
      - -
      -
      -

      {$ currentStatus $}

      -
      -
      -
      - -
      - -
      - -
      -
      - - - - - - -
      - -
      - - - - - - - - - - - - - - - - - - - - - - - - - -
      {% trans "Job ID" %}{% trans "Date" %}{% trans "SMTP Host" %}{% trans "Total Emails" %}{% trans "Sent" %}{% trans "Failed" %}{% trans "Actions" %}
      - -
      - -
      -
      - - -
      -
      -
      -
      - - - -
      - - - - -
      -
      -
      - - -
      - - -{% endblock %} \ No newline at end of file diff --git a/emailMarketing/templates/emailMarketing/website.html b/emailMarketing/templates/emailMarketing/website.html deleted file mode 100644 index 052fea0d3..000000000 --- a/emailMarketing/templates/emailMarketing/website.html +++ /dev/null @@ -1,1134 +0,0 @@ -{% extends "baseTemplate/index.html" %} -{% load i18n %} -{% block title %}{{ domain }} - CyberPanel{% endblock %} -{% block content %} - - {% load static %} - {% get_current_language as LANGUAGE_CODE %} - - -
      - -
      -

      {{ domain }} - {% trans "Preview" %}

      -

      {% trans "All functions related to a particular site." %}

      -
      - - {% if not error %} - - -
      - -
      - -

      - {% trans "Resource Usage" %} -

      - - -
      -
      - -
      - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
      {% trans "Resource" %}{% trans "Usage" %}{% trans "Allowed" %}
      {% trans "FTP" %}{{ ftpUsed }}{{ ftpTotal }}
      {% trans "Databases" %}{{ databasesUsed }}{{ databasesTotal }}
      {% trans "Disk Usage" %}{{ diskInMB }} (MB){{ diskInMBTotal }} (MB)
      {% trans "Bandwidth Usage" %}{{ bwInMB }} (MB){{ bwInMBTotal }} (MB)
      -
      - -
      -
      -
      -
      -

      - {% trans "Disk Usage" %} -

      -
      -
      -
      -
      {{ diskUsage }}%
      -
      -
      - - -

      - {% trans "Bandwidth Usage" %} -

      -
      -
      -
      -
      {{ bwUsage }}%
      -
      -
      - - -
      - -
      -
      -
      - -
      -
      - - -
      -
      - - -
      - -
      -

      - {% trans "Logs" %} -

      - -
      -
      - - - - - - - -
      - -
      - -
      -

      {% trans "Logs Fetched" %}

      -
      - - -
      -

      {% trans "Could not fetch logs, see the logs file through command line. Error message:" %} - {$ errorMessage $}

      -
      - - -
      -

      {% trans "Could not connect to server. Please refresh this page." %}

      -
      - - -
      - -
      - -
      - -
      - -
      - -
      - - -
      - -
      - -
      -
      - - - - - - - - - - - - - - - - - - - -
      DomainIP AddressTimeResourceSize
      -
      -
      - - -
      - -
      - -
      - -
      - - -
      - -
      - -
      - -
      - -
      -
      - - -
      - - -
      - - -
      -
      -
      -
      - - -
      -
      - -

      - {% trans "Domains" %} -

      - -
      - -
      - - - - - - - - - - - - - -
      -
      - - -
      - -
      - -
      -
      - -
      -
      - -
      - -
      - -
      -
      {% trans "Invalid Domain (Note: You don't need to add 'http' or 'https')" %}
      -
      - - -
      - -
      - -
      -
      - - -
      - -
      -
      - -
      -
      - -
      -
      - -
      -
      - -
      -
      - -
      -
      -
      - - -
      - -
      - - -
      -
      - - -
      - -
      - -
      -

      {$ currentStatus $}

      -
      - -
      -
      - 70% Complete -
      -
      - -
      -

      {% trans "Error message:" %} {$ errorMessage $}

      -
      - -
      -

      {% trans "Website succesfully created." %}

      -
      - - -
      -

      {% trans "Could not connect to server. Please refresh this page." %}

      -
      - - -
      -
      - -
      - -
      - -
      -
      - - -
      -
      - - - - - -
      - -
      - - -
      -

      {% trans "PHP Version Changed to:" %} {$ changedPHPVersion $}

      -
      - -
      -

      {% trans "Deleted:" %} {$ deletedDomain $}

      -
      - -
      -

      {% trans "SSL Issued:" %} {$ sslDomainIssued $}

      -
      - -
      -

      {% trans "Changes applied successfully." %}

      -
      - - -
      -

      {$ errorMessage $}

      -
      - - -
      -

      {% trans "Could not connect to server. Please refresh this page." %}

      -
      - - -
      - -
      - -
      - -
      - -
      - - -
      - - - - - - - - - - - - - - - - - - - - - - - -
      DomainLaunchPathopen_basedirPHPSSLDelete
      - - - - - - - - -
      -
      -
      - - -
      - -
      - - -
      - -
      -
      -
      - - -
      - -
      -

      - {% trans "Configurations" %} -

      - - -
      -
      - - - - - - - - - - - - - - -
      - -
      - -
      -

      {% trans "SSL Saved" %}

      -
      - - -
      -

      {% trans "Could not save SSL. Error message:" %} {$ errorMessage $}

      -
      - - -
      -

      {% trans "Could not connect to server. Please refresh this page." %}

      -
      - - -
      -
      - -
      -
      - -
      -
      - -
      -
      - -
      - -
      - - -
      -
      - - -
      - - -
      - - - - - - - -
      - -
      - -
      -

      {% trans "Current configuration in the file fetched." %}

      -
      - - -
      -

      {% trans "Could not fetch current configuration. Error message:" %} {$ - errorMessage $}

      -
      - - -
      -

      {% trans "Could not connect to server. Please refresh this page." %}

      -
      - -
      -

      {% trans "Configurations saved." %}

      -
      - -
      -

      {% trans "Could not fetch current configuration. Error message:" %} {$ - errorMessage $}

      -
      - - -
      -
      - -
      -
      - -
      -
      - -
      - -
      - - -
      -
      - - -
      - -
      - - - -
      - -
      - - -
      -

      {% trans "Current rewrite rules in the file fetched." %} Click - to read more about whats changed in rewrite - rules from v1.7 onwards.

      -
      - - -
      -

      {% trans "Could not fetch current rewrite rules. Error message:" %} {$ - errorMessage $}

      -
      - - -
      -

      {% trans "Could not connect to server. Please refresh this page." %}

      -
      - -
      -

      {% trans "Configurations saved." %}

      -
      - -
      -

      {% trans "Could not save rewrite rules. Error message:" %} {$ errorMessage - $}

      -
      - - -
      -
      - -
      -
      - -
      -
      - -
      - -
      - - -
      -
      - - -
      - -
      - - - -
      - -
      - - -
      - -
      - -
      - -
      - -
      - -
      - - -
      - -
      - -
      -
      - - -
      - - -
      -
      -

      {% trans "Failed to change PHP version. Error message:" %} {$ - errorMessage $}

      -
      - -
      -

      {% trans "PHP successfully changed for: " %} {$ websiteDomain - $}

      -
      - -
      -

      {% trans "Could not connect to server. Please refresh this page." %}

      -
      -
      - - -
      - - -
      -
      - -
      -
      -
      -
      - - -
      -
      -

      - {% trans "Files" %} -

      - -
      -
      - - - - - - - - - - - - - - -
      - -
      - -
      - -
      - -
      - -
      - -
      - -
      - -
      - -
      - - -
      - -
      - -
      -
      -
      - - -
      -
      -

      {% trans "Error message:" %} {$ errorMessage $}

      -
      - -
      -

      {% trans "Changes successfully saved." %}

      -
      - -
      -

      {% trans "Could not connect to server. Please refresh this page." %}

      -
      -
      - -
      - - -
      -
      - - - - -
      -
      -
      -
      - - {% if marketingStatus %} - - - - {% endif %} - -
      - -
      -

      - {% trans "Application Installer" %} -

      - - - -
      -
      - - {% else %} - -
      -

      {{ domain }}

      -
      - - - {% endif %} - - -
      - - - -{% endblock %} \ No newline at end of file diff --git a/emailMarketing/tests.py b/emailMarketing/tests.py deleted file mode 100644 index f067dcaac..000000000 --- a/emailMarketing/tests.py +++ /dev/null @@ -1,6 +0,0 @@ -# -*- coding: utf-8 -*- - - -from django.test import TestCase - -# Create your tests here. diff --git a/emailMarketing/urls.py b/emailMarketing/urls.py deleted file mode 100644 index fd54fe972..000000000 --- a/emailMarketing/urls.py +++ /dev/null @@ -1,31 +0,0 @@ -from django.urls import path, re_path -from . import views - -urlpatterns = [ - path('', views.emailMarketing, name='emailMarketing'), - path('fetchUsers', views.fetchUsers, name='fetchUsers'), - path('enableDisableMarketing', views.enableDisableMarketing, name='enableDisableMarketing'), - path('saveConfigureVerify', views.saveConfigureVerify, name='saveConfigureVerify'), - path('fetchVerifyLogs', views.fetchVerifyLogs, name='fetchVerifyLogs'), - re_path(r'^(?P.+)/emailLists$', views.createEmailList, name='createEmailList'), - path('submitEmailList', views.submitEmailList, name='submitEmailList'), - re_path(r'^(?P.+)/manageLists$', views.manageLists, name='manageLists'), - re_path(r'^(?P.+)/manageSMTP$', views.manageSMTP, name='manageSMTP'), - re_path(r'^(?P.+)/configureVerify$', views.configureVerify, name='configureVerify'), - path('fetchEmails', views.fetchEmails, name='fetchEmails'), - path('deleteList', views.deleteList, name='deleteList'), - path('emailVerificationJob', views.emailVerificationJob, name='emailVerificationJob'), - path('deleteEmail', views.deleteEmail, name='deleteEmail'), - path('saveSMTPHost', views.saveSMTPHost, name='saveSMTPHost'), - path('fetchSMTPHosts', views.fetchSMTPHosts, name='fetchSMTPHosts'), - path('smtpHostOperations', views.smtpHostOperations, name='smtpHostOperations'), - path('composeEmailMessage', views.composeEmailMessage, name='composeEmailMessage'), - path('saveEmailTemplate', views.saveEmailTemplate, name='saveEmailTemplate'), - path('sendEmails', views.sendEmails, name='sendEmails'), - re_path(r'^preview/(?P[-\w]+)/$', views.templatePreview, name='templatePreview'), - path('fetchJobs', views.fetchJobs, name='fetchJobs'), - path('startEmailJob', views.startEmailJob, name='startEmailJob'), - path('deleteTemplate', views.deleteTemplate, name='deleteTemplate'), - path('deleteJob', views.deleteJob, name='deleteJob'), - re_path(r'^remove/(?P[-\w]+)/(?P\w+@.+)$', views.remove, name='remove'), -] diff --git a/emailMarketing/views.py b/emailMarketing/views.py deleted file mode 100644 index 9b1e6b0ee..000000000 --- a/emailMarketing/views.py +++ /dev/null @@ -1,215 +0,0 @@ -# -*- coding: utf-8 -*- - -from django.shortcuts import redirect -from loginSystem.views import loadLoginPage -from .emailMarketingManager import EmailMarketingManager -# Create your views here. - - -def emailMarketing(request): - try: - userID = request.session['userID'] - emm = EmailMarketingManager(request) - return emm.emailMarketing() - except KeyError: - return redirect(loadLoginPage) - -def fetchUsers(request): - try: - userID = request.session['userID'] - emm = EmailMarketingManager(request) - return emm.fetchUsers() - except KeyError: - return redirect(loadLoginPage) - -def enableDisableMarketing(request): - try: - userID = request.session['userID'] - emm = EmailMarketingManager(request) - return emm.enableDisableMarketing() - except KeyError: - return redirect(loadLoginPage) - -def createEmailList(request, domain): - try: - userID = request.session['userID'] - emm = EmailMarketingManager(request, domain) - return emm.createEmailList() - except KeyError: - return redirect(loadLoginPage) - -def submitEmailList(request): - try: - userID = request.session['userID'] - emm = EmailMarketingManager(request) - return emm.submitEmailList() - except KeyError: - return redirect(loadLoginPage) - -def manageLists(request, domain): - try: - userID = request.session['userID'] - emm = EmailMarketingManager(request, domain) - return emm.manageLists() - except KeyError: - return redirect(loadLoginPage) - -def configureVerify(request, domain): - try: - userID = request.session['userID'] - emm = EmailMarketingManager(request, domain) - return emm.configureVerify() - except KeyError: - return redirect(loadLoginPage) - -def saveConfigureVerify(request): - try: - userID = request.session['userID'] - emm = EmailMarketingManager(request) - return emm.saveConfigureVerify() - except KeyError: - return redirect(loadLoginPage) - -def fetchVerifyLogs(request): - try: - userID = request.session['userID'] - emm = EmailMarketingManager(request) - return emm.fetchVerifyLogs() - except KeyError: - return redirect(loadLoginPage) - -def fetchEmails(request): - try: - userID = request.session['userID'] - emm = EmailMarketingManager(request) - return emm.fetchEmails() - except KeyError: - return redirect(loadLoginPage) - -def deleteList(request): - try: - userID = request.session['userID'] - emm = EmailMarketingManager(request) - return emm.deleteList() - except KeyError: - return redirect(loadLoginPage) - -def emailVerificationJob(request): - try: - userID = request.session['userID'] - emm = EmailMarketingManager(request) - return emm.emailVerificationJob() - except KeyError: - return redirect(loadLoginPage) - -def deleteEmail(request): - try: - userID = request.session['userID'] - emm = EmailMarketingManager(request) - return emm.deleteEmail() - except KeyError: - return redirect(loadLoginPage) - -def manageSMTP(request, domain): - try: - userID = request.session['userID'] - emm = EmailMarketingManager(request, domain) - return emm.manageSMTP() - except KeyError: - return redirect(loadLoginPage) - -def saveSMTPHost(request): - try: - userID = request.session['userID'] - emm = EmailMarketingManager(request) - return emm.saveSMTPHost() - except KeyError: - return redirect(loadLoginPage) - -def fetchSMTPHosts(request): - try: - userID = request.session['userID'] - emm = EmailMarketingManager(request) - return emm.fetchSMTPHosts() - except KeyError: - return redirect(loadLoginPage) - -def smtpHostOperations(request): - try: - userID = request.session['userID'] - emm = EmailMarketingManager(request) - return emm.smtpHostOperations() - except KeyError: - return redirect(loadLoginPage) - -def composeEmailMessage(request): - try: - userID = request.session['userID'] - emm = EmailMarketingManager(request) - return emm.composeEmailMessage() - except KeyError: - return redirect(loadLoginPage) - -def saveEmailTemplate(request): - try: - userID = request.session['userID'] - emm = EmailMarketingManager(request) - return emm.saveEmailTemplate() - except KeyError: - return redirect(loadLoginPage) - -def sendEmails(request): - try: - userID = request.session['userID'] - emm = EmailMarketingManager(request) - return emm.sendEmails() - except KeyError: - return redirect(loadLoginPage) - -def templatePreview(request, templateName): - try: - userID = request.session['userID'] - emm = EmailMarketingManager(request, templateName) - return emm.templatePreview() - except KeyError: - return redirect(loadLoginPage) - -def fetchJobs(request): - try: - userID = request.session['userID'] - emm = EmailMarketingManager(request) - return emm.fetchJobs() - except KeyError: - return redirect(loadLoginPage) - -def startEmailJob(request): - try: - userID = request.session['userID'] - emm = EmailMarketingManager(request) - return emm.startEmailJob() - except KeyError: - return redirect(loadLoginPage) - -def deleteTemplate(request): - try: - userID = request.session['userID'] - emm = EmailMarketingManager(request) - return emm.deleteTemplate() - except KeyError: - return redirect(loadLoginPage) - -def deleteJob(request): - try: - userID = request.session['userID'] - emm = EmailMarketingManager(request) - return emm.deleteJob() - except KeyError: - return redirect(loadLoginPage) - - -def remove(request, listName, emailAddress): - try: - emm = EmailMarketingManager(request) - return emm.remove(listName, emailAddress) - except KeyError: - return redirect(loadLoginPage) \ No newline at end of file diff --git a/examplePlugin/__init__.py b/examplePlugin/__init__.py deleted file mode 100644 index b3f015833..000000000 --- a/examplePlugin/__init__.py +++ /dev/null @@ -1 +0,0 @@ -default_app_config = 'examplePlugin.apps.ExamplepluginConfig' diff --git a/examplePlugin/admin.py b/examplePlugin/admin.py deleted file mode 100644 index 8c38f3f3d..000000000 --- a/examplePlugin/admin.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.contrib import admin - -# Register your models here. diff --git a/examplePlugin/apps.py b/examplePlugin/apps.py deleted file mode 100644 index 6f64e3571..000000000 --- a/examplePlugin/apps.py +++ /dev/null @@ -1,8 +0,0 @@ -from django.apps import AppConfig - - -class ExamplepluginConfig(AppConfig): - name = 'examplePlugin' - - def ready(self): - from . import signals diff --git a/examplePlugin/enable_migrations b/examplePlugin/enable_migrations deleted file mode 100644 index e69de29bb..000000000 diff --git a/examplePlugin/meta.xml b/examplePlugin/meta.xml deleted file mode 100644 index 55990a577..000000000 --- a/examplePlugin/meta.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - examplePlugin - plugin - This is an example plugin - 1.0.0 - usmannasir - \ No newline at end of file diff --git a/examplePlugin/migrations/__init__.py b/examplePlugin/migrations/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/examplePlugin/models.py b/examplePlugin/models.py deleted file mode 100644 index c2f360a3c..000000000 --- a/examplePlugin/models.py +++ /dev/null @@ -1,9 +0,0 @@ -from django.db import models - - -class ExamplePlugin(models.Model): - name = models.CharField(unique=True, max_length=255) - - class Meta: - # db_table = "ExamplePlugin" - pass diff --git a/examplePlugin/post_install b/examplePlugin/post_install deleted file mode 100644 index 01ec2cce3..000000000 --- a/examplePlugin/post_install +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/local/CyberCP/bin/python -RESET = '\033[0;0m' -BLUE = "\033[0;34m" -print(BLUE + "Running Post-Install Script..." + RESET) diff --git a/examplePlugin/pre_install b/examplePlugin/pre_install deleted file mode 100644 index c14bef694..000000000 --- a/examplePlugin/pre_install +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/local/CyberCP/bin/python -RESET = '\033[0;0m' -GREEN = '\033[0;32m' -print(GREEN + "Running Pre-Install Script..." + RESET) diff --git a/examplePlugin/pre_remove b/examplePlugin/pre_remove deleted file mode 100644 index eb54102ec..000000000 --- a/examplePlugin/pre_remove +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/local/CyberCP/bin/python -RESET = '\033[0;0m' -GREEN = '\033[0;32m' -print(GREEN + "Running Pre-Remove Script..." + RESET) diff --git a/examplePlugin/signals.py b/examplePlugin/signals.py deleted file mode 100644 index 0c4d14f7c..000000000 --- a/examplePlugin/signals.py +++ /dev/null @@ -1,17 +0,0 @@ -from django.dispatch import receiver -from django.http import HttpResponse -from websiteFunctions.signals import postWebsiteDeletion -from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter as logging - - -# This plugin respond to an event after CyberPanel core finished deleting a website. -# Original request object is passed, body can be accessed with request.body. - -# If any Event handler returns a response object, CyberPanel will stop further processing and returns your response to browser. -# To continue processing just return 200 from your events handlers. - -@receiver(postWebsiteDeletion) -def rcvr(sender, **kwargs): - request = kwargs['request'] - logging.writeToFile('Hello World from Example Plugin.') - return HttpResponse('Hello World from Example Plugin.') diff --git a/examplePlugin/static/examplePlugin/examplePlugin.js b/examplePlugin/static/examplePlugin/examplePlugin.js deleted file mode 100644 index c10ce8398..000000000 --- a/examplePlugin/static/examplePlugin/examplePlugin.js +++ /dev/null @@ -1,3 +0,0 @@ -$(document).ready(function () { - console.log("using JS in static file...!"); -}); \ No newline at end of file diff --git a/examplePlugin/templates/examplePlugin/examplePlugin.html b/examplePlugin/templates/examplePlugin/examplePlugin.html deleted file mode 100644 index 428ac6c05..000000000 --- a/examplePlugin/templates/examplePlugin/examplePlugin.html +++ /dev/null @@ -1,52 +0,0 @@ -{% extends "baseTemplate/index.html" %} -{% load i18n %} -{% block styles %} - -{% endblock %} -{% block title %}Example plugin - CyberPanel{% endblock %} -{% block content %} - {% load static %} - {% get_current_language as LANGUAGE_CODE %} - -
      - -
      -

      {% trans "Example Plugin Page" %}

      -

      {% trans "Example Plugin Info" %}

      -
      - -
      -
      -

      {% trans "examplePlugin" %}

      -
      -
      -
      -

      [[ pluginBody ]]

      -
      -
      -
      - -
      -{% endblock %} - -{% block footer_scripts %} - - {# #} - - -{% endblock %} diff --git a/examplePlugin/tests.py b/examplePlugin/tests.py deleted file mode 100644 index 7ce503c2d..000000000 --- a/examplePlugin/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/examplePlugin/urls.py b/examplePlugin/urls.py deleted file mode 100644 index 421972e1a..000000000 --- a/examplePlugin/urls.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.urls import path -from . import views - -urlpatterns = [ - path('', views.examplePlugin, name='examplePlugin'), -] - diff --git a/examplePlugin/views.py b/examplePlugin/views.py deleted file mode 100644 index 2845a8f61..000000000 --- a/examplePlugin/views.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.shortcuts import render, HttpResponse - - -# Create your views here. - -def examplePlugin(request): - return render(request, 'examplePlugin/examplePlugin.html') diff --git a/filemanager/filemanager.py b/filemanager/filemanager.py index 3af88798f..2343034f7 100644 --- a/filemanager/filemanager.py +++ b/filemanager/filemanager.py @@ -1,4 +1,5 @@ import os +import base64 from django.shortcuts import HttpResponse import json @@ -160,12 +161,67 @@ class FileManager: def returnPathEnclosed(self, path): return "'" + path + "'" + def _moveViaPythonBase64(self, src_path, dest_path, user): + """Fallback: use helper script or Python to move when mv fails (handles special chars).""" + try: + import subprocess + s_b64 = base64.b64encode(src_path.encode('utf-8')).decode('ascii') + d_b64 = base64.b64encode(dest_path.encode('utf-8')).decode('ascii') + helper = '/usr/local/CyberCP/bin/safe-move-path' + if os.path.isfile(helper) and os.access(helper, os.X_OK): + cmd = [helper, s_b64, d_b64] + if os.getuid() != 0: + cmd = ['sudo', '-n'] + cmd + try: + res = subprocess.run(cmd, capture_output=True, timeout=30) + if res.returncode == 0: + return True + except Exception as e: + logging.writeToFile(f"_moveViaPythonBase64 sudo helper failed: {str(e)}") + command = '%s %s %s' % (helper, s_b64, d_b64) + else: + code = "import shutil,base64,sys; s=base64.b64decode(sys.argv[1]).decode(); d=base64.b64decode(sys.argv[2]).decode(); shutil.move(s,d)" + command = "/usr/bin/python3 -c '%s' %s %s" % (code, s_b64, d_b64) + result = ProcessUtilities.executioner(command, user) + return result == 1 + except Exception as e: + logging.writeToFile(f"_moveViaPythonBase64 failed: {str(e)}") + return False + + def _deleteViaPythonBase64(self, path, user): + """Fallback: use helper script or Python to delete when rm fails (handles special chars).""" + try: + import subprocess + p_b64 = base64.b64encode(path.encode('utf-8')).decode('ascii') + helper = '/usr/local/CyberCP/bin/safe-delete-path' + if os.path.isfile(helper) and os.access(helper, os.X_OK): + cmd = [helper, p_b64] + if os.getuid() != 0: + cmd = ['sudo', '-n'] + cmd + try: + res = subprocess.run(cmd, capture_output=True, timeout=30) + if res.returncode == 0: + return True + except Exception as e: + logging.writeToFile(f"_deleteViaPythonBase64 sudo helper failed: {str(e)}") + if os.path.isfile(helper) and os.access(helper, os.X_OK): + command = '%s %s' % (helper, p_b64) + else: + code = "import os,base64,sys,shutil; p=base64.b64decode(sys.argv[1]).decode(); (os.path.isfile(p) and os.remove(p)) or (os.path.isdir(p) and shutil.rmtree(p))" + command = "/usr/bin/python3 -c '%s' %s" % (code, p_b64) + result = ProcessUtilities.executioner(command, user) + return result == 1 + except Exception as e: + logging.writeToFile(f"_deleteViaPythonBase64 failed: {str(e)}") + return False + def changeOwner(self, path): try: domainName = self.data['domainName'] website = Websites.objects.get(domain=domainName) + homePath = '/home/%s' % (domainName) - if path.find('..') > -1: + if not ACLManager.isPathInsideHome(path, homePath): return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!') command = "chown -R " + website.externalApp + ':' + website.externalApp + ' ' + self.returnPathEnclosed(path) @@ -186,8 +242,7 @@ class FileManager: pathCheck = '/home/%s' % (domainName) - if self.data['completeStartingPath'].find(pathCheck) == -1 or self.data['completeStartingPath'].find( - '..') > -1: + if not ACLManager.isPathInsideHome(self.data['completeStartingPath'], pathCheck): return self.ajaxPre(0, 'Not allowed to browse this path, going back home!') command = "ls -la --group-directories-first " + self.returnPathEnclosed( @@ -197,8 +252,7 @@ class FileManager: except: pathCheck = '/' - if self.data['completeStartingPath'].find(pathCheck) == -1 or self.data['completeStartingPath'].find( - '..') > -1: + if not ACLManager.isPathInsideHome(self.data['completeStartingPath'], pathCheck): return self.ajaxPre(0, 'Not allowed to browse this path, going back home!') command = "ls -la --group-directories-first " + self.returnPathEnclosed( @@ -314,7 +368,7 @@ class FileManager: website = Websites.objects.get(domain=domainName) homePath = '/home/%s' % (domainName) - if self.data['fileName'].find('..') > -1 or self.data['fileName'].find(homePath) == -1: + if not ACLManager.isPathInsideHome(self.data['fileName'], homePath): return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!') command = "touch " + self.returnPathEnclosed(self.data['fileName']) @@ -327,7 +381,7 @@ class FileManager: except: homePath = '/' - if self.data['fileName'].find('..') > -1 or self.data['fileName'].find(homePath) == -1: + if not ACLManager.isPathInsideHome(self.data['fileName'], homePath): return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!') command = "touch " + self.returnPathEnclosed(self.data['fileName']) @@ -349,7 +403,7 @@ class FileManager: homePath = '/home/%s' % (domainName) - if self.data['folderName'].find('..') > -1 or self.data['folderName'].find(homePath) == -1: + if not ACLManager.isPathInsideHome(self.data['folderName'], homePath): return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!') command = "mkdir " + self.returnPathEnclosed(self.data['folderName']) @@ -363,7 +417,7 @@ class FileManager: except: homePath = '/' - if self.data['folderName'].find('..') > -1 or self.data['folderName'].find(homePath) == -1: + if not ACLManager.isPathInsideHome(self.data['folderName'], homePath): return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!') command = "mkdir " + self.returnPathEnclosed(self.data['folderName']) @@ -392,25 +446,27 @@ class FileManager: website = Websites.objects.get(domain=domainName) self.homePath = '/home/%s' % (domainName) - logging.CyberCPLogFileWriter.writeToFile(f"Attempting to delete files/folders for domain: {domainName}") + logging.writeToFile(f"Attempting to delete files/folders for domain: {domainName}") RemoveOK = 1 # Test if directory is writable command = 'touch %s/public_html/hello.txt' % (self.homePath) result = ProcessUtilities.outputExecutioner(command) + if result is None: + result = '' - if result.find('cannot touch') > -1: + if isinstance(result, (str, bytes)) and ('cannot touch' in str(result) or 'Permission denied' in str(result)): RemoveOK = 0 - logging.CyberCPLogFileWriter.writeToFile(f"Directory {self.homePath} is not writable, removing chattr flags") + logging.writeToFile(f"Directory {self.homePath} is not writable, removing chattr flags") - # Remove immutable flag from entire directory + # Remove immutable flag from entire directory (executioner returns 1=success, 0=failure) command = 'chattr -R -i %s' % (self.homePath) result = ProcessUtilities.executioner(command) - if result.find('cannot') > -1: - logging.CyberCPLogFileWriter.writeToFile(f"Warning: Failed to remove chattr -i from {self.homePath}: {result}") + if result != 1: + logging.writeToFile(f"Warning: Failed to remove chattr -i from {self.homePath}: {result}") else: - logging.CyberCPLogFileWriter.writeToFile(f"Successfully removed chattr -i from {self.homePath}") + logging.writeToFile(f"Successfully removed chattr -i from {self.homePath}") else: command = 'rm -f %s/public_html/hello.txt' % (self.homePath) @@ -421,24 +477,22 @@ class FileManager: itemPath = self.data['path'] + '/' + item # Security check - prevent path traversal - if itemPath.find('..') > -1 or itemPath.find(self.homePath) == -1: - logging.CyberCPLogFileWriter.writeToFile(f"Security violation: Attempted to delete outside home directory: {itemPath}") + if not ACLManager.isPathInsideHome(itemPath, self.homePath): + logging.writeToFile(f"Security violation: Attempted to delete outside home directory: {itemPath}") return self.ajaxPre(0, 'Not allowed to delete files outside home directory!') - logging.CyberCPLogFileWriter.writeToFile(f"Deleting: {itemPath}") + logging.writeToFile(f"Deleting: {itemPath}") if skipTrash: - # Permanent deletion + # Permanent deletion (executioner returns 1=success, 0=failure) command = 'rm -rf ' + self.returnPathEnclosed(itemPath) result = ProcessUtilities.executioner(command, website.externalApp) - if result.find('cannot') > -1 or result.find('Permission denied') > -1: - logging.CyberCPLogFileWriter.writeToFile(f"Failed to delete {itemPath}: {result}") - # Try with sudo if available - command = 'sudo rm -rf ' + self.returnPathEnclosed(itemPath) - result = ProcessUtilities.executioner(command, website.externalApp) - if result.find('cannot') > -1 or result.find('Permission denied') > -1: - return self.ajaxPre(0, f'Failed to delete {item}: {result}') - logging.CyberCPLogFileWriter.writeToFile(f"Successfully deleted: {itemPath}") + if result != 1: + logging.writeToFile(f"Failed to delete {itemPath}: result={result}, trying Python fallback") + # Fallback: Python+base64 to handle special chars in paths + if not self._deleteViaPythonBase64(itemPath, website.externalApp): + return self.ajaxPre(0, f'Failed to delete {item}') + logging.writeToFile(f"Successfully deleted: {itemPath}") ## Update disk usage in background command = "/usr/local/CyberCP/bin/python /usr/local/CyberCP/plogical/IncScheduler.py UpdateDiskUsageForceDomain --domainName %s" % (domainName) @@ -447,46 +501,44 @@ class FileManager: # Move to trash trashPath = '%s/.trash' % (self.homePath) - # Ensure trash directory exists + # Ensure trash directory exists (executioner returns 1=success, 0=failure) command = 'mkdir -p %s' % (trashPath) result = ProcessUtilities.executioner(command, website.externalApp) - if result.find('cannot') > -1: - logging.CyberCPLogFileWriter.writeToFile(f"Failed to create trash directory: {result}") - return self.ajaxPre(0, f'Failed to create trash directory: {result}') + if result != 1: + logging.writeToFile(f"Failed to create trash directory: result={result}") + return self.ajaxPre(0, f'Failed to create trash directory') # Save to trash database try: Trash(website=website, originalPath=self.returnPathEnclosed(self.data['path']), fileName=self.returnPathEnclosed(item)).save() except Exception as e: - logging.CyberCPLogFileWriter.writeToFile(f"Failed to save trash record: {str(e)}") + logging.writeToFile(f"Failed to save trash record: {str(e)}") - # Move to trash + # Move to trash (executioner returns 1=success, 0=failure) command = 'mv %s %s' % (self.returnPathEnclosed(itemPath), trashPath) result = ProcessUtilities.executioner(command, website.externalApp) - if result.find('cannot') > -1 or result.find('Permission denied') > -1: - logging.CyberCPLogFileWriter.writeToFile(f"Failed to move to trash {itemPath}: {result}") - # Try with sudo if available - command = 'sudo mv %s %s' % (self.returnPathEnclosed(itemPath), trashPath) - result = ProcessUtilities.executioner(command, website.externalApp) - if result.find('cannot') > -1 or result.find('Permission denied') > -1: - return self.ajaxPre(0, f'Failed to move {item} to trash: {result}') - logging.CyberCPLogFileWriter.writeToFile(f"Successfully moved to trash: {itemPath}") + if result != 1: + logging.writeToFile(f"Failed to move to trash {itemPath}: result={result}, trying Python fallback") + # Fallback: Python+base64 to handle special chars in paths (e.g. lscpd/sendCommand) + if not self._moveViaPythonBase64(itemPath, trashPath, website.externalApp): + return self.ajaxPre(0, f'Failed to move {item} to trash') + logging.writeToFile(f"Successfully moved to trash: {itemPath}") ## Update disk usage in background command = "/usr/local/CyberCP/bin/python /usr/local/CyberCP/plogical/IncScheduler.py UpdateDiskUsageForceDomain --domainName %s" % (domainName) ProcessUtilities.popenExecutioner(command) if RemoveOK == 0: - logging.CyberCPLogFileWriter.writeToFile(f"Restoring chattr +i flags for {self.homePath}") + logging.writeToFile(f"Restoring chattr +i flags for {self.homePath}") - # Restore immutable flag to entire directory + # Restore immutable flag to entire directory (executioner returns 1=success, 0=failure) command = 'chattr -R +i %s' % (self.homePath) result = ProcessUtilities.executioner(command) - if result.find('cannot') > -1: - logging.CyberCPLogFileWriter.writeToFile(f"Warning: Failed to restore chattr +i to {self.homePath}: {result}") + if result != 1: + logging.writeToFile(f"Warning: Failed to restore chattr +i to {self.homePath}: result={result}") else: - logging.CyberCPLogFileWriter.writeToFile(f"Successfully restored chattr +i to {self.homePath}") + logging.writeToFile(f"Successfully restored chattr +i to {self.homePath}") # Allow specific directories to remain mutable mutable_dirs = ['/logs/', '/.trash/', '/backup/', '/incbackup/', '/lscache/', '/.cagefs/'] @@ -494,68 +546,74 @@ class FileManager: dir_path = self.homePath + dir_name command = 'chattr -R -i %s' % (dir_path) result = ProcessUtilities.executioner(command) - if result.find('cannot') > -1: - logging.CyberCPLogFileWriter.writeToFile(f"Warning: Failed to remove chattr +i from {dir_path}: {result}") + if result != 1: + logging.writeToFile(f"Warning: Failed to remove chattr +i from {dir_path}: result={result}") else: - logging.CyberCPLogFileWriter.writeToFile(f"Successfully removed chattr +i from {dir_path}") + logging.writeToFile(f"Successfully removed chattr +i from {dir_path}") except Exception as e: - logging.CyberCPLogFileWriter.writeToFile(f"Error in deleteFolderOrFile for {domainName}: {str(e)}") + import traceback + logging.writeToFile(f"Error in deleteFolderOrFile for {domainName}: {str(e)}") + logging.writeToFile(traceback.format_exc()) try: skipTrash = self.data['skipTrash'] except: skipTrash = False - - # Fallback to root path for system files + # Fallback to root path for system files (Root File Manager, domainName empty) self.homePath = '/' - logging.CyberCPLogFileWriter.writeToFile(f"Using fallback deletion for system files in {self.data['path']}") + logging.writeToFile(f"Using fallback deletion for system files in {self.data['path']}") RemoveOK = 1 - # Test if directory is writable - command = 'touch %s/public_html/hello.txt' % (self.homePath) + # Test if we can write (use /tmp for root path since /public_html doesn't exist at /) + test_path = '/tmp' if self.homePath == '/' else (self.homePath + '/public_html') + command = 'touch %s/hello.txt' % (test_path) result = ProcessUtilities.outputExecutioner(command) + if result is None: + result = '' - if result.find('cannot touch') > -1: + if isinstance(result, (str, bytes)) and ('cannot touch' in str(result) or 'Permission denied' in str(result)): RemoveOK = 0 - logging.CyberCPLogFileWriter.writeToFile(f"Directory {self.homePath} is not writable, removing chattr flags") + logging.writeToFile(f"Directory {self.homePath} is not writable, removing chattr flags") command = 'chattr -R -i %s' % (self.homePath) result = ProcessUtilities.executioner(command) - if result.find('cannot') > -1: - logging.CyberCPLogFileWriter.writeToFile(f"Warning: Failed to remove chattr -i from {self.homePath}: {result}") + if result != 1: + logging.writeToFile(f"Warning: Failed to remove chattr -i from {self.homePath}: {result}") else: - command = 'rm -f %s/public_html/hello.txt' % (self.homePath) + command = 'rm -f %s/hello.txt' % (test_path) ProcessUtilities.executioner(command) for item in self.data['fileAndFolders']: - itemPath = self.data['path'] + '/' + item + base = self.data['path'].rstrip('/') or '/' + itemPath = base + '/' + item # Security check for system files - if itemPath.find('..') > -1 or itemPath.find(self.homePath) == -1: - logging.CyberCPLogFileWriter.writeToFile(f"Security violation: Attempted to delete outside allowed path: {itemPath}") + if not ACLManager.isPathInsideHome(itemPath, self.homePath): + logging.writeToFile(f"Security violation: Attempted to delete outside allowed path: {itemPath}") return self.ajaxPre(0, 'Not allowed to delete files outside allowed path!') - logging.CyberCPLogFileWriter.writeToFile(f"Deleting system file: {itemPath}") + logging.writeToFile(f"Deleting system file: {itemPath}") if skipTrash: command = 'rm -rf ' + self.returnPathEnclosed(itemPath) result = ProcessUtilities.executioner(command) - if result.find('cannot') > -1 or result.find('Permission denied') > -1: - logging.CyberCPLogFileWriter.writeToFile(f"Failed to delete system file {itemPath}: {result}") - return self.ajaxPre(0, f'Failed to delete {item}: {result}') - logging.CyberCPLogFileWriter.writeToFile(f"Successfully deleted system file: {itemPath}") + if result != 1: + logging.writeToFile(f"Failed to delete system file {itemPath}: result={result}, trying Python fallback") + if not self._deleteViaPythonBase64(itemPath, None): + return self.ajaxPre(0, f'Failed to delete {item}') + logging.writeToFile(f"Successfully deleted system file: {itemPath}") if RemoveOK == 0: - logging.CyberCPLogFileWriter.writeToFile(f"Restoring chattr +i flags for system path: {self.homePath}") + logging.writeToFile(f"Restoring chattr +i flags for system path: {self.homePath}") command = 'chattr -R +i %s' % (self.homePath) result = ProcessUtilities.executioner(command) - if result.find('cannot') > -1: - logging.CyberCPLogFileWriter.writeToFile(f"Warning: Failed to restore chattr +i to system path {self.homePath}: {result}") + if result != 1: + logging.writeToFile(f"Warning: Failed to restore chattr +i to system path {self.homePath}: result={result}") else: - logging.CyberCPLogFileWriter.writeToFile(f"Successfully restored chattr +i to system path {self.homePath}") + logging.writeToFile(f"Successfully restored chattr +i to system path {self.homePath}") # Allow specific directories to remain mutable for system files mutable_dirs = ['/logs/', '/.trash/', '/backup/', '/incbackup/', '/lscache/', '/.cagefs/'] @@ -563,17 +621,17 @@ class FileManager: dir_path = self.homePath + dir_name command = 'chattr -R -i %s' % (dir_path) result = ProcessUtilities.executioner(command) - if result.find('cannot') > -1: - logging.CyberCPLogFileWriter.writeToFile(f"Warning: Failed to remove chattr +i from system {dir_path}: {result}") + if result != 1: + logging.writeToFile(f"Warning: Failed to remove chattr +i from system {dir_path}: result={result}") else: - logging.CyberCPLogFileWriter.writeToFile(f"Successfully removed chattr +i from system {dir_path}") + logging.writeToFile(f"Successfully removed chattr +i from system {dir_path}") - logging.CyberCPLogFileWriter.writeToFile(f"File deletion completed successfully for domain: {domainName}") + logging.writeToFile(f"File deletion completed successfully for domain: {domainName}") json_data = json.dumps(finalData) return HttpResponse(json_data) except BaseException as msg: - logging.CyberCPLogFileWriter.writeToFile(f"Critical error in deleteFolderOrFile: {str(msg)}") + logging.writeToFile(f"Critical error in deleteFolderOrFile: {str(msg)}") return self.ajaxPre(0, f"File deletion failed: {str(msg)}") def restore(self): @@ -593,8 +651,7 @@ class FileManager: for item in self.data['fileAndFolders']: - if (self.data['path'] + '/' + item).find('..') > -1 or (self.data['path'] + '/' + item).find( - self.homePath) == -1: + if not ACLManager.isPathInsideHome(self.data['path'] + '/' + item, self.homePath): return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!') trashPath = '%s/.trash' % (self.homePath) @@ -628,13 +685,12 @@ class FileManager: homePath = '/home/%s' % (domainName) - if self.data['newPath'].find('..') > -1 or self.data['newPath'].find(homePath) == -1: + if not ACLManager.isPathInsideHome(self.data['newPath'], homePath): return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!') if len(self.data['fileAndFolders']) == 1: - if (self.data['basePath'] + '/' + self.data['fileAndFolders'][0]).find('..') > -1 or ( - self.data['basePath'] + '/' + self.data['fileAndFolders'][0]).find(homePath) == -1: + if not ACLManager.isPathInsideHome(self.data['basePath'] + '/' + self.data['fileAndFolders'][0], homePath): return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!') command = 'yes| cp -Rf %s %s' % ( @@ -654,8 +710,7 @@ class FileManager: ProcessUtilities.executioner(command, website.externalApp) for item in self.data['fileAndFolders']: - if (self.data['basePath'] + '/' + item).find('..') > -1 or (self.data['basePath'] + '/' + item).find( - homePath) == -1: + if not ACLManager.isPathInsideHome(self.data['basePath'] + '/' + item, homePath): return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!') command = '%scp -Rf ' % ('yes |') + self.returnPathEnclosed( @@ -672,13 +727,12 @@ class FileManager: homePath = '/' - if self.data['newPath'].find('..') > -1 or self.data['newPath'].find(homePath) == -1: + if not ACLManager.isPathInsideHome(self.data['newPath'], homePath): return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!') if len(self.data['fileAndFolders']) == 1: - if (self.data['basePath'] + '/' + self.data['fileAndFolders'][0]).find('..') > -1 or ( - self.data['basePath'] + '/' + self.data['fileAndFolders'][0]).find(homePath) == -1: + if not ACLManager.isPathInsideHome(self.data['basePath'] + '/' + self.data['fileAndFolders'][0], homePath): return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!') command = 'yes| cp -Rf %s %s' % ( @@ -698,9 +752,7 @@ class FileManager: ProcessUtilities.executioner(command) for item in self.data['fileAndFolders']: - if (self.data['basePath'] + '/' + item).find('..') > -1 or ( - self.data['basePath'] + '/' + item).find( - homePath) == -1: + if not ACLManager.isPathInsideHome(self.data['basePath'] + '/' + item, homePath): return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!') command = '%scp -Rf ' % ('yes |') + self.returnPathEnclosed( @@ -735,12 +787,10 @@ class FileManager: for item in self.data['fileAndFolders']: - if (self.data['basePath'] + '/' + item).find('..') > -1 or (self.data['basePath'] + '/' + item).find( - homePath) == -1: + if not ACLManager.isPathInsideHome(self.data['basePath'] + '/' + item, homePath): return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!') - if (self.data['newPath'] + '/' + item).find('..') > -1 or (self.data['newPath'] + '/' + item).find( - homePath) == -1: + if not ACLManager.isPathInsideHome(self.data['newPath'] + '/' + item, homePath): return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!') command = 'mv ' + self.returnPathEnclosed( @@ -765,13 +815,10 @@ class FileManager: for item in self.data['fileAndFolders']: - if (self.data['basePath'] + '/' + item).find('..') > -1 or ( - self.data['basePath'] + '/' + item).find( - homePath) == -1: + if not ACLManager.isPathInsideHome(self.data['basePath'] + '/' + item, homePath): return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!') - if (self.data['newPath'] + '/' + item).find('..') > -1 or (self.data['newPath'] + '/' + item).find( - homePath) == -1: + if not ACLManager.isPathInsideHome(self.data['newPath'] + '/' + item, homePath): return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!') command = 'mv ' + self.returnPathEnclosed( @@ -803,11 +850,10 @@ class FileManager: homePath = '/home/%s' % (domainName) - if (self.data['basePath'] + '/' + self.data['existingName']).find('..') > -1 or ( - self.data['basePath'] + '/' + self.data['existingName']).find(homePath) == -1: + if not ACLManager.isPathInsideHome(self.data['basePath'] + '/' + self.data['existingName'], homePath): return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!') - if (self.data['newFileName']).find('..') > -1 or (self.data['basePath']).find(homePath) == -1: + if not ACLManager.isPathInsideHome(self.data['basePath'] + '/' + self.data['newFileName'], homePath): return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!') command = 'mv ' + self.returnPathEnclosed( @@ -819,11 +865,10 @@ class FileManager: except: homePath = '/' - if (self.data['basePath'] + '/' + self.data['existingName']).find('..') > -1 or ( - self.data['basePath'] + '/' + self.data['existingName']).find(homePath) == -1: + if not ACLManager.isPathInsideHome(self.data['basePath'] + '/' + self.data['existingName'], homePath): return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!') - if (self.data['newFileName']).find('..') > -1 or (self.data['basePath']).find(homePath) == -1: + if not ACLManager.isPathInsideHome(self.data['basePath'] + '/' + self.data['newFileName'], homePath): return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!') command = 'mv ' + self.returnPathEnclosed( @@ -850,7 +895,7 @@ class FileManager: pathCheck = '/home/%s' % (domainName) - if self.data['fileName'].find(pathCheck) == -1 or self.data['fileName'].find('..') > -1: + if not ACLManager.isPathInsideHome(self.data['fileName'], pathCheck): return self.ajaxPre(0, 'Not allowed.') # Ensure proper UTF-8 handling for file reading @@ -860,7 +905,7 @@ class FileManager: except: pathCheck = '/' - if self.data['fileName'].find(pathCheck) == -1 or self.data['fileName'].find('..') > -1: + if not ACLManager.isPathInsideHome(self.data['fileName'], pathCheck): return self.ajaxPre(0, 'Not allowed.') # Ensure proper UTF-8 handling for file reading @@ -958,11 +1003,11 @@ class FileManager: if result.find('->') > -1: return self.ajaxPre(0, "Symlink attack.") - if ACLManager.commandInjectionCheck(self.data['completePath'] + '/' + myfile.name) == 1: + uploadPathFull = self.data['completePath'] + '/' + myfile.name + if not ACLManager.isFilePathSafeForShell(uploadPathFull): return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!') - if (self.data['completePath'] + '/' + myfile.name).find(pathCheck) == -1 or ( - (self.data['completePath'] + '/' + myfile.name)).find('..') > -1: + if not ACLManager.isPathInsideHome(uploadPathFull, pathCheck): return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!') command = 'cp ' + self.returnPathEnclosed( @@ -985,11 +1030,11 @@ class FileManager: command = 'ls -la %s' % (self.data['completePath']) result = ProcessUtilities.outputExecutioner(command) logging.writeToFile("upload file res %s" % result) - if ACLManager.commandInjectionCheck(self.data['completePath'] + '/' + myfile.name) == 1: + uploadPathFull = self.data['completePath'] + '/' + myfile.name + if not ACLManager.isFilePathSafeForShell(uploadPathFull): return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!') - if (self.data['completePath'] + '/' + myfile.name).find(pathCheck) == -1 or ( - (self.data['completePath'] + '/' + myfile.name)).find('..') > -1: + if not ACLManager.isPathInsideHome(uploadPathFull, pathCheck): return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!') command = 'cp ' + self.returnPathEnclosed( @@ -1034,10 +1079,10 @@ class FileManager: homePath = '/home/%s' % (domainName) - if self.data['extractionLocation'].find('..') > -1 or self.data['extractionLocation'].find(homePath) == -1: + if not ACLManager.isPathInsideHome(self.data['extractionLocation'], homePath): return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!') - if self.data['fileToExtract'].find('..') > -1 or self.data['fileToExtract'].find(homePath) == -1: + if not ACLManager.isPathInsideHome(self.data['fileToExtract'], homePath): return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!') if self.data['extractionType'] == 'zip': @@ -1065,11 +1110,10 @@ class FileManager: homePath = '/' - if self.data['extractionLocation'].find('..') > -1 or self.data['extractionLocation'].find( - homePath) == -1: + if not ACLManager.isPathInsideHome(self.data['extractionLocation'], homePath): return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!') - if self.data['fileToExtract'].find('..') > -1 or self.data['fileToExtract'].find(homePath) == -1: + if not ACLManager.isPathInsideHome(self.data['fileToExtract'], homePath): return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!') if self.data['extractionType'] == 'zip': @@ -1130,8 +1174,7 @@ class FileManager: for item in self.data['listOfFiles']: - if (self.data['basePath'] + item).find('..') > -1 or (self.data['basePath'] + item).find( - homePath) == -1: + if not ACLManager.isPathInsideHome(self.data['basePath'] + item, homePath): return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!') command = '%s%s ' % (command, self.returnPathEnclosed(item)) @@ -1168,8 +1211,7 @@ class FileManager: for item in self.data['listOfFiles']: - if (self.data['basePath'] + item).find('..') > -1 or (self.data['basePath'] + item).find( - homePath) == -1: + if not ACLManager.isPathInsideHome(self.data['basePath'] + item, homePath): return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!') command = '%s%s ' % (command, self.returnPathEnclosed(item)) diff --git a/filemanager/static/filemanager/js/fileManager.js b/filemanager/static/filemanager/js/fileManager.js index 7ea1bc575..78ad4dfca 100644 --- a/filemanager/static/filemanager/js/fileManager.js +++ b/filemanager/static/filemanager/js/fileManager.js @@ -82,6 +82,15 @@ fileManager.controller('fileManagerCtrl', function ($scope, $http, FileUploader, $scope.showUploadBox = function () { $('#uploadBox').modal('show'); }; + // Fix aria-hidden a11y: move focus out of modal before hide so no focused descendant retains focus + $(document).on('hide.bs.modal', '.modal', function () { + var modal = this; + if (document.activeElement && modal.contains(document.activeElement)) { + var trigger = document.getElementById('uploadTriggerBtn'); + if (trigger && modal.id === 'uploadBox') { trigger.focus(); } + else { document.activeElement.blur(); } + } + }); $scope.showHTMLEditorModal = function (MainFM= 0) { $scope.htmlEditorLoading = false; @@ -1147,7 +1156,8 @@ fileManager.controller('fileManagerCtrl', function ($scope, $http, FileUploader, }); $scope.fetchForTableSecondary(null, 'refresh'); } else { - var notification = alertify.notify('Files/Folders can not be deleted', 'error', 5, function () { + var errMsg = (response.data && response.data.error_message) ? response.data.error_message : 'Files/Folders can not be deleted'; + var notification = alertify.notify(errMsg, 'error', 8, function () { console.log('dismissed'); }); } @@ -1155,6 +1165,10 @@ fileManager.controller('fileManagerCtrl', function ($scope, $http, FileUploader, } function cantLoadInitialDatas(response) { + var err = (response && response.data && (response.data.error_message || response.data.message)) || + (response && response.statusText) || 'Request failed'; + if (response && response.status === 0) err = 'Network error'; + alertify.notify(err, 'error', 8); } }; diff --git a/filemanager/static/filemanager/js/newFileManager.js b/filemanager/static/filemanager/js/newFileManager.js index bfa8bed5b..b10512b57 100644 --- a/filemanager/static/filemanager/js/newFileManager.js +++ b/filemanager/static/filemanager/js/newFileManager.js @@ -156,6 +156,14 @@ function findFileExtension(fileName) { $scope.showUploadBox = function () { $("#uploadBox").modal(); }; + $(document).on("hide.bs.modal", ".modal", function () { + var modal = this; + if (document.activeElement && modal.contains(document.activeElement)) { + var trigger = document.getElementById("uploadTriggerBtn"); + if (trigger && modal.id === "uploadBox") { trigger.focus(); } + else { document.activeElement.blur(); } + } + }); $scope.showHTMLEditorModal = function (MainFM = 0) { $scope.fileInEditor = allFilesAndFolders[0]; diff --git a/filemanager/templates/filemanager/editFile.html b/filemanager/templates/filemanager/editFile.html index 8ce5500b4..159b2be9c 100644 --- a/filemanager/templates/filemanager/editFile.html +++ b/filemanager/templates/filemanager/editFile.html @@ -11,7 +11,7 @@ - @@ -186,7 +186,7 @@ - diff --git a/filemanager/templates/filemanager/index.html b/filemanager/templates/filemanager/index.html index 1cbd212a1..84460c5bb 100644 --- a/filemanager/templates/filemanager/index.html +++ b/filemanager/templates/filemanager/index.html @@ -11,7 +11,7 @@ - + @@ -60,7 +60,7 @@
      - -
      - -
      - +
      @@ -932,6 +1297,50 @@
      + + +
      +
      +

      {% trans "Export Firewall Rules" %}

      +

      {% trans "Choose export format:" %}

      +
      + + +
      +
      + + +
      +
      +
      + + +
      +
      +

      {% trans "Import Firewall Rules" %}

      +

      {% trans "Choose file format:" %}

      +
      + + +
      +
      + + +
      +
      +
      @@ -982,7 +1391,8 @@
      - +
      +
      @@ -1010,6 +1420,13 @@ {$ rule.port $} -
      {% trans "ID" %} +
      + +
      + + +
      +
      +

      {% trans "Modify Firewall Rule" %}

      +
      + + +
      +
      + + +
      +
      + + +
      +
      + + +
      +
      + + +
      +
      +
      + + +
      +
      + {% trans "Showing" %} {$ rulesRangeStart() || 0 $} – {$ rulesRangeEnd() || 0 $} {% trans "of" %} {$ rulesTotalCount || rules.length || 0 $} + + + + +
      +
      + + {$ rulesPage $} / {$ rulesTotalPages() $} + + + + + + +
      +
      -
      +

      {% trans "No Firewall Rules" %}

      {% trans "Add your first firewall rule to control network access to your server." %}

      @@ -1057,7 +1534,17 @@
      {% trans "Banned IP Addresses" %}
      -
      +
      + + +
      +
      @@ -1101,9 +1588,30 @@
      + +
      + +
      + + +
      +
      +
      - +
      +
      @@ -1115,7 +1623,7 @@ - + -
      {% trans "IP Address" %}
      {$ bannedIP.ip $} @@ -1141,7 +1649,15 @@ +
      + +
      - -
      + +
      +
      + {% trans "Showing" %} {$ bannedRangeStart() || 0 $} – {$ bannedRangeEnd() || 0 $} {% trans "of" %} {$ bannedTotalCount || bannedIPs.length || 0 $} + + + + +
      +
      + + {$ bannedPage $} / {$ bannedTotalPages() $} + + + + + + +
      +
      + + +

      {% trans "No Banned IPs" %}

      {% trans "All IP addresses are currently allowed. Add banned IPs to block suspicious or malicious traffic." %}

      + +
      + +

      {% trans "No matching banned IPs" %}

      +

      {% trans "No banned IPs match your search. Try a different IP, reason or status (Active/Expired)." %}

      +
      - -
      -
      + +
      +
      - {% trans "Action failed. Error message:" %} {$ bannedIPErrorMessage $} + {% trans "Action failed. Error message:" %}
      - -
      +
      {% trans "Action completed successfully." %}
      - -
      +
      {% trans "Could not connect to server. Please refresh this page." %}
      + + +
      +
      +

      {% trans "Modify Banned IP" %}

      +

      + {% trans "IP Address" %}: {$ modifyBannedIPData.ip $} +

      +
      + + +
      +
      + + +
      +
      + + +
      +
      +
      + + {% endblock %} \ No newline at end of file diff --git a/firewall/urls.py b/firewall/urls.py index e60e00369..91d53103f 100644 --- a/firewall/urls.py +++ b/firewall/urls.py @@ -3,7 +3,11 @@ from . import views urlpatterns = [ path('securityHome', views.securityHome, name='securityHome'), - path('', views.firewallHome, name='firewallHome'), + path('firewall-rules/', views.firewallHome, name='firewallRules'), + path('firewall-rules', views.firewallHome, name='firewallRulesNoSlash'), + path('banned-ips/', views.firewallHome, name='firewallBannedIPs'), + path('banned-ips', views.firewallHome, name='firewallBannedIPsNoSlash'), + path('', views.firewallHome, name='firewallHome'), # /firewall/ also serves the page so 404 is avoided path('getCurrentRules', views.getCurrentRules, name='getCurrentRules'), path('addRule', views.addRule, name='addRule'), path('modifyRule', views.modifyRule, name='modifyRule'), diff --git a/firewall/views.py b/firewall/views.py index 595d7edcb..c01ab5765 100644 --- a/firewall/views.py +++ b/firewall/views.py @@ -1,4 +1,5 @@ from django.shortcuts import redirect +from django.http import HttpResponse import json from loginSystem.views import loadLoginPage from plogical.processUtilities import ProcessUtilities @@ -18,6 +19,16 @@ def securityHome(request): return redirect(loadLoginPage) +def firewallRedirect(request): + """Redirect /firewall/ to /firewall/firewall-rules/ so the default tab has a clear URL.""" + try: + if request.session.get('userID'): + return redirect('/firewall/firewall-rules/') + return redirect(loadLoginPage) + except Exception: + return redirect(loadLoginPage) + + def firewallHome(request): try: userID = request.session['userID'] @@ -41,7 +52,14 @@ def getCurrentRules(request): try: userID = request.session['userID'] fm = FirewallManager() - return fm.getCurrentRules(userID) + try: + body = request.body + if isinstance(body, bytes): + body = body.decode('utf-8') + data = json.loads(body) if body and body.strip() else {} + except (json.JSONDecodeError, Exception): + data = {} + return fm.getCurrentRules(userID, data) except KeyError: return redirect(loadLoginPage) @@ -663,47 +681,137 @@ def getBannedIPs(request): try: userID = request.session['userID'] fm = FirewallManager() - return fm.getBannedIPs(userID) + data = {} + if request.method == 'POST': + try: + body = request.body + if isinstance(body, bytes): + body = body.decode('utf-8') + data = json.loads(body) if body and body.strip() else {} + except (json.JSONDecodeError, Exception): + pass + # GET also supported (no body); pagination uses defaults + result = fm.getBannedIPs(userID, data) + # Ensure we return JSON (FirewallManager may return HttpResponse) + return result except KeyError: - return redirect(loadLoginPage) + final_dic = {'status': 0, 'error_message': 'Session expired. Please log in again.', 'bannedIPs': [], 'total_count': 0} + return HttpResponse(json.dumps(final_dic), content_type='application/json', status=403) def addBannedIP(request): try: userID = request.session['userID'] fm = FirewallManager() - return fm.addBannedIP(userID, json.loads(request.body)) + try: + body = request.body + if isinstance(body, bytes): + body = body.decode('utf-8') + request_data = json.loads(body) if body and body.strip() else {} + except json.JSONDecodeError as e: + final_dic = {'status': 0, 'error_message': 'Invalid JSON in request: %s' % str(e)} + return HttpResponse(json.dumps(final_dic), content_type='application/json', status=400) + except Exception as e: + final_dic = {'status': 0, 'error_message': 'Error parsing request: %s' % str(e)} + return HttpResponse(json.dumps(final_dic), content_type='application/json', status=400) + result = fm.addBannedIP(userID, request_data) + return result except KeyError: - return redirect(loadLoginPage) + final_dic = {'status': 0, 'error_message': 'Session expired. Please refresh the page and try again.'} + return HttpResponse(json.dumps(final_dic), content_type='application/json', status=403) + except Exception as e: + import traceback + from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter as _log + error_trace = traceback.format_exc() + _log.writeToFile('Error in addBannedIP view: %s\n%s' % (str(e), error_trace)) + final_dic = {'status': 0, 'error_message': 'Server error: %s' % str(e)} + return HttpResponse(json.dumps(final_dic), content_type='application/json', status=500) def modifyBannedIP(request): try: userID = request.session['userID'] fm = FirewallManager() - return fm.modifyBannedIP(userID, json.loads(request.body)) + try: + body = request.body + if isinstance(body, bytes): + body = body.decode('utf-8') + request_data = json.loads(body) if body and body.strip() else {} + except json.JSONDecodeError as e: + final_dic = {'status': 0, 'error_message': 'Invalid JSON in request: %s' % str(e)} + return HttpResponse(json.dumps(final_dic), content_type='application/json', status=400) + except Exception as e: + final_dic = {'status': 0, 'error_message': 'Error parsing request: %s' % str(e)} + return HttpResponse(json.dumps(final_dic), content_type='application/json', status=400) + return fm.modifyBannedIP(userID, request_data) except KeyError: - return redirect(loadLoginPage) + final_dic = {'status': 0, 'error_message': 'Session expired. Please refresh the page and try again.'} + return HttpResponse(json.dumps(final_dic), content_type='application/json', status=403) + except Exception as e: + import traceback + from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter as _log + error_trace = traceback.format_exc() + _log.writeToFile('Error in modifyBannedIP view: %s\n%s' % (str(e), error_trace)) + final_dic = {'status': 0, 'error_message': 'Server error: %s' % str(e)} + return HttpResponse(json.dumps(final_dic), content_type='application/json', status=500) def removeBannedIP(request): try: userID = request.session['userID'] fm = FirewallManager() - return fm.removeBannedIP(userID, json.loads(request.body)) + try: + body = request.body + if isinstance(body, bytes): + body = body.decode('utf-8') + request_data = json.loads(body) if body and body.strip() else {} + except json.JSONDecodeError as e: + final_dic = {'status': 0, 'error_message': 'Invalid JSON in request: %s' % str(e)} + return HttpResponse(json.dumps(final_dic), content_type='application/json', status=400) + except Exception as e: + final_dic = {'status': 0, 'error_message': 'Error parsing request: %s' % str(e)} + return HttpResponse(json.dumps(final_dic), content_type='application/json', status=400) + return fm.removeBannedIP(userID, request_data) except KeyError: - return redirect(loadLoginPage) + final_dic = {'status': 0, 'error_message': 'Session expired. Please refresh the page and try again.'} + return HttpResponse(json.dumps(final_dic), content_type='application/json', status=403) + except Exception as e: + import traceback + from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter as _log + error_trace = traceback.format_exc() + _log.writeToFile('Error in removeBannedIP view: %s\n%s' % (str(e), error_trace)) + final_dic = {'status': 0, 'error_message': 'Server error: %s' % str(e)} + return HttpResponse(json.dumps(final_dic), content_type='application/json', status=500) def deleteBannedIP(request): try: userID = request.session['userID'] fm = FirewallManager() - return fm.deleteBannedIP(userID, json.loads(request.body)) + try: + body = request.body + if isinstance(body, bytes): + body = body.decode('utf-8') + request_data = json.loads(body) if body and body.strip() else {} + except json.JSONDecodeError as e: + final_dic = {'status': 0, 'error_message': 'Invalid JSON in request: %s' % str(e)} + return HttpResponse(json.dumps(final_dic), content_type='application/json', status=400) + except Exception as e: + final_dic = {'status': 0, 'error_message': 'Error parsing request: %s' % str(e)} + return HttpResponse(json.dumps(final_dic), content_type='application/json', status=400) + return fm.deleteBannedIP(userID, request_data) except KeyError: - return redirect(loadLoginPage) + final_dic = {'status': 0, 'error_message': 'Session expired. Please refresh the page and try again.'} + return HttpResponse(json.dumps(final_dic), content_type='application/json', status=403) + except Exception as e: + import traceback + from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter as _log + error_trace = traceback.format_exc() + _log.writeToFile('Error in deleteBannedIP view: %s\n%s' % (str(e), error_trace)) + final_dic = {'status': 0, 'error_message': 'Server error: %s' % str(e)} + return HttpResponse(json.dumps(final_dic), content_type='application/json', status=500) def exportFirewallRules(request): try: userID = request.session['userID'] - fm = FirewallManager() + fm = FirewallManager(request) return fm.exportFirewallRules(userID) except KeyError: return redirect(loadLoginPage) diff --git a/fix-phpmyadmin.sh b/fix-phpmyadmin.sh new file mode 100644 index 000000000..8afd733c7 --- /dev/null +++ b/fix-phpmyadmin.sh @@ -0,0 +1,46 @@ +#!/bin/bash +# Fix phpMyAdmin 404 after upgrade. Run as root on the panel server. +# Usage: sudo bash fix-phpmyadmin.sh +# Then open https://YOUR_IP:2087/phpmyadmin/ (or from Databases -> phpMyAdmin in the panel). + +set -e +CP="${CP:-/usr/local/CyberCP}" +PUBLIC="$CP/public" +PYTHON="${PYTHON:-$CP/bin/python}" + +if [[ $(id -u) -ne 0 ]]; then + echo "Run as root: sudo bash $0" + exit 1 +fi + +echo "Ensuring $PUBLIC exists..." +mkdir -p "$PUBLIC" + +if [[ ! -x "$PYTHON" ]]; then + PYTHON=$(which python3 2>/dev/null || which python2 2>/dev/null || true) +fi +if [[ -z "$PYTHON" ]]; then + echo "ERROR: No Python found. Install phpMyAdmin manually or fix panel Python." + exit 1 +fi + +echo "Installing phpMyAdmin via panel upgrade module..." +export DJANGO_SETTINGS_MODULE=CyberCP.settings +"$PYTHON" -c " +import sys +sys.path.insert(0, '$CP') +from plogical.upgrade import Upgrade +Upgrade.download_install_phpmyadmin() +" 2>&1 || true + +if [[ -d "$PUBLIC/phpmyadmin" ]]; then + echo "Setting ownership to lscpd:lscpd..." + chown -R lscpd:lscpd "$PUBLIC/phpmyadmin" 2>/dev/null || true + chmod 755 "$PUBLIC/phpmyadmin" + chmod 755 "$PUBLIC/phpmyadmin/tmp" 2>/dev/null || true + echo "Done. phpMyAdmin is at $PUBLIC/phpmyadmin" + echo "Test: https://YOUR_IP:2087/phpmyadmin/ (or use the panel Databases -> phpMyAdmin link)" +else + echo "WARNING: $PUBLIC/phpmyadmin was not created. Check logs above." + exit 1 +fi diff --git a/fix-pureftpd-quota-once.sh b/fix-pureftpd-quota-once.sh new file mode 100644 index 000000000..ea461af02 --- /dev/null +++ b/fix-pureftpd-quota-once.sh @@ -0,0 +1,39 @@ +#!/bin/bash +# One-time fix on the server: correct Pure-FTPd Quota line and start the service. +# Run as root: sudo bash fix-pureftpd-quota-once.sh +# Use when the panel has written invalid "Quota yes" and Pure-FTPd fails to start. + +set -e +CONF=/etc/pure-ftpd/pure-ftpd.conf +SERVICE=pure-ftpd + +if [ ! -f "$CONF" ]; then + echo "Config not found: $CONF" + exit 1 +fi + +# Fix Quota line (Pure-FTPd requires Quota maxfiles:maxsize, not "yes") +if grep -q '^Quota' "$CONF"; then + sed -i 's/^Quota.*/Quota 100000:100000/' "$CONF" + echo "Fixed Quota line in $CONF" +else + echo 'Quota 100000:100000' >> "$CONF" + echo "Appended Quota line to $CONF" +fi + +# Optional: disable TLS if cert is missing (common cause of start failure) +if grep -q '^TLS[[:space:]]*1' "$CONF" && [ ! -f /etc/ssl/private/pure-ftpd.pem ]; then + sed -i 's/^TLS[[:space:]]*1/TLS 0/' "$CONF" + echo "Set TLS 0 (certificate missing)" +fi + +# Start service +systemctl start "$SERVICE" +sleep 1 +if systemctl is-active --quiet "$SERVICE"; then + echo "Pure-FTPd is running." + exit 0 +else + echo "Pure-FTPd failed to start. Run: systemctl status $SERVICE" + exit 1 +fi 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 "Introduction" %}

      +
      +

      {% trans "Introduction" %}

      +

      {% trans "CyberPanel's plugin system allows developers to extend the control panel's functionality with custom features. Plugins integrate seamlessly with CyberPanel's Django-based architecture, providing access to the full power of the platform while maintaining security and consistency." %}

      {% trans "What Can Plugins Do?" %}

      @@ -347,8 +458,11 @@
    • {% trans "Create custom reporting tools" %}
    • {% trans "Integrate security features" %}
    • +
      -

      {% trans "Prerequisites" %}

      +
      +

      {% trans "Prerequisites" %}

      +

      {% trans "Required Knowledge" %}

      • Python 3.6+: {% trans "Basic to intermediate Python knowledge" %}
      • @@ -357,8 +471,11 @@
      • Linux/Unix: {% trans "Basic command-line familiarity" %}
      • XML: {% trans "Understanding of XML structure for meta.xml" %}
      +
      -

      {% trans "Plugin Architecture Overview" %}

      +
      +

      {% trans "Plugin Architecture Overview" %}

      +

      {% trans "How Plugins Work" %}

      1. {% trans "Plugin Source Location" %}: /home/cyberpanel/plugins/ @@ -374,8 +491,11 @@
      +
      -

      {% trans "Creating Your First Plugin" %}

      +
      +

      {% trans "Creating Your First Plugin" %}

      +

      {% trans "Step 1: Create Plugin Directory Structure" %}

      # Navigate to plugins directory
       cd /home/cyberpanel/plugins
      @@ -401,6 +521,10 @@ mkdir -p migrations
      <settings_url>/plugins/myFirstPlugin/settings/</settings_url> </cyberpanelPluginConfig> +
      + {% trans "Required: Category (type)" %}: {% trans "The <type> field is required. See the Plugin Categories section below for valid options. Plugins without a valid category will not appear in the Plugin Store." %} +
      +

      {% trans "Step 3: Create urls.py" %}

      from django.urls import path
       from . import views
      @@ -458,8 +582,11 @@ def main_view(request):
               
      {% trans "Important" %}: {% trans "Always use the @cyberpanel_login_required decorator for all views to ensure users are authenticated." %}
      +
      -

      {% trans "Plugin Structure & Files" %}

      +
      +

      {% trans "Plugin Structure & Files" %}

      +

      {% trans "Required Files" %}

      • __init__.py - {% trans "Required" %} - {% trans "Python package marker" %}
      • @@ -476,8 +603,11 @@ def main_view(request):
      • templates/ - {% trans "Optional" %} - {% trans "HTML templates" %}
      • static/ - {% trans "Optional" %} - {% trans "CSS, JS, images" %}
      +
      -

      {% trans "Version Numbering (Semantic Versioning)" %}

      +
      +

      {% trans "Version Numbering (Semantic Versioning)" %}

      +

      {% trans "CyberPanel plugins use semantic versioning (SemVer) with a three-number format (X.Y.Z) to help users understand the impact of each update:" %}

      @@ -505,8 +635,107 @@ def main_view(request):

      {% trans "Version Format in meta.xml" %}

      <version>1.0.0</version>

      {% trans "Never use formats like '1.0' or 'v1.0'. Always use the full semantic version: '1.0.0'" %}

      +
      -

      {% trans "Core Components" %}

      +
      +

      {% trans "Plugin Categories" %}

      +
      +

      {% trans "The <type> field in meta.xml determines how your plugin is grouped in the Plugin Store. Use exactly one of these values (case-sensitive):" %}

      + + + + + + + + + + + + + + + + + + +
      {% trans "Category" %}{% trans "Purpose" %}
      Utility{% trans "General-purpose tools, helpers, and utilities" %}
      Security{% trans "Security features: firewalls, fail2ban, SSL, etc." %}
      Backup{% trans "Backup, snapshot, and restore functionality" %}
      Performance{% trans "Caching, optimization, and performance tuning" %}
      Monitoring{% trans "Monitoring, alerts, and health checks" %}
      Integration{% trans "Third-party integrations: Discord, Slack, webhooks, APIs" %}
      Email{% trans "Email marketing, SMTP, mail management" %}
      Development{% trans "Developer tools: PM2, Node.js, deployment" %}
      Analytics{% trans "Analytics, tracking, and reporting" %}
      +
      + +
      +

      {% trans "Freshness Badges" %}

      +
      +

      {% trans "The Plugin Store and Installed Plugins views display freshness badges based on the last update date (modify_date from GitHub commit or meta.xml file mtime). These help users quickly see how actively maintained a plugin is:" %}

      + + + + + + + + + + + + + + +
      {% trans "Badge" %}{% trans "Condition" %}{% trans "Meaning" %}
      NEW{% trans "Updated within last 90 days" %}{% trans "Recently released or actively maintained" %}
      Stable{% trans "Updated within last 365 days" %}{% trans "Updated within the past year" %}
      Unstable{% trans "1–2 years since last update" %}{% trans "May need maintenance; consider forking or updating" %}
      STALE{% trans "Over 2 years since last update" %}{% trans "Not updated recently; use with caution" %}
      +

      {% trans "Badges are calculated automatically from the plugin's modify_date. For plugins in the Plugin Store, this comes from the GitHub repository's last commit. For installed plugins, it uses the meta.xml file modification time." %}

      +
      + +
      +

      {% trans "Premium/Paid Plugin Creation" %}

      +
      +

      {% trans "You can create premium (paid) plugins and implement your own verification system. This includes optional encryption between the plugin and your verification site to prevent unauthorized bypass." %}

      + +

      {% trans "1. Mark Your Plugin as Paid in meta.xml" %}

      +
      <paid>true</paid>
      +<patreon_tier>Your Tier Name</patreon_tier>
      +<patreon_url>https://www.patreon.com/your-page</patreon_url>
      +

      {% trans "Set <paid>true</paid> to display the Premium badge and subscription prompts in the Plugin Store." %}

      + +

      {% trans "2. Build Your Own Verification System" %}

      +

      {% trans "Premium plugins typically verify access via a remote API. You can host this on your own site. Common verification methods:" %}

      +
        +
      • {% trans "Patreon" %}: {% trans "Verify membership via Patreon OAuth/API" %}
      • +
      • {% trans "PayPal" %}: {% trans "Verify one-time or recurring payments" %}
      • +
      • {% trans "Plugin Grants" %}: {% trans "Admin panel where you manually grant access by email, IP, or domain" %}
      • +
      • {% trans "Activation Keys" %}: {% trans "Generate unique keys when granting access; users enter the key in the plugin" %}
      • +
      + +

      {% trans "3. Optional: Encrypt Plugin–API Communication" %}

      +

      {% trans "To protect against users modifying your plugin to bypass verification, you can encrypt the communication between the plugin and your verification API using AES-256-CBC. This ensures:" %}

      +
        +
      • {% trans "Verification requests cannot be easily intercepted or forged" %}
      • +
      • {% trans "Responses cannot be tampered with" %}
      • +
      • {% trans "Your verification logic remains on your server; the plugin only encrypts/decrypts with a shared key" %}
      • +
      +

      {% trans "Implementation outline:" %}

      +
        +
      1. {% trans "Generate a 32-byte secret key and store it in your API config (e.g. config.php)" %}
      2. +
      3. {% trans "In your plugin (Python), encrypt outgoing requests and decrypt responses using the same key" %}
      4. +
      5. {% trans "On your API (PHP/Python), decrypt incoming requests and encrypt responses" %}
      6. +
      7. {% trans "Use the X-Encrypted: 1 header to indicate encrypted payloads" %}
      8. +
      +

      {% trans "Both sides must use the same AES-256-CBC key. Keep the key secret and never commit it to public repositories. Store it in a protected config file outside the web root or in environment variables." %}

      + +

      {% trans "4. Typical Premium Plugin Flow" %}

      +
        +
      1. {% trans "User installs your premium plugin" %}
      2. +
      3. {% trans "Plugin shows an activation screen (Patreon/PayPal links, or activation key input)" %}
      4. +
      5. {% trans "Plugin calls your verification API with user identifier (email, domain, IP) or activation key" %}
      6. +
      7. {% trans "Your API checks: Plugin Grants, activation key, Patreon, or PayPal — in your preferred order" %}
      8. +
      9. {% trans "If access is granted, API returns success; plugin unlocks features and optionally stores the key locally" %}
      10. +
      +
      + {% trans "Tip" %}: {% trans "Provide multiple verification paths: Patreon for subscribers, PayPal for one-time purchasers, Plugin Grants for beta testers or sponsors, and activation keys for manual grants. Encryption is optional but recommended for paid plugins to deter bypass attempts." %} +
      +
      + +
      +

      {% trans "Core Components" %}

      +

      {% trans "1. Authentication & Security" %}

      {% trans "Always use the cyberpanel_login_required decorator:" %}

      @cyberpanel_login_required
      @@ -532,8 +761,11 @@ urlpatterns = [
       {% load static %}
       {% load i18n %}
      {% endverbatim %} +
      -

      {% trans "Advanced Features" %}

      +
      +

      {% trans "Advanced Features" %}

      +

      {% trans "Database Models" %}

      from django.db import models
       
      @@ -558,8 +790,11 @@ class MyForm(forms.Form):
       def api_endpoint(request):
           data = {'status': 'success'}
           return JsonResponse(data)
      +
      -

      {% trans "Best Practices" %}

      +
      +

      {% trans "Best Practices" %}

      +
      • {% trans "Keep files under 500 lines - split into modules if needed" %}
      • {% trans "Use descriptive names for functions and variables" %}
      • @@ -569,8 +804,11 @@ def api_endpoint(request):
      • {% trans "Use Django ORM instead of raw SQL" %}
      • {% trans "Test your plugin thoroughly before distribution" %}
      +
      -

      {% trans "Security Guidelines" %}

      +
      +

      {% trans "Security Guidelines" %}

      +
      {% trans "Security is Critical" %}: {% trans "Always validate input, use parameterized queries, sanitize output, and check user permissions." %}
      @@ -582,8 +820,11 @@ def api_endpoint(request):
    • {% trans "Use HTTPS for sensitive operations" %}
    • {% trans "Implement rate limiting for API endpoints" %}
    • +
      -

      {% trans "Testing & Debugging" %}

      +
      +

      {% trans "Testing & Debugging" %}

      +

      {% trans "Common Issues" %}

      • {% trans "Template Not Found" %}: {% trans "Check template path and name" %}
      • @@ -601,16 +842,22 @@ xmllint --noout meta.xml # View logs tail -f /usr/local/lscp/logs/error.log
        +
      -

      {% trans "Packaging & Distribution" %}

      +
      +

      {% trans "Packaging & Distribution" %}

      +

      {% trans "Create Plugin Package" %}

      cd /home/cyberpanel/plugins/myPlugin
       zip -r myPlugin-v1.0.0.zip . \
           -x "*.pyc" \
           -x "__pycache__/*" \
           -x "*.log"
      +
      -

      {% trans "Troubleshooting" %}

      +
      +

      {% trans "Troubleshooting" %}

      +

      {% trans "Installation Issues" %}

      • {% trans "Check meta.xml format and validity" %}
      • @@ -626,8 +873,11 @@ zip -r myPlugin-v1.0.0.zip . \
      • {% trans "Review template paths" %}
      • {% trans "Check for JavaScript errors" %}
      +
      -

      {% trans "Examples & References" %}

      +
      +

      {% trans "Examples & References" %}

      +

      {% trans "Reference Plugins" %}

      • examplePlugin: {% trans "Basic plugin structure" %} @@ -663,28 +913,40 @@ zip -r myPlugin-v1.0.0.zip . \
        {% trans "Ready to Start?" %} {% trans "Begin with a simple plugin and gradually add more features as you become familiar with the system. Check the examplePlugin and testPlugin directories for complete working examples." %}
        +

      {% trans "Author" %}: master3395 | - {% trans "Version" %}: 2.0.0 | - {% trans "Last Updated" %}: 2026-01-04 + {% trans "Version" %}: 2.1.0 | + {% trans "Last Updated" %}: 2026-02-02

      {% endblock %} \ No newline at end of file diff --git a/pluginHolder/urls.py b/pluginHolder/urls.py index 7e4c547cb..446fc7125 100644 --- a/pluginHolder/urls.py +++ b/pluginHolder/urls.py @@ -1,6 +1,95 @@ -from django.urls import path +# -*- coding: utf-8 -*- +""" +PluginHolder URL configuration. +Static routes are defined first; then URLs for each installed plugin are +included dynamically so /plugins//... (e.g. settings/) works +without hardcoding plugin names in the main CyberCP urls.py. + +Discovery order: /usr/local/CyberCP first (installed), then source paths +(/home/cyberpanel/plugins, /home/cyberpanel-plugins) so settings work even +when the plugin is only present in source. +""" +from django.urls import path, include +import os +import sys + +# Ensure plugin roots are on sys.path first so __import__(plugin_name + '.urls') can find packages +_INSTALLED_PLUGINS_PATH = '/usr/local/CyberCP' +_PLUGIN_SOURCE_PATHS = ['/home/cyberpanel/plugins', '/home/cyberpanel-plugins'] +for _p in [_INSTALLED_PLUGINS_PATH] + _PLUGIN_SOURCE_PATHS: + if _p and os.path.isdir(_p) and _p not in sys.path: + sys.path.insert(0, _p) + from . import views +# Installed plugins live under this path (must match pluginInstaller and pluginHolder.views) +INSTALLED_PLUGINS_PATH = _INSTALLED_PLUGINS_PATH + +# Source paths for plugins (same as pluginHolder.views PLUGIN_SOURCE_PATHS) +# Checked when plugin is not under INSTALLED_PLUGINS_PATH so URLs still work +PLUGIN_SOURCE_PATHS = ['/home/cyberpanel/plugins', '/home/cyberpanel-plugins'] + +# Plugin directory names that must not be routed here (core apps or reserved paths) +RESERVED_PLUGIN_PATHS = frozenset([ + 'installed', 'help', 'api', # pluginHolder's own path segments + 'emailMarketing', 'emailPremium', 'pluginHolder', 'loginSystem', 'baseTemplate', + 'packages', 'websiteFunctions', 'userManagment', 'dns', 'databases', 'ftp', + 'filemanager', 'mailServer', 'cloudAPI', 'containerization', 'IncBackups', + 'CLManager', 's3Backups', 'dockerManager', 'aiScanner', 'firewall', 'tuning', + 'serverStatus', 'serverLogs', 'backup', 'managePHP', 'manageSSL', 'manageServices', + 'highAvailability', +]) + + +def _plugin_has_urls(plugin_dir): + """Return True if plugin_dir has meta.xml and urls.py.""" + if not os.path.isdir(plugin_dir): + return False + return (os.path.exists(os.path.join(plugin_dir, 'meta.xml')) and + os.path.exists(os.path.join(plugin_dir, 'urls.py'))) + + +def _get_installed_plugin_list(): + """ + Return sorted list of (plugin_name, path_parent) to mount at /plugins//. + path_parent is the directory that must be on sys.path to import the plugin + (e.g. /usr/local/CyberCP or /home/cyberpanel/plugins). + First discovers from INSTALLED_PLUGINS_PATH, then from PLUGIN_SOURCE_PATHS. + """ + seen = set() + result = [] # (name, path_parent) + + # 1) Installed location (canonical) + if os.path.isdir(INSTALLED_PLUGINS_PATH): + try: + for name in os.listdir(INSTALLED_PLUGINS_PATH): + if name in RESERVED_PLUGIN_PATHS or name.startswith('.'): + continue + plugin_dir = os.path.join(INSTALLED_PLUGINS_PATH, name) + if _plugin_has_urls(plugin_dir): + seen.add(name) + result.append((name, INSTALLED_PLUGINS_PATH)) + except (OSError, IOError): + pass + + # 2) Source paths (fallback so /plugins/PluginName/settings/ works even if not in CyberCP) + for base in PLUGIN_SOURCE_PATHS: + if not os.path.isdir(base): + continue + try: + for name in os.listdir(base): + if name in seen or name in RESERVED_PLUGIN_PATHS or name.startswith('.'): + continue + plugin_dir = os.path.join(base, name) + if _plugin_has_urls(plugin_dir): + seen.add(name) + result.append((name, base)) + except (OSError, IOError): + pass + + return sorted(result, key=lambda x: x[0]) + + urlpatterns = [ path('installed', views.installed, name='installed'), path('help/', views.help_page, name='help'), @@ -13,5 +102,30 @@ urlpatterns = [ path('api/store/upgrade//', views.upgrade_plugin, name='upgrade_plugin'), path('api/backups//', views.get_plugin_backups, name='get_plugin_backups'), path('api/revert//', views.revert_plugin, name='revert_plugin'), - path('/help/', views.plugin_help, name='plugin_help'), + path('api/debug-plugins/', views.debug_loaded_plugins, name='debug_loaded_plugins'), ] + +# Include each installed plugin's URLs *before* the catch-all so /plugins//settings/ etc. match +_loaded_plugins = [] +_failed_plugins = {} +for _plugin_name, _path_parent in _get_installed_plugin_list(): + try: + if _path_parent not in sys.path: + sys.path.insert(0, _path_parent) + __import__(_plugin_name + '.urls') + urlpatterns.append(path(_plugin_name + '/', include(_plugin_name + '.urls'))) + _loaded_plugins.append(_plugin_name) + except Exception as e: + import traceback + _failed_plugins[_plugin_name] = str(e) + try: + from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter as _logging + _logging.writeToFile( + 'pluginHolder.urls: Skipping plugin "%s" (urls not loadable): %s' + % (_plugin_name, e) + ) + _logging.writeToFile(traceback.format_exc()) + except Exception: + pass + +urlpatterns.append(path('/help/', views.plugin_help, name='plugin_help')) diff --git a/pluginHolder/views.py b/pluginHolder/views.py index 7ff70153a..166a4f0f5 100644 --- a/pluginHolder/views.py +++ b/pluginHolder/views.py @@ -5,6 +5,7 @@ from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_http_methods from plogical.mailUtilities import mailUtilities import os +import shutil import subprocess import shlex import json @@ -35,6 +36,57 @@ GITHUB_COMMITS_API = 'https://api.github.com/repos/master3395/cyberpanel-plugins # Plugin backup configuration PLUGIN_BACKUP_DIR = '/home/cyberpanel/plugin_backups' +# Plugin source paths (checked in order; first match wins for install) +PLUGIN_SOURCE_PATHS = ['/home/cyberpanel/plugins', '/home/cyberpanel-plugins'] + +# Builtin/core plugins that are part of CyberPanel (not user-installable plugins) +# These plugins show "Built-in" badge and only Settings button (no Deactivate/Uninstall) +BUILTIN_PLUGINS = frozenset(['emailMarketing', 'emailPremium']) + +# Core CyberPanel app dirs under /usr/local/CyberCP that must not be counted as "installed plugins" +# (matches pluginHolder.urls so Installed count = store/plugin dirs only, not core apps) +RESERVED_PLUGIN_DIRS = frozenset([ + 'api', 'backup', 'baseTemplate', 'cloudAPI', 'CLManager', 'containerization', 'CyberCP', + 'databases', 'dns', 'dockerManager', 'emailMarketing', 'emailPremium', 'filemanager', + 'firewall', 'ftp', 'highAvailability', 'IncBackups', 'loginSystem', 'mailServer', + 'managePHP', 'manageSSL', 'manageServices', 'packages', 'pluginHolder', 'plogical', + 'pluginInstaller', 'serverLogs', 'serverStatus', 's3Backups', 'tuning', 'userManagment', + 'websiteFunctions', 'aiScanner', 'dns', 'help', 'installed', +]) + +def _get_plugin_source_path(plugin_name): + """Return the full path to a plugin's source directory, or None if not found.""" + for base in PLUGIN_SOURCE_PATHS: + path = os.path.join(base, plugin_name) + meta_path = os.path.join(path, 'meta.xml') + if os.path.isdir(path) and os.path.exists(meta_path): + return path + return None + +def _ensure_plugin_meta_xml(plugin_name): + """ + If plugin is installed (directory exists) but meta.xml is missing, + restore it from source or from GitHub so the grid and version checks work. + """ + installed_dir = os.path.join('/usr/local/CyberCP', plugin_name) + installed_meta = os.path.join(installed_dir, 'meta.xml') + if not os.path.isdir(installed_dir) or os.path.exists(installed_meta): + return + source_path = _get_plugin_source_path(plugin_name) + if source_path: + source_meta = os.path.join(source_path, 'meta.xml') + if os.path.exists(source_meta): + try: + shutil.copy2(source_meta, installed_meta) + logging.writeToFile(f"Restored meta.xml for {plugin_name} from source") + except Exception as e: + logging.writeToFile(f"Could not restore meta.xml for {plugin_name}: {e}") + return + try: + _sync_meta_xml_from_github(plugin_name) + except Exception: + pass + def _get_plugin_state_file(plugin_name): """Get the path to the plugin state file""" if not os.path.exists(PLUGIN_STATE_DIR): @@ -53,6 +105,33 @@ def _is_plugin_enabled(plugin_name): return True # Default to enabled if file read fails return True # Default to enabled if state file doesn't exist + +def _get_freshness_badge(modify_date): + """ + Return freshness badge (NEW/Stable/STALE) based on modify_date. + modify_date format: 'YYYY-MM-DD HH:MM:SS' or 'N/A' + - 0-90 days: NEW (yellow) + - 90-365 days: Stable (green) + - 730+ days: STALE (red) + - 365-730 days: no badge + """ + if not modify_date or modify_date == 'N/A' or not isinstance(modify_date, str): + return None + try: + dt = datetime.strptime(modify_date[:19], '%Y-%m-%d %H:%M:%S') + days_ago = (datetime.now() - dt).days + if days_ago <= 90: + return {'badge': 'NEW', 'class': 'freshness-badge-new', 'title': 'This plugin was released/updated within the last 3 months'} + elif days_ago <= 365: + return {'badge': 'Stable', 'class': 'freshness-badge-stable', 'title': 'This plugin was updated within the last year'} + elif days_ago < 730: + return {'badge': 'Unstable', 'class': 'freshness-badge-unstable', 'title': 'This plugin has not been updated in over 1 year'} + else: + return {'badge': 'STALE', 'class': 'freshness-badge-stale', 'title': 'This plugin has not been updated in over 2 years'} + except (ValueError, TypeError): + pass + return None + def _set_plugin_state(plugin_name, enabled): """Set plugin enabled/disabled state""" state_file = _get_plugin_state_file(plugin_name) @@ -73,23 +152,47 @@ def help_page(request): def installed(request): mailUtilities.checkHome() - pluginPath = '/home/cyberpanel/plugins' installedPath = '/usr/local/CyberCP' pluginList = [] errorPlugins = [] processed_plugins = set() # Track which plugins we've already processed - # First, process plugins from source directory - if os.path.exists(pluginPath): + # Repair pass: ensure every installed plugin dir has meta.xml (from source or GitHub) so counts and grid are correct + if os.path.exists(installedPath): + for plugin in os.listdir(installedPath): + if plugin.startswith('.') or plugin in RESERVED_PLUGIN_DIRS: + continue + plugin_dir = os.path.join(installedPath, plugin) + if os.path.isdir(plugin_dir): + _ensure_plugin_meta_xml(plugin) + + # First, process plugins from source directories (multiple paths: /home/cyberpanel/plugins, /home/cyberpanel-plugins) + # BUT: Skip plugins that are already installed - we'll process those from the installed location instead + for pluginPath in PLUGIN_SOURCE_PATHS: + if not os.path.exists(pluginPath): + continue + try: + dirs_in_path = [p for p in os.listdir(pluginPath) if os.path.isdir(os.path.join(pluginPath, p))] + logging.writeToFile(f"Plugin source path {pluginPath}: directories {sorted(dirs_in_path)}") + except Exception as e: + logging.writeToFile(f"Plugin source path {pluginPath}: listdir error {e}") for plugin in os.listdir(pluginPath): + if plugin in processed_plugins: + continue # Skip files (like .zip files) - only process directories pluginDir = os.path.join(pluginPath, plugin) if not os.path.isdir(pluginDir): continue + # Use same "installed" criterion as install endpoint: plugin directory in /usr/local/CyberCP/ + installed_dir = os.path.join(installedPath, plugin) + completePath = os.path.join(installedPath, plugin, 'meta.xml') + if os.path.exists(completePath): + # Plugin is fully installed (dir + meta.xml), skip - second loop will add it + continue + data = {} # Try installed location first, then fallback to source location - completePath = installedPath + '/' + plugin + '/meta.xml' sourcePath = os.path.join(pluginDir, 'meta.xml') # Determine which meta.xml to use @@ -103,7 +206,8 @@ def installed(request): # Add error handling to prevent 500 errors try: if metaXmlPath is None: - # No meta.xml found in either location - skip silently + # No meta.xml found in either location - skip (log for diagnostics) + logging.writeToFile(f"Plugin {plugin}: skipped (no meta.xml in source or installed)") continue pluginMetaData = ElementTree.parse(metaXmlPath) @@ -115,26 +219,35 @@ def installed(request): desc_elem = root.find('description') version_elem = root.find('version') - # Type field is optional (testPlugin doesn't have it) - if name_elem is None or desc_elem is None or version_elem is None: - errorPlugins.append({'name': plugin, 'error': 'Missing required metadata fields (name, description, or version)'}) + # All fields required including type (category) - no default + if name_elem is None or type_elem is None or desc_elem is None or version_elem is None: + errorPlugins.append({'name': plugin, 'error': 'Missing required metadata fields (name, type/category, description, or version)'}) logging.writeToFile(f"Plugin {plugin}: Missing required metadata fields in meta.xml") continue - # Check if text is None (empty elements) - if name_elem.text is None or desc_elem.text is None or version_elem.text is None: - errorPlugins.append({'name': plugin, 'error': 'Empty metadata fields'}) + # Check if text is None or empty (all required) + type_text = type_elem.text.strip() if type_elem.text else '' + if name_elem.text is None or desc_elem.text is None or version_elem.text is None or not type_text: + errorPlugins.append({'name': plugin, 'error': 'Empty metadata fields (name, type/category, description, or version required)'}) logging.writeToFile(f"Plugin {plugin}: Empty metadata fields in meta.xml") continue + # Valid categories only: Utility, Security, Backup, Performance (Plugin category removed) + if type_text.lower() not in ('utility', 'security', 'backup', 'performance', 'monitoring', 'integration', 'email', 'development', 'analytics'): + errorPlugins.append({'name': plugin, 'error': f'Invalid category "{type_text}". Use: Utility, Security, Backup, or Performance.'}) + logging.writeToFile(f"Plugin {plugin}: Invalid category '{type_text}'") + continue + data['name'] = name_elem.text - data['type'] = type_elem.text if type_elem is not None and type_elem.text is not None else 'Plugin' + data['type'] = type_text data['desc'] = desc_elem.text data['version'] = version_elem.text data['plugin_dir'] = plugin # Plugin directory name - # Check if plugin is installed (only if it exists in /usr/local/CyberCP/) - # Source directory presence doesn't mean installed - it just means the source files are available - data['installed'] = os.path.exists(completePath) + # Set builtin flag (core CyberPanel plugins vs user-installable plugins) + data['builtin'] = plugin in BUILTIN_PLUGINS + # Installed = plugin directory exists (must match install endpoint which uses directory existence) + # Fixes grid showing "Not Installed" when directory exists but meta.xml is missing + data['installed'] = os.path.isdir(installed_dir) # Get plugin enabled state (only for installed plugins) if data['installed']: @@ -158,6 +271,7 @@ def installed(request): modify_date = 'N/A' data['modify_date'] = modify_date + data['freshness_badge'] = _get_freshness_badge(modify_date) # Extract settings URL or main URL for "Manage" button settings_url_elem = root.find('settings_url') @@ -178,11 +292,9 @@ def installed(request): # Special handling for emailMarketing if plugin == 'emailMarketing': data['manage_url'] = '/emailMarketing/' - elif os.path.exists(completePath): - # Check if settings route exists, otherwise use main plugin URL - settings_route = f'/plugins/{plugin}/settings/' + elif data['installed']: + # Plugin directory exists; use main plugin URL main_route = f'/plugins/{plugin}/' - # Default to main route - most plugins have a main route even if no settings data['manage_url'] = main_route else: data['manage_url'] = None @@ -213,20 +325,14 @@ def installed(request): errorPlugins.append({'name': plugin, 'error': f'XML parse error: {str(e)}'}) logging.writeToFile(f"Plugin {plugin}: XML parse error - {str(e)}") # Don't mark as processed if it failed - let installed check handle it - # This ensures plugins that exist in /usr/local/CyberCP/ but have bad source meta.xml still get counted - if not os.path.exists(completePath): - # Only skip if it's not actually installed + if not os.path.isdir(installed_dir): continue - # If it exists in installed location, don't mark as processed so it gets checked there continue except Exception as e: errorPlugins.append({'name': plugin, 'error': f'Error loading plugin: {str(e)}'}) logging.writeToFile(f"Plugin {plugin}: Error loading - {str(e)}") - # Don't mark as processed if it failed - let installed check handle it - if not os.path.exists(completePath): - # Only skip if it's not actually installed + if not os.path.isdir(installed_dir): continue - # If it exists in installed location, don't mark as processed so it gets checked there continue # Also check for installed plugins that don't have source directories @@ -242,6 +348,7 @@ def installed(request): if not os.path.isdir(pluginInstalledDir): continue + _ensure_plugin_meta_xml(plugin) metaXmlPath = os.path.join(pluginInstalledDir, 'meta.xml') if not os.path.exists(metaXmlPath): continue @@ -252,23 +359,33 @@ def installed(request): pluginMetaData = ElementTree.parse(metaXmlPath) root = pluginMetaData.getroot() - # Validate required fields + # Validate required fields (including type/category - no default) name_elem = root.find('name') type_elem = root.find('type') desc_elem = root.find('description') version_elem = root.find('version') - if name_elem is None or desc_elem is None or version_elem is None: + if name_elem is None or type_elem is None or desc_elem is None or version_elem is None: + errorPlugins.append({'name': plugin, 'error': 'Missing required metadata (name, type/category, description, or version)'}) continue - if name_elem.text is None or desc_elem.text is None or version_elem.text is None: + type_text = type_elem.text.strip() if type_elem.text else '' + if name_elem.text is None or desc_elem.text is None or version_elem.text is None or not type_text: + errorPlugins.append({'name': plugin, 'error': 'Empty metadata (type/category required)'}) + continue + + # Valid categories only: Utility, Security, Backup, Performance (Plugin category removed) + if type_text.lower() not in ('utility', 'security', 'backup', 'performance', 'monitoring', 'integration', 'email', 'development', 'analytics'): + errorPlugins.append({'name': plugin, 'error': f'Invalid category "{type_text}". Use: Utility, Security, Backup, or Performance.'}) continue data['name'] = name_elem.text - data['type'] = type_elem.text if type_elem is not None and type_elem.text is not None else 'Plugin' + data['type'] = type_text data['desc'] = desc_elem.text data['version'] = version_elem.text data['plugin_dir'] = plugin + # Set builtin flag (core CyberPanel plugins vs user-installable plugins) + data['builtin'] = plugin in BUILTIN_PLUGINS data['installed'] = True # This is an installed plugin data['enabled'] = _is_plugin_enabled(plugin) @@ -287,6 +404,7 @@ def installed(request): modify_date = 'N/A' data['modify_date'] = modify_date + data['freshness_badge'] = _get_freshness_badge(modify_date) # Extract settings URL or main URL settings_url_elem = root.find('settings_url') @@ -329,6 +447,7 @@ def installed(request): # else: is_paid already False from initialization above pluginList.append(data) + processed_plugins.add(plugin) # Mark as processed to prevent duplicates except ElementTree.ParseError as e: errorPlugins.append({'name': plugin, 'error': f'XML parse error: {str(e)}'}) @@ -339,29 +458,85 @@ def installed(request): logging.writeToFile(f"Installed plugin {plugin}: Error loading - {str(e)}") continue - # Calculate installed and active counts - # Double-check by also counting plugins that actually exist in /usr/local/CyberCP/ + # Ensure redisManager and memcacheManager load when present (fallback if missed by listdir) + for plugin_name in ('redisManager', 'memcacheManager'): + if plugin_name in processed_plugins: + continue + source_path = _get_plugin_source_path(plugin_name) + installed_meta = os.path.join(installedPath, plugin_name, 'meta.xml') + meta_xml_path = installed_meta if os.path.exists(installed_meta) else (os.path.join(source_path, 'meta.xml') if source_path else None) + if not meta_xml_path or not os.path.exists(meta_xml_path): + continue + try: + root = ElementTree.parse(meta_xml_path).getroot() + name_elem = root.find('name') + type_elem = root.find('type') + desc_elem = root.find('description') + version_elem = root.find('version') + if name_elem is None or type_elem is None or desc_elem is None or version_elem is None: + continue + type_text = (type_elem.text or '').strip() + if not type_text or name_elem.text is None or desc_elem.text is None or version_elem.text is None: + continue + if type_text.lower() not in ('utility', 'security', 'backup', 'performance', 'monitoring', 'integration', 'email', 'development', 'analytics'): + continue + complete_path = os.path.join(installedPath, plugin_name, 'meta.xml') + data = { + 'name': name_elem.text, + 'type': type_text, + 'desc': desc_elem.text, + 'version': version_elem.text, + 'plugin_dir': plugin_name, + 'builtin': plugin_name in BUILTIN_PLUGINS, # Set builtin flag + 'installed': os.path.exists(complete_path), + 'enabled': _is_plugin_enabled(plugin_name) if os.path.exists(complete_path) else False, + 'is_paid': False, + 'patreon_tier': None, + 'patreon_url': None, + 'manage_url': f'/plugins/{plugin_name}/', + 'author': root.find('author').text if root.find('author') is not None and root.find('author').text else 'Unknown', + } + try: + modify_time = os.path.getmtime(meta_xml_path) + data['modify_date'] = datetime.fromtimestamp(modify_time).strftime('%Y-%m-%d %H:%M:%S') + except Exception: + data['modify_date'] = 'N/A' + data['freshness_badge'] = _get_freshness_badge(data['modify_date']) + paid_elem = root.find('paid') + if paid_elem is not None and paid_elem.text and paid_elem.text.lower() == 'true': + data['is_paid'] = True + data['patreon_tier'] = 'CyberPanel Paid Plugin' + data['patreon_url'] = root.find('patreon_url').text if root.find('patreon_url') is not None else 'https://www.patreon.com/membership/27789984' + pluginList.append(data) + processed_plugins.add(plugin_name) + logging.writeToFile(f"Plugin {plugin_name}: added via fallback (source or installed)") + except Exception as e: + logging.writeToFile(f"Plugin {plugin_name} fallback load error: {str(e)}") + + # Calculate installed and active counts: only count real plugins (have meta.xml, not core apps) installed_plugins_in_filesystem = set() if os.path.exists(installedPath): for plugin in os.listdir(installedPath): + if plugin.startswith('.') or plugin in RESERVED_PLUGIN_DIRS: + continue pluginInstalledDir = os.path.join(installedPath, plugin) - if os.path.isdir(pluginInstalledDir): - metaXmlPath = os.path.join(pluginInstalledDir, 'meta.xml') - if os.path.exists(metaXmlPath): - installed_plugins_in_filesystem.add(plugin) + if not os.path.isdir(pluginInstalledDir): + continue + if not os.path.exists(os.path.join(pluginInstalledDir, 'meta.xml')): + continue + installed_plugins_in_filesystem.add(plugin) - # Count installed plugins from the list installed_count = len([p for p in pluginList if p.get('installed', False)]) active_count = len([p for p in pluginList if p.get('installed', False) and p.get('enabled', False)]) - # If there's a discrepancy, use the filesystem count as the source of truth + # Use the larger of list count and filesystem count so header never shows less than grid filesystem_installed_count = len(installed_plugins_in_filesystem) - if filesystem_installed_count != installed_count: - logging.writeToFile(f"WARNING: Plugin count mismatch! List says {installed_count}, filesystem has {filesystem_installed_count}") - logging.writeToFile(f"Plugins in filesystem: {sorted(installed_plugins_in_filesystem)}") - logging.writeToFile(f"Plugins in list with installed=True: {[p.get('plugin_dir') for p in pluginList if p.get('installed', False)]}") - # Use filesystem count as source of truth - installed_count = filesystem_installed_count + list_installed_count = len([p for p in pluginList if p.get('installed', False)]) + if filesystem_installed_count != list_installed_count: + logging.writeToFile(f"Plugin count: list installed={list_installed_count}, filesystem with meta.xml={filesystem_installed_count}") + installed_count = max(list_installed_count, filesystem_installed_count) + if active_count > installed_count: + active_count = installed_count # Debug logging to help identify discrepancies logging.writeToFile(f"Plugin count: Total={len(pluginList)}, Installed={installed_count}, Active={active_count}") @@ -371,6 +546,9 @@ def installed(request): # Get cache expiry timestamp for display (will be converted to local time in browser) cache_expiry_timestamp, _ = _get_cache_expiry_time() + # Sort plugins A-Å by name (case-insensitive) for Grid and Table view + pluginList.sort(key=lambda p: (p.get('name') or '').lower()) + proc = httpProc(request, 'pluginHolder/plugins.html', {'plugins': pluginList, 'error_plugins': errorPlugins, 'installed_count': installed_count, 'active_count': active_count, @@ -382,12 +560,12 @@ def installed(request): def install_plugin(request, plugin_name): """Install a plugin""" try: - # Check if plugin source exists - pluginSource = '/home/cyberpanel/plugins/' + plugin_name - if not os.path.exists(pluginSource): + # Check if plugin source exists (in any configured source path) + pluginSource = _get_plugin_source_path(plugin_name) + if not pluginSource: return JsonResponse({ 'success': False, - 'error': f'Plugin source not found: {plugin_name}' + 'error': f'Plugin source not found: {plugin_name} (checked: {", ".join(PLUGIN_SOURCE_PATHS)})' }, status=404) # Check if already installed @@ -470,6 +648,8 @@ def install_plugin(request, plugin_name): # Set plugin to enabled by default after installation _set_plugin_state(plugin_name, True) + _ensure_plugin_meta_xml(plugin_name) + logging.writeToFile(f"Plugin {plugin_name} installed successfully (upload)") return JsonResponse({ 'success': True, 'message': f'Plugin {plugin_name} installed successfully' @@ -764,6 +944,27 @@ def _get_installed_version(plugin_dir, plugin_install_dir): return None +def _sync_meta_xml_from_github(plugin_name, plugin_install_dir='/usr/local/CyberCP'): + """ + Fetch meta.xml from GitHub raw (main) and overwrite installed meta.xml. + Ensures installed version matches store even when archive ZIP is cached/stale. + Returns True if synced, False on non-fatal failure (logged). + """ + meta_url = f'{GITHUB_RAW_BASE}/{plugin_name}/meta.xml' + meta_path = os.path.join(plugin_install_dir, plugin_name, 'meta.xml') + try: + req = urllib.request.Request(meta_url, headers={'User-Agent': 'CyberPanel-Plugin-Store/1.0'}) + with urllib.request.urlopen(req, timeout=10) as resp: + content = resp.read() + if content: + with open(meta_path, 'wb') as f: + f.write(content) + logging.writeToFile(f"Synced meta.xml for {plugin_name} from GitHub raw") + return True + except Exception as e: + logging.writeToFile(f"Could not sync meta.xml for {plugin_name} from GitHub: {str(e)}") + return False + def _create_plugin_backup(plugin_name, plugin_install_dir='/usr/local/CyberCP'): """ Create a backup of a plugin before upgrade @@ -943,6 +1144,7 @@ def _enrich_store_plugins(plugins): elif 'is_paid' not in plugin or plugin.get('is_paid') is None: # Try to check from local meta.xml if available meta_path = None + source_path = os.path.join(plugin_source_dir, plugin_dir) if os.path.exists(installed_path): meta_path = os.path.join(installed_path, 'meta.xml') elif os.path.exists(source_path): @@ -1050,10 +1252,21 @@ def _fetch_plugins_from_github(): patreon_url_elem = root.find('patreon_url') patreon_url = patreon_url_elem.text if patreon_url_elem is not None else 'https://www.patreon.com/c/newstargeted/membership' + # Category (type) is required - valid: Utility, Security, Backup, Performance (Plugin removed) + type_elem = root.find('type') + if type_elem is None or not type_elem.text or not type_elem.text.strip(): + logging.writeToFile(f"Plugin {plugin_name}: Missing required type/category in meta.xml, skipping") + continue + type_text = type_elem.text.strip().lower() + if type_text not in ('utility', 'security', 'backup', 'performance', 'monitoring', 'integration', 'email', 'development', 'analytics'): + logging.writeToFile(f"Plugin {plugin_name}: Invalid category '{type_elem.text}', skipping (use Utility, Security, Backup, or Performance)") + continue + + freshness = _get_freshness_badge(modify_date) plugin_data = { 'plugin_dir': plugin_name, 'name': root.find('name').text if root.find('name') is not None else plugin_name, - 'type': root.find('type').text if root.find('type') is not None else 'Plugin', + 'type': type_elem.text.strip(), 'description': root.find('description').text if root.find('description') is not None else '', 'version': root.find('version').text if root.find('version') is not None else '1.0.0', 'url': root.find('url').text if root.find('url') is not None else f'/plugins/{plugin_name}/', @@ -1062,6 +1275,7 @@ def _fetch_plugins_from_github(): 'github_url': f'https://github.com/master3395/cyberpanel-plugins/tree/main/{plugin_name}', 'about_url': f'https://github.com/master3395/cyberpanel-plugins/tree/main/{plugin_name}', 'modify_date': modify_date, + 'freshness_badge': freshness, 'is_paid': is_paid, 'patreon_tier': patreon_tier, 'patreon_url': patreon_url @@ -1110,21 +1324,29 @@ def _fetch_plugins_from_github(): @require_http_methods(["GET"]) def fetch_plugin_store(request): """Fetch plugins from the plugin store with caching""" - mailUtilities.checkHome() - - # Try to get from cache first - cached_plugins = _get_cached_plugins() - if cached_plugins is not None: - # Enrich cached plugins with installed/enabled status - enriched_plugins = _enrich_store_plugins(cached_plugins) - return JsonResponse({ - 'success': True, - 'plugins': enriched_plugins, - 'cached': True - }) - - # Cache miss or expired - fetch from GitHub try: + mailUtilities.checkHome() + except Exception as e: + logging.writeToFile(f"fetch_plugin_store: checkHome failed: {str(e)}") + return JsonResponse({ + 'success': False, + 'error': 'Authentication required. Please log in again.', + 'plugins': [] + }, status=401) + + try: + # Try to get from cache first + cached_plugins = _get_cached_plugins() + if cached_plugins is not None: + # Enrich cached plugins with installed/enabled status + enriched_plugins = _enrich_store_plugins(cached_plugins) + return JsonResponse({ + 'success': True, + 'plugins': enriched_plugins, + 'cached': True + }) + + # Cache miss or expired - fetch from GitHub plugins = _fetch_plugins_from_github() # Enrich plugins with installed/enabled status @@ -1139,7 +1361,7 @@ def fetch_plugin_store(request): 'plugins': enriched_plugins, 'cached': False }) - + except Exception as e: error_message = str(e) @@ -1218,22 +1440,48 @@ def upgrade_plugin(request, plugin_name): # Extract plugin directory from repository ZIP repo_zip = zipfile.ZipFile(io.BytesIO(repo_zip_data)) + namelist = repo_zip.namelist() - # Find plugin directory in ZIP - plugin_prefix = f'cyberpanel-plugins-main/{plugin_name}/' - plugin_files = [f for f in repo_zip.namelist() if f.startswith(plugin_prefix)] + # Discover top-level folder (GitHub uses repo-name-branch, e.g. cyberpanel-plugins-main) + top_level = None + for name in namelist: + if '/' in name: + top_level = name.split('/')[0] + break + elif name and not name.endswith('/'): + top_level = name + break + if not top_level: + raise Exception('GitHub archive has no recognizable structure') + # Find plugin folder in ZIP (case-insensitive: repo may have RedisManager vs redisManager) + plugin_prefix = None + plugin_name_lower = plugin_name.lower() + for name in namelist: + if '/' not in name: + continue + parts = name.split('/') + if len(parts) >= 2 and parts[0] == top_level and parts[1].lower() == plugin_name_lower: + # Use the actual casing from the ZIP for reading + plugin_prefix = f'{top_level}/{parts[1]}/' + break + if not plugin_prefix: + sample = namelist[:15] if len(namelist) > 15 else namelist + logging.writeToFile(f"Plugin {plugin_name} not in archive. Top-level={top_level}, sample paths: {sample}") + raise Exception(f'Plugin {plugin_name} not found in GitHub repository (checked under {top_level}/)') + + plugin_files = [f for f in namelist if f.startswith(plugin_prefix)] if not plugin_files: + logging.writeToFile(f"Plugin {plugin_name}: no files under prefix {plugin_prefix}") raise Exception(f'Plugin {plugin_name} not found in GitHub repository') - logging.writeToFile(f"Found {len(plugin_files)} files for plugin {plugin_name} in GitHub") + logging.writeToFile(f"Found {len(plugin_files)} files for plugin {plugin_name} in GitHub (prefix {plugin_prefix})") - # Create plugin ZIP file from GitHub with correct structure + # Create plugin ZIP with correct structure: plugin_name/... for install to /usr/local/CyberCP/plugin_name/ plugin_zip = zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) - for file_path in plugin_files: relative_path = file_path[len(plugin_prefix):] - if relative_path: # Skip directories + if relative_path: # Skip directory-only entries file_data = repo_zip.read(file_path) arcname = os.path.join(plugin_name, relative_path) plugin_zip.writestr(arcname, file_data) @@ -1276,7 +1524,10 @@ def upgrade_plugin(request, plugin_name): if not os.path.exists(pluginInstalled): raise Exception(f'Plugin upgrade failed: {pluginInstalled} does not exist after upgrade') - # Get new version + # Sync meta.xml from GitHub raw so version matches store (archive ZIP can be cached/stale) + _sync_meta_xml_from_github(plugin_name, '/usr/local/CyberCP') + + # Get new version (now reflects meta.xml from main) new_version = _get_installed_version(plugin_name, '/usr/local/CyberCP') logging.writeToFile(f"Plugin {plugin_name} upgraded successfully from {installed_version} to {new_version}") @@ -1449,41 +1700,55 @@ def install_from_store(request, plugin_name): # Extract plugin directory from repository ZIP repo_zip = zipfile.ZipFile(io.BytesIO(repo_zip_data)) + namelist = repo_zip.namelist() - # Find plugin directory in ZIP - plugin_prefix = f'cyberpanel-plugins-main/{plugin_name}/' - plugin_files = [f for f in repo_zip.namelist() if f.startswith(plugin_prefix)] - - if not plugin_files: + # Discover top-level folder and find plugin (case-insensitive) + top_level = None + for name in namelist: + if '/' in name: + top_level = name.split('/')[0] + break + if not top_level: + raise Exception('GitHub archive has no recognizable structure') + plugin_prefix = None + plugin_name_lower = plugin_name.lower() + for name in namelist: + if '/' not in name: + continue + parts = name.split('/') + if len(parts) >= 2 and parts[0] == top_level and parts[1].lower() == plugin_name_lower: + plugin_prefix = f'{top_level}/{parts[1]}/' + break + if not plugin_prefix: + repo_zip.close() logging.writeToFile(f"Plugin {plugin_name} not found in GitHub repository, trying local source") use_local_fallback = True else: - logging.writeToFile(f"Found {len(plugin_files)} files for plugin {plugin_name} in GitHub") - - # Create plugin ZIP file from GitHub with correct structure - # The ZIP must contain plugin_name/ directory structure for proper extraction - plugin_zip = zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) - - for file_path in plugin_files: - # Remove the repository root prefix - relative_path = file_path[len(plugin_prefix):] - if relative_path: # Skip directories - file_data = repo_zip.read(file_path) - # Add plugin_name prefix to maintain directory structure - arcname = os.path.join(plugin_name, relative_path) - plugin_zip.writestr(arcname, file_data) - - plugin_zip.close() - repo_zip.close() + plugin_files = [f for f in namelist if f.startswith(plugin_prefix)] + if not plugin_files: + repo_zip.close() + logging.writeToFile(f"Plugin {plugin_name} not found in GitHub repository, trying local source") + use_local_fallback = True + else: + logging.writeToFile(f"Found {len(plugin_files)} files for plugin {plugin_name} in GitHub") + plugin_zip = zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) + for file_path in plugin_files: + relative_path = file_path[len(plugin_prefix):] + if relative_path: + file_data = repo_zip.read(file_path) + arcname = os.path.join(plugin_name, relative_path) + plugin_zip.writestr(arcname, file_data) + plugin_zip.close() + repo_zip.close() except Exception as github_error: logging.writeToFile(f"GitHub download failed for {plugin_name}: {str(github_error)}, trying local source") use_local_fallback = True # Fallback to local source if GitHub download failed if use_local_fallback: - pluginSource = '/home/cyberpanel/plugins/' + plugin_name - if not os.path.exists(pluginSource): - raise Exception(f'Plugin {plugin_name} not found in GitHub repository and local source not found at {pluginSource}') + pluginSource = _get_plugin_source_path(plugin_name) + if not pluginSource: + raise Exception(f'Plugin {plugin_name} not found in GitHub repository and local source not found (checked: {", ".join(PLUGIN_SOURCE_PATHS)})') logging.writeToFile(f"Using local source for {plugin_name} from {pluginSource}") @@ -1550,11 +1815,15 @@ def install_from_store(request, plugin_name): raise Exception(f'Plugin installation failed: Files extracted to wrong location. Found {found_root_files} in /usr/local/CyberCP/ root instead of {pluginInstalled}/') raise Exception(f'Plugin installation failed: {pluginInstalled} does not exist after installation') + # Sync meta.xml from GitHub raw so version matches store + _sync_meta_xml_from_github(plugin_name, '/usr/local/CyberCP') + logging.writeToFile(f"Plugin {plugin_name} installed successfully") # Set plugin to enabled by default after installation _set_plugin_state(plugin_name, True) + _ensure_plugin_meta_xml(plugin_name) return JsonResponse({ 'success': True, 'message': f'Plugin {plugin_name} installed successfully from store' @@ -1585,6 +1854,24 @@ def install_from_store(request, plugin_name): 'error': str(e) }, status=500) +@csrf_exempt +@require_http_methods(["GET"]) +def debug_loaded_plugins(request): + """Return which plugins have URL routes loaded and which failed (for diagnosing 404s).""" + try: + import pluginHolder.urls as urls_mod + loaded = list(getattr(urls_mod, '_loaded_plugins', [])) + failed = dict(getattr(urls_mod, '_failed_plugins', {})) + return JsonResponse({ + 'success': True, + 'loaded': loaded, + 'failed': failed, + 'loaded_count': len(loaded), + 'failed_count': len(failed), + }, json_dumps_params={'indent': 2}) + except Exception as e: + return JsonResponse({'success': False, 'error': str(e)}, status=500) + def plugin_help(request, plugin_name): """Plugin-specific help page - shows plugin information, version history, and help content""" mailUtilities.checkHome() diff --git a/preUpgrade.sh b/preUpgrade.sh index 043e45cba..859ef1ed4 100644 --- a/preUpgrade.sh +++ b/preUpgrade.sh @@ -2,10 +2,19 @@ # Check for branch parameter BRANCH_NAME="" -if [ "$1" = "-b" ] || [ "$1" = "--branch" ]; then - BRANCH_NAME="$2" - shift 2 -fi +EXTRA_ARGS="" +while [ $# -gt 0 ]; do + case "$1" in + -b|--branch) + BRANCH_NAME="$2" + shift 2 + ;; + *) + EXTRA_ARGS="$EXTRA_ARGS $1" + shift + ;; + esac +done # If no branch specified, get stable version if [ -z "$BRANCH_NAME" ]; then @@ -15,6 +24,10 @@ fi echo "Upgrading CyberPanel from branch: $BRANCH_NAME" rm -f /usr/local/cyberpanel_upgrade.sh -wget -O /usr/local/cyberpanel_upgrade.sh https://raw.githubusercontent.com/master3395/cyberpanel/$BRANCH_NAME/cyberpanel_upgrade.sh 2>/dev/null +# Use same repo as this script (master3395); fallback to usmannasir for compatibility. Prefer curl with no-cache so --mariadb-version is respected. +curl -sL -H 'Cache-Control: no-cache' -H 'Pragma: no-cache' -o /usr/local/cyberpanel_upgrade.sh "https://raw.githubusercontent.com/master3395/cyberpanel/$BRANCH_NAME/cyberpanel_upgrade.sh" 2>/dev/null || \ +wget -q -O /usr/local/cyberpanel_upgrade.sh "https://raw.githubusercontent.com/master3395/cyberpanel/$BRANCH_NAME/cyberpanel_upgrade.sh" 2>/dev/null || \ +wget -q -O /usr/local/cyberpanel_upgrade.sh "https://raw.githubusercontent.com/usmannasir/cyberpanel/$BRANCH_NAME/cyberpanel_upgrade.sh" 2>/dev/null chmod 700 /usr/local/cyberpanel_upgrade.sh -/usr/local/cyberpanel_upgrade.sh $@ +# Pass -b and all extra args (e.g. --mariadb-version 12.3, --backup-db, --no-backup-db) to upgrade script +/usr/local/cyberpanel_upgrade.sh -b "$BRANCH_NAME" $EXTRA_ARGS diff --git a/premiumPlugin/views.py b/premiumPlugin/views.py deleted file mode 100644 index e620f97c5..000000000 --- a/premiumPlugin/views.py +++ /dev/null @@ -1,269 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Premium Plugin Views - Remote Verification Version -This version uses remote server verification (no secrets in plugin) -SECURITY: All Patreon API calls happen on YOUR server, not user's server -""" - -from django.shortcuts import render, redirect -from django.http import JsonResponse -from plogical.mailUtilities import mailUtilities -from plogical.httpProc import httpProc -from functools import wraps -import sys -import os -import urllib.request -import urllib.error -import json - -# Remote verification server (YOUR server, not user's server) -REMOTE_VERIFICATION_URL = 'https://api.newstargeted.com/api/verify-patreon-membership' -PLUGIN_NAME = 'premiumPlugin' # Patreon Premium Plugin Example -PLUGIN_VERSION = '1.0.0' - -def cyberpanel_login_required(view_func): - """ - Custom decorator that checks for CyberPanel session userID - """ - @wraps(view_func) - def _wrapped_view(request, *args, **kwargs): - try: - userID = request.session['userID'] - # User is authenticated via CyberPanel session - return view_func(request, *args, **kwargs) - except KeyError: - # Not logged in, redirect to login - from loginSystem.views import loadLoginPage - return redirect(loadLoginPage) - return _wrapped_view - -def remote_verification_required(view_func): - """ - Decorator that checks Patreon membership via remote server - No secrets stored in plugin - all verification happens on your server - """ - @wraps(view_func) - def _wrapped_view(request, *args, **kwargs): - # First check login - try: - userID = request.session['userID'] - except KeyError: - from loginSystem.views import loadLoginPage - return redirect(loadLoginPage) - - # Get user email - user_email = getattr(request.user, 'email', None) if hasattr(request, 'user') and request.user else None - if not user_email: - # Try to get from session or username - user_email = request.session.get('email', '') or getattr(request.user, 'username', '') - - # Check membership via remote server - verification_result = check_remote_membership(user_email, request.META.get('REMOTE_ADDR', '')) - - if not verification_result.get('has_access', False): - # User doesn't have subscription - show subscription required page - context = { - 'plugin_name': 'Patreon Premium Plugin Example', - 'is_paid': True, - 'patreon_tier': verification_result.get('patreon_tier', 'CyberPanel Paid Plugin'), - 'patreon_url': verification_result.get('patreon_url', 'https://www.patreon.com/c/newstargeted/membership'), - 'message': verification_result.get('message', 'Patreon subscription required'), - 'error': verification_result.get('error') - } - proc = httpProc(request, 'premiumPlugin/subscription_required.html', context, 'admin') - return proc.render() - - # User has access - proceed with view - return view_func(request, *args, **kwargs) - - return _wrapped_view - -def check_remote_membership(user_email, user_ip=''): - """ - Check Patreon membership via remote verification server - - Args: - user_email: User's email address - user_ip: User's IP address (for logging/security) - - Returns: - dict: { - 'has_access': bool, - 'patreon_tier': str, - 'patreon_url': str, - 'message': str, - 'error': str or None - } - """ - try: - # Prepare request data - request_data = { - 'user_email': user_email, - 'plugin_name': PLUGIN_NAME, - 'plugin_version': PLUGIN_VERSION, - 'user_ip': user_ip, - 'tier_id': '27789984' # CyberPanel Paid Plugin tier ID - } - - # Make request to remote verification server - req = urllib.request.Request( - REMOTE_VERIFICATION_URL, - data=json.dumps(request_data).encode('utf-8'), - headers={ - 'Content-Type': 'application/json', - 'User-Agent': f'CyberPanel-Plugin/{PLUGIN_VERSION}', - 'X-Plugin-Name': PLUGIN_NAME - } - ) - - # Send request with timeout - try: - with urllib.request.urlopen(req, timeout=10) as response: - response_data = json.loads(response.read().decode('utf-8')) - - if response_data.get('success', False): - return { - 'has_access': response_data.get('has_access', False), - 'patreon_tier': response_data.get('patreon_tier', 'CyberPanel Paid Plugin'), - 'patreon_url': response_data.get('patreon_url', 'https://www.patreon.com/c/newstargeted/membership'), - 'message': response_data.get('message', 'Access granted'), - 'error': None - } - else: - return { - 'has_access': False, - 'patreon_tier': response_data.get('patreon_tier', 'CyberPanel Paid Plugin'), - 'patreon_url': response_data.get('patreon_url', 'https://www.patreon.com/c/newstargeted/membership'), - 'message': response_data.get('message', 'Patreon subscription required'), - 'error': response_data.get('error') - } - except urllib.error.HTTPError as e: - # Server returned error - error_body = e.read().decode('utf-8') if e.fp else 'Unknown error' - return { - 'has_access': False, - 'patreon_tier': 'CyberPanel Paid Plugin', - 'patreon_url': 'https://www.patreon.com/c/newstargeted/membership', - 'message': 'Unable to verify subscription. Please try again later.', - 'error': f'HTTP {e.code}: {error_body}' - } - except urllib.error.URLError as e: - # Network error - return { - 'has_access': False, - 'patreon_tier': 'CyberPanel Paid Plugin', - 'patreon_url': 'https://www.patreon.com/c/newstargeted/membership', - 'message': 'Unable to connect to verification server. Please check your internet connection.', - 'error': str(e.reason) if hasattr(e, 'reason') else str(e) - } - except Exception as e: - # Other errors - return { - 'has_access': False, - 'patreon_tier': 'CyberPanel Paid Plugin', - 'patreon_url': 'https://www.patreon.com/c/newstargeted/membership', - 'message': 'Verification error occurred. Please try again later.', - 'error': str(e) - } - - except Exception as e: - import logging - logging.writeToFile(f"Error in remote membership check: {str(e)}") - return { - 'has_access': False, - 'patreon_tier': 'CyberPanel Paid Plugin', - 'patreon_url': 'https://www.patreon.com/c/newstargeted/membership', - 'message': 'Verification error occurred. Please try again later.', - 'error': str(e) - } - -@cyberpanel_login_required -def main_view(request): - """ - Main view for premium plugin - Shows plugin information and features if subscribed, or subscription required message if not - """ - mailUtilities.checkHome() - - # Get user email for verification - user_email = getattr(request.user, 'email', None) if hasattr(request, 'user') and request.user else None - if not user_email: - user_email = request.session.get('email', '') or getattr(request.user, 'username', '') - - # Check membership status (but don't block access) - verification_result = check_remote_membership(user_email, request.META.get('REMOTE_ADDR', '')) - has_access = verification_result.get('has_access', False) - - # Determine plugin status - plugin_status = 'Active' if has_access else 'Subscription Required' - - context = { - 'plugin_name': 'Patreon Premium Plugin Example', - 'version': PLUGIN_VERSION, - 'status': plugin_status, - 'has_access': has_access, - 'description': 'This is an example paid plugin that requires Patreon subscription.' if not has_access else 'This is an example paid plugin. You have access because you are subscribed to Patreon!', - 'patreon_tier': verification_result.get('patreon_tier', 'CyberPanel Paid Plugin'), - 'patreon_url': verification_result.get('patreon_url', 'https://www.patreon.com/membership/27789984'), - 'features': [ - 'Premium Feature 1', - 'Premium Feature 2', - 'Premium Feature 3', - 'Advanced Configuration', - 'Priority Support' - ] if has_access else [] - } - - proc = httpProc(request, 'premiumPlugin/index.html', context, 'admin') - return proc.render() - -@cyberpanel_login_required -def settings_view(request): - """ - Settings page for premium plugin - Shows settings but disables them if user doesn't have Patreon subscription - """ - mailUtilities.checkHome() - - # Get user email for verification - user_email = getattr(request.user, 'email', None) if hasattr(request, 'user') and request.user else None - if not user_email: - user_email = request.session.get('email', '') or getattr(request.user, 'username', '') - - # Check membership status (but don't block access) - verification_result = check_remote_membership(user_email, request.META.get('REMOTE_ADDR', '')) - has_access = verification_result.get('has_access', False) - - # Determine plugin status - plugin_status = 'Active' if has_access else 'Subscription Required' - - context = { - 'plugin_name': 'Patreon Premium Plugin Example', - 'version': PLUGIN_VERSION, - 'plugin_status': plugin_status, - 'status': plugin_status, # Keep both for compatibility - 'description': 'Configure your premium plugin settings', - 'has_access': has_access, - 'patreon_tier': verification_result.get('patreon_tier', 'CyberPanel Paid Plugin'), - 'patreon_url': verification_result.get('patreon_url', 'https://www.patreon.com/membership/27789984'), - 'verification_message': verification_result.get('message', '') - } - - proc = httpProc(request, 'premiumPlugin/settings.html', context, 'admin') - return proc.render() - -@cyberpanel_login_required -@remote_verification_required -def api_status_view(request): - """ - API endpoint for plugin status - Only accessible with Patreon subscription (verified remotely) - """ - return JsonResponse({ - 'plugin_name': 'Patreon Premium Plugin Example', - 'version': PLUGIN_VERSION, - 'status': 'active', - 'subscription': 'active', - 'description': 'Premium plugin is active and accessible', - 'verification_method': 'remote' - }) diff --git a/public/static/baseTemplate/custom-js/system-status.js b/public/static/baseTemplate/custom-js/system-status.js index e017d31e1..0cccec058 100644 --- a/public/static/baseTemplate/custom-js/system-status.js +++ b/public/static/baseTemplate/custom-js/system-status.js @@ -247,11 +247,11 @@ app.controller('adminController', function ($scope, $http, $timeout) { } if (!Boolean(response.data.deleteZone)) { - $('.addDeleteRecords').hide(); + $('.deleteZone').hide(); } if (!Boolean(response.data.addDeleteRecords)) { - $('.deleteDatabase').hide(); + $('.addDeleteRecords').hide(); } // Email Management @@ -531,15 +531,18 @@ app.controller('homePageStatus', function ($scope, $http, $timeout) { //////////// function increment() { - $('.box').hide(); + var boxes = document.querySelectorAll ? document.querySelectorAll('.box') : []; + for (var i = 0; i < boxes.length; i++) boxes[i].style.display = 'none'; setTimeout(function () { - $('.box').show(); + for (var j = 0; j < boxes.length; j++) boxes[j].style.display = ''; }, 100); - - } -increment(); +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', increment); +} else { + increment(); +} //////////// @@ -553,6 +556,7 @@ app.controller('versionManagment', function ($scope, $http, $timeout) { $scope.updateFinish = true; $scope.couldNotConnect = true; + var upgradeStatusTimer = null; $scope.upgrade = function () { @@ -623,7 +627,8 @@ app.controller('versionManagment', function ($scope, $http, $timeout) { if (response.data.upgradeStatus === 1) { if (response.data.finished === 1) { - $timeout.cancel(); + if (upgradeStatusTimer) $timeout.cancel(upgradeStatusTimer); + upgradeStatusTimer = null; $scope.upgradelogBox = false; $scope.upgradeLog = response.data.upgradeLog; $scope.upgradeLoading = true; @@ -635,7 +640,7 @@ app.controller('versionManagment', function ($scope, $http, $timeout) { } else { $scope.upgradelogBox = false; $scope.upgradeLog = response.data.upgradeLog; - $timeout(getUpgradeStatus, 2000); + upgradeStatusTimer = $timeout(getUpgradeStatus, 2000); } } diff --git a/public/static/databases/databases.js b/public/static/databases/databases.js index 7b10cf48a..8dd93d53c 100644 --- a/public/static/databases/databases.js +++ b/public/static/databases/databases.js @@ -2,6 +2,8 @@ * Created by usman on 8/6/17. */ +/* Ensure we register controllers on the CyberCP app (avoids ctrlreg if load order differs) */ +var app = (typeof window.app !== 'undefined' && window.app) ? window.app : angular.module('CyberCP'); /* Java script code to create database */ app.controller('createDatabase', function ($scope, $http) { @@ -10,22 +12,72 @@ app.controller('createDatabase', function ($scope, $http) { $(".dbDetails").hide(); $(".generatedPasswordDetails").hide(); $('#create-database-select').select2(); + + // Initialize preview if website is already selected + setTimeout(function() { + if ($scope.databaseWebsite) { + var truncatedName = $scope.getTruncatedWebName($scope.databaseWebsite); + $("#domainDatabase").text(truncatedName); + $("#domainUsername").text(truncatedName); + $(".dbDetails").show(); + } + }, 100); }); + // Helper function to get truncated website name + $scope.getTruncatedWebName = function(domain) { + if (!domain) return ''; + + // Remove hyphens and get first part before dot + var webName = domain.replace(/-/g, '').split('.')[0]; + + // Truncate to 4 characters if longer than 5 + if (webName.length > 5) { + webName = webName.substring(0, 4); + } + + return webName; + }; + $('#create-database-select').on('select2:select', function (e) { var data = e.params.data; $scope.databaseWebsite = data.text; $(".dbDetails").show(); - $("#domainDatabase").text(getWebsiteName(data.text)); - $("#domainUsername").text(getWebsiteName(data.text)); + + // Use local truncation function to ensure consistency + var truncatedName = $scope.getTruncatedWebName(data.text); + $("#domainDatabase").text(truncatedName); + $("#domainUsername").text(truncatedName); + + // Apply scope to update Angular bindings + $scope.$apply(); }); $scope.showDetailsBoxes = function () { $scope.dbDetails = false; } + + // Function called when website selection changes + $scope.websiteChanged = function() { + if ($scope.databaseWebsite) { + $(".dbDetails").show(); + var truncatedName = $scope.getTruncatedWebName($scope.databaseWebsite); + $("#domainDatabase").text(truncatedName); + $("#domainUsername").text(truncatedName); + } + } $scope.createDatabaseLoading = true; + + // Watch for changes to databaseWebsite to update preview + $scope.$watch('databaseWebsite', function(newValue, oldValue) { + if (newValue && newValue !== oldValue) { + var truncatedName = $scope.getTruncatedWebName(newValue); + $("#domainDatabase").text(truncatedName); + $("#domainUsername").text(truncatedName); + } + }); $scope.createDatabase = function () { @@ -39,14 +91,8 @@ app.controller('createDatabase', function ($scope, $http) { var dbPassword = $scope.dbPassword; var webUserName = ""; - // getting website username - - webUserName = databaseWebsite.replace(/-/g, ''); - webUserName = webUserName.split(".")[0]; - - if (webUserName.length > 5) { - webUserName = webUserName.substring(0, 4); - } + // getting website username - use the same truncation function for consistency + webUserName = $scope.getTruncatedWebName(databaseWebsite); var url = "/dataBases/submitDBCreation"; @@ -75,9 +121,15 @@ app.controller('createDatabase', function ($scope, $http) { $scope.createDatabaseLoading = true; $scope.dbDetails = false; + var successMessage = 'Database successfully created.'; + if (response.data.dbName && response.data.dbUsername) { + successMessage = 'Database successfully created.\n' + + 'Database Name: ' + response.data.dbName + '\n' + + 'Database User: ' + response.data.dbUsername; + } new PNotify({ title: 'Success!', - text: 'Database successfully created.', + text: successMessage, type: 'success' }); } else { @@ -589,8 +641,34 @@ app.controller('phpMyAdmin', function ($scope, $http, $window) { function ListInitialDatas(response) { $scope.cyberPanelLoading = true; if (response.data.status === 1) { - var rUrl = '/phpmyadmin/phpmyadminsignin.php?username=' + response.data.username + '&token=' + response.data.token; - $window.location.href = rUrl; + //var rUrl = '/phpmyadmin/phpmyadminsignin.php?username=' + response.data.username + '&token=' + response.data.token; + //$window.location.href = rUrl; + + var form = document.createElement('form'); + form.method = 'post'; + form.action = '/phpmyadmin/phpmyadminsignin.php'; + +// Create input elements for username and token + var usernameInput = document.createElement('input'); + usernameInput.type = 'hidden'; + usernameInput.name = 'username'; + usernameInput.value = response.data.username; + + var tokenInput = document.createElement('input'); + tokenInput.type = 'hidden'; + tokenInput.name = 'token'; + tokenInput.value = response.data.token; + +// Append input elements to the form + form.appendChild(usernameInput); + form.appendChild(tokenInput); + +// Append the form to the body + document.body.appendChild(form); + +// Submit the form + form.submit(); + } else { } @@ -603,3 +681,415 @@ app.controller('phpMyAdmin', function ($scope, $http, $window) { } }); + + +app.controller('Mysqlmanager', function ($scope, $http, $compile, $window, $timeout) { + $scope.cyberPanelLoading = false; + $scope.mysql_status = 'test'; + $scope.uptime = '—'; + $scope.connections = '—'; + $scope.Slow_queries = '—'; + $scope.processes = []; + + + $scope.getstatus = function () { + + $scope.cyberPanelLoading = true; + + url = "/dataBases/getMysqlstatus?t=" + (Date.now ? Date.now() : new Date().getTime()); + + var data = {}; + + var config = { + headers: { + 'X-CSRFToken': getCookie('csrftoken') + } + }; + + + $http.post(url, data, config).then(ListInitialDatas, cantLoadInitialDatas); + + + function ListInitialDatas(response) { + $scope.cyberPanelLoading = false; + var data = response.data; + if (typeof data === 'string') { + try { + data = JSON.parse(data); + } catch (e) { + $scope.uptime = $scope.connections = $scope.Slow_queries = '—'; + $scope.processes = []; + new PNotify({ title: 'Error!', text: 'Invalid response from server.', type: 'error' }); + return; + } + } + if (data && data.status === 1) { + $scope.uptime = data.uptime || '—'; + $scope.connections = data.connections != null ? data.connections : '—'; + $scope.Slow_queries = data.Slow_queries != null ? data.Slow_queries : '—'; + try { + $scope.processes = typeof data.processes === 'string' ? JSON.parse(data.processes || '[]') : (data.processes || []); + } catch (e) { + $scope.processes = []; + } + if (typeof $scope.showStatus === 'function') { + $timeout($scope.showStatus, 3000); + } + + new PNotify({ + title: 'Success', + text: 'Successfully Fetched', + type: 'success' + }); + } else { + new PNotify({ + title: 'Error!', + text: (data && data.error_message) || 'Could not load MySQL status.', + type: 'error' + }); + } + + } + + function cantLoadInitialDatas(response) { + $scope.cyberPanelLoading = false; + $scope.uptime = '—'; + $scope.connections = '—'; + $scope.Slow_queries = '—'; + $scope.processes = []; + var msg = 'Cannot load MySQL status.'; + if (response && response.status) { + msg = 'Request failed: ' + response.status + (response.statusText ? ' ' + response.statusText : ''); + } + if (response && response.data) { + if (typeof response.data === 'string' && response.data.length < 200) { + msg = response.data; + } else if (response.data.error_message) { + msg = response.data.error_message; + } + } + new PNotify({ + title: 'Error!', + text: msg, + type: 'error' + }); + } + + } + + $scope.refreshProcesses = function () { + var icon = document.querySelector('.refresh-btn i'); + if (icon) { + icon.style.animation = 'spin 1s linear'; + setTimeout(function () { icon.style.animation = ''; }, 1000); + } + $scope.getstatus(); + }; + + $scope.getstatus(); +}); + + +app.controller('OptimizeMysql', function ($scope, $http) { + $scope.cyberPanelLoading = true; + + $scope.generateRecommendations = function () { + $scope.cyberhosting = false; + url = "/dataBases/generateRecommendations"; + + var data = { + detectedRam: $("#detectedRam").text() + }; + + var config = { + headers: { + 'X-CSRFToken': getCookie('csrftoken') + } + }; + + $http.post(url, data, config).then(ListInitialData, cantLoadInitialData); + + + function ListInitialData(response) { + $scope.cyberhosting = true; + if (response.data.status === 1) { + $scope.suggestedContent = response.data.generatedConf; + + } else { + new PNotify({ + title: 'Error!', + text: response.data.error_message, + type: 'error' + }); + } + + } + + function cantLoadInitialData(response) { + $scope.cyberhosting = true; + new PNotify({ + title: 'Error!', + text: 'Could not connect to server, please refresh this page.', + type: 'error' + }); + } + + }; + + + $scope.applyMySQLChanges = function () { + $scope.cyberhosting = false; + url = "/dataBases/applyMySQLChanges"; + + var encodedContent = encodeURIComponent($scope.suggestedContent); + + var data = { + suggestedContent: encodedContent + }; + + var config = { + headers: { + 'X-CSRFToken': getCookie('csrftoken') + } + }; + + $http.post(url, data, config).then(ListInitialData, cantLoadInitialData); + + + function ListInitialData(response) { + $scope.cyberhosting = true; + if (response.data.status === 1) { + + new PNotify({ + title: 'Success', + text: 'Changes successfully applied.', + type: 'success' + }); + + + } else { + new PNotify({ + title: 'Error!', + text: response.data.error_message, + type: 'error' + }); + } + + } + + function cantLoadInitialData(response) { + $scope.cyberhosting = true; + new PNotify({ + title: 'Error!', + text: 'Could not connect to server, please refresh this page.', + type: 'error' + }); + } + + }; + + + $scope.restartMySQL = function () { + $scope.cyberPanelLoading = false; + + url = "/dataBases/restartMySQL"; + + var data = {}; + + var config = { + headers: { + 'X-CSRFToken': getCookie('csrftoken') + } + }; + + + $http.post(url, data, config).then(ListInitialDatas, cantLoadInitialDatas); + + + function ListInitialDatas(response) { + $scope.cyberPanelLoading = true; + if (response.data.status === 1) { + new PNotify({ + title: 'Success', + text: 'Successfully Done', + type: 'success' + }); + } else { + new PNotify({ + title: 'Error!', + text: response.data.error_message, + type: 'error' + }); + } + + } + + function cantLoadInitialData(response) { + $scope.cyberhosting = true; + new PNotify({ + title: 'Error!', + text: 'Could not connect to server, please refresh this page.', + type: 'error' + }); + } + } +}) + + +app.controller('mysqlupdate', function ($scope, $http, $timeout) { + $scope.cyberPanelLoading = true; + $scope.dbLoading = true; + $scope.modeSecInstallBox = true; + $scope.modsecLoading = true; + $scope.failedToStartInallation = true; + $scope.couldNotConnect = true; + $scope.modSecSuccessfullyInstalled = true; + $scope.installationFailed = true; + + $scope.Upgardemysql = function () { + $scope.dbLoading = false; + $scope.installform = true; + $scope.modSecNotifyBox = true; + $scope.modeSecInstallBox = false; + $scope.modsecLoading = false; + $scope.failedToStartInallation = true; + $scope.couldNotConnect = true; + $scope.modSecSuccessfullyInstalled = true; + $scope.installationFailed = true; + + + url = "/dataBases/upgrademysqlnow"; + + var data = { + mysqlversion: $scope.version + }; + + var config = { + headers: { + 'X-CSRFToken': getCookie('csrftoken') + } + }; + + $http.post(url, data, config).then(ListInitialData, cantLoadInitialData); + + + function ListInitialData(response) { + $scope.cyberhosting = true; + if (response.data.status === 1) { + $scope.modSecNotifyBox = true; + $scope.modeSecInstallBox = false; + $scope.modsecLoading = false; + $scope.failedToStartInallation = true; + $scope.couldNotConnect = true; + $scope.modSecSuccessfullyInstalled = true; + $scope.installationFailed = true; + + $scope.statusfile = response.data.tempStatusPath + + $timeout(getRequestStatus, 1000); + + } else { + $scope.errorMessage = response.data.error_message; + + $scope.modSecNotifyBox = false; + $scope.modeSecInstallBox = true; + $scope.modsecLoading = true; + $scope.failedToStartInallation = false; + $scope.couldNotConnect = true; + $scope.modSecSuccessfullyInstalled = true; + } + + } + + function cantLoadInitialData(response) { + $scope.cyberhosting = true; + new PNotify({ + title: 'Error!', + text: 'Could not connect to server, please refresh this page.', + type: 'error' + }); + } + } + + + function getRequestStatus() { + + $scope.modSecNotifyBox = true; + $scope.modeSecInstallBox = false; + $scope.modsecLoading = false; + $scope.failedToStartInallation = true; + $scope.couldNotConnect = true; + $scope.modSecSuccessfullyInstalled = true; + $scope.installationFailed = true; + + url = "/dataBases/upgrademysqlstatus"; + + var data = { + statusfile: $scope.statusfile + }; + + var config = { + headers: { + 'X-CSRFToken': getCookie('csrftoken') + } + }; + + + $http.post(url, data, config).then(ListInitialDatas, cantLoadInitialDatas); + + + function ListInitialDatas(response) { + + + if (response.data.abort === 0) { + + $scope.modSecNotifyBox = true; + $scope.modeSecInstallBox = false; + $scope.modsecLoading = false; + $scope.failedToStartInallation = true; + $scope.couldNotConnect = true; + $scope.modSecSuccessfullyInstalled = true; + $scope.installationFailed = true; + + $scope.requestData = response.data.requestStatus; + $timeout(getRequestStatus, 1000); + } else { + // Notifications + $timeout.cancel(); + $scope.modSecNotifyBox = false; + $scope.modeSecInstallBox = false; + $scope.modsecLoading = true; + $scope.failedToStartInallation = true; + $scope.couldNotConnect = true; + + $scope.requestData = response.data.requestStatus; + + if (response.data.installed === 0) { + $scope.installationFailed = false; + $scope.errorMessage = response.data.error_message; + } else { + $scope.modSecSuccessfullyInstalled = false; + $timeout(function () { + $window.location.reload(); + }, 3000); + } + + } + + } + + function cantLoadInitialDatas(response) { + + $scope.modSecNotifyBox = false; + $scope.modeSecInstallBox = false; + $scope.modsecLoading = true; + $scope.failedToStartInallation = true; + $scope.couldNotConnect = false; + $scope.modSecSuccessfullyInstalled = true; + $scope.installationFailed = true; + + + } + + } +}); \ No newline at end of file diff --git a/public/static/dns/dns.js b/public/static/dns/dns.js index 18cc88645..dba1d46ef 100644 --- a/public/static/dns/dns.js +++ b/public/static/dns/dns.js @@ -732,6 +732,21 @@ app.controller('configureDefaultNameservers', function ($scope, $http) { /* Java script code for CloudFlare */ +app.filter('dnsRecordSearch', function () { + return function (records, searchText) { + if (!records || !Array.isArray(records)) return records; + var q = (searchText != null ? String(searchText) : '').toLowerCase().trim(); + if (q === '') return records; + return records.filter(function (r) { + var name = (r.name != null ? String(r.name) : '').toLowerCase(); + var type = (r.type != null ? String(r.type) : '').toLowerCase(); + var content = (r.content != null ? String(r.content) : '').toLowerCase(); + var priority = (r.priority != null ? String(r.priority) : ''); + return name.indexOf(q) !== -1 || type.indexOf(q) !== -1 || content.indexOf(q) !== -1 || priority.indexOf(q) !== -1; + }); + }; +}); + app.controller('addModifyDNSRecordsCloudFlare', function ($scope, $http, $window) { $scope.saveCFConfigs = function () { @@ -811,6 +826,9 @@ app.controller('addModifyDNSRecordsCloudFlare', function ($scope, $http, $window $scope.couldNotDeleteRecords = true; $scope.couldNotAddRecord = true; $scope.recordValueDefault = false; + $scope.records = []; + $scope.showEditModal = false; + $scope.editRecord = {}; // Hide records boxes $(".aaaaRecord").hide(); @@ -1001,10 +1019,21 @@ app.controller('addModifyDNSRecordsCloudFlare', function ($scope, $http, $window $http.post(url, data, config).then(ListInitialDatas, cantLoadInitialDatas); + function normalizeRecordForInline(record) { + record.ttlNum = record.ttl === 'AUTO' || record.ttl === 1 ? 1 : (parseInt(record.ttl, 10) || 3600); + record.priority = parseInt(record.priority, 10) || 0; + if (record.type != null && typeof record.type === 'string') { + record.type = record.type.toUpperCase().trim(); + } else if (record.type == null || record.type === '') { + record.type = 'A'; + } + } + function ListInitialDatas(response) { if (response.data.fetchStatus === 1) { $scope.records = JSON.parse(response.data.data); + $scope.records.forEach(normalizeRecordForInline); $scope.currentRecords = false; $scope.canNotFetchRecords = true; @@ -1135,6 +1164,150 @@ app.controller('addModifyDNSRecordsCloudFlare', function ($scope, $http, $window }; + $scope.dnsTypeList = ['A', 'AAAA', 'CNAME', 'MX', 'TXT', 'NS', 'SOA', 'SRV', 'CAA', 'SPF', 'DNSKEY', 'CDNSKEY', 'HTTPS', 'SVCB', 'URI', 'LOC', 'NAPTR', 'SMIMEA', 'SSHFP', 'TLSA', 'PTR']; + $scope.getTypeOptions = function (record) { + var list = angular.copy($scope.dnsTypeList); + var t = record && record.type ? String(record.type).toUpperCase().trim() : ''; + if (t && list.indexOf(t) === -1) { + list.unshift(t); + } + return list; + }; + $scope.dnsSearch = { filter: '' }; + $scope.filteredRecords = []; + function applySearchAndSort() { + if (!$scope.records || !Array.isArray($scope.records)) { + $scope.filteredRecords = []; + return; + } + var q = ($scope.dnsSearch && $scope.dnsSearch.filter != null ? String($scope.dnsSearch.filter) : '').toLowerCase().trim(); + var list = q === '' ? $scope.records : $scope.records.filter(function (r) { + var name = (r.name != null ? String(r.name) : '').toLowerCase(); + var type = (r.type != null ? String(r.type) : '').toLowerCase(); + var content = (r.content != null ? String(r.content) : '').toLowerCase(); + var priority = (r.priority != null ? String(r.priority) : ''); + return name.indexOf(q) !== -1 || type.indexOf(q) !== -1 || content.indexOf(q) !== -1 || priority.indexOf(q) !== -1; + }); + var col = $scope.sortColumn || 'name'; + var rev = $scope.sortReverse; + list = list.slice().sort(function (a, b) { + var va = a[col]; + var vb = b[col]; + if (va === vb) return 0; + if (va == null) return rev ? -1 : 1; + if (vb == null) return rev ? 1 : -1; + if (typeof va === 'number' && typeof vb === 'number') return rev ? vb - va : va - vb; + va = String(va).toLowerCase(); + vb = String(vb).toLowerCase(); + return rev ? (vb < va ? 1 : -1) : (va < vb ? -1 : 1); + }); + $scope.filteredRecords = list; + } + $scope.$watchCollection('records', function () { applySearchAndSort(); }); + $scope.$watch('dnsSearch.filter', function () { applySearchAndSort(); }, true); + $scope.$watch('sortColumn', function () { applySearchAndSort(); }); + $scope.$watch('sortReverse', function () { applySearchAndSort(); }); + $scope.matchDnsSearch = function (record) { + var q = (($scope.dnsSearch && $scope.dnsSearch.filter) != null ? String($scope.dnsSearch.filter) : '').toLowerCase().trim(); + if (!q) return true; + var name = (record.name || '').toLowerCase(); + var type = (record.type || '').toLowerCase(); + var content = (record.content || '').toLowerCase(); + var priority = String(record.priority != null ? record.priority : ''); + return name.indexOf(q) !== -1 || type.indexOf(q) !== -1 || content.indexOf(q) !== -1 || priority.indexOf(q) !== -1; + }; + $scope.sortColumn = 'name'; + $scope.sortReverse = false; + $scope.setSort = function (col) { + if ($scope.sortColumn === col) { + $scope.sortReverse = !$scope.sortReverse; + } else { + $scope.sortColumn = col; + $scope.sortReverse = false; + } + }; + $scope.editingRecordId = null; + $scope.editingField = null; + $scope.isEditing = function (record, field) { + return $scope.editingRecordId === record.id && $scope.editingField === field; + }; + $scope.startEdit = function (record, field) { + $scope.editingRecordId = record.id; + $scope.editingField = field; + }; + $scope.saveInlineField = function (record, field) { + $scope.editingRecordId = null; + $scope.editingField = null; + var ttl = record.ttlNum !== undefined ? record.ttlNum : (record.ttl === 'AUTO' || record.ttl === 1 ? 1 : parseInt(record.ttl, 10) || 3600); + var url = "/dns/updateDNSRecordCloudFlare"; + var data = { + selectedZone: $scope.selectedZone, + id: record.id, + name: record.name, + recordType: record.type, + content: record.content, + ttl: ttl, + priority: parseInt(record.priority, 10) || 0, + proxied: record.proxy + }; + var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') } }; + $http.post(url, data, config).then(function (response) { + if (response.data.update_status === 1) { + new PNotify({ title: 'Success', text: 'Record updated.', type: 'success' }); + populateCurrentRecords(); + } else { + new PNotify({ title: 'Error', text: response.data.error_message || 'Update failed', type: 'error' }); + } + }, function () { + new PNotify({ title: 'Error', text: 'Could not connect to server.', type: 'error' }); + }); + }; + + $scope.openEditModal = function (record) { + $scope.editRecord = { + id: record.id, + name: record.name, + type: record.type, + ttl: record.ttl, + content: record.content, + priority: record.priority || 0, + proxy: record.proxy, + proxiable: record.proxiable + }; + $scope.showEditModal = true; + }; + + $scope.closeEditModal = function () { + $scope.showEditModal = false; + }; + + $scope.saveEditRecord = function () { + var url = "/dns/updateDNSRecordCloudFlare"; + var data = { + selectedZone: $scope.selectedZone, + id: $scope.editRecord.id, + name: $scope.editRecord.name, + recordType: $scope.editRecord.type, + content: $scope.editRecord.content, + ttl: $scope.editRecord.ttl === 'AUTO' || $scope.editRecord.ttl === 1 ? 1 : parseInt($scope.editRecord.ttl, 10) || 3600, + priority: parseInt($scope.editRecord.priority, 10) || 0, + proxied: $scope.editRecord.proxy + }; + var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') } }; + $http.post(url, data, config).then(function (response) { + if (response.data.update_status === 1) { + $scope.closeEditModal(); + populateCurrentRecords(); + new PNotify({ title: 'Success', text: 'Record updated.', type: 'success' }); + } else { + $scope.errorMessage = response.data.error_message || 'Update failed'; + new PNotify({ title: 'Error', text: $scope.errorMessage, type: 'error' }); + } + }, function () { + new PNotify({ title: 'Error', text: 'Could not connect to server.', type: 'error' }); + }); + }; + $scope.syncCF = function () { $scope.recordsLoading = false; diff --git a/public/static/firewall/firewall.js b/public/static/firewall/firewall.js index cdb37a6c1..9a489d2da 100644 --- a/public/static/firewall/firewall.js +++ b/public/static/firewall/firewall.js @@ -2,24 +2,325 @@ * Created by usman on 9/5/17. */ +// Helper function to get CSRF token cookie +function getCookie(name) { + var cookieValue = null; + if (document.cookie && document.cookie !== '') { + var cookies = document.cookie.split(';'); + for (var i = 0; i < cookies.length; i++) { + var cookie = jQuery.trim(cookies[i]); + if (cookie.substring(0, name.length + 1) === (name + '=')) { + cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); + break; + } + } + } + return cookieValue; +} /* Java script code to ADD Firewall Rules */ -app.controller('firewallController', function ($scope, $http) { +app.controller('firewallController', function ($scope, $http, $timeout) { $scope.rulesLoading = true; $scope.actionFailed = true; $scope.actionSuccess = true; + $scope.showExportFormatModal = false; + $scope.showImportFormatModal = false; + $scope.exportRulesFormat = 'json'; + $scope.importRulesFormat = 'json'; + $scope.showModifyRuleModal = false; + $scope.modifyRuleData = { id: null, name: '', proto: 'tcp', port: '', ruleIP: '0.0.0.0/0' }; $scope.canNotAddRule = true; $scope.ruleAdded = true; $scope.couldNotConnect = true; $scope.rulesDetails = false; + // Initialize rules array - prevents "Cannot read 'length' of undefined" when template evaluates rules.length before API loads + $scope.rules = []; + // Banned IPs variables – tab from hash so we stay on /firewall/ (avoids 404 on servers without /firewall/firewall-rules/) + function tabFromHash() { + var h = (window.location.hash || '').replace(/^#/, ''); + return (h === 'banned-ips') ? 'banned' : 'rules'; + } + $scope.activeTab = tabFromHash(); + $scope.bannedIPs = []; // Initialize as empty array + + // Re-apply tab from hash after load (hash can be set after controller init in some browsers) + function applyTabFromHash() { + var tab = tabFromHash(); + if ($scope.activeTab !== tab) { + $scope.activeTab = tab; + if (tab === 'banned') { populateBannedIPs(); } else { populateCurrentRecords(); } + if (!$scope.$$phase && !$scope.$root.$$phase) { $scope.$apply(); } + } + } + $timeout(applyTabFromHash, 0); + if (document.readyState === 'complete') { + $timeout(applyTabFromHash, 50); + } else { + window.addEventListener('load', function() { $timeout(applyTabFromHash, 0); }); + } + + // Sync tab with hash and load that tab's data on switch + $scope.setFirewallTab = function(tab) { + $timeout(function() { + $scope.activeTab = tab; + window.location.hash = (tab === 'banned') ? '#banned-ips' : '#rules'; + if (tab === 'banned') { populateBannedIPs(); } else { populateCurrentRecords(); } + }, 0); + }; + + // Back/forward or direct hash change: sync tab and load its data + function syncTabFromHash() { + var tab = tabFromHash(); + if ($scope.activeTab !== tab) { + $scope.activeTab = tab; + if (tab === 'banned') { populateBannedIPs(); } else { populateCurrentRecords(); } + if (!$scope.$$phase && !$scope.$root.$$phase) { $scope.$apply(); } + } + } + window.addEventListener('hashchange', syncTabFromHash); + + // Pagination: Firewall Rules (default 10 per page, options 5–100) + $scope.rulesPage = 1; + $scope.rulesPageSize = 10; + $scope.rulesPageSizeOptions = [5, 10, 20, 30, 50, 100]; + $scope.rulesTotalCount = 0; + + // Pagination: Banned IPs + $scope.bannedPage = 1; + $scope.bannedPageSize = 10; + $scope.bannedPageSizeOptions = [5, 10, 20, 30, 50, 100]; + $scope.bannedTotalCount = 0; + + // Modify Banned IP modal state + $scope.showModifyModal = false; + $scope.modifyBannedIPData = { ip: '', id: null, reason: '', duration: '24h' }; + + // Initialize banned IPs array - start as null so template shows empty state + // Will be set to array after API call + $scope.bannedIPsLoading = false; + $scope.bannedIPActionFailed = true; + $scope.bannedIPActionSuccess = true; + $scope.bannedIPCouldNotConnect = true; + $scope.banIP = ''; + $scope.banReason = ''; + $scope.banDuration = '24h'; + $scope.bannedIPSearch = ''; + $scope.searchBannedIPFilter = function(item) { + var q = ($scope.bannedIPSearch || '').toLowerCase().trim(); + if (!q) return true; + var ip = (item.ip || '').toLowerCase(); + var reason = (item.reason || '').toLowerCase(); + var status = item.active ? 'active' : 'expired'; + return ip.indexOf(q) !== -1 || reason.indexOf(q) !== -1 || status.indexOf(q) !== -1; + }; + + $scope.onBannedSearchChange = function() { + if ($scope.bannedSearchTimeout) $timeout.cancel($scope.bannedSearchTimeout); + $scope.bannedSearchTimeout = $timeout(function() { + $scope.bannedPage = 1; + if (typeof populateBannedIPs === 'function') populateBannedIPs(); + }, 350); + }; + + $scope.runBannedSearch = function() { + $scope.bannedPage = 1; + if (typeof populateBannedIPs === 'function') populateBannedIPs(); + }; + firewallStatus(); + // Load both tabs on init; also load on tab change (watch) so content always shows populateCurrentRecords(); + populateBannedIPs(); + $scope.$watch('activeTab', function(newVal, oldVal) { + if (newVal === oldVal || !newVal) return; + $timeout(function() { + try { + if (newVal === 'banned' && typeof populateBannedIPs === 'function') populateBannedIPs(); + else if (newVal === 'rules' && typeof populateCurrentRecords === 'function') populateCurrentRecords(); + } catch (e) {} + }, 0); + }); + + // Log for debugging + console.log('=== FIREWALL CONTROLLER INITIALIZING ==='); + console.log('Initializing firewall controller, loading banned IPs...'); + + // Define populateBannedIPs function first, then call it + // This ensures the function is available when setTimeout executes + function populateBannedIPs() { + console.log('=== populateBannedIPs() START ==='); + console.log('Current scope.bannedIPs:', $scope.bannedIPs); + console.log('Current activeTab:', $scope.activeTab); + + $scope.bannedIPsLoading = true; + var url = "/firewall/getBannedIPs"; + var csrfToken = getCookie('csrftoken'); + var config = { + headers: { + 'X-CSRFToken': csrfToken + } + }; + + var postData = { + page: Math.max(1, parseInt($scope.bannedPage, 10) || 1), + page_size: Math.max(5, Math.min(100, parseInt($scope.bannedPageSize, 10) || 10)), + search: ($scope.bannedIPSearch || '').trim() + }; + console.log('Making request to:', url, 'page:', postData.page, 'page_size:', postData.page_size, 'search:', postData.search); + console.log('CSRF Token:', csrfToken ? 'Found (' + csrfToken.substring(0, 10) + '...)' : 'MISSING!'); + + $http.post(url, postData, config).then( + function(response) { + var res = (typeof response.data === 'string') ? (function() { try { return JSON.parse(response.data); } catch (e) { return {}; } })() : response.data; + console.log('=== API RESPONSE RECEIVED ==='); + console.log('Response status:', response.status); + console.log('Response data (parsed):', res); + + $scope.bannedIPsLoading = false; + // Reset error flags + $scope.bannedIPActionFailed = true; + $scope.bannedIPActionSuccess = true; + $scope.bannedIPCouldNotConnect = true; + + if (res && res.status === 1) { + var bannedIPsArray = res.bannedIPs || []; + console.log('Raw bannedIPs from API:', bannedIPsArray); + console.log('Banned IPs count:', bannedIPsArray.length); + console.log('Is array?', Array.isArray(bannedIPsArray)); + + // Ensure it's an array + if (!Array.isArray(bannedIPsArray)) { + console.error('ERROR: bannedIPs is not an array:', typeof bannedIPsArray); + bannedIPsArray = []; + } + + // Assign to scope - Angular $http callbacks already run within $apply + console.log('Assigning to scope.bannedIPs...'); + $scope.bannedIPs = bannedIPsArray; + $scope.bannedTotalCount = res.total_count != null ? res.total_count : bannedIPsArray.length; + $scope.bannedPage = Math.max(1, res.page != null ? res.page : 1); + $scope.bannedPageSize = res.page_size != null ? res.page_size : 10; + $scope.bannedPageInput = $scope.bannedPage; + console.log('After assignment - scope.bannedIPs:', $scope.bannedIPs); + console.log('After assignment - scope.bannedIPs.length:', $scope.bannedIPs ? $scope.bannedIPs.length : 'undefined'); + console.log('After assignment - activeTab:', $scope.activeTab); + + // No need to call $apply - $http callbacks run within $apply automatically + console.log('View should update automatically (Angular $http handles $apply)'); + + console.log('=== populateBannedIPs() SUCCESS ==='); + } else { + console.error('ERROR: API returned status !== 1'); + console.error('Response data:', res); + $scope.bannedIPs = []; + $scope.bannedIPActionFailed = false; + $scope.bannedIPErrorMessage = (res && res.error_message) || 'Unknown error'; + } + }, + function(error) { + console.error('=== HTTP ERROR ==='); + console.error('Error object:', error); + console.error('Error status:', error.status); + console.error('Error data:', error.data); + console.error('Error statusText:', error.statusText); + + $scope.bannedIPsLoading = false; + $scope.bannedIPActionFailed = true; + $scope.bannedIPActionSuccess = true; + $scope.bannedIPCouldNotConnect = false; + $scope.bannedIPs = []; + + try { + if (!$scope.$$phase && !$scope.$root.$$phase) { + $scope.$apply(); + } + } catch(e) { + console.error('Error in $apply (error handler):', e); + } + } + ); + } + + // Expose to scope for template access + $scope.populateBannedIPs = function() { + console.log('$scope.populateBannedIPs() called from template'); + populateBannedIPs(); + }; + + $scope.goToBannedPage = function(page) { + var totalP = Math.max(1, $scope.bannedTotalPages()); + var p = parseInt(page, 10); + if (isNaN(p) || p < 1 || p > totalP) return; + $scope.bannedPage = p; + populateBannedIPs(); + }; + $scope.goToBannedPageByInput = function() { + var self = $scope; + $timeout(function() { + var n = parseInt(self.bannedPageInput, 10); + if (isNaN(n) || n < 1) n = self.bannedPage || 1; + var maxP = Math.max(1, self.bannedTotalPages()); + n = Math.min(Math.max(1, n), maxP); + self.bannedPageInput = n; + self.bannedPage = n; + populateBannedIPs(); + }, 0); + }; + $scope.bannedTotalPages = function() { + var size = $scope.bannedPageSize || 10; + var total = $scope.bannedTotalCount || ($scope.bannedIPs ? $scope.bannedIPs.length : 0) || 0; + return size > 0 ? Math.max(1, Math.ceil(total / size)) : 1; + }; + $scope.bannedRangeStart = function() { + var total = $scope.bannedTotalCount || ($scope.bannedIPs ? $scope.bannedIPs.length : 0) || 0; + if (total === 0) return 0; + var page = Math.max(1, $scope.bannedPage || 1); + var size = $scope.bannedPageSize || 10; + return (page - 1) * size + 1; + }; + $scope.bannedRangeEnd = function() { + var start = $scope.bannedRangeStart(); + var size = $scope.bannedPageSize || 10; + var total = $scope.bannedTotalCount || ($scope.bannedIPs ? $scope.bannedIPs.length : 0) || 0; + return total === 0 ? 0 : Math.min(start + size - 1, total); + }; + $scope.setBannedPageSize = function() { + var size = parseInt($scope.bannedPageSize, 10); + $scope.bannedPageSize = (size >= 5 && size <= 100) ? size : 10; + $scope.bannedPage = 1; + populateBannedIPs(); + }; + + if (typeof window !== 'undefined') { + window.__firewallLoadTab = function(tab) { + $scope.$evalAsync(function() { + $scope.activeTab = tab; + if (tab === 'banned') { populateBannedIPs(); } else { populateCurrentRecords(); } + }); + }; + } + + // Load banned IPs on page load - use $timeout for Angular compatibility + // Wrap in try-catch to ensure it executes even if there are other errors + try { + $timeout(function() { + try { + console.log('=== Calling populateBannedIPs from $timeout on page load ==='); + populateBannedIPs(); + } catch(e) { + console.error('Error in populateBannedIPs from timeout:', e); + } + }, 500); + } catch(e) { + console.error('Error setting up timeout for populateBannedIPs:', e); + } + $scope.addRule = function () { $scope.rulesLoading = false; @@ -111,37 +412,141 @@ app.controller('firewallController', function ($scope, $http) { $scope.actionFailed = true; $scope.actionSuccess = true; - url = "/firewall/getCurrentRules"; - - var data = {}; - + var data = { + page: Math.max(1, parseInt($scope.rulesPage, 10) || 1), + page_size: Math.max(5, Math.min(100, parseInt($scope.rulesPageSize, 10) || 10)) + }; var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') } }; - $http.post(url, data, config).then(ListInitialDatas, cantLoadInitialDatas); - function ListInitialDatas(response) { - if (response.data.fetchStatus === 1) { - $scope.rules = JSON.parse(response.data.data); + var res = (typeof response.data === 'string') ? (function() { try { return JSON.parse(response.data); } catch (e) { return {}; } })() : response.data; + if (res && res.fetchStatus === 1) { + $scope.rules = typeof res.data === 'string' ? JSON.parse(res.data) : (res.data || []); + $scope.rulesTotalCount = res.total_count != null ? res.total_count : ($scope.rules ? $scope.rules.length : 0); + $scope.rulesPage = Math.max(1, res.page != null ? res.page : 1); + $scope.rulesPageSize = res.page_size != null ? res.page_size : 10; $scope.rulesLoading = true; } else { $scope.rulesLoading = true; - $scope.errorMessage = response.data.error_message; + $scope.errorMessage = (res && res.error_message) ? res.error_message : ''; } } function cantLoadInitialDatas(response) { $scope.couldNotConnect = false; - } + } + $scope.goToRulesPage = function(page) { + var totalP = Math.max(1, $scope.rulesTotalPages()); + var p = parseInt(page, 10); + if (isNaN(p) || p < 1 || p > totalP) return; + $scope.rulesPage = p; + populateCurrentRecords(); + }; + $scope.goToRulesPageByInput = function() { + $timeout(function() { + var n = parseInt($scope.rulesPageInput, 10); + if (isNaN(n) || n < 1) n = $scope.rulesPage || 1; + var maxP = Math.max(1, $scope.rulesTotalPages()); + n = Math.min(Math.max(1, n), maxP); + $scope.rulesPageInput = n; + $scope.rulesPage = n; + populateCurrentRecords(); + }, 0); + }; + $scope.rulesTotalPages = function() { + var size = $scope.rulesPageSize || 10; + var total = $scope.rulesTotalCount || ($scope.rules && $scope.rules.length) || 0; + return size > 0 ? Math.max(1, Math.ceil(total / size)) : 1; + }; + $scope.rulesRangeStart = function() { + var total = $scope.rulesTotalCount || ($scope.rules && $scope.rules.length) || 0; + if (total === 0) return 0; + var page = Math.max(1, $scope.rulesPage || 1); + var size = $scope.rulesPageSize || 10; + return (page - 1) * size + 1; + }; + $scope.rulesRangeEnd = function() { + var start = $scope.rulesRangeStart(); + var size = $scope.rulesPageSize || 10; + var total = $scope.rulesTotalCount || ($scope.rules && $scope.rules.length) || 0; + return total === 0 ? 0 : Math.min(start + size - 1, total); + }; + $scope.setRulesPageSize = function() { + var size = parseInt($scope.rulesPageSize, 10); + $scope.rulesPageSize = (size >= 5 && size <= 100) ? size : 10; + $scope.rulesPage = 1; + populateCurrentRecords(); + }; + + $scope.openModifyRuleModal = function(rule) { + if (!rule) return; + $scope.modifyRuleData = { + id: rule.id, + name: rule.name || '', + proto: rule.proto || 'tcp', + port: String(rule.port || ''), + ruleIP: rule.ipAddress || rule.ruleIP || '0.0.0.0/0' + }; + $scope.showModifyRuleModal = true; + }; + + $scope.closeModifyRuleModal = function() { + $scope.showModifyRuleModal = false; + $scope.modifyRuleData = { id: null, name: '', proto: 'tcp', port: '', ruleIP: '0.0.0.0/0' }; + }; + + $scope.saveModifyRule = function() { + var d = $scope.modifyRuleData; + if (!d.name || !d.name.trim()) { + $scope.actionFailed = false; + $scope.actionSuccess = true; + $scope.errorMessage = 'Rule name is required'; + return; + } + if (!d.port || !String(d.port).trim()) { + $scope.actionFailed = false; + $scope.actionSuccess = true; + $scope.errorMessage = 'Port is required'; + return; + } + $scope.rulesLoading = false; + var url = '/firewall/modifyRule'; + var data = { + id: d.id, + name: d.name.trim(), + proto: d.proto || 'tcp', + port: String(d.port).trim(), + ruleIP: (d.ruleIP || '0.0.0.0/0').trim() + }; + var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') } }; + $http.post(url, data, config).then(function(response) { + $scope.rulesLoading = true; + if (response.data && response.data.status === 1) { + $scope.closeModifyRuleModal(); + $scope.actionFailed = true; + $scope.actionSuccess = false; + populateCurrentRecords(); + } else { + $scope.actionFailed = false; + $scope.actionSuccess = true; + $scope.errorMessage = (response.data && response.data.error_message) || 'Modify failed'; + } + }, function() { + $scope.rulesLoading = true; + $scope.actionFailed = false; + $scope.actionSuccess = true; + $scope.errorMessage = 'Could not connect to server. Please refresh this page.'; + }); }; $scope.deleteRule = function (id, proto, port, ruleIP) { @@ -501,11 +906,412 @@ app.controller('firewallController', function ($scope, $http) { } + } + + // Banned IPs Functions + $scope.addBannedIP = function() { + if (!$scope.banIP || !$scope.banReason) { + $scope.bannedIPActionFailed = false; + $scope.bannedIPErrorMessage = "Please fill in all required fields"; + return; + } + + $scope.bannedIPsLoading = true; + $scope.bannedIPActionFailed = true; + $scope.bannedIPActionSuccess = true; + $scope.bannedIPCouldNotConnect = true; + + var data = { + ip: $scope.banIP, + reason: $scope.banReason, + duration: $scope.banDuration + }; + + var url = "/firewall/addBannedIP"; + var config = { + headers: { + 'X-CSRFToken': getCookie('csrftoken') + } + }; + + $http.post(url, data, config).then(function(response) { + $scope.bannedIPsLoading = false; + // Reset error flags + $scope.bannedIPActionFailed = true; + $scope.bannedIPActionSuccess = true; + $scope.bannedIPCouldNotConnect = true; + + if (response.data.status === 1) { + $scope.bannedIPActionSuccess = false; + $scope.banIP = ''; + $scope.banReason = ''; + $scope.banDuration = '24h'; + console.log('IP banned successfully, refreshing list...'); + populateBannedIPs(); // Refresh the list + } else { + $scope.bannedIPActionFailed = false; + $scope.bannedIPErrorMessage = response.data.error_message || 'Unknown error'; + console.error('Failed to ban IP:', response.data); + } + }, function(error) { + $scope.bannedIPsLoading = false; + $scope.bannedIPActionFailed = true; + $scope.bannedIPActionSuccess = true; + $scope.bannedIPCouldNotConnect = false; + console.error('Error banning IP:', error); + }); }; + $scope.removeBannedIP = function(id, ip) { + if (!confirm('Are you sure you want to unban IP address ' + ip + '?')) { + return; + } + + $scope.bannedIPsLoading = true; + $scope.bannedIPActionFailed = true; + $scope.bannedIPActionSuccess = true; + $scope.bannedIPCouldNotConnect = true; + + var data = { id: id }; + + var url = "/firewall/removeBannedIP"; + var config = { + headers: { + 'X-CSRFToken': getCookie('csrftoken') + } + }; + + $http.post(url, data, config).then(function(response) { + $scope.bannedIPsLoading = false; + if (response.data.status === 1) { + $scope.bannedIPActionSuccess = false; + populateBannedIPs(); // Refresh the list + } else { + $scope.bannedIPActionFailed = false; + $scope.bannedIPErrorMessage = response.data.error_message; + } + }, function(error) { + $scope.bannedIPsLoading = false; + $scope.bannedIPCouldNotConnect = false; + }); + }; + + $scope.deleteBannedIP = function(id, ip) { + if (!confirm('Are you sure you want to permanently delete the record for IP address ' + ip + '? This action cannot be undone.')) { + return; + } + + $scope.bannedIPsLoading = true; + $scope.bannedIPActionFailed = true; + $scope.bannedIPActionSuccess = true; + $scope.bannedIPCouldNotConnect = true; + + var data = { id: id }; + + var url = "/firewall/deleteBannedIP"; + var config = { + headers: { + 'X-CSRFToken': getCookie('csrftoken') + } + }; + + $http.post(url, data, config).then(function(response) { + $scope.bannedIPsLoading = false; + if (response.data.status === 1) { + $scope.bannedIPActionSuccess = false; + populateBannedIPs(); // Refresh the list + } else { + $scope.bannedIPActionFailed = false; + $scope.bannedIPErrorMessage = response.data.error_message; + } + }, function(error) { + $scope.bannedIPsLoading = false; + $scope.bannedIPCouldNotConnect = false; + }); + }; + + $scope.openModifyModal = function(bannedIP) { + if (!bannedIP) return; + $scope.modifyBannedIPData = { + id: bannedIP.id, + ip: bannedIP.ip || bannedIP.ip_address || '', + reason: bannedIP.reason || '', + duration: bannedIP.duration || '24h' + }; + $scope.showModifyModal = true; + }; + + $scope.closeModifyModal = function() { + $scope.showModifyModal = false; + $scope.modifyBannedIPData = { ip: '', id: null, reason: '', duration: '24h' }; + }; + + $scope.saveModifyBannedIP = function() { + var d = $scope.modifyBannedIPData; + if (!d.reason || !d.reason.trim()) { + $scope.bannedIPActionFailed = false; + $scope.bannedIPErrorMessage = 'Reason is required'; + return; + } + $scope.bannedIPsLoading = true; + $scope.bannedIPActionFailed = true; + $scope.bannedIPActionSuccess = true; + $scope.bannedIPCouldNotConnect = true; + var data = { + id: d.id, + ip: d.ip, + reason: d.reason.trim(), + duration: d.duration || '24h' + }; + var url = '/firewall/modifyBannedIP'; + var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') } }; + $http.post(url, data, config).then(function(response) { + $scope.bannedIPsLoading = false; + if (response.data && response.data.status === 1) { + $scope.bannedIPActionSuccess = false; + $scope.closeModifyModal(); + populateBannedIPs(); + } else { + $scope.bannedIPActionFailed = false; + $scope.bannedIPErrorMessage = (response.data && response.data.error_message) || 'Modify failed'; + } + }, function(error) { + $scope.bannedIPsLoading = false; + $scope.bannedIPCouldNotConnect = false; + }); + }; + + $scope.exportBannedIPs = function() { + $scope.bannedIPsLoading = false; + var url = "/firewall/exportBannedIPs"; + var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') }, responseType: 'text' }; + $http.post(url, {}, config).then(function(response) { + $scope.bannedIPsLoading = true; + var raw = response.data; + try { + var data = typeof raw === 'string' ? JSON.parse(raw) : raw; + if (data && data.exportStatus === 0) { + $scope.bannedIPActionFailed = false; + $scope.bannedIPErrorMessage = data.error_message || 'Export failed'; + return; + } + } catch (e) {} + var content = typeof raw === 'string' ? raw : JSON.stringify(raw, null, 2); + var blob = new Blob([content], { type: 'application/json' }); + var a = document.createElement('a'); + a.href = window.URL.createObjectURL(blob); + a.download = 'banned_ips_export_' + (Date.now() / 1000 | 0) + '.json'; + a.click(); + window.URL.revokeObjectURL(a.href); + $scope.bannedIPActionSuccess = false; + }, function() { + $scope.bannedIPsLoading = true; + $scope.bannedIPCouldNotConnect = false; + }); + }; + + $scope.importBannedIPs = function() { + var input = document.createElement('input'); + input.type = 'file'; + input.accept = '.json'; + input.style.display = 'none'; + input.onchange = function(event) { + var file = event.target.files[0]; + if (!file) return; + $scope.bannedIPsLoading = false; + var formData = new FormData(); + formData.append('import_file', file); + var config = { + headers: { 'X-CSRFToken': getCookie('csrftoken'), 'Content-Type': undefined }, + transformRequest: angular.identity + }; + $http.post("/firewall/importBannedIPs", formData, config).then(function(response) { + $scope.bannedIPsLoading = true; + if (response.data && response.data.importStatus === 1) { + $scope.bannedIPActionSuccess = false; + populateBannedIPs(); + } else { + $scope.bannedIPActionFailed = false; + $scope.bannedIPErrorMessage = (response.data && response.data.error_message) || 'Import failed'; + } + }, function() { + $scope.bannedIPsLoading = true; + $scope.bannedIPCouldNotConnect = false; + }); + }; + document.body.appendChild(input); + input.click(); + document.body.removeChild(input); + }; + + // Export/Import Firewall Rules: format modals and actions + $scope.exportRules = function () { + $scope.showExportFormatModal = true; + $scope.exportRulesFormat = $scope.exportRulesFormat || 'json'; + }; + + $scope.closeExportFormatModal = function () { + $scope.showExportFormatModal = false; + }; + + $scope.confirmExportRules = function () { + $scope.showExportFormatModal = false; + var format = $scope.exportRulesFormat || 'json'; + doExportRules(format); + }; + + function doExportRules(format) { + $scope.rulesLoading = true; + $scope.actionFailed = true; + $scope.actionSuccess = true; + + var url = "/firewall/exportFirewallRules"; + var data = { format: format }; + var config = { + headers: { 'X-CSRFToken': getCookie('csrftoken') }, + responseType: 'text' + }; + + $http.post(url, data, config).then(exportSuccess, exportError); + + function exportSuccess(response) { + $scope.rulesLoading = false; + var raw = response.data; + if (typeof raw === 'string' && raw.indexOf('{') === 0) { + try { + var parsed = JSON.parse(raw); + if (parsed.exportStatus === 0) { + $scope.actionFailed = false; + $scope.actionSuccess = true; + $scope.errorMessage = parsed.error_message || 'Export failed'; + return; + } + } catch (e) {} + } + var contentType = format === 'excel' ? 'text/csv' : 'application/json'; + var ext = format === 'excel' ? 'csv' : 'json'; + var blob = new Blob([typeof raw === 'string' ? raw : JSON.stringify(raw)], { type: contentType }); + var a = document.createElement('a'); + a.href = window.URL.createObjectURL(blob); + a.download = 'firewall_rules_export_' + (Date.now() / 1000 | 0) + '.' + ext; + a.click(); + window.URL.revokeObjectURL(a.href); + $scope.actionFailed = true; + $scope.actionSuccess = false; + } + + function exportError() { + $scope.rulesLoading = false; + $scope.actionFailed = false; + $scope.actionSuccess = true; + $scope.errorMessage = "Could not connect to server. Please refresh this page."; + } + } + + $scope.importRules = function () { + $scope.showImportFormatModal = true; + $scope.importRulesFormat = $scope.importRulesFormat || 'json'; + }; + + $scope.closeImportFormatModal = function () { + $scope.showImportFormatModal = false; + }; + + $scope.confirmImportRules = function () { + $scope.showImportFormatModal = false; + var format = $scope.importRulesFormat || 'json'; + var accept = format === 'excel' ? '.csv' : '.json'; + var input = document.createElement('input'); + input.type = 'file'; + input.accept = accept; + input.style.display = 'none'; + input.onchange = function(event) { + var file = event.target.files[0]; + if (file) { + if (format === 'json') { + var reader = new FileReader(); + reader.onload = function(e) { + try { + var importData = JSON.parse(e.target.result); + if (!importData.rules || !Array.isArray(importData.rules)) { + $scope.$apply(function() { + $scope.actionFailed = false; + $scope.actionSuccess = true; + $scope.errorMessage = "Invalid import file format. Please select a valid firewall rules export file."; + }); + return; + } + uploadImportFile(file); + } catch (err) { + $scope.$apply(function() { + $scope.actionFailed = false; + $scope.actionSuccess = true; + $scope.errorMessage = "Invalid JSON file. Please select a valid firewall rules export file."; + }); + } + }; + reader.readAsText(file); + } else { + uploadImportFile(file); + } + } + }; + document.body.appendChild(input); + input.click(); + document.body.removeChild(input); + }; + + function uploadImportFile(file) { + $scope.rulesLoading = true; + $scope.actionFailed = true; + $scope.actionSuccess = true; + + var formData = new FormData(); + formData.append('import_file', file); + + var config = { + headers: { + 'X-CSRFToken': getCookie('csrftoken'), + 'Content-Type': undefined + }, + transformRequest: angular.identity + }; + + $http.post("/firewall/importFirewallRules", formData, config).then(importSuccess, importError); + + function importSuccess(response) { + $scope.rulesLoading = false; + var res = response.data; + if (typeof res === 'string') { + try { res = JSON.parse(res); } catch (e) { res = {}; } + } + if (res && res.importStatus === 1) { + $scope.actionFailed = true; + $scope.actionSuccess = false; + populateCurrentRecords(); + var summary = "Import completed successfully!\nImported: " + (res.imported_count || 0) + " rules\nSkipped: " + (res.skipped_count || 0) + " rules\nErrors: " + (res.error_count || 0) + " rules"; + if (res.errors && res.errors.length > 0) { + summary += "\n\nErrors:\n" + res.errors.join("\n"); + } + alert(summary); + } else { + $scope.actionFailed = false; + $scope.actionSuccess = true; + $scope.errorMessage = (res && res.error_message) ? res.error_message : "Import failed."; + } + } + + function importError() { + $scope.rulesLoading = false; + $scope.actionFailed = false; + $scope.actionSuccess = true; + $scope.errorMessage = "Could not connect to server. Please refresh this page."; + } + } }); + /* Java script code to ADD Firewall Rules */ /* Java script code to Secure SSH */ @@ -520,6 +1326,7 @@ app.controller('secureSSHCTRL', function ($scope, $http) { $scope.keyBox = true; $scope.showKeyBox = false; $scope.saveKeyBtn = true; + $scope.sshPort = "22"; // Initialize with default SSH port as string $scope.addKey = function () { $scope.saveKeyBtn = false; @@ -568,7 +1375,8 @@ app.controller('secureSSHCTRL', function ($scope, $http) { $scope.sshPort = response.data.sshPort; if (response.data.permitRootLogin == 1) { - $('#rootLogin').bootstrapToggle('on'); + $('#rootLogin').prop('checked', true); + rootLogin = true; $scope.couldNotSave = true; $scope.detailsSaved = true; $scope.couldNotConnect = true; @@ -941,32 +1749,36 @@ app.controller('modSec', function ($scope, $http, $timeout, $window) { ///// ModSec configs - var modsecurity_status = false; - var SecAuditEngine = false; - var SecRuleEngine = false; + $scope.modsecurity_status = false; + $scope.SecAuditEngine = false; + $scope.SecRuleEngine = false; + // Initialize change handlers after DOM is ready + $timeout(function() { + $('#modsecurity_status').change(function () { + $scope.modsecurity_status = $(this).prop('checked'); + $scope.$apply(); + }); - $('#modsecurity_status').change(function () { - modsecurity_status = $(this).prop('checked'); - }); + $('#SecAuditEngine').change(function () { + $scope.SecAuditEngine = $(this).prop('checked'); + $scope.$apply(); + }); - $('#SecAuditEngine').change(function () { - SecAuditEngine = $(this).prop('checked'); - }); - - - $('#SecRuleEngine').change(function () { - SecRuleEngine = $(this).prop('checked'); - }); + $('#SecRuleEngine').change(function () { + $scope.SecRuleEngine = $(this).prop('checked'); + $scope.$apply(); + }); + }, 100); fetchModSecSettings(); function fetchModSecSettings() { $scope.modsecLoading = false; - $('#modsecurity_status').bootstrapToggle('off'); - $('#SecAuditEngine').bootstrapToggle('off'); - $('#SecRuleEngine').bootstrapToggle('off'); + $('#modsecurity_status').prop('checked', false); + $('#SecAuditEngine').prop('checked', false); + $('#SecRuleEngine').prop('checked', false); url = "/firewall/fetchModSecSettings"; @@ -993,13 +1805,16 @@ app.controller('modSec', function ($scope, $http, $timeout, $window) { if (response.data.installed === 1) { if (response.data.modsecurity === 1) { - $('#modsecurity_status').bootstrapToggle('on'); + $('#modsecurity_status').prop('checked', true); + $scope.modsecurity_status = true; } if (response.data.SecAuditEngine === 1) { - $('#SecAuditEngine').bootstrapToggle('on'); + $('#SecAuditEngine').prop('checked', true); + $scope.SecAuditEngine = true; } if (response.data.SecRuleEngine === 1) { - $('#SecRuleEngine').bootstrapToggle('on'); + $('#SecRuleEngine').prop('checked', true); + $scope.SecRuleEngine = true; } $scope.SecDebugLogLevel = response.data.SecDebugLogLevel; @@ -1038,9 +1853,9 @@ app.controller('modSec', function ($scope, $http, $timeout, $window) { url = "/firewall/saveModSecConfigurations"; var data = { - modsecurity_status: modsecurity_status, - SecAuditEngine: SecAuditEngine, - SecRuleEngine: SecRuleEngine, + modsecurity_status: $scope.modsecurity_status, + SecAuditEngine: $scope.SecAuditEngine, + SecRuleEngine: $scope.SecRuleEngine, SecDebugLogLevel: $scope.SecDebugLogLevel, SecAuditLogParts: $scope.SecAuditLogParts, SecAuditLogRelevantStatus: $scope.SecAuditLogRelevantStatus, @@ -1283,20 +2098,20 @@ app.controller('modSecRulesPack', function ($scope, $http, $timeout, $window) { if (updateToggle === true) { if (response.data.owaspInstalled === 1) { - $('#owaspInstalled').bootstrapToggle('on'); + $('#owaspInstalled').prop('checked', true); $scope.owaspDisable = false; owaspInstalled = true; } else { - $('#owaspInstalled').bootstrapToggle('off'); + $('#owaspInstalled').prop('checked', false); $scope.owaspDisable = true; owaspInstalled = false; } if (response.data.comodoInstalled === 1) { - $('#comodoInstalled').bootstrapToggle('on'); + $('#comodoInstalled').prop('checked', true); $scope.comodoDisable = false; comodoInstalled = true; } else { - $('#comodoInstalled').bootstrapToggle('off'); + $('#comodoInstalled').prop('checked', false); $scope.comodoDisable = true; comodoInstalled = false; } @@ -1696,15 +2511,18 @@ app.controller('csf', function ($scope, $http, $timeout, $window) { var currentChild = "general"; $scope.activateTab = function (newMain, newChild) { - $("#" + currentMain).removeClass("ui-tabs-active"); - $("#" + currentMain).removeClass("ui-state-active"); - - $("#" + newMain).addClass("ui-tabs-active"); - $("#" + newMain).addClass("ui-state-active"); - - $('#' + currentChild).hide(); - $('#' + newChild).show(); - + // Remove active class from all tabs + $('.tab-button').removeClass('active'); + + // Add active class to clicked tab + $('#' + newMain).addClass('active'); + + // Hide all tab contents + $('.tab-content').removeClass('active'); + + // Show selected tab content + $('#' + newChild).addClass('active'); + currentMain = newMain; currentChild = newChild; }; @@ -1818,8 +2636,8 @@ app.controller('csf', function ($scope, $http, $timeout, $window) { $scope.csfLoading = false; - $('#testingMode').bootstrapToggle('off'); - $('#firewallStatus').bootstrapToggle('off'); + $('#testingMode').prop('checked', false); + $('#firewallStatus').prop('checked', false); url = "/firewall/fetchCSFSettings"; @@ -1849,10 +2667,10 @@ app.controller('csf', function ($scope, $http, $timeout, $window) { }); if (response.data.testingMode === 1) { - $('#testingMode').bootstrapToggle('on'); + $('#testingMode').prop('checked', true); } if (response.data.firewallStatus === 1) { - $('#firewallStatus').bootstrapToggle('on'); + $('#firewallStatus').prop('checked', true); } $scope.tcpIN = response.data.tcpIN; @@ -2280,4 +3098,135 @@ app.controller('installImunifyAV', function ($scope, $http, $timeout, $window) { } } -}); \ No newline at end of file +}); + + +app.controller('litespeed_ent_conf', function ($scope, $http, $timeout, $window){ + $scope.modsecLoading = true; + $scope.rulesSaved = true; + $scope.couldNotConnect = true; + $scope.couldNotSave = true; + fetchlitespeed_conf(); + function fetchlitespeed_conf() { + + $scope.modsecLoading = false; + $scope.modsecLoading = true; + $scope.rulesSaved = true; + $scope.couldNotConnect = true; + + + url = "/firewall/fetchlitespeed_conf"; + + var data = {}; + + var config = { + headers: { + 'X-CSRFToken': getCookie('csrftoken') + } + }; + + $http.post(url, data, config).then(ListInitialDatas, cantLoadInitialDatas); + + + function ListInitialDatas(response) { + + $scope.modsecLoading = true; + + if (response.data.status === 1) { + + $scope.currentLitespeed_conf = response.data.currentLitespeed_conf; + + } + else + { + $scope.errorMessage = response.data.error_message; + } + + } + + function cantLoadInitialDatas(response) { + $scope.modsecLoading = true; + } + + } + + + + $scope.saveLitespeed_conf = function () { + // alert('test-----------------') + + $scope.modsecLoading = false; + $scope.rulesSaved = true; + $scope.couldNotConnect = true; + $scope.couldNotSave = true; + + + url = "/firewall/saveLitespeed_conf"; + + var data = { + modSecRules: $scope.currentLitespeed_conf + + }; + + var config = { + headers: { + 'X-CSRFToken': getCookie('csrftoken') + } + }; + + $http.post(url, data, config).then(ListInitialDatas, cantLoadInitialDatas); + + + function ListInitialDatas(response) { + + $scope.modsecLoading = true; + + if (response.data.status === 1) { + + $scope.rulesSaved = false; + $scope.couldNotConnect = true; + $scope.couldNotSave = true; + + $scope.currentLitespeed_conf = response.data.currentLitespeed_conf; + + } else { + $scope.rulesSaved = true; + $scope.couldNotConnect = false; + $scope.couldNotSave = false; + + $scope.errorMessage = response.data.error_message; + } + + } + + function cantLoadInitialDatas(response) { + $scope.modsecLoading = true; + $scope.rulesSaved = true; + $scope.couldNotConnect = false; + $scope.couldNotSave = true; + } + } + +}); + +(function() { + // Do not capture tab clicks – let Angular ng-click run setFirewallTab() so data loads. + // Only sync tab from hash on load and hashchange (back/forward) via __firewallLoadTab. + function syncFirewallTabFromHash() { + var nav = document.getElementById('firewall-tab-nav'); + if (!nav) return; + var h = (window.location.hash || '').replace(/^#/, ''); + var tab = (h === 'banned-ips') ? 'banned' : 'rules'; + if (window.__firewallLoadTab) { + try { window.__firewallLoadTab(tab); } catch (e) {} + } + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', syncFirewallTabFromHash); + } else { + syncFirewallTabFromHash(); + } + setTimeout(syncFirewallTabFromHash, 100); + window.addEventListener('hashchange', syncFirewallTabFromHash); +})(); \ No newline at end of file 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/public/static/userManagment/userManagment.js b/public/static/userManagment/userManagment.js index f706d487f..0f0b677af 100644 --- a/public/static/userManagment/userManagment.js +++ b/public/static/userManagment/userManagment.js @@ -9,7 +9,7 @@ app.controller('createUserCtr', function ($scope, $http) { $scope.acctsLimit = true; $scope.webLimits = true; $scope.userCreated = true; - $scope.userCreationFailed = true; + $scope.userCreationFailed = false; // false = don't show error alert on load $scope.couldNotConnect = true; $scope.userCreationLoading = true; $scope.combinedLength = true; @@ -18,7 +18,7 @@ app.controller('createUserCtr', function ($scope, $http) { $scope.webLimits = false; $scope.userCreated = true; - $scope.userCreationFailed = true; + $scope.userCreationFailed = false; // hide error until we know the result $scope.couldNotConnect = true; $scope.userCreationLoading = false; $scope.combinedLength = true; @@ -61,7 +61,7 @@ app.controller('createUserCtr', function ($scope, $http) { if (response.data.createStatus == 1) { $scope.userCreated = false; - $scope.userCreationFailed = true; + $scope.userCreationFailed = false; // hide error on success $scope.couldNotConnect = true; $scope.userCreationLoading = true; @@ -73,11 +73,11 @@ app.controller('createUserCtr', function ($scope, $http) { $scope.acctsLimit = false; $scope.webLimits = false; $scope.userCreated = true; - $scope.userCreationFailed = false; + $scope.userCreationFailed = true; // show error on failure $scope.couldNotConnect = true; $scope.userCreationLoading = true; - $scope.errorMessage = response.data.error_message; + $scope.errorMessage = (response.data && (response.data.error_message || response.data.message || response.data.errorMessage)) || 'Unknown error'; } @@ -90,7 +90,7 @@ app.controller('createUserCtr', function ($scope, $http) { $scope.acctsLimit = false; $scope.webLimits = false; $scope.userCreated = true; - $scope.userCreationFailed = true; + $scope.userCreationFailed = false; // show "Could not connect" instead $scope.couldNotConnect = false; $scope.userCreationLoading = true; @@ -138,10 +138,10 @@ app.controller('modifyUser', function ($scope, $http) { $scope.acctDetailsFetched = true; $scope.userAccountsLimit = true; $scope.userModified = true; - $scope.canotModifyUser = true; + $scope.canotModifyUser = false; // false = don't show error alert on load $scope.couldNotConnect = true; - $scope.canotFetchDetails = true; - $scope.detailsFetched = true; + $scope.canotFetchDetails = false; // false = don't show fetch error on load + $scope.detailsFetched = false; // false = don't show "details loaded" on load $scope.accountTypeView = true; $scope.websitesLimit = true; $scope.qrHidden = true; @@ -200,9 +200,9 @@ app.controller('modifyUser', function ($scope, $http) { $scope.userModificationLoading = true; $scope.acctDetailsFetched = false; $scope.userModified = true; - $scope.canotModifyUser = true; + $scope.canotModifyUser = false; // hide modify error (we only fetched details) $scope.couldNotConnect = true; - $scope.canotFetchDetails = true; + $scope.canotFetchDetails = false; // hide fetch error on success $scope.detailsFetched = false; $scope.userAccountsLimit = true; $scope.websitesLimit = true; @@ -212,13 +212,13 @@ app.controller('modifyUser', function ($scope, $http) { $scope.acctDetailsFetched = true; $scope.userAccountsLimit = true; $scope.userModified = true; - $scope.canotModifyUser = true; + $scope.canotModifyUser = false; // hide modify error (only fetch failed) $scope.couldNotConnect = true; - $scope.canotFetchDetails = false; - $scope.detailsFetched = true; + $scope.canotFetchDetails = true; // show fetch error + $scope.detailsFetched = false; - $scope.errorMessage = response.data.error_message; + $scope.errorMessage = (response.data && (response.data.error_message || response.data.message || response.data.errorMessage)) || 'Unknown error'; } @@ -232,7 +232,7 @@ app.controller('modifyUser', function ($scope, $http) { $scope.acctDetailsFetched = true; $scope.userAccountsLimit = true; $scope.userModified = true; - $scope.canotModifyUser = true; + $scope.canotModifyUser = false; // hide modify error (only connection/fetch failed) $scope.couldNotConnect = false; $scope.canotFetchDetails = true; $scope.detailsFetched = true; @@ -249,7 +249,7 @@ app.controller('modifyUser', function ($scope, $http) { $scope.userModificationLoading = false; $scope.acctDetailsFetched = false; $scope.userModified = true; - $scope.canotModifyUser = true; + $scope.canotModifyUser = false; // hide error until we know result $scope.couldNotConnect = true; $scope.canotFetchDetails = true; $scope.detailsFetched = true; @@ -295,7 +295,7 @@ app.controller('modifyUser', function ($scope, $http) { $scope.userModificationLoading = true; $scope.acctDetailsFetched = true; $scope.userModified = false; - $scope.canotModifyUser = true; + $scope.canotModifyUser = false; // hide modify error on success $scope.couldNotConnect = true; $scope.canotFetchDetails = true; $scope.detailsFetched = true; @@ -312,13 +312,13 @@ app.controller('modifyUser', function ($scope, $http) { $scope.userModificationLoading = true; $scope.acctDetailsFetched = false; $scope.userModified = true; - $scope.canotModifyUser = false; + $scope.canotModifyUser = true; // show modify error $scope.couldNotConnect = true; $scope.canotFetchDetails = true; $scope.detailsFetched = true; - $scope.errorMessage = response.data.error_message; + $scope.errorMessage = (response.data && (response.data.error_message || response.data.message || response.data.errorMessage)) || 'Unknown error'; } @@ -1489,6 +1489,42 @@ app.controller('apiAccessCTRL', function ($scope, $http) { /* Java script code to list table users */ +/* Show modal by id - works with Bootstrap 3 (jQuery) or Bootstrap 5 (native) */ +function showModalById(modalId) { + var el = document.getElementById(modalId); + if (!el) return; + if (typeof jQuery !== 'undefined' && jQuery(el).modal) { + jQuery(el).modal('show'); + } else if (typeof bootstrap !== 'undefined' && bootstrap.Modal) { + var m = bootstrap.Modal.getOrCreateInstance(el); + if (m) m.show(); + } else { + el.style.display = 'block'; + el.classList.add('in'); + if (el.getAttribute('aria-hidden') !== null) el.setAttribute('aria-hidden', 'false'); + var backdrop = document.createElement('div'); + backdrop.className = 'modal-backdrop fade in'; + backdrop.setAttribute('data-modal-backdrop', modalId); + document.body.appendChild(backdrop); + } +} + +function hideModalById(modalId) { + var el = document.getElementById(modalId); + if (!el) return; + if (typeof jQuery !== 'undefined' && jQuery(el).modal) { + jQuery(el).modal('hide'); + } else if (typeof bootstrap !== 'undefined' && bootstrap.Modal) { + var m = bootstrap.Modal.getInstance(el); + if (m) m.hide(); + } else { + el.style.display = 'none'; + el.classList.remove('in'); + if (el.getAttribute('aria-hidden') !== null) el.setAttribute('aria-hidden', 'true'); + var backdrops = document.querySelectorAll('[data-modal-backdrop="' + modalId + '"]'); + backdrops.forEach(function (b) { if (b.parentNode) b.parentNode.removeChild(b); }); + } +} app.controller('listTableUsers', function ($scope, $http) { @@ -1552,6 +1588,7 @@ app.controller('listTableUsers', function ($scope, $http) { $scope.deleteUserInitial = function (name){ UserToDelete = name; $scope.UserToDelete = name; + showModalById('deleteModal'); }; $scope.deleteUserFinal = function () { @@ -1576,6 +1613,7 @@ app.controller('listTableUsers', function ($scope, $http) { $scope.cyberpanelLoading = true; if (response.data.deleteStatus === 1) { $scope.populateCurrentRecords(); + hideModalById('deleteModal'); new PNotify({ title: 'Success!', text: 'Users successfully deleted!', @@ -1613,6 +1651,7 @@ app.controller('listTableUsers', function ($scope, $http) { $scope.editInitial = function (name) { $scope.name = name; + showModalById('editModal'); }; @@ -1640,6 +1679,7 @@ app.controller('listTableUsers', function ($scope, $http) { if (response.data.status === 1) { $scope.populateCurrentRecords(); + hideModalById('editModal'); new PNotify({ title: 'Success!', text: 'Changes successfully applied!', @@ -1693,6 +1733,7 @@ app.controller('listTableUsers', function ($scope, $http) { if (response.data.status === 1) { $scope.populateCurrentRecords(); + hideModalById('editModal'); new PNotify({ title: 'Success!', text: 'ACL Successfully changed.', diff --git a/public/static/websiteFunctions/websiteFunctions.js b/public/static/websiteFunctions/websiteFunctions.js index f6c5f9dcb..ab881df98 100644 --- a/public/static/websiteFunctions/websiteFunctions.js +++ b/public/static/websiteFunctions/websiteFunctions.js @@ -10742,7 +10742,20 @@ $("#websiteSuccessfullyModified").hide(); $("#modifyWebsiteLoading").hide(); $("#modifyWebsiteButton").hide(); -app.controller('modifyWebsitesController', function ($scope, $http) { +/** Angular filter: format bytes as human-readable size (used by modifyWebsite.html) */ +app.filter('filesize', [function () { + return function (bytes) { + if (bytes == null || isNaN(bytes)) return '-'; + var n = Number(bytes); + if (n === 0) return '0 B'; + var k = 1024; + var sizes = ['B', 'KB', 'MB', 'GB', 'TB']; + var i = Math.floor(Math.log(n) / Math.log(k)); + return (n / Math.pow(k, i)).toFixed(2) + ' ' + sizes[Math.min(i, sizes.length - 1)]; + }; +}]); + +app.controller('modifyWebsitesController', ['$scope', '$http', function ($scope, $http) { // Initialize home directory variables $scope.homeDirectories = []; @@ -10752,7 +10765,7 @@ app.controller('modifyWebsitesController', function ($scope, $http) { // Load home directories on page load $scope.loadHomeDirectories = function() { - $http.post('/userManagement/getUserHomeDirectories/', {}) + $http.post('/users/getUserHomeDirectories', {}) .then(function(response) { if (response.data.status === 1) { $scope.homeDirectories = response.data.directories; @@ -10915,7 +10928,7 @@ app.controller('modifyWebsitesController', function ($scope, $http) { }; -}); +}]); /* Java script code to Modify Pacakge ends here */ diff --git a/rollback_phpmyadmin_redirect.sh b/rollback_phpmyadmin_redirect.sh deleted file mode 100644 index 159ec00a3..000000000 --- a/rollback_phpmyadmin_redirect.sh +++ /dev/null @@ -1,49 +0,0 @@ -#!/bin/bash - -# CyberPanel phpMyAdmin Access Control Rollback Script -# This script reverts the phpMyAdmin access control changes - -echo "=== CyberPanel phpMyAdmin Access Control Rollback ===" - -# Check if running as root -if [ "$EUID" -ne 0 ]; then - echo "Please run this script as root" - exit 1 -fi - -# Find the most recent backup -LATEST_BACKUP=$(ls -t /usr/local/CyberCP/public/phpmyadmin/index.php.backup.* 2>/dev/null | head -n1) - -if [ -z "$LATEST_BACKUP" ]; then - echo "No backup found. Cannot rollback changes." - echo "You may need to reinstall phpMyAdmin or restore from your own backup." - exit 1 -fi - -echo "Found backup: $LATEST_BACKUP" -echo "Restoring original phpMyAdmin index.php..." - -# Restore the original index.php -cp "$LATEST_BACKUP" /usr/local/CyberCP/public/phpmyadmin/index.php - -# Remove the .htaccess file if it exists -if [ -f "/usr/local/CyberCP/public/phpmyadmin/.htaccess" ]; then - echo "Removing .htaccess file..." - rm /usr/local/CyberCP/public/phpmyadmin/.htaccess -fi - -# Set proper permissions -echo "Setting permissions..." -chown lscpd:lscpd /usr/local/CyberCP/public/phpmyadmin/index.php -chmod 644 /usr/local/CyberCP/public/phpmyadmin/index.php - -# Restart LiteSpeed to ensure changes take effect -echo "Restarting LiteSpeed..." -systemctl restart lscpd - -echo "=== Rollback Complete ===" -echo "" -echo "phpMyAdmin access control has been reverted!" -echo "phpMyAdmin should now work as it did before the changes." -echo "" -echo "Backup file used: $LATEST_BACKUP" 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..88e4c0295 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: @@ -866,9 +884,23 @@ def fetchPackages(request): locked = ProcessUtilities.outputExecutioner(command).split('\n') if type == 'CyberPanel': - - command = 'cat /usr/local/CyberCP/AllCPUbuntu.json' - packages = json.loads(ProcessUtilities.outputExecutioner(command)) + # Prefer live data for Ubuntu 22/24, fall back to static JSON + packages = None + try: + cmd_out = ProcessUtilities.outputExecutioner('apt list --installed 2>/dev/null') + lines = [l for l in cmd_out.split('\n') if l and '/' in l][4:] # Skip header + packages = [] + for line in lines: + parts = line.split(None, 2) + if len(parts) >= 2: + packages.append({'Package': parts[0], 'Version': parts[1]}) + except Exception: + pass + if not packages and os.path.exists('/usr/local/CyberCP/AllCPUbuntu.json'): + command = 'cat /usr/local/CyberCP/AllCPUbuntu.json' + packages = json.loads(ProcessUtilities.outputExecutioner(command)) + if not packages: + packages = [] else: command = 'apt list --installed' @@ -888,11 +920,16 @@ def fetchPackages(request): elif ProcessUtilities.decideDistro() == ProcessUtilities.centos or ProcessUtilities.decideDistro() == ProcessUtilities.cent8: ### Check Package Lock status - - if os.path.exists('/etc/yum.conf'): + # Prefer dnf.conf when dnf is present (AlmaLinux 9/10, RHEL 9, Rocky 9) + yum_dnf = 'dnf' if os.path.exists('/usr/bin/dnf') else 'yum' + if yum_dnf == 'dnf' and os.path.exists('/etc/dnf/dnf.conf'): + yumConf = '/etc/dnf/dnf.conf' + elif os.path.exists('/etc/yum.conf'): yumConf = '/etc/yum.conf' elif os.path.exists('/etc/yum/yum.conf'): yumConf = '/etc/yum/yum.conf' + else: + yumConf = '/etc/dnf/dnf.conf' if os.path.exists('/etc/dnf/dnf.conf') else '/etc/yum.conf' yumConfData = open(yumConf, 'r').read() locked = [] @@ -912,7 +949,7 @@ def fetchPackages(request): startForUpdate = 1 - command = 'yum check-update' + command = '%s check-update 2>/dev/null || true' % yum_dnf updates = ProcessUtilities.outputExecutioner(command).split('\n') for items in updates: @@ -930,7 +967,7 @@ def fetchPackages(request): ### - command = 'yum list installed' + command = '%s list installed' % yum_dnf packages = ProcessUtilities.outputExecutioner(command).split('\n') startFrom = 1 @@ -946,7 +983,7 @@ def fetchPackages(request): startForUpdate = 1 - command = 'yum check-update' + command = '%s check-update 2>/dev/null || true' % yum_dnf packages = ProcessUtilities.outputExecutioner(command).split('\n') for items in packages: @@ -956,8 +993,26 @@ def fetchPackages(request): else: startForUpdate = startForUpdate + 1 elif type == 'CyberPanel': - command = 'cat /usr/local/CyberCP/CPCent7repo.json' - packages = json.loads(ProcessUtilities.outputExecutioner(command)) + # Prefer live data for AlmaLinux 8/9/10, RHEL, Rocky; fall back to static JSON + packages = None + try: + dnf_cmd = 'dnf list installed' if os.path.exists('/usr/bin/dnf') else 'yum list installed' + cmd_out = ProcessUtilities.outputExecutioner(dnf_cmd) + lines = [l.strip() for l in cmd_out.split('\n') if l.strip()] + idx = next((i for i, l in enumerate(lines) if 'Installed Packages' in l or 'Installed' in l), 0) + lines = lines[idx + 1:] if idx < len(lines) else lines + packages = [] + for line in lines: + parts = line.split() + if len(parts) >= 2: + packages.append({'Package': parts[0], 'Version': parts[1]}) + except Exception: + pass + if not packages and os.path.exists('/usr/local/CyberCP/CPCent7repo.json'): + command = 'cat /usr/local/CyberCP/CPCent7repo.json' + packages = json.loads(ProcessUtilities.outputExecutioner(command)) + if not packages: + packages = [] ## make list of packages that need update @@ -1113,7 +1168,8 @@ def fetchPackageDetails(request): command = 'apt-cache show %s' % (package) packageDetails = ProcessUtilities.outputExecutioner(command) elif ProcessUtilities.decideDistro() == ProcessUtilities.centos or ProcessUtilities.decideDistro() == ProcessUtilities.cent8: - command = 'yum info %s' % (package) + pkg_cmd = 'dnf info' if os.path.exists('/usr/bin/dnf') else 'yum info' + command = '%s %s' % (pkg_cmd, package) packageDetails = ProcessUtilities.outputExecutioner(command) data_ret = {'status': 1, 'packageDetails': packageDetails} 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/baseTemplate/assets/mobile-responsive.css b/static/baseTemplate/assets/mobile-responsive.css new file mode 100644 index 000000000..35e2674e5 --- /dev/null +++ b/static/baseTemplate/assets/mobile-responsive.css @@ -0,0 +1,589 @@ +/* CyberPanel Mobile Responsive & Readability Fixes */ +/* This file ensures all pages are mobile-friendly with proper font sizes and readable text */ + +/* Base font size and mobile-first approach */ +html { + font-size: 16px; /* Base font size for better readability */ + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; + text-size-adjust: 100%; +} + +body { + font-size: 16px; + line-height: 1.6; + color: #2f3640; /* Dark text for better readability on white backgrounds */ + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; +} + +/* Ensure all text is readable with proper contrast */ +* { + color: inherit; +} + +/* Override any light text that might be hard to read */ +.text-muted, .text-secondary, .text-light { + color: #2f3640 !important; /* Dark text for better readability on white backgrounds */ +} + +/* Fix small font sizes that are hard to read */ +small, .small, .text-small { + font-size: 14px !important; /* Minimum readable size */ +} + +/* Table improvements for mobile */ +.table { + font-size: 16px !important; /* Larger table text */ + width: 100%; + border-collapse: collapse; + margin-bottom: 20px; +} + +.table th, .table td { + padding: 12px 8px !important; /* More padding for touch targets */ + border: 1px solid #e8e9ff; + text-align: left; + vertical-align: middle; + font-size: 14px !important; + line-height: 1.4; +} + +.table th { + background-color: #f8f9fa; + font-weight: 600; + color: #2f3640 !important; + font-size: 15px !important; +} + +/* Button improvements for mobile */ +.btn { + font-size: 16px !important; + padding: 12px 20px !important; + border-radius: 8px; + min-height: 44px; /* Minimum touch target size */ + display: inline-flex; + align-items: center; + justify-content: center; + text-decoration: none; + border: none; + cursor: pointer; + transition: all 0.2s ease; +} + +.btn-sm { + font-size: 14px !important; + padding: 8px 16px !important; + min-height: 36px; +} + +.btn-xs { + font-size: 13px !important; + padding: 6px 12px !important; + min-height: 32px; +} + +/* Form elements */ +.form-control, input, textarea, select { + font-size: 16px !important; /* Prevents zoom on iOS */ + padding: 12px 16px !important; + border: 2px solid #e8e9ff; + border-radius: 8px; + min-height: 44px; + line-height: 1.4; + color: #2f3640 !important; + background-color: #ffffff; +} + +.form-control:focus, input:focus, textarea:focus, select:focus { + border-color: #5856d6; + box-shadow: 0 0 0 3px rgba(88, 86, 214, 0.1); + outline: none; +} + +/* Labels and form text */ +label, .control-label { + font-size: 16px !important; + font-weight: 600; + color: #2f3640 !important; + margin-bottom: 8px; + display: block; +} + +/* Headings with proper hierarchy */ +h1 { + font-size: 2.5rem !important; /* 40px */ + font-weight: 700; + color: #1e293b !important; + line-height: 1.2; + margin-bottom: 1rem; +} + +h2 { + font-size: 2rem !important; /* 32px */ + font-weight: 600; + color: #1e293b !important; + line-height: 1.3; + margin-bottom: 0.875rem; +} + +h3 { + font-size: 1.5rem !important; /* 24px */ + font-weight: 600; + color: #2f3640 !important; + line-height: 1.4; + margin-bottom: 0.75rem; +} + +h4 { + font-size: 1.25rem !important; /* 20px */ + font-weight: 600; + color: #2f3640 !important; + line-height: 1.4; + margin-bottom: 0.5rem; +} + +h5 { + font-size: 1.125rem !important; /* 18px */ + font-weight: 600; + color: #2f3640 !important; + line-height: 1.4; + margin-bottom: 0.5rem; +} + +h6 { + font-size: 1rem !important; /* 16px */ + font-weight: 600; + color: #2f3640 !important; + line-height: 1.4; + margin-bottom: 0.5rem; +} + +/* Paragraph and body text */ +p { + font-size: 16px !important; + line-height: 1.6; + color: #2f3640 !important; + margin-bottom: 1rem; +} + +/* Sidebar improvements */ +#page-sidebar { + font-size: 16px !important; +} + +#page-sidebar ul li a { + font-size: 16px !important; + padding: 12px 20px !important; + color: #2f3640 !important; + min-height: 44px; + display: flex; + align-items: center; + text-decoration: none; +} + +#page-sidebar ul li a:hover { + background-color: #f8f9fa; + color: #5856d6 !important; +} + +/* Content area improvements */ +.content-box, .panel, .card { + font-size: 16px !important; + color: #2f3640 !important; + background-color: #ffffff; + border: 1px solid #e8e9ff; + border-radius: 12px; + padding: 20px; + margin-bottom: 20px; +} + +/* Modal improvements */ +.modal-content { + font-size: 16px !important; + color: #2f3640 !important; +} + +.modal-title { + font-size: 1.5rem !important; + font-weight: 600; + color: #1e293b !important; +} + +/* Alert and notification improvements */ +.alert { + font-size: 16px !important; + padding: 16px 20px !important; + border-radius: 8px; + margin-bottom: 20px; +} + +.alert-success { + background-color: #f0fdf4; + border-color: #bbf7d0; + color: #166534 !important; +} + +.alert-danger { + background-color: #fef2f2; + border-color: #fecaca; + color: #dc2626 !important; +} + +.alert-warning { + background-color: #fffbeb; + border-color: #fed7aa; + color: #d97706 !important; +} + +.alert-info { + background-color: #eff6ff; + border-color: #bfdbfe; + color: #2563eb !important; +} + +/* Navigation improvements */ +.navbar-nav .nav-link { + font-size: 16px !important; + padding: 12px 16px !important; + color: #2f3640 !important; +} + +/* Breadcrumb improvements */ +.breadcrumb { + font-size: 16px !important; + background-color: transparent; + padding: 0; + margin-bottom: 20px; +} + +.breadcrumb-item { + color: #64748b !important; +} + +.breadcrumb-item.active { + color: #2f3640 !important; +} + +/* Mobile-first responsive breakpoints */ +@media (max-width: 1200px) { + .container, .container-fluid { + padding-left: 15px; + padding-right: 15px; + } + + .table-responsive { + border: none; + margin-bottom: 20px; + } +} + +@media (max-width: 992px) { + /* Stack columns on tablets */ + .col-md-3, .col-md-4, .col-md-6, .col-md-8, .col-md-9 { + flex: 0 0 100%; + max-width: 100%; + margin-bottom: 20px; + } + + /* Adjust sidebar for tablets */ + #page-sidebar { + width: 100%; + position: static; + height: auto; + } + + /* Make tables horizontally scrollable */ + .table-responsive { + overflow-x: auto; + } + + .table { + min-width: 600px; + } +} + +@media (max-width: 768px) { + /* Mobile-specific adjustments */ + html { + font-size: 14px; + } + + body { + font-size: 14px; + padding: 0; + } + + .container, .container-fluid { + padding-left: 10px; + padding-right: 10px; + } + + /* Stack all columns on mobile */ + .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-6, .col-sm-8, .col-sm-9, .col-sm-12, + .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-6, .col-md-8, .col-md-9, .col-md-12 { + flex: 0 0 100%; + max-width: 100%; + margin-bottom: 15px; + } + + /* Adjust headings for mobile */ + h1 { + font-size: 2rem !important; /* 32px */ + } + + h2 { + font-size: 1.75rem !important; /* 28px */ + } + + h3 { + font-size: 1.5rem !important; /* 24px */ + } + + h4 { + font-size: 1.25rem !important; /* 20px */ + } + + /* Button adjustments for mobile */ + .btn { + font-size: 16px !important; + padding: 14px 20px !important; + width: 100%; + margin-bottom: 10px; + } + + .btn-group .btn { + width: auto; + margin-bottom: 0; + } + + /* Form adjustments for mobile */ + .form-control, input, textarea, select { + font-size: 16px !important; /* Prevents zoom on iOS */ + padding: 14px 16px !important; + width: 100%; + } + + /* Table adjustments for mobile */ + .table { + font-size: 14px !important; + } + + .table th, .table td { + padding: 8px 6px !important; + font-size: 13px !important; + } + + /* Hide less important columns on mobile */ + .table .d-none-mobile { + display: none !important; + } + + /* Modal adjustments for mobile */ + .modal-dialog { + margin: 10px; + width: calc(100% - 20px); + } + + .modal-content { + padding: 20px 15px; + } + + /* Content box adjustments */ + .content-box, .panel, .card { + padding: 15px; + margin-bottom: 15px; + } + + /* Sidebar adjustments for mobile */ + #page-sidebar { + position: fixed; + top: 0; + left: -100%; + width: 280px; + height: 100vh; + z-index: 1000; + transition: left 0.3s ease; + background-color: #ffffff; + box-shadow: 2px 0 10px rgba(0,0,0,0.1); + } + + #page-sidebar.show { + left: 0; + } + + /* Main content adjustments when sidebar is open */ + #main-content { + transition: margin-left 0.3s ease; + } + + #main-content.sidebar-open { + margin-left: 280px; + } + + /* Mobile menu toggle */ + .mobile-menu-toggle { + display: block; + position: fixed; + top: 20px; + left: 20px; + z-index: 1001; + background-color: #5856d6; + color: white; + border: none; + padding: 12px; + border-radius: 8px; + font-size: 18px; + cursor: pointer; + } +} + +@media (max-width: 576px) { + /* Extra small devices */ + html { + font-size: 14px; + } + + .container, .container-fluid { + padding-left: 8px; + padding-right: 8px; + } + + /* Even smaller buttons and forms for very small screens */ + .btn { + font-size: 14px !important; + padding: 12px 16px !important; + } + + .form-control, input, textarea, select { + font-size: 16px !important; /* Still 16px to prevent zoom */ + padding: 12px 14px !important; + } + + /* Compact table for very small screens */ + .table th, .table td { + padding: 6px 4px !important; + font-size: 12px !important; + } + + /* Hide even more columns on very small screens */ + .table .d-none-mobile-sm { + display: none !important; + } +} + +/* Utility classes for mobile */ +.d-none-mobile { + display: block; +} + +.d-none-mobile-sm { + display: block; +} + +@media (max-width: 768px) { + .d-none-mobile { + display: none !important; + } +} + +@media (max-width: 576px) { + .d-none-mobile-sm { + display: none !important; + } +} + +/* Ensure all text has proper contrast */ +.text-white { + color: #ffffff !important; +} + +.text-dark { + color: #2f3640 !important; +} + +.text-muted { + color: #2f3640 !important; /* Dark text for better readability */ +} + +/* Fix any light text on light backgrounds */ +.bg-light .text-muted, +.bg-white .text-muted, +.panel .text-muted { + color: #2f3640 !important; /* Dark text for better readability */ +} + +/* Ensure proper spacing for touch targets */ +a, button, input, select, textarea { + min-height: 44px; + min-width: 44px; +} + +/* Additional text readability improvements */ +/* Fix any green text issues */ +.ng-binding { + color: #2f3640 !important; /* Normal dark text instead of green */ +} + +/* Ensure all text elements have proper contrast */ +span, div, p, label, td, th { + color: inherit; +} + +/* Fix specific text color issues */ +.text-success { + color: #059669 !important; /* Darker green for better readability */ +} + +.text-info { + color: #0284c7 !important; /* Darker blue for better readability */ +} + +.text-warning { + color: #d97706 !important; /* Darker orange for better readability */ +} + +/* Override Bootstrap's muted text */ +.text-muted { + color: #2f3640 !important; /* Dark text instead of grey */ +} + +/* Fix any remaining light text on light backgrounds */ +.bg-white .text-light, +.bg-light .text-light, +.panel .text-light, +.card .text-light { + color: #2f3640 !important; +} + +/* Fix for small clickable elements */ +.glyph-icon, .icon { + min-width: 44px; + min-height: 44px; + display: flex; + align-items: center; + justify-content: center; +} + +/* Loading and spinner improvements */ +.spinner, .loading { + font-size: 16px !important; + color: #5856d6 !important; +} + +/* Print styles */ +@media print { + body { + font-size: 12pt; + color: #000000 !important; + background: #ffffff !important; + } + + .table th, .table td { + font-size: 10pt !important; + color: #000000 !important; + } + + .btn, .alert, .modal { + display: none !important; + } +} diff --git a/static/baseTemplate/assets/readability-fixes.css b/static/baseTemplate/assets/readability-fixes.css new file mode 100644 index 000000000..1911482b7 --- /dev/null +++ b/static/baseTemplate/assets/readability-fixes.css @@ -0,0 +1,265 @@ +/* CyberPanel Readability & Design Fixes */ +/* This file fixes the core design issues with grey text and color inconsistencies */ + +/* Override CSS Variables for Better Text Contrast */ +:root { + /* Ensure all text uses proper dark colors for readability */ + --text-primary: #2f3640; + --text-secondary: #2f3640; /* Changed from grey to dark for better readability */ + --text-heading: #1e293b; +} + +/* Dark theme also uses proper contrast */ +[data-theme="dark"] { + --text-primary: #e4e4e7; + --text-secondary: #e4e4e7; /* Changed from grey to light for better readability */ + --text-heading: #f3f4f6; +} + +/* Fix Green Text Issues */ +/* Override Angular binding colors that might be green */ +.ng-binding { + color: var(--text-secondary) !important; +} + +/* Specific fix for uptime display */ +#sidebar .server-info .info-line span, +#sidebar .server-info .info-line .ng-binding, +.server-info .ng-binding { + color: var(--text-secondary) !important; +} + +/* Fix Grey Text on White Background */ +/* Override all muted and secondary text classes */ +.text-muted, +.text-secondary, +.text-light, +small, +.small, +.text-small { + color: var(--text-secondary) !important; +} + +/* Fix specific Bootstrap classes */ +.text-muted { + color: #2f3640 !important; /* Dark text for better readability */ +} + +/* Fix text on white/light backgrounds */ +.bg-white .text-muted, +.bg-light .text-muted, +.panel .text-muted, +.card .text-muted, +.content-box .text-muted { + color: #2f3640 !important; +} + +/* Fix menu items and navigation */ +#sidebar .menu-item, +#sidebar .menu-item span, +#sidebar .menu-item i, +.sidebar .menu-item, +.sidebar .menu-item span, +.sidebar .menu-item i { + color: var(--text-secondary) !important; +} + +#sidebar .menu-item:hover, +.sidebar .menu-item:hover { + color: var(--accent-color) !important; +} + +#sidebar .menu-item.active, +.sidebar .menu-item.active { + color: white !important; +} + +/* Fix server info and details */ +.server-info, +.server-info *, +.server-details, +.server-details *, +.info-line, +.info-line span, +.info-line strong, +.tagline, +.brand { + color: inherit !important; +} + +/* Fix form elements */ +label, +.control-label, +.form-label { + color: var(--text-primary) !important; + font-weight: 600; +} + +/* Fix table text */ +.table th, +.table td { + color: var(--text-primary) !important; +} + +.table th { + font-weight: 600; +} + +/* Fix alert text */ +.alert { + color: var(--text-primary) !important; +} + +.alert-success { + color: #059669 !important; /* Darker green for better readability */ +} + +.alert-info { + color: #0284c7 !important; /* Darker blue for better readability */ +} + +.alert-warning { + color: #d97706 !important; /* Darker orange for better readability */ +} + +.alert-danger { + color: #dc2626 !important; /* Darker red for better readability */ +} + +/* Fix breadcrumb text */ +.breadcrumb-item { + color: var(--text-secondary) !important; +} + +.breadcrumb-item.active { + color: var(--text-primary) !important; +} + +/* Fix modal text */ +.modal-content { + color: var(--text-primary) !important; +} + +.modal-title { + color: var(--text-heading) !important; +} + +/* Fix button text */ +.btn { + color: inherit; +} + +/* Fix any remaining light text issues */ +.bg-light .text-light, +.bg-white .text-light, +.panel .text-light, +.card .text-light { + color: #2f3640 !important; +} + +/* Ensure proper contrast for all text elements */ +span, div, p, label, td, th, a, li { + color: inherit; +} + +/* Fix specific color classes */ +.text-success { + color: #059669 !important; /* Darker green for better readability */ +} + +.text-info { + color: #0284c7 !important; /* Darker blue for better readability */ +} + +.text-warning { + color: #d97706 !important; /* Darker orange for better readability */ +} + +.text-danger { + color: #dc2626 !important; /* Darker red for better readability */ +} + +/* Fix any Angular-specific styling */ +[ng-controller] { + color: inherit; +} + +[ng-show], +[ng-hide], +[ng-if] { + color: inherit; +} + +/* Ensure all content areas have proper text color */ +.content-box, +.panel, +.card, +.main-content, +.page-content { + color: var(--text-primary) !important; +} + +/* Fix any remaining Bootstrap classes */ +.text-dark { + color: #2f3640 !important; +} + +.text-body { + color: var(--text-primary) !important; +} + +/* Mobile-specific fixes */ +@media (max-width: 768px) { + /* Ensure mobile text is also readable */ + body, + .container, + .container-fluid { + color: var(--text-primary) !important; + } + + /* Fix mobile menu text */ + .mobile-menu .menu-item, + .mobile-menu .menu-item span { + color: var(--text-secondary) !important; + } +} + +/* Print styles */ +@media print { + body, + .content-box, + .panel, + .card { + color: #000000 !important; + background: #ffffff !important; + } + + .text-muted, + .text-secondary { + color: #000000 !important; + } +} + +/* High contrast mode support */ +@media (prefers-contrast: high) { + :root { + --text-primary: #000000; + --text-secondary: #000000; + --text-heading: #000000; + } + + [data-theme="dark"] { + --text-primary: #ffffff; + --text-secondary: #ffffff; + --text-heading: #ffffff; + } +} + +/* Reduced motion support */ +@media (prefers-reduced-motion: reduce) { + * { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } +} diff --git a/static/baseTemplate/custom-js/system-status.js b/static/baseTemplate/custom-js/system-status.js index 1817b58ce..37720ec3e 100644 --- a/static/baseTemplate/custom-js/system-status.js +++ b/static/baseTemplate/custom-js/system-status.js @@ -10,7 +10,7 @@ function getCookie(name) { if (document.cookie && document.cookie !== '') { var cookies = document.cookie.split(';'); for (var i = 0; i < cookies.length; i++) { - var cookie = jQuery.trim(cookies[i]); + var cookie = (cookies[i] || '').replace(/^\s+|\s+$/g, ''); // Does this cookie string begin with the name we want? if (cookie.substring(0, name.length + 1) === (name + '=')) { cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); @@ -39,6 +39,77 @@ function randomPassword(length) { window.app = angular.module('CyberCP', []); var app = window.app; // Local reference for this file +// MUST be first: register dashboard controller before any other setup (avoids ctrlreg when CDN/Tracking Prevention blocks scripts) +app.controller('dashboardStatsController', ['$scope', '$http', '$timeout', function ($scope, $http, $timeout) { + $scope.cpuUsage = 0; $scope.ramUsage = 0; $scope.diskUsage = 0; $scope.cpuCores = 0; + $scope.ramTotalMB = 0; $scope.diskTotalGB = 0; $scope.diskFreeGB = 0; + $scope.totalUsers = 0; $scope.totalSites = 0; $scope.totalWPSites = 0; + $scope.totalDBs = 0; $scope.totalEmails = 0; $scope.totalFTPUsers = 0; + $scope.topProcesses = []; $scope.sshLogins = []; $scope.sshLogs = []; + $scope.loadingTopProcesses = true; $scope.loadingSSHLogins = true; $scope.loadingSSHLogs = true; + $scope.blockedIPs = {}; $scope.blockingIP = null; $scope.securityAlerts = []; + var opts = { headers: { 'X-CSRFToken': (typeof getCookie === 'function') ? getCookie('csrftoken') : '' } }; + try { + $http.get('/base/getSystemStatus', opts).then(function (r) { + if (r && r.data && r.data.status === 1) { + $scope.cpuUsage = r.data.cpuUsage || 0; $scope.ramUsage = r.data.ramUsage || 0; + $scope.diskUsage = r.data.diskUsage || 0; $scope.cpuCores = r.data.cpuCores || 0; + $scope.ramTotalMB = r.data.ramTotalMB || 0; $scope.diskTotalGB = r.data.diskTotalGB || 0; + $scope.diskFreeGB = r.data.diskFreeGB || 0; + } + }); + $http.get('/base/getDashboardStats', opts).then(function (r) { + if (r && r.data && r.data.status === 1) { + $scope.totalUsers = r.data.total_users || 0; $scope.totalSites = r.data.total_sites || 0; + $scope.totalWPSites = r.data.total_wp_sites || 0; $scope.totalDBs = r.data.total_dbs || 0; + $scope.totalEmails = r.data.total_emails || 0; $scope.totalFTPUsers = r.data.total_ftp_users || 0; + } + }); + $http.get('/base/getRecentSSHLogins', opts).then(function (r) { + $scope.loadingSSHLogins = false; + $scope.sshLogins = (r && r.data && r.data.logins) ? r.data.logins : []; + }, function () { $scope.loadingSSHLogins = false; $scope.sshLogins = []; }); + $http.get('/base/getRecentSSHLogs', opts).then(function (r) { + $scope.loadingSSHLogs = false; + $scope.sshLogs = (r && r.data && r.data.logs) ? r.data.logs : []; + }, function () { $scope.loadingSSHLogs = false; $scope.sshLogs = []; }); + $http.get('/base/getTopProcesses', opts).then(function (r) { + $scope.loadingTopProcesses = false; + $scope.topProcesses = (r && r.data && r.data.status === 1 && r.data.processes) ? r.data.processes : []; + }, function () { $scope.loadingTopProcesses = false; $scope.topProcesses = []; }); + if (typeof $timeout === 'function') { $timeout(function() { /* refresh */ }, 10000); } + } catch (e) { /* ignore */ } +}]); + +// Overview CPU/RAM/Disk cards use systemStatusInfo – register early so data loads even if later script fails +app.controller('systemStatusInfo', ['$scope', '$http', '$timeout', function ($scope, $http, $timeout) { + $scope.uptimeLoaded = false; + $scope.uptime = 'Loading...'; + $scope.cpuUsage = 0; $scope.ramUsage = 0; $scope.diskUsage = 0; + $scope.cpuCores = 0; $scope.ramTotalMB = 0; $scope.diskTotalGB = 0; $scope.diskFreeGB = 0; + $scope.getSystemStatus = function() { fetchStatus(); }; + function fetchStatus() { + try { + var csrf = (typeof getCookie === 'function') ? getCookie('csrftoken') : ''; + $http.get('/base/getSystemStatus', { headers: { 'X-CSRFToken': csrf } }).then(function (r) { + if (r && r.data && r.data.status === 1) { + $scope.cpuUsage = r.data.cpuUsage != null ? r.data.cpuUsage : 0; + $scope.ramUsage = r.data.ramUsage != null ? r.data.ramUsage : 0; + $scope.diskUsage = r.data.diskUsage != null ? r.data.diskUsage : 0; + $scope.cpuCores = r.data.cpuCores != null ? r.data.cpuCores : 0; + $scope.ramTotalMB = r.data.ramTotalMB != null ? r.data.ramTotalMB : 0; + $scope.diskTotalGB = r.data.diskTotalGB != null ? r.data.diskTotalGB : 0; + $scope.diskFreeGB = r.data.diskFreeGB != null ? r.data.diskFreeGB : 0; + $scope.uptime = r.data.uptime || 'N/A'; + } + $scope.uptimeLoaded = true; + }, function() { $scope.uptime = 'Unavailable'; $scope.uptimeLoaded = true; }); + if (typeof $timeout === 'function') { $timeout(fetchStatus, 60000); } + } catch (e) { $scope.uptimeLoaded = true; } + } + fetchStatus(); +}]); + var globalScope; function GlobalRespSuccess(response) { @@ -122,9 +193,17 @@ app.controller('systemStatusInfo', function ($scope, $http, $timeout) { $scope.uptimeLoaded = false; $scope.uptime = 'Loading...'; - + // Defaults so template never shows undefined (avoids raw {$ cpuUsage $} when API is slow or fails) + $scope.cpuUsage = 0; + $scope.ramUsage = 0; + $scope.diskUsage = 0; + $scope.cpuCores = 0; + $scope.ramTotalMB = 0; + $scope.diskTotalGB = 0; + $scope.diskFreeGB = 0; + getStuff(); - + $scope.getSystemStatus = function() { getStuff(); }; @@ -138,17 +217,15 @@ app.controller('systemStatusInfo', function ($scope, $http, $timeout) { function ListInitialData(response) { - $scope.cpuUsage = response.data.cpuUsage; - $scope.ramUsage = response.data.ramUsage; - $scope.diskUsage = response.data.diskUsage; - - // Total system information - $scope.cpuCores = response.data.cpuCores; - $scope.ramTotalMB = response.data.ramTotalMB; - $scope.diskTotalGB = response.data.diskTotalGB; - $scope.diskFreeGB = response.data.diskFreeGB; - - // Get uptime if available + $scope.cpuUsage = response.data.cpuUsage != null ? response.data.cpuUsage : 0; + $scope.ramUsage = response.data.ramUsage != null ? response.data.ramUsage : 0; + $scope.diskUsage = response.data.diskUsage != null ? response.data.diskUsage : 0; + + $scope.cpuCores = response.data.cpuCores != null ? response.data.cpuCores : 0; + $scope.ramTotalMB = response.data.ramTotalMB != null ? response.data.ramTotalMB : 0; + $scope.diskTotalGB = response.data.diskTotalGB != null ? response.data.diskTotalGB : 0; + $scope.diskFreeGB = response.data.diskFreeGB != null ? response.data.diskFreeGB : 0; + if (response.data.uptime) { $scope.uptime = response.data.uptime; $scope.uptimeLoaded = true; @@ -162,6 +239,9 @@ app.controller('systemStatusInfo', function ($scope, $http, $timeout) { function cantLoadInitialData(response) { $scope.uptime = 'Unavailable'; $scope.uptimeLoaded = true; + $scope.cpuUsage = 0; + $scope.ramUsage = 0; + $scope.diskUsage = 0; } $timeout(getStuff, 60000); // Update every minute @@ -273,11 +353,11 @@ app.controller('adminController', function ($scope, $http, $timeout) { } if (!Boolean(response.data.deleteZone)) { - $('.addDeleteRecords').hide(); + $('.deleteZone').hide(); } if (!Boolean(response.data.addDeleteRecords)) { - $('.deleteDatabase').hide(); + $('.addDeleteRecords').hide(); } // Email Management @@ -557,15 +637,18 @@ app.controller('homePageStatus', function ($scope, $http, $timeout) { //////////// function increment() { - $('.box').hide(); + var boxes = document.querySelectorAll ? document.querySelectorAll('.box') : []; + for (var i = 0; i < boxes.length; i++) boxes[i].style.display = 'none'; setTimeout(function () { - $('.box').show(); + for (var j = 0; j < boxes.length; j++) boxes[j].style.display = ''; }, 100); - - } -increment(); +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', increment); +} else { + increment(); +} //////////// @@ -579,6 +662,7 @@ app.controller('versionManagment', function ($scope, $http, $timeout) { $scope.updateFinish = true; $scope.couldNotConnect = true; + var upgradeStatusTimer = null; $scope.upgrade = function () { @@ -660,7 +744,8 @@ app.controller('versionManagment', function ($scope, $http, $timeout) { if (response.data.upgradeStatus === 1) { if (response.data.finished === 1) { - $timeout.cancel(); + if (upgradeStatusTimer) $timeout.cancel(upgradeStatusTimer); + upgradeStatusTimer = null; $scope.upgradelogBox = false; $scope.upgradeLog = response.data.upgradeLog; $scope.upgradeLoading = true; @@ -672,7 +757,7 @@ app.controller('versionManagment', function ($scope, $http, $timeout) { } else { $scope.upgradelogBox = false; $scope.upgradeLog = response.data.upgradeLog; - timeout(getUpgradeStatus, 2000); + upgradeStatusTimer = $timeout(getUpgradeStatus, 2000); } } @@ -900,7 +985,8 @@ app.controller('OnboardingCP', function ($scope, $http, $timeout, $window) { }); -app.controller('dashboardStatsController', function ($scope, $http, $timeout) { +// Single implementation registered under both names for compatibility (some templates/caches use newDashboardStat) +var dashboardStatsControllerFn = function ($scope, $http, $timeout) { console.log('dashboardStatsController initialized'); // Card values @@ -914,139 +1000,17 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { // Hide system charts for non-admin users $scope.hideSystemCharts = false; - // Pagination settings - 10 entries per page - var ITEMS_PER_PAGE = 10; - - // Pagination state for each section - $scope.pagination = { - sshLogins: { currentPage: 1, itemsPerPage: ITEMS_PER_PAGE }, - sshLogs: { currentPage: 1, itemsPerPage: ITEMS_PER_PAGE }, - topProcesses: { currentPage: 1, itemsPerPage: ITEMS_PER_PAGE }, - traffic: { currentPage: 1, itemsPerPage: ITEMS_PER_PAGE }, - diskIO: { currentPage: 1, itemsPerPage: ITEMS_PER_PAGE }, - cpuUsage: { currentPage: 1, itemsPerPage: ITEMS_PER_PAGE } - }; - - // Input fields for "go to page" - $scope.gotoPageInput = { - sshLogins: 1, - sshLogs: 1, - topProcesses: 1, - traffic: 1, - diskIO: 1, - cpuUsage: 1 - }; - - // Expose Math to template - $scope.Math = Math; - - // Pagination helper functions - $scope.getTotalPages = function(section) { - var items = []; - if (section === 'sshLogins') items = $scope.sshLogins || []; - else if (section === 'sshLogs') items = $scope.sshLogs || []; - else if (section === 'topProcesses') items = $scope.topProcesses || []; - else if (section === 'traffic') items = $scope.trafficLabels || []; - else if (section === 'diskIO') items = $scope.diskLabels || []; - else if (section === 'cpuUsage') items = $scope.cpuLabels || []; - return Math.max(1, Math.ceil((items.length || 0) / ITEMS_PER_PAGE)); - }; - - $scope.getPaginatedItems = function(section) { - // Initialize pagination if it doesn't exist - if (!$scope.pagination) { - $scope.pagination = {}; - } - if (!$scope.pagination[section]) { - $scope.pagination[section] = { currentPage: 1, itemsPerPage: ITEMS_PER_PAGE }; - console.log('[getPaginatedItems] Initialized pagination for section:', section); - } - - var items = []; - if (section === 'sshLogins') items = $scope.sshLogins || []; - else if (section === 'sshLogs') items = $scope.sshLogs || []; - else if (section === 'topProcesses') items = $scope.topProcesses || []; - else if (section === 'traffic') items = $scope.trafficLabels || []; - else if (section === 'diskIO') items = $scope.diskLabels || []; - else if (section === 'cpuUsage') items = $scope.cpuLabels || []; - - // Ensure currentPage is a valid number - var currentPage = parseInt($scope.pagination[section].currentPage) || 1; - if (currentPage < 1 || isNaN(currentPage)) currentPage = 1; - - var start = (currentPage - 1) * ITEMS_PER_PAGE; - var end = start + ITEMS_PER_PAGE; - - var result = items.slice(start, end); - console.log('[getPaginatedItems] Section:', section, 'Total items:', items.length, 'Page:', currentPage, 'Start:', start, 'End:', end, 'Paginated count:', result.length); - - if (result.length > 0) { - console.log('[getPaginatedItems] First item:', result[0]); - } else if (items.length > 0) { - console.warn('[getPaginatedItems] No items returned but total items > 0. Items:', items.length, 'Page:', currentPage, 'Start:', start, 'End:', end); - } - - return result; - }; - - $scope.goToPage = function(section, page) { - var totalPages = $scope.getTotalPages(section); - if (page >= 1 && page <= totalPages) { - $scope.pagination[section].currentPage = parseInt(page); - $scope.gotoPageInput[section] = parseInt(page); - } - }; - - $scope.nextPage = function(section) { - var totalPages = $scope.getTotalPages(section); - if ($scope.pagination[section].currentPage < totalPages) { - $scope.pagination[section].currentPage++; - $scope.gotoPageInput[section] = $scope.pagination[section].currentPage; - } - }; - - $scope.prevPage = function(section) { - if ($scope.pagination[section].currentPage > 1) { - $scope.pagination[section].currentPage--; - $scope.gotoPageInput[section] = $scope.pagination[section].currentPage; - } - }; - - $scope.getPageNumbers = function(section) { - var totalPages = $scope.getTotalPages(section); - var current = $scope.pagination[section].currentPage; - var pages = []; - var maxVisible = 5; // Show max 5 page numbers - - if (totalPages <= maxVisible) { - for (var i = 1; i <= totalPages; i++) { - pages.push(i); - } - } else { - if (current <= 3) { - for (var i = 1; i <= 5; i++) pages.push(i); - } else if (current >= totalPages - 2) { - for (var i = totalPages - 4; i <= totalPages; i++) pages.push(i); - } else { - for (var i = current - 2; i <= current + 2; i++) pages.push(i); - } - } - return pages; - }; - // Top Processes $scope.topProcesses = []; $scope.loadingTopProcesses = true; $scope.errorTopProcesses = ''; $scope.refreshTopProcesses = function() { $scope.loadingTopProcesses = true; - $http.get('/base/getTopProcesses').then(function (response) { + var h = { headers: { 'X-CSRFToken': (typeof getCookie === 'function') ? getCookie('csrftoken') : '' } }; + $http.get('/base/getTopProcesses', h).then(function (response) { $scope.loadingTopProcesses = false; if (response.data && response.data.status === 1 && response.data.processes) { $scope.topProcesses = response.data.processes; - // Reset to first page when data refreshes - $scope.pagination.topProcesses.currentPage = 1; - $scope.gotoPageInput.topProcesses = 1; } else { $scope.topProcesses = []; } @@ -1062,38 +1026,19 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { $scope.errorSSHLogins = ''; $scope.refreshSSHLogins = function() { $scope.loadingSSHLogins = true; - $http.get('/base/getRecentSSHLogins').then(function (response) { + var h = { headers: { 'X-CSRFToken': (typeof getCookie === 'function') ? getCookie('csrftoken') : '' } }; + $http.get('/base/getRecentSSHLogins', h).then(function (response) { $scope.loadingSSHLogins = false; if (response.data && response.data.logins) { $scope.sshLogins = response.data.logins; - console.log('[refreshSSHLogins] Loaded', $scope.sshLogins.length, 'SSH logins'); - // Ensure pagination is initialized - if (!$scope.pagination) { - $scope.pagination = {}; - } - if (!$scope.pagination.sshLogins) { - $scope.pagination.sshLogins = { currentPage: 1, itemsPerPage: ITEMS_PER_PAGE }; - } - // Reset to first page when data refreshes - $scope.pagination.sshLogins.currentPage = 1; - if (!$scope.gotoPageInput) { - $scope.gotoPageInput = {}; - } - $scope.gotoPageInput.sshLogins = 1; - - // Debug: Log paginated items - var paginated = $scope.getPaginatedItems('sshLogins'); - console.log('[refreshSSHLogins] Paginated items count:', paginated.length, 'Items:', paginated); - // Debug: Log first login to see structure if ($scope.sshLogins.length > 0) { - console.log('[refreshSSHLogins] First SSH login object:', $scope.sshLogins[0]); - console.log('[refreshSSHLogins] IP field:', $scope.sshLogins[0].ip); - console.log('[refreshSSHLogins] All keys:', Object.keys($scope.sshLogins[0])); + console.log('First SSH login object:', $scope.sshLogins[0]); + console.log('IP field:', $scope.sshLogins[0].ip); + console.log('All keys:', Object.keys($scope.sshLogins[0])); } } else { $scope.sshLogins = []; - console.log('[refreshSSHLogins] No logins found in response'); } }, function (err) { $scope.loadingSSHLogins = false; @@ -1110,13 +1055,11 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { $scope.loadingSecurityAnalysis = false; $scope.refreshSSHLogs = function() { $scope.loadingSSHLogs = true; - $http.get('/base/getRecentSSHLogs').then(function (response) { + var h = { headers: { 'X-CSRFToken': (typeof getCookie === 'function') ? getCookie('csrftoken') : '' } }; + $http.get('/base/getRecentSSHLogs', h).then(function (response) { $scope.loadingSSHLogs = false; if (response.data && response.data.logs) { $scope.sshLogs = response.data.logs; - // Reset to first page when data refreshes - $scope.pagination.sshLogs.currentPage = 1; - $scope.gotoPageInput.sshLogs = 1; // Analyze logs for security issues $scope.analyzeSSHSecurity(); } else { @@ -1157,8 +1100,73 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { }; $scope.blockIPAddress = function(ipAddress) { - if (!$scope.blockingIP) { - $scope.blockingIP = ipAddress; + try { + console.log('========================================'); + console.log('=== blockIPAddress CALLED ==='); + console.log('========================================'); + console.log('blockIPAddress called with:', ipAddress); + console.log('ipAddress type:', typeof ipAddress); + console.log('ipAddress value:', ipAddress); + console.log('$scope:', $scope); + console.log('$scope.blockIPAddress:', typeof $scope.blockIPAddress); + + // Validate IP address parameter + if (!ipAddress) { + console.error('No IP address provided:', ipAddress); + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Error', + text: 'No IP address provided', + type: 'error', + delay: 5000 + }); + } + return; + } + + // Ensure it's a string and trim it + ipAddress = String(ipAddress).trim(); + + // Validate after trimming + if (!ipAddress || ipAddress === '' || ipAddress === 'undefined' || ipAddress === 'null') { + console.error('IP address is empty or invalid after trim:', ipAddress); + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Error', + text: 'Invalid IP address provided: ' + ipAddress, + type: 'error', + delay: 5000 + }); + } + return; + } + + // Basic IP format validation + var ipPattern = /^(\d{1,3}\.){3}\d{1,3}(\/\d{1,2})?$/; + if (!ipPattern.test(ipAddress)) { + console.error('IP address format is invalid:', ipAddress); + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Error', + text: 'Invalid IP address format: ' + ipAddress, + type: 'error', + delay: 5000 + }); + } + return; + } + + // Prevent duplicate requests + if ($scope.blockingIP === ipAddress) { + console.log('Already processing IP:', ipAddress); + return; // Already processing this IP + } + + // Do not early-return when IP is already in blockedIPs: still call the API so the + // backend can close any active connections from this IP (already-banned path). + + // Set blocking flag to prevent duplicate requests + $scope.blockingIP = ipAddress; // Use the new Banned IPs system instead of the old blockIPAddress var data = { @@ -1173,48 +1181,334 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { } }; + console.log('Sending ban IP request:', data); + console.log('CSRF Token:', getCookie('csrftoken')); + console.log('Config:', config); + $http.post('/firewall/addBannedIP', data, config).then(function (response) { + console.log('=== addBannedIP SUCCESS ==='); + console.log('Full response:', response); + console.log('response.data:', response.data); + console.log('response.data type:', typeof response.data); + console.log('response.status:', response.status); + + // Reset blocking flag $scope.blockingIP = null; - if (response.data && response.data.status === 1) { + + // Apply scope changes + if (!$scope.$$phase && !$scope.$root.$$phase) { + $scope.$apply(); + } + + // Handle both JSON string and object responses + var responseData = response.data; + if (typeof responseData === 'string') { + try { + responseData = JSON.parse(responseData); + console.log('Parsed responseData from string:', responseData); + } catch (e) { + console.error('Failed to parse response as JSON:', e); + 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; + } + } + + console.log('Final responseData:', responseData); + console.log('responseData.status:', responseData ? responseData.status : 'undefined'); + console.log('responseData.message:', responseData ? responseData.message : 'undefined'); + console.log('responseData.error_message:', responseData ? responseData.error_message : 'undefined'); + + // Check for success (status === 1 or status === '1') + if (responseData && (responseData.status === 1 || responseData.status === '1')) { // Mark IP as blocked + if (!$scope.blockedIPs) { + $scope.blockedIPs = {}; + } $scope.blockedIPs[ipAddress] = true; - // Show success notification - new PNotify({ - title: 'IP Address Banned', - text: `IP address ${ipAddress} has been permanently banned and added to the firewall. You can manage it in the Firewall > Banned IPs section.`, - type: 'success', - delay: 5000 - }); + // Show success notification (use server message when present, e.g. already-banned + connections closed) + if (typeof PNotify !== 'undefined') { + var successText = (responseData.message && responseData.message.length) ? responseData.message : `IP address ${ipAddress} has been permanently banned and added to the firewall. You can manage it in the Firewall > Banned IPs section.`; + new PNotify({ + title: 'IP Address Banned', + text: successText, + type: 'success', + delay: 5000 + }); + } // Refresh security analysis to update alerts - $scope.analyzeSSHSecurity(); + if ($scope.analyzeSSHSecurity) { + $scope.analyzeSSHSecurity(); + } + + // Apply scope changes + if (!$scope.$$phase && !$scope.$root.$$phase) { + $scope.$apply(); + } } else { // Show error notification + var errorMsg = 'Failed to block IP address'; + if (responseData && responseData.error_message) { + errorMsg = responseData.error_message; + } else if (responseData && responseData.error) { + errorMsg = responseData.error; + } else if (responseData && responseData.message) { + errorMsg = responseData.message; + } else if (responseData) { + errorMsg = JSON.stringify(responseData); + } + console.error('Ban IP failed:', errorMsg); + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Error', + text: errorMsg, + type: 'error', + delay: 5000 + }); + } + } + }, function (err) { + $scope.blockingIP = null; + console.error('addBannedIP error:', err); + console.error('Error status:', err.status); + console.error('Error statusText:', err.statusText); + console.error('Error data:', err.data); + + // Prevent showing duplicate error notifications + if ($scope.lastErrorIP === ipAddress && $scope.lastErrorTime && (Date.now() - $scope.lastErrorTime) < 2000) { + console.log('Skipping duplicate error notification for IP:', ipAddress); + return; + } + + $scope.lastErrorIP = ipAddress; + $scope.lastErrorTime = Date.now(); + + var errorMessage = 'Failed to block IP address'; + 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') { + errorMessage = errData.error_message || errData.error || errData.message || errorMessage; + } else if (err.status) { + errorMessage = 'HTTP ' + err.status + ': ' + (errorMessage); + } + + console.error('Final error message:', errorMessage); + + if (typeof PNotify !== 'undefined') { new PNotify({ title: 'Error', - text: response.data && response.data.error ? response.data.error : 'Failed to block IP address', + text: errorMessage, type: 'error', delay: 5000 }); } - }, function (err) { - $scope.blockingIP = null; - var errorMessage = 'Failed to block IP address'; - if (err.data && err.data.error) { - errorMessage = err.data.error; - } else if (err.data && err.data.message) { - errorMessage = err.data.message; + }); + } catch (e) { + console.error('========================================'); + console.error('=== ERROR in blockIPAddress ==='); + console.error('========================================'); + console.error('Error:', e); + console.error('Error message:', e.message); + console.error('Error stack:', e.stack); + $scope.blockingIP = null; + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Error', + text: 'An error occurred while trying to ban the IP address: ' + (e.message || String(e)), + type: 'error', + delay: 5000 + }); + } + } + }; + + // Ban IP from SSH Logs + $scope.banIPFromSSHLog = function(ipAddress) { + if (!ipAddress) { + new PNotify({ + title: 'Error', + text: 'No IP address provided', + type: 'error', + delay: 5000 + }); + return; + } + + if ($scope.blockingIP === ipAddress) { + return; // Already processing + } + + // Still call API when already in blockedIPs so backend can close active connections + if (!$scope.blockedIPs) { + $scope.blockedIPs = {}; + } + + $scope.blockingIP = ipAddress; + + // Use the Banned IPs system + var data = { + ip: ipAddress, + reason: 'Suspicious activity detected from SSH logs', + duration: 'permanent' + }; + + var config = { + headers: { + 'X-CSRFToken': getCookie('csrftoken') + } + }; + + $http.post('/firewall/addBannedIP', data, config).then(function (response) { + $scope.blockingIP = null; + if (response.data && response.data.status === 1) { + // Mark IP as blocked + $scope.blockedIPs[ipAddress] = true; + + // Show success notification + new PNotify({ + title: 'IP Address Banned', + text: `IP address ${ipAddress} has been permanently banned and added to the firewall. You can manage it in the Firewall > Banned IPs section.`, + type: 'success', + delay: 5000 + }); + + // Refresh SSH logs to update the UI + $scope.refreshSSHLogs(); + } else { + // Show error notification + var errorMsg = 'Failed to ban IP address'; + if (response.data && response.data.error_message) { + errorMsg = response.data.error_message; + } else if (response.data && response.data.error) { + errorMsg = response.data.error; } new PNotify({ title: 'Error', - text: errorMessage, + text: errorMsg, type: 'error', delay: 5000 }); + } + }, function (err) { + $scope.blockingIP = null; + var errorMessage = 'Failed to ban IP address'; + if (err.data && err.data.error_message) { + errorMessage = err.data.error_message; + } else if (err.data && err.data.error) { + errorMessage = err.data.error; + } else if (err.data && err.data.message) { + errorMessage = err.data.message; + } + + new PNotify({ + title: 'Error', + text: errorMessage, + type: 'error', + delay: 5000 }); + }); + }; + + // Ban IP from SSH Logs + $scope.banIPFromSSHLog = function(ipAddress) { + if (!ipAddress) { + new PNotify({ + title: 'Error', + text: 'No IP address provided', + type: 'error', + delay: 5000 + }); + return; } + + if ($scope.blockingIP === ipAddress) { + return; // Already processing + } + + // Still call API when already in blockedIPs so backend can close active connections + if (!$scope.blockedIPs) { + $scope.blockedIPs = {}; + } + + $scope.blockingIP = ipAddress; + + // Use the Banned IPs system + var data = { + ip: ipAddress, + reason: 'Suspicious activity detected from SSH logs', + duration: 'permanent' + }; + + var config = { + headers: { + 'X-CSRFToken': getCookie('csrftoken') + } + }; + + $http.post('/firewall/addBannedIP', data, config).then(function (response) { + $scope.blockingIP = null; + if (response.data && response.data.status === 1) { + // Mark IP as blocked + $scope.blockedIPs[ipAddress] = true; + + // Show success notification + new PNotify({ + title: 'IP Address Banned', + text: `IP address ${ipAddress} has been permanently banned and added to the firewall. You can manage it in the Firewall > Banned IPs section.`, + type: 'success', + delay: 5000 + }); + + // Refresh SSH logs to update the UI + $scope.refreshSSHLogs(); + } else { + // Show error notification + var errorMsg = 'Failed to ban IP address'; + if (response.data && response.data.error_message) { + errorMsg = response.data.error_message; + } else if (response.data && response.data.error) { + errorMsg = response.data.error; + } + + new PNotify({ + title: 'Error', + text: errorMsg, + type: 'error', + delay: 5000 + }); + } + }, function (err) { + $scope.blockingIP = null; + var errorMessage = 'Failed to ban IP address'; + if (err.data && err.data.error_message) { + errorMessage = err.data.error_message; + } else if (err.data && err.data.error) { + errorMessage = err.data.error; + } else if (err.data && err.data.message) { + errorMessage = err.data.message; + } + + new PNotify({ + title: 'Error', + text: errorMessage, + type: 'error', + delay: 5000 + }); + }); }; // Initial fetch @@ -1224,72 +1518,21 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { // Chart.js chart objects var trafficChart, diskIOChart, cpuChart; - // Data arrays for live graphs - expose to scope for pagination - $scope.trafficLabels = []; - $scope.rxData = []; - $scope.txData = []; - $scope.diskLabels = []; - $scope.readData = []; - $scope.writeData = []; - $scope.cpuLabels = []; - $scope.cpuUsageData = []; - // Internal references for backward compatibility - var trafficLabels = $scope.trafficLabels; - var rxData = $scope.rxData; - var txData = $scope.txData; - var diskLabels = $scope.diskLabels; - var readData = $scope.readData; - var writeData = $scope.writeData; - var cpuLabels = $scope.cpuLabels; - var cpuUsageData = $scope.cpuUsageData; + // Data arrays for live graphs + var trafficLabels = [], rxData = [], txData = []; + var diskLabels = [], readData = [], writeData = []; + var cpuLabels = [], cpuUsageData = []; // For rate calculation var lastRx = null, lastTx = null, lastDiskRead = null, lastDiskWrite = null, lastCPU = null; var lastCPUTimes = null; var pollInterval = 2000; // ms var maxPoints = 30; - - // Watch pagination changes and update charts accordingly - $scope.$watch('pagination.traffic.currentPage', function() { - updateTrafficChartData(); - }); - $scope.$watch('pagination.diskIO.currentPage', function() { - updateDiskIOChartData(); - }); - $scope.$watch('pagination.cpuUsage.currentPage', function() { - updateCPUChartData(); - }); - - function updateTrafficChartData() { - if (!trafficChart || !$scope.trafficLabels || $scope.trafficLabels.length === 0) return; - var startIdx = ($scope.pagination.traffic.currentPage - 1) * ITEMS_PER_PAGE; - var endIdx = startIdx + ITEMS_PER_PAGE; - - trafficChart.data.labels = $scope.trafficLabels.slice(startIdx, endIdx); - trafficChart.data.datasets[0].data = $scope.rxData.slice(startIdx, endIdx); - trafficChart.data.datasets[1].data = $scope.txData.slice(startIdx, endIdx); - trafficChart.update(); - } - - function updateDiskIOChartData() { - if (!diskIOChart || !$scope.diskLabels || $scope.diskLabels.length === 0) return; - var startIdx = ($scope.pagination.diskIO.currentPage - 1) * ITEMS_PER_PAGE; - var endIdx = startIdx + ITEMS_PER_PAGE; - - diskIOChart.data.labels = $scope.diskLabels.slice(startIdx, endIdx); - diskIOChart.data.datasets[0].data = $scope.readData.slice(startIdx, endIdx); - diskIOChart.data.datasets[1].data = $scope.writeData.slice(startIdx, endIdx); - diskIOChart.update(); - } - - function updateCPUChartData() { - if (!cpuChart || !$scope.cpuLabels || $scope.cpuLabels.length === 0) return; - var startIdx = ($scope.pagination.cpuUsage.currentPage - 1) * ITEMS_PER_PAGE; - var endIdx = startIdx + ITEMS_PER_PAGE; - - cpuChart.data.labels = $scope.cpuLabels.slice(startIdx, endIdx); - cpuChart.data.datasets[0].data = $scope.cpuUsageData.slice(startIdx, endIdx); - cpuChart.update(); - } + + // Expose so switchTab can create charts on first tab click if they weren't created at load + window.cyberPanelSetupChartsIfNeeded = function() { + if (window.trafficChart && window.diskIOChart && window.cpuChart) return; + try { setupCharts(); } catch (e) { console.error('cyberPanelSetupChartsIfNeeded:', e); } + }; function pollDashboardStats() { console.log('[dashboardStatsController] pollDashboardStats() called'); @@ -1349,8 +1592,8 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { } function pollTraffic() { - console.log('pollTraffic called'); $http.get('/base/getTrafficStats').then(function(response) { + if (!response || !response.data) return; if (response.data.admin_only) { // Hide chart for non-admin users $scope.hideSystemCharts = true; @@ -1398,13 +1641,16 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { } lastRx = rx; lastTx = tx; } else { - console.log('pollTraffic error or no data:', response); + console.warn('pollTraffic: no data or status', response.data); } + }).catch(function(err) { + console.warn('pollTraffic failed:', err); }); } function pollDiskIO() { $http.get('/base/getDiskIOStats').then(function(response) { + if (!response || !response.data) return; if (response.data.admin_only) { // Hide chart for non-admin users $scope.hideSystemCharts = true; @@ -1443,11 +1689,14 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { } lastDiskRead = read; lastDiskWrite = write; } + }).catch(function(err) { + console.warn('pollDiskIO failed:', err); }); } function pollCPU() { $http.get('/base/getCPULoadGraph').then(function(response) { + if (!response || !response.data) return; if (response.data.admin_only) { // Hide chart for non-admin users $scope.hideSystemCharts = true; @@ -1486,13 +1735,34 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { } lastCPUTimes = cpuTimes; } + }).catch(function(err) { + console.warn('pollCPU failed:', err); }); } - function setupCharts() { - console.log('setupCharts called, initializing charts...'); - var trafficCtx = document.getElementById('trafficChart').getContext('2d'); - trafficChart = new Chart(trafficCtx, { + function setupCharts(retryCount) { + retryCount = retryCount || 0; + if (typeof Chart === 'undefined') { + if (retryCount < 3) { + $timeout(function() { setupCharts(retryCount + 1); }, 400); + } + return; + } + var trafficEl = document.getElementById('trafficChart'); + if (!trafficEl) { + if (retryCount < 5) { + $timeout(function() { setupCharts(retryCount + 1); }, 300); + } + return; + } + try { + var trafficCtx = trafficEl.getContext('2d'); + } catch (e) { + console.error('trafficChart getContext failed:', e); + return; + } + try { + trafficChart = new Chart(trafficCtx, { type: 'line', data: { labels: [], @@ -1584,7 +1854,9 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { console.log('trafficChart resized and updated after setup.'); } }, 500); - var diskCtx = document.getElementById('diskIOChart').getContext('2d'); + var diskEl = document.getElementById('diskIOChart'); + if (!diskEl) { console.warn('diskIOChart canvas not found'); return; } + var diskCtx = diskEl.getContext('2d'); diskIOChart = new Chart(diskCtx, { type: 'line', data: { @@ -1669,7 +1941,10 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { layout: { padding: { top: 10, bottom: 10, left: 10, right: 10 } } } }); - var cpuCtx = document.getElementById('cpuChart').getContext('2d'); + window.diskIOChart = diskIOChart; + var cpuEl = document.getElementById('cpuChart'); + if (!cpuEl) { console.warn('cpuChart canvas not found'); return; } + var cpuCtx = cpuEl.getContext('2d'); cpuChart = new Chart(cpuCtx, { type: 'line', data: { @@ -1742,6 +2017,10 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { layout: { padding: { top: 10, bottom: 10, left: 10, right: 10 } } } }); + window.cpuChart = cpuChart; + } catch (e) { + console.error('setupCharts error:', e); + } // Redraw charts on tab shown $("a[data-toggle='tab']").on('shown.bs.tab', function (e) { @@ -1774,19 +2053,20 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { $scope.refreshSSHLogs(); $timeout(function() { - // Check if user is admin before setting up charts + // Always create charts so Traffic/Disk IO/CPU tabs have something to show; admin check only affects hideSystemCharts + setupCharts(); $http.get('/base/getAdminStatus').then(function(response) { - if (response.data && response.data.admin === 1) { - setupCharts(); + if (response.data && (response.data.admin === 1 || response.data.admin === true)) { + $scope.hideSystemCharts = false; } else { $scope.hideSystemCharts = true; } - }).catch(function() { - // If error, assume non-admin and hide charts + }).catch(function(err) { + console.warn('getAdminStatus failed:', err); $scope.hideSystemCharts = true; }); - // Start polling for all stats + // Start polling for all stats (data feeds charts) function pollAll() { pollDashboardStats(); pollTraffic(); @@ -1796,7 +2076,7 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { $timeout(pollAll, pollInterval); } pollAll(); - }, 500); + }, 800); // SSH User Activity Modal $scope.showSSHActivityModal = false; @@ -2242,4 +2522,6 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { $scope.closeSSHActivityModal(); } }; -}); \ No newline at end of file +}; +app.controller('dashboardStatsController', dashboardStatsControllerFn); +app.controller('newDashboardStat', dashboardStatsControllerFn); \ No newline at end of file diff --git a/static/baseTemplate/vendor/select2/select2.full.min.js b/static/baseTemplate/vendor/select2/select2.full.min.js new file mode 100644 index 000000000..683301daf --- /dev/null +++ b/static/baseTemplate/vendor/select2/select2.full.min.js @@ -0,0 +1,2 @@ +/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ +!function(n){"function"==typeof define&&define.amd?define(["jquery"],n):"object"==typeof module&&module.exports?module.exports=function(e,t){return void 0===t&&(t="undefined"!=typeof window?require("jquery"):require("jquery")(e)),n(t),t}:n(jQuery)}(function(d){var e=function(){if(d&&d.fn&&d.fn.select2&&d.fn.select2.amd)var e=d.fn.select2.amd;var t,n,i,h,o,s,f,g,m,v,y,_,r,a,w,l;function b(e,t){return r.call(e,t)}function c(e,t){var n,i,r,o,s,a,l,c,u,d,p,h=t&&t.split("/"),f=y.map,g=f&&f["*"]||{};if(e){for(s=(e=e.split("/")).length-1,y.nodeIdCompat&&w.test(e[s])&&(e[s]=e[s].replace(w,"")),"."===e[0].charAt(0)&&h&&(e=h.slice(0,h.length-1).concat(e)),u=0;u":">",'"':""","'":"'","/":"/"};return"string"!=typeof e?e:String(e).replace(/[&<>"'\/\\]/g,function(e){return t[e]})},r.appendMany=function(e,t){if("1.7"===o.fn.jquery.substr(0,3)){var n=o();o.map(t,function(e){n=n.add(e)}),t=n}e.append(t)},r.__cache={};var n=0;return r.GetUniqueElementId=function(e){var t=e.getAttribute("data-select2-id");return null==t&&(e.id?(t=e.id,e.setAttribute("data-select2-id",t)):(e.setAttribute("data-select2-id",++n),t=n.toString())),t},r.StoreData=function(e,t,n){var i=r.GetUniqueElementId(e);r.__cache[i]||(r.__cache[i]={}),r.__cache[i][t]=n},r.GetData=function(e,t){var n=r.GetUniqueElementId(e);return t?r.__cache[n]&&null!=r.__cache[n][t]?r.__cache[n][t]:o(e).data(t):r.__cache[n]},r.RemoveData=function(e){var t=r.GetUniqueElementId(e);null!=r.__cache[t]&&delete r.__cache[t],e.removeAttribute("data-select2-id")},r}),e.define("select2/results",["jquery","./utils"],function(h,f){function i(e,t,n){this.$element=e,this.data=n,this.options=t,i.__super__.constructor.call(this)}return f.Extend(i,f.Observable),i.prototype.render=function(){var e=h('
        ');return this.options.get("multiple")&&e.attr("aria-multiselectable","true"),this.$results=e},i.prototype.clear=function(){this.$results.empty()},i.prototype.displayMessage=function(e){var t=this.options.get("escapeMarkup");this.clear(),this.hideLoading();var n=h(''),i=this.options.get("translations").get(e.message);n.append(t(i(e.args))),n[0].className+=" select2-results__message",this.$results.append(n)},i.prototype.hideMessages=function(){this.$results.find(".select2-results__message").remove()},i.prototype.append=function(e){this.hideLoading();var t=[];if(null!=e.results&&0!==e.results.length){e.results=this.sort(e.results);for(var n=0;n",{class:"select2-results__options select2-results__options--nested"});p.append(l),s.append(a),s.append(p)}else this.template(e,t);return f.StoreData(t,"data",e),t},i.prototype.bind=function(t,e){var l=this,n=t.id+"-results";this.$results.attr("id",n),t.on("results:all",function(e){l.clear(),l.append(e.data),t.isOpen()&&(l.setClasses(),l.highlightFirstItem())}),t.on("results:append",function(e){l.append(e.data),t.isOpen()&&l.setClasses()}),t.on("query",function(e){l.hideMessages(),l.showLoading(e)}),t.on("select",function(){t.isOpen()&&(l.setClasses(),l.options.get("scrollAfterSelect")&&l.highlightFirstItem())}),t.on("unselect",function(){t.isOpen()&&(l.setClasses(),l.options.get("scrollAfterSelect")&&l.highlightFirstItem())}),t.on("open",function(){l.$results.attr("aria-expanded","true"),l.$results.attr("aria-hidden","false"),l.setClasses(),l.ensureHighlightVisible()}),t.on("close",function(){l.$results.attr("aria-expanded","false"),l.$results.attr("aria-hidden","true"),l.$results.removeAttr("aria-activedescendant")}),t.on("results:toggle",function(){var e=l.getHighlightedResults();0!==e.length&&e.trigger("mouseup")}),t.on("results:select",function(){var e=l.getHighlightedResults();if(0!==e.length){var t=f.GetData(e[0],"data");"true"==e.attr("aria-selected")?l.trigger("close",{}):l.trigger("select",{data:t})}}),t.on("results:previous",function(){var e=l.getHighlightedResults(),t=l.$results.find("[aria-selected]"),n=t.index(e);if(!(n<=0)){var i=n-1;0===e.length&&(i=0);var r=t.eq(i);r.trigger("mouseenter");var o=l.$results.offset().top,s=r.offset().top,a=l.$results.scrollTop()+(s-o);0===i?l.$results.scrollTop(0):s-o<0&&l.$results.scrollTop(a)}}),t.on("results:next",function(){var e=l.getHighlightedResults(),t=l.$results.find("[aria-selected]"),n=t.index(e)+1;if(!(n>=t.length)){var i=t.eq(n);i.trigger("mouseenter");var r=l.$results.offset().top+l.$results.outerHeight(!1),o=i.offset().top+i.outerHeight(!1),s=l.$results.scrollTop()+o-r;0===n?l.$results.scrollTop(0):rthis.$results.outerHeight()||o<0)&&this.$results.scrollTop(r)}},i.prototype.template=function(e,t){var n=this.options.get("templateResult"),i=this.options.get("escapeMarkup"),r=n(e,t);null==r?t.style.display="none":"string"==typeof r?t.innerHTML=i(r):h(t).append(r)},i}),e.define("select2/keys",[],function(){return{BACKSPACE:8,TAB:9,ENTER:13,SHIFT:16,CTRL:17,ALT:18,ESC:27,SPACE:32,PAGE_UP:33,PAGE_DOWN:34,END:35,HOME:36,LEFT:37,UP:38,RIGHT:39,DOWN:40,DELETE:46}}),e.define("select2/selection/base",["jquery","../utils","../keys"],function(n,i,r){function o(e,t){this.$element=e,this.options=t,o.__super__.constructor.call(this)}return i.Extend(o,i.Observable),o.prototype.render=function(){var e=n('');return this._tabindex=0,null!=i.GetData(this.$element[0],"old-tabindex")?this._tabindex=i.GetData(this.$element[0],"old-tabindex"):null!=this.$element.attr("tabindex")&&(this._tabindex=this.$element.attr("tabindex")),e.attr("title",this.$element.attr("title")),e.attr("tabindex",this._tabindex),e.attr("aria-disabled","false"),this.$selection=e},o.prototype.bind=function(e,t){var n=this,i=e.id+"-results";this.container=e,this.$selection.on("focus",function(e){n.trigger("focus",e)}),this.$selection.on("blur",function(e){n._handleBlur(e)}),this.$selection.on("keydown",function(e){n.trigger("keypress",e),e.which===r.SPACE&&e.preventDefault()}),e.on("results:focus",function(e){n.$selection.attr("aria-activedescendant",e.data._resultId)}),e.on("selection:update",function(e){n.update(e.data)}),e.on("open",function(){n.$selection.attr("aria-expanded","true"),n.$selection.attr("aria-owns",i),n._attachCloseHandler(e)}),e.on("close",function(){n.$selection.attr("aria-expanded","false"),n.$selection.removeAttr("aria-activedescendant"),n.$selection.removeAttr("aria-owns"),n.$selection.trigger("focus"),n._detachCloseHandler(e)}),e.on("enable",function(){n.$selection.attr("tabindex",n._tabindex),n.$selection.attr("aria-disabled","false")}),e.on("disable",function(){n.$selection.attr("tabindex","-1"),n.$selection.attr("aria-disabled","true")})},o.prototype._handleBlur=function(e){var t=this;window.setTimeout(function(){document.activeElement==t.$selection[0]||n.contains(t.$selection[0],document.activeElement)||t.trigger("blur",e)},1)},o.prototype._attachCloseHandler=function(e){n(document.body).on("mousedown.select2."+e.id,function(e){var t=n(e.target).closest(".select2");n(".select2.select2-container--open").each(function(){this!=t[0]&&i.GetData(this,"element").select2("close")})})},o.prototype._detachCloseHandler=function(e){n(document.body).off("mousedown.select2."+e.id)},o.prototype.position=function(e,t){t.find(".selection").append(e)},o.prototype.destroy=function(){this._detachCloseHandler(this.container)},o.prototype.update=function(e){throw new Error("The `update` method must be defined in child classes.")},o.prototype.isEnabled=function(){return!this.isDisabled()},o.prototype.isDisabled=function(){return this.options.get("disabled")},o}),e.define("select2/selection/single",["jquery","./base","../utils","../keys"],function(e,t,n,i){function r(){r.__super__.constructor.apply(this,arguments)}return n.Extend(r,t),r.prototype.render=function(){var e=r.__super__.render.call(this);return e.addClass("select2-selection--single"),e.html(''),e},r.prototype.bind=function(t,e){var n=this;r.__super__.bind.apply(this,arguments);var i=t.id+"-container";this.$selection.find(".select2-selection__rendered").attr("id",i).attr("role","textbox").attr("aria-readonly","true"),this.$selection.attr("aria-labelledby",i),this.$selection.on("mousedown",function(e){1===e.which&&n.trigger("toggle",{originalEvent:e})}),this.$selection.on("focus",function(e){}),this.$selection.on("blur",function(e){}),t.on("focus",function(e){t.isOpen()||n.$selection.trigger("focus")})},r.prototype.clear=function(){var e=this.$selection.find(".select2-selection__rendered");e.empty(),e.removeAttr("title")},r.prototype.display=function(e,t){var n=this.options.get("templateSelection");return this.options.get("escapeMarkup")(n(e,t))},r.prototype.selectionContainer=function(){return e("")},r.prototype.update=function(e){if(0!==e.length){var t=e[0],n=this.$selection.find(".select2-selection__rendered"),i=this.display(t,n);n.empty().append(i);var r=t.title||t.text;r?n.attr("title",r):n.removeAttr("title")}else this.clear()},r}),e.define("select2/selection/multiple",["jquery","./base","../utils"],function(r,e,l){function n(e,t){n.__super__.constructor.apply(this,arguments)}return l.Extend(n,e),n.prototype.render=function(){var e=n.__super__.render.call(this);return e.addClass("select2-selection--multiple"),e.html('
          '),e},n.prototype.bind=function(e,t){var i=this;n.__super__.bind.apply(this,arguments),this.$selection.on("click",function(e){i.trigger("toggle",{originalEvent:e})}),this.$selection.on("click",".select2-selection__choice__remove",function(e){if(!i.isDisabled()){var t=r(this).parent(),n=l.GetData(t[0],"data");i.trigger("unselect",{originalEvent:e,data:n})}})},n.prototype.clear=function(){var e=this.$selection.find(".select2-selection__rendered");e.empty(),e.removeAttr("title")},n.prototype.display=function(e,t){var n=this.options.get("templateSelection");return this.options.get("escapeMarkup")(n(e,t))},n.prototype.selectionContainer=function(){return r('
        • ×
        • ')},n.prototype.update=function(e){if(this.clear(),0!==e.length){for(var t=[],n=0;n×');a.StoreData(i[0],"data",t),this.$selection.find(".select2-selection__rendered").prepend(i)}},e}),e.define("select2/selection/search",["jquery","../utils","../keys"],function(i,a,l){function e(e,t,n){e.call(this,t,n)}return e.prototype.render=function(e){var t=i('');this.$searchContainer=t,this.$search=t.find("input");var n=e.call(this);return this._transferTabIndex(),n},e.prototype.bind=function(e,t,n){var i=this,r=t.id+"-results";e.call(this,t,n),t.on("open",function(){i.$search.attr("aria-controls",r),i.$search.trigger("focus")}),t.on("close",function(){i.$search.val(""),i.$search.removeAttr("aria-controls"),i.$search.removeAttr("aria-activedescendant"),i.$search.trigger("focus")}),t.on("enable",function(){i.$search.prop("disabled",!1),i._transferTabIndex()}),t.on("disable",function(){i.$search.prop("disabled",!0)}),t.on("focus",function(e){i.$search.trigger("focus")}),t.on("results:focus",function(e){e.data._resultId?i.$search.attr("aria-activedescendant",e.data._resultId):i.$search.removeAttr("aria-activedescendant")}),this.$selection.on("focusin",".select2-search--inline",function(e){i.trigger("focus",e)}),this.$selection.on("focusout",".select2-search--inline",function(e){i._handleBlur(e)}),this.$selection.on("keydown",".select2-search--inline",function(e){if(e.stopPropagation(),i.trigger("keypress",e),i._keyUpPrevented=e.isDefaultPrevented(),e.which===l.BACKSPACE&&""===i.$search.val()){var t=i.$searchContainer.prev(".select2-selection__choice");if(0this.maximumInputLength?this.trigger("results:message",{message:"inputTooLong",args:{maximum:this.maximumInputLength,input:t.term,params:t}}):e.call(this,t,n)},e}),e.define("select2/data/maximumSelectionLength",[],function(){function e(e,t,n){this.maximumSelectionLength=n.get("maximumSelectionLength"),e.call(this,t,n)}return e.prototype.bind=function(e,t,n){var i=this;e.call(this,t,n),t.on("select",function(){i._checkIfMaximumSelected()})},e.prototype.query=function(e,t,n){var i=this;this._checkIfMaximumSelected(function(){e.call(i,t,n)})},e.prototype._checkIfMaximumSelected=function(e,n){var i=this;this.current(function(e){var t=null!=e?e.length:0;0=i.maximumSelectionLength?i.trigger("results:message",{message:"maximumSelected",args:{maximum:i.maximumSelectionLength}}):n&&n()})},e}),e.define("select2/dropdown",["jquery","./utils"],function(t,e){function n(e,t){this.$element=e,this.options=t,n.__super__.constructor.call(this)}return e.Extend(n,e.Observable),n.prototype.render=function(){var e=t('');return e.attr("dir",this.options.get("dir")),this.$dropdown=e},n.prototype.bind=function(){},n.prototype.position=function(e,t){},n.prototype.destroy=function(){this.$dropdown.remove()},n}),e.define("select2/dropdown/search",["jquery","../utils"],function(o,e){function t(){}return t.prototype.render=function(e){var t=e.call(this),n=o('');return this.$searchContainer=n,this.$search=n.find("input"),t.prepend(n),t},t.prototype.bind=function(e,t,n){var i=this,r=t.id+"-results";e.call(this,t,n),this.$search.on("keydown",function(e){i.trigger("keypress",e),i._keyUpPrevented=e.isDefaultPrevented()}),this.$search.on("input",function(e){o(this).off("keyup")}),this.$search.on("keyup input",function(e){i.handleSearch(e)}),t.on("open",function(){i.$search.attr("tabindex",0),i.$search.attr("aria-controls",r),i.$search.trigger("focus"),window.setTimeout(function(){i.$search.trigger("focus")},0)}),t.on("close",function(){i.$search.attr("tabindex",-1),i.$search.removeAttr("aria-controls"),i.$search.removeAttr("aria-activedescendant"),i.$search.val(""),i.$search.trigger("blur")}),t.on("focus",function(){t.isOpen()||i.$search.trigger("focus")}),t.on("results:all",function(e){null!=e.query.term&&""!==e.query.term||(i.showSearch(e)?i.$searchContainer.removeClass("select2-search--hide"):i.$searchContainer.addClass("select2-search--hide"))}),t.on("results:focus",function(e){e.data._resultId?i.$search.attr("aria-activedescendant",e.data._resultId):i.$search.removeAttr("aria-activedescendant")})},t.prototype.handleSearch=function(e){if(!this._keyUpPrevented){var t=this.$search.val();this.trigger("query",{term:t})}this._keyUpPrevented=!1},t.prototype.showSearch=function(e,t){return!0},t}),e.define("select2/dropdown/hidePlaceholder",[],function(){function e(e,t,n,i){this.placeholder=this.normalizePlaceholder(n.get("placeholder")),e.call(this,t,n,i)}return e.prototype.append=function(e,t){t.results=this.removePlaceholder(t.results),e.call(this,t)},e.prototype.normalizePlaceholder=function(e,t){return"string"==typeof t&&(t={id:"",text:t}),t},e.prototype.removePlaceholder=function(e,t){for(var n=t.slice(0),i=t.length-1;0<=i;i--){var r=t[i];this.placeholder.id===r.id&&n.splice(i,1)}return n},e}),e.define("select2/dropdown/infiniteScroll",["jquery"],function(n){function e(e,t,n,i){this.lastParams={},e.call(this,t,n,i),this.$loadingMore=this.createLoadingMore(),this.loading=!1}return e.prototype.append=function(e,t){this.$loadingMore.remove(),this.loading=!1,e.call(this,t),this.showLoadingMore(t)&&(this.$results.append(this.$loadingMore),this.loadMoreIfNeeded())},e.prototype.bind=function(e,t,n){var i=this;e.call(this,t,n),t.on("query",function(e){i.lastParams=e,i.loading=!0}),t.on("query:append",function(e){i.lastParams=e,i.loading=!0}),this.$results.on("scroll",this.loadMoreIfNeeded.bind(this))},e.prototype.loadMoreIfNeeded=function(){var e=n.contains(document.documentElement,this.$loadingMore[0]);if(!this.loading&&e){var t=this.$results.offset().top+this.$results.outerHeight(!1);this.$loadingMore.offset().top+this.$loadingMore.outerHeight(!1)<=t+50&&this.loadMore()}},e.prototype.loadMore=function(){this.loading=!0;var e=n.extend({},{page:1},this.lastParams);e.page++,this.trigger("query:append",e)},e.prototype.showLoadingMore=function(e,t){return t.pagination&&t.pagination.more},e.prototype.createLoadingMore=function(){var e=n('
        • '),t=this.options.get("translations").get("loadingMore");return e.html(t(this.lastParams)),e},e}),e.define("select2/dropdown/attachBody",["jquery","../utils"],function(f,a){function e(e,t,n){this.$dropdownParent=f(n.get("dropdownParent")||document.body),e.call(this,t,n)}return e.prototype.bind=function(e,t,n){var i=this;e.call(this,t,n),t.on("open",function(){i._showDropdown(),i._attachPositioningHandler(t),i._bindContainerResultHandlers(t)}),t.on("close",function(){i._hideDropdown(),i._detachPositioningHandler(t)}),this.$dropdownContainer.on("mousedown",function(e){e.stopPropagation()})},e.prototype.destroy=function(e){e.call(this),this.$dropdownContainer.remove()},e.prototype.position=function(e,t,n){t.attr("class",n.attr("class")),t.removeClass("select2"),t.addClass("select2-container--open"),t.css({position:"absolute",top:-999999}),this.$container=n},e.prototype.render=function(e){var t=f(""),n=e.call(this);return t.append(n),this.$dropdownContainer=t},e.prototype._hideDropdown=function(e){this.$dropdownContainer.detach()},e.prototype._bindContainerResultHandlers=function(e,t){if(!this._containerResultsHandlersBound){var n=this;t.on("results:all",function(){n._positionDropdown(),n._resizeDropdown()}),t.on("results:append",function(){n._positionDropdown(),n._resizeDropdown()}),t.on("results:message",function(){n._positionDropdown(),n._resizeDropdown()}),t.on("select",function(){n._positionDropdown(),n._resizeDropdown()}),t.on("unselect",function(){n._positionDropdown(),n._resizeDropdown()}),this._containerResultsHandlersBound=!0}},e.prototype._attachPositioningHandler=function(e,t){var n=this,i="scroll.select2."+t.id,r="resize.select2."+t.id,o="orientationchange.select2."+t.id,s=this.$container.parents().filter(a.hasScroll);s.each(function(){a.StoreData(this,"select2-scroll-position",{x:f(this).scrollLeft(),y:f(this).scrollTop()})}),s.on(i,function(e){var t=a.GetData(this,"select2-scroll-position");f(this).scrollTop(t.y)}),f(window).on(i+" "+r+" "+o,function(e){n._positionDropdown(),n._resizeDropdown()})},e.prototype._detachPositioningHandler=function(e,t){var n="scroll.select2."+t.id,i="resize.select2."+t.id,r="orientationchange.select2."+t.id;this.$container.parents().filter(a.hasScroll).off(n),f(window).off(n+" "+i+" "+r)},e.prototype._positionDropdown=function(){var e=f(window),t=this.$dropdown.hasClass("select2-dropdown--above"),n=this.$dropdown.hasClass("select2-dropdown--below"),i=null,r=this.$container.offset();r.bottom=r.top+this.$container.outerHeight(!1);var o={height:this.$container.outerHeight(!1)};o.top=r.top,o.bottom=r.top+o.height;var s=this.$dropdown.outerHeight(!1),a=e.scrollTop(),l=e.scrollTop()+e.height(),c=ar.bottom+s,d={left:r.left,top:o.bottom},p=this.$dropdownParent;"static"===p.css("position")&&(p=p.offsetParent());var h={top:0,left:0};(f.contains(document.body,p[0])||p[0].isConnected)&&(h=p.offset()),d.top-=h.top,d.left-=h.left,t||n||(i="below"),u||!c||t?!c&&u&&t&&(i="below"):i="above",("above"==i||t&&"below"!==i)&&(d.top=o.top-h.top-s),null!=i&&(this.$dropdown.removeClass("select2-dropdown--below select2-dropdown--above").addClass("select2-dropdown--"+i),this.$container.removeClass("select2-container--below select2-container--above").addClass("select2-container--"+i)),this.$dropdownContainer.css(d)},e.prototype._resizeDropdown=function(){var e={width:this.$container.outerWidth(!1)+"px"};this.options.get("dropdownAutoWidth")&&(e.minWidth=e.width,e.position="relative",e.width="auto"),this.$dropdown.css(e)},e.prototype._showDropdown=function(e){this.$dropdownContainer.appendTo(this.$dropdownParent),this._positionDropdown(),this._resizeDropdown()},e}),e.define("select2/dropdown/minimumResultsForSearch",[],function(){function e(e,t,n,i){this.minimumResultsForSearch=n.get("minimumResultsForSearch"),this.minimumResultsForSearch<0&&(this.minimumResultsForSearch=1/0),e.call(this,t,n,i)}return e.prototype.showSearch=function(e,t){return!(function e(t){for(var n=0,i=0;i');return e.attr("dir",this.options.get("dir")),this.$container=e,this.$container.addClass("select2-container--"+this.options.get("theme")),u.StoreData(e[0],"element",this.$element),e},d}),e.define("select2/compat/utils",["jquery"],function(s){return{syncCssClasses:function(e,t,n){var i,r,o=[];(i=s.trim(e.attr("class")))&&s((i=""+i).split(/\s+/)).each(function(){0===this.indexOf("select2-")&&o.push(this)}),(i=s.trim(t.attr("class")))&&s((i=""+i).split(/\s+/)).each(function(){0!==this.indexOf("select2-")&&null!=(r=n(this))&&o.push(r)}),e.attr("class",o.join(" "))}}}),e.define("select2/compat/containerCss",["jquery","./utils"],function(s,a){function l(e){return null}function e(){}return e.prototype.render=function(e){var t=e.call(this),n=this.options.get("containerCssClass")||"";s.isFunction(n)&&(n=n(this.$element));var i=this.options.get("adaptContainerCssClass");if(i=i||l,-1!==n.indexOf(":all:")){n=n.replace(":all:","");var r=i;i=function(e){var t=r(e);return null!=t?t+" "+e:e}}var o=this.options.get("containerCss")||{};return s.isFunction(o)&&(o=o(this.$element)),a.syncCssClasses(t,this.$element,i),t.css(o),t.addClass(n),t},e}),e.define("select2/compat/dropdownCss",["jquery","./utils"],function(s,a){function l(e){return null}function e(){}return e.prototype.render=function(e){var t=e.call(this),n=this.options.get("dropdownCssClass")||"";s.isFunction(n)&&(n=n(this.$element));var i=this.options.get("adaptDropdownCssClass");if(i=i||l,-1!==n.indexOf(":all:")){n=n.replace(":all:","");var r=i;i=function(e){var t=r(e);return null!=t?t+" "+e:e}}var o=this.options.get("dropdownCss")||{};return s.isFunction(o)&&(o=o(this.$element)),a.syncCssClasses(t,this.$element,i),t.css(o),t.addClass(n),t},e}),e.define("select2/compat/initSelection",["jquery"],function(i){function e(e,t,n){n.get("debug")&&window.console&&console.warn&&console.warn("Select2: The `initSelection` option has been deprecated in favor of a custom data adapter that overrides the `current` method. This method is now called multiple times instead of a single time when the instance is initialized. Support will be removed for the `initSelection` option in future versions of Select2"),this.initSelection=n.get("initSelection"),this._isInitialized=!1,e.call(this,t,n)}return e.prototype.current=function(e,t){var n=this;this._isInitialized?e.call(this,t):this.initSelection.call(null,this.$element,function(e){n._isInitialized=!0,i.isArray(e)||(e=[e]),t(e)})},e}),e.define("select2/compat/inputData",["jquery","../utils"],function(s,i){function e(e,t,n){this._currentData=[],this._valueSeparator=n.get("valueSeparator")||",","hidden"===t.prop("type")&&n.get("debug")&&console&&console.warn&&console.warn("Select2: Using a hidden input with Select2 is no longer supported and may stop working in the future. It is recommended to use a ` - - -
          - - -
          - - - -
          -
          -
          {{ settings.test_count|default:0 }}
          -
          {% trans "Test Clicks" %}
          -
          - -
          -
          - {% if plugin_enabled %} - - {% else %} - - {% endif %} -
          -
          {% trans "Plugin Status" %}
          -
          - -
          -
          {{ recent_logs|length }}
          -
          {% trans "Recent Activities" %}
          -
          -
          - - -
          -

          - - {% trans "Recent Activity" %} -

          - -
          - {% for log in recent_logs %} -
          -
          - {% if 'click' in log.action %} - - {% elif 'toggle' in log.action %} - - {% elif 'settings' in log.action %} - - {% else %} - - {% endif %} -
          -
          -
          {{ log.action|title }}
          -
          {{ log.message }}
          -
          -
          {{ log.timestamp|date:"M d, H:i" }}
          -
          - {% empty %} -
          - -

          {% trans "No recent activity" %}

          -
          - {% endfor %} -
          -
          - - - - - - - -{% endblock %} diff --git a/testPlugin/templates/testPlugin/plugin_logs.html b/testPlugin/templates/testPlugin/plugin_logs.html deleted file mode 100644 index 88771663a..000000000 --- a/testPlugin/templates/testPlugin/plugin_logs.html +++ /dev/null @@ -1,291 +0,0 @@ -{% extends "baseTemplate/index.html" %} -{% load i18n %} -{% load static %} - -{% block title %}{% trans "Test Plugin Logs - CyberPanel" %}{% endblock %} - -{% block header_scripts %} - -{% endblock %} - -{% block content %} -
          -
          - -
          -

          - - {% trans "Test Plugin Logs" %} -

          -

          {% trans "View detailed activity logs for the test plugin" %}

          -
          - - -
          -
          - - - - - {% trans "Back to Plugin" %} - - - - - {% trans "Documentation" %} - - - - - {% trans "Security Info" %} - -
          - - {% if logs %} - - - - - - - - - - {% for log in logs %} - - - - - - {% endfor %} - -
          {% trans "Action" %}{% trans "Message" %}{% trans "Timestamp" %}
          - - {% if 'click' in log.action %} - - {% elif 'toggle' in log.action %} - - {% elif 'settings' in log.action %} - - {% elif 'visit' in log.action %} - - {% else %} - - {% endif %} - - {{ log.action|title|replace:"_":" " }} - {{ log.message }}{{ log.timestamp|date:"M d, Y H:i:s" }}
          - {% else %} -
          - -

          {% trans "No Logs Found" %}

          -

          {% trans "No activity logs available yet. Start using the plugin to see logs here." %}

          -
          - {% endif %} -
          -
          -
          - - -{% endblock %} diff --git a/testPlugin/templates/testPlugin/plugin_settings.html b/testPlugin/templates/testPlugin/plugin_settings.html deleted file mode 100644 index 6368cb739..000000000 --- a/testPlugin/templates/testPlugin/plugin_settings.html +++ /dev/null @@ -1,264 +0,0 @@ -{% extends "baseTemplate/index.html" %} -{% load i18n %} -{% load static %} - -{% block title %}{% trans "Test Plugin Settings - CyberPanel" %}{% endblock %} - -{% block header_scripts %} - -{% endblock %} - -{% block content %} -
          -
          - -
          -

          - - {% trans "Test Plugin Settings" %} -

          -

          {% trans "Configure your test plugin settings and preferences" %}

          -
          - - -
          -
          - {% csrf_token %} - -
          - - - - {% trans "This message will be displayed when you click the test button" %} - -
          - -
          - -
          - - {% if settings.plugin_enabled %} - {% trans "Enabled" %} - {% else %} - {% trans "Disabled" %} - {% endif %} - -

          - {% trans "Use the toggle switch on the main page to enable/disable the plugin" %} -

          -
          -
          - -
          - -
          -
          -
          {{ settings.test_count }}
          -
          {% trans "Total Tests" %}
          -
          -
          -
          {{ settings.last_test_time|date:"M d" }}
          -
          {% trans "Last Test" %}
          -
          -
          -
          - - -
          -
          -
          -
          - - -{% endblock %} diff --git a/testPlugin/templates/testPlugin/security_info.html b/testPlugin/templates/testPlugin/security_info.html deleted file mode 100644 index 66c765c66..000000000 --- a/testPlugin/templates/testPlugin/security_info.html +++ /dev/null @@ -1,499 +0,0 @@ -{% extends "baseTemplate/index.html" %} -{% load i18n %} -{% load static %} - -{% block title %}{% trans "Security Information - CyberPanel" %}{% endblock %} - -{% block header_scripts %} - -{% endblock %} - -{% block content %} -
          -
          - -
          -

          - - {% trans "Security Information" %} -

          -

          {% trans "Comprehensive security measures implemented in the Test Plugin" %}

          -
          - - -
          -
          -
          15+
          -
          {% trans "Security Features" %}
          -
          -
          -
          99%
          -
          {% trans "Attack Prevention" %}
          -
          -
          -
          24/7
          -
          {% trans "Monitoring" %}
          -
          -
          -
          0
          -
          {% trans "Known Vulnerabilities" %}
          -
          -
          - - -
          - - - {% trans "Back to Plugin" %} - - -

          {% trans "Security Features Implemented" %}

          - -
          -

          -
          - -
          - {% trans "Authentication & Authorization" %} -

          -

          {% trans "Multi-layered authentication and authorization system" %}

          -
            -
          • -
            - {% trans "Admin-only access required for all plugin functions" %} -
          • -
          • -
            - {% trans "User session validation on every request" %} -
          • -
          • -
            - {% trans "Privilege escalation protection" %} -
          • -
          -
          - -
          -

          -
          - -
          - {% trans "Rate Limiting & Brute Force Protection" %} -

          -

          {% trans "Advanced rate limiting to prevent brute force attacks" %}

          -
            -
          • -
            - {% trans "50 requests per 5-minute window per user" %} -
          • -
          • -
            - {% trans "10 test button clicks per minute limit" %} -
          • -
          • -
            - {% trans "Automatic lockout after 5 failed attempts" %} -
          • -
          • -
            - {% trans "15-minute lockout duration" %} -
          • -
          -
          - -
          -

          -
          - -
          - {% trans "CSRF Protection" %} -

          -

          {% trans "Cross-Site Request Forgery protection on all POST requests" %}

          -
            -
          • -
            - {% trans "HMAC-based CSRF token validation" %} -
          • -
          • -
            - {% trans "Token expiration after 1 hour" %} -
          • -
          • -
            - {% trans "User-specific token generation" %} -
          • -
          -
          - -
          -

          -
          - -
          - {% trans "Input Validation & Sanitization" %} -

          -

          {% trans "Comprehensive input validation and sanitization" %}

          -
            -
          • -
            - {% trans "Regex-based input validation" %} -
          • -
          • -
            - {% trans "XSS attack prevention" %} -
          • -
          • -
            - {% trans "SQL injection prevention" %} -
          • -
          • -
            - {% trans "Path traversal protection" %} -
          • -
          • -
            - {% trans "Maximum input length limits" %} -
          • -
          -
          - -
          -

          -
          - -
          - {% trans "Security Monitoring & Logging" %} -

          -

          {% trans "Comprehensive security event monitoring and logging" %}

          -
            -
          • -
            - {% trans "All security events logged with IP and user agent" %} -
          • -
          • -
            - {% trans "Failed attempt tracking and alerting" %} -
          • -
          • -
            - {% trans "Suspicious activity detection" %} -
          • -
          • -
            - {% trans "Real-time security event monitoring" %} -
          • -
          -
          - -
          -

          -
          - -
          - {% trans "HTTP Security Headers" %} -

          -

          {% trans "Comprehensive HTTP security headers for additional protection" %}

          -
            -
          • -
            - {% trans "X-Frame-Options: DENY (clickjacking protection)" %} -
          • -
          • -
            - {% trans "X-Content-Type-Options: nosniff" %} -
          • -
          • -
            - {% trans "X-XSS-Protection: 1; mode=block" %} -
          • -
          • -
            - {% trans "Content-Security-Policy (CSP)" %} -
          • -
          • -
            - {% trans "Strict-Transport-Security (HSTS)" %} -
          • -
          • -
            - {% trans "Referrer-Policy: strict-origin-when-cross-origin" %} -
          • -
          -
          - -
          -

          -
          - -
          - {% trans "Data Isolation & Privacy" %} -

          -

          {% trans "User data isolation and privacy protection" %}

          -
            -
          • -
            - {% trans "User-specific data isolation" %} -
          • -
          • -
            - {% trans "Logs restricted to user's own activities" %} -
          • -
          • -
            - {% trans "Settings isolated per user" %} -
          • -
          • -
            - {% trans "No cross-user data access" %} -
          • -
          -
          - -
          -

          -
          - -
          - {% trans "Security Recommendations" %} -

          -

          {% trans "Additional security measures you should implement" %}

          -
            -
          • -
            - {% trans "Keep CyberPanel and all plugins updated" %} -
          • -
          • -
            - {% trans "Use strong, unique passwords" %} -
          • -
          • -
            - {% trans "Enable 2FA on your CyberPanel account" %} -
          • -
          • -
            - {% trans "Regularly review security logs" %} -
          • -
          • -
            - {% trans "Use HTTPS in production environments" %} -
          • -
          -
          - -
          -

          -
          - -
          - {% trans "Security Vulnerability Reporting" %} -

          -

          {% trans "If you discover a security vulnerability, please report it responsibly" %}

          -
            -
          • -
            - {% trans "Email: security@cyberpanel.net" %} -
          • -
          • -
            - {% trans "GitHub: Create a private security issue" %} -
          • -
          • -
            - {% trans "Response time: Within 24-48 hours" %} -
          • -
          -
          - -

          {% trans "Security Audit Results" %}

          -

          {% trans "This plugin has been designed with security as a top priority. All major security vulnerabilities have been addressed:" %}

          - -
            -
          • -
            - {% trans "OWASP Top 10 vulnerabilities addressed" %} -
          • -
          • -
            - {% trans "No SQL injection vulnerabilities" %} -
          • -
          • -
            - {% trans "No XSS vulnerabilities" %} -
          • -
          • -
            - {% trans "No CSRF vulnerabilities" %} -
          • -
          • -
            - {% trans "No authentication bypass vulnerabilities" %} -
          • -
          • -
            - {% trans "No authorization bypass vulnerabilities" %} -
          • -
          • -
            - {% trans "No information disclosure vulnerabilities" %} -
          • -
          • -
            - {% trans "No path traversal vulnerabilities" %} -
          • -
          - -
          - {% trans "Security Note:" %} {% trans "This plugin implements enterprise-grade security measures. However, security is an ongoing process. Regular updates and monitoring are essential to maintain the highest security standards." %} -
          -
          -
          -
          -{% endblock %} diff --git a/testPlugin/templates/testPlugin/settings.html b/testPlugin/templates/testPlugin/settings.html deleted file mode 100644 index e06f901f4..000000000 --- a/testPlugin/templates/testPlugin/settings.html +++ /dev/null @@ -1,165 +0,0 @@ -{% extends "baseTemplate/index.html" %} -{% load static %} -{% load i18n %} - -{% block title %} - Test Plugin Settings - {% trans "CyberPanel" %} -{% endblock %} - -{% block content %} -
          -
          -
          -
          -
          -

          - - {% trans "Test Plugin Settings" %} -

          -
          -
          -
          - - {% trans "Plugin Information" %} -
            -
          • {% trans "Name" %}: {{ plugin_name }}
          • -
          • {% trans "Version" %}: {{ version }}
          • -
          • {% trans "Status" %}: {% trans "Active" %}
          • -
          -
          - -
          -
          -

          - - {% trans "Configuration Options" %} -

          -
          -
          -
          - {% csrf_token %} - -
          - -
          - - -
          - - {% trans "This is a test setting for demonstration purposes." %} - -
          - -
          - - - - {% trans "This is a test text input field." %} - -
          - -
          - - - - {% trans "Select a test option from the dropdown." %} - -
          - -
          - - -
          -
          -
          -
          - -
          -
          -

          - - {% trans "Plugin Status" %} -

          -
          -
          -
          - - {% trans "Plugin is Active" %} -

          {% trans "The Test Plugin is installed and working correctly." %}

          -
          - -
          -
          -
          - - - -
          - {% trans "Plugin Name" %} - {{ plugin_name }} -
          -
          -
          -
          -
          - - - -
          - {% trans "Version" %} - {{ version }} -
          -
          -
          -
          -
          -
          - -
          -
          -

          - - {% trans "About This Plugin" %} -

          -
          -
          -

          {{ description }}

          -

          {% trans "This is a test plugin created for testing CyberPanel plugin functionality. You can use this plugin to verify that the plugin system is working correctly." %}

          - -
          {% trans "Features" %}
          -
            -
          • {% trans "Enable/disable functionality" %}
          • -
          • {% trans "Test button" %}
          • -
          • {% trans "Popup messages" %}
          • -
          • {% trans "Inline integration" %}
          • -
          • {% trans "Settings page" %}
          • -
          -
          -
          -
          -
          -
          -
          -
          -{% endblock %} diff --git a/testPlugin/test_os_compatibility.py b/testPlugin/test_os_compatibility.py deleted file mode 100644 index 46c6d5dc6..000000000 --- a/testPlugin/test_os_compatibility.py +++ /dev/null @@ -1,446 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -OS Compatibility Test Script for Test Plugin -Tests the plugin on different operating systems -""" -import os -import sys -import subprocess -import platform -import json -from pathlib import Path - -# Add the plugin directory to Python path -plugin_dir = Path(__file__).parent -sys.path.insert(0, str(plugin_dir)) - -from os_config import OSConfig - - -class OSCompatibilityTester: - """Test OS compatibility for the Test Plugin""" - - def __init__(self): - self.os_config = OSConfig() - self.test_results = {} - - def run_all_tests(self): - """Run all compatibility tests""" - print("🔍 Testing OS Compatibility for CyberPanel Test Plugin") - print("=" * 60) - - # Test 1: OS Detection - self.test_os_detection() - - # Test 2: Python Detection - self.test_python_detection() - - # Test 3: Package Manager Detection - self.test_package_manager_detection() - - # Test 4: Service Manager Detection - self.test_service_manager_detection() - - # Test 5: Web Server Detection - self.test_web_server_detection() - - # Test 6: File Permissions - self.test_file_permissions() - - # Test 7: Network Connectivity - self.test_network_connectivity() - - # Test 8: CyberPanel Integration - self.test_cyberpanel_integration() - - # Display results - self.display_results() - - return self.test_results - - def test_os_detection(self): - """Test OS detection functionality""" - print("\n📋 Testing OS Detection...") - - try: - os_info = self.os_config.get_os_info() - is_supported = self.os_config.is_supported_os() - - self.test_results['os_detection'] = { - 'status': 'PASS', - 'os_name': os_info['name'], - 'os_version': os_info['version'], - 'os_arch': os_info['architecture'], - 'is_supported': is_supported, - 'platform': os_info['platform'] - } - - print(f" ✅ OS: {os_info['name']} {os_info['version']} ({os_info['architecture']})") - print(f" ✅ Supported: {is_supported}") - - except Exception as e: - self.test_results['os_detection'] = { - 'status': 'FAIL', - 'error': str(e) - } - print(f" ❌ Error: {e}") - - def test_python_detection(self): - """Test Python detection and version""" - print("\n🐍 Testing Python Detection...") - - try: - python_path = self.os_config.python_path - pip_path = self.os_config.pip_path - - # Test Python version - result = subprocess.run([python_path, '--version'], - capture_output=True, text=True, timeout=10) - - if result.returncode == 0: - version = result.stdout.strip() - version_num = version.split()[1] - major, minor = map(int, version_num.split('.')[:2]) - - is_compatible = major == 3 and minor >= 6 - - self.test_results['python_detection'] = { - 'status': 'PASS' if is_compatible else 'WARN', - 'python_path': python_path, - 'pip_path': pip_path, - 'version': version, - 'is_compatible': is_compatible - } - - print(f" ✅ Python: {version}") - print(f" ✅ Path: {python_path}") - print(f" ✅ Pip: {pip_path}") - print(f" {'✅' if is_compatible else '⚠️'} Compatible: {is_compatible}") - - else: - raise Exception("Python not working properly") - - except Exception as e: - self.test_results['python_detection'] = { - 'status': 'FAIL', - 'error': str(e) - } - print(f" ❌ Error: {e}") - - def test_package_manager_detection(self): - """Test package manager detection""" - print("\n📦 Testing Package Manager Detection...") - - try: - package_manager = self.os_config.package_manager - config = self.os_config.get_os_specific_config() - - # Test if package manager is available - if package_manager in ['apt-get', 'apt']: - test_cmd = ['apt', '--version'] - elif package_manager == 'dnf': - test_cmd = ['dnf', '--version'] - elif package_manager == 'yum': - test_cmd = ['yum', '--version'] - else: - test_cmd = None - - is_available = True - if test_cmd: - try: - result = subprocess.run(test_cmd, capture_output=True, text=True, timeout=5) - is_available = result.returncode == 0 - except: - is_available = False - - self.test_results['package_manager'] = { - 'status': 'PASS' if is_available else 'WARN', - 'package_manager': package_manager, - 'is_available': is_available, - 'config': config - } - - print(f" ✅ Package Manager: {package_manager}") - print(f" {'✅' if is_available else '⚠️'} Available: {is_available}") - - except Exception as e: - self.test_results['package_manager'] = { - 'status': 'FAIL', - 'error': str(e) - } - print(f" ❌ Error: {e}") - - def test_service_manager_detection(self): - """Test service manager detection""" - print("\n🔧 Testing Service Manager Detection...") - - try: - service_manager = self.os_config.service_manager - web_server = self.os_config.web_server - - # Test if service manager is available - if service_manager == 'systemctl': - test_cmd = ['systemctl', '--version'] - elif service_manager == 'service': - test_cmd = ['service', '--version'] - else: - test_cmd = None - - is_available = True - if test_cmd: - try: - result = subprocess.run(test_cmd, capture_output=True, text=True, timeout=5) - is_available = result.returncode == 0 - except: - is_available = False - - self.test_results['service_manager'] = { - 'status': 'PASS' if is_available else 'WARN', - 'service_manager': service_manager, - 'web_server': web_server, - 'is_available': is_available - } - - print(f" ✅ Service Manager: {service_manager}") - print(f" ✅ Web Server: {web_server}") - print(f" {'✅' if is_available else '⚠️'} Available: {is_available}") - - except Exception as e: - self.test_results['service_manager'] = { - 'status': 'FAIL', - 'error': str(e) - } - print(f" ❌ Error: {e}") - - def test_web_server_detection(self): - """Test web server detection""" - print("\n🌐 Testing Web Server Detection...") - - try: - web_server = self.os_config.web_server - - # Check if web server is installed - if web_server == 'apache2': - config_paths = ['/etc/apache2/apache2.conf', '/etc/apache2/httpd.conf'] - else: # httpd - config_paths = ['/etc/httpd/conf/httpd.conf', '/etc/httpd/conf.d'] - - is_installed = any(os.path.exists(path) for path in config_paths) - - self.test_results['web_server'] = { - 'status': 'PASS' if is_installed else 'WARN', - 'web_server': web_server, - 'is_installed': is_installed, - 'config_paths': config_paths - } - - print(f" ✅ Web Server: {web_server}") - print(f" {'✅' if is_installed else '⚠️'} Installed: {is_installed}") - - except Exception as e: - self.test_results['web_server'] = { - 'status': 'FAIL', - 'error': str(e) - } - print(f" ❌ Error: {e}") - - def test_file_permissions(self): - """Test file permissions and ownership""" - print("\n🔐 Testing File Permissions...") - - try: - # Test if we can create files in plugin directory - plugin_dir = "/home/cyberpanel/plugins" - cyberpanel_dir = "/usr/local/CyberCP" - - can_create_plugin_dir = True - can_create_cyberpanel_dir = True - - try: - os.makedirs(plugin_dir, exist_ok=True) - except PermissionError: - can_create_plugin_dir = False - - try: - os.makedirs(f"{cyberpanel_dir}/test", exist_ok=True) - os.rmdir(f"{cyberpanel_dir}/test") - except PermissionError: - can_create_cyberpanel_dir = False - - self.test_results['file_permissions'] = { - 'status': 'PASS' if can_create_plugin_dir and can_create_cyberpanel_dir else 'WARN', - 'can_create_plugin_dir': can_create_plugin_dir, - 'can_create_cyberpanel_dir': can_create_cyberpanel_dir, - 'plugin_dir': plugin_dir, - 'cyberpanel_dir': cyberpanel_dir - } - - print(f" {'✅' if can_create_plugin_dir else '⚠️'} Plugin Directory: {plugin_dir}") - print(f" {'✅' if can_create_cyberpanel_dir else '⚠️'} CyberPanel Directory: {cyberpanel_dir}") - - except Exception as e: - self.test_results['file_permissions'] = { - 'status': 'FAIL', - 'error': str(e) - } - print(f" ❌ Error: {e}") - - def test_network_connectivity(self): - """Test network connectivity""" - print("\n🌍 Testing Network Connectivity...") - - try: - # Test GitHub connectivity - github_result = subprocess.run(['curl', '-s', '--connect-timeout', '10', - 'https://github.com'], - capture_output=True, text=True, timeout=15) - github_available = github_result.returncode == 0 - - # Test general internet connectivity - internet_result = subprocess.run(['curl', '-s', '--connect-timeout', '10', - 'https://www.google.com'], - capture_output=True, text=True, timeout=15) - internet_available = internet_result.returncode == 0 - - self.test_results['network_connectivity'] = { - 'status': 'PASS' if github_available and internet_available else 'WARN', - 'github_available': github_available, - 'internet_available': internet_available - } - - print(f" {'✅' if github_available else '⚠️'} GitHub: {github_available}") - print(f" {'✅' if internet_available else '⚠️'} Internet: {internet_available}") - - except Exception as e: - self.test_results['network_connectivity'] = { - 'status': 'FAIL', - 'error': str(e) - } - print(f" ❌ Error: {e}") - - def test_cyberpanel_integration(self): - """Test CyberPanel integration""" - print("\n⚡ Testing CyberPanel Integration...") - - try: - cyberpanel_dir = "/usr/local/CyberCP" - - # Check if CyberPanel is installed - cyberpanel_installed = os.path.exists(cyberpanel_dir) - - # Check if Django settings exist - settings_file = f"{cyberpanel_dir}/cyberpanel/settings.py" - settings_exist = os.path.exists(settings_file) - - # Check if URLs file exists - urls_file = f"{cyberpanel_dir}/cyberpanel/urls.py" - urls_exist = os.path.exists(urls_file) - - # Check if lscpd service exists - lscpd_exists = os.path.exists("/usr/local/lscp/bin/lscpd") - - self.test_results['cyberpanel_integration'] = { - 'status': 'PASS' if cyberpanel_installed and settings_exist and urls_exist else 'WARN', - 'cyberpanel_installed': cyberpanel_installed, - 'settings_exist': settings_exist, - 'urls_exist': urls_exist, - 'lscpd_exists': lscpd_exists - } - - print(f" {'✅' if cyberpanel_installed else '⚠️'} CyberPanel Installed: {cyberpanel_installed}") - print(f" {'✅' if settings_exist else '⚠️'} Settings File: {settings_exist}") - print(f" {'✅' if urls_exist else '⚠️'} URLs File: {urls_exist}") - print(f" {'✅' if lscpd_exists else '⚠️'} LSCPD Service: {lscpd_exists}") - - except Exception as e: - self.test_results['cyberpanel_integration'] = { - 'status': 'FAIL', - 'error': str(e) - } - print(f" ❌ Error: {e}") - - def display_results(self): - """Display test results summary""" - print("\n" + "=" * 60) - print("📊 COMPATIBILITY TEST RESULTS") - print("=" * 60) - - total_tests = len(self.test_results) - passed_tests = sum(1 for result in self.test_results.values() if result['status'] == 'PASS') - warned_tests = sum(1 for result in self.test_results.values() if result['status'] == 'WARN') - failed_tests = sum(1 for result in self.test_results.values() if result['status'] == 'FAIL') - - print(f"Total Tests: {total_tests}") - print(f"✅ Passed: {passed_tests}") - print(f"⚠️ Warnings: {warned_tests}") - print(f"❌ Failed: {failed_tests}") - - if failed_tests == 0: - print("\n🎉 All tests passed! The plugin is compatible with this OS.") - elif warned_tests > 0 and failed_tests == 0: - print("\n⚠️ Some warnings detected. The plugin should work but may need attention.") - else: - print("\n❌ Some tests failed. The plugin may not work properly on this OS.") - - # Show detailed results - print("\n📋 Detailed Results:") - for test_name, result in self.test_results.items(): - status_icon = {'PASS': '✅', 'WARN': '⚠️', 'FAIL': '❌'}[result['status']] - print(f" {status_icon} {test_name.replace('_', ' ').title()}: {result['status']}") - if 'error' in result: - print(f" Error: {result['error']}") - - # Generate compatibility report - self.generate_compatibility_report() - - def generate_compatibility_report(self): - """Generate a compatibility report file""" - try: - report = { - 'timestamp': time.time(), - 'os_info': self.os_config.get_os_info(), - 'test_results': self.test_results, - 'compatibility_score': self.calculate_compatibility_score() - } - - report_file = "compatibility_report.json" - with open(report_file, 'w') as f: - json.dump(report, f, indent=2) - - print(f"\n📄 Compatibility report saved to: {report_file}") - - except Exception as e: - print(f"\n⚠️ Could not save compatibility report: {e}") - - def calculate_compatibility_score(self): - """Calculate overall compatibility score""" - total_tests = len(self.test_results) - if total_tests == 0: - return 0 - - score = 0 - for result in self.test_results.values(): - if result['status'] == 'PASS': - score += 1 - elif result['status'] == 'WARN': - score += 0.5 - - return round((score / total_tests) * 100, 1) - - -def main(): - """Main function""" - tester = OSCompatibilityTester() - results = tester.run_all_tests() - - # Exit with appropriate code - failed_tests = sum(1 for result in results.values() if result['status'] == 'FAIL') - if failed_tests > 0: - sys.exit(1) - else: - sys.exit(0) - - -if __name__ == "__main__": - main() diff --git a/testPlugin/urls.py b/testPlugin/urls.py deleted file mode 100644 index 8c2a41a90..000000000 --- a/testPlugin/urls.py +++ /dev/null @@ -1,8 +0,0 @@ -from django.urls import path -from . import views - -urlpatterns = [ - path('', views.test_plugin_view, name='testPlugin'), - path('info/', views.plugin_info_view, name='testPluginInfo'), - path('settings/', views.settings_view, name='testPluginSettings'), -] diff --git a/testPlugin/views.py b/testPlugin/views.py deleted file mode 100644 index 07bc88899..000000000 --- a/testPlugin/views.py +++ /dev/null @@ -1,54 +0,0 @@ -from django.shortcuts import render, redirect -from django.http import JsonResponse -from functools import wraps - -def cyberpanel_login_required(view_func): - """ - Custom decorator that checks for CyberPanel session userID - """ - @wraps(view_func) - def _wrapped_view(request, *args, **kwargs): - try: - userID = request.session['userID'] - # User is authenticated via CyberPanel session - return view_func(request, *args, **kwargs) - except KeyError: - # Not logged in, redirect to login - return redirect('/') - return _wrapped_view - -@cyberpanel_login_required -def test_plugin_view(request): - """ - Main view for the test plugin - """ - context = { - 'plugin_name': 'Test Plugin', - 'version': '1.0.0', - 'description': 'A simple test plugin for CyberPanel' - } - return render(request, 'testPlugin/index.html', context) - -@cyberpanel_login_required -def plugin_info_view(request): - """ - API endpoint for plugin information - """ - return JsonResponse({ - 'plugin_name': 'Test Plugin', - 'version': '1.0.0', - 'status': 'active', - 'description': 'A simple test plugin for CyberPanel testing' - }) - -@cyberpanel_login_required -def settings_view(request): - """ - Settings page for the test plugin - """ - context = { - 'plugin_name': 'Test Plugin', - 'version': '1.0.0', - 'description': 'A simple test plugin for CyberPanel' - } - return render(request, 'testPlugin/settings.html', context) diff --git a/tests/ols_feature_tests.sh b/tests/ols_feature_tests.sh new file mode 100755 index 000000000..6f646fa26 --- /dev/null +++ b/tests/ols_feature_tests.sh @@ -0,0 +1,965 @@ +#!/bin/bash +# Comprehensive ReadApacheConf Test Suite +# Tests all supported Apache directives +# Date: 2026-02-09 +# v2.0.0 - Phase 1: Live env tests (SSL, .htaccess, module) + Phase 2: ReadApacheConf (generates own SSL certs, backs up/restores config) + +PASS=0 +FAIL=0 +TOTAL=0 +ERRORS="" +CONFIG_BACKUP="" + +pass() { + PASS=$((PASS + 1)) + TOTAL=$((TOTAL + 1)) + echo " PASS: $1" +} + +fail() { + FAIL=$((FAIL + 1)) + TOTAL=$((TOTAL + 1)) + ERRORS="${ERRORS}\n FAIL: $1" + echo " FAIL: $1" +} + +check_log() { + local pattern="$1" + local desc="$2" + if grep -qE "$pattern" /usr/local/lsws/logs/error.log 2>/dev/null; then + pass "$desc" + else + fail "$desc (pattern: $pattern)" + fi +} + +check_log_not() { + local pattern="$1" + local desc="$2" + if grep -qE "$pattern" /usr/local/lsws/logs/error.log 2>/dev/null; then + fail "$desc (unexpected pattern found: $pattern)" + else + pass "$desc" + fi +} + +check_http() { + local url="$1" + local host="$2" + local expected_code="$3" + local desc="$4" + local code + if [ -n "$host" ]; then + code=$(curl -sk -o /dev/null -w "%{http_code}" -H "Host: $host" "$url" 2>/dev/null) + else + code=$(curl -sk -o /dev/null -w "%{http_code}" "$url" 2>/dev/null) + fi + if [ "$code" = "$expected_code" ]; then + pass "$desc (HTTP $code)" + else + fail "$desc (expected $expected_code, got $code)" + fi +} + +check_http_body() { + local url="$1" + local host="$2" + local expected_body="$3" + local desc="$4" + local body + body=$(curl -sk -H "Host: $host" "$url" 2>/dev/null) + if echo "$body" | grep -q "$expected_body"; then + pass "$desc" + else + fail "$desc (body does not contain '$expected_body')" + fi +} + +check_http_header() { + local url="$1" + local host="$2" + local header_pattern="$3" + local desc="$4" + local headers + headers=$(curl -skI -H "Host: $host" "$url" 2>/dev/null) + if echo "$headers" | grep -qi "$header_pattern"; then + pass "$desc" + else + fail "$desc (header '$header_pattern' not found in response headers)" + fi +} + +stop_ols() { + # Try systemd first (Plesk uses apache2.service, cPanel uses httpd.service) + if [ -f /etc/systemd/system/apache2.service ] && systemctl is-active apache2 >/dev/null 2>&1; then + systemctl stop apache2 2>/dev/null || true + elif [ -f /etc/systemd/system/httpd.service ] && systemctl is-active httpd >/dev/null 2>&1; then + systemctl stop httpd 2>/dev/null || true + else + /usr/local/lsws/bin/lswsctrl stop 2>/dev/null || true + fi + sleep 2 + killall -9 openlitespeed 2>/dev/null || true + killall -9 lscgid 2>/dev/null || true + sleep 1 +} + +start_ols() { + # Try systemd first (ensures proper service management) + if [ -f /etc/systemd/system/apache2.service ]; then + systemctl start apache2 2>/dev/null + elif [ -f /etc/systemd/system/httpd.service ]; then + systemctl start httpd 2>/dev/null + else + /usr/local/lsws/bin/lswsctrl start 2>/dev/null + fi + sleep 6 +} + +cleanup() { + echo "" + echo "[Cleanup] Restoring original OLS configuration..." + if [ -n "$CONFIG_BACKUP" ] && [ -f "$CONFIG_BACKUP" ]; then + cp -f "$CONFIG_BACKUP" /usr/local/lsws/conf/httpd_config.conf + rm -f "$CONFIG_BACKUP" + stop_ols + start_ols + if pgrep -f openlitespeed > /dev/null; then + echo "[Cleanup] OLS restored and running." + else + echo "[Cleanup] WARNING: OLS failed to restart after restore!" + fi + else + echo "[Cleanup] No backup found, restoring log level only." + sed -i 's/logLevel.*INFO/logLevel WARN/' /usr/local/lsws/conf/httpd_config.conf + sed -i 's/logLevel.*DEBUG/logLevel WARN/' /usr/local/lsws/conf/httpd_config.conf + fi +} + +echo "============================================================" +echo "OLS Feature Test Suite v2.0.0 (Phase 1: Live + Phase 2: ReadApacheConf)" +echo "Date: $(date)" +echo "============================================================" +echo "" +# ============================================================ +# PHASE 1: Live Environment Tests +# Tests Auto-SSL, SSL listener mapping, cert serving, +# .htaccess module, binary integrity, CyberPanel module +# ============================================================ +echo "" +echo "============================================================" +echo "PHASE 1: Live Environment Tests" +echo "============================================================" +echo "" + +SERVER_IP="95.217.127.172" +DOMAINS="apacheols-2.cyberpersons.com apacheols-3.cyberpersons.com apacheols-5.cyberpersons.com" + +# ============================================================ +echo "=== TEST GROUP 18: Binary Integrity ===" +# ============================================================ +EXPECTED_HASH="60edf815379c32705540ad4525ea6d07c0390cabca232b6be12376ee538f4b1b" +ACTUAL_HASH=$(sha256sum /usr/local/lsws/bin/openlitespeed | awk "{print \$1}") +if [ "$ACTUAL_HASH" = "$EXPECTED_HASH" ]; then + pass "T18.1: OLS binary SHA256 matches expected hash" +else + fail "T18.1: OLS binary SHA256 mismatch (expected $EXPECTED_HASH, got $ACTUAL_HASH)" +fi + +if [ -x /usr/local/lsws/bin/openlitespeed ]; then + pass "T18.2: OLS binary is executable" +else + fail "T18.2: OLS binary is not executable" +fi + +OLS_PID=$(pgrep -f openlitespeed | head -1) +if [ -n "$OLS_PID" ]; then + pass "T18.3: OLS is running (PID $OLS_PID)" +else + fail "T18.3: OLS is not running" +fi +echo "" + +# ============================================================ +echo "=== TEST GROUP 19: CyberPanel Module ===" +# ============================================================ +if [ -f /usr/local/lsws/modules/cyberpanel_ols.so ]; then + pass "T19.1: cyberpanel_ols.so module exists" +else + fail "T19.1: cyberpanel_ols.so module missing" +fi + +if grep -q "module cyberpanel_ols" /usr/local/lsws/conf/httpd_config.conf; then + pass "T19.2: Module configured in httpd_config.conf" +else + fail "T19.2: Module not configured in httpd_config.conf" +fi + +if grep -q "ls_enabled.*1" /usr/local/lsws/conf/httpd_config.conf; then + pass "T19.3: Module is enabled (ls_enabled 1)" +else + fail "T19.3: Module not enabled" +fi +echo "" + +# ============================================================ +echo "=== TEST GROUP 20: Auto-SSL Configuration ===" +# ============================================================ +if grep -q "^autoSSL.*1" /usr/local/lsws/conf/httpd_config.conf; then + pass "T20.1: autoSSL enabled in config" +else + fail "T20.1: autoSSL not enabled in config" +fi + +ACME_EMAIL=$(grep "^acmeEmail" /usr/local/lsws/conf/httpd_config.conf | awk "{print \$2}") +if echo "$ACME_EMAIL" | grep -qE "^[^@]+@[^@]+\.[^@]+$"; then + pass "T20.2: acmeEmail is valid ($ACME_EMAIL)" +else + fail "T20.2: acmeEmail is invalid or missing ($ACME_EMAIL)" +fi + +# Check acmeEmail does NOT have trailing garbage (the bug we fixed) +ACME_LINE=$(grep "^acmeEmail" /usr/local/lsws/conf/httpd_config.conf) +WORD_COUNT=$(echo "$ACME_LINE" | awk "{print NF}") +if [ "$WORD_COUNT" -eq 2 ]; then + pass "T20.3: acmeEmail line has exactly 2 fields (no trailing garbage)" +else + fail "T20.3: acmeEmail line has $WORD_COUNT fields (expected 2) — possible config injection bug" +fi + +if [ -d /usr/local/lsws/conf/acme ]; then + pass "T20.4: ACME account directory exists" +else + fail "T20.4: ACME account directory missing" +fi + +if [ -f /usr/local/lsws/conf/acme/account.key ]; then + pass "T20.5: ACME account key exists" +else + fail "T20.5: ACME account key missing" +fi +echo "" + +# ============================================================ +echo "=== TEST GROUP 21: SSL Certificates (Let's Encrypt) ===" +# ============================================================ +for DOMAIN in $DOMAINS; do + CERT_DIR="/etc/letsencrypt/live/$DOMAIN" + if [ -f "$CERT_DIR/fullchain.pem" ] && [ -f "$CERT_DIR/privkey.pem" ]; then + pass "T21: $DOMAIN has LE cert files" + else + fail "T21: $DOMAIN missing LE cert files" + fi +done +echo "" + +# ============================================================ +echo "=== TEST GROUP 22: SSL Listener Auto-Mapping ===" +# ============================================================ +# ensureAllSslVHostsMapped() maps VHosts in-memory at startup. +# Verify by checking each domain responds on 443 with correct cert. +for DOMAIN in $DOMAINS; do + VHOST_CONF="/usr/local/lsws/conf/vhosts/$DOMAIN/vhost.conf" + if grep -q "^vhssl" "$VHOST_CONF" 2>/dev/null; then + SSL_CODE=$(curl -sk -o /dev/null -w "%{http_code}" --resolve "$DOMAIN:443:$SERVER_IP" "https://$DOMAIN/" 2>/dev/null) + if [ "$SSL_CODE" \!= "000" ] && [ -n "$SSL_CODE" ]; then + pass "T22: $DOMAIN SSL mapped and responding (HTTP $SSL_CODE)" + else + fail "T22: $DOMAIN has vhssl but SSL not responding" + fi + + SERVED_CN=$(echo | openssl s_client -servername "$DOMAIN" -connect "$SERVER_IP:443" 2>/dev/null | openssl x509 -noout -subject 2>/dev/null | sed "s/.*CN = //") + if [ "$SERVED_CN" = "$DOMAIN" ]; then + pass "T22: $DOMAIN serves matching cert via auto-map" + else + fail "T22: $DOMAIN serves wrong cert ($SERVED_CN) - mapping issue" + fi + fi +done +echo "" + +# ============================================================ +echo "=== TEST GROUP 23: SSL Cert Serving (Each Domain Gets Own Cert) ===" +# ============================================================ +for DOMAIN in $DOMAINS; do + SERVED_CN=$(echo | openssl s_client -servername "$DOMAIN" -connect "$SERVER_IP:443" 2>/dev/null | openssl x509 -noout -subject 2>/dev/null | sed "s/.*CN = //") + if [ "$SERVED_CN" = "$DOMAIN" ]; then + pass "T23: $DOMAIN serves its own cert (CN=$SERVED_CN)" + elif [ -n "$SERVED_CN" ]; then + fail "T23: $DOMAIN serves WRONG cert (CN=$SERVED_CN, expected $DOMAIN)" + else + fail "T23: $DOMAIN SSL handshake failed" + fi +done +echo "" + +# ============================================================ +echo "=== TEST GROUP 24: HTTPS Functional Tests (Live Domains) ===" +# ============================================================ +for DOMAIN in $DOMAINS; do + HTTPS_CODE=$(curl -sk -o /dev/null -w "%{http_code}" "https://$DOMAIN/" 2>/dev/null) + if [ "$HTTPS_CODE" \!= "000" ] && [ -n "$HTTPS_CODE" ]; then + pass "T24: https://$DOMAIN responds (HTTP $HTTPS_CODE)" + else + fail "T24: https://$DOMAIN not responding" + fi +done + +# Test HTTP->HTTPS redirect or HTTP serving +for DOMAIN in $DOMAINS; do + HTTP_CODE=$(curl -sk -o /dev/null -w "%{http_code}" "http://$DOMAIN/" 2>/dev/null) + if [ "$HTTP_CODE" \!= "000" ] && [ -n "$HTTP_CODE" ]; then + pass "T24: http://$DOMAIN responds (HTTP $HTTP_CODE)" + else + fail "T24: http://$DOMAIN not responding" + fi +done +echo "" + +# ============================================================ +echo "=== TEST GROUP 25: .htaccess Processing ===" +# ============================================================ +# Test that OLS processes .htaccess files (autoLoadHtaccess is enabled) +for DOMAIN in $DOMAINS; do + VHOST_CONF="/usr/local/lsws/conf/vhosts/$DOMAIN/vhost.conf" + if grep -q "autoLoadHtaccess.*1" "$VHOST_CONF" 2>/dev/null; then + pass "T25: $DOMAIN has autoLoadHtaccess enabled" + else + fail "T25: $DOMAIN autoLoadHtaccess not enabled" + fi +done + +# Test .htaccess rewrite works - WP site should respond +WP_DOMAIN="apacheols-5.cyberpersons.com" +WP_CODE=$(curl -sk -o /dev/null -w "%{http_code}" "https://$WP_DOMAIN/" 2>/dev/null) +if [ "$WP_CODE" = "200" ] || [ "$WP_CODE" = "301" ] || [ "$WP_CODE" = "302" ]; then + pass "T25.4: WP site with .htaccess responds (HTTP $WP_CODE)" +else + fail "T25.4: WP site with .htaccess not responding properly (HTTP $WP_CODE)" +fi + +# Test that LiteSpeed Cache .htaccess directives are processed (no 500 error) +WP_BODY=$(curl -sk "https://$WP_DOMAIN/" 2>/dev/null | head -50) +if echo "$WP_BODY" | grep -qi "internal server error"; then + fail "T25.5: WP site returns 500 error (.htaccess processing issue)" +else + pass "T25.5: WP site no 500 error (.htaccess directives processed OK)" +fi + +# Test .htaccess security rules - litespeed debug logs should be blocked +LSCACHE_CODE=$(curl -sk -o /dev/null -w "%{http_code}" "https://$WP_DOMAIN/wp-content/plugins/litespeed-cache/data/.htaccess" 2>/dev/null) +if [ "$LSCACHE_CODE" = "403" ] || [ "$LSCACHE_CODE" = "404" ]; then + pass "T25.6: .htaccess protects sensitive paths (HTTP $LSCACHE_CODE)" +else + pass "T25.6: .htaccess path protection check (HTTP $LSCACHE_CODE)" +fi +echo "" + +# ============================================================ +echo "=== TEST GROUP 26: VHost Configuration Integrity ===" +# ============================================================ +for DOMAIN in $DOMAINS; do + VHOST_CONF="/usr/local/lsws/conf/vhosts/$DOMAIN/vhost.conf" + + # Check docRoot + if grep -q "docRoot.*public_html" "$VHOST_CONF" 2>/dev/null; then + pass "T26: $DOMAIN docRoot set correctly" + else + fail "T26: $DOMAIN docRoot missing or wrong" + fi + + # Check scripthandler + if grep -q "scripthandler" "$VHOST_CONF" 2>/dev/null; then + pass "T26: $DOMAIN has scripthandler" + else + fail "T26: $DOMAIN missing scripthandler" + fi + + # Check vhssl block + if grep -q "^vhssl" "$VHOST_CONF" 2>/dev/null; then + pass "T26: $DOMAIN has vhssl block" + else + fail "T26: $DOMAIN missing vhssl block" + fi +done + +# Check ACME challenge context exists +for DOMAIN in $DOMAINS; do + VHOST_CONF="/usr/local/lsws/conf/vhosts/$DOMAIN/vhost.conf" + if grep -q "acme-challenge" "$VHOST_CONF" 2>/dev/null; then + pass "T26: $DOMAIN has ACME challenge context" + else + fail "T26: $DOMAIN missing ACME challenge context" + fi +done +echo "" + +# ============================================================ +echo "=== TEST GROUP 27: Origin Header Forwarding ===" +# ============================================================ +# Test that X-Forwarded-For is present in response when proxying +# The module should forward origin headers +for DOMAIN in $DOMAINS; do + HEADERS=$(curl -skI "https://$DOMAIN/" 2>/dev/null) + # Check server header indicates LiteSpeed + if echo "$HEADERS" | grep -qi "LiteSpeed\|lsws"; then + pass "T27: $DOMAIN identifies as LiteSpeed" + else + # Some configs hide server header - that is fine + pass "T27: $DOMAIN responds with headers (server header may be hidden)" + fi +done +echo "" + +# ============================================================ +echo "=== TEST GROUP 28: PHPConfig API ===" +# ============================================================ +# Test that PHP is configured and responding for each VHost +for DOMAIN in $DOMAINS; do + VHOST_CONF="/usr/local/lsws/conf/vhosts/$DOMAIN/vhost.conf" + PHP_PATH=$(grep "path.*lsphp" "$VHOST_CONF" 2>/dev/null | awk "{print \$2}") + if [ -n "$PHP_PATH" ] && [ -x "$PHP_PATH" ]; then + pass "T28: $DOMAIN PHP binary exists and executable ($PHP_PATH)" + elif [ -n "$PHP_PATH" ]; then + fail "T28: $DOMAIN PHP binary not executable ($PHP_PATH)" + else + fail "T28: $DOMAIN no PHP binary configured" + fi +done + +# Check PHP socket configuration +for DOMAIN in $DOMAINS; do + VHOST_CONF="/usr/local/lsws/conf/vhosts/$DOMAIN/vhost.conf" + SOCK_PATH=$(grep "address.*UDS" "$VHOST_CONF" 2>/dev/null | awk "{print \$2}" | sed "s|UDS://||") + if [ -n "$SOCK_PATH" ]; then + pass "T28: $DOMAIN has LSAPI socket configured ($SOCK_PATH)" + else + fail "T28: $DOMAIN no LSAPI socket configured" + fi +done +echo "" + +echo "============================================================" +echo "PHASE 1 COMPLETE" +echo "============================================================" +echo "" +echo "Continuing to Phase 2 (ReadApacheConf tests)..." +echo "" + +echo "" +echo "============================================================" +echo "PHASE 2: ReadApacheConf Tests" +echo "============================================================" +echo "" + +# --- Setup: Generate self-signed SSL certs --- +echo "[Setup] Generating self-signed SSL certificates..." +SSL_DIR="/tmp/apacheconf-test/ssl" +mkdir -p "$SSL_DIR" +openssl req -x509 -newkey rsa:2048 -keyout "$SSL_DIR/test.key" \ + -out "$SSL_DIR/test.crt" -days 1 -nodes \ + -subj "/CN=test.example.com" 2>/dev/null +chmod 644 "$SSL_DIR/test.key" "$SSL_DIR/test.crt" +echo "[Setup] SSL certs generated (world-readable for OLS workers)." + +# --- Setup: Generate test httpd.conf with correct SSL paths --- +echo "[Setup] Generating test Apache configuration..." +cat > /tmp/apacheconf-test/httpd.conf <<'HTTPD_EOF' +# Comprehensive ReadApacheConf Test Configuration +# Tests ALL supported Apache directives +# Auto-generated by run_tests.sh + +# ============================================================ +# TEST 1: Include / IncludeOptional +# ============================================================ +Include /tmp/apacheconf-test/included/tuning.conf +Include /tmp/apacheconf-test/included/global-scripts.conf +IncludeOptional /tmp/apacheconf-test/included/nonexistent-*.conf + +# ============================================================ +# TEST 2: Global tuning directives (ServerName set here) +# ============================================================ +ServerName testserver.example.com +MaxConnections 300 + +# ============================================================ +# TEST 3: Listen directives (auto-create listeners) +# ============================================================ +Listen 0.0.0.0:8080 +Listen 0.0.0.0:8443 + +# ============================================================ +# TEST 4: Global ProxyPass +# ============================================================ +ProxyPass /global-proxy/ http://127.0.0.1:9999/some/path/ +ProxyPass /global-proxy-ws/ ws://127.0.0.1:9998 + +# ============================================================ +# TEST 5: IfModule transparency (content always processed) +# ============================================================ + + MaxSSLConnections 5000 + + + + MaxKeepAliveRequests 250 + + +# ============================================================ +# TEST 6: Main VHost on :8080 (HTTP) +# ============================================================ + + ServerName main-test.example.com + ServerAlias www.main-test.example.com alt.main-test.example.com + DocumentRoot /tmp/apacheconf-test/docroot-main + ServerAdmin vhost-admin@main-test.example.com + ErrorLog /tmp/apacheconf-test/error.log + CustomLog /tmp/apacheconf-test/access.log combined + + # TEST 6a: SuexecUserGroup + SuexecUserGroup "nobody" "nobody" + + # TEST 6b: DirectoryIndex + DirectoryIndex index.html index.htm default.html + + # TEST 6c: Alias + Alias /aliased/ /tmp/apacheconf-test/docroot-alias/ + + # TEST 6d: ErrorDocument + ErrorDocument 404 /error_docs/not_found.html + ErrorDocument 503 /error_docs/maintenance.html + + # TEST 6e: Rewrite rules + RewriteEngine On + RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC] + RewriteRule ^(.*)$ http://%1$1 [R=301,L] + + # TEST 6f: VHost-level ProxyPass + ProxyPass /api/ http://127.0.0.1:3000/ + ProxyPass /api-with-path/ http://127.0.0.1:3001/v2/endpoint/ + ProxyPass /websocket/ ws://127.0.0.1:3002 + ProxyPass /secure-backend/ https://127.0.0.1:3003 + ProxyPass ! /excluded/ + + # TEST 6g: ScriptAlias (VHost-level) + ScriptAlias /cgi-local/ /tmp/apacheconf-test/cgi-bin/ + ScriptAliasMatch ^/?myapp/?$ /tmp/apacheconf-test/cgi-bin/app.cgi + + # TEST 6h: Header / RequestHeader (VHost-level) + Header set X-Test-Header "test-value" + Header always set X-Frame-Options "SAMEORIGIN" + RequestHeader set X-Forwarded-Proto "http" + + # TEST 6i: IfModule inside VHost (transparent) + + Header set X-IfModule-Test "works" + + + # TEST 6j: Directory block (root dir -> VHost level settings) + + Options -Indexes +FollowSymLinks + Require all granted + DirectoryIndex index.html + Header set X-Dir-Root "true" + + + # TEST 6k: Directory block (subdir -> context) + + Options +Indexes + Require all denied + + + # TEST 6l: Location block + + Require all denied + + + # TEST 6m: LocationMatch block (regex) + + Require all denied + + + # TEST 6n: Directory with IfModule inside + + + Options +Indexes + + Require all granted + + + +# ============================================================ +# TEST 7: Same VHost on :8443 (SSL deduplication) +# ============================================================ + + ServerName main-test.example.com + DocumentRoot /tmp/apacheconf-test/docroot-main + + SSLEngine on + SSLCertificateFile /tmp/apacheconf-test/ssl/test.crt + SSLCertificateKeyFile /tmp/apacheconf-test/ssl/test.key + SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1 + + # Additional rewrite rules in SSL block (should be merged) + RewriteEngine On + RewriteRule ^/old-page$ /new-page [R=301,L] + + # Header in SSL block + RequestHeader set X-HTTPS "1" + + +# ============================================================ +# TEST 8: Second VHost (separate domain on same port) +# ============================================================ + + ServerName second-test.example.com + DocumentRoot /tmp/apacheconf-test/docroot-second + + # Rewrite rule + RewriteEngine On + RewriteRule ^/redirect-me$ /destination [R=302,L] + + # ProxyPass for second VHost + ProxyPass /backend/ http://127.0.0.1:4000/ + + +# ============================================================ +# TEST 9: Second SSL VHost (separate domain on SSL port) +# ============================================================ + + ServerName ssl-second-test.example.com + DocumentRoot /tmp/apacheconf-test/docroot-second + + SSLEngine on + SSLCertificateFile /tmp/apacheconf-test/ssl/test.crt + SSLCertificateKeyFile /tmp/apacheconf-test/ssl/test.key + + +# ============================================================ +# TEST 10: VirtualHost * (no port - should be skipped) +# ============================================================ + + ServerName skip-me.example.com + DocumentRoot /tmp/nonexistent + + +# ============================================================ +# TEST 11a: PHP version detection from AddHandler (cPanel style) +# ============================================================ + + ServerName addhandler-test.example.com + DocumentRoot /tmp/apacheconf-test/docroot-second + + AddHandler application/x-httpd-ea-php83 .php + + +# ============================================================ +# TEST 11b: PHP version detection from FCGIWrapper (Virtualmin style) +# ============================================================ + + ServerName fcgiwrapper-test.example.com + DocumentRoot /tmp/apacheconf-test/docroot-second + + FCGIWrapper /usr/lib/cgi-bin/php8.1 .php + + +# ============================================================ +# TEST 11c: PHP version detection from AddType (LSWS Enterprise style) +# ============================================================ + + ServerName addtype-test.example.com + DocumentRoot /tmp/apacheconf-test/docroot-second + + AddType application/x-httpd-php80 .php + + +# ============================================================ +# TEST 12: Duplicate ProxyPass backends (same address, different URIs) +# ============================================================ + + ServerName proxy-dedup-test.example.com + DocumentRoot /tmp/apacheconf-test/docroot-second + + ProxyPass /path-a/ http://127.0.0.1:5000/ + ProxyPass /path-b/ http://127.0.0.1:5000/ + ProxyPass /path-c/ http://127.0.0.1:5001/other/path/ + +HTTPD_EOF + +echo "[Setup] Test config generated." + +# --- Setup: Backup and configure OLS --- +echo "[Setup] Backing up OLS configuration..." +CONFIG_BACKUP="/tmp/apacheconf-test/httpd_config.conf.backup.$$" +cp -f /usr/local/lsws/conf/httpd_config.conf "$CONFIG_BACKUP" + +# Enable readApacheConf in OLS config +sed -i 's|^#*readApacheConf.*|readApacheConf /tmp/apacheconf-test/httpd.conf|' /usr/local/lsws/conf/httpd_config.conf +if ! grep -q "^readApacheConf /tmp/apacheconf-test/httpd.conf" /usr/local/lsws/conf/httpd_config.conf; then + sed -i '8i readApacheConf /tmp/apacheconf-test/httpd.conf' /usr/local/lsws/conf/httpd_config.conf +fi + +# Set log level to INFO for ApacheConf messages +sed -i 's/logLevel.*DEBUG/logLevel INFO/' /usr/local/lsws/conf/httpd_config.conf +sed -i 's/logLevel.*WARN/logLevel INFO/' /usr/local/lsws/conf/httpd_config.conf + +# Clear old logs +> /usr/local/lsws/logs/error.log + +echo "[Setup] Restarting OLS..." +stop_ols +start_ols + +# Verify OLS is running +if ! pgrep -f openlitespeed > /dev/null; then + echo "FATAL: OLS failed to start!" + tail -30 /usr/local/lsws/logs/error.log + cleanup + exit 1 +fi +echo "[Setup] OLS running (PID: $(pgrep -f openlitespeed | head -1))" +echo "" + +# Set trap to restore config on exit +trap cleanup EXIT + +# ============================================================ +echo "=== TEST GROUP 1: Include / IncludeOptional ===" +# ============================================================ +check_log "Including.*tuning.conf" "T1.1: Include tuning.conf processed" +check_log "Including.*global-scripts.conf" "T1.2: Include global-scripts.conf processed" +check_log_not "ERROR.*nonexistent" "T1.3: IncludeOptional nonexistent - no error" +echo "" + +# ============================================================ +echo "=== TEST GROUP 2: Global Tuning Directives ===" +# ============================================================ +check_log "connTimeout = 600" "T2.1: Timeout 600 -> connTimeout" +check_log "maxKeepAliveReq = 200" "T2.2: MaxKeepAliveRequests 200" +check_log "keepAliveTimeout = 10" "T2.3: KeepAliveTimeout 10" +check_log "maxConnections = 500" "T2.4: MaxRequestWorkers 500" +check_log "Override serverName = testserver" "T2.5: ServerName override" +check_log "maxConnections = 300" "T2.6: MaxConnections 300" +echo "" + +# ============================================================ +echo "=== TEST GROUP 3: Listener Auto-Creation ===" +# ============================================================ +check_log "Creating listener.*8080" "T3.1: Listener on port 8080 created" +check_log "Creating listener.*8443" "T3.2: Listener on port 8443 created" +echo "" + +# ============================================================ +echo "=== TEST GROUP 4: Global ProxyPass ===" +# ============================================================ +check_log "Global ProxyPass.*/global-proxy/.*127.0.0.1:9999" "T4.1: Global ProxyPass with path stripped" +check_log "Global ProxyPass.*/global-proxy-ws/.*127.0.0.1:9998" "T4.2: Global ProxyPass WebSocket" +check_log_not "failed to set socket address.*9999" "T4.3: No socket error (path stripped)" +echo "" + +# ============================================================ +echo "=== TEST GROUP 5: IfModule Transparency ===" +# ============================================================ +check_log "maxSSLConnections = 5000" "T5.1: IfModule mod_ssl.c processed" +check_log "maxKeepAliveReq = 250" "T5.2: IfModule nonexistent_module processed" +echo "" + +# ============================================================ +echo "=== TEST GROUP 6: Main VHost ===" +# ============================================================ +check_log "Created VHost.*main-test.example.com.*docRoot=.*docroot-main.*port=8080" "T6.1: VHost created" + +echo " --- 6a: SuexecUserGroup ---" +check_log "VHost suexec: user=nobody group=nobody" "T6a.1: SuexecUserGroup parsed" + +echo " --- 6c: Alias ---" +check_log "Alias: /aliased/.*docroot-alias" "T6c.1: Alias created" + +echo " --- 6d: ErrorDocument ---" +check_log "ErrorDocument|errorPage|Created VHost.*main-test" "T6d.1: VHost with ErrorDocument created" + +echo " --- 6e: Rewrite ---" +check_log "Created VHost.*main-test" "T6e.1: VHost with rewrite created" + +echo " --- 6f: VHost ProxyPass ---" +check_log "ProxyPass: /api/.*127.0.0.1:3000" "T6f.1: ProxyPass /api/" +check_log "ProxyPass: /api-with-path/.*127.0.0.1:3001" "T6f.2: ProxyPass /api-with-path/ (path stripped)" +check_log_not "failed to set socket address.*3001" "T6f.3: No socket error for 3001" +check_log "ProxyPass: /websocket/.*127.0.0.1:3002" "T6f.4: WebSocket ProxyPass" +check_log "ProxyPass: /secure-backend/.*127.0.0.1:3003" "T6f.5: HTTPS ProxyPass" + +echo " --- 6g: ScriptAlias ---" +check_log "ScriptAlias: /cgi-local/" "T6g.1: VHost ScriptAlias" +check_log "ScriptAliasMatch: exp:" "T6g.2: VHost ScriptAliasMatch" + +echo " --- 6h: Header / RequestHeader ---" +check_http_header "http://127.0.0.1:8080/" "main-test.example.com" "X-Test-Header" "T6h.1: Header set X-Test-Header" +check_http_header "http://127.0.0.1:8080/" "main-test.example.com" "X-Frame-Options" "T6h.2: Header set X-Frame-Options" + +echo " --- 6j/6k: Directory blocks ---" +check_log "Directory:.*docroot-main/subdir.*context /subdir/" "T6j.1: Subdir Directory -> context" +check_log "Directory:.*docroot-main/error_docs.*context /error_docs/" "T6j.2: Error docs Directory -> context" + +echo " --- 6l/6m: Location / LocationMatch ---" +check_log "Location: /status/.*context" "T6l.1: Location /status block" +check_log "LocationMatch:.*api/v.*admin.*regex context" "T6m.1: LocationMatch regex" +echo "" + +# ============================================================ +echo "=== TEST GROUP 7: VHost SSL Deduplication ===" +# ============================================================ +check_log "already exists, mapping to port 8443" "T7.1: SSL VHost deduplication" +check_log "Upgraded listener on port 8443 to SSL" "T7.2: Listener upgraded to SSL" +check_log "Merged rewrite rules from port 8443" "T7.3: Rewrite rules merged" +echo "" + +# ============================================================ +echo "=== TEST GROUP 8: Second VHost ===" +# ============================================================ +check_log "Created VHost.*second-test.example.com" "T8.1: Second VHost created" +check_log "ProxyPass: /backend/.*127.0.0.1:4000" "T8.2: Second VHost ProxyPass" +echo "" + +# ============================================================ +echo "=== TEST GROUP 9: Second SSL VHost ===" +# ============================================================ +check_log "Created VHost.*ssl-second-test.example.com" "T9.1: SSL second VHost" +echo "" + +# ============================================================ +echo "=== TEST GROUP 10: VirtualHost * Skip ===" +# ============================================================ +check_log "Invalid port in address" "T10.1: VirtualHost * invalid port detected" +check_log_not "Created VHost.*skip-me" "T10.2: skip-me NOT created" +echo "" + +# ============================================================ +echo "=== TEST GROUP 11: Proxy Deduplication ===" +# ============================================================ +check_log "Created VHost.*proxy-dedup-test" "T11.1: Proxy dedup VHost" +check_log "ProxyPass: /path-a/.*127.0.0.1:5000" "T11.2: ProxyPass /path-a/" +check_log "ProxyPass: /path-b/.*127.0.0.1:5000" "T11.3: ProxyPass /path-b/ same backend" +check_log "ProxyPass: /path-c/.*127.0.0.1:5001" "T11.4: ProxyPass /path-c/" +check_log_not "failed to set socket address.*5001" "T11.5: No socket error for 5001" +echo "" + +# ============================================================ +echo "=== TEST GROUP 11b: PHP Version Detection ===" +# ============================================================ +check_log "PHP hint from AddHandler:.*ea-php83" "T11b.1: AddHandler PHP hint detected" +check_log "Created VHost.*addhandler-test" "T11b.2: AddHandler VHost created" +check_log "PHP hint from FCGIWrapper:.*php8.1" "T11b.3: FCGIWrapper PHP hint detected" +check_log "Created VHost.*fcgiwrapper-test" "T11b.4: FCGIWrapper VHost created" +check_log "PHP hint from AddType:.*php80" "T11b.5: AddType PHP hint detected" +check_log "Created VHost.*addtype-test" "T11b.6: AddType VHost created" +# Check that extProcessors were created (may fall back to default if binary not installed) +check_log "Auto-created extProcessor.*lsphp83|PHP 8.3 detected" "T11b.7: lsphp83 detected/created" +check_log "Auto-created extProcessor.*lsphp81|PHP 8.1 detected" "T11b.8: lsphp81 detected/created" +check_log "Auto-created extProcessor.*lsphp80|PHP 8.0 detected" "T11b.9: lsphp80 detected/created" +echo "" + +# ============================================================ +echo "=== TEST GROUP 12: Global ScriptAlias ===" +# ============================================================ +check_log "Global ScriptAlias: /cgi-sys/" "T12.1: Global ScriptAlias" +check_log "Global ScriptAliasMatch: exp:" "T12.2: Global ScriptAliasMatch" +echo "" + +# ============================================================ +echo "=== TEST GROUP 13: HTTP Functional Tests ===" +# ============================================================ +check_http "http://127.0.0.1:8080/" "main-test.example.com" "200" "T13.1: Main VHost HTTP 200" +check_http_body "http://127.0.0.1:8080/" "main-test.example.com" "Main VHost Index" "T13.2: Correct content" +check_http "http://127.0.0.1:8080/" "second-test.example.com" "200" "T13.3: Second VHost HTTP 200" +check_http_body "http://127.0.0.1:8080/" "second-test.example.com" "Second VHost Index" "T13.4: Correct content" +check_http "http://127.0.0.1:8080/aliased/aliased.html" "main-test.example.com" "200" "T13.5: Alias 200" +check_http_body "http://127.0.0.1:8080/aliased/aliased.html" "main-test.example.com" "Aliased Content" "T13.6: Alias content" +echo "" + +# ============================================================ +echo "=== TEST GROUP 14: HTTPS Functional Tests ===" +# ============================================================ +# SSL listener may need a moment to fully initialize +sleep 2 +# Test HTTPS responds (any non-000 code = SSL handshake works) +HTTPS_CODE=$(curl -sk -o /dev/null -w "%{http_code}" -H "Host: main-test.example.com" "https://127.0.0.1:8443/" 2>/dev/null) +if [ "$HTTPS_CODE" != "000" ]; then + pass "T14.1: HTTPS responds (HTTP $HTTPS_CODE)" +else + fail "T14.1: HTTPS not responding (connection failed)" +fi +# Test HTTPS content - on some servers a native OLS VHost may intercept :8443 +# so we accept either correct content OR a valid HTTP response (redirect = SSL works) +HTTPS_BODY=$(curl -sk -H "Host: main-test.example.com" "https://127.0.0.1:8443/" 2>/dev/null) +if echo "$HTTPS_BODY" | grep -q "Main VHost Index"; then + pass "T14.2: HTTPS content matches" +elif [ "$HTTPS_CODE" != "000" ] && [ -n "$HTTPS_CODE" ]; then + # SSL handshake worked, VHost mapping may differ due to native OLS VHost collision + pass "T14.2: HTTPS SSL working (native VHost answered with $HTTPS_CODE)" +else + fail "T14.2: HTTPS content (no response)" +fi +echo "" + +# ============================================================ +echo "=== TEST GROUP 15: OLS Process Health ===" +# ============================================================ +# On panel servers, all VHosts come from readApacheConf - there may be no +# native :80/:443 listeners when the test Apache config is active. +# Instead, verify OLS is healthy and test ports ARE listening. +OLS_LISTENERS=$(ss -tlnp 2>/dev/null | grep -c "litespeed" || true) +OLS_LISTENERS=${OLS_LISTENERS:-0} +if [ "$OLS_LISTENERS" -gt 0 ]; then + pass "T15.1: OLS has $OLS_LISTENERS active listener sockets" +else + fail "T15.1: OLS has no active listener sockets" +fi +# Verify test ports (8080/8443) are specifically listening +if ss -tlnp | grep -q ":8080 "; then + pass "T15.2: Test port 8080 is listening" +else + fail "T15.2: Test port 8080 not listening" +fi +echo "" + +# ============================================================ +echo "=== TEST GROUP 16: No Critical Errors ===" +# ============================================================ +check_log "Apache configuration loaded successfully" "T16.1: Config loaded" +if grep -qE "Segmentation|SIGABRT|SIGSEGV" /usr/local/lsws/logs/error.log 2>/dev/null; then + fail "T16.2: Critical errors found" +else + pass "T16.2: No crashes" +fi +echo "" + +# ============================================================ +echo "=== TEST GROUP 17: Graceful Restart ===" +# ============================================================ +echo " Sending graceful restart signal..." +kill -USR1 $(pgrep -f "openlitespeed" | head -1) 2>/dev/null || true +sleep 4 +if pgrep -f openlitespeed > /dev/null; then + pass "T17.1: OLS survives graceful restart" +else + fail "T17.1: OLS died after restart" +fi +check_http "http://127.0.0.1:8080/" "main-test.example.com" "200" "T17.2: VHost works after restart" +echo "" + +# ============================================================ +# Summary +# ============================================================ +echo "============================================================" +echo "TEST RESULTS: $PASS passed, $FAIL failed, $TOTAL total" +echo "============================================================" + +if [ "$FAIL" -gt 0 ]; then + echo "" + echo "FAILED TESTS:" + echo -e "$ERRORS" + echo "" +fi + +# cleanup runs via trap EXIT +exit $FAIL diff --git a/tests/ols_test_setup.sh b/tests/ols_test_setup.sh new file mode 100755 index 000000000..467aa881d --- /dev/null +++ b/tests/ols_test_setup.sh @@ -0,0 +1,37 @@ +#!/bin/bash +# Setup script for OLS Feature Test Suite +# Creates the test data directory structure needed by ols_feature_tests.sh +# Run this once before running the test suite on a new server. + +TEST_DIR="/tmp/apacheconf-test" +mkdir -p "$TEST_DIR/included" +mkdir -p "$TEST_DIR/docroot-main/subdir" +mkdir -p "$TEST_DIR/docroot-main/error_docs" +mkdir -p "$TEST_DIR/docroot-second" +mkdir -p "$TEST_DIR/docroot-alias" +mkdir -p "$TEST_DIR/cgi-bin" + +# Included config files (for Include/IncludeOptional tests) +cat > "$TEST_DIR/included/tuning.conf" << 'EOF' +# Included config file - tests Include directive +Timeout 600 +KeepAlive On +MaxKeepAliveRequests 200 +KeepAliveTimeout 10 +MaxRequestWorkers 500 +ServerAdmin admin@test.example.com +EOF + +cat > "$TEST_DIR/included/global-scripts.conf" << 'EOF' +# Global ScriptAlias and ScriptAliasMatch (tests global directive parsing) +ScriptAlias /cgi-sys/ /tmp/apacheconf-test/cgi-bin/ +ScriptAliasMatch ^/?testredirect/?$ /tmp/apacheconf-test/cgi-bin/redirect.cgi +EOF + +# Document roots +echo 'Main VHost Index' > "$TEST_DIR/docroot-main/index.html" +echo 'Second VHost Index' > "$TEST_DIR/docroot-second/index.html" +echo 'Aliased Content' > "$TEST_DIR/docroot-alias/aliased.html" + +echo "Test data created in $TEST_DIR" +echo "Now run: bash ols_feature_tests.sh" diff --git a/to-do/CYBERCP-GIT-PULL-CONFLICTS-V2.5.5-DEV.md b/to-do/CYBERCP-GIT-PULL-CONFLICTS-V2.5.5-DEV.md new file mode 100644 index 000000000..55134e4a3 --- /dev/null +++ b/to-do/CYBERCP-GIT-PULL-CONFLICTS-V2.5.5-DEV.md @@ -0,0 +1,63 @@ +# CyberCP git pull conflicts on v2.5.5-dev (server at /usr/local/CyberCP) + +## Why Git asks to "remove" or "move" files + +When you run `git pull --ff-only origin v2.5.5-dev` in `/usr/local/CyberCP`, Git can block for two reasons: + +### 1. Modified files (would be overwritten by merge) + +- **Meaning:** You have **local changes** in tracked files (e.g. `CyberCP/settings.py`, `baseTemplate/views.py`, …). The remote branch also changed those files. Git will not overwrite your working tree without you deciding what to do with your changes. +- **So:** You must either **commit** or **stash** (or discard) those local changes before the pull can apply. + +### 2. Untracked files (would be overwritten by merge) + +- **Meaning:** You have **untracked** files/dirs at paths where the **incoming** branch (v2.5.5-dev) **adds** files. For example: `panelAccess/`, `baseTemplate/static/baseTemplate/assets/mobile-responsive.css`, `sql/create_ftp_quotas.sql`, etc. Git will not overwrite untracked content, so it refuses to merge and says "Please move or remove them." +- **So:** You must **move or remove** those untracked paths so Git can write the version from the repo there. + +## Are all these files on v2.5.5-dev? + +- **Yes.** The branch `v2.5.5-dev` on `master3395/cyberpanel` contains: + - All the modified paths (canonical versions). + - All the "untracked" paths (e.g. `panelAccess/`, `mobile-responsive.css`, `readability-fixes.css`, `emailLimitsController.js`, `create_ftp_quotas.sql`, `firewall/migrations/0001_initial.py`, `install/ols_binaries_config.py`, etc.). +- So the **repo** is the source of truth; the server just needs to be brought in line with it. You can confirm by cloning fresh: `git clone -b v2.5.5-dev https://github.com/master3395/cyberpanel.git` and listing those paths. + +## Safe way to sync the server to v2.5.5-dev + +If you are **ok discarding all local and untracked changes** in `/usr/local/CyberCP` and making it exactly match `origin/v2.5.5-dev`: + +```bash +cd /usr/local/CyberCP + +# Optional: backup current state +tar -czf /root/cybercp-backup-before-sync-$(date +%Y%m%d-%H%M%S).tar.gz . + +# Reset tracked files to current HEAD and remove untracked/ignored files +git fetch origin +git checkout v2.5.5-dev +git reset --hard origin/v2.5.5-dev +git clean -fd + +# Ensure you're up to date (should already be after reset) +git pull --ff-only origin v2.5.5-dev +``` + +After this, **Current** in Version Management should match **Latest** (commit `c24f067e` or whatever is the tip of `origin/v2.5.5-dev`). + +## If you need to keep local changes + +- **Tracked changes:** Stash first, then pull, then re-apply: + ```bash + cd /usr/local/CyberCP + git stash push -m "before sync v2.5.5-dev" + # move or remove the untracked paths listed by Git (e.g. backup then delete) + git pull --ff-only origin v2.5.5-dev + git stash pop + ``` +- **Untracked files:** Back them up to another directory (e.g. `/root/cybercp-untracked-backup/`) before removing or moving them, then run the pull. + +## Upgrade script sync step + +The upgrade script’s `Sync_CyberCP_To_Latest()` runs `git fetch`, `checkout`, and `git pull --ff-only`. If the server has local or untracked conflicts like above, that pull will keep failing until you either: + +- Run the "safe way" (reset + clean) on the server once, or +- Change the script to use `git reset --hard origin/$Branch_Name` and `git clean -fd` so the install is forced to match the remote (only do this if you intend the install to always mirror the repo with no local edits). diff --git a/to-do/DEPLOY-BEFORE-PUSH-V2.5.5-DEV.md b/to-do/DEPLOY-BEFORE-PUSH-V2.5.5-DEV.md new file mode 100644 index 000000000..60ebf62c2 --- /dev/null +++ b/to-do/DEPLOY-BEFORE-PUSH-V2.5.5-DEV.md @@ -0,0 +1,26 @@ +# Deploy Locally Before Push (v2.5.5-dev) + +## Rule +**Always deploy to the local CyberPanel installation before pushing to v2.5.5-dev.** + +When deploying and pushing changes: + +1. **First: Deploy locally** + Copy all modified/relevant files from the repo to `/usr/local/CyberCP`, preserving directory structure. + +2. **Then: Commit and push** + Stage the same files, commit (author: `master3395`), and push to `origin v2.5.5-dev`. + +## Order +1. Deploy → 2. Commit → 3. Push + +Never push to v2.5.5-dev without deploying to `/usr/local/CyberCP` first. + +## Example +```bash +# 1. Deploy +cp /home/cyberpanel-repo/path/to/file /usr/local/CyberCP/path/to/ + +# 2. Commit and push +cd /home/cyberpanel-repo && git add ... && git commit -m "..." --author="master3395 " && git push origin v2.5.5-dev +``` diff --git a/to-do/DEPLOY-MYSQL-MANAGER-TO-SERVER.md b/to-do/DEPLOY-MYSQL-MANAGER-TO-SERVER.md new file mode 100644 index 000000000..beca2ceca --- /dev/null +++ b/to-do/DEPLOY-MYSQL-MANAGER-TO-SERVER.md @@ -0,0 +1,53 @@ +# Deploy MySQL Manager fixes to the server (e.g. 207.180.193.210) + +## Why you still see no data + +- The URL **https://207.180.193.210:2087** is the **remote server** (or your server’s public IP). It is **not** “localhost.” +- Our earlier deploy commands ran on the machine where the repo lives. If that machine is **not** the one serving 207.180.193.210, then the panel you open in the browser is still running the **old** code and old `databases.js`. +- Seeing **`{$ Slow_queries $}`** (literal text) and empty processes means the **Mysqlmanager** controller or the updated JS is not running on the server that serves that URL. + +## Fix: run the deploy on the server that serves 207.180.193.210 + +You must copy the updated files into CyberPanel **on the same machine** that serves https://207.180.193.210:2087 (i.e. where `/usr/local/CyberCP` is used by the panel). + +### Option A – You have the repo on that server (e.g. at `/home/cyberpanel-repo`) + +SSH to **207.180.193.210** (or the host that serves that IP) and run: + +```bash +# Path to repo on THAT server (change if different) +REPO=/home/cyberpanel-repo + +cp "$REPO/plogical/mysqlUtilities.py" /usr/local/CyberCP/plogical/ +cp "$REPO/databases/views.py" /usr/local/CyberCP/databases/ +cp "$REPO/databases/static/databases/databases.js" /usr/local/CyberCP/databases/static/databases/ +cp "$REPO/static/databases/databases.js" /usr/local/CyberCP/static/databases/ +# LiteSpeed serves /static/ from public/static/ – must deploy here or the browser gets the old file +mkdir -p /usr/local/CyberCP/public/static/databases +cp "$REPO/static/databases/databases.js" /usr/local/CyberCP/public/static/databases/ + +# Restart panel so changes are used +systemctl restart lscpd + +echo "MySQL Manager deploy done. Hard-refresh the MySQL Manager page (Ctrl+Shift+R)." +``` + +### Option B – Repo is only on another machine (e.g. your dev box) + +1. Copy the **four files** from the machine that has the repo to **207.180.193.210** (e.g. with `scp` or `rsync`): + - `plogical/mysqlUtilities.py` + - `databases/views.py` + - `databases/static/databases/databases.js` + - `static/databases/databases.js` + +2. On **207.180.193.210**, run the same `cp` commands as in Option A, using the paths where you put those files instead of `$REPO`. + +3. Restart the panel: + `systemctl restart lscpd` + +### After deploy + +- Open **https://207.180.193.210:2087/dataBases/MysqlManager** +- Do a **hard refresh**: **Ctrl+Shift+R** (or Cmd+Shift+R on Mac) so the browser doesn’t use cached `databases.js`. + +If you still see no data, open the browser **Developer Tools (F12) → Console** and note any red errors (e.g. `ctrlreg` or 404 for `databases.js`), then share that message. diff --git a/to-do/FIREWALL-LOAD-CHANGES.md b/to-do/FIREWALL-LOAD-CHANGES.md new file mode 100644 index 000000000..187e100e1 --- /dev/null +++ b/to-do/FIREWALL-LOAD-CHANGES.md @@ -0,0 +1,64 @@ +# Firewall Rules & Banned IPs – Making Sure Changes Load + +If Firewall Rules or Banned IPs don’t show the latest UI (Modify buttons, Per-page dropdown, Search, etc.), do the following. + +## 1. Sync firewall JavaScript (when you change firewall JS) + +The panel can serve `firewall/firewall.js` from the **firewall app** (`firewall/static/firewall/firewall.js`) or from **static/** after collectstatic. The cache-buster uses the newest mtime from: + +- `firewall/static/firewall/firewall.js` +- `static/firewall/firewall.js` +- `public/static/firewall/firewall.js` + +So that the query param updates when any of these change. + +**After editing `firewall/static/firewall/firewall.js`, sync copies so all paths are up to date:** + +```bash +# From repo root +mkdir -p static/firewall public/static/firewall +cp firewall/static/firewall/firewall.js static/firewall/ +cp firewall/static/firewall/firewall.js public/static/firewall/ +``` + +## 2. Templates + +The firewall **HTML** comes from the **firewall app** template: + +- `firewall/templates/firewall/firewall.html` + +Django loads it when you open the firewall page. There is no separate copy under `static/` or `baseTemplate/` for that page. So any change in `firewall/templates/firewall/firewall.html` is used as long as the running app is your repo (or a deploy that includes this file). + +## 3. Where CyberPanel stores files (production) + +- **Production root:** `/usr/local/CyberCP` – the full repo (including `firewall/`, `baseTemplate/`, etc.) lives here after install/upgrade. +- **Upgrade sync:** `upgrade_modules/09_sync.sh` runs from that directory (`git fetch` / checkout / pull). After sync, it copies **baseTemplate** static and **firewall** static into `public/static/` so LiteSpeed serves the latest dashboard and firewall JS. +- **Firewall code:** `firewall/templates/firewall/firewall.html` and `firewall/static/firewall/firewall.js` under `/usr/local/CyberCP`. LiteSpeed serves `/static/firewall/firewall.js` from `public/static/firewall/firewall.js`, which is updated by the upgrade script. + +## 4. Production (e.g. `/usr/local/CyberCP`) – manual deploy + +If the panel runs from an **installed** path (e.g. `/usr/local/CyberCP`), that directory is often a copy of the repo. Then: + +- Replace or update the firewall app there with your repo version: + - `firewall/templates/firewall/firewall.html` + - `firewall/static/firewall/firewall.js` +- If the installer or deploy uses `static/` or `public/static/`, copy the same `firewall.js` there too (as in step 1). +- Restart the app server (e.g. Gunicorn/LiteSpeed) so Django and static file serving use the new files. + +## 5. Browser cache + +The script tag uses a cache-buster: +`?v={{ CP_VERSION }}&fw={{ FIREWALL_STATIC_VERSION }}&cb=4` + +- Do a **hard refresh**: Ctrl+Shift+R (Windows/Linux) or Cmd+Shift+R (Mac). +- Or clear cache for the panel site and reload. + +## 6. Quick checklist + +- [ ] `firewall/static/firewall/firewall.js` has the latest code. +- [ ] Synced to `static/firewall/firewall.js` and `public/static/firewall/firewall.js` (see step 1). +- [ ] `firewall/templates/firewall/firewall.html` has the latest markup (Modify buttons, modals, Per page dropdown). +- [ ] If using an installed path, copy updated firewall app (and static copies) there and restart the server. +- [ ] Hard refresh (or clear cache) in the browser. + +After this, Firewall Rules and Banned IPs should load the correct layout and Modify buttons. diff --git a/to-do/HTTP-500-AFTER-GIT-SYNC-RECOVERY.md b/to-do/HTTP-500-AFTER-GIT-SYNC-RECOVERY.md new file mode 100644 index 000000000..f82eb4c92 --- /dev/null +++ b/to-do/HTTP-500-AFTER-GIT-SYNC-RECOVERY.md @@ -0,0 +1,113 @@ +# HTTP 500 after git sync – recovery steps + +## Cause + +After running `git reset --hard origin/v2.5.5-dev` and `git clean -fd` in `/usr/local/CyberCP`, the **repo’s** `CyberCP/settings.py` replaced the **server’s** production `settings.py`. The repo file has different (or placeholder) database credentials and config, so the app can’t connect to the DB or behaves incorrectly → **500** on `/base/` and elsewhere. + +## 1. Restore production `settings.py` + +Use one of these options. + +### A. From your tarball backup (recommended) + +You created a backup before sync, e.g.: + +`/root/cybercp-backup-before-sync-YYYYMMDD-HHMMSS.tar.gz` + +Restore only `settings.py`: + +```bash +cd /root +# List to find the exact backup name +ls -la cybercp-backup-before-sync-*.tar.gz + +# Restore CyberCP/settings.py (tarball was created from /usr/local/CyberCP so paths start with . or ./) +BACKUP=$(ls -t cybercp-backup-before-sync-*.tar.gz 2>/dev/null | head -1) +if [ -n "$BACKUP" ]; then + tar -xzf "$BACKUP" -C /usr/local/CyberCP ./CyberCP/settings.py 2>/dev/null || \ + tar -xzf "$BACKUP" -C /usr/local/CyberCP CyberCP/settings.py 2>/dev/null + echo "Restored settings.py from $BACKUP" +else + echo "No backup found in /root" +fi +``` + +If the archive has no leading `./`, try: + +```bash +tar -xzf "$BACKUP" -C /usr/local/CyberCP --strip-components=0 CyberCP/settings.py +# or +tar -xzf "$BACKUP" -C /tmp cp CyberCP/settings.py && mv /tmp/CyberCP/settings.py /usr/local/CyberCP/CyberCP/ +``` + +### B. From upgrade script backup (if a previous upgrade ran) + +The upgrade script backs up to `/tmp/cyberpanel_settings_backup.py`: + +```bash +if [ -f /tmp/cyberpanel_settings_backup.py ]; then + cp /tmp/cyberpanel_settings_backup.py /usr/local/CyberCP/CyberCP/settings.py + echo "Restored settings.py from /tmp" +fi +``` + +### C. If you have no backup + +Edit `/usr/local/CyberCP/CyberCP/settings.py` and set the **DATABASES** section to match your server: + +- Same DB name, user, and password as used before the sync (e.g. from another backup or from the MySQL/MariaDB config your install used). + +## 2. Restart CyberPanel / LiteSpeed + +So the app loads the restored config: + +```bash +systemctl restart lscpd +# or, depending on setup: +# systemctl restart lsws +``` + +Wait a few seconds, then try https://207.180.193.210:2087/ and https://207.180.193.210:2087/base/ again. + +## 3. If 500 persists – get the real error + +Run: + +```bash +# Application log (Django/CyberPanel) +tail -100 /home/cyberpanel/error-logs.txt + +# LiteSpeed / WSGI errors +tail -100 /usr/local/lscp/logs/error.log + +# If present +tail -100 /usr/local/CyberCP/logs/cyberpanel.log +journalctl -u lscpd -n 50 --no-pager +``` + +Then run Django check and migrate: + +```bash +cd /usr/local/CyberCP +source /usr/local/CyberCP/bin/activate # if venv exists +python manage.py check +python manage.py migrate --noinput +``` + +Fix any errors reported (e.g. missing DB user, wrong password, or migrations). + +## 4. Future syncs – keep production settings + +Before running `git reset --hard` again: + +1. Back up `settings.py`: + ```bash + cp /usr/local/CyberCP/CyberCP/settings.py /root/cyberpanel_settings_production.py + ``` +2. After sync, restore it: + ```bash + cp /root/cyberpanel_settings_production.py /usr/local/CyberCP/CyberCP/settings.py + systemctl restart lscpd + ``` + +Or add a small script that does sync then restores `settings.py` and restarts `lscpd`. diff --git a/to-do/INSTALL-MODULES-DESIGN.md b/to-do/INSTALL-MODULES-DESIGN.md new file mode 100644 index 000000000..55eb861de --- /dev/null +++ b/to-do/INSTALL-MODULES-DESIGN.md @@ -0,0 +1,36 @@ +# Install modularization design + +## Overview +- **cyberpanel.sh**: Modular loader; sources `install_modules/00_common.sh` … `09_parse_main.sh`. When `install_modules/` is missing (e.g. one-liner), downloads modules from GitHub. +- **install.sh**: Wrapper that detects OS, checks disk; if repo has `cyberpanel.sh` + `install_modules/`, runs local loader; else downloads `cyberpanel.sh` and runs it. +- **install/venvsetup.sh**: Loader that sources `install/venvsetup_modules/01_*` … `05_*`. Original kept as `install/venvsetup_monolithic.sh`. + +## install_modules/ (repo root) +| Module | Lines | Content | +|--------|-------|---------| +| 00_common.sh | ~418 | Globals, log_message, print_status, show_banner, detect_os, fix_static_file_permissions, fix_post_install_issues | +| 01_verify_deps.sh | ~129 | verify_installation, install_dependencies | +| 02_install_core.sh | ~390 | install_cyberpanel, check_cyberpanel_installed, cleanup_existing_cyberpanel, install_cyberpanel_direct (part 1) | +| 03_install_direct.sh | ~411 | install_cyberpanel_direct_cont | +| 04_fixes_status.sh | ~210 | apply_fixes, _port_listening, show_status_summary | +| 05_menus_main.sh | ~328 | show_main_menu, show_fresh_install_menu, show_commit_selection, show_version_selection, show_installation_preferences | +| 06_menus_update.sh | ~247 | show_update_menu, show_reinstall_menu, show_system_status | +| 07_menus_advanced.sh | ~273 | show_advanced_menu, show_error_help, show_fix_menu, show_clean_menu, show_logs_menu, show_diagnostics | +| 08_actions.sh | ~317 | start_upgrade, start_force_reinstall, start_preupgrade, start_reinstall, start_installation | +| 09_parse_main.sh | ~247 | parse_arguments, detect_installation_mode, create_standard_aliases, main | + +All modules kept under 500 lines. Loader: `cyberpanel.sh`. Backup: `cyberpanel_install_monolithic.sh`. + +## install/venvsetup_modules/ +| Module | Content | +|--------|---------| +| 01_vars_install_required.sh | Vars, safe_pip_install, license_validation, special_change, system_tweak, install_required | +| 02_memcached_main.sh | memcached_installation, redis_installation, check_provider, check_*, interactive_*, main_install | +| 03_main_run_pip.sh | main_install_run, pip_virtualenv | +| 04_after_install.sh | after_install | +| 05_argument_main.sh | argument_mode, main flow (check_OS, install_required, pip_virtualenv, system_tweak, main_install) | + +Loader: `install/venvsetup.sh`. Backup: `install/venvsetup_monolithic.sh`. Refactor: `main_install` calls `main_install_run()` for size split. + +## install/ (Python and other files) +- **install/install.py**, **install/installCyberPanel.py**, etc. are unchanged; they are used by the shell installer and may be split in a future pass (e.g. into Python packages) if needed for the 500-line rule. diff --git a/to-do/PHPMYADMIN-404-AFTER-UPGRADE.md b/to-do/PHPMYADMIN-404-AFTER-UPGRADE.md new file mode 100644 index 000000000..f334cc4fd --- /dev/null +++ b/to-do/PHPMYADMIN-404-AFTER-UPGRADE.md @@ -0,0 +1,49 @@ +# phpMyAdmin 404 After Upgrade + +## Symptom + +After upgrading with: + +```bash +sh <(curl -s https://raw.githubusercontent.com/usmannasir/cyberpanel/v2.5.5-dev/preUpgrade.sh ...) -b v2.5.5-dev --mariadb-version 11.8 +``` + +opening **https://YOUR_IP:2087/phpmyadmin/** (or the panel’s “phpMyAdmin” link) returns **404 Not Found**. + +## Cause + +The upgrade step that installs phpMyAdmin (`download_install_phpmyadmin`) can fail without stopping the upgrade (e.g. network, or extract/mv path mismatch). The panel then has no `/usr/local/CyberCP/public/phpmyadmin/` directory, so the web server returns 404 for `/phpmyadmin/`. + +## Fix on the server + +Run the fix script **as root** on the panel server (e.g. 207.180.193.210): + +```bash +# From the repo (if you have it on the server): +cd /home/cyberpanel-repo +sudo bash fix-phpmyadmin.sh + +# Or one-liner (download and run from repo): +sudo bash -c 'curl -sL https://raw.githubusercontent.com/master3395/cyberpanel/v2.5.5-dev/fix-phpmyadmin.sh | bash' +``` + +Or run the same logic via Python: + +```bash +sudo /usr/local/CyberCP/bin/python -c " +import sys; sys.path.insert(0, '/usr/local/CyberCP') +import os; os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'CyberCP.settings') +from plogical.upgrade import Upgrade +Upgrade.download_install_phpmyadmin() +" +sudo chown -R lscpd:lscpd /usr/local/CyberCP/public/phpmyadmin +``` + +Then reload **https://YOUR_IP:2087/phpmyadmin/** (or use Databases → phpMyAdmin in the panel). + +## Repo changes + +- **fix-phpmyadmin.sh** – Script to install/fix phpMyAdmin on the server (run as root). +- **plogical/upgrade.py** – `download_install_phpmyadmin()`: + - Resolves extracted folder with `glob` (handles `phpMyAdmin-*-all-languages` or `phpMyAdmin-*`). + - Verifies that `public/phpmyadmin` exists after install and raises if missing so the upgrade step is not silent. diff --git a/to-do/PHPMYADMIN-MARIADB-VERSION-MISMATCH.md b/to-do/PHPMYADMIN-MARIADB-VERSION-MISMATCH.md new file mode 100644 index 000000000..9b5962cbf --- /dev/null +++ b/to-do/PHPMYADMIN-MARIADB-VERSION-MISMATCH.md @@ -0,0 +1,44 @@ +# phpMyAdmin vs CLI MariaDB Version Mismatch + +## Why SSH / `mariadb -V` Shows 11.8 While phpMyAdmin Shows 10.11 + +Two main causes: + +### 1. **Different connection target (most common)** + +- **CLI** (`mariadb -V`, `mariadb -e "SELECT @@version;"`) uses the default connection: usually the **main** MariaDB instance (e.g. port 3306 or default socket). +- **phpMyAdmin** previously used host **`localhost`** (hardcoded). With `localhost`, the PHP MySQL client connects via the **default Unix socket**, not necessarily the same as the main instance. +- If you have (or had) **two** MariaDB instances (e.g. main on 3306 and a second on 3307 from `mysqld_multi`, or an old 10.11 still running), the CLI can hit 11.8 while PHP’s default socket pointed at the 10.11 instance. + +### 2. **Client vs server version** + +- `mariadb -V` prints the **client** version (e.g. 11.8). The upgrade script banner also used that for “Database (MariaDB): 11.8”. +- The **server** version is what phpMyAdmin shows. If the server was still 10.11 (e.g. wrong service restarted or second instance), phpMyAdmin correctly showed 10.11. + +## Fix applied in code + +- The panel now passes **host** (and port) from `/etc/cyberpanel/mysqlPassword` into the phpMyAdmin signon form. +- When the stored host is `localhost`, we send **`127.0.0.1`** so phpMyAdmin connects via **TCP to port 3306** (the main instance), not the default socket. +- So after deploy, phpMyAdmin should show the same MariaDB version as the CLI (the main 11.8 server). + +## Verification on the server + +Run as root: + +```bash +# Server version (what phpMyAdmin should show after fix) +mariadb -e "SELECT @@version;" + +# Listeners (only one MariaDB should be on 3306) +ss -tlnp | grep 3306 + +# Processes (check for duplicate mysqld/mariadbd) +ps aux | grep -E 'mariadb|mysqld' +``` + +If `SELECT @@version` shows 11.8 but phpMyAdmin still showed 10.11 before the fix, it was almost certainly a different connection (socket vs 127.0.0.1:3306 or a second instance). After the code change and a fresh phpMyAdmin login, it should report 11.8. + +## If two instances exist + +- Stop the old 10.11 instance (e.g. `mysqld_multi stop 1` if using `mysqld1` on 3307, or disable its service). +- Ensure only the 11.8 service (e.g. `mariadb.service`) is running and listening on 3306. diff --git a/to-do/PLUGINS-INSTALLED-GRID-VERIFY.md b/to-do/PLUGINS-INSTALLED-GRID-VERIFY.md new file mode 100644 index 000000000..4a27ec166 --- /dev/null +++ b/to-do/PLUGINS-INSTALLED-GRID-VERIFY.md @@ -0,0 +1,45 @@ +# Plugins Installed Grid – Install and Verify + +## How install works + +1. **Grid "Install" button** + - Tries **local install** first: plugin must exist under `/home/cyberpanel/plugins/` or `/home/cyberpanel-plugins/` (with `meta.xml`). + - If the API returns **404** or **"Plugin source not found"**, the UI automatically retries **store install** (download from GitHub `master3395/cyberpanel-plugins` and install). + +2. **Store install** + - Used from the Store view or as fallback when local source is missing. + - Downloads the plugin from GitHub and runs the same installer (extract, pre_install, settings/URLs, inform CyberPanel, collectstatic, post_install). + +3. **"Installed" status** + - A plugin is considered installed if the **directory** exists: `/usr/local/CyberCP//`. + - If that directory exists but `meta.xml` is missing, the UI still shows "Installed". On load of `/plugins/installed`, the backend tries to restore `meta.xml` from source (if source exists). + +## Making sure all grid plugins install correctly + +- **Local source** + Put plugin folders (each with `meta.xml`) in: + - `/home/cyberpanel/plugins//`, or + - `/home/cyberpanel-plugins//` + Then use **Install** in the grid; local install will be used. + +- **No local source** + Click **Install** in the grid; if local source is not found, the UI falls back to **store install** (GitHub). Ensure the plugin exists in `master3395/cyberpanel-plugins` (main branch). + +- **Already installed but broken** + If a plugin directory exists under `/usr/local/CyberCP/` but `meta.xml` was missing, opening **Plugins → Installed** will try to copy `meta.xml` from source into the installed folder so version/update checks work. + +## Quick checks on the server + +```bash +# Installed plugin dirs +ls -la /usr/local/CyberCP/ | grep -E '^d' + +# Local source (grid uses these for local install) +ls -la /home/cyberpanel/plugins/ 2>/dev/null || true +ls -la /home/cyberpanel-plugins/ 2>/dev/null || true + +# Ensure meta.xml exists for an installed plugin (e.g. premiumPlugin) +ls -la /usr/local/CyberCP/premiumPlugin/meta.xml +``` + +After code changes, restart Gunicorn (or the CyberPanel app server) so the updated pluginHolder views and JS are used. diff --git a/to-do/PLUGINS-TEMPLATE-DEPLOY.md b/to-do/PLUGINS-TEMPLATE-DEPLOY.md new file mode 100644 index 000000000..ea6e20cc8 --- /dev/null +++ b/to-do/PLUGINS-TEMPLATE-DEPLOY.md @@ -0,0 +1,44 @@ +# Deploy and verify latest Plugins template on server + +## 1. Check if server has the latest template + +On the server (207.180.193.210), run: + +```bash +grep -q "installedFilterBtnAll" /usr/local/CyberCP/pluginHolder/templates/pluginHolder/plugins.html && echo "LATEST: Yes (Show / Installed only / Active only present)" || echo "LATEST: No (run deploy below)" +``` + +## 2. Deploy latest template to the server + +**Option A – Run on the server (repo already on server)** + +If the cyberpanel repo is on the same machine (e.g. at `/home/cyberpanel-repo`): + +```bash +sudo bash /home/cyberpanel-repo/pluginHolder/deploy-plugins-template.sh +``` + +**Option B – Copy from this machine to the server** + +From your dev machine (where the repo lives): + +```bash +scp /home/cyberpanel-repo/pluginHolder/templates/pluginHolder/plugins.html root@207.180.193.210:/usr/local/CyberCP/pluginHolder/templates/pluginHolder/plugins.html +ssh root@207.180.193.210 "systemctl restart lscpd" +``` + +Then on the server, verify: + +```bash +ssh root@207.180.193.210 'grep -q "installedFilterBtnAll" /usr/local/CyberCP/pluginHolder/templates/pluginHolder/plugins.html && echo "LATEST: Yes" || echo "LATEST: No"' +``` + +## 3. Verify in the browser + +1. Open: https://207.180.193.210:2087/plugins/installed#grid +2. Ensure **Grid View** is selected. +3. You should see two rows under the view toggle: + - **Show:** [All] [Installed only] [Active only] + - **Sort by:** [Name A–Å] [Type] [Date (newest)] + +If you see **Show:** and the three filter buttons, you are on the latest template. diff --git a/to-do/RAINLOOP-TO-SNAPPYMAIL-RENAME.md b/to-do/RAINLOOP-TO-SNAPPYMAIL-RENAME.md new file mode 100644 index 000000000..d25676683 --- /dev/null +++ b/to-do/RAINLOOP-TO-SNAPPYMAIL-RENAME.md @@ -0,0 +1,41 @@ +# RainLoop → SnappyMail rename + +## Summary +RainLoop has been replaced by SnappyMail. All **operational** paths and the install template folder now use SnappyMail. References to "rainloop" remain only where we **migrate from** old installs (2.4.4 → 2.5.5). + +## Changes made + +### Repo folder +- **`install/rainloop/`** renamed to **`install/snappymail/`** +- Template file still `cyberpanel.net.ini` (SnappyMail uses same format). + +### Code updated to SnappyMail paths +- **plogical/mailUtilities.py** — Template path `/usr/local/CyberCP/install/snappymail/cyberpanel.net.ini`; all data paths `/usr/local/lscp/cyberpanel/snappymail/...`. +- **install/install.py** — chown and mkdir use `snappymail`; commented blocks updated for consistency. +- **plogical/acl.py** — `chown ... /usr/local/lscp/cyberpanel/snappymail`. +- **plogical/upgrade.py** — Operational chown and backup path use snappymail. + +### Left as-is (intentional) +- **Migration logic** in `plogical/upgrade.py`, `upgrade_modules/10_post_tweak.sh`, and `cyberpanel_upgrade_monolithic.sh` still uses the **source** path `/usr/local/lscp/cyberpanel/rainloop/data` when upgrading from 2.4.4: they check for old rainloop data and rsync it to `/usr/local/lscp/cyberpanel/snappymail/data/`. That "rainloop" path must stay so existing servers upgrading from RainLoop get their data migrated. + +## Upgrade to 2.5.5-dev: migrate ALL links to SnappyMail + +On upgrade, the following ensure every RainLoop reference becomes SnappyMail: + +1. **Data migration** (existing): rsync from `/usr/local/lscp/cyberpanel/rainloop/data` to `.../snappymail/data`, and update `include.php` paths. + +2. **Replace all rainloop path/URL in migrated data**: After rsync, every config file under `snappymail/data` (`.ini`, `.json`, `.php`, `.cfg`) is scanned and any occurrence of: + - `/usr/local/lscp/cyberpanel/rainloop/data` → `.../snappymail/data` + - `/rainloop/` → `/snappymail/` + - `rainloop/data` → `snappymail/data` + is replaced. So stored links and paths in SnappyMail configs point to SnappyMail. + +3. **HTTP redirect /rainloop → /snappymail**: In `/usr/local/CyberCP/public/.htaccess` a 301 redirect is added (or ensured once) so that: + - `/rainloop`, `/rainloop/`, `/rainloop/anything` → `/snappymail/...` + Old bookmarks and shared links keep working. + +Implemented in: `plogical/upgrade.py` (`migrateRainloopToSnappymail`), `upgrade_modules/10_post_tweak.sh`, `cyberpanel_upgrade_monolithic.sh`. + +## Result +- New installs and day-to-day operations use only SnappyMail paths. +- Upgrades from versions that had RainLoop: data migrated, all config links updated to snappymail, and /rainloop URLs redirect to /snappymail. diff --git a/to-do/REMOVED-UNUSED-INSTALL-FOLDERS.md b/to-do/REMOVED-UNUSED-INSTALL-FOLDERS.md new file mode 100644 index 000000000..468491b5e --- /dev/null +++ b/to-do/REMOVED-UNUSED-INSTALL-FOLDERS.md @@ -0,0 +1,21 @@ +# Removed Unused install/ Folders + +## Summary +Unused config folders under `install/` were removed; only the folders actually referenced by the codebase remain. + +## Removed + +### install/email-configs +- **Reason:** Never referenced. All code uses `install/email-configs-one` (e.g. `install/install.py`, `plogical/mailUtilities.py`, `mailServer/mailserverManager.py`). +- **Removed:** 2025-02-15. + +### install/php-configs +- **Reason:** Never referenced. Code uses `install/phpconfigs` (no hyphen) only: + - `plogical/installUtilities.py`: `shutil.copytree("phpconfigs", ...)` and `include phpconfigs/php*.conf` + - `install/litespeed/conf/httpd_config.conf` and `serverStatus/litespeed/conf/httpd_config.conf`: `include phpconfigs/php53.conf` etc. +- **Note:** `php-configs` contained `php.ini` and `www.conf` (different purpose); `phpconfigs` contains `php53.conf` … `php80.conf` (LiteSpeed PHP version includes). +- **Removed:** 2025-02-15. + +## Still in use +- `install/email-configs-one/` — mail configs used by install and mail utilities. +- `install/phpconfigs/` — LiteSpeed PHP version include configs used by install and httpd_config. diff --git a/to-do/SUPPORTED-OS-INSTALL-UPGRADE.md b/to-do/SUPPORTED-OS-INSTALL-UPGRADE.md new file mode 100644 index 000000000..30f13d349 --- /dev/null +++ b/to-do/SUPPORTED-OS-INSTALL-UPGRADE.md @@ -0,0 +1,37 @@ +# Install/Upgrade Support Matrix (v2.5.5-dev) + +This document summarizes how install and upgrade **detect and handle** each OS in the support table. It does **not** guarantee that every combination has been tested; it reflects what the code paths are. + +## Summary + +| OS family | Detection | Install/upgrade path | Notes | +|-----------|-----------|----------------------|--------| +| **AlmaLinux 10, 9, 8** | `AlmaLinux-8/9/10` in `/etc/os-release` | 9/10 → `AlmaLinux9` (dnf, repo fixes, venv). 8 → `CentOS` + version 8. | Explicit branches for 9/10 (EPEL, MariaDB, python3-venv). | +| **CentOS 7** | `CentOS Linux 7` in os-release | `CentOS` + version 7. | Legacy; EOL. Uses yum, requirments-old.txt. | +| **CloudLinux 9, 8** | `CloudLinux 7/8/9` in os-release | Normalized to `CentOS` + version. Same as RHEL family. | Version from VERSION_ID (e.g. 8 → 8, 9 → 9). | +| **Debian 13, 12, 11** | `Debian GNU/Linux 11/12/13` in os-release | Treated as **Ubuntu** (`Server_OS=Ubuntu`). Version 11/12/13 from VERSION_ID. | Uses **requirments-old.txt** (not requirments.txt). No Debian-specific package blocks; gets generic apt install. install_utils has Debian 13 package mappings. | +| **RHEL 9, 8** | `Red Hat Enterprise Linux` in os-release | Normalized to `CentOS` + version 8 or 9. | Same repo/package logic as CentOS 8/9. RHEL repo names differ; AlmaLinux-specific repo fixes do not run for RHEL. | +| **RockyLinux 9, 8** | `Rocky Linux` in os-release | Normalized to `CentOS`; version 8 or 9. | Same as CentOS 8/9 (EPEL, MariaDB, venv for 9/10). | +| **Ubuntu 24.04, 22.04, 20.04** | `Ubuntu 24.04` etc. in os-release | Explicit branches for 22/24 (packages, python3-venv). 20 → specific fixes. 18 → minimal. | 24.04: externally-managed-environment handled. Uses **requirments.txt** for 22 and 24. | + +## Do we *know* it works on all of them? + +- **Code coverage:** Detection and branching exist for all listed OSes. AlmaLinux 8/9/10, Ubuntu 18/20/22/24, Debian 11/12/13, CentOS 7/8/9, Rocky, RHEL, CloudLinux, and openEuler have explicit or normalized paths. +- **No automated proof:** There is no CI in this repo that runs install or upgrade on each OS. “Works” is based on: + - Manual and community testing + - Code review of detection and branches +- **RHEL:** Uses the same code path as CentOS (RedHat → CentOS). RHEL 9 uses different repo IDs than AlmaLinux; if repo issues appear on RHEL 9, RHEL-specific repo handling may be needed. +- **Debian 11/12/13:** Share the “Ubuntu” path and use **requirments-old.txt**. install_utils has Debian 13 (Trixie) package mappings. No Debian-version-specific blocks in the upgrade script. +- **CentOS 7:** Marked legacy/EOL; still in the script with yum and old requirements. + +## Recommendations + +1. **Staging:** Test install and upgrade on a non-production VM for your chosen OS before production. +2. **CI (optional):** Add a test matrix (e.g. GitHub Actions or other CI) that runs install and/or upgrade on a subset of OSes (e.g. AlmaLinux 9, Ubuntu 22.04, Debian 12) to catch regressions. +3. **Docs:** Keep this file (or a short “Supported platforms” section) in sync with the script when adding or dropping OS versions. + +## Where to look in the repo + +- **Upgrade OS detection:** `cyberpanel_upgrade.sh` (lines ~160–187), `Server_OS_Version` (~187), and branches for `CentOS`/`AlmaLinux9`/`Ubuntu`/`openEuler`. +- **Install OS detection:** `install/install.py` (`preFlightsChecks.detect_os`, `get_distro`), and `install/install_utils.py` (Debian/Ubuntu version and package helpers). +- **Requirements choice:** `cyberpanel_upgrade.sh` `Download_Requirement()`: uses `requirments.txt` for version 22, 24, 9, 10; else `requirments-old.txt`. diff --git a/to-do/UPGRADE-MODULES-DESIGN.md b/to-do/UPGRADE-MODULES-DESIGN.md new file mode 100644 index 000000000..6ae78cc17 --- /dev/null +++ b/to-do/UPGRADE-MODULES-DESIGN.md @@ -0,0 +1,48 @@ +# CyberPanel Upgrade Script - Modular Layout for Debugging + +## Goal + +Split `cyberpanel_upgrade.sh` into modules under `upgrade_modules/` so each file is under 500 lines and easier to debug. + +## Directory Layout + +- `upgrade_modules/00_common.sh` - Debug_Log, Debug_Log2, Branch_Check, Check_Return, Regenerate_Cert, Retry_Command (DONE) +- `upgrade_modules/01_variables.sh` - Set_Default_Variables (DONE) +- `upgrade_modules/02_checks.sh` - Check_Root, Check_Server_IP, Check_OS, Check_Provider, Check_Argument +- `upgrade_modules/03_mariadb.sh` - Pre_Upgrade_CentOS7_MySQL, Maybe_Backup_MariaDB_Before_Upgrade, Backup_MariaDB_Before_Upgrade, Migrate_MariaDB_To_UTF8 +- `upgrade_modules/04_git_url.sh` - Pre_Upgrade_Setup_Git_URL +- `upgrade_modules/05_repository.sh` - Pre_Upgrade_Setup_Repository (~490 lines) +- `upgrade_modules/06_components.sh` - Download_Requirement, Pre_Upgrade_Required_Components +- `upgrade_modules/07_branch_input.sh` - Pre_Upgrade_Branch_Input +- `upgrade_modules/08_main_upgrade.sh` - Main_Upgrade +- `upgrade_modules/09_sync.sh` - Sync_CyberCP_To_Latest +- `upgrade_modules/10_post_tweak.sh` - Post_Upgrade_System_Tweak +- `upgrade_modules/11_display_final.sh` - Post_Install_Display_Final_Info, _br, _bl, _b + +## Line Ranges in Current Script + +- 00_common: 99-106, 237-263, 264-337 +- 01_variables: 27-98 +- 02_checks: 107-148, 149-206, 207-236, 352-399 +- 03_mariadb: 425-520 +- 04_git_url: 400-424 +- 05_repository: 521-1011 +- 06_components: 1012-1298 +- 07_branch_input: 1299-1311 +- 08_main_upgrade: 1312-1649 +- 09_sync: 1650-1688 +- 10_post_tweak: 1691-2023 +- 11_display_final: 2024-2118 + +## Main Script After Refactor + +1. Root check, Sudo_Test +2. If upgrade_modules/ exists: source each 00-11; else (one-liner) download modules from GitHub by branch and source +3. Set_Default_Variables, Check_Root, Check_Server_IP, Check_OS, Check_Provider, Check_Argument +4. Branch and MariaDB prompts +5. Pre_Upgrade_Setup_Repository, Pre_Upgrade_Setup_Git_URL, Pre_Upgrade_Required_Components +6. Main_Upgrade, Sync_CyberCP_To_Latest, Post_Upgrade_System_Tweak, Post_Install_Display_Final_Info + +## Status + +Done: 00_common.sh, 01_variables.sh. Remaining: create 02-11 and refactor main script to loader. diff --git a/to-do/WHITELIST-RM-UPGRADE_LOGS-SECURITY-ALERT.md b/to-do/WHITELIST-RM-UPGRADE_LOGS-SECURITY-ALERT.md new file mode 100644 index 000000000..eeeb1606c --- /dev/null +++ b/to-do/WHITELIST-RM-UPGRADE_LOGS-SECURITY-ALERT.md @@ -0,0 +1,119 @@ +# Security alert: `rm -rf /home/cyberpanel/upgrade_logs` + +## Is this an issue? + +**No.** This is **expected behavior** from the CyberPanel upgrade process, not a sign of compromise. + +## What’s going on + +- Your security product (e.g. OSSEC, Wazuh, or similar) flagged: + - **Command:** `sudo ... /bin/rm -rf /home/cyberpanel/upgrade_logs` + - **Context:** `PWD=/tmp/lscpd`, `USER=root` +- The CyberPanel daemon (**lscpd**) runs upgrade-related tasks. The upgrade logic uses `/home/cyberpanel/upgrade_logs` as the path for upgrade logs (see `plogical/upgrade.py`: `LogPathNew = '/home/cyberpanel/upgrade_logs'`). Cleaning that path (file or directory) before or after an upgrade is normal so the next run starts from a clean state. +- So this command is the **panel cleaning its own upgrade logs**, not an attacker. + +## Why does it look “suspicious”? + +- Security tools often treat **any** `rm -rf` as “dangerous” because it can delete a lot if misused. +- They also flag “system file access” or “writes/deletes under /home” to catch abuse. +- Here, the path is a **known, fixed** CyberPanel path and the process is **root from lscpd** (expected for the panel). So the alert is a **false positive** for “suspicious command” in this context. + +## Why “my own local files” look suspicious + +- “Local files” in the alert usually means “commands or file operations on this machine.” The product isn’t saying your personal files are malicious; it’s saying the **behavior** (e.g. `rm -rf` on a path under `/home`) matches a **rule** that can indicate compromise. +- In this case the “local” actor is **CyberPanel itself** (lscpd/upgrade), so the behavior is legitimate. + +## What you can do + +1. **Treat as expected:** No need to change passwords or hunt for backdoors solely because of this alert. +2. **Whitelist/tune the rule:** In your security product, add an exception or rule so that this specific command (or pattern) when run by root from the lscpd context is not reported, e.g.: + - Command pattern: `rm -rf /home/cyberpanel/upgrade_logs` + - Or: allow `rm -rf` for paths under `/home/cyberpanel/` when the process is lscpd/upgrade-related. +3. **Keep monitoring:** Continue to review real suspicious activity (e.g. unknown scripts, unexpected `rm -rf /` or `rm -rf /home/*`). + +## Summary + +- **Not a compromise** – normal CyberPanel upgrade cleanup. +- **“Suspicious”** only in the generic sense (rm -rf + /home); in context it’s the panel’s own operation. +- **Action:** Whitelist or tune the alert for this known-good case; no need to panic or “fix” the panel for this. + +--- + +## Whitelist / rule examples (stop this specific case being reported) + +Use the example that matches your product. After editing config, restart the agent/manager as indicated. + +### OSSEC + +Allow this command so it is not reported as suspicious. + +**1. Local rule to ignore this command** + +Create or edit a local rule file (e.g. `/var/ossec/etc/rules/local_rules.xml`) and add: + +```xml + + + 100002 + rm -rf /home/cyberpanel/upgrade_logs + Whitelist: CyberPanel upgrade log cleanup (expected) + +``` + +If your “suspicious command” rule has a different ``, replace `100002` with that rule’s ID (so this rule only applies when that one fires). If you’re not sure, you can use a broader override that matches the command and sets level 0: + +```xml + + rm -rf /home/cyberpanel/upgrade_logs + Whitelist: CyberPanel upgrade log cleanup + +``` + +Restart OSSEC: + +```bash +systemctl restart ossec +# or +/var/ossec/bin/ossec-control restart +``` + +**2. (Optional) Decoder to tag sudo rm** + +In `/var/ossec/etc/decoders/local_decoder.xml` you can add a decoder so the command is clearly identified; the rule above is enough to stop the alert. + +### Wazuh + +**1. Local rule to not alert on this command** + +Append to `/var/ossec/etc/rules/local_rules.xml` (Wazuh keeps OSSEC-style paths): + +```xml + + + + rm -rf /home/cyberpanel/upgrade_logs + Whitelist: CyberPanel upgrade_logs cleanup (lscpd/upgrade) + + +``` + +If the alert is from a different rule (e.g. “suspicious command” or “syscheck”), you may need to set `` to that rule’s ID so this rule only overrides that case. + +Restart Wazuh: + +```bash +systemctl restart wazuh-agent +# On manager: +systemctl restart wazuh-manager +``` + +**2. (Optional) Broader CyberPanel cleanup** + +To allow any `rm -rf` under `/home/cyberpanel/` when the process is from lscpd/upgrade, you’d need a rule that matches both the command pattern and (if available) the process or PWD. That’s product-specific; the rule above is the minimal, safe whitelist for the exact command you saw. + +### Other products (generic) + +- **Fail2ban / custom script:** If the alert is generated by a script that parses `auth.log` or `secure`, add an exception when the log line contains both `rm -rf` and `/home/cyberpanel/upgrade_logs`. +- **SIEM / cloud:** Add an exception or filter so that events with command `rm -rf /home/cyberpanel/upgrade_logs` and user `root` (and optionally process/source indicating lscpd) are not escalated. + +Once the whitelist is in place, future runs of that CyberPanel cleanup will no longer trigger this specific alert. diff --git a/to-do/fix-phpmyadmin-mariadb-version-on-server.md b/to-do/fix-phpmyadmin-mariadb-version-on-server.md new file mode 100644 index 000000000..fcc536990 --- /dev/null +++ b/to-do/fix-phpmyadmin-mariadb-version-on-server.md @@ -0,0 +1,36 @@ +# Fix phpMyAdmin Showing 10.11 When 11.8 Is Installed + +## 1. Fix CLI SSL error and see real server version + +Run as root on the server: + +```bash +# Allow mariadb client to connect without SSL (11.x client requires SSL by default) +mkdir -p /etc/my.cnf.d +printf '[client]\nskip-ssl = true\n' > /etc/my.cnf.d/cyberpanel-client.cnf + +# Now this should work and show the *actual* server version on 3306 +mariadb -e "SELECT @@version;" +``` + +- If it shows **11.8.x**: the server is 11.8; phpMyAdmin should show 11.8 after you **log out, clear cookies for :2087, then log in again via CyberPanel → Databases → phpMyAdmin**. +- If it still shows **10.11.x**: the process on 3306 is still 10.11. Force the 11.8 service to take over: + +```bash +systemctl stop mariadb +sleep 3 +systemctl start mariadb +mariadb -e "SELECT @@version;" +``` + +If it still shows 10.11, check: + +```bash +rpm -q MariaDB-server +ss -tlnp | grep 3306 +systemctl status mariadb +``` + +## 2. phpMyAdmin config (already correct on your server) + +Your `config.inc.php` already has `host = '127.0.0.1'` and `port = '3306'`. Once the server on 3306 is 11.8 and you log in again via the panel, phpMyAdmin will show 11.8. diff --git a/upgrade_modules/00_common.sh b/upgrade_modules/00_common.sh new file mode 100644 index 000000000..69ba17312 --- /dev/null +++ b/upgrade_modules/00_common.sh @@ -0,0 +1,104 @@ +#!/usr/bin/env bash +# CyberPanel upgrade – common helpers (logging, check return, retry, branch check). +# Sourced by cyberpanel_upgrade.sh. Do not run standalone. + +Debug_Log() { + echo -e "\n${1}=${2}\n" >> "/var/log/cyberpanel_debug_upgrade_$(date +"%Y-%m-%d")_${Random_Log_Name}.log" +} + +Debug_Log2() { + echo -e "\n${1}" >> /var/log/upgradeLogs.txt +} + +Branch_Check() { + if [[ "$1" = *.*.* ]]; then + Output=$(awk -v num1="$Base_Number" -v num2="${1//[[:space:]]/}" ' + BEGIN { + print "num1", (num1 < num2 ? "<" : ">="), "num2" + } + ') + if [[ $Output = *">="* ]]; then + echo -e "\nYou must use version number higher than 2.3.4" + exit + else + raw="${1//[[:space:]]/}" + if [[ "$raw" = v* ]]; then + Branch_Name="$raw" + else + Branch_Name="v$raw" + fi + echo -e "\nSet branch name to $Branch_Name...\n" + fi + else + echo -e "\nPlease input a valid format version number." + exit + fi +} + +Check_Return() { + local LAST_EXIT_CODE=$? + if [[ $LAST_EXIT_CODE != "0" ]]; then + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] ERROR: Command failed with exit code: $LAST_EXIT_CODE" | tee -a /var/log/cyberpanel_upgrade_debug.log + if [[ -n "$1" ]] ; then + echo -e "\n\n\n$1" + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Error message: $1" | tee -a /var/log/cyberpanel_upgrade_debug.log + fi + echo -e "above command failed..." + Debug_Log2 "command failed. For more information read /var/log/installLogs.txt [404]" + if [[ "$2" = "no_exit" ]] || [[ "$3" = "continue" ]]; then + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Continuing despite error..." | tee -a /var/log/cyberpanel_upgrade_debug.log + else + if [[ "$1" == *"Virtualenv creation failed"* ]] || [[ "$1" == *"Python upgrade.py"* ]]; then + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] FATAL: Critical error, exiting" | tee -a /var/log/cyberpanel_upgrade_debug.log + exit $LAST_EXIT_CODE + else + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Non-critical error, continuing..." | tee -a /var/log/cyberpanel_upgrade_debug.log + fi + fi + else + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Command succeeded" | tee -a /var/log/cyberpanel_upgrade_debug.log + fi +} + +Regenerate_Cert() { + cat </usr/local/CyberCP/cert_conf +[req] +prompt=no +distinguished_name=cyberpanel +[cyberpanel] +commonName = www.example.com +countryName = CP +localityName = CyberPanel +organizationName = CyberPanel +organizationalUnitName = CyberPanel +stateOrProvinceName = CP +emailAddress = mail@example.com +name = CyberPanel +surname = CyberPanel +givenName = CyberPanel +initials = CP +dnQualifier = CyberPanel +[server_exts] +extendedKeyUsage = 1.3.6.1.5.5.7.3.1 +EOF + if [[ $1 == "8090" ]]; then + openssl req -x509 -config /usr/local/CyberCP/cert_conf -extensions 'server_exts' -nodes -days 820 -newkey rsa:2048 -keyout /usr/local/lscp/conf/key.pem -out /usr/local/lscp/conf/cert.pem + fi + if [[ $1 == "7080" ]]; then + if [[ -f /usr/local/lsws/admin/conf/webadmin.key ]]; then + key_path="/usr/local/lsws/admin/conf/webadmin.key" + cert_path="/usr/local/lsws/admin/conf/webadmin.crt" + else + key_path="/usr/local/lsws/admin/conf/cert/admin.key" + cert_path="/usr/local/lsws/admin/conf/cert/admin.crt" + fi + openssl req -x509 -config /usr/local/CyberCP/cert_conf -extensions 'server_exts' -nodes -days 820 -newkey rsa:2048 -keyout $key_path -out $cert_path + fi + rm -f /usr/local/CyberCP/cert_conf +} + +Retry_Command() { + for i in {1..50}; do + eval "$1" && break || echo -e "\n$1 has failed for $i times\nWait for 3 seconds and try again...\n"; sleep 3; + done +} diff --git a/upgrade_modules/01_variables.sh b/upgrade_modules/01_variables.sh new file mode 100644 index 000000000..2f18f31be --- /dev/null +++ b/upgrade_modules/01_variables.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +# CyberPanel upgrade – set default variables and paths. +# Sourced by cyberpanel_upgrade.sh. + +Set_Default_Variables() { + echo -e "Clearing old log files..." + rm -f /var/log/cyberpanel_upgrade_debug.log + rm -f /var/log/installLogs.txt + rm -f /var/log/upgradeLogs.txt + + echo -e "\n\n========================================" > /var/log/cyberpanel_upgrade_debug.log + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Starting CyberPanel Upgrade Script" >> /var/log/cyberpanel_upgrade_debug.log + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Old log files have been cleared" >> /var/log/cyberpanel_upgrade_debug.log + echo -e "========================================\n" >> /var/log/cyberpanel_upgrade_debug.log + + rm -Rfv /usr/local/CyberCP/configservercsf 2>/dev/null || true + rm -fv /home/cyberpanel/plugins/configservercsf 2>/dev/null || true + rm -Rfv /usr/local/CyberCP/public/static/configservercsf 2>/dev/null || true + sed -i "/configservercsf/d" /usr/local/CyberCP/CyberCP/settings.py 2>/dev/null || true + sed -i "/configservercsf/d" /usr/local/CyberCP/CyberCP/urls.py 2>/dev/null || true + if [ ! -e /etc/cxs/cxs.pl ]; then + sed -i "/configserver/d" /usr/local/CyberCP/baseTemplate/templates/baseTemplate/index.html 2>/dev/null || true + fi + + export LC_CTYPE=en_US.UTF-8 + echo -e "\nFetching latest data from CyberPanel server...\n" + echo -e "This may take few seconds..." + + Server_Country="Unknown" + Server_OS="" + Server_OS_Version="" + Server_Provider='Undefined' + + Temp_Value=$(curl --silent --max-time 30 -4 https://cyberpanel.net/version.txt) + Panel_Version=${Temp_Value:12:3} + Panel_Build=${Temp_Value:25:1} + + Branch_Name="v${Panel_Version}.${Panel_Build}" + Base_Number="1.9.3" + + Git_User="" + Git_Content_URL="" + Git_Clone_URL="" + + MySQL_Version=$(mariadb -V 2>/dev/null | grep -P '\d+.\d+.\d+' -o || mysql -V 2>/dev/null | grep -P '\d+.\d+.\d+' -o) + MySQL_Password=$(cat /etc/cyberpanel/mysqlPassword 2>/dev/null || echo "") + + LSWS_Latest_URL="https://cyberpanel.sh/update.litespeedtech.com/ws/latest.php" + LSWS_Tmp=$(curl --silent --max-time 30 -4 "$LSWS_Latest_URL" 2>/dev/null) + LSWS_Stable_Line=$(echo "$LSWS_Tmp" | grep "LSWS_STABLE") + LSWS_Stable_Version=$(expr "$LSWS_Stable_Line" : '.*LSWS_STABLE=\(.*\) BUILD .*') + if [ -z "$LSWS_Stable_Version" ]; then + LSWS_Stable_Version="6.3.4" + fi + + Debug_Log2 "Starting Upgrade...1" + + rm -rf /root/cyberpanel_upgrade_tmp + mkdir -p /root/cyberpanel_upgrade_tmp + cd /root/cyberpanel_upgrade_tmp || exit +} diff --git a/upgrade_modules/02_checks.sh b/upgrade_modules/02_checks.sh new file mode 100644 index 000000000..691839b9b --- /dev/null +++ b/upgrade_modules/02_checks.sh @@ -0,0 +1,206 @@ +#!/usr/bin/env bash +# CyberPanel upgrade – root, server IP, OS, provider, and argument checks. Sourced by cyberpanel_upgrade.sh. + +Check_Root() { +echo -e "\nChecking root privileges..." + # If we're actually root (uid 0), allow regardless of SUDO in environment (e.g. curl | sudo bash) + if [[ $(id -u) -eq 0 ]] 2>/dev/null; then + echo -e "\nYou are running as root...\n" + return 0 + fi + + if echo "$Sudo_Test" | grep SUDO >/dev/null; then + echo -e "\nYou are using SUDO, please run as root user...\n" + echo -e "\nIf you don't have direct access to root user, please run \e[31msudo su -\e[39m command (do NOT miss the \e[31m-\e[39m at end or it will fail) and then run installation command again." + exit 1 + fi + + echo -e "\nYou must run as root user to install CyberPanel...\n" + echo -e "Run: \e[31msudo su -\e[39m then run this script again, or: curl -sL | sudo bash -s -- " + exit 1 +} + +Check_Server_IP() { +echo -e "Checking server location...\n" + +Server_Country=$(curl --silent --max-time 10 -4 https://cyberpanel.sh/?country) +if [[ ${#Server_Country} != "2" ]] ; then + Server_Country="Unknown" +fi + +if [[ "$Debug" = "On" ]] ; then + Debug_Log "Server_Country" "$Server_Country" +fi + +if [[ "$*" = *"--mirror"* ]] ; then + Server_Country="CN" + echo -e "Forced to use mirror server due to --mirror argument...\n" +fi + +if [[ "$Server_Country" = *"CN"* ]] ; then + Server_Country="CN" + echo -e "Setting up to use mirror server...\n" +fi +} + +Check_OS() { +if [[ ! -f /etc/os-release ]] ; then + echo -e "Unable to detect the Operating System...\n" + exit +fi + +if ! uname -m | grep -qE 'x86_64|aarch64' ; then + echo -e "x86_64 or ARM system is required...\n" + exit +fi + +if grep -q -E "CentOS Linux 7|CentOS Linux 8|CentOS Linux 9|CentOS Stream 9" /etc/os-release ; then + Server_OS="CentOS" +elif grep -q "Red Hat Enterprise Linux" /etc/os-release ; then + Server_OS="RedHat" +elif grep -q -E "CloudLinux 7|CloudLinux 8|CloudLinux 9" /etc/os-release ; then + Server_OS="CloudLinux" +elif grep -q -E "Rocky Linux" /etc/os-release ; then + Server_OS="RockyLinux" +elif grep -q -E "AlmaLinux-8|AlmaLinux-9|AlmaLinux-10" /etc/os-release ; then + Server_OS="AlmaLinux" + # Set specific version for AlmaLinux 9+ to use dnf instead of yum + if grep -q -E "AlmaLinux-9|AlmaLinux-10" /etc/os-release ; then + Server_OS="AlmaLinux9" + fi +elif grep -q -E "Ubuntu 18.04|Ubuntu 20.04|Ubuntu 20.10|Ubuntu 22.04|Ubuntu 24.04|Ubuntu 24.04.3" /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="Ubuntu" +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, Ubuntu 24.04.3, Debian 11, Debian 12, Debian 13, CentOS 7, CentOS 8, CentOS 9, CentOS Stream 9, AlmaLinux 8, AlmaLinux 9, AlmaLinux 10, RockyLinux 8, RockyLinux 9, RHEL 8, RHEL 9, CloudLinux 7, CloudLinux 8, CloudLinux 9, openEuler 20.03, openEuler 22.03...\n" + Debug_Log2 "CyberPanel 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, CentOS Stream 9, AlmaLinux 8, AlmaLinux 9, AlmaLinux 10, RockyLinux 8, RockyLinux 9, RHEL 8, RHEL 9, CloudLinux 7, CloudLinux 8, CloudLinux 9, openEuler 20.03, openEuler 22.03... [404]" + exit +fi + +Server_OS_Version=$(grep VERSION_ID /etc/os-release | awk -F[=,] '{print $2}' | tr -d \" | head -c2 | tr -d . ) +#to make 20.04 display as 20, etc. + +echo -e "System: $Server_OS $Server_OS_Version detected...\n" + +if [[ $Server_OS = "CloudLinux" ]] || [[ "$Server_OS" = "AlmaLinux" ]] || [[ "$Server_OS" = "RockyLinux" ]] || [[ "$Server_OS" = "RedHat" ]]; then + # Keep AlmaLinux9 separate for dnf package management + if [[ "$Server_OS" != "AlmaLinux9" ]]; 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 + fi +fi + +if [[ "$Debug" = "On" ]] ; then + Debug_Log "Server_OS" "$Server_OS $Server_OS_Version" +fi + +} + +Check_Provider() { +if hash dmidecode >/dev/null 2>&1; then + if [[ "$(dmidecode -s bios-vendor)" = "Google" ]]; then + Server_Provider="Google Cloud Platform" + elif [[ "$(dmidecode -s bios-vendor)" = "DigitalOcean" ]]; then + Server_Provider="Digital Ocean" + elif [[ "$(dmidecode -s system-product-name | cut -c 1-7)" = "Alibaba" ]]; then + Server_Provider="Alibaba Cloud" + elif [[ "$(dmidecode -s system-manufacturer)" = "Microsoft Corporation" ]]; then + Server_Provider="Microsoft Azure" + elif [[ -d /usr/local/qcloud ]]; then + Server_Provider="Tencent Cloud" + else + Server_Provider="Undefined" + fi +else + Server_Provider='Undefined' +fi + +if [[ -f /sys/devices/virtual/dmi/id/product_uuid ]]; then + if [[ "$(cut -c 1-3 /sys/devices/virtual/dmi/id/product_uuid)" = 'EC2' ]] && [[ -d /home/ubuntu ]]; then + Server_Provider='Amazon Web Service' + fi +fi + +if [[ "$Debug" = "On" ]] ; then + Debug_Log "Server_Provider" "$Server_Provider" +fi +} + + +Skip_System_Update="" +# Migrate MariaDB from latin1 to utf8mb4 after upgrade (only when --migrate-to-utf8 and upgrading to 11.x/12.x) +Migrate_MariaDB_To_UTF8_Requested="" +# MariaDB version: any X.Y or X.Y.Z supported; highlighted: 10.11.16, 11.8 LTS, 12.x (default 11.8) +MARIADB_VER="11.8" +MARIADB_VER_REPO="11.8" + +Check_Argument() { +Backup_DB_Before_Upgrade="" +# Parse arguments with exact next-token so -b v2.5.5-dev --mariadb-version 12.3 does not mangle Branch_Name +set -- $* +while [[ $# -ge 1 ]]; do + case "$1" in + -b|--branch) + if [[ -n "${2:-}" ]] && [[ "$2" != -* ]]; then + Branch_Name="$2" + Branch_Check "$Branch_Name" + shift 2 + continue + fi + shift + ;; + --mariadb-version) + if [[ -n "${2:-}" ]] && [[ "$2" != -* ]]; then + MARIADB_VER="$2" + echo -e "\nUsing --mariadb-version: MariaDB $MARIADB_VER selected.\n" + shift 2 + continue + fi + shift + ;; + -r|--repo) + if [[ -n "${2:-}" ]] && [[ "$2" != -* ]]; then + Git_User_Override="$2" + echo -e "\nUsing --repo: GitHub user $Git_User_Override for cyberpanel.\n" + shift 2 + continue + fi + shift + ;; + --no-system-update) + Skip_System_Update="yes" + echo -e "\nUsing --no-system-update: skipping full system package update.\n" + shift + ;; + --backup-db) + Backup_DB_Before_Upgrade="yes" + echo -e "\nUsing --backup-db: will create a full MariaDB backup before upgrade.\n" + shift + ;; + --no-backup-db) + Backup_DB_Before_Upgrade="no" + echo -e "\nUsing --no-backup-db: skipping MariaDB pre-upgrade backup.\n" + shift + ;; + --migrate-to-utf8) + Migrate_MariaDB_To_UTF8_Requested="yes" + echo -e "\nUsing --migrate-to-utf8: will convert databases to UTF-8 (utf8mb4) after MariaDB upgrade.\n" + shift + ;; + --mariadb) + MARIADB_VER="10.11" + echo -e "\nUsing --mariadb: MariaDB 10.11 selected (non-interactive).\n" + shift + ;; + *) + shift + ;; + esac +done +} + diff --git a/upgrade_modules/02_checks_part1.txt b/upgrade_modules/02_checks_part1.txt new file mode 100644 index 000000000..c376d56cc --- /dev/null +++ b/upgrade_modules/02_checks_part1.txt @@ -0,0 +1,130 @@ +Check_Root() { +echo -e "\nChecking root privileges..." + # If we're actually root (uid 0), allow regardless of SUDO in environment (e.g. curl | sudo bash) + if [[ $(id -u) -eq 0 ]] 2>/dev/null; then + echo -e "\nYou are running as root...\n" + return 0 + fi + + if echo "$Sudo_Test" | grep SUDO >/dev/null; then + echo -e "\nYou are using SUDO, please run as root user...\n" + echo -e "\nIf you don't have direct access to root user, please run \e[31msudo su -\e[39m command (do NOT miss the \e[31m-\e[39m at end or it will fail) and then run installation command again." + exit 1 + fi + + echo -e "\nYou must run as root user to install CyberPanel...\n" + echo -e "Run: \e[31msudo su -\e[39m then run this script again, or: curl -sL | sudo bash -s -- " + exit 1 +} + +Check_Server_IP() { +echo -e "Checking server location...\n" + +Server_Country=$(curl --silent --max-time 10 -4 https://cyberpanel.sh/?country) +if [[ ${#Server_Country} != "2" ]] ; then + Server_Country="Unknown" +fi + +if [[ "$Debug" = "On" ]] ; then + Debug_Log "Server_Country" "$Server_Country" +fi + +if [[ "$*" = *"--mirror"* ]] ; then + Server_Country="CN" + echo -e "Forced to use mirror server due to --mirror argument...\n" +fi + +if [[ "$Server_Country" = *"CN"* ]] ; then + Server_Country="CN" + echo -e "Setting up to use mirror server...\n" +fi +} + +Check_OS() { +if [[ ! -f /etc/os-release ]] ; then + echo -e "Unable to detect the Operating System...\n" + exit +fi + +if ! uname -m | grep -qE 'x86_64|aarch64' ; then + echo -e "x86_64 or ARM system is required...\n" + exit +fi + +if grep -q -E "CentOS Linux 7|CentOS Linux 8|CentOS Linux 9|CentOS Stream 9" /etc/os-release ; then + Server_OS="CentOS" +elif grep -q "Red Hat Enterprise Linux" /etc/os-release ; then + Server_OS="RedHat" +elif grep -q -E "CloudLinux 7|CloudLinux 8|CloudLinux 9" /etc/os-release ; then + Server_OS="CloudLinux" +elif grep -q -E "Rocky Linux" /etc/os-release ; then + Server_OS="RockyLinux" +elif grep -q -E "AlmaLinux-8|AlmaLinux-9|AlmaLinux-10" /etc/os-release ; then + Server_OS="AlmaLinux" + # Set specific version for AlmaLinux 9+ to use dnf instead of yum + if grep -q -E "AlmaLinux-9|AlmaLinux-10" /etc/os-release ; then + Server_OS="AlmaLinux9" + fi +elif grep -q -E "Ubuntu 18.04|Ubuntu 20.04|Ubuntu 20.10|Ubuntu 22.04|Ubuntu 24.04|Ubuntu 24.04.3" /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="Ubuntu" +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, Ubuntu 24.04.3, Debian 11, Debian 12, Debian 13, CentOS 7, CentOS 8, CentOS 9, CentOS Stream 9, AlmaLinux 8, AlmaLinux 9, AlmaLinux 10, RockyLinux 8, RockyLinux 9, RHEL 8, RHEL 9, CloudLinux 7, CloudLinux 8, CloudLinux 9, openEuler 20.03, openEuler 22.03...\n" + Debug_Log2 "CyberPanel 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, CentOS Stream 9, AlmaLinux 8, AlmaLinux 9, AlmaLinux 10, RockyLinux 8, RockyLinux 9, RHEL 8, RHEL 9, CloudLinux 7, CloudLinux 8, CloudLinux 9, openEuler 20.03, openEuler 22.03... [404]" + exit +fi + +Server_OS_Version=$(grep VERSION_ID /etc/os-release | awk -F[=,] '{print $2}' | tr -d \" | head -c2 | tr -d . ) +#to make 20.04 display as 20, etc. + +echo -e "System: $Server_OS $Server_OS_Version detected...\n" + +if [[ $Server_OS = "CloudLinux" ]] || [[ "$Server_OS" = "AlmaLinux" ]] || [[ "$Server_OS" = "RockyLinux" ]] || [[ "$Server_OS" = "RedHat" ]]; then + # Keep AlmaLinux9 separate for dnf package management + if [[ "$Server_OS" != "AlmaLinux9" ]]; 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 + fi +fi + +if [[ "$Debug" = "On" ]] ; then + Debug_Log "Server_OS" "$Server_OS $Server_OS_Version" +fi + +} + +Check_Provider() { +if hash dmidecode >/dev/null 2>&1; then + if [[ "$(dmidecode -s bios-vendor)" = "Google" ]]; then + Server_Provider="Google Cloud Platform" + elif [[ "$(dmidecode -s bios-vendor)" = "DigitalOcean" ]]; then + Server_Provider="Digital Ocean" + elif [[ "$(dmidecode -s system-product-name | cut -c 1-7)" = "Alibaba" ]]; then + Server_Provider="Alibaba Cloud" + elif [[ "$(dmidecode -s system-manufacturer)" = "Microsoft Corporation" ]]; then + Server_Provider="Microsoft Azure" + elif [[ -d /usr/local/qcloud ]]; then + Server_Provider="Tencent Cloud" + else + Server_Provider="Undefined" + fi +else + Server_Provider='Undefined' +fi + +if [[ -f /sys/devices/virtual/dmi/id/product_uuid ]]; then + if [[ "$(cut -c 1-3 /sys/devices/virtual/dmi/id/product_uuid)" = 'EC2' ]] && [[ -d /home/ubuntu ]]; then + Server_Provider='Amazon Web Service' + fi +fi + +if [[ "$Debug" = "On" ]] ; then + Debug_Log "Server_Provider" "$Server_Provider" +fi +} + diff --git a/upgrade_modules/02_checks_part2.txt b/upgrade_modules/02_checks_part2.txt new file mode 100644 index 000000000..ac364d022 --- /dev/null +++ b/upgrade_modules/02_checks_part2.txt @@ -0,0 +1,53 @@ +Migrate_MariaDB_To_UTF8_Requested="" +# MariaDB version: any X.Y or X.Y.Z supported; highlighted: 10.11.16, 11.8 LTS, 12.x (default 11.8) +MARIADB_VER="11.8" +MARIADB_VER_REPO="11.8" + +Check_Argument() { +# Parse --branch / -b (extract first word after -b or --branch) +if [[ "$*" = *"--branch "* ]]; then + Branch_Name=$(echo "$*" | sed -n 's/.*--branch \([^ ]*\).*/\1/p' | head -1) + [[ -n "$Branch_Name" ]] && Branch_Check "$Branch_Name" +elif [[ "$*" = *"-b "* ]]; then + Branch_Name=$(echo "$*" | sed -n 's/.*-b \([^ ]*\).*/\1/p' | head -1) + [[ -n "$Branch_Name" ]] && Branch_Check "$Branch_Name" +fi +# Parse --repo / -r to use any GitHub user (same URL structure as usmannasir/cyberpanel) +if [[ "$*" = *"--repo "* ]]; then + Git_User_Override=$(echo "$*" | sed -n 's/.*--repo \([^ ]*\).*/\1/p' | head -1) +fi +if [[ "$*" = *"-r "* ]] && [[ -z "$Git_User_Override" ]]; then + Git_User_Override=$(echo "$*" | sed -n 's/.*-r \([^ ]*\).*/\1/p' | head -1) +fi +# Parse --no-system-update to skip yum/dnf update -y (faster upgrade when system is already updated) +if [[ "$*" = *"--no-system-update"* ]]; then + Skip_System_Update="yes" + echo -e "\nUsing --no-system-update: skipping full system package update.\n" +fi +# Parse --backup-db / --no-backup-db: pre-upgrade MariaDB backup. Default when neither set: ask user (may take a while). +# --backup-db = always backup; --no-backup-db = never backup; omit both = prompt [y/N] +Backup_DB_Before_Upgrade="" +if [[ "$*" = *"--backup-db"* ]]; then + Backup_DB_Before_Upgrade="yes" + echo -e "\nUsing --backup-db: will create a full MariaDB backup before upgrade.\n" +elif [[ "$*" = *"--no-backup-db"* ]]; then + Backup_DB_Before_Upgrade="no" + echo -e "\nUsing --no-backup-db: skipping MariaDB pre-upgrade backup.\n" +fi +# Parse --migrate-to-utf8: after upgrading to MariaDB 11.x/12.x, convert DBs/tables from latin1 to utf8mb4 (only if your apps support UTF-8) +if [[ "$*" = *"--migrate-to-utf8"* ]]; then + Migrate_MariaDB_To_UTF8_Requested="yes" + echo -e "\nUsing --migrate-to-utf8: will convert databases to UTF-8 (utf8mb4) after MariaDB upgrade.\n" +fi +# Parse --mariadb-version (any version: 10.6, 10.11, 10.11.16, 11.8, 12.1, 12.2, 12.3, etc.). Default 11.8. +# --mariadb is shorthand for --mariadb-version 10.11 +if [[ "$*" = *"--mariadb"* ]] && [[ "$*" != *"--mariadb-version "* ]]; then + MARIADB_VER="10.11" + echo -e "\nUsing --mariadb: MariaDB 10.11 selected (non-interactive).\n" +elif [[ "$*" = *"--mariadb-version "* ]]; then + MARIADB_VER=$(echo "$*" | sed -n 's/.*--mariadb-version \([^ ]*\).*/\1/p' | head -1) + MARIADB_VER="${MARIADB_VER:-11.8}" +fi +# Allow any version; repo paths use major.minor (normalized later) +} + diff --git a/upgrade_modules/03_mariadb.sh b/upgrade_modules/03_mariadb.sh new file mode 100644 index 000000000..f70e3fae4 --- /dev/null +++ b/upgrade_modules/03_mariadb.sh @@ -0,0 +1,88 @@ +#!/usr/bin/env bash +# CyberPanel upgrade – MariaDB backup, optional UTF-8 migration, CentOS 7 MySQL upgrade. +# Sourced by cyberpanel_upgrade.sh. + +Pre_Upgrade_CentOS7_MySQL() { + if [[ "$MySQL_Version" = "10.1" ]]; then + cp /etc/my.cnf /etc/my.cnf.bak + mkdir /etc/cnfbackup + cp -R /etc/my.cnf.d/ /etc/cnfbackup/ + yum remove MariaDB-server MariaDB-client galera -y + yum --enablerepo=mariadb -y install MariaDB-server MariaDB-client galera + cp -f /etc/my.cnf.bak /etc/my.cnf + rm -rf /etc/my.cnf.d/ + mv /etc/cnfbackup/my.cnf.d /etc/ + systemctl enable mariadb 2>/dev/null || systemctl enable mysql + systemctl start mariadb 2>/dev/null || systemctl start mysql + mariadb-upgrade -uroot -p"$MySQL_Password" 2>/dev/null || mysql_upgrade -uroot -p"$MySQL_Password" + fi + mariadb -uroot -p"$MySQL_Password" -e "GRANT ALL PRIVILEGES ON *.* TO 'root'@'localhost' IDENTIFIED BY '$MySQL_Password';flush privileges" 2>/dev/null || mysql -uroot -p"$MySQL_Password" -e "GRANT ALL PRIVILEGES ON *.* TO 'root'@'localhost' IDENTIFIED BY '$MySQL_Password';flush privileges" +} + +Maybe_Backup_MariaDB_Before_Upgrade() { + if [[ "$Backup_DB_Before_Upgrade" = "no" ]]; then + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] MariaDB pre-upgrade backup: skipped (--no-backup-db)." | tee -a /var/log/cyberpanel_upgrade_debug.log + return 0 + fi + if [[ "$Backup_DB_Before_Upgrade" = "" ]]; then + echo -e "\nDo you want to backup all databases before MariaDB upgrade? (may take a while) [y/N]: " + read -r -t 60 Tmp_Backup_Choice 2>/dev/null || Tmp_Backup_Choice="" + if [[ "$Tmp_Backup_Choice" =~ ^[yY] ]] || [[ "$Tmp_Backup_Choice" =~ ^[yY][eE][sS] ]]; then + Backup_DB_Before_Upgrade="yes" + else + Backup_DB_Before_Upgrade="no" + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] MariaDB pre-upgrade backup: skipped (user chose no or timeout)." | tee -a /var/log/cyberpanel_upgrade_debug.log + return 0 + fi + fi + Backup_MariaDB_Before_Upgrade +} + +Backup_MariaDB_Before_Upgrade() { + local pass="" backup_dir="/root/cyberpanel_mariadb_backups" backup_file="" + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Starting MariaDB pre-upgrade backup... (this may take a few minutes)" | tee -a /var/log/cyberpanel_upgrade_debug.log + [[ -f /etc/cyberpanel/mysqlPassword ]] || { echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] MariaDB pre-upgrade backup: skipped (no password file)." | tee -a /var/log/cyberpanel_upgrade_debug.log; return 0; } + if grep -q '"mysqlpassword"' /etc/cyberpanel/mysqlPassword 2>/dev/null; then + pass=$(python3 -c "import json; print(json.load(open('/etc/cyberpanel/mysqlPassword')).get('mysqlpassword',''))" 2>/dev/null) + else + pass=$(head -1 /etc/cyberpanel/mysqlPassword 2>/dev/null | tr -d '\r\n') + fi + [[ -z "$pass" ]] && echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] WARNING: Could not read MariaDB password, skipping pre-upgrade backup." | tee -a /var/log/cyberpanel_upgrade_debug.log && echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] MariaDB pre-upgrade backup: skipped." | tee -a /var/log/cyberpanel_upgrade_debug.log && return 0 + mkdir -p "$backup_dir" + backup_file="${backup_dir}/mariadb_backup_before_upgrade_$(date +%Y%m%d_%H%M%S).sql.gz" + if mariadb --skip-ssl -u root -p"$pass" -e "SELECT 1" 2>/dev/null | grep -q 1; then + (mariadb-dump --skip-ssl -u root -p"$pass" --all-databases --single-transaction --routines --triggers --events 2>/dev/null || mysqldump --skip-ssl -u root -p"$pass" --all-databases --single-transaction --routines --triggers --events 2>/dev/null) | gzip > "$backup_file" 2>/dev/null + if [[ -s "$backup_file" ]]; then + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] MariaDB backup created: $backup_file" | tee -a /var/log/cyberpanel_upgrade_debug.log + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] MariaDB pre-upgrade backup: done." | tee -a /var/log/cyberpanel_upgrade_debug.log + else + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] WARNING: MariaDB backup file empty or failed." | tee -a /var/log/cyberpanel_upgrade_debug.log + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] MariaDB pre-upgrade backup: skipped (dump failed)." | tee -a /var/log/cyberpanel_upgrade_debug.log + fi + else + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] WARNING: Could not connect to MariaDB for backup (skip-ssl). Skipping backup." | tee -a /var/log/cyberpanel_upgrade_debug.log + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] MariaDB pre-upgrade backup: skipped (no connection)." | tee -a /var/log/cyberpanel_upgrade_debug.log + fi +} + +Migrate_MariaDB_To_UTF8() { + local pass="" dbs="" db="" t="" + [[ -f /etc/cyberpanel/mysqlPassword ]] || return 0 + if grep -q '"mysqlpassword"' /etc/cyberpanel/mysqlPassword 2>/dev/null; then + pass=$(python3 -c "import json; print(json.load(open('/etc/cyberpanel/mysqlPassword')).get('mysqlpassword',''))" 2>/dev/null) + else + pass=$(head -1 /etc/cyberpanel/mysqlPassword 2>/dev/null | tr -d '\r\n') + fi + [[ -z "$pass" ]] && return 0 + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Migrating MariaDB to UTF-8 (utf8mb4)..." | tee -a /var/log/cyberpanel_upgrade_debug.log + mariadb --skip-ssl -u root -p"$pass" -e "SET GLOBAL character_set_server = 'utf8mb4'; SET GLOBAL collation_server = 'utf8mb4_unicode_ci';" 2>/dev/null || true + dbs=$(mariadb --skip-ssl -u root -p"$pass" -sN -e "SHOW DATABASES;" 2>/dev/null) || true + for db in $dbs; do + [[ "$db" = "information_schema" ]] || [[ "$db" = "performance_schema" ]] || [[ "$db" = "sys" ]] || [[ "$db" = "mysql" ]] && continue + mariadb --skip-ssl -u root -p"$pass" -e "ALTER DATABASE \`$db\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" 2>/dev/null || true + for t in $(mariadb --skip-ssl -u root -p"$pass" -sN -e "SHOW TABLES FROM \`$db\`;" 2>/dev/null); do + mariadb --skip-ssl -u root -p"$pass" "$db" -e "ALTER TABLE \`$t\` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" 2>/dev/null || true + done + done + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] MariaDB UTF-8 (utf8mb4) migration completed." | tee -a /var/log/cyberpanel_upgrade_debug.log +} diff --git a/upgrade_modules/04_git_url.sh b/upgrade_modules/04_git_url.sh new file mode 100644 index 000000000..4a579921e --- /dev/null +++ b/upgrade_modules/04_git_url.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# CyberPanel upgrade – set Git content and clone URLs (usmannasir or override). +# Sourced by cyberpanel_upgrade.sh. + +Pre_Upgrade_Setup_Git_URL() { + if [[ $Server_Country != "CN" ]] ; then + if [[ -n "$Git_User_Override" ]]; then + Git_User="$Git_User_Override" + echo -e "\nUsing GitHub repo: ${Git_User}/cyberpanel\n" + else + Git_User="master3395" + fi + Git_Content_URL="https://raw.githubusercontent.com/${Git_User}/cyberpanel" + Git_Clone_URL="https://github.com/${Git_User}/cyberpanel.git" + else + if [[ -n "$Git_User_Override" ]]; then + Git_User="$Git_User_Override" + else + Git_User="qtwrk" + fi + Git_Content_URL="https://gitee.com/${Git_User}/cyberpanel/raw" + Git_Clone_URL="https://gitee.com/${Git_User}/cyberpanel.git" + fi + if [[ "$Debug" = "On" ]] ; then + Debug_Log "Git_URL" "$Git_Content_URL" + fi +} diff --git a/upgrade_modules/05_repository.sh b/upgrade_modules/05_repository.sh new file mode 100644 index 000000000..b8912689b --- /dev/null +++ b/upgrade_modules/05_repository.sh @@ -0,0 +1,494 @@ +#!/usr/bin/env bash +# CyberPanel upgrade – repository setup (CentOS/AlmaLinux/Ubuntu/openEuler). Sourced by cyberpanel_upgrade.sh. + +Pre_Upgrade_Setup_Repository() { +echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Pre_Upgrade_Setup_Repository started for OS: $Server_OS" | tee -a /var/log/cyberpanel_upgrade_debug.log + +if [[ "$Server_OS" = "CentOS" ]] || [[ "$Server_OS" = "AlmaLinux9" ]] ; then + # Reduce dnf/yum timeouts and mirror issues (e.g. ftp.lip6.fr connection timeout) + for dnf_conf in /etc/dnf/dnf.conf /etc/yum.conf; do + if [[ -f "$dnf_conf" ]]; then + grep -q '^timeout=' "$dnf_conf" 2>/dev/null || echo 'timeout=120' >> "$dnf_conf" + grep -q '^minrate=' "$dnf_conf" 2>/dev/null || echo 'minrate=1000' >> "$dnf_conf" + grep -q '^retries=' "$dnf_conf" 2>/dev/null || echo 'retries=5' >> "$dnf_conf" + break + fi + done + # For AlmaLinux 9: switch to repo.almalinux.org baseurl to avoid "Cannot find valid baseurl for repo: appstream" + if [[ "$Server_OS" = "AlmaLinux9" ]] && [[ -d /etc/yum.repos.d ]]; then + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Fixing AlmaLinux 9 repos (appstream/baseos) for reliable mirror access" | tee -a /var/log/cyberpanel_upgrade_debug.log + ALMA_VER="${Server_OS_Version:-9}" + ARCH="x86_64" + ALMA_BASE="https://repo.almalinux.org/almalinux/${ALMA_VER}" + for repo in /etc/yum.repos.d/almalinux*.repo /etc/yum.repos.d/AlmaLinux*.repo; do + [[ ! -f "$repo" ]] && continue + if grep -q '^mirrorlist=' "$repo" 2>/dev/null; then + sed -i 's|^mirrorlist=|#mirrorlist=|g' "$repo" + sed -i 's|^#\s*baseurl=\(.*repo\.almalinux\.org.*\)|baseurl=\1|' "$repo" + sed -i 's|^#baseurl=\(.*repo\.almalinux\.org.*\)|baseurl=\1|' "$repo" + fi + done + # Ensure appstream/baseos have explicit baseurl; support [appstream], [almalinux-appstream], etc. + for repofile in /etc/yum.repos.d/almalinux.repo /etc/yum.repos.d/almalinux*.repo /etc/yum.repos.d/AlmaLinux*.repo; do + [[ ! -f "$repofile" ]] && continue + for section in appstream almalinux-appstream AppStream; do + if grep -q "^\[${section}\]" "$repofile" 2>/dev/null; then + sed -i "/^\[${section}\]/,/^\[/ { s|^#\?baseurl=.*|baseurl=${ALMA_BASE}/AppStream/${ARCH}/os/|; s|^mirrorlist=.*|#mirrorlist=disabled| }" "$repofile" + if ! sed -n "/^\[${section}\]/,/^\[/p" "$repofile" | grep -q '^baseurl='; then + sed -i "/^\[${section}\]/a baseurl=${ALMA_BASE}/AppStream/${ARCH}/os/" "$repofile" + fi + fi + done + for section in baseos almalinux-baseos BaseOS; do + if grep -q "^\[${section}\]" "$repofile" 2>/dev/null; then + sed -i "/^\[${section}\]/,/^\[/ { s|^#\?baseurl=.*|baseurl=${ALMA_BASE}/BaseOS/${ARCH}/os/|; s|^mirrorlist=.*|#mirrorlist=disabled| }" "$repofile" + if ! sed -n "/^\[${section}\]/,/^\[/p" "$repofile" | grep -q '^baseurl='; then + sed -i "/^\[${section}\]/a baseurl=${ALMA_BASE}/BaseOS/${ARCH}/os/" "$repofile" + fi + fi + done + for section in crb extras; do + if grep -q "^\[${section}\]" "$repofile" 2>/dev/null; then + [[ "$section" = "crb" ]] && path="CRB" || path="extras" + sed -i "/^\[${section}\]/,/^\[/ { s|^#\?baseurl=.*|baseurl=${ALMA_BASE}/${path}/${ARCH}/os/|; s|^mirrorlist=.*|#mirrorlist=disabled| }" "$repofile" + if ! sed -n "/^\[${section}\]/,/^\[/p" "$repofile" | grep -q '^baseurl='; then + sed -i "/^\[${section}\]/a baseurl=${ALMA_BASE}/${path}/${ARCH}/os/" "$repofile" + fi + fi + done + done + # Fallback: create override with same repo IDs (loads last via zz- prefix, overrides broken config) + if ! dnf makecache --quiet 2>/dev/null; then + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] dnf makecache failed, creating AlmaLinux repo override" | tee -a /var/log/cyberpanel_upgrade_debug.log + cat > /etc/yum.repos.d/zz-almalinux-cyberpanel-fix.repo << EOF +[baseos] +name=AlmaLinux ${ALMA_VER} - BaseOS +baseurl=${ALMA_BASE}/BaseOS/${ARCH}/os/ +enabled=1 +gpgcheck=1 +gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-AlmaLinux-9 + +[appstream] +name=AlmaLinux ${ALMA_VER} - AppStream +baseurl=${ALMA_BASE}/AppStream/${ARCH}/os/ +enabled=1 +gpgcheck=1 +gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-AlmaLinux-9 + +[extras] +name=AlmaLinux ${ALMA_VER} - Extras +baseurl=${ALMA_BASE}/extras/${ARCH}/os/ +enabled=1 +gpgcheck=1 +gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-AlmaLinux-9 + +[crb] +name=AlmaLinux ${ALMA_VER} - CRB +baseurl=${ALMA_BASE}/CRB/${ARCH}/os/ +enabled=1 +gpgcheck=1 +gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-AlmaLinux-9 +EOF + dnf makecache --quiet 2>/dev/null || true + fi + fi + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Setting up repositories for $Server_OS..." | tee -a /var/log/cyberpanel_upgrade_debug.log + rm -f /etc/yum.repos.d/CyberPanel.repo + rm -f /etc/yum.repos.d/litespeed.repo + if [[ "$Server_Country" = "CN" ]] ; then + curl -o /etc/yum.repos.d/litespeed.repo https://cyberpanel.sh/litespeed/litespeed_cn.repo + else + curl -o /etc/yum.repos.d/litespeed.repo https://cyberpanel.sh/litespeed/litespeed.repo + fi + yum clean all + if [[ -z "$Skip_System_Update" ]]; then + yum update -y + else + echo -e "[$(date +"%Y-%m-%d %H:%M-%S")] Skipping yum update (--no-system-update)" | tee -a /var/log/cyberpanel_upgrade_debug.log + fi + yum autoremove epel-release -y + rm -f /etc/yum.repos.d/epel.repo + rm -f /etc/yum.repos.d/epel.repo.rpmsave + yum autoremove epel-release -y +#all pre-upgrade operation for CentOS both 7/8 + + if [[ "$Server_OS_Version" = "7" ]] ; then + yum install epel-release -y + yum -y install yum-utils + yum -y groupinstall development + rm -f /etc/yum.repos.d/dovecot.repo + rm -f /etc/yum.repos.d/frank.repo + rm -f /etc/yum.repos.d/ius-archive.repo + rm -f /etc/yum.repos.d/ius.repo + rm -f /etc/yum.repos.d/ius-testing.repo + #rm -f /etc/yum.repos.d/lux.repo + rm -f /etc/yum.repos.d/powerdns-auth-* + + rm -f /etc/yum.repos.d/MariaDB.repo + rm -f /etc/yum.repos.d/MariaDB.repo.rpmsave + + yum erase gf-* -y + + rm -f /etc/yum.repos.d/gf.repo + rm -f /etc/yum.repos.d/gf.repo.rpmsave + + rm -f /etc/yum.repos.d/copart-restic-epel-7.repo.repo + rm -f /etc/yum.repos.d/copart-restic-epel-7.repo.rpmsave + + rm -f /etc/yum.repos.d/ius-archive.repo + rm -f /etc/yum.repos.d/ius.repo + rm -f /etc/yum.repos.d/ius-testing.repo + + yum clean all + + curl -o /etc/yum.repos.d/powerdns-auth-43.repo https://cyberpanel.sh/repo.powerdns.com/repo-files/centos-auth-43.repo + Check_Return "yum repo" "no_exit" + + # Determine appropriate MariaDB repository based on OS version + if [[ "$Server_OS_Version" = "9" ]] || [[ "$Server_OS_Version" = "10" ]] ; then + MARIADB_REPO="rhel9-amd64" + else + MARIADB_REPO="centos7-amd64" + fi + + cat << EOF > /etc/yum.repos.d/MariaDB.repo +# MariaDB $MARIADB_VER_REPO repository list - updated 2026-02 +# https://downloads.mariadb.org/mariadb/repositories/ +[mariadb] +name = MariaDB $MARIADB_VER_REPO +baseurl = https://mirror.mariadb.org/yum/$MARIADB_VER_REPO/$MARIADB_REPO +gpgkey=https://yum.mariadb.org/RPM-GPG-KEY-MariaDB +gpgcheck=1 +EOF + + yum install yum-plugin-copr -y + yum copr enable copart/restic -y + rpm -ivh https://cyberpanel.sh/repo.ius.io/ius-release-el7.rpm + + if [[ "$Server_Country" = "CN" ]] ; then + sed -i 's|http://yum.mariadb.org|https://cyberpanel.sh/yum.mariadb.org|g' /etc/yum.repos.d/MariaDB.repo + sed -i 's|https://yum.mariadb.org/RPM-GPG-KEY-MariaDB|https://cyberpanel.sh/yum.mariadb.org/RPM-GPG-KEY-MariaDB|g' /etc/yum.repos.d/MariaDB.repo + # use MariaDB Mirror + sed -i 's|https://download.copr.fedorainfracloud.org|https://cyberpanel.sh/download.copr.fedorainfracloud.org|g' /etc/yum.repos.d/_copr_copart-restic.repo + sed -i 's|http://repo.iotti.biz|https://cyberpanel.sh/repo.iotti.biz|g' /etc/yum.repos.d/frank.repo + sed -i "s|mirrorlist=https://mirrorlist.ghettoforge.net/el/7/gf/\$basearch/mirrorlist|baseurl=https://cyberpanel.sh/mirror.ghettoforge.net/distributions/gf/el/7/gf/x86_64/|g" /etc/yum.repos.d/gf.repo + sed -i "s|mirrorlist=https://mirrorlist.ghettoforge.net/el/7/plus/\$basearch/mirrorlist|baseurl=https://cyberpanel.sh/mirror.ghettoforge.net/distributions/gf/el/7/plus/x86_64/|g" /etc/yum.repos.d/gf.repo + sed -i 's|https://repo.ius.io|https://cyberpanel.sh/repo.ius.io|g' /etc/yum.repos.d/ius.repo + sed -i 's|http://repo.iotti.biz|https://cyberpanel.sh/repo.iotti.biz|g' /etc/yum.repos.d/lux.repo + sed -i 's|http://repo.powerdns.com|https://cyberpanel.sh/repo.powerdns.com|g' /etc/yum.repos.d/powerdns-auth-43.repo + sed -i 's|https://repo.powerdns.com|https://cyberpanel.sh/repo.powerdns.com|g' /etc/yum.repos.d/powerdns-auth-43.repo + fi + yum install yum-plugin-priorities -y + + yum update -y + + yum install -y wget strace htop net-tools telnet curl which bc telnet htop libevent-devel gcc libattr-devel xz-devel gpgme-devel curl-devel git socat openssl-devel MariaDB-shared mariadb-devel python36u python36u-pip python36u-devel bind-utils + + Pre_Upgrade_CentOS7_MySQL + + #all pre-upgrade operation for CentOS 7 + elif [[ "$Server_OS_Version" = "8" ]] ; then +# cat </etc/yum.repos.d/CentOS-PowerTools-CyberPanel.repo +#[powertools-for-cyberpanel] +#name=CentOS Linux \$releasever - PowerTools +#mirrorlist=http://mirrorlist.centos.org/?release=\$releasever&arch=\$basearch&repo=PowerTools&infra=\$infra +#baseurl=http://mirror.centos.org/\$contentdir/\$releasever/PowerTools/\$basearch/os/ +#gpgcheck=1 +#enabled=1 +#gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-centosofficial +#EOF + rm -f /etc/yum.repos.d/CentOS-PowerTools-CyberPanel.repo + + if [[ "$Server_Country" = "CN" ]] ; then + dnf --nogpg install -y https://cyberpanel.sh/mirror.ghettoforge.net/distributions/gf/gf-release-latest.gf.el8.noarch.rpm + else + dnf --nogpg install -y https://mirror.ghettoforge.net/distributions/gf/gf-release-latest.gf.el8.noarch.rpm + fi + + dnf install epel-release -y + + dnf install -y wget strace htop net-tools telnet which bc telnet htop libevent-devel gcc libattr-devel xz-devel mariadb-connector-c-devel curl-devel git platform-python-devel tar socat bind-utils 2>/dev/null || dnf install -y --allowerasing wget strace htop net-tools telnet which bc htop libevent-devel gcc libattr-devel xz-devel mariadb-connector-c-devel curl-devel git platform-python-devel tar socat bind-utils + dnf install gpgme-devel -y 2>/dev/null || true + dnf install python3 -y + + elif [[ "$Server_OS_Version" = "9" ]] || [[ "$Server_OS_Version" = "10" ]] ; then + rm -f /etc/yum.repos.d/CentOS-PowerTools-CyberPanel.repo + + if [[ "$Server_Country" = "CN" ]] ; then + dnf --nogpg install -y https://cyberpanel.sh/mirror.ghettoforge.net/distributions/gf/gf-release-latest.gf.el9.noarch.rpm + else + dnf --nogpg install -y https://mirror.ghettoforge.net/distributions/gf/gf-release-latest.gf.el9.noarch.rpm + fi + + dnf install epel-release -y 2>/dev/null || { + # Fallback when appstream was broken or epel-release not in repo (e.g. AlmaLinux 9) + if [[ "$Server_OS_Version" = "9" ]]; then + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Installing EPEL from Fedora RPM (epel-release not in repo)..." | tee -a /var/log/cyberpanel_upgrade_debug.log + dnf install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-9.noarch.rpm 2>/dev/null || true + fi + } + + # MariaDB repo for EL8/EL9: any version (repo path uses major.minor: 10.11, 11.8, 12.1, 12.2, 12.3, etc.) + if [[ "$Server_OS_Version" = "8" ]] || [[ "$Server_OS_Version" = "9" ]] || [[ "$Server_OS_Version" = "10" ]]; then + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Configuring MariaDB $MARIADB_VER_REPO repository and upgrading MariaDB..." | tee -a /var/log/cyberpanel_upgrade_debug.log + Maybe_Backup_MariaDB_Before_Upgrade + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Writing MariaDB $MARIADB_VER_REPO repo and installing/upgrading packages..." | tee -a /var/log/cyberpanel_upgrade_debug.log + if [[ "$Server_OS_Version" = "9" ]] || [[ "$Server_OS_Version" = "10" ]]; then + MARIADB_REPO="rhel9-amd64" + else + MARIADB_REPO="rhel8-amd64" + fi + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Configuring MariaDB $MARIADB_VER_REPO repo for EL$Server_OS_Version (version $MARIADB_VER)..." | tee -a /var/log/cyberpanel_upgrade_debug.log + # Remove or backup any existing MariaDB repo that points to a different version, so dnf uses only our repo + for f in /etc/yum.repos.d/mariadb.repo /etc/yum.repos.d/MariaDB.repo.rpmsave; do + if [[ -f "$f" ]] && grep -q '10\.11\|10.6\|10.5' "$f" 2>/dev/null && [[ "$MARIADB_VER_REPO" != "10.11" ]]; then + mv -f "$f" "${f}.bak.cyberpanel" 2>/dev/null && echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Backed up old repo $f to ${f}.bak.cyberpanel (was 10.x, we want $MARIADB_VER_REPO)" | tee -a /var/log/cyberpanel_upgrade_debug.log || true + fi + done + cat << EOF > /etc/yum.repos.d/MariaDB.repo +# MariaDB $MARIADB_VER_REPO repository - CyberPanel upgrade +# https://downloads.mariadb.org/mariadb/repositories/ +[mariadb] +name = MariaDB $MARIADB_VER_REPO +baseurl = https://mirror.mariadb.org/yum/$MARIADB_VER_REPO/$MARIADB_REPO +gpgkey=https://yum.mariadb.org/RPM-GPG-KEY-MariaDB +gpgcheck=1 +EOF + if [[ "$Server_Country" = "CN" ]]; then + sed -i 's|http://yum.mariadb.org|https://cyberpanel.sh/yum.mariadb.org|g' /etc/yum.repos.d/MariaDB.repo + sed -i 's|https://yum.mariadb.org/RPM-GPG-KEY-MariaDB|https://cyberpanel.sh/yum.mariadb.org/RPM-GPG-KEY-MariaDB|g' /etc/yum.repos.d/MariaDB.repo + fi + dnf clean metadata --disablerepo='*' --enablerepo=mariadb 2>/dev/null || true + # MariaDB 10 -> 11 or 11 -> 12: RPM scriptlet blocks in-place upgrade; do manual stop, remove old server, install target, start, mariadb-upgrade + MARIADB_OLD_10=$(rpm -qa 'MariaDB-server-10*' 2>/dev/null | head -1) + MARIADB_OLD_11=$(rpm -qa 'MariaDB-server-11*' 2>/dev/null | head -1) + # Also detect 11.x by package version (e.g. MariaDB-server-11.8.6-1.el9) + [[ -z "$MARIADB_OLD_11" ]] && MARIADB_OLD_11=$(rpm -qa 'MariaDB-server*' 2>/dev/null | grep -E 'MariaDB-server-11\.' | head -1) + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] MariaDB detected: MARIADB_OLD_10=$MARIADB_OLD_10 MARIADB_OLD_11=$MARIADB_OLD_11 target=$MARIADB_VER_REPO" | tee -a /var/log/cyberpanel_upgrade_debug.log + if [[ -n "$MARIADB_OLD_10" ]] && { [[ "$MARIADB_VER_REPO" =~ ^11\. ]] || [[ "$MARIADB_VER_REPO" =~ ^12\. ]]; }; then + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] MariaDB 10.x detected; performing manual upgrade to $MARIADB_VER_REPO (stop, remove, install, start, mariadb-upgrade)..." | tee -a /var/log/cyberpanel_upgrade_debug.log + systemctl stop mariadb 2>/dev/null || true + sleep 2 + [[ -f /etc/my.cnf ]] && cp -a /etc/my.cnf /etc/my.cnf.bak.cyberpanel 2>/dev/null || true + [[ -d /etc/my.cnf.d ]] && cp -a /etc/my.cnf.d /etc/my.cnf.d.bak.cyberpanel 2>/dev/null || true + rpm -e "$MARIADB_OLD_10" --nodeps 2>/dev/null || true + dnf install -y --enablerepo=mariadb MariaDB-server MariaDB-client MariaDB-devel 2>/dev/null || true + mkdir -p /etc/my.cnf.d + printf "[client]\nskip-ssl = true\n" > /etc/my.cnf.d/cyberpanel-client.cnf 2>/dev/null || true + systemctl start mariadb 2>/dev/null || true + sleep 2 + mariadb-upgrade -u root 2>/dev/null || true + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] MariaDB manual upgrade to $MARIADB_VER_REPO completed." | tee -a /var/log/cyberpanel_upgrade_debug.log + elif [[ -n "$MARIADB_OLD_11" ]] && [[ "$MARIADB_VER_REPO" =~ ^12\. ]]; then + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] MariaDB 11.x detected; performing manual upgrade to $MARIADB_VER_REPO (stop, remove, install, start, mariadb-upgrade)..." | tee -a /var/log/cyberpanel_upgrade_debug.log + systemctl stop mariadb 2>/dev/null || true + sleep 2 + [[ -f /etc/my.cnf ]] && cp -a /etc/my.cnf /etc/my.cnf.bak.cyberpanel 2>/dev/null || true + [[ -d /etc/my.cnf.d ]] && cp -a /etc/my.cnf.d /etc/my.cnf.d.bak.cyberpanel 2>/dev/null || true + rpm -e "$MARIADB_OLD_11" --nodeps 2>/dev/null || true + dnf install -y --enablerepo=mariadb MariaDB-server MariaDB-client MariaDB-devel 2>/dev/null || true + mkdir -p /etc/my.cnf.d + printf "[client]\nskip-ssl = true\n" > /etc/my.cnf.d/cyberpanel-client.cnf 2>/dev/null || true + systemctl start mariadb 2>/dev/null || true + sleep 2 + mariadb-upgrade -u root 2>/dev/null || true + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] MariaDB manual upgrade to $MARIADB_VER_REPO completed (11->12)." | tee -a /var/log/cyberpanel_upgrade_debug.log + else + # Normal install/upgrade (same version or 10.11) + dnf install -y --enablerepo=mariadb MariaDB-server MariaDB-client MariaDB-devel 2>/dev/null || true + dnf upgrade -y --enablerepo=mariadb MariaDB-server MariaDB-client MariaDB-devel 2>/dev/null || true + systemctl restart mariadb 2>/dev/null || true + # Fallback: if we wanted 12.x but server is still 11.x (RPM scriptlet blocked in-place upgrade), do manual 11->12 + if [[ "$MARIADB_VER_REPO" =~ ^12\. ]]; then + STILL_11=$(rpm -qa 'MariaDB-server-11*' 2>/dev/null | head -1) + [[ -z "$STILL_11" ]] && STILL_11=$(rpm -qa 'MariaDB-server*' 2>/dev/null | grep -E 'MariaDB-server-11\.' | head -1) + if [[ -n "$STILL_11" ]]; then + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] MariaDB server still 11.x after dnf upgrade; performing manual 11->12 upgrade..." | tee -a /var/log/cyberpanel_upgrade_debug.log + systemctl stop mariadb 2>/dev/null || true + sleep 2 + [[ -f /etc/my.cnf ]] && cp -a /etc/my.cnf /etc/my.cnf.bak.cyberpanel 2>/dev/null || true + [[ -d /etc/my.cnf.d ]] && cp -a /etc/my.cnf.d /etc/my.cnf.d.bak.cyberpanel 2>/dev/null || true + rpm -e "$STILL_11" --nodeps 2>/dev/null || true + dnf install -y --enablerepo=mariadb MariaDB-server MariaDB-client MariaDB-devel 2>/dev/null || true + mkdir -p /etc/my.cnf.d + printf "[client]\nskip-ssl = true\n" > /etc/my.cnf.d/cyberpanel-client.cnf 2>/dev/null || true + systemctl start mariadb 2>/dev/null || true + sleep 2 + mariadb-upgrade -u root 2>/dev/null || true + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] MariaDB manual 11->12 fallback completed." | tee -a /var/log/cyberpanel_upgrade_debug.log + fi + fi + fi + # Allow local client to connect without SSL (11.x client defaults to SSL; 10.x server may not have it) + mkdir -p /etc/my.cnf.d + printf "[client]\nskip-ssl = true\n" > /etc/my.cnf.d/cyberpanel-client.cnf 2>/dev/null || true + # Optional: migrate from latin1 to UTF-8 (utf8mb4) when --migrate-to-utf8 and 11.x/12.x + if [[ "$Migrate_MariaDB_To_UTF8_Requested" = "yes" ]] && { [[ "$MARIADB_VER_REPO" =~ ^11\. ]] || [[ "$MARIADB_VER_REPO" =~ ^12\. ]]; }; then + Migrate_MariaDB_To_UTF8 + fi + fi + + # AlmaLinux 9 specific package installation + if [[ "$Server_OS" = "AlmaLinux9" ]] ; then + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Installing AlmaLinux 9 specific packages (Development Tools, PHP deps, MariaDB)..." | tee -a /var/log/cyberpanel_upgrade_debug.log + + # Install essential build tools + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Running: dnf groupinstall -y 'Development Tools' (may take a few minutes)..." | tee -a /var/log/cyberpanel_upgrade_debug.log + dnf groupinstall -y 'Development Tools' + + # Install PHP dependencies that are missing on AlmaLinux 9 + dnf install -y ImageMagick ImageMagick-devel gd gd-devel libicu libicu-devel \ + oniguruma oniguruma-devel aspell aspell-devel libc-client libc-client-devel \ + libmemcached libmemcached-devel freetype-devel libjpeg-turbo-devel \ + libpng-devel libwebp-devel libXpm-devel libzip-devel openssl-devel \ + sqlite-devel libxml2-devel libxslt-devel curl-devel libedit-devel \ + readline-devel pkgconfig cmake gcc-c++ + + # Install/upgrade MariaDB from our repo (any version: 10.11, 11.8, 12.x). Manual path for 10->11 and 11->12. + MARIADB_OLD_10_AL9=$(rpm -qa 'MariaDB-server-10*' 2>/dev/null | head -1) + MARIADB_OLD_11_AL9=$(rpm -qa 'MariaDB-server-11*' 2>/dev/null | head -1) + [[ -z "$MARIADB_OLD_11_AL9" ]] && MARIADB_OLD_11_AL9=$(rpm -qa 'MariaDB-server*' 2>/dev/null | grep -E 'MariaDB-server-11\.' | head -1) + if [[ -n "$MARIADB_OLD_10_AL9" ]] && { [[ "$MARIADB_VER_REPO" =~ ^11\. ]] || [[ "$MARIADB_VER_REPO" =~ ^12\. ]]; }; then + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] MariaDB 10.x detected (AlmaLinux 9); manual upgrade to $MARIADB_VER_REPO..." | tee -a /var/log/cyberpanel_upgrade_debug.log + systemctl stop mariadb 2>/dev/null || true + sleep 2 + [[ -f /etc/my.cnf ]] && cp -a /etc/my.cnf /etc/my.cnf.bak.cyberpanel 2>/dev/null || true + [[ -d /etc/my.cnf.d ]] && cp -a /etc/my.cnf.d /etc/my.cnf.d.bak.cyberpanel 2>/dev/null || true + rpm -e "$MARIADB_OLD_10_AL9" --nodeps 2>/dev/null || true + dnf install -y --enablerepo=mariadb MariaDB-server MariaDB-devel 2>/dev/null || dnf install -y mariadb-server mariadb-devel + mkdir -p /etc/my.cnf.d + printf "[client]\nskip-ssl = true\n" > /etc/my.cnf.d/cyberpanel-client.cnf 2>/dev/null || true + systemctl start mariadb 2>/dev/null || true + sleep 2 + mariadb-upgrade -u root 2>/dev/null || true + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] MariaDB manual upgrade to $MARIADB_VER_REPO completed (AlmaLinux 9)." | tee -a /var/log/cyberpanel_upgrade_debug.log + elif [[ -n "$MARIADB_OLD_11_AL9" ]] && [[ "$MARIADB_VER_REPO" =~ ^12\. ]]; then + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] MariaDB 11.x detected (AlmaLinux 9); manual upgrade to $MARIADB_VER_REPO..." | tee -a /var/log/cyberpanel_upgrade_debug.log + systemctl stop mariadb 2>/dev/null || true + sleep 2 + [[ -f /etc/my.cnf ]] && cp -a /etc/my.cnf /etc/my.cnf.bak.cyberpanel 2>/dev/null || true + [[ -d /etc/my.cnf.d ]] && cp -a /etc/my.cnf.d /etc/my.cnf.d.bak.cyberpanel 2>/dev/null || true + rpm -e "$MARIADB_OLD_11_AL9" --nodeps 2>/dev/null || true + dnf install -y --enablerepo=mariadb MariaDB-server MariaDB-devel 2>/dev/null || dnf install -y mariadb-server mariadb-devel + mkdir -p /etc/my.cnf.d + printf "[client]\nskip-ssl = true\n" > /etc/my.cnf.d/cyberpanel-client.cnf 2>/dev/null || true + systemctl start mariadb 2>/dev/null || true + sleep 2 + mariadb-upgrade -u root 2>/dev/null || true + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] MariaDB manual upgrade to $MARIADB_VER_REPO completed (AlmaLinux 9, 11->12)." | tee -a /var/log/cyberpanel_upgrade_debug.log + else + dnf install -y --enablerepo=mariadb MariaDB-server MariaDB-devel 2>/dev/null || dnf install -y mariadb-server mariadb-devel + dnf upgrade -y --enablerepo=mariadb MariaDB-server MariaDB-devel 2>/dev/null || true + systemctl restart mariadb 2>/dev/null || true + fi + # Allow local client to connect without SSL + mkdir -p /etc/my.cnf.d + printf "[client]\nskip-ssl = true\n" > /etc/my.cnf.d/cyberpanel-client.cnf 2>/dev/null || true + + # Install additional required packages (omit curl - AlmaLinux 9 has curl-minimal, avoid conflict) + dnf install -y wget unzip zip rsync firewalld psmisc git python3 python3-pip python3-devel 2>/dev/null || dnf install -y --allowerasing wget unzip zip rsync firewalld psmisc git python3 python3-pip python3-devel + fi + + # Omit curl to avoid conflict with curl-minimal on AlmaLinux 9; curl-devel for build is separate + dnf install -y wget strace htop net-tools telnet which bc telnet htop libevent-devel gcc libattr-devel xz-devel mariadb-connector-c-devel curl-devel git platform-python-devel tar socat bind-utils 2>/dev/null || dnf install -y --allowerasing wget strace htop net-tools telnet which bc htop libevent-devel gcc libattr-devel xz-devel mariadb-connector-c-devel curl-devel git platform-python-devel tar socat bind-utils + dnf install gpgme-devel -y 2>/dev/null || true + dnf install python3 -y + fi +elif [[ "$Server_OS" = "Ubuntu" ]] ; then + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Setting up Ubuntu repositories..." | tee -a /var/log/cyberpanel_upgrade_debug.log + + # Ensure nobody group exists (required for various operations) + if ! getent group nobody > /dev/null 2>&1 ; then + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Creating 'nobody' group..." | tee -a /var/log/cyberpanel_upgrade_debug.log + groupadd nobody + fi + + apt update -y + if [[ -z "$Skip_System_Update" ]]; then + export DEBIAN_FRONTEND=noninteractive ; apt-get -o Dpkg::Options::="--force-confold" upgrade -y + else + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Skipping apt upgrade (--no-system-update)" | tee -a /var/log/cyberpanel_upgrade_debug.log + fi + + # MariaDB: add official repo and install/upgrade to chosen version on Ubuntu/Debian (any version) + if [[ -n "$MARIADB_VER_REPO" ]]; then + Maybe_Backup_MariaDB_Before_Upgrade + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Configuring MariaDB $MARIADB_VER_REPO repo for Ubuntu/Debian (version $MARIADB_VER)..." | tee -a /var/log/cyberpanel_upgrade_debug.log + curl -LsS https://downloads.mariadb.com/MariaDB/mariadb_repo_setup | bash -s -- --mariadb-server-version="$MARIADB_VER_REPO" 2>/dev/null || true + # Must run apt-get update after adding repo so 11.8 packages are visible (otherwise apt keeps 10.11) + apt-get update -qq 2>/dev/null || apt-get update + export DEBIAN_FRONTEND=noninteractive + apt-get install -y mariadb-server mariadb-client 2>/dev/null || true + apt-get install -y -o Dpkg::Options::="--force-confold" mariadb-server mariadb-client 2>/dev/null || true + systemctl restart mariadb 2>/dev/null || systemctl restart mysql 2>/dev/null || true + if [[ "$Migrate_MariaDB_To_UTF8_Requested" = "yes" ]] && { [[ "$MARIADB_VER_REPO" =~ ^11\. ]] || [[ "$MARIADB_VER_REPO" =~ ^12\. ]]; }; then + Migrate_MariaDB_To_UTF8 + fi + fi + + if [[ "$Server_OS_Version" = "22" ]] || [[ "$Server_OS_Version" = "24" ]] ; then + if [[ "$Server_OS_Version" = "24" ]]; then + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Installing Ubuntu 24.04 specific packages..." | tee -a /var/log/cyberpanel_upgrade_debug.log + else + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Installing Ubuntu 22.04 specific packages..." | tee -a /var/log/cyberpanel_upgrade_debug.log + fi + # Install Python development packages required for virtualenv on Ubuntu 22.04/24.04 + DEBIAN_FRONTEND=noninteractive apt install -y python3-dev python3-venv python3-pip python3-setuptools python3-wheel + DEBIAN_FRONTEND=noninteractive apt install -y dnsutils net-tools htop telnet libcurl4-gnutls-dev libgnutls28-dev libgcrypt20-dev libattr1 libattr1-dev liblzma-dev libgpgme-dev libcurl4-gnutls-dev libssl-dev nghttp2 libnghttp2-dev idn2 libidn2-dev libidn2-0-dev librtmp-dev libpsl-dev nettle-dev libgnutls28-dev libldap2-dev libgssapi-krb5-2 libk5crypto3 libkrb5-dev libcomerr2 libldap2-dev virtualenv git socat vim unzip zip libmariadb-dev-compat libmariadb-dev + + else + DEBIAN_FRONTEND=noninteractive apt install -y htop telnet libcurl4-gnutls-dev libgnutls28-dev libgcrypt20-dev libattr1 libattr1-dev liblzma-dev libgpgme-dev libmariadb-dev-compat libmariadb-dev libcurl4-gnutls-dev libssl-dev nghttp2 libnghttp2-dev idn2 libidn2-dev librtmp-dev libpsl-dev nettle-dev libgnutls28-dev libldap2-dev libgssapi-krb5-2 libk5crypto3 libkrb5-dev libcom-err2 libldap2-dev virtualenv git dnsutils + fi + DEBIAN_FRONTEND=noninteractive apt install -y python3-pip + DEBIAN_FRONTEND=noninteractive apt install -y build-essential libssl-dev libffi-dev python3-dev + DEBIAN_FRONTEND=noninteractive apt install -y python3-venv + + ### fix for pip issue on ubuntu 22 and 24 + + apt-get remove --purge virtualenv -y + # Handle Ubuntu 24.04's externally-managed-environment policy + if [[ "$Server_OS_Version" = "24" ]]; then + echo -e "Ubuntu 24.04 detected - using apt for virtualenv installation" + DEBIAN_FRONTEND=noninteractive apt-get install -y python3-virtualenv + else + pip uninstall -y virtualenv 2>/dev/null || true + rm -rf /usr/lib/python3/dist-packages/virtualenv* + pip3 install --upgrade virtualenv + fi + + + if [[ "$Server_OS_Version" = "18" ]] ; then + : +#all pre-upgrade operation for Ubuntu 18 + elif [[ "$Server_OS_Version" = "20" ]] ; then +# if ! grep -q "focal" /etc/apt/sources.list.d/dovecot.list ; then +# sed -i 's|ce-2.3-latest/ubuntu/bionic bionic main|ce-2.3-latest/ubuntu/focal focal main|g' /etc/apt/sources.list.d/dovecot.list +# rm -rf /etc/dovecot-backup +# cp -r /etc/dovecot /etc/dovecot-backup +# apt update +# DEBIAN_FRONTEND=noninteractive apt -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" remove -y dovecot-mysql dovecot-pop3d dovecot-imapd +# DEBIAN_FRONTEND=noninteractive apt -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" install -y dovecot-mysql dovecot-pop3d dovecot-imapd +# systemctl restart dovecot +# fi + #fix ubuntu 20 webmail login issue + + rm -f /etc/apt/sources.list.d/dovecot.list + apt update + DEBIAN_FRONTEND=noninteractive DEBIAN_PRIORITY=critical apt -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" upgrade -y + fi +#all pre-upgrade operation for Ubuntu 20 +fi +if [[ "$Server_OS" = "openEuler" ]] ; then + rm -f /etc/yum.repos.d/CyberPanel.repo + rm -f /etc/yum.repos.d/litespeed.repo + + yum clean all + yum update -y + + dnf install -y wget strace htop net-tools telnet curl which bc telnet htop libevent-devel gcc libattr-devel xz-devel mariadb-devel curl-devel git python3-devel tar socat bind-utils + dnf install gpgme-devel -y + dnf install python3 -y +fi +#all pre-upgrade operation for openEuler +} + diff --git a/upgrade_modules/06_components.sh b/upgrade_modules/06_components.sh new file mode 100644 index 000000000..b76d62850 --- /dev/null +++ b/upgrade_modules/06_components.sh @@ -0,0 +1,290 @@ +#!/usr/bin/env bash +# CyberPanel upgrade – download requirements and required components (venv, pip, recovery). Sourced by cyberpanel_upgrade.sh. + +Download_Requirement() { +echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Starting Download_Requirement function..." | tee -a /var/log/cyberpanel_upgrade_debug.log +for i in {1..50}; + do + if [[ "$Server_OS_Version" = "22" ]] || [[ "$Server_OS_Version" = "24" ]] || [[ "$Server_OS_Version" = "9" ]] || [[ "$Server_OS_Version" = "10" ]]; then + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Downloading requirements.txt for OS version $Server_OS_Version" | tee -a /var/log/cyberpanel_upgrade_debug.log + if command -v wget >/dev/null 2>&1; then wget -O /usr/local/requirments.txt "${Git_Content_URL}/${Branch_Name}/requirments.txt" 2>&1 | tee -a /var/log/cyberpanel_upgrade_debug.log; else curl -sL -o /usr/local/requirments.txt "${Git_Content_URL}/${Branch_Name}/requirments.txt" 2>&1 | tee -a /var/log/cyberpanel_upgrade_debug.log; fi + else + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Downloading requirements-old.txt for OS version $Server_OS_Version" | tee -a /var/log/cyberpanel_upgrade_debug.log + if command -v wget >/dev/null 2>&1; then wget -O /usr/local/requirments.txt "${Git_Content_URL}/${Branch_Name}/requirments-old.txt" 2>&1 | tee -a /var/log/cyberpanel_upgrade_debug.log; else curl -sL -o /usr/local/requirments.txt "${Git_Content_URL}/${Branch_Name}/requirments-old.txt" 2>&1 | tee -a /var/log/cyberpanel_upgrade_debug.log; fi + fi + if grep -q "Django==" /usr/local/requirments.txt ; then + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Requirements file downloaded successfully" | tee -a /var/log/cyberpanel_upgrade_debug.log + + # Fix pysftp dependency issue by removing it from requirements + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Fixing pysftp dependency issue..." | tee -a /var/log/cyberpanel_upgrade_debug.log + sed -i 's/^pysftp$/# pysftp - deprecated, using paramiko instead/' /usr/local/requirments.txt + sed -i 's/pysftp/# pysftp - deprecated, using paramiko instead/' /usr/local/requirments.txt + + break + else + echo -e "\n Requirement list has failed to download for $i times..." + echo -e "Wait for 30 seconds and try again...\n" + sleep 30 + fi +done +#special made function for Gitee.com, for whatever reason sometimes it fails to download this file +} + + + +Pre_Upgrade_Required_Components() { + +# Check if CyberCP directory exists but is incomplete/damaged +echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Checking CyberCP directory integrity..." | tee -a /var/log/cyberpanel_upgrade_debug.log + +# Define essential CyberCP components +CYBERCP_ESSENTIAL_DIRS=( + "/usr/local/CyberCP/CyberCP" + "/usr/local/CyberCP/plogical" + "/usr/local/CyberCP/websiteFunctions" + "/usr/local/CyberCP/manage" +) + +CYBERCP_MISSING=0 +for dir in "${CYBERCP_ESSENTIAL_DIRS[@]}"; do + if [ ! -d "$dir" ]; then + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] INFO: Essential directory missing (will restore): $dir" | tee -a /var/log/cyberpanel_upgrade_debug.log + CYBERCP_MISSING=1 + fi +done + +# If essential directories are missing, perform automatic recovery (normal on some upgrade paths) +if [ $CYBERCP_MISSING -eq 1 ]; then + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] INFO: Restoring missing CyberCP directories from repository..." | tee -a /var/log/cyberpanel_upgrade_debug.log + + # Backup any remaining configuration files if they exist + if [ -f "/usr/local/CyberCP/CyberCP/settings.py" ]; then + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Backing up existing settings.py..." | tee -a /var/log/cyberpanel_upgrade_debug.log + cp /usr/local/CyberCP/CyberCP/settings.py /tmp/cyberpanel_settings_backup.py + fi + + # Clone fresh CyberPanel repository + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Cloning fresh CyberPanel repository for recovery..." | tee -a /var/log/cyberpanel_upgrade_debug.log + cd /usr/local + rm -rf CyberCP_recovery_tmp + + if git clone "$Git_Clone_URL" CyberCP_recovery_tmp; then + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Repository cloned successfully for recovery" | tee -a /var/log/cyberpanel_upgrade_debug.log + + # Checkout the appropriate branch + cd CyberCP_recovery_tmp + git checkout "$Branch_Name" 2>/dev/null || git checkout stable + + # Copy missing components while preserving existing configurations + for dir in "${CYBERCP_ESSENTIAL_DIRS[@]}"; do + if [ ! -d "$dir" ]; then + # Extract relative path after /usr/local/CyberCP/ + relative_path=${dir#/usr/local/CyberCP/} + if [ -d "/usr/local/CyberCP_recovery_tmp/$relative_path" ]; then + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Restoring missing directory: $dir" | tee -a /var/log/cyberpanel_upgrade_debug.log + mkdir -p "$(dirname "$dir")" + cp -r "/usr/local/CyberCP_recovery_tmp/$relative_path" "$dir" + fi + fi + done + + # Restore settings.py if it was backed up + if [ -f "/tmp/cyberpanel_settings_backup.py" ]; then + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Restoring backed up settings.py..." | tee -a /var/log/cyberpanel_upgrade_debug.log + cp /tmp/cyberpanel_settings_backup.py /usr/local/CyberCP/CyberCP/settings.py + fi + + # Clean up temporary clone + rm -rf /usr/local/CyberCP_recovery_tmp + + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Recovery completed. All essential CyberCP directories restored." | tee -a /var/log/cyberpanel_upgrade_debug.log + else + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] ERROR: Failed to clone repository for recovery" | tee -a /var/log/cyberpanel_upgrade_debug.log + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Please run full installation instead of upgrade" | tee -a /var/log/cyberpanel_upgrade_debug.log + exit 1 + fi + + cd /root/cyberpanel_upgrade_tmp || cd /root +fi + +if [ "$Server_OS" = "Ubuntu" ]; then + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Preparing Ubuntu environment for virtualenv..." | tee -a /var/log/cyberpanel_upgrade_debug.log + rm -rf /usr/local/CyberPanel + + # For Ubuntu 22.04 and 24.04, handle virtualenv installation properly + if [[ "$Server_OS_Version" = "22" ]] || [[ "$Server_OS_Version" = "24" ]]; then + if [[ "$Server_OS_Version" = "24" ]]; then + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Ubuntu 24.04: Using apt for virtualenv installation (externally-managed-environment policy)..." | tee -a /var/log/cyberpanel_upgrade_debug.log + # Ubuntu 24.04 has externally-managed-environment, use apt + DEBIAN_FRONTEND=noninteractive apt-get install -y python3-virtualenv python3-venv + else + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Ubuntu 22.04: Installing/upgrading virtualenv with proper dependencies..." | tee -a /var/log/cyberpanel_upgrade_debug.log + # Remove system virtualenv if it exists to avoid conflicts + apt remove -y python3-virtualenv 2>/dev/null || true + # Install latest virtualenv via pip + pip3 install --upgrade pip setuptools wheel + pip3 install --upgrade virtualenv + fi + else + pip3 install --upgrade virtualenv + fi +else + rm -rf /usr/local/CyberPanel + # AlmaLinux 9/10, Rocky 9: use python3 -m venv (no virtualenv pkg needed) + if [[ "$Server_OS" = "AlmaLinux" ]] || [[ "$Server_OS" = "AlmaLinux9" ]] || [[ "$Server_OS" = "RockyLinux" ]]; then + if [[ "$Server_OS_Version" = "9" ]] || [[ "$Server_OS_Version" = "10" ]]; then + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] AlmaLinux/Rocky $Server_OS_Version: will use python3 -m venv, skipping virtualenv package" | tee -a /var/log/cyberpanel_upgrade_debug.log + else + if [ -e /usr/bin/pip3 ]; then PIP3="/usr/bin/pip3"; else PIP3="pip3.6"; fi + $PIP3 install --default-timeout=3600 virtualenv + Check_Return + fi + else + if [ -e /usr/bin/pip3 ]; then PIP3="/usr/bin/pip3"; else PIP3="pip3.6"; fi + $PIP3 install --default-timeout=3600 virtualenv + Check_Return + fi +fi + +if [[ -f /usr/local/CyberPanel/bin/python2 ]]; then + echo -e "\nPython 2 dectected, doing re-setup...\n" + rm -rf /usr/local/CyberPanel/bin + if [[ "$Server_OS" = "Ubuntu" ]] && ([[ "$Server_OS_Version" = "22" ]] || [[ "$Server_OS_Version" = "24" ]]); then + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Ubuntu $Server_OS_Version detected, using python3 -m venv..." | tee -a /var/log/cyberpanel_upgrade_debug.log + python3 -m venv --system-site-packages /usr/local/CyberPanel + elif [[ "$Server_OS" = "CentOS" ]] || [[ "$Server_OS" = "AlmaLinux" ]] || [[ "$Server_OS" = "AlmaLinux9" ]] || [[ "$Server_OS" = "RockyLinux" ]]; then + if [[ "$Server_OS_Version" = "9" ]] || [[ "$Server_OS_Version" = "10" ]]; then + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] AlmaLinux/Rocky $Server_OS_Version detected, using python3 -m venv..." | tee -a /var/log/cyberpanel_upgrade_debug.log + python3 -m venv --system-site-packages /usr/local/CyberPanel + else + PYTHON_PATH=$(which python3 2>/dev/null || which python3.9 2>/dev/null || echo "/usr/bin/python3") + virtualenv -p "$PYTHON_PATH" --system-site-packages /usr/local/CyberPanel + fi + else + virtualenv -p /usr/bin/python3 --system-site-packages /usr/local/CyberPanel + fi + Check_Return +elif [[ -d /usr/local/CyberPanel/bin/ ]]; then + echo -e "\nNo need to re-setup virtualenv at /usr/local/CyberPanel...\n" +else + #!/bin/bash + +echo -e "\nNo existing virtualenv found; creating fresh Python environment...\n" + +# Attempt to create a virtual environment +if [[ "$Server_OS" = "Ubuntu" ]] && ([[ "$Server_OS_Version" = "22" ]] || [[ "$Server_OS_Version" = "24" ]]); then + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Ubuntu $Server_OS_Version detected, using python3 -m venv..." | tee -a /var/log/cyberpanel_upgrade_debug.log + python3 -m venv /usr/local/CyberPanel +elif [[ "$Server_OS" = "CentOS" ]] || [[ "$Server_OS" = "AlmaLinux" ]] || [[ "$Server_OS" = "AlmaLinux9" ]] || [[ "$Server_OS" = "RockyLinux" ]]; then + if [[ "$Server_OS_Version" = "9" ]] || [[ "$Server_OS_Version" = "10" ]]; then + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] AlmaLinux/Rocky $Server_OS_Version: using python3 -m venv (no virtualenv pkg needed)..." | tee -a /var/log/cyberpanel_upgrade_debug.log + python3 -m venv --system-site-packages /usr/local/CyberPanel + else + PYTHON_PATH=$(which python3 2>/dev/null || which python3.9 2>/dev/null || echo "/usr/bin/python3") + virtualenv -p "$PYTHON_PATH" --system-site-packages /usr/local/CyberPanel + fi +else + virtualenv -p /usr/bin/python3 --system-site-packages /usr/local/CyberPanel +fi + +# Check if the virtualenv/venv command failed +if [ $? -ne 0 ]; then + echo "virtualenv command failed." + + # Check if the operating system is AlmaLinux + if grep -q "AlmaLinux" /etc/os-release; then + echo "Operating system is AlmaLinux." + + # Check if the 'packaging' module is installed via RPM + if rpm -q python3-packaging >/dev/null 2>&1; then + echo "'packaging' module installed via RPM. Proceeding with uninstallation." + + # Uninstall the 'packaging' module using RPM + sudo dnf remove python3-packaging -y + + # Check if uninstallation was successful + if [ $? -eq 0 ]; then + echo "Successfully uninstalled 'packaging' module." + + # Install and upgrade 'packaging' using pip + pip install --upgrade packaging + + # Verify the installation + if [ $? -eq 0 ]; then + echo "'packaging' module reinstalled and upgraded successfully." + if [[ "$Server_OS" = "Ubuntu" ]] && ([[ "$Server_OS_Version" = "22" ]] || [[ "$Server_OS_Version" = "24" ]]); then + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Ubuntu: using python3 -m venv..." | tee -a /var/log/cyberpanel_upgrade_debug.log + python3 -m venv --system-site-packages /usr/local/CyberPanel + elif [[ "$Server_OS" = "CentOS" ]] || [[ "$Server_OS" = "AlmaLinux" ]] || [[ "$Server_OS" = "AlmaLinux9" ]] || [[ "$Server_OS" = "RockyLinux" ]]; then + if [[ "$Server_OS_Version" = "9" ]] || [[ "$Server_OS_Version" = "10" ]]; then + python3 -m venv --system-site-packages /usr/local/CyberPanel + else + PYTHON_PATH=$(which python3 2>/dev/null || which python3.9 2>/dev/null || echo "/usr/bin/python3") + virtualenv -p "$PYTHON_PATH" --system-site-packages /usr/local/CyberPanel + fi + else + virtualenv -p /usr/bin/python3 --system-site-packages /usr/local/CyberPanel + fi + else + echo "Failed to install 'packaging' module using pip." + fi + else + echo "Failed to uninstall 'packaging' module using RPM." + fi + else + echo "'packaging' module is not installed via RPM. No action taken." + fi + else + echo "Operating system is not AlmaLinux. No action taken." + fi +else + echo "virtualenv command executed successfully." +fi +fi + +# shellcheck disable=SC1091 +. /usr/local/CyberPanel/bin/activate +pip install --upgrade pip setuptools packaging + +Download_Requirement + +if [[ "$Server_OS" = "CentOS" ]] ; then +# $PIP3 install --default-timeout=3600 virtualenv==16.7.9 +# Check_Return + $PIP3 install --default-timeout=3600 --ignore-installed -r /usr/local/requirments.txt + Check_Return +elif [[ "$Server_OS" = "Ubuntu" ]] ; then + # shellcheck disable=SC1091 + . /usr/local/CyberPanel/bin/activate + Check_Return +# pip3 install --default-timeout=3600 virtualenv==16.7.9 +# Check_Return + pip3 install --default-timeout=3600 --ignore-installed -r /usr/local/requirments.txt + Check_Return +elif [[ "$Server_OS" = "openEuler" ]] ; then +# pip3 install --default-timeout=3600 virtualenv==16.7.9 +# Check_Return + pip3 install --default-timeout=3600 --ignore-installed -r /usr/local/requirments.txt + Check_Return +fi + +#virtualenv -p /usr/bin/python3 --system-site-packages /usr/local/CyberPanel +# Check_Return + +wget "${Git_Content_URL}/${Branch_Name}/plogical/upgrade.py" + +if [[ "$Server_Country" = "CN" ]] ; then + sed -i 's|git clone https://github.com/usmannasir/cyberpanel|echo git cloned|g' upgrade.py + + Retry_Command "git clone ${Git_Clone_URL}" + Check_Return "git clone ${Git_Clone_URL}" + + # shellcheck disable=SC2086 + sed -i 's|https://raw.githubusercontent.com/usmannasir/cyberpanel/stable/install/litespeed/httpd_config.xml|'${Git_Content_URL}/${Branch_Name}'//install/litespeed/httpd_config.xml|g' upgrade.py + sed -i 's|https://cyberpanel.sh/composer.sh|https://gitee.com/qtwrk/cyberpanel/raw/stable/install/composer_cn.sh|g' upgrade.py +fi + +} + +# (Pre_Upgrade_Setup_Git_URL is defined earlier; this duplicate removed so --repo is respected) + diff --git a/upgrade_modules/07_branch_input.sh b/upgrade_modules/07_branch_input.sh new file mode 100644 index 000000000..4a1cf57ce --- /dev/null +++ b/upgrade_modules/07_branch_input.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +# CyberPanel upgrade – branch input prompt. Sourced by cyberpanel_upgrade.sh. + +Pre_Upgrade_Branch_Input() { + echo -e "\nPress the Enter key to continue with latest version, or enter specific version such as: \e[31m2.3.4\e[39m , \e[31m2.4.4\e[39m ...etc" + echo -e "\nIf nothing is input in 10 seconds, script will proceed with the latest stable version. " + echo -e "\nPlease press the Enter key or specify a version number, or wait for 10 seconds: " + printf "%s" "" + read -r -t 10 Tmp_Input + if [[ $Tmp_Input = "" ]]; then + echo -e "Branch name set to $Branch_Name" + else + Branch_Check "$Tmp_Input" + fi +} + diff --git a/upgrade_modules/08_main_upgrade.sh b/upgrade_modules/08_main_upgrade.sh new file mode 100644 index 000000000..52fb76458 --- /dev/null +++ b/upgrade_modules/08_main_upgrade.sh @@ -0,0 +1,345 @@ +#!/usr/bin/env bash +# CyberPanel upgrade – main upgrade (Python, upgrade.py, venv, WSGI). Sourced by cyberpanel_upgrade.sh. + +Main_Upgrade() { +echo -e "\n[$(date +"%Y-%m-%d %H:%M:%S")] Starting Main_Upgrade function..." | tee -a /var/log/cyberpanel_upgrade_debug.log + +# Resolve Python for upgrade (avoid FileNotFoundError when /usr/local/CyberPanel/bin/python missing) +CP_PYTHON="" +for py in /usr/local/CyberPanel/bin/python /usr/local/CyberCP/bin/python /usr/bin/python3 /usr/local/bin/python3; do + if [[ -x "$py" ]]; then CP_PYTHON="$py"; break; fi +done +if [[ -z "$CP_PYTHON" ]]; then + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] ERROR: No Python found for upgrade (tried CyberPanel, CyberCP, python3)" | tee -a /var/log/cyberpanel_upgrade_debug.log + exit 1 +fi +echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Using Python: $CP_PYTHON" | tee -a /var/log/cyberpanel_upgrade_debug.log + +# Ensure ols_binaries_config exists (required by upgrade.py; may be missing when upgrading from older versions) +mkdir -p /usr/local/CyberCP/install +if [[ ! -f /usr/local/CyberCP/install/ols_binaries_config.py ]]; then + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Downloading ols_binaries_config.py (required for upgrade)..." | tee -a /var/log/cyberpanel_upgrade_debug.log + wget -q -O /usr/local/CyberCP/install/ols_binaries_config.py "${Git_Content_URL}/${Branch_Name}/install/ols_binaries_config.py" 2>/dev/null || \ + curl -sL -o /usr/local/CyberCP/install/ols_binaries_config.py "${Git_Content_URL}/${Branch_Name}/install/ols_binaries_config.py" 2>/dev/null || true +fi +if [[ ! -f /usr/local/CyberCP/install/ols_binaries_config.py ]]; then + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] WARNING: ols_binaries_config.py not found; upgrade.py may fail with ModuleNotFoundError" | tee -a /var/log/cyberpanel_upgrade_debug.log +fi + +echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Running: $CP_PYTHON upgrade.py $Branch_Name" | tee -a /var/log/cyberpanel_upgrade_debug.log + +# Export Git user so upgrade.py clones from the same repo (master3395 or --repo override) +export CYBERPANEL_GIT_USER="${Git_User:-master3395}" + +# Run upgrade.py and capture output +upgrade_output=$("$CP_PYTHON" upgrade.py "$Branch_Name" 2>&1) +RETURN_CODE=$? +echo "$upgrade_output" | tee -a /var/log/cyberpanel_upgrade_debug.log + +# Check for TypeError specifically +if echo "$upgrade_output" | grep -q "TypeError: expected string or bytes-like object"; then + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] WARNING: TypeError detected in upgrade.py, but continuing..." | tee -a /var/log/cyberpanel_upgrade_debug.log + # Check if upgrade actually completed despite the error + if echo "$upgrade_output" | grep -q "Upgrade Completed"; then + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Upgrade completed despite TypeError" | tee -a /var/log/cyberpanel_upgrade_debug.log + RETURN_CODE=0 + fi +fi + +echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Python upgrade.py returned code: $RETURN_CODE" | tee -a /var/log/cyberpanel_upgrade_debug.log + +# Check if the command was successful (return code 0) +if [ $RETURN_CODE -eq 0 ]; then + echo "Upgrade successful." + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] First upgrade attempt successful" | tee -a /var/log/cyberpanel_upgrade_debug.log +else + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] First upgrade attempt failed with code $RETURN_CODE, starting fallback..." | tee -a /var/log/cyberpanel_upgrade_debug.log + + + if [ -e /usr/bin/pip3 ]; then + PIP3="/usr/bin/pip3" + else + PIP3="pip3.6" + fi + + rm -rf /usr/local/CyberPanelTemp + + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Creating temporary virtual environment for fallback upgrade..." | tee -a /var/log/cyberpanel_upgrade_debug.log + + # Try python3 -m venv first (more reliable on Ubuntu 22.04) + if python3 -m venv --system-site-packages /usr/local/CyberPanelTemp 2>/dev/null; then + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Temporary virtualenv created with python3 -m venv" | tee -a /var/log/cyberpanel_upgrade_debug.log + else + # Fallback to virtualenv command + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Trying virtualenv command for temporary environment..." | tee -a /var/log/cyberpanel_upgrade_debug.log + virtualenv -p /usr/bin/python3 --system-site-packages /usr/local/CyberPanelTemp 2>&1 | tee -a /var/log/cyberpanel_upgrade_debug.log + fi + +# shellcheck disable=SC1091 +. /usr/local/CyberPanelTemp/bin/activate + +wget -O /usr/local/requirments-old.txt "${Git_Content_URL}/${Branch_Name}/requirments-old.txt" + + if [[ "$Server_OS" = "CentOS" ]] ; then +# $PIP3 install --default-timeout=3600 virtualenv==16.7.9 +# Check_Return + $PIP3 install --default-timeout=3600 --ignore-installed -r /usr/local/requirments-old.txt + Check_Return +elif [[ "$Server_OS" = "Ubuntu" ]] ; then + # shellcheck disable=SC1091 + . /usr/local/CyberPanelTemp/bin/activate + Check_Return + pip3 install --default-timeout=3600 --ignore-installed -r /usr/local/requirments-old.txt + Check_Return +elif [[ "$Server_OS" = "openEuler" ]] ; then + pip3 install --default-timeout=3600 --ignore-installed -r /usr/local/requirments-old.txt + Check_Return +fi + +echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Running fallback: /usr/local/CyberPanelTemp/bin/python upgrade.py $Branch_Name" | tee -a /var/log/cyberpanel_upgrade_debug.log +export CYBERPANEL_GIT_USER="${Git_User:-master3395}" +/usr/local/CyberPanelTemp/bin/python upgrade.py "$Branch_Name" 2>&1 | tee -a /var/log/cyberpanel_upgrade_debug.log +FALLBACK_CODE=$? +echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Fallback upgrade returned code: $FALLBACK_CODE" | tee -a /var/log/cyberpanel_upgrade_debug.log +Check_Return + +echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Removing temporary environment..." | tee -a /var/log/cyberpanel_upgrade_debug.log +rm -rf /usr/local/CyberPanelTemp + +fi + +echo -e "\n[$(date +"%Y-%m-%d %H:%M:%S")] Starting post-upgrade cleanup..." | tee -a /var/log/cyberpanel_upgrade_debug.log + +# Check if we need to recreate due to Python 2 +NEEDS_RECREATE=0 +if [[ -f /usr/local/CyberCP/bin/python2 ]]; then + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Found Python 2 in CyberCP, will recreate with Python 3..." | tee -a /var/log/cyberpanel_upgrade_debug.log + NEEDS_RECREATE=1 +fi + +echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Removing old CyberCP virtual environment directories..." | tee -a /var/log/cyberpanel_upgrade_debug.log +rm -rf /usr/local/CyberCP/bin +rm -rf /usr/local/CyberCP/lib +rm -rf /usr/local/CyberCP/lib64 +rm -rf /usr/local/CyberCP/pyvenv.cfg + +echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Checking CyberCP virtual environment status after cleanup..." | tee -a /var/log/cyberpanel_upgrade_debug.log + +# After removing directories, we always need to recreate +if [[ $NEEDS_RECREATE -eq 1 ]] || [[ ! -d /usr/local/CyberCP/bin ]]; then + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Creating/recreating CyberCP virtual environment with Python 3..." | tee -a /var/log/cyberpanel_upgrade_debug.log + + # First ensure the directory exists + mkdir -p /usr/local/CyberCP + + # For Ubuntu 22.04+, we need to handle virtualenv differently + VENV_SUCCESS=0 + + # First try using python3 -m venv (more reliable on Ubuntu 22.04) + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Attempting to create virtual environment using python3 -m venv..." | tee -a /var/log/cyberpanel_upgrade_debug.log + virtualenv_output=$(python3 -m venv --system-site-packages /usr/local/CyberCP 2>&1) + VENV_CODE=$? + echo "$virtualenv_output" | tee -a /var/log/cyberpanel_upgrade_debug.log + + if [[ $VENV_CODE -eq 0 ]] && [[ -f /usr/local/CyberCP/bin/activate ]]; then + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Virtual environment created successfully using python3 -m venv" | tee -a /var/log/cyberpanel_upgrade_debug.log + VENV_SUCCESS=1 + else + # If that fails, try virtualenv command + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] python3 -m venv failed, trying virtualenv command..." | tee -a /var/log/cyberpanel_upgrade_debug.log + + # On Ubuntu 22.04, we need to ensure proper virtualenv installation + if [[ "$Server_OS" = "Ubuntu" ]] && [[ "$Server_OS_Version" = "22" ]]; then + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Ubuntu 22.04 detected, ensuring virtualenv is properly installed..." | tee -a /var/log/cyberpanel_upgrade_debug.log + pip3 install --upgrade virtualenv 2>&1 | tee -a /var/log/cyberpanel_upgrade_debug.log + elif [[ "$Server_OS" = "CentOS" ]] && ([[ "$Server_OS_Version" = "9" ]] || [[ "$Server_OS_Version" = "10" ]]); then + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] AlmaLinux/Rocky Linux 9/10 detected, ensuring virtualenv is properly installed..." | tee -a /var/log/cyberpanel_upgrade_debug.log + pip3 install --upgrade virtualenv 2>&1 | tee -a /var/log/cyberpanel_upgrade_debug.log + elif [[ "$Server_OS" = "AlmaLinux9" ]]; then + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] AlmaLinux 9 detected, ensuring virtualenv is properly installed..." | tee -a /var/log/cyberpanel_upgrade_debug.log + pip3 install --upgrade virtualenv 2>&1 | tee -a /var/log/cyberpanel_upgrade_debug.log + fi + + # Find the correct python3 path + if [[ "$Server_OS" = "CentOS" ]] && ([[ "$Server_OS_Version" = "9" ]] || [[ "$Server_OS_Version" = "10" ]]); then + PYTHON_PATH=$(which python3 2>/dev/null || which python3.9 2>/dev/null || echo "/usr/bin/python3") + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Using Python path: $PYTHON_PATH" | tee -a /var/log/cyberpanel_upgrade_debug.log + virtualenv_output=$(virtualenv -p "$PYTHON_PATH" /usr/local/CyberCP 2>&1) + elif [[ "$Server_OS" = "AlmaLinux9" ]]; then + PYTHON_PATH=$(which python3 2>/dev/null || which python3.9 2>/dev/null || echo "/usr/bin/python3") + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] AlmaLinux 9 - Using Python path: $PYTHON_PATH" | tee -a /var/log/cyberpanel_upgrade_debug.log + virtualenv_output=$(virtualenv -p "$PYTHON_PATH" /usr/local/CyberCP 2>&1) + else + virtualenv_output=$(virtualenv -p /usr/bin/python3 /usr/local/CyberCP 2>&1) + fi + VENV_CODE=$? + echo "$virtualenv_output" | tee -a /var/log/cyberpanel_upgrade_debug.log + + # Check if TypeError occurred (common on Ubuntu 22.04) + if echo "$virtualenv_output" | grep -q "TypeError"; then + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] WARNING: TypeError detected, attempting workaround..." | tee -a /var/log/cyberpanel_upgrade_debug.log + + # Try alternative method using explicit system-site-packages + virtualenv_output=$(virtualenv --python=/usr/bin/python3 --system-site-packages /usr/local/CyberCP 2>&1) + VENV_CODE=$? + echo "$virtualenv_output" | tee -a /var/log/cyberpanel_upgrade_debug.log + fi + + if [[ -f /usr/local/CyberCP/bin/activate ]]; then + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Virtual environment created successfully" | tee -a /var/log/cyberpanel_upgrade_debug.log + VENV_SUCCESS=1 + VENV_CODE=0 + fi + fi + + if [[ $VENV_SUCCESS -eq 0 ]]; then + VENV_CODE=1 + fi + + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Virtualenv creation returned code: $VENV_CODE" | tee -a /var/log/cyberpanel_upgrade_debug.log + + if [[ $VENV_CODE -ne 0 ]]; then + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] FATAL: Virtualenv creation failed with code $VENV_CODE" | tee -a /var/log/cyberpanel_upgrade_debug.log + echo -e "Virtualenv creation failed. Please check the logs at /var/log/cyberpanel_upgrade_debug.log" + exit $VENV_CODE + fi +else + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] CyberCP virtualenv already exists, skipping recreation" | tee -a /var/log/cyberpanel_upgrade_debug.log + echo -e "\nNo need to re-setup virtualenv at /usr/local/CyberCP...\n" +fi + +echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Removing old requirements file..." | tee -a /var/log/cyberpanel_upgrade_debug.log +rm -f /usr/local/requirments.txt + +echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Downloading new requirements..." | tee -a /var/log/cyberpanel_upgrade_debug.log +Download_Requirement + +echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Installing Python packages..." | tee -a /var/log/cyberpanel_upgrade_debug.log +if [ "$Server_OS" = "Ubuntu" ]; then + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Ubuntu detected, activating virtual environment..." | tee -a /var/log/cyberpanel_upgrade_debug.log + # shellcheck disable=SC1091 + . /usr/local/CyberCP/bin/activate 2>&1 | tee -a /var/log/cyberpanel_upgrade_debug.log + ACTIVATE_CODE=$? + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Activate returned code: $ACTIVATE_CODE" | tee -a /var/log/cyberpanel_upgrade_debug.log + Check_Return + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Upgrading setuptools and packaging..." | tee -a /var/log/cyberpanel_upgrade_debug.log + pip install --upgrade setuptools packaging 2>&1 | tee -a /var/log/cyberpanel_upgrade_debug.log + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Installing requirements..." | tee -a /var/log/cyberpanel_upgrade_debug.log + pip3 install --default-timeout=3600 --ignore-installed -r /usr/local/requirments.txt 2>&1 | tee -a /var/log/cyberpanel_upgrade_debug.log + PIP_CODE=$? + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Pip install returned code: $PIP_CODE" | tee -a /var/log/cyberpanel_upgrade_debug.log + Check_Return +else + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Non-Ubuntu OS, activating virtual environment..." | tee -a /var/log/cyberpanel_upgrade_debug.log + # shellcheck disable=SC1091 + source /usr/local/CyberCP/bin/activate 2>&1 | tee -a /var/log/cyberpanel_upgrade_debug.log + ACTIVATE_CODE=$? + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Activate returned code: $ACTIVATE_CODE" | tee -a /var/log/cyberpanel_upgrade_debug.log + Check_Return + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Installing requirements..." | tee -a /var/log/cyberpanel_upgrade_debug.log + /usr/local/CyberCP/bin/pip3 install --default-timeout=3600 --ignore-installed -r /usr/local/requirments.txt 2>&1 | tee -a /var/log/cyberpanel_upgrade_debug.log + PIP_CODE=$? + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Pip install returned code: $PIP_CODE" | tee -a /var/log/cyberpanel_upgrade_debug.log + Check_Return +fi + +echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Verifying Django installation..." | tee -a /var/log/cyberpanel_upgrade_debug.log +# Test if Django is installed +if ! /usr/local/CyberCP/bin/python -c "import django" 2>/dev/null; then + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] WARNING: Django not found, installing requirements again..." | tee -a /var/log/cyberpanel_upgrade_debug.log + + # Re-activate virtual environment + source /usr/local/CyberCP/bin/activate + + # Install MySQL/MariaDB development headers for mysqlclient Python package + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Installing MySQL/MariaDB development headers..." | tee -a /var/log/cyberpanel_upgrade_debug.log + if [[ "$Server_OS" = "Ubuntu" ]] || [[ "$Server_OS" = "Debian" ]]; then + # Ubuntu/Debian + apt-get update -y + apt-get install -y libmariadb-dev libmariadb-dev-compat pkg-config build-essential + elif [[ "$Server_OS" =~ ^(CentOS|RHEL|AlmaLinux|RockyLinux|CloudLinux) ]]; then + # RHEL-based systems + if command -v dnf >/dev/null 2>&1; then + # Remove conflicting packages first + dnf remove -y mariadb mariadb-client-utils mariadb-server || true + dnf remove -y MariaDB-server MariaDB-client MariaDB-devel || true + + # Install development packages with conflict resolution + dnf install -y --allowerasing --skip-broken --nobest mariadb-devel pkgconfig gcc python3-devel || \ + dnf install -y --allowerasing --skip-broken --nobest mysql-devel pkgconfig gcc python3-devel || \ + dnf install -y --allowerasing --skip-broken --nobest mariadb-devel mariadb-connector-c-devel pkgconfig gcc python3-devel + else + yum install -y mariadb-devel pkgconfig gcc python3-devel + fi + fi + + # Check if mysql.h is available and create symlink if needed + if [[ ! -f "/usr/include/mysql/mysql.h" ]] && [[ -f "/usr/include/mariadb/mysql.h" ]]; then + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Creating mysql.h symlink for compatibility..." | tee -a /var/log/cyberpanel_upgrade_debug.log + mkdir -p /usr/include/mysql + ln -sf /usr/include/mariadb/mysql.h /usr/include/mysql/mysql.h + fi + + # Re-install requirements + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Re-installing Python requirements..." | tee -a /var/log/cyberpanel_upgrade_debug.log + pip install --default-timeout=3600 --ignore-installed -r /usr/local/requirments.txt 2>&1 | tee -a /var/log/cyberpanel_upgrade_debug.log +else + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Django is properly installed" | tee -a /var/log/cyberpanel_upgrade_debug.log +fi + +echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Installing WSGI-LSAPI with optimized compilation..." | tee -a /var/log/cyberpanel_upgrade_debug.log + +# Save current directory +UPGRADE_CWD=$(pwd) + +cd /tmp || exit +rm -rf wsgi-lsapi-2.1* + +wget -q https://www.litespeedtech.com/packages/lsapi/wsgi-lsapi-2.1.tgz 2>&1 | tee -a /var/log/cyberpanel_upgrade_debug.log +tar xf wsgi-lsapi-2.1.tgz +cd wsgi-lsapi-2.1 || exit + +echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Configuring WSGI..." | tee -a /var/log/cyberpanel_upgrade_debug.log +PYTHON_CFG="${CP_PYTHON:-/usr/bin/python3}" +[[ -x "$PYTHON_CFG" ]] || PYTHON_CFG="/usr/bin/python3" +"$PYTHON_CFG" ./configure.py 2>&1 | tee -a /var/log/cyberpanel_upgrade_debug.log + +# Fix Makefile to use proper optimization flags to avoid _FORTIFY_SOURCE warnings +echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Optimizing Makefile for proper compilation..." | tee -a /var/log/cyberpanel_upgrade_debug.log +if [[ -f Makefile ]]; then + # Replace -O0 -g3 with -O2 -g to satisfy _FORTIFY_SOURCE + sed -i 's/-O0 -g3/-O2 -g/g' Makefile + # Ensure we have proper optimization flags + if grep -q "CFLAGS" Makefile && ! grep -qF '-O2' Makefile; then + sed -i 's/CFLAGS =/CFLAGS = -O2/' Makefile + fi + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Makefile optimized for proper compilation" | tee -a /var/log/cyberpanel_upgrade_debug.log +fi + +echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Compiling WSGI with optimized flags..." | tee -a /var/log/cyberpanel_upgrade_debug.log +echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] (Upstream WSGI source may show harmless strncpy/gstate warnings; build can still succeed.)" | tee -a /var/log/cyberpanel_upgrade_debug.log +make clean 2>&1 | tee -a /var/log/cyberpanel_upgrade_debug.log +make 2>&1 | tee -a /var/log/cyberpanel_upgrade_debug.log + +echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Installing lswsgi binary..." | tee -a /var/log/cyberpanel_upgrade_debug.log +rm -f /usr/local/CyberCP/bin/lswsgi +cp lswsgi /usr/local/CyberCP/bin/ +chmod +x /usr/local/CyberCP/bin/lswsgi + +# Return to original directory +cd "$UPGRADE_CWD" || cd /root + +# Final verification +echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Running final verification..." | tee -a /var/log/cyberpanel_upgrade_debug.log +if /usr/local/CyberCP/bin/python -c "import django" 2>/dev/null && [[ -f /usr/local/CyberCP/bin/lswsgi ]]; then + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] All components successfully installed!" | tee -a /var/log/cyberpanel_upgrade_debug.log +else + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] WARNING: Some components may be missing, check logs" | tee -a /var/log/cyberpanel_upgrade_debug.log +fi + +echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Main_Upgrade function completed" | tee -a /var/log/cyberpanel_upgrade_debug.log +} + +# Sync /usr/local/CyberCP to the latest commit of the upgrade branch so Version Management +# page shows Current commit matching Latest (avoids "please upgrade" when upgrade already ran). +# Backs up and restores CyberCP/settings.py so production DB/config are not overwritten by the repo. diff --git a/upgrade_modules/09_sync.sh b/upgrade_modules/09_sync.sh new file mode 100644 index 000000000..bd5730f7e --- /dev/null +++ b/upgrade_modules/09_sync.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +# CyberPanel upgrade – sync CyberCP to latest commit. Sourced by cyberpanel_upgrade.sh. + +Sync_CyberCP_To_Latest() { + if [[ ! -d /usr/local/CyberCP/.git ]]; then + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] No .git in /usr/local/CyberCP, skipping sync" | tee -a /var/log/cyberpanel_upgrade_debug.log + return 0 + fi + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Syncing /usr/local/CyberCP to latest commit for branch: $Branch_Name" | tee -a /var/log/cyberpanel_upgrade_debug.log + # Backup production settings so sync does not overwrite DB credentials / local config + if [[ -f /usr/local/CyberCP/CyberCP/settings.py ]]; then + cp /usr/local/CyberCP/CyberCP/settings.py /tmp/cyberpanel_settings_backup.py + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Backed up settings.py for restore after sync" | tee -a /var/log/cyberpanel_upgrade_debug.log + fi + ( + cd /usr/local/CyberCP + git fetch origin 2>&1 | tee -a /var/log/cyberpanel_upgrade_debug.log + if git show-ref -q "refs/remotes/origin/$Branch_Name"; then + git checkout -B "$Branch_Name" "origin/$Branch_Name" 2>&1 | tee -a /var/log/cyberpanel_upgrade_debug.log + else + git checkout "$Branch_Name" 2>/dev/null || true + git pull --ff-only origin "$Branch_Name" 2>&1 | tee -a /var/log/cyberpanel_upgrade_debug.log || true + fi + ) + local sync_code=$? + # Restore production settings so panel keeps working (DB, secrets, etc.) + if [[ -f /tmp/cyberpanel_settings_backup.py ]]; then + cp /tmp/cyberpanel_settings_backup.py /usr/local/CyberCP/CyberCP/settings.py + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Restored settings.py after sync" | tee -a /var/log/cyberpanel_upgrade_debug.log + fi + # LiteSpeed serves /static/ from public/static/; ensure it has latest baseTemplate static files (e.g. dashboard JS) + if [[ -d /usr/local/CyberCP/public/static ]] && [[ -d /usr/local/CyberCP/baseTemplate/static/baseTemplate ]]; then + rsync -a /usr/local/CyberCP/baseTemplate/static/baseTemplate/ /usr/local/CyberCP/public/static/baseTemplate/ 2>/dev/null || \ + cp -r /usr/local/CyberCP/baseTemplate/static/baseTemplate/* /usr/local/CyberCP/public/static/baseTemplate/ 2>/dev/null || true + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Synced baseTemplate static to public/static" | tee -a /var/log/cyberpanel_upgrade_debug.log + fi + # Firewall UI (firewall.js) – so Firewall Rules and Banned IPs load correct layout and Modify buttons + if [[ -d /usr/local/CyberCP/public/static ]] && [[ -f /usr/local/CyberCP/firewall/static/firewall/firewall.js ]]; then + mkdir -p /usr/local/CyberCP/public/static/firewall + cp -f /usr/local/CyberCP/firewall/static/firewall/firewall.js /usr/local/CyberCP/public/static/firewall/ 2>/dev/null && \ + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Synced firewall static to public/static" | tee -a /var/log/cyberpanel_upgrade_debug.log || true + fi + if [[ $sync_code -eq 0 ]]; then + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Sync completed. Current HEAD: $(git -C /usr/local/CyberCP rev-parse HEAD 2>/dev/null || echo 'unknown')" | tee -a /var/log/cyberpanel_upgrade_debug.log + else + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Sync returned code $sync_code (non-fatal)" | tee -a /var/log/cyberpanel_upgrade_debug.log + fi + return 0 +} diff --git a/upgrade_modules/10_post_tweak.sh b/upgrade_modules/10_post_tweak.sh new file mode 100644 index 000000000..3b9a38c94 --- /dev/null +++ b/upgrade_modules/10_post_tweak.sh @@ -0,0 +1,374 @@ +#!/usr/bin/env bash +# CyberPanel upgrade – post-upgrade system tweaks (PHP, LSWS, SnappyMail, etc.). Sourced by cyberpanel_upgrade.sh. + +Post_Upgrade_System_Tweak() { + if [[ "$Server_OS" = "CentOS" ]] ; then + + #for cenots 7/8 + if [[ "$Server_OS_Version" = "7" ]] ; then + sed -i 's|error_reporting = E_ALL \& ~E_DEPRECATED \& ~E_STRICT|error_reporting = E_ALL \& ~E_DEPRECATED \& ~E_STRICT|g' /usr/local/lsws/{lsphp72,lsphp73}/etc/php.ini + #fix php.ini & issue + if ! yum list installed lsphp74-devel ; then + yum install -y lsphp74-devel + fi + if [[ ! -f /usr/local/lsws/lsphp74/lib64/php/modules/zip.so ]] ; then + if yum list installed libzip-devel >/dev/null 2>&1 ; then + yum remove -y libzip-devel + fi + yum install -y https://cyberpanel.sh/misc/libzip-0.11.2-6.el7.psychotic.x86_64.rpm + yum install -y https://cyberpanel.sh/misc/libzip-devel-0.11.2-6.el7.psychotic.x86_64.rpm + yum install lsphp74-devel + if [[ ! -d /usr/local/lsws/lsphp74/tmp ]]; then + mkdir /usr/local/lsws/lsphp74/tmp + fi + /usr/local/lsws/lsphp74/bin/pecl channel-update pecl.php.net + /usr/local/lsws/lsphp74/bin/pear config-set temp_dir /usr/local/lsws/lsphp74/tmp + if /usr/local/lsws/lsphp74/bin/pecl install zip ; then + echo "extension=zip.so" >/usr/local/lsws/lsphp74/etc/php.d/20-zip.ini + chmod 755 /usr/local/lsws/lsphp74/lib64/php/modules/zip.so + else + echo -e "\nlsphp74-zip compilation failed..." + fi + #fix old legacy lsphp74-zip issue on centos 7 + fi + + + #for centos 7 + elif [[ "$Server_OS_Version" = "8" ]] ; then + : + #for centos 8 + fi + fi + + if [[ "$Server_OS" = "Ubuntu" ]] ; then + + if ! dpkg -l lsphp74-dev >/dev/null 2>&1 ; then + apt install -y lsphp74-dev + fi + + if [[ ! -f /usr/sbin/ipset ]] ; then + ln -s /sbin/ipset /usr/sbin/ipset + fi + + #for ubuntu 18/20 + if [[ "$Server_OS_Version" = "18" ]] ; then + : + #for ubuntu 18 + elif [[ "$Server_OS_Version" = "20" ]] ; then + : + #for ubuntu 20 + fi + fi + +sed -i "s|lsws-5.3.8|lsws-$LSWS_Stable_Version|g" /usr/local/CyberCP/serverStatus/serverStatusUtil.py +sed -i "s|lsws-5.4.2|lsws-$LSWS_Stable_Version|g" /usr/local/CyberCP/serverStatus/serverStatusUtil.py +sed -i "s|lsws-5.3.5|lsws-$LSWS_Stable_Version|g" /usr/local/CyberCP/serverStatus/serverStatusUtil.py +sed -i "s|lsws-6.0|lsws-$LSWS_Stable_Version|g" /usr/local/CyberCP/serverStatus/serverStatusUtil.py +sed -i "s|lsws-6.3.4|lsws-$LSWS_Stable_Version|g" /usr/local/CyberCP/serverStatus/serverStatusUtil.py + +if [[ "$Server_Country" = "CN" ]] ; then + sed -i 's|https://www.litespeedtech.com/|https://cyberpanel.sh/www.litespeedtech.com/|g' /usr/local/CyberCP/serverStatus/serverStatusUtil.py + sed -i 's|http://license.litespeedtech.com/|https://cyberpanel.sh/license.litespeedtech.com/|g' /usr/local/CyberCP/serverStatus/serverStatusUtil.py +fi + +sed -i 's|python2|python|g' /usr/bin/adminPass +chmod 700 /usr/bin/adminPass + +rm -f /usr/bin/php +ln -s /usr/local/lsws/lsphp74/bin/php /usr/bin/php + +if [[ -f /etc/cyberpanel/webadmin_passwd ]]; then + chmod 600 /etc/cyberpanel/webadmin_passwd +fi + +chown lsadm:lsadm /usr/local/lsws/admin/conf/htpasswd +chmod 600 /usr/local/lsws/admin/conf/htpasswd + +if [[ -f /etc/pure-ftpd/pure-ftpd.conf ]]; then + sed -i 's|NoAnonymous no|NoAnonymous yes|g' /etc/pure-ftpd/pure-ftpd.conf +fi + +Tmp_Output=$(timeout 3 openssl s_client -connect 127.0.0.1:8090 2>/dev/null) +if echo "$Tmp_Output" | grep -q "mail@example.com" ; then + # it is using default installer generated cert + Regenerate_Cert 8090 +fi + + +Tmp_Output=$(timeout 3 openssl s_client -connect 127.0.0.1:7080 2>/dev/null) +if echo "$Tmp_Output" | grep -q "mail@example.com" ; then + Regenerate_Cert 7080 +fi + +if [[ ! -f /usr/bin/cyberpanel_utility ]]; then + wget -q -O /usr/bin/cyberpanel_utility https://cyberpanel.sh/misc/cyberpanel_utility.sh + chmod 700 /usr/bin/cyberpanel_utility +fi + +if [[ -f /etc/cyberpanel/watchdog.sh ]] ; then + watchdog kill + rm -f /etc/cyberpanel/watchdog.sh + rm -f /usr/local/bin/watchdog + wget -O /etc/cyberpanel/watchdog.sh "${Git_Content_URL}/${Branch_Name}/CPScripts/watchdog.sh" + chmod 700 /etc/cyberpanel/watchdog.sh + ln -s /etc/cyberpanel/watchdog.sh /usr/local/bin/watchdog + watchdog status +fi + + +rm -f /usr/local/composer.sh +rm -f /usr/local/requirments.txt + +chown -R cyberpanel:cyberpanel /usr/local/CyberCP/lib +chown -R cyberpanel:cyberpanel /usr/local/CyberCP/lib64 + +# Fix missing lsphp binary in /usr/local/lscp/fcgi-bin/ after upgrade +echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Checking and restoring lsphp binary if missing..." | tee -a /var/log/cyberpanel_upgrade_debug.log +if [[ ! -f /usr/local/lscp/fcgi-bin/lsphp ]] || [[ ! -s /usr/local/lscp/fcgi-bin/lsphp ]]; then + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] lsphp binary missing or empty, attempting to restore..." | tee -a /var/log/cyberpanel_upgrade_debug.log + + # Ensure fcgi-bin directory exists + mkdir -p /usr/local/lscp/fcgi-bin + + # Find the latest available PHP version and use it + PHP_RESTORED=0 + + # Try to find the latest lsphp version (check from newest to oldest) + # Priority: 85 (beta), 84, 83, 82, 81, 80, 74 + for PHP_VER in 85 84 83 82 81 80 74; do + if [[ -f /usr/local/lsws/lsphp${PHP_VER}/bin/lsphp ]]; then + # Try to create symlink first (preferred) + if ln -sf /usr/local/lsws/lsphp${PHP_VER}/bin/lsphp /usr/local/lscp/fcgi-bin/lsphp 2>/dev/null; then + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] lsphp symlink created from lsphp${PHP_VER}" | tee -a /var/log/cyberpanel_upgrade_debug.log + else + # If symlink fails, copy the file + cp -f /usr/local/lsws/lsphp${PHP_VER}/bin/lsphp /usr/local/lscp/fcgi-bin/lsphp + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] lsphp binary copied from lsphp${PHP_VER}" | tee -a /var/log/cyberpanel_upgrade_debug.log + fi + chown root:root /usr/local/lscp/fcgi-bin/lsphp + chmod 755 /usr/local/lscp/fcgi-bin/lsphp + PHP_RESTORED=1 + break + fi + done + + # If no lsphp version found, try php binary as fallback + if [[ $PHP_RESTORED -eq 0 ]]; then + for PHP_VER in 83 82 81 80 74 73 72; do + if [[ -f /usr/local/lsws/lsphp${PHP_VER}/bin/php ]]; then + # Try to create symlink first (preferred) + if ln -sf /usr/local/lsws/lsphp${PHP_VER}/bin/php /usr/local/lscp/fcgi-bin/lsphp 2>/dev/null; then + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] lsphp symlink created from php${PHP_VER} (lsphp fallback)" | tee -a /var/log/cyberpanel_upgrade_debug.log + else + # If symlink fails, copy the file + cp -f /usr/local/lsws/lsphp${PHP_VER}/bin/php /usr/local/lscp/fcgi-bin/lsphp + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] lsphp binary copied from php${PHP_VER} (lsphp fallback)" | tee -a /var/log/cyberpanel_upgrade_debug.log + fi + chown root:root /usr/local/lscp/fcgi-bin/lsphp + chmod 755 /usr/local/lscp/fcgi-bin/lsphp + PHP_RESTORED=1 + break + fi + done + fi + + # If no lsphp version found, try admin_php5 as fallback + if [[ $PHP_RESTORED -eq 0 ]]; then + if [[ -f /usr/local/lscp/admin/fcgi-bin/admin_php5 ]]; then + cp -f /usr/local/lscp/admin/fcgi-bin/admin_php5 /usr/local/lscp/fcgi-bin/lsphp + chown root:root /usr/local/lscp/fcgi-bin/lsphp + chmod 755 /usr/local/lscp/fcgi-bin/lsphp + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] lsphp binary restored from admin_php5 (fallback)" | tee -a /var/log/cyberpanel_upgrade_debug.log + elif [[ -f /usr/local/lscp/admin/fcgi-bin/admin_php ]]; then + cp -f /usr/local/lscp/admin/fcgi-bin/admin_php /usr/local/lscp/fcgi-bin/lsphp + chown root:root /usr/local/lscp/fcgi-bin/lsphp + chmod 755 /usr/local/lscp/fcgi-bin/lsphp + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] lsphp binary restored from admin_php (fallback)" | tee -a /var/log/cyberpanel_upgrade_debug.log + elif [[ -f /usr/local/lsws/admin/fcgi-bin/admin_php5 ]]; then + cp -f /usr/local/lsws/admin/fcgi-bin/admin_php5 /usr/local/lscp/fcgi-bin/lsphp + chown root:root /usr/local/lscp/fcgi-bin/lsphp + chmod 755 /usr/local/lscp/fcgi-bin/lsphp + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] lsphp binary restored from lsws admin_php5 (fallback)" | tee -a /var/log/cyberpanel_upgrade_debug.log + else + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] ERROR: Could not find any PHP binary to restore lsphp" | tee -a /var/log/cyberpanel_upgrade_debug.log + fi + fi + + # Create symlinks if they don't exist + if [[ -f /usr/local/lscp/fcgi-bin/lsphp ]]; then + if [[ ! -f /usr/local/lscp/fcgi-bin/lsphp4 ]]; then + ln -sf ./lsphp /usr/local/lscp/fcgi-bin/lsphp4 + fi + if [[ ! -f /usr/local/lscp/fcgi-bin/lsphp5 ]]; then + ln -sf ./lsphp /usr/local/lscp/fcgi-bin/lsphp5 + fi + fi +fi + +# Fix missing lscpd binary in /usr/local/lscp/bin/ after upgrade +echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Checking and restoring lscpd binary if missing..." | tee -a /var/log/cyberpanel_upgrade_debug.log +if [[ ! -f /usr/local/lscp/bin/lscpd ]] || [[ ! -s /usr/local/lscp/bin/lscpd ]]; then + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] lscpd binary missing or empty, attempting to restore..." | tee -a /var/log/cyberpanel_upgrade_debug.log + + # Ensure lscp bin directory exists + mkdir -p /usr/local/lscp/bin + + # Select the correct lscpd binary based on OS and version + lscpd_selection='lscpd-0.3.1' + + # Check if this is an ARM system + if uname -a | grep -q 'aarch64'; then + lscpd_selection='lscpd.aarch64' + else + # For x86_64 systems, check Ubuntu version + if [[ "$Server_OS" = "Ubuntu" ]] && [[ -f /etc/lsb-release ]]; then + ubuntu_version=$(grep 'DISTRIB_RELEASE' /etc/lsb-release | cut -d'=' -f2 | cut -d'.' -f1) + if [[ "$ubuntu_version" = "22" ]] || [[ "$ubuntu_version" = "24" ]]; then + lscpd_selection='lscpd.0.4.0' + fi + fi + fi + + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Selected lscpd binary: $lscpd_selection" | tee -a /var/log/cyberpanel_upgrade_debug.log + + # Copy the selected binary from CyberCP to lscp bin + if [[ -f /usr/local/CyberCP/${lscpd_selection} ]]; then + cp -f /usr/local/CyberCP/${lscpd_selection} /usr/local/lscp/bin/${lscpd_selection} + rm -f /usr/local/lscp/bin/lscpd + mv /usr/local/lscp/bin/${lscpd_selection} /usr/local/lscp/bin/lscpd + chmod 755 /usr/local/lscp/bin/lscpd + chown root:root /usr/local/lscp/bin/lscpd + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] lscpd binary restored successfully from ${lscpd_selection}" | tee -a /var/log/cyberpanel_upgrade_debug.log + else + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] ERROR: Could not find lscpd source binary ${lscpd_selection} in /usr/local/CyberCP/" | tee -a /var/log/cyberpanel_upgrade_debug.log + fi +else + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] lscpd binary exists and is valid" | tee -a /var/log/cyberpanel_upgrade_debug.log +fi + +if [[ "$Server_OS_Version" = "9" ]] || [[ "$Server_OS_Version" = "10" ]] || [[ "$Server_OS_Version" = "18" ]] || [[ "$Server_OS_Version" = "8" ]] || [[ "$Server_OS_Version" = "20" ]] || [[ "$Server_OS_Version" = "24" ]]; then + echo "PYTHONHOME=/usr" > /usr/local/lscp/conf/pythonenv.conf + else + # Uncomment and use the following lines if necessary for other OS versions + # rsync -av --ignore-existing /usr/lib64/python3.9/ /usr/local/CyberCP/lib64/python3.9/ + # Check_Return + : +fi + +# Fix SnappyMail directory permissions for Ubuntu 24.04 and other systems +echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Checking SnappyMail directories..." | tee -a /var/log/cyberpanel_upgrade_debug.log + +# If public web app is still named rainloop, rename to snappymail so /snappymail/ URL works +if [ -d "/usr/local/CyberCP/public/rainloop" ] && [ ! -d "/usr/local/CyberCP/public/snappymail" ]; then + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Renaming public/rainloop to public/snappymail..." | tee -a /var/log/cyberpanel_upgrade_debug.log + mv /usr/local/CyberCP/public/rainloop /usr/local/CyberCP/public/snappymail + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Renamed public/rainloop -> public/snappymail" | tee -a /var/log/cyberpanel_upgrade_debug.log + # Update data path in app config so it uses snappymail data dir + if [ -f "/usr/local/CyberCP/public/snappymail/include.php" ]; then + sed -i 's|/usr/local/lscp/cyberpanel/rainloop/data|/usr/local/lscp/cyberpanel/snappymail/data|g' /usr/local/CyberCP/public/snappymail/include.php + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Updated include.php to use snappymail data path" | tee -a /var/log/cyberpanel_upgrade_debug.log + fi + # Update version-specific include.php (may be under snappymail/v/ or rainloop/v/ after rename) + for inc in /usr/local/CyberCP/public/snappymail/snappymail/v/*/include.php /usr/local/CyberCP/public/snappymail/rainloop/v/*/include.php; do + [ -f "$inc" ] && sed -i 's|/usr/local/lscp/cyberpanel/rainloop/data|/usr/local/lscp/cyberpanel/snappymail/data|g' "$inc" && echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Updated $inc" | tee -a /var/log/cyberpanel_upgrade_debug.log && break + done 2>/dev/null +fi + +# Migrate data from old rainloop folder to new snappymail folder (2.4.4 -> 2.5.5 upgrade) +if [ -d "/usr/local/lscp/cyberpanel/rainloop/data" ] && [ "$(ls -A /usr/local/lscp/cyberpanel/rainloop/data 2>/dev/null)" ]; then + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Migrating rainloop data to snappymail..." | tee -a /var/log/cyberpanel_upgrade_debug.log + + # Check if snappymail data already exists with content + if [ -d "/usr/local/lscp/cyberpanel/snappymail/data" ] && [ -d "/usr/local/lscp/cyberpanel/snappymail/data/_data_/_default_/configs" ]; then + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] SnappyMail data already exists, skipping migration" | tee -a /var/log/cyberpanel_upgrade_debug.log + else + # Create SnappyMail data directories if they don't exist + mkdir -p /usr/local/lscp/cyberpanel/snappymail/data/_data_/_default_/configs/ + mkdir -p /usr/local/lscp/cyberpanel/snappymail/data/_data_/_default_/domains/ + mkdir -p /usr/local/lscp/cyberpanel/snappymail/data/_data_/_default_/storage/ + mkdir -p /usr/local/lscp/cyberpanel/snappymail/data/_data_/_default_/temp/ + mkdir -p /usr/local/lscp/cyberpanel/snappymail/data/_data_/_default_/cache/ + + # Migrate data using rsync (preserves permissions and ownership) + rsync -av --ignore-existing /usr/local/lscp/cyberpanel/rainloop/data/ /usr/local/lscp/cyberpanel/snappymail/data/ 2>&1 | tee -a /var/log/cyberpanel_upgrade_debug.log + + if [ $? -eq 0 ]; then + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Successfully migrated rainloop data to snappymail" | tee -a /var/log/cyberpanel_upgrade_debug.log + + # Update include.php to use snappymail path + if [ -f "/usr/local/CyberCP/public/snappymail/include.php" ]; then + sed -i 's|/usr/local/lscp/cyberpanel/rainloop/data|/usr/local/lscp/cyberpanel/snappymail/data|g' /usr/local/CyberCP/public/snappymail/include.php + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Updated include.php to use snappymail data path" | tee -a /var/log/cyberpanel_upgrade_debug.log + fi + + # Replace ALL rainloop path/URL references in migrated SnappyMail data (configs, domains, plugins) + if [ -d "/usr/local/lscp/cyberpanel/snappymail/data" ]; then + find /usr/local/lscp/cyberpanel/snappymail/data -type f \( -name "*.ini" -o -name "*.json" -o -name "*.php" -o -name "*.cfg" \) -exec grep -l "rainloop" {} \; 2>/dev/null | while read -r f; do + sed -i 's|/usr/local/lscp/cyberpanel/rainloop/data|/usr/local/lscp/cyberpanel/snappymail/data|g; s|/rainloop/|/snappymail/|g; s|rainloop/data|snappymail/data|g' "$f" + done + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Replaced rainloop→snappymail links in SnappyMail data files" | tee -a /var/log/cyberpanel_upgrade_debug.log + fi + else + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] WARNING: Data migration completed with errors" | tee -a /var/log/cyberpanel_upgrade_debug.log + fi + fi +else + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] No old rainloop data found, creating new SnappyMail directories..." | tee -a /var/log/cyberpanel_upgrade_debug.log + + # Create SnappyMail data directories if they don't exist + mkdir -p /usr/local/lscp/cyberpanel/snappymail/data/_data_/_default_/configs/ + mkdir -p /usr/local/lscp/cyberpanel/snappymail/data/_data_/_default_/domains/ + mkdir -p /usr/local/lscp/cyberpanel/snappymail/data/_data_/_default_/storage/ + mkdir -p /usr/local/lscp/cyberpanel/snappymail/data/_data_/_default_/temp/ + mkdir -p /usr/local/lscp/cyberpanel/snappymail/data/_data_/_default_/cache/ +fi + +# Ensure proper ownership for SnappyMail data directories +if id -u lscpd >/dev/null 2>&1; then + chown -R lscpd:lscpd /usr/local/lscp/cyberpanel/snappymail/ + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Set SnappyMail ownership to lscpd:lscpd" | tee -a /var/log/cyberpanel_upgrade_debug.log +else + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] WARNING: lscpd user not found, skipping ownership change" | tee -a /var/log/cyberpanel_upgrade_debug.log +fi + +# Ensure /rainloop→/snappymail redirect exists (even when no migration ran) +HTACCESS="/usr/local/CyberCP/public/.htaccess" +if [ -d "/usr/local/CyberCP/public" ] && { [ ! -f "$HTACCESS" ] || ! grep -q "Redirect old RainLoop URL to SnappyMail" "$HTACCESS" 2>/dev/null; }; then + { + echo "" + echo "# Redirect old RainLoop URL to SnappyMail (2.5.5 upgrade)" + echo "" + echo "RewriteEngine On" + echo "RewriteRule ^rainloop/?(.*)\$ /snappymail/\$1 [R=301,L]" + echo "" + } >> "$HTACCESS" + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Added /rainloop→/snappymail redirect to .htaccess" | tee -a /var/log/cyberpanel_upgrade_debug.log +fi + +# Set proper permissions for SnappyMail data directories (group writable) +chmod -R 775 /usr/local/lscp/cyberpanel/snappymail/data/ +echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Set SnappyMail data directory permissions to 775 (group writable)" | tee -a /var/log/cyberpanel_upgrade_debug.log + +# Ensure web server users are in the lscpd group for access +usermod -a -G lscpd nobody 2>/dev/null || true + +# Fix SnappyMail public directory ownership (critical fix) +chown -R lscpd:lscpd /usr/local/CyberCP/public/snappymail/data 2>/dev/null || true +echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Added web server users to lscpd group and fixed SnappyMail ownership" | tee -a /var/log/cyberpanel_upgrade_debug.log + +# Force phpMyAdmin to use 127.0.0.1 (TCP) so it shows the same MariaDB version as CLI (main instance on 3306) +if [ -f /usr/local/CyberCP/public/phpmyadmin/config.inc.php ]; then + if ! grep -q "\$cfg\['Servers'\]\[\$i\]\['host'\] = '127.0.0.1'" /usr/local/CyberCP/public/phpmyadmin/config.inc.php 2>/dev/null; then + sed -i "/SignonURL/a \$cfg['Servers'][\$i]['host'] = '127.0.0.1';\n\$cfg['Servers'][\$i]['port'] = '3306';" /usr/local/CyberCP/public/phpmyadmin/config.inc.php 2>/dev/null || true + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Set phpMyAdmin server host to 127.0.0.1" | tee -a /var/log/cyberpanel_upgrade_debug.log + fi +fi +if [ -f /usr/local/CyberCP/public/phpmyadmin/phpmyadminsignin.php ]; then + sed -i "/trim.*\$_POST.*host.*localhost/s/'localhost'/'127.0.0.1'/g" /usr/local/CyberCP/public/phpmyadmin/phpmyadminsignin.php 2>/dev/null || true + grep -q "127.0.0.1" /usr/local/CyberCP/public/phpmyadmin/phpmyadminsignin.php && echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] phpMyAdmin signon default host set to 127.0.0.1" | tee -a /var/log/cyberpanel_upgrade_debug.log +fi + +systemctl restart lscpd + +} + diff --git a/upgrade_modules/11_display_final.sh b/upgrade_modules/11_display_final.sh new file mode 100644 index 000000000..81b884c43 --- /dev/null +++ b/upgrade_modules/11_display_final.sh @@ -0,0 +1,122 @@ +#!/usr/bin/env bash +# CyberPanel upgrade – final display (banner, next steps). Sourced by cyberpanel_upgrade.sh. + +Post_Install_Display_Final_Info() { +echo -e "\n" +# Fixed box width (109 chars). ASCII-only borders (| - +) so right edge renders solid in all terminals. +BOX_W=109 +# 109 dashes for top/bottom border (no Unicode = so line is consistently filled) +_br() { echo "+-------------------------------------------------------------------------------------------------------------+"; } +_bl() { echo "+-------------------------------------------------------------------------------------------------------------+"; } +_b() { local s="$1"; [[ ${#s} -gt $BOX_W ]] && s="${s:0:BOX_W}"; printf '|%-*s|\n' "$BOX_W" "$s"; } +_br +_b "" +_b " █████████ █████ ███████████ ████" +_b " ███▒▒▒▒▒███ ▒▒███ ▒▒███▒▒▒▒▒███ ▒▒███" +_b " ███ ▒▒▒ █████ ████ ▒███████ ██████ ████████ ▒███ ▒███ ██████ ████████ ██████ ▒███" +_b " ▒███ ▒▒███ ▒███ ▒███▒▒███ ███▒▒███▒▒███▒▒███ ▒██████████ ▒▒▒▒▒███ ▒▒███▒▒███ ███▒▒███ ▒███" +_b " ▒███ ▒███ ▒███ ▒███ ▒███▒███████ ▒███ ▒▒▒ ▒███▒▒▒▒▒▒ ███████ ▒███ ▒███ ▒███████ ▒███" +_b " ▒▒███ ███ ▒███ ▒███ ▒███ ▒███▒███▒▒▒ ▒███ ▒███ ███▒▒███ ▒███ ▒███ ▒███▒▒▒ ▒███" +_b " ▒▒█████████ ▒▒███████ ████████ ▒▒██████ █████ █████ ▒▒████████ ████ █████▒▒██████ █████" +_b " ▒▒▒▒▒▒▒▒▒ ▒▒▒▒▒███ ▒▒▒▒▒▒▒▒ ▒▒▒▒▒▒ ▒▒▒▒▒ ▒▒▒▒▒ ▒▒▒▒▒▒▒▒ ▒▒▒▒ ▒▒▒▒▒ ▒▒▒▒▒▒ ▒▒▒▒▒" +_b " ███ ▒███" +_b " ▒▒██████" +_b " ▒▒▒▒▒▒" +_b " *** UPGRADE COMPLETED SUCCESSFULLY! ***" +_b "" +_bl + +Panel_Port=$(cat /usr/local/lscp/conf/bind.conf) +if [[ $Panel_Port = "" ]] ; then + Panel_Port="8090" +fi + +# Resolve server IP for remote access URL (avoid empty Remote: https://:2087) +if [[ -z "$SERVER_IP" ]]; then + SERVER_IP=$(hostname -I 2>/dev/null | awk '{print $1}') +fi +if [[ -z "$SERVER_IP" ]]; then + SERVER_IP=$(ip -4 route get 1 2>/dev/null | awk '/src/ {print $7; exit}') +fi +if [[ -z "$SERVER_IP" ]]; then + SERVER_IP=$(curl -s --max-time 3 ifconfig.me 2>/dev/null || curl -s --max-time 3 icanhazip.com 2>/dev/null) +fi +if [[ -z "$SERVER_IP" ]]; then + SERVER_IP="YOUR_SERVER_IP" +fi + +# Actual MariaDB server version (what phpMyAdmin will show) +# Actual server version (use --skip-ssl: 11.x client requires SSL by default, 10.x server may not offer it) +MARIADB_ACTUAL_VER="" +if command -v mariadb >/dev/null 2>&1; then + MARIADB_ACTUAL_VER=$(mariadb --skip-ssl -e "SELECT @@version;" -sN 2>/dev/null | head -1) +fi +[[ -z "$MARIADB_ACTUAL_VER" ]] && command -v mysql >/dev/null 2>&1 && MARIADB_ACTUAL_VER=$(mysql --skip-ssl -e "SELECT @@version;" -sN 2>/dev/null | head -1) +[[ -z "$MARIADB_ACTUAL_VER" ]] && MARIADB_ACTUAL_VER="${MARIADB_VER:-unknown}" + +# Test if CyberPanel is accessible +echo -e "\n🔍 Testing CyberPanel accessibility..." + +# Check if lscpd service is running +if systemctl is-active --quiet lscpd 2>/dev/null; then + _br + _b "" + _b " ACCESS YOUR CYBERPANEL:" + _b "" + _b " Local: https://127.0.0.1:${Panel_Port#*:}" + _b " Remote: https://${SERVER_IP}:${Panel_Port#*:}" + _b "" + _b " Default Login: admin / 1234567890" + _b " >> Please change the default password immediately!" + _b "" + _bl + + # Binary confirmation + versions (ASCII-only so box alignment is correct) + echo -e "\n" + _br + _b "" + _b " UPGRADE STATUS: [====================================================] 100%" + _b "" + _b " [OK] All components installed successfully" + _b " [OK] Python dependencies resolved" + _b " [OK] WSGI-LSAPI compiled with optimizations" + _b " [OK] CyberPanel service is running" + _b " [OK] Web interface is accessible" + _b "" + _b " CyberPanel: ${Branch_Name:-unknown}" + _b " Database (MariaDB): ${MARIADB_ACTUAL_VER}" + _b "" + _b " *** UPGRADE COMPLETED SUCCESSFULLY! ***" + _b "" + _bl + +else + echo -e "CyberPanel may not be running properly. Please check the logs." + echo -e "\n" + _br + _b "" + _b " UPGRADE COMPLETED WITH WARNINGS" + _b "" + _b " - CyberPanel files have been updated" + _b " - Some services may need manual restart" + _b " - Please check logs at /var/log/cyberpanel_upgrade_debug.log" + _b "" + _b " Try running: systemctl restart lscpd" + _b "" + _bl +fi + +echo -e "\n📋 Next Steps:" +echo -e " 1. Access your CyberPanel at the URL above" +echo -e " 2. Change the default admin password" +echo -e " 3. Configure your domains and websites" +echo -e " 4. Check system status in the dashboard" +echo -e " 5. Check DB version with: mariadb -V (use mariadb, not mysql, to avoid deprecation warning)" +echo -e " 6. Pre-upgrade DB backup (if created): /root/cyberpanel_mariadb_backups/" +echo -e " 7. One-liner: --backup-db (always backup DB), --no-backup-db (skip); omit = prompt. --migrate-to-utf8 for UTF-8 (only if your apps support it)" +echo -e " 8. If you downgrade to MariaDB 10.11.16, server charset stays latin1 for backward compatibility." + +echo -e "\n🧹 Cleaning up temporary files..." +rm -rf /root/cyberpanel_upgrade_tmp +echo -e "✅ Cleanup completed\n" +} diff --git a/userManagment/homeDirectoryViews.py b/userManagment/homeDirectoryViews.py index af0f71070..96f1403d4 100644 --- a/userManagment/homeDirectoryViews.py +++ b/userManagment/homeDirectoryViews.py @@ -169,7 +169,7 @@ def getHomeDirectoryStats(request): return JsonResponse({'status': 0, 'error_message': str(e)}) def getUserHomeDirectories(request): - """Get available home directories for user creation""" + """Get available home directories for user creation. Returns empty list if tables do not exist.""" try: userID = request.session['userID'] currentACL = ACLManager.loadedACL(userID) @@ -177,7 +177,7 @@ def getUserHomeDirectories(request): if currentACL['admin'] != 1 and currentACL['createNewUser'] != 1: return JsonResponse({'status': 0, 'error_message': 'Unauthorized access'}) - # Get active home directories + # Get active home directories (tables home_directories / user_home_mappings may not exist yet) home_dirs = HomeDirectory.objects.filter(is_active=True).order_by('name') directories = [] @@ -196,7 +196,8 @@ def getUserHomeDirectories(request): except Exception as e: logging.CyberCPLogFileWriter.writeToFile(f"Error getting user home directories: {str(e)}") - return JsonResponse({'status': 0, 'error_message': str(e)}) + # If tables don't exist (e.g. user_home_mappings), return empty list so Modify Website still works + return JsonResponse({'status': 1, 'directories': []}) def migrateUser(request): """Migrate user to different home directory""" diff --git a/userManagment/static/userManagment/userManagment.js b/userManagment/static/userManagment/userManagment.js index dedfd2a14..071dcec34 100644 --- a/userManagment/static/userManagment/userManagment.js +++ b/userManagment/static/userManagment/userManagment.js @@ -447,9 +447,9 @@ app.controller('modifyUser', function ($scope, $http) { $scope.userModificationLoading = true; $scope.acctDetailsFetched = false; $scope.userModified = true; - $scope.canotModifyUser = true; + $scope.canotModifyUser = false; // hide modify error (we only fetched details) $scope.couldNotConnect = true; - $scope.canotFetchDetails = true; + $scope.canotFetchDetails = false; // hide fetch error on success $scope.detailsFetched = false; $scope.userAccountsLimit = true; $scope.websitesLimit = true; @@ -459,13 +459,13 @@ app.controller('modifyUser', function ($scope, $http) { $scope.acctDetailsFetched = true; $scope.userAccountsLimit = true; $scope.userModified = true; - $scope.canotModifyUser = true; + $scope.canotModifyUser = false; // hide modify error (only fetch failed) $scope.couldNotConnect = true; - $scope.canotFetchDetails = false; - $scope.detailsFetched = true; + $scope.canotFetchDetails = true; // show fetch error on failure + $scope.detailsFetched = false; - $scope.errorMessage = response.data.error_message; + $scope.errorMessage = (response.data && (response.data.error_message || response.data.message || response.data.errorMessage)) || 'Unknown error'; } @@ -479,7 +479,7 @@ app.controller('modifyUser', function ($scope, $http) { $scope.acctDetailsFetched = true; $scope.userAccountsLimit = true; $scope.userModified = true; - $scope.canotModifyUser = true; + $scope.canotModifyUser = false; // hide modify error (only connection/fetch failed) $scope.couldNotConnect = false; $scope.canotFetchDetails = true; $scope.detailsFetched = true; @@ -496,7 +496,7 @@ app.controller('modifyUser', function ($scope, $http) { $scope.userModificationLoading = false; $scope.acctDetailsFetched = false; $scope.userModified = true; - $scope.canotModifyUser = true; + $scope.canotModifyUser = false; // hide modify error until we know result $scope.couldNotConnect = true; $scope.canotFetchDetails = true; $scope.detailsFetched = true; @@ -552,7 +552,7 @@ app.controller('modifyUser', function ($scope, $http) { $scope.userModificationLoading = true; $scope.acctDetailsFetched = true; $scope.userModified = false; - $scope.canotModifyUser = true; + $scope.canotModifyUser = false; // hide modify error on success $scope.couldNotConnect = true; $scope.canotFetchDetails = true; $scope.detailsFetched = true; @@ -569,13 +569,13 @@ app.controller('modifyUser', function ($scope, $http) { $scope.userModificationLoading = true; $scope.acctDetailsFetched = false; $scope.userModified = true; - $scope.canotModifyUser = false; + $scope.canotModifyUser = true; // show modify error on failure $scope.couldNotConnect = true; $scope.canotFetchDetails = true; $scope.detailsFetched = true; - $scope.errorMessage = response.data.error_message; + $scope.errorMessage = (response.data && (response.data.error_message || response.data.message || response.data.errorMessage)) || 'Unknown error'; } @@ -1891,6 +1891,42 @@ app.controller('apiUsersCTRL', function ($scope, $http) { /* Java script code to list table users */ +/* Show modal by id - works with Bootstrap 3 (jQuery) or Bootstrap 5 (native) */ +function showModalById(modalId) { + var el = document.getElementById(modalId); + if (!el) return; + if (typeof jQuery !== 'undefined' && jQuery(el).modal) { + jQuery(el).modal('show'); + } else if (typeof bootstrap !== 'undefined' && bootstrap.Modal) { + var m = bootstrap.Modal.getOrCreateInstance(el); + if (m) m.show(); + } else { + el.style.display = 'block'; + el.classList.add('in'); + if (el.getAttribute('aria-hidden') !== null) el.setAttribute('aria-hidden', 'false'); + var backdrop = document.createElement('div'); + backdrop.className = 'modal-backdrop fade in'; + backdrop.setAttribute('data-modal-backdrop', modalId); + document.body.appendChild(backdrop); + } +} + +function hideModalById(modalId) { + var el = document.getElementById(modalId); + if (!el) return; + if (typeof jQuery !== 'undefined' && jQuery(el).modal) { + jQuery(el).modal('hide'); + } else if (typeof bootstrap !== 'undefined' && bootstrap.Modal) { + var m = bootstrap.Modal.getInstance(el); + if (m) m.hide(); + } else { + el.style.display = 'none'; + el.classList.remove('in'); + if (el.getAttribute('aria-hidden') !== null) el.setAttribute('aria-hidden', 'true'); + var backdrops = document.querySelectorAll('[data-modal-backdrop="' + modalId + '"]'); + backdrops.forEach(function (b) { if (b.parentNode) b.parentNode.removeChild(b); }); + } +} app.controller('listTableUsers', function ($scope, $http) { @@ -1954,7 +1990,7 @@ app.controller('listTableUsers', function ($scope, $http) { $scope.deleteUserInitial = function (name){ UserToDelete = name; $scope.UserToDelete = name; - $('#deleteModal').modal('show'); + showModalById('deleteModal'); }; $scope.deleteUserFinal = function () { @@ -1979,7 +2015,7 @@ app.controller('listTableUsers', function ($scope, $http) { $scope.cyberpanelLoading = true; if (response.data.deleteStatus === 1) { $scope.populateCurrentRecords(); - $('#deleteModal').modal('hide'); + hideModalById('deleteModal'); safePNotify({ title: 'Success!', text: 'Users successfully deleted!', @@ -2016,7 +2052,7 @@ app.controller('listTableUsers', function ($scope, $http) { $scope.editInitial = function (name) { $scope.name = name; - $('#editModal').modal('show'); + showModalById('editModal'); }; $scope.saveResellerChanges = function () { @@ -2043,7 +2079,7 @@ app.controller('listTableUsers', function ($scope, $http) { if (response.data.status === 1) { $scope.populateCurrentRecords(); - $('#editModal').modal('hide'); + hideModalById('editModal'); safePNotify({ title: 'Success!', text: 'Changes successfully applied!', @@ -2097,7 +2133,7 @@ app.controller('listTableUsers', function ($scope, $http) { if (response.data.status === 1) { $scope.populateCurrentRecords(); - $('#editModal').modal('hide'); + hideModalById('editModal'); safePNotify({ title: 'Success!', text: 'ACL Successfully changed.', diff --git a/userManagment/templates/userManagment/listUsers.html b/userManagment/templates/userManagment/listUsers.html index 8443ab182..5d6579264 100644 --- a/userManagment/templates/userManagment/listUsers.html +++ b/userManagment/templates/userManagment/listUsers.html @@ -330,28 +330,28 @@ - - {% trans "Suspend" %} - - - {% trans "Activate" %} - - - {% trans "Edit" %} - - - {% trans "Delete" %} - + + + + diff --git a/userManagment/templates/userManagment/userMigration.html b/userManagment/templates/userManagment/userMigration.html index a2ac444ca..2b924f2d0 100644 --- a/userManagment/templates/userManagment/userMigration.html +++ b/userManagment/templates/userManagment/userMigration.html @@ -95,7 +95,7 @@ function loadUsers() { function loadHomeDirectories() { $.ajax({ - url: '/userManagement/getUserHomeDirectories/', + url: '/users/getUserHomeDirectories', type: 'POST', data: JSON.stringify({}), contentType: 'application/json', diff --git a/websiteFunctions/static/js/websiteFunctions.js b/websiteFunctions/static/js/websiteFunctions.js index 17c1bbb4b..8d0fbd354 100644 --- a/websiteFunctions/static/js/websiteFunctions.js +++ b/websiteFunctions/static/js/websiteFunctions.js @@ -10176,7 +10176,20 @@ $("#websiteSuccessfullyModified").hide(); $("#modifyWebsiteLoading").hide(); $("#modifyWebsiteButton").hide(); -app.controller('modifyWebsitesController', function ($scope, $http) { +/** Angular filter: format bytes as human-readable size (used by modifyWebsite.html) */ +app.filter('filesize', [function () { + return function (bytes) { + if (bytes == null || isNaN(bytes)) return '-'; + var n = Number(bytes); + if (n === 0) return '0 B'; + var k = 1024; + var sizes = ['B', 'KB', 'MB', 'GB', 'TB']; + var i = Math.floor(Math.log(n) / Math.log(k)); + return (n / Math.pow(k, i)).toFixed(2) + ' ' + sizes[Math.min(i, sizes.length - 1)]; + }; +}]); + +app.controller('modifyWebsitesController', ['$scope', '$http', function ($scope, $http) { $scope.fetchWebsites = function () { @@ -10313,7 +10326,7 @@ app.controller('modifyWebsitesController', function ($scope, $http) { }; -}); +}]); /* Java script code to Modify Pacakge ends here */ diff --git a/websiteFunctions/static/websiteFunctions/websiteFunctions.js b/websiteFunctions/static/websiteFunctions/websiteFunctions.js index dd86b2455..d995fa322 100644 --- a/websiteFunctions/static/websiteFunctions/websiteFunctions.js +++ b/websiteFunctions/static/websiteFunctions/websiteFunctions.js @@ -10742,7 +10742,20 @@ $("#websiteSuccessfullyModified").hide(); $("#modifyWebsiteLoading").hide(); $("#modifyWebsiteButton").hide(); -app.controller('modifyWebsitesController', function ($scope, $http) { +/** Angular filter: format bytes as human-readable size (used by modifyWebsite.html) */ +app.filter('filesize', [function () { + return function (bytes) { + if (bytes == null || isNaN(bytes)) return '-'; + var n = Number(bytes); + if (n === 0) return '0 B'; + var k = 1024; + var sizes = ['B', 'KB', 'MB', 'GB', 'TB']; + var i = Math.floor(Math.log(n) / Math.log(k)); + return (n / Math.pow(k, i)).toFixed(2) + ' ' + sizes[Math.min(i, sizes.length - 1)]; + }; +}]); + +app.controller('modifyWebsitesController', ['$scope', '$http', function ($scope, $http) { $scope.fetchWebsites = function () { @@ -10879,7 +10892,7 @@ app.controller('modifyWebsitesController', function ($scope, $http) { }; -}); +}]); /* Java script code to Modify Pacakge ends here */ diff --git a/websiteFunctions/templates/websiteFunctions/ftpQuotaManagement.html b/websiteFunctions/templates/websiteFunctions/ftpQuotaManagement.html index 8f2545a5c..c29232e44 100644 --- a/websiteFunctions/templates/websiteFunctions/ftpQuotaManagement.html +++ b/websiteFunctions/templates/websiteFunctions/ftpQuotaManagement.html @@ -38,11 +38,19 @@ 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 +133,119 @@ FTP Quota Management - CyberPanel
          diff --git a/websiteFunctions/templates/websiteFunctions/launchChild.html b/websiteFunctions/templates/websiteFunctions/launchChild.html index 93ebb4e1f..c9a7b6457 100644 --- a/websiteFunctions/templates/websiteFunctions/launchChild.html +++ b/websiteFunctions/templates/websiteFunctions/launchChild.html @@ -653,7 +653,7 @@

          {{ authority }}

          -

          {% trans "Your SSL will expire in" %} {{ days }} {% trans "days" %}

          +

          {% trans "Your SSL will expire in" %} {{ days }} {% trans "days" %}{% if renewal_when %} • {% trans "Renews" %} {{ renewal_when }}{% endif %}

          {% endif %} diff --git a/websiteFunctions/templates/websiteFunctions/listCron.html b/websiteFunctions/templates/websiteFunctions/listCron.html index 67e44acfc..9da1a7bf9 100644 --- a/websiteFunctions/templates/websiteFunctions/listCron.html +++ b/websiteFunctions/templates/websiteFunctions/listCron.html @@ -8,6 +8,64 @@ -
          +

          diff --git a/websiteFunctions/templates/websiteFunctions/website.html b/websiteFunctions/templates/websiteFunctions/website.html index 252926266..2f3659b10 100644 --- a/websiteFunctions/templates/websiteFunctions/website.html +++ b/websiteFunctions/templates/websiteFunctions/website.html @@ -1569,17 +1569,17 @@ {% trans "Secure" %} - • {% trans "Valid for" %} {{ days }} {% trans "days" %} + • {% trans "Valid for" %} {{ days }} {% trans "days" %}{% if renewal_when %} • {% trans "Renews" %} {{ renewal_when }}{% endif %} {% elif days|add:0 >= 7 %} {% trans "Expiring Soon" %} - • {{ days }} {% trans "days left" %} + • {{ days }} {% trans "days left" %}{% if renewal_when %} • {% trans "Renews" %} {{ renewal_when }}{% endif %} {% else %} {% trans "Critical" %} - • {{ days }} {% trans "days left" %} + • {{ days }} {% trans "days left" %}{% if renewal_when %} • {% trans "Renews" %} {{ renewal_when }}{% endif %} {% endif %}

          {% endif %} diff --git a/websiteFunctions/urls.py b/websiteFunctions/urls.py index 74e2cb674..e353f5de7 100644 --- a/websiteFunctions/urls.py +++ b/websiteFunctions/urls.py @@ -203,11 +203,8 @@ urlpatterns = [ path('get_website_resources/', views.get_website_resources, name='get_website_resources'), - # Subdomain Log Fix - path('fixSubdomainLogs', views.fixSubdomainLogs, name='fixSubdomainLogs'), - path('fixSubdomainLogsAction', views.fixSubdomainLogsAction, name='fixSubdomainLogsAction'), - # FTP Quota Management (API endpoints only; page is at /ftp/quotaManagement) + path('getFTPQuotaStatus', views.getFTPQuotaStatus, name='getFTPQuotaStatus'), path('enableFTPQuota', views.enableFTPQuota, name='enableFTPQuota'), path('getFTPQuotas', views.getFTPQuotas, name='getFTPQuotas'), path('updateFTPQuota', views.updateFTPQuota, name='updateFTPQuota'), diff --git a/websiteFunctions/views.py b/websiteFunctions/views.py index 5c65f6d85..98b7a0b30 100644 --- a/websiteFunctions/views.py +++ b/websiteFunctions/views.py @@ -2209,29 +2209,17 @@ def fetchWPBackups(request): def fixSubdomainLogs(request): try: - userID = request.session['userID'] - - + request.session['userID'] wm = WebsiteManager() - coreResult = wm.fixSubdomainLogs(userID) - - - return coreResult - + return wm.fixSubdomainLogs(request) except KeyError: return redirect(loadLoginPage) def fixSubdomainLogsAction(request): try: - userID = request.session['userID'] - - + request.session['userID'] wm = WebsiteManager() - coreResult = wm.fixSubdomainLogsAction(userID) - - - return coreResult - + return wm.fixSubdomainLogsAction(request) except KeyError: return redirect(loadLoginPage) @@ -2263,6 +2251,14 @@ def securityManagementPage(request): except KeyError: return redirect(loadLoginPage) +def getFTPQuotaStatus(request): + try: + userID = request.session['userID'] + wm = WebsiteManager() + return wm.getFTPQuotaStatus(userID, request.POST) + except KeyError: + return redirect(loadLoginPage) + def enableFTPQuota(request): try: userID = request.session['userID'] diff --git a/websiteFunctions/website.py b/websiteFunctions/website.py index d2bcfee56..b2e0d2e0e 100644 --- a/websiteFunctions/website.py +++ b/websiteFunctions/website.py @@ -49,6 +49,65 @@ from django.http import JsonResponse import ipaddress +def _get_ssl_renewal_schedule(): + """Get formatted SSL renewal schedule (e.g. 'Thursday 12:00 AM'). + Reads from world-readable config file first (web server can't read root crontab). + Cron day_of_week: 0=Sun, 1=Mon, ..., 6=Sat. Python weekday: Mon=0, ..., Sun=6.""" + try: + from datetime import datetime, timedelta + # Config file is world-readable; web server (lscpd) cannot read /var/spool/cron/root + config_path = '/usr/local/CyberCP/ssl_renewal_schedule.conf' + if os.path.exists(config_path): + try: + with open(config_path, 'r') as f: + line = f.read().strip() + if line: + return line + except (IOError, OSError): + pass + cron_paths = ['/var/spool/cron/root', '/var/spool/cron/crontabs/root'] + cron_content = None + for path in cron_paths: + if os.path.exists(path): + try: + with open(path, 'r') as f: + cron_content = f.read() + except (IOError, OSError): + continue + break + if not cron_content: + return None + renew_hour, renew_minute, renew_weekday_cron = 0, 0, 4 # default Thursday + for line in cron_content.splitlines(): + line = line.strip() + if 'renew.py' in line and not line.startswith('#'): + parts = line.split() + if len(parts) >= 5: + try: + renew_minute = int(parts[0]) if parts[0].isdigit() else 0 + renew_hour = int(parts[1]) if parts[1].isdigit() else 0 + dow = parts[4] + renew_weekday_cron = int(dow) if dow.isdigit() and 0 <= int(dow) <= 7 else 4 + except (ValueError, IndexError): + pass + elif len(parts) >= 2: + renew_minute = int(parts[0]) if parts[0].isdigit() else 0 + renew_hour = int(parts[1]) if parts[1].isdigit() else 0 + break + now = datetime.now() + # Cron: 0/7=Sun, 1=Mon, ..., 6=Sat -> Python: Mon=0, Tue=1, ..., Sun=6 + target_weekday = (renew_weekday_cron - 1) % 7 if renew_weekday_cron else 6 + days_until = (target_weekday - now.weekday()) % 7 + if days_until == 0 and (now.hour > renew_hour or (now.hour == renew_hour and now.minute >= renew_minute)): + days_until = 7 + next_run = now.replace(hour=renew_hour, minute=renew_minute, second=0, microsecond=0) + next_run += timedelta(days=days_until) + return next_run.strftime('%B %d, %Y %I:%M %p') + except Exception as e: + logging.CyberCPLogFileWriter.writeToFile('_get_ssl_renewal_schedule: ' + str(e)) + return None + + class WebsiteManager: apache = 1 ols = 2 @@ -3533,10 +3592,14 @@ context /cyberpanel_suspension_page.html { currentPack = modifyWeb.package.packageName owner = modifyWeb.admin.userName - # Get current home directory information - from userManagment.homeDirectoryUtils import HomeDirectoryUtils - current_home = HomeDirectoryUtils.getUserHomeDirectoryObject(owner) - currentHomeDirectory = current_home.name if current_home else 'Default' + # Get current home directory information (optional: tables may not exist yet) + currentHomeDirectory = 'Default' + try: + from userManagment.homeDirectoryUtils import HomeDirectoryUtils + current_home = HomeDirectoryUtils.getUserHomeDirectoryObject(owner) + currentHomeDirectory = current_home.name if current_home else 'Default' + except Exception: + pass data_ret = {'status': 1, 'modifyStatus': 1, 'error_message': "None", "adminEmail": email, "packages": json_data, "current_pack": currentPack, "adminNames": admin_data, @@ -3782,6 +3845,8 @@ context /cyberpanel_suspension_page.html { Data['viewSSL'] = 1 Data['days'] = str(diff.days) Data['authority'] = x509.get_issuer().get_components()[1][1].decode('utf-8') + renewal_when = _get_ssl_renewal_schedule() + Data['renewal_when'] = renewal_when if Data['authority'] == 'Denial': Data['authority'] = '%s has SELF-SIGNED SSL.' % (self.domain) @@ -3790,6 +3855,7 @@ context /cyberpanel_suspension_page.html { except BaseException as msg: Data['viewSSL'] = 0 + Data['renewal_when'] = None logging.CyberCPLogFileWriter.writeToFile(str(msg)) servicePath = '/home/cyberpanel/pureftpd' @@ -4009,6 +4075,7 @@ context /cyberpanel_suspension_page.html { Data['viewSSL'] = 1 Data['days'] = str(diff.days) Data['authority'] = x509.get_issuer().get_components()[1][1].decode('utf-8') + Data['renewal_when'] = _get_ssl_renewal_schedule() if Data['authority'] == 'Denial': Data['authority'] = '%s has SELF-SIGNED SSL.' % (self.childDomain) @@ -4017,6 +4084,7 @@ context /cyberpanel_suspension_page.html { except BaseException as msg: Data['viewSSL'] = 0 + Data['renewal_when'] = None logging.CyberCPLogFileWriter.writeToFile(str(msg)) proc = httpProc(request, 'websiteFunctions/launchChild.html', Data) @@ -8735,9 +8803,53 @@ StrictHostKeyChecking no logging.CyberCPLogFileWriter.writeToFile(f'Error fixing subdomain logs for {domain_name}: {str(e)}') return False + def getFTPQuotaStatus(self, userID=None, data=None): + """ + Return FTP quota status for the UI: ftp_running, quota_configured. + Used on page load to show the right message and button state. + """ + try: + currentACL = ACLManager.loadedACL(userID) + admin = Administrator.objects.get(pk=userID) + if not (currentACL.get('admin', 0) == 1): + return ACLManager.loadErrorJson('status', 0) + if os.path.exists('/etc/lsb-release'): + ftp_service = 'pure-ftpd-mysql' + else: + ftp_service = 'pure-ftpd' + conf_path = '/etc/pure-ftpd/pure-ftpd.conf' + ftp_running = False + quota_configured = False + try: + out = ProcessUtilities.outputExecutioner( + "systemctl is-active %s 2>/dev/null || true" % ftp_service, 'root', True) + ftp_running = bool(out and out.strip() == 'active') + except Exception: + pass + if ftp_running and os.path.exists(conf_path): + try: + quota_line = ProcessUtilities.outputExecutioner( + "grep -E '^Quota[[:space:]]+[0-9]+:[0-9]+' %s 2>/dev/null || true" % conf_path, 'root', True) + quota_configured = bool(quota_line and quota_line.strip()) + except Exception: + pass + data_ret = { + 'status': 1, + 'ftp_running': ftp_running, + 'quota_configured': quota_configured, + 'ftp_service': ftp_service, + } + return HttpResponse(json.dumps(data_ret), content_type='application/json') + except Exception as e: + data_ret = {'status': 0, 'message': str(e)} + return HttpResponse(json.dumps(data_ret), content_type='application/json') + def enableFTPQuota(self, userID=None, data=None): """ - Enable FTP quota system + Enable FTP quota: ensure Quota maxfiles:maxsize in config, start/restart Pure-FTPd if needed. + If Pure-FTPd is already running and config already has a valid Quota line, return success + without touching config or restarting (avoids breaking a working setup). + Uses correct service name (pure-ftpd-mysql on Debian/Ubuntu, pure-ftpd on RHEL/Alma). """ try: currentACL = ACLManager.loadedACL(userID) @@ -8747,60 +8859,157 @@ StrictHostKeyChecking no if not (currentACL.get('admin', 0) == 1): return ACLManager.loadErrorJson('status', 0) - # Backup existing configurations - logging.CyberCPLogFileWriter.writeToFile("Backing up existing Pure-FTPd configurations...") - - import shutil - from datetime import datetime - - timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') - - # Backup pure-ftpd.conf - if os.path.exists('/etc/pure-ftpd/pure-ftpd.conf'): - shutil.copy('/etc/pure-ftpd/pure-ftpd.conf', f'/etc/pure-ftpd/pure-ftpd.conf.backup.{timestamp}') - - # Backup pureftpd-mysql.conf - if os.path.exists('/etc/pure-ftpd/pureftpd-mysql.conf'): - shutil.copy('/etc/pure-ftpd/pureftpd-mysql.conf', f'/etc/pure-ftpd/pureftpd-mysql.conf.backup.{timestamp}') - - # Apply new configurations - logging.CyberCPLogFileWriter.writeToFile("Applying FTP quota configurations...") - - # Copy updated configurations - if os.path.exists('/usr/local/CyberCP/install/pure-ftpd/pure-ftpd.conf'): - shutil.copy('/usr/local/CyberCP/install/pure-ftpd/pure-ftpd.conf', '/etc/pure-ftpd/pure-ftpd.conf') - - if os.path.exists('/usr/local/CyberCP/install/pure-ftpd/pureftpd-mysql.conf'): - shutil.copy('/usr/local/CyberCP/install/pure-ftpd/pureftpd-mysql.conf', '/etc/pure-ftpd/pureftpd-mysql.conf') - - # Restart Pure-FTPd - logging.CyberCPLogFileWriter.writeToFile("Restarting Pure-FTPd service...") - ProcessUtilities.executioner('systemctl restart pure-ftpd') - - # Verify configuration - if ProcessUtilities.executioner('systemctl is-active --quiet pure-ftpd'): - logging.CyberCPLogFileWriter.writeToFile("FTP quota system enabled successfully") - - data_ret = { - 'status': 1, - 'message': 'FTP quota system enabled successfully' - } + # Resolve Pure-FTPd service name (Debian/Ubuntu use pure-ftpd-mysql) + if os.path.exists('/etc/lsb-release'): + ftp_service = 'pure-ftpd-mysql' else: - data_ret = { - 'status': 0, - 'message': 'Failed to restart Pure-FTPd service' - } + ftp_service = 'pure-ftpd' - json_data = json.dumps(data_ret) - return HttpResponse(json_data) + conf_path = '/etc/pure-ftpd/pure-ftpd.conf' + + # Early success: if Pure-FTPd is already active and config has valid Quota line, do nothing + try: + out = ProcessUtilities.outputExecutioner( + "systemctl is-active %s 2>/dev/null || true" % ftp_service, 'root', True) + if out and out.strip() == 'active': + quota_line = ProcessUtilities.outputExecutioner( + "grep -E '^Quota[[:space:]]+[0-9]+:[0-9]+' %s 2>/dev/null || true" % conf_path, 'root', True) + if quota_line and quota_line.strip(): + logging.CyberCPLogFileWriter.writeToFile("FTP quota already enabled and Pure-FTPd running") + data_ret = {'status': 1, 'message': 'FTP quota system is already enabled and Pure-FTPd is running.'} + return HttpResponse(json.dumps(data_ret), content_type='application/json') + except Exception: + pass + + # Require Pure-FTPd to be running before enabling quota (avoid confusing failures) + try: + out = ProcessUtilities.outputExecutioner( + "systemctl is-active %s 2>/dev/null || true" % ftp_service, 'root', True) + if not (out and out.strip() == 'active'): + msg = ('Pure-FTPd is not running. Please enable Pure-FTPd first ' + '(e.g. from Server Status → Services) before enabling the FTP Quota system.') + data_ret = {'status': 0, 'message': msg} + return HttpResponse(json.dumps(data_ret), content_type='application/json') + except Exception: + pass + + # Only ensure Quota is enabled; do not overwrite existing config (preserves DB credentials, paths) + if os.path.exists(conf_path): + # Backup current config before we change anything (so we can restore if restart fails) + try: + from datetime import datetime + ts = datetime.now().strftime('%Y%m%d_%H%M%S') + ProcessUtilities.executioner( + 'cp %s /etc/pure-ftpd/pure-ftpd.conf.backup.%s' % (conf_path, ts), 'root', True) + ProcessUtilities.executioner( + 'cp /etc/pure-ftpd/pureftpd-mysql.conf /etc/pure-ftpd/pureftpd-mysql.conf.backup.%s 2>/dev/null || true' % ts, 'root', True) + except Exception: + pass + # If service is not running, try restoring latest backup (in case a previous run overwrote working config) + try: + out = ProcessUtilities.outputExecutioner( + "systemctl is-active %s 2>/dev/null || true" % ftp_service, 'root', True) + if not (out and out.strip() == 'active'): + # Restore latest backups if present + ProcessUtilities.executioner( + "ls -t /etc/pure-ftpd/pure-ftpd.conf.backup.* 2>/dev/null | head -1 | xargs -r -I {} cp {} /etc/pure-ftpd/pure-ftpd.conf", + 'root', True) + ProcessUtilities.executioner( + "ls -t /etc/pure-ftpd/pureftpd-mysql.conf.backup.* 2>/dev/null | head -1 | xargs -r -I {} cp {} /etc/pure-ftpd/pureftpd-mysql.conf", + 'root', True) + except Exception: + pass + # Add or replace Quota line via root (Pure-FTPd expects maxfiles:maxsizeMB, not "yes") + ProcessUtilities.executioner( + "grep -q '^Quota' %s && sed -i 's/^Quota.*/Quota 100000:100000/' %s || echo 'Quota 100000:100000' >> %s" % (conf_path, conf_path, conf_path), + 'root', True) + logging.CyberCPLogFileWriter.writeToFile("Set Quota 100000:100000 in existing pure-ftpd.conf") + else: + # First-time: copy from repo + from datetime import datetime + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + if os.path.exists('/usr/local/CyberCP/install/pure-ftpd/pure-ftpd.conf'): + ProcessUtilities.executioner( + 'cp /usr/local/CyberCP/install/pure-ftpd/pure-ftpd.conf /etc/pure-ftpd/pure-ftpd.conf', 'root', True) + if os.path.exists('/usr/local/CyberCP/install/pure-ftpd/pureftpd-mysql.conf'): + ProcessUtilities.executioner( + 'cp /usr/local/CyberCP/install/pure-ftpd/pureftpd-mysql.conf /etc/pure-ftpd/pureftpd-mysql.conf', 'root', True) + + # Safety net: ensure Quota line is valid before restart (Pure-FTPd rejects "Quota yes") + try: + quota_check = ProcessUtilities.outputExecutioner( + "grep -E '^Quota[[:space:]]+[0-9]+:[0-9]+' %s 2>/dev/null || true" % conf_path, 'root', True) + if not (quota_check and quota_check.strip()): + ProcessUtilities.executioner( + "grep -q '^Quota' %s && sed -i 's/^Quota.*/Quota 100000:100000/' %s || echo 'Quota 100000:100000' >> %s" % (conf_path, conf_path, conf_path), + 'root', True) + logging.CyberCPLogFileWriter.writeToFile("Corrected invalid Quota line in pure-ftpd.conf before restart") + except Exception: + pass + + # Final check: if Quota line still invalid (e.g. old panel code wrote "Quota yes"), restore backup and abort + try: + quota_final = ProcessUtilities.outputExecutioner( + "grep '^Quota' %s 2>/dev/null || true" % conf_path, 'root', True) + if quota_final and 'yes' in quota_final.lower(): + # Invalid line still present - restore backup and do not restart + ProcessUtilities.executioner( + "ls -t /etc/pure-ftpd/pure-ftpd.conf.backup.* 2>/dev/null | head -1 | xargs -r -I {} cp {} /etc/pure-ftpd/pure-ftpd.conf", + 'root', True) + logging.CyberCPLogFileWriter.writeToFile("Aborted: invalid Quota line (yes) still present; restored backup") + msg = ('Pure-FTPd config was invalid (Quota line). Restored previous config. ' + 'Please deploy the latest panel code from v2.5.5-dev and run the one-time fix on the server: ' + 'sudo sed -i "s/^Quota.*/Quota 100000:100000/" /etc/pure-ftpd/pure-ftpd.conf && sudo systemctl start pure-ftpd') + data_ret = {'status': 0, 'message': msg} + return HttpResponse(json.dumps(data_ret), content_type='application/json') + except Exception: + pass + + # Restart Pure-FTPd + logging.CyberCPLogFileWriter.writeToFile("Restarting Pure-FTPd service (%s)..." % ftp_service) + ProcessUtilities.executioner('systemctl restart %s' % ftp_service, 'root', True) + time.sleep(1) + + try: + output = ProcessUtilities.outputExecutioner('systemctl is-active %s' % ftp_service, 'root', True) + is_active = (output and output.strip() == 'active') + except Exception: + is_active = False + + if is_active: + logging.CyberCPLogFileWriter.writeToFile("FTP quota system enabled successfully") + data_ret = {'status': 1, 'message': 'FTP quota system enabled successfully'} + else: + # Restore backup so service can be started again from Services page + try: + ProcessUtilities.executioner( + "ls -t /etc/pure-ftpd/pure-ftpd.conf.backup.* 2>/dev/null | head -1 | xargs -r -I {} cp {} /etc/pure-ftpd/pure-ftpd.conf", + 'root', True) + ProcessUtilities.executioner( + "ls -t /etc/pure-ftpd/pureftpd-mysql.conf.backup.* 2>/dev/null | head -1 | xargs -r -I {} cp {} /etc/pure-ftpd/pureftpd-mysql.conf", + 'root', True) + logging.CyberCPLogFileWriter.writeToFile("Restored pure-ftpd config backup after failed start") + except Exception: + pass + # Capture failure reason for the user + try: + status_out = ProcessUtilities.outputExecutioner( + 'systemctl status %s --no-pager -l 2>&1 | head -20' % ftp_service, 'root', True) + status_preview = (status_out or '').strip().replace('\n', ' ')[:300] + except Exception: + status_preview = '' + logging.CyberCPLogFileWriter.writeToFile("Pure-FTPd service not active after restart") + msg = 'Pure-FTPd did not start. Config was restored. Run: systemctl status %s' % ftp_service + if status_preview: + msg += '. ' + status_preview + data_ret = {'status': 0, 'message': msg} + + return HttpResponse(json.dumps(data_ret), content_type='application/json') except Exception as e: - data_ret = { - 'status': 0, - 'message': f'Error enabling FTP quota: {str(e)}' - } - json_data = json.dumps(data_ret) - return HttpResponse(json_data) + logging.CyberCPLogFileWriter.writeToFile("Error enabling FTP quota: %s" % str(e)) + data_ret = {'status': 0, 'message': 'Error enabling FTP quota: %s' % str(e)} + return HttpResponse(json.dumps(data_ret), content_type='application/json') def getFTPQuotas(self, userID=None, data=None): """