mirror of
https://github.com/usmannasir/cyberpanel.git
synced 2026-03-18 02:00:16 +01:00
Add Modify Date column, GitHub commit date fetching, and plugin store caching
- Added Modify Date column to both Table View and Plugin Store - Implemented GitHub API integration to fetch last commit dates - Added caching system for plugin store to prevent rate limit errors - Enhanced plugin store with installed/enabled status enrichment - Added comprehensive plugin development guide - Updated testPlugin meta.xml author to usmannasir
This commit is contained in:
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
658
pluginHolder/templates/pluginHolder/help.html
Normal file
658
pluginHolder/templates/pluginHolder/help.html
Normal file
@@ -0,0 +1,658 @@
|
||||
{% 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="#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>
|
||||
<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>
|
||||
|
||||
<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="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>
|
||||
<pre><code>{% extends "baseTemplate/index.html" %}
|
||||
{% load static %}
|
||||
{% load i18n %}</code></pre>
|
||||
|
||||
<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,6 +60,12 @@ def _set_plugin_state(plugin_name, enabled):
|
||||
logging.writeToFile(f"Error writing plugin state for {plugin_name}: {str(e)}")
|
||||
return False
|
||||
|
||||
def help_page(request):
|
||||
"""Display plugin development help page"""
|
||||
mailUtilities.checkHome()
|
||||
proc = httpProc(request, 'pluginHolder/help.html', {}, 'admin')
|
||||
return proc.render()
|
||||
|
||||
def installed(request):
|
||||
mailUtilities.checkHome()
|
||||
pluginPath = '/home/cyberpanel/plugins'
|
||||
@@ -114,6 +132,43 @@ def installed(request):
|
||||
else:
|
||||
data['enabled'] = False
|
||||
|
||||
# Get modify date from GitHub (last commit date) or local file as fallback
|
||||
modify_date = 'N/A'
|
||||
try:
|
||||
# Try to get last commit date from GitHub API
|
||||
plugin_name_for_api = plugin.replace('Plugin', '').lower() if 'Plugin' in plugin else plugin.lower()
|
||||
commits_url = f"{GITHUB_COMMITS_API}?path={plugin}&per_page=1"
|
||||
commits_req = urllib.request.Request(
|
||||
commits_url,
|
||||
headers={
|
||||
'User-Agent': 'CyberPanel-Plugin-Store/1.0',
|
||||
'Accept': 'application/vnd.github.v3+json'
|
||||
}
|
||||
)
|
||||
|
||||
with urllib.request.urlopen(commits_req, timeout=5) as commits_response:
|
||||
commits_data = json.loads(commits_response.read().decode('utf-8'))
|
||||
if commits_data and len(commits_data) > 0:
|
||||
commit_date = commits_data[0].get('commit', {}).get('author', {}).get('date', '')
|
||||
if commit_date:
|
||||
try:
|
||||
dt = datetime.fromisoformat(commit_date.replace('Z', '+00:00'))
|
||||
modify_date = dt.strftime('%Y-%m-%d %H:%M:%S')
|
||||
except Exception:
|
||||
modify_date = commit_date[:19].replace('T', ' ')
|
||||
except Exception as e:
|
||||
# Fallback to local file modification time
|
||||
try:
|
||||
if os.path.exists(metaXmlPath):
|
||||
modify_time = os.path.getmtime(metaXmlPath)
|
||||
modify_date = datetime.fromtimestamp(modify_time).strftime('%Y-%m-%d %H:%M:%S')
|
||||
else:
|
||||
modify_date = 'N/A'
|
||||
except Exception:
|
||||
modify_date = 'N/A'
|
||||
|
||||
data['modify_date'] = modify_date
|
||||
|
||||
# Extract settings URL or main URL for "Manage" button
|
||||
settings_url_elem = root.find('settings_url')
|
||||
url_elem = root.find('url')
|
||||
@@ -336,4 +391,278 @@ def disable_plugin(request, plugin_name):
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}, status=500)
|
||||
|
||||
def _ensure_cache_dir():
|
||||
"""Ensure cache directory exists"""
|
||||
try:
|
||||
if not os.path.exists(PLUGIN_STORE_CACHE_DIR):
|
||||
os.makedirs(PLUGIN_STORE_CACHE_DIR, mode=0o755)
|
||||
except Exception as e:
|
||||
logging.writeToFile(f"Error creating cache directory: {str(e)}")
|
||||
|
||||
def _get_cached_plugins(allow_expired=False):
|
||||
"""Get plugins from cache if available and not expired
|
||||
|
||||
Args:
|
||||
allow_expired: If True, return cache even if expired (for fallback)
|
||||
"""
|
||||
try:
|
||||
if not os.path.exists(PLUGIN_STORE_CACHE_FILE):
|
||||
return None
|
||||
|
||||
# Check if cache is expired
|
||||
cache_mtime = os.path.getmtime(PLUGIN_STORE_CACHE_FILE)
|
||||
cache_age = time.time() - cache_mtime
|
||||
|
||||
if cache_age > PLUGIN_STORE_CACHE_DURATION:
|
||||
if not allow_expired:
|
||||
logging.writeToFile(f"Plugin store cache expired (age: {cache_age:.0f}s)")
|
||||
return None
|
||||
else:
|
||||
logging.writeToFile(f"Using expired cache as fallback (age: {cache_age:.0f}s)")
|
||||
|
||||
# Read cache file
|
||||
with open(PLUGIN_STORE_CACHE_FILE, 'r', encoding='utf-8') as f:
|
||||
cache_data = json.load(f)
|
||||
|
||||
if not allow_expired or cache_age <= PLUGIN_STORE_CACHE_DURATION:
|
||||
logging.writeToFile(f"Using cached plugin store data (age: {cache_age:.0f}s)")
|
||||
return cache_data.get('plugins', [])
|
||||
except Exception as e:
|
||||
logging.writeToFile(f"Error reading plugin store cache: {str(e)}")
|
||||
return None
|
||||
|
||||
def _save_plugins_cache(plugins):
|
||||
"""Save plugins to cache"""
|
||||
try:
|
||||
_ensure_cache_dir()
|
||||
cache_data = {
|
||||
'plugins': plugins,
|
||||
'cached_at': datetime.now().isoformat(),
|
||||
'cache_duration': PLUGIN_STORE_CACHE_DURATION
|
||||
}
|
||||
with open(PLUGIN_STORE_CACHE_FILE, 'w', encoding='utf-8') as f:
|
||||
json.dump(cache_data, f, indent=2, ensure_ascii=False)
|
||||
logging.writeToFile("Plugin store cache saved successfully")
|
||||
except Exception as e:
|
||||
logging.writeToFile(f"Error saving plugin store cache: {str(e)}")
|
||||
|
||||
def _enrich_store_plugins(plugins):
|
||||
"""Enrich store plugins with installed/enabled status from local system"""
|
||||
enriched = []
|
||||
plugin_source_dir = '/home/cyberpanel/plugins'
|
||||
plugin_install_dir = '/usr/local/CyberCP'
|
||||
|
||||
for plugin in plugins:
|
||||
plugin_dir = plugin.get('plugin_dir', '')
|
||||
if not plugin_dir:
|
||||
continue
|
||||
|
||||
# Check if plugin is installed locally
|
||||
installed_path = os.path.join(plugin_install_dir, plugin_dir)
|
||||
source_path = os.path.join(plugin_source_dir, plugin_dir)
|
||||
|
||||
plugin['installed'] = os.path.exists(installed_path) or os.path.exists(source_path)
|
||||
|
||||
# Check if plugin is enabled (only if installed)
|
||||
if plugin['installed']:
|
||||
plugin['enabled'] = _is_plugin_enabled(plugin_dir)
|
||||
else:
|
||||
plugin['enabled'] = False
|
||||
|
||||
enriched.append(plugin)
|
||||
|
||||
return enriched
|
||||
|
||||
def _fetch_plugins_from_github():
|
||||
"""Fetch plugins from GitHub repository"""
|
||||
plugins = []
|
||||
|
||||
try:
|
||||
# Fetch repository contents
|
||||
req = urllib.request.Request(
|
||||
GITHUB_REPO_API,
|
||||
headers={
|
||||
'User-Agent': 'CyberPanel-Plugin-Store/1.0',
|
||||
'Accept': 'application/vnd.github.v3+json'
|
||||
}
|
||||
)
|
||||
|
||||
with urllib.request.urlopen(req, timeout=10) as response:
|
||||
contents = json.loads(response.read().decode('utf-8'))
|
||||
|
||||
# Filter for directories (plugins)
|
||||
plugin_dirs = [item for item in contents if item.get('type') == 'dir' and not item.get('name', '').startswith('.')]
|
||||
|
||||
for plugin_dir in plugin_dirs:
|
||||
plugin_name = plugin_dir.get('name', '')
|
||||
if not plugin_name:
|
||||
continue
|
||||
|
||||
try:
|
||||
# Fetch meta.xml from raw GitHub
|
||||
meta_xml_url = f"{GITHUB_RAW_BASE}/{plugin_name}/meta.xml"
|
||||
meta_req = urllib.request.Request(
|
||||
meta_xml_url,
|
||||
headers={'User-Agent': 'CyberPanel-Plugin-Store/1.0'}
|
||||
)
|
||||
|
||||
with urllib.request.urlopen(meta_req, timeout=10) as meta_response:
|
||||
meta_xml_content = meta_response.read().decode('utf-8')
|
||||
|
||||
# Parse meta.xml
|
||||
root = ElementTree.fromstring(meta_xml_content)
|
||||
|
||||
# Fetch last commit date for this plugin from GitHub
|
||||
modify_date = 'N/A'
|
||||
try:
|
||||
commits_url = f"{GITHUB_COMMITS_API}?path={plugin_name}&per_page=1"
|
||||
commits_req = urllib.request.Request(
|
||||
commits_url,
|
||||
headers={
|
||||
'User-Agent': 'CyberPanel-Plugin-Store/1.0',
|
||||
'Accept': 'application/vnd.github.v3+json'
|
||||
}
|
||||
)
|
||||
|
||||
with urllib.request.urlopen(commits_req, timeout=10) as commits_response:
|
||||
commits_data = json.loads(commits_response.read().decode('utf-8'))
|
||||
if commits_data and len(commits_data) > 0:
|
||||
commit_date = commits_data[0].get('commit', {}).get('author', {}).get('date', '')
|
||||
if commit_date:
|
||||
# Parse ISO 8601 date and format it
|
||||
try:
|
||||
from datetime import datetime
|
||||
dt = datetime.fromisoformat(commit_date.replace('Z', '+00:00'))
|
||||
modify_date = dt.strftime('%Y-%m-%d %H:%M:%S')
|
||||
except Exception:
|
||||
modify_date = commit_date[:19].replace('T', ' ') # Fallback formatting
|
||||
except Exception as e:
|
||||
logging.writeToFile(f"Could not fetch commit date for {plugin_name}: {str(e)}")
|
||||
modify_date = 'N/A'
|
||||
|
||||
plugin_data = {
|
||||
'plugin_dir': plugin_name,
|
||||
'name': root.find('name').text if root.find('name') is not None else plugin_name,
|
||||
'type': root.find('type').text if root.find('type') is not None else 'Plugin',
|
||||
'description': root.find('description').text if root.find('description') is not None else '',
|
||||
'version': root.find('version').text if root.find('version') is not None else '1.0.0',
|
||||
'url': root.find('url').text if root.find('url') is not None else f'/plugins/{plugin_name}/',
|
||||
'settings_url': root.find('settings_url').text if root.find('settings_url') is not None else f'/plugins/{plugin_name}/settings/',
|
||||
'author': root.find('author').text if root.find('author') is not None else 'Unknown',
|
||||
'github_url': f'https://github.com/master3395/cyberpanel-plugins/tree/main/{plugin_name}',
|
||||
'about_url': f'https://github.com/master3395/cyberpanel-plugins/tree/main/{plugin_name}',
|
||||
'modify_date': modify_date
|
||||
}
|
||||
|
||||
plugins.append(plugin_data)
|
||||
logging.writeToFile(f"Fetched plugin: {plugin_name} (last modified: {modify_date})")
|
||||
|
||||
except urllib.error.HTTPError as e:
|
||||
if e.code == 403:
|
||||
# Rate limit hit - log and break
|
||||
logging.writeToFile(f"GitHub API rate limit exceeded (403) for plugin {plugin_name}")
|
||||
raise # Re-raise to be caught by outer handler
|
||||
elif e.code == 404:
|
||||
# meta.xml not found, skip this plugin
|
||||
logging.writeToFile(f"meta.xml not found for plugin {plugin_name}, skipping")
|
||||
continue
|
||||
else:
|
||||
logging.writeToFile(f"HTTP error {e.code} fetching {plugin_name}: {str(e)}")
|
||||
continue
|
||||
except Exception as e:
|
||||
logging.writeToFile(f"Error processing plugin {plugin_name}: {str(e)}")
|
||||
continue
|
||||
|
||||
return plugins
|
||||
|
||||
except urllib.error.HTTPError as e:
|
||||
if e.code == 403:
|
||||
error_msg = "GitHub API rate limit exceeded. Using cached data if available."
|
||||
logging.writeToFile(f"GitHub API 403 error: {error_msg}")
|
||||
raise Exception(error_msg)
|
||||
else:
|
||||
error_msg = f"GitHub API error {e.code}: {str(e)}"
|
||||
logging.writeToFile(error_msg)
|
||||
raise Exception(error_msg)
|
||||
except urllib.error.URLError as e:
|
||||
error_msg = f"Network error fetching plugins: {str(e)}"
|
||||
logging.writeToFile(error_msg)
|
||||
raise Exception(error_msg)
|
||||
except Exception as e:
|
||||
error_msg = f"Error fetching plugins from GitHub: {str(e)}"
|
||||
logging.writeToFile(error_msg)
|
||||
raise Exception(error_msg)
|
||||
|
||||
@csrf_exempt
|
||||
@require_http_methods(["GET"])
|
||||
def fetch_plugin_store(request):
|
||||
"""Fetch plugins from the plugin store with caching"""
|
||||
mailUtilities.checkHome()
|
||||
|
||||
# Try to get from cache first
|
||||
cached_plugins = _get_cached_plugins()
|
||||
if cached_plugins is not None:
|
||||
# Enrich cached plugins with installed/enabled status
|
||||
enriched_plugins = _enrich_store_plugins(cached_plugins)
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'plugins': enriched_plugins,
|
||||
'cached': True
|
||||
})
|
||||
|
||||
# Cache miss or expired - fetch from GitHub
|
||||
try:
|
||||
plugins = _fetch_plugins_from_github()
|
||||
|
||||
# Enrich plugins with installed/enabled status
|
||||
enriched_plugins = _enrich_store_plugins(plugins)
|
||||
|
||||
# Save to cache (save original, not enriched, to keep cache clean)
|
||||
if plugins:
|
||||
_save_plugins_cache(plugins)
|
||||
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'plugins': enriched_plugins,
|
||||
'cached': False
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
error_message = str(e)
|
||||
|
||||
# If rate limited, try to use stale cache as fallback
|
||||
if '403' in error_message or 'rate limit' in error_message.lower():
|
||||
stale_cache = _get_cached_plugins(allow_expired=True) # Get cache even if expired
|
||||
if stale_cache is not None:
|
||||
logging.writeToFile("Using stale cache due to rate limit")
|
||||
enriched_plugins = _enrich_store_plugins(stale_cache)
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'plugins': enriched_plugins,
|
||||
'cached': True,
|
||||
'warning': 'Using cached data due to GitHub rate limit. Data may be outdated.'
|
||||
})
|
||||
|
||||
# No cache available, return error
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': error_message,
|
||||
'plugins': []
|
||||
}, status=500)
|
||||
|
||||
@csrf_exempt
|
||||
@require_http_methods(["POST"])
|
||||
def install_from_store(request, plugin_name):
|
||||
"""Install plugin from store"""
|
||||
mailUtilities.checkHome()
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': 'Plugin store installation not implemented'
|
||||
}, status=501)
|
||||
|
||||
def plugin_help(request, plugin_name):
|
||||
"""Plugin-specific help page"""
|
||||
mailUtilities.checkHome()
|
||||
return redirect('/plugins/help/')
|
||||
|
||||
@@ -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