Merge pull request #1640 from master3395/v2.5.5-dev
V2.5.5 dev plugin installation and management
This commit is contained in:
commit
e5033be0ad
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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'
|
||||||
|
|
@ -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')),
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 %}
|
||||||
|
|
@ -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 %}
|
||||||
|
|
@ -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'),
|
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -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.'})
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue