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
This commit is contained in:
master3395
2026-01-04 21:04:51 +01:00
parent b1adb8f52e
commit ed7d4743b6
10 changed files with 662 additions and 356 deletions

View File

@@ -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

View File

@@ -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'

View File

@@ -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')),

View File

@@ -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 <plugin> and <cyberpanelPluginConfig> 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)

View File

@@ -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

View File

@@ -2,8 +2,8 @@
<plugin>
<name>Test Plugin</name>
<type>Utility</type>
<description>A comprehensive test plugin for CyberPanel with enable/disable functionality, test button, popup messages, and inline integration</description>
<version>1.0.0</version>
<description>A comprehensive test plugin for CyberPanel with enable/disable functionality, test button, popup messages, and inline integration</description>
<author>CyberPanel Development Team</author>
<website>https://github.com/cyberpanel/testPlugin</website>
<license>MIT</license>
@@ -21,4 +21,6 @@
<popup_messages>true</popup_messages>
<inline_integration>true</inline_integration>
</settings>
<url>/plugins/testPlugin/</url>
<settings_url>/plugins/testPlugin/settings/</settings_url>
</plugin>

View File

@@ -0,0 +1,71 @@
{% extends "baseTemplate/base.html" %}
{% load static %}
{% load i18n %}
{% block title %}
Test Plugin - {% trans "CyberPanel" %}
{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row">
<div class="col-lg-12">
<div class="card">
<div class="card-header">
<h3 class="card-title">
<i class="fas fa-cog"></i>
{% trans "Test Plugin Dashboard" %}
</h3>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<div class="info-box">
<span class="info-box-icon bg-info">
<i class="fas fa-info-circle"></i>
</span>
<div class="info-box-content">
<span class="info-box-text">{% trans "Plugin Name" %}</span>
<span class="info-box-number">{{ plugin_name }}</span>
</div>
</div>
</div>
<div class="col-md-6">
<div class="info-box">
<span class="info-box-icon bg-success">
<i class="fas fa-tag"></i>
</span>
<div class="info-box-content">
<span class="info-box-text">{% trans "Version" %}</span>
<span class="info-box-number">{{ version }}</span>
</div>
</div>
</div>
</div>
<div class="alert alert-info">
<h4><i class="icon fa fa-info"></i> {% trans "Plugin Information" %}</h4>
<p>{{ description }}</p>
</div>
<div class="card">
<div class="card-header">
<h3 class="card-title">
<i class="fas fa-check-circle"></i>
{% trans "Test Plugin Status" %}
</h3>
</div>
<div class="card-body">
<div class="alert alert-success">
<i class="fas fa-check"></i>
{% trans "Test Plugin is working correctly!" %}
</div>
<p>{% trans "This is a test plugin created for testing CyberPanel plugin functionality." %}</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,165 @@
{% extends "baseTemplate/index.html" %}
{% load static %}
{% load i18n %}
{% block title %}
Test Plugin Settings - {% trans "CyberPanel" %}
{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row">
<div class="col-lg-12">
<div class="card">
<div class="card-header">
<h3 class="card-title">
<i class="fas fa-cog"></i>
{% trans "Test Plugin Settings" %}
</h3>
</div>
<div class="card-body">
<div class="alert alert-info">
<i class="fas fa-info-circle"></i>
<strong>{% trans "Plugin Information" %}</strong>
<ul class="mb-0 mt-2">
<li><strong>{% trans "Name" %}:</strong> {{ plugin_name }}</li>
<li><strong>{% trans "Version" %}:</strong> {{ version }}</li>
<li><strong>{% trans "Status" %}:</strong> <span class="badge badge-success">{% trans "Active" %}</span></li>
</ul>
</div>
<div class="card">
<div class="card-header">
<h3 class="card-title">
<i class="fas fa-sliders-h"></i>
{% trans "Configuration Options" %}
</h3>
</div>
<div class="card-body">
<form method="post" action="">
{% csrf_token %}
<div class="form-group">
<label for="test_setting_1">
<i class="fas fa-toggle-on"></i>
{% trans "Enable Test Feature" %}
</label>
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="test_setting_1" name="test_setting_1" checked>
<label class="custom-control-label" for="test_setting_1">
{% trans "Enable this test feature" %}
</label>
</div>
<small class="form-text text-muted">
{% trans "This is a test setting for demonstration purposes." %}
</small>
</div>
<div class="form-group">
<label for="test_setting_2">
<i class="fas fa-text-width"></i>
{% trans "Test Text Input" %}
</label>
<input type="text" class="form-control" id="test_setting_2" name="test_setting_2" placeholder="{% trans 'Enter test value' %}" value="Test Value">
<small class="form-text text-muted">
{% trans "This is a test text input field." %}
</small>
</div>
<div class="form-group">
<label for="test_setting_3">
<i class="fas fa-list"></i>
{% trans "Test Select Option" %}
</label>
<select class="form-control" id="test_setting_3" name="test_setting_3">
<option value="option1">{% trans "Option 1" %}</option>
<option value="option2" selected>{% trans "Option 2" %}</option>
<option value="option3">{% trans "Option 3" %}</option>
</select>
<small class="form-text text-muted">
{% trans "Select a test option from the dropdown." %}
</small>
</div>
<div class="form-group">
<button type="submit" class="btn btn-primary">
<i class="fas fa-save"></i>
{% trans "Save Settings" %}
</button>
<button type="reset" class="btn btn-secondary">
<i class="fas fa-undo"></i>
{% trans "Reset" %}
</button>
</div>
</form>
</div>
</div>
<div class="card">
<div class="card-header">
<h3 class="card-title">
<i class="fas fa-check-circle"></i>
{% trans "Plugin Status" %}
</h3>
</div>
<div class="card-body">
<div class="alert alert-success">
<i class="fas fa-check"></i>
<strong>{% trans "Plugin is Active" %}</strong>
<p class="mb-0 mt-2">{% trans "The Test Plugin is installed and working correctly." %}</p>
</div>
<div class="row">
<div class="col-md-6">
<div class="info-box">
<span class="info-box-icon bg-info">
<i class="fas fa-info-circle"></i>
</span>
<div class="info-box-content">
<span class="info-box-text">{% trans "Plugin Name" %}</span>
<span class="info-box-number">{{ plugin_name }}</span>
</div>
</div>
</div>
<div class="col-md-6">
<div class="info-box">
<span class="info-box-icon bg-success">
<i class="fas fa-tag"></i>
</span>
<div class="info-box-content">
<span class="info-box-text">{% trans "Version" %}</span>
<span class="info-box-number">{{ version }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h3 class="card-title">
<i class="fas fa-question-circle"></i>
{% trans "About This Plugin" %}
</h3>
</div>
<div class="card-body">
<p>{{ description }}</p>
<p>{% 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." %}</p>
<h5>{% trans "Features" %}</h5>
<ul>
<li>{% trans "Enable/disable functionality" %}</li>
<li>{% trans "Test button" %}</li>
<li>{% trans "Popup messages" %}</li>
<li>{% trans "Inline integration" %}</li>
<li>{% trans "Settings page" %}</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -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'),
]

View File

@@ -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)