Merge branch 'v2.5.5-dev' of https://github.com/master3395/cyberpanel into v2.5.5-dev

This commit is contained in:
master3395
2026-01-22 19:25:37 +01:00
12 changed files with 4173 additions and 52 deletions

View File

@@ -83,9 +83,13 @@ INSTALLED_APPS = [
]
# Add plugins that are installed (plugin installer handles adding/removing)
# discordWebhooks is added by plugin installer when plugin is installed
# Plugins are added by plugin installer when plugins are installed
if os.path.exists('/usr/local/CyberCP/discordWebhooks/__init__.py'):
INSTALLED_APPS.append('discordWebhooks')
if os.path.exists('/usr/local/CyberCP/fail2ban/__init__.py'):
INSTALLED_APPS.append('fail2ban')
if os.path.exists('/usr/local/CyberCP/pm2Manager/__init__.py'):
INSTALLED_APPS.append('pm2Manager')
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',

File diff suppressed because it is too large Load Diff

View File

@@ -3,5 +3,5 @@
<name>Email Marketing</name>
<type>plugin</type>
<description>Email Marketing plugin for CyberPanel.</description>
<version>1.0</version>
<version>1.0.0</version>
</cyberpanelPluginConfig>

View File

@@ -3,5 +3,6 @@
<name>examplePlugin</name>
<type>plugin</type>
<description>This is an example plugin</description>
<version>0</version>
<version>1.0.0</version>
<author>usmannasir</author>
</cyberpanelPluginConfig>

View File

