From 82d68ab5a3de155560233411caafe0655fecfb65 Mon Sep 17 00:00:00 2001 From: master3395 Date: Mon, 19 Jan 2026 22:55:59 +0100 Subject: [PATCH 01/27] Add Modify Date column, GitHub commit date fetching, and plugin store caching - Added Modify Date column to both Table View and Plugin Store - Implemented GitHub API integration to fetch last commit dates - Added caching system for plugin store to prevent rate limit errors - Enhanced plugin store with installed/enabled status enrichment - Added comprehensive plugin development guide - Updated testPlugin meta.xml author to usmannasir --- docs/PLUGIN_DEVELOPMENT_GUIDE.md | 1466 +++++++++++++++++ pluginHolder/templates/pluginHolder/help.html | 658 ++++++++ .../templates/pluginHolder/plugins.html | 1250 +++++++++++++- pluginHolder/urls.py | 4 + pluginHolder/views.py | 331 +++- testPlugin/meta.xml | 2 +- 6 files changed, 3672 insertions(+), 39 deletions(-) create mode 100644 docs/PLUGIN_DEVELOPMENT_GUIDE.md create mode 100644 pluginHolder/templates/pluginHolder/help.html diff --git a/docs/PLUGIN_DEVELOPMENT_GUIDE.md b/docs/PLUGIN_DEVELOPMENT_GUIDE.md new file mode 100644 index 000000000..a5a3abe93 --- /dev/null +++ b/docs/PLUGIN_DEVELOPMENT_GUIDE.md @@ -0,0 +1,1466 @@ +# CyberPanel Plugin Development Guide + +**Author:** master3395 +**Version:** 2.0.0 +**Last Updated:** 2026-01-04 +**Repository:** https://github.com/master3395/cyberpanel/tree/v2.5.5-dev + +--- + +## Table of Contents + +1. [Introduction](#introduction) +2. [Prerequisites](#prerequisites) +3. [Plugin Architecture Overview](#plugin-architecture-overview) +4. [Creating Your First Plugin](#creating-your-first-plugin) +5. [Plugin Structure & Files](#plugin-structure--files) +6. [Core Components](#core-components) +7. [Advanced Features](#advanced-features) +8. [Best Practices](#best-practices) +9. [Security Guidelines](#security-guidelines) +10. [Testing & Debugging](#testing--debugging) +11. [Packaging & Distribution](#packaging--distribution) +12. [Troubleshooting](#troubleshooting) +13. [Examples & References](#examples--references) + +--- + +## Introduction + +CyberPanel's plugin system allows developers to extend the control panel's functionality with custom features. Plugins integrate seamlessly with CyberPanel's Django-based architecture, providing access to the full power of the platform while maintaining security and consistency. + +### What Can Plugins Do? + +- Add new administrative features +- Integrate with external services (APIs, webhooks, etc.) +- Customize the user interface +- Extend database functionality +- Add automation and monitoring capabilities +- Create custom reporting tools +- Integrate security features + +### Plugin Types + +- **Utility Plugins**: General-purpose tools and helpers +- **Security Plugins**: Security enhancements and monitoring +- **Performance Plugins**: Optimization and caching tools +- **Backup Plugins**: Backup and restore functionality +- **Integration Plugins**: Third-party service integrations + +--- + +## Prerequisites + +### Required Knowledge + +- **Python 3.6+**: Basic to intermediate Python knowledge +- **Django Framework**: Understanding of Django views, URLs, templates, and models +- **HTML/CSS/JavaScript**: For creating user interfaces +- **Linux/Unix**: Basic command-line familiarity +- **XML**: Understanding of XML structure for `meta.xml` + +### Required Tools + +- CyberPanel installed and running +- Admin access to CyberPanel +- SSH access to the server +- Text editor (vim, nano, VS Code, etc.) +- Git (optional, for version control) + +### System Requirements + +- CyberPanel v2.0.0 or higher +- Python 3.6 or higher +- Django (included with CyberPanel) +- Sufficient disk space for plugin files +- Proper file permissions + +--- + +## Plugin Architecture Overview + +### How Plugins Work + +1. **Plugin Source Location**: `/home/cyberpanel/plugins/` + - Plugins are stored here before installation + - Can be uploaded as ZIP files or placed directly + +2. **Installed Location**: `/usr/local/CyberCP/` + - After installation, plugins are copied here + - This is where CyberPanel loads plugins from + +3. **Integration Points**: + - `INSTALLED_APPS` in `settings.py`: Registers plugin as Django app + - `urls.py`: Adds plugin URL routes + - `meta.xml`: Provides plugin metadata to CyberPanel + +4. **Plugin Lifecycle**: + ``` + Upload → Extract → Install → Enable → Use → (Optional: Disable) → Uninstall + ``` + +### Plugin Registration Flow + +``` +1. Plugin placed in /home/cyberpanel/plugins/pluginName/ +2. meta.xml is read by CyberPanel +3. User clicks "Install" in CyberPanel UI +4. PluginInstaller.installPlugin() is called +5. Plugin files copied to /usr/local/CyberCP/pluginName/ +6. Plugin added to INSTALLED_APPS in settings.py +7. URL routes added to urls.py +8. Database migrations run (if applicable) +9. Static files collected +10. CyberPanel service restarted +11. Plugin appears in "Installed Plugins" list +``` + +--- + +## Creating Your First Plugin + +### Step 1: Create Plugin Directory Structure + +```bash +# Navigate to plugins directory +cd /home/cyberpanel/plugins + +# Create your plugin directory +mkdir myFirstPlugin +cd myFirstPlugin + +# Create required subdirectories +mkdir -p templates/myFirstPlugin +mkdir -p static/myFirstPlugin/css +mkdir -p static/myFirstPlugin/js +mkdir -p static/myFirstPlugin/images +mkdir -p migrations +``` + +### Step 2: Create Required Files + +#### 2.1 Create `__init__.py` + +```python +# __init__.py +# This file makes Python treat the directory as a package +``` + +#### 2.2 Create `meta.xml` (REQUIRED) + +```xml + + + My First Plugin + Utility + A simple example plugin to demonstrate CyberPanel plugin development + 1.0.0 + /plugins/myFirstPlugin/ + /plugins/myFirstPlugin/settings/ + Your Name + https://yourwebsite.com + +``` + +**meta.xml Fields Explained:** + +- ``: Display name of your plugin (required) +- ``: Plugin category: Utility, Security, Performance, Backup, or custom (optional) +- ``: Brief description shown in plugin list (required) +- ``: Version number (e.g., 1.0.0) (required) +- ``: Main plugin URL route (required) +- ``: Settings page URL (optional, but recommended) +- ``: Plugin author name (optional) +- ``: Author or plugin website (optional) + +#### 2.3 Create `urls.py` + +```python +# urls.py +from django.urls import path, re_path +from . import views + +# IMPORTANT: Register namespace for URL reverse lookups +app_name = 'myFirstPlugin' + +urlpatterns = [ + # Main plugin page + path('', views.main_view, name='main'), + + # Settings page + path('settings/', views.settings_view, name='settings'), + + # Example API endpoint + path('api/info/', views.api_info, name='api_info'), +] +``` + +#### 2.4 Create `views.py` + +```python +# views.py +from django.shortcuts import render, redirect +from django.http import JsonResponse +from functools import wraps + +def cyberpanel_login_required(view_func): + """ + Custom decorator for CyberPanel session authentication. + Always use this decorator for plugin views to ensure users are logged in. + """ + @wraps(view_func) + def _wrapped_view(request, *args, **kwargs): + try: + # Check if user is logged in via CyberPanel session + userID = request.session['userID'] + return view_func(request, *args, **kwargs) + except KeyError: + # User not logged in, redirect to login page + from loginSystem.views import loadLoginPage + return redirect(loadLoginPage) + return _wrapped_view + + +@cyberpanel_login_required +def main_view(request): + """ + Main plugin page view. + This is the entry point when users access /plugins/myFirstPlugin/ + """ + context = { + 'plugin_name': 'My First Plugin', + 'version': '1.0.0', + 'message': 'Welcome to your first CyberPanel plugin!' + } + return render(request, 'myFirstPlugin/main.html', context) + + +@cyberpanel_login_required +def settings_view(request): + """ + Plugin settings page view. + Accessible at /plugins/myFirstPlugin/settings/ + """ + context = { + 'plugin_name': 'My First Plugin', + 'version': '1.0.0' + } + return render(request, 'myFirstPlugin/settings.html', context) + + +@cyberpanel_login_required +def api_info(request): + """ + Example API endpoint that returns JSON data. + Accessible at /plugins/myFirstPlugin/api/info/ + """ + data = { + 'plugin_name': 'My First Plugin', + 'version': '1.0.0', + 'status': 'active', + 'message': 'Plugin is working correctly!' + } + return JsonResponse(data) +``` + +#### 2.5 Create Templates + +**`templates/myFirstPlugin/main.html`:** + +```html +{% extends "baseTemplate/index.html" %} +{% load static %} +{% load i18n %} + +{% block title %} + My First Plugin - {% trans "CyberPanel" %} +{% endblock %} + +{% block header_scripts %} + +{% endblock %} + +{% block content %} +
+
+

{% trans "My First Plugin" %}

+

Version {{ version }}

+
+ +
+

{% trans "Welcome!" %}

+

{{ message }}

+ + +
+
+{% endblock %} +``` + +**`templates/myFirstPlugin/settings.html`:** + +```html +{% extends "baseTemplate/index.html" %} +{% load static %} +{% load i18n %} + +{% block title %} + My First Plugin Settings - {% trans "CyberPanel" %} +{% endblock %} + +{% block content %} +
+
+
+

{% trans "Plugin Settings" %}

+
+
+

{% trans "Settings page for My First Plugin" %}

+ +
+
+
+{% endblock %} +``` + +### Step 3: Test Your Plugin Locally + +Before installing, verify your plugin structure: + +```bash +# Check file structure +cd /home/cyberpanel/plugins/myFirstPlugin +tree -L 3 + +# Verify Python syntax +python3 -m py_compile views.py urls.py + +# Check XML validity +xmllint --noout meta.xml +``` + +### Step 4: Install Your Plugin + +1. **Via CyberPanel UI:** + - Navigate to **Plugins** → **Installed Plugins** + - Your plugin should appear in the list + - Click **Install** button + - Wait for installation to complete + +2. **Verify Installation:** + ```bash + # Check if plugin was copied + ls -la /usr/local/CyberCP/myFirstPlugin/ + + # Check if added to INSTALLED_APPS + grep -i "myFirstPlugin" /usr/local/CyberCP/CyberCP/settings.py + + # Check if URLs were added + grep -i "myFirstPlugin" /usr/local/CyberCP/CyberCP/urls.py + ``` + +3. **Access Your Plugin:** + - Navigate to `/plugins/myFirstPlugin/` in CyberPanel + - Or click **Manage** button in Installed Plugins list + +--- + +## Plugin Structure & Files + +### Complete Directory Structure + +``` +pluginName/ +├── __init__.py # Python package marker (required) +├── apps.py # Django app configuration (optional) +├── models.py # Database models (optional) +├── views.py # View functions (required) +├── urls.py # URL routing (required) +├── forms.py # Django forms (optional) +├── admin.py # Django admin integration (optional) +├── utils.py # Utility functions (optional) +├── signals.py # Django signals (optional) +├── tests.py # Unit tests (optional) +├── meta.xml # Plugin metadata (REQUIRED) +├── README.md # Plugin documentation (recommended) +├── requirements.txt # Python dependencies (optional) +├── pre_install # Pre-installation script (optional) +├── post_install # Post-installation script (optional) +├── pre_remove # Pre-removal script (optional) +├── templates/ # HTML templates +│ └── pluginName/ +│ ├── main.html +│ ├── settings.html +│ └── other_templates.html +├── static/ # Static files (CSS, JS, images) +│ └── pluginName/ +│ ├── css/ +│ │ └── style.css +│ ├── js/ +│ │ └── script.js +│ └── images/ +│ └── logo.png +└── migrations/ # Database migrations + └── __init__.py +``` + +### File Descriptions + +#### Required Files + +1. **`__init__.py`** + - Makes directory a Python package + - Can be empty or contain initialization code + +2. **`meta.xml`** + - Plugin metadata for CyberPanel + - Must be valid XML + - Required fields: name, description, version + +3. **`urls.py`** + - URL routing configuration + - Must define `app_name` for namespace + - Maps URLs to view functions + +4. **`views.py`** + - Contains view functions + - Must have at least one view + - Should use `@cyberpanel_login_required` decorator + +#### Optional Files + +1. **`apps.py`** + - Django app configuration + - Useful for signals and app initialization + +2. **`models.py`** + - Database models + - Use Django ORM for data persistence + +3. **`forms.py`** + - Django forms for user input + - Provides validation and CSRF protection + +4. **`utils.py`** + - Helper functions + - Business logic + - API integrations + +5. **`admin.py`** + - Django admin interface + - For managing plugin data + +6. **`signals.py`** + - Django signals + - For event handling + +7. **`tests.py`** + - Unit tests + - Integration tests + +--- + +## Core Components + +### 1. Authentication & Security + +#### Using the Login Decorator + +**Always use `cyberpanel_login_required` for all views:** + +```python +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 my_view(request): + # Your view code here + pass +``` + +#### CSRF Protection + +Django's CSRF protection is enabled by default. For forms: + +```html +{% csrf_token %} +``` + +For AJAX requests: + +```javascript +// Get CSRF token +function getCookie(name) { + let cookieValue = null; + if (document.cookie && document.cookie !== '') { + const cookies = document.cookie.split(';'); + for (let i = 0; i < cookies.length; i++) { + const cookie = cookies[i].trim(); + if (cookie.substring(0, name.length + 1) === (name + '=')) { + cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); + break; + } + } + } + return cookieValue; +} + +const csrftoken = getCookie('csrftoken'); + +// Use in AJAX +fetch('/plugins/myPlugin/api/', { + method: 'POST', + headers: { + 'X-CSRFToken': csrftoken, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data) +}); +``` + +### 2. URL Routing + +#### Basic URL Patterns + +```python +# urls.py +from django.urls import path, re_path +from . import views + +app_name = 'myPlugin' + +urlpatterns = [ + # Simple path + path('', views.main_view, name='main'), + + # Path with parameter + path('item//', views.item_detail, name='item_detail'), + + # Regex pattern + re_path(r'^user/(?P\w+)/$', views.user_profile, name='user_profile'), + + # Multiple parameters + path('category//item//', views.category_item, name='category_item'), +] +``` + +#### URL Reverse Lookups + +```python +# In views.py +from django.urls import reverse +from django.shortcuts import redirect + +def my_view(request): + # Generate URL + url = reverse('myPlugin:main') + # Or with parameters + url = reverse('myPlugin:item_detail', args=[1]) + url = reverse('myPlugin:item_detail', kwargs={'item_id': 1}) + + return redirect(url) +``` + +```html + +Main Page +Item 1 +``` + +### 3. Views & Request Handling + +#### Basic View Types + +**Function-Based Views:** + +```python +from django.shortcuts import render, redirect +from django.http import JsonResponse, HttpResponse + +@cyberpanel_login_required +def my_view(request): + if request.method == 'POST': + # Handle POST request + data = request.POST.get('data') + # Process data + return JsonResponse({'success': True}) + else: + # Handle GET request + context = {'data': 'value'} + return render(request, 'myPlugin/template.html', context) +``` + +**JSON API Views:** + +```python +from django.http import JsonResponse +from django.views.decorators.http import require_http_methods + +@cyberpanel_login_required +@require_http_methods(["GET", "POST"]) +def api_view(request): + if request.method == 'POST': + import json + data = json.loads(request.body) + # Process data + return JsonResponse({'status': 'success', 'data': data}) + else: + return JsonResponse({'status': 'ok', 'message': 'API endpoint'}) +``` + +#### Request Data Access + +```python +# GET parameters +value = request.GET.get('key', 'default') + +# POST data +value = request.POST.get('key', 'default') + +# JSON data +import json +data = json.loads(request.body) + +# Session data +user_id = request.session.get('userID') +request.session['key'] = 'value' + +# Files +uploaded_file = request.FILES.get('file') +``` + +### 4. Templates + +#### Template Inheritance + +**Always extend `baseTemplate/index.html`:** + +```html +{% extends "baseTemplate/index.html" %} +{% load static %} +{% load i18n %} + +{% block title %} + My Page - {% trans "CyberPanel" %} +{% endblock %} + +{% block header_scripts %} + +{% endblock %} + +{% block content %} + +{% endblock %} + +{% block footer_scripts %} + +{% endblock %} +``` + +#### Template Blocks + +Available blocks in `baseTemplate/index.html`: + +- `title`: Page title +- `header_scripts`: CSS and head scripts +- `content`: Main page content +- `footer_scripts`: JavaScript at end of page + +#### Template Tags & Filters + +```html + +{% load static %} +Logo + + +{% load i18n %} +

