mirror of
https://github.com/usmannasir/cyberpanel.git
synced 2026-01-29 02:39:06 +01:00
Merge branch 'v2.5.5-dev' of https://github.com/master3395/cyberpanel into v2.5.5-dev
This commit is contained in:
@@ -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',
|
||||
|
||||
1466
docs/PLUGIN_DEVELOPMENT_GUIDE.md
Normal file
1466
docs/PLUGIN_DEVELOPMENT_GUIDE.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
|
||||
692
pluginHolder/templates/pluginHolder/help.html
Normal file
692
pluginHolder/templates/pluginHolder/help.html
Normal 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><?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></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><version>1.0.0</version></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/<int:item_id>/', 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
@@ -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'),
|
||||
]
|
||||
|
||||
@@ -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): [](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: 
|
||||
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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user