@@ -7,12 +7,16 @@ try {
define('PMA_SIGNON_SESSIONNAME', 'SignonSession');
define('PMA_DISABLE_SSL_PEER_VALIDATION', TRUE);
if (isset($_POST['token'])) {
// Handle both GET and POST parameters for token and username
$token = isset($_POST['token']) ? $_POST['token'] : (isset($_GET['token']) ? $_GET['token'] : null);
$username = isset($_POST['username']) ? $_POST['username'] : (isset($_GET['username']) ? $_GET['username'] : null);
if ($token && $username) {
### Get credentials using the token
$token = htmlspecialchars($_POST['token'], ENT_QUOTES, 'UTF-8');
$username = htmlspecialchars($_POST['username'], ENT_QUOTES, 'UTF-8');
$token = htmlspecialchars($token, ENT_QUOTES, 'UTF-8');
$username = htmlspecialchars($username, ENT_QUOTES, 'UTF-8');
//$url = "/dataBases/fetchDetailsPHPMYAdmin?token=" . $token . '&username=' . $username;
$url = "/dataBases/fetchDetailsPHPMYAdmin";
@@ -27,7 +31,7 @@ try {
echo '</form>';
echo '<script>document.getElementById("redirectForm").submit();</script>';
} else if (isset($_POST['logout'])) {
} else if (isset($_POST['logout']) || isset($_GET['logout'])) {
$params = session_get_cookie_params();
setcookie(session_name(), '', time() - 86400, $params["path"], $params["domain"], $params["secure"], $params["httponly"]);
session_destroy();

View File

@@ -1185,7 +1185,7 @@ module cyberpanel_ols {
pass
# Try to fetch latest phpMyAdmin version from GitHub
phpmyadmin_version = '5.2.2' # Fallback version
phpmyadmin_version = '5.2.3' # Fallback version
try:
from plogical.versionFetcher import get_latest_phpmyadmin_version
latest_version = get_latest_phpmyadmin_version()

View File

@@ -22,7 +22,7 @@ class VersionFetcher:
# Fallback versions in case API is unavailable
FALLBACK_VERSIONS = {
'phpmyadmin': '5.2.2',
'phpmyadmin': '5.2.3',
'snappymail': '2.38.2'
}

View File

@@ -0,0 +1,692 @@
{% extends "baseTemplate/index.html" %}
{% load i18n %}
{% load static %}
{% block title %}{% trans "Plugin Development Help - CyberPanel" %}{% endblock %}
{% block header_scripts %}
<style>
.help-wrapper {
background: transparent;
padding: 20px;
max-width: 1400px;
margin: 0 auto;
}
.help-header {
background: var(--bg-primary, white);
border-radius: 12px;
padding: 30px;
margin-bottom: 25px;
box-shadow: var(--shadow-md, 0 2px 8px rgba(0,0,0,0.08));
border: 1px solid var(--border-primary, #e8e9ff);
}
.help-header h1 {
font-size: 32px;
font-weight: 700;
color: var(--text-primary, #2f3640);
margin: 0 0 10px 0;
display: flex;
align-items: center;
gap: 15px;
}
.help-header .icon {
width: 56px;
height: 56px;
background: #5856d6;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 28px;
box-shadow: 0 4px 12px rgba(88,86,214,0.3);
}
.help-header p {
font-size: 16px;
color: var(--text-secondary, #64748b);
margin: 0;
line-height: 1.6;
}
.help-content {
background: var(--bg-primary, white);
border-radius: 12px;
padding: 40px;
box-shadow: var(--shadow-md, 0 2px 8px rgba(0,0,0,0.08));
border: 1px solid var(--border-primary, #e8e9ff);
}
.help-content h2 {
font-size: 24px;
font-weight: 700;
color: var(--text-primary, #2f3640);
margin: 30px 0 15px 0;
padding-bottom: 10px;
border-bottom: 2px solid #5856d6;
}
.help-content h3 {
font-size: 20px;
font-weight: 600;
color: var(--text-primary, #2f3640);
margin: 25px 0 12px 0;
}
.help-content h4 {
font-size: 18px;
font-weight: 600;
color: var(--text-primary, #2f3640);
margin: 20px 0 10px 0;
}
.help-content p {
font-size: 15px;
color: var(--text-secondary, #64748b);
line-height: 1.8;
margin: 12px 0;
}
.help-content ul, .help-content ol {
margin: 15px 0;
padding-left: 30px;
}
.help-content li {
font-size: 15px;
color: var(--text-secondary, #64748b);
line-height: 1.8;
margin: 8px 0;
}
.help-content code {
background: var(--bg-secondary, #f8f9ff);
padding: 2px 6px;
border-radius: 4px;
font-family: 'Courier New', monospace;
font-size: 14px;
color: #5856d6;
}
.help-content pre {
background: #1e1e1e;
color: #d4d4d4;
padding: 20px;
border-radius: 8px;
overflow-x: auto;
margin: 20px 0;
font-family: 'Courier New', monospace;
font-size: 14px;
line-height: 1.6;
}
.help-content pre code {
background: transparent;
color: inherit;
padding: 0;
}
.help-content blockquote {
border-left: 4px solid #5856d6;
padding-left: 20px;
margin: 20px 0;
font-style: italic;
color: var(--text-secondary, #64748b);
}
.help-content table {
width: 100%;
border-collapse: collapse;
margin: 20px 0;
}
.help-content table th,
.help-content table td {
padding: 12px;
border: 1px solid var(--border-primary, #e8e9ff);
text-align: left;
}
.help-content table th {
background: var(--bg-secondary, #f8f9ff);
font-weight: 600;
color: var(--text-primary, #2f3640);
}
.help-content table td {
color: var(--text-secondary, #64748b);
}
.help-content .alert {
padding: 15px 20px;
border-radius: 8px;
margin: 20px 0;
border-left: 4px solid;
}
.help-content .alert-info {
background: #e0f2fe;
border-color: #0ea5e9;
color: #0c4a6e;
}
.help-content .alert-warning {
background: #fef3c7;
border-color: #f59e0b;
color: #92400e;
}
.help-content .alert-success {
background: #d1fae5;
border-color: #10b981;
color: #065f46;
}
.help-content .alert-danger {
background: #fee2e2;
border-color: #ef4444;
color: #991b1b;
}
.toc {
background: var(--bg-secondary, #f8f9ff);
border-radius: 8px;
padding: 20px;
margin: 30px 0;
border: 1px solid var(--border-primary, #e8e9ff);
}
.toc h3 {
margin-top: 0;
color: var(--text-primary, #2f3640);
}
.toc ul {
list-style: none;
padding-left: 0;
}
.toc li {
margin: 8px 0;
}
.toc a {
color: #5856d6;
text-decoration: none;
font-size: 15px;
}
.toc a:hover {
text-decoration: underline;
}
.code-block {
position: relative;
}
.code-block::before {
content: attr(data-language);
position: absolute;
top: 10px;
right: 15px;
background: rgba(255,255,255,0.1);
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
color: #d4d4d4;
}
.badge {
display: inline-block;
padding: 4px 10px;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
}
.badge-required {
background: #fee2e2;
color: #991b1b;
}
.badge-optional {
background: #d1fae5;
color: #065f46;
}
.back-link {
display: inline-flex;
align-items: center;
gap: 8px;
color: #5856d6;
text-decoration: none;
font-weight: 600;
margin-bottom: 20px;
transition: all 0.3s ease;
}
.back-link:hover {
color: #4a90e2;
text-decoration: underline;
}
@media (max-width: 768px) {
.help-wrapper {
padding: 15px;
}
.help-content {
padding: 25px;
}
.help-header h1 {
font-size: 24px;
flex-direction: column;
}
.help-content pre {
font-size: 12px;
padding: 15px;
}
}
</style>
{% endblock %}
{% block content %}
<div class="help-wrapper">
<a href="/plugins/installed" class="back-link">
<i class="fas fa-arrow-left"></i>
{% trans "Back to Installed Plugins" %}
</a>
<div class="help-header">
<h1>
<div class="icon">
<i class="fas fa-book"></i>
</div>
{% trans "Plugin Development Guide" %}
</h1>
<p>{% trans "Comprehensive guide to creating plugins for CyberPanel. Learn how to build, package, and distribute your own plugins." %}</p>
</div>
<div class="help-content">
<div class="toc">
<h3>{% trans "Table of Contents" %}</h3>
<ul>
<li><a href="#introduction">{% trans "Introduction" %}</a></li>
<li><a href="#prerequisites">{% trans "Prerequisites" %}</a></li>
<li><a href="#architecture">{% trans "Plugin Architecture Overview" %}</a></li>
<li><a href="#first-plugin">{% trans "Creating Your First Plugin" %}</a></li>
<li><a href="#structure">{% trans "Plugin Structure & Files" %}</a></li>
<li><a href="#versioning">{% trans "Version Numbering (Semantic Versioning)" %}</a></li>
<li><a href="#components">{% trans "Core Components" %}</a></li>
<li><a href="#advanced">{% trans "Advanced Features" %}</a></li>
<li><a href="#best-practices">{% trans "Best Practices" %}</a></li>
<li><a href="#security">{% trans "Security Guidelines" %}</a></li>
<li><a href="#testing">{% trans "Testing & Debugging" %}</a></li>
<li><a href="#packaging">{% trans "Packaging & Distribution" %}</a></li>
<li><a href="#troubleshooting">{% trans "Troubleshooting" %}</a></li>
<li><a href="#examples">{% trans "Examples & References" %}</a></li>
</ul>
</div>
<h2 id="introduction">{% trans "Introduction" %}</h2>
<p>{% 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." %}</p>
<h3>{% trans "What Can Plugins Do?" %}</h3>
<ul>
<li>{% trans "Add new administrative features" %}</li>
<li>{% trans "Integrate with external services (APIs, webhooks, etc.)" %}</li>
<li>{% trans "Customize the user interface" %}</li>
<li>{% trans "Extend database functionality" %}</li>
<li>{% trans "Add automation and monitoring capabilities" %}</li>
<li>{% trans "Create custom reporting tools" %}</li>
<li>{% trans "Integrate security features" %}</li>
</ul>
<h2 id="prerequisites">{% trans "Prerequisites" %}</h2>
<h3>{% trans "Required Knowledge" %}</h3>
<ul>
<li><strong>Python 3.6+</strong>: {% trans "Basic to intermediate Python knowledge" %}</li>
<li><strong>Django Framework</strong>: {% trans "Understanding of Django views, URLs, templates, and models" %}</li>
<li><strong>HTML/CSS/JavaScript</strong>: {% trans "For creating user interfaces" %}</li>
<li><strong>Linux/Unix</strong>: {% trans "Basic command-line familiarity" %}</li>
<li><strong>XML</strong>: {% trans "Understanding of XML structure for meta.xml" %}</li>
</ul>
<h2 id="architecture">{% trans "Plugin Architecture Overview" %}</h2>
<h3>{% trans "How Plugins Work" %}</h3>
<ol>
<li><strong>{% trans "Plugin Source Location" %}</strong>: <code>/home/cyberpanel/plugins/</code>
<ul>
<li>{% trans "Plugins are stored here before installation" %}</li>
<li>{% trans "Can be uploaded as ZIP files or placed directly" %}</li>
</ul>
</li>
<li><strong>{% trans "Installed Location" %}</strong>: <code>/usr/local/CyberCP/</code>
<ul>
<li>{% trans "After installation, plugins are copied here" %}</li>
<li>{% trans "This is where CyberPanel loads plugins from" %}</li>
</ul>
</li>
</ol>
<h2 id="first-plugin">{% trans "Creating Your First Plugin" %}</h2>
<h3>{% trans "Step 1: Create Plugin Directory Structure" %}</h3>
<pre><code># 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</code></pre>
<h3>{% trans "Step 2: Create meta.xml (REQUIRED)" %}</h3>
<pre><code>&lt;?xml version="1.0" encoding="UTF-8"?&gt;
&lt;cyberpanelPluginConfig&gt;
&lt;name&gt;My First Plugin&lt;/name&gt;
&lt;type&gt;Utility&lt;/type&gt;
&lt;description&gt;A simple example plugin&lt;/description&gt;
&lt;version&gt;1.0.0&lt;/version&gt;
&lt;url&gt;/plugins/myFirstPlugin/&lt;/url&gt;
&lt;settings_url&gt;/plugins/myFirstPlugin/settings/&lt;/settings_url&gt;
&lt;/cyberpanelPluginConfig&gt;</code></pre>
<h3>{% trans "Step 3: Create urls.py" %}</h3>
<pre><code>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'),
]</code></pre>
<h3>{% trans "Step 4: Create views.py" %}</h3>
<pre><code>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)</code></pre>
<h3>{% trans "Step 5: Create Templates" %}</h3>
<p>{% trans "Templates must extend baseTemplate/index.html:" %}</p>
{% verbatim %}
<pre><code>{% 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 %}</code></pre>
{% endverbatim %}
<div class="alert alert-info">
<strong>{% trans "Important" %}:</strong> {% trans "Always use the @cyberpanel_login_required decorator for all views to ensure users are authenticated." %}
</div>
<h2 id="structure">{% trans "Plugin Structure & Files" %}</h2>
<h3>{% trans "Required Files" %}</h3>
<ul>
<li><code>__init__.py</code> - <span class="badge badge-required">{% trans "Required" %}</span> - {% trans "Python package marker" %}</li>
<li><code>meta.xml</code> - <span class="badge badge-required">{% trans "Required" %}</span> - {% trans "Plugin metadata" %}</li>
<li><code>urls.py</code> - <span class="badge badge-required">{% trans "Required" %}</span> - {% trans "URL routing" %}</li>
<li><code>views.py</code> - <span class="badge badge-required">{% trans "Required" %}</span> - {% trans "View functions" %}</li>
</ul>
<h3>{% trans "Optional Files" %}</h3>
<ul>
<li><code>models.py</code> - <span class="badge badge-optional">{% trans "Optional" %}</span> - {% trans "Database models" %}</li>
<li><code>forms.py</code> - <span class="badge badge-optional">{% trans "Optional" %}</span> - {% trans "Django forms" %}</li>
<li><code>utils.py</code> - <span class="badge badge-optional">{% trans "Optional" %}</span> - {% trans "Utility functions" %}</li>
<li><code>templates/</code> - <span class="badge badge-optional">{% trans "Optional" %}</span> - {% trans "HTML templates" %}</li>
<li><code>static/</code> - <span class="badge badge-optional">{% trans "Optional" %}</span> - {% trans "CSS, JS, images" %}</li>
</ul>
<h2 id="versioning">{% trans "Version Numbering (Semantic Versioning)" %}</h2>
<p>{% trans "CyberPanel plugins use semantic versioning (SemVer) with a three-number format (X.Y.Z) to help users understand the impact of each update:" %}</p>
<div style="margin: 25px 0; padding: 20px; background: var(--bg-secondary, #f8f9ff); border-radius: 8px; border-left: 4px solid #5856d6;">
<h3 style="margin-top: 0; color: var(--text-primary, #2f3640);">{% trans "Major Version (X.0.0)" %} <span style="color: #dc2626;"></span></h3>
<p style="margin-bottom: 0;">{% trans "Breaking changes that may require action from users. New major features, complete redesigns, or changes that break existing functionality." %}</p>
<p style="margin-top: 8px; color: var(--text-secondary, #64748b); font-size: 14px;"><strong>{% trans "Example" %}:</strong> 2.8.0 → 3.0.0</p>
</div>
<div style="margin: 25px 0; padding: 20px; background: var(--bg-secondary, #f8f9ff); border-radius: 8px; border-left: 4px solid #3b82f6;">
<h3 style="margin-top: 0; color: var(--text-primary, #2f3640);">{% trans "Minor Version (0.X.0)" %} <span style="color: #2563eb;"></span></h3>
<p style="margin-bottom: 0;">{% trans "New features added in a backward-compatible manner. Enhancements and improvements that don't break existing functionality." %}</p>
<p style="margin-top: 8px; color: var(--text-secondary, #64748b); font-size: 14px;"><strong>{% trans "Example" %}:</strong> 3.0.0 → 3.1.0</p>
</div>
<div style="margin: 25px 0; padding: 20px; background: var(--bg-secondary, #f8f9ff); border-radius: 8px; border-left: 4px solid #10b981;">
<h3 style="margin-top: 0; color: var(--text-primary, #2f3640);">{% trans "Patch Version (0.0.X)" %} <span style="color: #059669;"></span></h3>
<p style="margin-bottom: 0;">{% trans "Bug fixes and minor improvements. Backward-compatible fixes that address issues without adding new features." %}</p>
<p style="margin-top: 8px; color: var(--text-secondary, #64748b); font-size: 14px;"><strong>{% trans "Example" %}:</strong> 3.1.0 → 3.1.1</p>
</div>
<div class="alert alert-info" style="margin-top: 25px;">
<strong>{% trans "Important" %}:</strong> {% trans "Always use the three-number format (X.Y.Z) in your meta.xml file. This makes it easier to track updates and understand the scope of changes. Start with version 1.0.0 for your first release." %}
</div>
<h3>{% trans "Version Format in meta.xml" %}</h3>
<pre><code>&lt;version&gt;1.0.0&lt;/version&gt;</code></pre>
<p>{% trans "Never use formats like '1.0' or 'v1.0'. Always use the full semantic version: '1.0.0'" %}</p>
<h2 id="components">{% trans "Core Components" %}</h2>
<h3>{% trans "1. Authentication & Security" %}</h3>
<p>{% trans "Always use the cyberpanel_login_required decorator:" %}</p>
<pre><code>@cyberpanel_login_required
def my_view(request):
# Your view code
pass</code></pre>
<h3>{% trans "2. URL Routing" %}</h3>
<pre><code>from django.urls import path
from . import views
app_name = 'myPlugin'
urlpatterns = [
path('', views.main_view, name='main'),
path('item/&lt;int:item_id&gt;/', views.item_detail, name='item_detail'),
]</code></pre>
<h3>{% trans "3. Templates" %}</h3>
<p>{% trans "Always extend baseTemplate/index.html:" %}</p>
{% verbatim %}
<pre><code>{% extends "baseTemplate/index.html" %}
{% load static %}
{% load i18n %}</code></pre>
{% endverbatim %}
<h2 id="advanced">{% trans "Advanced Features" %}</h2>
<h3>{% trans "Database Models" %}</h3>
<pre><code>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'</code></pre>
<h3>{% trans "Forms" %}</h3>
<pre><code>from django import forms
class MyForm(forms.Form):
name = forms.CharField(max_length=255, required=True)
email = forms.EmailField(required=True)</code></pre>
<h3>{% trans "API Endpoints" %}</h3>
<pre><code>from django.http import JsonResponse
@cyberpanel_login_required
def api_endpoint(request):
data = {'status': 'success'}
return JsonResponse(data)</code></pre>
<h2 id="best-practices">{% trans "Best Practices" %}</h2>
<ul>
<li>{% trans "Keep files under 500 lines - split into modules if needed" %}</li>
<li>{% trans "Use descriptive names for functions and variables" %}</li>
<li>{% trans "Follow PEP 8 Python style guide" %}</li>
<li>{% trans "Add comments for complex logic" %}</li>
<li>{% trans "Always validate user input" %}</li>
<li>{% trans "Use Django ORM instead of raw SQL" %}</li>
<li>{% trans "Test your plugin thoroughly before distribution" %}</li>
</ul>
<h2 id="security">{% trans "Security Guidelines" %}</h2>
<div class="alert alert-warning">
<strong>{% trans "Security is Critical" %}:</strong> {% trans "Always validate input, use parameterized queries, sanitize output, and check user permissions." %}
</div>
<ul>
<li>{% trans "Always validate and sanitize user input" %}</li>
<li>{% trans "Use Django ORM or parameterized queries" %}</li>
<li>{% trans "Escape HTML in templates (Django does this by default)" %}</li>
<li>{% trans "Validate file uploads (type, size, content)" %}</li>
<li>{% trans "Use HTTPS for sensitive operations" %}</li>
<li>{% trans "Implement rate limiting for API endpoints" %}</li>
</ul>
<h2 id="testing">{% trans "Testing & Debugging" %}</h2>
<h3>{% trans "Common Issues" %}</h3>
<ul>
<li><strong>{% trans "Template Not Found" %}</strong>: {% trans "Check template path and name" %}</li>
<li><strong>{% trans "URL Not Found" %}</strong>: {% trans "Verify URL patterns and app_name" %}</li>
<li><strong>{% trans "Import Errors" %}</strong>: {% trans "Check Python syntax and import paths" %}</li>
<li><strong>{% trans "Static Files Not Loading" %}</strong>: {% trans "Run collectstatic command" %}</li>
</ul>
<h3>{% trans "Debugging Commands" %}</h3>
<pre><code># 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</code></pre>
<h2 id="packaging">{% trans "Packaging & Distribution" %}</h2>
<h3>{% trans "Create Plugin Package" %}</h3>
<pre><code>cd /home/cyberpanel/plugins/myPlugin
zip -r myPlugin-v1.0.0.zip . \
-x "*.pyc" \
-x "__pycache__/*" \
-x "*.log"</code></pre>
<h2 id="troubleshooting">{% trans "Troubleshooting" %}</h2>
<h3>{% trans "Installation Issues" %}</h3>
<ul>
<li>{% trans "Check meta.xml format and validity" %}</li>
<li>{% trans "Verify plugin directory exists" %}</li>
<li>{% trans "Check file permissions" %}</li>
<li>{% trans "Review CyberPanel logs" %}</li>
</ul>
<h3>{% trans "Runtime Issues" %}</h3>
<ul>
<li>{% trans "Verify URL routing" %}</li>
<li>{% trans "Check authentication decorator" %}</li>
<li>{% trans "Review template paths" %}</li>
<li>{% trans "Check for JavaScript errors" %}</li>
</ul>
<h2 id="examples">{% trans "Examples & References" %}</h2>
<h3>{% trans "Reference Plugins" %}</h3>
<ul>
<li><strong>examplePlugin</strong>: {% trans "Basic plugin structure" %}
<ul>
<li>{% trans "Location" %}: <code>/usr/local/CyberCP/examplePlugin/</code></li>
<li>{% trans "URL" %}: <code>/plugins/examplePlugin/</code></li>
</ul>
</li>
<li><strong>testPlugin</strong>: {% trans "Comprehensive example" %}
<ul>
<li>{% trans "Location" %}: <code>/usr/local/CyberCP/testPlugin/</code></li>
<li>{% trans "URL" %}: <code>/plugins/testPlugin/</code></li>
<li>{% trans "Author" %}: <strong>usmannasir</strong></li>
</ul>
</li>
<li><strong>discordWebhooks</strong>: {% trans "Discord webhook integration plugin" %}
<ul>
<li>{% trans "Location" %}: <code>/usr/local/CyberCP/discordWebhooks/</code></li>
<li>{% trans "URL" %}: <code>/plugins/discordWebhooks/</code></li>
<li>{% trans "Author" %}: <strong>Master3395</strong></li>
</ul>
</li>
</ul>
<h3>{% trans "Useful Resources" %}</h3>
<ul>
<li><a href="https://cyberpanel.net/KnowledgeBase/" target="_blank">{% trans "CyberPanel Documentation" %}</a></li>
<li><a href="https://docs.djangoproject.com/" target="_blank">{% trans "Django Documentation" %}</a></li>
<li><a href="https://github.com/master3395/cyberpanel/tree/v2.5.5-dev" target="_blank">{% trans "CyberPanel GitHub Repository" %}</a></li>
<li><a href="https://github.com/master3395/cyberpanel/tree/v2.5.5-dev/docs/PLUGIN_DEVELOPMENT_GUIDE.md" target="_blank">{% trans "Full Plugin Development Guide (Markdown)" %}</a></li>
</ul>
<div class="alert alert-success">
<strong>{% trans "Ready to Start?" %}</strong> {% 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." %}
</div>
<div style="margin-top: 40px; padding-top: 20px; border-top: 2px solid var(--border-primary, #e8e9ff);">
<p style="text-align: center; color: var(--text-secondary, #64748b);">
<strong>{% trans "Author" %}:</strong> master3395 |
<strong>{% trans "Version" %}:</strong> 2.0.0 |
<strong>{% trans "Last Updated" %}:</strong> 2026-01-04
</p>
</div>
</div>
</div>
<script>
// Smooth scrolling for anchor links
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function (e) {
e.preventDefault();
const target = document.querySelector(this.getAttribute('href'));
if (target) {
target.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
});
});
</script>
{% endblock %}

File diff suppressed because it is too large Load Diff

View File

@@ -3,8 +3,12 @@ from . import views
urlpatterns = [
path('installed', views.installed, name='installed'),
path('help/', views.help_page, name='help'),
path('api/install/<str:plugin_name>/', views.install_plugin, name='install_plugin'),
path('api/uninstall/<str:plugin_name>/', views.uninstall_plugin, name='uninstall_plugin'),
path('api/enable/<str:plugin_name>/', views.enable_plugin, name='enable_plugin'),
path('api/disable/<str:plugin_name>/', views.disable_plugin, name='disable_plugin'),
path('api/store/plugins/', views.fetch_plugin_store, name='fetch_plugin_store'),
path('api/store/install/<str:plugin_name>/', views.install_from_store, name='install_from_store'),
path('<str:plugin_name>/help/', views.plugin_help, name='plugin_help'),
]

View File

@@ -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,12 +60,21 @@ 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'
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
@@ -63,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
@@ -114,24 +135,55 @@ def installed(request):
else:
data['enabled'] = False
# 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):
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 for "Manage" button
settings_url_elem = root.find('settings_url')
url_elem = root.find('url')
# Priority: settings_url > url > default pattern
if settings_url_elem is not None and settings_url_elem.text:
# Special handling for core plugins that don't use /plugins/ prefix
if plugin == 'emailMarketing':
# emailMarketing is a core CyberPanel plugin, uses /emailMarketing/ not /plugins/emailMarketing/
data['manage_url'] = '/emailMarketing/'
elif settings_url_elem is not None and settings_url_elem.text:
data['manage_url'] = settings_url_elem.text
elif url_elem is not None and url_elem.text:
data['manage_url'] = url_elem.text
else:
# Default: try /plugins/{plugin_dir}/settings/ or /plugins/{plugin_dir}/
# Only set if plugin is installed (we can't know if the URL exists otherwise)
if os.path.exists(completePath):
data['manage_url'] = f'/plugins/{plugin}/settings/'
# Special handling for emailMarketing
if plugin == 'emailMarketing':
data['manage_url'] = '/emailMarketing/'
elif os.path.exists(completePath):
# Check if settings route exists, otherwise use main plugin URL
settings_route = f'/plugins/{plugin}/settings/'
main_route = f'/plugins/{plugin}/'
# Default to main route - most plugins have a main route even if no settings
data['manage_url'] = main_route
else:
data['manage_url'] = None
# Extract author information
author_elem = root.find('author')
if author_elem is not None and author_elem.text:
data['author'] = author_elem.text
else:
data['author'] = 'Unknown'
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)}")
@@ -140,6 +192,100 @@ 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')
# Priority: settings_url > url > default pattern
# Special handling for core plugins that don't use /plugins/ prefix
if plugin == 'emailMarketing':
# emailMarketing is a core CyberPanel plugin, uses /emailMarketing/ not /plugins/emailMarketing/
data['manage_url'] = '/emailMarketing/'
elif settings_url_elem is not None and settings_url_elem.text:
data['manage_url'] = settings_url_elem.text
elif url_elem is not None and url_elem.text:
data['manage_url'] = url_elem.text
else:
# Default to /plugins/{plugin}/ for regular plugins
# Special handling for emailMarketing
if plugin == 'emailMarketing':
data['manage_url'] = '/emailMarketing/'
else:
# Default to main plugin route (most plugins work from main route)
data['manage_url'] = f'/plugins/{plugin}/'
# Extract author information
author_elem = root.find('author')
if author_elem is not None and author_elem.text:
data['author'] = author_elem.text
else:
data['author'] = 'Unknown'
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')
@@ -336,4 +482,583 @@ 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 GitHub store"""
mailUtilities.checkHome()
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 (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):
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 - shows plugin information, version history, and help content"""
mailUtilities.checkHome()
# 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 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"""
<h2>Plugin Information</h2>
<p><strong>Name:</strong> {plugin_display_name}</p>
<p><strong>Type:</strong> {plugin_type}</p>
<p><strong>Version:</strong> {plugin_version}</p>
<p><strong>Author:</strong> {plugin_author}</p>
<h2>Description</h2>
<p>{plugin_description}</p>
<h2>Usage</h2>
<p>For detailed information about this plugin, please visit the GitHub repository or check the plugin's documentation.</p>
"""
else:
# Convert markdown to HTML (basic conversion)
import re
# Convert linked images first (badges): [![alt](img_url)](link_url)
help_content = re.sub(
r'\[!\[([^\]]*)\]\(([^\)]+)\)\]\(([^\)]+)\)',
r'<a href="\3" target="_blank" rel="noopener noreferrer"><img src="\2" alt="\1" style="display:inline-block;margin:0 4px;vertical-align:middle;"></a>',
help_content
)
# Convert regular images: ![alt](img_url)
help_content = re.sub(
r'!\[([^\]]*)\]\(([^\)]+)\)',
r'<img src="\2" alt="\1" style="display:inline-block;margin:4px 0;max-width:100%;">',
help_content
)
# Convert regular links: [text](url)
help_content = re.sub(
r'\[([^\]]+)\]\(([^\)]+)\)',
r'<a href="\2" target="_blank" rel="noopener noreferrer">\1</a>',
help_content
)
# Convert headings
help_content = re.sub(r'^### (.*?)$', r'<h3>\1</h3>', help_content, flags=re.MULTILINE)
help_content = re.sub(r'^## (.*?)$', r'<h2>\1</h2>', help_content, flags=re.MULTILINE)
help_content = re.sub(r'^# (.*?)$', r'<h1>\1</h1>', help_content, flags=re.MULTILINE)
# Convert formatting
help_content = re.sub(r'\*\*(.*?)\*\*', r'<strong>\1</strong>', help_content)
help_content = re.sub(r'\*(.*?)\*', r'<em>\1</em>', help_content)
help_content = re.sub(r'`([^`]+)`', r'<code>\1</code>', help_content)
# Convert lists
help_content = re.sub(r'^\- (.*?)$', r'<li>\1</li>', help_content, flags=re.MULTILINE)
help_content = re.sub(r'^(\d+)\. (.*?)$', r'<li>\2</li>', help_content, flags=re.MULTILINE)
# 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('<') and not line.startswith('http') and not '<img' in line and not '<a' in line:
processed_lines.append(f'<p>{line}</p>')
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'<h3>\1</h3>', changelog_html, flags=re.MULTILINE)
changelog_html = re.sub(r'^### (.*?)$', r'<h4>\1</h4>', changelog_html, flags=re.MULTILINE)
changelog_html = re.sub(r'^\- (.*?)$', r'<li>\1</li>', changelog_html, flags=re.MULTILINE)
changelog_html = re.sub(r'\*\*(.*?)\*\*', r'<strong>\1</strong>', changelog_html)
# Wrap in pre for code-like formatting
changelog_html = f'<div class="changelog-content"><h2>Version History</h2><pre>{changelog_html}</pre></div>'
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()

View File

@@ -4,7 +4,7 @@
<type>Utility</type>
<version>1.0.0</version>
<description>A comprehensive test plugin for CyberPanel with enable/disable functionality, test button, popup messages, and inline integration</description>
<author>CyberPanel Development Team</author>
<author>usmannasir</author>
<website>https://github.com/cyberpanel/testPlugin</website>
<license>MIT</license>
<dependencies>