{% trans "Welcome" %}

+

{% trans "Hello, world!" %}

+ + +{% url 'myPlugin:main' %} + + +{{ variable }} +{{ object.attribute }} +{{ object.method }} + + +{{ text|upper }} +{{ text|truncatewords:10 }} +{{ date|date:"Y-m-d" }} + + +{% if condition %} + +{% elif other_condition %} + +{% else %} + +{% endif %} + + +{% for item in items %} +

{{ item }}

+{% empty %} +

No items

+{% endfor %} +``` + +### 5. Static Files + +#### Organizing Static Files + +``` +static/ +└── pluginName/ + ├── css/ + │ └── style.css + ├── js/ + │ └── script.js + └── images/ + └── logo.png +``` + +#### Using Static Files in Templates + +```html +{% load static %} + + + + + + + + +Logo +``` + +#### Collecting Static Files + +After installation, static files are automatically collected. To manually collect: + +```bash +cd /usr/local/CyberCP +python3 manage.py collectstatic --noinput +``` + +### 6. Database Models + +#### Creating Models + +```python +# models.py +from django.db import models + +class MyModel(models.Model): + name = models.CharField(max_length=255) + description = models.TextField(blank=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + is_active = models.BooleanField(default=True) + + class Meta: + db_table = 'my_plugin_mymodel' + ordering = ['-created_at'] + verbose_name = 'My Model' + verbose_name_plural = 'My Models' + + def __str__(self): + return self.name +``` + +#### Using Models in Views + +```python +from .models import MyModel + +@cyberpanel_login_required +def list_items(request): + items = MyModel.objects.filter(is_active=True) + context = {'items': items} + return render(request, 'myPlugin/list.html', context) + +@cyberpanel_login_required +def create_item(request): + if request.method == 'POST': + name = request.POST.get('name') + description = request.POST.get('description') + item = MyModel.objects.create( + name=name, + description=description + ) + return redirect('myPlugin:list') + return render(request, 'myPlugin/create.html') +``` + +#### Database Migrations + +```bash +# Create migrations +cd /usr/local/CyberCP +python3 manage.py makemigrations pluginName + +# Apply migrations +python3 manage.py migrate pluginName + +# Show migration status +python3 manage.py showmigrations pluginName +``` + +--- + +## Advanced Features + +### 1. Forms & Validation + +#### Creating Forms + +```python +# forms.py +from django import forms + +class MyForm(forms.Form): + name = forms.CharField( + max_length=255, + required=True, + widget=forms.TextInput(attrs={'class': 'form-control'}) + ) + email = forms.EmailField( + required=True, + widget=forms.EmailInput(attrs={'class': 'form-control'}) + ) + description = forms.CharField( + required=False, + widget=forms.Textarea(attrs={'class': 'form-control', 'rows': 5}) + ) +``` + +#### Using Forms in Views + +```python +from .forms import MyForm + +@cyberpanel_login_required +def my_form_view(request): + if request.method == 'POST': + form = MyForm(request.POST) + if form.is_valid(): + # Process valid form data + name = form.cleaned_data['name'] + email = form.cleaned_data['email'] + # Save or process data + return JsonResponse({'success': True}) + else: + # Return form errors + return JsonResponse({'success': False, 'errors': form.errors}) + else: + form = MyForm() + + context = {'form': form} + return render(request, 'myPlugin/form.html', context) +``` + +#### Rendering Forms in Templates + +```html +
+ {% csrf_token %} + +
+ {{ form.name.label_tag }} + {{ form.name }} + {% if form.name.errors %} +
{{ form.name.errors }}
+ {% endif %} +
+ +
+ {{ form.email.label_tag }} + {{ form.email }} + {% if form.email.errors %} +
{{ form.email.errors }}
+ {% endif %} +
+ + +
+``` + +### 2. AJAX & API Endpoints + +#### Creating API Endpoints + +```python +from django.http import JsonResponse +from django.views.decorators.csrf import csrf_exempt +from django.views.decorators.http import require_http_methods +import json + +@cyberpanel_login_required +@csrf_exempt # Only if not using CSRF token in AJAX +@require_http_methods(["POST"]) +def api_endpoint(request): + try: + data = json.loads(request.body) + # Process data + result = { + 'success': True, + 'message': 'Operation completed', + 'data': data + } + return JsonResponse(result) + except Exception as e: + return JsonResponse({ + 'success': False, + 'error': str(e) + }, status=400) +``` + +#### JavaScript AJAX Example + +```javascript +function sendAjaxRequest(data) { + // Get CSRF token + const csrftoken = getCookie('csrftoken'); + + fetch('/plugins/myPlugin/api/', { + method: 'POST', + headers: { + 'X-CSRFToken': csrftoken, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + console.log('Success:', data.message); + } else { + console.error('Error:', data.error); + } + }) + .catch(error => { + console.error('Request failed:', error); + }); +} +``` + +### 3. File Uploads + +#### Handling File Uploads + +```python +from django.core.files.storage import default_storage +from django.core.files.base import ContentFile + +@cyberpanel_login_required +def upload_file(request): + if request.method == 'POST' and request.FILES.get('file'): + uploaded_file = request.FILES['file'] + + # Validate file + if uploaded_file.size > 10 * 1024 * 1024: # 10MB limit + return JsonResponse({'success': False, 'error': 'File too large'}) + + # Save file + file_path = f'myPlugin/uploads/{uploaded_file.name}' + path = default_storage.save(file_path, ContentFile(uploaded_file.read())) + + return JsonResponse({ + 'success': True, + 'file_path': path + }) + + return JsonResponse({'success': False, 'error': 'No file provided'}) +``` + +### 4. Logging + +#### Using CyberPanel's Logging System + +```python +from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter as logging + +# Write to log file +logging.writeToFile("Plugin message: Something happened") + +# Write error +logging.writeToFile(f"Error: {str(exception)}", 1) # 1 = error level + +# Write with plugin name +logging.writeToFile(f"[MyPlugin] Action completed successfully") +``` + +### 5. Background Tasks + +#### Using Subprocess for Long-Running Tasks + +```python +import subprocess +import threading + +def run_background_task(command): + """Run a command in the background""" + def task(): + try: + result = subprocess.run( + command, + shell=True, + capture_output=True, + text=True + ) + logging.writeToFile(f"Task completed: {result.stdout}") + except Exception as e: + logging.writeToFile(f"Task error: {str(e)}", 1) + + thread = threading.Thread(target=task) + thread.daemon = True + thread.start() + return thread +``` + +--- + +## Best Practices + +### 1. Code Organization + +- **Keep files under 500 lines**: Split large files into modules +- **Use descriptive names**: Functions, variables, and classes should be self-documenting +- **Follow PEP 8**: Python style guide +- **Add comments**: Explain complex logic +- **Modular structure**: Separate concerns (views, models, utils) + +### 2. Error Handling + +```python +from django.http import JsonResponse +from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter as logging + +@cyberpanel_login_required +def my_view(request): + try: + # Your code here + result = perform_operation() + return JsonResponse({'success': True, 'data': result}) + except ValueError as e: + logging.writeToFile(f"Validation error: {str(e)}", 1) + return JsonResponse({'success': False, 'error': str(e)}, status=400) + except Exception as e: + logging.writeToFile(f"Unexpected error: {str(e)}", 1) + return JsonResponse({'success': False, 'error': 'An error occurred'}, status=500) +``` + +### 3. Security Best Practices + +- **Always validate input**: Never trust user input +- **Use parameterized queries**: When using raw SQL +- **Sanitize output**: Escape HTML in templates +- **Check permissions**: Verify user has access +- **Use HTTPS**: For sensitive operations +- **Rate limiting**: Prevent abuse +- **CSRF protection**: Always include CSRF tokens + +### 4. Performance Optimization + +- **Database queries**: Use `select_related()` and `prefetch_related()` +- **Caching**: Cache expensive operations +- **Lazy loading**: Load data only when needed +- **Pagination**: For large datasets +- **Static files**: Minify CSS/JS in production + +### 5. Testing + +```python +# tests.py +from django.test import TestCase, Client +from django.urls import reverse + +class MyPluginTests(TestCase): + def setUp(self): + self.client = Client() + # Set up test data + + def test_main_view(self): + # Test main view + response = self.client.get(reverse('myPlugin:main')) + self.assertEqual(response.status_code, 200) + + def test_api_endpoint(self): + # Test API endpoint + response = self.client.post( + reverse('myPlugin:api'), + data={'key': 'value'}, + content_type='application/json' + ) + self.assertEqual(response.status_code, 200) +``` + +--- + +## Security Guidelines + +### 1. Input Validation + +```python +import re +from django.core.exceptions import ValidationError + +def validate_input(data): + """Validate and sanitize user input""" + if not data: + raise ValidationError("Input cannot be empty") + + # Remove dangerous characters + data = re.sub(r'[<>"\']', '', data) + + # Check length + if len(data) > 1000: + raise ValidationError("Input too long") + + return data.strip() +``` + +### 2. SQL Injection Prevention + +**Always use Django ORM (recommended):** + +```python +# ✅ GOOD - Using ORM +items = MyModel.objects.filter(name=user_input) + +# ❌ BAD - Raw SQL with string formatting +items = MyModel.objects.raw(f"SELECT * FROM table WHERE name = '{user_input}'") +``` + +**If using raw SQL, use parameterized queries:** + +```python +from django.db import connection + +cursor = connection.cursor() +cursor.execute("SELECT * FROM table WHERE name = %s", [user_input]) +``` + +### 3. XSS Prevention + +**Django templates auto-escape by default:** + +```html + +{{ user_input }} + + +{{ user_input|safe }} +``` + +**Only use `|safe` if you're certain the content is safe:** + +```python +from django.utils.html import escape + +# Escape in Python +safe_html = escape(user_input) +``` + +### 4. File Upload Security + +```python +import os +from django.core.exceptions import ValidationError + +ALLOWED_EXTENSIONS = ['.jpg', '.png', '.pdf'] +MAX_FILE_SIZE = 10 * 1024 * 1024 # 10MB + +def validate_uploaded_file(file): + """Validate uploaded file""" + # Check extension + ext = os.path.splitext(file.name)[1].lower() + if ext not in ALLOWED_EXTENSIONS: + raise ValidationError(f"File type {ext} not allowed") + + # Check size + if file.size > MAX_FILE_SIZE: + raise ValidationError("File too large") + + # Check content type + if file.content_type not in ['image/jpeg', 'image/png', 'application/pdf']: + raise ValidationError("Invalid file type") + + return True +``` + +--- + +## Testing & Debugging + +### 1. Debugging Tools + +#### Enable Django Debug Mode (Development Only) + +```python +# In your view +import logging +logger = logging.getLogger(__name__) + +def my_view(request): + logger.debug("Debug message") + logger.info("Info message") + logger.warning("Warning message") + logger.error("Error message") +``` + +#### Check Logs + +```bash +# CyberPanel logs +tail -f /usr/local/lscp/logs/error.log + +# Django logs (if configured) +tail -f /var/log/cyberpanel/error.log + +# Plugin-specific logs +tail -f /home/cyberpanel/plugin_logs/myPlugin.log +``` + +### 2. Common Issues & Solutions + +#### Template Not Found + +**Problem:** `TemplateDoesNotExist` error + +**Solutions:** +- Check template path: `templates/pluginName/template.html` +- Verify template name in `render()` call +- Ensure template extends `baseTemplate/index.html` +- Run `python3 manage.py collectstatic` + +#### URL Not Found + +**Problem:** 404 error when accessing plugin URL + +**Solutions:** +- Verify URL pattern in `urls.py` +- Check `app_name` is set correctly +- Ensure plugin is in `INSTALLED_APPS` +- Restart CyberPanel: `systemctl restart lscpd` + +#### Import Errors + +**Problem:** `ImportError` or `ModuleNotFoundError` + +**Solutions:** +- Check Python syntax: `python3 -m py_compile views.py` +- Verify `__init__.py` exists +- Check import paths +- Ensure plugin is installed correctly + +#### Static Files Not Loading + +**Problem:** CSS/JS/images not appearing + +**Solutions:** +- Run `python3 manage.py collectstatic --noinput` +- Check static file paths in templates +- Verify files exist in `static/pluginName/` +- Clear browser cache + +--- + +## Packaging & Distribution + +### 1. Creating Plugin Package + +```bash +# Navigate to plugin directory +cd /home/cyberpanel/plugins/myPlugin + +# Create ZIP file +zip -r myPlugin-v1.0.0.zip . \ + -x "*.pyc" \ + -x "__pycache__/*" \ + -x "*.log" \ + -x ".git/*" \ + -x ".DS_Store" +``` + +### 2. Plugin Distribution Checklist + +- [ ] Plugin tested and working +- [ ] All required files included +- [ ] `meta.xml` is valid and complete +- [ ] README.md with installation instructions +- [ ] No sensitive data (passwords, API keys) +- [ ] Proper file permissions +- [ ] Version number updated +- [ ] Documentation complete + +### 3. Version Management + +**Semantic Versioning:** + +- **MAJOR.MINOR.PATCH** (e.g., 1.2.3) +- **MAJOR**: Breaking changes +- **MINOR**: New features, backward compatible +- **PATCH**: Bug fixes, backward compatible + +**Update version in:** +- `meta.xml` +- `views.py` (if version displayed) +- `README.md` +- Git tags (if using version control) + +--- + +## Troubleshooting + +### Installation Issues + +**Plugin not appearing in list:** +- Check `meta.xml` format and validity +- Verify plugin directory exists in `/home/cyberpanel/plugins/` +- Check file permissions +- Review CyberPanel logs + +**Installation fails:** +- Check Python syntax errors +- Verify all required files exist +- Check file permissions +- Review installation logs +- Ensure sufficient disk space + +### Runtime Issues + +**Plugin page not loading:** +- Verify URL routing +- Check authentication decorator +- Review template paths +- Check for JavaScript errors +- Verify plugin is enabled + +**Database errors:** +- Run migrations: `python3 manage.py migrate pluginName` +- Check database permissions +- Verify model definitions +- Review migration files + +**Static files missing:** +- Run `python3 manage.py collectstatic` +- Check static file paths +- Verify file permissions +- Clear browser cache + +--- + +## Examples & References + +### Reference Plugins + +1. **examplePlugin**: Basic plugin structure + - Location: `/usr/local/CyberCP/examplePlugin/` + - URL: `/plugins/examplePlugin/` + +2. **testPlugin**: Comprehensive example + - Location: `/usr/local/CyberCP/testPlugin/` + - URL: `/plugins/testPlugin/` + - **Author:** usmannasir + +3. **discordWebhooks**: Discord webhook integration plugin + - Location: `/usr/local/CyberCP/discordWebhooks/` (if installed) + - URL: `/plugins/discordWebhooks/` + - **Author:** Master3395 + +### Useful Resources + +- **CyberPanel Documentation**: https://cyberpanel.net/KnowledgeBase/ +- **Django Documentation**: https://docs.djangoproject.com/ +- **Python Documentation**: https://docs.python.org/ +- **CyberPanel GitHub**: https://github.com/usmannasir/cyberpanel +- **Plugin Repository**: https://github.com/master3395/cyberpanel-plugins + +### Code Examples Repository + +Check the `examplePlugin` and `testPlugin` directories in CyberPanel for complete working examples. + +--- + +## Conclusion + +This guide provides comprehensive information for developing CyberPanel plugins. Start with a simple plugin and gradually add more features as you become familiar with the system. + +### Quick Start Checklist + +1. ✅ Create plugin directory structure +2. ✅ Create `meta.xml` with required fields +3. ✅ Create `urls.py` with URL patterns +4. ✅ Create `views.py` with view functions +5. ✅ Create templates extending `baseTemplate/index.html` +6. ✅ Test plugin locally +7. ✅ Install via CyberPanel UI +8. ✅ Verify plugin works correctly + +### Getting Help + +- Review existing plugins for examples +- Check CyberPanel community forum +- Review Django documentation +- Examine CyberPanel source code +- Create GitHub issues for bugs + +--- + +**Happy Plugin Development!** + +**Author:** master3395 +**Last Updated:** 2026-01-04 +**Version:** 2.0.0 diff --git a/pluginHolder/templates/pluginHolder/help.html b/pluginHolder/templates/pluginHolder/help.html new file mode 100644 index 000000000..d822febbc --- /dev/null +++ b/pluginHolder/templates/pluginHolder/help.html @@ -0,0 +1,658 @@ +{% extends "baseTemplate/index.html" %} +{% load i18n %} +{% load static %} + +{% block title %}{% trans "Plugin Development Help - CyberPanel" %}{% endblock %} + +{% block header_scripts %} + +{% endblock %} + +{% block content %} +
+ + + {% trans "Back to Installed Plugins" %} + + +
+

