Merge pull request #1640 from master3395/v2.5.5-dev

V2.5.5 dev plugin installation and management
This commit is contained in:
Master3395 2026-01-04 21:33:07 +01:00 committed by GitHub
commit e5033be0ad
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 1156 additions and 356 deletions

View File

@ -190,7 +190,8 @@ class secMiddleware:
pathActual.find('saveSpamAssassinConfigurations') > -1 or pathActual.find('saveSpamAssassinConfigurations') > -1 or
pathActual.find('docker') > -1 or pathActual.find('cloudAPI') > -1 or pathActual.find('docker') > -1 or pathActual.find('cloudAPI') > -1 or
pathActual.find('verifyLogin') > -1 or pathActual.find('submitUserCreation') > -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: if isAPIEndpoint:
# For API endpoints, still check for the most dangerous command injection characters # 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 'mailServer', # Depends on websiteFunctions, ChildDomains
# Apps with multiple or complex dependencies # Apps with multiple or complex dependencies
'emailPremium', # Depends on mailServer 'emailPremium',
'discordWebhooks', # Depends on mailServer
'testPlugin', # Test plugin
'emailMarketing', # Depends on websiteFunctions and loginSystem 'emailMarketing', # Depends on websiteFunctions and loginSystem
'cloudAPI', # Depends on websiteFunctions 'cloudAPI', # Depends on websiteFunctions
'containerization', # Depends on websiteFunctions 'containerization', # Depends on websiteFunctions
@ -126,7 +128,7 @@ DATABASES = {
'ENGINE': 'django.db.backends.mysql', 'ENGINE': 'django.db.backends.mysql',
'NAME': 'cyberpanel', 'NAME': 'cyberpanel',
'USER': 'cyberpanel', 'USER': 'cyberpanel',
'PASSWORD': 'SLTUIUxqhulwsh', 'PASSWORD': '1XTy1XOV0BZPnM',
'HOST': 'localhost', 'HOST': 'localhost',
'PORT': '' 'PORT': ''
}, },
@ -134,7 +136,7 @@ DATABASES = {
'ENGINE': 'django.db.backends.mysql', 'ENGINE': 'django.db.backends.mysql',
'NAME': 'mysql', 'NAME': 'mysql',
'USER': 'root', 'USER': 'root',
'PASSWORD': 'SLTUIUxqhulwsh', 'PASSWORD': '1XTy1XOV0BZPnM',
'HOST': 'localhost', 'HOST': 'localhost',
'PORT': '', 'PORT': '',
}, },
@ -211,6 +213,10 @@ DATA_UPLOAD_MAX_MEMORY_SIZE = 2147483648
# Security settings # Security settings
X_FRAME_OPTIONS = 'SAMEORIGIN' X_FRAME_OPTIONS = 'SAMEORIGIN'
# Login URL - CyberPanel uses root path for login
LOGIN_URL = '/'
LOGIN_REDIRECT_URL = '/'
# Default primary key field type # Default primary key field type
# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

View File

