From ed7d4743b66de8a8c05f49b60a4e8ce2b787b485 Mon Sep 17 00:00:00 2001 From: master3395 Date: Sun, 4 Jan 2026 21:04:51 +0100 Subject: [PATCH 1/3] Add plugin system enhancements and testPlugin - Enhanced plugin installer to properly extract and install plugins - Added security middleware exception for plugin webhook endpoints - Improved plugin listing with better error handling - Added testPlugin as example plugin for CyberPanel plugin system - Updated INSTALLED_APPS and URL routing for plugins Author: master3395 --- CyberCP/secMiddleware.py | 3 +- CyberCP/settings.py | 12 +- CyberCP/urls.py | 3 +- pluginHolder/views.py | 327 +++++++++++++++- pluginInstaller/pluginInstaller.py | 47 ++- testPlugin/meta.xml | 4 +- testPlugin/templates/testPlugin/index.html | 71 ++++ testPlugin/templates/testPlugin/settings.html | 165 ++++++++ testPlugin/urls.py | 16 +- testPlugin/views.py | 370 +++--------------- 10 files changed, 662 insertions(+), 356 deletions(-) create mode 100644 testPlugin/templates/testPlugin/index.html create mode 100644 testPlugin/templates/testPlugin/settings.html diff --git a/CyberCP/secMiddleware.py b/CyberCP/secMiddleware.py index 7eed86cfd..4f998c190 100644 --- a/CyberCP/secMiddleware.py +++ b/CyberCP/secMiddleware.py @@ -190,7 +190,8 @@ class secMiddleware: pathActual.find('saveSpamAssassinConfigurations') > -1 or pathActual.find('docker') > -1 or pathActual.find('cloudAPI') > -1 or pathActual.find('verifyLogin') > -1 or pathActual.find('submitUserCreation') > -1 or - pathActual.find('/api/') > -1 or pathActual.find('aiscanner/scheduled-scans') > -1) + pathActual.find('/api/') > -1 or pathActual.find('aiscanner/scheduled-scans') > -1 or + pathActual.find('plugins/discordWebhooks/webhook/') > -1) if isAPIEndpoint: # For API endpoints, still check for the most dangerous command injection characters diff --git a/CyberCP/settings.py b/CyberCP/settings.py index 9392a9a91..33d5c8408 100644 --- a/CyberCP/settings.py +++ b/CyberCP/settings.py @@ -54,7 +54,9 @@ INSTALLED_APPS = [ 'mailServer', # Depends on websiteFunctions, ChildDomains # Apps with multiple or complex dependencies - 'emailPremium', # Depends on mailServer + 'emailPremium', + 'discordWebhooks', # Depends on mailServer + 'testPlugin', # Test plugin 'emailMarketing', # Depends on websiteFunctions and loginSystem 'cloudAPI', # Depends on websiteFunctions 'containerization', # Depends on websiteFunctions @@ -126,7 +128,7 @@ DATABASES = { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'cyberpanel', 'USER': 'cyberpanel', - 'PASSWORD': 'SLTUIUxqhulwsh', + 'PASSWORD': '1XTy1XOV0BZPnM', 'HOST': 'localhost', 'PORT': '' }, @@ -134,7 +136,7 @@ DATABASES = { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'mysql', 'USER': 'root', - 'PASSWORD': 'SLTUIUxqhulwsh', + 'PASSWORD': '1XTy1XOV0BZPnM', 'HOST': 'localhost', 'PORT': '', }, @@ -211,6 +213,10 @@ DATA_UPLOAD_MAX_MEMORY_SIZE = 2147483648 # Security settings X_FRAME_OPTIONS = 'SAMEORIGIN' +# Login URL - CyberPanel uses root path for login +LOGIN_URL = '/' +LOGIN_REDIRECT_URL = '/' + # Default primary key field type # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' \ No newline at end of file diff --git a/CyberCP/urls.py b/CyberCP/urls.py index 9aebb6674..83ed80688 100644 --- a/CyberCP/urls.py +++ b/CyberCP/urls.py @@ -40,7 +40,8 @@ urlpatterns = [ path('filemanager/', include('filemanager.urls')), path('emailPremium/', include('emailPremium.urls')), path('manageservices/', include('manageServices.urls')), - path('plugins/', include('pluginHolder.urls')), + path('plugins/testPlugin/', include('testPlugin.urls')), path('plugins/discordWebhooks/',include('discordWebhooks.urls')), +path('plugins/', include('pluginHolder.urls')), path('emailMarketing/', include('emailMarketing.urls')), path('cloudAPI/', include('cloudAPI.urls')), path('docker/', include('dockerManager.urls')), diff --git a/pluginHolder/views.py b/pluginHolder/views.py index 25ecc2b6a..1f5a0507c 100644 --- a/pluginHolder/views.py +++ b/pluginHolder/views.py @@ -1,28 +1,339 @@ # -*- coding: utf-8 -*- from django.shortcuts import render +from django.http import JsonResponse +from django.views.decorators.csrf import csrf_exempt +from django.views.decorators.http import require_http_methods from plogical.mailUtilities import mailUtilities import os +import subprocess +import shlex +import json from xml.etree import ElementTree from plogical.httpProc import httpProc +from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter as logging +import sys +sys.path.append('/usr/local/CyberCP') +from pluginInstaller.pluginInstaller import pluginInstaller + +# Plugin state file location +PLUGIN_STATE_DIR = '/home/cyberpanel/plugin_states' + +def _get_plugin_state_file(plugin_name): + """Get the path to the plugin state file""" + if not os.path.exists(PLUGIN_STATE_DIR): + os.makedirs(PLUGIN_STATE_DIR, mode=0o755) + return os.path.join(PLUGIN_STATE_DIR, plugin_name + '.state') + +def _is_plugin_enabled(plugin_name): + """Check if a plugin is enabled""" + state_file = _get_plugin_state_file(plugin_name) + if os.path.exists(state_file): + try: + with open(state_file, 'r') as f: + state = f.read().strip() + return state == 'enabled' + except: + return True # Default to enabled if file read fails + return True # Default to enabled if state file doesn't exist + +def _set_plugin_state(plugin_name, enabled): + """Set plugin enabled/disabled state""" + state_file = _get_plugin_state_file(plugin_name) + try: + with open(state_file, 'w') as f: + f.write('enabled' if enabled else 'disabled') + os.chmod(state_file, 0o644) + return True + except Exception as e: + logging.writeToFile(f"Error writing plugin state for {plugin_name}: {str(e)}") + return False def installed(request): mailUtilities.checkHome() pluginPath = '/home/cyberpanel/plugins' pluginList = [] + errorPlugins = [] if os.path.exists(pluginPath): for plugin in os.listdir(pluginPath): + # Skip files (like .zip files) - only process directories + pluginDir = os.path.join(pluginPath, plugin) + if not os.path.isdir(pluginDir): + continue + data = {} + # Try installed location first, then fallback to source location completePath = '/usr/local/CyberCP/' + plugin + '/meta.xml' - pluginMetaData = ElementTree.parse(completePath) + sourcePath = os.path.join(pluginDir, 'meta.xml') + + # Determine which meta.xml to use + metaXmlPath = None + if os.path.exists(completePath): + metaXmlPath = completePath + elif os.path.exists(sourcePath): + # Plugin not installed but has source meta.xml - use it + metaXmlPath = sourcePath + + # Add error handling to prevent 500 errors + try: + if metaXmlPath is None: + # No meta.xml found in either location - skip silently + continue + + pluginMetaData = ElementTree.parse(metaXmlPath) + root = pluginMetaData.getroot() + + # Validate required fields exist (handle both and formats) + name_elem = root.find('name') + type_elem = root.find('type') + desc_elem = root.find('description') + version_elem = root.find('version') + + # Type field is optional (testPlugin doesn't have it) + if name_elem is None or desc_elem is None or version_elem is None: + errorPlugins.append({'name': plugin, 'error': 'Missing required metadata fields (name, description, or version)'}) + logging.writeToFile(f"Plugin {plugin}: Missing required metadata fields in meta.xml") + continue + + # Check if text is None (empty elements) + if name_elem.text is None or desc_elem.text is None or version_elem.text is None: + errorPlugins.append({'name': plugin, 'error': 'Empty metadata fields'}) + logging.writeToFile(f"Plugin {plugin}: Empty metadata fields in meta.xml") + continue + + data['name'] = name_elem.text + data['type'] = type_elem.text if type_elem is not None and type_elem.text is not None else 'Plugin' + data['desc'] = desc_elem.text + data['version'] = version_elem.text + data['plugin_dir'] = plugin # Plugin directory name + data['installed'] = os.path.exists(completePath) # True if installed, False if only in source + + # Get plugin enabled state (only for installed plugins) + if data['installed']: + data['enabled'] = _is_plugin_enabled(plugin) + else: + data['enabled'] = False + + # Extract settings URL or main URL for "Manage" button + settings_url_elem = root.find('settings_url') + url_elem = root.find('url') + + # Priority: settings_url > url > default pattern + if settings_url_elem is not None and settings_url_elem.text: + data['manage_url'] = settings_url_elem.text + elif url_elem is not None and url_elem.text: + data['manage_url'] = url_elem.text + else: + # Default: try /plugins/{plugin_dir}/settings/ or /plugins/{plugin_dir}/ + # Only set if plugin is installed (we can't know if the URL exists otherwise) + if os.path.exists(completePath): + data['manage_url'] = f'/plugins/{plugin}/settings/' + else: + data['manage_url'] = None - data['name'] = pluginMetaData.find('name').text - data['type'] = pluginMetaData.find('type').text - data['desc'] = pluginMetaData.find('description').text - data['version'] = pluginMetaData.find('version').text - - pluginList.append(data) + pluginList.append(data) + except ElementTree.ParseError as e: + errorPlugins.append({'name': plugin, 'error': f'XML parse error: {str(e)}'}) + logging.writeToFile(f"Plugin {plugin}: XML parse error - {str(e)}") + continue + except Exception as e: + errorPlugins.append({'name': plugin, 'error': f'Error loading plugin: {str(e)}'}) + logging.writeToFile(f"Plugin {plugin}: Error loading - {str(e)}") + continue proc = httpProc(request, 'pluginHolder/plugins.html', - {'plugins': pluginList}, 'admin') + {'plugins': pluginList, 'error_plugins': errorPlugins}, 'admin') return proc.render() + +@csrf_exempt +@require_http_methods(["POST"]) +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): + return JsonResponse({ + 'success': False, + 'error': f'Plugin source not found: {plugin_name}' + }, status=404) + + # Check if already installed + pluginInstalled = '/usr/local/CyberCP/' + plugin_name + if os.path.exists(pluginInstalled): + return JsonResponse({ + 'success': False, + 'error': f'Plugin already installed: {plugin_name}' + }, status=400) + + # Create zip file for installation (pluginInstaller expects a zip) + import tempfile + import shutil + temp_dir = tempfile.mkdtemp() + zip_path = os.path.join(temp_dir, plugin_name + '.zip') + + # Create zip from source directory + shutil.make_archive(os.path.join(temp_dir, plugin_name), 'zip', pluginSource) + + # Verify zip file was created + if not os.path.exists(zip_path): + shutil.rmtree(temp_dir, ignore_errors=True) + return JsonResponse({ + 'success': False, + 'error': f'Failed to create zip file for {plugin_name}' + }, status=500) + + # Copy zip to current directory (pluginInstaller expects it in cwd) + original_cwd = os.getcwd() + os.chdir(temp_dir) + + try: + # Verify zip file exists in current directory + zip_file = plugin_name + '.zip' + if not os.path.exists(zip_file): + raise Exception(f'Zip file {zip_file} not found in temp directory') + + # Install using pluginInstaller + pluginInstaller.installPlugin(plugin_name) + + # Verify plugin was actually installed + pluginInstalled = '/usr/local/CyberCP/' + plugin_name + if not os.path.exists(pluginInstalled): + raise Exception(f'Plugin installation failed: {pluginInstalled} does not exist after installation') + + # Set plugin to enabled by default after installation + _set_plugin_state(plugin_name, True) + + return JsonResponse({ + 'success': True, + 'message': f'Plugin {plugin_name} installed successfully' + }) + finally: + os.chdir(original_cwd) + # Cleanup + shutil.rmtree(temp_dir, ignore_errors=True) + + except Exception as e: + logging.writeToFile(f"Error installing plugin {plugin_name}: {str(e)}") + return JsonResponse({ + 'success': False, + 'error': str(e) + }, status=500) + +@csrf_exempt +@require_http_methods(["POST"]) +def uninstall_plugin(request, plugin_name): + """Uninstall a plugin - but keep source files and settings""" + try: + # Check if plugin is installed + pluginInstalled = '/usr/local/CyberCP/' + plugin_name + if not os.path.exists(pluginInstalled): + return JsonResponse({ + 'success': False, + 'error': f'Plugin not installed: {plugin_name}' + }, status=404) + + # Custom uninstall that keeps source files + # We need to remove from settings.py, urls.py, and remove installed directory + # but NOT remove from /home/cyberpanel/plugins/ + + # Remove from settings.py + pluginInstaller.removeFromSettings(plugin_name) + + # Remove from URLs + pluginInstaller.removeFromURLs(plugin_name) + + # Remove interface link + pluginInstaller.removeInterfaceLink(plugin_name) + + # Remove migrations if enabled + if pluginInstaller.migrationsEnabled(plugin_name): + pluginInstaller.removeMigrations(plugin_name) + + # Remove installed directory (but keep source in /home/cyberpanel/plugins/) + pluginInstaller.removeFiles(plugin_name) + + # DON'T call informCyberPanelRemoval - we want to keep the source directory + # so users can reinstall the plugin later + + # Restart service + pluginInstaller.restartGunicorn() + + # Keep state file - we want to remember if it was enabled/disabled + # So user can reinstall and have same state + + return JsonResponse({ + 'success': True, + 'message': f'Plugin {plugin_name} uninstalled successfully (source files and settings preserved)' + }) + + except Exception as e: + logging.writeToFile(f"Error uninstalling plugin {plugin_name}: {str(e)}") + return JsonResponse({ + 'success': False, + 'error': str(e) + }, status=500) + +@csrf_exempt +@require_http_methods(["POST"]) +def enable_plugin(request, plugin_name): + """Enable a plugin""" + try: + # Check if plugin is installed + pluginInstalled = '/usr/local/CyberCP/' + plugin_name + if not os.path.exists(pluginInstalled): + return JsonResponse({ + 'success': False, + 'error': f'Plugin not installed: {plugin_name}' + }, status=404) + + # Set plugin state to enabled + if _set_plugin_state(plugin_name, True): + return JsonResponse({ + 'success': True, + 'message': f'Plugin {plugin_name} enabled successfully' + }) + else: + return JsonResponse({ + 'success': False, + 'error': 'Failed to update plugin state' + }, status=500) + + except Exception as e: + logging.writeToFile(f"Error enabling plugin {plugin_name}: {str(e)}") + return JsonResponse({ + 'success': False, + 'error': str(e) + }, status=500) + +@csrf_exempt +@require_http_methods(["POST"]) +def disable_plugin(request, plugin_name): + """Disable a plugin""" + try: + # Check if plugin is installed + pluginInstalled = '/usr/local/CyberCP/' + plugin_name + if not os.path.exists(pluginInstalled): + return JsonResponse({ + 'success': False, + 'error': f'Plugin not installed: {plugin_name}' + }, status=404) + + # Set plugin state to disabled + if _set_plugin_state(plugin_name, False): + return JsonResponse({ + 'success': True, + 'message': f'Plugin {plugin_name} disabled successfully' + }) + else: + return JsonResponse({ + 'success': False, + 'error': 'Failed to update plugin state' + }, status=500) + + except Exception as e: + logging.writeToFile(f"Error disabling plugin {plugin_name}: {str(e)}") + return JsonResponse({ + 'success': False, + 'error': str(e) + }, status=500) diff --git a/pluginInstaller/pluginInstaller.py b/pluginInstaller/pluginInstaller.py index 114a9fd64..960017370 100644 --- a/pluginInstaller/pluginInstaller.py +++ b/pluginInstaller/pluginInstaller.py @@ -20,6 +20,7 @@ class pluginInstaller: Generate URL pattern compatible with both Django 2.x and 3.x+ Django 2.x uses url() with regex patterns Django 3.x+ prefers path() with simpler patterns + Plugins are routed under /plugins/pluginName/ to match meta.xml URLs """ try: django_version = django.get_version() @@ -28,17 +29,17 @@ class pluginInstaller: pluginInstaller.stdOut(f"Django version detected: {django_version}") if major_version >= 3: - # Django 3.x+ - use path() syntax + # Django 3.x+ - use path() syntax with /plugins/ prefix pluginInstaller.stdOut(f"Using path() syntax for Django 3.x+ compatibility") - return " path('" + pluginName + "/',include('" + pluginName + ".urls')),\n" + return " path('plugins/" + pluginName + "/',include('" + pluginName + ".urls')),\n" else: - # Django 2.x - use url() syntax with regex + # Django 2.x - use url() syntax with regex and /plugins/ prefix pluginInstaller.stdOut(f"Using url() syntax for Django 2.x compatibility") - return " url(r'^" + pluginName + "/',include('" + pluginName + ".urls')),\n" + return " url(r'^plugins/" + pluginName + "/',include('" + pluginName + ".urls')),\n" except Exception as e: # Fallback to modern path() syntax if version detection fails pluginInstaller.stdOut(f"Django version detection failed: {str(e)}, using path() syntax as fallback") - return " path('" + pluginName + "/',include('" + pluginName + ".urls')),\n" + return " path('plugins/" + pluginName + "/',include('" + pluginName + ".urls')),\n" @staticmethod def stdOut(message): @@ -59,8 +60,14 @@ class pluginInstaller: @staticmethod def extractPlugin(pluginName): pathToPlugin = pluginName + '.zip' - command = 'unzip ' + pathToPlugin + ' -d /usr/local/CyberCP' - subprocess.call(shlex.split(command)) + command = 'unzip -o ' + pathToPlugin + ' -d /usr/local/CyberCP' + result = subprocess.run(shlex.split(command), capture_output=True, text=True) + if result.returncode != 0: + raise Exception(f"Failed to extract plugin {pluginName}: {result.stderr}") + # Verify extraction succeeded + pluginPath = '/usr/local/CyberCP/' + pluginName + if not os.path.exists(pluginPath): + raise Exception(f"Plugin extraction failed: {pluginPath} does not exist after extraction") @staticmethod def upgradingSettingsFile(pluginName): @@ -78,16 +85,38 @@ class pluginInstaller: @staticmethod def upgradingURLs(pluginName): + """ + Add plugin URL pattern to urls.py + Plugin URLs must be inserted BEFORE the generic 'plugins/' line + to ensure proper route matching (more specific routes first) + """ data = open("/usr/local/CyberCP/CyberCP/urls.py", 'r').readlines() writeToFile = open("/usr/local/CyberCP/CyberCP/urls.py", 'w') + urlPatternAdded = False for items in data: - if items.find("manageservices") > -1: + # Insert plugin URL BEFORE the generic 'plugins/' line + # This ensures more specific routes are matched first + if items.find("path('plugins/', include('pluginHolder.urls'))") > -1 or items.find("path(\"plugins/\", include('pluginHolder.urls'))") > -1: + if not urlPatternAdded: + writeToFile.writelines(pluginInstaller.getUrlPattern(pluginName)) + urlPatternAdded = True writeToFile.writelines(items) - writeToFile.writelines(pluginInstaller.getUrlPattern(pluginName)) else: writeToFile.writelines(items) + # Fallback: if 'plugins/' line not found, insert after 'manageservices' + if not urlPatternAdded: + pluginInstaller.stdOut(f"Warning: 'plugins/' line not found, using fallback insertion after 'manageservices'") + writeToFile.close() + writeToFile = open("/usr/local/CyberCP/CyberCP/urls.py", 'w') + for items in data: + if items.find("manageservices") > -1: + writeToFile.writelines(items) + writeToFile.writelines(pluginInstaller.getUrlPattern(pluginName)) + else: + writeToFile.writelines(items) + writeToFile.close() @staticmethod diff --git a/testPlugin/meta.xml b/testPlugin/meta.xml index 5f6f1bae0..56ba394f1 100644 --- a/testPlugin/meta.xml +++ b/testPlugin/meta.xml @@ -2,8 +2,8 @@ Test Plugin Utility - A comprehensive test plugin for CyberPanel with enable/disable functionality, test button, popup messages, and inline integration 1.0.0 + A comprehensive test plugin for CyberPanel with enable/disable functionality, test button, popup messages, and inline integration CyberPanel Development Team https://github.com/cyberpanel/testPlugin MIT @@ -21,4 +21,6 @@ true true + /plugins/testPlugin/ + /plugins/testPlugin/settings/ diff --git a/testPlugin/templates/testPlugin/index.html b/testPlugin/templates/testPlugin/index.html new file mode 100644 index 000000000..ed5250a1e --- /dev/null +++ b/testPlugin/templates/testPlugin/index.html @@ -0,0 +1,71 @@ +{% 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/settings.html b/testPlugin/templates/testPlugin/settings.html new file mode 100644 index 000000000..e06f901f4 --- /dev/null +++ b/testPlugin/templates/testPlugin/settings.html @@ -0,0 +1,165 @@ +{% 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/urls.py b/testPlugin/urls.py index af0f720eb..8c2a41a90 100644 --- a/testPlugin/urls.py +++ b/testPlugin/urls.py @@ -1,18 +1,8 @@ -# -*- coding: utf-8 -*- from django.urls import path from . import views -app_name = 'testPlugin' - urlpatterns = [ - path('', views.plugin_home, name='plugin_home'), - path('test/', views.test_button, name='test_button'), - path('toggle/', views.toggle_plugin, name='toggle_plugin'), - path('settings/', views.plugin_settings, name='plugin_settings'), - path('update-settings/', views.update_settings, name='update_settings'), - path('install/', views.install_plugin, name='install_plugin'), - path('uninstall/', views.uninstall_plugin, name='uninstall_plugin'), - path('logs/', views.plugin_logs, name='plugin_logs'), - path('docs/', views.plugin_docs, name='plugin_docs'), - path('security/', views.security_info, name='security_info'), + 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 index 395ab60fe..07bc88899 100644 --- a/testPlugin/views.py +++ b/testPlugin/views.py @@ -1,324 +1,54 @@ -# -*- coding: utf-8 -*- -import json -import os -from django.shortcuts import render, get_object_or_404 -from django.http import JsonResponse, HttpResponse -from django.contrib.auth.decorators import login_required -from django.views.decorators.csrf import csrf_exempt -from django.views.decorators.http import require_http_methods -from django.contrib import messages -from django.utils import timezone -from django.core.cache import cache -from plogical.httpProc import httpProc -from .models import TestPluginSettings, TestPluginLog -from .security import secure_view, admin_required, SecurityManager +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 -@admin_required -@secure_view(require_csrf=False, rate_limit=True, log_activity=True) -def plugin_home(request): - """Main plugin page with inline integration""" - try: - # Get or create plugin settings - settings, created = TestPluginSettings.objects.get_or_create( - user=request.user, - defaults={'plugin_enabled': True} - ) - - # Get recent logs (limit to user's own logs for security) - recent_logs = TestPluginLog.objects.filter(user=request.user).order_by('-timestamp')[:10] - - context = { - 'settings': settings, - 'recent_logs': recent_logs, - 'plugin_enabled': settings.plugin_enabled, - } - - # Log page visit - TestPluginLog.objects.create( - user=request.user, - action='page_visit', - message='Visited plugin home page' - ) - - proc = httpProc(request, 'testPlugin/plugin_home.html', context, 'admin') - return proc.render() - - except Exception as e: - SecurityManager.log_security_event(request, f"Error in plugin_home: {str(e)}", "view_error") - return JsonResponse({'status': 0, 'error_message': 'An error occurred while loading the page.'}) +@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' + }) -@admin_required -@secure_view(require_csrf=True, rate_limit=True, log_activity=True) -@require_http_methods(["POST"]) -def test_button(request): - """Handle test button click and show popup message""" - try: - settings, created = TestPluginSettings.objects.get_or_create( - user=request.user, - defaults={'plugin_enabled': True} - ) - - if not settings.plugin_enabled: - SecurityManager.log_security_event(request, "Test button clicked while plugin disabled", "security_violation") - return JsonResponse({ - 'status': 0, - 'error_message': 'Plugin is disabled. Please enable it first.' - }) - - # Rate limiting for test button (max 10 clicks per minute) - test_key = f"test_button_{request.user.id}" - test_count = cache.get(test_key, 0) - if test_count >= 10: - SecurityManager.record_failed_attempt(request, "Test button rate limit exceeded") - return JsonResponse({ - 'status': 0, - 'error_message': 'Too many test button clicks. Please wait before trying again.' - }, status=429) - - cache.set(test_key, test_count + 1, 60) # 1 minute window - - # Increment test count - settings.test_count += 1 - settings.save() - - # Create log entry - TestPluginLog.objects.create( - user=request.user, - action='test_button_click', - message=f'Test button clicked (count: {settings.test_count})' - ) - - # Sanitize custom message - safe_message = SecurityManager.sanitize_input(settings.custom_message) - - # Prepare popup message - popup_message = { - 'type': 'success', - 'title': 'Test Successful!', - 'message': f'{safe_message} (Clicked {settings.test_count} times)', - 'timestamp': timezone.now().strftime('%Y-%m-%d %H:%M:%S') - } - - return JsonResponse({ - 'status': 1, - 'popup_message': popup_message, - 'test_count': settings.test_count - }) - - except Exception as e: - SecurityManager.log_security_event(request, f"Error in test_button: {str(e)}", "view_error") - return JsonResponse({'status': 0, 'error_message': 'An error occurred while processing the test.'}) - - -@admin_required -@secure_view(require_csrf=True, rate_limit=True, log_activity=True) -@require_http_methods(["POST"]) -def toggle_plugin(request): - """Toggle plugin enable/disable state""" - try: - settings, created = TestPluginSettings.objects.get_or_create( - user=request.user, - defaults={'plugin_enabled': True} - ) - - # Toggle the state - settings.plugin_enabled = not settings.plugin_enabled - settings.save() - - # Log the action - action = 'enabled' if settings.plugin_enabled else 'disabled' - TestPluginLog.objects.create( - user=request.user, - action='plugin_toggle', - message=f'Plugin {action}' - ) - - SecurityManager.log_security_event(request, f"Plugin {action} by user", "plugin_toggle") - - return JsonResponse({ - 'status': 1, - 'enabled': settings.plugin_enabled, - 'message': f'Plugin {action} successfully' - }) - - except Exception as e: - SecurityManager.log_security_event(request, f"Error in toggle_plugin: {str(e)}", "view_error") - return JsonResponse({'status': 0, 'error_message': 'An error occurred while toggling the plugin.'}) - - -@admin_required -@secure_view(require_csrf=False, rate_limit=True, log_activity=True) -def plugin_settings(request): - """Plugin settings page""" - try: - settings, created = TestPluginSettings.objects.get_or_create( - user=request.user, - defaults={'plugin_enabled': True} - ) - - context = { - 'settings': settings, - } - - proc = httpProc(request, 'testPlugin/plugin_settings.html', context, 'admin') - return proc.render() - - except Exception as e: - SecurityManager.log_security_event(request, f"Error in plugin_settings: {str(e)}", "view_error") - return JsonResponse({'status': 0, 'error_message': 'An error occurred while loading settings.'}) - - -@admin_required -@secure_view(require_csrf=True, rate_limit=True, log_activity=True) -@require_http_methods(["POST"]) -def update_settings(request): - """Update plugin settings""" - try: - settings, created = TestPluginSettings.objects.get_or_create( - user=request.user, - defaults={'plugin_enabled': True} - ) - - data = json.loads(request.body) - custom_message = data.get('custom_message', settings.custom_message) - - # Validate and sanitize input - is_valid, error_msg = SecurityManager.validate_input(custom_message, 'custom_message', 1000) - if not is_valid: - SecurityManager.record_failed_attempt(request, f"Invalid input: {error_msg}") - return JsonResponse({ - 'status': 0, - 'error_message': f'Invalid input: {error_msg}' - }, status=400) - - # Sanitize the message - custom_message = SecurityManager.sanitize_input(custom_message) - - settings.custom_message = custom_message - settings.save() - - # Log the action - TestPluginLog.objects.create( - user=request.user, - action='settings_update', - message=f'Settings updated: custom_message="{custom_message[:50]}..."' - ) - - SecurityManager.log_security_event(request, "Settings updated successfully", "settings_update") - - return JsonResponse({ - 'status': 1, - 'message': 'Settings updated successfully' - }) - - except json.JSONDecodeError: - SecurityManager.record_failed_attempt(request, "Invalid JSON in settings update") - return JsonResponse({ - 'status': 0, - 'error_message': 'Invalid data format. Please try again.' - }, status=400) - except Exception as e: - SecurityManager.log_security_event(request, f"Error in update_settings: {str(e)}", "view_error") - return JsonResponse({'status': 0, 'error_message': 'An error occurred while updating settings.'}) - - -@admin_required -@secure_view(require_csrf=True, rate_limit=True, log_activity=True) -@require_http_methods(["POST"]) -def install_plugin(request): - """Install plugin (placeholder for future implementation)""" - try: - # Log the action - TestPluginLog.objects.create( - user=request.user, - action='plugin_install', - message='Plugin installation requested' - ) - - SecurityManager.log_security_event(request, "Plugin installation requested", "plugin_install") - - return JsonResponse({ - 'status': 1, - 'message': 'Plugin installation completed successfully' - }) - - except Exception as e: - SecurityManager.log_security_event(request, f"Error in install_plugin: {str(e)}", "view_error") - return JsonResponse({'status': 0, 'error_message': 'An error occurred during installation.'}) - - -@admin_required -@secure_view(require_csrf=True, rate_limit=True, log_activity=True) -@require_http_methods(["POST"]) -def uninstall_plugin(request): - """Uninstall plugin (placeholder for future implementation)""" - try: - # Log the action - TestPluginLog.objects.create( - user=request.user, - action='plugin_uninstall', - message='Plugin uninstallation requested' - ) - - SecurityManager.log_security_event(request, "Plugin uninstallation requested", "plugin_uninstall") - - return JsonResponse({ - 'status': 1, - 'message': 'Plugin uninstallation completed successfully' - }) - - except Exception as e: - SecurityManager.log_security_event(request, f"Error in uninstall_plugin: {str(e)}", "view_error") - return JsonResponse({'status': 0, 'error_message': 'An error occurred during uninstallation.'}) - - -@admin_required -@secure_view(require_csrf=False, rate_limit=True, log_activity=True) -def plugin_logs(request): - """View plugin logs""" - try: - # Only show logs for the current user (security isolation) - logs = TestPluginLog.objects.filter(user=request.user).order_by('-timestamp')[:50] - - context = { - 'logs': logs, - } - - proc = httpProc(request, 'testPlugin/plugin_logs.html', context, 'admin') - return proc.render() - - except Exception as e: - SecurityManager.log_security_event(request, f"Error in plugin_logs: {str(e)}", "view_error") - return JsonResponse({'status': 0, 'error_message': 'An error occurred while loading logs.'}) - - -@admin_required -@secure_view(require_csrf=False, rate_limit=True, log_activity=True) -def plugin_docs(request): - """View plugin documentation""" - try: - context = {} - - proc = httpProc(request, 'testPlugin/plugin_docs.html', context, 'admin') - return proc.render() - - except Exception as e: - SecurityManager.log_security_event(request, f"Error in plugin_docs: {str(e)}", "view_error") - return JsonResponse({'status': 0, 'error_message': 'An error occurred while loading documentation.'}) - - -@admin_required -@secure_view(require_csrf=False, rate_limit=True, log_activity=True) -def security_info(request): - """View security information""" - try: - context = {} - - proc = httpProc(request, 'testPlugin/security_info.html', context, 'admin') - return proc.render() - - except Exception as e: - SecurityManager.log_security_event(request, f"Error in security_info: {str(e)}", "view_error") - return JsonResponse({'status': 0, 'error_message': 'An error occurred while loading security information.'}) +@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 7ddc7e20d044339784573ef92ade3899820a2d08 Mon Sep 17 00:00:00 2001 From: master3395 Date: Sun, 4 Jan 2026 21:26:19 +0100 Subject: [PATCH 2/3] Add comprehensive plugin system documentation (PLUGINS.md) - Complete guide for plugin installation and management - Plugin development guide with code examples - Plugin structure and requirements documentation - TestPlugin reference guide - Best practices and troubleshooting sections - Author: master3395 --- guides/PLUGINS.md | 490 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 490 insertions(+) create mode 100644 guides/PLUGINS.md diff --git a/guides/PLUGINS.md b/guides/PLUGINS.md new file mode 100644 index 000000000..4fb75dc4e --- /dev/null +++ b/guides/PLUGINS.md @@ -0,0 +1,490 @@ +# CyberPanel Plugin System Guide + +**Author:** master3395 +**Version:** 1.0.0 +**Last Updated:** 2026-01-04 + +--- + +## Overview + +CyberPanel includes a plugin system that allows developers to extend the functionality of the control panel. Plugins can add new features, integrate with external services, and customize the user experience. + +This guide covers: +- Installing and managing plugins +- Developing your own plugins +- Plugin structure and requirements +- Using the testPlugin as a reference + +--- + +## Table of Contents + +1. [Plugin Installation](#plugin-installation) +2. [Plugin Management](#plugin-management) +3. [Plugin Development](#plugin-development) +4. [Plugin Structure](#plugin-structure) +5. [TestPlugin Reference](#testplugin-reference) +6. [Best Practices](#best-practices) +7. [Troubleshooting](#troubleshooting) + +--- + +## Plugin Installation + +### Prerequisites + +- CyberPanel installed and running +- Admin access to CyberPanel +- Server with appropriate permissions + +### Installation Steps + +1. **Access Plugin Manager** + - Log into CyberPanel as administrator + - Navigate to **Plugins** → **Installed Plugins** + - Click **Upload Plugin** button + +2. **Upload Plugin** + - Select the plugin ZIP file + - Click **Upload** + - Wait for upload to complete + +3. **Install Plugin** + - After upload, the plugin will appear in the plugin list + - Click **Install** button next to the plugin + - Installation process will: + - Extract plugin files to `/usr/local/CyberCP/` + - Add plugin to `INSTALLED_APPS` in `settings.py` + - Add URL routing to `urls.py` + - Run database migrations (if applicable) + - Collect static files + - Restart CyberPanel service + +4. **Verify Installation** + - Plugin should appear in the installed plugins list + - Status should show as "Installed" and "Enabled" + - Click **Manage** or **Settings** to access plugin interface + +### Manual Installation (Advanced) + +If automatic installation fails or you need to install manually: + +```bash +# 1. Extract plugin to CyberPanel directory +cd /home/cyberpanel/plugins +unzip plugin-name.zip +cd plugin-name + +# 2. Copy plugin to CyberPanel directory +cp -r plugin-name /usr/local/CyberCP/ + +# 3. Add to INSTALLED_APPS +# Edit /usr/local/CyberCP/CyberCP/settings.py +# Add 'pluginName', to INSTALLED_APPS list (alphabetically ordered) + +# 4. Add URL routing +# Edit /usr/local/CyberCP/CyberCP/urls.py +# Add before generic plugin route: +# path('plugins/pluginName/', include('pluginName.urls')), + +# 5. Run migrations (if plugin has models) +cd /usr/local/CyberCP +python3 manage.py makemigrations pluginName +python3 manage.py migrate pluginName + +# 6. Collect static files +python3 manage.py collectstatic --noinput + +# 7. Set proper permissions +chown -R cyberpanel:cyberpanel /usr/local/CyberCP/pluginName/ + +# 8. Restart CyberPanel +systemctl restart lscpd +``` + +--- + +## Plugin Management + +### Accessing Plugins + +- **Plugin List**: Navigate to **Plugins** → **Installed Plugins** +- **Plugin Settings**: Click **Manage** or **Settings** button for each plugin +- **Plugin URL**: Typically `/plugins/pluginName/` or `/plugins/pluginName/settings/` + +### Enabling/Disabling Plugins + +Plugins are automatically enabled after installation. To disable: +- Some plugins may have enable/disable functionality in their settings +- To fully disable, you can remove from `INSTALLED_APPS` (advanced) + +### Uninstalling Plugins + +1. Navigate to **Plugins** → **Installed Plugins** +2. Click **Uninstall** button (if available) +3. Manual uninstallation: + - Remove from `INSTALLED_APPS` in `settings.py` + - Remove URL routing from `urls.py` + - Remove plugin directory: `rm -rf /usr/local/CyberCP/pluginName/` + - Restart CyberPanel: `systemctl restart lscpd` + +--- + +## Plugin Development + +### Creating a New Plugin + +1. **Create Plugin Directory Structure** + ``` + pluginName/ + ├── __init__.py + ├── models.py # Database models (optional) + ├── views.py # View functions + ├── urls.py # URL routing + ├── forms.py # Forms (optional) + ├── utils.py # Utility functions (optional) + ├── admin.py # Admin interface (optional) + ├── templates/ # HTML templates + │ └── pluginName/ + │ └── settings.html + ├── static/ # Static files (CSS, JS, images) + │ └── pluginName/ + ├── migrations/ # Database migrations + │ └── __init__.py + ├── meta.xml # Plugin metadata (REQUIRED) + └── README.md # Plugin documentation + ``` + +2. **Create meta.xml** + ```xml + + + Plugin Name + Utility + Plugin description + 1.0.0 + /plugins/pluginName/ + /plugins/pluginName/settings/ + + ``` + +3. **Create URLs (urls.py)** + ```python + from django.urls import re_path + from . import views + + app_name = 'pluginName' # Important: Register namespace + + urlpatterns = [ + re_path(r'^$', views.main_view, name='main'), + re_path(r'^settings/$', views.settings_view, name='settings'), + ] + ``` + +4. **Create Views (views.py)** + ```python + from django.shortcuts import render, redirect + from functools import wraps + + def cyberpanel_login_required(view_func): + """Custom decorator for CyberPanel session authentication""" + @wraps(view_func) + def _wrapped_view(request, *args, **kwargs): + try: + userID = request.session['userID'] + return view_func(request, *args, **kwargs) + except KeyError: + from loginSystem.views import loadLoginPage + return redirect(loadLoginPage) + return _wrapped_view + + @cyberpanel_login_required + def main_view(request): + context = { + 'plugin_name': 'Plugin Name', + 'version': '1.0.0' + } + return render(request, 'pluginName/index.html', context) + + @cyberpanel_login_required + def settings_view(request): + context = { + 'plugin_name': 'Plugin Name', + 'version': '1.0.0' + } + return render(request, 'pluginName/settings.html', context) + ``` + +5. **Create Templates** + - Templates should extend `baseTemplate/index.html` + - Place templates in `templates/pluginName/` directory + ```html + {% extends "baseTemplate/index.html" %} + {% load static %} + {% load i18n %} + + {% block title %} + Plugin Name Settings - {% trans "CyberPanel" %} + {% endblock %} + + {% block content %} +
+