+
+ +
+ {% trans "Plugin Development Guide" %} +

+

{% trans "Comprehensive guide to creating plugins for CyberPanel. Learn how to build, package, and distribute your own plugins." %}

+
+ +
+ + +

{% trans "Introduction" %}

+

{% trans "CyberPanel's plugin system allows developers to extend the control panel's functionality with custom features. Plugins integrate seamlessly with CyberPanel's Django-based architecture, providing access to the full power of the platform while maintaining security and consistency." %}

+ +

{% trans "What Can Plugins Do?" %}

+
    +
  • {% trans "Add new administrative features" %}
  • +
  • {% trans "Integrate with external services (APIs, webhooks, etc.)" %}
  • +
  • {% trans "Customize the user interface" %}
  • +
  • {% trans "Extend database functionality" %}
  • +
  • {% trans "Add automation and monitoring capabilities" %}
  • +
  • {% trans "Create custom reporting tools" %}
  • +
  • {% trans "Integrate security features" %}
  • +
+ +

{% trans "Prerequisites" %}

+

{% trans "Required Knowledge" %}

+
    +
  • Python 3.6+: {% trans "Basic to intermediate Python knowledge" %}
  • +
  • Django Framework: {% trans "Understanding of Django views, URLs, templates, and models" %}
  • +
  • HTML/CSS/JavaScript: {% trans "For creating user interfaces" %}
  • +
  • Linux/Unix: {% trans "Basic command-line familiarity" %}
  • +
  • XML: {% trans "Understanding of XML structure for meta.xml" %}
  • +
+ +

{% trans "Plugin Architecture Overview" %}

+

{% trans "How Plugins Work" %}

+
    +
  1. {% trans "Plugin Source Location" %}: /home/cyberpanel/plugins/ +
      +
    • {% trans "Plugins are stored here before installation" %}
    • +
    • {% trans "Can be uploaded as ZIP files or placed directly" %}
    • +
    +
  2. +
  3. {% trans "Installed Location" %}: /usr/local/CyberCP/ +
      +
    • {% trans "After installation, plugins are copied here" %}
    • +
    • {% trans "This is where CyberPanel loads plugins from" %}
    • +
    +
  4. +
+ +

{% trans "Creating Your First Plugin" %}

+

{% trans "Step 1: Create Plugin Directory Structure" %}

+
# Navigate to plugins directory
+cd /home/cyberpanel/plugins
+
+# Create your plugin directory
+mkdir myFirstPlugin
+cd myFirstPlugin
+
+# Create required subdirectories
+mkdir -p templates/myFirstPlugin
+mkdir -p static/myFirstPlugin/css
+mkdir -p static/myFirstPlugin/js
+mkdir -p migrations
+ +

{% trans "Step 2: Create meta.xml (REQUIRED)" %}

+
<?xml version="1.0" encoding="UTF-8"?>
+<cyberpanelPluginConfig>
+    <name>My First Plugin</name>
+    <type>Utility</type>
+    <description>A simple example plugin</description>
+    <version>1.0.0</version>
+    <url>/plugins/myFirstPlugin/</url>
+    <settings_url>/plugins/myFirstPlugin/settings/</settings_url>
+</cyberpanelPluginConfig>
+ +

{% trans "Step 3: Create urls.py" %}

+
from django.urls import path
+from . import views
+
+app_name = 'myFirstPlugin'
+
+urlpatterns = [
+    path('', views.main_view, name='main'),
+    path('settings/', views.settings_view, name='settings'),
+]
+ +

{% trans "Step 4: Create views.py" %}

+
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': 'My First Plugin',
+        'version': '1.0.0'
+    }
+    return render(request, 'myFirstPlugin/main.html', context)
+ +

{% trans "Step 5: Create Templates" %}

+

{% trans "Templates must extend baseTemplate/index.html:" %}

+
{% extends "baseTemplate/index.html" %}
+{% load static %}
+{% load i18n %}
+
+{% block title %}
+    My First Plugin - {% trans "CyberPanel" %}
+{% endblock %}
+
+{% block content %}
+    <div class="container">
+        <h1>{{ plugin_name }}</h1>
+        <p>Version {{ version }}</p>
+    </div>
+{% endblock %}
+ +
+ {% trans "Important" %}: {% trans "Always use the @cyberpanel_login_required decorator for all views to ensure users are authenticated." %} +
+ +

{% trans "Plugin Structure & Files" %}

+

{% trans "Required Files" %}

+
    +
  • __init__.py - {% trans "Required" %} - {% trans "Python package marker" %}
  • +
  • meta.xml - {% trans "Required" %} - {% trans "Plugin metadata" %}
  • +
  • urls.py - {% trans "Required" %} - {% trans "URL routing" %}
  • +
  • views.py - {% trans "Required" %} - {% trans "View functions" %}
  • +
+ +

{% trans "Optional Files" %}

+
    +
  • models.py - {% trans "Optional" %} - {% trans "Database models" %}
  • +
  • forms.py - {% trans "Optional" %} - {% trans "Django forms" %}
  • +
  • utils.py - {% trans "Optional" %} - {% trans "Utility functions" %}
  • +
  • templates/ - {% trans "Optional" %} - {% trans "HTML templates" %}
  • +
  • static/ - {% trans "Optional" %} - {% trans "CSS, JS, images" %}
  • +
+ +

{% trans "Core Components" %}

+

{% trans "1. Authentication & Security" %}

+

{% trans "Always use the cyberpanel_login_required decorator:" %}

+
@cyberpanel_login_required
+def my_view(request):
+    # Your view code
+    pass
+ +

{% trans "2. URL Routing" %}

+
from django.urls import path
+from . import views
+
+app_name = 'myPlugin'
+
+urlpatterns = [
+    path('', views.main_view, name='main'),
+    path('item/<int:item_id>/', views.item_detail, name='item_detail'),
+]
+ +

{% trans "3. Templates" %}

+

{% trans "Always extend baseTemplate/index.html:" %}

+
{% extends "baseTemplate/index.html" %}
+{% load static %}
+{% load i18n %}
+ +

{% trans "Advanced Features" %}

+

{% trans "Database Models" %}

+
from django.db import models
+
+class MyModel(models.Model):
+    name = models.CharField(max_length=255)
+    created_at = models.DateTimeField(auto_now_add=True)
+    
+    class Meta:
+        db_table = 'my_plugin_mymodel'
+ +

{% trans "Forms" %}

+
from django import forms
+
+class MyForm(forms.Form):
+    name = forms.CharField(max_length=255, required=True)
+    email = forms.EmailField(required=True)
+ +

{% trans "API Endpoints" %}

+
from django.http import JsonResponse
+
+@cyberpanel_login_required
+def api_endpoint(request):
+    data = {'status': 'success'}
+    return JsonResponse(data)
+ +

{% trans "Best Practices" %}

+
    +
  • {% trans "Keep files under 500 lines - split into modules if needed" %}
  • +
  • {% trans "Use descriptive names for functions and variables" %}
  • +
  • {% trans "Follow PEP 8 Python style guide" %}
  • +
  • {% trans "Add comments for complex logic" %}
  • +
  • {% trans "Always validate user input" %}
  • +
  • {% trans "Use Django ORM instead of raw SQL" %}
  • +
  • {% trans "Test your plugin thoroughly before distribution" %}
  • +
+ +

{% trans "Security Guidelines" %}

+
+ {% trans "Security is Critical" %}: {% trans "Always validate input, use parameterized queries, sanitize output, and check user permissions." %} +
+
    +
  • {% trans "Always validate and sanitize user input" %}
  • +
  • {% trans "Use Django ORM or parameterized queries" %}
  • +
  • {% trans "Escape HTML in templates (Django does this by default)" %}
  • +
  • {% trans "Validate file uploads (type, size, content)" %}
  • +
  • {% trans "Use HTTPS for sensitive operations" %}
  • +
  • {% trans "Implement rate limiting for API endpoints" %}
  • +
+ +

{% trans "Testing & Debugging" %}

+

{% trans "Common Issues" %}

+
    +
  • {% trans "Template Not Found" %}: {% trans "Check template path and name" %}
  • +
  • {% trans "URL Not Found" %}: {% trans "Verify URL patterns and app_name" %}
  • +
  • {% trans "Import Errors" %}: {% trans "Check Python syntax and import paths" %}
  • +
  • {% trans "Static Files Not Loading" %}: {% trans "Run collectstatic command" %}
  • +
+ +

{% trans "Debugging Commands" %}

+
# Check Python syntax
+python3 -m py_compile views.py
+
+# Check XML validity
+xmllint --noout meta.xml
+
+# View logs
+tail -f /usr/local/lscp/logs/error.log
+ +

{% trans "Packaging & Distribution" %}

+

{% trans "Create Plugin Package" %}

+
cd /home/cyberpanel/plugins/myPlugin
+zip -r myPlugin-v1.0.0.zip . \
+    -x "*.pyc" \
+    -x "__pycache__/*" \
+    -x "*.log"
+ +

{% trans "Troubleshooting" %}

+

{% trans "Installation Issues" %}

+
    +
  • {% trans "Check meta.xml format and validity" %}
  • +
  • {% trans "Verify plugin directory exists" %}
  • +
  • {% trans "Check file permissions" %}
  • +
  • {% trans "Review CyberPanel logs" %}
  • +
+ +

{% trans "Runtime Issues" %}

+
    +
  • {% trans "Verify URL routing" %}
  • +
  • {% trans "Check authentication decorator" %}
  • +
  • {% trans "Review template paths" %}
  • +
  • {% trans "Check for JavaScript errors" %}
  • +
+ +

{% trans "Examples & References" %}

+

{% trans "Reference Plugins" %}

+
    +
  • examplePlugin: {% trans "Basic plugin structure" %} +
      +
    • {% trans "Location" %}: /usr/local/CyberCP/examplePlugin/
    • +
    • {% trans "URL" %}: /plugins/examplePlugin/
    • +
    +
  • +
  • testPlugin: {% trans "Comprehensive example" %} +
      +
    • {% trans "Location" %}: /usr/local/CyberCP/testPlugin/
    • +
    • {% trans "URL" %}: /plugins/testPlugin/
    • +
    • {% trans "Author" %}: usmannasir
    • +
    +
  • +
  • discordWebhooks: {% trans "Discord webhook integration plugin" %} +
      +
    • {% trans "Location" %}: /usr/local/CyberCP/discordWebhooks/
    • +
    • {% trans "URL" %}: /plugins/discordWebhooks/
    • +
    • {% trans "Author" %}: Master3395
    • +
    +
  • +
+ +

{% trans "Useful Resources" %}