@ -40,7 +40,8 @@ urlpatterns = [
path('filemanager/', include('filemanager.urls')), path('filemanager/', include('filemanager.urls')),
path('emailPremium/', include('emailPremium.urls')), path('emailPremium/', include('emailPremium.urls')),
path('manageservices/', include('manageServices.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('emailMarketing/', include('emailMarketing.urls')),
path('cloudAPI/', include('cloudAPI.urls')), path('cloudAPI/', include('cloudAPI.urls')),
path('docker/', include('dockerManager.urls')), path('docker/', include('dockerManager.urls')),

View File

@ -43,6 +43,9 @@ Welcome to the CyberPanel documentation hub! This folder contains all guides, tu
### 🎨 Customization & Design ### 🎨 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 - **[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 ### 🔐 Security & Authentication
- **[2FA Authentication Guide](2FA_AUTHENTICATION_GUIDE.md)** - Complete guide for Two-Factor Authentication and WebAuthn/Passkey setup - **[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) - **Email Marketing**: [Mautic Installation Guide](MAUTIC_INSTALLATION_GUIDE.md)
- **Storage Management**: [Home Directory Management Guide](HOME_DIRECTORY_MANAGEMENT_GUIDE.md) - **Storage Management**: [Home Directory Management Guide](HOME_DIRECTORY_MANAGEMENT_GUIDE.md)
- **Customization & Design**: [Custom CSS Guide](CUSTOM_CSS_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) - **Command Line Interface**: [CLI Command Reference](CLI_COMMAND_REFERENCE.md)
- **Development**: [Contributing Guide](CONTRIBUTING.md) - **Development**: [Contributing Guide](CONTRIBUTING.md)

490
guides/PLUGINS.md Normal file
View File

@ -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
<?xml version="1.0" encoding="UTF-8"?>
<cyberpanelPluginConfig>
<name>Plugin Name</name>
<type>Utility</type>
<description>Plugin description</description>
<version>1.0.0</version>
<url>/plugins/pluginName/</url>
<settings_url>/plugins/pluginName/settings/</settings_url>
</cyberpanelPluginConfig>
```
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 %}
<div class="container">
<h1>Plugin Name Settings</h1>
<!-- Your plugin content here -->
</div>
{% 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

View File

@ -1,28 +1,339 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from django.shortcuts import render 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 from plogical.mailUtilities import mailUtilities
import os import os
import subprocess
import shlex
import json
from xml.etree import ElementTree from xml.etree import ElementTree
from plogical.httpProc import httpProc 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): def installed(request):
mailUtilities.checkHome() mailUtilities.checkHome()
pluginPath = '/home/cyberpanel/plugins' pluginPath = '/home/cyberpanel/plugins'
pluginList = [] pluginList = []
errorPlugins = []
if os.path.exists(pluginPath): if os.path.exists(pluginPath):
for plugin in os.listdir(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 = {} data = {}
# Try installed location first, then fallback to source location
completePath = '/usr/local/CyberCP/' + plugin + '/meta.xml' 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 pluginList.append(data)
data['type'] = pluginMetaData.find('type').text except ElementTree.ParseError as e:
data['desc'] = pluginMetaData.find('description').text errorPlugins.append({'name': plugin, 'error': f'XML parse error: {str(e)}'})
data['version'] = pluginMetaData.find('version').text logging.writeToFile(f"Plugin {plugin}: XML parse error - {str(e)}")
continue
pluginList.append(data) 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', proc = httpProc(request, 'pluginHolder/plugins.html',
{'plugins': pluginList}, 'admin') {'plugins': pluginList, 'error_plugins': errorPlugins}, 'admin')
return proc.render() 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+ Generate URL pattern compatible with both Django 2.x and 3.x+
Django 2.x uses url() with regex patterns Django 2.x uses url() with regex patterns
Django 3.x+ prefers path() with simpler patterns Django 3.x+ prefers path() with simpler patterns
Plugins are routed under /plugins/pluginName/ to match meta.xml URLs
""" """
try: try:
django_version = django.get_version() django_version = django.get_version()
@ -28,17 +29,17 @@ class pluginInstaller:
pluginInstaller.stdOut(f"Django version detected: {django_version}") pluginInstaller.stdOut(f"Django version detected: {django_version}")
if major_version >= 3: 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") 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: 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") 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: except Exception as e:
# Fallback to modern path() syntax if version detection fails # Fallback to modern path() syntax if version detection fails
pluginInstaller.stdOut(f"Django version detection failed: {str(e)}, using path() syntax as fallback") 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 @staticmethod
def stdOut(message): def stdOut(message):
@ -59,8 +60,14 @@ class pluginInstaller:
@staticmethod @staticmethod
def extractPlugin(pluginName): def extractPlugin(pluginName):
pathToPlugin = pluginName + '.zip' pathToPlugin = pluginName + '.zip'
command = 'unzip ' + pathToPlugin + ' -d /usr/local/CyberCP' command = 'unzip -o ' + pathToPlugin + ' -d /usr/local/CyberCP'
subprocess.call(shlex.split(command)) 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 @staticmethod
def upgradingSettingsFile(pluginName): def upgradingSettingsFile(pluginName):
@ -78,16 +85,38 @@ class pluginInstaller:
@staticmethod @staticmethod
def upgradingURLs(pluginName): 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() data = open("/usr/local/CyberCP/CyberCP/urls.py", 'r').readlines()
writeToFile = open("/usr/local/CyberCP/CyberCP/urls.py", 'w') writeToFile = open("/usr/local/CyberCP/CyberCP/urls.py", 'w')
urlPatternAdded = False
for items in data: 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(items)
writeToFile.writelines(pluginInstaller.getUrlPattern(pluginName))
else: else:
writeToFile.writelines(items) 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() writeToFile.close()
@staticmethod @staticmethod

View File

@ -2,8 +2,8 @@
<plugin> <plugin>
<name>Test Plugin</name> <name>Test Plugin</name>
<type>Utility</type> <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> <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> <author>CyberPanel Development Team</author>
<website>https://github.com/cyberpanel/testPlugin</website> <website>https://github.com/cyberpanel/testPlugin</website>
<license>MIT</license> <license>MIT</license>
@ -21,4 +21,6 @@
<popup_messages>true</popup_messages> <popup_messages>true</popup_messages>
<inline_integration>true</inline_integration> <inline_integration>true</inline_integration>
</settings> </settings>
<url>/plugins/testPlugin/</url>
<settings_url>/plugins/testPlugin/settings/</settings_url>
</plugin> </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 django.urls import path
from . import views from . import views
app_name = 'testPlugin'
urlpatterns = [ urlpatterns = [
path('', views.plugin_home, name='plugin_home'), path('', views.test_plugin_view, name='testPlugin'),
path('test/', views.test_button, name='test_button'), path('info/', views.plugin_info_view, name='testPluginInfo'),
path('toggle/', views.toggle_plugin, name='toggle_plugin'), path('settings/', views.settings_view, name='testPluginSettings'),
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'),
] ]

View File

@ -1,324 +1,54 @@
# -*- coding: utf-8 -*- from django.shortcuts import render, redirect
import json from django.http import JsonResponse
import os from functools import wraps
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
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 @cyberpanel_login_required
@secure_view(require_csrf=False, rate_limit=True, log_activity=True) def test_plugin_view(request):
def plugin_home(request): """
"""Main plugin page with inline integration""" Main view for the test plugin
try: """
# Get or create plugin settings context = {
settings, created = TestPluginSettings.objects.get_or_create( 'plugin_name': 'Test Plugin',
user=request.user, 'version': '1.0.0',
defaults={'plugin_enabled': True} 'description': 'A simple test plugin for CyberPanel'
) }
return render(request, 'testPlugin/index.html', context)
# 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 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 @cyberpanel_login_required
@secure_view(require_csrf=True, rate_limit=True, log_activity=True) def settings_view(request):
@require_http_methods(["POST"]) """
def test_button(request): Settings page for the test plugin
"""Handle test button click and show popup message""" """
try: context = {
settings, created = TestPluginSettings.objects.get_or_create( 'plugin_name': 'Test Plugin',
user=request.user, 'version': '1.0.0',
defaults={'plugin_enabled': True} 'description': 'A simple test plugin for CyberPanel'
) }
return render(request, 'testPlugin/settings.html', context)
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.'})