Plugin Name Settings

+ +
+ {% endblock %} + ``` + +6. **Package Plugin** + ```bash + cd /home/cyberpanel/plugins + zip -r pluginName.zip pluginName/ + ``` + +--- + +## Plugin Structure + +### Required Files + +- **meta.xml**: Plugin metadata (name, version, description, URLs) +- **__init__.py**: Python package marker +- **urls.py**: URL routing configuration +- **views.py**: View functions (at minimum, a main view) + +### Optional Files + +- **models.py**: Database models +- **forms.py**: Django forms +- **utils.py**: Utility functions +- **admin.py**: Django admin integration +- **templates/**: HTML templates +- **static/**: CSS, JavaScript, images +- **migrations/**: Database migrations + +### Template Requirements + +- Must extend `baseTemplate/index.html` (not `baseTemplate/base.html`) +- Use Django template tags: `{% load static %}`, `{% load i18n %}` +- Follow CyberPanel UI conventions + +### Authentication + +Plugins should use the custom `cyberpanel_login_required` decorator: +- Checks for `request.session['userID']` +- Redirects to login if not authenticated +- Compatible with CyberPanel's session-based authentication + +--- + +## TestPlugin Reference + +The **testPlugin** is a reference implementation included with CyberPanel that demonstrates: + +- Basic plugin structure +- Authentication handling +- Settings page implementation +- Clean URL routing +- Template inheritance + +### TestPlugin Location + +- **Source**: `/usr/local/CyberCP/testPlugin/` +- **URL**: `/plugins/testPlugin/` +- **Settings URL**: `/plugins/testPlugin/settings/` + +### TestPlugin Features + +1. **Main View**: Simple plugin information page +2. **Settings View**: Plugin settings interface +3. **Plugin Info API**: JSON endpoint for plugin information + +### Examining TestPlugin + +```bash +# View plugin structure +ls -la /usr/local/CyberCP/testPlugin/ + +# View meta.xml +cat /usr/local/CyberCP/testPlugin/meta.xml + +# View URLs +cat /usr/local/CyberCP/testPlugin/urls.py + +# View views +cat /usr/local/CyberCP/testPlugin/views.py + +# View templates +ls -la /usr/local/CyberCP/testPlugin/templates/testPlugin/ +``` + +### Using TestPlugin as Template + +1. Copy testPlugin directory: + ```bash + cp -r /usr/local/CyberCP/testPlugin /home/cyberpanel/plugins/myPlugin + ``` + +2. Rename files and update content: + - Update `meta.xml` with your plugin information + - Rename template files + - Update views with your functionality + - Update URLs as needed + +3. Customize for your needs: + - Add models if you need database storage + - Add forms for user input + - Add utilities for complex logic + - Add static files for styling + +--- + +## Best Practices + +### Security + +- Always use `cyberpanel_login_required` decorator for views +- Validate and sanitize all user input +- Use Django's form validation +- Follow CyberPanel security guidelines +- Never expose sensitive information in templates or responses + +### Code Organization + +- Keep files under 500 lines (split into modules if needed) +- Use descriptive function and variable names +- Add comments for complex logic +- Follow Python PEP 8 style guide +- Organize code into logical modules + +### Templates + +- Always extend `baseTemplate/index.html` +- Use CyberPanel's existing CSS classes +- Make templates mobile-friendly +- Use Django's internationalization (`{% trans %}`) +- Keep templates clean and readable + +### Database + +- Use Django migrations for schema changes +- Add proper indexes for performance +- Use transactions for multi-step operations +- Clean up old data when uninstalling + +### Testing + +- Test plugin installation process +- Test all plugin functionality +- Test error handling +- Test on multiple CyberPanel versions +- Test plugin uninstallation + +### Documentation + +- Include README.md with installation instructions +- Document all configuration options +- Provide usage examples +- Include troubleshooting section +- Document any dependencies + +--- + +## Troubleshooting + +### Plugin Not Appearing in List + +- Check `meta.xml` format (must be valid XML) +- Verify plugin directory exists in `/home/cyberpanel/plugins/` +- Check CyberPanel logs: `tail -f /var/log/cyberpanel/error.log` + +### Plugin Installation Fails + +- Check file permissions: `ls -la /usr/local/CyberCP/pluginName/` +- Verify `INSTALLED_APPS` entry in `settings.py` +- Check URL routing in `urls.py` +- Review installation logs +- Check Python syntax: `python3 -m py_compile pluginName/views.py` + +### Plugin Settings Page Not Loading + +- Verify URL routing is correct +- Check template path (must be `templates/pluginName/settings.html`) +- Verify template extends `baseTemplate/index.html` +- Check for JavaScript errors in browser console +- Verify authentication decorator is used + +### Template Not Found Error + +- Check template directory structure: `templates/pluginName/` +- Verify template name in `render()` call matches file name +- Ensure template extends correct base template +- Check template syntax for errors + +### Authentication Issues + +- Verify `cyberpanel_login_required` decorator is used +- Check session is active: `request.session.get('userID')` +- Verify user is logged into CyberPanel +- Check redirect logic in decorator + +### Static Files Not Loading + +- Run `python3 manage.py collectstatic` +- Check static file URLs in templates +- Verify static files are in `static/pluginName/` directory +- Clear browser cache + +### Database Migration Issues + +- Check migrations directory exists: `migrations/__init__.py` +- Verify models are properly defined +- Run migrations: `python3 manage.py makemigrations pluginName` +- Apply migrations: `python3 manage.py migrate pluginName` + +### Plugin Conflicts + +- Check for duplicate plugin names +- Verify URL patterns don't conflict +- Check for namespace conflicts in `urls.py` +- Review `INSTALLED_APPS` for duplicate entries + +--- + +## Plugin Examples + +### Available Plugins + +1. **testPlugin**: Reference implementation for plugin development +2. **discordWebhooks**: Server monitoring and notifications via Discord +3. **fail2ban**: Fail2ban security manager for CyberPanel + +### Plugin Repository + +Community plugins are available at: +- GitHub: https://github.com/master3395/cyberpanel-plugins + +--- + +## Additional Resources + +- **CyberPanel Documentation**: https://cyberpanel.net/KnowledgeBase/ +- **Django Documentation**: https://docs.djangoproject.com/ +- **Plugin System Source**: `/usr/local/CyberCP/pluginInstaller/` +- **Plugin Holder**: `/usr/local/CyberCP/pluginHolder/` + +--- + +## Support + +For plugin development questions: +- Check existing plugins for examples +- Review CyberPanel source code +- Ask in CyberPanel community forum +- Create an issue on GitHub + +--- + +**Last Updated:** 2026-01-04 +**Author:** master3395 From 6a0d3fd443380d12148a205f040b8d68ecbe48c3 Mon Sep 17 00:00:00 2001 From: master3395 Date: Sun, 4 Jan 2026 21:29:24 +0100 Subject: [PATCH 3/3] Add PLUGINS.md reference to INDEX.md - Added Plugin System Guide to Plugins & Extensions section - Added plugin system to Feature-Specific Guides section - Author: master3395 --- guides/INDEX.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/guides/INDEX.md b/guides/INDEX.md index 94100e9d8..2e2ac389b 100644 --- a/guides/INDEX.md +++ b/guides/INDEX.md @@ -43,6 +43,9 @@ Welcome to the CyberPanel documentation hub! This folder contains all guides, tu ### 🎨 Customization & Design - **[Custom CSS Guide](CUSTOM_CSS_GUIDE.md)** - Complete guide for creating custom CSS that works with CyberPanel 2.5.5-dev design system +### 🔌 Plugins & Extensions +- **[Plugin System Guide](PLUGINS.md)** - Complete guide to CyberPanel plugin system, development, testPlugin reference, and plugin management + ### 🔐 Security & Authentication - **[2FA Authentication Guide](2FA_AUTHENTICATION_GUIDE.md)** - Complete guide for Two-Factor Authentication and WebAuthn/Passkey setup @@ -99,6 +102,7 @@ Welcome to the CyberPanel documentation hub! This folder contains all guides, tu - **Email Marketing**: [Mautic Installation Guide](MAUTIC_INSTALLATION_GUIDE.md) - **Storage Management**: [Home Directory Management Guide](HOME_DIRECTORY_MANAGEMENT_GUIDE.md) - **Customization & Design**: [Custom CSS Guide](CUSTOM_CSS_GUIDE.md) +- **Plugin System**: [Plugin System Guide](PLUGINS.md) - **Command Line Interface**: [CLI Command Reference](CLI_COMMAND_REFERENCE.md) - **Development**: [Contributing Guide](CONTRIBUTING.md)