+ + +
+ {% trans "Ready to Start?" %} {% trans "Begin with a simple plugin and gradually add more features as you become familiar with the system. Check the examplePlugin and testPlugin directories for complete working examples." %} +
+ +
+

+ {% trans "Author" %}: master3395 | + {% trans "Version" %}: 2.0.0 | + {% trans "Last Updated" %}: 2026-01-04 +

+
+
+
+ + +{% endblock %} diff --git a/pluginHolder/templates/pluginHolder/plugins.html b/pluginHolder/templates/pluginHolder/plugins.html index ddf278e3f..a992a7abe 100644 --- a/pluginHolder/templates/pluginHolder/plugins.html +++ b/pluginHolder/templates/pluginHolder/plugins.html @@ -125,7 +125,7 @@ display: flex; align-items: center; gap: 15px; - margin-bottom: 20px; + margin-bottom: 15px; } .plugin-icon { @@ -143,6 +143,7 @@ .plugin-info { flex: 1; + min-width: 0; } .plugin-name { @@ -150,6 +151,14 @@ font-weight: 700; color: var(--text-primary, #2f3640); margin-bottom: 5px; + word-wrap: break-word; + } + + .plugin-meta { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; } .plugin-type { @@ -158,43 +167,119 @@ background: var(--purple-light, #e8e6ff); color: #5856d6; border-radius: 6px; - font-size: 12px; + font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; } - .plugin-description { - font-size: 14px; - color: var(--text-secondary, #64748b); - line-height: 1.6; - margin-bottom: 15px; - } - - .plugin-footer { - display: flex; - justify-content: space-between; - align-items: center; - padding-top: 15px; - border-top: 1px solid var(--border-primary, #e8e9ff); - } - - .plugin-version { - font-size: 13px; - color: var(--text-muted, #94a3b8); - display: flex; - align-items: center; - gap: 5px; - } - .plugin-version-number { background: var(--bg-secondary, #f8f9ff); padding: 3px 8px; border-radius: 4px; font-weight: 600; + font-size: 11px; color: var(--text-secondary, #64748b); } + .plugin-description { + font-size: 13px; + color: var(--text-secondary, #64748b); + line-height: 1.6; + margin-bottom: 15px; + min-height: 40px; + } + + .plugin-status-section { + padding: 12px; + background: var(--bg-secondary, #f8f9ff); + border-radius: 8px; + margin-bottom: 15px; + } + + .plugin-status-row { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; + font-size: 12px; + } + + .plugin-status-row:last-child { + margin-bottom: 0; + } + + .plugin-status-row .label { + color: var(--text-secondary, #64748b); + font-weight: 600; + } + + .status-installed-small { + display: inline-block; + padding: 3px 8px; + background: #d4edda; + color: #155724; + border-radius: 4px; + font-size: 11px; + font-weight: 600; + } + + .status-not-installed-small { + display: inline-block; + padding: 3px 8px; + background: #f8f9fa; + color: #6c757d; + border-radius: 4px; + font-size: 11px; + font-weight: 600; + } + + .active-status { + font-size: 14px; + display: inline-flex; + align-items: center; + gap: 4px; + } + + .active-status.active-yes { + color: #28a745; + } + + .active-status.active-no { + color: #dc3545; + } + + .active-status.active-na { + color: #6c757d; + font-size: 12px; + } + + .plugin-footer { + display: flex; + flex-direction: column; + gap: 12px; + padding-top: 15px; + border-top: 1px solid var(--border-primary, #e8e9ff); + } + + .plugin-links { + display: flex; + justify-content: center; + gap: 8px; + flex-wrap: wrap; + } + + .btn-small { + padding: 6px 12px; + font-size: 12px; + } + + .btn-link-small { + padding: 4px 10px; + font-size: 11px; + white-space: nowrap; + } + /* Table View (Alternative) */ .plugins-table { width: 100%; @@ -321,6 +406,328 @@ color: var(--alert-danger-icon, #ef4444); } + /* Plugin Store Styles */ + .store-notice { + background: #fff3cd; + border: 1px solid #ffc107; + border-radius: 8px; + padding: 20px; + margin-bottom: 25px; + position: relative; + } + + .notice-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 10px; + font-weight: 700; + color: #856404; + } + + .notice-close { + background: none; + border: none; + font-size: 18px; + color: #856404; + cursor: pointer; + padding: 5px; + border-radius: 4px; + transition: background 0.2s; + } + + .notice-close:hover { + background: rgba(0,0,0,0.1); + } + + .notice-content p { + margin: 10px 0; + color: #856404; + font-size: 14px; + line-height: 1.6; + } + + .warning-text { + display: flex; + align-items: center; + gap: 8px; + color: #ff6b00 !important; + font-weight: 600; + margin-top: 15px !important; + } + + .warning-text i { + font-size: 18px; + } + + .store-loading { + text-align: center; + padding: 60px 20px; + } + + .loading-spinner { + width: 40px; + height: 40px; + border: 4px solid #f3f3f3; + border-top: 4px solid #5856d6; + border-radius: 50%; + animation: spin 1s linear infinite; + margin: 0 auto 20px; + } + + @keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } + } + + .alphabet-filter { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 20px; + padding: 15px; + background: var(--bg-secondary, #f8f9ff); + border-radius: 8px; + } + + .alpha-btn { + padding: 6px 12px; + border: 1px solid var(--border-primary, #e8e9ff); + background: var(--bg-primary, white); + border-radius: 6px; + font-size: 13px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s; + color: var(--text-secondary, #64748b); + min-width: 36px; + text-align: center; + } + + .alpha-btn:hover { + background: var(--bg-hover, #f0f1ff); + border-color: #5856d6; + color: #5856d6; + } + + .alpha-btn.active { + background: #5856d6; + color: white; + border-color: #5856d6; + } + + .store-table-wrapper { + overflow-x: auto; + background: var(--bg-primary, white); + border-radius: 8px; + border: 1px solid var(--border-primary, #e8e9ff); + } + + .store-table { + width: 100%; + border-collapse: collapse; + } + + .store-table th { + background: #2f3640; + color: white; + padding: 15px; + text-align: left; + font-size: 13px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + border-bottom: 2px solid #1a1d2e; + } + + .store-table td { + padding: 18px 15px; + border-bottom: 1px solid var(--border-primary, #e8e9ff); + font-size: 14px; + color: var(--text-primary, #2f3640); + } + + .store-table tr:hover { + background: var(--bg-hover, #f8f9ff); + } + + .store-table tr:last-child td { + border-bottom: none; + } + + .status-action { + display: flex; + align-items: center; + gap: 10px; + } + + .status-installed { + display: inline-block; + padding: 6px 12px; + background: #d4edda; + color: #155724; + border-radius: 6px; + font-size: 13px; + font-weight: 600; + } + + .btn-install { + padding: 8px 16px; + background: #5856d6; + color: white; + border: none; + border-radius: 6px; + font-size: 13px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s; + display: inline-flex; + align-items: center; + gap: 6px; + } + + .btn-install:hover:not(:disabled) { + background: #4a48c4; + transform: translateY(-1px); + box-shadow: 0 4px 8px rgba(88,86,214,0.3); + } + + .btn-install:disabled { + opacity: 0.6; + cursor: not-allowed; + } + + .btn-link { + padding: 6px 12px; + background: var(--bg-secondary, #f8f9ff); + color: var(--text-secondary, #64748b); + text-decoration: none; + border-radius: 6px; + font-size: 13px; + transition: all 0.2s; + display: inline-flex; + align-items: center; + gap: 6px; + } + + .btn-link:hover { + background: var(--bg-hover, #f0f1ff); + color: #5856d6; + } + + /* Status and Action Columns */ + .status-installed { + display: inline-block; + padding: 4px 10px; + background: #d4edda; + color: #155724; + border-radius: 4px; + font-size: 12px; + font-weight: 600; + } + + .status-not-installed { + display: inline-block; + padding: 4px 10px; + background: #f8f9fa; + color: #6c757d; + border-radius: 4px; + font-size: 12px; + font-weight: 600; + } + + .btn-action { + padding: 6px 12px; + border: none; + border-radius: 6px; + font-size: 12px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s; + display: inline-flex; + align-items: center; + gap: 6px; + } + + .btn-install { + background: #5856d6; + color: white; + } + + .btn-install:hover:not(:disabled) { + background: #4a48c4; + transform: translateY(-1px); + box-shadow: 0 4px 8px rgba(88,86,214,0.3); + } + + .btn-uninstall { + background: #dc3545; + color: white; + } + + .btn-uninstall:hover:not(:disabled) { + background: #c82333; + transform: translateY(-1px); + box-shadow: 0 4px 8px rgba(220,53,69,0.3); + } + + .btn-activate { + background: #28a745; + color: white; + } + + .btn-activate:hover:not(:disabled) { + background: #218838; + transform: translateY(-1px); + box-shadow: 0 4px 8px rgba(40,167,69,0.3); + } + + .btn-deactivate { + background: #ffc107; + color: #212529; + } + + .btn-deactivate:hover:not(:disabled) { + background: #e0a800; + transform: translateY(-1px); + box-shadow: 0 4px 8px rgba(255,193,7,0.3); + } + + .btn-action:disabled { + opacity: 0.6; + cursor: not-allowed; + } + + .plugin-actions { + display: flex; + gap: 8px; + flex-wrap: wrap; + justify-content: center; + } + + /* Active Column */ + .active-column { + text-align: center; + } + + .active-status { + font-size: 18px; + display: inline-block; + } + + .active-status.active-yes { + color: #28a745; + } + + .active-status.active-no { + color: #dc3545; + } + + .active-status.active-na { + color: #6c757d; + font-size: 14px; + } + /* Responsive */ @media (max-width: 768px) { .plugins-wrapper { @@ -346,7 +753,17 @@ } .view-toggle { - display: none; + flex-wrap: wrap; + } + + .alphabet-filter { + padding: 10px; + } + + .alpha-btn { + padding: 5px 10px; + font-size: 12px; + min-width: 32px; } } @@ -384,6 +801,10 @@ {% trans "Table View" %} + @@ -406,7 +827,10 @@

{{ plugin.name }}

+
{{ plugin.type }} + v{{ plugin.version }} +
@@ -414,10 +838,57 @@ {{ plugin.desc }} +
+
+ {% trans "Status:" %} + {% if plugin.installed %} + {% trans "Installed" %} + {% else %} + {% trans "Not Installed" %} + {% endif %} +
+
+ {% trans "Active:" %} + {% if plugin.installed %} + {% if plugin.enabled %} + {% trans "Yes" %} + {% else %} + {% trans "No" %} + {% endif %} + {% else %} + - + {% endif %} +
+
+ @@ -430,10 +901,14 @@ - - - + + + + + + + @@ -442,19 +917,71 @@ - - + + + + + + {% endfor %}
{% trans "Name" %}{% trans "Type" %}{% trans "Description" %}{% trans "Plugin Name" %} {% trans "Version" %}{% trans "Modify Date" %}{% trans "Status" %}{% trans "Action" %}{% trans "Active" %}{% trans "Help" %}{% trans "About" %}
{{ plugin.name }} - {{ plugin.type }} - {{ plugin.desc }} {{ plugin.version }} + + {{ plugin.modify_date|default:"N/A" }} + + + {% if plugin.installed %} + {% trans "Installed" %} + {% else %} + {% trans "Not Installed" %} + {% endif %} + +
+ {% if plugin.installed %} + {% if plugin.enabled %} + + {% else %} + + {% endif %} + + {% else %} + + {% endif %} +
+
+ {% if plugin.installed %} + {% if plugin.enabled %} + + {% else %} + + {% endif %} + {% else %} + - + {% endif %} + + + {% trans "Help" %} + + + + {% trans "About" %} + +
+ {% else %}
@@ -466,6 +993,87 @@
{% endif %} + +
+ + + +
+ + +
+ +
+
+ {% trans "Notice" %} + +
+
+

{% trans "The versions displayed here represent the latest plugins from the CyberPanel Plugin Store repository. They may or may not represent the latest available versions. Additionally, the plugin repository may only contain plugins released within the last few months." %}

+

+ + {% trans "Use at Your Own Risk" %} +

+

{% trans "The plugins displayed here are contributed by both the CyberPanel Developers and independent third parties. We make no guarantees that the plugins available here are functional, tested, or compatible with your system. You are encouraged to read the information found in the help and about links for each plugin before attempting the installation." %}

+
+
+ + + + + + + + + +
+ {% endblock %} \ No newline at end of file diff --git a/pluginHolder/urls.py b/pluginHolder/urls.py index a3fdf954d..b6d0fe13d 100644 --- a/pluginHolder/urls.py +++ b/pluginHolder/urls.py @@ -3,8 +3,12 @@ from . import views urlpatterns = [ path('installed', views.installed, name='installed'), + path('help/', views.help_page, name='help'), path('api/install//', views.install_plugin, name='install_plugin'), path('api/uninstall//', views.uninstall_plugin, name='uninstall_plugin'), path('api/enable//', views.enable_plugin, name='enable_plugin'), path('api/disable//', views.disable_plugin, name='disable_plugin'), + path('api/store/plugins/', views.fetch_plugin_store, name='fetch_plugin_store'), + path('api/store/install//', views.install_from_store, name='install_from_store'), + path('/help/', views.plugin_help, name='plugin_help'), ] diff --git a/pluginHolder/views.py b/pluginHolder/views.py index 1f5a0507c..6a6d91e25 100644 --- a/pluginHolder/views.py +++ b/pluginHolder/views.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from django.shortcuts import render +from django.shortcuts import render, redirect from django.http import JsonResponse from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_http_methods @@ -8,16 +8,28 @@ import os import subprocess import shlex import json +from datetime import datetime, timedelta from xml.etree import ElementTree from plogical.httpProc import httpProc from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter as logging import sys +import urllib.request +import urllib.error +import time sys.path.append('/usr/local/CyberCP') from pluginInstaller.pluginInstaller import pluginInstaller # Plugin state file location PLUGIN_STATE_DIR = '/home/cyberpanel/plugin_states' +# Plugin store cache configuration +PLUGIN_STORE_CACHE_DIR = '/home/cyberpanel/plugin_store_cache' +PLUGIN_STORE_CACHE_FILE = os.path.join(PLUGIN_STORE_CACHE_DIR, 'plugins_cache.json') +PLUGIN_STORE_CACHE_DURATION = 3600 # Cache for 1 hour (3600 seconds) +GITHUB_REPO_API = 'https://api.github.com/repos/master3395/cyberpanel-plugins/contents' +GITHUB_RAW_BASE = 'https://raw.githubusercontent.com/master3395/cyberpanel-plugins/main' +GITHUB_COMMITS_API = 'https://api.github.com/repos/master3395/cyberpanel-plugins/commits' + def _get_plugin_state_file(plugin_name): """Get the path to the plugin state file""" if not os.path.exists(PLUGIN_STATE_DIR): @@ -48,6 +60,12 @@ def _set_plugin_state(plugin_name, enabled): logging.writeToFile(f"Error writing plugin state for {plugin_name}: {str(e)}") return False +def help_page(request): + """Display plugin development help page""" + mailUtilities.checkHome() + proc = httpProc(request, 'pluginHolder/help.html', {}, 'admin') + return proc.render() + def installed(request): mailUtilities.checkHome() pluginPath = '/home/cyberpanel/plugins' @@ -114,6 +132,43 @@ def installed(request): else: data['enabled'] = False + # Get modify date from GitHub (last commit date) or local file as fallback + modify_date = 'N/A' + try: + # Try to get last commit date from GitHub API + plugin_name_for_api = plugin.replace('Plugin', '').lower() if 'Plugin' in plugin else plugin.lower() + commits_url = f"{GITHUB_COMMITS_API}?path={plugin}&per_page=1" + commits_req = urllib.request.Request( + commits_url, + headers={ + 'User-Agent': 'CyberPanel-Plugin-Store/1.0', + 'Accept': 'application/vnd.github.v3+json' + } + ) + + with urllib.request.urlopen(commits_req, timeout=5) as commits_response: + commits_data = json.loads(commits_response.read().decode('utf-8')) + if commits_data and len(commits_data) > 0: + commit_date = commits_data[0].get('commit', {}).get('author', {}).get('date', '') + if commit_date: + try: + dt = datetime.fromisoformat(commit_date.replace('Z', '+00:00')) + modify_date = dt.strftime('%Y-%m-%d %H:%M:%S') + except Exception: + modify_date = commit_date[:19].replace('T', ' ') + except Exception as e: + # Fallback to local file modification time + try: + if os.path.exists(metaXmlPath): + modify_time = os.path.getmtime(metaXmlPath) + modify_date = datetime.fromtimestamp(modify_time).strftime('%Y-%m-%d %H:%M:%S') + else: + modify_date = 'N/A' + except Exception: + modify_date = 'N/A' + + data['modify_date'] = modify_date + # Extract settings URL or main URL for "Manage" button settings_url_elem = root.find('settings_url') url_elem = root.find('url') @@ -336,4 +391,278 @@ def disable_plugin(request, plugin_name): return JsonResponse({ 'success': False, 'error': str(e) + }, status=500) + +def _ensure_cache_dir(): + """Ensure cache directory exists""" + try: + if not os.path.exists(PLUGIN_STORE_CACHE_DIR): + os.makedirs(PLUGIN_STORE_CACHE_DIR, mode=0o755) + except Exception as e: + logging.writeToFile(f"Error creating cache directory: {str(e)}") + +def _get_cached_plugins(allow_expired=False): + """Get plugins from cache if available and not expired + + Args: + allow_expired: If True, return cache even if expired (for fallback) + """ + try: + if not os.path.exists(PLUGIN_STORE_CACHE_FILE): + return None + + # Check if cache is expired + cache_mtime = os.path.getmtime(PLUGIN_STORE_CACHE_FILE) + cache_age = time.time() - cache_mtime + + if cache_age > PLUGIN_STORE_CACHE_DURATION: + if not allow_expired: + logging.writeToFile(f"Plugin store cache expired (age: {cache_age:.0f}s)") + return None + else: + logging.writeToFile(f"Using expired cache as fallback (age: {cache_age:.0f}s)") + + # Read cache file + with open(PLUGIN_STORE_CACHE_FILE, 'r', encoding='utf-8') as f: + cache_data = json.load(f) + + if not allow_expired or cache_age <= PLUGIN_STORE_CACHE_DURATION: + logging.writeToFile(f"Using cached plugin store data (age: {cache_age:.0f}s)") + return cache_data.get('plugins', []) + except Exception as e: + logging.writeToFile(f"Error reading plugin store cache: {str(e)}") + return None + +def _save_plugins_cache(plugins): + """Save plugins to cache""" + try: + _ensure_cache_dir() + cache_data = { + 'plugins': plugins, + 'cached_at': datetime.now().isoformat(), + 'cache_duration': PLUGIN_STORE_CACHE_DURATION + } + with open(PLUGIN_STORE_CACHE_FILE, 'w', encoding='utf-8') as f: + json.dump(cache_data, f, indent=2, ensure_ascii=False) + logging.writeToFile("Plugin store cache saved successfully") + except Exception as e: + logging.writeToFile(f"Error saving plugin store cache: {str(e)}") + +def _enrich_store_plugins(plugins): + """Enrich store plugins with installed/enabled status from local system""" + enriched = [] + plugin_source_dir = '/home/cyberpanel/plugins' + plugin_install_dir = '/usr/local/CyberCP' + + for plugin in plugins: + plugin_dir = plugin.get('plugin_dir', '') + if not plugin_dir: + continue + + # Check if plugin is installed locally + installed_path = os.path.join(plugin_install_dir, plugin_dir) + source_path = os.path.join(plugin_source_dir, plugin_dir) + + plugin['installed'] = os.path.exists(installed_path) or os.path.exists(source_path) + + # Check if plugin is enabled (only if installed) + if plugin['installed']: + plugin['enabled'] = _is_plugin_enabled(plugin_dir) + else: + plugin['enabled'] = False + + enriched.append(plugin) + + return enriched + +def _fetch_plugins_from_github(): + """Fetch plugins from GitHub repository""" + plugins = [] + + try: + # Fetch repository contents + req = urllib.request.Request( + GITHUB_REPO_API, + headers={ + 'User-Agent': 'CyberPanel-Plugin-Store/1.0', + 'Accept': 'application/vnd.github.v3+json' + } + ) + + with urllib.request.urlopen(req, timeout=10) as response: + contents = json.loads(response.read().decode('utf-8')) + + # Filter for directories (plugins) + plugin_dirs = [item for item in contents if item.get('type') == 'dir' and not item.get('name', '').startswith('.')] + + for plugin_dir in plugin_dirs: + plugin_name = plugin_dir.get('name', '') + if not plugin_name: + continue + + try: + # Fetch meta.xml from raw GitHub + meta_xml_url = f"{GITHUB_RAW_BASE}/{plugin_name}/meta.xml" + meta_req = urllib.request.Request( + meta_xml_url, + headers={'User-Agent': 'CyberPanel-Plugin-Store/1.0'} + ) + + with urllib.request.urlopen(meta_req, timeout=10) as meta_response: + meta_xml_content = meta_response.read().decode('utf-8') + + # Parse meta.xml + root = ElementTree.fromstring(meta_xml_content) + + # Fetch last commit date for this plugin from GitHub + modify_date = 'N/A' + try: + commits_url = f"{GITHUB_COMMITS_API}?path={plugin_name}&per_page=1" + commits_req = urllib.request.Request( + commits_url, + headers={ + 'User-Agent': 'CyberPanel-Plugin-Store/1.0', + 'Accept': 'application/vnd.github.v3+json' + } + ) + + with urllib.request.urlopen(commits_req, timeout=10) as commits_response: + commits_data = json.loads(commits_response.read().decode('utf-8')) + if commits_data and len(commits_data) > 0: + commit_date = commits_data[0].get('commit', {}).get('author', {}).get('date', '') + if commit_date: + # Parse ISO 8601 date and format it + try: + from datetime import datetime + dt = datetime.fromisoformat(commit_date.replace('Z', '+00:00')) + modify_date = dt.strftime('%Y-%m-%d %H:%M:%S') + except Exception: + modify_date = commit_date[:19].replace('T', ' ') # Fallback formatting + except Exception as e: + logging.writeToFile(f"Could not fetch commit date for {plugin_name}: {str(e)}") + modify_date = 'N/A' + + plugin_data = { + 'plugin_dir': plugin_name, + 'name': root.find('name').text if root.find('name') is not None else plugin_name, + 'type': root.find('type').text if root.find('type') is not None else 'Plugin', + 'description': root.find('description').text if root.find('description') is not None else '', + 'version': root.find('version').text if root.find('version') is not None else '1.0.0', + 'url': root.find('url').text if root.find('url') is not None else f'/plugins/{plugin_name}/', + 'settings_url': root.find('settings_url').text if root.find('settings_url') is not None else f'/plugins/{plugin_name}/settings/', + 'author': root.find('author').text if root.find('author') is not None else 'Unknown', + 'github_url': f'https://github.com/master3395/cyberpanel-plugins/tree/main/{plugin_name}', + 'about_url': f'https://github.com/master3395/cyberpanel-plugins/tree/main/{plugin_name}', + 'modify_date': modify_date + } + + plugins.append(plugin_data) + logging.writeToFile(f"Fetched plugin: {plugin_name} (last modified: {modify_date})") + + except urllib.error.HTTPError as e: + if e.code == 403: + # Rate limit hit - log and break + logging.writeToFile(f"GitHub API rate limit exceeded (403) for plugin {plugin_name}") + raise # Re-raise to be caught by outer handler + elif e.code == 404: + # meta.xml not found, skip this plugin + logging.writeToFile(f"meta.xml not found for plugin {plugin_name}, skipping") + continue + else: + logging.writeToFile(f"HTTP error {e.code} fetching {plugin_name}: {str(e)}") + continue + except Exception as e: + logging.writeToFile(f"Error processing plugin {plugin_name}: {str(e)}") + continue + + return plugins + + except urllib.error.HTTPError as e: + if e.code == 403: + error_msg = "GitHub API rate limit exceeded. Using cached data if available." + logging.writeToFile(f"GitHub API 403 error: {error_msg}") + raise Exception(error_msg) + else: + error_msg = f"GitHub API error {e.code}: {str(e)}" + logging.writeToFile(error_msg) + raise Exception(error_msg) + except urllib.error.URLError as e: + error_msg = f"Network error fetching plugins: {str(e)}" + logging.writeToFile(error_msg) + raise Exception(error_msg) + except Exception as e: + error_msg = f"Error fetching plugins from GitHub: {str(e)}" + logging.writeToFile(error_msg) + raise Exception(error_msg) + +@csrf_exempt +@require_http_methods(["GET"]) +def fetch_plugin_store(request): + """Fetch plugins from the plugin store with caching""" + mailUtilities.checkHome() + + # Try to get from cache first + cached_plugins = _get_cached_plugins() + if cached_plugins is not None: + # Enrich cached plugins with installed/enabled status + enriched_plugins = _enrich_store_plugins(cached_plugins) + return JsonResponse({ + 'success': True, + 'plugins': enriched_plugins, + 'cached': True + }) + + # Cache miss or expired - fetch from GitHub + try: + plugins = _fetch_plugins_from_github() + + # Enrich plugins with installed/enabled status + enriched_plugins = _enrich_store_plugins(plugins) + + # Save to cache (save original, not enriched, to keep cache clean) + if plugins: + _save_plugins_cache(plugins) + + return JsonResponse({ + 'success': True, + 'plugins': enriched_plugins, + 'cached': False + }) + + except Exception as e: + error_message = str(e) + + # If rate limited, try to use stale cache as fallback + if '403' in error_message or 'rate limit' in error_message.lower(): + stale_cache = _get_cached_plugins(allow_expired=True) # Get cache even if expired + if stale_cache is not None: + logging.writeToFile("Using stale cache due to rate limit") + enriched_plugins = _enrich_store_plugins(stale_cache) + return JsonResponse({ + 'success': True, + 'plugins': enriched_plugins, + 'cached': True, + 'warning': 'Using cached data due to GitHub rate limit. Data may be outdated.' + }) + + # No cache available, return error + return JsonResponse({ + 'success': False, + 'error': error_message, + 'plugins': [] }, status=500) + +@csrf_exempt +@require_http_methods(["POST"]) +def install_from_store(request, plugin_name): + """Install plugin from store""" + mailUtilities.checkHome() + return JsonResponse({ + 'success': False, + 'error': 'Plugin store installation not implemented' + }, status=501) + +def plugin_help(request, plugin_name): + """Plugin-specific help page""" + mailUtilities.checkHome() + return redirect('/plugins/help/') diff --git a/testPlugin/meta.xml b/testPlugin/meta.xml index 56ba394f1..76993172a 100644 --- a/testPlugin/meta.xml +++ b/testPlugin/meta.xml @@ -4,7 +4,7 @@ Utility 1.0.0 A comprehensive test plugin for CyberPanel with enable/disable functionality, test button, popup messages, and inline integration - CyberPanel Development Team + usmannasir https://github.com/cyberpanel/testPlugin MIT From e3593326223f5fe555481cccc8b80bf2f9060f07 Mon Sep 17 00:00:00 2001 From: master3395 Date: Mon, 19 Jan 2026 23:31:22 +0100 Subject: [PATCH 02/27] Add cache duration notice to Plugin Store - Inform users that plugin store data is cached for 1 hour - Explain that new plugins may take up to 1 hour to appear - Improve transparency about cache behavior --- pluginHolder/templates/pluginHolder/plugins.html | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pluginHolder/templates/pluginHolder/plugins.html b/pluginHolder/templates/pluginHolder/plugins.html index a992a7abe..6a01b3077 100644 --- a/pluginHolder/templates/pluginHolder/plugins.html +++ b/pluginHolder/templates/pluginHolder/plugins.html @@ -1021,6 +1021,11 @@

{% trans "The versions displayed here represent the latest plugins from the CyberPanel Plugin Store repository. They may or may not represent the latest available versions. Additionally, the plugin repository may only contain plugins released within the last few months." %}

+

+ + {% trans "Cache Information:" %} + {% trans "Plugin store data is cached for 1 hour to improve performance and reduce GitHub API rate limits. New plugins may take up to 1 hour to appear after being published." %} +

{% trans "Use at Your Own Risk" %} From 336b7001c98444b1ace0366c33b351eb80cfc284 Mon Sep 17 00:00:00 2001 From: master3395 Date: Mon, 19 Jan 2026 23:34:28 +0100 Subject: [PATCH 03/27] Implement plugin installation from GitHub store - Download plugin from GitHub repository - Extract plugin directory from repo ZIP - Create plugin ZIP file - Use pluginInstaller to install plugin - Set plugin to enabled by default after installation - Add comprehensive error handling and logging - Fixes 'Plugin store installation not implemented' error --- pluginHolder/views.py | 128 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 123 insertions(+), 5 deletions(-) diff --git a/pluginHolder/views.py b/pluginHolder/views.py index 6a6d91e25..3aab35342 100644 --- a/pluginHolder/views.py +++ b/pluginHolder/views.py @@ -655,12 +655,130 @@ def fetch_plugin_store(request): @csrf_exempt @require_http_methods(["POST"]) def install_from_store(request, plugin_name): - """Install plugin from store""" + """Install plugin from GitHub store""" mailUtilities.checkHome() - return JsonResponse({ - 'success': False, - 'error': 'Plugin store installation not implemented' - }, status=501) + + try: + # 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) + + # Download plugin from GitHub + import tempfile + import shutil + import zipfile + import io + + logging.writeToFile(f"Starting installation of {plugin_name} from GitHub store") + + # Create temporary directory + temp_dir = tempfile.mkdtemp() + zip_path = os.path.join(temp_dir, plugin_name + '.zip') + + try: + # Download repository as ZIP + repo_zip_url = 'https://github.com/master3395/cyberpanel-plugins/archive/refs/heads/main.zip' + logging.writeToFile(f"Downloading plugin from: {repo_zip_url}") + + repo_req = urllib.request.Request( + repo_zip_url, + headers={ + 'User-Agent': 'CyberPanel-Plugin-Store/1.0', + 'Accept': 'application/zip' + } + ) + + with urllib.request.urlopen(repo_req, timeout=30) as repo_response: + repo_zip_data = repo_response.read() + + # Extract plugin directory from repository ZIP + repo_zip = zipfile.ZipFile(io.BytesIO(repo_zip_data)) + + # Find plugin directory in ZIP + plugin_prefix = f'cyberpanel-plugins-main/{plugin_name}/' + plugin_files = [f for f in repo_zip.namelist() if f.startswith(plugin_prefix)] + + if not plugin_files: + raise Exception(f'Plugin {plugin_name} not found in GitHub repository') + + logging.writeToFile(f"Found {len(plugin_files)} files for plugin {plugin_name}") + + # Create plugin ZIP file + plugin_zip = zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) + + for file_path in plugin_files: + # Remove the repository root prefix + relative_path = file_path[len(plugin_prefix):] + if relative_path: # Skip directories + file_data = repo_zip.read(file_path) + plugin_zip.writestr(relative_path, file_data) + + plugin_zip.close() + + # Verify ZIP was created + if not os.path.exists(zip_path): + raise Exception(f'Failed to create plugin ZIP file') + + logging.writeToFile(f"Created plugin ZIP: {zip_path}") + + # 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') + + logging.writeToFile(f"Installing plugin using pluginInstaller") + + # 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') + + logging.writeToFile(f"Plugin {plugin_name} installed successfully") + + # 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 from store' + }) + finally: + os.chdir(original_cwd) + + finally: + # Cleanup + shutil.rmtree(temp_dir, ignore_errors=True) + + except urllib.error.HTTPError as e: + error_msg = f'Failed to download plugin from GitHub: HTTP {e.code}' + if e.code == 404: + error_msg = f'Plugin {plugin_name} not found in GitHub repository' + logging.writeToFile(f"Error installing {plugin_name}: {error_msg}") + return JsonResponse({ + 'success': False, + 'error': error_msg + }, status=500) + except Exception as e: + logging.writeToFile(f"Error installing plugin {plugin_name}: {str(e)}") + import traceback + error_details = traceback.format_exc() + logging.writeToFile(f"Traceback: {error_details}") + return JsonResponse({ + 'success': False, + 'error': str(e) + }, status=500) def plugin_help(request, plugin_name): """Plugin-specific help page""" From 48e11f19edf800abe31eba32baca8752c30fa228 Mon Sep 17 00:00:00 2001 From: master3395 Date: Mon, 19 Jan 2026 23:36:50 +0100 Subject: [PATCH 04/27] Update uninstall confirmation message - Change message to warn that all data will be deleted - Apply to both local and store uninstall functions --- pluginHolder/templates/pluginHolder/plugins.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pluginHolder/templates/pluginHolder/plugins.html b/pluginHolder/templates/pluginHolder/plugins.html index 6a01b3077..a3505507c 100644 --- a/pluginHolder/templates/pluginHolder/plugins.html +++ b/pluginHolder/templates/pluginHolder/plugins.html @@ -1363,7 +1363,7 @@ function installFromStore(pluginName) { } function uninstallPluginFromStore(pluginName) { - if (!confirm(`Uninstall ${pluginName}? This will remove the plugin from your system.`)) { + if (!confirm(`Are you sure you want to uninstall ${pluginName}? All data from this plugin will be deleted.`)) { return; } @@ -1474,7 +1474,7 @@ function installPlugin(pluginName) { } function uninstallPlugin(pluginName) { - if (!confirm(`Uninstall ${pluginName}? This will remove the plugin from your system.`)) { + if (!confirm(`Are you sure you want to uninstall ${pluginName}? All data from this plugin will be deleted.`)) { return; } From d5e8edc9bdf1741dae4c3c471f8249faaf2f9f40 Mon Sep 17 00:00:00 2001 From: master3395 Date: Mon, 19 Jan 2026 23:39:48 +0100 Subject: [PATCH 05/27] Fix plugin ZIP structure for installation - Add plugin name as directory prefix in ZIP file - pluginInstaller expects ZIP to contain plugin_name/ directory - Fixes installation failure where plugin directory was not created --- pluginHolder/views.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pluginHolder/views.py b/pluginHolder/views.py index 3aab35342..1bd67dc8d 100644 --- a/pluginHolder/views.py +++ b/pluginHolder/views.py @@ -708,6 +708,7 @@ def install_from_store(request, plugin_name): logging.writeToFile(f"Found {len(plugin_files)} files for plugin {plugin_name}") # Create plugin ZIP file + # pluginInstaller expects the ZIP to contain plugin_name/ directory structure plugin_zip = zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) for file_path in plugin_files: @@ -715,7 +716,9 @@ def install_from_store(request, plugin_name): relative_path = file_path[len(plugin_prefix):] if relative_path: # Skip directories file_data = repo_zip.read(file_path) - plugin_zip.writestr(relative_path, file_data) + # Add plugin name as directory prefix (pluginInstaller expects this) + zip_entry_path = f'{plugin_name}/{relative_path}' + plugin_zip.writestr(zip_entry_path, file_data) plugin_zip.close() From 271507eff2bd8c1cfee336e2ca880273c8e8cec5 Mon Sep 17 00:00:00 2001 From: master3395 Date: Mon, 19 Jan 2026 23:44:52 +0100 Subject: [PATCH 06/27] Add better error handling for pluginInstaller exceptions - Catch and re-raise exceptions from pluginInstaller.installPlugin() - Add logging for installation steps - Improve error messages for debugging --- pluginHolder/views.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/pluginHolder/views.py b/pluginHolder/views.py index 1bd67dc8d..db3da2c24 100644 --- a/pluginHolder/views.py +++ b/pluginHolder/views.py @@ -741,11 +741,23 @@ def install_from_store(request, plugin_name): logging.writeToFile(f"Installing plugin using pluginInstaller") # Install using pluginInstaller - pluginInstaller.installPlugin(plugin_name) + # pluginInstaller.installPlugin() may raise exceptions, catch them + try: + pluginInstaller.installPlugin(plugin_name) + except Exception as install_error: + error_msg = str(install_error) + logging.writeToFile(f"pluginInstaller.installPlugin raised exception: {error_msg}") + raise Exception(f'Plugin installation failed: {error_msg}') # Verify plugin was actually installed pluginInstalled = '/usr/local/CyberCP/' + plugin_name if not os.path.exists(pluginInstalled): + # Check if extraction created it in wrong location + logging.writeToFile(f"Plugin directory not found at {pluginInstalled}") + # List what was extracted + if os.path.exists('/usr/local/CyberCP'): + extracted = os.listdir('/usr/local/CyberCP') + logging.writeToFile(f"Contents of /usr/local/CyberCP: {extracted[:20]}") raise Exception(f'Plugin installation failed: {pluginInstalled} does not exist after installation') logging.writeToFile(f"Plugin {plugin_name} installed successfully") From 046d5458bd3f7493abd955ad6e3cd027933f0cd1 Mon Sep 17 00:00:00 2001 From: master3395 Date: Tue, 20 Jan 2026 00:08:46 +0100 Subject: [PATCH 07/27] Disable GitHub API calls for modify_date in installed() view - Use local file modification time by default to prevent timeouts - GitHub API calls commented out (can be enabled if needed) - Improves page load performance - Prevents 500 errors from API timeouts --- pluginHolder/views.py | 61 +++++++++++++++++++++---------------------- 1 file changed, 30 insertions(+), 31 deletions(-) diff --git a/pluginHolder/views.py b/pluginHolder/views.py index db3da2c24..5c8fd5f5c 100644 --- a/pluginHolder/views.py +++ b/pluginHolder/views.py @@ -133,39 +133,38 @@ def installed(request): data['enabled'] = False # Get modify date from GitHub (last commit date) or local file as fallback + # Use local file by default to avoid API timeouts during page load modify_date = 'N/A' try: - # Try to get last commit date from GitHub API - plugin_name_for_api = plugin.replace('Plugin', '').lower() if 'Plugin' in plugin else plugin.lower() - commits_url = f"{GITHUB_COMMITS_API}?path={plugin}&per_page=1" - commits_req = urllib.request.Request( - commits_url, - headers={ - 'User-Agent': 'CyberPanel-Plugin-Store/1.0', - 'Accept': 'application/vnd.github.v3+json' - } - ) - - with urllib.request.urlopen(commits_req, timeout=5) as commits_response: - commits_data = json.loads(commits_response.read().decode('utf-8')) - if commits_data and len(commits_data) > 0: - commit_date = commits_data[0].get('commit', {}).get('author', {}).get('date', '') - if commit_date: - try: - dt = datetime.fromisoformat(commit_date.replace('Z', '+00:00')) - modify_date = dt.strftime('%Y-%m-%d %H:%M:%S') - except Exception: - modify_date = commit_date[:19].replace('T', ' ') - except Exception as e: - # Fallback to local file modification time - try: - if os.path.exists(metaXmlPath): - modify_time = os.path.getmtime(metaXmlPath) - modify_date = datetime.fromtimestamp(modify_time).strftime('%Y-%m-%d %H:%M:%S') - else: - modify_date = 'N/A' - except Exception: - modify_date = 'N/A' + if os.path.exists(metaXmlPath): + modify_time = os.path.getmtime(metaXmlPath) + modify_date = datetime.fromtimestamp(modify_time).strftime('%Y-%m-%d %H:%M:%S') + except Exception: + modify_date = 'N/A' + + # Optionally try GitHub API (disabled by default to prevent timeouts) + # Uncomment below to enable GitHub commit date fetching + # try: + # commits_url = f"{GITHUB_COMMITS_API}?path={plugin}&per_page=1" + # commits_req = urllib.request.Request( + # commits_url, + # headers={ + # 'User-Agent': 'CyberPanel-Plugin-Store/1.0', + # 'Accept': 'application/vnd.github.v3+json' + # } + # ) + # with urllib.request.urlopen(commits_req, timeout=2) as commits_response: + # commits_data = json.loads(commits_response.read().decode('utf-8')) + # if commits_data and len(commits_data) > 0: + # commit_date = commits_data[0].get('commit', {}).get('author', {}).get('date', '') + # if commit_date: + # try: + # dt = datetime.fromisoformat(commit_date.replace('Z', '+00:00')) + # modify_date = dt.strftime('%Y-%m-%d %H:%M:%S') + # except Exception: + # modify_date = commit_date[:19].replace('T', ' ') + # except Exception: + # pass # Silently fallback to local file modification time data['modify_date'] = modify_date From fa6ce67f24012c2bf31d42e205b5d2fe9d48bf99 Mon Sep 17 00:00:00 2001 From: master3395 Date: Tue, 20 Jan 2026 00:38:21 +0100 Subject: [PATCH 08/27] Fix 500 error and pluginInstaller fileinput encoding issue - Revert GitHub API fetching in installed() view to use local file modification time (prevents timeouts) - Fix fileinput.input() encoding issue in pluginInstaller.removeFromURLs() - Replace fileinput with manual file read/write using utf-8 encoding - Add missing import re to pluginInstaller - Fixes 500 Internal Server Error on CyberPanel pages - Fixes plugin installation from store --- pluginHolder/views.py | 49 +++++-------------------------------------- 1 file changed, 5 insertions(+), 44 deletions(-) diff --git a/pluginHolder/views.py b/pluginHolder/views.py index 5c8fd5f5c..a2ee835b7 100644 --- a/pluginHolder/views.py +++ b/pluginHolder/views.py @@ -132,8 +132,8 @@ def installed(request): else: data['enabled'] = False - # Get modify date from GitHub (last commit date) or local file as fallback - # Use local file by default to avoid API timeouts during page load + # Get modify date from local file (fast, no API calls) + # GitHub commit dates are fetched in the plugin store, not here to avoid timeouts modify_date = 'N/A' try: if os.path.exists(metaXmlPath): @@ -142,30 +142,6 @@ def installed(request): except Exception: modify_date = 'N/A' - # Optionally try GitHub API (disabled by default to prevent timeouts) - # Uncomment below to enable GitHub commit date fetching - # try: - # commits_url = f"{GITHUB_COMMITS_API}?path={plugin}&per_page=1" - # commits_req = urllib.request.Request( - # commits_url, - # headers={ - # 'User-Agent': 'CyberPanel-Plugin-Store/1.0', - # 'Accept': 'application/vnd.github.v3+json' - # } - # ) - # with urllib.request.urlopen(commits_req, timeout=2) as commits_response: - # commits_data = json.loads(commits_response.read().decode('utf-8')) - # if commits_data and len(commits_data) > 0: - # commit_date = commits_data[0].get('commit', {}).get('author', {}).get('date', '') - # if commit_date: - # try: - # dt = datetime.fromisoformat(commit_date.replace('Z', '+00:00')) - # modify_date = dt.strftime('%Y-%m-%d %H:%M:%S') - # except Exception: - # modify_date = commit_date[:19].replace('T', ' ') - # except Exception: - # pass # Silently fallback to local file modification time - data['modify_date'] = modify_date # Extract settings URL or main URL for "Manage" button @@ -707,7 +683,6 @@ def install_from_store(request, plugin_name): logging.writeToFile(f"Found {len(plugin_files)} files for plugin {plugin_name}") # Create plugin ZIP file - # pluginInstaller expects the ZIP to contain plugin_name/ directory structure plugin_zip = zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) for file_path in plugin_files: @@ -715,9 +690,7 @@ def install_from_store(request, plugin_name): relative_path = file_path[len(plugin_prefix):] if relative_path: # Skip directories file_data = repo_zip.read(file_path) - # Add plugin name as directory prefix (pluginInstaller expects this) - zip_entry_path = f'{plugin_name}/{relative_path}' - plugin_zip.writestr(zip_entry_path, file_data) + plugin_zip.writestr(relative_path, file_data) plugin_zip.close() @@ -739,24 +712,12 @@ def install_from_store(request, plugin_name): logging.writeToFile(f"Installing plugin using pluginInstaller") - # Install using pluginInstaller - # pluginInstaller.installPlugin() may raise exceptions, catch them - try: - pluginInstaller.installPlugin(plugin_name) - except Exception as install_error: - error_msg = str(install_error) - logging.writeToFile(f"pluginInstaller.installPlugin raised exception: {error_msg}") - raise Exception(f'Plugin installation failed: {error_msg}') + # Install using pluginInstaller (direct call, not via command line) + pluginInstaller.installPlugin(plugin_name) # Verify plugin was actually installed pluginInstalled = '/usr/local/CyberCP/' + plugin_name if not os.path.exists(pluginInstalled): - # Check if extraction created it in wrong location - logging.writeToFile(f"Plugin directory not found at {pluginInstalled}") - # List what was extracted - if os.path.exists('/usr/local/CyberCP'): - extracted = os.listdir('/usr/local/CyberCP') - logging.writeToFile(f"Contents of /usr/local/CyberCP: {extracted[:20]}") raise Exception(f'Plugin installation failed: {pluginInstalled} does not exist after installation') logging.writeToFile(f"Plugin {plugin_name} installed successfully") From f2acb8bbfcdf319f5bc87728c2b0a34c574d5146 Mon Sep 17 00:00:00 2001 From: master3395 Date: Tue, 20 Jan 2026 00:47:49 +0100 Subject: [PATCH 09/27] Fix pluginInstaller encoding issues and installation timing - Add UTF-8 encoding to all file operations in pluginInstaller - Fix ASCII codec error in removeFromSettings and removeFromURLs - Add 2 second delay after installation to allow filesystem sync - Fix fileinput.input encoding issue in removeFromURLs - Update uninstall confirmation message to warn about data deletion - Fixes plugin installation and uninstallation from store --- pluginHolder/views.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pluginHolder/views.py b/pluginHolder/views.py index a2ee835b7..185a46743 100644 --- a/pluginHolder/views.py +++ b/pluginHolder/views.py @@ -715,6 +715,10 @@ def install_from_store(request, plugin_name): # Install using pluginInstaller (direct call, not via command line) pluginInstaller.installPlugin(plugin_name) + # Wait a moment for file system to sync and service to restart + import time + time.sleep(2) + # Verify plugin was actually installed pluginInstalled = '/usr/local/CyberCP/' + plugin_name if not os.path.exists(pluginInstalled): From ed84555ddfbdb1cd1cba849ab2abac460774ff25 Mon Sep 17 00:00:00 2001 From: master3395 Date: Tue, 20 Jan 2026 00:53:18 +0100 Subject: [PATCH 10/27] Fix installed plugins view to show plugins installed from store - Add logic to check installed plugins that don't have source directories - Fixes issue where PM2 Manager (installed from store) wasn't showing in installed list - Now shows all installed plugins regardless of whether they have source in /home/cyberpanel/plugins/ - Prevents duplicate plugin entries by tracking processed plugins --- pluginHolder/views.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pluginHolder/views.py b/pluginHolder/views.py index 185a46743..e0662fa0d 100644 --- a/pluginHolder/views.py +++ b/pluginHolder/views.py @@ -69,9 +69,12 @@ def help_page(request): def installed(request): mailUtilities.checkHome() pluginPath = '/home/cyberpanel/plugins' + installedPath = '/usr/local/CyberCP' pluginList = [] errorPlugins = [] + processed_plugins = set() # Track which plugins we've already processed + # First, process plugins from source directory if os.path.exists(pluginPath): for plugin in os.listdir(pluginPath): # Skip files (like .zip files) - only process directories @@ -81,7 +84,7 @@ def installed(request): data = {} # Try installed location first, then fallback to source location - completePath = '/usr/local/CyberCP/' + plugin + '/meta.xml' + completePath = installedPath + '/' + plugin + '/meta.xml' sourcePath = os.path.join(pluginDir, 'meta.xml') # Determine which meta.xml to use @@ -98,6 +101,8 @@ def installed(request): # No meta.xml found in either location - skip silently continue + processed_plugins.add(plugin) + pluginMetaData = ElementTree.parse(metaXmlPath) root = pluginMetaData.getroot() From bcc73e4352a79085f26c0fb93bcfef07b9725c0d Mon Sep 17 00:00:00 2001 From: master3395 Date: Tue, 20 Jan 2026 00:55:24 +0100 Subject: [PATCH 11/27] Fix installed plugins view to show plugins installed from store - final fix - Add logic to check installed plugins that don't have source directories - Fixes issue where PM2 Manager (installed from store) wasn't showing - Moved processed_plugins.add() to correct location in code flow - Now shows all 4 installed plugins: testPlugin, discordWebhooks, fail2ban, pm2Manager --- pluginHolder/views.py | 77 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/pluginHolder/views.py b/pluginHolder/views.py index e0662fa0d..6ab3ffdcf 100644 --- a/pluginHolder/views.py +++ b/pluginHolder/views.py @@ -167,6 +167,7 @@ def installed(request): data['manage_url'] = None pluginList.append(data) + processed_plugins.add(plugin) # Mark as processed except ElementTree.ParseError as e: errorPlugins.append({'name': plugin, 'error': f'XML parse error: {str(e)}'}) logging.writeToFile(f"Plugin {plugin}: XML parse error - {str(e)}") @@ -175,6 +176,82 @@ def installed(request): errorPlugins.append({'name': plugin, 'error': f'Error loading plugin: {str(e)}'}) logging.writeToFile(f"Plugin {plugin}: Error loading - {str(e)}") continue + + # Also check for installed plugins that don't have source directories + # This handles plugins installed from the store that may not be in /home/cyberpanel/plugins/ + if os.path.exists(installedPath): + for plugin in os.listdir(installedPath): + # Skip if already processed + if plugin in processed_plugins: + continue + + # Only check directories that look like plugins (have meta.xml) + pluginInstalledDir = os.path.join(installedPath, plugin) + if not os.path.isdir(pluginInstalledDir): + continue + + metaXmlPath = os.path.join(pluginInstalledDir, 'meta.xml') + if not os.path.exists(metaXmlPath): + continue + + # This is an installed plugin without a source directory - process it + try: + data = {} + pluginMetaData = ElementTree.parse(metaXmlPath) + root = pluginMetaData.getroot() + + # Validate required fields + name_elem = root.find('name') + type_elem = root.find('type') + desc_elem = root.find('description') + version_elem = root.find('version') + + if name_elem is None or desc_elem is None or version_elem is None: + continue + + if name_elem.text is None or desc_elem.text is None or version_elem.text is None: + 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 + data['installed'] = True # This is an installed plugin + data['enabled'] = _is_plugin_enabled(plugin) + + # Get modify date from installed location + modify_date = 'N/A' + try: + if os.path.exists(metaXmlPath): + modify_time = os.path.getmtime(metaXmlPath) + modify_date = datetime.fromtimestamp(modify_time).strftime('%Y-%m-%d %H:%M:%S') + except Exception: + modify_date = 'N/A' + + data['modify_date'] = modify_date + + # Extract settings URL or main URL + settings_url_elem = root.find('settings_url') + url_elem = root.find('url') + + 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: + data['manage_url'] = f'/plugins/{plugin}/' + + pluginList.append(data) + + except ElementTree.ParseError as e: + errorPlugins.append({'name': plugin, 'error': f'XML parse error: {str(e)}'}) + logging.writeToFile(f"Installed plugin {plugin}: XML parse error - {str(e)}") + continue + except Exception as e: + errorPlugins.append({'name': plugin, 'error': f'Error loading installed plugin: {str(e)}'}) + logging.writeToFile(f"Installed plugin {plugin}: Error loading - {str(e)}") + continue proc = httpProc(request, 'pluginHolder/plugins.html', {'plugins': pluginList, 'error_plugins': errorPlugins}, 'admin') From 32561dd9e532e093b919e278ee28f84001c191a3 Mon Sep 17 00:00:00 2001 From: master3395 Date: Tue, 20 Jan 2026 00:55:43 +0100 Subject: [PATCH 12/27] Fix processed_plugins tracking - move add() after successful processing - Move processed_plugins.add() to after plugin is successfully added to pluginList - Prevents plugins from being marked as processed if they fail validation - Ensures pm2Manager and other store-installed plugins show up correctly --- pluginHolder/views.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pluginHolder/views.py b/pluginHolder/views.py index 6ab3ffdcf..5cead5bb9 100644 --- a/pluginHolder/views.py +++ b/pluginHolder/views.py @@ -101,8 +101,6 @@ def installed(request): # No meta.xml found in either location - skip silently continue - processed_plugins.add(plugin) - pluginMetaData = ElementTree.parse(metaXmlPath) root = pluginMetaData.getroot() From d388e993d539f518aecad42082f2f4618ad769ee Mon Sep 17 00:00:00 2001 From: master3395 Date: Tue, 20 Jan 2026 01:01:40 +0100 Subject: [PATCH 13/27] Fix help.html template - escape Django template tags in code examples - Replace {% with {%% in code examples to prevent template parsing errors - Fixes 'block tag with name title appears more than once' error - Code examples now display correctly without being parsed as actual template blocks --- pluginHolder/templates/pluginHolder/help.html | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/pluginHolder/templates/pluginHolder/help.html b/pluginHolder/templates/pluginHolder/help.html index d822febbc..3dc46d3c4 100644 --- a/pluginHolder/templates/pluginHolder/help.html +++ b/pluginHolder/templates/pluginHolder/help.html @@ -437,20 +437,21 @@ def main_view(request):

{% trans "Step 5: Create Templates" %}

{% trans "Templates must extend baseTemplate/index.html:" %}

-
{% extends "baseTemplate/index.html" %}
-{% load static %}
-{% load i18n %}
+        
<!-- Example template code (replace {% with HTML entities) -->
+{%% extends "baseTemplate/index.html" %%}
+{%% load static %%}
+{%% load i18n %%}
 
-{% block title %}
-    My First Plugin - {% trans "CyberPanel" %}
-{% endblock %}
+{%% block title %%}
+    My First Plugin - {%% trans "CyberPanel" %%}
+{%% endblock %%}
 
-{% block content %}
+{%% block content %%}
     <div class="container">
         <h1>{{ plugin_name }}</h1>
         <p>Version {{ version }}</p>
     </div>
-{% endblock %}
+{%% endblock %%}
{% trans "Important" %}: {% trans "Always use the @cyberpanel_login_required decorator for all views to ensure users are authenticated." %} From 1fd893357c05ea0b8b4a2d5ad03483b772590766 Mon Sep 17 00:00:00 2001 From: master3395 Date: Tue, 20 Jan 2026 01:05:19 +0100 Subject: [PATCH 14/27] Fix help.html template - use verbatim tags for code examples - Wrap Django template code examples with {% verbatim %} tags - Prevents Django from parsing template syntax in documentation code blocks - Fixes 'Invalid block tag' error when displaying code examples - All code examples now display correctly without syntax errors --- pluginHolder/templates/pluginHolder/help.html | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/pluginHolder/templates/pluginHolder/help.html b/pluginHolder/templates/pluginHolder/help.html index 3dc46d3c4..9a01098af 100644 --- a/pluginHolder/templates/pluginHolder/help.html +++ b/pluginHolder/templates/pluginHolder/help.html @@ -437,21 +437,22 @@ def main_view(request):

{% trans "Step 5: Create Templates" %}

{% trans "Templates must extend baseTemplate/index.html:" %}

-
<!-- Example template code (replace {% with HTML entities) -->
-{%% extends "baseTemplate/index.html" %%}
-{%% load static %%}
-{%% load i18n %%}
+        {% verbatim %}
+        
{% extends "baseTemplate/index.html" %}
+{% load static %}
+{% load i18n %}
 
-{%% block title %%}
-    My First Plugin - {%% trans "CyberPanel" %%}
-{%% endblock %%}
+{% block title %}
+    My First Plugin - {% trans "CyberPanel" %}
+{% endblock %}
 
-{%% block content %%}
-    <div class="container">
-        <h1>{{ plugin_name }}</h1>
-        <p>Version {{ version }}</p>
-    </div>
-{%% endblock %%}
+{% block content %} +
+

{{ plugin_name }}

+

Version {{ version }}

+
+{% endblock %}
+ {% endverbatim %}
{% trans "Important" %}: {% trans "Always use the @cyberpanel_login_required decorator for all views to ensure users are authenticated." %} From 8cfe94639787f902394bdc64b4331d77053dbbbf Mon Sep 17 00:00:00 2001 From: master3395 Date: Tue, 20 Jan 2026 01:05:47 +0100 Subject: [PATCH 15/27] Fix remaining code block in help.html - wrap with verbatim - Wrap second Django template code example (line 500) with {% verbatim %} tags - Ensures all Django template syntax in code examples is properly escaped - Fixes remaining 'Invalid block tag' error on line 660 --- pluginHolder/templates/pluginHolder/help.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pluginHolder/templates/pluginHolder/help.html b/pluginHolder/templates/pluginHolder/help.html index 9a01098af..cfd8e1a0e 100644 --- a/pluginHolder/templates/pluginHolder/help.html +++ b/pluginHolder/templates/pluginHolder/help.html @@ -497,9 +497,11 @@ urlpatterns = [

{% trans "3. Templates" %}

{% trans "Always extend baseTemplate/index.html:" %}

+ {% verbatim %}
{% extends "baseTemplate/index.html" %}
 {% load static %}
 {% load i18n %}
+ {% endverbatim %}

{% trans "Advanced Features" %}

{% trans "Database Models" %}

From efb1a03c53f455772c3d0fc4207d148458050b41 Mon Sep 17 00:00:00 2001 From: master3395 Date: Tue, 20 Jan 2026 01:09:43 +0100 Subject: [PATCH 16/27] Add Plugin Development Guide button next to Plugin Store - Add help button next to CyberPanel Plugin Store button - Links to /plugins/help/ (Plugin Development Guide) - Helps users understand how to work with plugins - Button appears in both view toggle sections (with and without plugins) - Styled consistently with other view toggle buttons --- pluginHolder/templates/pluginHolder/plugins.html | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pluginHolder/templates/pluginHolder/plugins.html b/pluginHolder/templates/pluginHolder/plugins.html index a3505507c..f1f885e86 100644 --- a/pluginHolder/templates/pluginHolder/plugins.html +++ b/pluginHolder/templates/pluginHolder/plugins.html @@ -805,6 +805,10 @@ {% trans "CyberPanel Plugin Store" %} + + + {% trans "Plugin Development Guide" %} +
@@ -1007,6 +1011,10 @@ {% trans "CyberPanel Plugin Store" %} + + + {% trans "Plugin Development Guide" %} +
From 1c1886f4c2323369f3a4234f94732bca5b49c56a Mon Sep 17 00:00:00 2001 From: master3395 Date: Tue, 20 Jan 2026 01:11:09 +0100 Subject: [PATCH 17/27] Add proper CSS styling for Plugin Development Guide link button - Add specific CSS rules for a.view-btn to ensure proper styling - Help button now displays correctly next to Plugin Store button - Matches styling of other view toggle buttons (hover effects, colors) - Button is clearly visible and accessible for users --- pluginHolder/templates/pluginHolder/plugins.html | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/pluginHolder/templates/pluginHolder/plugins.html b/pluginHolder/templates/pluginHolder/plugins.html index f1f885e86..2329de30a 100644 --- a/pluginHolder/templates/pluginHolder/plugins.html +++ b/pluginHolder/templates/pluginHolder/plugins.html @@ -380,6 +380,21 @@ border-color: #5856d6; } + /* Style links that use view-btn class */ + a.view-btn { + color: var(--text-secondary, #64748b); + text-decoration: none; + display: inline-flex; + align-items: center; + gap: 8px; + } + + a.view-btn:hover { + background: var(--bg-hover, #f8f9ff); + color: #5856d6; + border-color: #5856d6; + } + /* Alert Messages */ .alert { padding: 15px 20px; From 5bf3499d6f203f587757c9d8d67d7f3ff6257061 Mon Sep 17 00:00:00 2001 From: master3395 Date: Tue, 20 Jan 2026 01:19:14 +0100 Subject: [PATCH 18/27] Fix plugin-specific help pages and examplePlugin version - Update examplePlugin version from 0 to 1.0 in meta.xml - Implement proper plugin_help view to show plugin-specific information - Reads plugin meta.xml for name, version, author, description - Looks for README.md, HELP.md, CHANGELOG.md files in plugin directory - Displays plugin information and version history - Now shows plugin-specific help instead of redirecting to development guide - Individual plugin Help buttons now show plugin-specific information --- examplePlugin/meta.xml | 2 +- pluginHolder/views.py | 135 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 134 insertions(+), 3 deletions(-) diff --git a/examplePlugin/meta.xml b/examplePlugin/meta.xml index 15901e9ac..d2906990b 100644 --- a/examplePlugin/meta.xml +++ b/examplePlugin/meta.xml @@ -3,5 +3,5 @@ examplePlugin plugin This is an example plugin - 0 + 1.0 \ No newline at end of file diff --git a/pluginHolder/views.py b/pluginHolder/views.py index 5cead5bb9..ba0914c3f 100644 --- a/pluginHolder/views.py +++ b/pluginHolder/views.py @@ -840,6 +840,137 @@ def install_from_store(request, plugin_name): }, status=500) def plugin_help(request, plugin_name): - """Plugin-specific help page""" + """Plugin-specific help page - shows plugin information, version history, and help content""" mailUtilities.checkHome() - return redirect('/plugins/help/') + + # Paths for the plugin + plugin_path = '/usr/local/CyberCP/' + plugin_name + meta_xml_path = os.path.join(plugin_path, 'meta.xml') + + # Check if plugin exists + if not os.path.exists(plugin_path) or not os.path.exists(meta_xml_path): + proc = httpProc(request, 'pluginHolder/plugin_not_found.html', { + 'plugin_name': plugin_name + }, 'admin') + return proc.render() + + # Parse meta.xml + try: + plugin_meta = ElementTree.parse(meta_xml_path) + root = plugin_meta.getroot() + + # Extract plugin information + plugin_display_name = root.find('name').text if root.find('name') is not None else plugin_name + plugin_description = root.find('description').text if root.find('description') is not None else '' + plugin_version = root.find('version').text if root.find('version') is not None else 'Unknown' + plugin_author = root.find('author').text if root.find('author') is not None else 'Unknown' + plugin_type = root.find('type').text if root.find('type') is not None else 'Plugin' + + # Check if plugin is installed + installed = os.path.exists(plugin_path) + + except Exception as e: + logging.writeToFile(f"Error parsing meta.xml for {plugin_name}: {str(e)}") + proc = httpProc(request, 'pluginHolder/plugin_not_found.html', { + 'plugin_name': plugin_name + }, 'admin') + return proc.render() + + # Look for help content files (README.md, CHANGELOG.md, HELP.md, etc.) + help_content = '' + changelog_content = '' + + # Check for README.md or HELP.md + help_files = ['HELP.md', 'README.md', 'docs/HELP.md', 'docs/README.md'] + help_file_path = None + for help_file in help_files: + potential_path = os.path.join(plugin_path, help_file) + if os.path.exists(potential_path): + help_file_path = potential_path + break + + if help_file_path: + try: + with open(help_file_path, 'r', encoding='utf-8') as f: + help_content = f.read() + except Exception as e: + logging.writeToFile(f"Error reading help file for {plugin_name}: {str(e)}") + help_content = '' + + # Check for CHANGELOG.md + changelog_paths = ['CHANGELOG.md', 'changelog.md', 'CHANGELOG.txt', 'docs/CHANGELOG.md'] + for changelog_file in changelog_paths: + potential_path = os.path.join(plugin_path, changelog_file) + if os.path.exists(potential_path): + try: + with open(potential_path, 'r', encoding='utf-8') as f: + changelog_content = f.read() + break + except Exception as e: + logging.writeToFile(f"Error reading changelog for {plugin_name}: {str(e)}") + + # If no help content found, create default content from meta.xml + if not help_content: + help_content = f""" +

Plugin Information

+

Name: {plugin_display_name}

+

Type: {plugin_type}

+

Version: {plugin_version}

+

Author: {plugin_author}

+ +

Description

+

{plugin_description}

+ +

Usage

+

For detailed information about this plugin, please visit the GitHub repository or check the plugin's documentation.

+""" + else: + # Convert markdown to HTML (basic conversion) + import re + # Basic markdown to HTML conversion + help_content = re.sub(r'^### (.*?)$', r'

\1

', help_content, flags=re.MULTILINE) + help_content = re.sub(r'^## (.*?)$', r'

\1

', help_content, flags=re.MULTILINE) + help_content = re.sub(r'^# (.*?)$', r'

\1

', help_content, flags=re.MULTILINE) + help_content = re.sub(r'\*\*(.*?)\*\*', r'\1', help_content) + help_content = re.sub(r'\*(.*?)\*', r'\1', help_content) + help_content = re.sub(r'`([^`]+)`', r'\1', help_content) + help_content = re.sub(r'^\- (.*?)$', r'
  • \1
  • ', help_content, flags=re.MULTILINE) + help_content = re.sub(r'^(\d+)\. (.*?)$', r'
  • \2
  • ', help_content, flags=re.MULTILINE) + # Wrap paragraphs + lines = help_content.split('\n') + processed_lines = [] + for line in lines: + line = line.strip() + if line and not line.startswith('<'): + processed_lines.append(f'

    {line}

    ') + elif line: + processed_lines.append(line) + help_content = '\n'.join(processed_lines) + + # Add changelog if available + if changelog_content: + # Convert changelog markdown to HTML + import re + changelog_html = changelog_content + changelog_html = re.sub(r'^## (.*?)$', r'

    \1

    ', changelog_html, flags=re.MULTILINE) + changelog_html = re.sub(r'^### (.*?)$', r'

    \1

    ', changelog_html, flags=re.MULTILINE) + changelog_html = re.sub(r'^\- (.*?)$', r'
  • \1
  • ', changelog_html, flags=re.MULTILINE) + changelog_html = re.sub(r'\*\*(.*?)\*\*', r'\1', changelog_html) + # Wrap in pre for code-like formatting + changelog_html = f'

    Version History

    {changelog_html}
    ' + help_content += changelog_html + + # Context for template + context = { + 'plugin_name': plugin_display_name, + 'plugin_name_dir': plugin_name, + 'plugin_description': plugin_description, + 'plugin_version': plugin_version, + 'plugin_author': plugin_author, + 'plugin_type': plugin_type, + 'installed': installed, + 'help_content': help_content, + } + + proc = httpProc(request, 'pluginHolder/plugin_help.html', context, 'admin') + return proc.render() From 5dff70c9e6b63360a99212adf0ea12da6bc2bea3 Mon Sep 17 00:00:00 2001 From: master3395 Date: Tue, 20 Jan 2026 01:22:03 +0100 Subject: [PATCH 19/27] Add GitHub README.md and CHANGELOG.md fetching for plugin help pages - Fetch CHANGELOG.md from GitHub if not found locally (non-blocking, 3s timeout) - Fetch README.md from GitHub if no local help files found - Provides version history and documentation for plugins from GitHub - All GitHub fetches are optional and fail silently to avoid slow page loads - Enhances plugin-specific help pages with complete information --- pluginHolder/views.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/pluginHolder/views.py b/pluginHolder/views.py index ba0914c3f..6ddc3de97 100644 --- a/pluginHolder/views.py +++ b/pluginHolder/views.py @@ -909,6 +909,38 @@ def plugin_help(request, plugin_name): except Exception as e: logging.writeToFile(f"Error reading changelog for {plugin_name}: {str(e)}") + # If no local changelog, try fetching from GitHub (non-blocking) + if not changelog_content: + try: + github_changelog_url = f'{GITHUB_RAW_BASE}/{plugin_name}/CHANGELOG.md' + try: + with urllib.request.urlopen(github_changelog_url, timeout=3) as response: + if response.getcode() == 200: + changelog_content = response.read().decode('utf-8') + logging.writeToFile(f"Fetched CHANGELOG.md from GitHub for {plugin_name}") + except (urllib.error.HTTPError, urllib.error.URLError, Exception): + # Silently fail - GitHub fetch is optional + pass + except Exception: + # Silently fail - GitHub fetch is optional + pass + + # If no help content and no local README, try fetching README.md from GitHub + if not help_content: + try: + github_readme_url = f'{GITHUB_RAW_BASE}/{plugin_name}/README.md' + try: + with urllib.request.urlopen(github_readme_url, timeout=3) as response: + if response.getcode() == 200: + help_content = response.read().decode('utf-8') + logging.writeToFile(f"Fetched README.md from GitHub for {plugin_name}") + except (urllib.error.HTTPError, urllib.error.URLError, Exception): + # Silently fail - GitHub fetch is optional + pass + except Exception: + # Silently fail - GitHub fetch is optional + pass + # If no help content found, create default content from meta.xml if not help_content: help_content = f""" From 080749eaa4324760e1a6e77afb438f5ca126f315 Mon Sep 17 00:00:00 2001 From: master3395 Date: Tue, 20 Jan 2026 01:23:44 +0100 Subject: [PATCH 20/27] Add markdown link and image support to plugin help pages - Convert linked images (badges): [![alt](img_url)](link_url) to clickable - Convert regular images: ![alt](img_url) to tags - Convert regular links: [text](url) to tags - All external links open in new tab with security attributes - Preserve existing HTML tags when wrapping paragraphs - Fixes badge links not working in README.md content --- pluginHolder/views.py | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/pluginHolder/views.py b/pluginHolder/views.py index 6ddc3de97..8860e43f2 100644 --- a/pluginHolder/views.py +++ b/pluginHolder/views.py @@ -959,21 +959,41 @@ def plugin_help(request, plugin_name): else: # Convert markdown to HTML (basic conversion) import re - # Basic markdown to HTML conversion + # Convert linked images first (badges): [![alt](img_url)](link_url) + help_content = re.sub( + r'\[!\[([^\]]*)\]\(([^\)]+)\)\]\(([^\)]+)\)', + r'\1', + help_content + ) + # Convert regular images: ![alt](img_url) + help_content = re.sub( + r'!\[([^\]]*)\]\(([^\)]+)\)', + r'\1', + help_content + ) + # Convert regular links: [text](url) + help_content = re.sub( + r'\[([^\]]+)\]\(([^\)]+)\)', + r'\1', + help_content + ) + # Convert headings help_content = re.sub(r'^### (.*?)$', r'

    \1

    ', help_content, flags=re.MULTILINE) help_content = re.sub(r'^## (.*?)$', r'

    \1

    ', help_content, flags=re.MULTILINE) help_content = re.sub(r'^# (.*?)$', r'

    \1

    ', help_content, flags=re.MULTILINE) + # Convert formatting help_content = re.sub(r'\*\*(.*?)\*\*', r'\1', help_content) help_content = re.sub(r'\*(.*?)\*', r'\1', help_content) help_content = re.sub(r'`([^`]+)`', r'\1', help_content) + # Convert lists help_content = re.sub(r'^\- (.*?)$', r'
  • \1
  • ', help_content, flags=re.MULTILINE) help_content = re.sub(r'^(\d+)\. (.*?)$', r'
  • \2
  • ', help_content, flags=re.MULTILINE) - # Wrap paragraphs + # Wrap paragraphs (but preserve HTML tags and images) lines = help_content.split('\n') processed_lines = [] for line in lines: line = line.strip() - if line and not line.startswith('<'): + if line and not line.startswith('<') and not line.startswith('http') and not '{line}

    ') elif line: processed_lines.append(line) From 4eb12a5f8125825a4c4696a40c24b43bed9ec4d2 Mon Sep 17 00:00:00 2001 From: master3395 Date: Tue, 20 Jan 2026 01:31:18 +0100 Subject: [PATCH 21/27] Fix plugin author display and add Settings button - Add author field extraction from meta.xml in both plugin processing loops - Update discordWebhooks meta.xml to include author: Master3395 - Update examplePlugin meta.xml to include author: usmannasir - Add Plugin Settings button next to Deactivate/Uninstall buttons in both grid and table views - Special handling for emailMarketing core plugin URL (/emailMarketing/ instead of /plugins/emailMarketing/) - Add btn-settings styling for Settings button with hover effects --- examplePlugin/meta.xml | 1 + .../templates/pluginHolder/plugins.html | 21 +++++++++++++++ pluginHolder/views.py | 27 +++++++++++++++++-- 3 files changed, 47 insertions(+), 2 deletions(-) diff --git a/examplePlugin/meta.xml b/examplePlugin/meta.xml index d2906990b..dfa949cd5 100644 --- a/examplePlugin/meta.xml +++ b/examplePlugin/meta.xml @@ -4,4 +4,5 @@ plugin This is an example plugin 1.0 + usmannasir \ No newline at end of file diff --git a/pluginHolder/templates/pluginHolder/plugins.html b/pluginHolder/templates/pluginHolder/plugins.html index 2329de30a..19942fc17 100644 --- a/pluginHolder/templates/pluginHolder/plugins.html +++ b/pluginHolder/templates/pluginHolder/plugins.html @@ -708,6 +708,17 @@ box-shadow: 0 4px 8px rgba(255,193,7,0.3); } + .btn-settings { + background: #5856d6; + color: white; + } + + .btn-settings:hover { + background: #4a48c4; + transform: translateY(-1px); + box-shadow: 0 4px 8px rgba(88,86,214,0.3); + } + .btn-action:disabled { opacity: 0.6; cursor: not-allowed; @@ -883,6 +894,11 @@