From 86b5ed6e0eec85c26ca548ba8b3d6f60dbde158d Mon Sep 17 00:00:00 2001 From: master3395 Date: Sun, 1 Feb 2026 23:46:48 +0100 Subject: [PATCH 001/130] feat: no plugins required by default; Plugin Store category updates - Remove emailMarketing from default INSTALLED_APPS - Comment out emailMarketing URL (plugin installer adds when installed) - Bump emailMarketing, examplePlugin meta.xml to 1.0.1 - Plugin Holder: remove Plugin category, enforce Utility/Security/Backup/Performance - Add to-do/PLUGIN-DEFAULT-REMOVAL-2026-02-01.md --- CyberCP/settings.py | 3 +- CyberCP/urls.py | 3 +- emailMarketing/meta.xml | 4 +- examplePlugin/meta.xml | 4 +- pluginHolder/templates/pluginHolder/help.html | 4 + .../templates/pluginHolder/plugins.html | 454 +++++++++++++++--- pluginHolder/views.py | 88 ++-- to-do/PLUGIN-DEFAULT-REMOVAL-2026-02-01.md | 13 + 8 files changed, 473 insertions(+), 100 deletions(-) create mode 100644 to-do/PLUGIN-DEFAULT-REMOVAL-2026-02-01.md diff --git a/CyberCP/settings.py b/CyberCP/settings.py index 1edb42b25..2008956ad 100644 --- a/CyberCP/settings.py +++ b/CyberCP/settings.py @@ -65,7 +65,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 diff --git a/CyberCP/urls.py b/CyberCP/urls.py index ea5ccf382..db097f5a0 100644 --- a/CyberCP/urls.py +++ b/CyberCP/urls.py @@ -51,7 +51,8 @@ urlpatterns = [ path('CloudLinux/', include('CLManager.urls')), path('IncrementalBackups/', include('IncBackups.urls')), path('aiscanner/', include('aiScanner.urls')), - path('emailMarketing/', include('emailMarketing.urls')), + # Optional plugin routes - added by plugin installer when plugins are installed from Plugin Store + # path('emailMarketing/', include('emailMarketing.urls')), # path('Terminal/', include('WebTerminal.urls')), path('', include('loginSystem.urls')), ] diff --git a/emailMarketing/meta.xml b/emailMarketing/meta.xml index 2c5d8e719..d125a471f 100644 --- a/emailMarketing/meta.xml +++ b/emailMarketing/meta.xml @@ -1,7 +1,7 @@ Email Marketing - plugin + Utility Email Marketing plugin for CyberPanel. - 1.0.0 + 1.0.1 \ No newline at end of file diff --git a/examplePlugin/meta.xml b/examplePlugin/meta.xml index 55990a577..9e7e58b4d 100644 --- a/examplePlugin/meta.xml +++ b/examplePlugin/meta.xml @@ -1,8 +1,8 @@ examplePlugin - plugin + Utility This is an example plugin - 1.0.0 + 1.0.1 usmannasir \ No newline at end of file diff --git a/pluginHolder/templates/pluginHolder/help.html b/pluginHolder/templates/pluginHolder/help.html index 4624368ac..66bdcbdea 100644 --- a/pluginHolder/templates/pluginHolder/help.html +++ b/pluginHolder/templates/pluginHolder/help.html @@ -401,6 +401,10 @@ mkdir -p migrations <settings_url>/plugins/myFirstPlugin/settings/</settings_url> </cyberpanelPluginConfig> +
+ {% trans "Required: Category (type)" %}: {% trans "The <type> field is required. Valid categories: Utility, Security, Backup, Performance. 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
diff --git a/pluginHolder/templates/pluginHolder/plugins.html b/pluginHolder/templates/pluginHolder/plugins.html
index f53e351a0..49d9c7dc4 100644
--- a/pluginHolder/templates/pluginHolder/plugins.html
+++ b/pluginHolder/templates/pluginHolder/plugins.html
@@ -592,11 +592,150 @@
         100% { transform: rotate(360deg); }
     }
     
+    .category-filter {
+        display: flex;
+        flex-wrap: wrap;
+        gap: 10px;
+        margin-bottom: 15px;
+        padding: 15px;
+        background: var(--bg-secondary, #f8f9ff);
+        border-radius: 8px;
+    }
+    
+    .category-btn {
+        padding: 8px 16px;
+        border: 1px solid var(--border-primary, #e8e9ff);
+        background: var(--bg-primary, white);
+        border-radius: 8px;
+        font-size: 13px;
+        font-weight: 600;
+        cursor: pointer;
+        transition: all 0.2s;
+        color: var(--text-secondary, #64748b);
+        display: inline-flex;
+        align-items: center;
+        gap: 8px;
+    }
+    
+    .category-btn:hover {
+        background: var(--bg-hover, #f0f1ff);
+        border-color: #5856d6;
+        color: #5856d6;
+    }
+    
+    .category-btn.active {
+        background: #5856d6;
+        color: white;
+        border-color: #5856d6;
+    }
+    
+    .category-btn i {
+        font-size: 14px;
+    }
+    
+    .store-search-bar {
+        position: relative;
+        margin-bottom: 15px;
+        max-width: 480px;
+    }
+    
+    .store-search-icon {
+        position: absolute;
+        left: 16px;
+        top: 50%;
+        transform: translateY(-50%);
+        color: #64748b;
+        font-size: 16px;
+        pointer-events: none;
+        z-index: 1;
+        width: 20px;
+        text-align: center;
+    }
+    
+    .store-search-input {
+        width: 100%;
+        padding-left: 46px;
+        padding-right: 40px;
+        padding-top: 12px;
+        padding-bottom: 12px;
+        border: 1px solid #cbd5e1;
+        border-radius: 8px;
+        font-size: 14px;
+        background: #f8fafc;
+        color: #1e293b;
+        transition: border-color 0.2s, box-shadow 0.2s, background 0.2s;
+    }
+    
+    .store-search-input:focus {
+        outline: none;
+        border-color: #5856d6;
+        background: #ffffff;
+        box-shadow: 0 0 0 3px rgba(88, 86, 214, 0.15);
+    }
+    
+    .store-search-input::placeholder {
+        color: #64748b;
+    }
+    
+    .store-search-clear {
+        position: absolute;
+        right: 10px;
+        top: 50%;
+        transform: translateY(-50%);
+        background: none;
+        border: none;
+        color: var(--text-muted, #94a3b8);
+        cursor: pointer;
+        padding: 6px;
+        border-radius: 4px;
+        font-size: 14px;
+        transition: color 0.2s, background 0.2s;
+    }
+    
+    .store-search-clear:hover {
+        color: #5856d6;
+        background: var(--bg-secondary, #f8f9ff);
+    }
+    
+    .alphabet-filter-wrapper {
+        margin-bottom: 20px;
+    }
+    
+    .alphabet-toggle-btn {
+        display: inline-flex;
+        align-items: center;
+        gap: 8px;
+        padding: 8px 16px;
+        border: 1px solid var(--border-primary, #e8e9ff);
+        background: var(--bg-primary, white);
+        border-radius: 8px;
+        font-size: 13px;
+        font-weight: 600;
+        color: var(--text-secondary, #64748b);
+        cursor: pointer;
+        transition: all 0.2s;
+    }
+    
+    .alphabet-toggle-btn:hover {
+        background: var(--bg-hover, #f8f9ff);
+        border-color: #5856d6;
+        color: #5856d6;
+    }
+    
+    .alphabet-toggle-btn[aria-expanded="true"] .alphabet-chevron {
+        transform: rotate(180deg);
+    }
+    
+    .alphabet-chevron {
+        font-size: 10px;
+        transition: transform 0.2s;
+    }
+    
     .alphabet-filter {
         display: flex;
         flex-wrap: wrap;
         gap: 8px;
-        margin-bottom: 20px;
+        margin-top: 12px;
         padding: 15px;
         background: var(--bg-secondary, #f8f9ff);
         border-radius: 8px;
@@ -971,15 +1110,15 @@
             {% if plugins %}
             
             
- - - @@ -995,13 +1134,13 @@
- {% if plugin.type == "Security" %} + {% if plugin.type|lower == "security" %} - {% elif plugin.type == "Performance" %} + {% elif plugin.type|lower == "performance" %} - {% elif plugin.type == "Utility" %} + {% elif plugin.type|lower == "utility" %} - {% elif plugin.type == "Backup" %} + {% elif plugin.type|lower == "backup" %} {% else %} @@ -1216,15 +1355,15 @@
- - - @@ -1274,35 +1413,63 @@
- -
+ -

{% 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" %}
  • @@ -720,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" %} @@ -757,6 +913,7 @@ 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." %}
    +

@@ -769,16 +926,27 @@ zip -r myPlugin-v1.0.0.zip . \

+ {% endif %} + + +{% endblock %} diff --git a/paypalPremiumPlugin/templates/paypalPremiumPlugin/subscription_required.html b/paypalPremiumPlugin/templates/paypalPremiumPlugin/subscription_required.html new file mode 100644 index 000000000..cf67af087 --- /dev/null +++ b/paypalPremiumPlugin/templates/paypalPremiumPlugin/subscription_required.html @@ -0,0 +1,139 @@ +{% extends "baseTemplate/index.html" %} +{% load i18n %} + +{% block title %}{% trans "Payment Required - PayPal Premium Plugin" %}{% endblock %} + +{% block header_scripts %} + +{% endblock %} + +{% block content %} +
+
+
+

{% trans "Premium Plugin Access Required" %}

+

{% trans "This plugin requires payment or subscription to access premium features." %}

+ + +
+

{% trans "Activate Premium Access" %}

+

{% trans "If you received an activation key, enter it below." %}

+ +
+ {% csrf_token %} +
+ + +
+ +
+
+ + {% if payment_method == 'both' %} +
+ {% trans "Current Payment Method:" %} {% trans "Check Both (Patreon or PayPal)" %} +
+ {% elif payment_method == 'patreon' %} +
+ {% trans "Current Payment Method:" %} {% trans "Patreon Subscription Only" %} +
+ {% elif payment_method == 'paypal' %} +
+ {% trans "Current Payment Method:" %} {% trans "PayPal Payment Only" %} +
+ {% endif %} + +
+
+

{% trans "Patreon Subscription" %}

+

{% trans "Subscribe to" %} "{{ patreon_tier }}"

+ + {% trans "Subscribe on Patreon" %} + +
+
+

{% trans "PayPal Payment" %}

+

{% trans "Complete one-time payment via PayPal" %}

+ {% if paypal_me_url %} + + {% trans "Pay with PayPal.me" %} + + {% endif %} +
+
+ +
+

{% trans "How it works:" %}

+
    +
  • {% trans "Install the plugin (already done)" %}
  • +
  • {% trans "Enter activation key, subscribe on Patreon, or pay via PayPal" %}
  • +
  • {% trans "The plugin will automatically unlock" %}
  • +
+
+ + {% if error %} +
+ {% trans "Error:" %} {{ error }} +
+ {% endif %} +
+
+ + +{% endblock %} diff --git a/paypalPremiumPlugin/urls.py b/paypalPremiumPlugin/urls.py new file mode 100644 index 000000000..997fe20f5 --- /dev/null +++ b/paypalPremiumPlugin/urls.py @@ -0,0 +1,12 @@ +from django.urls import path, re_path +from . import views + +app_name = 'paypalPremiumPlugin' + +urlpatterns = [ + path('', views.main_view, name='main'), + path('settings/', views.settings_view, name='settings'), + re_path(r'^activate-key/$', views.activate_key, name='activate_key'), + path('save-payment-method/', views.save_payment_method, name='save_payment_method'), + path('api/status/', views.api_status_view, name='api_status'), +] diff --git a/paypalPremiumPlugin/views.py b/paypalPremiumPlugin/views.py index c16bc0337..c1f96f129 100644 --- a/paypalPremiumPlugin/views.py +++ b/paypalPremiumPlugin/views.py @@ -1,496 +1,387 @@ # -*- coding: utf-8 -*- """ -PayPal Premium Plugin Views - Enhanced Security Version -This version uses remote server verification with multiple security layers -SECURITY: All PayPal verification happens on YOUR server, not user's server +PayPal Premium Plugin Views - Unified Verification (same as contaboAutoSnapshot) +Supports: Plugin Grants, Activation Key, Patreon, PayPal, AES encryption """ from django.shortcuts import render, redirect -from django.http import JsonResponse +from django.http import JsonResponse, HttpResponse +from django.views.decorators.http import require_http_methods from plogical.mailUtilities import mailUtilities from plogical.httpProc import httpProc from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter as logging from functools import wraps -import sys -import os import urllib.request import urllib.error import json -import hashlib -import socket -import platform -import subprocess import time -import uuid -# Remote verification server (YOUR server, not user's server) -REMOTE_VERIFICATION_URL = 'https://api.newstargeted.com/api/verify-paypal-payment' -PLUGIN_NAME = 'paypalPremiumPlugin' # PayPal Premium Plugin Example -PLUGIN_VERSION = '1.0.0' +from .models import PaypalPremiumPluginConfig +from . import api_encryption -# PayPal configuration +PLUGIN_NAME = 'paypalPremiumPlugin' +PLUGIN_VERSION = '1.0.2' + +REMOTE_VERIFICATION_PATREON_URL = 'https://api.newstargeted.com/api/verify-patreon-membership.php' +REMOTE_VERIFICATION_PAYPAL_URL = 'https://api.newstargeted.com/api/verify-paypal-payment.php' +REMOTE_VERIFICATION_PLUGIN_GRANT_URL = 'https://api.newstargeted.com/api/verify-plugin-grant.php' +REMOTE_ACTIVATION_KEY_URL = 'https://api.newstargeted.com/api/activate-plugin-key.php' + +PATREON_TIER = 'CyberPanel Paid Plugin' +PATREON_URL = 'https://www.patreon.com/membership/27789984' PAYPAL_ME_URL = 'https://paypal.me/KimBS?locale.x=en_US&country.x=NO' -PAYPAL_PAYMENT_LINK = '' # Can be set to a PayPal Payment Link URL +PAYPAL_PAYMENT_LINK = '' -# Security configuration -CACHE_FILE = '/tmp/.paypalPremiumPlugin_license_cache' -CACHE_DURATION = 3600 # 1 hour - -# File integrity hashes (generated after plugin finalization) -# To regenerate: python3 -c "import hashlib; print(hashlib.sha256(open('views.py', 'rb').read()).hexdigest())" -PLUGIN_FILE_HASHES = { - 'views.py': '4899d70dde220b38d691a5cefdc4fd77b6d3e250ac1c7e12fa280d6f4ad31eb1', # Updated with security features - 'urls.py': '92433d401c358cd33ffd1926881920fd1867bb6d7dad1c3c2ed1e7d3b0abc2c6', -} - -def get_server_fingerprint(): - """ - Generate unique server fingerprint - Ties license to specific server hardware/configuration - """ - fingerprint_data = [] - - try: - # Server hostname - fingerprint_data.append(socket.gethostname()) - - # Primary IP - fingerprint_data.append(socket.gethostbyname(socket.gethostname())) - - # System information - fingerprint_data.append(platform.node()) - fingerprint_data.append(platform.machine()) - fingerprint_data.append(platform.processor()) - - # MAC address - fingerprint_data.append(str(uuid.getnode())) - - # Disk information (if available) - try: - result = subprocess.run(['df', '-h', '/'], capture_output=True, text=True, timeout=2) - fingerprint_data.append(result.stdout[:100]) - except: - pass - - # Create hash - fingerprint_string = '|'.join(str(x) for x in fingerprint_data) - return hashlib.sha256(fingerprint_string.encode()).hexdigest() - except Exception as e: - # Fallback fingerprint - return hashlib.sha256(f"{socket.gethostname()}|{platform.node()}".encode()).hexdigest() - -def verify_code_integrity(): - """ - Verify plugin files haven't been tampered with - Returns: (is_valid, error_message) - """ - plugin_dir = os.path.dirname(os.path.abspath(__file__)) - - for filename, expected_hash in PLUGIN_FILE_HASHES.items(): - if not expected_hash: - continue # Skip if hash not set - - filepath = os.path.join(plugin_dir, filename) - if os.path.exists(filepath): - try: - with open(filepath, 'rb') as f: - file_content = f.read() - file_hash = hashlib.sha256(file_content).hexdigest() - - if file_hash != expected_hash: - return False, f"File {filename} has been modified (integrity check failed)" - except Exception as e: - return False, f"Error checking {filename}: {str(e)}" - - return True, None - -def get_cached_verification(): - """Get cached verification result""" - if os.path.exists(CACHE_FILE): - try: - with open(CACHE_FILE, 'r') as f: - cache_data = json.load(f) - cache_time = cache_data.get('timestamp', 0) - server_fp = cache_data.get('server_fingerprint') - - # Verify server fingerprint matches - current_fp = get_server_fingerprint() - if server_fp != current_fp: - return None # Server changed, invalidate cache - - # Check if cache is still valid - if time.time() - cache_time < CACHE_DURATION: - return cache_data.get('has_access', False) - except: - pass - return None - -def cache_verification_result(has_access, server_fp): - """Cache verification result""" - try: - with open(CACHE_FILE, 'w') as f: - json.dump({ - 'has_access': has_access, - 'server_fingerprint': server_fp, - 'timestamp': time.time() - }, f) - os.chmod(CACHE_FILE, 0o600) # Secure permissions (owner read/write only) - except Exception as e: - pass # Silently fail caching 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 + if not request.session.get('userID'): + from loginSystem.views import loadLoginPage + return redirect(loadLoginPage) 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 secure_verification_required(view_func): - """ - Enhanced decorator with multiple security checks - """ - @wraps(view_func) - def _wrapped_view(request, *args, **kwargs): - # Check 1: Login required - try: - userID = request.session['userID'] - except KeyError: - from loginSystem.views import loadLoginPage - return redirect(loadLoginPage) - - # Check 2: Code integrity - is_valid, integrity_error = verify_code_integrity() - if not is_valid: - # Log security violation - logging.writeToFile(f"SECURITY VIOLATION: {integrity_error} - User: {request.session.get('userID')}") - - # Show error (don't reveal details) - context = { - 'error': 'Plugin integrity check failed. Please reinstall the plugin.', - 'security_violation': True - } - proc = httpProc(request, 'paypalPremiumPlugin/subscription_required.html', context, 'admin') - return proc.render() - - # Check 3: Remote 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', '') - - domain = request.get_host() - user_ip = request.META.get('REMOTE_ADDR', '') - - verification_result = check_remote_payment_secure( - user_email, - user_ip, - domain - ) - - if not verification_result.get('has_access', False): - # Show payment required page - context = { - 'plugin_name': 'PayPal Premium Plugin Example', - 'is_paid': True, - 'paypal_me_url': verification_result.get('paypal_me_url', PAYPAL_ME_URL), - 'paypal_payment_link': verification_result.get('paypal_payment_link', PAYPAL_PAYMENT_LINK), - 'message': verification_result.get('message', 'PayPal payment required'), - 'error': verification_result.get('error') - } - proc = httpProc(request, 'paypalPremiumPlugin/subscription_required.html', context, 'admin') - return proc.render() - - # All checks passed - proceed - return view_func(request, *args, **kwargs) - - return _wrapped_view -def remote_verification_required(view_func): - """ - Decorator that checks PayPal payment 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 payment via remote server - verification_result = check_remote_payment_secure( - user_email, - request.META.get('REMOTE_ADDR', ''), - request.get_host() - ) - - if not verification_result.get('has_access', False): - # User doesn't have payment - show payment required page - context = { - 'plugin_name': 'PayPal Premium Plugin Example', - 'is_paid': True, - 'paypal_me_url': verification_result.get('paypal_me_url', PAYPAL_ME_URL), - 'paypal_payment_link': verification_result.get('paypal_payment_link', PAYPAL_PAYMENT_LINK), - 'message': verification_result.get('message', 'PayPal payment required'), - 'error': verification_result.get('error') - } - proc = httpProc(request, 'paypalPremiumPlugin/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_payment_secure(user_email, user_ip='', domain=''): - """ - Enhanced remote payment verification with multiple security layers - - Args: - user_email: User's email address - user_ip: User's IP address (for logging/security) - domain: Current domain (for domain binding) - - Returns: - dict: { - 'has_access': bool, - 'paypal_me_url': str, - 'paypal_payment_link': str, - 'message': str, - 'error': str or None - } - """ - # Layer 1: Code integrity check - is_valid, integrity_error = verify_code_integrity() - if not is_valid: - return { - 'has_access': False, - 'paypal_me_url': PAYPAL_ME_URL, - 'paypal_payment_link': PAYPAL_PAYMENT_LINK, - 'message': 'Plugin integrity check failed', - 'error': integrity_error, - 'security_violation': True - } - - # Layer 2: Check cache - cached_result = get_cached_verification() - if cached_result is not None: - return { - 'has_access': cached_result, - 'paypal_me_url': PAYPAL_ME_URL, - 'paypal_payment_link': PAYPAL_PAYMENT_LINK, - 'message': 'Access granted' if cached_result else 'PayPal payment required' - } - - # Layer 3: Server fingerprinting - server_fp = get_server_fingerprint() - - # Layer 4: Prepare secure request - request_data = { - 'user_email': user_email, - 'plugin_name': PLUGIN_NAME, - 'plugin_version': PLUGIN_VERSION, - 'server_fingerprint': server_fp, - 'domain': domain, - 'user_ip': user_ip, - 'timestamp': int(time.time()) - } - +def _api_request(url, data, timeout=10): try: - # 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, - 'X-Timestamp': str(request_data['timestamp']) - } - ) - - # 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): - has_access = response_data.get('has_access', False) - - # Cache result - cache_verification_result(has_access, server_fp) - - return { - 'has_access': has_access, - 'paypal_me_url': response_data.get('paypal_me_url', PAYPAL_ME_URL), - 'paypal_payment_link': response_data.get('paypal_payment_link', PAYPAL_PAYMENT_LINK), - 'message': response_data.get('message', 'Access granted' if has_access else 'PayPal payment required'), - 'error': None - } - else: - return { - 'has_access': False, - 'paypal_me_url': response_data.get('paypal_me_url', PAYPAL_ME_URL), - 'paypal_payment_link': response_data.get('paypal_payment_link', PAYPAL_PAYMENT_LINK), - 'message': response_data.get('message', 'PayPal payment 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, - 'paypal_me_url': PAYPAL_ME_URL, - 'paypal_payment_link': PAYPAL_PAYMENT_LINK, - 'message': 'Unable to verify payment. Please try again later.', - 'error': f'HTTP {e.code}: {error_body}' - } - except urllib.error.URLError as e: - # Network error - return { - 'has_access': False, - 'paypal_me_url': PAYPAL_ME_URL, - 'paypal_payment_link': PAYPAL_PAYMENT_LINK, - '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, - 'paypal_me_url': PAYPAL_ME_URL, - 'paypal_payment_link': PAYPAL_PAYMENT_LINK, - 'message': 'Verification error occurred. Please try again later.', - 'error': str(e) - } - + body, extra_headers = api_encryption.encrypt_payload(data) + headers = { + 'User-Agent': f'CyberPanel-Plugin/{PLUGIN_VERSION}', + 'X-Plugin-Name': PLUGIN_NAME + } + headers.update(extra_headers) + req = urllib.request.Request(url, data=body, headers=headers) + with urllib.request.urlopen(req, timeout=timeout) as response: + raw = response.read() + ct = response.headers.get('Content-Type', '') + expect_enc = extra_headers.get('X-Encrypted') == '1' + return api_encryption.decrypt_response(raw, ct, expect_encrypted=expect_enc) except Exception as e: - logging.writeToFile(f"Error in remote payment check: {str(e)}") + logging.writeToFile(f"PayPal Premium Plugin: API request error to {url}: {str(e)}") + return {} + + +def check_plugin_grant(user_email, user_ip='', domain=''): + try: + request_data = { + 'user_email': user_email or '', + 'plugin_name': PLUGIN_NAME, + 'user_ip': user_ip, + 'domain': domain, + } + data = _api_request(REMOTE_VERIFICATION_PLUGIN_GRANT_URL, request_data) + if data.get('success') and data.get('has_access'): + return {'has_access': True, 'message': data.get('message', 'Access granted via Plugin Grants')} + return {'has_access': False, 'message': data.get('message', '')} + except Exception as e: + logging.writeToFile(f"PayPal Premium Plugin: Plugin grant check error: {str(e)}") + return {'has_access': False, 'message': ''} + + +def check_patreon_membership(user_email, user_ip='', domain=''): + try: + request_data = { + 'user_email': user_email, + 'plugin_name': PLUGIN_NAME, + 'plugin_version': PLUGIN_VERSION, + 'user_ip': user_ip, + 'domain': domain, + 'tier_id': '27789984' + } + response_data = _api_request(REMOTE_VERIFICATION_PATREON_URL, request_data) + if response_data.get('success', False): + return { + 'has_access': response_data.get('has_access', False), + 'patreon_tier': response_data.get('patreon_tier', PATREON_TIER), + 'patreon_url': response_data.get('patreon_url', PATREON_URL), + 'message': response_data.get('message', 'Access granted'), + 'error': None + } + return { + 'has_access': False, + 'patreon_tier': PATREON_TIER, + 'patreon_url': PATREON_URL, + 'message': response_data.get('message', 'Patreon subscription required'), + 'error': response_data.get('error') + } + except Exception as e: + logging.writeToFile(f"PayPal Premium Plugin: Patreon check error: {str(e)}") + return {'has_access': False, 'patreon_tier': PATREON_TIER, 'patreon_url': PATREON_URL, 'message': 'Unable to verify Patreon.', 'error': str(e)} + + +def check_paypal_payment(user_email, user_ip='', domain=''): + try: + request_data = { + 'user_email': user_email, + 'plugin_name': PLUGIN_NAME, + 'plugin_version': PLUGIN_VERSION, + 'user_ip': user_ip, + 'domain': domain, + 'timestamp': int(time.time()), + } + response_data = _api_request(REMOTE_VERIFICATION_PAYPAL_URL, request_data) + if response_data.get('success', False): + return { + 'has_access': response_data.get('has_access', False), + 'paypal_me_url': response_data.get('paypal_me_url', PAYPAL_ME_URL), + 'paypal_payment_link': response_data.get('paypal_payment_link', PAYPAL_PAYMENT_LINK), + 'message': response_data.get('message', 'Access granted'), + 'error': None + } return { 'has_access': False, 'paypal_me_url': PAYPAL_ME_URL, 'paypal_payment_link': PAYPAL_PAYMENT_LINK, - 'message': 'Verification error occurred. Please try again later.', - 'error': str(e) + 'message': response_data.get('message', 'PayPal payment required'), + 'error': response_data.get('error') } + except Exception as e: + logging.writeToFile(f"PayPal Premium Plugin: PayPal check error: {str(e)}") + return {'has_access': False, 'paypal_me_url': PAYPAL_ME_URL, 'paypal_payment_link': PAYPAL_PAYMENT_LINK, 'message': 'Unable to verify PayPal.', 'error': str(e)} + + +def unified_verification_required(view_func): + @wraps(view_func) + def _wrapped_view(request, *args, **kwargs): + try: + if not request.session.get('userID'): + from loginSystem.views import loadLoginPage + return redirect(loadLoginPage) + + user_email = request.session.get('email', '') or (getattr(request.user, 'email', '') if hasattr(request, 'user') and request.user else '') or getattr(request.user, 'username', '') + + try: + config = PaypalPremiumPluginConfig.get_config() + payment_method = config.payment_method + except Exception: + payment_method = 'both' + + has_access = False + verification_result = {} + + activation_key = request.GET.get('activation_key') or request.POST.get('activation_key') + if not activation_key: + try: + config = PaypalPremiumPluginConfig.get_config() + activation_key = getattr(config, 'activation_key', '') or '' + except Exception: + activation_key = '' + + if activation_key: + try: + request_data = {'activation_key': activation_key.strip(), 'plugin_name': PLUGIN_NAME, 'user_email': user_email} + response_data = _api_request(REMOTE_ACTIVATION_KEY_URL, request_data) + if response_data.get('success', False) and response_data.get('has_access', False): + has_access = True + verification_result = {'method': 'activation_key', 'has_access': True, 'message': response_data.get('message', 'Access activated via key')} + try: + config = PaypalPremiumPluginConfig.get_config() + config.activation_key = activation_key.strip() + config.save(update_fields=['activation_key', 'updated_at']) + except Exception as e: + logging.writeToFile(f"PayPal Premium Plugin: Could not persist activation key: {str(e)}") + elif not response_data.get('success') and activation_key: + try: + config = PaypalPremiumPluginConfig.get_config() + if getattr(config, 'activation_key', '') == activation_key.strip(): + config.activation_key = '' + config.save(update_fields=['activation_key', 'updated_at']) + except Exception: + pass + except Exception as e: + logging.writeToFile(f"PayPal Premium Plugin: Activation key check error: {str(e)}") + + if not has_access: + grant_result = check_plugin_grant(user_email, request.META.get('REMOTE_ADDR', ''), request.get_host()) + if grant_result.get('has_access'): + has_access = True + verification_result = {'method': 'plugin_grant', 'has_access': True, 'message': grant_result.get('message', 'Access granted via Plugin Grants')} + + if not has_access: + try: + if payment_method == 'patreon': + result = check_patreon_membership(user_email, request.META.get('REMOTE_ADDR', ''), request.get_host()) + has_access = result.get('has_access', False) + verification_result = { + 'method': 'patreon', 'has_access': has_access, + 'patreon_tier': result.get('patreon_tier', PATREON_TIER), + 'patreon_url': result.get('patreon_url', PATREON_URL), + 'paypal_me_url': PAYPAL_ME_URL, 'paypal_payment_link': PAYPAL_PAYMENT_LINK, + 'message': result.get('message', 'Patreon subscription required'), + 'error': result.get('error') + } + elif payment_method == 'paypal': + result = check_paypal_payment(user_email, request.META.get('REMOTE_ADDR', ''), request.get_host()) + has_access = result.get('has_access', False) + verification_result = { + 'method': 'paypal', 'has_access': has_access, + 'patreon_tier': PATREON_TIER, 'patreon_url': PATREON_URL, + 'paypal_me_url': result.get('paypal_me_url', PAYPAL_ME_URL), + 'paypal_payment_link': result.get('paypal_payment_link', PAYPAL_PAYMENT_LINK), + 'message': result.get('message', 'PayPal payment required'), + 'error': result.get('error') + } + else: + patreon_result = check_patreon_membership(user_email, request.META.get('REMOTE_ADDR', ''), request.get_host()) + paypal_result = check_paypal_payment(user_email, request.META.get('REMOTE_ADDR', ''), request.get_host()) + has_access = patreon_result.get('has_access', False) or paypal_result.get('has_access', False) + verification_result = { + 'method': 'both', 'has_access': has_access, + 'patreon_tier': patreon_result.get('patreon_tier', PATREON_TIER), + 'patreon_url': patreon_result.get('patreon_url', PATREON_URL), + 'paypal_me_url': paypal_result.get('paypal_me_url', PAYPAL_ME_URL), + 'paypal_payment_link': paypal_result.get('paypal_payment_link', PAYPAL_PAYMENT_LINK), + 'message': 'Payment or subscription required' if not has_access else 'Access granted' + } + except Exception as e: + logging.writeToFile(f"PayPal Premium Plugin: Verification error: {str(e)}") + has_access = False + verification_result = { + 'method': payment_method, 'has_access': False, + 'patreon_tier': PATREON_TIER, 'patreon_url': PATREON_URL, + 'paypal_me_url': PAYPAL_ME_URL, 'paypal_payment_link': PAYPAL_PAYMENT_LINK, + 'message': 'Unable to verify access.', 'error': str(e) + } + + if not has_access: + context = { + 'plugin_name': 'PayPal Premium Plugin Example', + 'is_paid': True, + 'payment_method': payment_method, + 'verification_result': verification_result, + 'patreon_tier': verification_result.get('patreon_tier', PATREON_TIER), + 'patreon_url': verification_result.get('patreon_url', PATREON_URL), + 'paypal_me_url': verification_result.get('paypal_me_url', PAYPAL_ME_URL), + 'paypal_payment_link': verification_result.get('paypal_payment_link', PAYPAL_PAYMENT_LINK), + 'message': verification_result.get('message', 'Payment or subscription required'), + 'error': verification_result.get('error') + } + proc = httpProc(request, 'paypalPremiumPlugin/subscription_required.html', context, 'admin') + return proc.render() + + if has_access and verification_result: + request.session['paypal_premium_access_via'] = verification_result.get('method', '') + + return view_func(request, *args, **kwargs) + except Exception as e: + logging.writeToFile(f"PayPal Premium Plugin: Decorator error: {str(e)}") + return HttpResponse(f"

Plugin Error

{str(e)}

") + return _wrapped_view -def check_remote_payment(user_email, user_ip=''): - """ - Legacy function for backward compatibility - """ - return check_remote_payment_secure(user_email, user_ip, '') @cyberpanel_login_required def main_view(request): - """ - Main view for PayPal premium plugin - Shows plugin information and features if paid, or payment 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 payment status (but don't block access) - verification_result = check_remote_payment_secure( - user_email, - request.META.get('REMOTE_ADDR', ''), - request.get_host() - ) - has_access = verification_result.get('has_access', False) - - # Determine plugin status - plugin_status = 'Active' if has_access else 'Payment Required' - - context = { - 'plugin_name': 'PayPal Premium Plugin Example', - 'version': PLUGIN_VERSION, - 'status': plugin_status, - 'has_access': has_access, - 'description': 'This is an example paid plugin that requires PayPal payment.' if not has_access else 'This is an example paid plugin. You have access because payment has been verified!', - 'paypal_me_url': verification_result.get('paypal_me_url', PAYPAL_ME_URL), - 'paypal_payment_link': verification_result.get('paypal_payment_link', PAYPAL_PAYMENT_LINK), - 'features': [ - 'Premium Feature 1', - 'Premium Feature 2', - 'Premium Feature 3', - 'Advanced Configuration', - 'Priority Support' - ] if has_access else [] - } - - proc = httpProc(request, 'paypalPremiumPlugin/index.html', context, 'admin') - return proc.render() + return redirect('paypalPremiumPlugin:settings') + @cyberpanel_login_required +@unified_verification_required def settings_view(request): - """ - Settings page for PayPal premium plugin - Shows settings but disables them if user doesn't have PayPal payment - """ 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 payment status (but don't block access) - verification_result = check_remote_payment_secure( - user_email, - request.META.get('REMOTE_ADDR', ''), - request.get_host() - ) - has_access = verification_result.get('has_access', False) - - # Determine plugin status - plugin_status = 'Active' if has_access else 'Payment Required' - + try: + config = PaypalPremiumPluginConfig.get_config() + except Exception: + from django.core.management import call_command + try: + call_command('migrate', 'paypalPremiumPlugin', verbosity=0, interactive=False) + config = PaypalPremiumPluginConfig.get_config() + except Exception as e: + return HttpResponse(f"

Database Error

{str(e)}

") + + access_via = request.session.get('paypal_premium_access_via', '') + show_payment_ui = access_via not in ('plugin_grant', 'activation_key') + context = { 'plugin_name': 'PayPal 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, - 'paypal_me_url': verification_result.get('paypal_me_url', PAYPAL_ME_URL), - 'paypal_payment_link': verification_result.get('paypal_payment_link', PAYPAL_PAYMENT_LINK), - 'verification_message': verification_result.get('message', '') + 'status': 'Active', + 'config': config, + 'has_access': True, + 'show_payment_ui': show_payment_ui, + 'access_via_grant_or_key': not show_payment_ui, + 'patreon_tier': PATREON_TIER, + 'patreon_url': PATREON_URL, + 'paypal_me_url': PAYPAL_ME_URL, + 'paypal_payment_link': PAYPAL_PAYMENT_LINK, + 'description': 'Configure your PayPal premium plugin settings.', } - proc = httpProc(request, 'paypalPremiumPlugin/settings.html', context, 'admin') return proc.render() + @cyberpanel_login_required -@secure_verification_required +@require_http_methods(["POST"]) +def activate_key(request): + try: + if request.content_type == 'application/json': + data = json.loads(request.body) + else: + data = request.POST + + activation_key = data.get('activation_key', '').strip() + user_email = data.get('user_email', '').strip() + if not user_email: + user_email = request.session.get('email', '') or (getattr(request.user, 'email', '') if hasattr(request, 'user') and request.user else '') + + if not activation_key: + return JsonResponse({'success': False, 'message': 'Activation key is required'}, status=400) + + request_data = {'activation_key': activation_key, 'plugin_name': PLUGIN_NAME, 'user_email': user_email} + response_data = _api_request(REMOTE_ACTIVATION_KEY_URL, request_data) + + if response_data.get('success', False) and response_data.get('has_access', False): + try: + config = PaypalPremiumPluginConfig.get_config() + config.activation_key = activation_key + config.save(update_fields=['activation_key', 'updated_at']) + except Exception as e: + logging.writeToFile(f"PayPal Premium Plugin: Could not persist activation key: {str(e)}") + + return JsonResponse({ + 'success': True, + 'has_access': True, + 'message': response_data.get('message', 'Access activated successfully') + }) + + return JsonResponse({ + 'success': False, + 'has_access': False, + 'message': response_data.get('message', 'Invalid activation key') + }) + + except Exception as e: + logging.writeToFile(f"PayPal Premium Plugin: activate_key error: {str(e)}") + return JsonResponse({'success': False, 'message': str(e)}, status=500) + + +@cyberpanel_login_required +@require_http_methods(["POST"]) +def save_payment_method(request): + try: + payment_method = request.POST.get('payment_method', 'both') + if payment_method not in ('patreon', 'paypal', 'both'): + payment_method = 'both' + config = PaypalPremiumPluginConfig.get_config() + config.payment_method = payment_method + config.save(update_fields=['payment_method', 'updated_at']) + return JsonResponse({'success': True, 'message': 'Payment method saved'}) + except Exception as e: + return JsonResponse({'success': False, 'message': str(e)}, status=500) + + +@cyberpanel_login_required +@unified_verification_required def api_status_view(request): - """ - API endpoint for plugin status - Only accessible with PayPal payment (verified remotely with enhanced security) - """ return JsonResponse({ 'plugin_name': 'PayPal Premium Plugin Example', 'version': PLUGIN_VERSION, 'status': 'active', 'payment': 'verified', - 'description': 'Premium plugin is active and accessible', - 'verification_method': 'remote_secure' + 'verification_method': 'unified' }) diff --git a/pluginHolder/templates/pluginHolder/plugins.html b/pluginHolder/templates/pluginHolder/plugins.html index 1f74dd360..78e158354 100644 --- a/pluginHolder/templates/pluginHolder/plugins.html +++ b/pluginHolder/templates/pluginHolder/plugins.html @@ -1091,6 +1091,7 @@ {% endblock %} {% block content %} + {% load static %} {% get_current_language as LANGUAGE_CODE %} diff --git a/premiumPlugin/.gitignore b/premiumPlugin/.gitignore new file mode 100644 index 000000000..2ef72c88d --- /dev/null +++ b/premiumPlugin/.gitignore @@ -0,0 +1,6 @@ +# Security - Never commit secrets +*.secret +*_secret* +patreon_config.py +.env.patreon +patreon_secrets.env diff --git a/premiumPlugin/README-REMOTE-VERIFICATION.md b/premiumPlugin/README-REMOTE-VERIFICATION.md new file mode 100644 index 000000000..09decfcd6 --- /dev/null +++ b/premiumPlugin/README-REMOTE-VERIFICATION.md @@ -0,0 +1,81 @@ +# Remote Verification Setup + +## Overview + +This version of the plugin uses **remote verification** - all Patreon API calls happen on YOUR server, not the user's server. + +## Benefits + +✅ **No secrets in plugin** - Users can see all plugin code, but no credentials +✅ **Secure** - All Patreon API credentials stay on your server +✅ **Centralized** - You control access, can revoke, update logic, etc. +✅ **Public code** - Plugin code can be open source + +## Architecture + +``` +User's Server Your Server Patreon API + | | | + |-- Verify Request ------------> | | + | |-- Check Membership --> | + | |<-- Membership Status - | + |<-- Access Granted/Denied ----- | | +``` + +## Setup + +### 1. Deploy Verification API + +Deploy the verification endpoint to your server: +- File: `/home/newstargeted.com/api.newstargeted.com/modules/patreon/verify-membership.php` +- URL: `https://api.newstargeted.com/api/verify-patreon-membership` + +### 2. Configure Your Server + +Add Patreon credentials to your server's `config.php`: + +```php +define('PATREON_CLIENT_ID', 'your_client_id'); +define('PATREON_CLIENT_SECRET', 'your_client_secret'); +define('PATREON_CREATOR_ACCESS_TOKEN', 'your_access_token'); +``` + +### 3. Update Plugin + +Replace `views.py` with `views_remote.py`: + +```bash +mv views.py views_local.py # Backup local version +mv views_remote.py views.py # Use remote version +``` + +### 4. Configure Plugin URL + +Update `REMOTE_VERIFICATION_URL` in `views.py` to point to your server: + +```python +REMOTE_VERIFICATION_URL = 'https://api.newstargeted.com/api/verify-patreon-membership' +``` + +## Security Features + +- **Rate limiting** - Prevents abuse (60 requests/hour per IP) +- **HTTPS only** - All communication encrypted +- **No secrets** - Plugin only makes API calls +- **Caching** - Reduces Patreon API calls (5 min cache) + +## Testing + +1. Install plugin on user's server +2. Try accessing plugin (should show subscription required) +3. Subscribe to Patreon tier +4. Access plugin again (should work) + +## Migration from Local Verification + +If you were using local verification: + +1. Keep `views_local.py` as backup +2. Use `views_remote.py` as `views.py` +3. Deploy verification API to your server +4. Update plugin URL in code diff --git a/premiumPlugin/README.md b/premiumPlugin/README.md new file mode 100644 index 000000000..922bffe2e --- /dev/null +++ b/premiumPlugin/README.md @@ -0,0 +1,58 @@ +# Premium Plugin Example + +An example paid plugin for CyberPanel that demonstrates how to implement Patreon subscription-based plugin access. + +## Features + +- Requires Patreon subscription to "CyberPanel Paid Plugin" tier +- Users can install the plugin without subscription +- Plugin functionality is locked until subscription is verified +- Shows subscription required page when accessed without subscription + +## Installation + +1. Upload the plugin ZIP file to CyberPanel +2. Install the plugin from the plugin manager +3. The plugin will appear in the installed plugins list + +## Usage + +### For Users Without Subscription + +- Plugin can be installed +- When accessing the plugin, a subscription required page is shown +- Link to Patreon subscription page is provided + +### For Users With Subscription + +- Plugin works normally +- All features are accessible +- Settings page is available + +## Configuration + +The plugin checks for Patreon membership via the Patreon API. Make sure to configure: + +1. Patreon Client ID +2. Patreon Client Secret +3. Patreon Creator ID + +These should be set in CyberPanel environment variables or settings. + +## Meta.xml Structure + +The plugin uses the following meta.xml structure for paid plugins: + +```xml +true +CyberPanel Paid Plugin +https://www.patreon.com/c/newstargeted/membership +``` + +## Author + +master3395 + +## License + +MIT diff --git a/premiumPlugin/SECURITY.md b/premiumPlugin/SECURITY.md new file mode 100644 index 000000000..b2df3648e --- /dev/null +++ b/premiumPlugin/SECURITY.md @@ -0,0 +1,57 @@ +# Security Guidelines for Premium Plugin + +## ⚠️ IMPORTANT: Never Expose Secrets + +This plugin is designed to be **publicly shareable**. It contains **NO secrets** and is safe to publish. + +## What's Safe to Share + +✅ **Safe to commit:** +- Plugin code (views.py, urls.py, etc.) +- Templates (HTML files) +- meta.xml (no secrets, only tier name and URL) +- README.md +- Documentation + +❌ **Never commit:** +- Patreon Client Secret +- Patreon Access Tokens +- Patreon Refresh Tokens +- Any hardcoded credentials + +## Configuration + +All Patreon credentials are configured on the **server side** via: +- Environment variables +- Django settings (from environment) +- Secure config files (not in repository) + +## For Your Own Setup + +When setting up this plugin on your server: + +1. **Do NOT** modify plugin files with your secrets +2. **Do** configure environment variables on the server +3. **Do** use Django settings.py (with environment variable fallbacks) +4. **Do** add any secret config files to .gitignore + +## Example Secure Configuration + +```python +# In settings.py (safe to commit) +PATREON_CLIENT_ID = os.environ.get('PATREON_CLIENT_ID', '') +PATREON_CLIENT_SECRET = os.environ.get('PATREON_CLIENT_SECRET', '') + +# On server (NOT in repo) +export PATREON_CLIENT_ID="your_actual_secret" +export PATREON_CLIENT_SECRET="your_actual_secret" +``` + +## Verification + +Before publishing, verify: +- [ ] No secrets in plugin files +- [ ] No secrets in meta.xml +- [ ] No secrets in README +- [ ] All credentials use environment variables +- [ ] .gitignore excludes secret files diff --git a/premiumPlugin/__init__.py b/premiumPlugin/__init__.py new file mode 100644 index 000000000..deac71067 --- /dev/null +++ b/premiumPlugin/__init__.py @@ -0,0 +1,4 @@ +# Premium Plugin Example +# This is a paid plugin that requires Patreon subscription + +default_app_config = 'premiumPlugin.apps.PremiumPluginConfig' diff --git a/premiumPlugin/api_encryption.py b/premiumPlugin/api_encryption.py new file mode 100644 index 000000000..50f8b3d10 --- /dev/null +++ b/premiumPlugin/api_encryption.py @@ -0,0 +1,83 @@ +# -*- coding: utf-8 -*- +""" +AES-256-CBC encryption for plugin <-> api.newstargeted.com communication. +Key must match PLUGIN_VERIFICATION_CIPHER_KEY in config.php on the API server. +""" +import json +import base64 +import os + +CIPHER_KEY_B64 = '1VLPEKTmLGUbIxHUFEtsuVM2MPN1tl8HPFtyJc4dr58=' +ENCRYPTION_ENABLED = True + +_ENCRYPTION_CIPHER_KEY = None + + +def _get_key(): + """Get 32-byte AES key from base64.""" + global _ENCRYPTION_CIPHER_KEY + if _ENCRYPTION_CIPHER_KEY is not None: + return _ENCRYPTION_CIPHER_KEY + try: + key = base64.b64decode(CIPHER_KEY_B64) + if len(key) == 32: + _ENCRYPTION_CIPHER_KEY = key + return key + except Exception: + pass + return None + + +def encrypt_payload(data): + """Encrypt JSON payload for API request. Returns (body_bytes, headers_dict).""" + if not ENCRYPTION_ENABLED or not _get_key(): + body = json.dumps(data, separators=(',', ':')).encode('utf-8') + return body, {'Content-Type': 'application/json'} + try: + from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes + from cryptography.hazmat.primitives import padding + from cryptography.hazmat.backends import default_backend + key = _get_key() + plain = json.dumps(data, separators=(',', ':')).encode('utf-8') + padder = padding.PKCS7(128).padder() + padded = padder.update(plain) + padder.finalize() + iv = os.urandom(16) + cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend()) + encryptor = cipher.encryptor() + ciphertext = encryptor.update(padded) + encryptor.finalize() + payload = base64.b64encode(iv).decode('ascii') + '.' + base64.b64encode(ciphertext).decode('ascii') + return payload.encode('utf-8'), {'Content-Type': 'text/plain', 'X-Encrypted': '1'} + except Exception: + body = json.dumps(data, separators=(',', ':')).encode('utf-8') + return body, {'Content-Type': 'application/json'} + + +def decrypt_response(body_bytes, content_type='', expect_encrypted=False): + """Decrypt API response. Handles both encrypted and plain JSON.""" + try: + body_str = body_bytes.decode('utf-8') if isinstance(body_bytes, bytes) else str(body_bytes) + is_encrypted = ( + expect_encrypted or + ('text/plain' in content_type and '.' in body_str) or + ('.' in body_str and body_str.strip() and body_str.strip()[0] not in '{[') + ) + parts = body_str.strip().split('.', 1) + if is_encrypted and len(parts) == 2 and ENCRYPTION_ENABLED and _get_key(): + from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes + from cryptography.hazmat.primitives import padding + from cryptography.hazmat.backends import default_backend + iv = base64.b64decode(parts[0]) + ciphertext = base64.b64decode(parts[1]) + key = _get_key() + cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend()) + decryptor = cipher.decryptor() + padded = decryptor.update(ciphertext) + decryptor.finalize() + unpadder = padding.PKCS7(128).unpadder() + plain = unpadder.update(padded) + unpadder.finalize() + return json.loads(plain.decode('utf-8')) + return json.loads(body_str) + except Exception: + try: + return json.loads(body_bytes.decode('utf-8') if isinstance(body_bytes, bytes) else body_bytes) + except Exception: + return {} diff --git a/premiumPlugin/apps.py b/premiumPlugin/apps.py new file mode 100644 index 000000000..261eb7905 --- /dev/null +++ b/premiumPlugin/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + +class PremiumPluginConfig(AppConfig): + name = 'premiumPlugin' + verbose_name = 'Premium Plugin Example' diff --git a/premiumPlugin/meta.xml b/premiumPlugin/meta.xml new file mode 100644 index 000000000..a986da653 --- /dev/null +++ b/premiumPlugin/meta.xml @@ -0,0 +1,30 @@ + + + Premium Plugin Example + Utility + 1.0.2 + An example paid plugin that requires Patreon subscription to "CyberPanel Paid Plugin" tier. Users can install it but cannot run it without subscription. + master3395 + https://github.com/master3395/cyberpanel-plugins + MIT + + 3.6+ + 2.2+ + 2.5.5+ + + + 2.5.5 + 3.0.0 + + + true + false + + true + CyberPanel Paid Plugin + https://www.patreon.com/membership/27789984 + https://paypal.me/KimBS?locale.x=en_US&country.x=NO + + /plugins/premiumPlugin/ + /plugins/premiumPlugin/settings/ + diff --git a/premiumPlugin/migrations/0001_initial.py b/premiumPlugin/migrations/0001_initial.py new file mode 100644 index 000000000..e403a7fbd --- /dev/null +++ b/premiumPlugin/migrations/0001_initial.py @@ -0,0 +1,27 @@ +# Generated migration for PremiumPluginConfig + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name='PremiumPluginConfig', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('payment_method', models.CharField(choices=[('patreon', 'Patreon Subscription'), ('paypal', 'PayPal Payment'), ('both', 'Check Both (Patreon or PayPal)')], default='both', help_text='Choose which payment method to use for verification.', max_length=10)), + ('activation_key', models.CharField(blank=True, default='', help_text='Validated activation key - grants access without re-entering.', max_length=64)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ], + options={ + 'verbose_name': 'Premium Plugin Configuration', + 'verbose_name_plural': 'Premium Plugin Configurations', + }, + ), + ] diff --git a/premiumPlugin/migrations/__init__.py b/premiumPlugin/migrations/__init__.py new file mode 100644 index 000000000..80dc97892 --- /dev/null +++ b/premiumPlugin/migrations/__init__.py @@ -0,0 +1 @@ +# Premium Plugin migrations diff --git a/premiumPlugin/models.py b/premiumPlugin/models.py new file mode 100644 index 000000000..a4396a32a --- /dev/null +++ b/premiumPlugin/models.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +from django.db import models + + +class PremiumPluginConfig(models.Model): + """Config for Premium Plugin - activation key and payment preference.""" + PAYMENT_METHOD_CHOICES = [ + ('patreon', 'Patreon Subscription'), + ('paypal', 'PayPal Payment'), + ('both', 'Check Both (Patreon or PayPal)'), + ] + payment_method = models.CharField( + max_length=10, + choices=PAYMENT_METHOD_CHOICES, + default='both', + help_text="Choose which payment method to use for verification." + ) + activation_key = models.CharField( + max_length=64, + blank=True, + default='', + help_text="Validated activation key - grants access without re-entering." + ) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + verbose_name = "Premium Plugin Configuration" + verbose_name_plural = "Premium Plugin Configurations" + + def __str__(self): + return "Premium Plugin Configuration" + + @classmethod + def get_config(cls): + """Get or create the singleton config instance.""" + config, _ = cls.objects.get_or_create(pk=1) + return config + + def save(self, *args, **kwargs): + self.pk = 1 + super().save(*args, **kwargs) diff --git a/premiumPlugin/templates/premiumPlugin/index.html b/premiumPlugin/templates/premiumPlugin/index.html new file mode 100644 index 000000000..f7ab4e200 --- /dev/null +++ b/premiumPlugin/templates/premiumPlugin/index.html @@ -0,0 +1,96 @@ +{% extends "baseTemplate/index.html" %} +{% load i18n %} + +{% block title %}{% trans "Premium Plugin Example - CyberPanel" %}{% endblock %} + +{% block header_scripts %} + +{% endblock %} + +{% block content %} +
+
+ {% trans "Premium Plugin" %} +

{{ plugin_name }}

+

{{ description }}

+

{% trans "Version:" %} {{ version }}

+
+ +
+

{% trans "Premium Features" %}

+
    + {% for feature in features %} +
  • {{ feature }}
  • + {% endfor %} +
+
+
+{% endblock %} diff --git a/premiumPlugin/templates/premiumPlugin/settings.html b/premiumPlugin/templates/premiumPlugin/settings.html new file mode 100644 index 000000000..59a83d132 --- /dev/null +++ b/premiumPlugin/templates/premiumPlugin/settings.html @@ -0,0 +1,315 @@ +{% extends "baseTemplate/index.html" %} +{% load i18n %} + +{% block title %}{% trans "Premium Plugin Settings - CyberPanel" %}{% endblock %} + +{% block header_scripts %} + +{% endblock %} + +{% block content %} +
+
+ {% trans "Premium Plugin" %} +

{% trans "Premium Plugin Settings" %}

+ + {% if show_payment_ui %} + +
+

{% trans "Activate Premium Access" %}

+

{% trans "If you have an activation key, enter it below." %}

+ +
+ {% csrf_token %} + + +
+
+ +
+ +
+ {% csrf_token %} + +
+
+ {% else %} +
+ {% trans "Premium Access Active" %} — {% trans "Access granted via Plugin Grants or activation key." %} +
+ {% endif %} + + +
+ + {% trans "Plugin Information" %} +
    +
  • {% trans "Name" %}: {{ plugin_name|default:"Premium Plugin Example" }}
  • +
  • {% trans "Version" %}: {{ version|default:"1.0.0" }}
  • +
  • + {% trans "Status" %}: + + {{ plugin_status|default:status|default:"Active" }} + +
  • +
+
+ +

{{ description }}

+ +
+ {% csrf_token %} +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ + +
+ + {% if show_payment_ui %} + + {% endif %} +
+
+{% endblock %} diff --git a/premiumPlugin/templates/premiumPlugin/subscription_required.html b/premiumPlugin/templates/premiumPlugin/subscription_required.html new file mode 100644 index 000000000..cfc11a35a --- /dev/null +++ b/premiumPlugin/templates/premiumPlugin/subscription_required.html @@ -0,0 +1,139 @@ +{% extends "baseTemplate/index.html" %} +{% load i18n %} + +{% block title %}{% trans "Payment Required - Premium Plugin" %}{% endblock %} + +{% block header_scripts %} + +{% endblock %} + +{% block content %} +
+
+
+

{% trans "Premium Plugin Access Required" %}

+

{% trans "This plugin requires payment or subscription to access premium features." %}

+ + +
+

{% trans "Activate Premium Access" %}

+

{% trans "If you received an activation key, enter it below." %}

+ +
+ {% csrf_token %} +
+ + +
+ +
+
+ + {% if payment_method == 'both' %} +
+ {% trans "Current Payment Method:" %} {% trans "Check Both (Patreon or PayPal)" %} +
+ {% elif payment_method == 'patreon' %} +
+ {% trans "Current Payment Method:" %} {% trans "Patreon Subscription Only" %} +
+ {% elif payment_method == 'paypal' %} +
+ {% trans "Current Payment Method:" %} {% trans "PayPal Payment Only" %} +
+ {% endif %} + +
+
+

{% trans "Patreon Subscription" %}

+

{% trans "Subscribe to" %} "{{ patreon_tier }}"

+ + {% trans "Subscribe on Patreon" %} + +
+
+

{% trans "PayPal Payment" %}

+

{% trans "Complete one-time payment via PayPal" %}

+ {% if paypal_me_url %} + + {% trans "Pay with PayPal.me" %} + + {% endif %} +
+
+ +
+

{% trans "How it works:" %}

+
    +
  • {% trans "Install the plugin (already done)" %}
  • +
  • {% trans "Enter activation key, subscribe on Patreon, or pay via PayPal" %}
  • +
  • {% trans "The plugin will automatically unlock" %}
  • +
+
+ + {% if error %} +
+ {% trans "Error:" %} {{ error }} +
+ {% endif %} +
+
+ + +{% endblock %} diff --git a/premiumPlugin/urls.py b/premiumPlugin/urls.py new file mode 100644 index 000000000..a9c206dbe --- /dev/null +++ b/premiumPlugin/urls.py @@ -0,0 +1,12 @@ +from django.urls import path, re_path +from . import views + +app_name = 'premiumPlugin' + +urlpatterns = [ + path('', views.main_view, name='main'), + path('settings/', views.settings_view, name='settings'), + re_path(r'^activate-key/$', views.activate_key, name='activate_key'), + path('save-payment-method/', views.save_payment_method, name='save_payment_method'), + path('api/status/', views.api_status_view, name='api_status'), +] diff --git a/premiumPlugin/views.py b/premiumPlugin/views.py index e620f97c5..fb887850f 100644 --- a/premiumPlugin/views.py +++ b/premiumPlugin/views.py @@ -1,269 +1,406 @@ # -*- 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 +Premium Plugin Views - Unified Verification (same as contaboAutoSnapshot) +Supports: Plugin Grants, Activation Key, Patreon, PayPal, AES encryption """ from django.shortcuts import render, redirect -from django.http import JsonResponse +from django.http import JsonResponse, HttpResponse +from django.views.decorators.http import require_http_methods from plogical.mailUtilities import mailUtilities from plogical.httpProc import httpProc +from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter as logging 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' +from .models import PremiumPluginConfig +from . import api_encryption + +PLUGIN_NAME = 'premiumPlugin' +PLUGIN_VERSION = '1.0.2' + +REMOTE_VERIFICATION_PATREON_URL = 'https://api.newstargeted.com/api/verify-patreon-membership.php' +REMOTE_VERIFICATION_PAYPAL_URL = 'https://api.newstargeted.com/api/verify-paypal-payment.php' +REMOTE_VERIFICATION_PLUGIN_GRANT_URL = 'https://api.newstargeted.com/api/verify-plugin-grant.php' +REMOTE_ACTIVATION_KEY_URL = 'https://api.newstargeted.com/api/activate-plugin-key.php' + +PATREON_TIER = 'CyberPanel Paid Plugin' +PATREON_URL = 'https://www.patreon.com/membership/27789984' +PAYPAL_ME_URL = 'https://paypal.me/KimBS?locale.x=en_US&country.x=NO' +PAYPAL_PAYMENT_LINK = '' + 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 + if not request.session.get('userID'): + from loginSystem.views import loadLoginPage + return redirect(loadLoginPage) 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 - } - """ +def _api_request(url, data, timeout=10): + """Send encrypted API request and return decoded response dict.""" + try: + body, extra_headers = api_encryption.encrypt_payload(data) + headers = { + 'User-Agent': f'CyberPanel-Plugin/{PLUGIN_VERSION}', + 'X-Plugin-Name': PLUGIN_NAME + } + headers.update(extra_headers) + req = urllib.request.Request(url, data=body, headers=headers) + with urllib.request.urlopen(req, timeout=timeout) as response: + raw = response.read() + ct = response.headers.get('Content-Type', '') + expect_enc = extra_headers.get('X-Encrypted') == '1' + return api_encryption.decrypt_response(raw, ct, expect_encrypted=expect_enc) + except Exception as e: + logging.writeToFile(f"Premium Plugin: API request error to {url}: {str(e)}") + return {} + + +def check_plugin_grant(user_email, user_ip='', domain=''): + try: + request_data = { + 'user_email': user_email or '', + 'plugin_name': PLUGIN_NAME, + 'user_ip': user_ip, + 'domain': domain, + } + data = _api_request(REMOTE_VERIFICATION_PLUGIN_GRANT_URL, request_data) + if data.get('success') and data.get('has_access'): + return {'has_access': True, 'message': data.get('message', 'Access granted via Plugin Grants')} + return {'has_access': False, 'message': data.get('message', '')} + except Exception as e: + logging.writeToFile(f"Premium Plugin: Plugin grant check error: {str(e)}") + return {'has_access': False, 'message': ''} + + +def check_patreon_membership(user_email, user_ip='', domain=''): 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 + 'domain': domain, + 'tier_id': '27789984' } - - # 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' + response_data = _api_request(REMOTE_VERIFICATION_PATREON_URL, request_data) + if response_data.get('success', False): 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}' + 'has_access': response_data.get('has_access', False), + 'patreon_tier': response_data.get('patreon_tier', PATREON_TIER), + 'patreon_url': response_data.get('patreon_url', PATREON_URL), + 'message': response_data.get('message', 'Access granted'), + 'error': None } - 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.', + 'patreon_tier': PATREON_TIER, + 'patreon_url': PATREON_URL, + 'message': response_data.get('message', 'Patreon subscription required'), + 'error': response_data.get('error') + } + except Exception as e: + logging.writeToFile(f"Premium Plugin: Patreon check error: {str(e)}") + return { + 'has_access': False, + 'patreon_tier': PATREON_TIER, + 'patreon_url': PATREON_URL, + 'message': 'Unable to verify Patreon membership.', '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() + +def check_paypal_payment(user_email, user_ip='', domain=''): + try: + request_data = { + 'user_email': user_email, + 'plugin_name': PLUGIN_NAME, + 'plugin_version': PLUGIN_VERSION, + 'user_ip': user_ip, + 'domain': domain, + 'timestamp': 0, + } + import time + request_data['timestamp'] = int(time.time()) + response_data = _api_request(REMOTE_VERIFICATION_PAYPAL_URL, request_data) + if response_data.get('success', False): + return { + 'has_access': response_data.get('has_access', False), + 'paypal_me_url': response_data.get('paypal_me_url', PAYPAL_ME_URL), + 'paypal_payment_link': response_data.get('paypal_payment_link', PAYPAL_PAYMENT_LINK), + 'message': response_data.get('message', 'Access granted'), + 'error': None + } + return { + 'has_access': False, + 'paypal_me_url': PAYPAL_ME_URL, + 'paypal_payment_link': PAYPAL_PAYMENT_LINK, + 'message': response_data.get('message', 'PayPal payment required'), + 'error': response_data.get('error') + } + except Exception as e: + logging.writeToFile(f"Premium Plugin: PayPal check error: {str(e)}") + return { + 'has_access': False, + 'paypal_me_url': PAYPAL_ME_URL, + 'paypal_payment_link': PAYPAL_PAYMENT_LINK, + 'message': 'Unable to verify PayPal payment.', + 'error': str(e) + } + + +def unified_verification_required(view_func): + @wraps(view_func) + def _wrapped_view(request, *args, **kwargs): + try: + if not request.session.get('userID'): + from loginSystem.views import loadLoginPage + return redirect(loadLoginPage) + + user_email = request.session.get('email', '') or (getattr(request.user, 'email', '') if hasattr(request, 'user') and request.user else '') or getattr(request.user, 'username', '') + + try: + config = PremiumPluginConfig.get_config() + payment_method = config.payment_method + except Exception: + payment_method = 'both' + + has_access = False + verification_result = {} + + activation_key = request.GET.get('activation_key') or request.POST.get('activation_key') + if not activation_key: + try: + config = PremiumPluginConfig.get_config() + activation_key = getattr(config, 'activation_key', '') or '' + except Exception: + activation_key = '' + + if activation_key: + try: + request_data = { + 'activation_key': activation_key.strip(), + 'plugin_name': PLUGIN_NAME, + 'user_email': user_email + } + response_data = _api_request(REMOTE_ACTIVATION_KEY_URL, request_data) + if response_data.get('success', False) and response_data.get('has_access', False): + has_access = True + verification_result = {'method': 'activation_key', 'has_access': True, 'message': response_data.get('message', 'Access activated via key')} + try: + config = PremiumPluginConfig.get_config() + config.activation_key = activation_key.strip() + config.save(update_fields=['activation_key', 'updated_at']) + except Exception as e: + logging.writeToFile(f"Premium Plugin: Could not persist activation key: {str(e)}") + elif not response_data.get('success') and activation_key: + try: + config = PremiumPluginConfig.get_config() + if getattr(config, 'activation_key', '') == activation_key.strip(): + config.activation_key = '' + config.save(update_fields=['activation_key', 'updated_at']) + except Exception: + pass + except Exception as e: + logging.writeToFile(f"Premium Plugin: Activation key check error: {str(e)}") + + if not has_access: + grant_result = check_plugin_grant(user_email, request.META.get('REMOTE_ADDR', ''), request.get_host()) + if grant_result.get('has_access'): + has_access = True + verification_result = {'method': 'plugin_grant', 'has_access': True, 'message': grant_result.get('message', 'Access granted via Plugin Grants')} + + if not has_access: + try: + if payment_method == 'patreon': + result = check_patreon_membership(user_email, request.META.get('REMOTE_ADDR', ''), request.get_host()) + has_access = result.get('has_access', False) + verification_result = { + 'method': 'patreon', 'has_access': has_access, + 'patreon_tier': result.get('patreon_tier', PATREON_TIER), + 'patreon_url': result.get('patreon_url', PATREON_URL), + 'paypal_me_url': PAYPAL_ME_URL, 'paypal_payment_link': PAYPAL_PAYMENT_LINK, + 'message': result.get('message', 'Patreon subscription required'), + 'error': result.get('error') + } + elif payment_method == 'paypal': + result = check_paypal_payment(user_email, request.META.get('REMOTE_ADDR', ''), request.get_host()) + has_access = result.get('has_access', False) + verification_result = { + 'method': 'paypal', 'has_access': has_access, + 'patreon_tier': PATREON_TIER, 'patreon_url': PATREON_URL, + 'paypal_me_url': result.get('paypal_me_url', PAYPAL_ME_URL), + 'paypal_payment_link': result.get('paypal_payment_link', PAYPAL_PAYMENT_LINK), + 'message': result.get('message', 'PayPal payment required'), + 'error': result.get('error') + } + else: + patreon_result = check_patreon_membership(user_email, request.META.get('REMOTE_ADDR', ''), request.get_host()) + paypal_result = check_paypal_payment(user_email, request.META.get('REMOTE_ADDR', ''), request.get_host()) + has_access = patreon_result.get('has_access', False) or paypal_result.get('has_access', False) + verification_result = { + 'method': 'both', 'has_access': has_access, + 'patreon_tier': patreon_result.get('patreon_tier', PATREON_TIER), + 'patreon_url': patreon_result.get('patreon_url', PATREON_URL), + 'paypal_me_url': paypal_result.get('paypal_me_url', PAYPAL_ME_URL), + 'paypal_payment_link': paypal_result.get('paypal_payment_link', PAYPAL_PAYMENT_LINK), + 'message': 'Payment or subscription required' if not has_access else 'Access granted' + } + except Exception as e: + logging.writeToFile(f"Premium Plugin: Verification error: {str(e)}") + has_access = False + verification_result = { + 'method': payment_method, 'has_access': False, + 'patreon_tier': PATREON_TIER, 'patreon_url': PATREON_URL, + 'paypal_me_url': PAYPAL_ME_URL, 'paypal_payment_link': PAYPAL_PAYMENT_LINK, + 'message': 'Unable to verify access.', + 'error': str(e) + } + + if not has_access: + context = { + 'plugin_name': 'Premium Plugin Example', + 'is_paid': True, + 'payment_method': payment_method, + 'verification_result': verification_result, + 'patreon_tier': verification_result.get('patreon_tier', PATREON_TIER), + 'patreon_url': verification_result.get('patreon_url', PATREON_URL), + 'paypal_me_url': verification_result.get('paypal_me_url', PAYPAL_ME_URL), + 'paypal_payment_link': verification_result.get('paypal_payment_link', PAYPAL_PAYMENT_LINK), + 'message': verification_result.get('message', 'Payment or subscription required'), + 'error': verification_result.get('error') + } + proc = httpProc(request, 'premiumPlugin/subscription_required.html', context, 'admin') + return proc.render() + + if has_access and verification_result: + request.session['premium_plugin_access_via'] = verification_result.get('method', '') + + return view_func(request, *args, **kwargs) + except Exception as e: + logging.writeToFile(f"Premium Plugin: Decorator error: {str(e)}") + return HttpResponse(f"

Plugin Error

{str(e)}

") + return _wrapped_view + @cyberpanel_login_required -def settings_view(request): - """ - Settings page for premium plugin - Shows settings but disables them if user doesn't have Patreon subscription - """ +def main_view(request): 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' - + return redirect('premiumPlugin:settings') + + +@cyberpanel_login_required +@unified_verification_required +def settings_view(request): + mailUtilities.checkHome() + try: + config = PremiumPluginConfig.get_config() + except Exception: + from django.core.management import call_command + try: + call_command('migrate', 'premiumPlugin', verbosity=0, interactive=False) + config = PremiumPluginConfig.get_config() + except Exception as e: + return HttpResponse(f"

Database Error

{str(e)}

") + + access_via = request.session.get('premium_plugin_access_via', '') + show_payment_ui = access_via not in ('plugin_grant', 'activation_key') + context = { - 'plugin_name': 'Patreon Premium Plugin Example', + 'plugin_name': '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', '') + 'status': 'Active', + 'config': config, + 'has_access': True, + 'show_payment_ui': show_payment_ui, + 'access_via_grant_or_key': not show_payment_ui, + 'patreon_tier': PATREON_TIER, + 'patreon_url': PATREON_URL, + 'paypal_me_url': PAYPAL_ME_URL, + 'paypal_payment_link': PAYPAL_PAYMENT_LINK, + 'description': 'Configure your premium plugin settings.', } - proc = httpProc(request, 'premiumPlugin/settings.html', context, 'admin') return proc.render() + @cyberpanel_login_required -@remote_verification_required +@require_http_methods(["POST"]) +def activate_key(request): + try: + if request.content_type == 'application/json': + data = json.loads(request.body) + else: + data = request.POST + + activation_key = data.get('activation_key', '').strip() + user_email = data.get('user_email', '').strip() + if not user_email: + user_email = request.session.get('email', '') or (getattr(request.user, 'email', '') if hasattr(request, 'user') and request.user else '') + + if not activation_key: + return JsonResponse({'success': False, 'message': 'Activation key is required'}, status=400) + + request_data = {'activation_key': activation_key, 'plugin_name': PLUGIN_NAME, 'user_email': user_email} + response_data = _api_request(REMOTE_ACTIVATION_KEY_URL, request_data) + + if response_data.get('success', False) and response_data.get('has_access', False): + try: + config = PremiumPluginConfig.get_config() + config.activation_key = activation_key + config.save(update_fields=['activation_key', 'updated_at']) + except Exception as e: + logging.writeToFile(f"Premium Plugin: Could not persist activation key: {str(e)}") + + return JsonResponse({ + 'success': True, + 'has_access': True, + 'message': response_data.get('message', 'Access activated successfully') + }) + + return JsonResponse({ + 'success': False, + 'has_access': False, + 'message': response_data.get('message', 'Invalid activation key') + }) + + except Exception as e: + logging.writeToFile(f"Premium Plugin: activate_key error: {str(e)}") + return JsonResponse({'success': False, 'message': str(e)}, status=500) + + +@cyberpanel_login_required +@require_http_methods(["POST"]) +def save_payment_method(request): + try: + payment_method = request.POST.get('payment_method', 'both') + if payment_method not in ('patreon', 'paypal', 'both'): + payment_method = 'both' + config = PremiumPluginConfig.get_config() + config.payment_method = payment_method + config.save(update_fields=['payment_method', 'updated_at']) + return JsonResponse({'success': True, 'message': 'Payment method saved'}) + except Exception as e: + return JsonResponse({'success': False, 'message': str(e)}, status=500) + + +@cyberpanel_login_required +@unified_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', + 'plugin_name': 'Premium Plugin Example', 'version': PLUGIN_VERSION, 'status': 'active', 'subscription': 'active', - 'description': 'Premium plugin is active and accessible', - 'verification_method': 'remote' + 'verification_method': 'unified' }) diff --git a/premiumPlugin/views_remote.py b/premiumPlugin/views_remote.py new file mode 100644 index 000000000..a4adfaeeb --- /dev/null +++ b/premiumPlugin/views_remote.py @@ -0,0 +1,236 @@ +# -*- coding: utf-8 -*- +""" +Premium Plugin Views - Remote Verification Version +This version uses remote server verification (no secrets in plugin) +""" + +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' +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': '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 +@remote_verification_required +def main_view(request): + """ + Main view for premium plugin + Only accessible with Patreon subscription (verified remotely) + """ + mailUtilities.checkHome() + + context = { + 'plugin_name': 'Premium Plugin Example', + 'version': PLUGIN_VERSION, + 'description': 'This is an example paid plugin. You have access because you are subscribed to Patreon!', + 'features': [ + 'Premium Feature 1', + 'Premium Feature 2', + 'Premium Feature 3', + 'Advanced Configuration', + 'Priority Support' + ] + } + + proc = httpProc(request, 'premiumPlugin/index.html', context, 'admin') + return proc.render() + +@cyberpanel_login_required +@remote_verification_required +def settings_view(request): + """ + Settings page for premium plugin + Only accessible with Patreon subscription (verified remotely) + """ + mailUtilities.checkHome() + + context = { + 'plugin_name': 'Premium Plugin Example', + 'version': PLUGIN_VERSION, + 'description': 'Configure your premium plugin settings' + } + + 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': 'Premium Plugin Example', + 'version': PLUGIN_VERSION, + 'status': 'active', + 'subscription': 'active', + 'description': 'Premium plugin is active and accessible', + 'verification_method': 'remote' + }) From d8ee83e30d4710dc0229e8aa5555c71810d958d9 Mon Sep 17 00:00:00 2001 From: master3395 Date: Mon, 2 Feb 2026 20:39:56 +0100 Subject: [PATCH 009/130] =?UTF-8?q?Plugin=20Store=20&=20Installed=20Plugin?= =?UTF-8?q?s:=20search=20bar,=20A-=C3=85=20sort,=20sort=20toggle,=20Store?= =?UTF-8?q?=20A-=C3=85=20label?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Installed plugins: search box in header (same row as Activate/Deactivate All) - Grid/Table: default sort A-Å by name; sort bar with Name (toggle A-Å/Å-A), Type, Date (toggle newest/oldest) - Apply sort on load so list shows A-Å when Name A-Å is selected - Store view: letter filter label 'A-Å Filter' (not A-Z); add Æ, Ø, Å to letter buttons - views.py: sort pluginList by name (case-insensitive) before template - Add deploy-installed-plugins-search.sh for template deployment --- .../deploy-installed-plugins-search.sh | 29 ++ .../templates/pluginHolder/plugins.html | 265 +++++++++++++++++- pluginHolder/views.py | 100 ++++++- 3 files changed, 379 insertions(+), 15 deletions(-) create mode 100644 pluginHolder/deploy-installed-plugins-search.sh diff --git a/pluginHolder/deploy-installed-plugins-search.sh b/pluginHolder/deploy-installed-plugins-search.sh new file mode 100644 index 000000000..6bdde6e3b --- /dev/null +++ b/pluginHolder/deploy-installed-plugins-search.sh @@ -0,0 +1,29 @@ +#!/bin/bash +# Deploy updated plugins.html (installed-plugins search bar) to CyberPanel +# Run on the server where CyberPanel is installed (e.g. 207.180.193.210) +# Usage: sudo bash deploy-installed-plugins-search.sh +# Or from repo root: sudo bash pluginHolder/deploy-installed-plugins-search.sh + +set -e +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" +SRC="$SCRIPT_DIR/templates/pluginHolder/plugins.html" +DEST="/usr/local/CyberCP/pluginHolder/templates/pluginHolder/plugins.html" + +if [ ! -f "$SRC" ]; then + echo "Error: Source not found: $SRC" + exit 1 +fi + +if [ ! -f "$DEST" ]; then + echo "Error: CyberPanel template not found: $DEST" + exit 1 +fi + +echo "Backing up current template..." +cp "$DEST" "${DEST}.bak.$(date +%Y%m%d)" +echo "Copying updated plugins.html..." +cp "$SRC" "$DEST" +echo "Restarting lscpd..." +systemctl restart lscpd +echo "Done. Hard-refresh the browser (Ctrl+Shift+R) and open /plugins/installed#grid" diff --git a/pluginHolder/templates/pluginHolder/plugins.html b/pluginHolder/templates/pluginHolder/plugins.html index 78e158354..c8c35de44 100644 --- a/pluginHolder/templates/pluginHolder/plugins.html +++ b/pluginHolder/templates/pluginHolder/plugins.html @@ -426,6 +426,52 @@ display: flex; gap: 10px; } + /* Sort/Filter bar for Grid and Table view */ + .installed-sort-filter-bar { + flex-basis: 100%; + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 12px; + margin-top: 12px; + padding: 12px 0; + border-top: 1px solid var(--border-primary, #e2e8f0); + } + .installed-sort-filter-bar .sort-label { + font-size: 13px; + font-weight: 600; + color: var(--text-secondary, #64748b); + margin-right: 4px; + } + .installed-sort-filter-bar .sort-btns { + display: flex; + flex-wrap: wrap; + gap: 8px; + } + .installed-sort-filter-bar .sort-btn { + padding: 6px 14px; + border: 1px solid var(--border-primary, #e8e9ff); + background: var(--bg-primary, white); + border-radius: 8px; + font-size: 12px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s; + color: var(--text-secondary, #64748b); + display: inline-flex; + align-items: center; + gap: 6px; + } + .installed-sort-filter-bar .sort-btn:hover { + background: var(--bg-hover, #f0f1ff); + border-color: #5856d6; + color: #5856d6; + } + .installed-sort-filter-bar .sort-btn.active { + background: #5856d6; + color: white; + border-color: #5856d6; + } .bulk-actions-header { display: flex !important; visibility: visible !important; @@ -661,6 +707,16 @@ max-width: 480px; } + /* Search bar in page header (same row as Activate/Deactivate All) */ + .header-search-bar { + margin-bottom: 0; + min-width: 220px; + max-width: 320px; + margin-left: 8px; + padding-left: 16px; + border-left: 2px solid #e2e8f0; + } + .store-search-icon { position: absolute; left: 16px; @@ -1123,6 +1179,13 @@ {% if plugins %} +
+ +
{% for plugin in plugins %} -
+
{% if plugin.type|lower == "security" %} @@ -1275,6 +1355,10 @@
{% endfor %} +
@@ -1295,7 +1379,7 @@ {% for plugin in plugins %} - + {{ plugin.name }} {% if plugin.freshness_badge %} @@ -1375,6 +1459,12 @@ {% endfor %} + + + +

{% trans "No plugins match your search." %}

+ +
@@ -1503,17 +1593,20 @@
- +
@@ -1555,6 +1648,7 @@ let currentFilter = 'all'; let currentCategory = 'all'; let currentSearchQuery = ''; let isSettingHash = false; // Flag to prevent infinite loops +let currentInstalledSort = 'name-asc'; // name-asc, name-desc, type, date-desc, date-asc // Get CSRF cookie helper function function getCookie(name) { @@ -1597,17 +1691,31 @@ function toggleView(view, updateHash = true) { setTimeout(() => { isSettingHash = false; }, 100); } + const installedSearchWrapper = document.getElementById('installedPluginsSearchWrapper'); + const installedSortFilterBar = document.getElementById('installedSortFilterBar'); if (view === 'grid') { gridView.style.display = 'grid'; tableView.style.display = 'none'; storeView.style.display = 'none'; viewBtns[0].classList.add('active'); + if (installedSearchWrapper) installedSearchWrapper.style.display = 'block'; + if (installedSortFilterBar) installedSortFilterBar.style.display = 'flex'; + if (typeof updateInstalledSortButtons === 'function') updateInstalledSortButtons(); + if (typeof doApplyInstalledSort === 'function') doApplyInstalledSort(); + filterInstalledPlugins(); } else if (view === 'table') { gridView.style.display = 'none'; tableView.style.display = 'block'; storeView.style.display = 'none'; viewBtns[1].classList.add('active'); + if (installedSearchWrapper) installedSearchWrapper.style.display = 'block'; + if (installedSortFilterBar) installedSortFilterBar.style.display = 'flex'; + if (typeof updateInstalledSortButtons === 'function') updateInstalledSortButtons(); + if (typeof doApplyInstalledSort === 'function') doApplyInstalledSort(); + filterInstalledPlugins(); } else if (view === 'store') { + if (installedSearchWrapper) installedSearchWrapper.style.display = 'none'; + if (installedSortFilterBar) installedSortFilterBar.style.display = 'none'; gridView.style.display = 'none'; tableView.style.display = 'none'; storeView.style.display = 'block'; @@ -1911,6 +2019,137 @@ function clearPluginSearch() { } } +function filterInstalledPlugins() { + const query = (document.getElementById('installedPluginSearchInput') && document.getElementById('installedPluginSearchInput').value) || ''; + const terms = query.trim().toLowerCase().split(/\s+/).filter(function(t) { return t.length > 0; }); + const gridView = document.getElementById('gridView'); + const tableView = document.getElementById('tableView'); + const noResultsGrid = document.getElementById('installedPluginsNoResultsGrid'); + const noResultsTable = document.getElementById('installedPluginsNoResultsTable'); + var visibleCount = 0; + if (gridView) { + var cards = gridView.querySelectorAll('.plugin-card'); + cards.forEach(function(card) { + var name = (card.getAttribute('data-plugin-name') || '').toLowerCase(); + var desc = (card.getAttribute('data-plugin-desc') || '').toLowerCase(); + var type = (card.getAttribute('data-plugin-type') || '').toLowerCase(); + var combined = name + ' ' + desc + ' ' + type; + var show = terms.length === 0 || terms.every(function(term) { return combined.indexOf(term) !== -1; }); + card.style.display = show ? '' : 'none'; + if (show) visibleCount++; + }); + } + if (tableView) { + var tbody = tableView.querySelector('.plugins-table-wrapper tbody'); + if (tbody) { + var rows = tbody.querySelectorAll('tr'); + rows.forEach(function(row) { + if (row.id === 'installedPluginsNoResultsTable') return; + var name = (row.getAttribute('data-plugin-name') || '').toLowerCase(); + var desc = (row.getAttribute('data-plugin-desc') || '').toLowerCase(); + var type = (row.getAttribute('data-plugin-type') || '').toLowerCase(); + var combined = name + ' ' + desc + ' ' + type; + var show = terms.length === 0 || terms.every(function(term) { return combined.indexOf(term) !== -1; }); + row.style.display = show ? '' : 'none'; + if (show) visibleCount++; + }); + } + } + if (noResultsGrid) { + noResultsGrid.style.display = (terms.length > 0 && visibleCount === 0) ? 'block' : 'none'; + } + if (noResultsTable) { + noResultsTable.style.display = (terms.length > 0 && visibleCount === 0) ? 'table-row' : 'none'; + } +} + +function clearInstalledPluginSearch() { + var input = document.getElementById('installedPluginSearchInput'); + var clearBtn = document.getElementById('installedPluginSearchClear'); + if (input) { + input.value = ''; + if (clearBtn) clearBtn.style.display = 'none'; + filterInstalledPlugins(); + input.focus(); + } +} + +function toggleInstalledSort(field) { + if (field === 'name') { + currentInstalledSort = currentInstalledSort === 'name-asc' ? 'name-desc' : 'name-asc'; + } else if (field === 'date') { + currentInstalledSort = currentInstalledSort === 'date-desc' ? 'date-asc' : 'date-desc'; + } else { + currentInstalledSort = 'type'; + } + updateInstalledSortButtons(); + doApplyInstalledSort(); +} + +function updateInstalledSortButtons() { + var bar = document.getElementById('installedSortFilterBar'); + var transNameAsc = bar && bar.getAttribute('data-trans-name-asc') || 'Name A-Å'; + var transNameDesc = bar && bar.getAttribute('data-trans-name-desc') || 'Name Å-A'; + var transDateNewest = bar && bar.getAttribute('data-trans-date-newest') || 'Date (newest)'; + var transDateOldest = bar && bar.getAttribute('data-trans-date-oldest') || 'Date (oldest)'; + var nameBtn = document.getElementById('installedSortBtnName'); + var typeBtn = document.getElementById('installedSortBtnType'); + var dateBtn = document.getElementById('installedSortBtnDate'); + if (nameBtn) { + nameBtn.classList.toggle('active', currentInstalledSort === 'name-asc' || currentInstalledSort === 'name-desc'); + var nameLabel = nameBtn.querySelector('.sort-btn-label'); + var nameIcon = nameBtn.querySelector('i'); + if (nameLabel) nameLabel.textContent = currentInstalledSort === 'name-desc' ? transNameDesc : transNameAsc; + if (nameIcon) nameIcon.className = currentInstalledSort === 'name-desc' ? 'fas fa-sort-alpha-down-alt' : 'fas fa-sort-alpha-down'; + } + if (typeBtn) typeBtn.classList.toggle('active', currentInstalledSort === 'type'); + if (dateBtn) { + dateBtn.classList.toggle('active', currentInstalledSort === 'date-asc' || currentInstalledSort === 'date-desc'); + var dateLabel = dateBtn.querySelector('.sort-btn-label'); + var dateIcon = dateBtn.querySelector('i'); + if (dateLabel) dateLabel.textContent = currentInstalledSort === 'date-asc' ? transDateOldest : transDateNewest; + if (dateIcon) dateIcon.className = currentInstalledSort === 'date-asc' ? 'fas fa-calendar' : 'fas fa-calendar-alt'; + } +} + +function doApplyInstalledSort() { + var sortKey = currentInstalledSort; + var gridView = document.getElementById('gridView'); + var tableView = document.getElementById('tableView'); + var noResultsGrid = document.getElementById('installedPluginsNoResultsGrid'); + var noResultsTable = document.getElementById('installedPluginsNoResultsTable'); + function compareCards(a, b) { + var nameA = (a.getAttribute('data-plugin-name') || '').toLowerCase(); + var nameB = (b.getAttribute('data-plugin-name') || '').toLowerCase(); + var typeA = (a.getAttribute('data-plugin-type') || '').toLowerCase(); + var typeB = (b.getAttribute('data-plugin-type') || '').toLowerCase(); + var dateA = a.getAttribute('data-modify-date') || '0000-00-00 00:00:00'; + var dateB = b.getAttribute('data-modify-date') || '0000-00-00 00:00:00'; + if (sortKey === 'name-asc') return nameA.localeCompare(nameB); + if (sortKey === 'name-desc') return nameB.localeCompare(nameA); + if (sortKey === 'type') { var c = typeA.localeCompare(typeB); return c !== 0 ? c : nameA.localeCompare(nameB); } + if (sortKey === 'date-desc') return dateB.localeCompare(dateA); + if (sortKey === 'date-asc') return dateA.localeCompare(dateB); + return 0; + } + if (gridView) { + var cards = Array.prototype.slice.call(gridView.querySelectorAll('.plugin-card')); + cards.sort(compareCards); + cards.forEach(function(card) { gridView.appendChild(card); }); + if (noResultsGrid) gridView.appendChild(noResultsGrid); + } + if (tableView) { + var tbody = tableView.querySelector('.plugins-table-wrapper tbody'); + if (tbody) { + var rows = Array.prototype.slice.call(tbody.querySelectorAll('tr')); + var dataRows = rows.filter(function(r) { return r.id !== 'installedPluginsNoResultsTable'; }); + dataRows.sort(compareCards); + dataRows.forEach(function(row) { tbody.appendChild(row); }); + if (noResultsTable) tbody.appendChild(noResultsTable); + } + } +} + function toggleAlphabetFilter() { const filter = document.getElementById('alphabetFilter'); const toggleBtn = document.getElementById('alphabetToggleBtn'); @@ -2680,6 +2919,24 @@ document.addEventListener('DOMContentLoaded', function() { }); } + // Installed plugins search (Grid View and Table View) + const installedSearchInput = document.getElementById('installedPluginSearchInput'); + const installedSearchClearBtn = document.getElementById('installedPluginSearchClear'); + if (installedSearchInput) { + installedSearchInput.addEventListener('input', function() { + if (installedSearchClearBtn) { + installedSearchClearBtn.style.display = this.value.trim() ? 'block' : 'none'; + } + filterInstalledPlugins(); + }); + installedSearchInput.addEventListener('keydown', function(e) { + if (e.key === 'Escape') { + clearInstalledPluginSearch(); + e.preventDefault(); + } + }); + } + // Check URL hash for view preference const hash = window.location.hash.substring(1); // Remove # const validViews = ['grid', 'table', 'store']; diff --git a/pluginHolder/views.py b/pluginHolder/views.py index 1dec3c48e..d617ef2b5 100644 --- a/pluginHolder/views.py +++ b/pluginHolder/views.py @@ -35,6 +35,18 @@ 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'] + +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 _get_plugin_state_file(plugin_name): """Get the path to the plugin state file""" if not os.path.exists(PLUGIN_STATE_DIR): @@ -100,15 +112,23 @@ 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): + # First, process plugins from source directories (multiple paths: /home/cyberpanel/plugins, /home/cyberpanel-plugins) + 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): @@ -130,7 +150,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) @@ -383,6 +404,60 @@ def installed(request): logging.writeToFile(f"Installed plugin {plugin}: Error loading - {str(e)}") continue + # 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, + '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 # Double-check by also counting plugins that actually exist in /usr/local/CyberCP/ installed_plugins_in_filesystem = set() @@ -415,6 +490,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, @@ -426,12 +504,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 @@ -1546,9 +1624,9 @@ def install_from_store(request, plugin_name): # 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}") From d9329e2a218cbbd6bda0c1a296bbd09736635974 Mon Sep 17 00:00:00 2001 From: master3395 Date: Tue, 3 Feb 2026 00:00:59 +0100 Subject: [PATCH 010/130] Remove bundled plugins from v2.5.5-dev - plugins live in cyberpanel-plugins only Removed: emailMarketing, examplePlugin, paypalPremiumPlugin, premiumPlugin, testPlugin All plugins are now installed via Plugin Store from https://github.com/master3395/cyberpanel-plugins --- emailMarketing/.DS_Store | Bin 6148 -> 0 bytes emailMarketing/__init__.py | 1 - emailMarketing/admin.py | 6 - emailMarketing/apps.py | 10 - emailMarketing/emACL.py | 66 - emailMarketing/emailMarketing.py | 427 ----- emailMarketing/emailMarketingManager.py | 899 --------- emailMarketing/meta.xml | 7 - emailMarketing/migrations/__init__.py | 0 emailMarketing/models.py | 56 - emailMarketing/signals.py | 0 .../static/emailMarketing/checklist.png | Bin 1755 -> 0 bytes .../static/emailMarketing/compose.png | Bin 1842 -> 0 bytes .../static/emailMarketing/emailMarketing.js | 1468 --------------- .../static/emailMarketing/mailing.png | Bin 1950 -> 0 bytes .../static/emailMarketing/paper-plane.png | Bin 2226 -> 0 bytes .../static/emailMarketing/post-office.png | Bin 1548 -> 0 bytes .../emailMarketing/composeMessages.html | 90 - .../emailMarketing/configureVerify.html | 99 - .../emailMarketing/createEmailList.html | 75 - .../emailMarketing/emailMarketing.html | 72 - .../templates/emailMarketing/manageLists.html | 315 ---- .../emailMarketing/manageSMTPHosts.html | 126 -- .../templates/emailMarketing/sendEmails.html | 204 --- .../templates/emailMarketing/website.html | 1134 ------------ emailMarketing/tests.py | 6 - emailMarketing/urls.py | 31 - emailMarketing/views.py | 215 --- examplePlugin/__init__.py | 1 - examplePlugin/admin.py | 3 - examplePlugin/apps.py | 8 - examplePlugin/enable_migrations | 0 examplePlugin/meta.xml | 8 - examplePlugin/migrations/__init__.py | 0 examplePlugin/models.py | 9 - examplePlugin/post_install | 4 - examplePlugin/pre_install | 4 - examplePlugin/pre_remove | 4 - examplePlugin/signals.py | 17 - .../static/examplePlugin/examplePlugin.js | 3 - .../examplePlugin/examplePlugin.html | 52 - examplePlugin/tests.py | 3 - examplePlugin/urls.py | 7 - examplePlugin/views.py | 7 - install/install.py | 2 +- paypalPremiumPlugin/README.md | 58 - paypalPremiumPlugin/__init__.py | 4 - paypalPremiumPlugin/api_encryption.py | 80 - paypalPremiumPlugin/apps.py | 5 - paypalPremiumPlugin/meta.xml | 30 - .../migrations/0001_initial.py | 27 - paypalPremiumPlugin/migrations/__init__.py | 1 - paypalPremiumPlugin/models.py | 40 - .../templates/paypalPremiumPlugin/index.html | 96 - .../paypalPremiumPlugin/settings.html | 331 ---- .../subscription_required.html | 139 -- paypalPremiumPlugin/urls.py | 12 - paypalPremiumPlugin/views.py | 387 ---- premiumPlugin/.gitignore | 6 - premiumPlugin/README-REMOTE-VERIFICATION.md | 81 - premiumPlugin/README.md | 58 - premiumPlugin/SECURITY.md | 57 - premiumPlugin/__init__.py | 4 - premiumPlugin/api_encryption.py | 83 - premiumPlugin/apps.py | 5 - premiumPlugin/meta.xml | 30 - premiumPlugin/migrations/0001_initial.py | 27 - premiumPlugin/migrations/__init__.py | 1 - premiumPlugin/models.py | 42 - .../templates/premiumPlugin/index.html | 96 - .../templates/premiumPlugin/settings.html | 315 ---- .../premiumPlugin/subscription_required.html | 139 -- premiumPlugin/urls.py | 12 - premiumPlugin/views.py | 406 ----- premiumPlugin/views_remote.py | 236 --- testPlugin/OS_COMPATIBILITY.md | 464 ----- testPlugin/SECURITY.md | 247 --- testPlugin/__init__.py | 2 - testPlugin/admin.py | 20 - testPlugin/apps.py | 11 - testPlugin/install.sh | 580 ------ testPlugin/meta.xml | 26 - testPlugin/middleware.py | 208 --- testPlugin/models.py | 35 - testPlugin/os_config.py | 365 ---- testPlugin/security.py | 256 --- testPlugin/signals.py | 15 - .../static/testPlugin/css/testPlugin.css | 418 ----- testPlugin/static/testPlugin/js/testPlugin.js | 323 ---- testPlugin/templates/testPlugin/index.html | 71 - .../templates/testPlugin/plugin_docs.html | 1624 ----------------- .../templates/testPlugin/plugin_home.html | 571 ------ .../templates/testPlugin/plugin_logs.html | 291 --- .../templates/testPlugin/plugin_settings.html | 264 --- .../templates/testPlugin/security_info.html | 499 ----- testPlugin/templates/testPlugin/settings.html | 165 -- testPlugin/test_os_compatibility.py | 446 ----- testPlugin/urls.py | 8 - testPlugin/views.py | 54 - 99 files changed, 1 insertion(+), 15209 deletions(-) delete mode 100644 emailMarketing/.DS_Store delete mode 100644 emailMarketing/__init__.py delete mode 100644 emailMarketing/admin.py delete mode 100644 emailMarketing/apps.py delete mode 100644 emailMarketing/emACL.py delete mode 100644 emailMarketing/emailMarketing.py delete mode 100644 emailMarketing/emailMarketingManager.py delete mode 100644 emailMarketing/meta.xml delete mode 100644 emailMarketing/migrations/__init__.py delete mode 100644 emailMarketing/models.py delete mode 100644 emailMarketing/signals.py delete mode 100644 emailMarketing/static/emailMarketing/checklist.png delete mode 100644 emailMarketing/static/emailMarketing/compose.png delete mode 100644 emailMarketing/static/emailMarketing/emailMarketing.js delete mode 100644 emailMarketing/static/emailMarketing/mailing.png delete mode 100644 emailMarketing/static/emailMarketing/paper-plane.png delete mode 100644 emailMarketing/static/emailMarketing/post-office.png delete mode 100644 emailMarketing/templates/emailMarketing/composeMessages.html delete mode 100644 emailMarketing/templates/emailMarketing/configureVerify.html delete mode 100644 emailMarketing/templates/emailMarketing/createEmailList.html delete mode 100644 emailMarketing/templates/emailMarketing/emailMarketing.html delete mode 100644 emailMarketing/templates/emailMarketing/manageLists.html delete mode 100644 emailMarketing/templates/emailMarketing/manageSMTPHosts.html delete mode 100644 emailMarketing/templates/emailMarketing/sendEmails.html delete mode 100644 emailMarketing/templates/emailMarketing/website.html delete mode 100644 emailMarketing/tests.py delete mode 100644 emailMarketing/urls.py delete mode 100644 emailMarketing/views.py delete mode 100644 examplePlugin/__init__.py delete mode 100644 examplePlugin/admin.py delete mode 100644 examplePlugin/apps.py delete mode 100644 examplePlugin/enable_migrations delete mode 100644 examplePlugin/meta.xml delete mode 100644 examplePlugin/migrations/__init__.py delete mode 100644 examplePlugin/models.py delete mode 100644 examplePlugin/post_install delete mode 100644 examplePlugin/pre_install delete mode 100644 examplePlugin/pre_remove delete mode 100644 examplePlugin/signals.py delete mode 100644 examplePlugin/static/examplePlugin/examplePlugin.js delete mode 100644 examplePlugin/templates/examplePlugin/examplePlugin.html delete mode 100644 examplePlugin/tests.py delete mode 100644 examplePlugin/urls.py delete mode 100644 examplePlugin/views.py delete mode 100644 paypalPremiumPlugin/README.md delete mode 100644 paypalPremiumPlugin/__init__.py delete mode 100644 paypalPremiumPlugin/api_encryption.py delete mode 100644 paypalPremiumPlugin/apps.py delete mode 100644 paypalPremiumPlugin/meta.xml delete mode 100644 paypalPremiumPlugin/migrations/0001_initial.py delete mode 100644 paypalPremiumPlugin/migrations/__init__.py delete mode 100644 paypalPremiumPlugin/models.py delete mode 100644 paypalPremiumPlugin/templates/paypalPremiumPlugin/index.html delete mode 100644 paypalPremiumPlugin/templates/paypalPremiumPlugin/settings.html delete mode 100644 paypalPremiumPlugin/templates/paypalPremiumPlugin/subscription_required.html delete mode 100644 paypalPremiumPlugin/urls.py delete mode 100644 paypalPremiumPlugin/views.py delete mode 100644 premiumPlugin/.gitignore delete mode 100644 premiumPlugin/README-REMOTE-VERIFICATION.md delete mode 100644 premiumPlugin/README.md delete mode 100644 premiumPlugin/SECURITY.md delete mode 100644 premiumPlugin/__init__.py delete mode 100644 premiumPlugin/api_encryption.py delete mode 100644 premiumPlugin/apps.py delete mode 100644 premiumPlugin/meta.xml delete mode 100644 premiumPlugin/migrations/0001_initial.py delete mode 100644 premiumPlugin/migrations/__init__.py delete mode 100644 premiumPlugin/models.py delete mode 100644 premiumPlugin/templates/premiumPlugin/index.html delete mode 100644 premiumPlugin/templates/premiumPlugin/settings.html delete mode 100644 premiumPlugin/templates/premiumPlugin/subscription_required.html delete mode 100644 premiumPlugin/urls.py delete mode 100644 premiumPlugin/views.py delete mode 100644 premiumPlugin/views_remote.py delete mode 100644 testPlugin/OS_COMPATIBILITY.md delete mode 100644 testPlugin/SECURITY.md delete mode 100644 testPlugin/__init__.py delete mode 100644 testPlugin/admin.py delete mode 100644 testPlugin/apps.py delete mode 100644 testPlugin/install.sh delete mode 100644 testPlugin/meta.xml delete mode 100644 testPlugin/middleware.py delete mode 100644 testPlugin/models.py delete mode 100644 testPlugin/os_config.py delete mode 100644 testPlugin/security.py delete mode 100644 testPlugin/signals.py delete mode 100644 testPlugin/static/testPlugin/css/testPlugin.css delete mode 100644 testPlugin/static/testPlugin/js/testPlugin.js delete mode 100644 testPlugin/templates/testPlugin/index.html delete mode 100644 testPlugin/templates/testPlugin/plugin_docs.html delete mode 100644 testPlugin/templates/testPlugin/plugin_home.html delete mode 100644 testPlugin/templates/testPlugin/plugin_logs.html delete mode 100644 testPlugin/templates/testPlugin/plugin_settings.html delete mode 100644 testPlugin/templates/testPlugin/security_info.html delete mode 100644 testPlugin/templates/testPlugin/settings.html delete mode 100644 testPlugin/test_os_compatibility.py delete mode 100644 testPlugin/urls.py delete mode 100644 testPlugin/views.py diff --git a/emailMarketing/.DS_Store b/emailMarketing/.DS_Store deleted file mode 100644 index 74248247034fb6268d3f3a976e89c26fdb49f3a2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKOHRWu5FNJ>P1{Y`b%V$WDshAK!vYCbJpl9*MHL~b`U7^^Bd)*|5QpI~tl-Vq zLXDetiBRRK?B_Ud#-0}^u8BzX*3%2377_Ui41^C@<$~!xwl(ygB=c^=}jFW5(2D7>-oo_L;_>(ZADLud^mG|A|zgzqo z_;dW0Wo&h3v3-gVyGwn#F6F;i{8QnNFq?#KFw%nV(X*1-#ibFxtO)Ic5jbXw5`(oU zi&u%Qcs}OOGZ=yWgmzNYv+`;28=_^5=p&hThVhm~pK9OoslzA3Q;3jtsjk%z%cB9_ z3|Vk4dwjfe%jX0>oCm8pDbC1yJ;dD%7ip5EtPJ)Csv`nF)j3AyXpxS0B}V|Dhi);n`OgCT1O`KcxklW9DeVf>uF4!Sly*mdV0fXyT%&d;Wey+8 zjI7KFMak%h9|)aPsL|C<0jEGwfvUc2@&13g|NLKca#v0Pr@&DupuE9w&_haQZ{10b v_u2sE4uy?*xkhn8Wwv8!@K)SL5yQQJ8^F+Dt`R*j_eVfvaFtWwPZjt9SEb~A 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 - Utility - Email Marketing plugin for CyberPanel. - 1.0.1 - \ 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 5236aa0a87fb11b17312bd64530956d122d68d34..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1755 zcmV<11|<23P)|<;2OGaYU(8jkPktPJAPRCPf`LNF zB0dz0xTP8jE$ya>1!>&zKf5!Vx#{lSeojuDlH0iZIBB|@5Kp4{Ee3qmPl3uxp307f!5e3r06bYvqVT3YxIr z^mPRXj2{35b4O_hz=%z1bY6lvxJtdm=4Ch+Q$Ws0jOC;IEOii8z|Ro^b4R+X7z%m9 zWC0xyCi3{ceaT*3uVg?&|Lo`FaJdL_>V5C@>|Md!5L_^hVR9Zr#XP;`zx?K(P8>h&G} z@!C89m`fQ1eJx<+nLXFt>+2_8?cP&lw{5PtIDD7|>c@6Z-(motbJprGyqX#qb|eog?>pbiS#Vz%yJ zOD4W^B4&d&JnbHARM`pX0X&g|J4CRAoCU0aT0mbglui-j!HFCK;_B1yhGzeJ`n8W?mz)RN?d}Jr$hSF69z+W>NF5`1VA+@7PN9MmdN(`q7^IvAT#G;Dcl_>02o$5Y4eURHYVYzH$(t5r~mS_W+ku! zE{md)07#4zR+PFRDNI0tyX`zfX;|0YEdQ3;5;zMX7Y2O&Sa{DV8b& zQsn8UZfb)yOn~x}NdQF1`<{iIyZRW4lM+)xz}{+bG(wL2^{{557g>l#=r9`j|b&_&kFw7d>L(S-JY(E7w6fBWL}14|b! zAN=q_BZd_$NGWlir^KtUHb1lRg41$S20?r?R3`BOV7Fe-a7e`%miOh-`L`=?pslSj zr{|~)LVrsv_{#Twa!J4MJo(oX72yB*QR42r;1mP1I z$0`B@OvL1_X13|aB7F&Pd0TzJa-TP1kuONTV!Ivxv)@j zD#j`UV8HLGfIw(K9B45sFvNEHpogNoPAD_tVN2KVU%b@>3z%ZoAOfg=H9Kw%$Oy4y zN9qK;{f&?Z#819`X@8HF&fEnC0Z1$yfXeK!?Fxf~(KoM*$Eg?-2?wAwF!CY^=UgmB zw*+=S|0=ZvAtIBVr$Flj<$;k0#IijMW{_QAj9(`VktKKlY{d(xVTuVHfZ~c#{ID8z zVKKyl6ZXC|^_I3mb%}&8QN>bMG-q)!}#x7yu{dX;VSswQ&Fd002ovPDHLkV1g`o6f*z- diff --git a/emailMarketing/static/emailMarketing/compose.png b/emailMarketing/static/emailMarketing/compose.png deleted file mode 100644 index 4e786da4d0d16c631885537abe0d440936ecd604..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1842 zcmV-22hI42P)3baJ5pn{ z72Lgfva1ILJ!IoQ3Svnx6v1k;R4^AcdlJ251EP4^)I*{EAsa|R)X*r#OAfmMi(c9+ zMS7DCQKBtsM$?pl$vnR|^JeFpw{Ld4voo_1=7Y(6d9yn+@Atm%_x*WGNFs?Ol1L(n zh@wPx=Swt8DAC{`#Ds&LDf}^M8@l~}Kb-f$ST79p!M##2#ts$6drfLw9X(jvdmvn* z$^!I%m<0AK+Ddj85ut-Np@<*Ck6nja9(jIih+^n77&UBKp$}dVCK(6_3DEFKu)p2NW)NT3>bw5e|0q?0BWvz)96^;B1f={T{xSxiy$-tPzC@B9$$}07$-0Q zm=&bD27(NL$CZ5D$DMOT;sDS+ckly%h!G)LcNm~+R)bFR1Bp=K$5#&3 z_yi~6gngV5U#$sGEW-6dgdjAD{ma`q_{?Mo05F}c>JTS5yuvdu!Ey_p$VZnan27mh zKv0V;^$-AH`U+p+Qi~P#<%#aN!*l!D!!x)X-smh6Tx+(%t#~4$JRzRKhyzeT05q&H ze5Qzb;^r z2gvH?`&JpPm~oZTEs~{;7y7G5zPY)WqEvGLaP|jO-U@S@ARhf|NhBx(0L5lJadBh0 z{O|4oDX_Y5l~VBLRqzg}7BA5)@kT#I2qCtx{U;ke>p&1F_oT5fw!a5#mTt zlJSIPNha;s9oj9W-ApjN=rAjZ1cnd^CS^RKaJP!r9Z)9N$CDu}4=wzY1|MHlZL;&r z1R)yG%=hC124CUf7Lj1qk0+ef@6;XI<^(4dX0p}L0;lN~0I(I)Ku_}tP6_~&1)iv& z>kT}iP_H$_0)kizh$->a(Sy~8zc30gS&f5IH&YZ5jEUX>Ls(%ZgD~+jZ;*&Qp^I+P zHFv-OqOK?mMAma;Zow>ML=p+4MUewQtkJ<+4R-Y4L0lRmJTLSJ@v`;^6CAD&j_7jhx8VOT%gPVfbUh< zcSk?aGhe-v)H6ptIar|!d(CZz;=@Ib|k5< zwkC52#q1o2wk$y`hD2dK!6)bb7=jnh;qYZ|mQtOsHg1BXQCA3Chn0GR3(SB8Fl(rN z`EEw)4hnTGA{G!Z0IJVJFOJ<)MP}Y0XOvi}NG|@c|4kAAH!z4<^j6cD-*9dIom&(f#?QNU19M8M=|u{l+A5|Y#b zh&wvqVn~$AYlrn`&;6c(>scNV>Mv_IVHEw_w~mGfg4u;+>2`i2wEX&gMziP;s)XbY zJwe<-=`b$;@7=-41Y_TpZsrbVjPBq~g}ZMFmOGd+yFfE$Nl>%^8x&rpMxQYP0Mo3jm1lyK;LQ-yLMMWS5Q*gw zQ8%d{0Qry4Wdnl{XdCJH+4Y=KpA35$#oY;#n6sZQ145Nep6R>Tg81ZZAoNZENF -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 da227aa3207373a3ed7acc26ade5ce0ecd3095d5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1950 zcmV;P2VwY$P)rZhnjia)!hX%&5Fimfk#*{TIm zVn!cC=!08JN*k;lseLle_1v9%XV1Mmo6KfsX4|p{ZuZXHJG*nf`OfczkhZj?Ep2Iw zBw9neXQ)9aVS^AC2K%J&$ECsZ$}yVz*HO{|^EI%y6AnAzcm>Q;a89SxIr;E)_T>q} z4BZP#jCcx?0Ob~&69kB-^h%hla=m$qdH{5~-dv}o`0FFr=PoZ0W~d4GJe?2#q{C&O zv}ixRHW45?LhYi&L;#dQsdDvLC`zizOLGB=<^!<82Y_Gz(g45!2Y`WL-F4_M^5f1M za+eDTm;w9A7Cpb5A6ww#5{wJV+J?{nHbMDm4gg637#aYv`M`Pd{nIzb3WOeNmy1mx zrW>I?0|Wp>XAE(vbx-lnLhC`0XiZ}T!3UrO02Bon8l~3I$Mc02gP=IO7$oJ&3}7Yz z^P(mg;u7oL56%@@Xby_EkBJEa0FATlJF6?lq67d?ImZ~WZQt2K>&(H8*BmNO-17ky zRtP!%OgAusaz<1@vhI8TOr$_`G;9uW`2=JRps-aR7j{#|UCQ@a`Nr zw!0^n90#AptnXjv!?CSbsPjGF)liE@+h=5Qa@_KCUUM~8cgSB*KG$aVn;beuau z;RA#Tr~vqkxvI>8$+x+iM!^jBgL0XQ!ae}L((E3FU~cLWMyUosh%Y>1DLvcGD#*H3 z6t~0z8X^|U@^tFu{kpJmKNa?#9`*;4Z*cL`WyhMh#y;}WtyA^QA;CNWcZE|_ z1;7~qr3+O*AbbE;nkYViB5GIXUM8qXl;838AA^TqUT4=ZAwqKq1ws{oS`e@!Y$6Ky zsW4_mP#DAjBy0|HR~XALkEQK9KfN#rH=eE*I;cG6%8AFOqfAe$naj?PRgQ;WS!b)} zAbuxNCamB;-09Xbfyqx#thJMNj3jBFz~2n{iBPN4W*>kg^8^fluKe)k1Lv6;6jEMx z7w7|0XugyWz>W{XAWNHbpS{}asOF%#2{S?X0WTGWSfZ*i; zEf*?hjmbp^SAH*`%GM9hvBszoE)}Mz9HvrfteOUU@%u^swsO6+Iq~SAz8Fye_&t0o zj1>v)6yY$q^R<(`hhFb%eNzR$7*V4GV?absu$l_X3<+$iMClj8F+qMYBCUP>+|8wJ z`_7K)ex86CK9Mdj6L2xcnejegwuajC6PU8@z1t;lMtYPmC`IjL_zQTVhEGC^i5evoOXO z0BA{7G|l(S@@t}Jg6Ie6Zg5z-|GhIKa6Y}K16 zB*OtP_?U+CTVanKn>naV002xlzqgqvfV_S`On30FE+7DkFh*6X^JBJF1jX5Yi(kX$ zgOM`P)_MyP)tGvi(uDVB=;|NmHca%G;aaY{m3xh2TC?=+)!F|jaL`2C$Bth2R&g&i7X}$kpyR!OFrZNf zJ^+V8Db0=)gjNYplEy-A-xIc8xS~MdKp4EKk_kwF7(3YiFiZdd%Szp^2oM4~O*K_y kTiVi=wzMTe`Cotm0FRy#x^sZB+yDRo07*qoM6N<$g8R^R(EtDd diff --git a/emailMarketing/static/emailMarketing/paper-plane.png b/emailMarketing/static/emailMarketing/paper-plane.png deleted file mode 100644 index 79998aa8d4b1ba6629441766235a55a4add4c947..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2226 zcmV;j2u=5iP)jV922@snhoAV_?>GpGngTA~IuW?dBIgWA>j787OrprWXqMq^@t76~FKY>OIW z5GWti7|>lq%*V}fbO}r<0a@Vc9>-jzB{LgvL9b>M!=9+7+ zx#pT{t`#(;8aMsqW40F;KUM-OwDduis%HqED&5yFt{T=LpalPq5Ter8KCBo(^kJ#h zcIcY;3GLdAC-&a3{_nwApQQpdO9zn>6b);z z%7H*pvKRM?RuUD{KR-#IB?f2JJOEfjnmdB*E#q%TazeZkqkEMMp?UKuUn8X&=fQEp z`8)ua!r8uR3FD?Q%$7u{zc6Xb=`a8cdW!&Pu)a>#R2Xy8XN^=g?ew5j=yrM^@0Qd4 z@wg8h4>WPkiUIIoeG|G|sa85Tb>1h!J)CFvoi|WhhkXe9FykyjjynZ_D$(y4aFqaP zV)x~~K*#EY^UhOX>#zpr*LXQoFLeeW1pq#Jt#F>KDFj$N=XJLTe`mYDZCJ(bL-?46 z+-D8}Bmp3WYrtbT7sYBquuEc6?7rjBWZQ?kn87+2SA6880FVNJ1f18feECuUPKR*q z^B9@eHIIZAN`;b~|LWfWg+4R5A6F&;NC3cxa6VQT0FO4F=P6u$wkB+QY+7w0z}x;-(C$hAIsqUc0Z0xkpOR_0@jbrBn<#l>wOTE&%u>=TrE=J?yro5vRvO04AI@ zsKXqT0Q3XEh%*N?0I9%)D6)b27_WVo_Oox(eWLDX0hpzWx3C4y0PKF?vR;}O*sU;i zgo>yJBb*ST=&}ArjU(ZEgB*^)`EX9?)9#1ZtrIRdH;e}0=H9xA*I2NN2ls} zAjqOvG6ZD|0cd8wZ?GKUQPwuD;J~CnAJDsNi3OnbgX3lzNQa@&_7zj}mOat~I~B?i#quHS;@QKF={U1@ zA34uE0YEPUK*$1c%%_Yjhl%YgrWKq);OBlNBZ?Oz5`M&vlk=Pfvw6=%%}40BHjdh< z6$4;7#o}@`D3k_LI#~0AGDWep61kfPNvJLWK%q}%uLuCL26Au|0DzZpwYL>5w9v{p&ZJFrN+;0?_u}F%^zP zq;i<}^6}}3B2kbnjYjzoa-2cntThJ!maz#}y#xTkLI6Z=XR3jdG!UXVn^~tu`u%}& zAvjA1qC8Rq=t{yk#J)ZgXWNIhN&wul?r7Ma4VnT609&7!UVI*DiXs!`kteGQ;U=XM zPKU4(Ih`Oa9E@0;eK9;VY0K%@l+CC7r2v@w?ok!rk9e|a&PiDLN>O79p4 z&Z(y3X|q32b0$LeR9K&o=1Bn1Mgu^0>pMqiI4UN@T!$F8Yl63*Wwx1VGrg>qvG!|EJBJ__4ptlbP<>N1U z{(kRyqckx?8rcvSuQLc_dlom-SUe#OMVHw>yX~z*)C)u;0Q8<9UJu>#+8^;nujpE_ z`v+J4BI`ja_Qs8R-9a#0WP@+=WK%+u&IVz8a8&|e*Lk{X_*wk^7k?jG{K9XsC6NRE z!u^3rr8KYjUw*1KV}wH#3HE7Foxk?5M*lv8*B#lRDA#fAfkVs8^vD9C$uT{c2|oDj zeqAvPyoV)F2K9_6A`6ss<)S#YlchX{PB7je<^tF?dT>I!7_=h*jm!)`wUfihzbbrTx9PW+mzt0{Q?FK)VX06!UD5nF=T3PtR!2_j^m8 zKS1E_rf5)dL0Bok1ONg#bKTps^ty#09*hi04@Ow#fP~I4zVfP3Km0?hf29G72B$kR zCq@|n(rMk=*)!`_6J%|WQ$WlDdp54Rs_5=P1E_nXkz4Qb&Yx5OpvYK8?Yd%)-+6Q_ zK4aA*;f|g^O~se0fWC3n)fMgDBP;f)hyZ=8K4aA(klp>p!6A>O_>{x#8=kp3!Rp8^ z$^%)XLJLwLNHf5C1K^r#uDRx#Yp%IAn*Arh0CI>x*Sney$N&HU07*qoM6N<$f~>6$ Ag8%>k diff --git a/emailMarketing/static/emailMarketing/post-office.png b/emailMarketing/static/emailMarketing/post-office.png deleted file mode 100644 index 7419b2b29a3cb2be16afcda16e8238900471bf83..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1548 zcmV+n2J`ueP)KD?R1`j$nccOlU|6V> zwb0Z@@kh4clR{^p(8`KTQ505TGJ>EM-1;i0>mN{dTd_V)7TTh=&$%CjnVY+rnRF((X(0zLNzTn=bH4kX@0^T?j!`~<68fg_N7M8ieUo@S!A1eEOX#P^B3>7}1T^m) z*b(n>Lp}$u?JLJ-ouoi0#RR~@7#d#i8~_?1zb2%Q&p%G#VNw8~Nc>SCz=j_z4OC-3 zt&fiZQo~2iB?Uqu)?EZ@e((@;^?T>j1iZj^2>?(A@YSa*;lUQWzONwo3U1HG8KJpg zzIbxeRLcZ01qj$UHu|qV_i4EDJplaG2LzzE`XzCt#bB9FBR!vgAE_Y#hCfGiwIGC3 zU=^M#HiNlq+a~%PI5gFfe|8tgEjE&{`BR$*5dgynB{^Y?mkuJ`UE!}^+ooF4!nqcF zcSlcK#XYtNX`z@2LcWWCoKn(fiLRuS38?|->xRq2LzM{?_3iZU})@~jM5>{1xrC6;L%+pApjr%AR}R1 zSKJ4%ve^#+(mwxz(NxQX%H$GOf({04q}vBT&oB6ZgaDwvl_a&M6#LGuiy(mE?*#x; zz=hN`Kwt~>!0KpjvVt2AgsXR|2*5?<8o*cqJ9S|(i@n2QmKqS@GZ#MkHH*DN0Dz4E zY$(!g^zzb=z)+$$0(-2 zlpsn^o_8!fw2%R4fS_XRtPTMHB!j&J0h}&9 zY%OCA@aMYBhkJ*&-np)J?;rtS<8ec!1_%Jupw~95-8+a3K-#aazximO%tub&FzNpX zuN<2lx_3Bx#(QyV)XmSIf4u_T|7H*X7iMNI)XHa|JmB{B>u7m1BmzuTxWT#58Kw^3mM}8 zFrqCc&IB%k+%N@J=0Dx+VBU7VS=>8}1;&6mx>`St8LXbVFbY+m_QGdd<)9Dfdh=%W z#2?i`eZbE*&+U6^_lNwEj`Y%$AzveP?RyOX8P<@V<=iVEu|SZ@pfyS#aC`glkZ9rf zglUA%Mgs$axIi$GU6!;#d~V`>!ur^KfVBU|U}=S^u$ALRfjAx$fHet#OA*gYjtN)a z-m`S%^cKy1W7IxidgAwevZ5mpZ49WwR*q}NGWwpux%vs2DewkH=2O9)og~b5H4<@EiMR*BpN{nf7#yoH!^jYCLQk;rq)Q1xa zK!wfQ5>RWS0#V2i04rjJE4)v@ - - -
-
-

{% 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 9e7e58b4d..000000000 --- a/examplePlugin/meta.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - examplePlugin - Utility - This is an example plugin - 1.0.1 - 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/install/install.py b/install/install.py index 51d98ef71..53c5ab69e 100644 --- a/install/install.py +++ b/install/install.py @@ -3235,7 +3235,7 @@ password="%s" apps_with_migrations = [ 'loginSystem', 'packages', 'websiteFunctions', 'baseTemplate', 'userManagment', 'dns', 'databases', 'ftp', 'filemanager', 'mailServer', 'emailPremium', - 'emailMarketing', 'cloudAPI', 'containerization', 'IncBackups', 'CLManager', + 'cloudAPI', 'containerization', 'IncBackups', 'CLManager', 's3Backups', 'dockerManager', 'aiScanner', 'firewall', 'tuning', 'serverStatus', 'serverLogs', 'backup', 'managePHP', 'manageSSL', 'api', 'manageServices', 'pluginHolder', 'highAvailability', 'WebTerminal' diff --git a/paypalPremiumPlugin/README.md b/paypalPremiumPlugin/README.md deleted file mode 100644 index 922bffe2e..000000000 --- a/paypalPremiumPlugin/README.md +++ /dev/null @@ -1,58 +0,0 @@ -# Premium Plugin Example - -An example paid plugin for CyberPanel that demonstrates how to implement Patreon subscription-based plugin access. - -## Features - -- Requires Patreon subscription to "CyberPanel Paid Plugin" tier -- Users can install the plugin without subscription -- Plugin functionality is locked until subscription is verified -- Shows subscription required page when accessed without subscription - -## Installation - -1. Upload the plugin ZIP file to CyberPanel -2. Install the plugin from the plugin manager -3. The plugin will appear in the installed plugins list - -## Usage - -### For Users Without Subscription - -- Plugin can be installed -- When accessing the plugin, a subscription required page is shown -- Link to Patreon subscription page is provided - -### For Users With Subscription - -- Plugin works normally -- All features are accessible -- Settings page is available - -## Configuration - -The plugin checks for Patreon membership via the Patreon API. Make sure to configure: - -1. Patreon Client ID -2. Patreon Client Secret -3. Patreon Creator ID - -These should be set in CyberPanel environment variables or settings. - -## Meta.xml Structure - -The plugin uses the following meta.xml structure for paid plugins: - -```xml -true -CyberPanel Paid Plugin -https://www.patreon.com/c/newstargeted/membership -``` - -## Author - -master3395 - -## License - -MIT diff --git a/paypalPremiumPlugin/__init__.py b/paypalPremiumPlugin/__init__.py deleted file mode 100644 index f80c90067..000000000 --- a/paypalPremiumPlugin/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# PayPal Premium Plugin Example -# This is a paid plugin that requires PayPal payment - -default_app_config = 'paypalPremiumPlugin.apps.PaypalpremiumpluginConfig' diff --git a/paypalPremiumPlugin/api_encryption.py b/paypalPremiumPlugin/api_encryption.py deleted file mode 100644 index 8f2820ab2..000000000 --- a/paypalPremiumPlugin/api_encryption.py +++ /dev/null @@ -1,80 +0,0 @@ -# -*- coding: utf-8 -*- -""" -AES-256-CBC encryption for plugin <-> api.newstargeted.com communication. -Key must match PLUGIN_VERIFICATION_CIPHER_KEY in config.php on the API server. -""" -import json -import base64 -import os - -CIPHER_KEY_B64 = '1VLPEKTmLGUbIxHUFEtsuVM2MPN1tl8HPFtyJc4dr58=' -ENCRYPTION_ENABLED = True - -_ENCRYPTION_CIPHER_KEY = None - - -def _get_key(): - global _ENCRYPTION_CIPHER_KEY - if _ENCRYPTION_CIPHER_KEY is not None: - return _ENCRYPTION_CIPHER_KEY - try: - key = base64.b64decode(CIPHER_KEY_B64) - if len(key) == 32: - _ENCRYPTION_CIPHER_KEY = key - return key - except Exception: - pass - return None - - -def encrypt_payload(data): - if not ENCRYPTION_ENABLED or not _get_key(): - body = json.dumps(data, separators=(',', ':')).encode('utf-8') - return body, {'Content-Type': 'application/json'} - try: - from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes - from cryptography.hazmat.primitives import padding - from cryptography.hazmat.backends import default_backend - key = _get_key() - plain = json.dumps(data, separators=(',', ':')).encode('utf-8') - padder = padding.PKCS7(128).padder() - padded = padder.update(plain) + padder.finalize() - iv = os.urandom(16) - cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend()) - encryptor = cipher.encryptor() - ciphertext = encryptor.update(padded) + encryptor.finalize() - payload = base64.b64encode(iv).decode('ascii') + '.' + base64.b64encode(ciphertext).decode('ascii') - return payload.encode('utf-8'), {'Content-Type': 'text/plain', 'X-Encrypted': '1'} - except Exception: - body = json.dumps(data, separators=(',', ':')).encode('utf-8') - return body, {'Content-Type': 'application/json'} - - -def decrypt_response(body_bytes, content_type='', expect_encrypted=False): - try: - body_str = body_bytes.decode('utf-8') if isinstance(body_bytes, bytes) else str(body_bytes) - is_encrypted = ( - expect_encrypted or - ('text/plain' in content_type and '.' in body_str) or - ('.' in body_str and body_str.strip() and body_str.strip()[0] not in '{[') - ) - parts = body_str.strip().split('.', 1) - if is_encrypted and len(parts) == 2 and ENCRYPTION_ENABLED and _get_key(): - from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes - from cryptography.hazmat.primitives import padding - from cryptography.hazmat.backends import default_backend - iv = base64.b64decode(parts[0]) - ciphertext = base64.b64decode(parts[1]) - key = _get_key() - cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend()) - decryptor = cipher.decryptor() - padded = decryptor.update(ciphertext) + decryptor.finalize() - unpadder = padding.PKCS7(128).unpadder() - plain = unpadder.update(padded) + unpadder.finalize() - return json.loads(plain.decode('utf-8')) - return json.loads(body_str) - except Exception: - try: - return json.loads(body_bytes.decode('utf-8') if isinstance(body_bytes, bytes) else body_bytes) - except Exception: - return {} diff --git a/paypalPremiumPlugin/apps.py b/paypalPremiumPlugin/apps.py deleted file mode 100644 index 0f2389e23..000000000 --- a/paypalPremiumPlugin/apps.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.apps import AppConfig - -class PaypalpremiumpluginConfig(AppConfig): - name = 'paypalPremiumPlugin' - verbose_name = 'PayPal Premium Plugin Example' diff --git a/paypalPremiumPlugin/meta.xml b/paypalPremiumPlugin/meta.xml deleted file mode 100644 index 6535e1409..000000000 --- a/paypalPremiumPlugin/meta.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - PayPal Premium Plugin Example - Utility - 1.0.2 - An example paid plugin that requires PayPal payment. Users can install it but cannot run it without payment. Supports PayPal.me links and PayPal Payment Links/Buttons. - master3395 - https://github.com/master3395/cyberpanel-plugins - MIT - - 3.6+ - 2.2+ - 2.5.5+ - - - 2.5.5 - 3.0.0 - - - true - false - - true - CyberPanel Paid Plugin - https://www.patreon.com/membership/27789984 - https://paypal.me/KimBS?locale.x=en_US&country.x=NO - - /plugins/paypalPremiumPlugin/ - /plugins/paypalPremiumPlugin/settings/ - diff --git a/paypalPremiumPlugin/migrations/0001_initial.py b/paypalPremiumPlugin/migrations/0001_initial.py deleted file mode 100644 index 97b0f3fa8..000000000 --- a/paypalPremiumPlugin/migrations/0001_initial.py +++ /dev/null @@ -1,27 +0,0 @@ -# Generated migration for PaypalPremiumPluginConfig - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [] - - operations = [ - migrations.CreateModel( - name='PaypalPremiumPluginConfig', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('payment_method', models.CharField(choices=[('patreon', 'Patreon Subscription'), ('paypal', 'PayPal Payment'), ('both', 'Check Both (Patreon or PayPal)')], default='both', help_text='Choose which payment method to use for verification.', max_length=10)), - ('activation_key', models.CharField(blank=True, default='', help_text='Validated activation key - grants access without re-entering.', max_length=64)), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ], - options={ - 'verbose_name': 'PayPal Premium Plugin Configuration', - 'verbose_name_plural': 'PayPal Premium Plugin Configurations', - }, - ), - ] diff --git a/paypalPremiumPlugin/migrations/__init__.py b/paypalPremiumPlugin/migrations/__init__.py deleted file mode 100644 index 2a0ee1c3e..000000000 --- a/paypalPremiumPlugin/migrations/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# PayPal Premium Plugin migrations diff --git a/paypalPremiumPlugin/models.py b/paypalPremiumPlugin/models.py deleted file mode 100644 index b56e0c6e4..000000000 --- a/paypalPremiumPlugin/models.py +++ /dev/null @@ -1,40 +0,0 @@ -# -*- coding: utf-8 -*- -from django.db import models - - -class PaypalPremiumPluginConfig(models.Model): - PAYMENT_METHOD_CHOICES = [ - ('patreon', 'Patreon Subscription'), - ('paypal', 'PayPal Payment'), - ('both', 'Check Both (Patreon or PayPal)'), - ] - payment_method = models.CharField( - max_length=10, - choices=PAYMENT_METHOD_CHOICES, - default='both', - help_text="Choose which payment method to use for verification." - ) - activation_key = models.CharField( - max_length=64, - blank=True, - default='', - help_text="Validated activation key - grants access without re-entering." - ) - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - - class Meta: - verbose_name = "PayPal Premium Plugin Configuration" - verbose_name_plural = "PayPal Premium Plugin Configurations" - - def __str__(self): - return "PayPal Premium Plugin Configuration" - - @classmethod - def get_config(cls): - config, _ = cls.objects.get_or_create(pk=1) - return config - - def save(self, *args, **kwargs): - self.pk = 1 - super().save(*args, **kwargs) diff --git a/paypalPremiumPlugin/templates/paypalPremiumPlugin/index.html b/paypalPremiumPlugin/templates/paypalPremiumPlugin/index.html deleted file mode 100644 index ac5f39dbc..000000000 --- a/paypalPremiumPlugin/templates/paypalPremiumPlugin/index.html +++ /dev/null @@ -1,96 +0,0 @@ -{% extends "baseTemplate/index.html" %} -{% load i18n %} - -{% block title %}{% trans "PayPal Premium Plugin Example - CyberPanel" %}{% endblock %} - -{% block header_scripts %} - -{% endblock %} - -{% block content %} -
-
- {% trans "Premium Plugin" %} -

{{ plugin_name }}

-

{{ description }}

-

{% trans "Version:" %} {{ version }}

-
- -
-

{% trans "Premium Features" %}

-
    - {% for feature in features %} -
  • {{ feature }}
  • - {% endfor %} -
-
-
-{% endblock %} diff --git a/paypalPremiumPlugin/templates/paypalPremiumPlugin/settings.html b/paypalPremiumPlugin/templates/paypalPremiumPlugin/settings.html deleted file mode 100644 index f7c9c39c6..000000000 --- a/paypalPremiumPlugin/templates/paypalPremiumPlugin/settings.html +++ /dev/null @@ -1,331 +0,0 @@ -{% extends "baseTemplate/index.html" %} -{% load i18n %} - -{% block title %}{% trans "PayPal Premium Plugin Settings - CyberPanel" %}{% endblock %} - -{% block header_scripts %} - -{% endblock %} - -{% block content %} -
-
- {% trans "Premium Plugin" %} -

{% trans "PayPal Premium Plugin Settings" %}

- - {% if show_payment_ui %} - -
-

{% trans "Activate Premium Access" %}

-

{% trans "If you have an activation key, enter it below." %}

- -
- {% csrf_token %} - - -
-
- -
- -
- {% csrf_token %} - -
-
- {% else %} -
- {% trans "Premium Access Active" %} — {% trans "Access granted via Plugin Grants or activation key." %} -
- {% endif %} - - -
- - {% trans "Plugin Information" %} -
    -
  • {% trans "Name" %}: {{ plugin_name|default:"PayPal Premium Plugin Example" }}
  • -
  • {% trans "Version" %}: {{ version|default:"1.0.0" }}
  • -
  • - {% trans "Status" %}: - - {{ plugin_status|default:status|default:"Active" }} - -
  • -
-
- -

{{ description }}

- -
- {% csrf_token %} -
- - -
- -
- - -
- -
- - -
- -
- -
- - -
- - {% if show_payment_ui %} - - {% endif %} -
-
-{% endblock %} diff --git a/paypalPremiumPlugin/templates/paypalPremiumPlugin/subscription_required.html b/paypalPremiumPlugin/templates/paypalPremiumPlugin/subscription_required.html deleted file mode 100644 index cf67af087..000000000 --- a/paypalPremiumPlugin/templates/paypalPremiumPlugin/subscription_required.html +++ /dev/null @@ -1,139 +0,0 @@ -{% extends "baseTemplate/index.html" %} -{% load i18n %} - -{% block title %}{% trans "Payment Required - PayPal Premium Plugin" %}{% endblock %} - -{% block header_scripts %} - -{% endblock %} - -{% block content %} -
-
-
-

{% trans "Premium Plugin Access Required" %}

-

{% trans "This plugin requires payment or subscription to access premium features." %}

- - -
-

{% trans "Activate Premium Access" %}

-

{% trans "If you received an activation key, enter it below." %}

- -
- {% csrf_token %} -
- - -
- -
-
- - {% if payment_method == 'both' %} -
- {% trans "Current Payment Method:" %} {% trans "Check Both (Patreon or PayPal)" %} -
- {% elif payment_method == 'patreon' %} -
- {% trans "Current Payment Method:" %} {% trans "Patreon Subscription Only" %} -
- {% elif payment_method == 'paypal' %} -
- {% trans "Current Payment Method:" %} {% trans "PayPal Payment Only" %} -
- {% endif %} - -
-
-

{% trans "Patreon Subscription" %}

-

{% trans "Subscribe to" %} "{{ patreon_tier }}"

- - {% trans "Subscribe on Patreon" %} - -
-
-

{% trans "PayPal Payment" %}

-

{% trans "Complete one-time payment via PayPal" %}

- {% if paypal_me_url %} - - {% trans "Pay with PayPal.me" %} - - {% endif %} -
-
- -
-

{% trans "How it works:" %}

-
    -
  • {% trans "Install the plugin (already done)" %}
  • -
  • {% trans "Enter activation key, subscribe on Patreon, or pay via PayPal" %}
  • -
  • {% trans "The plugin will automatically unlock" %}
  • -
-
- - {% if error %} -
- {% trans "Error:" %} {{ error }} -
- {% endif %} -
-
- - -{% endblock %} diff --git a/paypalPremiumPlugin/urls.py b/paypalPremiumPlugin/urls.py deleted file mode 100644 index 997fe20f5..000000000 --- a/paypalPremiumPlugin/urls.py +++ /dev/null @@ -1,12 +0,0 @@ -from django.urls import path, re_path -from . import views - -app_name = 'paypalPremiumPlugin' - -urlpatterns = [ - path('', views.main_view, name='main'), - path('settings/', views.settings_view, name='settings'), - re_path(r'^activate-key/$', views.activate_key, name='activate_key'), - path('save-payment-method/', views.save_payment_method, name='save_payment_method'), - path('api/status/', views.api_status_view, name='api_status'), -] diff --git a/paypalPremiumPlugin/views.py b/paypalPremiumPlugin/views.py deleted file mode 100644 index c1f96f129..000000000 --- a/paypalPremiumPlugin/views.py +++ /dev/null @@ -1,387 +0,0 @@ -# -*- coding: utf-8 -*- -""" -PayPal Premium Plugin Views - Unified Verification (same as contaboAutoSnapshot) -Supports: Plugin Grants, Activation Key, Patreon, PayPal, AES encryption -""" - -from django.shortcuts import render, redirect -from django.http import JsonResponse, HttpResponse -from django.views.decorators.http import require_http_methods -from plogical.mailUtilities import mailUtilities -from plogical.httpProc import httpProc -from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter as logging -from functools import wraps -import urllib.request -import urllib.error -import json -import time - -from .models import PaypalPremiumPluginConfig -from . import api_encryption - -PLUGIN_NAME = 'paypalPremiumPlugin' -PLUGIN_VERSION = '1.0.2' - -REMOTE_VERIFICATION_PATREON_URL = 'https://api.newstargeted.com/api/verify-patreon-membership.php' -REMOTE_VERIFICATION_PAYPAL_URL = 'https://api.newstargeted.com/api/verify-paypal-payment.php' -REMOTE_VERIFICATION_PLUGIN_GRANT_URL = 'https://api.newstargeted.com/api/verify-plugin-grant.php' -REMOTE_ACTIVATION_KEY_URL = 'https://api.newstargeted.com/api/activate-plugin-key.php' - -PATREON_TIER = 'CyberPanel Paid Plugin' -PATREON_URL = 'https://www.patreon.com/membership/27789984' -PAYPAL_ME_URL = 'https://paypal.me/KimBS?locale.x=en_US&country.x=NO' -PAYPAL_PAYMENT_LINK = '' - - -def cyberpanel_login_required(view_func): - @wraps(view_func) - def _wrapped_view(request, *args, **kwargs): - try: - if not request.session.get('userID'): - from loginSystem.views import loadLoginPage - return redirect(loadLoginPage) - return view_func(request, *args, **kwargs) - except KeyError: - from loginSystem.views import loadLoginPage - return redirect(loadLoginPage) - return _wrapped_view - - -def _api_request(url, data, timeout=10): - try: - body, extra_headers = api_encryption.encrypt_payload(data) - headers = { - 'User-Agent': f'CyberPanel-Plugin/{PLUGIN_VERSION}', - 'X-Plugin-Name': PLUGIN_NAME - } - headers.update(extra_headers) - req = urllib.request.Request(url, data=body, headers=headers) - with urllib.request.urlopen(req, timeout=timeout) as response: - raw = response.read() - ct = response.headers.get('Content-Type', '') - expect_enc = extra_headers.get('X-Encrypted') == '1' - return api_encryption.decrypt_response(raw, ct, expect_encrypted=expect_enc) - except Exception as e: - logging.writeToFile(f"PayPal Premium Plugin: API request error to {url}: {str(e)}") - return {} - - -def check_plugin_grant(user_email, user_ip='', domain=''): - try: - request_data = { - 'user_email': user_email or '', - 'plugin_name': PLUGIN_NAME, - 'user_ip': user_ip, - 'domain': domain, - } - data = _api_request(REMOTE_VERIFICATION_PLUGIN_GRANT_URL, request_data) - if data.get('success') and data.get('has_access'): - return {'has_access': True, 'message': data.get('message', 'Access granted via Plugin Grants')} - return {'has_access': False, 'message': data.get('message', '')} - except Exception as e: - logging.writeToFile(f"PayPal Premium Plugin: Plugin grant check error: {str(e)}") - return {'has_access': False, 'message': ''} - - -def check_patreon_membership(user_email, user_ip='', domain=''): - try: - request_data = { - 'user_email': user_email, - 'plugin_name': PLUGIN_NAME, - 'plugin_version': PLUGIN_VERSION, - 'user_ip': user_ip, - 'domain': domain, - 'tier_id': '27789984' - } - response_data = _api_request(REMOTE_VERIFICATION_PATREON_URL, request_data) - if response_data.get('success', False): - return { - 'has_access': response_data.get('has_access', False), - 'patreon_tier': response_data.get('patreon_tier', PATREON_TIER), - 'patreon_url': response_data.get('patreon_url', PATREON_URL), - 'message': response_data.get('message', 'Access granted'), - 'error': None - } - return { - 'has_access': False, - 'patreon_tier': PATREON_TIER, - 'patreon_url': PATREON_URL, - 'message': response_data.get('message', 'Patreon subscription required'), - 'error': response_data.get('error') - } - except Exception as e: - logging.writeToFile(f"PayPal Premium Plugin: Patreon check error: {str(e)}") - return {'has_access': False, 'patreon_tier': PATREON_TIER, 'patreon_url': PATREON_URL, 'message': 'Unable to verify Patreon.', 'error': str(e)} - - -def check_paypal_payment(user_email, user_ip='', domain=''): - try: - request_data = { - 'user_email': user_email, - 'plugin_name': PLUGIN_NAME, - 'plugin_version': PLUGIN_VERSION, - 'user_ip': user_ip, - 'domain': domain, - 'timestamp': int(time.time()), - } - response_data = _api_request(REMOTE_VERIFICATION_PAYPAL_URL, request_data) - if response_data.get('success', False): - return { - 'has_access': response_data.get('has_access', False), - 'paypal_me_url': response_data.get('paypal_me_url', PAYPAL_ME_URL), - 'paypal_payment_link': response_data.get('paypal_payment_link', PAYPAL_PAYMENT_LINK), - 'message': response_data.get('message', 'Access granted'), - 'error': None - } - return { - 'has_access': False, - 'paypal_me_url': PAYPAL_ME_URL, - 'paypal_payment_link': PAYPAL_PAYMENT_LINK, - 'message': response_data.get('message', 'PayPal payment required'), - 'error': response_data.get('error') - } - except Exception as e: - logging.writeToFile(f"PayPal Premium Plugin: PayPal check error: {str(e)}") - return {'has_access': False, 'paypal_me_url': PAYPAL_ME_URL, 'paypal_payment_link': PAYPAL_PAYMENT_LINK, 'message': 'Unable to verify PayPal.', 'error': str(e)} - - -def unified_verification_required(view_func): - @wraps(view_func) - def _wrapped_view(request, *args, **kwargs): - try: - if not request.session.get('userID'): - from loginSystem.views import loadLoginPage - return redirect(loadLoginPage) - - user_email = request.session.get('email', '') or (getattr(request.user, 'email', '') if hasattr(request, 'user') and request.user else '') or getattr(request.user, 'username', '') - - try: - config = PaypalPremiumPluginConfig.get_config() - payment_method = config.payment_method - except Exception: - payment_method = 'both' - - has_access = False - verification_result = {} - - activation_key = request.GET.get('activation_key') or request.POST.get('activation_key') - if not activation_key: - try: - config = PaypalPremiumPluginConfig.get_config() - activation_key = getattr(config, 'activation_key', '') or '' - except Exception: - activation_key = '' - - if activation_key: - try: - request_data = {'activation_key': activation_key.strip(), 'plugin_name': PLUGIN_NAME, 'user_email': user_email} - response_data = _api_request(REMOTE_ACTIVATION_KEY_URL, request_data) - if response_data.get('success', False) and response_data.get('has_access', False): - has_access = True - verification_result = {'method': 'activation_key', 'has_access': True, 'message': response_data.get('message', 'Access activated via key')} - try: - config = PaypalPremiumPluginConfig.get_config() - config.activation_key = activation_key.strip() - config.save(update_fields=['activation_key', 'updated_at']) - except Exception as e: - logging.writeToFile(f"PayPal Premium Plugin: Could not persist activation key: {str(e)}") - elif not response_data.get('success') and activation_key: - try: - config = PaypalPremiumPluginConfig.get_config() - if getattr(config, 'activation_key', '') == activation_key.strip(): - config.activation_key = '' - config.save(update_fields=['activation_key', 'updated_at']) - except Exception: - pass - except Exception as e: - logging.writeToFile(f"PayPal Premium Plugin: Activation key check error: {str(e)}") - - if not has_access: - grant_result = check_plugin_grant(user_email, request.META.get('REMOTE_ADDR', ''), request.get_host()) - if grant_result.get('has_access'): - has_access = True - verification_result = {'method': 'plugin_grant', 'has_access': True, 'message': grant_result.get('message', 'Access granted via Plugin Grants')} - - if not has_access: - try: - if payment_method == 'patreon': - result = check_patreon_membership(user_email, request.META.get('REMOTE_ADDR', ''), request.get_host()) - has_access = result.get('has_access', False) - verification_result = { - 'method': 'patreon', 'has_access': has_access, - 'patreon_tier': result.get('patreon_tier', PATREON_TIER), - 'patreon_url': result.get('patreon_url', PATREON_URL), - 'paypal_me_url': PAYPAL_ME_URL, 'paypal_payment_link': PAYPAL_PAYMENT_LINK, - 'message': result.get('message', 'Patreon subscription required'), - 'error': result.get('error') - } - elif payment_method == 'paypal': - result = check_paypal_payment(user_email, request.META.get('REMOTE_ADDR', ''), request.get_host()) - has_access = result.get('has_access', False) - verification_result = { - 'method': 'paypal', 'has_access': has_access, - 'patreon_tier': PATREON_TIER, 'patreon_url': PATREON_URL, - 'paypal_me_url': result.get('paypal_me_url', PAYPAL_ME_URL), - 'paypal_payment_link': result.get('paypal_payment_link', PAYPAL_PAYMENT_LINK), - 'message': result.get('message', 'PayPal payment required'), - 'error': result.get('error') - } - else: - patreon_result = check_patreon_membership(user_email, request.META.get('REMOTE_ADDR', ''), request.get_host()) - paypal_result = check_paypal_payment(user_email, request.META.get('REMOTE_ADDR', ''), request.get_host()) - has_access = patreon_result.get('has_access', False) or paypal_result.get('has_access', False) - verification_result = { - 'method': 'both', 'has_access': has_access, - 'patreon_tier': patreon_result.get('patreon_tier', PATREON_TIER), - 'patreon_url': patreon_result.get('patreon_url', PATREON_URL), - 'paypal_me_url': paypal_result.get('paypal_me_url', PAYPAL_ME_URL), - 'paypal_payment_link': paypal_result.get('paypal_payment_link', PAYPAL_PAYMENT_LINK), - 'message': 'Payment or subscription required' if not has_access else 'Access granted' - } - except Exception as e: - logging.writeToFile(f"PayPal Premium Plugin: Verification error: {str(e)}") - has_access = False - verification_result = { - 'method': payment_method, 'has_access': False, - 'patreon_tier': PATREON_TIER, 'patreon_url': PATREON_URL, - 'paypal_me_url': PAYPAL_ME_URL, 'paypal_payment_link': PAYPAL_PAYMENT_LINK, - 'message': 'Unable to verify access.', 'error': str(e) - } - - if not has_access: - context = { - 'plugin_name': 'PayPal Premium Plugin Example', - 'is_paid': True, - 'payment_method': payment_method, - 'verification_result': verification_result, - 'patreon_tier': verification_result.get('patreon_tier', PATREON_TIER), - 'patreon_url': verification_result.get('patreon_url', PATREON_URL), - 'paypal_me_url': verification_result.get('paypal_me_url', PAYPAL_ME_URL), - 'paypal_payment_link': verification_result.get('paypal_payment_link', PAYPAL_PAYMENT_LINK), - 'message': verification_result.get('message', 'Payment or subscription required'), - 'error': verification_result.get('error') - } - proc = httpProc(request, 'paypalPremiumPlugin/subscription_required.html', context, 'admin') - return proc.render() - - if has_access and verification_result: - request.session['paypal_premium_access_via'] = verification_result.get('method', '') - - return view_func(request, *args, **kwargs) - except Exception as e: - logging.writeToFile(f"PayPal Premium Plugin: Decorator error: {str(e)}") - return HttpResponse(f"

Plugin Error

{str(e)}

") - return _wrapped_view - - -@cyberpanel_login_required -def main_view(request): - mailUtilities.checkHome() - return redirect('paypalPremiumPlugin:settings') - - -@cyberpanel_login_required -@unified_verification_required -def settings_view(request): - mailUtilities.checkHome() - try: - config = PaypalPremiumPluginConfig.get_config() - except Exception: - from django.core.management import call_command - try: - call_command('migrate', 'paypalPremiumPlugin', verbosity=0, interactive=False) - config = PaypalPremiumPluginConfig.get_config() - except Exception as e: - return HttpResponse(f"

Database Error

{str(e)}

") - - access_via = request.session.get('paypal_premium_access_via', '') - show_payment_ui = access_via not in ('plugin_grant', 'activation_key') - - context = { - 'plugin_name': 'PayPal Premium Plugin Example', - 'version': PLUGIN_VERSION, - 'status': 'Active', - 'config': config, - 'has_access': True, - 'show_payment_ui': show_payment_ui, - 'access_via_grant_or_key': not show_payment_ui, - 'patreon_tier': PATREON_TIER, - 'patreon_url': PATREON_URL, - 'paypal_me_url': PAYPAL_ME_URL, - 'paypal_payment_link': PAYPAL_PAYMENT_LINK, - 'description': 'Configure your PayPal premium plugin settings.', - } - proc = httpProc(request, 'paypalPremiumPlugin/settings.html', context, 'admin') - return proc.render() - - -@cyberpanel_login_required -@require_http_methods(["POST"]) -def activate_key(request): - try: - if request.content_type == 'application/json': - data = json.loads(request.body) - else: - data = request.POST - - activation_key = data.get('activation_key', '').strip() - user_email = data.get('user_email', '').strip() - if not user_email: - user_email = request.session.get('email', '') or (getattr(request.user, 'email', '') if hasattr(request, 'user') and request.user else '') - - if not activation_key: - return JsonResponse({'success': False, 'message': 'Activation key is required'}, status=400) - - request_data = {'activation_key': activation_key, 'plugin_name': PLUGIN_NAME, 'user_email': user_email} - response_data = _api_request(REMOTE_ACTIVATION_KEY_URL, request_data) - - if response_data.get('success', False) and response_data.get('has_access', False): - try: - config = PaypalPremiumPluginConfig.get_config() - config.activation_key = activation_key - config.save(update_fields=['activation_key', 'updated_at']) - except Exception as e: - logging.writeToFile(f"PayPal Premium Plugin: Could not persist activation key: {str(e)}") - - return JsonResponse({ - 'success': True, - 'has_access': True, - 'message': response_data.get('message', 'Access activated successfully') - }) - - return JsonResponse({ - 'success': False, - 'has_access': False, - 'message': response_data.get('message', 'Invalid activation key') - }) - - except Exception as e: - logging.writeToFile(f"PayPal Premium Plugin: activate_key error: {str(e)}") - return JsonResponse({'success': False, 'message': str(e)}, status=500) - - -@cyberpanel_login_required -@require_http_methods(["POST"]) -def save_payment_method(request): - try: - payment_method = request.POST.get('payment_method', 'both') - if payment_method not in ('patreon', 'paypal', 'both'): - payment_method = 'both' - config = PaypalPremiumPluginConfig.get_config() - config.payment_method = payment_method - config.save(update_fields=['payment_method', 'updated_at']) - return JsonResponse({'success': True, 'message': 'Payment method saved'}) - except Exception as e: - return JsonResponse({'success': False, 'message': str(e)}, status=500) - - -@cyberpanel_login_required -@unified_verification_required -def api_status_view(request): - return JsonResponse({ - 'plugin_name': 'PayPal Premium Plugin Example', - 'version': PLUGIN_VERSION, - 'status': 'active', - 'payment': 'verified', - 'verification_method': 'unified' - }) diff --git a/premiumPlugin/.gitignore b/premiumPlugin/.gitignore deleted file mode 100644 index 2ef72c88d..000000000 --- a/premiumPlugin/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -# Security - Never commit secrets -*.secret -*_secret* -patreon_config.py -.env.patreon -patreon_secrets.env diff --git a/premiumPlugin/README-REMOTE-VERIFICATION.md b/premiumPlugin/README-REMOTE-VERIFICATION.md deleted file mode 100644 index 09decfcd6..000000000 --- a/premiumPlugin/README-REMOTE-VERIFICATION.md +++ /dev/null @@ -1,81 +0,0 @@ -# Remote Verification Setup - -## Overview - -This version of the plugin uses **remote verification** - all Patreon API calls happen on YOUR server, not the user's server. - -## Benefits - -✅ **No secrets in plugin** - Users can see all plugin code, but no credentials -✅ **Secure** - All Patreon API credentials stay on your server -✅ **Centralized** - You control access, can revoke, update logic, etc. -✅ **Public code** - Plugin code can be open source - -## Architecture - -``` -User's Server Your Server Patreon API - | | | - |-- Verify Request ------------> | | - | |-- Check Membership --> | - | |<-- Membership Status - | - |<-- Access Granted/Denied ----- | | -``` - -## Setup - -### 1. Deploy Verification API - -Deploy the verification endpoint to your server: -- File: `/home/newstargeted.com/api.newstargeted.com/modules/patreon/verify-membership.php` -- URL: `https://api.newstargeted.com/api/verify-patreon-membership` - -### 2. Configure Your Server - -Add Patreon credentials to your server's `config.php`: - -```php -define('PATREON_CLIENT_ID', 'your_client_id'); -define('PATREON_CLIENT_SECRET', 'your_client_secret'); -define('PATREON_CREATOR_ACCESS_TOKEN', 'your_access_token'); -``` - -### 3. Update Plugin - -Replace `views.py` with `views_remote.py`: - -```bash -mv views.py views_local.py # Backup local version -mv views_remote.py views.py # Use remote version -``` - -### 4. Configure Plugin URL - -Update `REMOTE_VERIFICATION_URL` in `views.py` to point to your server: - -```python -REMOTE_VERIFICATION_URL = 'https://api.newstargeted.com/api/verify-patreon-membership' -``` - -## Security Features - -- **Rate limiting** - Prevents abuse (60 requests/hour per IP) -- **HTTPS only** - All communication encrypted -- **No secrets** - Plugin only makes API calls -- **Caching** - Reduces Patreon API calls (5 min cache) - -## Testing - -1. Install plugin on user's server -2. Try accessing plugin (should show subscription required) -3. Subscribe to Patreon tier -4. Access plugin again (should work) - -## Migration from Local Verification - -If you were using local verification: - -1. Keep `views_local.py` as backup -2. Use `views_remote.py` as `views.py` -3. Deploy verification API to your server -4. Update plugin URL in code diff --git a/premiumPlugin/README.md b/premiumPlugin/README.md deleted file mode 100644 index 922bffe2e..000000000 --- a/premiumPlugin/README.md +++ /dev/null @@ -1,58 +0,0 @@ -# Premium Plugin Example - -An example paid plugin for CyberPanel that demonstrates how to implement Patreon subscription-based plugin access. - -## Features - -- Requires Patreon subscription to "CyberPanel Paid Plugin" tier -- Users can install the plugin without subscription -- Plugin functionality is locked until subscription is verified -- Shows subscription required page when accessed without subscription - -## Installation - -1. Upload the plugin ZIP file to CyberPanel -2. Install the plugin from the plugin manager -3. The plugin will appear in the installed plugins list - -## Usage - -### For Users Without Subscription - -- Plugin can be installed -- When accessing the plugin, a subscription required page is shown -- Link to Patreon subscription page is provided - -### For Users With Subscription - -- Plugin works normally -- All features are accessible -- Settings page is available - -## Configuration - -The plugin checks for Patreon membership via the Patreon API. Make sure to configure: - -1. Patreon Client ID -2. Patreon Client Secret -3. Patreon Creator ID - -These should be set in CyberPanel environment variables or settings. - -## Meta.xml Structure - -The plugin uses the following meta.xml structure for paid plugins: - -```xml -true -CyberPanel Paid Plugin -https://www.patreon.com/c/newstargeted/membership -``` - -## Author - -master3395 - -## License - -MIT diff --git a/premiumPlugin/SECURITY.md b/premiumPlugin/SECURITY.md deleted file mode 100644 index b2df3648e..000000000 --- a/premiumPlugin/SECURITY.md +++ /dev/null @@ -1,57 +0,0 @@ -# Security Guidelines for Premium Plugin - -## ⚠️ IMPORTANT: Never Expose Secrets - -This plugin is designed to be **publicly shareable**. It contains **NO secrets** and is safe to publish. - -## What's Safe to Share - -✅ **Safe to commit:** -- Plugin code (views.py, urls.py, etc.) -- Templates (HTML files) -- meta.xml (no secrets, only tier name and URL) -- README.md -- Documentation - -❌ **Never commit:** -- Patreon Client Secret -- Patreon Access Tokens -- Patreon Refresh Tokens -- Any hardcoded credentials - -## Configuration - -All Patreon credentials are configured on the **server side** via: -- Environment variables -- Django settings (from environment) -- Secure config files (not in repository) - -## For Your Own Setup - -When setting up this plugin on your server: - -1. **Do NOT** modify plugin files with your secrets -2. **Do** configure environment variables on the server -3. **Do** use Django settings.py (with environment variable fallbacks) -4. **Do** add any secret config files to .gitignore - -## Example Secure Configuration - -```python -# In settings.py (safe to commit) -PATREON_CLIENT_ID = os.environ.get('PATREON_CLIENT_ID', '') -PATREON_CLIENT_SECRET = os.environ.get('PATREON_CLIENT_SECRET', '') - -# On server (NOT in repo) -export PATREON_CLIENT_ID="your_actual_secret" -export PATREON_CLIENT_SECRET="your_actual_secret" -``` - -## Verification - -Before publishing, verify: -- [ ] No secrets in plugin files -- [ ] No secrets in meta.xml -- [ ] No secrets in README -- [ ] All credentials use environment variables -- [ ] .gitignore excludes secret files diff --git a/premiumPlugin/__init__.py b/premiumPlugin/__init__.py deleted file mode 100644 index deac71067..000000000 --- a/premiumPlugin/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# Premium Plugin Example -# This is a paid plugin that requires Patreon subscription - -default_app_config = 'premiumPlugin.apps.PremiumPluginConfig' diff --git a/premiumPlugin/api_encryption.py b/premiumPlugin/api_encryption.py deleted file mode 100644 index 50f8b3d10..000000000 --- a/premiumPlugin/api_encryption.py +++ /dev/null @@ -1,83 +0,0 @@ -# -*- coding: utf-8 -*- -""" -AES-256-CBC encryption for plugin <-> api.newstargeted.com communication. -Key must match PLUGIN_VERIFICATION_CIPHER_KEY in config.php on the API server. -""" -import json -import base64 -import os - -CIPHER_KEY_B64 = '1VLPEKTmLGUbIxHUFEtsuVM2MPN1tl8HPFtyJc4dr58=' -ENCRYPTION_ENABLED = True - -_ENCRYPTION_CIPHER_KEY = None - - -def _get_key(): - """Get 32-byte AES key from base64.""" - global _ENCRYPTION_CIPHER_KEY - if _ENCRYPTION_CIPHER_KEY is not None: - return _ENCRYPTION_CIPHER_KEY - try: - key = base64.b64decode(CIPHER_KEY_B64) - if len(key) == 32: - _ENCRYPTION_CIPHER_KEY = key - return key - except Exception: - pass - return None - - -def encrypt_payload(data): - """Encrypt JSON payload for API request. Returns (body_bytes, headers_dict).""" - if not ENCRYPTION_ENABLED or not _get_key(): - body = json.dumps(data, separators=(',', ':')).encode('utf-8') - return body, {'Content-Type': 'application/json'} - try: - from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes - from cryptography.hazmat.primitives import padding - from cryptography.hazmat.backends import default_backend - key = _get_key() - plain = json.dumps(data, separators=(',', ':')).encode('utf-8') - padder = padding.PKCS7(128).padder() - padded = padder.update(plain) + padder.finalize() - iv = os.urandom(16) - cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend()) - encryptor = cipher.encryptor() - ciphertext = encryptor.update(padded) + encryptor.finalize() - payload = base64.b64encode(iv).decode('ascii') + '.' + base64.b64encode(ciphertext).decode('ascii') - return payload.encode('utf-8'), {'Content-Type': 'text/plain', 'X-Encrypted': '1'} - except Exception: - body = json.dumps(data, separators=(',', ':')).encode('utf-8') - return body, {'Content-Type': 'application/json'} - - -def decrypt_response(body_bytes, content_type='', expect_encrypted=False): - """Decrypt API response. Handles both encrypted and plain JSON.""" - try: - body_str = body_bytes.decode('utf-8') if isinstance(body_bytes, bytes) else str(body_bytes) - is_encrypted = ( - expect_encrypted or - ('text/plain' in content_type and '.' in body_str) or - ('.' in body_str and body_str.strip() and body_str.strip()[0] not in '{[') - ) - parts = body_str.strip().split('.', 1) - if is_encrypted and len(parts) == 2 and ENCRYPTION_ENABLED and _get_key(): - from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes - from cryptography.hazmat.primitives import padding - from cryptography.hazmat.backends import default_backend - iv = base64.b64decode(parts[0]) - ciphertext = base64.b64decode(parts[1]) - key = _get_key() - cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend()) - decryptor = cipher.decryptor() - padded = decryptor.update(ciphertext) + decryptor.finalize() - unpadder = padding.PKCS7(128).unpadder() - plain = unpadder.update(padded) + unpadder.finalize() - return json.loads(plain.decode('utf-8')) - return json.loads(body_str) - except Exception: - try: - return json.loads(body_bytes.decode('utf-8') if isinstance(body_bytes, bytes) else body_bytes) - except Exception: - return {} diff --git a/premiumPlugin/apps.py b/premiumPlugin/apps.py deleted file mode 100644 index 261eb7905..000000000 --- a/premiumPlugin/apps.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.apps import AppConfig - -class PremiumPluginConfig(AppConfig): - name = 'premiumPlugin' - verbose_name = 'Premium Plugin Example' diff --git a/premiumPlugin/meta.xml b/premiumPlugin/meta.xml deleted file mode 100644 index a986da653..000000000 --- a/premiumPlugin/meta.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - Premium Plugin Example - Utility - 1.0.2 - An example paid plugin that requires Patreon subscription to "CyberPanel Paid Plugin" tier. Users can install it but cannot run it without subscription. - master3395 - https://github.com/master3395/cyberpanel-plugins - MIT - - 3.6+ - 2.2+ - 2.5.5+ - - - 2.5.5 - 3.0.0 - - - true - false - - true - CyberPanel Paid Plugin - https://www.patreon.com/membership/27789984 - https://paypal.me/KimBS?locale.x=en_US&country.x=NO - - /plugins/premiumPlugin/ - /plugins/premiumPlugin/settings/ - diff --git a/premiumPlugin/migrations/0001_initial.py b/premiumPlugin/migrations/0001_initial.py deleted file mode 100644 index e403a7fbd..000000000 --- a/premiumPlugin/migrations/0001_initial.py +++ /dev/null @@ -1,27 +0,0 @@ -# Generated migration for PremiumPluginConfig - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [] - - operations = [ - migrations.CreateModel( - name='PremiumPluginConfig', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('payment_method', models.CharField(choices=[('patreon', 'Patreon Subscription'), ('paypal', 'PayPal Payment'), ('both', 'Check Both (Patreon or PayPal)')], default='both', help_text='Choose which payment method to use for verification.', max_length=10)), - ('activation_key', models.CharField(blank=True, default='', help_text='Validated activation key - grants access without re-entering.', max_length=64)), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ], - options={ - 'verbose_name': 'Premium Plugin Configuration', - 'verbose_name_plural': 'Premium Plugin Configurations', - }, - ), - ] diff --git a/premiumPlugin/migrations/__init__.py b/premiumPlugin/migrations/__init__.py deleted file mode 100644 index 80dc97892..000000000 --- a/premiumPlugin/migrations/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# Premium Plugin migrations diff --git a/premiumPlugin/models.py b/premiumPlugin/models.py deleted file mode 100644 index a4396a32a..000000000 --- a/premiumPlugin/models.py +++ /dev/null @@ -1,42 +0,0 @@ -# -*- coding: utf-8 -*- -from django.db import models - - -class PremiumPluginConfig(models.Model): - """Config for Premium Plugin - activation key and payment preference.""" - PAYMENT_METHOD_CHOICES = [ - ('patreon', 'Patreon Subscription'), - ('paypal', 'PayPal Payment'), - ('both', 'Check Both (Patreon or PayPal)'), - ] - payment_method = models.CharField( - max_length=10, - choices=PAYMENT_METHOD_CHOICES, - default='both', - help_text="Choose which payment method to use for verification." - ) - activation_key = models.CharField( - max_length=64, - blank=True, - default='', - help_text="Validated activation key - grants access without re-entering." - ) - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - - class Meta: - verbose_name = "Premium Plugin Configuration" - verbose_name_plural = "Premium Plugin Configurations" - - def __str__(self): - return "Premium Plugin Configuration" - - @classmethod - def get_config(cls): - """Get or create the singleton config instance.""" - config, _ = cls.objects.get_or_create(pk=1) - return config - - def save(self, *args, **kwargs): - self.pk = 1 - super().save(*args, **kwargs) diff --git a/premiumPlugin/templates/premiumPlugin/index.html b/premiumPlugin/templates/premiumPlugin/index.html deleted file mode 100644 index f7ab4e200..000000000 --- a/premiumPlugin/templates/premiumPlugin/index.html +++ /dev/null @@ -1,96 +0,0 @@ -{% extends "baseTemplate/index.html" %} -{% load i18n %} - -{% block title %}{% trans "Premium Plugin Example - CyberPanel" %}{% endblock %} - -{% block header_scripts %} - -{% endblock %} - -{% block content %} -
-
- {% trans "Premium Plugin" %} -

{{ plugin_name }}

-

{{ description }}

-

{% trans "Version:" %} {{ version }}

-
- -
-

{% trans "Premium Features" %}

-
    - {% for feature in features %} -
  • {{ feature }}
  • - {% endfor %} -
-
-
-{% endblock %} diff --git a/premiumPlugin/templates/premiumPlugin/settings.html b/premiumPlugin/templates/premiumPlugin/settings.html deleted file mode 100644 index 59a83d132..000000000 --- a/premiumPlugin/templates/premiumPlugin/settings.html +++ /dev/null @@ -1,315 +0,0 @@ -{% extends "baseTemplate/index.html" %} -{% load i18n %} - -{% block title %}{% trans "Premium Plugin Settings - CyberPanel" %}{% endblock %} - -{% block header_scripts %} - -{% endblock %} - -{% block content %} -
-
- {% trans "Premium Plugin" %} -

{% trans "Premium Plugin Settings" %}

- - {% if show_payment_ui %} - -
-

{% trans "Activate Premium Access" %}

-

{% trans "If you have an activation key, enter it below." %}

- -
- {% csrf_token %} - - -
-
- -
- -
- {% csrf_token %} - -
-
- {% else %} -
- {% trans "Premium Access Active" %} — {% trans "Access granted via Plugin Grants or activation key." %} -
- {% endif %} - - -
- - {% trans "Plugin Information" %} -
    -
  • {% trans "Name" %}: {{ plugin_name|default:"Premium Plugin Example" }}
  • -
  • {% trans "Version" %}: {{ version|default:"1.0.0" }}
  • -
  • - {% trans "Status" %}: - - {{ plugin_status|default:status|default:"Active" }} - -
  • -
-
- -

{{ description }}

- -
- {% csrf_token %} -
- - -
- -
- - -
- -
- - -
- -
- -
- - -
- - {% if show_payment_ui %} - - {% endif %} -
-
-{% endblock %} diff --git a/premiumPlugin/templates/premiumPlugin/subscription_required.html b/premiumPlugin/templates/premiumPlugin/subscription_required.html deleted file mode 100644 index cfc11a35a..000000000 --- a/premiumPlugin/templates/premiumPlugin/subscription_required.html +++ /dev/null @@ -1,139 +0,0 @@ -{% extends "baseTemplate/index.html" %} -{% load i18n %} - -{% block title %}{% trans "Payment Required - Premium Plugin" %}{% endblock %} - -{% block header_scripts %} - -{% endblock %} - -{% block content %} -
-
-
-

{% trans "Premium Plugin Access Required" %}

-

{% trans "This plugin requires payment or subscription to access premium features." %}

- - -
-

{% trans "Activate Premium Access" %}

-

{% trans "If you received an activation key, enter it below." %}

- -
- {% csrf_token %} -
- - -
- -
-
- - {% if payment_method == 'both' %} -
- {% trans "Current Payment Method:" %} {% trans "Check Both (Patreon or PayPal)" %} -
- {% elif payment_method == 'patreon' %} -
- {% trans "Current Payment Method:" %} {% trans "Patreon Subscription Only" %} -
- {% elif payment_method == 'paypal' %} -
- {% trans "Current Payment Method:" %} {% trans "PayPal Payment Only" %} -
- {% endif %} - -
-
-

{% trans "Patreon Subscription" %}

-

{% trans "Subscribe to" %} "{{ patreon_tier }}"

- - {% trans "Subscribe on Patreon" %} - -
-
-

{% trans "PayPal Payment" %}

-

{% trans "Complete one-time payment via PayPal" %}

- {% if paypal_me_url %} - - {% trans "Pay with PayPal.me" %} - - {% endif %} -
-
- -
-

{% trans "How it works:" %}

-
    -
  • {% trans "Install the plugin (already done)" %}
  • -
  • {% trans "Enter activation key, subscribe on Patreon, or pay via PayPal" %}
  • -
  • {% trans "The plugin will automatically unlock" %}
  • -
-
- - {% if error %} -
- {% trans "Error:" %} {{ error }} -
- {% endif %} -
-
- - -{% endblock %} diff --git a/premiumPlugin/urls.py b/premiumPlugin/urls.py deleted file mode 100644 index a9c206dbe..000000000 --- a/premiumPlugin/urls.py +++ /dev/null @@ -1,12 +0,0 @@ -from django.urls import path, re_path -from . import views - -app_name = 'premiumPlugin' - -urlpatterns = [ - path('', views.main_view, name='main'), - path('settings/', views.settings_view, name='settings'), - re_path(r'^activate-key/$', views.activate_key, name='activate_key'), - path('save-payment-method/', views.save_payment_method, name='save_payment_method'), - path('api/status/', views.api_status_view, name='api_status'), -] diff --git a/premiumPlugin/views.py b/premiumPlugin/views.py deleted file mode 100644 index fb887850f..000000000 --- a/premiumPlugin/views.py +++ /dev/null @@ -1,406 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Premium Plugin Views - Unified Verification (same as contaboAutoSnapshot) -Supports: Plugin Grants, Activation Key, Patreon, PayPal, AES encryption -""" - -from django.shortcuts import render, redirect -from django.http import JsonResponse, HttpResponse -from django.views.decorators.http import require_http_methods -from plogical.mailUtilities import mailUtilities -from plogical.httpProc import httpProc -from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter as logging -from functools import wraps -import urllib.request -import urllib.error -import json - -from .models import PremiumPluginConfig -from . import api_encryption - -PLUGIN_NAME = 'premiumPlugin' -PLUGIN_VERSION = '1.0.2' - -REMOTE_VERIFICATION_PATREON_URL = 'https://api.newstargeted.com/api/verify-patreon-membership.php' -REMOTE_VERIFICATION_PAYPAL_URL = 'https://api.newstargeted.com/api/verify-paypal-payment.php' -REMOTE_VERIFICATION_PLUGIN_GRANT_URL = 'https://api.newstargeted.com/api/verify-plugin-grant.php' -REMOTE_ACTIVATION_KEY_URL = 'https://api.newstargeted.com/api/activate-plugin-key.php' - -PATREON_TIER = 'CyberPanel Paid Plugin' -PATREON_URL = 'https://www.patreon.com/membership/27789984' -PAYPAL_ME_URL = 'https://paypal.me/KimBS?locale.x=en_US&country.x=NO' -PAYPAL_PAYMENT_LINK = '' - - -def cyberpanel_login_required(view_func): - @wraps(view_func) - def _wrapped_view(request, *args, **kwargs): - try: - if not request.session.get('userID'): - from loginSystem.views import loadLoginPage - return redirect(loadLoginPage) - return view_func(request, *args, **kwargs) - except KeyError: - from loginSystem.views import loadLoginPage - return redirect(loadLoginPage) - return _wrapped_view - - -def _api_request(url, data, timeout=10): - """Send encrypted API request and return decoded response dict.""" - try: - body, extra_headers = api_encryption.encrypt_payload(data) - headers = { - 'User-Agent': f'CyberPanel-Plugin/{PLUGIN_VERSION}', - 'X-Plugin-Name': PLUGIN_NAME - } - headers.update(extra_headers) - req = urllib.request.Request(url, data=body, headers=headers) - with urllib.request.urlopen(req, timeout=timeout) as response: - raw = response.read() - ct = response.headers.get('Content-Type', '') - expect_enc = extra_headers.get('X-Encrypted') == '1' - return api_encryption.decrypt_response(raw, ct, expect_encrypted=expect_enc) - except Exception as e: - logging.writeToFile(f"Premium Plugin: API request error to {url}: {str(e)}") - return {} - - -def check_plugin_grant(user_email, user_ip='', domain=''): - try: - request_data = { - 'user_email': user_email or '', - 'plugin_name': PLUGIN_NAME, - 'user_ip': user_ip, - 'domain': domain, - } - data = _api_request(REMOTE_VERIFICATION_PLUGIN_GRANT_URL, request_data) - if data.get('success') and data.get('has_access'): - return {'has_access': True, 'message': data.get('message', 'Access granted via Plugin Grants')} - return {'has_access': False, 'message': data.get('message', '')} - except Exception as e: - logging.writeToFile(f"Premium Plugin: Plugin grant check error: {str(e)}") - return {'has_access': False, 'message': ''} - - -def check_patreon_membership(user_email, user_ip='', domain=''): - try: - request_data = { - 'user_email': user_email, - 'plugin_name': PLUGIN_NAME, - 'plugin_version': PLUGIN_VERSION, - 'user_ip': user_ip, - 'domain': domain, - 'tier_id': '27789984' - } - response_data = _api_request(REMOTE_VERIFICATION_PATREON_URL, request_data) - if response_data.get('success', False): - return { - 'has_access': response_data.get('has_access', False), - 'patreon_tier': response_data.get('patreon_tier', PATREON_TIER), - 'patreon_url': response_data.get('patreon_url', PATREON_URL), - 'message': response_data.get('message', 'Access granted'), - 'error': None - } - return { - 'has_access': False, - 'patreon_tier': PATREON_TIER, - 'patreon_url': PATREON_URL, - 'message': response_data.get('message', 'Patreon subscription required'), - 'error': response_data.get('error') - } - except Exception as e: - logging.writeToFile(f"Premium Plugin: Patreon check error: {str(e)}") - return { - 'has_access': False, - 'patreon_tier': PATREON_TIER, - 'patreon_url': PATREON_URL, - 'message': 'Unable to verify Patreon membership.', - 'error': str(e) - } - - -def check_paypal_payment(user_email, user_ip='', domain=''): - try: - request_data = { - 'user_email': user_email, - 'plugin_name': PLUGIN_NAME, - 'plugin_version': PLUGIN_VERSION, - 'user_ip': user_ip, - 'domain': domain, - 'timestamp': 0, - } - import time - request_data['timestamp'] = int(time.time()) - response_data = _api_request(REMOTE_VERIFICATION_PAYPAL_URL, request_data) - if response_data.get('success', False): - return { - 'has_access': response_data.get('has_access', False), - 'paypal_me_url': response_data.get('paypal_me_url', PAYPAL_ME_URL), - 'paypal_payment_link': response_data.get('paypal_payment_link', PAYPAL_PAYMENT_LINK), - 'message': response_data.get('message', 'Access granted'), - 'error': None - } - return { - 'has_access': False, - 'paypal_me_url': PAYPAL_ME_URL, - 'paypal_payment_link': PAYPAL_PAYMENT_LINK, - 'message': response_data.get('message', 'PayPal payment required'), - 'error': response_data.get('error') - } - except Exception as e: - logging.writeToFile(f"Premium Plugin: PayPal check error: {str(e)}") - return { - 'has_access': False, - 'paypal_me_url': PAYPAL_ME_URL, - 'paypal_payment_link': PAYPAL_PAYMENT_LINK, - 'message': 'Unable to verify PayPal payment.', - 'error': str(e) - } - - -def unified_verification_required(view_func): - @wraps(view_func) - def _wrapped_view(request, *args, **kwargs): - try: - if not request.session.get('userID'): - from loginSystem.views import loadLoginPage - return redirect(loadLoginPage) - - user_email = request.session.get('email', '') or (getattr(request.user, 'email', '') if hasattr(request, 'user') and request.user else '') or getattr(request.user, 'username', '') - - try: - config = PremiumPluginConfig.get_config() - payment_method = config.payment_method - except Exception: - payment_method = 'both' - - has_access = False - verification_result = {} - - activation_key = request.GET.get('activation_key') or request.POST.get('activation_key') - if not activation_key: - try: - config = PremiumPluginConfig.get_config() - activation_key = getattr(config, 'activation_key', '') or '' - except Exception: - activation_key = '' - - if activation_key: - try: - request_data = { - 'activation_key': activation_key.strip(), - 'plugin_name': PLUGIN_NAME, - 'user_email': user_email - } - response_data = _api_request(REMOTE_ACTIVATION_KEY_URL, request_data) - if response_data.get('success', False) and response_data.get('has_access', False): - has_access = True - verification_result = {'method': 'activation_key', 'has_access': True, 'message': response_data.get('message', 'Access activated via key')} - try: - config = PremiumPluginConfig.get_config() - config.activation_key = activation_key.strip() - config.save(update_fields=['activation_key', 'updated_at']) - except Exception as e: - logging.writeToFile(f"Premium Plugin: Could not persist activation key: {str(e)}") - elif not response_data.get('success') and activation_key: - try: - config = PremiumPluginConfig.get_config() - if getattr(config, 'activation_key', '') == activation_key.strip(): - config.activation_key = '' - config.save(update_fields=['activation_key', 'updated_at']) - except Exception: - pass - except Exception as e: - logging.writeToFile(f"Premium Plugin: Activation key check error: {str(e)}") - - if not has_access: - grant_result = check_plugin_grant(user_email, request.META.get('REMOTE_ADDR', ''), request.get_host()) - if grant_result.get('has_access'): - has_access = True - verification_result = {'method': 'plugin_grant', 'has_access': True, 'message': grant_result.get('message', 'Access granted via Plugin Grants')} - - if not has_access: - try: - if payment_method == 'patreon': - result = check_patreon_membership(user_email, request.META.get('REMOTE_ADDR', ''), request.get_host()) - has_access = result.get('has_access', False) - verification_result = { - 'method': 'patreon', 'has_access': has_access, - 'patreon_tier': result.get('patreon_tier', PATREON_TIER), - 'patreon_url': result.get('patreon_url', PATREON_URL), - 'paypal_me_url': PAYPAL_ME_URL, 'paypal_payment_link': PAYPAL_PAYMENT_LINK, - 'message': result.get('message', 'Patreon subscription required'), - 'error': result.get('error') - } - elif payment_method == 'paypal': - result = check_paypal_payment(user_email, request.META.get('REMOTE_ADDR', ''), request.get_host()) - has_access = result.get('has_access', False) - verification_result = { - 'method': 'paypal', 'has_access': has_access, - 'patreon_tier': PATREON_TIER, 'patreon_url': PATREON_URL, - 'paypal_me_url': result.get('paypal_me_url', PAYPAL_ME_URL), - 'paypal_payment_link': result.get('paypal_payment_link', PAYPAL_PAYMENT_LINK), - 'message': result.get('message', 'PayPal payment required'), - 'error': result.get('error') - } - else: - patreon_result = check_patreon_membership(user_email, request.META.get('REMOTE_ADDR', ''), request.get_host()) - paypal_result = check_paypal_payment(user_email, request.META.get('REMOTE_ADDR', ''), request.get_host()) - has_access = patreon_result.get('has_access', False) or paypal_result.get('has_access', False) - verification_result = { - 'method': 'both', 'has_access': has_access, - 'patreon_tier': patreon_result.get('patreon_tier', PATREON_TIER), - 'patreon_url': patreon_result.get('patreon_url', PATREON_URL), - 'paypal_me_url': paypal_result.get('paypal_me_url', PAYPAL_ME_URL), - 'paypal_payment_link': paypal_result.get('paypal_payment_link', PAYPAL_PAYMENT_LINK), - 'message': 'Payment or subscription required' if not has_access else 'Access granted' - } - except Exception as e: - logging.writeToFile(f"Premium Plugin: Verification error: {str(e)}") - has_access = False - verification_result = { - 'method': payment_method, 'has_access': False, - 'patreon_tier': PATREON_TIER, 'patreon_url': PATREON_URL, - 'paypal_me_url': PAYPAL_ME_URL, 'paypal_payment_link': PAYPAL_PAYMENT_LINK, - 'message': 'Unable to verify access.', - 'error': str(e) - } - - if not has_access: - context = { - 'plugin_name': 'Premium Plugin Example', - 'is_paid': True, - 'payment_method': payment_method, - 'verification_result': verification_result, - 'patreon_tier': verification_result.get('patreon_tier', PATREON_TIER), - 'patreon_url': verification_result.get('patreon_url', PATREON_URL), - 'paypal_me_url': verification_result.get('paypal_me_url', PAYPAL_ME_URL), - 'paypal_payment_link': verification_result.get('paypal_payment_link', PAYPAL_PAYMENT_LINK), - 'message': verification_result.get('message', 'Payment or subscription required'), - 'error': verification_result.get('error') - } - proc = httpProc(request, 'premiumPlugin/subscription_required.html', context, 'admin') - return proc.render() - - if has_access and verification_result: - request.session['premium_plugin_access_via'] = verification_result.get('method', '') - - return view_func(request, *args, **kwargs) - except Exception as e: - logging.writeToFile(f"Premium Plugin: Decorator error: {str(e)}") - return HttpResponse(f"

Plugin Error

{str(e)}

") - return _wrapped_view - - -@cyberpanel_login_required -def main_view(request): - mailUtilities.checkHome() - return redirect('premiumPlugin:settings') - - -@cyberpanel_login_required -@unified_verification_required -def settings_view(request): - mailUtilities.checkHome() - try: - config = PremiumPluginConfig.get_config() - except Exception: - from django.core.management import call_command - try: - call_command('migrate', 'premiumPlugin', verbosity=0, interactive=False) - config = PremiumPluginConfig.get_config() - except Exception as e: - return HttpResponse(f"

Database Error

{str(e)}

") - - access_via = request.session.get('premium_plugin_access_via', '') - show_payment_ui = access_via not in ('plugin_grant', 'activation_key') - - context = { - 'plugin_name': 'Premium Plugin Example', - 'version': PLUGIN_VERSION, - 'status': 'Active', - 'config': config, - 'has_access': True, - 'show_payment_ui': show_payment_ui, - 'access_via_grant_or_key': not show_payment_ui, - 'patreon_tier': PATREON_TIER, - 'patreon_url': PATREON_URL, - 'paypal_me_url': PAYPAL_ME_URL, - 'paypal_payment_link': PAYPAL_PAYMENT_LINK, - 'description': 'Configure your premium plugin settings.', - } - proc = httpProc(request, 'premiumPlugin/settings.html', context, 'admin') - return proc.render() - - -@cyberpanel_login_required -@require_http_methods(["POST"]) -def activate_key(request): - try: - if request.content_type == 'application/json': - data = json.loads(request.body) - else: - data = request.POST - - activation_key = data.get('activation_key', '').strip() - user_email = data.get('user_email', '').strip() - if not user_email: - user_email = request.session.get('email', '') or (getattr(request.user, 'email', '') if hasattr(request, 'user') and request.user else '') - - if not activation_key: - return JsonResponse({'success': False, 'message': 'Activation key is required'}, status=400) - - request_data = {'activation_key': activation_key, 'plugin_name': PLUGIN_NAME, 'user_email': user_email} - response_data = _api_request(REMOTE_ACTIVATION_KEY_URL, request_data) - - if response_data.get('success', False) and response_data.get('has_access', False): - try: - config = PremiumPluginConfig.get_config() - config.activation_key = activation_key - config.save(update_fields=['activation_key', 'updated_at']) - except Exception as e: - logging.writeToFile(f"Premium Plugin: Could not persist activation key: {str(e)}") - - return JsonResponse({ - 'success': True, - 'has_access': True, - 'message': response_data.get('message', 'Access activated successfully') - }) - - return JsonResponse({ - 'success': False, - 'has_access': False, - 'message': response_data.get('message', 'Invalid activation key') - }) - - except Exception as e: - logging.writeToFile(f"Premium Plugin: activate_key error: {str(e)}") - return JsonResponse({'success': False, 'message': str(e)}, status=500) - - -@cyberpanel_login_required -@require_http_methods(["POST"]) -def save_payment_method(request): - try: - payment_method = request.POST.get('payment_method', 'both') - if payment_method not in ('patreon', 'paypal', 'both'): - payment_method = 'both' - config = PremiumPluginConfig.get_config() - config.payment_method = payment_method - config.save(update_fields=['payment_method', 'updated_at']) - return JsonResponse({'success': True, 'message': 'Payment method saved'}) - except Exception as e: - return JsonResponse({'success': False, 'message': str(e)}, status=500) - - -@cyberpanel_login_required -@unified_verification_required -def api_status_view(request): - return JsonResponse({ - 'plugin_name': 'Premium Plugin Example', - 'version': PLUGIN_VERSION, - 'status': 'active', - 'subscription': 'active', - 'verification_method': 'unified' - }) diff --git a/premiumPlugin/views_remote.py b/premiumPlugin/views_remote.py deleted file mode 100644 index a4adfaeeb..000000000 --- a/premiumPlugin/views_remote.py +++ /dev/null @@ -1,236 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Premium Plugin Views - Remote Verification Version -This version uses remote server verification (no secrets in plugin) -""" - -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' -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': '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 -@remote_verification_required -def main_view(request): - """ - Main view for premium plugin - Only accessible with Patreon subscription (verified remotely) - """ - mailUtilities.checkHome() - - context = { - 'plugin_name': 'Premium Plugin Example', - 'version': PLUGIN_VERSION, - 'description': 'This is an example paid plugin. You have access because you are subscribed to Patreon!', - 'features': [ - 'Premium Feature 1', - 'Premium Feature 2', - 'Premium Feature 3', - 'Advanced Configuration', - 'Priority Support' - ] - } - - proc = httpProc(request, 'premiumPlugin/index.html', context, 'admin') - return proc.render() - -@cyberpanel_login_required -@remote_verification_required -def settings_view(request): - """ - Settings page for premium plugin - Only accessible with Patreon subscription (verified remotely) - """ - mailUtilities.checkHome() - - context = { - 'plugin_name': 'Premium Plugin Example', - 'version': PLUGIN_VERSION, - 'description': 'Configure your premium plugin settings' - } - - 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': 'Premium Plugin Example', - 'version': PLUGIN_VERSION, - 'status': 'active', - 'subscription': 'active', - 'description': 'Premium plugin is active and accessible', - 'verification_method': 'remote' - }) diff --git a/testPlugin/OS_COMPATIBILITY.md b/testPlugin/OS_COMPATIBILITY.md deleted file mode 100644 index b01441843..000000000 --- a/testPlugin/OS_COMPATIBILITY.md +++ /dev/null @@ -1,464 +0,0 @@ -# OS Compatibility Guide - CyberPanel Test Plugin - -## 🌐 Supported Operating Systems - -The CyberPanel Test Plugin is designed to work seamlessly across all CyberPanel-supported operating systems with comprehensive multi-OS compatibility. - -### ✅ Currently Supported OS - -| Operating System | Version | Support Status | Python Version | Package Manager | Service Manager | -|------------------|---------|----------------|----------------|-----------------|-----------------| -| **Ubuntu** | 22.04 | ✅ Full Support | 3.10+ | apt-get | systemctl | -| **Ubuntu** | 20.04 | ✅ Full Support | 3.8+ | apt-get | systemctl | -| **Debian** | 13 | ✅ Full Support | 3.11+ | apt-get | systemctl | -| **Debian** | 12 | ✅ Full Support | 3.10+ | apt-get | systemctl | -| **Debian** | 11 | ✅ Full Support | 3.9+ | apt-get | systemctl | -| **AlmaLinux** | 10 | ✅ Full Support | 3.11+ | dnf | systemctl | -| **AlmaLinux** | 9 | ✅ Full Support | 3.9+ | dnf | systemctl | -| **AlmaLinux** | 8 | ✅ Full Support | 3.6+ | dnf/yum | systemctl | -| **RockyLinux** | 9 | ✅ Full Support | 3.9+ | dnf | systemctl | -| **RockyLinux** | 8 | ✅ Full Support | 3.6+ | dnf | systemctl | -| **RHEL** | 9 | ✅ Full Support | 3.9+ | dnf | systemctl | -| **RHEL** | 8 | ✅ Full Support | 3.6+ | dnf | systemctl | -| **CloudLinux** | 8 | ✅ Full Support | 3.6+ | yum | systemctl | -| **CentOS** | 9 | ✅ Full Support | 3.9+ | dnf | systemctl | - -### 🔧 Third-Party OS Support - -| Operating System | Compatibility | Notes | -|------------------|---------------|-------| -| **Fedora** | ✅ Compatible | Uses dnf package manager | -| **openEuler** | ⚠️ Limited | Community-supported, limited testing | -| **Other RHEL derivatives** | ⚠️ Limited | May work with AlmaLinux/RockyLinux packages | - -## 🚀 Installation Compatibility - -### Automatic OS Detection - -The installation script automatically detects your operating system and configures the plugin accordingly: - -```bash -# The script automatically detects: -# - OS name and version -# - Python executable path -# - Package manager (apt-get, dnf, yum) -# - Service manager (systemctl, service) -# - Web server (apache2, httpd) -``` - -### OS-Specific Configurations - -#### Ubuntu/Debian Systems -```bash -# Package Manager: apt-get -# Python: python3 -# Pip: pip3 -# Service Manager: systemctl -# Web Server: apache2 -# User/Group: cyberpanel:cyberpanel -``` - -#### RHEL-based Systems (AlmaLinux, RockyLinux, RHEL, CentOS) -```bash -# Package Manager: dnf (RHEL 8+) / yum (RHEL 7) -# Python: python3 -# Pip: pip3 -# Service Manager: systemctl -# Web Server: httpd -# User/Group: cyberpanel:cyberpanel -``` - -#### CloudLinux -```bash -# Package Manager: yum -# Python: python3 -# Pip: pip3 -# Service Manager: systemctl -# Web Server: httpd -# User/Group: cyberpanel:cyberpanel -``` - -## 🐍 Python Compatibility - -### Supported Python Versions - -| Python Version | Ubuntu 22.04 | Ubuntu 20.04 | AlmaLinux 9 | AlmaLinux 8 | RockyLinux 9 | RockyLinux 8 | RHEL 9 | RHEL 8 | CloudLinux 8 | -|----------------|--------------|--------------|-------------|-------------|--------------|--------------|-------|-------|--------------| -| **3.6** | ❌ | ❌ | ❌ | ✅ | ❌ | ✅ | ❌ | ✅ | ✅ | -| **3.7** | ❌ | ❌ | ❌ | ✅ | ❌ | ✅ | ❌ | ✅ | ✅ | -| **3.8** | ❌ | ✅ | ❌ | ✅ | ❌ | ✅ | ❌ | ✅ | ✅ | -| **3.9** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| **3.10** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| **3.11** | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| **3.12** | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | - -### Python Path Detection - -The plugin automatically detects the correct Python executable: - -```python -# Detection order: -1. python3.12 -2. python3.11 -3. python3.10 -4. python3.9 -5. python3.8 -6. python3.7 -7. python3.6 -8. python3 -9. python (fallback) -``` - -## 📦 Package Manager Compatibility - -### Ubuntu/Debian (apt-get) -```bash -# Required packages -apt-get update -apt-get install -y python3 python3-pip python3-venv git curl -apt-get install -y build-essential python3-dev - -# Python packages -pip3 install Django>=2.2,<4.0 django-cors-headers Pillow requests psutil -``` - -### RHEL-based (dnf/yum) -```bash -# RHEL 8+ (dnf) -dnf install -y python3 python3-pip python3-devel git curl -dnf install -y gcc gcc-c++ make - -# RHEL 7 (yum) -yum install -y python3 python3-pip python3-devel git curl -yum install -y gcc gcc-c++ make - -# Python packages -pip3 install Django>=2.2,<4.0 django-cors-headers Pillow requests psutil -``` - -### CloudLinux (yum) -```bash -# Required packages -yum install -y python3 python3-pip python3-devel git curl -yum install -y gcc gcc-c++ make - -# Python packages -pip3 install Django>=2.2,<4.0 django-cors-headers Pillow requests psutil -``` - -## 🔧 Service Management Compatibility - -### systemd (All supported OS) -```bash -# Service management commands -systemctl start lscpd -systemctl restart lscpd -systemctl status lscpd -systemctl enable lscpd - -# Web server management -systemctl start apache2 # Ubuntu/Debian -systemctl start httpd # RHEL-based -systemctl restart apache2 # Ubuntu/Debian -systemctl restart httpd # RHEL-based -``` - -### Legacy init.d (Fallback) -```bash -# Service management commands -service lscpd start -service lscpd restart -service lscpd status - -# Web server management -service apache2 start # Ubuntu/Debian -service httpd start # RHEL-based -``` - -## 🌐 Web Server Compatibility - -### Apache2 (Ubuntu/Debian) -```bash -# Configuration paths -/etc/apache2/apache2.conf -/etc/apache2/sites-available/ -/etc/apache2/sites-enabled/ - -# Service management -systemctl start apache2 -systemctl restart apache2 -systemctl status apache2 -``` - -### HTTPD (RHEL-based) -```bash -# Configuration paths -/etc/httpd/conf/httpd.conf -/etc/httpd/conf.d/ - -# Service management -systemctl start httpd -systemctl restart httpd -systemctl status httpd -``` - -## 🔐 Security Compatibility - -### SELinux (RHEL-based systems) -```bash -# Check SELinux status -sestatus - -# Set proper context for plugin files -setsebool -P httpd_can_network_connect 1 -chcon -R -t httpd_exec_t /usr/local/CyberCP/testPlugin/ -``` - -### AppArmor (Ubuntu/Debian) -```bash -# Check AppArmor status -aa-status - -# Allow Apache to access plugin files -aa-complain apache2 -``` - -### Firewall Compatibility -```bash -# Ubuntu/Debian (ufw) -ufw allow 8090/tcp -ufw allow 80/tcp -ufw allow 443/tcp - -# RHEL-based (firewalld) -firewall-cmd --permanent --add-port=8090/tcp -firewall-cmd --permanent --add-port=80/tcp -firewall-cmd --permanent --add-port=443/tcp -firewall-cmd --reload - -# iptables (legacy) -iptables -A INPUT -p tcp --dport 8090 -j ACCEPT -iptables -A INPUT -p tcp --dport 80 -j ACCEPT -iptables -A INPUT -p tcp --dport 443 -j ACCEPT -``` - -## 🧪 Testing Compatibility - -### Run Compatibility Test -```bash -# Navigate to plugin directory -cd /usr/local/CyberCP/testPlugin - -# Run compatibility test -python3 test_os_compatibility.py - -# Or make it executable and run -chmod +x test_os_compatibility.py -./test_os_compatibility.py -``` - -### Test Results -The compatibility test checks: -- ✅ OS detection and version -- ✅ Python installation and version -- ✅ Package manager availability -- ✅ Service manager functionality -- ✅ Web server configuration -- ✅ File permissions and ownership -- ✅ Network connectivity -- ✅ CyberPanel integration - -### Sample Output -``` -🔍 Testing OS Compatibility for CyberPanel Test Plugin -============================================================ - -📋 Testing OS Detection... - ✅ OS: ubuntu 22.04 (x86_64) - ✅ Supported: True - -🐍 Testing Python Detection... - ✅ Python: Python 3.10.12 - ✅ Path: /usr/bin/python3 - ✅ Pip: /usr/bin/pip3 - ✅ Compatible: True - -📦 Testing Package Manager Detection... - ✅ Package Manager: apt-get - ✅ Available: True - -🔧 Testing Service Manager Detection... - ✅ Service Manager: systemctl - ✅ Web Server: apache2 - ✅ Available: True - -🌐 Testing Web Server Detection... - ✅ Web Server: apache2 - ✅ Installed: True - -🔐 Testing File Permissions... - ✅ Plugin Directory: /home/cyberpanel/plugins - ✅ CyberPanel Directory: /usr/local/CyberCP - -🌍 Testing Network Connectivity... - ✅ GitHub: True - ✅ Internet: True - -⚡ Testing CyberPanel Integration... - ✅ CyberPanel Installed: True - ✅ Settings File: True - ✅ URLs File: True - ✅ LSCPD Service: True - -============================================================ -📊 COMPATIBILITY TEST RESULTS -============================================================ -Total Tests: 8 -✅ Passed: 8 -⚠️ Warnings: 0 -❌ Failed: 0 - -🎉 All tests passed! The plugin is compatible with this OS. -``` - -## 🚨 Troubleshooting - -### Common Issues by OS - -#### Ubuntu/Debian Issues -```bash -# Python not found -sudo apt-get update -sudo apt-get install -y python3 python3-pip - -# Permission denied -sudo chown -R cyberpanel:cyberpanel /home/cyberpanel/plugins -sudo chown -R cyberpanel:cyberpanel /usr/local/CyberCP/testPlugin - -# Service not starting -sudo systemctl daemon-reload -sudo systemctl restart lscpd -``` - -#### RHEL-based Issues -```bash -# Python not found -sudo dnf install -y python3 python3-pip -# or -sudo yum install -y python3 python3-pip - -# SELinux issues -sudo setsebool -P httpd_can_network_connect 1 -sudo chcon -R -t httpd_exec_t /usr/local/CyberCP/testPlugin/ - -# Permission denied -sudo chown -R cyberpanel:cyberpanel /home/cyberpanel/plugins -sudo chown -R cyberpanel:cyberpanel /usr/local/CyberCP/testPlugin -``` - -#### CloudLinux Issues -```bash -# Python not found -sudo yum install -y python3 python3-pip - -# CageFS issues -cagefsctl --enable cyberpanel -cagefsctl --update - -# Permission denied -sudo chown -R cyberpanel:cyberpanel /home/cyberpanel/plugins -sudo chown -R cyberpanel:cyberpanel /usr/local/CyberCP/testPlugin -``` - -### Debug Commands -```bash -# Check OS information -cat /etc/os-release -uname -a - -# Check Python installation -python3 --version -which python3 -which pip3 - -# Check services -systemctl status lscpd -systemctl status apache2 # Ubuntu/Debian -systemctl status httpd # RHEL-based - -# Check file permissions -ls -la /home/cyberpanel/plugins/ -ls -la /usr/local/CyberCP/testPlugin/ - -# Check CyberPanel logs -tail -f /home/cyberpanel/logs/cyberpanel.log -tail -f /home/cyberpanel/logs/django.log -``` - -## 📋 Installation Checklist - -### Pre-Installation -- [ ] Verify OS is supported -- [ ] Check Python 3.6+ is installed -- [ ] Ensure CyberPanel is installed and running -- [ ] Verify internet connectivity -- [ ] Check available disk space (minimum 100MB) - -### Installation -- [ ] Download installation script -- [ ] Run as root user -- [ ] Monitor installation output -- [ ] Verify plugin files are created -- [ ] Check Django settings are updated -- [ ] Confirm URL configuration is added - -### Post-Installation -- [ ] Test plugin access via web interface -- [ ] Verify all features work correctly -- [ ] Check security settings -- [ ] Run compatibility test -- [ ] Review installation logs - -## 🔄 Updates and Maintenance - -### Updating the Plugin -```bash -# Navigate to plugin directory -cd /usr/local/CyberCP/testPlugin - -# Pull latest changes -git pull origin main - -# Restart services -sudo systemctl restart lscpd -sudo systemctl restart apache2 # Ubuntu/Debian -sudo systemctl restart httpd # RHEL-based -``` - -### Uninstalling the Plugin -```bash -# Run uninstall script -sudo ./install.sh --uninstall - -# Or manually remove -sudo rm -rf /usr/local/CyberCP/testPlugin -sudo rm -f /home/cyberpanel/plugins/testPlugin -``` - -## 📞 Support - -### OS-Specific Support -- **Ubuntu/Debian**: Check Ubuntu/Debian documentation -- **RHEL-based**: Check Red Hat documentation -- **CloudLinux**: Check CloudLinux documentation - -### Plugin Support -- **GitHub Issues**: https://github.com/cyberpanel/testPlugin/issues -- **CyberPanel Forums**: https://forums.cyberpanel.net/ -- **Documentation**: https://cyberpanel.net/docs/ - ---- - -**Last Updated**: September 2025 -**Compatibility Version**: 1.0.0 -**Next Review**: March 2026 diff --git a/testPlugin/SECURITY.md b/testPlugin/SECURITY.md deleted file mode 100644 index ea6cee1b0..000000000 --- a/testPlugin/SECURITY.md +++ /dev/null @@ -1,247 +0,0 @@ -# Security Implementation - CyberPanel Test Plugin - -## 🔒 Security Overview - -The CyberPanel Test Plugin has been designed with **enterprise-grade security** as the top priority. This document outlines all security measures implemented to protect against common web application vulnerabilities and attacks. - -## 🛡️ Security Features Implemented - -### 1. Authentication & Authorization -- **Admin-only access** required for all plugin functions -- **User session validation** on every request -- **Privilege escalation protection** -- **Role-based access control** (RBAC) - -### 2. Rate Limiting & Brute Force Protection -- **50 requests per 5-minute window** per user -- **10 test button clicks per minute** limit -- **Automatic lockout** after 5 failed attempts -- **15-minute lockout duration** -- **Progressive punishment system** - -### 3. CSRF Protection -- **HMAC-based CSRF token validation** -- **Token expiration** after 1 hour -- **User-specific token generation** -- **Secure token verification** - -### 4. Input Validation & Sanitization -- **Regex-based input validation** -- **XSS attack prevention** -- **SQL injection prevention** -- **Path traversal protection** -- **Maximum input length limits** (1000 characters) -- **Character whitelisting** - -### 5. Security Monitoring & Logging -- **All security events logged** with IP and user agent -- **Failed attempt tracking** and alerting -- **Suspicious activity detection** -- **Real-time security event monitoring** -- **Comprehensive audit trail** - -### 6. HTTP Security Headers -- **X-Frame-Options: DENY** (clickjacking protection) -- **X-Content-Type-Options: nosniff** -- **X-XSS-Protection: 1; mode=block** -- **Content-Security-Policy (CSP)** -- **Strict-Transport-Security (HSTS)** -- **Referrer-Policy: strict-origin-when-cross-origin** -- **Permissions-Policy** - -### 7. Data Isolation & Privacy -- **User-specific data isolation** -- **Logs restricted** to user's own activities -- **Settings isolated** per user -- **No cross-user data access** - -## 🔍 Security Middleware - -The plugin includes a comprehensive security middleware that performs: - -### Request Analysis -- **Suspicious pattern detection** -- **SQL injection attempt detection** -- **XSS attempt detection** -- **Path traversal attempt detection** -- **Malicious payload identification** - -### Response Protection -- **Security headers injection** -- **Content Security Policy enforcement** -- **Clickjacking protection** -- **MIME type sniffing prevention** - -## 🚨 Attack Prevention - -### OWASP Top 10 Protection -1. **A01: Broken Access Control** ✅ Protected -2. **A02: Cryptographic Failures** ✅ Protected -3. **A03: Injection** ✅ Protected -4. **A04: Insecure Design** ✅ Protected -5. **A05: Security Misconfiguration** ✅ Protected -6. **A06: Vulnerable Components** ✅ Protected -7. **A07: Authentication Failures** ✅ Protected -8. **A08: Software Integrity Failures** ✅ Protected -9. **A09: Logging Failures** ✅ Protected -10. **A10: Server-Side Request Forgery** ✅ Protected - -### Specific Attack Vectors Blocked -- **SQL Injection** - Regex pattern matching + parameterized queries -- **Cross-Site Scripting (XSS)** - Input sanitization + CSP headers -- **Cross-Site Request Forgery (CSRF)** - HMAC token validation -- **Brute Force Attacks** - Rate limiting + account lockout -- **Path Traversal** - Pattern detection + input validation -- **Clickjacking** - X-Frame-Options header -- **Session Hijacking** - Secure session management -- **Privilege Escalation** - Role-based access control - -## 📊 Security Metrics - -- **15+ Security Features** implemented -- **99% Attack Prevention** rate -- **24/7 Security Monitoring** active -- **0 Known Vulnerabilities** in current version -- **Enterprise-grade** security standards - -## 🔧 Security Configuration - -### Rate Limiting Settings -```python -RATE_LIMIT_WINDOW = 300 # 5 minutes -MAX_REQUESTS_PER_WINDOW = 50 -MAX_FAILED_ATTEMPTS = 5 -LOCKOUT_DURATION = 900 # 15 minutes -``` - -### Input Validation Settings -```python -SAFE_STRING_PATTERN = re.compile(r'^[a-zA-Z0-9\s\-_.,!?@#$%^&*()+=\[\]{}|\\:";\'<>?/~`]*$') -MAX_MESSAGE_LENGTH = 1000 -``` - -### CSRF Token Settings -```python -TOKEN_EXPIRATION = 3600 # 1 hour -HMAC_ALGORITHM = 'sha256' -``` - -## 🚀 Security Best Practices - -### For Developers -1. **Always validate input** before processing -2. **Use parameterized queries** for database operations -3. **Implement proper error handling** without information disclosure -4. **Log security events** for monitoring -5. **Keep dependencies updated** -6. **Use HTTPS** in production -7. **Implement proper session management** - -### For Administrators -1. **Keep CyberPanel updated** -2. **Use strong, unique passwords** -3. **Enable 2FA** on admin accounts -4. **Regularly review security logs** -5. **Monitor failed login attempts** -6. **Use HTTPS** in production environments -7. **Regular security audits** - -## 🔍 Security Monitoring - -### Logged Events -- **Authentication attempts** (successful and failed) -- **Authorization failures** -- **Rate limit violations** -- **Suspicious request patterns** -- **Input validation failures** -- **Security policy violations** -- **System errors and exceptions** - -### Monitoring Dashboard -Access the security information page at: `/testPlugin/security/` - -## 🛠️ Security Testing - -### Automated Tests -- **Unit tests** for all security functions -- **Integration tests** for security middleware -- **Penetration testing** scenarios -- **Vulnerability scanning** - -### Manual Testing -- **OWASP ZAP** security testing -- **Burp Suite** penetration testing -- **Manual security review** -- **Code security audit** - -## 📋 Security Checklist - -- [x] Authentication implemented -- [x] Authorization implemented -- [x] CSRF protection enabled -- [x] Rate limiting configured -- [x] Input validation active -- [x] XSS protection enabled -- [x] SQL injection protection -- [x] Security headers configured -- [x] Logging implemented -- [x] Error handling secure -- [x] Session management secure -- [x] Data isolation implemented -- [x] Security monitoring active - -## 🚨 Incident Response - -### Security Incident Procedure -1. **Immediate Response** - - Block suspicious IP addresses - - Review security logs - - Assess impact - -2. **Investigation** - - Analyze attack vectors - - Identify compromised accounts - - Document findings - -3. **Recovery** - - Patch vulnerabilities - - Reset compromised accounts - - Update security measures - -4. **Post-Incident** - - Review security policies - - Update monitoring rules - - Conduct security training - -## 📞 Security Contact - -For security-related issues or vulnerability reports: - -- **Email**: security@cyberpanel.net -- **GitHub**: Create a private security issue -- **Response Time**: Within 24-48 hours - -## 🔄 Security Updates - -Security is an ongoing process. Regular updates include: - -- **Security patches** for vulnerabilities -- **Enhanced monitoring** capabilities -- **Improved detection** algorithms -- **Updated security policies** -- **New protection mechanisms** - -## 📚 Additional Resources - -- [OWASP Top 10](https://owasp.org/www-project-top-ten/) -- [Django Security](https://docs.djangoproject.com/en/stable/topics/security/) -- [CyberPanel Security](https://cyberpanel.net/docs/) -- [Web Application Security](https://cheatsheetseries.owasp.org/) - ---- - -**Security Note**: 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. - -**Last Updated**: December 2024 -**Security Version**: 1.0.0 -**Next Review**: March 2025 diff --git a/testPlugin/__init__.py b/testPlugin/__init__.py deleted file mode 100644 index 695a722b6..000000000 --- a/testPlugin/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# -*- coding: utf-8 -*- -default_app_config = 'testPlugin.apps.TestPluginConfig' diff --git a/testPlugin/admin.py b/testPlugin/admin.py deleted file mode 100644 index cd858aeea..000000000 --- a/testPlugin/admin.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -from django.contrib import admin -from .models import TestPluginSettings, TestPluginLog - - -@admin.register(TestPluginSettings) -class TestPluginSettingsAdmin(admin.ModelAdmin): - list_display = ['user', 'plugin_enabled', 'test_count', 'last_test_time'] - list_filter = ['plugin_enabled', 'last_test_time'] - search_fields = ['user__username', 'custom_message'] - readonly_fields = ['last_test_time'] - - -@admin.register(TestPluginLog) -class TestPluginLogAdmin(admin.ModelAdmin): - list_display = ['timestamp', 'action', 'message', 'user'] - list_filter = ['action', 'timestamp', 'user'] - search_fields = ['action', 'message', 'user__username'] - readonly_fields = ['timestamp'] - date_hierarchy = 'timestamp' diff --git a/testPlugin/apps.py b/testPlugin/apps.py deleted file mode 100644 index ae29de970..000000000 --- a/testPlugin/apps.py +++ /dev/null @@ -1,11 +0,0 @@ -# -*- coding: utf-8 -*- -from django.apps import AppConfig - - -class TestPluginConfig(AppConfig): - name = 'testPlugin' - verbose_name = 'Test Plugin' - - def ready(self): - # Import signal handlers - import testPlugin.signals diff --git a/testPlugin/install.sh b/testPlugin/install.sh deleted file mode 100644 index b87eb2140..000000000 --- a/testPlugin/install.sh +++ /dev/null @@ -1,580 +0,0 @@ -#!/bin/bash - -# Test Plugin Installation Script for CyberPanel -# Multi-OS Compatible Installation Script -# Supports: Ubuntu, Debian, AlmaLinux, RockyLinux, RHEL, CloudLinux, CentOS - -set -e - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' # No Color - -# Configuration -PLUGIN_NAME="testPlugin" -PLUGIN_DIR="/home/cyberpanel/plugins" -CYBERPANEL_DIR="/usr/local/CyberCP" -GITHUB_REPO="https://github.com/cyberpanel/testPlugin.git" -TEMP_DIR="/tmp/cyberpanel_plugin_install" - -# OS Detection Variables -OS_NAME="" -OS_VERSION="" -OS_ARCH="" -PYTHON_CMD="" -PIP_CMD="" -SERVICE_CMD="" -WEB_SERVER="" - -# Function to print colored output -print_status() { - echo -e "${BLUE}[INFO]${NC} $1" -} - -print_success() { - echo -e "${GREEN}[SUCCESS]${NC} $1" -} - -print_warning() { - echo -e "${YELLOW}[WARNING]${NC} $1" -} - -print_error() { - echo -e "${RED}[ERROR]${NC} $1" -} - -# Function to detect operating system -detect_os() { - print_status "Detecting operating system..." - - if [ -f /etc/os-release ]; then - . /etc/os-release - OS_NAME="$ID" - OS_VERSION="$VERSION_ID" - elif [ -f /etc/redhat-release ]; then - OS_NAME="rhel" - OS_VERSION=$(cat /etc/redhat-release | grep -oE '[0-9]+\.[0-9]+' | head -1) - elif [ -f /etc/debian_version ]; then - OS_NAME="debian" - OS_VERSION=$(cat /etc/debian_version) - else - print_error "Unable to detect operating system" - exit 1 - fi - - # Detect architecture - OS_ARCH=$(uname -m) - - print_success "Detected: $OS_NAME $OS_VERSION ($OS_ARCH)" - - # Set OS-specific configurations - configure_os_specific -} - -# Function to configure OS-specific settings -configure_os_specific() { - case "$OS_NAME" in - "ubuntu"|"debian") - PYTHON_CMD="python3" - PIP_CMD="pip3" - SERVICE_CMD="systemctl" - WEB_SERVER="apache2" - ;; - "almalinux"|"rocky"|"rhel"|"centos"|"cloudlinux") - PYTHON_CMD="python3" - PIP_CMD="pip3" - SERVICE_CMD="systemctl" - WEB_SERVER="httpd" - ;; - *) - print_warning "Unknown OS: $OS_NAME. Using default configurations." - PYTHON_CMD="python3" - PIP_CMD="pip3" - SERVICE_CMD="systemctl" - WEB_SERVER="httpd" - ;; - esac - - print_status "Using Python: $PYTHON_CMD" - print_status "Using Pip: $PIP_CMD" - print_status "Using Service Manager: $SERVICE_CMD" - print_status "Using Web Server: $WEB_SERVER" -} - -# Function to check if running as root -check_root() { - if [[ $EUID -ne 0 ]]; then - print_error "This script must be run as root" - exit 1 - fi -} - -# Function to check if CyberPanel is installed -check_cyberpanel() { - if [ ! -d "$CYBERPANEL_DIR" ]; then - print_error "CyberPanel is not installed at $CYBERPANEL_DIR" - print_error "Please install CyberPanel first: https://cyberpanel.net/docs/" - exit 1 - fi - - # Check if CyberPanel is running - if ! $SERVICE_CMD is-active --quiet lscpd; then - print_warning "CyberPanel service (lscpd) is not running. Starting it..." - $SERVICE_CMD start lscpd - fi - - print_success "CyberPanel installation verified" -} - -# Function to check Python installation -check_python() { - print_status "Checking Python installation..." - - if ! command -v $PYTHON_CMD &> /dev/null; then - print_error "Python3 is not installed. Installing..." - install_python - fi - - # Check Python version (require 3.6+) - PYTHON_VERSION=$($PYTHON_CMD -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')") - PYTHON_MAJOR=$(echo $PYTHON_VERSION | cut -d. -f1) - PYTHON_MINOR=$(echo $PYTHON_VERSION | cut -d. -f2) - - if [ "$PYTHON_MAJOR" -lt 3 ] || ([ "$PYTHON_MAJOR" -eq 3 ] && [ "$PYTHON_MINOR" -lt 6 ]); then - print_error "Python 3.6+ is required. Found: $PYTHON_VERSION" - exit 1 - fi - - print_success "Python $PYTHON_VERSION is available" -} - -# Function to install Python if needed -install_python() { - case "$OS_NAME" in - "ubuntu"|"debian") - apt-get update - apt-get install -y python3 python3-pip python3-venv - ;; - "almalinux"|"rocky"|"rhel"|"centos"|"cloudlinux") - if command -v dnf &> /dev/null; then - dnf install -y python3 python3-pip - elif command -v yum &> /dev/null; then - yum install -y python3 python3-pip - else - print_error "No package manager found (dnf/yum)" - exit 1 - fi - ;; - esac -} - -# Function to check pip installation -check_pip() { - print_status "Checking pip installation..." - - if ! command -v $PIP_CMD &> /dev/null; then - print_error "pip3 is not installed. Installing..." - install_pip - fi - - print_success "pip3 is available" -} - -# Function to install pip if needed -install_pip() { - case "$OS_NAME" in - "ubuntu"|"debian") - apt-get install -y python3-pip - ;; - "almalinux"|"rocky"|"rhel"|"centos"|"cloudlinux") - if command -v dnf &> /dev/null; then - dnf install -y python3-pip - elif command -v yum &> /dev/null; then - yum install -y python3-pip - fi - ;; - esac -} - -# Function to check required packages -check_packages() { - print_status "Checking required packages..." - - # Check for git - if ! command -v git &> /dev/null; then - print_error "git is not installed. Installing..." - install_git - fi - - # Check for curl - if ! command -v curl &> /dev/null; then - print_error "curl is not installed. Installing..." - install_curl - fi - - print_success "All required packages are available" -} - -# Function to install git -install_git() { - case "$OS_NAME" in - "ubuntu"|"debian") - apt-get update - apt-get install -y git - ;; - "almalinux"|"rocky"|"rhel"|"centos"|"cloudlinux") - if command -v dnf &> /dev/null; then - dnf install -y git - elif command -v yum &> /dev/null; then - yum install -y git - fi - ;; - esac -} - -# Function to install curl -install_curl() { - case "$OS_NAME" in - "ubuntu"|"debian") - apt-get update - apt-get install -y curl - ;; - "almalinux"|"rocky"|"rhel"|"centos"|"cloudlinux") - if command -v dnf &> /dev/null; then - dnf install -y curl - elif command -v yum &> /dev/null; then - yum install -y curl - fi - ;; - esac -} - -# Function to create plugin directory -create_plugin_directory() { - print_status "Creating plugin directory structure..." - - # Create main plugin directory - mkdir -p "$PLUGIN_DIR" - - # Create CyberPanel plugin directory - mkdir -p "$CYBERPANEL_DIR/$PLUGIN_NAME" - - # Set proper permissions - chown -R cyberpanel:cyberpanel "$PLUGIN_DIR" 2>/dev/null || chown -R root:root "$PLUGIN_DIR" - chmod -R 755 "$PLUGIN_DIR" - - chown -R cyberpanel:cyberpanel "$CYBERPANEL_DIR/$PLUGIN_NAME" 2>/dev/null || chown -R root:root "$CYBERPANEL_DIR/$PLUGIN_NAME" - chmod -R 755 "$CYBERPANEL_DIR/$PLUGIN_NAME" - - print_success "Plugin directory structure created" -} - -# Function to download plugin -download_plugin() { - print_status "Downloading plugin from GitHub..." - - # Clean up temp directory - rm -rf "$TEMP_DIR" - mkdir -p "$TEMP_DIR" - - # Clone the repository - if ! git clone "$GITHUB_REPO" "$TEMP_DIR"; then - print_error "Failed to download plugin from GitHub" - print_error "Please check your internet connection and try again" - exit 1 - fi - - print_success "Plugin downloaded successfully" -} - -# Function to install plugin files -install_plugin_files() { - print_status "Installing plugin files..." - - # Copy plugin files - cp -r "$TEMP_DIR"/* "$CYBERPANEL_DIR/$PLUGIN_NAME/" - - # Create symlink - ln -sf "$CYBERPANEL_DIR/$PLUGIN_NAME" "$PLUGIN_DIR/$PLUGIN_NAME" - - # Set proper ownership and permissions - chown -R cyberpanel:cyberpanel "$CYBERPANEL_DIR/$PLUGIN_NAME" 2>/dev/null || chown -R root:root "$CYBERPANEL_DIR/$PLUGIN_NAME" - chmod -R 755 "$CYBERPANEL_DIR/$PLUGIN_NAME" - - # Make scripts executable - chmod +x "$CYBERPANEL_DIR/$PLUGIN_NAME/install.sh" 2>/dev/null || true - - print_success "Plugin files installed" -} - -# Function to update Django settings -update_django_settings() { - print_status "Updating Django settings..." - - SETTINGS_FILE="$CYBERPANEL_DIR/cyberpanel/settings.py" - - # Check if plugin is already in INSTALLED_APPS - if ! grep -q "'$PLUGIN_NAME'" "$SETTINGS_FILE"; then - # Add plugin to INSTALLED_APPS - sed -i "/INSTALLED_APPS = \[/a\ '$PLUGIN_NAME'," "$SETTINGS_FILE" - print_success "Added $PLUGIN_NAME to INSTALLED_APPS" - else - print_warning "$PLUGIN_NAME already in INSTALLED_APPS" - fi -} - -# Function to update URL configuration -update_urls() { - print_status "Updating URL configuration..." - - URLS_FILE="$CYBERPANEL_DIR/cyberpanel/urls.py" - - # Check if plugin URLs are already included - if ! grep -q "path(\"$PLUGIN_NAME/\"" "$URLS_FILE"; then - # Add plugin URLs - sed -i "/urlpatterns = \[/a\ path(\"$PLUGIN_NAME/\", include(\"$PLUGIN_NAME.urls\"))," "$URLS_FILE" - print_success "Added $PLUGIN_NAME URLs" - else - print_warning "$PLUGIN_NAME URLs already configured" - fi -} - -# Function to run database migrations -run_migrations() { - print_status "Running database migrations..." - - cd "$CYBERPANEL_DIR" - - # Create migrations - if ! $PYTHON_CMD manage.py makemigrations $PLUGIN_NAME; then - print_warning "No migrations to create for $PLUGIN_NAME" - fi - - # Apply migrations - if ! $PYTHON_CMD manage.py migrate $PLUGIN_NAME; then - print_warning "No migrations to apply for $PLUGIN_NAME" - fi - - print_success "Database migrations completed" -} - -# Function to collect static files -collect_static() { - print_status "Collecting static files..." - - cd "$CYBERPANEL_DIR" - - if ! $PYTHON_CMD manage.py collectstatic --noinput; then - print_warning "Static file collection failed, but continuing..." - else - print_success "Static files collected" - fi -} - -# Function to restart services -restart_services() { - print_status "Restarting CyberPanel services..." - - # Restart lscpd - if $SERVICE_CMD is-active --quiet lscpd; then - $SERVICE_CMD restart lscpd - print_success "lscpd service restarted" - else - print_warning "lscpd service not running" - fi - - # Restart web server - if $SERVICE_CMD is-active --quiet $WEB_SERVER; then - $SERVICE_CMD restart $WEB_SERVER - print_success "$WEB_SERVER service restarted" - else - print_warning "$WEB_SERVER service not running" - fi - - # Additional service restart for different OS - case "$OS_NAME" in - "ubuntu"|"debian") - if $SERVICE_CMD is-active --quiet cyberpanel; then - $SERVICE_CMD restart cyberpanel - print_success "cyberpanel service restarted" - fi - ;; - "almalinux"|"rocky"|"rhel"|"centos"|"cloudlinux") - if $SERVICE_CMD is-active --quiet cyberpanel; then - $SERVICE_CMD restart cyberpanel - print_success "cyberpanel service restarted" - fi - ;; - esac -} - -# Function to verify installation -verify_installation() { - print_status "Verifying installation..." - - # Check if plugin directory exists - if [ ! -d "$CYBERPANEL_DIR/$PLUGIN_NAME" ]; then - print_error "Plugin directory not found" - return 1 - fi - - # Check if symlink exists - if [ ! -L "$PLUGIN_DIR/$PLUGIN_NAME" ]; then - print_error "Plugin symlink not found" - return 1 - fi - - # Check if meta.xml exists - if [ ! -f "$CYBERPANEL_DIR/$PLUGIN_NAME/meta.xml" ]; then - print_error "Plugin meta.xml not found" - return 1 - fi - - print_success "Installation verified successfully" - return 0 -} - -# Function to display installation summary -display_summary() { - echo "" - echo "==========================================" - print_success "Test Plugin Installation Complete!" - echo "==========================================" - echo "Plugin Name: $PLUGIN_NAME" - echo "Installation Directory: $CYBERPANEL_DIR/$PLUGIN_NAME" - echo "Plugin Directory: $PLUGIN_DIR/$PLUGIN_NAME" - echo "Access URL: https://your-domain:8090/testPlugin/" - echo "Operating System: $OS_NAME $OS_VERSION ($OS_ARCH)" - echo "Python Version: $($PYTHON_CMD --version)" - echo "" - echo "Features Installed:" - echo "✓ Enable/Disable Toggle" - echo "✓ Test Button with Popup Messages" - echo "✓ Settings Page" - echo "✓ Activity Logs" - echo "✓ Inline Integration" - echo "✓ Complete Documentation" - echo "✓ Official CyberPanel Guide" - echo "✓ Advanced Development Guide" - echo "✓ Enterprise-Grade Security" - echo "✓ Brute Force Protection" - echo "✓ CSRF Protection" - echo "✓ XSS Prevention" - echo "✓ SQL Injection Protection" - echo "✓ Rate Limiting" - echo "✓ Security Monitoring" - echo "✓ Security Information Page" - echo "✓ Multi-OS Compatibility" - echo "" - echo "Supported Operating Systems:" - echo "✓ Ubuntu 22.04, 20.04" - echo "✓ Debian (compatible)" - echo "✓ AlmaLinux 8, 9, 10" - echo "✓ RockyLinux 8, 9" - echo "✓ RHEL 8, 9" - echo "✓ CloudLinux 8" - echo "✓ CentOS 9" - echo "" - echo "To uninstall, run: $0 --uninstall" - echo "==========================================" -} - -# Function to uninstall plugin -uninstall_plugin() { - print_status "Uninstalling $PLUGIN_NAME..." - - # Remove plugin files - rm -rf "$CYBERPANEL_DIR/$PLUGIN_NAME" - rm -f "$PLUGIN_DIR/$PLUGIN_NAME" - - # Remove from Django settings - SETTINGS_FILE="$CYBERPANEL_DIR/cyberpanel/settings.py" - if [ -f "$SETTINGS_FILE" ]; then - sed -i "/'$PLUGIN_NAME',/d" "$SETTINGS_FILE" - print_success "Removed $PLUGIN_NAME from INSTALLED_APPS" - fi - - # Remove from URLs - URLS_FILE="$CYBERPANEL_DIR/cyberpanel/urls.py" - if [ -f "$URLS_FILE" ]; then - sed -i "/path(\"$PLUGIN_NAME\/\"/d" "$URLS_FILE" - print_success "Removed $PLUGIN_NAME URLs" - fi - - # Restart services - restart_services - - print_success "Plugin uninstalled successfully" -} - -# Main installation function -install_plugin() { - print_status "Starting Test Plugin installation..." - - # Detect OS - detect_os - - # Check requirements - check_root - check_cyberpanel - check_python - check_pip - check_packages - - # Install plugin - create_plugin_directory - download_plugin - install_plugin_files - update_django_settings - update_urls - run_migrations - collect_static - restart_services - - # Verify installation - if verify_installation; then - display_summary - else - print_error "Installation verification failed" - exit 1 - fi -} - -# Main script logic -main() { - case "${1:-}" in - "--uninstall") - check_root - uninstall_plugin - ;; - "--help"|"-h") - echo "Usage: $0 [OPTIONS]" - echo "Options:" - echo " --uninstall Uninstall the plugin" - echo " --help, -h Show this help message" - echo "" - echo "Supported Operating Systems:" - echo " Ubuntu 22.04, 20.04" - echo " Debian (compatible)" - echo " AlmaLinux 8, 9, 10" - echo " RockyLinux 8, 9" - echo " RHEL 8, 9" - echo " CloudLinux 8" - echo " CentOS 9" - ;; - "") - install_plugin - ;; - *) - print_error "Unknown option: $1" - echo "Use --help for usage information" - exit 1 - ;; - esac -} - -# Run main function -main "$@" \ No newline at end of file diff --git a/testPlugin/meta.xml b/testPlugin/meta.xml deleted file mode 100644 index 76993172a..000000000 --- a/testPlugin/meta.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - Test Plugin - Utility - 1.0.0 - A comprehensive test plugin for CyberPanel with enable/disable functionality, test button, popup messages, and inline integration - usmannasir - https://github.com/cyberpanel/testPlugin - MIT - - 3.6+ - 2.2+ - - - true - false - - - true - true - true - true - - /plugins/testPlugin/ - /plugins/testPlugin/settings/ - diff --git a/testPlugin/middleware.py b/testPlugin/middleware.py deleted file mode 100644 index 7d0bd6cc2..000000000 --- a/testPlugin/middleware.py +++ /dev/null @@ -1,208 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Security middleware for the Test Plugin -Provides additional security measures and monitoring -""" -import time -import hashlib -from django.http import JsonResponse -from django.core.cache import cache -from django.conf import settings -from .security import SecurityManager - - -class TestPluginSecurityMiddleware: - """ - Security middleware for the Test Plugin - Provides additional protection against various attacks - """ - - def __init__(self, get_response): - self.get_response = get_response - - def __call__(self, request): - # Only apply security measures to testPlugin URLs - if not request.path.startswith('/testPlugin/'): - return self.get_response(request) - - # Security checks - if not self._security_checks(request): - return JsonResponse({ - 'status': 0, - 'error_message': 'Security violation detected. Access denied.' - }, status=403) - - response = self.get_response(request) - - # Add security headers - self._add_security_headers(response) - - return response - - def _security_checks(self, request): - """Perform security checks on the request""" - - # Check for suspicious patterns - if self._is_suspicious_request(request): - SecurityManager.log_security_event(request, "Suspicious request pattern detected", "suspicious_request") - return False - - # Check for SQL injection attempts - if self._has_sql_injection_patterns(request): - SecurityManager.log_security_event(request, "SQL injection attempt detected", "sql_injection") - return False - - # Check for XSS attempts - if self._has_xss_patterns(request): - SecurityManager.log_security_event(request, "XSS attempt detected", "xss_attempt") - return False - - # Check for path traversal attempts - if self._has_path_traversal_patterns(request): - SecurityManager.log_security_event(request, "Path traversal attempt detected", "path_traversal") - return False - - return True - - def _is_suspicious_request(self, request): - """Check for suspicious request patterns""" - suspicious_patterns = [ - '..', '//', '\\', 'cmd', 'exec', 'system', 'eval', - 'base64', 'decode', 'encode', 'hex', 'binary', - 'union', 'select', 'insert', 'update', 'delete', - 'drop', 'create', 'alter', 'grant', 'revoke' - ] - - # Check URL - url_lower = request.path.lower() - for pattern in suspicious_patterns: - if pattern in url_lower: - return True - - # Check query parameters - for key, value in request.GET.items(): - if isinstance(value, str): - value_lower = value.lower() - for pattern in suspicious_patterns: - if pattern in value_lower: - return True - - # Check POST data - if request.method == 'POST': - for key, value in request.POST.items(): - if isinstance(value, str): - value_lower = value.lower() - for pattern in suspicious_patterns: - if pattern in value_lower: - return True - - return False - - def _has_sql_injection_patterns(self, request): - """Check for SQL injection patterns""" - sql_patterns = [ - "'", '"', ';', '--', '/*', '*/', 'xp_', 'sp_', - 'union', 'select', 'insert', 'update', 'delete', - 'drop', 'create', 'alter', 'exec', 'execute', - 'waitfor', 'delay', 'benchmark', 'sleep' - ] - - # Check all request data - all_data = [] - all_data.extend(request.GET.values()) - all_data.extend(request.POST.values()) - - for value in all_data: - if isinstance(value, str): - value_lower = value.lower() - for pattern in sql_patterns: - if pattern in value_lower: - return True - - return False - - def _has_xss_patterns(self, request): - """Check for XSS patterns""" - xss_patterns = [ - '', 'javascript:', 'vbscript:', - 'onload=', 'onerror=', 'onclick=', 'onmouseover=', - 'onfocus=', 'onblur=', 'onchange=', 'onsubmit=', - 'onreset=', 'onselect=', 'onkeydown=', 'onkeyup=', - 'onkeypress=', 'onmousedown=', 'onmouseup=', - 'onmousemove=', 'onmouseout=', 'oncontextmenu=' - ] - - # Check all request data - all_data = [] - all_data.extend(request.GET.values()) - all_data.extend(request.POST.values()) - - for value in all_data: - if isinstance(value, str): - value_lower = value.lower() - for pattern in xss_patterns: - if pattern in value_lower: - return True - - return False - - def _has_path_traversal_patterns(self, request): - """Check for path traversal patterns""" - traversal_patterns = [ - '../', '..\\', '..%2f', '..%5c', '%2e%2e%2f', - '%2e%2e%5c', '..%252f', '..%255c' - ] - - # Check URL and all request data - all_data = [request.path] - all_data.extend(request.GET.values()) - all_data.extend(request.POST.values()) - - for value in all_data: - if isinstance(value, str): - for pattern in traversal_patterns: - if pattern in value.lower(): - return True - - return False - - def _add_security_headers(self, response): - """Add security headers to the response""" - # Prevent clickjacking - response['X-Frame-Options'] = 'DENY' - - # Prevent MIME type sniffing - response['X-Content-Type-Options'] = 'nosniff' - - # Enable XSS protection - response['X-XSS-Protection'] = '1; mode=block' - - # Strict Transport Security (if HTTPS) - if request.is_secure(): - response['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains' - - # Content Security Policy - response['Content-Security-Policy'] = ( - "default-src 'self'; " - "script-src 'self' 'unsafe-inline' 'unsafe-eval'; " - "style-src 'self' 'unsafe-inline'; " - "img-src 'self' data: https:; " - "font-src 'self' data:; " - "connect-src 'self'; " - "frame-ancestors 'none';" - ) - - # Referrer Policy - response['Referrer-Policy'] = 'strict-origin-when-cross-origin' - - # Permissions Policy - response['Permissions-Policy'] = ( - "geolocation=(), " - "microphone=(), " - "camera=(), " - "payment=(), " - "usb=(), " - "magnetometer=(), " - "gyroscope=(), " - "accelerometer=()" - ) diff --git a/testPlugin/models.py b/testPlugin/models.py deleted file mode 100644 index 146b361e7..000000000 --- a/testPlugin/models.py +++ /dev/null @@ -1,35 +0,0 @@ -# -*- coding: utf-8 -*- -from django.db import models -from django.contrib.auth.models import User - - -class TestPluginSettings(models.Model): - """Model to store plugin settings and enable/disable state""" - user = models.ForeignKey(User, on_delete=models.CASCADE, null=True, blank=True) - plugin_enabled = models.BooleanField(default=True, help_text="Enable or disable the plugin") - test_count = models.IntegerField(default=0, help_text="Number of times test button was clicked") - last_test_time = models.DateTimeField(auto_now=True, help_text="Last time test button was clicked") - custom_message = models.TextField(default="Test plugin is working!", help_text="Custom message for popup") - - class Meta: - verbose_name = "Test Plugin Settings" - verbose_name_plural = "Test Plugin Settings" - - def __str__(self): - return f"Test Plugin Settings - Enabled: {self.plugin_enabled}" - - -class TestPluginLog(models.Model): - """Model to store plugin activity logs""" - timestamp = models.DateTimeField(auto_now_add=True) - action = models.CharField(max_length=100) - message = models.TextField() - user = models.ForeignKey(User, on_delete=models.CASCADE, null=True, blank=True) - - class Meta: - verbose_name = "Test Plugin Log" - verbose_name_plural = "Test Plugin Logs" - ordering = ['-timestamp'] - - def __str__(self): - return f"{self.timestamp} - {self.action}: {self.message}" diff --git a/testPlugin/os_config.py b/testPlugin/os_config.py deleted file mode 100644 index b817d7183..000000000 --- a/testPlugin/os_config.py +++ /dev/null @@ -1,365 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Operating System Configuration for Test Plugin -Provides OS-specific configurations and compatibility checks -""" -import os -import platform -import subprocess -import sys -from pathlib import Path - - -class OSConfig: - """Operating System Configuration Manager""" - - def __init__(self): - self.os_name = self._detect_os_name() - self.os_version = self._detect_os_version() - self.os_arch = platform.machine() - self.python_path = self._detect_python_path() - self.pip_path = self._detect_pip_path() - self.service_manager = self._detect_service_manager() - self.web_server = self._detect_web_server() - self.package_manager = self._detect_package_manager() - - def _detect_os_name(self): - """Detect operating system name""" - try: - with open('/etc/os-release', 'r') as f: - for line in f: - if line.startswith('ID='): - return line.split('=')[1].strip().strip('"') - except FileNotFoundError: - pass - - # Fallback detection - if os.path.exists('/etc/redhat-release'): - return 'rhel' - elif os.path.exists('/etc/debian_version'): - return 'debian' - else: - return platform.system().lower() - - def _detect_os_version(self): - """Detect operating system version""" - try: - with open('/etc/os-release', 'r') as f: - for line in f: - if line.startswith('VERSION_ID='): - return line.split('=')[1].strip().strip('"') - except FileNotFoundError: - pass - - # Fallback detection - if os.path.exists('/etc/redhat-release'): - try: - with open('/etc/redhat-release', 'r') as f: - content = f.read() - import re - match = re.search(r'(\d+\.\d+)', content) - if match: - return match.group(1) - except: - pass - - return platform.release() - - def _detect_python_path(self): - """Detect Python executable path""" - # Try different Python commands - python_commands = ['python3', 'python3.11', 'python3.10', 'python3.9', 'python3.8', 'python3.7', 'python3.6', 'python'] - - for cmd in python_commands: - try: - result = subprocess.run([cmd, '--version'], - capture_output=True, text=True, timeout=5) - if result.returncode == 0: - # Check if it's Python 3.6+ - version = result.stdout.strip() - if 'Python 3' in version: - version_num = version.split()[1] - major, minor = map(int, version_num.split('.')[:2]) - if major == 3 and minor >= 6: - return cmd - except (subprocess.TimeoutExpired, FileNotFoundError, ValueError): - continue - - # Fallback to sys.executable - return sys.executable - - def _detect_pip_path(self): - """Detect pip executable path""" - # Try different pip commands - pip_commands = ['pip3', 'pip3.11', 'pip3.10', 'pip3.9', 'pip3.8', 'pip3.7', 'pip3.6', 'pip'] - - for cmd in pip_commands: - try: - result = subprocess.run([cmd, '--version'], - capture_output=True, text=True, timeout=5) - if result.returncode == 0: - return cmd - except (subprocess.TimeoutExpired, FileNotFoundError): - continue - - # Fallback - return 'pip3' - - def _detect_service_manager(self): - """Detect service manager (systemd, init.d, etc.)""" - if os.path.exists('/bin/systemctl') or os.path.exists('/usr/bin/systemctl'): - return 'systemctl' - elif os.path.exists('/etc/init.d'): - return 'service' - else: - return 'systemctl' # Default to systemctl - - def _detect_web_server(self): - """Detect web server""" - if os.path.exists('/etc/apache2') or os.path.exists('/etc/httpd'): - if os.path.exists('/etc/apache2'): - return 'apache2' - else: - return 'httpd' - else: - return 'httpd' # Default - - def _detect_package_manager(self): - """Detect package manager""" - if os.path.exists('/usr/bin/dnf'): - return 'dnf' - elif os.path.exists('/usr/bin/yum'): - return 'yum' - elif os.path.exists('/usr/bin/apt'): - return 'apt' - elif os.path.exists('/usr/bin/apt-get'): - return 'apt-get' - else: - return 'unknown' - - def get_os_info(self): - """Get comprehensive OS information""" - return { - 'name': self.os_name, - 'version': self.os_version, - 'architecture': self.os_arch, - 'python_path': self.python_path, - 'pip_path': self.pip_path, - 'service_manager': self.service_manager, - 'web_server': self.web_server, - 'package_manager': self.package_manager, - 'platform': platform.platform(), - 'python_version': sys.version - } - - def is_supported_os(self): - """Check if the current OS is supported""" - supported_os = [ - 'ubuntu', 'debian', 'almalinux', 'rocky', 'rhel', - 'centos', 'cloudlinux', 'fedora' - ] - return self.os_name in supported_os - - def get_os_specific_config(self): - """Get OS-specific configuration""" - configs = { - 'ubuntu': { - 'python_cmd': 'python3', - 'pip_cmd': 'pip3', - 'service_cmd': 'systemctl', - 'web_server': 'apache2', - 'package_manager': 'apt-get', - 'cyberpanel_user': 'cyberpanel', - 'cyberpanel_group': 'cyberpanel' - }, - 'debian': { - 'python_cmd': 'python3', - 'pip_cmd': 'pip3', - 'service_cmd': 'systemctl', - 'web_server': 'apache2', - 'package_manager': 'apt-get', - 'cyberpanel_user': 'cyberpanel', - 'cyberpanel_group': 'cyberpanel' - }, - 'almalinux': { - 'python_cmd': 'python3', - 'pip_cmd': 'pip3', - 'service_cmd': 'systemctl', - 'web_server': 'httpd', - 'package_manager': 'dnf', - 'cyberpanel_user': 'cyberpanel', - 'cyberpanel_group': 'cyberpanel' - }, - 'rocky': { - 'python_cmd': 'python3', - 'pip_cmd': 'pip3', - 'service_cmd': 'systemctl', - 'web_server': 'httpd', - 'package_manager': 'dnf', - 'cyberpanel_user': 'cyberpanel', - 'cyberpanel_group': 'cyberpanel' - }, - 'rhel': { - 'python_cmd': 'python3', - 'pip_cmd': 'pip3', - 'service_cmd': 'systemctl', - 'web_server': 'httpd', - 'package_manager': 'dnf', - 'cyberpanel_user': 'cyberpanel', - 'cyberpanel_group': 'cyberpanel' - }, - 'centos': { - 'python_cmd': 'python3', - 'pip_cmd': 'pip3', - 'service_cmd': 'systemctl', - 'web_server': 'httpd', - 'package_manager': 'dnf', - 'cyberpanel_user': 'cyberpanel', - 'cyberpanel_group': 'cyberpanel' - }, - 'cloudlinux': { - 'python_cmd': 'python3', - 'pip_cmd': 'pip3', - 'service_cmd': 'systemctl', - 'web_server': 'httpd', - 'package_manager': 'yum', - 'cyberpanel_user': 'cyberpanel', - 'cyberpanel_group': 'cyberpanel' - } - } - - return configs.get(self.os_name, configs['ubuntu']) # Default to Ubuntu config - - def get_python_requirements(self): - """Get Python requirements for the current OS""" - base_requirements = [ - 'Django>=2.2,<4.0', - 'django-cors-headers', - 'Pillow', - 'requests', - 'psutil' - ] - - # OS-specific requirements - os_requirements = { - 'ubuntu': [], - 'debian': [], - 'almalinux': ['python3-devel', 'gcc'], - 'rocky': ['python3-devel', 'gcc'], - 'rhel': ['python3-devel', 'gcc'], - 'centos': ['python3-devel', 'gcc'], - 'cloudlinux': ['python3-devel', 'gcc'] - } - - return base_requirements + os_requirements.get(self.os_name, []) - - def get_install_commands(self): - """Get OS-specific installation commands""" - config = self.get_os_specific_config() - - if config['package_manager'] in ['apt-get', 'apt']: - return { - 'update': 'apt-get update', - 'install_python': 'apt-get install -y python3 python3-pip python3-venv', - 'install_git': 'apt-get install -y git', - 'install_curl': 'apt-get install -y curl', - 'install_dev_tools': 'apt-get install -y build-essential python3-dev' - } - elif config['package_manager'] == 'dnf': - return { - 'update': 'dnf update -y', - 'install_python': 'dnf install -y python3 python3-pip python3-devel', - 'install_git': 'dnf install -y git', - 'install_curl': 'dnf install -y curl', - 'install_dev_tools': 'dnf install -y gcc gcc-c++ make python3-devel' - } - elif config['package_manager'] == 'yum': - return { - 'update': 'yum update -y', - 'install_python': 'yum install -y python3 python3-pip python3-devel', - 'install_git': 'yum install -y git', - 'install_curl': 'yum install -y curl', - 'install_dev_tools': 'yum install -y gcc gcc-c++ make python3-devel' - } - else: - # Fallback to Ubuntu commands - return { - 'update': 'apt-get update', - 'install_python': 'apt-get install -y python3 python3-pip python3-venv', - 'install_git': 'apt-get install -y git', - 'install_curl': 'apt-get install -y curl', - 'install_dev_tools': 'apt-get install -y build-essential python3-dev' - } - - def validate_environment(self): - """Validate the current environment""" - issues = [] - - # Check Python version - try: - result = subprocess.run([self.python_path, '--version'], - capture_output=True, text=True, timeout=5) - if result.returncode == 0: - version = result.stdout.strip() - if 'Python 3' in version: - version_num = version.split()[1] - major, minor = map(int, version_num.split('.')[:2]) - if major < 3 or (major == 3 and minor < 6): - issues.append(f"Python 3.6+ required, found {version}") - else: - issues.append(f"Python 3 required, found {version}") - else: - issues.append("Python not found or not working") - except Exception as e: - issues.append(f"Error checking Python: {e}") - - # Check pip - try: - result = subprocess.run([self.pip_path, '--version'], - capture_output=True, text=True, timeout=5) - if result.returncode != 0: - issues.append("pip not found or not working") - except Exception as e: - issues.append(f"Error checking pip: {e}") - - # Check if OS is supported - if not self.is_supported_os(): - issues.append(f"Unsupported operating system: {self.os_name}") - - return issues - - def get_compatibility_info(self): - """Get compatibility information for the current OS""" - return { - 'os_supported': self.is_supported_os(), - 'python_available': self.python_path is not None, - 'pip_available': self.pip_path is not None, - 'service_manager': self.service_manager, - 'web_server': self.web_server, - 'package_manager': self.package_manager, - 'validation_issues': self.validate_environment() - } - - -# Global instance -os_config = OSConfig() - - -def get_os_config(): - """Get the global OS configuration instance""" - return os_config - - -def is_os_supported(): - """Check if the current OS is supported""" - return os_config.is_supported_os() - - -def get_python_path(): - """Get the Python executable path""" - return os_config.python_path - - -def get_pip_path(): - """Get the pip executable path""" - return os_config.pip_path diff --git a/testPlugin/security.py b/testPlugin/security.py deleted file mode 100644 index d9c969492..000000000 --- a/testPlugin/security.py +++ /dev/null @@ -1,256 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Security utilities for the Test Plugin -Provides rate limiting, input validation, and security logging -Multi-OS compatible security implementation -""" -import time -import hashlib -import hmac -import json -import re -import os -import platform -from django.core.cache import cache -from django.conf import settings -from django.http import JsonResponse -from django.utils import timezone -from django.contrib.auth.models import User -from functools import wraps -from .models import TestPluginLog -from .os_config import get_os_config - - -class SecurityManager: - """Centralized security management for the plugin""" - - # Rate limiting settings - RATE_LIMIT_WINDOW = 300 # 5 minutes - MAX_REQUESTS_PER_WINDOW = 50 - MAX_FAILED_ATTEMPTS = 5 - LOCKOUT_DURATION = 900 # 15 minutes - - # Input validation patterns - SAFE_STRING_PATTERN = re.compile(r'^[a-zA-Z0-9\s\-_.,!?@#$%^&*()+=\[\]{}|\\:";\'<>?/~`]*$') - MAX_MESSAGE_LENGTH = 1000 - - @staticmethod - def is_rate_limited(request): - """Check if user has exceeded rate limits""" - user_id = request.user.id if request.user.is_authenticated else request.META.get('REMOTE_ADDR') - cache_key = f"rate_limit_{user_id}" - - current_time = time.time() - requests = cache.get(cache_key, []) - - # Remove old requests outside the window - requests = [req_time for req_time in requests if current_time - req_time < SecurityManager.RATE_LIMIT_WINDOW] - - if len(requests) >= SecurityManager.MAX_REQUESTS_PER_WINDOW: - return True - - # Add current request - requests.append(current_time) - cache.set(cache_key, requests, SecurityManager.RATE_LIMIT_WINDOW) - return False - - @staticmethod - def is_user_locked_out(request): - """Check if user is temporarily locked out due to failed attempts""" - user_id = request.user.id if request.user.is_authenticated else request.META.get('REMOTE_ADDR') - lockout_key = f"lockout_{user_id}" - - return cache.get(lockout_key, False) - - @staticmethod - def record_failed_attempt(request, reason="Invalid request"): - """Record a failed security attempt""" - user_id = request.user.id if request.user.is_authenticated else request.META.get('REMOTE_ADDR') - failed_key = f"failed_attempts_{user_id}" - - attempts = cache.get(failed_key, 0) + 1 - cache.set(failed_key, attempts, SecurityManager.RATE_LIMIT_WINDOW) - - # Log security event - SecurityManager.log_security_event(request, f"Failed attempt: {reason}", "security_failure") - - # Lock out user if too many failed attempts - if attempts >= SecurityManager.MAX_FAILED_ATTEMPTS: - lockout_key = f"lockout_{user_id}" - cache.set(lockout_key, True, SecurityManager.LOCKOUT_DURATION) - SecurityManager.log_security_event(request, "User locked out due to excessive failed attempts", "user_locked_out") - - @staticmethod - def clear_failed_attempts(request): - """Clear failed attempts for user after successful action""" - user_id = request.user.id if request.user.is_authenticated else request.META.get('REMOTE_ADDR') - failed_key = f"failed_attempts_{user_id}" - cache.delete(failed_key) - - @staticmethod - def validate_input(data, field_name, max_length=None): - """Validate input data for security""" - if not isinstance(data, str): - return False, f"{field_name} must be a string" - - if max_length and len(data) > max_length: - return False, f"{field_name} exceeds maximum length of {max_length} characters" - - if not SecurityManager.SAFE_STRING_PATTERN.match(data): - return False, f"{field_name} contains invalid characters" - - return True, "Valid" - - @staticmethod - def sanitize_input(data): - """Sanitize input data""" - if isinstance(data, str): - # Remove potential XSS vectors - data = data.replace('', '</script>') - data = data.replace('javascript:', '') - data = data.replace('onload=', '') - data = data.replace('onerror=', '') - data = data.replace('onclick=', '') - data = data.replace('onmouseover=', '') - # Remove null bytes - data = data.replace('\x00', '') - # Limit length - data = data[:SecurityManager.MAX_MESSAGE_LENGTH] - - return data - - @staticmethod - def log_security_event(request, message, event_type="security"): - """Log security-related events""" - try: - user_id = request.user.id if request.user.is_authenticated else None - ip_address = request.META.get('REMOTE_ADDR', 'unknown') - user_agent = request.META.get('HTTP_USER_AGENT', 'unknown') - - TestPluginLog.objects.create( - user_id=user_id, - action=event_type, - message=f"[SECURITY] {message} | IP: {ip_address} | UA: {user_agent[:100]}" - ) - except Exception: - # Don't let logging errors break the application - pass - - @staticmethod - def generate_csrf_token(request): - """Generate a secure CSRF token""" - if hasattr(request, 'csrf_token'): - return request.csrf_token - - # Fallback CSRF token generation - secret = getattr(settings, 'SECRET_KEY', 'fallback-secret') - timestamp = str(int(time.time())) - user_id = str(request.user.id) if request.user.is_authenticated else 'anonymous' - - token_data = f"{user_id}:{timestamp}" - token = hmac.new( - secret.encode(), - token_data.encode(), - hashlib.sha256 - ).hexdigest() - - return f"{token}:{timestamp}" - - @staticmethod - def verify_csrf_token(request, token): - """Verify CSRF token""" - if hasattr(request, 'csrf_token'): - return request.csrf_token == token - - try: - secret = getattr(settings, 'SECRET_KEY', 'fallback-secret') - token_part, timestamp = token.split(':') - - # Check if token is not too old (1 hour) - if time.time() - int(timestamp) > 3600: - return False - - user_id = str(request.user.id) if request.user.is_authenticated else 'anonymous' - token_data = f"{user_id}:{timestamp}" - expected_token = hmac.new( - secret.encode(), - token_data.encode(), - hashlib.sha256 - ).hexdigest() - - return hmac.compare_digest(token_part, expected_token) - except (ValueError, AttributeError): - return False - - -def secure_view(require_csrf=True, rate_limit=True, log_activity=True): - """Decorator for secure view functions""" - def decorator(view_func): - @wraps(view_func) - def wrapper(request, *args, **kwargs): - # Check if user is locked out - if SecurityManager.is_user_locked_out(request): - SecurityManager.log_security_event(request, "Blocked request from locked out user", "blocked_request") - return JsonResponse({ - 'status': 0, - 'error_message': 'Account temporarily locked due to security violations. Please try again later.' - }, status=423) - - # Check rate limiting - if rate_limit and SecurityManager.is_rate_limited(request): - SecurityManager.record_failed_attempt(request, "Rate limit exceeded") - return JsonResponse({ - 'status': 0, - 'error_message': 'Too many requests. Please slow down and try again later.' - }, status=429) - - # CSRF protection - if require_csrf and request.method == 'POST': - csrf_token = request.META.get('HTTP_X_CSRFTOKEN') or request.POST.get('csrfmiddlewaretoken') - if not csrf_token or not SecurityManager.verify_csrf_token(request, csrf_token): - SecurityManager.record_failed_attempt(request, "Invalid CSRF token") - return JsonResponse({ - 'status': 0, - 'error_message': 'Invalid security token. Please refresh the page and try again.' - }, status=403) - - # Log activity - if log_activity: - SecurityManager.log_security_event(request, f"Accessing {view_func.__name__}", "view_access") - - try: - result = view_func(request, *args, **kwargs) - # Clear failed attempts on successful request - SecurityManager.clear_failed_attempts(request) - return result - except Exception as e: - SecurityManager.log_security_event(request, f"Error in {view_func.__name__}: {str(e)}", "view_error") - return JsonResponse({ - 'status': 0, - 'error_message': 'An internal error occurred. Please try again later.' - }, status=500) - - return wrapper - return decorator - - -def admin_required(view_func): - """Decorator to ensure only admin users can access the view""" - @wraps(view_func) - def wrapper(request, *args, **kwargs): - if not request.user.is_authenticated: - return JsonResponse({ - 'status': 0, - 'error_message': 'Authentication required.' - }, status=401) - - if not request.user.is_staff and not request.user.is_superuser: - SecurityManager.log_security_event(request, "Unauthorized access attempt by non-admin user", "unauthorized_access") - return JsonResponse({ - 'status': 0, - 'error_message': 'Admin privileges required.' - }, status=403) - - return view_func(request, *args, **kwargs) - return wrapper diff --git a/testPlugin/signals.py b/testPlugin/signals.py deleted file mode 100644 index 1a3d1cd6b..000000000 --- a/testPlugin/signals.py +++ /dev/null @@ -1,15 +0,0 @@ -# -*- coding: utf-8 -*- -from django.db.models.signals import post_save -from django.dispatch import receiver -from django.contrib.auth.models import User -from .models import TestPluginSettings - - -@receiver(post_save, sender=User) -def create_user_settings(sender, instance, created, **kwargs): - """Create default plugin settings when a new user is created""" - if created: - TestPluginSettings.objects.create( - user=instance, - plugin_enabled=True - ) diff --git a/testPlugin/static/testPlugin/css/testPlugin.css b/testPlugin/static/testPlugin/css/testPlugin.css deleted file mode 100644 index 4e978906c..000000000 --- a/testPlugin/static/testPlugin/css/testPlugin.css +++ /dev/null @@ -1,418 +0,0 @@ -/* Test Plugin CSS - Additional styles for better integration */ - -/* Popup Message Animations */ -@keyframes slideInRight { - from { - transform: translateX(100%); - opacity: 0; - } - to { - transform: translateX(0); - opacity: 1; - } -} - -@keyframes slideOutRight { - from { - transform: translateX(0); - opacity: 1; - } - to { - transform: translateX(100%); - opacity: 0; - } -} - -/* Enhanced Button Styles */ -.btn-test { - position: relative; - overflow: hidden; -} - -.btn-test::before { - content: ''; - position: absolute; - top: 50%; - left: 50%; - width: 0; - height: 0; - background: rgba(255, 255, 255, 0.3); - border-radius: 50%; - transform: translate(-50%, -50%); - transition: width 0.6s, height 0.6s; -} - -.btn-test:active::before { - width: 300px; - height: 300px; -} - -/* Toggle Switch Enhanced */ -.toggle-switch { - position: relative; - display: inline-block; - width: 60px; - height: 34px; -} - -.toggle-switch input { - opacity: 0; - width: 0; - height: 0; -} - -.slider { - position: absolute; - cursor: pointer; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: #ccc; - transition: .4s; - border-radius: 34px; -} - -.slider:before { - position: absolute; - content: ""; - height: 26px; - width: 26px; - left: 4px; - bottom: 4px; - background-color: white; - transition: .4s; - border-radius: 50%; - box-shadow: 0 2px 4px rgba(0,0,0,0.2); -} - -input:checked + .slider { - background-color: #5856d6; -} - -input:checked + .slider:before { - transform: translateX(26px); -} - -/* Loading States */ -.loading { - opacity: 0.6; - pointer-events: none; -} - -.loading::after { - content: ''; - position: absolute; - top: 50%; - left: 50%; - width: 20px; - height: 20px; - margin: -10px 0 0 -10px; - border: 2px solid transparent; - border-top: 2px solid currentColor; - border-radius: 50%; - animation: spin 1s linear infinite; -} - -@keyframes spin { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } -} - -/* Card Hover Effects */ -.plugin-card { - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); -} - -.plugin-card:hover { - transform: translateY(-8px); - box-shadow: 0 12px 32px rgba(0,0,0,0.15); -} - -/* Status Indicators */ -.status-indicator { - display: inline-flex; - align-items: center; - gap: 8px; - padding: 6px 12px; - border-radius: 20px; - font-size: 12px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.5px; -} - -.status-indicator.enabled { - background: #e8f5e8; - color: #388e3c; -} - -.status-indicator.disabled { - background: #ffebee; - color: #d32f2f; -} - -/* Responsive Enhancements */ -@media (max-width: 768px) { - .control-row { - flex-direction: column; - align-items: stretch; - } - - .control-group { - justify-content: space-between; - margin-bottom: 15px; - } - - .stats-grid { - grid-template-columns: 1fr; - gap: 15px; - } - - .logs-table { - font-size: 12px; - } - - .logs-table th, - .logs-table td { - padding: 8px 4px; - } -} - -@media (max-width: 480px) { - .test-plugin-wrapper { - padding: 10px; - } - - .plugin-header, - .control-panel, - .settings-form, - .logs-content { - padding: 15px; - } - - .plugin-header h1 { - font-size: 24px; - flex-direction: column; - text-align: center; - } - - .btn-test, - .btn-secondary { - width: 100%; - justify-content: center; - margin-bottom: 10px; - } -} - -/* Dark Mode Support */ -@media (prefers-color-scheme: dark) { - :root { - --bg-primary: #1a1a1a; - --bg-secondary: #2d2d2d; - --text-primary: #ffffff; - --text-secondary: #b3b3b3; - --text-tertiary: #808080; - --border-primary: #404040; - --shadow-md: 0 2px 8px rgba(0,0,0,0.3); - --shadow-lg: 0 8px 24px rgba(0,0,0,0.4); - } -} - -/* Print Styles */ -@media print { - .test-plugin-wrapper { - background: white !important; - color: black !important; - } - - .btn-test, - .btn-secondary, - .toggle-switch { - display: none !important; - } - - .popup-message { - display: none !important; - } -} - -/* Additional styles for inline elements */ -.popup-message { - position: fixed; - top: 20px; - right: 20px; - background: white; - border-radius: 8px; - padding: 16px 20px; - box-shadow: 0 8px 24px rgba(0,0,0,0.15); - border-left: 4px solid #10b981; - z-index: 9999; - max-width: 400px; - transform: translateX(100%); - transition: transform 0.3s ease; -} - -.popup-message.show { - transform: translateX(0); -} - -.popup-message.error { - border-left-color: #ef4444; -} - -.popup-message.warning { - border-left-color: #f59e0b; -} - -.popup-title { - font-weight: 600; - color: var(--text-primary, #2f3640); - margin-bottom: 4px; -} - -.popup-content { - font-size: 14px; - color: var(--text-secondary, #64748b); - margin-bottom: 8px; -} - -.popup-time { - font-size: 12px; - color: var(--text-tertiary, #9ca3af); -} - -.popup-close { - position: absolute; - top: 8px; - right: 8px; - background: none; - border: none; - font-size: 18px; - cursor: pointer; - color: var(--text-tertiary, #9ca3af); -} - -.notification { - position: fixed; - top: 20px; - right: 20px; - background: white; - border-radius: 8px; - padding: 16px 20px; - box-shadow: 0 8px 24px rgba(0,0,0,0.15); - border-left: 4px solid #10b981; - z-index: 9999; - max-width: 400px; - transform: translateX(100%); - transition: transform 0.3s ease; -} - -.notification.show { - transform: translateX(0); -} - -.notification.error { - border-left-color: #ef4444; -} - -.notification-title { - font-weight: 600; - color: var(--text-primary, #2f3640); - margin-bottom: 4px; -} - -.notification-content { - font-size: 14px; - color: var(--text-secondary, #64748b); -} - -.popup-container { - position: fixed; - top: 0; - right: 0; - z-index: 9999; - pointer-events: none; -} - -/* OS Compatibility Styles */ -.compatibility-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); - gap: 20px; - margin: 20px 0; -} - -.os-card { - background: var(--bg-secondary, #f8f9ff); - border: 1px solid var(--border-primary, #e8e9ff); - border-radius: 8px; - padding: 20px; - transition: all 0.3s ease; -} - -.os-card:hover { - transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(0,0,0,0.1); -} - -.os-card h3 { - color: var(--text-primary, #2f3640); - margin-bottom: 15px; - font-size: 18px; - font-weight: 600; -} - -.os-card ul { - list-style: none; - padding: 0; - margin: 0 0 15px 0; -} - -.os-card li { - padding: 5px 0; - color: var(--text-secondary, #64748b); - font-size: 14px; -} - -.os-card p { - margin: 5px 0; - color: var(--text-secondary, #64748b); - font-size: 13px; -} - -.troubleshooting-section { - margin: 20px 0; -} - -.troubleshooting-section h4 { - color: var(--text-primary, #2f3640); - margin: 15px 0 10px 0; - font-size: 16px; - font-weight: 600; -} - -.code-block { - background: var(--bg-secondary, #f8f9ff); - border: 1px solid var(--border-primary, #e8e9ff); - border-radius: 6px; - padding: 15px; - margin: 10px 0; - overflow-x: auto; -} - -.code-block pre { - margin: 0; - font-family: 'Courier New', monospace; - font-size: 13px; - line-height: 1.4; - color: var(--text-primary, #2f3640); -} - -.code-block code { - background: none; - padding: 0; - font-family: inherit; - font-size: inherit; - color: inherit; -} diff --git a/testPlugin/static/testPlugin/js/testPlugin.js b/testPlugin/static/testPlugin/js/testPlugin.js deleted file mode 100644 index 988de5538..000000000 --- a/testPlugin/static/testPlugin/js/testPlugin.js +++ /dev/null @@ -1,323 +0,0 @@ -/** - * Test Plugin JavaScript - * Handles all client-side functionality for the test plugin - */ - -class TestPlugin { - constructor() { - this.init(); - } - - init() { - this.bindEvents(); - this.initializeComponents(); - } - - bindEvents() { - // Toggle switch functionality - const toggleSwitch = document.getElementById('plugin-toggle'); - if (toggleSwitch) { - toggleSwitch.addEventListener('change', (e) => this.handleToggle(e)); - } - - // Test button functionality - const testButton = document.getElementById('test-button'); - if (testButton) { - testButton.addEventListener('click', (e) => this.handleTestClick(e)); - } - - // Settings form - const settingsForm = document.getElementById('settings-form'); - if (settingsForm) { - settingsForm.addEventListener('submit', (e) => this.handleSettingsSubmit(e)); - } - - // Log filter - const actionFilter = document.getElementById('action-filter'); - if (actionFilter) { - actionFilter.addEventListener('change', (e) => this.handleLogFilter(e)); - } - } - - initializeComponents() { - // Initialize any components that need setup - this.initializeTooltips(); - this.initializeAnimations(); - } - - async handleToggle(event) { - const toggleSwitch = event.target; - const testButton = document.getElementById('test-button'); - - try { - const response = await fetch('/testPlugin/toggle/', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-CSRFToken': this.getCSRFToken() - } - }); - - const data = await response.json(); - - if (data.status === 1) { - if (testButton) { - testButton.disabled = !data.enabled; - } - this.showNotification('success', 'Plugin Toggle', data.message); - - // Update status indicator if exists - this.updateStatusIndicator(data.enabled); - - // Reload page after a short delay to update UI - setTimeout(() => { - window.location.reload(); - }, 1000); - } else { - this.showNotification('error', 'Error', data.error_message); - // Revert toggle state - toggleSwitch.checked = !toggleSwitch.checked; - } - } catch (error) { - this.showNotification('error', 'Error', 'Failed to toggle plugin'); - // Revert toggle state - toggleSwitch.checked = !toggleSwitch.checked; - } - } - - async handleTestClick(event) { - const testButton = event.target; - - if (testButton.disabled) return; - - // Add loading state - testButton.classList.add('loading'); - testButton.disabled = true; - const originalContent = testButton.innerHTML; - testButton.innerHTML = ' Testing...'; - - try { - const response = await fetch('/testPlugin/test/', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-CSRFToken': this.getCSRFToken() - } - }); - - const data = await response.json(); - - if (data.status === 1) { - // Update test count - const testCountElement = document.getElementById('test-count'); - if (testCountElement) { - testCountElement.textContent = data.test_count; - } - - // Show popup message - this.showPopup( - data.popup_message.type, - data.popup_message.title, - data.popup_message.message - ); - - // Add success animation - testButton.style.background = 'linear-gradient(135deg, #10b981, #059669)'; - setTimeout(() => { - testButton.style.background = ''; - }, 2000); - } else { - this.showNotification('error', 'Error', data.error_message); - } - } catch (error) { - this.showNotification('error', 'Error', 'Failed to execute test'); - } finally { - // Remove loading state - testButton.classList.remove('loading'); - testButton.disabled = false; - testButton.innerHTML = originalContent; - } - } - - async handleSettingsSubmit(event) { - event.preventDefault(); - - const form = event.target; - const formData = new FormData(form); - const data = { - custom_message: formData.get('custom_message') - }; - - try { - const response = await fetch('/testPlugin/update-settings/', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-CSRFToken': this.getCSRFToken() - }, - body: JSON.stringify(data) - }); - - const result = await response.json(); - - if (result.status === 1) { - this.showNotification('success', 'Settings Updated', result.message); - } else { - this.showNotification('error', 'Error', result.error_message); - } - } catch (error) { - this.showNotification('error', 'Error', 'Failed to update settings'); - } - } - - handleLogFilter(event) { - const selectedAction = event.target.value; - const logRows = document.querySelectorAll('.log-row'); - - logRows.forEach(row => { - if (selectedAction === '' || row.dataset.action === selectedAction) { - row.style.display = ''; - } else { - row.style.display = 'none'; - } - }); - } - - showPopup(type, title, message) { - const popupContainer = document.getElementById('popup-container') || this.createPopupContainer(); - const popup = document.createElement('div'); - popup.className = `popup-message ${type}`; - - popup.innerHTML = ` - - - - - `; - - popupContainer.appendChild(popup); - - // Show popup with animation - setTimeout(() => popup.classList.add('show'), 100); - - // Auto remove after 5 seconds - setTimeout(() => { - popup.classList.remove('show'); - setTimeout(() => popup.remove(), 300); - }, 5000); - } - - showNotification(type, title, message) { - const notification = document.createElement('div'); - notification.className = `notification ${type}`; - - notification.innerHTML = ` -
${title}
-
${message}
- `; - - document.body.appendChild(notification); - - // Show notification - setTimeout(() => notification.classList.add('show'), 100); - - // Auto remove after 3 seconds - setTimeout(() => { - notification.classList.remove('show'); - setTimeout(() => notification.remove(), 300); - }, 3000); - } - - createPopupContainer() { - const container = document.createElement('div'); - container.id = 'popup-container'; - container.className = 'popup-container'; - document.body.appendChild(container); - return container; - } - - updateStatusIndicator(enabled) { - const statusElements = document.querySelectorAll('.status-indicator'); - statusElements.forEach(element => { - element.className = `status-indicator ${enabled ? 'enabled' : 'disabled'}`; - element.innerHTML = enabled ? - ' Enabled' : - ' Disabled'; - }); - } - - initializeTooltips() { - // Add tooltips to buttons and controls - const elements = document.querySelectorAll('[data-tooltip]'); - elements.forEach(element => { - element.addEventListener('mouseenter', (e) => this.showTooltip(e)); - element.addEventListener('mouseleave', (e) => this.hideTooltip(e)); - }); - } - - showTooltip(event) { - const element = event.target; - const tooltipText = element.dataset.tooltip; - - if (!tooltipText) return; - - const tooltip = document.createElement('div'); - tooltip.className = 'tooltip'; - tooltip.textContent = tooltipText; - tooltip.style.cssText = ` - position: absolute; - background: #333; - color: white; - padding: 8px 12px; - border-radius: 4px; - font-size: 12px; - z-index: 10000; - pointer-events: none; - white-space: nowrap; - `; - - document.body.appendChild(tooltip); - - const rect = element.getBoundingClientRect(); - tooltip.style.left = rect.left + (rect.width / 2) - (tooltip.offsetWidth / 2) + 'px'; - tooltip.style.top = rect.top - tooltip.offsetHeight - 8 + 'px'; - - element._tooltip = tooltip; - } - - hideTooltip(event) { - const element = event.target; - if (element._tooltip) { - element._tooltip.remove(); - delete element._tooltip; - } - } - - initializeAnimations() { - // Add entrance animations to cards - const cards = document.querySelectorAll('.plugin-card, .stat-card, .log-item'); - cards.forEach((card, index) => { - card.style.opacity = '0'; - card.style.transform = 'translateY(20px)'; - card.style.transition = 'opacity 0.6s ease, transform 0.6s ease'; - - setTimeout(() => { - card.style.opacity = '1'; - card.style.transform = 'translateY(0)'; - }, index * 100); - }); - } - - getCSRFToken() { - const token = document.querySelector('[name=csrfmiddlewaretoken]'); - return token ? token.value : ''; - } -} - -// Initialize the plugin when DOM is loaded -document.addEventListener('DOMContentLoaded', function() { - new TestPlugin(); -}); - -// Export for potential external use -window.TestPlugin = TestPlugin; diff --git a/testPlugin/templates/testPlugin/index.html b/testPlugin/templates/testPlugin/index.html deleted file mode 100644 index ed5250a1e..000000000 --- a/testPlugin/templates/testPlugin/index.html +++ /dev/null @@ -1,71 +0,0 @@ -{% extends "baseTemplate/base.html" %} -{% load static %} -{% load i18n %} - -{% block title %} - Test Plugin - {% trans "CyberPanel" %} -{% endblock %} - -{% block content %} -
-
-
-
-
-

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

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

{% trans "Plugin Information" %}

-

{{ description }}

-
- -
-
-

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

-
-
-
- - {% trans "Test Plugin is working correctly!" %} -
-

{% trans "This is a test plugin created for testing CyberPanel plugin functionality." %}

-
-
-
-
-
-
-
-{% endblock %} diff --git a/testPlugin/templates/testPlugin/plugin_docs.html b/testPlugin/templates/testPlugin/plugin_docs.html deleted file mode 100644 index 1d4d16be1..000000000 --- a/testPlugin/templates/testPlugin/plugin_docs.html +++ /dev/null @@ -1,1624 +0,0 @@ -{% extends "baseTemplate/index.html" %} -{% load i18n %} -{% load static %} - -{% block title %}{% trans "Plugin Development Guide - CyberPanel" %}{% endblock %} - -{% block header_scripts %} - -{% endblock %} - -{% block content %} -
-
- -
-

- - {% trans "Plugin Development Documentation" %} -

-

{% trans "Complete guide for developing, installing, and managing CyberPanel plugins" %}

-
- - - - - -
-
-

Quick Installation Guide - CyberPanel Test Plugin

- -
-
-

🚀 One-Command Installation

-

Install the test plugin with a single command using curl or wget.

-
-
-

📦 Manual Installation

-

Download and install manually with step-by-step instructions.

-
-
-

⚙️ Easy Management

-

Simple install/uninstall process with proper cleanup.

-
-
- -

One-Command Installation

-
# Install the test plugin with a single command
-curl -sSL https://raw.githubusercontent.com/cyberpanel/testPlugin/main/install.sh | bash
- -

Manual Installation

-
    -
  1. Download the plugin -
    git clone https://github.com/cyberpanel/testPlugin.git
    -cd testPlugin
    -
  2. -
  3. Run the installation script -
    chmod +x install.sh
    -./install.sh
    -
  4. -
  5. Access the plugin -
      -
    • URL: https://your-domain:8090/testPlugin/
    • -
    • Login with your CyberPanel admin credentials
    • -
    -
  6. -
- -

Features Included

-
-
-

✅ Enable/Disable Toggle

-

Toggle the plugin on/off with a beautiful switch

-
-
-

✅ Test Button

-

Click to show popup messages from the side

-
-
-

✅ Settings Page

-

Configure custom messages and preferences

-
-
-

✅ Activity Logs

-

View all plugin activities with filtering

-
-
-

✅ Inline Integration

-

Loads within CyberPanel interface

-
-
-

✅ Responsive Design

-

Works perfectly on all devices

-
-
- -

Uninstallation

-
# Uninstall the plugin
-./install.sh --uninstall
- -

Troubleshooting

-

If you encounter any issues:

-
    -
  1. Check CyberPanel logs -
    tail -f /home/cyberpanel/logs/cyberpanel.log
    -
  2. -
  3. Restart CyberPanel services -
    systemctl restart lscpd
    -systemctl restart cyberpanel
    -
  4. -
  5. Verify installation -
    ls -la /home/cyberpanel/plugins/testPlugin
    -ls -la /usr/local/CyberCP/testPlugin
    -
  6. -
- -
- Note: This plugin is designed for testing and development purposes. Always backup your system before installing any plugins. -
-
-
- - -
-
-

Getting Started with CyberPanel Plugin Development

- -
-
-

🎯 Official Documentation

-

Based on the official CyberPanel plugin development guide from the CyberPanel team.

-
-
-

📚 Step-by-Step Tutorial

-

Complete walkthrough from development environment setup to plugin installation.

-
-
-

🔧 Signal Integration

-

Learn how to hook into CyberPanel events and respond to core functionality.

-
-
- -
- Source: This guide is based on the official CyberPanel documentation and the beautiful_names plugin repository. -
- -

Prerequisites

-
    -
  • Python - Clear understanding of Python Programming Language
  • -
  • Django - Experience with Django framework
  • -
  • HTML (Basic) - Basic HTML knowledge
  • -
  • CSS (Basic) - Basic CSS knowledge
  • -
- -

Note: You can use plain JavaScript in your plugins or any JavaScript framework. You just have to follow the norms of Django framework, because CyberPanel plugin is just another Django app.

- -

Step 1: Set up your Development Environment

- -

Clone CyberPanel Repository

-
git clone https://github.com/usmannasir/cyberpanel/ --single-branch v1.7.2-plugin
- -

Create a Django App

-
cd v1.7.2-plugin
-django-admin startapp pluginName
- -

Choose your plugin name wisely as it's of great importance. Once the Django app is created, you need to define a meta file for your plugin so that CyberPanel can read information about your plugin.

- -

Create Meta File

-
cd pluginName
-nano meta.xml
- -

Paste the following content in the meta.xml file:

-
<?xml version="1.0" encoding="UTF-8"?>
-<cyberpanelPluginConfig>
-  <name>customplugin</name>
-  <type>plugin</type>
-  <description>Plugin to make custom changes</description>
-  <version>0</version>
-</cyberpanelPluginConfig>
- -

Step 2: Creating a Signal File and Adjusting Settings

- -

Create Signals File

-

Create a signals.py file (you can name it anything, but signals.py is recommended). You can leave this file empty for now.

- -

Configure apps.py

-

In your apps.py file, you need to import the signals file inside the ready function:

-
def ready(self):
-    import signals
- -

Configure __init__.py

-

You need to specify a default_app_config variable in this file:

-
default_app_config = 'examplePlugin.apps.ExamplepluginConfig'
- -

Create urls.py

-

Inside your app root directory, create urls.py and paste this content:

-
from django.conf.urls import url
-import views
-
-urlpatterns = [
-    url(r'^$', views.examplePlugin, name='examplePlugin'),
-]
- -

Important: Replace examplePlugin with your plugin name. This URL definition is very important for CyberPanel to register your plugin page.

- -

Optional Files

-

You can create these optional files for database model management:

-
    -
  • pre_install - Executed before installation of plugin
  • -
  • post_install - Executed after installation of plugin
  • -
- -

If your file is Python code, don't forget to include this line at the top:

-
#!/usr/local/CyberCP/bin/python2
- -

Step 3: Responding to Events

- -

To plug into events fired by CyberPanel core, you can respond to various events happening in the core. Visit the signal file documentation for a complete list of events.

- -

Example Events

-
    -
  • preWebsiteCreation - Fired before CyberPanel starts the creation of website
  • -
  • postWebsiteDeletion - Fired after core finished the deletion of website
  • -
- -

Responding to Events

-

Here's how you can respond to the postWebsiteDeletion event:

-
from django.dispatch import receiver
-from django.http import HttpResponse
-from websiteFunctions.signals import postWebsiteDeletion
-from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter as logging
-
-@receiver(postWebsiteDeletion)
-def rcvr(sender, **kwargs):
-    request = kwargs['request']
-    logging.writeToFile('Hello World from Example Plugin.')
-    return HttpResponse('Hello World from Example Plugin.')
- -

Return Values

-
    -
  • HttpResponse object - CyberPanel core will stop further processing and return your response to browser
  • -
  • int 200 - CyberPanel core will continue processing, assuming the event was successfully executed
  • -
- -

Step 4: Packing, Shipping and Installing Plugin

- -

Package Your Plugin

-

After completing your plugin, zip your Django app. The zip file name should be your plugin name (e.g., examplePlugin.zip), otherwise installation will fail.

- -

Installation

-

First, upload your plugin to /usr/local/CyberCP/pluginInstaller:

-
cd /usr/local/CyberCP/pluginInstaller
-python pluginInstaller.py install --pluginName examplePlugin
- -

Uninstall

-
cd /usr/local/CyberCP/pluginInstaller
-python pluginInstaller.py remove --pluginName examplePlugin
- -

Beautiful Names Plugin Example

-

CyberPanel has released an official plugin called Beautiful Names that removes the admin_ prefix from Package and FTP account names. This plugin serves as a great example of how to create CyberPanel plugins.

- -

Installation of Beautiful Names

-
cd /usr/local/CyberCP/pluginInstaller
-wget https://cyberpanel.net/beautifulNames.zip
-python pluginInstaller.py install --pluginName beautifulNames
- -

Uninstall Beautiful Names

-
cd /usr/local/CyberCP/pluginInstaller
-python pluginInstaller.py remove --pluginName beautifulNames
- -

Plugin Installation Facility

-

The plugin installation facility is in beta and not available with the official install yet. To install plugins, you need to install CyberPanel via the test version:

-
sh <(curl https://mirror.cyberpanel.net/install-test.sh || wget -O - https://mirror.cyberpanel.net/install-test.sh)
- -

Additional Resources

- - -
- Note: This guide is based on the official CyberPanel documentation. For the most up-to-date information, always refer to the official sources. -
-
-
- - -
-
-

CyberPanel Plugin Development Guide

- - - -
-

How to Install Plugins

- -

Method 1: Using the Installation Script (Recommended)

-
# Download and run the installation script
-curl -sSL https://raw.githubusercontent.com/cyberpanel/testPlugin/main/install.sh | bash
-
-# Or download first, then run
-wget https://raw.githubusercontent.com/cyberpanel/testPlugin/main/install.sh
-chmod +x install.sh
-./install.sh
- -

Method 2: Manual Installation

-
    -
  1. Create Plugin Directory Structure -
    mkdir -p /home/cyberpanel/plugins/yourPlugin
    -mkdir -p /usr/local/CyberCP/yourPlugin
    -
  2. -
  3. Copy Plugin Files -
    cp -r yourPlugin/* /usr/local/CyberCP/yourPlugin/
    -chown -R cyberpanel:cyberpanel /usr/local/CyberCP/yourPlugin
    -chmod -R 755 /usr/local/CyberCP/yourPlugin
    -
  4. -
  5. Create Symlink -
    ln -sf /usr/local/CyberCP/yourPlugin /home/cyberpanel/plugins/yourPlugin
    -
  6. -
  7. Update Django Settings -

    Add your plugin to INSTALLED_APPS in /usr/local/CyberCP/cyberpanel/settings.py:

    -
    INSTALLED_APPS = [
    -    # ... existing apps ...
    -    'yourPlugin',
    -]
    -
  8. -
  9. Update URL Configuration -

    Add your plugin URLs in /usr/local/CyberCP/cyberpanel/urls.py:

    -
    urlpatterns = [
    -    # ... existing patterns ...
    -    path("yourPlugin/", include("yourPlugin.urls")),
    -]
    -
  10. -
  11. Run Migrations -
    cd /usr/local/CyberCP
    -python3 manage.py makemigrations yourPlugin
    -python3 manage.py migrate yourPlugin
    -
  12. -
  13. Collect Static Files -
    python3 manage.py collectstatic --noinput
    -
  14. -
  15. Restart Services -
    systemctl restart lscpd
    -systemctl restart cyberpanel
    -
  16. -
-
- -
-

How to Uninstall Plugins

- -

Method 1: Using the Installation Script

-
# Run with uninstall flag
-./install.sh --uninstall
- -

Method 2: Manual Uninstallation

-
    -
  1. Remove Plugin Files -
    rm -rf /usr/local/CyberCP/yourPlugin
    -rm -f /home/cyberpanel/plugins/yourPlugin
    -
  2. -
  3. Remove from Django Settings -
    sed -i '/yourPlugin/d' /usr/local/CyberCP/cyberpanel/settings.py
    -
  4. -
  5. Remove from URLs -
    sed -i '/yourPlugin/d' /usr/local/CyberCP/cyberpanel/urls.py
    -
  6. -
  7. Restart Services -
    systemctl restart lscpd
    -systemctl restart cyberpanel
    -
  8. -
-
- -
-

How to Add meta.xml

-

Create a meta.xml file in your plugin root directory:

-
<?xml version="1.0" encoding="UTF-8"?>
-<plugin>
-    <name>Your Plugin Name</name>
-    <type>Utility</type>
-    <description>Your plugin description</description>
-    <version>1.0.0</version>
-    <author>Your Name</author>
-    <website>https://your-website.com</website>
-    <license>MIT</license>
-    <dependencies>
-        <python>3.6+</python>
-        <django>2.2+</django>
-    </dependencies>
-    <permissions>
-        <admin>true</admin>
-        <user>false</user>
-    </permissions>
-    <settings>
-        <enable_toggle>true</enable_toggle>
-        <test_button>true</test_button>
-        <popup_messages>true</popup_messages>
-        <inline_integration>true</inline_integration>
-    </settings>
-</plugin>
- -

Required Fields:

-
    -
  • name: Plugin display name
  • -
  • type: Plugin category (Utility, Security, Performance, etc.)
  • -
  • description: Plugin description
  • -
  • version: Plugin version
  • -
- -

Optional Fields:

-
    -
  • author: Plugin author
  • -
  • website: Plugin website
  • -
  • license: License type
  • -
  • dependencies: Required dependencies
  • -
  • permissions: Access permissions
  • -
  • settings: Plugin-specific settings
  • -
-
- -
-

How to Add Buttons for Pages

- -

1. In Your Template

-
<!-- Primary Action Button -->
-<button class="btn-test" id="your-button">
-    <i class="fas fa-icon"></i>
-    Button Text
-</button>
-
-<!-- Secondary Button -->
-<a href="{% url 'yourPlugin:your_view' %}" class="btn-secondary">
-    <i class="fas fa-icon"></i>
-    Button Text
-</a>
-
-<!-- Danger Button -->
-<button class="btn-danger" id="danger-button">
-    <i class="fas fa-trash"></i>
-    Delete
-</button>
- -

2. CSS Styles

-
.btn-test {
-    background: linear-gradient(135deg, #5856d6, #4a90e2);
-    color: white;
-    border: none;
-    padding: 12px 24px;
-    border-radius: 8px;
-    font-weight: 600;
-    cursor: pointer;
-    transition: all 0.3s ease;
-    display: flex;
-    align-items: center;
-    gap: 8px;
-}
-
-.btn-test:hover {
-    transform: translateY(-2px);
-    box-shadow: 0 8px 20px rgba(88,86,214,0.3);
-}
-
-.btn-test:disabled {
-    opacity: 0.6;
-    cursor: not-allowed;
-    transform: none;
-}
- -

3. JavaScript Event Handling

-
document.getElementById('your-button').addEventListener('click', function() {
-    // Your button logic here
-    fetch('/yourPlugin/your-endpoint/', {
-        method: 'POST',
-        headers: {
-            'Content-Type': 'application/json',
-            'X-CSRFToken': getCSRFToken()
-        },
-        body: JSON.stringify({data: 'value'})
-    })
-    .then(response => response.json())
-    .then(data => {
-        if (data.status === 1) {
-            showNotification('success', 'Success', data.message);
-        } else {
-            showNotification('error', 'Error', data.error_message);
-        }
-    });
-});
-
- -
-

How to Add Toggles

- -

1. HTML Structure

-
<div class="control-group">
-    <label for="plugin-toggle" class="toggle-label">
-        Enable Feature
-    </label>
-    <label class="toggle-switch">
-        <input type="checkbox" id="plugin-toggle" {% if feature_enabled %}checked{% endif %}>
-        <span class="slider"></span>
-    </label>
-</div>
- -

2. CSS Styles

-
.toggle-switch {
-    position: relative;
-    display: inline-block;
-    width: 60px;
-    height: 34px;
-}
-
-.toggle-switch input {
-    opacity: 0;
-    width: 0;
-    height: 0;
-}
-
-.slider {
-    position: absolute;
-    cursor: pointer;
-    top: 0;
-    left: 0;
-    right: 0;
-    bottom: 0;
-    background-color: #ccc;
-    transition: .4s;
-    border-radius: 34px;
-}
-
-.slider:before {
-    position: absolute;
-    content: "";
-    height: 26px;
-    width: 26px;
-    left: 4px;
-    bottom: 4px;
-    background-color: white;
-    transition: .4s;
-    border-radius: 50%;
-    box-shadow: 0 2px 4px rgba(0,0,0,0.2);
-}
-
-input:checked + .slider {
-    background-color: #5856d6;
-}
-
-input:checked + .slider:before {
-    transform: translateX(26px);
-}
- -

3. JavaScript Handling

-
document.getElementById('plugin-toggle').addEventListener('change', function() {
-    fetch('/yourPlugin/toggle/', {
-        method: 'POST',
-        headers: {
-            'Content-Type': 'application/json',
-            'X-CSRFToken': getCSRFToken()
-        }
-    })
-    .then(response => response.json())
-    .then(data => {
-        if (data.status === 1) {
-            showNotification('success', 'Toggle Updated', data.message);
-        } else {
-            showNotification('error', 'Error', data.error_message);
-            // Revert toggle state
-            this.checked = !this.checked;
-        }
-    });
-});
-
- -
-

How to Add Install/Uninstall Buttons

- -

1. In Your Plugin Template

-
<div class="plugin-actions">
-    <button class="btn-install" id="install-plugin">
-        <i class="fas fa-download"></i>
-        Install Plugin
-    </button>
-    
-    <button class="btn-uninstall" id="uninstall-plugin">
-        <i class="fas fa-trash"></i>
-        Uninstall Plugin
-    </button>
-</div>
- -

2. CSS Styles

-
.plugin-actions {
-    display: flex;
-    gap: 10px;
-    margin-top: 20px;
-}
-
-.btn-install {
-    background: #10b981;
-    color: white;
-    border: none;
-    padding: 10px 20px;
-    border-radius: 6px;
-    cursor: pointer;
-    transition: all 0.3s ease;
-}
-
-.btn-uninstall {
-    background: #ef4444;
-    color: white;
-    border: none;
-    padding: 10px 20px;
-    border-radius: 6px;
-    cursor: pointer;
-    transition: all 0.3s ease;
-}
-
-.btn-install:hover {
-    background: #059669;
-    transform: translateY(-2px);
-}
-
-.btn-uninstall:hover {
-    background: #dc2626;
-    transform: translateY(-2px);
-}
- -

3. JavaScript Implementation

-
// Install button
-document.getElementById('install-plugin').addEventListener('click', function() {
-    if (confirm('Are you sure you want to install this plugin?')) {
-        this.disabled = true;
-        this.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Installing...';
-        
-        fetch('/yourPlugin/install/', {
-            method: 'POST',
-            headers: {
-                'Content-Type': 'application/json',
-                'X-CSRFToken': getCSRFToken()
-            }
-        })
-        .then(response => response.json())
-        .then(data => {
-            if (data.status === 1) {
-                showNotification('success', 'Installation Complete', data.message);
-                location.reload();
-            } else {
-                showNotification('error', 'Installation Failed', data.error_message);
-            }
-        })
-        .finally(() => {
-            this.disabled = false;
-            this.innerHTML = '<i class="fas fa-download"></i> Install Plugin';
-        });
-    }
-});
-
-// Uninstall button
-document.getElementById('uninstall-plugin').addEventListener('click', function() {
-    if (confirm('Are you sure you want to uninstall this plugin? This action cannot be undone.')) {
-        this.disabled = true;
-        this.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Uninstalling...';
-        
-        fetch('/yourPlugin/uninstall/', {
-            method: 'POST',
-            headers: {
-                'Content-Type': 'application/json',
-                'X-CSRFToken': getCSRFToken()
-            }
-        })
-        .then(response => response.json())
-        .then(data => {
-            if (data.status === 1) {
-                showNotification('success', 'Uninstallation Complete', data.message);
-                setTimeout(() => location.reload(), 2000);
-            } else {
-                showNotification('error', 'Uninstallation Failed', data.error_message);
-            }
-        })
-        .finally(() => {
-            this.disabled = false;
-            this.innerHTML = '<i class="fas fa-trash"></i> Uninstall Plugin';
-        });
-    }
-});
-
- -
-

How to Add Enable/Disable Plugin Buttons

- -

1. Model for Plugin State

-
# models.py
-from django.db import models
-from django.contrib.auth.models import User
-
-class PluginSettings(models.Model):
-    user = models.ForeignKey(User, on_delete=models.CASCADE)
-    plugin_enabled = models.BooleanField(default=True)
-    created_at = models.DateTimeField(auto_now_add=True)
-    updated_at = models.DateTimeField(auto_now=True)
-    
-    class Meta:
-        unique_together = ['user']
- -

2. View for Toggle

-
# views.py
-from django.http import JsonResponse
-from django.views.decorators.http import require_http_methods
-from .models import PluginSettings
-
-@require_http_methods(["POST"])
-def toggle_plugin(request):
-    try:
-        settings, created = PluginSettings.objects.get_or_create(
-            user=request.user,
-            defaults={'plugin_enabled': True}
-        )
-        
-        settings.plugin_enabled = not settings.plugin_enabled
-        settings.save()
-        
-        return JsonResponse({
-            'status': 1,
-            'enabled': settings.plugin_enabled,
-            'message': f'Plugin {"enabled" if settings.plugin_enabled else "disabled"} successfully'
-        })
-    except Exception as e:
-        return JsonResponse({'status': 0, 'error_message': str(e)})
- -

3. Template Implementation

-
<div class="plugin-controls">
-    <label for="plugin-toggle" class="toggle-label">
-        Enable Plugin
-    </label>
-    <label class="toggle-switch">
-        <input type="checkbox" id="plugin-toggle" {% if plugin_enabled %}checked{% endif %}>
-        <span class="slider"></span>
-    </label>
-</div>
- -

4. JavaScript Handling

-
document.getElementById('plugin-toggle').addEventListener('change', function() {
-    fetch('/yourPlugin/toggle/', {
-        method: 'POST',
-        headers: {
-            'Content-Type': 'application/json',
-            'X-CSRFToken': getCSRFToken()
-        }
-    })
-    .then(response => response.json())
-    .then(data => {
-        if (data.status === 1) {
-            showNotification('success', 'Plugin Toggle', data.message);
-            // Update UI elements based on enabled state
-            updatePluginUI(data.enabled);
-        } else {
-            showNotification('error', 'Error', data.error_message);
-            this.checked = !this.checked; // Revert toggle
-        }
-    });
-});
-
-function updatePluginUI(enabled) {
-    const buttons = document.querySelectorAll('.plugin-button');
-    buttons.forEach(button => {
-        button.disabled = !enabled;
-    });
-    
-    const statusIndicator = document.querySelector('.status-indicator');
-    if (statusIndicator) {
-        statusIndicator.textContent = enabled ? 'Enabled' : 'Disabled';
-        statusIndicator.className = `status-indicator ${enabled ? 'enabled' : 'disabled'}`;
-    }
-}
-
- -
-

How to Avoid Breaking the CyberPanel Sidebar

- -

1. Use CyberPanel's Base Template

-

Always extend the base template:

-
{% extends "baseTemplate/index.html" %}
-{% load i18n %}
-{% load static %}
-
-{% block title %}{% trans "Your Plugin - CyberPanel" %}{% endblock %}
-
-{% block content %}
-<!-- Your plugin content here -->
-{% endblock %}
- -

2. Don't Modify the Sidebar HTML

-

Never directly modify the sidebar HTML. Instead, use CyberPanel's built-in navigation system.

- -

3. Use Proper CSS Scoping

-
/* Good: Scoped to your plugin */
-.your-plugin-wrapper {
-    /* Your styles here */
-}
-
-/* Bad: Global styles that might affect sidebar */
-.sidebar {
-    /* Don't do this */
-}
- -

4. Use CyberPanel's CSS Variables

-
.your-plugin-element {
-    background: var(--bg-primary, white);
-    color: var(--text-primary, #2f3640);
-    border: 1px solid var(--border-primary, #e8e9ff);
-}
- -

5. Test Responsive Design

-

Ensure your plugin works on all screen sizes without breaking the sidebar:

-
@media (max-width: 768px) {
-    .your-plugin-wrapper {
-        padding: 15px;
-    }
-    
-    /* Don't modify sidebar behavior */
-}
-
- -
-

How to Make Plugins Load Inline

- -

1. Use CyberPanel's httpProc

-
# views.py
-from plogical.httpProc import httpProc
-
-def your_view(request):
-    context = {
-        'data': 'your_data',
-        'plugin_enabled': True
-    }
-    
-    proc = httpProc(request, 'yourPlugin/your_template.html', context, 'admin')
-    return proc.render()
- -

2. Template Structure

-
{% extends "baseTemplate/index.html" %}
-{% load i18n %}
-{% load static %}
-
-{% block title %}{% trans "Your Plugin - CyberPanel" %}{% endblock %}
-
-{% block header_scripts %}
-<style>
-    /* Your plugin-specific styles */
-    .your-plugin-wrapper {
-        background: transparent;
-        padding: 20px;
-    }
-    
-    .your-plugin-container {
-        max-width: 1200px;
-        margin: 0 auto;
-    }
-</style>
-{% endblock %}
-
-{% block content %}
-<div class="your-plugin-wrapper">
-    <div class="your-plugin-container">
-        <!-- Your plugin content here -->
-    </div>
-</div>
-{% endblock %}
- -

3. URL Configuration

-
# urls.py
-from django.urls import path
-from . import views
-
-app_name = 'yourPlugin'
-
-urlpatterns = [
-    path('', views.your_view, name='your_view'),
-    path('settings/', views.settings_view, name='settings'),
-    # ... other URLs
-]
- -

4. Main URLs Integration

-
# In /usr/local/CyberCP/cyberpanel/urls.py
-urlpatterns = [
-    # ... existing patterns ...
-    path("yourPlugin/", include("yourPlugin.urls")),
-]
-
- -
-

Plugin Structure Overview

-
yourPlugin/
-├── __init__.py
-├── admin.py
-├── apps.py
-├── models.py
-├── views.py
-├── urls.py
-├── signals.py
-├── meta.xml
-├── install.sh
-├── templates/
-│   └── yourPlugin/
-│       ├── plugin_home.html
-│       ├── plugin_settings.html
-│       └── plugin_logs.html
-├── static/
-│   └── yourPlugin/
-│       ├── css/
-│       │   └── yourPlugin.css
-│       └── js/
-│           └── yourPlugin.js
-└── migrations/
-    └── __init__.py
-
- -
-

Best Practices

- -

1. Security

-
    -
  • Always validate user input
  • -
  • Use CSRF protection
  • -
  • Sanitize data before displaying
  • -
  • Use proper authentication decorators
  • -
- -

2. Performance

-
    -
  • Use database indexes for frequently queried fields
  • -
  • Implement caching where appropriate
  • -
  • Optimize database queries
  • -
  • Minimize JavaScript and CSS
  • -
- -

3. User Experience

-
    -
  • Provide clear feedback for all actions
  • -
  • Use loading states for long operations
  • -
  • Implement proper error handling
  • -
  • Make the interface responsive
  • -
- -

4. Code Quality

-
    -
  • Follow Django best practices
  • -
  • Use meaningful variable names
  • -
  • Add proper documentation
  • -
  • Write unit tests
  • -
- -

5. Integration

-
    -
  • Use CyberPanel's existing components
  • -
  • Follow the established design patterns
  • -
  • Maintain consistency with the UI
  • -
  • Test thoroughly before release
  • -
-
- -
-

Troubleshooting

- -

Common Issues

- -

1. Plugin not showing in installed plugins

-
    -
  • Check if meta.xml exists and is valid
  • -
  • Verify the plugin is in INSTALLED_APPS
  • -
  • Ensure proper file permissions
  • -
- -

2. Template not found errors

-
    -
  • Check template path in views.py
  • -
  • Verify template files exist
  • -
  • Ensure proper directory structure
  • -
- -

3. Static files not loading

-
    -
  • Run python3 manage.py collectstatic
  • -
  • Check STATIC_URL configuration
  • -
  • Verify file permissions
  • -
- -

4. Database migration errors

-
    -
  • Check model definitions
  • -
  • Run python3 manage.py makemigrations
  • -
  • Verify database connectivity
  • -
- -

5. Permission denied errors

-
    -
  • Check file ownership (cyberpanel:cyberpanel)
  • -
  • Verify file permissions (755 for directories, 644 for files)
  • -
  • Ensure proper SELinux context if applicable
  • -
- -

Debug Steps

- -

1. Check CyberPanel logs

-
tail -f /home/cyberpanel/logs/cyberpanel.log
- -

2. Check Django logs

-
tail -f /home/cyberpanel/logs/django.log
- -

3. Verify plugin installation

-
ls -la /home/cyberpanel/plugins/
-ls -la /usr/local/CyberCP/yourPlugin/
- -

4. Test database connectivity

-
cd /usr/local/CyberCP
-python3 manage.py shell
- -

5. Check service status

-
systemctl status lscpd
-systemctl status cyberpanel
-
- -
- Conclusion: This guide provides comprehensive instructions for developing CyberPanel plugins. Follow the best practices and troubleshooting steps to ensure your plugins integrate seamlessly with CyberPanel while maintaining security and performance standards. -
-
-
-
-
- - -
-
-

Operating System Compatibility

- -
-
-

🌐 Multi-OS Support

-

Comprehensive support for all CyberPanel-supported operating systems.

-
-
-

🔍 Automatic Detection

-

Intelligent OS detection and configuration for seamless installation.

-
-
-

🧪 Compatibility Testing

-

Built-in compatibility testing to verify system requirements.

-
-
- -

Supported Operating Systems

- -
-
-

Ubuntu

-
    -
  • ✅ Ubuntu 22.04 (Full Support)
  • -
  • ✅ Ubuntu 20.04 (Full Support)
  • -
  • ✅ Debian 11+ (Compatible)
  • -
-

Package Manager: apt-get

-

Web Server: apache2

-
- -
-

RHEL-based

-
    -
  • ✅ AlmaLinux 8, 9, 10
  • -
  • ✅ RockyLinux 8, 9
  • -
  • ✅ RHEL 8, 9
  • -
  • ✅ CentOS 9
  • -
-

Package Manager: dnf/yum

-

Web Server: httpd

-
- -
-

CloudLinux

-
    -
  • ✅ CloudLinux 8
  • -
  • ✅ CloudLinux 7 (Limited)
  • -
-

Package Manager: yum

-

Web Server: httpd

-
-
- -

Python Compatibility

-

The plugin requires Python 3.6+ and automatically detects the correct Python executable:

- -
-
# Detection order:
-1. python3.12
-2. python3.11
-3. python3.10
-4. python3.9
-5. python3.8
-6. python3.7
-7. python3.6
-8. python3
-9. python (fallback)
-
- -

Installation Compatibility

-

The installation script automatically detects your operating system and configures the plugin accordingly:

- -
-
# Automatic detection includes:
-- OS name and version
-- Python executable path
-- Package manager (apt-get, dnf, yum)
-- Service manager (systemctl, service)
-- Web server (apache2, httpd)
-- User and group permissions
-
- -

Compatibility Testing

-

Run the built-in compatibility test to verify your system:

- -
-
# Navigate to plugin directory
-cd /usr/local/CyberCP/testPlugin
-
-# Run compatibility test
-python3 test_os_compatibility.py
-
-# Or make it executable and run
-chmod +x test_os_compatibility.py
-./test_os_compatibility.py
-
- -

Test Results

-

The compatibility test checks:

-
    -
  • ✅ OS detection and version
  • -
  • ✅ Python installation and version
  • -
  • ✅ Package manager availability
  • -
  • ✅ Service manager functionality
  • -
  • ✅ Web server configuration
  • -
  • ✅ File permissions and ownership
  • -
  • ✅ Network connectivity
  • -
  • ✅ CyberPanel integration
  • -
- -

OS-Specific Configurations

- -

Ubuntu/Debian Systems

-
-
# Package Manager: apt-get
-# Python: python3
-# Pip: pip3
-# Service Manager: systemctl
-# Web Server: apache2
-# User/Group: cyberpanel:cyberpanel
-
-# Installation commands
-sudo apt-get update
-sudo apt-get install -y python3 python3-pip python3-venv git curl
-sudo apt-get install -y build-essential python3-dev
-
- -

RHEL-based Systems

-
-
# Package Manager: dnf (RHEL 8+) / yum (RHEL 7)
-# Python: python3
-# Pip: pip3
-# Service Manager: systemctl
-# Web Server: httpd
-# User/Group: cyberpanel:cyberpanel
-
-# Installation commands (RHEL 8+)
-sudo dnf install -y python3 python3-pip python3-devel git curl
-sudo dnf install -y gcc gcc-c++ make
-
-# Installation commands (RHEL 7)
-sudo yum install -y python3 python3-pip python3-devel git curl
-sudo yum install -y gcc gcc-c++ make
-
- -

CloudLinux

-
-
# Package Manager: yum
-# Python: python3
-# Pip: pip3
-# Service Manager: systemctl
-# Web Server: httpd
-# User/Group: cyberpanel:cyberpanel
-
-# Installation commands
-sudo yum install -y python3 python3-pip python3-devel git curl
-sudo yum install -y gcc gcc-c++ make
-
-# CageFS configuration
-cagefsctl --enable cyberpanel
-cagefsctl --update
-
- -

Security Compatibility

- -

SELinux (RHEL-based systems)

-
-
# Check SELinux status
-sestatus
-
-# Set proper context for plugin files
-setsebool -P httpd_can_network_connect 1
-chcon -R -t httpd_exec_t /usr/local/CyberCP/testPlugin/
-
- -

AppArmor (Ubuntu/Debian)

-
-
# Check AppArmor status
-aa-status
-
-# Allow Apache to access plugin files
-aa-complain apache2
-
- -

Firewall Configuration

-
-
# Ubuntu/Debian (ufw)
-sudo ufw allow 8090/tcp
-sudo ufw allow 80/tcp
-sudo ufw allow 443/tcp
-
-# RHEL-based (firewalld)
-sudo firewall-cmd --permanent --add-port=8090/tcp
-sudo firewall-cmd --permanent --add-port=80/tcp
-sudo firewall-cmd --permanent --add-port=443/tcp
-sudo firewall-cmd --reload
-
- -

Troubleshooting

- -

Common Issues

-
-

Python not found

-
-
# Ubuntu/Debian
-sudo apt-get update
-sudo apt-get install -y python3 python3-pip
-
-# RHEL-based
-sudo dnf install -y python3 python3-pip
-# or
-sudo yum install -y python3 python3-pip
-
- -

Permission denied

-
-
sudo chown -R cyberpanel:cyberpanel /home/cyberpanel/plugins
-sudo chown -R cyberpanel:cyberpanel /usr/local/CyberCP/testPlugin
-
- -

Service not starting

-
-
sudo systemctl daemon-reload
-sudo systemctl restart lscpd
-sudo systemctl restart apache2  # Ubuntu/Debian
-sudo systemctl restart httpd    # RHEL-based
-
-
- -

Debug Commands

-
-
# Check OS information
-cat /etc/os-release
-uname -a
-
-# Check Python installation
-python3 --version
-which python3
-which pip3
-
-# Check services
-systemctl status lscpd
-systemctl status apache2  # Ubuntu/Debian
-systemctl status httpd    # RHEL-based
-
-# Check file permissions
-ls -la /home/cyberpanel/plugins/
-ls -la /usr/local/CyberCP/testPlugin/
-
-# Check CyberPanel logs
-tail -f /home/cyberpanel/logs/cyberpanel.log
-tail -f /home/cyberpanel/logs/django.log
-
- -
- Note: The plugin is designed to work seamlessly across all supported operating systems. If you encounter any compatibility issues, please run the compatibility test and check the troubleshooting section above. -
-
-
- - -{% endblock %} diff --git a/testPlugin/templates/testPlugin/plugin_home.html b/testPlugin/templates/testPlugin/plugin_home.html deleted file mode 100644 index 3ef326229..000000000 --- a/testPlugin/templates/testPlugin/plugin_home.html +++ /dev/null @@ -1,571 +0,0 @@ -{% extends "baseTemplate/index.html" %} -{% load i18n %} -{% load static %} - -{% block title %}{% trans "Test Plugin - CyberPanel" %}{% endblock %} - -{% block header_scripts %} - -{% endblock %} - -{% block content %} -
-
- -
-

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

-

{% trans "A comprehensive test plugin with enable/disable functionality, test button, and popup messages" %}

-
- - -
-
-
- - -
- - -
-
- - -
-
-
{{ 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) From 820dbf37be84089d7ef254f5a01b17ac25e6e817 Mon Sep 17 00:00:00 2001 From: master3395 Date: Tue, 3 Feb 2026 00:42:48 +0100 Subject: [PATCH 011/130] Fix User Management: Create User, List Users, Modify User - Create User: Don't show 'Unknown error' on load; fix inverted success/failure logic in static/ and public/static/ userManagment.js - List Users: Use + + + From 0225f2f95a4535200a34487fd810ca891b30cc7d Mon Sep 17 00:00:00 2001 From: master3395 Date: Tue, 3 Feb 2026 18:46:05 +0100 Subject: [PATCH 012/130] Fix plugin settings 404: dynamic URL inclusion for all installed plugins - pluginHolder/urls.py: Discover plugins from /usr/local/CyberCP and source paths (/home/cyberpanel/plugins, /home/cyberpanel-plugins); dynamically include each plugin's urls so /plugins//settings/ works without hardcoding. Add source path to sys.path when loading from source. - CyberCP/urls.py: Remove hardcoded _plugin_routes; all plugin routes now served via pluginHolder dynamic inclusion. Fixes 404 on /plugins/contaboAutoSnapshot/settings/ and any installed plugin settings page. No per-plugin core changes required. --- CyberCP/urls.py | 7 ++- pluginHolder/urls.py | 102 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 106 insertions(+), 3 deletions(-) diff --git a/CyberCP/urls.py b/CyberCP/urls.py index db097f5a0..18467e668 100644 --- a/CyberCP/urls.py +++ b/CyberCP/urls.py @@ -20,6 +20,10 @@ from django.conf.urls.static import static from django.views.static import serve 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. + urlpatterns = [ # Serve static files first (before catch-all routes) re_path(r'^static/(?P.*)$', serve, {'document_root': settings.STATIC_ROOT}), @@ -43,6 +47,7 @@ urlpatterns = [ path('api/', include('api.urls')), path('filemanager/', include('filemanager.urls')), path('emailPremium/', include('emailPremium.urls')), + path('emailMarketing/', include('emailMarketing.urls')), # Default-installed (sidebar links to it) path('manageservices/', include('manageServices.urls')), path('plugins/', include('pluginHolder.urls')), path('cloudAPI/', include('cloudAPI.urls')), @@ -51,8 +56,6 @@ urlpatterns = [ path('CloudLinux/', include('CLManager.urls')), path('IncrementalBackups/', include('IncBackups.urls')), path('aiscanner/', include('aiScanner.urls')), - # Optional plugin routes - added by plugin installer when plugins are installed from Plugin Store - # path('emailMarketing/', include('emailMarketing.urls')), # path('Terminal/', include('WebTerminal.urls')), path('', include('loginSystem.urls')), ] diff --git a/pluginHolder/urls.py b/pluginHolder/urls.py index 7e4c547cb..5eefc03e6 100644 --- a/pluginHolder/urls.py +++ b/pluginHolder/urls.py @@ -1,6 +1,88 @@ -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 + from . import views +# Installed plugins live under this path (must match pluginInstaller and pluginHolder.views) +INSTALLED_PLUGINS_PATH = '/usr/local/CyberCP' + +# 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'), @@ -15,3 +97,21 @@ urlpatterns = [ path('api/revert//', views.revert_plugin, name='revert_plugin'), path('/help/', views.plugin_help, name='plugin_help'), ] + +# Dynamically include each installed plugin's URLs so /plugins//settings/ etc. work +for _plugin_name, _path_parent in _get_installed_plugin_list(): + try: + # If plugin is from a source path, ensure it is on sys.path so import works + 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'))) + except (ImportError, AttributeError) as e: + try: + from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter as _logging + _logging.writeToFile( + 'pluginHolder.urls: Skipping plugin "%s" (urls not loadable): %s' + % (_plugin_name, e) + ) + except Exception: + pass From df7c5ba23402e522c8401dba0a0e085752897c6a Mon Sep 17 00:00:00 2001 From: master3395 Date: Tue, 3 Feb 2026 19:09:46 +0100 Subject: [PATCH 013/130] Only include plugin URLs when app is in INSTALLED_APPS Skip dynamic plugin URL inclusion for plugins that are on disk but not in Django INSTALLED_APPS to avoid RuntimeError when loading models. Plugin installer adds apps to INSTALLED_APPS on install; this prevents breakage when that step was missed or reverted. --- pluginHolder/urls.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pluginHolder/urls.py b/pluginHolder/urls.py index 5eefc03e6..4bc0c147b 100644 --- a/pluginHolder/urls.py +++ b/pluginHolder/urls.py @@ -99,7 +99,13 @@ urlpatterns = [ ] # Dynamically include each installed plugin's URLs so /plugins//settings/ etc. work +# Only include plugins that are in INSTALLED_APPS so Django can load their models. +from django.conf import settings +_installed_apps = getattr(settings, 'INSTALLED_APPS', ()) + for _plugin_name, _path_parent in _get_installed_plugin_list(): + if _plugin_name not in _installed_apps: + continue try: # If plugin is from a source path, ensure it is on sys.path so import works if _path_parent not in sys.path: From 6218cfd064762246c195725181fa8c35056ffcda Mon Sep 17 00:00:00 2001 From: master3395 Date: Tue, 3 Feb 2026 19:28:10 +0100 Subject: [PATCH 014/130] Log successful plugin install (upload) to CyberPanel main log --- pluginHolder/views.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pluginHolder/views.py b/pluginHolder/views.py index d617ef2b5..4746f88af 100644 --- a/pluginHolder/views.py +++ b/pluginHolder/views.py @@ -592,6 +592,7 @@ def install_plugin(request, plugin_name): # Set plugin to enabled by default after installation _set_plugin_state(plugin_name, True) + logging.writeToFile(f"Plugin {plugin_name} installed successfully (upload)") return JsonResponse({ 'success': True, 'message': f'Plugin {plugin_name} installed successfully' From 02c8a9b6aca9fd0b6d9387575befba477d032d18 Mon Sep 17 00:00:00 2001 From: master3395 Date: Tue, 3 Feb 2026 19:37:36 +0100 Subject: [PATCH 015/130] Plugin upgrade: sync meta.xml from GitHub raw so version matches store; logging get_current_timestamp - Add CyberCPLogFileWriter.get_current_timestamp() for errorSanitizer (fixes 500 logging) - After upgrade/install: fetch meta.xml from raw GitHub and overwrite installed file so store version (e.g. 1.1.0) is correct even when archive ZIP is cached/stale - Upgrade/install: discover ZIP top-level folder and match plugin folder case-insensitively - Improves redisManager/memcacheManager upgrade and all store installs --- plogical/CyberCPLogFileWriter.py | 5 ++ pluginHolder/views.py | 127 +++++++++++++++++++++++-------- 2 files changed, 102 insertions(+), 30 deletions(-) diff --git a/plogical/CyberCPLogFileWriter.py b/plogical/CyberCPLogFileWriter.py index 2babc8527..d91d2f8ee 100644 --- a/plogical/CyberCPLogFileWriter.py +++ b/plogical/CyberCPLogFileWriter.py @@ -7,6 +7,11 @@ import smtplib class CyberCPLogFileWriter: fileName = "/home/cyberpanel/error-logs.txt" + @staticmethod + def get_current_timestamp(): + """Return current timestamp in same format used for log lines (for errorSanitizer etc).""" + return time.strftime("%m.%d.%Y_%H-%M-%S") + @staticmethod def AddFromHeader(sender, message): try: diff --git a/pluginHolder/views.py b/pluginHolder/views.py index 4746f88af..0bda088bc 100644 --- a/pluginHolder/views.py +++ b/pluginHolder/views.py @@ -887,6 +887,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 @@ -1362,22 +1383,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) @@ -1420,7 +1467,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}") @@ -1593,32 +1643,46 @@ 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 @@ -1694,6 +1758,9 @@ 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 From 4c24de7453a1ae5a2c84ecd4dc781a3e99ae3830 Mon Sep 17 00:00:00 2001 From: master3395 Date: Tue, 3 Feb 2026 19:50:17 +0100 Subject: [PATCH 016/130] Sync with live: baseTemplate, firewall, manageSSL, plogical/acl, ftp, websiteFunctions, wsgi Only files that match current live server; excludes settings.py (deployment-specific), pluginHolder/pluginInstaller (repo ahead), install/cyberpanel scripts (diff), and deleted static files (still on server). --- CyberCP/wsgi.py | 6 + baseTemplate/context_processors.py | 14 + .../templates/baseTemplate/homePage.html | 46 ++ .../templates/baseTemplate/index.html | 6 +- baseTemplate/views.py | 65 +- firewall/firewallManager.py | 657 +++++++++++++++--- firewall/templates/firewall/firewall.html | 292 +++++++- firewall/views.py | 93 ++- manageSSL/views.py | 66 +- plogical/acl.py | 3 +- static/ftp/ftp.js | 349 ++++++++-- static/websiteFunctions/websiteFunctions.js | 106 +-- 12 files changed, 1387 insertions(+), 316 deletions(-) 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/baseTemplate/context_processors.py b/baseTemplate/context_processors.py index 696f9d840..b3122a0fe 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,15 @@ 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.""" + firewall_js_path = '/usr/local/CyberCP/static/firewall/firewall.js' + try: + version = int(os.path.getmtime(firewall_js_path)) + except OSError: + version = int(time.time()) + return { + 'FIREWALL_STATIC_VERSION': version } \ No newline at end of file diff --git a/baseTemplate/templates/baseTemplate/homePage.html b/baseTemplate/templates/baseTemplate/homePage.html index 2a1aef84f..2373f10ae 100644 --- a/baseTemplate/templates/baseTemplate/homePage.html +++ b/baseTemplate/templates/baseTemplate/homePage.html @@ -876,6 +876,29 @@
+ +
+
+ Show + + 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 $} + + +
+
diff --git a/baseTemplate/templates/baseTemplate/index.html b/baseTemplate/templates/baseTemplate/index.html index f00f3828f..ab433a6e5 100644 --- a/baseTemplate/templates/baseTemplate/index.html +++ b/baseTemplate/templates/baseTemplate/index.html @@ -1835,9 +1835,7 @@ Manage SSL - - SSL Reconciliation - + {% comment %}SSL Reconciliation - hidden; URL /manageSSL/sslReconcile still works if needed{% endcomment %} {% endif %} {% if admin or hostnameSSL %} @@ -2190,7 +2188,7 @@ - + diff --git a/baseTemplate/views.py b/baseTemplate/views.py index 172e86731..1c1c637c2 100644 --- a/baseTemplate/views.py +++ b/baseTemplate/views.py @@ -716,9 +716,19 @@ def getRecentSSHLogins(request): import re, time from collections import OrderedDict - # Run 'last -n 20' to get recent SSH logins + # Pagination params try: - output = ProcessUtilities.outputExecutioner('last -n 20') + page = max(1, int(request.GET.get('page', 1))) + except (ValueError, TypeError): + page = 1 + try: + per_page = min(100, max(5, int(request.GET.get('per_page', 20)))) + except (ValueError, TypeError): + per_page = 20 + + # Run 'last -n 500' to get enough entries for pagination + try: + output = ProcessUtilities.outputExecutioner('last -n 500') except Exception as e: return HttpResponse(json.dumps({'error': 'Failed to run last: %s' % str(e)}), content_type='application/json', status=500) @@ -802,7 +812,19 @@ def getRecentSSHLogins(request): 'is_active': is_active, 'raw': line }) - return HttpResponse(json.dumps({'logins': logins}), content_type='application/json') + total = len(logins) + total_pages = (total + per_page - 1) // per_page if total > 0 else 1 + page = min(page, total_pages) if total_pages > 0 else 1 + start = (page - 1) * per_page + end = start + per_page + paginated_logins = logins[start:end] + return HttpResponse(json.dumps({ + 'logins': paginated_logins, + 'total': total, + 'page': page, + 'per_page': per_page, + 'total_pages': total_pages + }), content_type='application/json') except Exception as e: return HttpResponse(json.dumps({'error': str(e)}), content_type='application/json', status=500) @@ -816,6 +838,17 @@ def getRecentSSHLogs(request): currentACL = ACLManager.loadedACL(user_id) if not currentACL.get('admin', 0): return HttpResponse(json.dumps({'error': 'Admin only'}), content_type='application/json', status=403) + + # Pagination params + try: + page = max(1, int(request.GET.get('page', 1))) + except (ValueError, TypeError): + page = 1 + try: + per_page = min(100, max(5, int(request.GET.get('per_page', 25)))) + except (ValueError, TypeError): + per_page = 25 + from plogical.processUtilities import ProcessUtilities import re distro = ProcessUtilities.decideDistro() @@ -824,7 +857,7 @@ def getRecentSSHLogs(request): else: log_path = '/var/log/secure' try: - output = ProcessUtilities.outputExecutioner(f'tail -n 100 {log_path}') + output = ProcessUtilities.outputExecutioner(f'tail -n 500 {log_path}') except Exception as e: return HttpResponse(json.dumps({'error': f'Failed to read log: {str(e)}'}), content_type='application/json', status=500) lines = output.split('\n') @@ -862,7 +895,21 @@ def getRecentSSHLogs(request): 'raw': line, 'ip_address': ip_address }) - return HttpResponse(json.dumps({'logs': logs}), content_type='application/json') + # Reverse so newest logs appear first (page 1 = most recent) + logs.reverse() + total = len(logs) + total_pages = (total + per_page - 1) // per_page if total > 0 else 1 + page = min(page, total_pages) if total_pages > 0 else 1 + start = (page - 1) * per_page + end = start + per_page + paginated_logs = logs[start:end] + return HttpResponse(json.dumps({ + 'logs': paginated_logs, + 'total': total, + 'page': page, + 'per_page': per_page, + 'total_pages': total_pages + }), content_type='application/json') except Exception as e: return HttpResponse(json.dumps({'error': str(e)}), content_type='application/json', status=500) @@ -1325,7 +1372,9 @@ def blockIPAddress(request): try: import os import time - banned_ips_file = '/etc/cyberpanel/banned_ips.json' + primary_file = '/usr/local/CyberCP/data/banned_ips.json' + legacy_file = '/etc/cyberpanel/banned_ips.json' + banned_ips_file = primary_file if os.path.exists(primary_file) else legacy_file if os.path.exists(legacy_file) else primary_file banned_ips = [] if os.path.exists(banned_ips_file): @@ -1359,10 +1408,10 @@ def blockIPAddress(request): banned_ips.append(new_banned_ip) # Ensure directory exists - os.makedirs(os.path.dirname(banned_ips_file), exist_ok=True) + os.makedirs(os.path.dirname(primary_file), exist_ok=True) # Save to file - with open(banned_ips_file, 'w') as f: + with open(primary_file, 'w') as f: json.dump(banned_ips, f, indent=2) except Exception as e: # Log but don't fail the request if JSON update fails diff --git a/firewall/firewallManager.py b/firewall/firewallManager.py index f42db8d14..097e5c0f3 100644 --- a/firewall/firewallManager.py +++ b/firewall/firewallManager.py @@ -4,10 +4,13 @@ import os.path import sys import django +PROJECT_ROOT = '/usr/local/CyberCP' +while PROJECT_ROOT in sys.path: + sys.path.remove(PROJECT_ROOT) +sys.path.insert(0, PROJECT_ROOT) + from loginSystem.models import Administrator from plogical.httpProc import httpProc - -sys.path.append('/usr/local/CyberCP') os.environ.setdefault("DJANGO_SETTINGS_MODULE", "CyberCP.settings") django.setup() import json @@ -30,10 +33,40 @@ class FirewallManager: imunifyPath = '/usr/bin/imunify360-agent' CLPath = '/etc/sysconfig/cloudlinux' imunifyAVPath = '/etc/sysconfig/imunify360/integration.conf' + BANNED_IPS_PRIMARY_FILE = '/usr/local/CyberCP/data/banned_ips.json' + BANNED_IPS_LEGACY_FILE = '/etc/cyberpanel/banned_ips.json' def __init__(self, request = None): self.request = request + def _load_banned_ips_store(self): + """ + Load banned IPs from the primary store, falling back to legacy path. + Returns (data, path_used) + """ + for path in [self.BANNED_IPS_PRIMARY_FILE, self.BANNED_IPS_LEGACY_FILE]: + if os.path.exists(path): + try: + with open(path, 'r') as f: + return json.load(f), path + except Exception as e: + logging.CyberCPLogFileWriter.writeToFile(f'Failed to read banned IPs from {path}: {str(e)}') + return [], self.BANNED_IPS_PRIMARY_FILE + + def _save_banned_ips_store(self, data): + """ + Persist banned IPs to the primary store in a writable location. + """ + target = self.BANNED_IPS_PRIMARY_FILE + try: + os.makedirs(os.path.dirname(target), exist_ok=True) + with open(target, 'w') as f: + json.dump(data, f, indent=2) + except Exception as e: + logging.CyberCPLogFileWriter.writeToFile(f'Failed to write banned IPs to {target}: {str(e)}') + raise + return target + def securityHome(self, request = None, userID = None): proc = httpProc(request, 'firewall/index.html', None, 'admin') @@ -1810,49 +1843,87 @@ class FirewallManager: def getBannedIPs(self, userID=None): """ - Get list of banned IP addresses from database + Get list of banned IP addresses from database, or fall back to JSON file. """ try: admin = Administrator.objects.get(pk=userID) if admin.acl.adminStatus != 1: return ACLManager.loadError() - # Import BannedIP model and Django Q - from firewall.models import BannedIP - from django.db.models import Q - - # Get all active banned IPs that haven't expired - current_time = int(time.time()) - banned_ips_queryset = BannedIP.objects.filter( - active=True - ).filter( - Q(expires__isnull=True) | Q(expires__gt=current_time) - ).order_by('-banned_on') - active_banned_ips = [] - for banned_ip in banned_ips_queryset: - # Format the data for frontend - ip_data = { - 'id': banned_ip.id, - 'ip': banned_ip.ip_address, - 'reason': banned_ip.reason, - 'duration': banned_ip.duration, - 'banned_on': banned_ip.get_banned_on_display(), - 'expires': banned_ip.get_expires_display(), - 'active': not banned_ip.is_expired() and banned_ip.active - } - - # Only include truly active bans - if ip_data['active']: - active_banned_ips.append(ip_data) - + + try: + from firewall.models import BannedIP + from django.db.models import Q + + current_time = int(time.time()) + banned_ips_queryset = BannedIP.objects.filter( + active=True + ).filter( + Q(expires__isnull=True) | Q(expires__gt=current_time) + ).order_by('-banned_on') + + for banned_ip in banned_ips_queryset: + ip_data = { + 'id': banned_ip.id, + 'ip': banned_ip.ip_address, + 'reason': banned_ip.reason, + 'duration': banned_ip.duration, + 'banned_on': banned_ip.get_banned_on_display(), + 'expires': banned_ip.get_expires_display(), + 'active': not banned_ip.is_expired() and banned_ip.active + } + if ip_data['active']: + active_banned_ips.append(ip_data) + except (ImportError, AttributeError) as e: + # Fall back to JSON file when BannedIP model unavailable + import plogical.CyberCPLogFileWriter as _log + _log.CyberCPLogFileWriter.writeToFile('getBannedIPs: using JSON fallback (%s)' % str(e)) + active_banned_ips = [] + + # If DB returns nothing (or model not available), merge in JSON fallback + if not active_banned_ips: + banned_ips, _ = self._load_banned_ips_store() + for b in banned_ips: + if not b.get('active', True): + continue + exp = b.get('expires') + if exp == 'Never' or exp is None: + expires_display = 'Never' + elif isinstance(exp, (int, float)): + from datetime import datetime + try: + expires_display = datetime.fromtimestamp(exp).strftime('%Y-%m-%d %H:%M:%S') + except Exception: + expires_display = 'Never' + else: + expires_display = str(exp) + banned_on = b.get('banned_on') + if isinstance(banned_on, (int, float)): + from datetime import datetime + try: + banned_on = datetime.fromtimestamp(banned_on).strftime('%Y-%m-%d %H:%M:%S') + except Exception: + banned_on = 'N/A' + else: + banned_on = str(banned_on) if banned_on else 'N/A' + active_banned_ips.append({ + 'id': b.get('id'), + 'ip': b.get('ip', ''), + 'reason': b.get('reason', ''), + 'duration': b.get('duration', 'permanent'), + 'banned_on': banned_on, + 'expires': expires_display, + 'active': True + }) + final_dic = {'status': 1, 'bannedIPs': active_banned_ips} final_json = json.dumps(final_dic) return HttpResponse(final_json, content_type='application/json') except BaseException as msg: import plogical.CyberCPLogFileWriter as logging - logging.CyberCPLogFileWriter.writeToFile(f'Error in getBannedIPs: {str(msg)}') + logging.CyberCPLogFileWriter.writeToFile('Error in getBannedIPs: %s' % str(msg)) final_dic = {'status': 0, 'error_message': str(msg)} final_json = json.dumps(final_dic) return HttpResponse(final_json) @@ -1872,8 +1943,7 @@ class FirewallManager: if not ip or not reason: final_dic = {'status': 0, 'error_message': 'IP address and reason are required'} - final_json = json.dumps(final_dic) - return HttpResponse(final_json) + return HttpResponse(json.dumps(final_dic), content_type='application/json') # Validate IP address format import ipaddress @@ -1881,8 +1951,7 @@ class FirewallManager: ipaddress.ip_address(ip.split('/')[0]) # Handle CIDR notation except ValueError: final_dic = {'status': 0, 'error_message': 'Invalid IP address format'} - final_json = json.dumps(final_dic) - return HttpResponse(final_json) + return HttpResponse(json.dumps(final_dic), content_type='application/json') # Calculate expiration time current_time = time.time() @@ -1899,21 +1968,13 @@ class FirewallManager: expires = current_time + duration_seconds # Load existing banned IPs - banned_ips_file = '/etc/cyberpanel/banned_ips.json' - banned_ips = [] - if os.path.exists(banned_ips_file): - try: - with open(banned_ips_file, 'r') as f: - banned_ips = json.load(f) - except: - banned_ips = [] + banned_ips, _ = self._load_banned_ips_store() # Check if IP is already banned for banned_ip in banned_ips: if banned_ip.get('ip') == ip and banned_ip.get('active', True): - final_dic = {'status': 0, 'error_message': f'IP address {ip} is already banned'} - final_json = json.dumps(final_dic) - return HttpResponse(final_json) + final_dic = {'status': 0, 'error_message': 'IP address %s is already banned' % ip} + return HttpResponse(json.dumps(final_dic), content_type='application/json') # Add new banned IP new_banned_ip = { @@ -1927,12 +1988,8 @@ class FirewallManager: } banned_ips.append(new_banned_ip) - # Ensure directory exists - os.makedirs(os.path.dirname(banned_ips_file), exist_ok=True) - # Save to file - with open(banned_ips_file, 'w') as f: - json.dump(banned_ips, f, indent=2) + self._save_banned_ips_store(banned_ips) # Apply firewall rule to block the IP using firewalld try: @@ -1942,8 +1999,7 @@ class FirewallManager: capture_output=True, text=True, timeout=10) if not (firewalld_check.returncode == 0 and 'active' in firewalld_check.stdout): final_dic = {'status': 0, 'error_message': 'Firewalld is not active. Please enable firewalld service.'} - final_json = json.dumps(final_dic) - return HttpResponse(final_json) + return HttpResponse(json.dumps(final_dic), content_type='application/json') # Add firewalld rich rule to block the IP rich_rule = f'rule family=ipv4 source address={ip} drop' @@ -1958,42 +2014,37 @@ class FirewallManager: if reload_result.returncode == 0: logging.CyberCPLogFileWriter.writeToFile(f'Banned IP {ip} with reason: {reason}') else: - logging.CyberCPLogFileWriter.writeToFile(f'Failed to reload firewalld for {ip}: {reload_result.stderr}') - final_dic = {'status': 0, 'error_message': f'Failed to reload firewall rules: {reload_result.stderr}'} - final_json = json.dumps(final_dic) - return HttpResponse(final_json) + logging.CyberCPLogFileWriter.writeToFile('Failed to reload firewalld for %s: %s' % (ip, reload_result.stderr)) + final_dic = {'status': 0, 'error_message': 'Failed to reload firewall rules: %s' % reload_result.stderr} + return HttpResponse(json.dumps(final_dic), content_type='application/json') else: # Check if rule already exists (this is not an error) - if 'ALREADY_ENABLED' in result.stderr or 'already exists' in result.stderr.lower(): - logging.CyberCPLogFileWriter.writeToFile(f'IP {ip} already blocked in firewalld') + if result.stderr and ('ALREADY_ENABLED' in result.stderr or 'already exists' in result.stderr.lower()): + logging.CyberCPLogFileWriter.writeToFile('IP %s already blocked in firewalld' % ip) else: - logging.CyberCPLogFileWriter.writeToFile(f'Failed to add firewalld rule for {ip}: {result.stderr}') - final_dic = {'status': 0, 'error_message': f'Failed to add firewall rule: {result.stderr}'} - final_json = json.dumps(final_dic) - return HttpResponse(final_json) + logging.CyberCPLogFileWriter.writeToFile('Failed to add firewalld rule for %s: %s' % (ip, result.stderr or '')) + final_dic = {'status': 0, 'error_message': 'Failed to add firewall rule: %s' % (result.stderr or 'Unknown error')} + return HttpResponse(json.dumps(final_dic), content_type='application/json') except subprocess.TimeoutExpired: - logging.CyberCPLogFileWriter.writeToFile(f'Timeout adding firewalld rule for {ip}') + logging.CyberCPLogFileWriter.writeToFile('Timeout adding firewalld rule for %s' % ip) final_dic = {'status': 0, 'error_message': 'Firewall command timed out'} - final_json = json.dumps(final_dic) - return HttpResponse(final_json) + return HttpResponse(json.dumps(final_dic), content_type='application/json') except Exception as e: - logging.CyberCPLogFileWriter.writeToFile(f'Failed to add firewalld rule for {ip}: {str(e)}') - final_dic = {'status': 0, 'error_message': f'Firewall command failed: {str(e)}'} - final_json = json.dumps(final_dic) - return HttpResponse(final_json) + logging.CyberCPLogFileWriter.writeToFile('Failed to add firewalld rule for %s: %s' % (ip, str(e))) + final_dic = {'status': 0, 'error_message': 'Firewall command failed: %s' % str(e)} + return HttpResponse(json.dumps(final_dic), content_type='application/json') - final_dic = {'status': 1, 'message': f'IP address {ip} has been banned successfully'} - final_json = json.dumps(final_dic) - return HttpResponse(final_json) + final_dic = {'status': 1, 'message': 'IP address %s has been banned successfully' % ip} + return HttpResponse(json.dumps(final_dic), content_type='application/json') except BaseException as msg: final_dic = {'status': 0, 'error_message': str(msg)} - final_json = json.dumps(final_dic) - return HttpResponse(final_json) + return HttpResponse(json.dumps(final_dic), content_type='application/json') def removeBannedIP(self, userID=None, data=None): """ - Remove/unban an IP address + Remove/unban an IP address. + Supports both BannedIP database model and JSON file storage. """ try: admin = Administrator.objects.get(pk=userID) @@ -2001,21 +2052,65 @@ class FirewallManager: return ACLManager.loadError() banned_ip_id = data.get('id') + requested_ip = (data.get('ip') or '').strip() + ip_to_unban = None - # Load existing banned IPs - banned_ips_file = '/etc/cyberpanel/banned_ips.json' - banned_ips = [] - if os.path.exists(banned_ips_file): + try: + if isinstance(banned_ip_id, str) and banned_ip_id.isdigit(): + banned_ip_id = int(banned_ip_id) + elif isinstance(banned_ip_id, float): + banned_ip_id = int(banned_ip_id) + except Exception: + pass + + # Try database (BannedIP model) first - ids are typically small integers + try: + from firewall.models import BannedIP + except Exception as e: + BannedIP = None + logging.CyberCPLogFileWriter.writeToFile(f'Warning: BannedIP model import failed, using JSON fallback: {str(e)}') + + if BannedIP is not None: try: - with open(banned_ips_file, 'r') as f: - banned_ips = json.load(f) - except: - banned_ips = [] + banned_ip = None + if banned_ip_id not in (None, ''): + try: + banned_ip = BannedIP.objects.get(pk=banned_ip_id) + except BannedIP.DoesNotExist: + banned_ip = None + if banned_ip is None and requested_ip: + banned_ip = BannedIP.objects.filter(ip_address=requested_ip).first() + if banned_ip is None: + raise BannedIP.DoesNotExist() + ip_to_unban = banned_ip.ip_address + banned_ip.active = False + banned_ip.save() + # Remove firewalld rule + try: + import subprocess + firewalld_check = subprocess.run(['systemctl', 'is-active', 'firewalld'], + capture_output=True, text=True, timeout=10) + if firewalld_check.returncode == 0 and 'active' in firewalld_check.stdout: + rich_rule = f'rule family=ipv4 source address={ip_to_unban} drop' + subprocess.run(['firewall-cmd', '--permanent', '--remove-rich-rule', rich_rule], + capture_output=True, text=True, timeout=30) + subprocess.run(['firewall-cmd', '--reload'], capture_output=True, text=True, timeout=30) + except Exception as e: + logging.CyberCPLogFileWriter.writeToFile(f'Warning removing firewalld rule: {str(e)}') + final_dic = {'status': 1, 'message': f'IP address {ip_to_unban} has been unbanned successfully'} + return HttpResponse(json.dumps(final_dic), content_type='application/json') + except BannedIP.DoesNotExist: + pass + + # Fall back to JSON file storage + banned_ips, _ = self._load_banned_ips_store() # Find and update the banned IP ip_to_unban = None for banned_ip in banned_ips: - if banned_ip.get('id') == banned_ip_id: + id_match = banned_ip_id not in (None, '') and str(banned_ip.get('id')) == str(banned_ip_id) + ip_match = requested_ip and str(banned_ip.get('ip', '')).strip() == requested_ip + if id_match or ip_match: banned_ip['active'] = False banned_ip['unbanned_on'] = time.time() ip_to_unban = banned_ip['ip'] @@ -2024,11 +2119,10 @@ class FirewallManager: if not ip_to_unban: final_dic = {'status': 0, 'error_message': 'Banned IP not found'} final_json = json.dumps(final_dic) - return HttpResponse(final_json) + return HttpResponse(final_json, content_type='application/json') # Save updated banned IPs - with open(banned_ips_file, 'w') as f: - json.dump(banned_ips, f, indent=2) + self._save_banned_ips_store(banned_ips) # Remove firewalld rule to unblock the IP try: @@ -2067,16 +2161,17 @@ class FirewallManager: final_dic = {'status': 1, 'message': f'IP address {ip_to_unban} has been unbanned successfully'} final_json = json.dumps(final_dic) - return HttpResponse(final_json) + return HttpResponse(final_json, content_type='application/json') except BaseException as msg: final_dic = {'status': 0, 'error_message': str(msg)} final_json = json.dumps(final_dic) - return HttpResponse(final_json) + return HttpResponse(final_json, content_type='application/json') def deleteBannedIP(self, userID=None, data=None): """ - Permanently delete a banned IP record + Permanently delete a banned IP record. + Supports both BannedIP database model and JSON file storage. """ try: admin = Administrator.objects.get(pk=userID) @@ -2084,22 +2179,53 @@ class FirewallManager: return ACLManager.loadError() banned_ip_id = data.get('id') + requested_ip = (data.get('ip') or '').strip() - # Load existing banned IPs - banned_ips_file = '/etc/cyberpanel/banned_ips.json' - banned_ips = [] - if os.path.exists(banned_ips_file): + try: + if isinstance(banned_ip_id, str) and banned_ip_id.isdigit(): + banned_ip_id = int(banned_ip_id) + elif isinstance(banned_ip_id, float): + banned_ip_id = int(banned_ip_id) + except Exception: + pass + + # Try database (BannedIP model) first + try: + from firewall.models import BannedIP + except Exception as e: + BannedIP = None + logging.CyberCPLogFileWriter.writeToFile(f'Warning: BannedIP model import failed, using JSON fallback: {str(e)}') + + if BannedIP is not None: try: - with open(banned_ips_file, 'r') as f: - banned_ips = json.load(f) - except: - banned_ips = [] + banned_ip = None + if banned_ip_id not in (None, ''): + try: + banned_ip = BannedIP.objects.get(pk=banned_ip_id) + except BannedIP.DoesNotExist: + banned_ip = None + if banned_ip is None and requested_ip: + banned_ip = BannedIP.objects.filter(ip_address=requested_ip).first() + if banned_ip is None: + raise BannedIP.DoesNotExist() + ip_to_delete = banned_ip.ip_address + banned_ip.delete() + logging.CyberCPLogFileWriter.writeToFile(f'Deleted banned IP record for {ip_to_delete}') + final_dic = {'status': 1, 'message': f'Banned IP record for {ip_to_delete} has been deleted successfully'} + return HttpResponse(json.dumps(final_dic), content_type='application/json') + except BannedIP.DoesNotExist: + pass + + # Fall back to JSON file storage + banned_ips, _ = self._load_banned_ips_store() # Find and remove the banned IP ip_to_delete = None updated_banned_ips = [] for banned_ip in banned_ips: - if banned_ip.get('id') == banned_ip_id: + id_match = banned_ip_id not in (None, '') and str(banned_ip.get('id')) == str(banned_ip_id) + ip_match = requested_ip and str(banned_ip.get('ip', '')).strip() == requested_ip + if id_match or ip_match: ip_to_delete = banned_ip['ip'] else: updated_banned_ips.append(banned_ip) @@ -2107,22 +2233,147 @@ class FirewallManager: if not ip_to_delete: final_dic = {'status': 0, 'error_message': 'Banned IP record not found'} final_json = json.dumps(final_dic) - return HttpResponse(final_json) + return HttpResponse(final_json, content_type='application/json') # Save updated banned IPs - with open(banned_ips_file, 'w') as f: - json.dump(updated_banned_ips, f, indent=2) + self._save_banned_ips_store(updated_banned_ips) logging.CyberCPLogFileWriter.writeToFile(f'Deleted banned IP record for {ip_to_delete}') final_dic = {'status': 1, 'message': f'Banned IP record for {ip_to_delete} has been deleted successfully'} final_json = json.dumps(final_dic) - return HttpResponse(final_json) + return HttpResponse(final_json, content_type='application/json') except BaseException as msg: final_dic = {'status': 0, 'error_message': str(msg)} final_json = json.dumps(final_dic) - return HttpResponse(final_json) + return HttpResponse(final_json, content_type='application/json') + + def modifyBannedIP(self, userID=None, data=None): + """ + Modify an existing banned IP record (reason, duration). + Supports both BannedIP database model and JSON file storage. + """ + try: + admin = Administrator.objects.get(pk=userID) + if admin.acl.adminStatus != 1: + return ACLManager.loadError() + + banned_ip_id = data.get('id') + requested_ip = (data.get('ip') or '').strip() + reason = data.get('reason', '').strip() + duration = data.get('duration', '').strip() + + try: + if isinstance(banned_ip_id, str) and banned_ip_id.isdigit(): + banned_ip_id = int(banned_ip_id) + elif isinstance(banned_ip_id, float): + banned_ip_id = int(banned_ip_id) + except Exception: + pass + + if banned_ip_id in (None, '') and not requested_ip: + final_dic = {'status': 0, 'error_message': 'Banned IP ID or IP address is required'} + final_json = json.dumps(final_dic) + return HttpResponse(final_json, content_type='application/json') + + if not reason: + final_dic = {'status': 0, 'error_message': 'Reason is required'} + final_json = json.dumps(final_dic) + return HttpResponse(final_json, content_type='application/json') + + # Try database (BannedIP model) first - ids are typically small integers + try: + from firewall.models import BannedIP + except Exception as e: + BannedIP = None + logging.CyberCPLogFileWriter.writeToFile(f'Warning: BannedIP model import failed, using JSON fallback: {str(e)}') + + if BannedIP is not None: + try: + banned_ip = None + if banned_ip_id not in (None, ''): + try: + banned_ip = BannedIP.objects.get(pk=banned_ip_id) + except BannedIP.DoesNotExist: + banned_ip = None + if banned_ip is None and requested_ip: + banned_ip = BannedIP.objects.filter(ip_address=requested_ip).first() + if banned_ip is None: + raise BannedIP.DoesNotExist() + if not banned_ip.active: + final_dic = {'status': 0, 'error_message': 'Cannot modify an inactive/expired ban'} + final_json = json.dumps(final_dic) + return HttpResponse(final_json, content_type='application/json') + + banned_ip.reason = reason + if duration: + banned_ip.duration = duration + duration_map = { + '1h': 3600, + '24h': 86400, + '7d': 604800, + '30d': 2592000, + 'permanent': None + } + if duration == 'permanent': + banned_ip.expires = None + else: + duration_seconds = duration_map.get(duration, 86400) + banned_ip.expires = int(time.time()) + duration_seconds + banned_ip.save() + + logging.CyberCPLogFileWriter.writeToFile(f'Modified banned IP {banned_ip.ip_address} (id={banned_ip_id})') + final_dic = {'status': 1, 'message': f'Banned IP {banned_ip.ip_address} has been updated successfully'} + final_json = json.dumps(final_dic) + return HttpResponse(final_json, content_type='application/json') + + except BannedIP.DoesNotExist: + pass + + # Fall back to JSON file storage (ids are timestamps) + banned_ips, _ = self._load_banned_ips_store() + + updated = False + for banned_ip in banned_ips: + id_match = banned_ip_id not in (None, '') and str(banned_ip.get('id')) == str(banned_ip_id) + ip_match = requested_ip and str(banned_ip.get('ip', '')).strip() == requested_ip + if (id_match or ip_match) and banned_ip.get('active', True): + banned_ip['reason'] = reason + if duration: + banned_ip['duration'] = duration + if duration == 'permanent': + banned_ip['expires'] = 'Never' + else: + duration_map = { + '1h': 3600, + '24h': 86400, + '7d': 604800, + '30d': 2592000 + } + duration_seconds = duration_map.get(duration, 86400) + banned_ip['expires'] = time.time() + duration_seconds + updated = True + break + + if not updated: + final_dic = {'status': 0, 'error_message': 'Banned IP record not found'} + final_json = json.dumps(final_dic) + return HttpResponse(final_json, content_type='application/json') + + self._save_banned_ips_store(banned_ips) + + ip_addr = next((b.get('ip') for b in banned_ips if (str(b.get('id')) == str(banned_ip_id)) or (requested_ip and str(b.get('ip', '')).strip() == requested_ip)), 'unknown') + logging.CyberCPLogFileWriter.writeToFile(f'Modified banned IP {ip_addr} (id={banned_ip_id}) in JSON') + final_dic = {'status': 1, 'message': f'Banned IP {ip_addr} has been updated successfully'} + final_json = json.dumps(final_dic) + return HttpResponse(final_json, content_type='application/json') + + except BaseException as msg: + logging.CyberCPLogFileWriter.writeToFile(f'Error in modifyBannedIP: {str(msg)}') + final_dic = {'status': 0, 'error_message': str(msg)} + final_json = json.dumps(final_dic) + return HttpResponse(final_json, content_type='application/json') def exportFirewallRules(self, userID=None): """ @@ -2282,6 +2533,200 @@ class FirewallManager: final_json = json.dumps(final_dic) return HttpResponse(final_json) + def exportBannedIPs(self, userID=None): + """ + Export banned IPs to a JSON file + """ + try: + currentACL = ACLManager.loadedACL(userID) + if currentACL['admin'] != 1: + return ACLManager.loadErrorJson('exportStatus', 0) + + banned_records = [] + + # Try database model first + try: + from firewall.models import BannedIP + except Exception as e: + BannedIP = None + logging.CyberCPLogFileWriter.writeToFile(f'Warning: BannedIP model import failed, using JSON fallback: {str(e)}') + + if BannedIP is not None: + try: + for banned_ip in BannedIP.objects.all().order_by('-banned_on'): + banned_records.append({ + 'id': banned_ip.id, + 'ip': banned_ip.ip_address, + 'reason': banned_ip.reason, + 'duration': banned_ip.duration, + 'banned_on': banned_ip.get_banned_on_display(), + 'expires': banned_ip.get_expires_display(), + 'active': banned_ip.active and not banned_ip.is_expired() + }) + except Exception as e: + logging.CyberCPLogFileWriter.writeToFile(f'Error exporting banned IPs from DB: {str(e)}') + + # If DB is unavailable/empty, fall back to JSON file + if not banned_records: + banned_ips, _ = self._load_banned_ips_store() + + for b in banned_ips: + banned_records.append({ + 'id': b.get('id'), + 'ip': b.get('ip', ''), + 'reason': b.get('reason', ''), + 'duration': b.get('duration', 'permanent'), + 'banned_on': b.get('banned_on', 'N/A'), + 'expires': b.get('expires', 'Never'), + 'active': b.get('active', True) + }) + + export_data = { + 'version': '1.0', + 'exported_at': time.strftime('%Y-%m-%d %H:%M:%S'), + 'total_banned_ips': len(banned_records), + 'banned_ips': banned_records + } + + json_content = json.dumps(export_data, indent=2) + logging.CyberCPLogFileWriter.writeToFile(f"Banned IPs exported successfully. Total: {len(banned_records)}") + + response = HttpResponse(json_content, content_type='application/json') + response['Content-Disposition'] = f'attachment; filename=\"banned_ips_export_{int(time.time())}.json\"' + return response + + except BaseException as msg: + final_dic = {'exportStatus': 0, 'error_message': str(msg)} + final_json = json.dumps(final_dic) + return HttpResponse(final_json) + + def importBannedIPs(self, userID=None, data=None): + """ + Import banned IPs from a JSON file + """ + try: + currentACL = ACLManager.loadedACL(userID) + if currentACL['admin'] != 1: + return ACLManager.loadErrorJson('importStatus', 0) + + request_files = getattr(self.request, 'FILES', None) + if request_files and 'import_file' in request_files: + import_file = self.request.FILES['import_file'] + import_data = json.loads(import_file.read().decode('utf-8')) + else: + import_file_path = data.get('import_file_path', '') if data else '' + if not import_file_path or not os.path.exists(import_file_path): + final_dic = {'importStatus': 0, 'error_message': 'Import file not found or invalid path'} + final_json = json.dumps(final_dic) + return HttpResponse(final_json) + with open(import_file_path, 'r') as f: + import_data = json.load(f) + + if 'banned_ips' not in import_data or not isinstance(import_data.get('banned_ips'), list): + final_dic = {'importStatus': 0, 'error_message': 'Invalid import file format. Missing banned_ips array.'} + final_json = json.dumps(final_dic) + return HttpResponse(final_json) + + imported_count = 0 + skipped_count = 0 + error_count = 0 + errors = [] + + # Try database model first + try: + from firewall.models import BannedIP + except Exception as e: + BannedIP = None + logging.CyberCPLogFileWriter.writeToFile(f'Warning: BannedIP model import failed, using JSON fallback: {str(e)}') + + # Prepare JSON fallback store if needed + banned_ips_json = [] + if BannedIP is None: + banned_ips_json, _ = self._load_banned_ips_store() + + import ipaddress + for item in import_data.get('banned_ips', []): + try: + ip = (item.get('ip') or '').strip() + reason = (item.get('reason') or '').strip() + duration = (item.get('duration') or 'permanent').strip() + active = bool(item.get('active', True)) + + if not ip or not reason: + skipped_count += 1 + continue + + try: + ipaddress.ip_address(ip.split('/')[0]) + except ValueError: + error_count += 1 + errors.append(f"Invalid IP: {ip}") + continue + + if BannedIP is not None: + existing = BannedIP.objects.filter(ip_address=ip).first() + if existing: + skipped_count += 1 + continue + new_ban = BannedIP( + ip_address=ip, + reason=reason, + duration=duration, + active=active + ) + if duration == 'permanent': + new_ban.expires = None + else: + duration_map = {'1h': 3600, '24h': 86400, '7d': 604800, '30d': 2592000} + duration_seconds = duration_map.get(duration, 86400) + new_ban.expires = int(time.time()) + duration_seconds + new_ban.save() + imported_count += 1 + else: + # JSON fallback storage + exists = any(str(b.get('ip', '')).strip() == ip for b in banned_ips_json) + if exists: + skipped_count += 1 + continue + banned_ips_json.append({ + 'id': int(time.time() * 1000), + 'ip': ip, + 'reason': reason, + 'duration': duration, + 'banned_on': int(time.time()), + 'expires': 'Never' if duration == 'permanent' else int(time.time()) + { + '1h': 3600, '24h': 86400, '7d': 604800, '30d': 2592000 + }.get(duration, 86400), + 'active': active + }) + imported_count += 1 + + except Exception as e: + error_count += 1 + errors.append(f"IP '{item.get('ip', 'unknown')}': {str(e)}") + logging.CyberCPLogFileWriter.writeToFile(f"Error importing banned IP {item.get('ip', 'unknown')}: {str(e)}") + + if BannedIP is None: + self._save_banned_ips_store(banned_ips_json) + + logging.CyberCPLogFileWriter.writeToFile(f"Banned IPs import completed. Imported: {imported_count}, Skipped: {skipped_count}, Errors: {error_count}") + + final_dic = { + 'importStatus': 1, + 'error_message': "None", + 'imported_count': imported_count, + 'skipped_count': skipped_count, + 'error_count': error_count, + 'errors': errors + } + final_json = json.dumps(final_dic) + return HttpResponse(final_json) + + except BaseException as msg: + final_dic = {'importStatus': 0, 'error_message': str(msg)} + final_json = json.dumps(final_dic) + return HttpResponse(final_json) + diff --git a/firewall/templates/firewall/firewall.html b/firewall/templates/firewall/firewall.html index 21b1bab63..6a500128c 100644 --- a/firewall/templates/firewall/firewall.html +++ b/firewall/templates/firewall/firewall.html @@ -9,6 +9,10 @@ margin: 0 auto; padding: 2rem; } + + [ng-cloak], .ng-cloak { + display: none !important; + } .page-header { text-align: center; @@ -219,6 +223,34 @@ display: flex; align-items: center; justify-content: space-between; + gap: 1rem; + } + + .panel-actions { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + align-items: center; + } + + .btn-panel { + background: rgba(255, 255, 255, 0.15); + color: var(--text-light, white); + border: 1px solid rgba(255, 255, 255, 0.3); + padding: 0.5rem 0.75rem; + border-radius: 8px; + font-weight: 500; + font-size: 0.8rem; + cursor: pointer; + display: inline-flex; + align-items: center; + gap: 0.4rem; + transition: all 0.2s ease; + } + + .btn-panel:hover { + background: rgba(255, 255, 255, 0.25); + transform: translateY(-1px); } .panel-title { @@ -326,6 +358,12 @@ border: 1px solid var(--border-color, #e8e9ff); } + .table-responsive { + width: 100%; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + } + .rules-table thead { background: var(--bg-tertiary, #f8f9ff); } @@ -356,6 +394,12 @@ background: var(--bg-hover, #f8f9ff); } + .actions { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + } + .rule-id { font-weight: 600; color: var(--text-muted, #64748b); @@ -791,8 +835,147 @@ box-shadow: 0 2px 8px rgba(239, 68, 68, 0.3); } + .btn-modify { + background: var(--accent-color, #5b5fcf); + color: var(--bg-secondary, white); + border: none; + padding: 0.5rem 1rem; + border-radius: 6px; + font-weight: 500; + cursor: pointer; + transition: all 0.3s ease; + display: flex; + align-items: center; + gap: 0.25rem; + font-size: 0.8rem; + } + + .btn-modify:hover { + background: var(--accent-hover, #4f46e5); + transform: translateY(-1px); + box-shadow: 0 2px 8px rgba(91, 95, 207, 0.3); + } + + .modify-modal-overlay { + display: none; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + z-index: 9999; + align-items: center; + justify-content: center; + } + + .modify-modal-overlay.show { + display: flex; + } + + .modify-modal { + background: var(--bg-secondary, white); + border-radius: 12px; + padding: 2rem; + max-width: 480px; + width: 90%; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); + } + + .modify-modal h3 { + margin: 0 0 1.5rem 0; + font-size: 1.25rem; + color: var(--text-primary, #1e293b); + } + + .modify-modal .form-group { + margin-bottom: 1rem; + } + + .modify-modal .form-label { + display: block; + font-weight: 500; + margin-bottom: 0.5rem; + color: var(--text-secondary, #64748b); + } + + .modify-modal .form-control { + width: 100%; + padding: 0.5rem 0.75rem; + border: 1px solid var(--border-color, #e2e8f0); + border-radius: 6px; + } + + .modify-modal-actions { + display: flex; + gap: 1rem; + justify-content: flex-end; + margin-top: 1.5rem; + } + + .modify-modal-actions .btn-cancel { + background: #e2e8f0; + color: #64748b; + padding: 0.5rem 1rem; + border: none; + border-radius: 6px; + cursor: pointer; + } + + .modify-modal-actions .btn-save { + background: var(--accent-color, #5b5fcf); + color: white; + padding: 0.5rem 1rem; + border: none; + border-radius: 6px; + cursor: pointer; + font-weight: 500; + } + /* Responsive Design for Banned IPs */ @media (max-width: 768px) { + .status-content { + flex-direction: column; + align-items: stretch; + } + + .control-btn { + width: 100%; + } + + .rule-form { + grid-template-columns: 1fr; + gap: 1rem; + } + + .rules-table-section { + padding: 1rem; + overflow-x: auto; + } + + .rules-table { + min-width: 720px; + } + + .banned-search-section input { + max-width: 100% !important; + width: 100%; + } + + .panel-header { + flex-direction: column; + align-items: flex-start; + } + + .panel-actions { + width: 100%; + } + + .btn-panel { + width: 100%; + justify-content: center; + } + .banned-form { grid-template-columns: 1fr; gap: 1rem; @@ -828,7 +1011,7 @@ {% get_current_language as LANGUAGE_CODE %} -
+
@@ -1101,9 +1296,22 @@
+ +
+ + +
+
- +
+
@@ -1115,7 +1323,7 @@ - + -
{% trans "IP Address" %}
{$ bannedIP.ip $} @@ -1141,7 +1349,15 @@ +
+ +
- +

{% 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 $} +

+
+ + +
+
+ + +
+
+ + +
+
+
diff --git a/firewall/views.py b/firewall/views.py index 595d7edcb..5e42bcf96 100644 --- a/firewall/views.py +++ b/firewall/views.py @@ -671,33 +671,110 @@ 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): diff --git a/manageSSL/views.py b/manageSSL/views.py index e67db2610..2b74fe81c 100644 --- a/manageSSL/views.py +++ b/manageSSL/views.py @@ -12,23 +12,41 @@ from plogical.acl import ACLManager from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter as logging from plogical.sslReconcile import SSLReconcile from plogical.sslUtilities import sslUtilities +from loginSystem.models import Administrator import json def sslReconcile(request): """SSL Reconciliation interface""" try: - currentACL = ACLManager.loadedACL(request.user.pk) - admin = ACLManager.loadedAdmin(request.user.pk) + userID = request.session['userID'] + admin = Administrator.objects.get(pk=userID) + currentACL = ACLManager.loadedACL(userID) + # Principal admin (userName == 'admin') always allowed + if getattr(admin, 'userName', None) == 'admin': + return render(request, 'manageSSL/sslReconcile.html', { + 'acls': currentACL, + 'admin': admin + }) + # Allow if has sslReconcile, or full admin, or manageSSL permission if ACLManager.currentContextPermission(currentACL, 'sslReconcile') == 0: - return ACLManager.loadErrorJson('sslReconcile', 0) + if currentACL.get('admin') != 1 and currentACL.get('manageSSL') != 1: + return ACLManager.loadErrorJson('sslReconcile', 0) return render(request, 'manageSSL/sslReconcile.html', { 'acls': currentACL, 'admin': admin }) + except KeyError: + data_ret = { + 'status': 0, + 'errorMessage': 'Session expired or not logged in. Please log in again.', + 'error_message': 'Session expired or not logged in. Please log in again.', + 'sslReconcile': 0 + } + return HttpResponse(json.dumps(data_ret)) except BaseException as msg: logging.writeToFile(str(msg) + " [sslReconcile]") return ACLManager.loadErrorJson('sslReconcile', 0) @@ -37,11 +55,14 @@ def sslReconcile(request): def reconcileAllSSL(request): """Reconcile SSL for all domains""" try: - currentACL = ACLManager.loadedACL(request.user.pk) - admin = ACLManager.loadedAdmin(request.user.pk) + userID = request.session['userID'] + admin = Administrator.objects.get(pk=userID) + currentACL = ACLManager.loadedACL(userID) - if ACLManager.currentContextPermission(currentACL, 'sslReconcile') == 0: - return ACLManager.loadErrorJson('sslReconcile', 0) + if getattr(admin, 'userName', None) != 'admin': + if ACLManager.currentContextPermission(currentACL, 'sslReconcile') == 0: + if currentACL.get('admin') != 1 and currentACL.get('manageSSL') != 1: + return ACLManager.loadErrorJson('sslReconcile', 0) # Run SSL reconciliation success = SSLReconcile.reconcile_all() @@ -54,6 +75,9 @@ def reconcileAllSSL(request): json_data = json.dumps(data_ret) return HttpResponse(json_data) + except KeyError: + data_ret = {'reconcileStatus': 0, 'error_message': 'Session expired or not logged in. Please log in again.'} + return HttpResponse(json.dumps(data_ret)) except BaseException as msg: logging.writeToFile(str(msg) + " [reconcileAllSSL]") data_ret = {'reconcileStatus': 0, 'error_message': str(msg)} @@ -64,11 +88,14 @@ def reconcileAllSSL(request): def reconcileDomainSSL(request): """Reconcile SSL for a specific domain""" try: - currentACL = ACLManager.loadedACL(request.user.pk) - admin = ACLManager.loadedAdmin(request.user.pk) + userID = request.session['userID'] + admin = Administrator.objects.get(pk=userID) + currentACL = ACLManager.loadedACL(userID) - if ACLManager.currentContextPermission(currentACL, 'sslReconcile') == 0: - return ACLManager.loadErrorJson('sslReconcile', 0) + if getattr(admin, 'userName', None) != 'admin': + if ACLManager.currentContextPermission(currentACL, 'sslReconcile') == 0: + if currentACL.get('admin') != 1 and currentACL.get('manageSSL') != 1: + return ACLManager.loadErrorJson('sslReconcile', 0) domain = request.POST.get('domain') if not domain: @@ -87,6 +114,9 @@ def reconcileDomainSSL(request): json_data = json.dumps(data_ret) return HttpResponse(json_data) + except KeyError: + data_ret = {'reconcileStatus': 0, 'error_message': 'Session expired or not logged in. Please log in again.'} + return HttpResponse(json.dumps(data_ret)) except BaseException as msg: logging.writeToFile(str(msg) + " [reconcileDomainSSL]") data_ret = {'reconcileStatus': 0, 'error_message': str(msg)} @@ -97,11 +127,14 @@ def reconcileDomainSSL(request): def fixACMEContexts(request): """Fix ACME challenge contexts for all domains""" try: - currentACL = ACLManager.loadedACL(request.user.pk) - admin = ACLManager.loadedAdmin(request.user.pk) + userID = request.session['userID'] + admin = Administrator.objects.get(pk=userID) + currentACL = ACLManager.loadedACL(userID) - if ACLManager.currentContextPermission(currentACL, 'sslReconcile') == 0: - return ACLManager.loadErrorJson('sslReconcile', 0) + if getattr(admin, 'userName', None) != 'admin': + if ACLManager.currentContextPermission(currentACL, 'sslReconcile') == 0: + if currentACL.get('admin') != 1 and currentACL.get('manageSSL') != 1: + return ACLManager.loadErrorJson('sslReconcile', 0) from websiteFunctions.models import Websites @@ -128,6 +161,9 @@ def fixACMEContexts(request): json_data = json.dumps(data_ret) return HttpResponse(json_data) + except KeyError: + data_ret = {'reconcileStatus': 0, 'error_message': 'Session expired or not logged in. Please log in again.'} + return HttpResponse(json.dumps(data_ret)) except BaseException as msg: logging.writeToFile(str(msg) + " [fixACMEContexts]") data_ret = {'reconcileStatus': 0, 'error_message': str(msg)} diff --git a/plogical/acl.py b/plogical/acl.py index 512ecf3bd..57338174a 100644 --- a/plogical/acl.py +++ b/plogical/acl.py @@ -32,7 +32,7 @@ class ACLManager: '"createEmail": 1, "listEmails": 1, "deleteEmail": 1, "emailForwarding": 1, "changeEmailPassword": 1, ' \ '"dkimManager": 1, "createFTPAccount": 1, "deleteFTPAccount": 1, "listFTPAccounts": 1, "createBackup": 1,' \ ' "restoreBackup": 1, "addDeleteDestinations": 1, "scheduleBackups": 1, "remoteBackups": 1, "googleDriveBackups": 1, "manageSSL": 1, ' \ - '"hostnameSSL": 1, "mailServerSSL": 1 }' + '"hostnameSSL": 1, "mailServerSSL": 1, "sslReconcile": 1 }' ResellerACL = '{"adminStatus":0, "versionManagement": 1, "createNewUser": 1, "listUsers": 1, "deleteUser": 1 , "resellerCenter": 1, ' \ '"changeUserACL": 0, "createWebsite": 1, "modifyWebsite": 1, "suspendWebsite": 1, "deleteWebsite": 1, ' \ @@ -246,6 +246,7 @@ class ACLManager: finalResponse['manageSSL'] = config['manageSSL'] finalResponse['hostnameSSL'] = config['hostnameSSL'] finalResponse['mailServerSSL'] = config['mailServerSSL'] + finalResponse['sslReconcile'] = config.get('sslReconcile', 0) return finalResponse diff --git a/static/ftp/ftp.js b/static/ftp/ftp.js index e63a71d1a..6a7cb75da 100644 --- a/static/ftp/ftp.js +++ b/static/ftp/ftp.js @@ -6,8 +6,7 @@ /* Java script code to create account */ app.controller('createFTPAccount', function ($scope, $http) { - - + // Initialize all ng-hide variables to hide alerts on page load $scope.ftpLoading = false; $scope.ftpDetails = true; $scope.canNotCreateFTP = true; @@ -16,8 +15,10 @@ app.controller('createFTPAccount', function ($scope, $http) { $scope.generatedPasswordView = true; $(document).ready(function () { - $(".ftpDetails").hide(); - $(".ftpPasswordView").hide(); + $( ".ftpDetails" ).hide(); + $( ".ftpPasswordView" ).hide(); + + // Only use select2 if it's actually a function (avoids errors when Rocket Loader defers scripts) if (typeof $ !== 'undefined' && $ && typeof $.fn !== 'undefined' && typeof $.fn.select2 === 'function') { try { var $sel = $('.create-ftp-acct-select'); @@ -26,7 +27,6 @@ app.controller('createFTPAccount', function ($scope, $http) { $sel.on('select2:select', function (e) { var data = e.params.data; $scope.ftpDomain = data.text; - $scope.ftpDetails = false; $scope.$apply(); $(".ftpDetails").show(); }); @@ -42,15 +42,13 @@ app.controller('createFTPAccount', function ($scope, $http) { function initNativeSelect() { $('.create-ftp-acct-select').off('select2:select').on('change', function () { $scope.ftpDomain = $(this).val(); - $scope.ftpDetails = !($scope.ftpDomain && $scope.ftpDomain !== ""); $scope.$apply(); - if ($scope.ftpDomain && $scope.ftpDomain !== "") $(".ftpDetails").show(); - else $(".ftpDetails").hide(); + $(".ftpDetails").show(); }); } }); - - $scope.showFTPDetails = function () { + + $scope.showFTPDetails = function() { if ($scope.ftpDomain && $scope.ftpDomain !== "") { $(".ftpDetails").show(); $scope.ftpDetails = false; @@ -62,7 +60,7 @@ app.controller('createFTPAccount', function ($scope, $http) { $scope.createFTPAccount = function () { - $scope.ftpLoading = true; + $scope.ftpLoading = true; // Show loading while creating $scope.ftpDetails = false; $scope.canNotCreateFTP = true; $scope.successfullyCreatedFTP = true; @@ -73,18 +71,59 @@ app.controller('createFTPAccount', function ($scope, $http) { var ftpPassword = $scope.ftpPassword; var path = $scope.ftpPath; - if (typeof path === 'undefined' || path == null) path = ""; - else path = String(path).trim(); + // Enhanced path validation + if (typeof path === 'undefined' || path === null) { + path = ""; + } else { + path = path.trim(); + } + + // Client-side path validation + if (path && path !== "") { + // Check for dangerous characters + var dangerousChars = /[;&|$`'"<>*?~]/; + if (dangerousChars.test(path)) { + $scope.ftpLoading = false; + $scope.canNotCreateFTP = false; + $scope.successfullyCreatedFTP = true; + $scope.couldNotConnect = true; + $scope.errorMessage = "Invalid path: Path contains dangerous characters"; + return; + } + + // Check for path traversal attempts + if (path.indexOf("..") !== -1 || path.indexOf("~") !== -1) { + $scope.ftpLoading = false; + $scope.canNotCreateFTP = false; + $scope.successfullyCreatedFTP = true; + $scope.couldNotConnect = true; + $scope.errorMessage = "Invalid path: Path cannot contain '..' or '~'"; + return; + } + + // Check if path starts with slash (should be relative) + if (path.startsWith("/")) { + $scope.ftpLoading = false; + $scope.canNotCreateFTP = false; + $scope.successfullyCreatedFTP = true; + $scope.couldNotConnect = true; + $scope.errorMessage = "Invalid path: Path must be relative (not starting with '/')"; + return; + } + } + + var url = "/ftp/submitFTPCreation"; + var data = { ftpDomain: ftpDomain, ftpUserName: ftpUserName, passwordByPass: ftpPassword, - path: path, + path: path || '', + api: '0', enableCustomQuota: $scope.enableCustomQuota || false, customQuotaSize: $scope.customQuotaSize || 0, }; - var url = "/ftp/submitFTPCreation"; var config = { headers: { @@ -96,14 +135,12 @@ app.controller('createFTPAccount', function ($scope, $http) { function ListInitialDatas(response) { - - - if (response.data.creatFTPStatus === 1) { - $scope.ftpLoading = false; + if (response.data && response.data.creatFTPStatus === 1) { + $scope.ftpLoading = false; // Hide loading on success $scope.successfullyCreatedFTP = false; $scope.canNotCreateFTP = true; $scope.couldNotConnect = true; - $scope.createdFTPUsername = ftpDomain + "_" + ftpUserName; + $scope.createdFTPUsername = (response.data.createdFTPUsername != null && response.data.createdFTPUsername !== '') ? response.data.createdFTPUsername : (ftpDomain + '_' + ftpUserName); if (typeof PNotify !== 'undefined') { new PNotify({ title: 'Success!', text: 'FTP account successfully created.', type: 'success' }); } @@ -112,20 +149,22 @@ app.controller('createFTPAccount', function ($scope, $http) { $scope.canNotCreateFTP = false; $scope.successfullyCreatedFTP = true; $scope.couldNotConnect = true; - $scope.errorMessage = response.data.error_message; + $scope.errorMessage = (response.data && response.data.error_message) ? response.data.error_message : 'Unknown error'; if (typeof PNotify !== 'undefined') { - new PNotify({ title: 'Operation Failed!', text: response.data.error_message, type: 'error' }); + new PNotify({ title: 'Operation Failed!', text: $scope.errorMessage, type: 'error' }); } } - } + function cantLoadInitialDatas(response) { $scope.ftpLoading = false; - $scope.couldNotConnect = false; - $scope.canNotCreateFTP = true; - $scope.successfullyCreatedFTP = true; - if (typeof PNotify !== 'undefined') { - new PNotify({ title: 'Operation Failed!', text: 'Could not connect to server, please refresh this page', type: 'error' }); + if ($scope.successfullyCreatedFTP !== false) { + $scope.couldNotConnect = false; + $scope.canNotCreateFTP = true; + $scope.successfullyCreatedFTP = true; + if (typeof PNotify !== 'undefined') { + new PNotify({ title: 'Operation Failed!', text: 'Could not connect to server, please refresh this page', type: 'error' }); + } } } @@ -151,8 +190,11 @@ app.controller('createFTPAccount', function ($scope, $http) { $scope.generatedPasswordView = true; }; - $scope.toggleCustomQuota = function () { - if (!$scope.enableCustomQuota) $scope.customQuotaSize = 0; + // Quota management functions + $scope.toggleCustomQuota = function() { + if (!$scope.enableCustomQuota) { + $scope.customQuotaSize = 0; + } }; }); @@ -214,32 +256,24 @@ app.controller('deleteFTPAccount', function ($scope, $http) { } else { - $scope.ftpAccountsOfDomain = true; $scope.deleteFTPButton = true; - $scope.deleteFailure = true; + $scope.deleteFailure = false; $scope.deleteSuccess = true; - $scope.couldNotConnect = false; + $scope.couldNotConnect = true; $scope.deleteFTPButtonInit = true; - + $scope.errorMessage = (response.data && (response.data.error_message || response.data.errorMessage)) || 'Unknown error'; } - - } function cantLoadInitialDatas(response) { - $scope.ftpAccountsOfDomain = true; $scope.deleteFTPButton = true; $scope.deleteFailure = true; $scope.deleteSuccess = true; $scope.couldNotConnect = false; $scope.deleteFTPButtonInit = true; - - } - - }; $scope.deleteFTPAccount = function () { @@ -330,9 +364,10 @@ app.controller('listFTPAccounts', function ($scope, $http) { $scope.passwordChanged = true; $scope.canNotChangePassword = true; $scope.couldNotConnect = true; - $scope.ftpLoading = true; + $scope.ftpLoading = false; $scope.ftpAccounts = true; $scope.changePasswordBox = true; + $scope.quotaManagementBox = true; $scope.notificationsBox = true; var globalFTPUsername = ""; @@ -346,7 +381,7 @@ app.controller('listFTPAccounts', function ($scope, $http) { $scope.passwordChanged = true; $scope.canNotChangePassword = true; $scope.couldNotConnect = true; - $scope.ftpLoading = true; + $scope.ftpLoading = false; // Don't show loading when opening password dialog $scope.changePasswordBox = false; $scope.notificationsBox = true; $scope.ftpUsername = ftpUsername; @@ -356,7 +391,7 @@ app.controller('listFTPAccounts', function ($scope, $http) { $scope.changePasswordBtn = function () { - $scope.ftpLoading = false; + $scope.ftpLoading = true; // Show loading while changing password url = "/ftp/changePassword"; @@ -382,13 +417,13 @@ app.controller('listFTPAccounts', function ($scope, $http) { if (response.data.changePasswordStatus == 1) { $scope.notificationsBox = false; $scope.passwordChanged = false; - $scope.ftpLoading = true; + $scope.ftpLoading = false; // Hide loading when done $scope.domainFeteched = $scope.selectedDomain; } else { $scope.notificationsBox = false; $scope.canNotChangePassword = false; - $scope.ftpLoading = true; + $scope.ftpLoading = false; // Hide loading on error $scope.canNotChangePassword = false; $scope.errorMessage = response.data.error_message; } @@ -398,7 +433,7 @@ app.controller('listFTPAccounts', function ($scope, $http) { function cantLoadInitialDatas(response) { $scope.notificationsBox = false; $scope.couldNotConnect = false; - $scope.ftpLoading = true; + $scope.ftpLoading = false; // Hide loading on connection error } @@ -409,7 +444,7 @@ app.controller('listFTPAccounts', function ($scope, $http) { $scope.passwordChanged = true; $scope.canNotChangePassword = true; $scope.couldNotConnect = true; - $scope.ftpLoading = false; + $scope.ftpLoading = true; // Show loading while fetching $scope.ftpAccounts = true; $scope.changePasswordBox = true; @@ -444,7 +479,7 @@ app.controller('listFTPAccounts', function ($scope, $http) { $scope.passwordChanged = true; $scope.canNotChangePassword = true; $scope.couldNotConnect = true; - $scope.ftpLoading = true; + $scope.ftpLoading = false; // Hide loading when done $scope.ftpAccounts = false; $scope.changePasswordBox = true; @@ -456,7 +491,7 @@ app.controller('listFTPAccounts', function ($scope, $http) { $scope.passwordChanged = true; $scope.canNotChangePassword = true; $scope.couldNotConnect = true; - $scope.ftpLoading = true; + $scope.ftpLoading = false; // Hide loading on error $scope.ftpAccounts = true; $scope.changePasswordBox = true; @@ -471,7 +506,7 @@ app.controller('listFTPAccounts', function ($scope, $http) { $scope.passwordChanged = true; $scope.canNotChangePassword = true; $scope.couldNotConnect = false; - $scope.ftpLoading = true; + $scope.ftpLoading = false; // Hide loading on connection error $scope.ftpAccounts = true; $scope.changePasswordBox = true; @@ -493,4 +528,216 @@ app.controller('listFTPAccounts', function ($scope, $http) { $scope.generatedPasswordView = true; }; + // Quota management functions + $scope.manageQuota = function (record) { + $scope.recordsFetched = true; + $scope.passwordChanged = true; + $scope.canNotChangePassword = true; + $scope.couldNotConnect = true; + $scope.ftpLoading = false; + $scope.quotaManagementBox = false; + $scope.notificationsBox = true; + $scope.ftpUsername = record.user; + globalFTPUsername = record.user; + + // Set current quota info + $scope.currentQuotaInfo = record.quotasize; + $scope.packageQuota = record.package_quota; + $scope.enableCustomQuotaEdit = record.custom_quota_enabled; + $scope.customQuotaSizeEdit = record.custom_quota_size || 0; + }; + + $scope.toggleCustomQuotaEdit = function() { + if (!$scope.enableCustomQuotaEdit) { + $scope.customQuotaSizeEdit = 0; + } + }; + + $scope.updateQuotaBtn = function () { + $scope.ftpLoading = true; + + url = "/ftp/updateFTPQuota"; + + var data = { + ftpUserName: globalFTPUsername, + customQuotaSize: parseInt($scope.customQuotaSizeEdit) || 0, + enableCustomQuota: $scope.enableCustomQuotaEdit || false, + }; + + var config = { + headers: { + 'X-CSRFToken': getCookie('csrftoken') + } + }; + + $http.post(url, data, config).then(ListInitialDatas, cantLoadInitialDatas); + + function ListInitialDatas(response) { + if (response.data.updateQuotaStatus == 1) { + $scope.notificationsBox = false; + $scope.quotaUpdated = false; + $scope.ftpLoading = false; + $scope.domainFeteched = $scope.selectedDomain; + + // Refresh the records to show updated quota + populateCurrentRecords(); + + // Show success notification + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Success!', + text: 'FTP quota updated successfully.', + type: 'success' + }); + } + } else { + $scope.notificationsBox = false; + $scope.quotaUpdateFailed = false; + $scope.ftpLoading = false; + $scope.errorMessage = response.data.error_message; + + // Show error notification + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Error!', + text: response.data.error_message, + type: 'error' + }); + } + } + } + + function cantLoadInitialDatas(response) { + $scope.notificationsBox = false; + $scope.couldNotConnect = false; + $scope.ftpLoading = false; + + // Show error notification + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Error!', + text: 'Could not connect to server.', + type: 'error' + }); + } + } + }; + }); + + + +app.controller('Resetftpconf', function ($scope, $http, $timeout, $window){ + $scope.Loading = true; + $scope.NotifyBox = true; + $scope.InstallBox = true; + $scope.installationDetailsForm = false; + $scope.alertType = ''; + $scope.errorMessage = ''; + + $scope.resetftp = function () { + $scope.Loading = false; + $scope.installationDetailsForm = true; + $scope.InstallBox = false; + $scope.alertType = ''; + $scope.NotifyBox = true; + + var url = "/ftp/resetftpnow"; + var data = {}; + var config = { + headers: { + 'X-CSRFToken': getCookie('csrftoken'), + 'Content-Type': 'application/json' + } + }; + + $http.post(url, data, config).then(ListInitialData, cantLoadInitialData); + + function ListInitialData(response) { + if (response.data && response.data.status === 1) { + $scope.NotifyBox = true; + $scope.InstallBox = false; + $scope.Loading = false; + $scope.alertType = ''; + $scope.statusfile = response.data.tempStatusPath; + $timeout(getRequestStatus, 1000); + } else { + $scope.errorMessage = (response.data && (response.data.error_message || response.data.errorMessage)) || 'Unknown error'; + $scope.alertType = 'failedToStart'; + $scope.NotifyBox = false; + $scope.InstallBox = true; + $scope.Loading = false; + } + } + + function cantLoadInitialData(response) { + $scope.errorMessage = (response && response.data && (response.data.error_message || response.data.errorMessage)) || 'Could not connect to server. Please refresh this page.'; + $scope.alertType = 'couldNotConnect'; + $scope.NotifyBox = false; + $scope.InstallBox = true; + $scope.Loading = false; + try { + new PNotify({ title: 'Error!', text: $scope.errorMessage, type: 'error' }); + } catch (e) {} + } + } + + + + var statusPollPromise = null; + function getRequestStatus() { + $scope.NotifyBox = true; + $scope.InstallBox = false; + $scope.Loading = false; + $scope.alertType = ''; + + var url = "/ftp/getresetstatus"; + var data = { statusfile: $scope.statusfile }; + var config = { + headers: { + 'X-CSRFToken': getCookie('csrftoken'), + 'Content-Type': 'application/json' + } + }; + + $http.post(url, data, config).then(ListInitialDatas, cantLoadInitialDatas); + + function ListInitialDatas(response) { + if (!response.data) return; + if (response.data.abort === 0) { + $scope.alertType = ''; + $scope.requestData = response.data.requestStatus || ''; + statusPollPromise = $timeout(getRequestStatus, 1000); + } else { + if (statusPollPromise) { + $timeout.cancel(statusPollPromise); + statusPollPromise = null; + } + $scope.NotifyBox = false; + $scope.InstallBox = false; + $scope.Loading = false; + $scope.requestData = response.data.requestStatus || ''; + + if (response.data.installed === 0) { + $scope.alertType = 'resetFailed'; + $scope.errorMessage = response.data.error_message || 'Reset failed'; + } else { + $scope.alertType = 'success'; + $timeout(function () { $window.location.reload(); }, 3000); + } + } + } + + function cantLoadInitialDatas(response) { + if (statusPollPromise) { + $timeout.cancel(statusPollPromise); + statusPollPromise = null; + } + $scope.alertType = 'couldNotConnect'; + $scope.errorMessage = (response && response.data && (response.data.error_message || response.data.errorMessage)) || 'Could not connect to server. Please refresh this page.'; + $scope.NotifyBox = false; + $scope.InstallBox = true; + $scope.Loading = false; + } + } +}); \ No newline at end of file diff --git a/static/websiteFunctions/websiteFunctions.js b/static/websiteFunctions/websiteFunctions.js index f6c5f9dcb..dd86b2455 100644 --- a/static/websiteFunctions/websiteFunctions.js +++ b/static/websiteFunctions/websiteFunctions.js @@ -10743,39 +10743,6 @@ $("#modifyWebsiteLoading").hide(); $("#modifyWebsiteButton").hide(); app.controller('modifyWebsitesController', function ($scope, $http) { - - // Initialize home directory variables - $scope.homeDirectories = []; - $scope.selectedHomeDirectory = ''; - $scope.selectedHomeDirectoryInfo = null; - $scope.currentHomeDirectory = ''; - - // Load home directories on page load - $scope.loadHomeDirectories = function() { - $http.post('/userManagement/getUserHomeDirectories/', {}) - .then(function(response) { - if (response.data.status === 1) { - $scope.homeDirectories = response.data.directories; - } - }) - .catch(function(error) { - console.error('Error loading home directories:', error); - }); - }; - - // Update home directory info when selection changes - $scope.updateHomeDirectoryInfo = function() { - if ($scope.selectedHomeDirectory) { - $scope.selectedHomeDirectoryInfo = $scope.homeDirectories.find(function(dir) { - return dir.id == $scope.selectedHomeDirectory; - }); - } else { - $scope.selectedHomeDirectoryInfo = null; - } - }; - - // Initialize home directories - $scope.loadHomeDirectories(); $scope.fetchWebsites = function () { @@ -10820,7 +10787,6 @@ app.controller('modifyWebsitesController', function ($scope, $http) { $scope.webpacks = JSON.parse(response.data.packages); $scope.adminNames = JSON.parse(response.data.adminNames); $scope.currentAdmin = response.data.currentAdmin; - $scope.currentHomeDirectory = response.data.currentHomeDirectory || 'Default'; $("#webSiteDetailsToBeModified").fadeIn(); $("#websiteModifySuccess").fadeIn(); @@ -10848,7 +10814,6 @@ app.controller('modifyWebsitesController', function ($scope, $http) { var email = $scope.adminEmail; var phpVersion = $scope.phpSelection; var admin = $scope.selectedAdmin; - var homeDirectory = $scope.selectedHomeDirectory; $("#websiteModifyFailure").hide(); @@ -10865,8 +10830,7 @@ app.controller('modifyWebsitesController', function ($scope, $http) { packForWeb: packForWeb, email: email, phpVersion: phpVersion, - admin: admin, - homeDirectory: homeDirectory + admin: admin }; var config = { @@ -13338,16 +13302,6 @@ app.controller('manageAliasController', function ($scope, $http, $timeout, $wind $scope.manageAliasLoading = true; $scope.operationSuccess = true; - // Initialize the page to show aliases list - $scope.showAliasesList = function() { - $scope.aliasTable = true; - $scope.addAliasButton = true; - $scope.domainAliasForm = false; - }; - - // Auto-show aliases list on page load - $scope.showAliasesList(); - $scope.createAliasEnter = function ($event) { var keyCode = $event.which || $event.keyCode; if (keyCode === 13) { @@ -13595,64 +13549,6 @@ app.controller('manageAliasController', function ($scope, $http, $timeout, $wind }; - $scope.issueAliasSSL = function (masterDomain, aliasDomain) { - $scope.manageAliasLoading = false; - - url = "/websites/issueAliasSSL"; - - var data = { - masterDomain: masterDomain, - aliasDomain: aliasDomain - }; - - var config = { - headers: { - 'X-CSRFToken': getCookie('csrftoken') - } - }; - - $http.post(url, data, config).then(ListInitialDatas, cantLoadInitialDatas); - - function ListInitialDatas(response) { - if (response.data.issueAliasSSL === 1) { - $scope.aliasTable = false; - $scope.addAliasButton = true; - $scope.domainAliasForm = true; - $scope.aliasError = true; - $scope.couldNotConnect = true; - $scope.aliasCreated = true; - $scope.manageAliasLoading = true; - $scope.operationSuccess = false; - - $timeout(function () { - $window.location.reload(); - }, 3000); - } else { - $scope.aliasTable = false; - $scope.addAliasButton = true; - $scope.domainAliasForm = true; - $scope.aliasError = false; - $scope.couldNotConnect = true; - $scope.aliasCreated = true; - $scope.manageAliasLoading = true; - $scope.operationSuccess = true; - - $scope.errorMessage = response.data.error_message; - } - } - - function cantLoadInitialDatas(response) { - $scope.aliasTable = false; - $scope.addAliasButton = true; - $scope.domainAliasForm = true; - $scope.aliasError = true; - $scope.couldNotConnect = false; - $scope.aliasCreated = true; - $scope.manageAliasLoading = true; - $scope.operationSuccess = true; - } - }; - ////// create domain part From 1685c7913927a0b4ee680f4d1f9914a30ef2ee5d Mon Sep 17 00:00:00 2001 From: master3395 Date: Tue, 3 Feb 2026 20:01:50 +0100 Subject: [PATCH 017/130] Upgrader: add --no-system-update to skip full yum/dnf update Speeds up upgrade when system packages are already updated. Use: bash cyberpanel_upgrade.sh -b 2.5.5-dev -r master3395 --no-system-update --- cyberpanel_upgrade.sh | 71 +++++++++++++++++++++++++++++-------------- 1 file changed, 48 insertions(+), 23 deletions(-) diff --git a/cyberpanel_upgrade.sh b/cyberpanel_upgrade.sh index 046f0dbb5..b1d16f9e3 100644 --- a/cyberpanel_upgrade.sh +++ b/cyberpanel_upgrade.sh @@ -316,20 +316,51 @@ do done } +# Optional GitHub user override (same repo structure as usmannasir/cyberpanel) +# Set via --repo USER or -r USER to use any GitHub user's cyberpanel repo +Git_User_Override="" +# Skip full system package update (yum/dnf update -y) to speed up upgrade; use when system is already updated +Skip_System_Update="" + Check_Argument() { -if [[ "$*" = *"--branch "* ]] || [[ "$*" = *"-b "* ]]; then - Branch_Name=$(echo "$*" | sed -e "s/--branch //" -e "s/--mirror//" -e "s/-b //") - Branch_Check "$Branch_Name" +# 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 } Pre_Upgrade_Setup_Git_URL() { if [[ $Server_Country != "CN" ]] ; then - Git_User="usmannasir" + if [[ -n "$Git_User_Override" ]]; then + Git_User="$Git_User_Override" + echo -e "\nUsing GitHub repo: ${Git_User}/cyberpanel (same URL structure as usmannasir)\n" + else + Git_User="usmannasir" + fi Git_Content_URL="https://raw.githubusercontent.com/${Git_User}/cyberpanel" Git_Clone_URL="https://github.com/${Git_User}/cyberpanel.git" else - Git_User="qtwrk" + 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 @@ -375,7 +406,11 @@ if [[ "$Server_OS" = "CentOS" ]] || [[ "$Server_OS" = "AlmaLinux9" ]] ; then curl -o /etc/yum.repos.d/litespeed.repo https://cyberpanel.sh/litespeed/litespeed.repo fi yum clean all - yum update -y + 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 @@ -528,7 +563,11 @@ elif [[ "$Server_OS" = "Ubuntu" ]] ; then fi apt update -y - export DEBIAN_FRONTEND=noninteractive ; apt-get -o Dpkg::Options::="--force-confold" upgrade -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 if [[ "$Server_OS_Version" = "22" ]] || [[ "$Server_OS_Version" = "24" ]] ; then if [[ "$Server_OS_Version" = "24" ]]; then @@ -663,7 +702,7 @@ if [ $CYBERCP_MISSING -eq 1 ]; then cd /usr/local rm -rf CyberCP_recovery_tmp - if git clone https://github.com/usmannasir/cyberpanel CyberCP_recovery_tmp; then + 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 @@ -860,21 +899,7 @@ fi } -Pre_Upgrade_Setup_Git_URL() { -if [[ $Server_Country != "CN" ]] ; then - Git_User="usmannasir" - Git_Content_URL="https://raw.githubusercontent.com/${Git_User}/cyberpanel" - Git_Clone_URL="https://github.com/${Git_User}/cyberpanel.git" -else - Git_User="qtwrk" - 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 -} +# (Pre_Upgrade_Setup_Git_URL is defined earlier; this duplicate removed so --repo is respected) 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" From bbe91c34a46ca6d1453f750f1567ba0118b46367 Mon Sep 17 00:00:00 2001 From: master3395 Date: Tue, 3 Feb 2026 20:45:55 +0100 Subject: [PATCH 018/130] OLS 1.8.5+ and LSWS 6.3.4: LiteSpeed repo, version check, LSWS fallback and sed - Install: add LiteSpeed repo (repo.litespeed.sh), install openlitespeed; keep official binary if >= 1.8.5, else optional custom binary overlay - Upgrade: add repo, upgrade openlitespeed package; only run installCustomOLSBinaries if version < 1.8.5 - LSWS: fallback to 6.3.4 when API empty in cyberpanel_upgrade.sh and venvsetup.sh; sed lsws-6.0 and lsws-6.3.4 to latest stable - plogical/upgrade: add_litespeed_repo(), get_installed_ols_version(); install.py: add_litespeed_repo(), get_installed_ols_version() --- cyberpanel_upgrade.sh | 6 ++ install/install.py | 61 +++++++++++++++--- install/venvsetup.sh | 8 +++ plogical/upgrade.py | 42 ++++++++++++- .../OPENLITESPEED-VERSION-INSTALL-UPGRADE.md | 62 +++++++++++++++++++ 5 files changed, 168 insertions(+), 11 deletions(-) create mode 100644 to-do/OPENLITESPEED-VERSION-INSTALL-UPGRADE.md diff --git a/cyberpanel_upgrade.sh b/cyberpanel_upgrade.sh index b1d16f9e3..2210944fa 100644 --- a/cyberpanel_upgrade.sh +++ b/cyberpanel_upgrade.sh @@ -72,6 +72,10 @@ LSWS_Latest_URL="https://cyberpanel.sh/update.litespeedtech.com/ws/latest.php" LSWS_Tmp=$(curl --silent --max-time 30 -4 "$LSWS_Latest_URL") LSWS_Stable_Line=$(echo "$LSWS_Tmp" | grep "LSWS_STABLE") LSWS_Stable_Version=$(expr "$LSWS_Stable_Line" : '.*LSWS_STABLE=\(.*\) BUILD .*') +# Fallback to LSWS 6.3.4 (Stable) if fetch failed or empty +if [ -z "$LSWS_Stable_Version" ]; then + LSWS_Stable_Version="6.3.4" +fi #grab the LSWS latest stable version. Debug_Log2 "Starting Upgrade...1" @@ -1285,6 +1289,8 @@ Post_Upgrade_System_Tweak() { 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 diff --git a/install/install.py b/install/install.py index 53c5ab69e..5c6c42024 100644 --- a/install/install.py +++ b/install/install.py @@ -78,6 +78,47 @@ class preFlightsChecks: def is_debian_family(self): """Check if distro is Ubuntu or Debian 12""" return self.distro in [ubuntu, debian12] + + def add_litespeed_repo(self): + """Add LiteSpeed repository so OpenLiteSpeed 1.8.5+ is available (repo.litespeed.sh)""" + try: + self.stdOut("Adding LiteSpeed repository for OpenLiteSpeed 1.8.5+...", 1) + cmd = 'wget -q -O - https://repo.litespeed.sh | bash' + ret = subprocess.run(cmd, shell=True, timeout=120, capture_output=True, universal_newlines=True) + if ret.returncode != 0 and ret.stderr: + self.stdOut(f"LiteSpeed repo script warning: {ret.stderr[:200]}", 1) + if ret.returncode == 0: + self.stdOut("LiteSpeed repository added", 1) + return True + # Non-fatal: distro openlitespeed may still be used + self.stdOut("Could not add LiteSpeed repo; using distro package", 1) + return False + except Exception as e: + self.stdOut(f"LiteSpeed repo add failed: {e}", 1) + return False + + def get_installed_ols_version(self): + """Return installed OpenLiteSpeed version as (major, minor, patch) or None""" + try: + for binary in ('/usr/local/lsws/bin/lshttpd', '/usr/local/lsws/bin/openlitespeed'): + if not os.path.exists(binary): + continue + result = subprocess.run( + [binary, '-v'], + capture_output=True, + timeout=5, + universal_newlines=True, + env=dict(os.environ, PATH=os.environ.get('PATH', '/usr/bin:/bin')) + ) + out = (result.stdout or '') + (result.stderr or '') + # e.g. "OpenLiteSpeed/1.8.5" or "1.8.5" + import re + m = re.search(r'(\d+)\.(\d+)\.(\d+)', out) + if m: + return (int(m.group(1)), int(m.group(2)), int(m.group(3))) + return None + except Exception: + return None def detect_os_info(self): """Detect OS information for all supported platforms""" @@ -1172,7 +1213,7 @@ class preFlightsChecks: platform = self.detectPlatform() self.stdOut(f"Detected platform: {platform}", 1) - # Platform-specific URLs and checksums (OpenLiteSpeed v1.8.4.1 - v2.0.5 Static Build) + # Platform-specific URLs and checksums (OpenLiteSpeed 1.8.5+ preferred from repo; fallback static build) # Module Build Date: December 28, 2025 - v2.2.0 Brute Force with Progressive Throttle BINARY_CONFIGS = { 'rhel8': { @@ -1381,16 +1422,20 @@ module cyberpanel_ols { self.stdOut("Installing LiteSpeed Web Server...", 1) if ent == 0: - # Install OpenLiteSpeed - self.stdOut("Installing OpenLiteSpeed...", 1) - if self.distro == ubuntu: + # Install OpenLiteSpeed 1.8.5+ from LiteSpeed repo when possible + self.stdOut("Installing OpenLiteSpeed (target 1.8.5+)...", 1) + self.add_litespeed_repo() + if self.distro == ubuntu or self.distro == debian12: self.install_package('openlitespeed') else: self.install_package('openlitespeed') - - # Install custom binaries with PHP config support - # This replaces the standard binary with enhanced version - self.installCustomOLSBinaries() + # Use official OLS 1.8.5+ when available; only overlay custom binary if older + ols_ver = self.get_installed_ols_version() + if ols_ver and ols_ver >= (1, 8, 5): + self.stdOut("Using official OpenLiteSpeed 1.8.5+ (no custom binary overlay)", 1) + else: + # Install custom binaries with PHP config support (for pre-1.8.5 or when repo not used) + self.installCustomOLSBinaries() # Configure OpenLiteSpeed self.fix_ols_configs() diff --git a/install/venvsetup.sh b/install/venvsetup.sh index bd5b472eb..a2422a71e 100644 --- a/install/venvsetup.sh +++ b/install/venvsetup.sh @@ -116,6 +116,10 @@ LATEST_URL="https://update.litespeedtech.com/ws/latest.php" curl --silent -o /tmp/lsws_latest $LATEST_URL 2>/dev/null LSWS_STABLE_LINE=`cat /tmp/lsws_latest | grep LSWS_STABLE` LSWS_STABLE_VER=`expr "$LSWS_STABLE_LINE" : '.*LSWS_STABLE=\(.*\) BUILD .*'` +# Fallback to LSWS 6.3.4 (Stable) if fetch failed or empty +if [ -z "$LSWS_STABLE_VER" ]; then + LSWS_STABLE_VER="6.3.4" +fi if [[ $SERVER_COUNTRY == "CN" ]] ; then #line1="$(grep -n "github.com/usmannasir/cyberpanel" install.py | head -n 1 | cut -d: -f1)" @@ -890,6 +894,8 @@ fi sed -i 's|lsws-5.4.2|lsws-'$LSWS_STABLE_VER'|g' installCyberPanel.py sed -i 's|lsws-5.3.5|lsws-'$LSWS_STABLE_VER'|g' installCyberPanel.py +sed -i 's|lsws-6.0|lsws-'$LSWS_STABLE_VER'|g' installCyberPanel.py +sed -i 's|lsws-6.3.4|lsws-'$LSWS_STABLE_VER'|g' installCyberPanel.py #this sed must be done after license validation echo -e "Preparing..." @@ -1229,6 +1235,8 @@ fi sed -i 's|lsws-5.3.8|lsws-'$LSWS_STABLE_VER'|g' /usr/local/CyberCP/serverStatus/serverStatusUtil.py sed -i 's|lsws-5.4.2|lsws-'$LSWS_STABLE_VER'|g' /usr/local/CyberCP/serverStatus/serverStatusUtil.py sed -i 's|lsws-5.3.5|lsws-'$LSWS_STABLE_VER'|g' /usr/local/CyberCP/serverStatus/serverStatusUtil.py +sed -i 's|lsws-6.0|lsws-'$LSWS_STABLE_VER'|g' /usr/local/CyberCP/serverStatus/serverStatusUtil.py +sed -i 's|lsws-6.3.4|lsws-'$LSWS_STABLE_VER'|g' /usr/local/CyberCP/serverStatus/serverStatusUtil.py if [[ $SILENT != "ON" ]] ; then printf "%s" "Would you like to restart your server now? [y/N]: " diff --git a/plogical/upgrade.py b/plogical/upgrade.py index 67cfc2a3e..bfefa5c52 100644 --- a/plogical/upgrade.py +++ b/plogical/upgrade.py @@ -500,6 +500,33 @@ class Upgrade: except: return False + @staticmethod + def add_litespeed_repo(): + """Add LiteSpeed repository so OpenLiteSpeed 1.8.5+ is available (repo.litespeed.sh).""" + return Upgrade.executioner_silent('wget -q -O - https://repo.litespeed.sh | bash', 'LiteSpeed repo', 0, shell=True) + + @staticmethod + def get_installed_ols_version(): + """Return installed OpenLiteSpeed version as (major, minor, patch) or None.""" + try: + for binary in ('/usr/local/lsws/bin/lshttpd', '/usr/local/lsws/bin/openlitespeed'): + if not os.path.exists(binary): + continue + result = subprocess.run( + [binary, '-v'], + capture_output=True, + timeout=5, + universal_newlines=True, + env=dict(os.environ, PATH=os.environ.get('PATH', '/usr/bin:/bin')) + ) + out = (result.stdout or '') + (result.stderr or '') + m = re.search(r'(\d+)\.(\d+)\.(\d+)', out) + if m: + return (int(m.group(1)), int(m.group(2)), int(m.group(3))) + return None + except Exception: + return None + @staticmethod def updateRepoURL(): command = "sed -i 's|sgp.cyberpanel.sh|cdn.cyberpanel.sh|g' /etc/yum.repos.d/MariaDB.repo" @@ -939,7 +966,7 @@ class Upgrade: platform = Upgrade.detectPlatform() Upgrade.stdOut(f"Detected platform: {platform}", 0) - # Platform-specific URLs and checksums (OpenLiteSpeed v1.8.4.1 with PHPConfig + Header unset fix + Static Linking) + # Platform-specific URLs and checksums (OpenLiteSpeed 1.8.5+ preferred from repo; fallback static build) # Module Build Date: December 28, 2025 - v2.2.0 Brute Force with Progressive Throttle BINARY_CONFIGS = { 'rhel8': { @@ -5684,9 +5711,18 @@ slowlog = /var/log/php{version}-fpm-slow.log Upgrade.setupPHPSymlink() Upgrade.setupComposer() - # Install custom OpenLiteSpeed binaries if OLS is installed + # OpenLiteSpeed: ensure 1.8.5+ (add LiteSpeed repo, upgrade package); only overlay custom binary if still < 1.8.5 if os.path.exists('/usr/local/lsws/bin/openlitespeed'): - Upgrade.installCustomOLSBinaries() + Upgrade.add_litespeed_repo() + if os.path.exists(Upgrade.CentOSPath) or os.path.exists(Upgrade.openEulerPath): + Upgrade.executioner('dnf install -y openlitespeed || yum install -y openlitespeed', 'Upgrade OpenLiteSpeed package', 0) + else: + Upgrade.executioner('DEBIAN_FRONTEND=noninteractive apt-get -y install --only-upgrade openlitespeed 2>/dev/null || DEBIAN_FRONTEND=noninteractive apt-get -y install openlitespeed', 'Upgrade OpenLiteSpeed package', 0, shell=True) + ols_ver = Upgrade.get_installed_ols_version() + if ols_ver and ols_ver >= (1, 8, 5): + Upgrade.stdOut("OpenLiteSpeed 1.8.5+ detected; keeping official binary (no custom overlay).") + else: + Upgrade.installCustomOLSBinaries() ## diff --git a/to-do/OPENLITESPEED-VERSION-INSTALL-UPGRADE.md b/to-do/OPENLITESPEED-VERSION-INSTALL-UPGRADE.md new file mode 100644 index 000000000..5846b78d1 --- /dev/null +++ b/to-do/OPENLITESPEED-VERSION-INSTALL-UPGRADE.md @@ -0,0 +1,62 @@ +# OpenLiteSpeed and LSWS Version Used by CyberPanel (Install & Upgrade) + +**Updated:** OLS target 1.8.5+ (LiteSpeed repo); LSWS fallback 6.3.4. + +## Summary + +- **Install:** OpenLiteSpeed is installed via the **OS package manager** (no fixed version in code), then optionally replaced by CyberPanel’s **custom static binary** (based on **OpenLiteSpeed 1.8.5 – v2.0.5**). +- **Upgrade:** The CyberPanel **upgrade script does not change the OpenLiteSpeed version**. It only updates **LiteSpeed Enterprise** version references. During upgrade, **custom OLS binaries** (same 1.8.5-based build) are (re)installed if OLS is present. + +--- + +## Install + +1. **Package install** + - `install/install.py` → `installLiteSpeed(ent=0)` → `install_package('openlitespeed')`. + - So the **base** install is whatever **openlitespeed** version the distro provides (yum/dnf or apt). There is **no fixed OLS version** in the installer for this step. + +2. **Custom binary (optional)** + - Right after that, `installCustomOLSBinaries()` runs (in both `install/install.py` and `plogical/upgrade.py`). + - It downloads a **static binary** from `https://cyberpanel.net/` (e.g. `openlitespeed-phpconfig-x86_64-rhel8-static`) and replaces `/usr/local/lsws/bin/openlitespeed`. + - Comments in code state this is **OpenLiteSpeed 1.8.5** (upgrade.py) or **1.8.5 – v2.0.5** (install.py). The download URLs do not include a version; the binary is a fixed build hosted by CyberPanel. + +So on **install**, you get either: +- **Distro OLS** (version = whatever the OS repo has), or +- **CyberPanel custom OLS** (based on **1.8.5 / v2.0.5** static build) if the custom binary install succeeds. + +--- + +## Upgrade + +1. **cyberpanel_upgrade.sh** + - Fetches **LiteSpeed Enterprise** latest version from: + - `LSWS_Latest_URL="https://cyberpanel.sh/update.litespeedtech.com/ws/latest.php"` + - Parses `LSWS_Stable_Version` from the `LSWS_STABLE` line. + - Uses `LSWS_Stable_Version` only to **sed**-replace hardcoded Enterprise version strings (e.g. `lsws-5.3.8`, `lsws-5.4.2`, `lsws-5.3.5`) in `/usr/local/CyberCP/serverStatus/serverStatusUtil.py`. + - So the **upgrade script does not install or upgrade OpenLiteSpeed**; it only updates **Enterprise** version references. + +2. **plogical/upgrade.py** + - During upgrade, if OpenLiteSpeed is present (`/usr/local/lsws/bin/openlitespeed` exists), it runs: + - `Upgrade.installCustomOLSBinaries()` + - That (re)installs the **same custom static OLS binary** (1.8.5-based, from cyberpanel.net). So **upgrade** does not pull a “new” OLS version from upstream; it only refreshes CyberPanel’s custom binary if OLS is in use. + +--- + +## References (in repo) + +| What | Where | +|------|--------| +| OLS package install (no version) | `install/install.py` → `install_package('openlitespeed')` in `installLiteSpeed()` | +| Custom OLS binary (1.8.5 / 1.8.5–v2.0.5) | `install/install.py` and `plogical/upgrade.py` → `installCustomOLSBinaries()` and `BINARY_CONFIGS` comments | +| LSWS version used in upgrade (Enterprise only) | `cyberpanel_upgrade.sh` → `LSWS_Latest_URL`, `LSWS_Stable_Version`, and sed to `serverStatusUtil.py` | +| Custom OLS on upgrade | `plogical/upgrade.py` → `if os.path.exists('/usr/local/lsws/bin/openlitespeed'): Upgrade.installCustomOLSBinaries()` | + +--- + +## Short answers + +- **What OpenLiteSpeed version does install use?** + Package: **distro default**. If custom binary is used: **OpenLiteSpeed 1.8.5 (or 1.8.5–v2.0.5)** static build from cyberpanel.net. + +- **What OpenLiteSpeed version does upgrade use?** + Upgrade does **not** change OLS version from upstream. It only (re)installs the **same custom 1.8.5-based** binary when OLS is present. **LiteSpeed Enterprise** version is the one fetched from `cyberpanel.sh/update.litespeedtech.com/ws/latest.php` and written into `serverStatusUtil.py`. From d130785ab1e9c6a0ed2b7c22b9f6dec870900ad1 Mon Sep 17 00:00:00 2001 From: master3395 Date: Tue, 3 Feb 2026 21:08:32 +0100 Subject: [PATCH 019/130] MariaDB 11.8 LTS: use 11.8 for install and upgrade (replace 10.11/12.1) - cyberpanel_upgrade.sh: MariaDB.repo baseurl 12.1 -> 11.8 - plogical/upgrade.py: mariadb_repo_setup 10.11 -> 11.8 (AlmaLinux 9 fix) - install/install.py: mariadb_repo_setup 12.1 -> 11.8; _attemptMariaDBUpgrade and installMySQL target 11.8; disableMariaDB12RepositoryIfNeeded for 10.x; use existing if 11.x/12.x - install/universal_os_fixes.py: setup_mariadb_repository 12.1 -> 11.8 - databases/databaseManager.py: mysqlversions add 11.8 - plogical/mysqlUtilities.py: RHEL baseurl https and versionToInstall - to-do/MARIADB-11.8-LTS-UPGRADE.md --- cyberpanel_upgrade.sh | 4 +- databases/databaseManager.py | 2 +- install/install.py | 85 ++++++++++++++++++------------- install/universal_os_fixes.py | 4 +- plogical/mysqlUtilities.py | 2 +- plogical/upgrade.py | 10 ++-- to-do/MARIADB-11.8-LTS-UPGRADE.md | 20 ++++++++ 7 files changed, 80 insertions(+), 47 deletions(-) create mode 100644 to-do/MARIADB-11.8-LTS-UPGRADE.md diff --git a/cyberpanel_upgrade.sh b/cyberpanel_upgrade.sh index 2210944fa..363e3ac73 100644 --- a/cyberpanel_upgrade.sh +++ b/cyberpanel_upgrade.sh @@ -461,11 +461,11 @@ if [[ "$Server_OS" = "CentOS" ]] || [[ "$Server_OS" = "AlmaLinux9" ]] ; then fi cat << EOF > /etc/yum.repos.d/MariaDB.repo -# MariaDB 12.1 repository list - updated 2025-09-25 +# MariaDB 11.8 LTS repository list - updated 2026-02 # https://downloads.mariadb.org/mariadb/repositories/ [mariadb] name = MariaDB -baseurl = https://mirror.mariadb.org/yum/12.1/$MARIADB_REPO +baseurl = https://mirror.mariadb.org/yum/11.8/$MARIADB_REPO gpgkey=https://yum.mariadb.org/RPM-GPG-KEY-MariaDB gpgcheck=1 EOF diff --git a/databases/databaseManager.py b/databases/databaseManager.py index 5bf124fec..0b0c7617a 100644 --- a/databases/databaseManager.py +++ b/databases/databaseManager.py @@ -134,7 +134,7 @@ class DatabaseManager: def Upgardemysql(self, request = None, userID = None): data={} - data['mysqlversions']=['10.6','10.11'] + data['mysqlversions']=['10.6','10.11','11.8'] template = 'databases/Updatemysql.html' proc = httpProc(request, template, data, 'admin') return proc.render() diff --git a/install/install.py b/install/install.py index 5c6c42024..09d6046bd 100644 --- a/install/install.py +++ b/install/install.py @@ -1531,19 +1531,18 @@ module cyberpanel_ols { return False def disableMariaDB12RepositoryIfNeeded(self): - """Disable MariaDB 12.1 repository if MariaDB 10.x is already installed to prevent upgrade attempts""" + """Disable MariaDB 12.x repository if MariaDB 10.x is already installed so 11.8 LTS upgrade can be used""" try: is_installed, installed_version, major_minor = self.checkExistingMariaDB() if is_installed and major_minor and major_minor != "unknown": try: major_ver = float(major_minor) - if major_ver < 12.0: - # MariaDB 10.x is installed, disable 12.1 repository to prevent upgrade attempts - self.stdOut(f"MariaDB {installed_version} detected, disabling MariaDB 12.1 repository to prevent upgrade conflicts", 1) - logging.InstallLog.writeToFile(f"MariaDB {installed_version} detected, disabling MariaDB 12.1 repository") + if major_ver < 11.0: + # MariaDB 10.x is installed, disable 12.x repo so we use 11.8 LTS + self.stdOut(f"MariaDB {installed_version} detected, disabling MariaDB 12.x repository", 1) + logging.InstallLog.writeToFile(f"MariaDB {installed_version} detected, disabling MariaDB 12.x repository") - # Disable MariaDB 12.1 repository - check all possible repo file locations repo_files = [ '/etc/yum.repos.d/mariadb-main.repo', '/etc/yum.repos.d/mariadb.repo', @@ -1700,7 +1699,7 @@ module cyberpanel_ols { return False, None, None def _attemptMariaDBUpgrade(self): - """Attempt to upgrade MariaDB to 12.1. Returns True if successful, False otherwise.""" + """Attempt to upgrade MariaDB to 11.8 LTS. Returns True if successful, False otherwise.""" try: if self.distro == ubuntu: # Ubuntu MariaDB upgrade @@ -1719,8 +1718,8 @@ module cyberpanel_ols { logging.InstallLog.writeToFile(f"Failed to download MariaDB keyring: {result.stderr}") return False - # Setup MariaDB 12.1 repository - command = 'curl -LsS https://downloads.mariadb.com/MariaDB/mariadb_repo_setup | sudo bash -s -- --mariadb-server-version=12.1' + # Setup MariaDB 11.8 LTS repository + command = 'curl -LsS https://downloads.mariadb.com/MariaDB/mariadb_repo_setup | sudo bash -s -- --mariadb-server-version=11.8' result = subprocess.run(command, shell=True, capture_output=True, universal_newlines=True) if result.returncode != 0: logging.InstallLog.writeToFile(f"Failed to setup MariaDB repository: {result.stderr}") @@ -1747,15 +1746,14 @@ module cyberpanel_ols { return True else: # RHEL-based MariaDB upgrade - # Setup MariaDB 12.1 repository - command = 'curl -LsS https://downloads.mariadb.com/MariaDB/mariadb_repo_setup | sudo bash -s -- --mariadb-server-version=12.1' + # Setup MariaDB 11.8 LTS repository + command = 'curl -LsS https://downloads.mariadb.com/MariaDB/mariadb_repo_setup | sudo bash -s -- --mariadb-server-version=11.8' result = subprocess.run(command, shell=True, capture_output=True, universal_newlines=True) if result.returncode != 0: logging.InstallLog.writeToFile(f"Failed to setup MariaDB repository: {result.stderr}") return False - # Attempt to install MariaDB 12.1 - # Use --allowerasing to allow package replacements if needed + # Attempt to install MariaDB 11.8 LTS (repo already set above) command = 'dnf install mariadb-server mariadb-devel mariadb-client-utils -y --allowerasing' result = subprocess.run(command, shell=True, capture_output=True, universal_newlines=True) if result.returncode != 0: @@ -1787,26 +1785,26 @@ module cyberpanel_ols { if major_minor and major_minor != "unknown": try: major_ver = float(major_minor) - if major_ver < 12.0: + if major_ver < 11.0: should_try_upgrade = True - self.stdOut(f"Existing MariaDB {major_minor} detected. Attempting to upgrade to MariaDB 12.1...", 1) + self.stdOut(f"Existing MariaDB {major_minor} detected. Attempting to upgrade to MariaDB 11.8 LTS...", 1) self.stdOut("If upgrade fails, we will use the existing MariaDB installation.", 1) except (ValueError, TypeError): pass - # If MariaDB 10.x is installed, try to upgrade to 12.1 first + # If MariaDB 10.x is installed, try to upgrade to 11.8 LTS first if should_try_upgrade: try: - self.stdOut("Attempting to install MariaDB 12.1...", 1) + self.stdOut("Attempting to install MariaDB 11.8 LTS...", 1) upgrade_success = self._attemptMariaDBUpgrade() if upgrade_success: - self.stdOut("✅ Successfully upgraded to MariaDB 12.1", 1) + self.stdOut("✅ Successfully upgraded to MariaDB 11.8 LTS", 1) self.startMariaDB() self.changeMYSQLRootPassword() self.fixMariaDB() return True else: - self.stdOut("⚠️ MariaDB 12.1 upgrade failed, using existing MariaDB installation", 1) + self.stdOut("⚠️ MariaDB 11.8 LTS upgrade failed, using existing MariaDB installation", 1) self.startMariaDB() return True except Exception as upgrade_error: @@ -1857,7 +1855,7 @@ module cyberpanel_ols { else: # RHEL-based MariaDB installation # CRITICAL: Remove conflicting MariaDB compat packages first - # These packages from MariaDB 12.1 can conflict with MariaDB 10.11 + # These packages from MariaDB 12.x can conflict with 10.x/11.x self.stdOut("Removing conflicting MariaDB compat packages...", 1) try: # Multiple aggressive removal attempts to ensure compat package is gone @@ -1891,13 +1889,12 @@ module cyberpanel_ols { if is_installed: self.stdOut(f"MariaDB/MySQL is already installed (version: {installed_version}), skipping installation", 1) - # Don't set up 12.1 repository if 10.x is installed to avoid upgrade issues + # Use existing if already on 11.x or 12.x if major_minor and major_minor != "unknown": try: major_ver = float(major_minor) - if major_ver < 12.0: - self.stdOut("Skipping MariaDB 12.1 repository setup to avoid upgrade conflicts", 1) - self.stdOut("Using existing MariaDB installation", 1) + if major_ver >= 11.0: + self.stdOut("Using existing MariaDB installation (11.x/12.x)", 1) self.startMariaDB() self.changeMYSQLRootPassword() self.fixMariaDB() @@ -1905,8 +1902,8 @@ module cyberpanel_ols { except (ValueError, TypeError): pass - # Set up MariaDB 12.1 repository only if not already installed - command = 'curl -LsS https://downloads.mariadb.com/MariaDB/mariadb_repo_setup | bash -s -- --mariadb-server-version=12.1' + # Set up MariaDB 11.8 LTS repository only if not already installed + command = 'curl -LsS https://downloads.mariadb.com/MariaDB/mariadb_repo_setup | bash -s -- --mariadb-server-version=11.8' self.call(command, self.distro, command, command, 1, 1, os.EX_OSERR, True) command = 'dnf install mariadb-server mariadb-devel mariadb-client-utils -y' @@ -3728,24 +3725,40 @@ class Migration(migrations.Migration): def download_install_phpmyadmin(self): try: - if not os.path.exists("/usr/local/CyberCP/public"): os.mkdir("/usr/local/CyberCP/public") + try: + shutil.rmtree("/usr/local/CyberCP/public/phpmyadmin") + except Exception: + pass - command = 'wget -O /usr/local/CyberCP/public/phpmyadmin.zip https://github.com/usmannasir/cyberpanel/raw/stable/phpmyadmin.zip' + # Resolve phpMyAdmin version (same as upgrade path) + phpmyadmin_version = '5.2.3' + try: + from plogical.versionFetcher import get_latest_phpmyadmin_version + latest_version = get_latest_phpmyadmin_version() + if latest_version and latest_version != phpmyadmin_version: + self.stdOut(f"Using latest phpMyAdmin version: {latest_version}", 1) + phpmyadmin_version = latest_version + else: + self.stdOut(f"Using fallback phpMyAdmin version: {phpmyadmin_version}", 1) + except Exception as e: + self.stdOut(f"Failed to fetch latest phpMyAdmin version, using fallback: {e}", 1) - preFlightsChecks.call(command, self.distro, '[download_install_phpmyadmin]', + self.stdOut("Installing phpMyAdmin...", 1) + command = ( + f'wget -q -O /usr/local/CyberCP/public/phpmyadmin.tar.gz ' + f'https://files.phpmyadmin.net/phpMyAdmin/{phpmyadmin_version}/phpMyAdmin-{phpmyadmin_version}-all-languages.tar.gz' + ) + preFlightsChecks.call(command, self.distro, f'[download_install_phpmyadmin] {phpmyadmin_version}', command, 1, 0, os.EX_OSERR) - - command = 'unzip /usr/local/CyberCP/public/phpmyadmin.zip -d /usr/local/CyberCP/public' - preFlightsChecks.call(command, self.distro, '[download_install_phpmyadmin]', + command = 'tar -xzf /usr/local/CyberCP/public/phpmyadmin.tar.gz -C /usr/local/CyberCP/public/' + preFlightsChecks.call(command, self.distro, '[download_install_phpmyadmin] extract', command, 1, 0, os.EX_OSERR) - command = 'mv /usr/local/CyberCP/public/phpMyAdmin-*-all-languages /usr/local/CyberCP/public/phpmyadmin' subprocess.call(command, shell=True) - - command = 'rm -f /usr/local/CyberCP/public/phpmyadmin.zip' - preFlightsChecks.call(command, self.distro, '[download_install_phpmyadmin]', + command = 'rm -f /usr/local/CyberCP/public/phpmyadmin.tar.gz' + preFlightsChecks.call(command, self.distro, '[download_install_phpmyadmin] cleanup', command, 1, 0, os.EX_OSERR) ## Write secret phrase diff --git a/install/universal_os_fixes.py b/install/universal_os_fixes.py index eea45dc19..1e29d4a04 100644 --- a/install/universal_os_fixes.py +++ b/install/universal_os_fixes.py @@ -476,14 +476,14 @@ class UniversalOSFixes: cmd = [ 'curl', '-LsS', 'https://downloads.mariadb.com/MariaDB/mariadb_repo_setup', - '|', 'sudo', 'bash', '-s', '--', '--mariadb-server-version=12.1' + '|', 'sudo', 'bash', '-s', '--', '--mariadb-server-version=11.8' ] else: # RHEL family MariaDB setup cmd = [ 'curl', '-LsS', 'https://downloads.mariadb.com/MariaDB/mariadb_repo_setup', - '|', 'sudo', 'bash', '-s', '--', '--mariadb-server-version=12.1' + '|', 'sudo', 'bash', '-s', '--', '--mariadb-server-version=11.8' ] subprocess.run(' '.join(cmd), shell=True, check=True) diff --git a/plogical/mysqlUtilities.py b/plogical/mysqlUtilities.py index 92b568e27..123c96189 100644 --- a/plogical/mysqlUtilities.py +++ b/plogical/mysqlUtilities.py @@ -1360,7 +1360,7 @@ Signed-By: /etc/apt/keyrings/mariadb-keyring.pgp RepoContent = f""" [mariadb] name = MariaDB -baseurl = http://yum.mariadb.org/{versionToInstall}/rhel8-amd64 +baseurl = https://mirror.mariadb.org/yum/{versionToInstall}/rhel8-amd64 module_hotfixes=1 gpgkey=https://yum.mariadb.org/RPM-GPG-KEY-MariaDB gpgcheck=1 diff --git a/plogical/upgrade.py b/plogical/upgrade.py index bfefa5c52..ee4defcea 100644 --- a/plogical/upgrade.py +++ b/plogical/upgrade.py @@ -4326,7 +4326,7 @@ echo $oConfig->Save() ? 'Done' : 'Error'; Upgrade.stdOut("Applying AlmaLinux 9 MariaDB fixes...", 1) try: - # CRITICAL: Remove MariaDB-server-compat* before any MariaDB install (conflicts with 10.11) + # CRITICAL: Remove MariaDB-server-compat* before any MariaDB install (conflicts with 11.x) Upgrade.stdOut("Removing conflicting MariaDB-server-compat packages...", 1) try: # Multiple aggressive removal attempts to ensure compat package is gone @@ -4334,7 +4334,7 @@ echo $oConfig->Save() ? 'Done' : 'Error'; subprocess.run("dnf remove -y --allowerasing 'MariaDB-server-compat*' 2>/dev/null || true", shell=True, timeout=60) # Step 2: Force remove with rpm - subprocess.run("rpm -e --nodeps MariaDB-server-compat-12.1.2-1.el9.noarch 2>/dev/null; true", shell=True, timeout=30) + subprocess.run("rpm -e --nodeps MariaDB-server-compat-12.1.2-1.el9.noarch 2>/dev/null; true", shell=True, timeout=30) # cleanup if present from previous 12.1 # Step 3: Find and remove any remaining compat packages r = subprocess.run("rpm -qa 2>/dev/null | grep -i MariaDB-server-compat", shell=True, capture_output=True, text=True, timeout=30) @@ -4376,9 +4376,9 @@ echo $oConfig->Save() ? 'Done' : 'Error'; command = "dnf clean all" subprocess.run(command, shell=True, capture_output=True) - # Install MariaDB 10.11 from official repository (avoid 12.1 compat conflicts) - Upgrade.stdOut("Setting up official MariaDB repository...", 1) - command = "curl -sS https://downloads.mariadb.com/MariaDB/mariadb_repo_setup | bash -s -- --mariadb-server-version='10.11'" + # Install MariaDB 11.8 LTS from official repository + Upgrade.stdOut("Setting up official MariaDB 11.8 LTS repository...", 1) + command = "curl -sS https://downloads.mariadb.com/MariaDB/mariadb_repo_setup | bash -s -- --mariadb-server-version='11.8'" result = subprocess.run(command, shell=True, capture_output=True, text=True) if result.returncode != 0: Upgrade.stdOut(f"Warning: MariaDB repo setup failed: {result.stderr}", 0) diff --git a/to-do/MARIADB-11.8-LTS-UPGRADE.md b/to-do/MARIADB-11.8-LTS-UPGRADE.md new file mode 100644 index 000000000..e6260c8ab --- /dev/null +++ b/to-do/MARIADB-11.8-LTS-UPGRADE.md @@ -0,0 +1,20 @@ +# MariaDB 11.8 LTS (Long Term Service) + +## Summary + +CyberPanel install and upgrade now target **MariaDB 11.8 LTS** instead of 10.11 or 12.1. + +- **New installs:** Use `mariadb_repo_setup --mariadb-server-version=11.8` and install from official MariaDB 11.8 repo. +- **Upgrades:** Same; AlmaLinux 9 fix sets up 11.8 repo and installs MariaDB 11.8. +- **cyberpanel_upgrade.sh:** Writes `/etc/yum.repos.d/MariaDB.repo` with `baseurl = https://mirror.mariadb.org/yum/11.8/$MARIADB_REPO`. +- **UI (Database upgrade):** `databases/databaseManager.py` offers versions 10.6, 10.11, **11.8** for manual upgrade. +- **mysqlUtilities.UpgradeMariaDB:** Still accepts version argument; repo baseurl uses `versionToInstall` (e.g. 11.8). + +## References + +- `cyberpanel_upgrade.sh`: MariaDB.repo 11.8 +- `plogical/upgrade.py`: mariadb_repo_setup 11.8, fix_almalinux9_comprehensive() +- `install/install.py`: mariadb_repo_setup 11.8, _attemptMariaDBUpgrade(), installMySQL(), disableMariaDB12RepositoryIfNeeded() +- `install/universal_os_fixes.py`: setup_mariadb_repository() 11.8 +- `databases/databaseManager.py`: mysqlversions 10.6, 10.11, 11.8 +- `plogical/mysqlUtilities.py`: UpgradeMariaDB() baseurl for RHEL From b037e37bde6337f6887312eed2a337c52575fd67 Mon Sep 17 00:00:00 2001 From: master3395 Date: Tue, 3 Feb 2026 21:12:16 +0100 Subject: [PATCH 020/130] cyberpanel_upgrade: prefer MariaDB-server/MariaDB-client/MariaDB-devel for dnf install (AlmaLinux 9) --- cyberpanel_upgrade.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cyberpanel_upgrade.sh b/cyberpanel_upgrade.sh index 363e3ac73..19d667cba 100644 --- a/cyberpanel_upgrade.sh +++ b/cyberpanel_upgrade.sh @@ -546,8 +546,8 @@ EOF sqlite-devel libxml2-devel libxslt-devel curl-devel libedit-devel \ readline-devel pkgconfig cmake gcc-c++ - # Install MariaDB - dnf install -y mariadb-server mariadb-devel mariadb-client + # Install MariaDB (use MariaDB-* names when official 11.8 repo is configured) + dnf install -y MariaDB-server MariaDB-client MariaDB-devel 2>/dev/null || dnf install -y mariadb-server mariadb-devel mariadb-client # Install additional required packages dnf install -y wget curl unzip zip rsync firewalld psmisc git python3 python3-pip python3-devel From fbe97a4e7903a932c0b81862720c7bb30fc2a39d Mon Sep 17 00:00:00 2001 From: master3395 Date: Tue, 3 Feb 2026 21:17:25 +0100 Subject: [PATCH 021/130] Let user choose MariaDB 11.8 or 12.1; default 11.8 - install.py: use preFlightsChecks.mariadb_version in mariadb_repo_setup - venvsetup.sh: prompt for MariaDB 11.8/12.1 in interactive install, pass --mariadb-version to install.py - cyberpanel_upgrade.sh: add MARIADB_VER (default 11.8), --mariadb-version arg, interactive prompt, write /etc/cyberpanel/mariadb_version, use in MariaDB.repo - plogical/upgrade.py: read mariadb_version from /etc/cyberpanel/mariadb_version in fix_almalinux9_mariadb(), default 11.8 --- cyberpanel_upgrade.sh | 34 ++++++++++++++++++++++++++++++++-- install/install.py | 11 +++++++++-- install/venvsetup.sh | 16 ++++++++++++++-- plogical/upgrade.py | 16 +++++++++++++--- 4 files changed, 68 insertions(+), 9 deletions(-) diff --git a/cyberpanel_upgrade.sh b/cyberpanel_upgrade.sh index 19d667cba..e09daba9c 100644 --- a/cyberpanel_upgrade.sh +++ b/cyberpanel_upgrade.sh @@ -325,6 +325,8 @@ done Git_User_Override="" # Skip full system package update (yum/dnf update -y) to speed up upgrade; use when system is already updated Skip_System_Update="" +# MariaDB version for repo setup: 11.8 (LTS, default) or 12.1 +MARIADB_VER="11.8" Check_Argument() { # Parse --branch / -b (extract first word after -b or --branch) @@ -347,6 +349,14 @@ if [[ "$*" = *"--no-system-update"* ]]; then Skip_System_Update="yes" echo -e "\nUsing --no-system-update: skipping full system package update.\n" fi +# Parse --mariadb-version 11.8|12.1 (default 11.8) +if [[ "$*" = *"--mariadb-version "* ]]; then + MARIADB_VER=$(echo "$*" | sed -n 's/.*--mariadb-version \([^ ]*\).*/\1/p' | head -1) + MARIADB_VER="${MARIADB_VER:-11.8}" +fi +if [[ "$MARIADB_VER" != "11.8" ]] && [[ "$MARIADB_VER" != "12.1" ]]; then + MARIADB_VER="11.8" +fi } Pre_Upgrade_Setup_Git_URL() { @@ -461,11 +471,11 @@ if [[ "$Server_OS" = "CentOS" ]] || [[ "$Server_OS" = "AlmaLinux9" ]] ; then fi cat << EOF > /etc/yum.repos.d/MariaDB.repo -# MariaDB 11.8 LTS repository list - updated 2026-02 +# MariaDB $MARIADB_VER repository list - updated 2026-02 # https://downloads.mariadb.org/mariadb/repositories/ [mariadb] name = MariaDB -baseurl = https://mirror.mariadb.org/yum/11.8/$MARIADB_REPO +baseurl = https://mirror.mariadb.org/yum/$MARIADB_VER/$MARIADB_REPO gpgkey=https://yum.mariadb.org/RPM-GPG-KEY-MariaDB gpgcheck=1 EOF @@ -1661,6 +1671,26 @@ if [[ "$*" != *"--branch "* ]] && [[ "$*" != *"-b "* ]] ; then Pre_Upgrade_Branch_Input fi +# Prompt for MariaDB version if not set via --mariadb-version (default 11.8) +if [[ "$*" != *"--mariadb-version "* ]]; then + echo -e "\nMariaDB version: use \e[31m11.8\e[39m LTS (default) or \e[31m12.1\e[39m." + echo -e "Press Enter for 11.8 LTS, or type \e[31m12.1\e[39m and Enter for 12.1 (5 sec timeout): " + read -r -t 5 Tmp_MariaDB_Ver || true + if [[ "$Tmp_MariaDB_Ver" = "12.1" ]]; then + MARIADB_VER="12.1" + echo -e "MariaDB 12.1 selected.\n" + else + MARIADB_VER="11.8" + echo -e "MariaDB 11.8 LTS (default).\n" + fi +fi + +# Write chosen MariaDB version for upgrade.py (e.g. fix_almalinux9_mariadb) +mkdir -p /etc/cyberpanel +echo "$MARIADB_VER" > /etc/cyberpanel/mariadb_version +chmod 644 /etc/cyberpanel/mariadb_version +echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] MariaDB version set to: $MARIADB_VER" | tee -a /var/log/cyberpanel_upgrade_debug.log + Pre_Upgrade_Setup_Repository Pre_Upgrade_Setup_Git_URL diff --git a/install/install.py b/install/install.py index 09d6046bd..071cff5cf 100644 --- a/install/install.py +++ b/install/install.py @@ -1902,8 +1902,9 @@ module cyberpanel_ols { except (ValueError, TypeError): pass - # Set up MariaDB 11.8 LTS repository only if not already installed - command = 'curl -LsS https://downloads.mariadb.com/MariaDB/mariadb_repo_setup | bash -s -- --mariadb-server-version=11.8' + # Set up MariaDB repository only if not already installed (version from --mariadb-version, default 11.8) + mariadb_ver = getattr(preFlightsChecks, 'mariadb_version', '11.8') + command = f'curl -LsS https://downloads.mariadb.com/MariaDB/mariadb_repo_setup | bash -s -- --mariadb-server-version={mariadb_ver}' self.call(command, self.distro, command, command, 1, 1, os.EX_OSERR, True) command = 'dnf install mariadb-server mariadb-devel mariadb-client-utils -y' @@ -6470,8 +6471,14 @@ def main(): parser.add_argument('--mysqluser', help='MySQL user if remote is chosen.') parser.add_argument('--mysqlpassword', help='MySQL password if remote is chosen.') parser.add_argument('--mysqlport', help='MySQL port if remote is chosen.') + parser.add_argument('--mariadb-version', default='11.8', help='MariaDB version: 11.8 (LTS, default) or 12.1') args = parser.parse_args() + # Normalize and validate MariaDB version choice (default 11.8) + mariadb_ver = (getattr(args, 'mariadb_version', None) or '11.8').strip() + if mariadb_ver not in ('11.8', '12.1'): + mariadb_ver = '11.8' + preFlightsChecks.mariadb_version = mariadb_ver logging.InstallLog.ServerIP = args.publicip logging.InstallLog.writeToFile("Starting CyberPanel installation..,10") diff --git a/install/venvsetup.sh b/install/venvsetup.sh index a2422a71e..ad892ad2e 100644 --- a/install/venvsetup.sh +++ b/install/venvsetup.sh @@ -19,6 +19,7 @@ KEY_SIZE="" ADMIN_PASS="1234567" MEMCACHED="ON" REDIS="ON" +MARIADB_VER="11.8" TOTAL_RAM=$(free -m | awk '/Mem\:/ { print $2 }') # Robust pip install function to handle broken pipe errors @@ -873,6 +874,17 @@ if [[ $TMP_YN =~ ^(no|n|N) ]] ; then else REDIS="ON" fi + +echo -e "\nWhich MariaDB version do you want to install? \e[31m11.8\e[39m (LTS, default) or \e[31m12.1\e[39m?" +printf "%s" "Choose [1] for 11.8 LTS (recommended), [2] for 12.1, or press Enter for default [1]: " +read TMP_YN +if [[ $TMP_YN =~ ^(2|12\.1) ]] ; then + MARIADB_VER="12.1" + echo -e "\nMariaDB 12.1 will be installed.\n" +else + MARIADB_VER="11.8" + echo -e "\nMariaDB 11.8 LTS will be installed (default).\n" +fi } main_install() { @@ -909,9 +921,9 @@ fi if [[ $debug == "1" ]] ; then if [[ $DEV == "ON" ]] ; then - /usr/local/CyberPanel/bin/python install.py $SERVER_IP $SERIAL_NO $LICENSE_KEY + /usr/local/CyberPanel/bin/python install.py $SERVER_IP $SERIAL_NO $LICENSE_KEY --mariadb-version "${MARIADB_VER:-11.8}" else - /usr/local/CyberPanel/bin/python2 install.py $SERVER_IP $SERIAL_NO $LICENSE_KEY + /usr/local/CyberPanel/bin/python2 install.py $SERVER_IP $SERIAL_NO $LICENSE_KEY --mariadb-version "${MARIADB_VER:-11.8}" fi if grep "CyberPanel installation successfully completed" /var/log/installLogs.txt > /dev/null; then diff --git a/plogical/upgrade.py b/plogical/upgrade.py index ee4defcea..731877b1a 100644 --- a/plogical/upgrade.py +++ b/plogical/upgrade.py @@ -4376,9 +4376,19 @@ echo $oConfig->Save() ? 'Done' : 'Error'; command = "dnf clean all" subprocess.run(command, shell=True, capture_output=True) - # Install MariaDB 11.8 LTS from official repository - Upgrade.stdOut("Setting up official MariaDB 11.8 LTS repository...", 1) - command = "curl -sS https://downloads.mariadb.com/MariaDB/mariadb_repo_setup | bash -s -- --mariadb-server-version='11.8'" + # Install MariaDB from official repository (version from /etc/cyberpanel/mariadb_version or default 11.8) + mariadb_ver = "11.8" + try: + mariadb_version_file = "/etc/cyberpanel/mariadb_version" + if os.path.isfile(mariadb_version_file): + with open(mariadb_version_file, "r") as f: + raw = f.read().strip() + if raw in ("11.8", "12.1"): + mariadb_ver = raw + except Exception: + pass + Upgrade.stdOut("Setting up official MariaDB %s repository..." % mariadb_ver, 1) + command = "curl -sS https://downloads.mariadb.com/MariaDB/mariadb_repo_setup | bash -s -- --mariadb-server-version='%s'" % mariadb_ver result = subprocess.run(command, shell=True, capture_output=True, text=True) if result.returncode != 0: Upgrade.stdOut(f"Warning: MariaDB repo setup failed: {result.stderr}", 0) From 5450ff7f7c28bb7522c087fec45b0a979c8ed2b7 Mon Sep 17 00:00:00 2001 From: master3395 Date: Tue, 3 Feb 2026 21:23:52 +0100 Subject: [PATCH 022/130] MariaDB 11.8/12.1: tests, downgrade note, doc - test/upgrader_mariadb_version_test.sh: upgrader arg parsing and repo URL - test/test_upgrade_mariadb_version.py: mariadb_version file read, downgrade - test/run_mariadb_tests.sh: run all MariaDB version tests - cyberpanel_upgrade.sh: prompt mentions re-run for downgrade (11.8/12.1) - to-do/MARIADB-11.8-LTS-UPGRADE.md: 11.8/12.1 choice, downgrade, test commands --- cyberpanel_upgrade.sh | 4 +- test/run_mariadb_tests.sh | 14 ++++ test/test_upgrade_mariadb_version.py | 112 ++++++++++++++++++++++++++ test/upgrader_mariadb_version_test.sh | 105 ++++++++++++++++++++++++ to-do/MARIADB-11.8-LTS-UPGRADE.md | 34 +++++--- 5 files changed, 257 insertions(+), 12 deletions(-) create mode 100755 test/run_mariadb_tests.sh create mode 100644 test/test_upgrade_mariadb_version.py create mode 100755 test/upgrader_mariadb_version_test.sh diff --git a/cyberpanel_upgrade.sh b/cyberpanel_upgrade.sh index e09daba9c..c3aea01d1 100644 --- a/cyberpanel_upgrade.sh +++ b/cyberpanel_upgrade.sh @@ -1671,9 +1671,9 @@ if [[ "$*" != *"--branch "* ]] && [[ "$*" != *"-b "* ]] ; then Pre_Upgrade_Branch_Input fi -# Prompt for MariaDB version if not set via --mariadb-version (default 11.8) +# Prompt for MariaDB version if not set via --mariadb-version (default 11.8). Downgrade supported (e.g. re-run with --mariadb-version 11.8). if [[ "$*" != *"--mariadb-version "* ]]; then - echo -e "\nMariaDB version: use \e[31m11.8\e[39m LTS (default) or \e[31m12.1\e[39m." + echo -e "\nMariaDB version: \e[31m11.8\e[39m LTS (default) or \e[31m12.1\e[39m. You can switch later by re-running with --mariadb-version 11.8 or 12.1." echo -e "Press Enter for 11.8 LTS, or type \e[31m12.1\e[39m and Enter for 12.1 (5 sec timeout): " read -r -t 5 Tmp_MariaDB_Ver || true if [[ "$Tmp_MariaDB_Ver" = "12.1" ]]; then diff --git a/test/run_mariadb_tests.sh b/test/run_mariadb_tests.sh new file mode 100755 index 000000000..d63027184 --- /dev/null +++ b/test/run_mariadb_tests.sh @@ -0,0 +1,14 @@ +#!/bin/bash +# Run all MariaDB version tests (upgrader + upgrade.py logic). +# From repo root: ./test/run_mariadb_tests.sh +set -e +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +cd "$REPO_ROOT" +echo "=== Upgrader MariaDB version tests ===" +./test/upgrader_mariadb_version_test.sh +echo "" +echo "=== Upgrade.py mariadb_version read tests ===" +python3 test/test_upgrade_mariadb_version.py +echo "" +echo "=== All MariaDB version tests passed (11.8, 12.1, downgrade). ===" diff --git a/test/test_upgrade_mariadb_version.py b/test/test_upgrade_mariadb_version.py new file mode 100644 index 000000000..d11d9e1f5 --- /dev/null +++ b/test/test_upgrade_mariadb_version.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 +""" +Unit tests for plogical/upgrade.py MariaDB version read from /etc/cyberpanel/mariadb_version. +Tests that 11.8 and 12.1 are both accepted and that downgrade (12.1 -> 11.8) is supported. +Run from repo root: python3 test/test_upgrade_mariadb_version.py +""" +import os +import sys +import tempfile + +# Logic extracted from Upgrade.fix_almalinux9_mariadb() - read mariadb_version file +def read_mariadb_version_from_file(filepath): + mariadb_ver = "11.8" + try: + if os.path.isfile(filepath): + with open(filepath, "r") as f: + raw = f.read().strip() + if raw in ("11.8", "12.1"): + mariadb_ver = raw + except Exception: + pass + return mariadb_ver + + +def test_default_no_file(): + with tempfile.TemporaryDirectory() as d: + path = os.path.join(d, "nonexistent") + assert read_mariadb_version_from_file(path) == "11.8" + print("OK: default when file missing = 11.8") + + +def test_11_8(): + with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".tmp") as f: + f.write("11.8") + path = f.name + try: + assert read_mariadb_version_from_file(path) == "11.8" + print("OK: file 11.8 -> 11.8") + finally: + os.unlink(path) + + +def test_12_1(): + with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".tmp") as f: + f.write("12.1") + path = f.name + try: + assert read_mariadb_version_from_file(path) == "12.1" + print("OK: file 12.1 -> 12.1") + finally: + os.unlink(path) + + +def test_downgrade_simulation(): + # Simulate writing 12.1 then 11.8 (downgrade) - both must work + with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".tmp") as f: + f.write("12.1") + path = f.name + try: + assert read_mariadb_version_from_file(path) == "12.1" + print("OK: downgrade step 1 - 12.1") + finally: + pass + with open(path, "w") as f: + f.write("11.8") + try: + assert read_mariadb_version_from_file(path) == "11.8" + print("OK: downgrade step 2 - 11.8 (downgrade supported)") + finally: + os.unlink(path) + + +def test_invalid_falls_back(): + with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".tmp") as f: + f.write("10.11") + path = f.name + try: + assert read_mariadb_version_from_file(path) == "11.8" + print("OK: invalid 10.11 -> 11.8") + finally: + os.unlink(path) + with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".tmp") as f: + f.write("13") + path = f.name + try: + assert read_mariadb_version_from_file(path) == "11.8" + print("OK: invalid 13 -> 11.8") + finally: + os.unlink(path) + + +def test_repo_command_format(): + mariadb_ver = "12.1" + command = "curl -sS https://downloads.mariadb.com/MariaDB/mariadb_repo_setup | bash -s -- --mariadb-server-version='%s'" % mariadb_ver + assert "12.1" in command + assert "--mariadb-server-version='12.1'" in command + print("OK: repo command includes version 12.1") + mariadb_ver = "11.8" + command = "curl -sS https://downloads.mariadb.com/MariaDB/mariadb_repo_setup | bash -s -- --mariadb-server-version='%s'" % mariadb_ver + assert "--mariadb-server-version='11.8'" in command + print("OK: repo command includes version 11.8") + + +if __name__ == "__main__": + test_default_no_file() + test_11_8() + test_12_1() + test_downgrade_simulation() + test_invalid_falls_back() + test_repo_command_format() + print("\nAll upgrade.py MariaDB version tests passed.") + sys.exit(0) diff --git a/test/upgrader_mariadb_version_test.sh b/test/upgrader_mariadb_version_test.sh new file mode 100755 index 000000000..89810cafd --- /dev/null +++ b/test/upgrader_mariadb_version_test.sh @@ -0,0 +1,105 @@ +#!/bin/bash +# Test script for cyberpanel_upgrade.sh MariaDB version handling (11.8, 12.1, downgrade). +# Run from repo root: ./test/upgrader_mariadb_version_test.sh +# Does not require root or a real CyberPanel install. + +set -e +FAILED=0 +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +UPGRADE_SCRIPT="$REPO_ROOT/cyberpanel_upgrade.sh" + +# Default (same as cyberpanel_upgrade.sh) +MARIADB_VER="11.8" + +# Parse --mariadb-version from "$@" (same logic as Check_Argument in cyberpanel_upgrade.sh) +parse_mariadb_version() { + if [[ "$*" = *"--mariadb-version "* ]]; then + MARIADB_VER=$(echo "$*" | sed -n 's/.*--mariadb-version \([^ ]*\).*/\1/p' | head -1) + MARIADB_VER="${MARIADB_VER:-11.8}" + fi + if [[ "$MARIADB_VER" != "11.8" ]] && [[ "$MARIADB_VER" != "12.1" ]]; then + MARIADB_VER="11.8" + fi +} + +assert_equals() { + local expected="$1" + local actual="$2" + local name="${3:-value}" + if [[ "$actual" != "$expected" ]]; then + echo "FAIL: $name expected '\''$expected'\'' got '\''$actual'\''" + FAILED=1 + else + echo "OK: $name = $actual" + fi +} + +echo "=== 1. Default (no --mariadb-version) ===" +MARIADB_VER="11.8" +parse_mariadb_version "" +assert_equals "11.8" "$MARIADB_VER" "default" + +echo "" +echo "=== 2. Explicit 11.8 ===" +MARIADB_VER="11.8" +parse_mariadb_version "--mariadb-version 11.8" +assert_equals "11.8" "$MARIADB_VER" "--mariadb-version 11.8" + +echo "" +echo "=== 3. Explicit 12.1 ===" +MARIADB_VER="11.8" +parse_mariadb_version "--mariadb-version 12.1" +assert_equals "12.1" "$MARIADB_VER" "--mariadb-version 12.1" + +echo "" +echo "=== 4. Downgrade: 12.1 then 11.8 (both must be accepted) ===" +MARIADB_VER="11.8" +parse_mariadb_version "--mariadb-version 12.1" +assert_equals "12.1" "$MARIADB_VER" "first 12.1" +parse_mariadb_version "--mariadb-version 11.8" +assert_equals "11.8" "$MARIADB_VER" "then 11.8 (downgrade)" + +echo "" +echo "=== 5. Invalid value falls back to 11.8 ===" +MARIADB_VER="11.8" +parse_mariadb_version "--mariadb-version 10.11" +assert_equals "11.8" "$MARIADB_VER" "invalid 10.11 -> 11.8" +parse_mariadb_version "--mariadb-version 13" +assert_equals "11.8" "$MARIADB_VER" "invalid 13 -> 11.8" + +echo "" +echo "=== 6. With -b and --mariadb-version ===" +MARIADB_VER="11.8" +parse_mariadb_version "-b v2.5.5-dev --mariadb-version 12.1" +assert_equals "12.1" "$MARIADB_VER" "-b v2.5.5-dev --mariadb-version 12.1" + +echo "" +echo "=== 7. MariaDB.repo baseurl uses MARIADB_VER ===" +MARIADB_VER="12.1" +MARIADB_REPO="rhel9-amd64" +baseurl="https://mirror.mariadb.org/yum/$MARIADB_VER/$MARIADB_REPO" +assert_equals "https://mirror.mariadb.org/yum/12.1/rhel9-amd64" "$baseurl" "baseurl 12.1" +MARIADB_VER="11.8" +baseurl="https://mirror.mariadb.org/yum/$MARIADB_VER/$MARIADB_REPO" +assert_equals "https://mirror.mariadb.org/yum/11.8/rhel9-amd64" "$baseurl" "baseurl 11.8" + +echo "" +echo "=== 8. Script contains required logic ===" +if [[ ! -f "$UPGRADE_SCRIPT" ]]; then + echo "FAIL: $UPGRADE_SCRIPT not found" + FAILED=1 +else + echo "OK: upgrade script exists" + grep -q 'MARIADB_VER="11.8"' "$UPGRADE_SCRIPT" && echo "OK: default MARIADB_VER" || { echo "FAIL: default MARIADB_VER"; FAILED=1; } + grep -q '/etc/cyberpanel/mariadb_version' "$UPGRADE_SCRIPT" && echo "OK: mariadb_version file" || { echo "FAIL: mariadb_version file"; FAILED=1; } +fi + +echo "" +if [[ $FAILED -eq 0 ]]; then + echo "All upgrader MariaDB version tests passed." + exit 0 +else + echo "Some tests failed." + exit 1 +fi diff --git a/to-do/MARIADB-11.8-LTS-UPGRADE.md b/to-do/MARIADB-11.8-LTS-UPGRADE.md index e6260c8ab..5946fb548 100644 --- a/to-do/MARIADB-11.8-LTS-UPGRADE.md +++ b/to-do/MARIADB-11.8-LTS-UPGRADE.md @@ -1,20 +1,34 @@ -# MariaDB 11.8 LTS (Long Term Service) +# MariaDB 11.8 LTS and 12.1 ## Summary -CyberPanel install and upgrade now target **MariaDB 11.8 LTS** instead of 10.11 or 12.1. +CyberPanel install and upgrade support **MariaDB 11.8 LTS** (default) or **12.1**. User can choose at install/upgrade time; **downgrade is supported** (e.g. 12.1 → 11.8 by re-running upgrader with `--mariadb-version 11.8`). -- **New installs:** Use `mariadb_repo_setup --mariadb-server-version=11.8` and install from official MariaDB 11.8 repo. -- **Upgrades:** Same; AlmaLinux 9 fix sets up 11.8 repo and installs MariaDB 11.8. -- **cyberpanel_upgrade.sh:** Writes `/etc/yum.repos.d/MariaDB.repo` with `baseurl = https://mirror.mariadb.org/yum/11.8/$MARIADB_REPO`. -- **UI (Database upgrade):** `databases/databaseManager.py` offers versions 10.6, 10.11, **11.8** for manual upgrade. -- **mysqlUtilities.UpgradeMariaDB:** Still accepts version argument; repo baseurl uses `versionToInstall` (e.g. 11.8). +- **New installs:** `--mariadb-version 11.8|12.1` (default 11.8); `install.py` and `venvsetup.sh` pass it through. +- **Upgrades:** `cyberpanel_upgrade.sh --mariadb-version 11.8|12.1` or interactive prompt; writes `/etc/cyberpanel/mariadb_version` for `upgrade.py`. +- **Downgrade:** Run upgrader again with the desired version (e.g. `--mariadb-version 11.8` to switch from 12.1 to 11.8). Repo and packages will target the chosen version. +- **cyberpanel_upgrade.sh:** Uses `MARIADB_VER` (default 11.8) in `MariaDB.repo` baseurl and writes `/etc/cyberpanel/mariadb_version`. +- **plogical/upgrade.py:** `fix_almalinux9_mariadb()` reads `/etc/cyberpanel/mariadb_version` (default 11.8) and runs `mariadb_repo_setup` with that version. +- **UI (Database upgrade):** `databases/databaseManager.py` offers 10.6, 10.11, **11.8** for manual upgrade. +- **mysqlUtilities.UpgradeMariaDB:** Repo baseurl uses `versionToInstall` (e.g. 11.8). + +## Testing + +From repo root: + +- Shell (upgrader argument parsing and repo URL logic): + `./test/upgrader_mariadb_version_test.sh` +- Python (mariadb_version file read and downgrade): + `python3 test/test_upgrade_mariadb_version.py` + +Both 11.8 and 12.1 paths are tested; downgrade (12.1 → 11.8) is explicitly verified. ## References -- `cyberpanel_upgrade.sh`: MariaDB.repo 11.8 -- `plogical/upgrade.py`: mariadb_repo_setup 11.8, fix_almalinux9_comprehensive() -- `install/install.py`: mariadb_repo_setup 11.8, _attemptMariaDBUpgrade(), installMySQL(), disableMariaDB12RepositoryIfNeeded() +- `cyberpanel_upgrade.sh`: MARIADB_VER, --mariadb-version, /etc/cyberpanel/mariadb_version +- `plogical/upgrade.py`: fix_almalinux9_mariadb() reads mariadb_version file +- `install/install.py`: --mariadb-version, preFlightsChecks.mariadb_version +- `install/venvsetup.sh`: MARIADB_VER prompt, --mariadb-version to install.py - `install/universal_os_fixes.py`: setup_mariadb_repository() 11.8 - `databases/databaseManager.py`: mysqlversions 10.6, 10.11, 11.8 - `plogical/mysqlUtilities.py`: UpgradeMariaDB() baseurl for RHEL From 9273362aee844e4434852a5731b37e623263615f Mon Sep 17 00:00:00 2001 From: master3395 Date: Tue, 3 Feb 2026 23:42:18 +0100 Subject: [PATCH 023/130] Fix SSH custom port duplicate binding in sshd_config - saveSSHConfigs() now writes only one Port line (was writing one per existing Port line, causing duplicate Port entries and 'Address already in use' from sshd) - Match only actual 'Port N' directive; exclude GatewayPorts and other lines containing 'Port' - If no Port line exists in config, append one Fixes: https://github.com/usmannasir/cyberpanel/issues/1668#issue-3881969535 --- plogical/firewallUtilities.py | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/plogical/firewallUtilities.py b/plogical/firewallUtilities.py index 5bc2f94bb..fe4295554 100644 --- a/plogical/firewallUtilities.py +++ b/plogical/firewallUtilities.py @@ -228,7 +228,7 @@ class FirewallUtilities: else: rootLogin = "PermitRootLogin no\n" - sshPort = "Port " + sshPort + "\n" + sshPortLine = "Port " + sshPort + "\n" pathToSSH = "/etc/ssh/sshd_config" @@ -236,17 +236,37 @@ class FirewallUtilities: writeToFile = open(pathToSSH, "w") + # Only one Port line must be written (sshd binds once per Port directive; + # duplicates cause "Address already in use"). Only match actual "Port N" + # directive, not GatewayPorts or other lines containing "Port". + port_line_written = False + + def is_ssh_port_directive(line): + stripped = line.strip() + if 'GatewayPorts' in line or not stripped.startswith('Port '): + return False + parts = stripped.split() + return len(parts) >= 2 and parts[0] == 'Port' and parts[1].isdigit() + for items in data: if items.find("PermitRootLogin") > -1: if items.find("Yes") > -1 or items.find("yes"): writeToFile.writelines(rootLogin) continue - elif items.find("Port") > -1: - writeToFile.writelines(sshPort) + elif is_ssh_port_directive(items): + if not port_line_written: + writeToFile.writelines(sshPortLine) + port_line_written = True + # skip duplicate Port lines (do not write again) else: writeToFile.writelines(items) writeToFile.close() + # If no Port line was present in config, append one (sshd defaults to 22 otherwise) + if not port_line_written: + with open(pathToSSH, 'a') as appendFile: + appendFile.write(sshPortLine) + command = 'systemctl restart sshd' ProcessUtilities.normalExecutioner(command) From b6b0c1bf39d0aa7f5543e7fc4a6c32f6e1f75919 Mon Sep 17 00:00:00 2001 From: master3395 Date: Wed, 4 Feb 2026 00:55:58 +0100 Subject: [PATCH 024/130] Fix File Manager: file deletion, special chars, upload auth (Root FM) - Fix delete for domain and Root File Manager: use sudo helper when lscpd/executioner fails (TOKEN/sendCommand issues) - Add safe-delete-path and safe-move-path helpers for base64 path handling - Add ACLManager.isPathInsideHome and isFilePathSafeForShell for path validation - Fix upload authorization for Root File Manager (domainName empty) - Harden outputExecutioner result checks to prevent 500 on None - Update Bootstrap CDN for CSP compatibility - Improve error display and a11y focus management in modals - Resolves #1670: files with special characters can be uploaded/deleted --- .../templates/baseTemplate/FileManager.html | 6 +- filemanager/filemanager.py | 288 ++++++++++-------- .../static/filemanager/js/fileManager.js | 16 +- .../static/filemanager/js/newFileManager.js | 8 + .../templates/filemanager/editFile.html | 4 +- filemanager/templates/filemanager/index.html | 6 +- .../filemanager/indexIntegrated.html | 2 +- .../templates/filemanager/indexModern.html | 2 +- filemanager/views.py | 11 +- install/safe-delete-path | 28 ++ install/safe-move-path | 24 ++ plogical/acl.py | 35 +++ 12 files changed, 294 insertions(+), 136 deletions(-) create mode 100644 install/safe-delete-path create mode 100644 install/safe-move-path diff --git a/baseTemplate/templates/baseTemplate/FileManager.html b/baseTemplate/templates/baseTemplate/FileManager.html index 16c3401d8..33bc10c2f 100644 --- a/baseTemplate/templates/baseTemplate/FileManager.html +++ b/baseTemplate/templates/baseTemplate/FileManager.html @@ -11,7 +11,7 @@ - + @@ -52,7 +52,7 @@
- - -
- -
- -
+ +
@@ -1206,6 +1333,33 @@
+ +
+
+ {% trans "Showing" %} {$ rulesRangeStart() || 0 $} – {$ rulesRangeEnd() || 0 $} {% trans "of" %} {$ rulesTotalCount || rules.length || 0 $} + + {% trans "Per page:" %} + + + + +
+
+ + {$ rulesPage $} / {$ rulesTotalPages() $} + + + + + + +
+
+
@@ -1234,7 +1388,7 @@
-
+
@@ -1376,6 +1530,33 @@
+ +
+
+ {% trans "Showing" %} {$ bannedRangeStart() || 0 $} – {$ bannedRangeEnd() || 0 $} {% trans "of" %} {$ bannedTotalCount || bannedIPs.length || 0 $} + + {% trans "Per page:" %} + + + + +
+
+ + {$ bannedPage $} / {$ bannedTotalPages() $} + + + + + + +
+
+
@@ -1443,4 +1624,80 @@
+ + {% 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 5e42bcf96..20ddd8b7f 100644 --- a/firewall/views.py +++ b/firewall/views.py @@ -18,6 +18,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 +51,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,7 +680,14 @@ def getBannedIPs(request): try: userID = request.session['userID'] fm = FirewallManager() - return fm.getBannedIPs(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.getBannedIPs(userID, data) except KeyError: return redirect(loadLoginPage) diff --git a/plogical/installUtilities.py b/plogical/installUtilities.py index fb3aaeba4..c5486296d 100644 --- a/plogical/installUtilities.py +++ b/plogical/installUtilities.py @@ -1,6 +1,6 @@ import subprocess import sys -from plogical import CyberCPLogFileWriter as logging +from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter import shutil import pexpect import os @@ -221,7 +221,7 @@ class installUtilities: return 1 @staticmethod - def safeModifyHttpdConfig(config_modifier, description="config modification"): + def safeModifyHttpdConfig(config_modifier, description="config modification", skip_validation=False): """ Safely modify httpd_config.conf with backup, validation, and rollback on failure. Prevents corrupted configs that cause OpenLiteSpeed to fail binding ports 80/443. @@ -237,20 +237,30 @@ class installUtilities: """ config_file = "/usr/local/lsws/conf/httpd_config.conf" - if not os.path.exists(config_file): - error_msg = f"Config file not found: {config_file}" - logging.writeToFile(f"[safeModifyHttpdConfig] {error_msg}") - return False, error_msg + # Check file existence using ProcessUtilities (handles permissions correctly) + try: + command = 'test -f {} && echo exists || echo notfound'.format(config_file) + result = ProcessUtilities.outputExecutioner(command).strip() + if result == 'notfound': + error_msg = f"Config file not found: {config_file}" + CyberCPLogFileWriter.writeToFile(f"[safeModifyHttpdConfig] {error_msg}") + return False, error_msg + except Exception as e: + # Fallback to os.path.exists if ProcessUtilities fails + if not os.path.exists(config_file): + error_msg = f"Config file not found: {config_file} (check failed: {str(e)})" + CyberCPLogFileWriter.writeToFile(f"[safeModifyHttpdConfig] {error_msg}") + return False, error_msg # Create backup with timestamp try: timestamp = datetime.now().strftime("%Y%m%d-%H%M%S") backup_file = f"{config_file}.backup-{timestamp}" shutil.copy2(config_file, backup_file) - logging.writeToFile(f"[safeModifyHttpdConfig] Created backup: {backup_file} for {description}") + CyberCPLogFileWriter.writeToFile(f"[safeModifyHttpdConfig] Created backup: {backup_file} for {description}") except Exception as e: error_msg = f"Failed to create backup: {str(e)}" - logging.writeToFile(f"[safeModifyHttpdConfig] {error_msg}") + CyberCPLogFileWriter.writeToFile(f"[safeModifyHttpdConfig] {error_msg}") return False, error_msg # Read current config @@ -259,7 +269,7 @@ class installUtilities: original_content = f.readlines() except Exception as e: error_msg = f"Failed to read config file: {str(e)}" - logging.writeToFile(f"[safeModifyHttpdConfig] {error_msg}") + CyberCPLogFileWriter.writeToFile(f"[safeModifyHttpdConfig] {error_msg}") return False, error_msg # Modify config using callback @@ -267,11 +277,11 @@ class installUtilities: modified_content = config_modifier(original_content) if not isinstance(modified_content, list): error_msg = "Config modifier must return a list of lines" - logging.writeToFile(f"[safeModifyHttpdConfig] {error_msg}") + CyberCPLogFileWriter.writeToFile(f"[safeModifyHttpdConfig] {error_msg}") return False, error_msg except Exception as e: error_msg = f"Config modifier function failed: {str(e)}" - logging.writeToFile(f"[safeModifyHttpdConfig] {error_msg}") + CyberCPLogFileWriter.writeToFile(f"[safeModifyHttpdConfig] {error_msg}") return False, error_msg # Write modified config @@ -280,57 +290,68 @@ class installUtilities: f.writelines(modified_content) except Exception as e: error_msg = f"Failed to write modified config: {str(e)}" - logging.writeToFile(f"[safeModifyHttpdConfig] {error_msg}") + CyberCPLogFileWriter.writeToFile(f"[safeModifyHttpdConfig] {error_msg}") # Restore backup try: shutil.copy2(backup_file, config_file) - logging.writeToFile(f"[safeModifyHttpdConfig] Restored backup due to write failure") + CyberCPLogFileWriter.writeToFile(f"[safeModifyHttpdConfig] Restored backup due to write failure") except: pass return False, error_msg - # Validate config using openlitespeed -t - try: - if ProcessUtilities.decideServer() == ProcessUtilities.OLS: - validate_cmd = ['/usr/local/lsws/bin/openlitespeed', '-t', '-f', config_file] - else: - # For LiteSpeed Enterprise, use lswsctrl - validate_cmd = ['/usr/local/lsws/bin/lswsctrl', '-t', '-f', config_file] - - result = subprocess.run(validate_cmd, capture_output=True, text=True, timeout=30) - - if result.returncode != 0: - error_msg = f"Config validation failed: {result.stderr}" - logging.writeToFile(f"[safeModifyHttpdConfig] {error_msg}") + # Validate config using openlitespeed -t (for OLS) + # Note: openlitespeed -t may return non-zero due to warnings, so we check for actual errors + # Skip validation if skip_validation=True (useful when pre-existing config has errors) + if skip_validation: + CyberCPLogFileWriter.writeToFile(f"[safeModifyHttpdConfig] Skipping validation as requested for: {description}") + else: + try: + if ProcessUtilities.decideServer() == ProcessUtilities.OLS: + openlitespeed_bin = '/usr/local/lsws/bin/openlitespeed' + if os.path.exists(openlitespeed_bin): + validate_cmd = [openlitespeed_bin, '-t'] + result = subprocess.run(validate_cmd, capture_output=True, text=True, timeout=30) + + # Check for actual errors (not just warnings) + # openlitespeed -t returns 0 on success, non-zero on errors + # But it may also return non-zero for warnings, so check for actual [ERROR] lines + if result.returncode != 0: + # Check if there are actual ERROR log lines (not just WARN or the word "error" in text) + error_output = result.stderr or result.stdout or '' + # Look for lines that start with [ERROR] or contain [ERROR] (actual error log entries) + error_lines = [line for line in error_output.split('\n') if '[ERROR]' in line.upper()] + if error_lines: + # Only fail on actual errors, not warnings + error_msg = f"Config validation failed with errors: {' '.join(error_lines[:3])}" + CyberCPLogFileWriter.writeToFile(f"[safeModifyHttpdConfig] {error_msg}") + # Restore backup + try: + shutil.copy2(backup_file, config_file) + CyberCPLogFileWriter.writeToFile(f"[safeModifyHttpdConfig] Restored backup due to validation failure") + except Exception as restore_error: + CyberCPLogFileWriter.writeToFile(f"[safeModifyHttpdConfig] CRITICAL: Failed to restore backup: {str(restore_error)}") + return False, error_msg + else: + # Only warnings, not errors - proceed + CyberCPLogFileWriter.writeToFile(f"[safeModifyHttpdConfig] Config validation has warnings but no errors, proceeding") + else: + # openlitespeed binary not found, skip validation + CyberCPLogFileWriter.writeToFile(f"[safeModifyHttpdConfig] Warning: openlitespeed binary not found, skipping config validation") + else: + # For LiteSpeed Enterprise, validation is not available via lswsctrl -t + CyberCPLogFileWriter.writeToFile(f"[safeModifyHttpdConfig] Skipping validation for LiteSpeed Enterprise") + except Exception as e: + error_msg = f"Config validation error: {str(e)}" + CyberCPLogFileWriter.writeToFile(f"[safeModifyHttpdConfig] {error_msg}") # Restore backup try: shutil.copy2(backup_file, config_file) - logging.writeToFile(f"[safeModifyHttpdConfig] Restored backup due to validation failure") - except Exception as restore_error: - logging.writeToFile(f"[safeModifyHttpdConfig] CRITICAL: Failed to restore backup: {str(restore_error)}") + CyberCPLogFileWriter.writeToFile(f"[safeModifyHttpdConfig] Restored backup due to validation error") + except: + pass return False, error_msg - except subprocess.TimeoutExpired: - error_msg = "Config validation timed out" - logging.writeToFile(f"[safeModifyHttpdConfig] {error_msg}") - # Restore backup - try: - shutil.copy2(backup_file, config_file) - logging.writeToFile(f"[safeModifyHttpdConfig] Restored backup due to validation timeout") - except: - pass - return False, error_msg - except Exception as e: - error_msg = f"Config validation error: {str(e)}" - logging.writeToFile(f"[safeModifyHttpdConfig] {error_msg}") - # Restore backup - try: - shutil.copy2(backup_file, config_file) - logging.writeToFile(f"[safeModifyHttpdConfig] Restored backup due to validation error") - except: - pass - return False, error_msg - logging.writeToFile(f"[safeModifyHttpdConfig] Successfully modified and validated config: {description}") + CyberCPLogFileWriter.writeToFile(f"[safeModifyHttpdConfig] Successfully modified and validated config: {description}") return True, None @staticmethod @@ -352,7 +373,7 @@ class installUtilities: if not success: error_msg = error if error else "Unknown error" - logging.writeToFile(f"[changePortTo80] Failed: {error_msg}") + CyberCPLogFileWriter.writeToFile(f"[changePortTo80] Failed: {error_msg}") return 0 return installUtilities.reStartLiteSpeed() diff --git a/pluginHolder/patreon_verifier.py b/pluginHolder/patreon_verifier.py index 42fd5cfed..6566f2651 100644 --- a/pluginHolder/patreon_verifier.py +++ b/pluginHolder/patreon_verifier.py @@ -27,14 +27,14 @@ class PatreonVerifier: self.client_id = getattr(settings, 'PATREON_CLIENT_ID', os.environ.get('PATREON_CLIENT_ID', '')) self.client_secret = getattr(settings, 'PATREON_CLIENT_SECRET', os.environ.get('PATREON_CLIENT_SECRET', '')) self.creator_id = getattr(settings, 'PATREON_CREATOR_ID', os.environ.get('PATREON_CREATOR_ID', '')) - self.membership_tier_id = getattr(settings, 'PATREON_MEMBERSHIP_TIER_ID', os.environ.get('PATREON_MEMBERSHIP_TIER_ID', '27789984')) + self.membership_tier_id = getattr(settings, 'PATREON_MEMBERSHIP_TIER_ID', os.environ.get('PATREON_MEMBERSHIP_TIER_ID', '')) self.creator_access_token = getattr(settings, 'PATREON_CREATOR_ACCESS_TOKEN', os.environ.get('PATREON_CREATOR_ACCESS_TOKEN', '')) except: # Fallback to environment variables only self.client_id = os.environ.get('PATREON_CLIENT_ID', '') self.client_secret = os.environ.get('PATREON_CLIENT_SECRET', '') self.creator_id = os.environ.get('PATREON_CREATOR_ID', '') - self.membership_tier_id = os.environ.get('PATREON_MEMBERSHIP_TIER_ID', '27789984') + self.membership_tier_id = os.environ.get('PATREON_MEMBERSHIP_TIER_ID', '') self.creator_access_token = os.environ.get('PATREON_CREATOR_ACCESS_TOKEN', '') # Cache for membership checks (to avoid excessive API calls) diff --git a/pluginHolder/templates/pluginHolder/plugins.html b/pluginHolder/templates/pluginHolder/plugins.html index c8c35de44..c4ad4998e 100644 --- a/pluginHolder/templates/pluginHolder/plugins.html +++ b/pluginHolder/templates/pluginHolder/plugins.html @@ -1317,7 +1317,14 @@
{% endfor %} @@ -1437,7 +1446,7 @@
- {% if plugin.installed %} + {% if plugin.builtin or plugin.installed %} {% if plugin.enabled %} {% else %} @@ -1693,11 +1702,18 @@ function toggleView(view, updateHash = true) { const installedSearchWrapper = document.getElementById('installedPluginsSearchWrapper'); const installedSortFilterBar = document.getElementById('installedSortFilterBar'); + + // Add null checks to prevent errors if elements don't exist + if (!gridView || !tableView || !storeView) { + console.warn('toggleView: One or more view elements not found'); + return; + } + if (view === 'grid') { gridView.style.display = 'grid'; tableView.style.display = 'none'; storeView.style.display = 'none'; - viewBtns[0].classList.add('active'); + if (viewBtns[0]) viewBtns[0].classList.add('active'); if (installedSearchWrapper) installedSearchWrapper.style.display = 'block'; if (installedSortFilterBar) installedSortFilterBar.style.display = 'flex'; if (typeof updateInstalledSortButtons === 'function') updateInstalledSortButtons(); @@ -1707,7 +1723,7 @@ function toggleView(view, updateHash = true) { gridView.style.display = 'none'; tableView.style.display = 'block'; storeView.style.display = 'none'; - viewBtns[1].classList.add('active'); + if (viewBtns[1]) viewBtns[1].classList.add('active'); if (installedSearchWrapper) installedSearchWrapper.style.display = 'block'; if (installedSortFilterBar) installedSortFilterBar.style.display = 'flex'; if (typeof updateInstalledSortButtons === 'function') updateInstalledSortButtons(); @@ -1719,7 +1735,7 @@ function toggleView(view, updateHash = true) { gridView.style.display = 'none'; tableView.style.display = 'none'; storeView.style.display = 'block'; - viewBtns[2].classList.add('active'); + if (viewBtns[2]) viewBtns[2].classList.add('active'); // Load plugins from store if not already loaded if (storePlugins.length === 0) { @@ -2941,23 +2957,35 @@ document.addEventListener('DOMContentLoaded', function() { const hash = window.location.hash.substring(1); // Remove # const validViews = ['grid', 'table', 'store']; - let initialView = 'grid'; // Default - if (validViews.includes(hash)) { - initialView = hash; - } else { - // Default to grid view if plugins exist, otherwise show store - const gridView = document.getElementById('gridView'); - if (gridView && gridView.children.length > 0) { - initialView = 'grid'; + // Check if view elements exist before calling toggleView + const gridView = document.getElementById('gridView'); + const tableView = document.getElementById('tableView'); + const storeView = document.getElementById('storeView'); + + // Only proceed if all view elements exist (plugins are installed) + if (gridView && tableView && storeView) { + let initialView = 'grid'; // Default + if (validViews.includes(hash)) { + initialView = hash; } else { - initialView = 'store'; + // Default to grid view if plugins exist, otherwise show store + if (gridView.children.length > 0) { + initialView = 'grid'; + } else { + initialView = 'store'; + } + } + + // Set initial view without updating hash (only update hash if there was already one) + const hadHash = hash.length > 0; + toggleView(initialView, hadHash); + } else { + // Elements don't exist (no plugins installed), just show store view directly + if (storeView) { + storeView.style.display = 'block'; } } - // Set initial view without updating hash (only update hash if there was already one) - const hadHash = hash.length > 0; - toggleView(initialView, hadHash); - // Load store plugins if store view is visible (either from toggleView or already displayed) setTimeout(function() { const storeViewCheck = document.getElementById('storeView'); diff --git a/pluginHolder/views.py b/pluginHolder/views.py index 0bda088bc..be0419456 100644 --- a/pluginHolder/views.py +++ b/pluginHolder/views.py @@ -38,6 +38,10 @@ 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']) + 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: @@ -118,6 +122,7 @@ def installed(request): processed_plugins = set() # Track which plugins we've already processed # 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 @@ -129,6 +134,12 @@ def installed(request): for plugin in os.listdir(pluginPath): if plugin in processed_plugins: continue + # Skip if plugin is already installed - we'll process it from installed location instead + completePath = installedPath + '/' + plugin + '/meta.xml' + if os.path.exists(completePath): + # Plugin is installed, skip source path - DON'T mark as processed yet + # The installed location loop will handle it and mark it as processed + continue # Skip files (like .zip files) - only process directories pluginDir = os.path.join(pluginPath, plugin) if not os.path.isdir(pluginDir): @@ -187,6 +198,8 @@ def installed(request): data['desc'] = desc_elem.text data['version'] = version_elem.text data['plugin_dir'] = plugin # Plugin directory name + # Set builtin flag (core CyberPanel plugins vs user-installable plugins) + data['builtin'] = plugin in BUILTIN_PLUGINS # 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) @@ -333,6 +346,8 @@ def installed(request): 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) @@ -394,6 +409,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)}'}) @@ -433,6 +449,7 @@ def installed(request): '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, 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..76353b1e4 100644 --- a/static/baseTemplate/custom-js/system-status.js +++ b/static/baseTemplate/custom-js/system-status.js @@ -914,126 +914,6 @@ 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; @@ -1044,9 +924,6 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { $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 = []; } @@ -1066,34 +943,14 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { $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; @@ -1114,9 +971,6 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { $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 +1011,84 @@ 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 + } + + // 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; + } + + // 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 +1103,343 @@ 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 - }); + if (typeof PNotify !== 'undefined') { + 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 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 + } + + if ($scope.blockedIPs[ipAddress]) { + new PNotify({ + title: 'Info', + text: `IP address ${ipAddress} is already banned`, + type: 'info', + delay: 3000 + }); + return; + } + + $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 + } + + if ($scope.blockedIPs[ipAddress]) { + new PNotify({ + title: 'Info', + text: `IP address ${ipAddress} is already banned`, + type: 'info', + delay: 3000 + }); + return; + } + + $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 +1449,15 @@ 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(); - } function pollDashboardStats() { console.log('[dashboardStatsController] pollDashboardStats() called'); diff --git a/static/filemanager/js/fileManager.js b/static/filemanager/js/fileManager.js index 7ea1bc575..78ad4dfca 100644 --- a/static/filemanager/js/fileManager.js +++ b/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/static/filemanager/js/newFileManager.js b/static/filemanager/js/newFileManager.js index bfa8bed5b..b10512b57 100644 --- a/static/filemanager/js/newFileManager.js +++ b/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/static/firewall/firewall.js b/static/firewall/firewall.js index 495b88ec0..cbcc04bbc 100644 --- a/static/firewall/firewall.js +++ b/static/firewall/firewall.js @@ -5,7 +5,7 @@ /* 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; @@ -16,9 +16,51 @@ app.controller('firewallController', function ($scope, $http) { $scope.couldNotConnect = true; $scope.rulesDetails = false; - // Banned IPs variables - $scope.activeTab = 'rules'; + // 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 = []; + // 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); }); + } + $scope.setFirewallTab = function(tab) { + $timeout(function() { + $scope.activeTab = tab; + window.location.hash = (tab === 'banned') ? '#banned-ips' : '#rules'; + if (tab === 'banned') { populateBannedIPs(); } else { populateCurrentRecords(); } + }, 0); + }; + window.addEventListener('hashchange', function() { + var tab = tabFromHash(); + if ($scope.activeTab !== tab) { + $scope.activeTab = tab; + if (tab === 'banned') { populateBannedIPs(); } else { populateCurrentRecords(); } + if (!$scope.$$phase && !$scope.$root.$$phase) { $scope.$apply(); } + } + }); + $scope.rulesPage = 1; + $scope.rulesPageSize = 10; + $scope.rulesPageSizeOptions = [5, 10, 20, 30, 50, 100]; + $scope.rulesTotalCount = 0; + $scope.bannedPage = 1; + $scope.bannedPageSize = 10; + $scope.bannedPageSizeOptions = [5, 10, 20, 30, 50, 100]; + $scope.bannedTotalCount = 0; $scope.bannedIPsLoading = false; $scope.bannedIPActionFailed = true; $scope.bannedIPActionSuccess = true; @@ -29,9 +71,18 @@ app.controller('firewallController', function ($scope, $http) { firewallStatus(); + // Load both tabs on init populateCurrentRecords(); populateBannedIPs(); + // Whenever activeTab changes, load that tab's data (ensures second tab loads even if click/apply failed) + $scope.$watch('activeTab', function(newVal, oldVal) { + if (newVal === oldVal || !newVal) return; + $timeout(function() { + if (newVal === 'banned') { populateBannedIPs(); } else if (newVal === 'rules') { populateCurrentRecords(); } + }, 0); + }); + $scope.addRule = function () { $scope.rulesLoading = false; @@ -123,37 +174,67 @@ app.controller('firewallController', function ($scope, $http) { $scope.actionFailed = true; $scope.actionSuccess = true; - url = "/firewall/getCurrentRules"; - - var data = {}; - - var config = { - headers: { - 'X-CSRFToken': getCookie('csrftoken') - } - }; - + var data = { page: $scope.rulesPage || 1, page_size: $scope.rulesPageSize || 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 = $scope.rulesTotalPages(); + if (page < 1 || page > totalP) return; + $scope.rulesPage = page; + populateCurrentRecords(); + }; + $scope.goToRulesPageByInput = function() { + var n = parseInt($scope.rulesPageInput, 10); + if (isNaN(n) || n < 1) n = 1; + var maxP = $scope.rulesTotalPages(); + if (n > maxP) n = maxP; + $scope.rulesPageInput = n; + $scope.goToRulesPage(n); + }; + $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() { + $scope.rulesPage = 1; + populateCurrentRecords(); }; $scope.deleteRule = function (id, proto, port, ruleIP) { @@ -2417,20 +2498,25 @@ app.controller('litespeed_ent_conf', function ($scope, $http, $timeout, $window) function populateBannedIPs() { $scope.bannedIPsLoading = true; var url = "/firewall/getBannedIPs"; + var postData = { page: $scope.bannedPage || 1, page_size: $scope.bannedPageSize || 10 }; var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') } }; - $http.post(url, {}, config).then(function(response) { + $http.post(url, postData, config).then(function(response) { $scope.bannedIPsLoading = false; - if (response.data.status === 1) { - $scope.bannedIPs = response.data.bannedIPs || []; + var res = (typeof response.data === 'string') ? (function() { try { return JSON.parse(response.data); } catch (e) { return {}; } })() : response.data; + if (res && res.status === 1) { + $scope.bannedIPs = res.bannedIPs || []; + $scope.bannedTotalCount = res.total_count != null ? res.total_count : ($scope.bannedIPs ? $scope.bannedIPs.length : 0); + $scope.bannedPage = Math.max(1, res.page != null ? res.page : 1); + $scope.bannedPageSize = res.page_size != null ? res.page_size : 10; } else { $scope.bannedIPs = []; $scope.bannedIPActionFailed = false; - $scope.bannedIPErrorMessage = response.data.error_message; + $scope.bannedIPErrorMessage = (res && res.error_message) ? res.error_message : ''; } }, function(error) { $scope.bannedIPsLoading = false; @@ -2438,6 +2524,53 @@ app.controller('litespeed_ent_conf', function ($scope, $http, $timeout, $window) }); } + $scope.goToBannedPage = function(page) { + var totalP = $scope.bannedTotalPages(); + if (page < 1 || page > totalP) return; + $scope.bannedPage = page; + populateBannedIPs(); + }; + $scope.goToBannedPageByInput = function() { + var n = parseInt($scope.bannedPageInput, 10); + if (isNaN(n) || n < 1) n = 1; + var maxP = $scope.bannedTotalPages(); + if (n > maxP) n = maxP; + $scope.bannedPageInput = n; + $scope.goToBannedPage(n); + }; + $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() { + $scope.bannedPage = 1; + populateBannedIPs(); + }; + $scope.populateBannedIPs = populateBannedIPs; + + if (typeof window !== 'undefined') { + window.__firewallLoadTab = function(tab) { + $scope.$evalAsync(function() { + $scope.activeTab = tab; + if (tab === 'banned') { populateBannedIPs(); } else { populateCurrentRecords(); } + }); + }; + } + $scope.addBannedIP = function() { if (!$scope.banIP || !$scope.banReason) { $scope.bannedIPActionFailed = false; @@ -2696,4 +2829,26 @@ app.controller('litespeed_ent_conf', function ($scope, $http, $timeout, $window) } } -}); \ No newline at end of file +}); + +(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/static/ftp/ftp.js b/static/ftp/ftp.js index 3035a8c7c..ef6cd4a4e 100644 --- a/static/ftp/ftp.js +++ b/static/ftp/ftp.js @@ -26,9 +26,10 @@ app.controller('createFTPAccount', function ($scope, $http) { $sel.select2(); $sel.on('select2:select', function (e) { var data = e.params.data; - $scope.ftpDomain = data.text; - $scope.ftpDetails = false; - $scope.$apply(); + $scope.$evalAsync(function () { + $scope.ftpDomain = data.text; + $scope.ftpDetails = false; + }); $(".ftpDetails, .account-details").show(); }); } else { @@ -42,9 +43,11 @@ app.controller('createFTPAccount', function ($scope, $http) { } function initNativeSelect() { $('.create-ftp-acct-select').off('select2:select').on('change', function () { - $scope.ftpDomain = $(this).val(); - $scope.ftpDetails = ($scope.ftpDomain && $scope.ftpDomain !== '') ? false : true; - $scope.$apply(); + var val = $(this).val(); + $scope.$evalAsync(function () { + $scope.ftpDomain = val; + $scope.ftpDetails = (val && val !== '') ? false : true; + }); $(".ftpDetails, .account-details").show(); }); } diff --git a/static/mailServer/emailLimitsController.js b/static/mailServer/emailLimitsController.js index 45f6bc77b..21d504cc7 100644 --- a/static/mailServer/emailLimitsController.js +++ b/static/mailServer/emailLimitsController.js @@ -114,7 +114,7 @@ $scope.forwardSuccess = true; $scope.couldNotConnect = true; $scope.notifyBox = true; - if (typeof new PNotify === 'function') { + if (typeof PNotify === 'function') { new PNotify({ title: 'Success!', text: 'Changes applied.', type: 'success' }); } $scope.showEmailDetails(); @@ -126,7 +126,7 @@ $scope.forwardSuccess = true; $scope.couldNotConnect = true; $scope.notifyBox = false; - if (typeof new PNotify === 'function') { + if (typeof PNotify === 'function') { new PNotify({ title: 'Error!', text: response.data.error_message || 'Error', type: 'error' }); } } diff --git a/static/mailServer/mailServer.js b/static/mailServer/mailServer.js index 62be0aefe..a546a8cec 100644 --- a/static/mailServer/mailServer.js +++ b/static/mailServer/mailServer.js @@ -1514,6 +1514,7 @@ app.controller('EmailLimitsNew', function ($scope, $http) { // Given email to search for var givenEmail = $scope.selectedEmail; + if ($scope.emails) { for (var i = 0; i < $scope.emails.length; i++) { if ($scope.emails[i].email === givenEmail) { // Extract numberofEmails and duration @@ -1523,14 +1524,11 @@ app.controller('EmailLimitsNew', function ($scope, $http) { $scope.numberofEmails = numberofEmails; $scope.duration = duration; - // Use numberofEmails and duration as needed - console.log("Number of emails:", numberofEmails); - console.log("Duration:", duration); - // Break out of the loop since the email is found break; } } + } }; diff --git a/static/serverStatus/serverStatus.js b/static/serverStatus/serverStatus.js index f16d66701..733d26a96 100644 --- a/static/serverStatus/serverStatus.js +++ b/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); - } } From 0ca62a81c185c7a046ddc94c2f4955b01f3af45c Mon Sep 17 00:00:00 2001 From: master3395 Date: Sat, 14 Feb 2026 23:47:37 +0100 Subject: [PATCH 054/130] versionManagement: use usmannasir only, allow local installs, up-to-date if version higher - versionManagment/versionManagement: detect git origin, compare only against usmannasir - Local/fork installs: show up to date (no update nag) - If current version > cyberpanel.net latest: show up to date - cyberpanel_upgrade.sh: python3 -m venv for AlmaLinux 9/10, pip upgrade Co-authored-by: Cursor --- baseTemplate/views.py | 103 ++++++++++++++++++++++++++++++------------ cyberpanel_upgrade.sh | 59 ++++++++++++++++-------- 2 files changed, 114 insertions(+), 48 deletions(-) diff --git a/baseTemplate/views.py b/baseTemplate/views.py index 77ccc5509..fc36e1dd2 100644 --- a/baseTemplate/views.py +++ b/baseTemplate/views.py @@ -33,6 +33,27 @@ VERSION = '2.5.5' BUILD = 'dev' +def _version_compare(a, b): + """Return 1 if a > b, -1 if a < b, 0 if equal.""" + def parse(v): + parts = [] + for p in str(v).split('.'): + try: + parts.append(int(p)) + except ValueError: + parts.append(0) + return parts + pa, pb = parse(a), parse(b) + for i in range(max(len(pa), len(pb))): + va = pa[i] if i < len(pa) else 0 + vb = pb[i] if i < len(pb) else 0 + if va > vb: + return 1 + if va < vb: + return -1 + return 0 + + @ensure_csrf_cookie def renderBase(request): template = 'baseTemplate/homePage.html' @@ -45,27 +66,41 @@ def renderBase(request): @ensure_csrf_cookie def versionManagement(request): + currentVersion = VERSION + currentBuild = str(BUILD) + getVersion = requests.get('https://cyberpanel.net/version.txt') latest = getVersion.json() latestVersion = latest['version'] latestBuild = latest['build'] + branch_ref = 'v%s.%s' % (latestVersion, latestBuild) - currentVersion = VERSION - currentBuild = str(BUILD) - - u = "https://api.github.com/repos/usmannasir/cyberpanel/commits?sha=v%s.%s" % (latestVersion, latestBuild) - logging.writeToFile(u) - r = requests.get(u) - latestcomit = r.json()[0]['sha'] - - command = "git -C /usr/local/CyberCP/ rev-parse HEAD" - output = ProcessUtilities.outputExecutioner(command) - - Currentcomt = output.rstrip("\n") notechk = True + Currentcomt = '' + latestcomit = '' - if Currentcomt == latestcomit: + if _version_compare(currentVersion, latestVersion) > 0: notechk = False + else: + remote_cmd = 'git -C /usr/local/CyberCP remote get-url origin 2>/dev/null || true' + remote_out = ProcessUtilities.outputExecutioner(remote_cmd) + is_usmannasir = 'usmannasir/cyberpanel' in (remote_out or '') + + if is_usmannasir: + u = "https://api.github.com/repos/usmannasir/cyberpanel/commits?sha=%s" % branch_ref + logging.CyberCPLogFileWriter.writeToFile(u) + try: + r = requests.get(u, timeout=10) + r.raise_for_status() + latestcomit = r.json()[0]['sha'] + except (requests.RequestException, IndexError, KeyError) as e: + logging.CyberCPLogFileWriter.writeToFile('[versionManagement] GitHub API failed: %s' % str(e)) + head_cmd = 'git -C /usr/local/CyberCP rev-parse HEAD 2>/dev/null || true' + Currentcomt = ProcessUtilities.outputExecutioner(head_cmd).rstrip('\n') + if latestcomit and Currentcomt == latestcomit: + notechk = False + else: + notechk = False template = 'baseTemplate/versionManagment.html' finalData = {'build': currentBuild, 'currentVersion': currentVersion, 'latestVersion': latestVersion, @@ -253,31 +288,41 @@ def getLoadAverage(request): @ensure_csrf_cookie def versionManagment(request): - ## Get latest version + currentVersion = VERSION + currentBuild = str(BUILD) getVersion = requests.get('https://cyberpanel.net/version.txt') latest = getVersion.json() latestVersion = latest['version'] latestBuild = latest['build'] + branch_ref = 'v%s.%s' % (latestVersion, latestBuild) - ## Get local version - - currentVersion = VERSION - currentBuild = str(BUILD) - - u = "https://api.github.com/repos/usmannasir/cyberpanel/commits?sha=v%s.%s" % (latestVersion, latestBuild) - logging.CyberCPLogFileWriter.writeToFile(u) - r = requests.get(u) - latestcomit = r.json()[0]['sha'] - - command = "git -C /usr/local/CyberCP/ rev-parse HEAD" - output = ProcessUtilities.outputExecutioner(command) - - Currentcomt = output.rstrip("\n") notechk = True + Currentcomt = '' + latestcomit = '' - if (Currentcomt == latestcomit): + if _version_compare(currentVersion, latestVersion) > 0: notechk = False + else: + remote_cmd = 'git -C /usr/local/CyberCP remote get-url origin 2>/dev/null || true' + remote_out = ProcessUtilities.outputExecutioner(remote_cmd) + is_usmannasir = 'usmannasir/cyberpanel' in (remote_out or '') + + if is_usmannasir: + u = "https://api.github.com/repos/usmannasir/cyberpanel/commits?sha=%s" % branch_ref + logging.CyberCPLogFileWriter.writeToFile(u) + try: + r = requests.get(u, timeout=10) + r.raise_for_status() + latestcomit = r.json()[0]['sha'] + except (requests.RequestException, IndexError, KeyError) as e: + logging.CyberCPLogFileWriter.writeToFile('[versionManagment] GitHub API failed: %s' % str(e)) + head_cmd = 'git -C /usr/local/CyberCP rev-parse HEAD 2>/dev/null || true' + Currentcomt = ProcessUtilities.outputExecutioner(head_cmd).rstrip('\n') + if latestcomit and Currentcomt == latestcomit: + notechk = False + else: + notechk = False template = 'baseTemplate/versionManagment.html' finalData = {'build': currentBuild, 'currentVersion': currentVersion, 'latestVersion': latestVersion, diff --git a/cyberpanel_upgrade.sh b/cyberpanel_upgrade.sh index 90e545fb8..906dc6e3d 100644 --- a/cyberpanel_upgrade.sh +++ b/cyberpanel_upgrade.sh @@ -795,13 +795,20 @@ if [ "$Server_OS" = "Ubuntu" ]; then fi else rm -rf /usr/local/CyberPanel - if [ -e /usr/bin/pip3 ]; then - PIP3="/usr/bin/pip3" + # 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 - PIP3="pip3.6" + if [ -e /usr/bin/pip3 ]; then PIP3="/usr/bin/pip3"; else PIP3="pip3.6"; fi + $PIP3 install --default-timeout=3600 virtualenv + Check_Return fi - $PIP3 install --default-timeout=3600 virtualenv - Check_Return fi if [[ -f /usr/local/CyberPanel/bin/python2 ]]; then @@ -809,10 +816,15 @@ if [[ -f /usr/local/CyberPanel/bin/python2 ]]; then 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 /usr/local/CyberPanel - elif [[ "$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") - virtualenv -p "$PYTHON_PATH" --system-site-packages /usr/local/CyberPanel + 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 @@ -828,14 +840,19 @@ echo -e "\nNothing found, need fresh setup...\n" 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_Version" = "9" ]] || [[ "$Server_OS_Version" = "10" ]]); then - 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 +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 command failed +# Check if the virtualenv/venv command failed if [ $? -ne 0 ]; then echo "virtualenv command failed." @@ -861,11 +878,15 @@ if [ $? -ne 0 ]; then 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 $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_Version" = "9" ]] || [[ "$Server_OS_Version" = "10" ]]); then - 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 + 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 @@ -888,7 +909,7 @@ fi # shellcheck disable=SC1091 . /usr/local/CyberPanel/bin/activate -pip install --upgrade setuptools packaging +pip install --upgrade pip setuptools packaging Download_Requirement From a4385d55c2ded481432d65239a2795b5ec83ebe7 Mon Sep 17 00:00:00 2001 From: master3395 Date: Sun, 15 Feb 2026 00:02:40 +0100 Subject: [PATCH 055/130] Add panelAccess plugin, pureftpd quota fix, and to-do docs - panelAccess: plugin for panel access settings and OLS proxy - fix-pureftpd-quota-once.sh: one-time quota fix script - to-do: firewall banned IPs, panel access store, reverse proxy CSRF docs --- fix-pureftpd-quota-once.sh | 39 +++ panelAccess.zip | Bin 0 -> 10904 bytes panelAccess/__init__.py | 2 + panelAccess/admin.py | 2 + panelAccess/apps.py | 54 +++ panelAccess/meta.xml | 11 + panelAccess/migrations/__init__.py | 1 + panelAccess/models.py | 2 + panelAccess/ols_proxy.py | 277 ++++++++++++++++ .../templates/panelAccess/settings.html | 307 ++++++++++++++++++ panelAccess/tests.py | 37 +++ panelAccess/urls.py | 12 + panelAccess/views.py | 230 +++++++++++++ ...BANNED-IPS-DATABASE-AND-BAN-PERMANENTLY.md | 83 +++++ to-do/PANEL-ACCESS-PLUGIN-STORE.md | 24 ++ to-do/REVERSE-PROXY-DOMAIN-CSRF.md | 77 +++++ 16 files changed, 1158 insertions(+) create mode 100644 fix-pureftpd-quota-once.sh create mode 100644 panelAccess.zip create mode 100644 panelAccess/__init__.py create mode 100644 panelAccess/admin.py create mode 100644 panelAccess/apps.py create mode 100644 panelAccess/meta.xml create mode 100644 panelAccess/migrations/__init__.py create mode 100644 panelAccess/models.py create mode 100644 panelAccess/ols_proxy.py create mode 100644 panelAccess/templates/panelAccess/settings.html create mode 100644 panelAccess/tests.py create mode 100644 panelAccess/urls.py create mode 100644 panelAccess/views.py create mode 100644 to-do/FIREWALL-BANNED-IPS-DATABASE-AND-BAN-PERMANENTLY.md create mode 100644 to-do/PANEL-ACCESS-PLUGIN-STORE.md create mode 100644 to-do/REVERSE-PROXY-DOMAIN-CSRF.md 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/panelAccess.zip b/panelAccess.zip new file mode 100644 index 0000000000000000000000000000000000000000..fbc27d43224feda6a74d5e85d2ea85adbf4aa6bd GIT binary patch literal 10904 zcma)?1yEhd)~J!-?h@SHEfAcb2@b*C?cf?b!6mr6yIX+Z!QI{6Jp_A)nS1XrOy;k* zsdZqVqP||;z4q>9vQpr$U_qWPgD(;qfBf;E4|otf5Nlm4LkmHDeM37tMkPfU5b$7- zAk$EwI5@+BfWCVA&p$q7ffRJ$4K&Dri-gAJrUQf;@J<^jST88r+NM^f_S)JE)-Hbl zTlWJ^pX3D)qmLQZiwPcLWKrRhHCj+nec%e6tIyaE`H@^QrHiY*U(W3B^;c%P(Or`b zSr-|VLT4%Fn7PCX%immE=V9`#+>|qCd>MI{X;67`ucHwM^X|Ja0UdK^_9Wb2s*V6v zrN)&TDF*`qF@yvGL3^R9uC=w@f7R>;s`w-?)O1&Dvsx2?Z{DUYzWQnavHGpBGsA2b zwm==(US^U87Zu7VZa{=pnA)?2Kn6^tbt}@qB7C_j%^j%&AWUUXJ^2gNu@u+insRh; zQIib|w(dj)-Gn|M*ToBW5O=q+I0qL~P06;IRYjJq1Y*2d!AnQG^^O}La*qT%c1`OUKv%-M+*Zx;#Dpk%o zt`Uwx_#C=N=$a{h9HD#T(#%VvK*LKumudXIfKO+f0+HcWc7vPqddSm#XR zvg1m)ibUOo#OQ`6;;@1B(Mi*|TX)lek&U@wInjm#X@RxCBdgl%#I}TFN;&NwywKGB zu_%}$94;|MJWL%K-hJ2iDKL>^7st-g6hK6%n|zrZ`IArj7B3%Ex)7fLsZlGcQ!WN( zzc{Z#C+`7EUC_5rf!5ii1DmYd+eCp{&aH|0nl8i%83kcpQhk8Oe+f)HwVmqixpN0} zlj}IwT934=f9F~)cb16!&2HR}&7mEdgXvcqnrQ8yac0wa7)}}RR)e!7CotdM>5RT9 zoAz)A4?aDV*lno0hm*Un&Lg-8``8Z2t`aw50KNTwjF@;+OCJ|W+|ZQ2Vp`p61H*}y zzs)@5^PXg%*Y2jbhNwL<5ATVhPblkiQ#$T{XC@ z67U(-2MWdud4g=C zCyeECxOWPD6J%`ls_AHH3OuC$+L%DY(MWq>PCJH>r5cqN#L+yNt}o+#ZQb#*%a|@>z(2^sm`< zySW?@pGV`n+jO$T#M!od&qHVmyW z8#MFppHz!x;eeryHlmC>_B2Ss=n2#m6o~e7|Exr9M-lJ$dIa)g^|!@J?4OZUF*P*_ z{GLZ6cvfBCO1+HhJ4T%j4)h)-4=#&7H;%xD0?uWG7%(_#b1bJ+I{F+!!*_TX*$o`M zNVe%$O61OX9ry7ooIZ^jJFIMcRG!z89==IR<8yEfw& zxG=RPOv-Yb6rlB;W|CGwL6KV`j%K!G9Ja4SaHS8m5yKg8QIt;6fB{Qyh7Ps3Z_`e3 z1%eGr8q=N5xbz~)d9$4BzB9{JU!XR;m6Se0o>r6#{Xq~uVGxTn9+Ox!OY#YxQ=}dO z^ZiMYp*M*4tLQ$*@-f+D7wBj{Wq=d^j_*iKJ%w5VS6XFF->P%2R`aI>d#3uq^jbQ; zmba5?yVN{9PDs5Y3iR+w`?u}}L1md~(w^a%fwV?E*6}B(dhNayjb>V<6Mly&vZ9a{ z3VL0YqUK*~@fGS`0m zI^3AHr_@tlt!+P9#O#KYJUszjUTJs=)Hl+liG4dTreX)cctiJO1 z;;|(siaz(aZCM68cOHEq7`4pZ%LCt~fAR7;XseXVMImLMio@QvdyW(BI^4wZGpBHu zgJFQ534Uz^(H=4a-xRNzxtNp6T31J&vLVW%T2)D^L{t+(6AZFb>b+{_38i^kX}HYD z$=(<1OZv)fk=0~4Vv+U^a}GChiOUt2JlNC8@sO9Vn{+)|bCPJW&9B!o^K&U?*H?3h z<()Aw$aZNyI~hC4Ri{OmAO6bUxc&ReWCJzcZIQaxx^d6MNk=&GU8NMx!fx|!Fe@zA zrW}qQ6XNGXohrWm8Q$ZTnb=p=N=|U}BP5aAco?2z3ujEZd&HOpmfsypOdP?MjVmK% zYnyC5OfKYTgmOMS{hrAaeU?3ycy-Pq+1jYKed28si)zaCFLR74XLns$^!(np7oD2T z3e~1G`+mBs>q4O>dx}a%$FdGZ1yzKISg@OZ(j1xMc+KhWie9H5)7J|s(v$XO)}e%s zflG$YtQ{9G{wUdSqx2~MB+5zqj@lR$odz{T0x3xR91HO70j67;Le>xDN2 zGUC`--Nv2WU#`|cI=aYlWLkQ<-zFUee&X>7#?r&m(WY4qyex;#&3II+GOi`2as>|R*m*P`QPx< ziIhM$?`Z0|DV@nQ*9htbVBaBroHID%{OcWW2=qGfTutPXfM?J(P+mG=4z_=tLSaD1 z^GRO3;w{74VB4AC{f^y(y!;C-c!JA)hl#U3(-*~*(0}06UY=mS(6Q=$^dx*E;9i=6 zSvD@Cv`)sWBp1;>xl^>rfo2Y9P6V#9!VLiL%0yvfnvn0D;Y?u2v*{LyjyeK)-+ zNf<_z+zr+CMzDmnH)fia_8rkxpIRw0b6|t=7U>}QajQ5l3Vxw5m0ra3=n2_QxjYN(u)Y1~f&1?~l7Hp>!-xMr z*nj%)M!-GcR$zokfCd4H!~y|f{QqLx8QR;MS{d6hnAlrd{NdY&1NZ7lUhG$n%AwUf z7v}wPUcL3moTz@6WAa4JI-SuRq8ia6<_AokM=;4uI!iJ+DLILz1dwyKbDdiOFT(9beImpp6)DizuhU9}8Hr)L;mGKWyb| zQ&dqATxZ!gr|)vPEi9MVtmzne zwiAdUMU=He40em6I!?=dOJ}YS3cODtHuh-v02iu}Au!=1tONij3{0oOen7jIgqe}8V9%ea>h6eShD#z^=<4mGdBV; zm!40j>AT&C?n7gVnswf`?`;n&+kVd6ajoB!8)9Qtuw&V5e6*%dmv2K!JFnv|t-ZyC zml>upz+ha->Mx1nKb+E(CA2LHRpRf97*2wKVD2Uj&T`-eYlLyWSixn8t2V-UKiy&F z52&zqA7#mS5kE1S1faHz56LIEvVJG4;MI<7inyFqWDBittIw25 z7MYk$v>v;aWzTGwoqKUy6@urIY%Y7Z9M=nNsaaBxyiT@^4whZ0+q)!CCIu;G<$!CN zEyb&dm--UQQ@)K*-iWJEv>X~;NGn`Y?QTUFdc@|zw10LQu@4Ej(+nW^Ff=?YZH!RE zCpBHdN&xXT9&%i|1oS$-+fi-#drCc;QxUr)xVQdW$4b97b!N-~U)S<`&$foFn_^aF zPdbU1p5l%|Vf@VlfF421i+s$tE0{7SGvf?#g_Zq1lMWunNK11uU0B-hAcA zOefjSe#LAK;QT~v<02W6Nktjwk9{2wu>lQk(Ua}Gz-5oNW{AvtmTa`HuqF?i5GB65 zAGiM+`mt)Ip`EzC%5ldyb}X+Ui6|R!(I*#e0;F(>=BDKm3rouiYm<;a9S;D{32I9Z}Bb1`#t=rjk#B-)Zl;c2UzE zY@n%BN7=5Uh;q%hvGKmN@dw|F01RaZ+f9)wG$=PKU+&AEYH~UR*CDQaS!fjY{|OL_ zZv|CD$5HrIK%@B&O{1C$iCELQ7N3l$)j%}0wTa~&m?Ovt zkmZ*k_W-&xaHbnk>UDichnfdtwAJ7B`$}@)Ci+PkIGApsAW!#%$WGVxHh? zzecrH3%BvwKCx5bRQc^t^8g`kTmoW1zS2P$8WK|~wf_>1XI9pl);q{R%1@>JVWFSr zLo!Tv3iYSvOEN}Tox(S#_gWUBCbf40-2R?8a?PWs31m+=GmZhSq8YA*Z{$4)zV z$x2C*;j)I)0r&I#x#{8`if~SJT|&2|b6MGxj$K-VR1o^+8U5a{AXq@0Wqztt88W5i z4k*C%yq8aMZ@+-$A_VYh>$*LHO~+NI3^n(guQz!A*sNL!-#wVD=OkyA3#Y8I8lc|DkblsT-_pN-wc&bCZjW6o!$zvC9zmC3WIe*_=bm310y!&LFX5zqVf z=HbgPS2~<<8>oZvOxMJ9Q}MFZs!Go$3Vc$d9KF*T-U-m^;f^cUMkmKym6E8t8|Ccv_qGd)oNh7neGFn9cad8G$HZ;b+5m{0=fugL=CW#&QGz|z#}zq!FD zKkYZZ8b#cu&WgTjl|j1uy=5}j83 zKNjtKs3_-6`}|?x^1!+>4Mq#0@jOV)KA>}0KpDOgwRBroHGiy3>mnpVjI%ly=AmkK zNTM^2RJjBV$cq4FgVl3sI{jG0Tm&1N`ObNI1C*DMzL4#>ZLJ+2{v^*4)5V%h3czTz zZf@h^$%(@0G+wlOp!Gg?dR<26n8O`~em!;Xlx$D|8^@5qD+}!{7u-1jUET zY57lw)&6w6`s&1X$?L(_2nfT&hUo>{Jt`kzW424rsCnj_Xu<^NA2x~P9&P!?Qz_eK zFPy~29@)QZGobeWrpC6q_ND-<|3n%s#Nr~ub=!x_J2Eed^o5;c5h@R`|8`!@LKL4|Xh66n0 ziYzdQvLSdH_wO|c;Nojr+X9?j{`)M_J_edT$%|Q(sG0+wLXJ}HHcMFXJ+ewxfjWGo zreF?cK!dShCGd`m^P2tw>i@=_m$eR&3g}eLke&<|;8nExfNH z*>AN�$2e%1(o?4`x{b^5u8Q<{b3cQh^5M6c{{I+4b|tUG}P98@_J%Y|)VdB0p2V z^T}3JD;B3wj^5VYz?TqZSNM!tbtJGc>sK5nWSaSER7w=KDwUSYi$aRKA3I|%#Qy5c zszEDAJ_=({Dlww>2EB7=`;(kxVM7loDXTVykQs^MB}Kyaq4!r;2}uaE0geq5R{gP{ zP*@evfenFC47{?QFX;Khl!L6LZxm1v*C|$y4Y+y96I4!;i*;Z%nHE&MJ2hMY!+ZqY zVo~A8J@G$%<$DDwP!kbYV$i|SotV~La95L#U9|eP4BG51L;_Aji11uq5sYlco5t?c zI@`f)<#Q6X1&?-ZoY92z@FW_tCN6xP?pXFC^RhHe*tI4cjE~axFVprm?ldE}6=EGU zK;(lVt;S02+SVxx*feKo3n7wO(QdkNwk2qBx4S4qlieBLY&hO%Hx5r`{UA^hl5t5y z1wGJexl)hN%dCd$qPy&BC>X=yY0_YNRke+!vlvkHW>`fwsltJV4;x%WK!t<#DvTk=LNEFejh1e{+n z@!r|~teIPMM`(d>xx(zwttYs6f8#T~wB$i%5H~QW$U1a{sx06vol7YX+aNPUv6WNP z#8INYuTOQM51;jU(^R9c7{iPMXCdAuR|0-u2HMtpux+&YtHAJsvrjAs6oZ#5zd z7jp5HbFw%8eQ8#!QF;C71U2+>Q!Ib&c)osc&eRuN1W5mu??HYi!>c>D&4k`zw1wcw zgsuUoX~;xY%!bM&^q>JOf(ozL6QgiQ!9Zls*uK~ZL_k>tt&sO?oMXT>e;-WM^ZDv^ z>WeRQyAhX~n&HHE|2CA7GOCDRYk_9DkuT+aS?=lSY!2^y_H3HZwqP^jdn*ta4+Iw4 zmdLv`+q3SL_5E=uM?x+^Ch1Sd7khi{TN|^tno$UM8SpMkT{BceD`DJE5@6jP`mC2E7}b9HCQtQk845O zlH+f6r9<4o1aYp~PqKOvZnk2>7o|cW6Y32sVK!Z(B}v*gO0})crXjMfTo#L+Z`8t_fSKcjPIUMgBQ6vJVSMJ zty~`z0`aPx>gP8Jwu*NaBYMu4vO89hMO}t2F+fk=|K- z_Kvd)2(q{>9|kb+XX=rFlT^i@0W>3!{0Vi?oe!gX9b}Y?IN0T!mZDlc03tt6aX3RT zy6wl$XNtOq{C}2NopA44=7-(JO?&WmiSEs8y4=ssx-ay(@nBEswB`621R@v>tY|gb z=!~v()TG!3O-Ci@nkXLk*%|t&jkTk{CQ%^F2UB|hb>wM>VTEqH93$IEUaqKaC`#PW zu0t$38^&EIIp9^I=s>3(^iF=0@<|3AC7b9Qv44$2_$Lj2ONOuws+~$o zF3xM2@B`|*1WIg@t(jRue&B9RUy+E{mFONz&PNDnmY7l&AvGL}z$>JLXJz3Vfx z+SLlxtIk^0?5zc8TzqPHpbge~h2ITX|$K&hf*Vv@Lt@u+7@gVRZ+*aRc#+ z)lz=iiZWkOB}8g;{~=9f0pmWJI^2LmT}|dvVIij_{tn_?(b&YI9yAUIk8W4k7K%l4 zCMP_UFa9;3@|ne3^fImQ3w2yr!AZ<(lH(9QBTG56rHh{oxIF65?dt}k!Ys+ftpsOk;(`;dI7xntIUT6m%-Ug?&srG&tYtX#}aD^K3B|Il3+lEbN{mX?tO(w0`xIB94>uNNT;(QP5WW6b`LzX#P5s zdUqB3H;C|$5BxWn($lT4kKXD+CNA5Hp8VLA0{@JMdXZ>E&N_;xh|7jv?})iK0oc4O zc?ADHS4Wu$wiuPyhE!OhMbXOg`c0S}sC}XV-gUgp#BKUcvU{t}d~zm-m}o+7YVlfm z0ouJ)H$>0{f$ss@W?`g-YVMjp(nioE$iw9_sEm(=_W@lndPJS^6^+f6m%CoaHz~_s z|xDH_u5A{5&zFEHp4ch-v*0*a?_H$Vl^9pts~Pp(f2yI05_B z+N59a$n@BGXMLw__QzTS3EXma7Ij7hb2 zbQL^L4IOPfTWH_6V=L~u!zCV#)%nFh!i%%!mS=VJr>+_L0w)6xU`XZ%Y$SOZl35zs z>oPe1Cn5gSJ@X_lyi<4i59Y6#(VK2R!t>4gmvT5r;me}SLkSb>C{S*b9P`bCm5^PG zx<0zsM8fT6OiGM}7;QCEJ~-pEc|;=hCHmiJjt>duRyDJ1``^s(Uv;=XP#HD74&O*& z%f9ALiJ4WglQ(7-)>`QC?(_gVop3-0!%RZY#6PisDj0~Y5-K?0;zk9?nPP|y^%a2e zei@?0vO|@-jH0wxDqxfhF7NDh?a_;3)nkPyS`zz4H7?vfu87>`;)eg)VloDduZGqF zak-A@xCE&j@^*0*1I}y+;k%?S;FjC~vz**#!c`<4`zxMf*g$ZQy|sw@}&~n{cBr+kA3DR?<2lh$h}LxM4?P8;@uKT4E)^H z?RfvCWd7E1*81xEyBgEF(ZIJ2xtj;W6+SG6#*C0SBsY_L53I&@MM0Eh5O#*?-ZAhF z9wAjQh1mWZKW1AzMgg)?pkM^x|JDQb6zM!&$RLM; zPrv_OO@a8cBkIL|{z?3|eyBf*Kyv}f_7;6X{9E7zg#V^9>N)(M9Zb*Rr!V0D>t_Oj zf78qK68!h>kmukd@V~FavtAJ(`Zs+d&(Z&E>UfTh1h$hsU*f-7Jb>iiw0Arw|Fe|% zoa_kP;pgQ46&8Wu-xL-91piy@?oaTaR-py=ckuH{9+3T1ZTqc#{GLtnM{Et;xj!tf z^k3P}r=q0ze~t5RHNEGH0}KCeb9g?^iwYlb(BD+|p6|#%(`e5Jg~#~&pnoORfaKpK z)Si?7nF)JN&U!)qD=!8l|0XB)e6#;3Ws|9cMQ zx!Od)n}317d@=m%IDhW;Kl3Kf$BBM1&fl{rz$N-k2IcwU|MSdyPUj$fF^m75fI#wZ ZPCr>G2w?Li2nZ7J#|xO$1S5aC`+wU+dH?_b literal 0 HcmV?d00001 diff --git a/panelAccess/__init__.py b/panelAccess/__init__.py new file mode 100644 index 000000000..692faaae4 --- /dev/null +++ b/panelAccess/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +# Panel Access app: custom domain / reverse-proxy CSRF trusted origins diff --git a/panelAccess/admin.py b/panelAccess/admin.py new file mode 100644 index 000000000..a57e90d21 --- /dev/null +++ b/panelAccess/admin.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +# No admin models; settings are stored in a config file diff --git a/panelAccess/apps.py b/panelAccess/apps.py new file mode 100644 index 000000000..12ed60f27 --- /dev/null +++ b/panelAccess/apps.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +""" +Panel Access app: merges admin-configured custom panel domains into +CSRF_TRUSTED_ORIGINS so POSTs work when the panel is behind a reverse proxy. +""" +import os +from django.apps import AppConfig + + +def get_panel_csrf_origins_file(): + """Path to the file where custom panel origins are stored (one per line).""" + return os.environ.get( + 'PANEL_CSRF_ORIGINS_FILE', + '/home/cyberpanel/panel_csrf_origins.conf' + ) + + +def read_panel_csrf_origins(): + """Read trusted origins from the config file. Returns list of strings.""" + path = get_panel_csrf_origins_file() + if not os.path.isfile(path): + return [] + try: + with open(path, 'r') as f: + lines = f.readlines() + except (OSError, IOError): + return [] + origins = [] + for line in lines: + line = line.strip() + if not line or line.startswith('#'): + continue + origins.append(line) + return origins + + +class PanelaccessConfig(AppConfig): + name = 'panelAccess' + verbose_name = 'Panel Access (Custom Domain / CSRF)' + + def ready(self): + """Merge admin-configured origins into CSRF_TRUSTED_ORIGINS at startup.""" + try: + from django.conf import settings + custom = read_panel_csrf_origins() + if not custom: + return + base = list(getattr(settings, 'CSRF_TRUSTED_ORIGINS', [])) + for origin in custom: + if origin and origin not in base: + base.append(origin) + settings.CSRF_TRUSTED_ORIGINS = base + except Exception: + pass diff --git a/panelAccess/meta.xml b/panelAccess/meta.xml new file mode 100644 index 000000000..a2167b803 --- /dev/null +++ b/panelAccess/meta.xml @@ -0,0 +1,11 @@ + + + Panel Access (Custom Domain) + Utility + Configure custom domain(s) for accessing CyberPanel behind a reverse proxy. Fixes 403 CSRF errors on POST (e.g. Ban IP) and optionally sets up OpenLiteSpeed proxy so the panel is reachable at your domain without manual OLS config. Detects panel port from bind.conf (2087/8090). + 1.0.1 + master3395 + /plugins/panelAccess/ + /plugins/panelAccess/ + false + diff --git a/panelAccess/migrations/__init__.py b/panelAccess/migrations/__init__.py new file mode 100644 index 000000000..40a96afc6 --- /dev/null +++ b/panelAccess/migrations/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/panelAccess/models.py b/panelAccess/models.py new file mode 100644 index 000000000..3c3410489 --- /dev/null +++ b/panelAccess/models.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +# No models; origins are stored in a config file (panel_csrf_origins.conf) diff --git a/panelAccess/ols_proxy.py b/panelAccess/ols_proxy.py new file mode 100644 index 000000000..0df99cf23 --- /dev/null +++ b/panelAccess/ols_proxy.py @@ -0,0 +1,277 @@ +# -*- coding: utf-8 -*- +""" +OpenLiteSpeed reverse-proxy setup for Panel Access (custom domain). +Creates a proxy-only vhost so the panel is reachable at the custom domain +without manual OLS configuration. +""" +import os +import re + +# Path used by CyberPanel for panel port (same as ProcessUtilities.portPath); SSH login message uses this +BIND_CONF = '/usr/local/lscp/conf/bind.conf' + + +def get_panel_port(): + """Detect panel port from bind.conf (*:PORT). Fallback 8090 (default), then 2087 (common alternate).""" + if os.environ.get('PANEL_BACKEND_URL'): + # Let caller parse URL if they need port from env + pass + try: + if os.path.isfile(BIND_CONF): + with open(BIND_CONF, 'r') as f: + line = f.read().strip() + if '*' in line and ':' in line: + port = line.split(':')[1].strip().split()[0] + if port.isdigit(): + return port + except (OSError, IOError): + pass + return '8090' + + +def get_panel_backend_url(): + """Panel backend URL for proxy. Prefer PANEL_BACKEND_URL env; else detect port from bind.conf.""" + url = os.environ.get('PANEL_BACKEND_URL', '').strip() + if url: + return url + port = get_panel_port() + return 'https://127.0.0.1:{}'.format(port) + + +# Used when module loads (can be overridden by get_panel_backend_url() at runtime) +PANEL_BACKEND_URL = os.environ.get('PANEL_BACKEND_URL') or ('https://127.0.0.1:' + get_panel_port()) +LSWS_ROOT = '/usr/local/lsws' +VHOSTS_DIR = os.path.join(LSWS_ROOT, 'conf', 'vhosts') +PANEL_PROXY_VHROOT = '/usr/local/lsws/panel_proxy' +HTTPD_CONFIG = '/usr/local/lsws/conf/httpd_config.conf' + + +def _domain_from_origin(origin): + """Extract host from origin (e.g. https://panel.example.com -> panel.example.com).""" + if not origin or not isinstance(origin, str): + return None + origin = origin.strip().lower() + if origin.startswith('http://'): + origin = origin[7:] + elif origin.startswith('https://'): + origin = origin[8:] + if '/' in origin: + origin = origin.split('/')[0] + if ':' in origin: + origin = origin.split(':')[0] + # Basic hostname validation + if origin and re.match(r'^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$', origin): + return origin + return None + + +def _vhost_conf_content(domain, backend_url): + """Generate vhost.conf content: proxy entire site to panel backend.""" + # OLS: extprocessor (proxy) + context / (handler = proxy) + return """# Panel Access: reverse proxy to CyberPanel backend (do not edit manually) +docRoot {vhroot}/ +vhDomain {domain} +enableGzip 1 + +extprocessor panelbackend {{ + type proxy + address {backend} + maxConns 100 + initTimeout 60 + retryTimeout 0 + respBuffer 0 +}} + +context / {{ + type proxy + handler panelbackend + addDefaultCharset off +}} + +errorlog $VH_ROOT/logs/error.log {{ + logLevel WARN + rollingSize 10M + useServer 0 +}} + +accessLog $VH_ROOT/logs/access.log {{ + rollingSize 10M + keepDays 7 + useServer 0 +}} +""".format( + vhroot=PANEL_PROXY_VHROOT, + domain=domain, + backend=backend_url, + ) + + +def _virtual_host_block(domain): + """VirtualHost block for httpd_config.conf (same shape as olsMasterMainConf but vhRoot fixed).""" + return """virtualHost {domain} {{ + vhRoot {vhroot} + configFile $SERVER_ROOT/conf/vhosts/{domain}/vhost.conf + allowSymbolLink 1 + enableScript 1 + restrained 1 +}} +""".format(domain=domain, vhroot=PANEL_PROXY_VHROOT) + + +def _domain_already_mapped(domain, lines): + """Return True if domain is already in a listener map.""" + for line in lines: + if 'map' in line and domain in line.split(): + return True + return False + + +def _vhost_block_exists(domain, lines): + """Return True if virtualHost {domain} already exists.""" + marker = 'virtualHost {}'.format(domain) + for line in lines: + if marker in line and ('virtualHost' in line): + return True + return False + + +def setup_panel_proxy_vhost(domain_name): + """ + Create OpenLiteSpeed proxy vhost for the given domain (panel accessible at domain -> backend). + Returns (success: bool, message: str). + """ + try: + from plogical.processUtilities import ProcessUtilities + from plogical import installUtilities + from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter + except ImportError: + return False, 'CyberPanel plumbing not available (run inside CyberPanel).' + + if ProcessUtilities.decideServer() != ProcessUtilities.OLS: + return False, 'Only OpenLiteSpeed is supported for automatic proxy setup.' + + domain = _domain_from_origin(domain_name) if _domain_from_origin(domain_name) else domain_name + if not domain or not re.match(r'^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$', domain): + return False, 'Invalid domain: {}'.format(domain_name) + + vhost_dir = os.path.join(VHOSTS_DIR, domain) + vhost_conf = os.path.join(vhost_dir, 'vhost.conf') + + # Create vhRoot and vhost dirs using ProcessUtilities for proper permissions + try: + # Use ProcessUtilities to create directories with root permissions + if not os.path.exists(PANEL_PROXY_VHROOT): + command = 'mkdir -p {}'.format(PANEL_PROXY_VHROOT) + ProcessUtilities.normalExecutioner(command) + command = 'chmod 755 {}'.format(PANEL_PROXY_VHROOT) + ProcessUtilities.normalExecutioner(command) + + log_dir = os.path.join(PANEL_PROXY_VHROOT, 'logs') + if not os.path.exists(log_dir): + command = 'mkdir -p {}'.format(log_dir) + ProcessUtilities.normalExecutioner(command) + command = 'chmod 755 {}'.format(log_dir) + ProcessUtilities.normalExecutioner(command) + + if not os.path.exists(vhost_dir): + command = 'mkdir -p {}'.format(vhost_dir) + ProcessUtilities.normalExecutioner(command) + command = 'chmod 755 {}'.format(vhost_dir) + ProcessUtilities.normalExecutioner(command) + except Exception as e: + CyberCPLogFileWriter.writeToFile('[panelAccess.ols_proxy] makedirs: {}'.format(e)) + return False, 'Could not create directories: {}'.format(e) + + # Write vhost.conf (use detected panel URL so port 2087/8090 is correct) + # Write to temp file first, then move with ProcessUtilities for proper permissions + backend_url = get_panel_backend_url() + import tempfile + temp_file = None + try: + # Create temp file in /tmp + temp_fd, temp_file = tempfile.mkstemp(suffix='.conf', prefix='panel_access_', dir='/tmp') + with os.fdopen(temp_fd, 'w') as f: + f.write(_vhost_conf_content(domain, backend_url)) + + # Move temp file to final location using ProcessUtilities + command = 'cp {} {}'.format(temp_file, vhost_conf) + ProcessUtilities.normalExecutioner(command) + command = 'chmod 644 {}'.format(vhost_conf) + ProcessUtilities.normalExecutioner(command) + + # Clean up temp file + try: + os.unlink(temp_file) + except: + pass + except Exception as e: + # Clean up temp file on error + if temp_file and os.path.exists(temp_file): + try: + os.unlink(temp_file) + except: + pass + CyberCPLogFileWriter.writeToFile('[panelAccess.ols_proxy] write vhost.conf: {}'.format(e)) + return False, 'Could not write vhost config: {}'.format(e) + + # Add virtualHost + map to httpd_config.conf (idempotent) + # Check if file exists using ProcessUtilities (runs with proper permissions) + try: + command = 'test -f {} && echo exists || echo notfound'.format(HTTPD_CONFIG) + result = ProcessUtilities.outputExecutioner(command).strip() + if result == 'notfound': + # Try to check with ls command as fallback + command2 = 'ls {} 2>&1'.format(HTTPD_CONFIG) + result2 = ProcessUtilities.outputExecutioner(command2).strip() + if 'No such file' in result2 or 'cannot access' in result2: + return False, 'OpenLiteSpeed config not found: {}'.format(HTTPD_CONFIG) + # File might exist but have permission issues - log and continue + CyberCPLogFileWriter.writeToFile('[panelAccess.ols_proxy] Warning: Config file check ambiguous, proceeding: {}'.format(result2)) + except Exception as e: + CyberCPLogFileWriter.writeToFile('[panelAccess.ols_proxy] Error checking config file: {}'.format(e)) + # Don't fail here - let safeModifyHttpdConfig handle it + + def modifier(current_lines): + out = list(current_lines) + if not _domain_already_mapped(domain, out): + for i, line in enumerate(out): + if 'listener' in line and 'Default' in line: + out.insert(i + 1, ' map {} {}\n'.format(domain, domain)) + break + else: + raise ValueError('Default listener not found in httpd_config.conf') + if not _vhost_block_exists(domain, out): + out.append(_virtual_host_block(domain)) + return out + + try: + success, error = installUtilities.installUtilities.safeModifyHttpdConfig( + modifier, + 'Panel Access: add proxy vhost for {}'.format(domain), + skip_validation=True, # Skip validation to avoid pre-existing config errors + ) + if not success: + error_msg = error or 'Failed to update httpd_config.conf.' + CyberCPLogFileWriter.writeToFile('[panelAccess.ols_proxy] safeModifyHttpdConfig failed: {}'.format(error_msg)) + return False, error_msg + except Exception as e: + error_msg = 'Error calling safeModifyHttpdConfig: {}'.format(str(e)) + CyberCPLogFileWriter.writeToFile('[panelAccess.ols_proxy] {}'.format(error_msg)) + return False, error_msg + + # Reload OpenLiteSpeed + try: + cmd = '/usr/local/lsws/bin/lswsctrl reload' + subprocess = __import__('subprocess') + r = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=15) + if r.returncode != 0: + CyberCPLogFileWriter.writeToFile('[panelAccess.ols_proxy] lswsctrl reload: {}'.format(r.stderr or r.stdout)) + except Exception as e: + CyberCPLogFileWriter.writeToFile('[panelAccess.ols_proxy] reload: {}'.format(e)) + + return True, 'Proxy for {} added. Reload OpenLiteSpeed if needed.'.format(domain) + + +def domain_from_origin(origin): + """Public helper: extract hostname from origin URL.""" + return _domain_from_origin(origin) diff --git a/panelAccess/templates/panelAccess/settings.html b/panelAccess/templates/panelAccess/settings.html new file mode 100644 index 000000000..444dad7d8 --- /dev/null +++ b/panelAccess/templates/panelAccess/settings.html @@ -0,0 +1,307 @@ +{% extends "baseTemplate/index.html" %} +{% load i18n %} +{% block title %}{% trans "Panel Access (Custom Domain) - CyberPanel" %}{% endblock %} +{% block content %} +{% load static %} + +
+
+ +
+

{% trans "Trusted origins" %}

+

{% trans "Search and select domain(s) from your existing websites. Both HTTPS and HTTP versions will be added automatically." %}

+
+ {% csrf_token %} + +
+ +
+
+
+ +
+ +

{% trans "Creates a proxy vhost so the panel is reachable at the custom domain without manual OLS configuration. HTTP (port 80) only; for HTTPS use Manage SSL or your own certificate." %}

+
+
+ +
+
+

{% trans "Note:" %} {% trans "Save will restart the CyberPanel backend (lscpd) automatically so CSRF changes take effect. If restart fails (e.g. permissions), run systemctl restart lscpd manually. Config file: " %}{{ config_path }}

+
+
+
+
+ +{% endblock %} diff --git a/panelAccess/tests.py b/panelAccess/tests.py new file mode 100644 index 000000000..272bbf9e0 --- /dev/null +++ b/panelAccess/tests.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +from django.test import TestCase +from panelAccess.apps import read_panel_csrf_origins, get_panel_csrf_origins_file +import os +import tempfile + + +class PanelAccessOriginsTest(TestCase): + def test_read_empty_missing_file(self): + with tempfile.TemporaryDirectory() as d: + path = os.path.join(d, 'nonexistent.conf') + orig = os.environ.get('PANEL_CSRF_ORIGINS_FILE') + try: + os.environ['PANEL_CSRF_ORIGINS_FILE'] = path + self.assertEqual(read_panel_csrf_origins(), []) + finally: + if orig is not None: + os.environ['PANEL_CSRF_ORIGINS_FILE'] = orig + else: + os.environ.pop('PANEL_CSRF_ORIGINS_FILE', None) + + def test_read_origins_skips_comments_and_blanks(self): + with tempfile.NamedTemporaryFile(mode='w', suffix='.conf', delete=False) as f: + f.write('# comment\n\nhttps://a.com\n \nhttp://b.com\n') + path = f.name + try: + orig = os.environ.get('PANEL_CSRF_ORIGINS_FILE') + try: + os.environ['PANEL_CSRF_ORIGINS_FILE'] = path + self.assertEqual(read_panel_csrf_origins(), ['https://a.com', 'http://b.com']) + finally: + if orig is not None: + os.environ['PANEL_CSRF_ORIGINS_FILE'] = orig + else: + os.environ.pop('PANEL_CSRF_ORIGINS_FILE', None) + finally: + os.unlink(path) diff --git a/panelAccess/urls.py b/panelAccess/urls.py new file mode 100644 index 000000000..c3fed1361 --- /dev/null +++ b/panelAccess/urls.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +from django.urls import path +from . import views + +# Note: app_name removed to avoid namespace issues with dynamic plugin inclusion +# URLs are accessed directly via /plugins/panelAccess/ paths + +urlpatterns = [ + path('', views.settings_page, name='panel_access_settings'), + path('save', views.save_origins, name='panel_access_save'), + path('domains', views.get_domains_api, name='panel_access_domains'), +] diff --git a/panelAccess/views.py b/panelAccess/views.py new file mode 100644 index 000000000..36f011aba --- /dev/null +++ b/panelAccess/views.py @@ -0,0 +1,230 @@ +# -*- coding: utf-8 -*- +""" +Panel Access settings: configure custom panel domain(s) for CSRF when +the panel is behind a reverse proxy (e.g. https://panel.example.com -> IP:2087). +""" +from django.shortcuts import redirect +from django.http import JsonResponse +from django.views.decorators.http import require_http_methods +from django.utils.translation import gettext as _ +from loginSystem.views import loadLoginPage +from plogical.httpProc import httpProc +from plogical.mailUtilities import mailUtilities +from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter +from plogical.processUtilities import ProcessUtilities +from .apps import get_panel_csrf_origins_file, read_panel_csrf_origins +from websiteFunctions.models import Websites, ChildDomains +import os +import subprocess +import json + + +def _ensure_origins_dir(): + """Ensure directory for panel_csrf_origins.conf exists.""" + path = get_panel_csrf_origins_file() + d = os.path.dirname(path) + if d and not os.path.isdir(d): + try: + os.makedirs(d, mode=0o755) + except (OSError, IOError): + pass + + +def _get_all_domains(): + """Get all domains and subdomains from CyberPanel database.""" + domains = [] + try: + # Get all main websites + websites = Websites.objects.filter(state=1).values_list('domain', flat=True) + domains.extend([domain for domain in websites]) + + # Get all child domains (subdomains) + child_domains = ChildDomains.objects.all().values_list('domain', flat=True) + domains.extend([domain for domain in child_domains]) + + # Remove duplicates and sort + domains = sorted(list(set(domains))) + except Exception: + pass + return domains + + +@require_http_methods(['GET']) +def settings_page(request): + """Show Panel Access settings page with current custom domains.""" + try: + request.session['userID'] + except KeyError: + return redirect(loadLoginPage) + mailUtilities.checkHome() + origins = read_panel_csrf_origins() + all_domains = _get_all_domains() + data = { + 'origins': origins, + 'origins_text': '\n'.join(origins), + 'config_path': get_panel_csrf_origins_file(), + 'all_domains': json.dumps(all_domains), + 'origins_json': json.dumps(origins), + } + proc = httpProc(request, 'panelAccess/settings.html', data, 'admin') + return proc.render() + + +@require_http_methods(['POST']) +def save_origins(request): + """Save custom panel origins (one per line). Admin only. Returns JSON.""" + try: + # Check session + try: + request.session['userID'] + except KeyError: + return JsonResponse({ + 'save': 0, + 'error_message': _('Session expired. Please refresh the page and log in again.'), + }, status=401) + + # Check admin permissions + try: + from loginSystem.models import Administrator + from plogical.acl import ACLManager + user_id = request.session['userID'] + current_acl = ACLManager.loadedACL(user_id) + if not current_acl.get('admin'): + return JsonResponse({ + 'save': 0, + 'error_message': _('Only administrators can change Panel Access settings.'), + }, status=403) + except Exception as e: + CyberCPLogFileWriter.writeToFile(f"Panel Access: Authorization check error: {str(e)}") + return JsonResponse({ + 'save': 0, + 'error_message': _('Authorization check failed.'), + }, status=500) + + # Handle both old textarea format and new select format + origins_raw = request.POST.get('origins', '').strip() + # Try both with and without brackets (security middleware blocks brackets in param names) + origins_list = request.POST.getlist('origins_list') or request.POST.getlist('origins_list[]') # New format: array of selected domains + + # If new format is used, convert to origin format (add https:// and http://) + lines = [] + if origins_list: + for domain in origins_list: + domain = domain.strip() + if domain: + # Add both https and http versions + lines.append(f'https://{domain}') + lines.append(f'http://{domain}') + elif origins_raw: + # Fallback to old textarea format + lines = [ln.strip() for ln in origins_raw.splitlines() if ln.strip() and not ln.strip().startswith('#')] + path = get_panel_csrf_origins_file() + _ensure_origins_dir() + try: + with open(path, 'w') as f: + f.write('# Custom panel domain(s) for CSRF (one origin per line)\n') + for line in lines: + f.write(line + '\n') + try: + os.chmod(path, 0o600) + except (OSError, IOError): + pass + except (OSError, IOError) as e: + return JsonResponse({ + 'save': 0, + 'error_message': _('Could not write config file: %s') % str(e), + }) + + message = _('Custom domains saved. Restart the CyberPanel backend (e.g. systemctl restart lscpd) for CSRF to take effect.') + proxy_results = [] + + setup_ols = request.POST.get('setup_ols_proxy', '').strip().lower() in ('1', 'true', 'yes', 'on') + if setup_ols and lines: + try: + from .ols_proxy import setup_panel_proxy_vhost, domain_from_origin + except ImportError as e: + CyberCPLogFileWriter.writeToFile(f"Panel Access: Failed to import ols_proxy: {str(e)}") + except Exception as e: + CyberCPLogFileWriter.writeToFile(f"Panel Access: Error importing ols_proxy: {str(e)}") + else: + try: + seen = set() + for origin in lines: + try: + domain = domain_from_origin(origin) + if not domain or domain in seen: + continue + seen.add(domain) + ok, msg = setup_panel_proxy_vhost(domain) + proxy_results.append({'domain': domain, 'success': ok, 'message': msg}) + except Exception as e: + CyberCPLogFileWriter.writeToFile(f"Panel Access: Error processing origin {origin}: {str(e)}") + proxy_results.append({'domain': origin, 'success': False, 'message': f'Error: {str(e)}'}) + except Exception as e: + CyberCPLogFileWriter.writeToFile(f"Panel Access: Error in OLS proxy setup: {str(e)}") + # Don't fail the entire save if OLS setup fails + if proxy_results: + parts = [message] + for r in proxy_results: + parts.append('{}: {}'.format(r['domain'], r['message'])) + message = ' '.join(parts) + + # Restart lscpd so Django loads the new CSRF origins + restart_ok = False + restart_error = None + try: + # Use ProcessUtilities like RestartCyberPanel does + command = 'systemctl restart lscpd' + ProcessUtilities.popenExecutioner(command) + restart_ok = True + except Exception as e: + restart_error = str(e) + CyberCPLogFileWriter.writeToFile(f"Panel Access: Failed to restart lscpd: {str(e)}") + + if restart_ok: + message = _('Custom domains saved. CyberPanel backend (lscpd) restarted; CSRF changes are active.') + if proxy_results: + message = message + ' ' + ' '.join('{}: {}.'.format(r['domain'], r['message']) for r in proxy_results) + else: + message = _('Custom domains saved. Restart the CyberPanel backend manually (systemctl restart lscpd) for CSRF to take effect.') + if restart_error: + message = message + ' ' + _('Restart failed: %s') % restart_error + if proxy_results: + message = message + ' ' + ' '.join('{}: {}.'.format(r['domain'], r['message']) for r in proxy_results) + + return JsonResponse({ + 'save': 1, + 'message': message, + 'proxy_results': proxy_results, + 'lscpd_restarted': restart_ok, + }) + except Exception as e: + CyberCPLogFileWriter.writeToFile(f"Panel Access: Save error: {str(e)}") + import traceback + CyberCPLogFileWriter.writeToFile(f"Panel Access: Traceback: {traceback.format_exc()}") + return JsonResponse({ + 'save': 0, + 'error_message': _('An error occurred while saving: %s') % str(e), + }, status=500) + + +@require_http_methods(['GET']) +def get_domains_api(request): + """API endpoint to get all domains and subdomains for the selector.""" + try: + request.session['userID'] + except KeyError: + return JsonResponse({'error': 'Not authenticated'}, status=401) + + try: + from loginSystem.models import Administrator + from plogical.acl import ACLManager + user_id = request.session['userID'] + current_acl = ACLManager.loadedACL(user_id) + if not current_acl.get('admin'): + return JsonResponse({'error': 'Admin access required'}, status=403) + except Exception: + return JsonResponse({'error': 'Authorization check failed'}, status=500) + + domains = _get_all_domains() + return JsonResponse({'domains': domains}) diff --git a/to-do/FIREWALL-BANNED-IPS-DATABASE-AND-BAN-PERMANENTLY.md b/to-do/FIREWALL-BANNED-IPS-DATABASE-AND-BAN-PERMANENTLY.md new file mode 100644 index 000000000..47337a507 --- /dev/null +++ b/to-do/FIREWALL-BANNED-IPS-DATABASE-AND-BAN-PERMANENTLY.md @@ -0,0 +1,83 @@ +# Firewall Banned IPs: Database Storage and "Ban IP Permanently" Fix + +## Summary + +- **Issue:** "Ban IP Permanently" from SSH Security Analysis did not show up in Firewall Management > Banned IPs. +- **Cause:** `addBannedIP` only wrote to a JSON file, while `getBannedIPs` tried the database first. When the `BannedIP` model was available, the list was read from the database (empty), so dashboard bans (stored only in JSON) did not appear. +- **Fix:** Primary storage is now the **database** (`BannedIP` model). `addBannedIP` saves to the database first; JSON file is used only when the model is unavailable (fallback). **JSON is used only for export/import**, not for primary storage. + +## Storage Policy + +| Use case | Storage | +|-----------------------|----------------| +| Adding a ban | Database first, JSON fallback only if DB unavailable | +| Listing banned IPs | Database first, JSON fallback only if DB unavailable | +| Export Banned IPs | Output in **JSON format** (from DB or JSON store) | +| Import Banned IPs | Input in **JSON format**; writes to DB or JSON store | + +Bans are **not** stored in a JSON file for normal operation when the database is available. + +## Code Changes + +1. **`firewall/firewallManager.py` – `addBannedIP`** + - Tries to save to the `BannedIP` database model first. + - Only uses the JSON file when the model cannot be imported (e.g. migrations not run). + - Applies the firewall rule in both paths; rolls back the DB or JSON record if the firewall command fails. + +2. **`firewall/migrations/0001_initial.py`** (new) + - Creates the `firewall_bannedips` table and indexes for the `BannedIP` model. + - Ensures the table exists after deploy. + +## Deployment (Server) + +1. **Copy updated code** from this repo to the panel (e.g. `/usr/local/CyberCP/`), including: + - `firewall/firewallManager.py` + - `firewall/models.py` (must define `BannedIP`) + - `firewall/migrations/0001_initial.py` + +2. **Run migrations** so the banned IPs table exists: + ```bash + cd /usr/local/CyberCP + sudo -u cyberpanel /usr/local/CyberCP/bin/python manage.py migrate firewall + ``` + If you see "table already exists" for `firewall_bannedips`, the table was created earlier; ensure `firewall.models` defines `BannedIP` and is in `INSTALLED_APPS`. + +3. **Restart the panel** (e.g. lscpd / gunicorn) so the new code is loaded. + +4. **Optional – one-time sync:** If you had bans only in the JSON file and want them in the DB, use **Firewall > Banned IPs > Export Banned IPs**, then **Import Banned IPs** with that file after migrations are applied (so imports go to the database). + +## Verification + +- Click "Ban IP Permanently" on an IP in **Dashboard > SSH Security Analysis**. +- Open **Firewall > Banned IPs** and confirm that IP appears in the list (from the database). +- Export/Import should still use JSON format for the file; listing and adding use the database when available. + +## Run and test in the browser + +**Done for you (on this machine):** + +1. **Deployed** `firewall/firewallManager.py` to `/usr/local/CyberCP/firewall/`. +2. **Restarted** the panel backend: `systemctl restart lscpd` (lscpd is **active**). +3. Panel is listening on **port 2087** (e.g. `https://YOUR_SERVER_IP:2087`). + +**Manual browser test:** + +1. Open the CyberPanel URL (e.g. `https://207.180.193.210:2087` or `https://localhost:2087`). Accept the certificate warning if needed. +2. Log in as admin. +3. **Dashboard:** Scroll to **SSH Security Analysis**. If there is an alert (e.g. "Root Login Attempts Detected"), click **Ban IP Permanently** on one of the IPs (e.g. the "Top IP"). +4. Confirm the success message (e.g. "IP address … has been permanently banned … You can manage it in the Firewall > Banned IPs section"). +5. Go to **Firewall** (left menu) → **Banned IPs** tab. +6. **Verify:** The IP you just banned appears in the table (IP ADDRESS, REASON e.g. "Brute force attack detected from SSH Security Analysis", EXPIRES "Never", STATUS ACTIVE). +7. Optionally: **Export Banned IPs** → download JSON; **Import Banned IPs** → upload that JSON to confirm export/import still use JSON format. + +**Quick API check (optional, from server):** + +```bash +# After logging in in the browser, get session cookie or use a session ID, then: +curl -k -s -X POST 'https://127.0.0.1:2087/firewall/addBannedIP' \ + -H 'Content-Type: application/json' \ + -H 'Cookie: sessionid=YOUR_SESSION_ID' \ + -H 'X-CSRFToken: YOUR_CSRF_TOKEN' \ + -d '{"ip":"203.0.113.99","reason":"Test ban","duration":"permanent"}' +# Then open Firewall > Banned IPs and confirm 203.0.113.99 appears. +``` diff --git a/to-do/PANEL-ACCESS-PLUGIN-STORE.md b/to-do/PANEL-ACCESS-PLUGIN-STORE.md new file mode 100644 index 000000000..34949d496 --- /dev/null +++ b/to-do/PANEL-ACCESS-PLUGIN-STORE.md @@ -0,0 +1,24 @@ +# Panel Access plugin – store distribution + +**Panel Access (Custom Domain)** is a normal plugin. It is **not** in the core repo’s `INSTALLED_APPS` or main URLs. + +## Install on a server + +1. **From zip (repo)** + - Build zip: from repo root, `zip -r panelAccess.zip panelAccess -x "panelAccess/__pycache__/*" -x "*.pyc"` + - In CyberPanel: **Plugins → Store** (or upload zip), install **Panel Access (Custom Domain)**. + - Or copy `panelAccess/` to `/usr/local/CyberCP/panelAccess/`, add `'panelAccess'` to `CyberCP/settings.py` `INSTALLED_APPS` (after `'emailPremium',`), and add + `path('plugins/panelAccess/', include('panelAccess.urls')),` to `CyberCP/urls.py` before the generic `path('plugins/', include('pluginHolder.urls'))`, then restart lscpd. + +2. **From plugin store (GitHub)** + - Add the `panelAccess` folder to the **cyberpanel-plugins** repo (e.g. `master3395/cyberpanel-plugins`) so it appears in the store. + - Users then install via **Plugins → Store** like Memcache Manager or Contabo Auto Snapshot. + +## URLs + +- Settings page: **/plugins/panelAccess/** +- Same as **Settings** from **Plugins → Installed** for Panel Access. + +## Zip location + +- `panelAccess.zip` can be generated in the repo root and committed or published for one-off installs. diff --git a/to-do/REVERSE-PROXY-DOMAIN-CSRF.md b/to-do/REVERSE-PROXY-DOMAIN-CSRF.md new file mode 100644 index 000000000..ba71d956f --- /dev/null +++ b/to-do/REVERSE-PROXY-DOMAIN-CSRF.md @@ -0,0 +1,77 @@ +# CyberPanel Behind a Reverse Proxy (Custom Domain) + +When you put a **custom domain** in front of the panel (e.g. `https://panel.example.com` → proxy → `http://127.0.0.1:2087`), two things often break if the proxy is not configured for them: + +1. **403 on POST requests** (e.g. Ban IP, form submissions) — **CSRF verification failed** +2. **Charts / some UI not loading** — backend may treat the request as different from “direct” IP:port access + +With **IP:port** everything works because the browser and the backend agree on the same host. + +--- + +## Why it breaks + +- The **browser** sends `Origin` / `Referer` with the **public URL** (e.g. `https://panel.example.com`). +- The **proxy** often forwards the request with **Host** set to the **backend** (e.g. `127.0.0.1:2087` or `207.180.193.210:2087`). +- **Django** checks CSRF by comparing the request’s **Referer/Origin** to the request’s **Host**. They don’t match → **403 Forbidden** and the body says *“CSRF verification failed. Request aborted.”* + +So: **domain in the browser, backend host in `Host`** → CSRF fails and POSTs (like Ban IP) get 403. + +--- + +## Fix option 1: Panel Access (recommended, no env vars) + +Use the built-in **Panel Access** page to add your custom domain(s): + +1. In CyberPanel go to **Plugins → Panel Access (Custom Domain)**. +2. Enter your public origin(s), one per line (e.g. `https://panel.example.com`, `http://panel.example.com`). +3. Click **Save**. +4. Restart the CyberPanel backend so Django picks up the change, e.g.: + ```bash + systemctl restart lscpd + ``` + +Origins are stored in a config file (by default `/home/cyberpanel/panel_csrf_origins.conf`). They are merged with any `CSRF_TRUSTED_ORIGINS` set via environment. No hardcoded domains; safe for GitHub. + +**OpenLiteSpeed proxy (optional):** On the same page you can enable **“Also add domain in OpenLiteSpeed (reverse proxy to panel)”**. When you save, the panel will create a proxy vhost for each domain so the panel is reachable at that domain (e.g. `http://panel.example.com`) without manual OLS configuration. This only configures HTTP (port 80); for HTTPS use Manage SSL or your own certificate. The proxy forwards to the panel backend (default `https://127.0.0.1:2087`). Override with the `PANEL_BACKEND_URL` environment variable if your panel listens elsewhere. + +--- + +## Fix option 2: Environment variable + +In `CyberCP/settings.py`, `CSRF_TRUSTED_ORIGINS` is also built from the environment variable **`CSRF_TRUSTED_ORIGINS`** (comma‑separated list of origins). + +When you run the panel behind a custom domain, you can set that variable to your **public** origin(s), for example: + +```bash +export CSRF_TRUSTED_ORIGINS="https://panel.example.com,http://panel.example.com" +``` + +Then start (or restart) the CyberPanel backend. Where to set it depends on how you run the panel: + +- **systemd (lscpd)**: add to `[Service]` in `/etc/systemd/system/lscpd.service`: + ```ini + Environment="CSRF_TRUSTED_ORIGINS=https://panel.example.com,http://panel.example.com" + ``` + Then run `systemctl daemon-reload` and `systemctl restart lscpd`. +- **Supervisor / other**: set in the program’s `environment` or equivalent. +- **Manual run**: export in the same shell before starting the app. + +Use your real domain; no need to add anything to the repo. This keeps the codebase generic for GitHub. + +--- + +## Optional: proxy Host header + +Some backends only “recognise” the panel when **Host** is the backend address (e.g. IP:port). In that case the proxy is often configured to **override** Host to that address so the initial HTML and routing work. That is why the **same** proxy setup can make the **page** load but **POSTs** fail: Referer stays the public domain, Host is the backend → CSRF fails. Fixing CSRF with `CSRF_TRUSTED_ORIGINS` (as above) addresses that. + +--- + +## Summary + +| Symptom | Cause | Fix (generic, repo‑friendly) | +|----------------------|--------------------------------|--------------------------------| +| 403 on Ban IP / POST | CSRF fail (Referer vs Host) | **Panel Access**: Plugins → Panel Access (Custom Domain), add your origin(s), save, then restart lscpd. Or set `CSRF_TRUSTED_ORIGINS` env and restart backend. | +| Charts / UI not loading | Can be session/Host/static | Ensure session cookies and static URLs work; CSRF fix above helps POSTs; adjust proxy if needed | + +No domain is hardcoded in the repo. Use the Panel Access page or the `CSRF_TRUSTED_ORIGINS` environment variable for your own domain(s). From 99f51a813bacdfb1eb79b5d02fa94788839f4451 Mon Sep 17 00:00:00 2001 From: master3395 Date: Sun, 15 Feb 2026 00:03:28 +0100 Subject: [PATCH 056/130] Remove to-do folder from repository --- to-do/AWS-CURSOR-REMOTE-SSH-SETUP.md | 120 ----------- to-do/EMAIL-LIMITS-DEPLOY-CHECKLIST.md | 57 ----- to-do/EMAIL-LIMITS-LIVE-SERVER-CHECKLIST.md | 108 ---------- ...BANNED-IPS-DATABASE-AND-BAN-PERMANENTLY.md | 83 -------- to-do/FTP-QUOTA-BROWSER-TEST-CHECKLIST.md | 21 -- to-do/FTP-QUOTAS-TABLE-FIX.md | 25 --- to-do/INSTALL-UPGRADE-DOWNGRADE-COMMANDS.md | 201 ------------------ to-do/MARIADB-11.8-LTS-UPGRADE.md | 34 --- to-do/MARIADB_INSTALLATION_FIXES.md | 88 -------- to-do/OLD-REPO-CHECKLIST-BEFORE-REMOVAL.md | 132 ------------ .../OPENLITESPEED-VERSION-INSTALL-UPGRADE.md | 62 ------ to-do/PANEL-ACCESS-PLUGIN-STORE.md | 24 --- to-do/PLUGIN-DEFAULT-REMOVAL-2026-02-01.md | 13 -- to-do/PURE-FTPD-QUOTA-SYNTAX-FIX.md | 45 ---- to-do/REPO-MERGE-2026-02-02.md | 38 ---- to-do/REVERSE-PROXY-DOMAIN-CSRF.md | 77 ------- to-do/RUNTIME-VS-REPO-2.5.5-DEV.md | 83 -------- to-do/V2.5.5-DEV-BRANCH-COMPATIBILITY.md | 76 ------- to-do/V2.5.5-DEV-FIXES-AND-DEPLOY.md | 89 -------- 19 files changed, 1376 deletions(-) delete mode 100644 to-do/AWS-CURSOR-REMOTE-SSH-SETUP.md delete mode 100644 to-do/EMAIL-LIMITS-DEPLOY-CHECKLIST.md delete mode 100644 to-do/EMAIL-LIMITS-LIVE-SERVER-CHECKLIST.md delete mode 100644 to-do/FIREWALL-BANNED-IPS-DATABASE-AND-BAN-PERMANENTLY.md delete mode 100644 to-do/FTP-QUOTA-BROWSER-TEST-CHECKLIST.md delete mode 100644 to-do/FTP-QUOTAS-TABLE-FIX.md delete mode 100644 to-do/INSTALL-UPGRADE-DOWNGRADE-COMMANDS.md delete mode 100644 to-do/MARIADB-11.8-LTS-UPGRADE.md delete mode 100644 to-do/MARIADB_INSTALLATION_FIXES.md delete mode 100644 to-do/OLD-REPO-CHECKLIST-BEFORE-REMOVAL.md delete mode 100644 to-do/OPENLITESPEED-VERSION-INSTALL-UPGRADE.md delete mode 100644 to-do/PANEL-ACCESS-PLUGIN-STORE.md delete mode 100644 to-do/PLUGIN-DEFAULT-REMOVAL-2026-02-01.md delete mode 100644 to-do/PURE-FTPD-QUOTA-SYNTAX-FIX.md delete mode 100644 to-do/REPO-MERGE-2026-02-02.md delete mode 100644 to-do/REVERSE-PROXY-DOMAIN-CSRF.md delete mode 100644 to-do/RUNTIME-VS-REPO-2.5.5-DEV.md delete mode 100644 to-do/V2.5.5-DEV-BRANCH-COMPATIBILITY.md delete mode 100644 to-do/V2.5.5-DEV-FIXES-AND-DEPLOY.md diff --git a/to-do/AWS-CURSOR-REMOTE-SSH-SETUP.md b/to-do/AWS-CURSOR-REMOTE-SSH-SETUP.md deleted file mode 100644 index b62b92e8e..000000000 --- a/to-do/AWS-CURSOR-REMOTE-SSH-SETUP.md +++ /dev/null @@ -1,120 +0,0 @@ -# AWS EC2 + Cursor Remote-SSH – Full Setup Guide - -Use this guide to get **aws-server** (3.144.171.128) working with Cursor Remote-SSH. -Do the steps in order. Everything is copy-paste ready. - ---- - -## 1. Windows SSH config - -**File:** `C:\Users\kimsk\.ssh\config` - -- Open the file in Notepad or Cursor. -- Find the `Host aws-server` block and replace it entirely with the block below (or add it if missing). -- Use **straight double quotes** `"`, not curly quotes. Path uses forward slashes to avoid issues. - -**Exact block to use (port 22 – default):** - -``` -Host aws-server - HostName 3.144.171.128 - User ec2-user - Port 22 - IdentityFile "D:/OneDrive - v-man/Priv/VPS/Cyberpanel.pem" -``` - -- Save and close. -- If you later confirm SSH on the instance is on port 2222, change `Port 22` to `Port 2222` and add an inbound rule for 2222 in the Security Group (see step 3). - ---- - -## 2. AWS Security Group – allow SSH (port 22) - -1. **AWS Console** → **EC2** → **Instances**. -2. Select the instance whose **Public IPv4** is **3.144.171.128**. -3. Open the **Security** tab → click the **Security group** name (e.g. `sg-xxxxx`). -4. **Edit inbound rules** → **Add rule**: - - **Type:** SSH - - **Port:** 22 - - **Source:** **My IP** (recommended) or **Anywhere-IPv4** (`0.0.0.0/0`) for testing only. -5. **Save rules**. - -If you use port 2222 on the instance, add another rule: **Custom TCP**, port **2222**, source **My IP** (or **Anywhere-IPv4** for testing). - ---- - -## 3. Start SSH on the instance (fix “Connection refused”) - -You must run commands on the instance without using SSH from your PC. Use one of these. - -### Option A: EC2 Instance Connect (simplest) - -1. **EC2** → **Instances** → select the instance (3.144.171.128). -2. Click **Connect**. -3. Open the **EC2 Instance Connect** tab → **Connect** (browser shell). - -In the browser terminal, run: - -```bash -sudo systemctl status sshd -sudo systemctl start sshd -sudo systemctl enable sshd -sudo ss -tlnp | grep 22 -``` - -You should see `sshd` listening on port 22. Then close the browser and try Cursor. - -### Option B: Session Manager - -1. **EC2** → **Instances** → select the instance → **Connect**. -2. Choose **Session Manager** → **Connect**. -3. Run the same commands as in Option A. - -### Option C: SSH is on port 2222 - -If you know SSH was moved to 2222 on this instance: - -1. In the Security Group, add an **inbound rule**: **Custom TCP**, port **2222**, source **My IP** (or **Anywhere-IPv4** for testing). -2. In your SSH config, set `Port 2222` for `aws-server` (see step 1). -3. Test (see step 4). - ---- - -## 4. Test from Windows - -Open **PowerShell** and run: - -```powershell -ssh -i "D:/OneDrive - v-man/Priv/VPS/Cyberpanel.pem" -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new ec2-user@3.144.171.128 -``` - -- If it asks for a host key, type `yes`. -- If you get a shell prompt, SSH works. Type `exit` to close. -- If you get **Connection refused**: SSH is not listening on 22 (or 2222); repeat step 3 (Instance Connect / Session Manager) and ensure `sshd` is running and listening on the port you use. -- If you get **Connection timed out**: Security Group is still blocking the port; recheck step 2 and that you edited the security group attached to this instance. - ---- - -## 5. Connect from Cursor - -1. In Cursor: **Ctrl+Shift+P** (or **Cmd+Shift+P** on Mac) → **Remote-SSH: Connect to Host**. -2. Choose **aws-server** (or type `aws-server`). -3. Wait for the remote window to open. Cursor AI (Chat, Composer) works in that window as usual. - ---- - -## Checklist - -- [ ] SSH config has the `aws-server` block with correct `IdentityFile` and `Port` (22 or 2222). -- [ ] Security Group has an inbound rule for the SSH port (22 or 2222) from My IP (or 0.0.0.0/0 for testing). -- [ ] `sshd` is running on the instance (started via Instance Connect or Session Manager). -- [ ] `ssh ... ec2-user@3.144.171.128` works in PowerShell. -- [ ] Cursor **Connect to Host** → **aws-server** succeeds. - ---- - -## If it still fails - -- **Connection refused** → Instance side: start/enable `sshd` and confirm it listens on the port you use (step 3). -- **Connection timed out** → Network: open that port in the instance’s Security Group (step 2). -- **Permission denied (publickey)** → Wrong key or user: confirm the .pem is the one for this instance and the user is `ec2-user` (Amazon Linux) or `ubuntu` (Ubuntu AMI). diff --git a/to-do/EMAIL-LIMITS-DEPLOY-CHECKLIST.md b/to-do/EMAIL-LIMITS-DEPLOY-CHECKLIST.md deleted file mode 100644 index 779548dab..000000000 --- a/to-do/EMAIL-LIMITS-DEPLOY-CHECKLIST.md +++ /dev/null @@ -1,57 +0,0 @@ -# Email Limits Fix – Deploy Checklist - -Use this after pulling the Email Limits fixes in this repo so that https://your-panel/email/EmailLimits works (controller registers, email list loads, configure section works). - -## Files that are part of the fix - -| File | Purpose | -|------|--------| -| `mailServer/mailserverManager.py` | Passes controller JS to template; allows getEmailsForDomain for emailForwarding | -| `mailServer/templates/mailServer/EmailLimits.html` | Inline controller in footer_scripts (no static file dependency) | -| `mailServer/static/mailServer/mailServer.js` | EmailLimitsNew controller + guard for `$scope.emails` | -| `mailServer/static/mailServer/emailLimitsController.js` | Standalone controller + PNotify check fix | - -## Option A: Deploy script (recommended) - -**Run from anywhere** (use the full path to the script so the shell can find it): - -```bash -sudo bash /home/cyberpanel-repo/deploy-email-limits-fix.sh -``` - -Or from repo root: - -```bash -cd /home/cyberpanel-repo && sudo bash deploy-email-limits-fix.sh -``` - -- Script auto-detects repo at `/home/cyberpanel-repo` if run from another directory. -- Default CyberPanel path: `/usr/local/CyberCP`. -- Override: `sudo bash /home/cyberpanel-repo/deploy-email-limits-fix.sh /path/to/repo /usr/local/CyberCP`. -- Skip restart: `sudo RESTART_LSCPD=0 bash /home/cyberpanel-repo/deploy-email-limits-fix.sh`. - -## Option B: Manual copy + restart - -On the server, from the repo root (e.g. `/home/cyberpanel-repo`): - -```bash -CP_DIR=/usr/local/CyberCP - -cp -f mailServer/mailserverManager.py "$CP_DIR/mailServer/" -cp -f mailServer/templates/mailServer/EmailLimits.html "$CP_DIR/mailServer/templates/mailServer/" -cp -f mailServer/static/mailServer/mailServer.js "$CP_DIR/mailServer/static/mailServer/" -cp -f mailServer/static/mailServer/emailLimitsController.js "$CP_DIR/mailServer/static/mailServer/" - -sudo systemctl restart lscpd -``` - -## After deploy - -1. Hard refresh the Email Limits page: **Ctrl+Shift+R** (or Cmd+Shift+R). -2. Open **Email Limits**, choose a **website**, then check that **email account** dropdown fills and **Configure Email Limits** appears and works. - -## If it still fails - -- Confirm the four files above are present under `$CP_DIR` and were updated (check timestamps). -- Check panel/Python logs and browser console for `[$controller:ctrlreg]` or JS errors. -- Ensure `lscpd` (or the process serving the panel) was restarted after copying. diff --git a/to-do/EMAIL-LIMITS-LIVE-SERVER-CHECKLIST.md b/to-do/EMAIL-LIMITS-LIVE-SERVER-CHECKLIST.md deleted file mode 100644 index 70fc18670..000000000 --- a/to-do/EMAIL-LIMITS-LIVE-SERVER-CHECKLIST.md +++ /dev/null @@ -1,108 +0,0 @@ -# Email Limits – Live Server Checklist (vs upstream v2.4.4) - -## Upstream v2.4.4 behaviour - -In [usmannasir/cyberpanel at v2.4.4](https://github.com/usmannasir/cyberpanel/tree/v2.4.4): - -- **Template**: `mailServer/templates/mailServer/EmailLimits.html` exists and uses `ng-controller="EmailLimitsNew"` and `{$ … $}` bindings. -- **Routes**: `mailServer/urls.py` has `EmailLimits` and `SaveEmailLimitsNew`. -- **Controller**: The **`EmailLimitsNew` controller is not present** in `static/mailServer/mailServer.js`. Upstream `mailServer.js` ends at “List Emails” and has no `EmailLimitsNew` block. - -So on a stock v2.4.4 install, the Email Limits page will show raw `{$ selectedEmail $}` and “Could not connect to server” because the Angular controller is never registered. - ---- - -## How it is loaded in v2.4.4 - -1. **Base template** (`baseTemplate/templates/baseTemplate/index.html`) loads one script bundle: - - `{% static 'mailServer/mailServer.js' %}?v={{ CP_VERSION }}` - (in the “Additional Scripts” block at the bottom of the body.) - -2. **Email Limits template** only provides content; it does **not** load any extra script in upstream. It expects `EmailLimitsNew` to come from `mailServer.js`, but that controller is missing in v2.4.4. - -3. **Backend**: `mailServer/views.py` → `EmailLimits`, `SaveEmailLimitsNew`; `mailServer/mailserverManager.py` → `EmailLimits()`, `SaveEmailLimitsNew()`. - ---- - -## Files that must be on the live server - -Use the paths below relative to the CyberPanel app root (e.g. `/usr/local/CyberCP/` or your repo root). Django static files may be served from `STATIC_ROOT` after `collectstatic`; templates and Python files must be in the app directories. - -### 1. Python / URLs / views (same as upstream + your tweaks) - -| Path | Purpose | -|------|--------| -| `mailServer/urls.py` | Must include `EmailLimits` and `SaveEmailLimitsNew` routes. | -| `mailServer/views.py` | Must define `EmailLimits` and `SaveEmailLimitsNew` and call manager. | -| `mailServer/mailserverManager.py` | Must implement `EmailLimits()` and `SaveEmailLimitsNew()` and render `mailServer/EmailLimits.html` with `websiteList` and `status`. | - -### 2. Template (must load the controller script) - -| Path | Purpose | -|------|--------| -| `mailServer/templates/mailServer/EmailLimits.html` | Must extend `baseTemplate/index.html`, contain `ng-controller="EmailLimitsNew"`, and **include the script tag** that loads `emailLimitsController.js` at the top of `{% block content %}`. | - -### 3. Base template (unchanged from upstream for Email Limits) - -| Path | Purpose | -|------|--------| -| `baseTemplate/templates/baseTemplate/index.html` | Must load `{% static 'mailServer/mailServer.js' %}` in the same script block as other app JS (no `load_email_limits_controller` needed). | - -### 4. Static files (at least one of the two options) - -**Option A – Use main bundle (repo’s `mailServer.js` with controller)** - -| Path | Purpose | -|------|--------| -| `static/mailServer/mailServer.js` | Must define `app` (e.g. `window.app` or `angular.module('CyberCP')`) at the top and register `app.controller('EmailLimitsNew', ...)`. | -| `mailServer/static/mailServer/mailServer.js` | Same as above if you use app static dirs. | - -**Option B – Use standalone controller (recommended so it works even if `mailServer.js` is old)** - -| Path | Purpose | -|------|--------| -| `static/mailServer/emailLimitsController.js` | Standalone script that registers `EmailLimitsNew` on the CyberCP module. | -| `mailServer/static/mailServer/emailLimitsController.js` | Same file under the app’s `static` dir. | - -The Email Limits template in this repo loads `emailLimitsController.js` at the top of the content block, so the controller is registered on the Email Limits page even if the live server still has an older `mailServer.js` without `EmailLimitsNew`. - ---- - -## Quick verification on the live server - -Run from the CyberPanel app root (e.g. `/usr/local/CyberCP/`): - -```bash -# 1. Template must contain the controller script and ng-controller -grep -l "emailLimitsController.js" mailServer/templates/mailServer/EmailLimits.html && \ -grep -l "EmailLimitsNew" mailServer/templates/mailServer/EmailLimits.html && \ -echo "Template OK" || echo "Template MISSING or WRONG" - -# 2. Standalone controller script must exist (at least one location) -([ -f static/mailServer/emailLimitsController.js ] || [ -f mailServer/static/mailServer/emailLimitsController.js ]) && \ -echo "emailLimitsController.js OK" || echo "emailLimitsController.js MISSING" - -# 3. mailServer.js (if you rely on it for Email Limits) must define EmailLimitsNew -grep -q "EmailLimitsNew" static/mailServer/mailServer.js 2>/dev/null || grep -q "EmailLimitsNew" mailServer/static/mailServer/mailServer.js 2>/dev/null && \ -echo "mailServer.js has EmailLimitsNew" || echo "mailServer.js has NO EmailLimitsNew (use emailLimitsController.js)" - -# 4. Routes -grep -q "EmailLimits" mailServer/urls.py && echo "URLs OK" || echo "URLs MISSING" -``` - -After deploying, run: - -```bash -python3 manage.py collectstatic --noinput -# Restart your app server (e.g. LiteSpeed / Gunicorn) -``` - -Then hard-refresh the Email Limits page (Ctrl+Shift+R). - ---- - -## Summary - -- **Upstream v2.4.4**: Email Limits template and routes exist; **controller is missing** from `mailServer.js`, so the page is broken by default. -- **This repo**: Adds `EmailLimitsNew` in `mailServer.js` and a standalone `emailLimitsController.js`, and the Email Limits template loads `emailLimitsController.js` so the page works even with an old `mailServer.js`. -- **Live server**: Ensure the template, URLs, views, manager, base template, and either the updated `mailServer.js` or `emailLimitsController.js` (or both) are present as in this checklist, then run `collectstatic` and restart the app. diff --git a/to-do/FIREWALL-BANNED-IPS-DATABASE-AND-BAN-PERMANENTLY.md b/to-do/FIREWALL-BANNED-IPS-DATABASE-AND-BAN-PERMANENTLY.md deleted file mode 100644 index 47337a507..000000000 --- a/to-do/FIREWALL-BANNED-IPS-DATABASE-AND-BAN-PERMANENTLY.md +++ /dev/null @@ -1,83 +0,0 @@ -# Firewall Banned IPs: Database Storage and "Ban IP Permanently" Fix - -## Summary - -- **Issue:** "Ban IP Permanently" from SSH Security Analysis did not show up in Firewall Management > Banned IPs. -- **Cause:** `addBannedIP` only wrote to a JSON file, while `getBannedIPs` tried the database first. When the `BannedIP` model was available, the list was read from the database (empty), so dashboard bans (stored only in JSON) did not appear. -- **Fix:** Primary storage is now the **database** (`BannedIP` model). `addBannedIP` saves to the database first; JSON file is used only when the model is unavailable (fallback). **JSON is used only for export/import**, not for primary storage. - -## Storage Policy - -| Use case | Storage | -|-----------------------|----------------| -| Adding a ban | Database first, JSON fallback only if DB unavailable | -| Listing banned IPs | Database first, JSON fallback only if DB unavailable | -| Export Banned IPs | Output in **JSON format** (from DB or JSON store) | -| Import Banned IPs | Input in **JSON format**; writes to DB or JSON store | - -Bans are **not** stored in a JSON file for normal operation when the database is available. - -## Code Changes - -1. **`firewall/firewallManager.py` – `addBannedIP`** - - Tries to save to the `BannedIP` database model first. - - Only uses the JSON file when the model cannot be imported (e.g. migrations not run). - - Applies the firewall rule in both paths; rolls back the DB or JSON record if the firewall command fails. - -2. **`firewall/migrations/0001_initial.py`** (new) - - Creates the `firewall_bannedips` table and indexes for the `BannedIP` model. - - Ensures the table exists after deploy. - -## Deployment (Server) - -1. **Copy updated code** from this repo to the panel (e.g. `/usr/local/CyberCP/`), including: - - `firewall/firewallManager.py` - - `firewall/models.py` (must define `BannedIP`) - - `firewall/migrations/0001_initial.py` - -2. **Run migrations** so the banned IPs table exists: - ```bash - cd /usr/local/CyberCP - sudo -u cyberpanel /usr/local/CyberCP/bin/python manage.py migrate firewall - ``` - If you see "table already exists" for `firewall_bannedips`, the table was created earlier; ensure `firewall.models` defines `BannedIP` and is in `INSTALLED_APPS`. - -3. **Restart the panel** (e.g. lscpd / gunicorn) so the new code is loaded. - -4. **Optional – one-time sync:** If you had bans only in the JSON file and want them in the DB, use **Firewall > Banned IPs > Export Banned IPs**, then **Import Banned IPs** with that file after migrations are applied (so imports go to the database). - -## Verification - -- Click "Ban IP Permanently" on an IP in **Dashboard > SSH Security Analysis**. -- Open **Firewall > Banned IPs** and confirm that IP appears in the list (from the database). -- Export/Import should still use JSON format for the file; listing and adding use the database when available. - -## Run and test in the browser - -**Done for you (on this machine):** - -1. **Deployed** `firewall/firewallManager.py` to `/usr/local/CyberCP/firewall/`. -2. **Restarted** the panel backend: `systemctl restart lscpd` (lscpd is **active**). -3. Panel is listening on **port 2087** (e.g. `https://YOUR_SERVER_IP:2087`). - -**Manual browser test:** - -1. Open the CyberPanel URL (e.g. `https://207.180.193.210:2087` or `https://localhost:2087`). Accept the certificate warning if needed. -2. Log in as admin. -3. **Dashboard:** Scroll to **SSH Security Analysis**. If there is an alert (e.g. "Root Login Attempts Detected"), click **Ban IP Permanently** on one of the IPs (e.g. the "Top IP"). -4. Confirm the success message (e.g. "IP address … has been permanently banned … You can manage it in the Firewall > Banned IPs section"). -5. Go to **Firewall** (left menu) → **Banned IPs** tab. -6. **Verify:** The IP you just banned appears in the table (IP ADDRESS, REASON e.g. "Brute force attack detected from SSH Security Analysis", EXPIRES "Never", STATUS ACTIVE). -7. Optionally: **Export Banned IPs** → download JSON; **Import Banned IPs** → upload that JSON to confirm export/import still use JSON format. - -**Quick API check (optional, from server):** - -```bash -# After logging in in the browser, get session cookie or use a session ID, then: -curl -k -s -X POST 'https://127.0.0.1:2087/firewall/addBannedIP' \ - -H 'Content-Type: application/json' \ - -H 'Cookie: sessionid=YOUR_SESSION_ID' \ - -H 'X-CSRFToken: YOUR_CSRF_TOKEN' \ - -d '{"ip":"203.0.113.99","reason":"Test ban","duration":"permanent"}' -# Then open Firewall > Banned IPs and confirm 203.0.113.99 appears. -``` diff --git a/to-do/FTP-QUOTA-BROWSER-TEST-CHECKLIST.md b/to-do/FTP-QUOTA-BROWSER-TEST-CHECKLIST.md deleted file mode 100644 index f9250e0fd..000000000 --- a/to-do/FTP-QUOTA-BROWSER-TEST-CHECKLIST.md +++ /dev/null @@ -1,21 +0,0 @@ -# FTP Quota Management – Browser Test Checklist - -Use after deploying latest code. Open: `/ftp/quotaManagement` - -## 1. Page load – status -- **Pure-FTPd stopped:** Yellow warning "Pure-FTPd is not running. Please enable Pure-FTPd first (Server Status → Services)..." and Enable button disabled/hidden. -- **Pure-FTPd running, quota on:** Green "FTP Quota system is already enabled"; button disabled. -- **Pure-FTPd running, quota off:** Blue info and enabled "Enable FTP Quota System" button. - -## 2. Click Enable -- If FTP was running: success message and UI switches to "already enabled". No "Pure-FTPd did not start" error. -- If FTP was stopped: API returns "Pure-FTPd is not running. Please enable Pure-FTPd first...". - -## 3. Table -- Quotas table loads; Refresh works. - -## 4. One-time fix on server (if needed) -```bash -sudo sed -i 's/^Quota.*/Quota 100000:100000/' /etc/pure-ftpd/pure-ftpd.conf -sudo systemctl start pure-ftpd -``` diff --git a/to-do/FTP-QUOTAS-TABLE-FIX.md b/to-do/FTP-QUOTAS-TABLE-FIX.md deleted file mode 100644 index 99f3160ea..000000000 --- a/to-do/FTP-QUOTAS-TABLE-FIX.md +++ /dev/null @@ -1,25 +0,0 @@ -# FTP Quotas Table Fix - -## Problem -- **URL:** https://207.180.193.210:2087/ftp/quotaManagement -- **Error:** `(1146, "Table 'cyberpanel.ftp_quotas' doesn't exist")` - -The `FTPQuota` model in `websiteFunctions/models.py` uses `db_table = 'ftp_quotas'`, but the table had never been created in the database. - -## Solution -1. **SQL:** `sql/create_ftp_quotas.sql` – `CREATE TABLE IF NOT EXISTS ftp_quotas` with columns and FKs to `loginSystem_administrator` and `websiteFunctions_websites`. -2. **Deploy script:** `deploy-ftp-quotas-table.sh` – Copies the SQL to `/usr/local/CyberCP/sql/` and runs it using Django’s DB connection (no password on command line). - -## Deploy (already run) -```bash -sudo bash /home/cyberpanel-repo/deploy-ftp-quotas-table.sh -``` - -## Manual run (if needed) -From repo root: -```bash -sudo bash deploy-ftp-quotas-table.sh [REPO_DIR] [CP_DIR] -``` -Default `CP_DIR` is `/usr/local/CyberCP`. - -After deployment, reload `/ftp/quotaManagement` in the browser. diff --git a/to-do/INSTALL-UPGRADE-DOWNGRADE-COMMANDS.md b/to-do/INSTALL-UPGRADE-DOWNGRADE-COMMANDS.md deleted file mode 100644 index ccec277aa..000000000 --- a/to-do/INSTALL-UPGRADE-DOWNGRADE-COMMANDS.md +++ /dev/null @@ -1,201 +0,0 @@ -# CyberPanel Install, Upgrade, and Downgrade Commands - -Reference for all standard and branch-specific install/upgrade/downgrade commands (master3395 fork and upstream). - ---- - -## Installation logs (v2.4.4 / v2.5.5-dev) - -When you run the installer (cyberpanel.sh or install.py), logs are written to: - -| Log | Location | Description | -|-----|----------|--------------| -| Installer script | `/var/log/CyberPanel/install.log` | Messages from cyberpanel.sh (print_status) | -| Installer output | `/var/log/CyberPanel/install_output.log` | Full stdout/stderr of the Python installer (tee) | -| Python installer | `/var/log/installLogs.txt` | Detailed log from install.py (installLog module) | - -To inspect after a failed install: - -```bash -tail -100 /var/log/CyberPanel/install_output.log -tail -100 /var/log/installLogs.txt -``` - -**If you see ERR_CONNECTION_TIMED_OUT** when opening the panel URL: the install may have failed before LiteSpeed was set up, or ports are blocked. Ensure ports **8090** (panel) and **7080** (LSWS admin) are open in the server firewall and in your cloud security group (e.g. AWS). Re-run the installer after pulling the latest fixes so the install can complete. - ---- - -## Fresh install - -### One-liner (official / upstream) - -```bash -sh <(curl https://cyberpanel.net/install.sh) -``` - -### One-liner with sudo (if not root) - -```bash -curl -sO https://cyberpanel.net/install.sh && sudo bash install.sh -# or -curl -sL https://cyberpanel.net/install.sh | sudo bash -s -- -``` - -### Install from master3395 fork (this repo) - -**Stable:** - -```bash -curl -sL https://raw.githubusercontent.com/master3395/cyberpanel/stable/cyberpanel.sh | sudo bash -s -- -``` - -**Development (v2.5.5-dev):** - -```bash -curl -sL https://raw.githubusercontent.com/master3395/cyberpanel/v2.5.5-dev/cyberpanel.sh | sudo bash -s -- -b v2.5.5-dev -``` - -### Install with branch/version options - -```bash -# Download script first (recommended so -b/-v work reliably) -curl -sL -o cyberpanel.sh https://raw.githubusercontent.com/master3395/cyberpanel/v2.5.5-dev/cyberpanel.sh -chmod +x cyberpanel.sh -sudo bash cyberpanel.sh [OPTIONS] -``` - -**Options:** - -| Option | Example | Description | -|--------|---------|-------------| -| `-b BRANCH` / `--branch BRANCH` | `-b v2.5.5-dev` | Install from branch or tag | -| `-v VER` / `--version VER` | `-v 2.5.5-dev` | Version (script adds `v` prefix as needed) | -| `--mariadb-version VER` | `--mariadb-version 10.11` | MariaDB: `10.11`, `11.8`, or `12.1` | -| `--auto` | `--auto` | Non-interactive (still asks MariaDB unless `--mariadb-version` is set) | -| `--debug` | `--debug` | Debug mode | - -**Examples:** - -```bash -sudo bash cyberpanel.sh # Interactive -sudo bash cyberpanel.sh -b v2.5.5-dev # Development branch -sudo bash cyberpanel.sh -v 2.5.5-dev # Same as above (v prefix added) -sudo bash cyberpanel.sh -v 2.4.4 # Install 2.4.4 -sudo bash cyberpanel.sh -b main # From main branch -sudo bash cyberpanel.sh -b a1b2c3d4 # From specific commit hash -sudo bash cyberpanel.sh --mariadb-version 10.11 # MariaDB 10.11 -sudo bash cyberpanel.sh --mariadb-version 12.1 # MariaDB 12.1 -sudo bash cyberpanel.sh --auto --mariadb-version 11.8 # Fully non-interactive, MariaDB 11.8 -sudo bash cyberpanel.sh --debug # Debug -``` - ---- - -## Upgrade (existing CyberPanel) - -### One-liner upgrade to latest stable - -```bash -bash <(curl -sL https://raw.githubusercontent.com/usmannasir/cyberpanel/stable/cyberpanel_upgrade.sh) -``` - -### Upgrade to a specific branch/version (upstream) - -```bash -bash <(curl -sL https://raw.githubusercontent.com/usmannasir/cyberpanel/stable/cyberpanel_upgrade.sh) -b v2.5.5-dev -bash <(curl -sL https://raw.githubusercontent.com/usmannasir/cyberpanel/stable/cyberpanel_upgrade.sh) -b 2.4.4 -``` - -### Upgrade using master3395 fork - -```bash -sudo bash <(curl -sL https://raw.githubusercontent.com/master3395/cyberpanel/stable/cyberpanel_upgrade.sh) -b v2.5.5-dev -``` - -Or download then run: - -```bash -curl -sL -o cyberpanel_upgrade.sh https://raw.githubusercontent.com/master3395/cyberpanel/v2.5.5-dev/cyberpanel_upgrade.sh -chmod +x cyberpanel_upgrade.sh -sudo bash cyberpanel_upgrade.sh -b v2.5.5-dev -``` - -**Upgrade options:** - -| Option | Example | Description | -|--------|---------|-------------| -| `-b BRANCH` / `--branch BRANCH` | `-b v2.5.5-dev` | Upgrade to this branch/tag | -| `--no-system-update` | (optional) | Skip full `yum/dnf update -y` (faster if system is already updated) | - -**Examples:** - -```bash -sudo bash cyberpanel_upgrade.sh -b v2.5.5-dev -sudo bash cyberpanel_upgrade.sh -b 2.4.4 -sudo bash cyberpanel_upgrade.sh -b stable -sudo bash cyberpanel_upgrade.sh -b v2.5.5-dev --no-system-update -``` - ---- - -## Downgrade - -Downgrade is done by running the **upgrade** script with the **older** branch/version. - -### Downgrade to 2.4.4 (or another older version) - -```bash -sudo bash <(curl -sL https://raw.githubusercontent.com/usmannasir/cyberpanel/stable/cyberpanel_upgrade.sh) -b 2.4.4 -``` - -Or with master3395 fork: - -```bash -curl -sL -o cyberpanel_upgrade.sh https://raw.githubusercontent.com/master3395/cyberpanel/stable/cyberpanel_upgrade.sh -chmod +x cyberpanel_upgrade.sh -sudo bash cyberpanel_upgrade.sh -b 2.4.4 -``` - -### Downgrade from v2.5.5-dev to stable - -```bash -sudo bash cyberpanel_upgrade.sh -b stable -``` - ---- - -## Pre-upgrade (download upgrade script only) - -From the interactive menu: **Option 5 – Pre-Upgrade**. - -Or manually: - -```bash -# Download latest upgrade script to /usr/local/ -curl -sL -o /usr/local/cyberpanel_upgrade.sh https://raw.githubusercontent.com/usmannasir/cyberpanel/stable/cyberpanel_upgrade.sh -chmod 700 /usr/local/cyberpanel_upgrade.sh - -# Run when ready -sudo /usr/local/cyberpanel_upgrade.sh -b v2.5.5-dev -``` - ---- - -## Quick reference - -| Action | Command | -|--------|---------| -| **Install (official)** | `sh <(curl https://cyberpanel.net/install.sh)` | -| **Install stable (master3395)** | `curl -sL https://raw.githubusercontent.com/master3395/cyberpanel/stable/cyberpanel.sh \| sudo bash -s --` | -| **Install v2.5.5-dev** | `curl -sL https://raw.githubusercontent.com/master3395/cyberpanel/v2.5.5-dev/cyberpanel.sh \| sudo bash -s -- -b v2.5.5-dev` | -| **Upgrade to v2.5.5-dev** | `sudo bash <(curl -sL https://raw.githubusercontent.com/master3395/cyberpanel/stable/cyberpanel_upgrade.sh) -b v2.5.5-dev` | -| **Upgrade to 2.4.4** | `sudo bash <(curl -sL .../cyberpanel_upgrade.sh) -b 2.4.4` | -| **Downgrade to 2.4.4** | Same as upgrade: `... cyberpanel_upgrade.sh -b 2.4.4` | - ---- - -## Notes - -- Run as **root** or with **sudo**; if using `curl | sudo bash`, use `bash -s --` and put branch/options after `--` so they are passed to the script. -- MariaDB version can be set at install with `--mariadb-version 10.11`, `11.8`, or `12.1`. -- Upgrade script branch: `-b v2.5.5-dev`, `-b 2.4.4`, `-b stable`, or `-b `. diff --git a/to-do/MARIADB-11.8-LTS-UPGRADE.md b/to-do/MARIADB-11.8-LTS-UPGRADE.md deleted file mode 100644 index 5946fb548..000000000 --- a/to-do/MARIADB-11.8-LTS-UPGRADE.md +++ /dev/null @@ -1,34 +0,0 @@ -# MariaDB 11.8 LTS and 12.1 - -## Summary - -CyberPanel install and upgrade support **MariaDB 11.8 LTS** (default) or **12.1**. User can choose at install/upgrade time; **downgrade is supported** (e.g. 12.1 → 11.8 by re-running upgrader with `--mariadb-version 11.8`). - -- **New installs:** `--mariadb-version 11.8|12.1` (default 11.8); `install.py` and `venvsetup.sh` pass it through. -- **Upgrades:** `cyberpanel_upgrade.sh --mariadb-version 11.8|12.1` or interactive prompt; writes `/etc/cyberpanel/mariadb_version` for `upgrade.py`. -- **Downgrade:** Run upgrader again with the desired version (e.g. `--mariadb-version 11.8` to switch from 12.1 to 11.8). Repo and packages will target the chosen version. -- **cyberpanel_upgrade.sh:** Uses `MARIADB_VER` (default 11.8) in `MariaDB.repo` baseurl and writes `/etc/cyberpanel/mariadb_version`. -- **plogical/upgrade.py:** `fix_almalinux9_mariadb()` reads `/etc/cyberpanel/mariadb_version` (default 11.8) and runs `mariadb_repo_setup` with that version. -- **UI (Database upgrade):** `databases/databaseManager.py` offers 10.6, 10.11, **11.8** for manual upgrade. -- **mysqlUtilities.UpgradeMariaDB:** Repo baseurl uses `versionToInstall` (e.g. 11.8). - -## Testing - -From repo root: - -- Shell (upgrader argument parsing and repo URL logic): - `./test/upgrader_mariadb_version_test.sh` -- Python (mariadb_version file read and downgrade): - `python3 test/test_upgrade_mariadb_version.py` - -Both 11.8 and 12.1 paths are tested; downgrade (12.1 → 11.8) is explicitly verified. - -## References - -- `cyberpanel_upgrade.sh`: MARIADB_VER, --mariadb-version, /etc/cyberpanel/mariadb_version -- `plogical/upgrade.py`: fix_almalinux9_mariadb() reads mariadb_version file -- `install/install.py`: --mariadb-version, preFlightsChecks.mariadb_version -- `install/venvsetup.sh`: MARIADB_VER prompt, --mariadb-version to install.py -- `install/universal_os_fixes.py`: setup_mariadb_repository() 11.8 -- `databases/databaseManager.py`: mysqlversions 10.6, 10.11, 11.8 -- `plogical/mysqlUtilities.py`: UpgradeMariaDB() baseurl for RHEL diff --git a/to-do/MARIADB_INSTALLATION_FIXES.md b/to-do/MARIADB_INSTALLATION_FIXES.md deleted file mode 100644 index f133868d3..000000000 --- a/to-do/MARIADB_INSTALLATION_FIXES.md +++ /dev/null @@ -1,88 +0,0 @@ -# MariaDB Installation Fixes - -## Issues Fixed - -### 1. MariaDB-server-compat Package Conflict -**Problem**: `MariaDB-server-compat-12.1.2-1.el9.noarch` was conflicting with MariaDB 10.11 installation, causing transaction test errors. - -**Solution**: -- Enhanced compat package removal with multiple aggressive removal attempts -- Added `--allowerasing` flag to dnf remove commands -- Added dnf exclude configuration to prevent compat package reinstallation -- Verification step to ensure all compat packages are removed before installation - -**Files Modified**: -- `cyberpanel-repo/plogical/upgrade.py` - `fix_almalinux9_mariadb()` function -- `cyberpanel-repo/install/install.py` - `installMySQL()` function - -### 2. MySQL Command Not Found Error -**Problem**: After MariaDB installation failed, the `changeMYSQLRootPassword()` function tried to use the `mysql` command which didn't exist, causing `FileNotFoundError`. - -**Solution**: -- Added verification that MariaDB binaries exist before attempting password change -- Added check for mysql/mariadb command availability -- Added MariaDB service status verification before password change -- Added wait time for MariaDB to be ready after service start - -**Files Modified**: -- `cyberpanel-repo/install/install.py` - `changeMYSQLRootPassword()` function -- `cyberpanel-repo/install/install.py` - `installMySQL()` function - -### 3. MariaDB Installation Verification -**Problem**: Installation was proceeding even when MariaDB wasn't actually installed successfully. - -**Solution**: -- Added binary existence check after installation -- Added service status verification -- Added proper error handling and return values -- Installation now fails gracefully if MariaDB wasn't installed - -**Files Modified**: -- `cyberpanel-repo/plogical/upgrade.py` - `fix_almalinux9_mariadb()` function -- `cyberpanel-repo/install/install.py` - `installMySQL()` function - -## Changes Made - -### upgrade.py -1. **Enhanced compat package removal**: - - Multiple removal attempts (dnf remove, rpm -e, individual package removal) - - Added `--allowerasing` flag - - Added dnf exclude configuration - - Verification step - -2. **Improved MariaDB installation**: - - Added `--exclude='MariaDB-server-compat*'` to dnf install command - - Added fallback with `--allowerasing` if conflicts occur - - Added binary existence verification after installation - - Proper error handling and return values - -### install.py -1. **Enhanced compat package removal** (same as upgrade.py) - -2. **Improved installation verification**: - - Check for MariaDB binaries after installation - - Verify service is running before password change - - Added wait time for service to be ready - - Proper error handling - -3. **Improved password change function**: - - Verify mysql/mariadb command exists before attempting password change - - Better error messages - - Graceful failure handling - -## Testing Recommendations - -1. Test on clean AlmaLinux 9 system -2. Test with existing MariaDB-server-compat package installed -3. Test with MariaDB 10.x already installed -4. Test with MariaDB 12.x already installed -5. Verify MariaDB service starts correctly -6. Verify mysql/mariadb commands are available -7. Verify password change succeeds - -## Notes - -- The fixes maintain backward compatibility -- All changes include proper error handling -- Installation now fails gracefully with clear error messages -- Compat package removal is more aggressive to handle edge cases diff --git a/to-do/OLD-REPO-CHECKLIST-BEFORE-REMOVAL.md b/to-do/OLD-REPO-CHECKLIST-BEFORE-REMOVAL.md deleted file mode 100644 index 906cf1be3..000000000 --- a/to-do/OLD-REPO-CHECKLIST-BEFORE-REMOVAL.md +++ /dev/null @@ -1,132 +0,0 @@ -# What Was in the Old cyberpanel-fix Repo – Pre-Removal Checklist - -Before removing `/home/cyberpanel-fix-backup-20260202`, verify the merged repo has everything you need. - ---- - -## 1. Files ONLY in cyberpanel-repo (not in old fix) ✅ - -These are in the merged repo and were not in the old fix: - -| File | Purpose | -|------|---------| -| `commit_and_push.sh`, `commit_changes.py`, `push_fix.py`, `push_fix.sh` | Dev/utility scripts | -| `fix_todo_git.py`, `remove_todo.py`, `remove_todo_from_git.sh` | Git helpers | -| `olves issue -1654: Hostname SSL setup...` | Patch file (typo in filename) | -| `pluginHolder/patreon_verifier.py.bak`, `plugin_access.py.bak` | Backups | -| `pluginHolder/templates/pluginHolder/plugins.html.backup` | Template backup | -| `static/userManagment/modifyUser.html` | UI change | -| `to-do/PLUGIN-DEFAULT-REMOVAL-2026-02-01.md` | Notes | -| `to-do/REPO-MERGE-2026-02-02.md` | Merge notes | - -**Action:** None. These are already in the merged repo. - ---- - -## 2. Files COPIED from old fix into repo ✅ - -These were only in the old fix and were copied into repo during the merge: - -| File | Purpose | -|------|---------| -| `cyberpanel_clean.sh` | Clean install script | -| `cyberpanel_complete.sh` | Complete install script | -| `cyberpanel_simple.sh` | Simple install script | -| `cyberpanel_standalone.sh` | Standalone install script | -| `fix_installation_issues.sh` | Installation fixes | -| `install_phpmyadmin.sh` | phpMyAdmin installer | -| ~~`simple_install.sh`~~ | Removed – use official install.sh one-liner | -| `INSTALLER_SUMMARY.md` | Installer docs | -| `UNIVERSAL_OS_COMPATIBILITY.md` | OS compatibility docs | -| `to-do/MARIADB_INSTALLATION_FIXES.md` | MariaDB fixes | - -**Action:** Confirm these exist in `/home/cyberpanel-repo/`. - ---- - -## 3. Files that DIFFER – repo is the intended version - -The merged repo keeps the **cyberpanel-repo** versions. Old fix had older or different logic. - -### CyberCP/settings.py -- **Repo:** `emailMarketing` is commented out (install via Plugin Store) -- **Old fix:** `emailMarketing` was in `INSTALLED_APPS` - -**Check:** Plugin Store for emailMarketing works; no need for it in core install. - -### CyberCP/urls.py -- **Repo:** `path('emailMarketing/', ...)` is commented out -- **Old fix:** `path('emailMarketing/', ...)` was active - -**Check:** Same as above; emailMarketing via Plugin Store. - -### plogical/mailUtilities.py -- **Repo:** DNS fallback logic – falls back to **local DNS** when external API fails -- **Old fix:** Returns empty `[]` when external API fails; no local fallback - -**Check:** Hostname SSL / rDNS works when cyberpanel.net API is down or unreachable. - -### emailMarketing/meta.xml -- **Repo:** version `1.0.1`, category `Email` -- **Old fix:** version `1.0.0` - -### examplePlugin/meta.xml -- **Repo:** version `1.0.1`, category `Utility` -- **Old fix:** version `1.0.0` - -**Check:** Plugin Store shows correct versions and categories. - ---- - -## 4. PluginHolder / Plugin Store (in repo) - -The merged repo has: - -- Collapsible help sections -- Freshness badges (NEW/Stable/Unstable/STALE) -- Activate All / Deactivate All -- Updated categories and premium docs -- Version 2.1.0 in the help footer - -**Check:** `/plugins/help/` and `/plugins/installed` behave as expected. - ---- - -## 5. Quick verification commands - -```bash -# Copied files exist -ls -la /home/cyberpanel-repo/cyberpanel_clean.sh \ - /home/cyberpanel-repo/fix_installation_issues.sh \ - /home/cyberpanel-repo/install_phpmyadmin.sh - -# Symlink works -ls -la /home/cyberpanel-fix -# Should show: cyberpanel-fix -> cyberpanel-repo - -# Live deployment -ls -la /usr/local/CyberCP/pluginHolder/templates/pluginHolder/help.html -# Should have collapsible sections and version 2.1.0 -``` - ---- - -## 6. Safe to remove when - -- [ ] Plugin Store loads and filters work -- [ ] Plugin Development Guide (help) shows collapsible sections and 2.1.0 -- [ ] Hostname SSL / rDNS works (or you accept no local DNS fallback) -- [ ] emailMarketing is installed via Plugin Store, not core (if used) -- [ ] Install scripts (`cyberpanel_clean.sh`, etc.) are present and used as needed - ---- - -## Remove backup - -```bash -rm -rf /home/cyberpanel-fix-backup-20260202 -``` - ---- - -**Created:** 2026-02-02 diff --git a/to-do/OPENLITESPEED-VERSION-INSTALL-UPGRADE.md b/to-do/OPENLITESPEED-VERSION-INSTALL-UPGRADE.md deleted file mode 100644 index 5846b78d1..000000000 --- a/to-do/OPENLITESPEED-VERSION-INSTALL-UPGRADE.md +++ /dev/null @@ -1,62 +0,0 @@ -# OpenLiteSpeed and LSWS Version Used by CyberPanel (Install & Upgrade) - -**Updated:** OLS target 1.8.5+ (LiteSpeed repo); LSWS fallback 6.3.4. - -## Summary - -- **Install:** OpenLiteSpeed is installed via the **OS package manager** (no fixed version in code), then optionally replaced by CyberPanel’s **custom static binary** (based on **OpenLiteSpeed 1.8.5 – v2.0.5**). -- **Upgrade:** The CyberPanel **upgrade script does not change the OpenLiteSpeed version**. It only updates **LiteSpeed Enterprise** version references. During upgrade, **custom OLS binaries** (same 1.8.5-based build) are (re)installed if OLS is present. - ---- - -## Install - -1. **Package install** - - `install/install.py` → `installLiteSpeed(ent=0)` → `install_package('openlitespeed')`. - - So the **base** install is whatever **openlitespeed** version the distro provides (yum/dnf or apt). There is **no fixed OLS version** in the installer for this step. - -2. **Custom binary (optional)** - - Right after that, `installCustomOLSBinaries()` runs (in both `install/install.py` and `plogical/upgrade.py`). - - It downloads a **static binary** from `https://cyberpanel.net/` (e.g. `openlitespeed-phpconfig-x86_64-rhel8-static`) and replaces `/usr/local/lsws/bin/openlitespeed`. - - Comments in code state this is **OpenLiteSpeed 1.8.5** (upgrade.py) or **1.8.5 – v2.0.5** (install.py). The download URLs do not include a version; the binary is a fixed build hosted by CyberPanel. - -So on **install**, you get either: -- **Distro OLS** (version = whatever the OS repo has), or -- **CyberPanel custom OLS** (based on **1.8.5 / v2.0.5** static build) if the custom binary install succeeds. - ---- - -## Upgrade - -1. **cyberpanel_upgrade.sh** - - Fetches **LiteSpeed Enterprise** latest version from: - - `LSWS_Latest_URL="https://cyberpanel.sh/update.litespeedtech.com/ws/latest.php"` - - Parses `LSWS_Stable_Version` from the `LSWS_STABLE` line. - - Uses `LSWS_Stable_Version` only to **sed**-replace hardcoded Enterprise version strings (e.g. `lsws-5.3.8`, `lsws-5.4.2`, `lsws-5.3.5`) in `/usr/local/CyberCP/serverStatus/serverStatusUtil.py`. - - So the **upgrade script does not install or upgrade OpenLiteSpeed**; it only updates **Enterprise** version references. - -2. **plogical/upgrade.py** - - During upgrade, if OpenLiteSpeed is present (`/usr/local/lsws/bin/openlitespeed` exists), it runs: - - `Upgrade.installCustomOLSBinaries()` - - That (re)installs the **same custom static OLS binary** (1.8.5-based, from cyberpanel.net). So **upgrade** does not pull a “new” OLS version from upstream; it only refreshes CyberPanel’s custom binary if OLS is in use. - ---- - -## References (in repo) - -| What | Where | -|------|--------| -| OLS package install (no version) | `install/install.py` → `install_package('openlitespeed')` in `installLiteSpeed()` | -| Custom OLS binary (1.8.5 / 1.8.5–v2.0.5) | `install/install.py` and `plogical/upgrade.py` → `installCustomOLSBinaries()` and `BINARY_CONFIGS` comments | -| LSWS version used in upgrade (Enterprise only) | `cyberpanel_upgrade.sh` → `LSWS_Latest_URL`, `LSWS_Stable_Version`, and sed to `serverStatusUtil.py` | -| Custom OLS on upgrade | `plogical/upgrade.py` → `if os.path.exists('/usr/local/lsws/bin/openlitespeed'): Upgrade.installCustomOLSBinaries()` | - ---- - -## Short answers - -- **What OpenLiteSpeed version does install use?** - Package: **distro default**. If custom binary is used: **OpenLiteSpeed 1.8.5 (or 1.8.5–v2.0.5)** static build from cyberpanel.net. - -- **What OpenLiteSpeed version does upgrade use?** - Upgrade does **not** change OLS version from upstream. It only (re)installs the **same custom 1.8.5-based** binary when OLS is present. **LiteSpeed Enterprise** version is the one fetched from `cyberpanel.sh/update.litespeedtech.com/ws/latest.php` and written into `serverStatusUtil.py`. diff --git a/to-do/PANEL-ACCESS-PLUGIN-STORE.md b/to-do/PANEL-ACCESS-PLUGIN-STORE.md deleted file mode 100644 index 34949d496..000000000 --- a/to-do/PANEL-ACCESS-PLUGIN-STORE.md +++ /dev/null @@ -1,24 +0,0 @@ -# Panel Access plugin – store distribution - -**Panel Access (Custom Domain)** is a normal plugin. It is **not** in the core repo’s `INSTALLED_APPS` or main URLs. - -## Install on a server - -1. **From zip (repo)** - - Build zip: from repo root, `zip -r panelAccess.zip panelAccess -x "panelAccess/__pycache__/*" -x "*.pyc"` - - In CyberPanel: **Plugins → Store** (or upload zip), install **Panel Access (Custom Domain)**. - - Or copy `panelAccess/` to `/usr/local/CyberCP/panelAccess/`, add `'panelAccess'` to `CyberCP/settings.py` `INSTALLED_APPS` (after `'emailPremium',`), and add - `path('plugins/panelAccess/', include('panelAccess.urls')),` to `CyberCP/urls.py` before the generic `path('plugins/', include('pluginHolder.urls'))`, then restart lscpd. - -2. **From plugin store (GitHub)** - - Add the `panelAccess` folder to the **cyberpanel-plugins** repo (e.g. `master3395/cyberpanel-plugins`) so it appears in the store. - - Users then install via **Plugins → Store** like Memcache Manager or Contabo Auto Snapshot. - -## URLs - -- Settings page: **/plugins/panelAccess/** -- Same as **Settings** from **Plugins → Installed** for Panel Access. - -## Zip location - -- `panelAccess.zip` can be generated in the repo root and committed or published for one-off installs. diff --git a/to-do/PLUGIN-DEFAULT-REMOVAL-2026-02-01.md b/to-do/PLUGIN-DEFAULT-REMOVAL-2026-02-01.md deleted file mode 100644 index 302845ded..000000000 --- a/to-do/PLUGIN-DEFAULT-REMOVAL-2026-02-01.md +++ /dev/null @@ -1,13 +0,0 @@ -# Plugin Default Removal - 2026-02-01 - -## Summary -CyberPanel repository no longer requires any plugins by default. Plugins are installed by users from the [Plugin Store](https://github.com/master3395/cyberpanel-plugins) via the CyberPanel Plugin Manager. - -## Changes -- **settings.py**: Removed `emailMarketing` from `INSTALLED_APPS` -- **urls.py**: Commented out `emailMarketing` route (plugin installer adds it when plugin is installed) - -## Plugin Installation -Users install plugins from: https://github.com/master3395/cyberpanel-plugins - -The plugin installer adds apps to `INSTALLED_APPS` and URL routes when plugins are installed via the Plugin Store UI. diff --git a/to-do/PURE-FTPD-QUOTA-SYNTAX-FIX.md b/to-do/PURE-FTPD-QUOTA-SYNTAX-FIX.md deleted file mode 100644 index 09dba530a..000000000 --- a/to-do/PURE-FTPD-QUOTA-SYNTAX-FIX.md +++ /dev/null @@ -1,45 +0,0 @@ -# Pure-FTPd Quota Syntax Fix (2026-02-04) - -## Problem -Pure-FTPd failed to start with: -``` -/etc/pure-ftpd/pure-ftpd.conf:35:1: syntax error line 35: [Quota ...]. -``` - -## Cause -The config used `Quota yes`, but Pure-FTPd expects **`Quota maxfiles:maxsize`** (e.g. `Quota 1000:10` for 1000 files and 10 MB). The value is not a boolean. - -## Fix applied - -### On the server -- `/etc/pure-ftpd/pure-ftpd.conf`: line 35 set to `Quota 100000:100000` (high default so MySQL per-user quotas apply). -- Service started successfully: `systemctl start pure-ftpd`. - -### In the repo -- **install/pure-ftpd/pure-ftpd.conf** and **install/pure-ftpd-one/pure-ftpd.conf**: `Quota yes` → `Quota 100000:100000`. -- **websiteFunctions/website.py** (`enableFTPQuota`): sed/echo now write `Quota 100000:100000` instead of `Quota yes` (or tabs). - -## One-time fix on server (if "Enable" still breaks it) -Run on the server as root (copy script from repo or run inline): - -**Option A – script (repo root: `fix-pureftpd-quota-once.sh`):** -```bash -sudo bash /path/to/fix-pureftpd-quota-once.sh -``` - -**Option B – inline:** -```bash -sudo sed -i 's/^Quota.*/Quota 100000:100000/' /etc/pure-ftpd/pure-ftpd.conf -# If TLS 1 is set but cert missing, disable TLS: -sudo sed -i 's/^TLS[[:space:]]*1/TLS 0/' /etc/pure-ftpd/pure-ftpd.conf -sudo systemctl start pure-ftpd -``` -Then deploy the latest panel code so "Enable" uses the correct Quota syntax. - -## Code safeguards (enableFTPQuota) -- **Backup before modify**: Timestamped backup of `pure-ftpd.conf` and `pureftpd-mysql.conf` before any change. -- **Safety net before restart**: If the Quota line is not valid (`Quota maxfiles:maxsize`), it is corrected to `Quota 100000:100000` so Pure-FTPd never gets an invalid line on restart. - -## Reference -- Upstream: https://github.com/jedisct1/pure-ftpd/blob/master/pure-ftpd.conf.in (comment: "Quota 1000:10"). -- `pure-ftpd --help`: `-n --quota `. diff --git a/to-do/REPO-MERGE-2026-02-02.md b/to-do/REPO-MERGE-2026-02-02.md deleted file mode 100644 index 9497772be..000000000 --- a/to-do/REPO-MERGE-2026-02-02.md +++ /dev/null @@ -1,38 +0,0 @@ -# CyberPanel Repo Merge – 2026-02-02 - -## Summary - -`cyberpanel-repo` and `cyberpanel-fix` have been merged into a single working directory. - -## What Was Done - -1. **Unique files copied from cyberpanel-fix into cyberpanel-repo:** - - `cyberpanel_clean.sh` - - `cyberpanel_complete.sh` - - `cyberpanel_simple.sh` - - `cyberpanel_standalone.sh` - - `fix_installation_issues.sh` - - `install_phpmyadmin.sh` - - ~~`simple_install.sh`~~ (removed; use official install.sh) - - `INSTALLER_SUMMARY.md` - - `UNIVERSAL_OS_COMPATIBILITY.md` - - `to-do/MARIADB_INSTALLATION_FIXES.md` - -2. **cyberpanel-fix backup:** Renamed to `cyberpanel-fix-backup-20260202` - -3. **Symlink created:** `cyberpanel-fix` → `cyberpanel-repo` - - Paths like `/home/cyberpanel-fix/` now resolve to `/home/cyberpanel-repo/` - -## Single Source of Truth - -Use **`/home/cyberpanel-repo`** (or `/home/cyberpanel-fix` via symlink) for all CyberPanel development and deployment. - -## Backup Location - -The previous cyberpanel-fix tree is preserved at: -`/home/cyberpanel-fix-backup-20260202` - -You can remove it after confirming everything works: -```bash -rm -rf /home/cyberpanel-fix-backup-20260202 -``` diff --git a/to-do/REVERSE-PROXY-DOMAIN-CSRF.md b/to-do/REVERSE-PROXY-DOMAIN-CSRF.md deleted file mode 100644 index ba71d956f..000000000 --- a/to-do/REVERSE-PROXY-DOMAIN-CSRF.md +++ /dev/null @@ -1,77 +0,0 @@ -# CyberPanel Behind a Reverse Proxy (Custom Domain) - -When you put a **custom domain** in front of the panel (e.g. `https://panel.example.com` → proxy → `http://127.0.0.1:2087`), two things often break if the proxy is not configured for them: - -1. **403 on POST requests** (e.g. Ban IP, form submissions) — **CSRF verification failed** -2. **Charts / some UI not loading** — backend may treat the request as different from “direct” IP:port access - -With **IP:port** everything works because the browser and the backend agree on the same host. - ---- - -## Why it breaks - -- The **browser** sends `Origin` / `Referer` with the **public URL** (e.g. `https://panel.example.com`). -- The **proxy** often forwards the request with **Host** set to the **backend** (e.g. `127.0.0.1:2087` or `207.180.193.210:2087`). -- **Django** checks CSRF by comparing the request’s **Referer/Origin** to the request’s **Host**. They don’t match → **403 Forbidden** and the body says *“CSRF verification failed. Request aborted.”* - -So: **domain in the browser, backend host in `Host`** → CSRF fails and POSTs (like Ban IP) get 403. - ---- - -## Fix option 1: Panel Access (recommended, no env vars) - -Use the built-in **Panel Access** page to add your custom domain(s): - -1. In CyberPanel go to **Plugins → Panel Access (Custom Domain)**. -2. Enter your public origin(s), one per line (e.g. `https://panel.example.com`, `http://panel.example.com`). -3. Click **Save**. -4. Restart the CyberPanel backend so Django picks up the change, e.g.: - ```bash - systemctl restart lscpd - ``` - -Origins are stored in a config file (by default `/home/cyberpanel/panel_csrf_origins.conf`). They are merged with any `CSRF_TRUSTED_ORIGINS` set via environment. No hardcoded domains; safe for GitHub. - -**OpenLiteSpeed proxy (optional):** On the same page you can enable **“Also add domain in OpenLiteSpeed (reverse proxy to panel)”**. When you save, the panel will create a proxy vhost for each domain so the panel is reachable at that domain (e.g. `http://panel.example.com`) without manual OLS configuration. This only configures HTTP (port 80); for HTTPS use Manage SSL or your own certificate. The proxy forwards to the panel backend (default `https://127.0.0.1:2087`). Override with the `PANEL_BACKEND_URL` environment variable if your panel listens elsewhere. - ---- - -## Fix option 2: Environment variable - -In `CyberCP/settings.py`, `CSRF_TRUSTED_ORIGINS` is also built from the environment variable **`CSRF_TRUSTED_ORIGINS`** (comma‑separated list of origins). - -When you run the panel behind a custom domain, you can set that variable to your **public** origin(s), for example: - -```bash -export CSRF_TRUSTED_ORIGINS="https://panel.example.com,http://panel.example.com" -``` - -Then start (or restart) the CyberPanel backend. Where to set it depends on how you run the panel: - -- **systemd (lscpd)**: add to `[Service]` in `/etc/systemd/system/lscpd.service`: - ```ini - Environment="CSRF_TRUSTED_ORIGINS=https://panel.example.com,http://panel.example.com" - ``` - Then run `systemctl daemon-reload` and `systemctl restart lscpd`. -- **Supervisor / other**: set in the program’s `environment` or equivalent. -- **Manual run**: export in the same shell before starting the app. - -Use your real domain; no need to add anything to the repo. This keeps the codebase generic for GitHub. - ---- - -## Optional: proxy Host header - -Some backends only “recognise” the panel when **Host** is the backend address (e.g. IP:port). In that case the proxy is often configured to **override** Host to that address so the initial HTML and routing work. That is why the **same** proxy setup can make the **page** load but **POSTs** fail: Referer stays the public domain, Host is the backend → CSRF fails. Fixing CSRF with `CSRF_TRUSTED_ORIGINS` (as above) addresses that. - ---- - -## Summary - -| Symptom | Cause | Fix (generic, repo‑friendly) | -|----------------------|--------------------------------|--------------------------------| -| 403 on Ban IP / POST | CSRF fail (Referer vs Host) | **Panel Access**: Plugins → Panel Access (Custom Domain), add your origin(s), save, then restart lscpd. Or set `CSRF_TRUSTED_ORIGINS` env and restart backend. | -| Charts / UI not loading | Can be session/Host/static | Ensure session cookies and static URLs work; CSRF fix above helps POSTs; adjust proxy if needed | - -No domain is hardcoded in the repo. Use the Panel Access page or the `CSRF_TRUSTED_ORIGINS` environment variable for your own domain(s). diff --git a/to-do/RUNTIME-VS-REPO-2.5.5-DEV.md b/to-do/RUNTIME-VS-REPO-2.5.5-DEV.md deleted file mode 100644 index d272848f7..000000000 --- a/to-do/RUNTIME-VS-REPO-2.5.5-DEV.md +++ /dev/null @@ -1,83 +0,0 @@ -# Runtime vs Repo: What Belongs in cyberpanel-repo for 2.5.5-dev - -## Goal - -When users upgrade to **our** (master3395) 2.5.5-dev, the panel should look and behave the same. That means **default** look-and-feel and behavior must be defined in the repo, not only “generated” on the server. - ---- - -## What is “runtime generated”? - -On the live server, after install/upgrade you have: - -1. **From the repo (clone/copy)** - All app code, templates, static sources, migrations, `version.txt`, default `settings.py`, etc. - → This **should** be in the repo (and already is). - -2. **Generated at install/upgrade** - - Python venv under `/usr/local/CyberCP/bin`, `lib`, `lib64` - - `collectstatic` output under `/usr/local/CyberCP/public/static` - - `version` table and `baseTemplate_cyberpanelcosmetic` row (if created by code/migrations) - - `lscpd` binary copy, symlinks, etc. - → The **sources** that produce these (e.g. static sources, migrations) **should** be in the repo. - -3. **Per-server / preserved** - - `CyberCP/settings.py` — upgrade **merges** only the `DATABASES` section from the old server; the rest (e.g. `INSTALLED_APPS`) comes from the **new** clone. - - `baseTemplate/static/baseTemplate/custom/` (custom CSS files) - - DB row `baseTemplate_cyberpanelcosmetic.MainDashboardCSS` (custom dashboard CSS) - - `.git/`, phpMyAdmin config, SnappyMail data, etc. - → **Defaults** that define “how 2.5.5-dev looks” should be in the repo; **per-server overrides** stay on the server. - ---- - -## What we need in the repo so 2.5.5-dev “looks the same” - -- **Templates, static sources, JS/CSS** - Already in repo (e.g. `baseTemplate/`, `static/`). No change needed for “same look” unless you change the design. - -- **Default `settings.py`** - Already in repo. Upgrade keeps DB credentials from the server and uses repo for everything else (e.g. `INSTALLED_APPS`). - So 2.5.5-dev behavior is driven by the repo’s `settings.py`. - -- **Version** - `baseTemplate/views.py` has `VERSION = '2.5.5'`, `BUILD = 'dev'`. Repo’s `version.txt` is `{"version":"2.5.5","build":"dev"}`. - Upgrade also writes version into the DB. So version “same as 2.5.5-dev” is already defined in the repo. - -- **Default “look” (cosmetic)** - - Code already creates a default `CyberPanelCosmetic` row with **empty** `MainDashboardCSS` if none exists (`baseTemplate/context_processors.py`, `plogical/httpProc.py`, `loginSystem/views.py`). - - If **your live server** has custom dashboard CSS (in DB or in `baseTemplate/static/baseTemplate/custom/`), that is **your** customization. - - To make “our 2.5.5-dev” ship with that same look as default, you have two options: - - 1. **Data migration** - Add a baseTemplate data migration that does: - - `CyberPanelCosmetic.objects.get_or_create(pk=1, defaults={'MainDashboardCSS': ''})` - so every new/upgraded install gets that default look. - - 2. **Static default** - Put the CSS in a static file under `baseTemplate/static/` and include it in the base template so the default theme matches your live server. - -- **Migrations** - All schema (and optional data) migrations must be in the repo so every 2.5.5-dev install/upgrade runs the same schema and, if you add it, the same default cosmetic data. - ---- - -## What should **not** be in the repo - -- **Secrets**: DB password, `SECRET_KEY`, API keys. - Keep in `settings.py` only placeholders or env reads; real values stay on the server (or in config.php / env per your rules). - -- **User data**: sites, users, mail, backups. - These are per-server. - -- **Generated artifacts**: venv, `collectstatic` output, compiled binaries. - Repo holds the **source**; install/upgrade generates these on the server. - ---- - -## Summary - -- **Yes:** “Runtime generated” **defaults** that define how 2.5.5-dev looks and behaves **should** be reflected in the repo (templates, static sources, migrations, default cosmetic logic or data). -- **Already in repo:** App code, default settings structure, version, static sources, migrations. So 2.5.5-dev upgrades already get the same **code** and **default look** (empty custom CSS). -- **Optional:** If your live server has a **specific** custom look (e.g. custom dashboard CSS), and you want that to be the **default** for everyone on 2.5.5-dev, add it to the repo via a data migration or default static CSS as above. - -No change is **required** for “same look” unless you want to ship a non-empty default cosmetic (e.g. your current dashboard CSS) as part of 2.5.5-dev. diff --git a/to-do/V2.5.5-DEV-BRANCH-COMPATIBILITY.md b/to-do/V2.5.5-DEV-BRANCH-COMPATIBILITY.md deleted file mode 100644 index 733f3c90a..000000000 --- a/to-do/V2.5.5-DEV-BRANCH-COMPATIBILITY.md +++ /dev/null @@ -1,76 +0,0 @@ -# v2.5.5-dev Branch Compatibility Check - -**Date:** 2026-02-04 -**Branches compared:** [v2.5.5-dev](https://github.com/master3395/cyberpanel/tree/v2.5.5-dev) vs [v2.4.4](https://github.com/master3395/cyberpanel/tree/v2.4.4) vs [stable](https://github.com/master3395/cyberpanel/tree/stable) - ---- - -## 1. Will your v2.5.5-dev changes work? - -**Yes.** The Ban IP / Firewall Banned IPs changes on v2.5.5-dev are self-contained and consistent: - -| Component | Status | -|-----------|--------| -| **plogical/firewallUtilities.py** | `blockIP` / `unblockIP` use `result == 1` for success; log file path is `/usr/local/CyberCP/data/blocked_ips.log` (writable by `cyberpanel`). | -| **plogical/processUtilities.py** | When running as root, `executioner` uses `normalExecutioner` return value (1 = success, 0 = fail). | -| **firewall/firewallManager.py** | `addBannedIP` uses `FirewallUtilities.blockIP`; ACL and errors return JSON with `error_message` and `error`; rollback on block failure. | -| **firewall/views.py** | `addBannedIP` parses JSON body and calls `fm.addBannedIP(userID, request_data)`. | -| **firewall/urls.py** | Routes `getBannedIPs`, `addBannedIP`, `modifyBannedIP`, `removeBannedIP`, `deleteBannedIP`, `exportBannedIPs`, `importBannedIPs` are present. | -| **baseTemplate/views.py** | `blockIPAddress` uses `FirewallUtilities.blockIP` (no subprocess). | -| **baseTemplate (homePage + system-status.js)** | Ban IP calls `/firewall/addBannedIP` with `ip`, `reason`, `duration`; shows server `error_message` in notifications. | - -**Deployment requirements (already applied on your server):** - -- `/usr/local/CyberCP/data` owned by `cyberpanel:cyberpanel` (for `banned_ips.json` and `blocked_ips.log`). -- Deploy updated files from v2.5.5-dev into `/usr/local/CyberCP/` and restart `lscpd`. - ---- - -## 2. Does v2.5.5-dev have all functions from v2.4.4 and stable? - -**Summary:** - -- **v2.5.5-dev has more than v2.4.4 and stable** in terms of features (Banned IPs, FTP quotas, email limits, user management, bandwidth management, etc.). It is a development branch built on top of the same base. -- **v2.5.5-dev is missing a few items that exist only on stable** (backports or stable-only fixes). Nothing critical for the Ban IP feature; mainly scripts and tests. - -### v2.5.5-dev has everything from v2.4.4 that matters - -- v2.4.4 is older (fewer commits). v2.5.5-dev contains the same core apps (firewall, baseTemplate, loginSystem, backup, etc.) plus many additions. -- **Firewall:** v2.4.4 has no Banned IPs routes; v2.5.5-dev adds the full Banned IPs feature (getBannedIPs, addBannedIP, modify, remove, delete, export, import). - -### v2.5.5-dev vs stable - -- **Stable has ~86 files that differ from v2.5.5-dev**, including: - - **run_migration.py** – present on stable, **not** on v2.5.5-dev (migration helper). - - **test_firewall_blocking.py** – test script on stable. - - **rollback_phpmyadmin_redirect.sh** – rollback script on stable. - - **install/**, **plogical/** (e.g. mysqlUtilities, upgrade, backup, sslUtilities), **pluginInstaller** – some fixes/improvements on stable that may not be in v2.5.5-dev. - -- **v2.5.5-dev has 3652+ files changed vs stable** – it has many more features (user management, website functions, bandwidth, FTP quotas, email limits, Banned IPs, etc.). - -So: - -- **Feature parity:** v2.5.5-dev has **all the main functions** from v2.4.4 and **adds** Banned IPs and other features. It does **not** lack core features that v2.4.4 or stable have. -- **Stable-only extras:** Stable has a few **extra** scripts/fixes (e.g. `run_migration.py`, `rollback_phpmyadmin_redirect.sh`, some plogical/install changes). If you need those, you can cherry-pick or merge from stable into v2.5.5-dev. - ---- - -## 3. Directory layout comparison - -| In stable, not in v2.5.5-dev (by name) | In v2.5.5-dev, not in stable | -|----------------------------------------|------------------------------| -| emailMarketing (or different layout) | bin, docs, modules, public, sql, test, to-do | -| examplePlugin | (v2.5.5-dev has more structure) | -| guides | | -| scripts | | -| testPlugin | test (different name) | - -Your current repo (v2.5.5-dev) includes `emailMarketing`, `websiteFunctions`, `firewall`, `baseTemplate`, etc. The diff is mostly naming (e.g. test vs testPlugin) and stable having a few extra scripts/docs. - ---- - -## 4. Recommendation - -1. **Use v2.5.5-dev as-is for Ban IP and current features** – the changes are consistent and will work with the deployment steps above. -2. **Periodically merge or cherry-pick from stable** into v2.5.5-dev if you want stable’s migration script, phpMyAdmin rollback script, and any plogical/install fixes. -3. **You do have all the functions from v2.4.4 and stable** in the sense of core product behavior; v2.5.5-dev adds more (Banned IPs, etc.) and is only missing some optional stable-only scripts/fixes. diff --git a/to-do/V2.5.5-DEV-FIXES-AND-DEPLOY.md b/to-do/V2.5.5-DEV-FIXES-AND-DEPLOY.md deleted file mode 100644 index b58f4721a..000000000 --- a/to-do/V2.5.5-DEV-FIXES-AND-DEPLOY.md +++ /dev/null @@ -1,89 +0,0 @@ -# v2.5.5-dev: Fixes and Deploy Guide - -This document lists fixes included in **v2.5.5-dev** and how to deploy them on a CyberPanel server. - ---- - -## Version and cache busting - -- **baseTemplate:** `CP_VERSION` in `baseTemplate/templates/baseTemplate/index.html` now uses `CYBERPANEL_FULL_VERSION` from context (from `baseTemplate/views.py`: `VERSION = '2.5.5'`, `BUILD = 'dev'`), so static URLs use `?v=2.5.5.dev` and cache busting matches the branch. - ---- - -## 1. Email Limits page - -- **Issue:** Raw Angular bindings (`{$ selectedEmail $}`) and `EmailLimitsNew` controller not registered. -- **Files:** `mailServer/mailserverManager.py`, `mailServer/templates/mailServer/EmailLimits.html`, `mailServer/static/mailServer/mailServer.js`, `mailServer/static/mailServer/emailLimitsController.js`, and mirrored under `static/mailServer/`. -- **Deploy:** - ```bash - sudo bash /home/cyberpanel-repo/deploy-email-limits-fix.sh - ``` -- **Details:** See `to-do/EMAIL-LIMITS-DEPLOY-CHECKLIST.md`. - ---- - -## 2. FTP Create Account page - -- **Issue:** After selecting a website, the “FTP Account Details” form (username, password, path, quota) did not appear. -- **Files:** `ftp/templates/ftp/createFTPAccount.html` (inline script + polling + Angular scope sync), `ftp/static/ftp/ftp.js`, `static/ftp/ftp.js`, `public/static/ftp/ftp.js`. -- **Deploy:** - ```bash - sudo bash /home/cyberpanel-repo/deploy-ftp-create-account-fix.sh - ``` -- **After deploy:** Hard-refresh `/ftp/createFTPAccount` (Ctrl+Shift+R). - ---- - -## 3. FTP Quota Management page - -- **Issue:** `(1146, "Table 'cyberpanel.ftp_quotas' doesn't exist")` on `/ftp/quotaManagement`. -- **Files:** `sql/create_ftp_quotas.sql`, `websiteFunctions/models.py` (FTPQuota model already present). -- **Deploy:** - ```bash - sudo bash /home/cyberpanel-repo/deploy-ftp-quotas-table.sh - ``` -- **Details:** See `to-do/FTP-QUOTAS-TABLE-FIX.md`. - ---- - -## 4. mailUtilities indentation fix - -- **File:** `plogical/mailUtilities.py` (indentation fix in DNS query try/except block). -- **Deploy:** Copy to `/usr/local/CyberCP/plogical/mailUtilities.py` and restart lscpd if needed. - ---- - -## Deploy all fixes (in order) - -Run on the server (e.g. from repo root): - -```bash -sudo bash deploy-email-limits-fix.sh -sudo bash deploy-ftp-create-account-fix.sh -sudo bash deploy-ftp-quotas-table.sh -``` - -Then hard-refresh the FTP Create Account page in the browser. No need to restart lscpd after the FTP quotas table script (it only runs SQL). - ---- - -## Files changed / added in v2.5.5-dev (fixes) - -| Path | Description | -|------|-------------| -| `baseTemplate/templates/baseTemplate/index.html` | CP_VERSION from CYBERPANEL_FULL_VERSION | -| `mailServer/` (Email Limits) | Controller, template, getEmailsForDomain permission | -| `ftp/templates/ftp/createFTPAccount.html` | Inline fallback + polling for details form | -| `ftp/static/ftp/ftp.js`, `static/ftp/ftp.js`, `public/static/ftp/ftp.js` | showFTPDetails, select2/change handlers | -| `websiteFunctions/models.py` | FTPQuota model (table created via sql script) | -| `sql/create_ftp_quotas.sql` | CREATE TABLE ftp_quotas | -| `plogical/mailUtilities.py` | DNS block indentation fix | -| `deploy-email-limits-fix.sh` | Deploy Email Limits fix | -| `deploy-ftp-create-account-fix.sh` | Deploy FTP Create Account template | -| `deploy-ftp-quotas-table.sh` | Create ftp_quotas table | - ---- - -## Optional (not committed by default) - -- **CyberCP/urls.py:** If `emailMarketing` is commented out for local runserver, leave it uncommitted or revert before pushing so production keeps the route. From 1ff66216a6e1cc09565a4e2100ed4c337a24627d Mon Sep 17 00:00:00 2001 From: master3395 Date: Sun, 15 Feb 2026 00:27:45 +0100 Subject: [PATCH 057/130] Version Management: always show v2.5.5-dev as up to date; upgrade fixes - versionManagment: explicitly treat 2.5.5 dev as up to date (before version compare) - Add try/except for cyberpanel.net/version.txt fetch - Add /base/versionManagement URL alias for versionManagment - cyberpanel_upgrade: ols_binaries_config download, AlmaLinux mirror fix, SERVER_IP display --- baseTemplate/urls.py | 1 + baseTemplate/views.py | 27 +++++++++++++++++-------- cyberpanel_upgrade.sh | 46 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 66 insertions(+), 8 deletions(-) diff --git a/baseTemplate/urls.py b/baseTemplate/urls.py index 313b0f15d..fd30efb2e 100644 --- a/baseTemplate/urls.py +++ b/baseTemplate/urls.py @@ -7,6 +7,7 @@ urlpatterns = [ re_path(r'^getAdminStatus$', views.getAdminStatus, name='getSystemInformation'), re_path(r'^getLoadAverage$', views.getLoadAverage, name='getLoadAverage'), re_path(r'^versionManagment$', views.versionManagment, name='versionManagment'), + re_path(r'^versionManagement$', views.versionManagment, name='versionManagement'), re_path(r'^design$', views.design, name='design'), re_path(r'^getthemedata$', views.getthemedata, name='getthemedata'), re_path(r'^upgrade$', views.upgrade, name='upgrade'), diff --git a/baseTemplate/views.py b/baseTemplate/views.py index fc36e1dd2..03346c7ea 100644 --- a/baseTemplate/views.py +++ b/baseTemplate/views.py @@ -291,19 +291,30 @@ def versionManagment(request): currentVersion = VERSION currentBuild = str(BUILD) - getVersion = requests.get('https://cyberpanel.net/version.txt') - latest = getVersion.json() - latestVersion = latest['version'] - latestBuild = latest['build'] - branch_ref = 'v%s.%s' % (latestVersion, latestBuild) - notechk = True Currentcomt = '' latestcomit = '' + latestVersion = '0' + latestBuild = '0' - if _version_compare(currentVersion, latestVersion) > 0: + try: + getVersion = requests.get('https://cyberpanel.net/version.txt', timeout=10) + getVersion.raise_for_status() + latest = getVersion.json() + latestVersion = str(latest.get('version', '0')) + latestBuild = str(latest.get('build', '0')) + except (requests.RequestException, ValueError, KeyError) as e: + logging.CyberCPLogFileWriter.writeToFile('[versionManagment] cyberpanel.net/version.txt failed: %s' % str(e)) + if currentVersion == '2.5.5' and currentBuild == 'dev': + notechk = False + + branch_ref = 'v%s.%s' % (latestVersion, latestBuild) + + if notechk and (currentVersion == '2.5.5' and currentBuild == 'dev'): notechk = False - else: + elif notechk and _version_compare(currentVersion, latestVersion) > 0: + notechk = False + elif notechk: remote_cmd = 'git -C /usr/local/CyberCP remote get-url origin 2>/dev/null || true' remote_out = ProcessUtilities.outputExecutioner(remote_cmd) is_usmannasir = 'usmannasir/cyberpanel' in (remote_out or '') diff --git a/cyberpanel_upgrade.sh b/cyberpanel_upgrade.sh index 906dc6e3d..318b4aad7 100644 --- a/cyberpanel_upgrade.sh +++ b/cyberpanel_upgrade.sh @@ -428,6 +428,26 @@ 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 slow mirrors (e.g. ftp.lip6.fr timeout) + if [[ "$Server_OS" = "AlmaLinux9" ]] && [[ -d /etc/yum.repos.d ]]; then + 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 + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Switching AlmaLinux repos to repo.almalinux.org to avoid mirror timeouts" | tee -a /var/log/cyberpanel_upgrade_debug.log + sed -i 's|^mirrorlist=|#mirrorlist=|g' "$repo" + sed -i 's|^#baseurl=\(.*repo\.almalinux\.org.*\)|baseurl=\1|' "$repo" + fi + done + 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 @@ -979,6 +999,18 @@ if [[ -z "$CP_PYTHON" ]]; then 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 # Run upgrade.py and capture output @@ -1633,6 +1665,20 @@ 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 + # Test if CyberPanel is accessible echo -e "\n🔍 Testing CyberPanel accessibility..." From 50055f7c480b6966ff7e757fbe0add976b7f7c35 Mon Sep 17 00:00:00 2001 From: master3395 Date: Sun, 15 Feb 2026 00:34:49 +0100 Subject: [PATCH 058/130] README: add v2.5.5-dev upgrade commands; preUpgrade pass -b; add --mariadb flag - README: Upgrade to v2.5.5-dev section with non-interactive commands - preUpgrade.sh: pass -b branch to cyberpanel_upgrade.sh (fixes branch not used) - cyberpanel_upgrade.sh: add --mariadb for MariaDB 10.11 non-interactive --- README.md | 24 ++++++++++++++++++++++++ cyberpanel_upgrade.sh | 10 +++++++--- preUpgrade.sh | 20 +++++++++++++++----- 3 files changed, 46 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 1f1111bde..57b0508e5 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,30 @@ 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: + +```bash +# Upgrade to v2.5.5-dev without prompts +sh <(curl -s https://raw.githubusercontent.com/master3395/cyberpanel/v2.5.5-dev/preUpgrade.sh || wget -O - https://raw.githubusercontent.com/master3395/cyberpanel/v2.5.5-dev/preUpgrade.sh) -b v2.5.5-dev + +# With MariaDB 10.11 (non-interactive) +sh <(curl -s https://raw.githubusercontent.com/master3395/cyberpanel/v2.5.5-dev/preUpgrade.sh || wget -O - https://raw.githubusercontent.com/master3395/cyberpanel/v2.5.5-dev/preUpgrade.sh) -b v2.5.5-dev --mariadb + +# Or use --mariadb-version for other versions +sh <(curl -s https://raw.githubusercontent.com/master3395/cyberpanel/v2.5.5-dev/preUpgrade.sh || wget -O - https://raw.githubusercontent.com/master3395/cyberpanel/v2.5.5-dev/preUpgrade.sh) -b v2.5.5-dev --mariadb-version 10.11 +sh <(curl -s https://raw.githubusercontent.com/master3395/cyberpanel/v2.5.5-dev/preUpgrade.sh || wget -O - https://raw.githubusercontent.com/master3395/cyberpanel/v2.5.5-dev/preUpgrade.sh) -b v2.5.5-dev --mariadb-version 11.8 +``` + +**Full non-interactive command** (v2.5.5-dev + MariaDB 10.11): + +```bash +sh <(curl -s https://raw.githubusercontent.com/master3395/cyberpanel/v2.5.5-dev/preUpgrade.sh || wget -O - https://raw.githubusercontent.com/master3395/cyberpanel/v2.5.5-dev/preUpgrade.sh) -b v2.5.5-dev --mariadb +``` + +--- + ## Troubleshooting (common) **Command not found** — install curl/wget/git/python3 diff --git a/cyberpanel_upgrade.sh b/cyberpanel_upgrade.sh index 318b4aad7..7ba04b661 100644 --- a/cyberpanel_upgrade.sh +++ b/cyberpanel_upgrade.sh @@ -367,7 +367,11 @@ if [[ "$*" = *"--no-system-update"* ]]; then echo -e "\nUsing --no-system-update: skipping full system package update.\n" fi # Parse --mariadb-version 10.11|11.8|12.1 (default 11.8) -if [[ "$*" = *"--mariadb-version "* ]]; then +# --mariadb is shorthand for --mariadb-version 10.11 (MariaDB default, matches 10.11.x-MariaDB Server) +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 @@ -1768,8 +1772,8 @@ if [[ "$*" != *"--branch "* ]] && [[ "$*" != *"-b "* ]] ; then Pre_Upgrade_Branch_Input fi -# Prompt for MariaDB version if not set via --mariadb-version (default 11.8). Options: 10.11, 11.8, 12.1. -if [[ "$*" != *"--mariadb-version "* ]]; then +# Prompt for MariaDB version if not set via --mariadb or --mariadb-version (default 11.8). Options: 10.11, 11.8, 12.1. +if [[ "$*" != *"--mariadb"* ]] && [[ "$*" != *"--mariadb-version "* ]]; then echo -e "\nMariaDB version: \e[31m10.11\e[39m, \e[31m11.8\e[39m LTS (default) or \e[31m12.1\e[39m. You can switch later by re-running with --mariadb-version 10.11, 11.8 or 12.1." echo -e "Press Enter for 11.8 LTS, or type \e[31m10.11\e[39m or \e[31m12.1\e[39m (5 sec timeout): " read -r -t 5 Tmp_MariaDB_Ver || true diff --git a/preUpgrade.sh b/preUpgrade.sh index 043e45cba..2e1ea4962 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 @@ -17,4 +26,5 @@ 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 chmod 700 /usr/local/cyberpanel_upgrade.sh -/usr/local/cyberpanel_upgrade.sh $@ +# Pass -b so upgrade script skips branch prompt and uses our branch +/usr/local/cyberpanel_upgrade.sh -b "$BRANCH_NAME" $EXTRA_ARGS From bd16cb60d8e6d435ba93a43dba9ae00d2f56ecaa Mon Sep 17 00:00:00 2001 From: master3395 Date: Sun, 15 Feb 2026 00:36:24 +0100 Subject: [PATCH 059/130] README: update date to January 15, 2026 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 57b0508e5..1a7922c36 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/) From 0a82d91956392da8f6375038984ee973bfa12c5e Mon Sep 17 00:00:00 2001 From: master3395 Date: Sun, 15 Feb 2026 00:53:32 +0100 Subject: [PATCH 060/130] Version management: fork-aware logic, show Current/Latest commit for forks - Treat v2.5.5-dev as up to date; suppress upgrade banner for forks - Always fetch and display Current Commit (local HEAD) - For forks: fetch Latest Commit from fork's GitHub API - Add _version_compare helper; wrap cyberpanel.net fetch in try/except --- baseTemplate/views.py | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/baseTemplate/views.py b/baseTemplate/views.py index 03346c7ea..311313b8c 100644 --- a/baseTemplate/views.py +++ b/baseTemplate/views.py @@ -26,6 +26,7 @@ from loginSystem.models import Administrator from packages.models import Package from django.views.decorators.http import require_GET, require_POST import pwd +import re # Create your views here. @@ -310,15 +311,19 @@ def versionManagment(request): branch_ref = 'v%s.%s' % (latestVersion, latestBuild) + # Always fetch local HEAD for display + head_cmd = 'git -C /usr/local/CyberCP rev-parse HEAD 2>/dev/null || true' + Currentcomt = (ProcessUtilities.outputExecutioner(head_cmd) or '').rstrip('\n') + + remote_cmd = 'git -C /usr/local/CyberCP remote get-url origin 2>/dev/null || true' + remote_out = ProcessUtilities.outputExecutioner(remote_cmd) + is_usmannasir = 'usmannasir/cyberpanel' in (remote_out or '') + if notechk and (currentVersion == '2.5.5' and currentBuild == 'dev'): notechk = False elif notechk and _version_compare(currentVersion, latestVersion) > 0: notechk = False elif notechk: - remote_cmd = 'git -C /usr/local/CyberCP remote get-url origin 2>/dev/null || true' - remote_out = ProcessUtilities.outputExecutioner(remote_cmd) - is_usmannasir = 'usmannasir/cyberpanel' in (remote_out or '') - if is_usmannasir: u = "https://api.github.com/repos/usmannasir/cyberpanel/commits?sha=%s" % branch_ref logging.CyberCPLogFileWriter.writeToFile(u) @@ -328,13 +333,26 @@ def versionManagment(request): latestcomit = r.json()[0]['sha'] except (requests.RequestException, IndexError, KeyError) as e: logging.CyberCPLogFileWriter.writeToFile('[versionManagment] GitHub API failed: %s' % str(e)) - head_cmd = 'git -C /usr/local/CyberCP rev-parse HEAD 2>/dev/null || true' - Currentcomt = ProcessUtilities.outputExecutioner(head_cmd).rstrip('\n') if latestcomit and Currentcomt == latestcomit: notechk = False else: notechk = False + # For forks: fetch latest commit from the actual remote for display + if not latestcomit and remote_out: + m = re.search(r'github\.com[:/]([^/]+)/([^/]+?)(?:\.git)?$', remote_out.strip()) + if m: + owner, repo = m.group(1), m.group(2).rstrip('.git') + branch_cmd = 'git -C /usr/local/CyberCP rev-parse --abbrev-ref HEAD 2>/dev/null || true' + local_branch = ProcessUtilities.outputExecutioner(branch_cmd).rstrip('\n') or 'v2.5.5-dev' + try: + u = "https://api.github.com/repos/%s/%s/commits?sha=%s" % (owner, repo, local_branch) + r = requests.get(u, timeout=10) + r.raise_for_status() + latestcomit = r.json()[0]['sha'] + except (requests.RequestException, IndexError, KeyError): + pass + template = 'baseTemplate/versionManagment.html' finalData = {'build': currentBuild, 'currentVersion': currentVersion, 'latestVersion': latestVersion, 'latestBuild': latestBuild, 'latestcomit': latestcomit, "Currentcomt": Currentcomt, From a32636c99e75697ebb763f950965bc748c6b2525 Mon Sep 17 00:00:00 2001 From: master3395 Date: Sun, 15 Feb 2026 01:00:52 +0100 Subject: [PATCH 061/130] Version management: dev branch uses v2.5.5-dev, show 'not up to date' when behind - On dev (2.5.5 dev): show Latest Version 2.5.5, Latest Build dev - Compare Current Commit vs usmannasir v2.5.5-dev latest (e75ca45...) - Show yellow banner when behind; fork users compare to upstream dev - Stable branch unchanged (cyberpanel.net) --- baseTemplate/views.py | 60 ++++++++++++++++++++++++------------------- 1 file changed, 33 insertions(+), 27 deletions(-) diff --git a/baseTemplate/views.py b/baseTemplate/views.py index 311313b8c..a67ddcf74 100644 --- a/baseTemplate/views.py +++ b/baseTemplate/views.py @@ -298,6 +298,8 @@ def versionManagment(request): latestVersion = '0' latestBuild = '0' + on_dev_branch = (currentVersion == '2.5.5' and currentBuild == 'dev') + try: getVersion = requests.get('https://cyberpanel.net/version.txt', timeout=10) getVersion.raise_for_status() @@ -306,10 +308,15 @@ def versionManagment(request): latestBuild = str(latest.get('build', '0')) except (requests.RequestException, ValueError, KeyError) as e: logging.CyberCPLogFileWriter.writeToFile('[versionManagment] cyberpanel.net/version.txt failed: %s' % str(e)) - if currentVersion == '2.5.5' and currentBuild == 'dev': - notechk = False + if on_dev_branch: + latestVersion, latestBuild = '2.5.5', 'dev' - branch_ref = 'v%s.%s' % (latestVersion, latestBuild) + # Dev branch: compare against v2.5.5-dev, show dev version info + if on_dev_branch: + branch_ref = 'v2.5.5-dev' + latestVersion, latestBuild = '2.5.5', 'dev' + else: + branch_ref = 'v%s.%s' % (latestVersion, latestBuild) # Always fetch local HEAD for display head_cmd = 'git -C /usr/local/CyberCP rev-parse HEAD 2>/dev/null || true' @@ -319,39 +326,38 @@ def versionManagment(request): remote_out = ProcessUtilities.outputExecutioner(remote_cmd) is_usmannasir = 'usmannasir/cyberpanel' in (remote_out or '') - if notechk and (currentVersion == '2.5.5' and currentBuild == 'dev'): - notechk = False - elif notechk and _version_compare(currentVersion, latestVersion) > 0: + # Stable: newer than cyberpanel.net = up to date; dev: compare commits + if not on_dev_branch and notechk and _version_compare(currentVersion, latestVersion) > 0: notechk = False elif notechk: - if is_usmannasir: - u = "https://api.github.com/repos/usmannasir/cyberpanel/commits?sha=%s" % branch_ref + # Dev branch: always use usmannasir v2.5.5-dev as canonical "latest" + # Forks: use usmannasir for Latest Commit so all dev users compare to same upstream + fetch_branch = branch_ref if (is_usmannasir or on_dev_branch) else None + if fetch_branch: + u = "https://api.github.com/repos/usmannasir/cyberpanel/commits?sha=%s" % fetch_branch logging.CyberCPLogFileWriter.writeToFile(u) try: r = requests.get(u, timeout=10) r.raise_for_status() latestcomit = r.json()[0]['sha'] + if Currentcomt and latestcomit and Currentcomt == latestcomit: + notechk = False except (requests.RequestException, IndexError, KeyError) as e: logging.CyberCPLogFileWriter.writeToFile('[versionManagment] GitHub API failed: %s' % str(e)) - if latestcomit and Currentcomt == latestcomit: - notechk = False - else: - notechk = False - - # For forks: fetch latest commit from the actual remote for display - if not latestcomit and remote_out: - m = re.search(r'github\.com[:/]([^/]+)/([^/]+?)(?:\.git)?$', remote_out.strip()) - if m: - owner, repo = m.group(1), m.group(2).rstrip('.git') - branch_cmd = 'git -C /usr/local/CyberCP rev-parse --abbrev-ref HEAD 2>/dev/null || true' - local_branch = ProcessUtilities.outputExecutioner(branch_cmd).rstrip('\n') or 'v2.5.5-dev' - try: - u = "https://api.github.com/repos/%s/%s/commits?sha=%s" % (owner, repo, local_branch) - r = requests.get(u, timeout=10) - r.raise_for_status() - latestcomit = r.json()[0]['sha'] - except (requests.RequestException, IndexError, KeyError): - pass + elif not on_dev_branch: + # Stable fork: fetch from fork's branch + m = re.search(r'github\.com[:/]([^/]+)/([^/]+?)(?:\.git)?$', (remote_out or '').strip()) + if m: + owner, repo = m.group(1), m.group(2).rstrip('.git') + try: + u = "https://api.github.com/repos/%s/%s/commits?sha=%s" % (owner, repo, branch_ref) + r = requests.get(u, timeout=10) + r.raise_for_status() + latestcomit = r.json()[0]['sha'] + if Currentcomt and latestcomit and Currentcomt == latestcomit: + notechk = False + except (requests.RequestException, IndexError, KeyError): + pass template = 'baseTemplate/versionManagment.html' finalData = {'build': currentBuild, 'currentVersion': currentVersion, 'latestVersion': latestVersion, From 3cb149ba4f3b24379842ed9783660278c854134d Mon Sep 17 00:00:00 2001 From: master3395 Date: Sun, 15 Feb 2026 01:16:01 +0100 Subject: [PATCH 062/130] Version management: fix upgrade polling, branch dropdown, log visibility - Fix ReferenceError: use $timeout instead of timeout, store timer for cancel - Add v2.5.5-dev to Select Branch dropdown (remove dev filter) - Limit branch list to latest 10 by version - Fix Upgrade Progress Log text visibility (light text on dark bg) --- .../baseTemplate/custom-js/system-status.js | 6 +- .../baseTemplate/versionManagment.html | 56 +++++++++++++------ .../baseTemplate/custom-js/system-status.js | 6 +- .../baseTemplate/custom-js/system-status.js | 6 +- 4 files changed, 51 insertions(+), 23 deletions(-) diff --git a/baseTemplate/static/baseTemplate/custom-js/system-status.js b/baseTemplate/static/baseTemplate/custom-js/system-status.js index 9afb9976d..f26ebe047 100644 --- a/baseTemplate/static/baseTemplate/custom-js/system-status.js +++ b/baseTemplate/static/baseTemplate/custom-js/system-status.js @@ -579,6 +579,7 @@ app.controller('versionManagment', function ($scope, $http, $timeout) { $scope.updateFinish = true; $scope.couldNotConnect = true; + var upgradeStatusTimer = null; $scope.upgrade = function () { @@ -660,7 +661,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 +674,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/baseTemplate/templates/baseTemplate/versionManagment.html b/baseTemplate/templates/baseTemplate/versionManagment.html index b4b8a566a..202db35ec 100644 --- a/baseTemplate/templates/baseTemplate/versionManagment.html +++ b/baseTemplate/templates/baseTemplate/versionManagment.html @@ -199,9 +199,9 @@ word-break: break-all; } - /* Progress log */ + /* Progress log - dark background for terminal-style visibility */ .log-container { - background: var(--text-primary, #1e1e1e); + background: #1e1e1e; border-radius: 8px; padding: 20px; margin-top: 20px; @@ -215,11 +215,12 @@ min-height: 300px; background: transparent; border: none; - color: var(--bg-secondary, #f0f0f0); + color: #e8e8e8; font-family: 'SF Mono', Monaco, monospace; font-size: 13px; line-height: 1.6; resize: vertical; + caret-color: #e8e8e8; } .log-textarea:focus { @@ -337,30 +338,51 @@
From 9390551ebd111573f4c73db4c484781dc77f576b Mon Sep 17 00:00:00 2001 From: master3395 Date: Mon, 16 Feb 2026 03:16:18 +0100 Subject: [PATCH 111/130] Firewall: fix search bar, Search button, Modify centering, system-status $ error - Search input: add firewall-search-input class, blue focus instead of red (avoids read-only/error look) - Search button: use btn-search with round futuristic style (match Ban IP/Overview) - Actions column: center Modify/Unban/Delete in Firewall Rules and Banned IPs tables - system-status.js: increment() uses document.querySelectorAll (no jQuery), fixes $ is not defined - upgrade_modules/09_sync.sh: sync firewall static to public/static during upgrade - to-do/FIREWALL-LOAD-CHANGES.md: doc on file locations and deploy steps --- baseTemplate/context_processors.py | 22 +- .../baseTemplate/custom-js/system-status.js | 95 +- .../templates/baseTemplate/index.html | 20 +- firewall/firewallManager.py | 302 +++-- firewall/static/firewall/firewall.js | 443 ++++--- firewall/templates/firewall/firewall.html | 279 +++- firewall/views.py | 27 +- .../baseTemplate/custom-js/system-status.js | 13 +- public/static/firewall/firewall.js | 1055 ++++++++++++++- .../baseTemplate/custom-js/system-status.js | 214 ++- static/firewall/firewall.js | 1163 ++++++++++------- to-do/FIREWALL-LOAD-CHANGES.md | 64 + upgrade_modules/09_sync.sh | 6 + 13 files changed, 2851 insertions(+), 852 deletions(-) create mode 100644 to-do/FIREWALL-LOAD-CHANGES.md diff --git a/baseTemplate/context_processors.py b/baseTemplate/context_processors.py index b3122a0fe..db7aa8d90 100644 --- a/baseTemplate/context_processors.py +++ b/baseTemplate/context_processors.py @@ -55,11 +55,25 @@ def notification_preferences_context(request): } def firewall_static_context(request): - """Expose a cache-busting token for firewall static assets.""" - firewall_js_path = '/usr/local/CyberCP/static/firewall/firewall.js' + """Expose a cache-busting token for firewall static assets (bumps when firewall.js changes).""" try: - version = int(os.path.getmtime(firewall_js_path)) - except OSError: + 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 diff --git a/baseTemplate/static/baseTemplate/custom-js/system-status.js b/baseTemplate/static/baseTemplate/custom-js/system-status.js index 4da778e02..9924462f9 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) { @@ -566,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(); +} //////////// @@ -932,7 +1006,8 @@ var dashboardStatsControllerFn = 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; @@ -951,7 +1026,8 @@ var dashboardStatsControllerFn = 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; @@ -979,7 +1055,8 @@ var dashboardStatsControllerFn = 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; diff --git a/baseTemplate/templates/baseTemplate/index.html b/baseTemplate/templates/baseTemplate/index.html index bef27a093..41f04ac3b 100644 --- a/baseTemplate/templates/baseTemplate/index.html +++ b/baseTemplate/templates/baseTemplate/index.html @@ -26,17 +26,14 @@ - + + - - - - @@ -45,9 +42,9 @@ - - - + + +
@@ -903,15 +920,32 @@ - - - - {{ record.type }} - + + + + + + + + + + + + + + + + - - - - Date: Mon, 16 Feb 2026 14:58:18 +0100 Subject: [PATCH 118/130] CloudFlare DNS: TTL dropdown use ng-value so AUTO shows correctly --- .../dns/addDeleteDNSRecordsCloudFlare.html | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/dns/templates/dns/addDeleteDNSRecordsCloudFlare.html b/dns/templates/dns/addDeleteDNSRecordsCloudFlare.html index 775243dbd..3531d204f 100644 --- a/dns/templates/dns/addDeleteDNSRecordsCloudFlare.html +++ b/dns/templates/dns/addDeleteDNSRecordsCloudFlare.html @@ -930,13 +930,13 @@ From 34b9d53ff7491dd3b8e7a9527ac51924897dfcdd Mon Sep 17 00:00:00 2001 From: master3395 Date: Mon, 16 Feb 2026 15:08:40 +0100 Subject: [PATCH 119/130] CloudFlare DNS: sortable columns, search bar (filter all records) --- .../dns/addDeleteDNSRecordsCloudFlare.html | 44 ++++++++++++++++--- public/static/dns/dns.js | 11 +++++ static/dns/dns.js | 11 +++++ 3 files changed, 59 insertions(+), 7 deletions(-) diff --git a/dns/templates/dns/addDeleteDNSRecordsCloudFlare.html b/dns/templates/dns/addDeleteDNSRecordsCloudFlare.html index 3531d204f..0f89162ff 100644 --- a/dns/templates/dns/addDeleteDNSRecordsCloudFlare.html +++ b/dns/templates/dns/addDeleteDNSRecordsCloudFlare.html @@ -565,6 +565,14 @@ } .inline-select { min-width: 90px; cursor: pointer; } .inline-number { width: 70px; max-width: 80px; } + .sortable-th { cursor: pointer; user-select: none; white-space: nowrap; } + .sortable-th:hover { background: var(--bg-hover, #f0f1ff); } + .sort-icon { margin-left: 4px; opacity: 0.7; font-size: 0.75rem; } + .dns-search-wrap { position: relative; max-width: 400px; } + .dns-search-icon { position: absolute; left: 12px; top: 50%; transform: translateY(-50%); color: #94a3b8; pointer-events: none; } + .dns-search-input { padding-left: 36px; padding-right: 36px; border-radius: 8px; border: 1px solid var(--border-primary, #e2e8f0); } + .dns-search-clear { position: absolute; right: 10px; top: 50%; transform: translateY(-50%); cursor: pointer; color: #94a3b8; padding: 4px; } + .dns-search-clear:hover { color: #64748b; }
@@ -898,6 +906,12 @@ {% trans "DNS Records" %} +
+ + + +
+
Loading DNS records...
@@ -906,20 +920,36 @@ 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" %}
diff --git a/public/static/dns/dns.js b/public/static/dns/dns.js index ccd15fdde..2a9790505 100644 --- a/public/static/dns/dns.js +++ b/public/static/dns/dns.js @@ -1158,6 +1158,17 @@ app.controller('addModifyDNSRecordsCloudFlare', function ($scope, $http, $window } return list; }; + $scope.dnsSearchFilter = ''; + $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) { diff --git a/static/dns/dns.js b/static/dns/dns.js index b122605ff..6fec9b07b 100644 --- a/static/dns/dns.js +++ b/static/dns/dns.js @@ -1162,6 +1162,17 @@ app.controller('addModifyDNSRecordsCloudFlare', function ($scope, $http, $window } return list; }; + $scope.dnsSearchFilter = ''; + $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) { From 048a62d74a2f71c75699cd3970312444bc86a093 Mon Sep 17 00:00:00 2001 From: master3395 Date: Mon, 16 Feb 2026 15:14:00 +0100 Subject: [PATCH 120/130] CloudFlare DNS: fix search (custom filter), search icon left of box --- .../dns/addDeleteDNSRecordsCloudFlare.html | 13 +++++++------ public/static/dns/dns.js | 9 +++++++++ static/dns/dns.js | 9 +++++++++ 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/dns/templates/dns/addDeleteDNSRecordsCloudFlare.html b/dns/templates/dns/addDeleteDNSRecordsCloudFlare.html index 0f89162ff..02a542769 100644 --- a/dns/templates/dns/addDeleteDNSRecordsCloudFlare.html +++ b/dns/templates/dns/addDeleteDNSRecordsCloudFlare.html @@ -568,9 +568,10 @@ .sortable-th { cursor: pointer; user-select: none; white-space: nowrap; } .sortable-th:hover { background: var(--bg-hover, #f0f1ff); } .sort-icon { margin-left: 4px; opacity: 0.7; font-size: 0.75rem; } - .dns-search-wrap { position: relative; max-width: 400px; } - .dns-search-icon { position: absolute; left: 12px; top: 50%; transform: translateY(-50%); color: #94a3b8; pointer-events: none; } - .dns-search-input { padding-left: 36px; padding-right: 36px; border-radius: 8px; border: 1px solid var(--border-primary, #e2e8f0); } + .dns-search-wrap { position: relative; max-width: 440px; display: flex; align-items: center; } + .dns-search-icon-left { display: inline-flex; align-items: center; justify-content: center; width: 40px; height: 38px; margin-right: 0; padding-right: 0; color: #94a3b8; background: var(--bg-hover, #f1f5f9); border: 1px solid var(--border-primary, #e2e8f0); border-right: none; border-radius: 8px 0 0 8px; flex-shrink: 0; } + .dns-search-input { flex: 1; padding-left: 12px; padding-right: 36px; border-radius: 0 8px 8px 0; border: 1px solid var(--border-primary, #e2e8f0); border-left: none; min-width: 0; } + .dns-search-wrap:focus-within .dns-search-icon-left { background: var(--bg-primary, #fff); color: #5b5fcf; } .dns-search-clear { position: absolute; right: 10px; top: 50%; transform: translateY(-50%); cursor: pointer; color: #94a3b8; padding: 4px; } .dns-search-clear:hover { color: #64748b; } @@ -907,7 +908,7 @@
- +
@@ -920,7 +921,7 @@ No DNS records found. -
+
{% trans "No records match your search." %}
@@ -949,7 +950,7 @@
diff --git a/public/static/dns/dns.js b/public/static/dns/dns.js index 2a9790505..6ac62ff9c 100644 --- a/public/static/dns/dns.js +++ b/public/static/dns/dns.js @@ -1159,6 +1159,15 @@ app.controller('addModifyDNSRecordsCloudFlare', function ($scope, $http, $window return list; }; $scope.dnsSearchFilter = ''; + $scope.matchDnsSearch = function (record) { + var q = ($scope.dnsSearchFilter || '').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) { diff --git a/static/dns/dns.js b/static/dns/dns.js index 6fec9b07b..df8c99f2b 100644 --- a/static/dns/dns.js +++ b/static/dns/dns.js @@ -1163,6 +1163,15 @@ app.controller('addModifyDNSRecordsCloudFlare', function ($scope, $http, $window return list; }; $scope.dnsSearchFilter = ''; + $scope.matchDnsSearch = function (record) { + var q = ($scope.dnsSearchFilter || '').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) { From 8734b275952f267920a0013be0fad0440fdbfcdb Mon Sep 17 00:00:00 2001 From: master3395 Date: Mon, 16 Feb 2026 15:29:09 +0100 Subject: [PATCH 121/130] CloudFlare DNS: mobile-friendly layout, enableProxy 500 fix, search filter --- dns/dnsManager.py | 20 +++---- .../dns/addDeleteDNSRecordsCloudFlare.html | 58 ++++++++++++++++++- dns/views.py | 7 ++- public/static/dns/dns.js | 15 +++++ static/dns/dns.js | 15 +++++ 5 files changed, 97 insertions(+), 18 deletions(-) diff --git a/dns/dnsManager.py b/dns/dnsManager.py index 45874d1e7..3d8ac699c 100644 --- a/dns/dnsManager.py +++ b/dns/dnsManager.py @@ -1163,22 +1163,17 @@ class DNSManager: params = {'name': zoneDomain, 'per_page': 50} cf = CloudFlare.CloudFlare(email=self.email, token=self.key) - ## Get zone - zones = cf.zones.get(params=params) + if not zones: + final_dic = {'status': 0, 'delete_status': 0, 'error_message': 'Zone not found'} + return HttpResponse(json.dumps(final_dic), status=400) zone = zones[0] - - ## - zone_id = zone['id'] params = {'name': name} dns_records = cf.zones.dns_records.get(zone_id, params=params) - ## - - if value == True: new_r_proxied_flag = False else: @@ -1192,11 +1187,9 @@ class DNSManager: r_proxied = dns_record['proxied'] if r_proxied == new_r_proxied_flag: - # Nothing to do continue dns_record_id = dns_record['id'] - new_dns_record = { 'type': r_type, 'name': r_name, @@ -1204,12 +1197,13 @@ class DNSManager: 'ttl': r_ttl, 'proxied': new_r_proxied_flag } - cf.zones.dns_records.put(zone_id, dns_record_id, data=new_dns_record) final_dic = {'status': 1, 'delete_status': 1, 'error_message': "None"} - final_json = json.dumps(final_dic) - return HttpResponse(final_json) + return HttpResponse(json.dumps(final_dic)) + + final_dic = {'status': 1, 'delete_status': 1, 'error_message': "None"} + return HttpResponse(json.dumps(final_dic)) except BaseException as msg: final_dic = {'status': 0, 'delete_status': 0, 'error_message': str(msg)} diff --git a/dns/templates/dns/addDeleteDNSRecordsCloudFlare.html b/dns/templates/dns/addDeleteDNSRecordsCloudFlare.html index 02a542769..e77902b56 100644 --- a/dns/templates/dns/addDeleteDNSRecordsCloudFlare.html +++ b/dns/templates/dns/addDeleteDNSRecordsCloudFlare.html @@ -574,6 +574,58 @@ .dns-search-wrap:focus-within .dns-search-icon-left { background: var(--bg-primary, #fff); color: #5b5fcf; } .dns-search-clear { position: absolute; right: 10px; top: 50%; transform: translateY(-50%); cursor: pointer; color: #94a3b8; padding: 4px; } .dns-search-clear:hover { color: #64748b; } + + .dns-table-wrap { width: 100%; } + + @media (max-width: 768px) { + .modern-container { padding: 1rem; } + .page-header { margin-bottom: 1.5rem; } + .page-title { font-size: 1.5rem; flex-wrap: wrap; justify-content: center; gap: 0.5rem; } + .page-title .docs-link { font-size: 0.8rem; padding: 0.4rem 0.75rem; } + .page-subtitle { font-size: 0.95rem; } + .card-header { padding: 1rem 1.25rem; } + .card-title { font-size: 1.1rem; } + .card-body { padding: 1.25rem; } + .modern-tabs { flex-wrap: wrap; gap: 0.5rem; margin-bottom: 1.25rem; } + .modern-tab { padding: 0.75rem 1rem; font-size: 0.9rem; min-height: 44px; } + .form-section { margin-bottom: 1.5rem; } + .form-section .row.mb-4 .col-md-8 { margin-bottom: 0.75rem; } + .form-section .row.mb-4 .col-md-4 { margin-top: 0 !important; } + .form-section .row.mb-4 .col-md-4 .btn-primary { margin-top: 0; min-height: 44px; } + .record-tabs { overflow-x: auto; overflow-y: hidden; -webkit-overflow-scrolling: touch; flex-wrap: nowrap; padding: 0.5rem; margin-bottom: 1rem; scrollbar-width: thin; } + .record-tabs::-webkit-scrollbar { height: 6px; } + .record-tab { flex: 0 0 auto; min-width: 44px; min-height: 44px; padding: 0.5rem 0.75rem; font-size: 0.8rem; } + .record-form.active { flex-direction: column; align-items: stretch; } + .record-form .form-group { min-width: 0; margin-bottom: 1rem; } + .record-form .btn-primary { width: 100%; min-height: 44px; justify-content: center; } + .dns-search-wrap { max-width: 100%; } + .dns-table-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; margin: 0 -1.25rem; padding: 0 1.25rem; touch-action: pan-x; } + .records-table { margin-top: 1rem; font-size: 12px; } + .records-table th, .records-table td { padding: 10px 8px; } + .records-table th { font-size: 10px; } + .records-table td:nth-child(1) { min-width: 140px; max-width: 200px; } + .records-table td:nth-child(4) { min-width: 120px; max-width: 220px; } + .records-table td:nth-child(6) { min-width: 56px; } + .records-table td:nth-child(7) { min-width: 48px; min-height: 44px; } + .delete-icon { min-width: 32px; min-height: 32px; padding: 6px; box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center; } + .inline-select { min-width: 70px; } + .inline-number { width: 56px; max-width: 64px; } + .sortable-th { padding: 10px 8px; min-height: 44px; } + .btn-primary { min-height: 44px; padding: 0.75rem 1.25rem; } + .edit-record-modal { margin: 1rem; padding: 1.25rem; width: 95%; max-width: none; } + .disabled-notice { padding: 1.5rem; } + } + + @media (max-width: 576px) { + .modern-container { padding: 0.75rem; } + .page-title { font-size: 1.25rem; } + .card-body { padding: 1rem; } + .dns-table-wrap { margin: 0 -1rem; padding: 0 1rem; } + .records-table th, .records-table td { padding: 8px 6px; font-size: 11px; } + .records-table th { font-size: 9px; } + .cell-value { max-width: 120px; } + .value-cell { max-width: 140px; } + }
@@ -921,10 +973,11 @@ No DNS records found.
-
+
{% trans "No records match your search." %}
+
@@ -950,7 +1003,7 @@ - +
@@ -1019,6 +1072,7 @@
+
diff --git a/dns/views.py b/dns/views.py index 3a19fa540..f2f3930d0 100644 --- a/dns/views.py +++ b/dns/views.py @@ -364,10 +364,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/public/static/dns/dns.js b/public/static/dns/dns.js index 6ac62ff9c..48f86198c 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 () { diff --git a/static/dns/dns.js b/static/dns/dns.js index df8c99f2b..42a700831 100644 --- a/static/dns/dns.js +++ b/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 () { From c697bea9eb5e82e90b00567e99690907d60a32ef Mon Sep 17 00:00:00 2001 From: master3395 Date: Mon, 16 Feb 2026 18:02:27 +0100 Subject: [PATCH 122/130] CloudFlare DNS: fix search (dnsSearch.filter scope binding) --- dns/templates/dns/addDeleteDNSRecordsCloudFlare.html | 8 ++++---- public/static/dns/dns.js | 4 ++-- static/dns/dns.js | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/dns/templates/dns/addDeleteDNSRecordsCloudFlare.html b/dns/templates/dns/addDeleteDNSRecordsCloudFlare.html index e77902b56..3efd5a51f 100644 --- a/dns/templates/dns/addDeleteDNSRecordsCloudFlare.html +++ b/dns/templates/dns/addDeleteDNSRecordsCloudFlare.html @@ -961,8 +961,8 @@
- - + +
@@ -973,7 +973,7 @@ No DNS records found.
-
+
{% trans "No records match your search." %}
@@ -1003,7 +1003,7 @@
diff --git a/public/static/dns/dns.js b/public/static/dns/dns.js index 48f86198c..467141998 100644 --- a/public/static/dns/dns.js +++ b/public/static/dns/dns.js @@ -1173,9 +1173,9 @@ app.controller('addModifyDNSRecordsCloudFlare', function ($scope, $http, $window } return list; }; - $scope.dnsSearchFilter = ''; + $scope.dnsSearch = { filter: '' }; $scope.matchDnsSearch = function (record) { - var q = ($scope.dnsSearchFilter || '').toLowerCase().trim(); + 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(); diff --git a/static/dns/dns.js b/static/dns/dns.js index 42a700831..a60f11fc4 100644 --- a/static/dns/dns.js +++ b/static/dns/dns.js @@ -1177,9 +1177,9 @@ app.controller('addModifyDNSRecordsCloudFlare', function ($scope, $http, $window } return list; }; - $scope.dnsSearchFilter = ''; + $scope.dnsSearch = { filter: '' }; $scope.matchDnsSearch = function (record) { - var q = ($scope.dnsSearchFilter || '').toLowerCase().trim(); + 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(); From 8fb838f91cc2c6c29dd1fe01a298ac4baec01721 Mon Sep 17 00:00:00 2001 From: master3395 Date: Mon, 16 Feb 2026 18:06:13 +0100 Subject: [PATCH 123/130] CloudFlare DNS: search via controller filteredRecords + $watch (no filter pipe) --- .../dns/addDeleteDNSRecordsCloudFlare.html | 4 +-- public/static/dns/dns.js | 33 +++++++++++++++++++ static/dns/dns.js | 33 +++++++++++++++++++ 3 files changed, 68 insertions(+), 2 deletions(-) diff --git a/dns/templates/dns/addDeleteDNSRecordsCloudFlare.html b/dns/templates/dns/addDeleteDNSRecordsCloudFlare.html index 3efd5a51f..c7703bf51 100644 --- a/dns/templates/dns/addDeleteDNSRecordsCloudFlare.html +++ b/dns/templates/dns/addDeleteDNSRecordsCloudFlare.html @@ -973,7 +973,7 @@ No DNS records found. -
+
{% trans "No records match your search." %}
@@ -1003,7 +1003,7 @@
diff --git a/public/static/dns/dns.js b/public/static/dns/dns.js index 467141998..dba1d46ef 100644 --- a/public/static/dns/dns.js +++ b/public/static/dns/dns.js @@ -1174,6 +1174,39 @@ app.controller('addModifyDNSRecordsCloudFlare', function ($scope, $http, $window 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; diff --git a/static/dns/dns.js b/static/dns/dns.js index a60f11fc4..c0f4d34cf 100644 --- a/static/dns/dns.js +++ b/static/dns/dns.js @@ -1178,6 +1178,39 @@ app.controller('addModifyDNSRecordsCloudFlare', function ($scope, $http, $window 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; From 1d22c961c1901dea8e2da4c688f69f9d593b15e3 Mon Sep 17 00:00:00 2001 From: master3395 Date: Mon, 16 Feb 2026 18:09:58 +0100 Subject: [PATCH 124/130] DNS: cache-bust dns.js (DNS_STATIC_VERSION), no-cache headers on CloudFlare page --- CyberCP/settings.py | 1 + baseTemplate/context_processors.py | 25 +++++++++++++++++++ .../templates/baseTemplate/index.html | 2 +- dns/views.py | 7 +++++- 4 files changed, 33 insertions(+), 2 deletions(-) diff --git a/CyberCP/settings.py b/CyberCP/settings.py index 38d2cc4d5..c4d987dd8 100644 --- a/CyberCP/settings.py +++ b/CyberCP/settings.py @@ -141,6 +141,7 @@ TEMPLATES = [ 'baseTemplate.context_processors.cosmetic_context', 'baseTemplate.context_processors.notification_preferences_context', 'baseTemplate.context_processors.firewall_static_context', + 'baseTemplate.context_processors.dns_static_context', ], }, }, diff --git a/baseTemplate/context_processors.py b/baseTemplate/context_processors.py index db7aa8d90..83215a38a 100644 --- a/baseTemplate/context_processors.py +++ b/baseTemplate/context_processors.py @@ -77,4 +77,29 @@ def firewall_static_context(request): 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/templates/baseTemplate/index.html b/baseTemplate/templates/baseTemplate/index.html index 1be72d59e..57d215791 100644 --- a/baseTemplate/templates/baseTemplate/index.html +++ b/baseTemplate/templates/baseTemplate/index.html @@ -2220,7 +2220,7 @@ - + diff --git a/dns/views.py b/dns/views.py index f2f3930d0..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) From faf6f8fff64174df8d03b573ac9610c2f783a9f9 Mon Sep 17 00:00:00 2001 From: master3395 Date: Mon, 16 Feb 2026 18:27:22 +0100 Subject: [PATCH 125/130] MySQL Manager: fix empty status/processes, error messages, deploy-safe --- databases/static/databases/databases.js | 47 +++++++++++++++---- .../templates/databases/mysqlmanager.html | 19 -------- databases/views.py | 15 +++--- plogical/mysqlUtilities.py | 23 ++++----- 4 files changed, 58 insertions(+), 46 deletions(-) diff --git a/databases/static/databases/databases.js b/databases/static/databases/databases.js index 0c5bbb12e..0eb8469fb 100644 --- a/databases/static/databases/databases.js +++ b/databases/static/databases/databases.js @@ -683,7 +683,11 @@ app.controller('phpMyAdmin', function ($scope, $http, $window) { app.controller('Mysqlmanager', function ($scope, $http, $compile, $window, $timeout) { $scope.cyberPanelLoading = false; - $scope.mysql_status = 'test' + $scope.mysql_status = 'test'; + $scope.uptime = '—'; + $scope.connections = '—'; + $scope.Slow_queries = '—'; + $scope.processes = []; $scope.getstatus = function () { @@ -706,12 +710,27 @@ app.controller('Mysqlmanager', function ($scope, $http, $compile, $window, $time function ListInitialDatas(response) { $scope.cyberPanelLoading = false; - if (response.data.status === 1) { - $scope.uptime = response.data.uptime; - $scope.connections = response.data.connections; - $scope.Slow_queries = response.data.Slow_queries; - $scope.processes = JSON.parse(response.data.processes); - $timeout($scope.showStatus, 3000); + var data = response.data; + if (typeof data === 'string') { + try { + data = JSON.parse(data); + } catch (e) { + 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', @@ -721,7 +740,7 @@ app.controller('Mysqlmanager', function ($scope, $http, $compile, $window, $time } else { new PNotify({ title: 'Error!', - text: response.data.error_message, + text: (data && data.error_message) || 'Could not load MySQL status.', type: 'error' }); } @@ -730,15 +749,25 @@ app.controller('Mysqlmanager', function ($scope, $http, $compile, $window, $time function cantLoadInitialDatas(response) { $scope.cyberPanelLoading = false; + var msg = (response.data && response.data.error_message) || (response.statusText || 'Cannot load MySQL status.'); new PNotify({ title: 'Error!', - text: "cannot load", + 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(); }); diff --git a/databases/templates/databases/mysqlmanager.html b/databases/templates/databases/mysqlmanager.html index d88200dcd..9ebac5523 100644 --- a/databases/templates/databases/mysqlmanager.html +++ b/databases/templates/databases/mysqlmanager.html @@ -667,23 +667,4 @@ - - {% endblock %} diff --git a/databases/views.py b/databases/views.py index 4b473f9e1..e32f96371 100644 --- a/databases/views.py +++ b/databases/views.py @@ -390,17 +390,18 @@ def UpgradeMySQL(request): def getMysqlstatus(request): try: userID = request.session['userID'] - finalData = mysqlUtilities.showStatus() - currentACL = ACLManager.loadedACL(userID) - if currentACL['admin'] == 1: - pass - else: + if currentACL['admin'] != 1: return ACLManager.loadErrorJson('FilemanagerAdmin', 0) - finalData = json.dumps(finalData) - return HttpResponse(finalData) + finalData = mysqlUtilities.showStatus() + if finalData == 0: + finalData = {'status': 0, 'error_message': 'Could not connect to MySQL or fetch status.'} + else: + finalData.setdefault('status', 1) + body = json.dumps(finalData) + return HttpResponse(body, content_type='application/json') except KeyError: return redirect(loadLoginPage) diff --git a/plogical/mysqlUtilities.py b/plogical/mysqlUtilities.py index 123c96189..28756e8d1 100644 --- a/plogical/mysqlUtilities.py +++ b/plogical/mysqlUtilities.py @@ -547,30 +547,31 @@ password=%s checker = 0 for items in result: - if len(str(items[1])) == 0: + # SHOW PROCESSLIST: Id, User, Host, db, Command, Time, State, Info [, Progress] + if items[3] is None or len(str(items[3])) == 0: database = 'NULL' else: - database = items[1] + database = items[3] - if len(str(items[6])) == 0: - state = 'NULL' - else: + if len(items) > 6 and items[6] is not None and len(str(items[6])) > 0: state = items[6] - - if len(str(items[7])) == '': - info = 'NULL' else: + state = 'NULL' + + if len(items) > 7 and items[7] is not None and len(str(items[7])) > 0: info = items[7] + else: + info = 'NULL' dic = { 'id': items[0], 'user': items[1], 'database': database, - 'command': items[4], - 'time': items[5], + 'command': items[4] if len(items) > 4 else '', + 'time': items[5] if len(items) > 5 else 0, 'state': state, 'info': info, - 'progress': items[8], + 'progress': items[8] if len(items) > 8 else 0, } if checker == 0: From d8c8af72bf41a3eae8fbeab3f20e1907d1648308 Mon Sep 17 00:00:00 2001 From: master3395 Date: Mon, 16 Feb 2026 19:24:39 +0100 Subject: [PATCH 126/130] MySQL Manager: fix status/processes load, Query Info 'No query', MariaDB-safe parsing, deploy docs - databases.js: app ref for ctrlreg, cache-buster, error handling, public/static sync - mysqlUtilities: column-name process list (MariaDB), SHOW GLOBAL STATUS safe parse, Query Info 'No query' - mysqlmanager.html: Query Info fallback for NULL - Deploy docs: DEPLOY-BEFORE-PUSH, DEPLOY-MYSQL-MANAGER-TO-SERVER (public/static) - Other: CyberPanelUpgrade, upgrade, websiteFunctions, userManagment, website --- databases/static/databases/databases.js | 22 +- .../templates/databases/mysqlmanager.html | 4 +- plogical/CyberPanelUpgrade.py | 1 + plogical/mysqlUtilities.py | 72 ++- plogical/upgrade.py | 45 ++ public/static/databases/databases.js | 516 +++++++++++++++++- .../websiteFunctions/websiteFunctions.js | 2 +- static/databases/databases.js | 67 ++- to-do/DEPLOY-BEFORE-PUSH-V2.5.5-DEV.md | 26 + to-do/DEPLOY-MYSQL-MANAGER-TO-SERVER.md | 53 ++ userManagment/homeDirectoryViews.py | 7 +- .../userManagment/userMigration.html | 2 +- websiteFunctions/website.py | 12 +- 13 files changed, 765 insertions(+), 64 deletions(-) create mode 100644 to-do/DEPLOY-BEFORE-PUSH-V2.5.5-DEV.md create mode 100644 to-do/DEPLOY-MYSQL-MANAGER-TO-SERVER.md diff --git a/databases/static/databases/databases.js b/databases/static/databases/databases.js index 0eb8469fb..8dd93d53c 100644 --- a/databases/static/databases/databases.js +++ b/databases/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) { @@ -694,7 +696,7 @@ app.controller('Mysqlmanager', function ($scope, $http, $compile, $window, $time $scope.cyberPanelLoading = true; - url = "/dataBases/getMysqlstatus"; + url = "/dataBases/getMysqlstatus?t=" + (Date.now ? Date.now() : new Date().getTime()); var data = {}; @@ -715,6 +717,8 @@ app.controller('Mysqlmanager', function ($scope, $http, $compile, $window, $time 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; } @@ -749,7 +753,21 @@ app.controller('Mysqlmanager', function ($scope, $http, $compile, $window, $time function cantLoadInitialDatas(response) { $scope.cyberPanelLoading = false; - var msg = (response.data && response.data.error_message) || (response.statusText || 'Cannot load MySQL status.'); + $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, diff --git a/databases/templates/databases/mysqlmanager.html b/databases/templates/databases/mysqlmanager.html index 9ebac5523..16b495e76 100644 --- a/databases/templates/databases/mysqlmanager.html +++ b/databases/templates/databases/mysqlmanager.html @@ -644,8 +644,8 @@ + ng-bind="(process.info && process.info !== 'NULL') ? process.info : 'No query'" + ng-attr-title="{$ (process.info && process.info !== 'NULL') ? process.info : 'No query' $}">
diff --git a/plogical/CyberPanelUpgrade.py b/plogical/CyberPanelUpgrade.py index a9179374b..df6abdac6 100644 --- a/plogical/CyberPanelUpgrade.py +++ b/plogical/CyberPanelUpgrade.py @@ -79,6 +79,7 @@ class UpgradeCyberPanel: Upgrade.CLMigrations() Upgrade.IncBackupMigrations() Upgrade.applyLoginSystemMigrations() + Upgrade.homeDirectoryMigrations() Upgrade.s3BackupMigrations() Upgrade.containerMigrations() Upgrade.manageServiceMigrations() diff --git a/plogical/mysqlUtilities.py b/plogical/mysqlUtilities.py index 28756e8d1..92eef09f8 100644 --- a/plogical/mysqlUtilities.py +++ b/plogical/mysqlUtilities.py @@ -531,47 +531,63 @@ password=%s data['status'] = 1 for items in result: - if items[0] == 'Uptime': - data['uptime'] = mysqlUtilities.GetTime(items[1]) - elif items[0] == 'Connections': - data['connections'] = items[1] - elif items[0] == 'Slow_queries': - data['Slow_queries'] = items[1] + if not items or len(items) < 2: + continue + key = (items[0] or '').strip() + val = items[1] + if key == 'Uptime': + try: + data['uptime'] = mysqlUtilities.GetTime(val) + except (TypeError, ValueError): + data['uptime'] = str(val) if val is not None else '0' + elif key == 'Connections': + data['connections'] = val + elif key == 'Slow_queries': + data['Slow_queries'] = val - ## Process List - - cursor.execute("show processlist") + ## Process List (column-name based so it works with any MySQL/MariaDB version) + cursor.execute("SHOW FULL PROCESSLIST") + if cursor.description: + col_names = [col[0].lower() if col[0] else '' for col in cursor.description] + else: + col_names = [] result = cursor.fetchall() json_data = "[" checker = 0 - for items in result: - # SHOW PROCESSLIST: Id, User, Host, db, Command, Time, State, Info [, Progress] - if items[3] is None or len(str(items[3])) == 0: + for row in result: + if col_names: + row_dict = dict(zip(col_names, row)) if len(col_names) == len(row) else {} + else: + row_dict = {} + # Support both MySQL and MariaDB column names (Id/id, User/user, db/DB, etc.) + def get_col(*keys, default=None): + for k in keys: + v = row_dict.get(k) if row_dict else None + if v is not None and (not isinstance(v, str) or len(str(v).strip()) > 0): + return v + return default + + database = get_col('db', 'database', default='NULL') + if database is None or (isinstance(database, str) and len(database.strip()) == 0): database = 'NULL' - else: - database = items[3] - - if len(items) > 6 and items[6] is not None and len(str(items[6])) > 0: - state = items[6] - else: + state = get_col('state', default='NULL') + if state is None or (isinstance(state, str) and len(str(state).strip()) == 0): state = 'NULL' - - if len(items) > 7 and items[7] is not None and len(str(items[7])) > 0: - info = items[7] - else: - info = 'NULL' + info = get_col('info', default=None) + if info is None or (isinstance(info, str) and len(str(info).strip()) == 0): + info = 'No query' dic = { - 'id': items[0], - 'user': items[1], + 'id': get_col('id', default=0), + 'user': get_col('user', default=''), 'database': database, - 'command': items[4] if len(items) > 4 else '', - 'time': items[5] if len(items) > 5 else 0, + 'command': get_col('command', default=''), + 'time': get_col('time', 'time_ms', default=0), 'state': state, 'info': info, - 'progress': items[8] if len(items) > 8 else 0, + 'progress': get_col('progress', default=0), } if checker == 0: diff --git a/plogical/upgrade.py b/plogical/upgrade.py index 6f534aba0..b10ffb670 100644 --- a/plogical/upgrade.py +++ b/plogical/upgrade.py @@ -2382,6 +2382,50 @@ CREATE TABLE `websiteFunctions_backupsv2` (`id` integer AUTO_INCREMENT NOT NULL except OSError as msg: Upgrade.stdOut(str(msg) + " [applyLoginSystemMigrations]") + @staticmethod + def homeDirectoryMigrations(): + """Create home_directories and user_home_mappings tables if missing (Modify Website home directory feature).""" + try: + connection, cursor = Upgrade.setupConnection('cyberpanel') + try: + cursor.execute(""" + CREATE TABLE IF NOT EXISTS `home_directories` ( + `id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, + `name` varchar(50) NOT NULL UNIQUE, + `path` varchar(255) NOT NULL UNIQUE, + `is_active` tinyint(1) NOT NULL DEFAULT 1, + `is_default` tinyint(1) NOT NULL DEFAULT 0, + `max_users` integer NOT NULL DEFAULT 0, + `description` longtext, + `created_at` datetime(6) NOT NULL, + `updated_at` datetime(6) NOT NULL + ) + """) + except Exception: + pass + try: + cursor.execute(""" + CREATE TABLE IF NOT EXISTS `user_home_mappings` ( + `id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, + `user_id` integer NOT NULL UNIQUE, + `home_directory_id` integer NOT NULL, + `created_at` datetime(6) NOT NULL, + `updated_at` datetime(6) NOT NULL, + CONSTRAINT `user_home_mappings_user_id_fk` FOREIGN KEY (`user_id`) + REFERENCES `loginSystem_administrator` (`id`) ON DELETE CASCADE, + CONSTRAINT `user_home_mappings_home_directory_id_fk` FOREIGN KEY (`home_directory_id`) + REFERENCES `home_directories` (`id`) ON DELETE CASCADE + ) + """) + except Exception: + pass + try: + connection.close() + except Exception: + pass + except Exception: + pass + @staticmethod def s3BackupMigrations(): try: @@ -5851,6 +5895,7 @@ slowlog = /var/log/php{version}-fpm-slow.log ## Upgrade.applyLoginSystemMigrations() + Upgrade.homeDirectoryMigrations() ## Put function here to update custom ACLs 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/websiteFunctions/websiteFunctions.js b/public/static/websiteFunctions/websiteFunctions.js index 060b8a7e1..ab881df98 100644 --- a/public/static/websiteFunctions/websiteFunctions.js +++ b/public/static/websiteFunctions/websiteFunctions.js @@ -10765,7 +10765,7 @@ app.controller('modifyWebsitesController', ['$scope', '$http', function ($scope, // 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; diff --git a/static/databases/databases.js b/static/databases/databases.js index 0c5bbb12e..8dd93d53c 100644 --- a/static/databases/databases.js +++ b/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) { @@ -683,14 +685,18 @@ app.controller('phpMyAdmin', function ($scope, $http, $window) { app.controller('Mysqlmanager', function ($scope, $http, $compile, $window, $timeout) { $scope.cyberPanelLoading = false; - $scope.mysql_status = 'test' + $scope.mysql_status = 'test'; + $scope.uptime = '—'; + $scope.connections = '—'; + $scope.Slow_queries = '—'; + $scope.processes = []; $scope.getstatus = function () { $scope.cyberPanelLoading = true; - url = "/dataBases/getMysqlstatus"; + url = "/dataBases/getMysqlstatus?t=" + (Date.now ? Date.now() : new Date().getTime()); var data = {}; @@ -706,12 +712,29 @@ app.controller('Mysqlmanager', function ($scope, $http, $compile, $window, $time function ListInitialDatas(response) { $scope.cyberPanelLoading = false; - if (response.data.status === 1) { - $scope.uptime = response.data.uptime; - $scope.connections = response.data.connections; - $scope.Slow_queries = response.data.Slow_queries; - $scope.processes = JSON.parse(response.data.processes); - $timeout($scope.showStatus, 3000); + 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', @@ -721,7 +744,7 @@ app.controller('Mysqlmanager', function ($scope, $http, $compile, $window, $time } else { new PNotify({ title: 'Error!', - text: response.data.error_message, + text: (data && data.error_message) || 'Could not load MySQL status.', type: 'error' }); } @@ -730,15 +753,39 @@ app.controller('Mysqlmanager', function ($scope, $http, $compile, $window, $time 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: "cannot load", + 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(); }); 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/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/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/website.py b/websiteFunctions/website.py index 0f004f2f3..b2e0d2e0e 100644 --- a/websiteFunctions/website.py +++ b/websiteFunctions/website.py @@ -3592,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, From ea05bb1671f1f47bc6edf16171623c02475bf34e Mon Sep 17 00:00:00 2001 From: master3395 Date: Mon, 16 Feb 2026 19:25:54 +0100 Subject: [PATCH 127/130] Add Select2 vendor assets (baseTemplate and static) --- .../static/baseTemplate/vendor/select2/select2.full.min.js | 2 ++ baseTemplate/static/baseTemplate/vendor/select2/select2.min.css | 1 + static/baseTemplate/vendor/select2/select2.full.min.js | 2 ++ static/baseTemplate/vendor/select2/select2.min.css | 1 + 4 files changed, 6 insertions(+) create mode 100644 baseTemplate/static/baseTemplate/vendor/select2/select2.full.min.js create mode 100644 baseTemplate/static/baseTemplate/vendor/select2/select2.min.css create mode 100644 static/baseTemplate/vendor/select2/select2.full.min.js create mode 100644 static/baseTemplate/vendor/select2/select2.min.css 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 `');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 `