mirror of
https://github.com/usmannasir/cyberpanel.git
synced 2026-05-09 16:05:36 +02:00
@@ -65,7 +65,8 @@ INSTALLED_APPS = [
|
||||
|
||||
# Apps with multiple or complex dependencies
|
||||
'emailPremium',
|
||||
'emailMarketing', # Depends on websiteFunctions and loginSystem
|
||||
# Optional plugins (e.g. emailMarketing, discordWebhooks) - install via Plugin Store
|
||||
# from https://github.com/master3395/cyberpanel-plugins - plugin installer adds them
|
||||
'cloudAPI', # Depends on websiteFunctions
|
||||
'containerization', # Depends on websiteFunctions
|
||||
'IncBackups', # Depends on websiteFunctions and loginSystem
|
||||
|
||||
@@ -51,7 +51,8 @@ urlpatterns = [
|
||||
path('CloudLinux/', include('CLManager.urls')),
|
||||
path('IncrementalBackups/', include('IncBackups.urls')),
|
||||
path('aiscanner/', include('aiScanner.urls')),
|
||||
path('emailMarketing/', include('emailMarketing.urls')),
|
||||
# Optional plugin routes - added by plugin installer when plugins are installed from Plugin Store
|
||||
# path('emailMarketing/', include('emailMarketing.urls')),
|
||||
# path('Terminal/', include('WebTerminal.urls')),
|
||||
path('', include('loginSystem.urls')),
|
||||
]
|
||||
|
||||
@@ -129,6 +129,11 @@ def getAdminStatus(request):
|
||||
|
||||
|
||||
def getSystemStatus(request):
|
||||
default_fallback = {
|
||||
'cpuUsage': 0, 'ramUsage': 0, 'diskUsage': 0,
|
||||
'cpuCores': 2, 'ramTotalMB': 4096, 'diskTotalGB': 100,
|
||||
'diskFreeGB': 100, 'uptime': 'N/A'
|
||||
}
|
||||
try:
|
||||
val = request.session['userID']
|
||||
currentACL = ACLManager.loadedACL(val)
|
||||
@@ -215,31 +220,13 @@ def getSystemStatus(request):
|
||||
|
||||
except KeyError as e:
|
||||
logging.CyberCPLogFileWriter.writeToFile(f'[getSystemStatus] KeyError - No session userID: {str(e)}')
|
||||
# Return default values on error
|
||||
default_data = {
|
||||
'cpuUsage': 0,
|
||||
'ramUsage': 0,
|
||||
'diskUsage': 0,
|
||||
'cpuCores': 2,
|
||||
'ramTotalMB': 4096,
|
||||
'diskTotalGB': 100,
|
||||
'diskFreeGB': 100,
|
||||
'uptime': 'N/A'
|
||||
}
|
||||
return HttpResponse(json.dumps(default_data))
|
||||
return HttpResponse(json.dumps(default_fallback))
|
||||
except Exception as e:
|
||||
# Return default values on error
|
||||
default_data = {
|
||||
'cpuUsage': 0,
|
||||
'ramUsage': 0,
|
||||
'diskUsage': 0,
|
||||
'cpuCores': 2,
|
||||
'ramTotalMB': 4096,
|
||||
'diskTotalGB': 100,
|
||||
'diskFreeGB': 100,
|
||||
'uptime': 'N/A'
|
||||
}
|
||||
return HttpResponse(json.dumps(default_data))
|
||||
logging.CyberCPLogFileWriter.writeToFile(f'[getSystemStatus] Exception: {str(e)}')
|
||||
try:
|
||||
return HttpResponse(json.dumps(default_fallback))
|
||||
except Exception:
|
||||
return HttpResponse('{"cpuUsage":0,"ramUsage":0,"diskUsage":0,"cpuCores":2,"ramTotalMB":4096,"diskTotalGB":100,"diskFreeGB":100,"uptime":"N/A"}', content_type='application/json')
|
||||
|
||||
|
||||
def getLoadAverage(request):
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# CyberPanel phpMyAdmin Access Control Deployment Script
|
||||
# This script implements redirect functionality for unauthenticated phpMyAdmin access
|
||||
|
||||
echo "=== CyberPanel phpMyAdmin Access Control Deployment ==="
|
||||
|
||||
# Check if running as root
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
echo "Please run this script as root"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Backup original phpMyAdmin index.php if it exists
|
||||
if [ -f "/usr/local/CyberCP/public/phpmyadmin/index.php" ]; then
|
||||
echo "Backing up original phpMyAdmin index.php..."
|
||||
cp /usr/local/CyberCP/public/phpmyadmin/index.php /usr/local/CyberCP/public/phpmyadmin/index.php.backup.$(date +%Y%m%d_%H%M%S)
|
||||
fi
|
||||
|
||||
# Deploy the redirect index.php
|
||||
echo "Deploying phpMyAdmin access control..."
|
||||
cp /usr/local/CyberCP/phpmyadmin_index_redirect.php /usr/local/CyberCP/public/phpmyadmin/index.php
|
||||
|
||||
# Deploy .htaccess for additional protection
|
||||
echo "Deploying .htaccess protection..."
|
||||
cp /usr/local/CyberCP/phpmyadmin_htaccess /usr/local/CyberCP/public/phpmyadmin/.htaccess
|
||||
|
||||
# Set proper permissions
|
||||
echo "Setting permissions..."
|
||||
chown lscpd:lscpd /usr/local/CyberCP/public/phpmyadmin/index.php
|
||||
chmod 644 /usr/local/CyberCP/public/phpmyadmin/index.php
|
||||
chown lscpd:lscpd /usr/local/CyberCP/public/phpmyadmin/.htaccess
|
||||
chmod 644 /usr/local/CyberCP/public/phpmyadmin/.htaccess
|
||||
|
||||
# Restart LiteSpeed to ensure changes take effect
|
||||
echo "Restarting LiteSpeed..."
|
||||
systemctl restart lscpd
|
||||
|
||||
echo "=== Deployment Complete ==="
|
||||
echo ""
|
||||
echo "phpMyAdmin access control has been deployed successfully!"
|
||||
echo ""
|
||||
echo "What this does:"
|
||||
echo "- Users trying to access phpMyAdmin directly without being logged into CyberPanel"
|
||||
echo " will now be redirected to the CyberPanel login page (/base/)"
|
||||
echo "- Authenticated users will continue to access phpMyAdmin normally"
|
||||
echo ""
|
||||
echo "To revert changes, restore the backup:"
|
||||
echo "cp /usr/local/CyberCP/public/phpmyadmin/index.php.backup.* /usr/local/CyberCP/public/phpmyadmin/index.php"
|
||||
echo ""
|
||||
echo "Test the implementation by:"
|
||||
echo "1. Opening an incognito/private browser window"
|
||||
echo "2. Going to https://your-server:2087/phpmyadmin/"
|
||||
echo "3. You should be redirected to the CyberPanel login page"
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<cyberpanelPluginConfig>
|
||||
<name>Email Marketing</name>
|
||||
<type>plugin</type>
|
||||
<type>Utility</type>
|
||||
<description>Email Marketing plugin for CyberPanel.</description>
|
||||
<version>1.0.0</version>
|
||||
<version>1.0.1</version>
|
||||
</cyberpanelPluginConfig>
|
||||
@@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<cyberpanelPluginConfig>
|
||||
<name>examplePlugin</name>
|
||||
<type>plugin</type>
|
||||
<type>Utility</type>
|
||||
<description>This is an example plugin</description>
|
||||
<version>1.0.0</version>
|
||||
<version>1.0.1</version>
|
||||
<author>usmannasir</author>
|
||||
</cyberpanelPluginConfig>
|
||||
@@ -274,6 +274,100 @@
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Collapsible sections */
|
||||
.help-section {
|
||||
border: 1px solid var(--border-primary, #e8e9ff);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.help-section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 20px;
|
||||
background: var(--bg-secondary, #f8f9ff);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.help-section-header:hover {
|
||||
background: #eef0ff;
|
||||
}
|
||||
|
||||
.help-section-header h2 {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary, #2f3640);
|
||||
}
|
||||
|
||||
.help-section-header .section-toggle {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 6px;
|
||||
background: rgba(88,86,214,0.15);
|
||||
color: #5856d6;
|
||||
transition: transform 0.25s ease;
|
||||
}
|
||||
|
||||
.help-section.expanded .section-toggle {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.help-section-body {
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
transition: max-height 0.35s ease-out;
|
||||
}
|
||||
|
||||
.help-section.expanded .help-section-body {
|
||||
max-height: 8000px;
|
||||
transition: max-height 0.5s ease-in;
|
||||
}
|
||||
|
||||
.help-section-content {
|
||||
padding: 20px 24px 24px;
|
||||
border-top: 1px solid var(--border-primary, #e8e9ff);
|
||||
}
|
||||
|
||||
.help-section-content h2 {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.help-section-content > h3:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.help-expand-all {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-top: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.help-expand-all button {
|
||||
padding: 6px 14px;
|
||||
font-size: 13px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border-primary, #e8e9ff);
|
||||
background: var(--bg-primary, white);
|
||||
color: #5856d6;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.help-expand-all button:hover {
|
||||
background: #eef0ff;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.help-wrapper {
|
||||
padding: 15px;
|
||||
@@ -314,7 +408,13 @@
|
||||
</div>
|
||||
|
||||
<div class="help-content">
|
||||
<div class="toc">
|
||||
<div class="help-section toc-collapsible expanded">
|
||||
<div class="help-section-header" data-section="toc">
|
||||
<h2>{% trans "Table of Contents" %}</h2>
|
||||
<span class="section-toggle"><i class="fas fa-chevron-down"></i></span>
|
||||
</div>
|
||||
<div class="help-section-body">
|
||||
<div class="help-section-content toc">
|
||||
<h3>{% trans "Table of Contents" %}</h3>
|
||||
<ul>
|
||||
<li><a href="#introduction">{% trans "Introduction" %}</a></li>
|
||||
@@ -323,6 +423,9 @@
|
||||
<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="#categories">{% trans "Plugin Categories" %}</a></li>
|
||||
<li><a href="#badges">{% trans "Freshness Badges" %}</a></li>
|
||||
<li><a href="#premium">{% trans "Premium/Paid Plugin Creation" %}</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>
|
||||
@@ -332,9 +435,17 @@
|
||||
<li><a href="#troubleshooting">{% trans "Troubleshooting" %}</a></li>
|
||||
<li><a href="#examples">{% trans "Examples & References" %}</a></li>
|
||||
</ul>
|
||||
<div class="help-expand-all">
|
||||
<button type="button" onclick="document.querySelectorAll('.help-section').forEach(s=>s.classList.add('expanded'))">{% trans "Expand all" %}</button>
|
||||
<button type="button" onclick="document.querySelectorAll('.help-section').forEach(s=>s.classList.remove('expanded'))">{% trans "Collapse all" %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 id="introduction">{% trans "Introduction" %}</h2>
|
||||
<div class="help-section expanded" id="section-introduction">
|
||||
<div class="help-section-header" data-section="introduction"><h2 id="introduction">{% trans "Introduction" %}</h2><span class="section-toggle"><i class="fas fa-chevron-down"></i></span></div>
|
||||
<div class="help-section-body"><div class="help-section-content">
|
||||
<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>
|
||||
@@ -347,8 +458,11 @@
|
||||
<li>{% trans "Create custom reporting tools" %}</li>
|
||||
<li>{% trans "Integrate security features" %}</li>
|
||||
</ul>
|
||||
</div></div></div>
|
||||
|
||||
<h2 id="prerequisites">{% trans "Prerequisites" %}</h2>
|
||||
<div class="help-section" id="section-prerequisites">
|
||||
<div class="help-section-header" data-section="prerequisites"><h2 id="prerequisites">{% trans "Prerequisites" %}</h2><span class="section-toggle"><i class="fas fa-chevron-down"></i></span></div>
|
||||
<div class="help-section-body"><div class="help-section-content">
|
||||
<h3>{% trans "Required Knowledge" %}</h3>
|
||||
<ul>
|
||||
<li><strong>Python 3.6+</strong>: {% trans "Basic to intermediate Python knowledge" %}</li>
|
||||
@@ -357,8 +471,11 @@
|
||||
<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>
|
||||
</div></div></div>
|
||||
|
||||
<h2 id="architecture">{% trans "Plugin Architecture Overview" %}</h2>
|
||||
<div class="help-section" id="section-architecture">
|
||||
<div class="help-section-header" data-section="architecture"><h2 id="architecture">{% trans "Plugin Architecture Overview" %}</h2><span class="section-toggle"><i class="fas fa-chevron-down"></i></span></div>
|
||||
<div class="help-section-body"><div class="help-section-content">
|
||||
<h3>{% trans "How Plugins Work" %}</h3>
|
||||
<ol>
|
||||
<li><strong>{% trans "Plugin Source Location" %}</strong>: <code>/home/cyberpanel/plugins/</code>
|
||||
@@ -374,8 +491,11 @@
|
||||
</ul>
|
||||
</li>
|
||||
</ol>
|
||||
</div></div></div>
|
||||
|
||||
<h2 id="first-plugin">{% trans "Creating Your First Plugin" %}</h2>
|
||||
<div class="help-section" id="section-first-plugin">
|
||||
<div class="help-section-header" data-section="first-plugin"><h2 id="first-plugin">{% trans "Creating Your First Plugin" %}</h2><span class="section-toggle"><i class="fas fa-chevron-down"></i></span></div>
|
||||
<div class="help-section-body"><div class="help-section-content">
|
||||
<h3>{% trans "Step 1: Create Plugin Directory Structure" %}</h3>
|
||||
<pre><code># Navigate to plugins directory
|
||||
cd /home/cyberpanel/plugins
|
||||
@@ -401,6 +521,10 @@ mkdir -p migrations</code></pre>
|
||||
<settings_url>/plugins/myFirstPlugin/settings/</settings_url>
|
||||
</cyberpanelPluginConfig></code></pre>
|
||||
|
||||
<div class="alert alert-warning">
|
||||
<strong>{% trans "Required: Category (type)" %}:</strong> {% trans "The <type> field is required. See the Plugin Categories section below for valid options. Plugins without a valid category will not appear in the Plugin Store." %}
|
||||
</div>
|
||||
|
||||
<h3>{% trans "Step 3: Create urls.py" %}</h3>
|
||||
<pre><code>from django.urls import path
|
||||
from . import views
|
||||
@@ -458,8 +582,11 @@ def main_view(request):
|
||||
<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>
|
||||
</div></div></div>
|
||||
|
||||
<h2 id="structure">{% trans "Plugin Structure & Files" %}</h2>
|
||||
<div class="help-section" id="section-structure">
|
||||
<div class="help-section-header" data-section="structure"><h2 id="structure">{% trans "Plugin Structure & Files" %}</h2><span class="section-toggle"><i class="fas fa-chevron-down"></i></span></div>
|
||||
<div class="help-section-body"><div class="help-section-content">
|
||||
<h3>{% trans "Required Files" %}</h3>
|
||||
<ul>
|
||||
<li><code>__init__.py</code> - <span class="badge badge-required">{% trans "Required" %}</span> - {% trans "Python package marker" %}</li>
|
||||
@@ -476,8 +603,11 @@ def main_view(request):
|
||||
<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>
|
||||
</div></div></div>
|
||||
|
||||
<h2 id="versioning">{% trans "Version Numbering (Semantic Versioning)" %}</h2>
|
||||
<div class="help-section" id="section-versioning">
|
||||
<div class="help-section-header" data-section="versioning"><h2 id="versioning">{% trans "Version Numbering (Semantic Versioning)" %}</h2><span class="section-toggle"><i class="fas fa-chevron-down"></i></span></div>
|
||||
<div class="help-section-body"><div class="help-section-content">
|
||||
<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;">
|
||||
@@ -505,8 +635,107 @@ def main_view(request):
|
||||
<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>
|
||||
</div></div></div>
|
||||
|
||||
<h2 id="components">{% trans "Core Components" %}</h2>
|
||||
<div class="help-section" id="section-categories">
|
||||
<div class="help-section-header" data-section="categories"><h2 id="categories">{% trans "Plugin Categories" %}</h2><span class="section-toggle"><i class="fas fa-chevron-down"></i></span></div>
|
||||
<div class="help-section-body"><div class="help-section-content">
|
||||
<p>{% trans "The <type> field in meta.xml determines how your plugin is grouped in the Plugin Store. Use exactly one of these values (case-sensitive):" %}</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Category" %}</th>
|
||||
<th>{% trans "Purpose" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td><code>Utility</code></td><td>{% trans "General-purpose tools, helpers, and utilities" %}</td></tr>
|
||||
<tr><td><code>Security</code></td><td>{% trans "Security features: firewalls, fail2ban, SSL, etc." %}</td></tr>
|
||||
<tr><td><code>Backup</code></td><td>{% trans "Backup, snapshot, and restore functionality" %}</td></tr>
|
||||
<tr><td><code>Performance</code></td><td>{% trans "Caching, optimization, and performance tuning" %}</td></tr>
|
||||
<tr><td><code>Monitoring</code></td><td>{% trans "Monitoring, alerts, and health checks" %}</td></tr>
|
||||
<tr><td><code>Integration</code></td><td>{% trans "Third-party integrations: Discord, Slack, webhooks, APIs" %}</td></tr>
|
||||
<tr><td><code>Email</code></td><td>{% trans "Email marketing, SMTP, mail management" %}</td></tr>
|
||||
<tr><td><code>Development</code></td><td>{% trans "Developer tools: PM2, Node.js, deployment" %}</td></tr>
|
||||
<tr><td><code>Analytics</code></td><td>{% trans "Analytics, tracking, and reporting" %}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div></div></div>
|
||||
|
||||
<div class="help-section" id="section-badges">
|
||||
<div class="help-section-header" data-section="badges"><h2 id="badges">{% trans "Freshness Badges" %}</h2><span class="section-toggle"><i class="fas fa-chevron-down"></i></span></div>
|
||||
<div class="help-section-body"><div class="help-section-content">
|
||||
<p>{% trans "The Plugin Store and Installed Plugins views display freshness badges based on the last update date (modify_date from GitHub commit or meta.xml file mtime). These help users quickly see how actively maintained a plugin is:" %}</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Badge" %}</th>
|
||||
<th>{% trans "Condition" %}</th>
|
||||
<th>{% trans "Meaning" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td><span style="background:#fef08a;color:#854d0e;padding:2px 8px;border-radius:4px;font-size:12px;font-weight:600;">NEW</span></td><td>{% trans "Updated within last 90 days" %}</td><td>{% trans "Recently released or actively maintained" %}</td></tr>
|
||||
<tr><td><span style="background:#bbf7d0;color:#166534;padding:2px 8px;border-radius:4px;font-size:12px;font-weight:600;">Stable</span></td><td>{% trans "Updated within last 365 days" %}</td><td>{% trans "Updated within the past year" %}</td></tr>
|
||||
<tr><td><span style="background:#e5e7eb;color:#4b5563;padding:2px 8px;border-radius:4px;font-size:12px;font-weight:600;">Unstable</span></td><td>{% trans "1–2 years since last update" %}</td><td>{% trans "May need maintenance; consider forking or updating" %}</td></tr>
|
||||
<tr><td><span style="background:#fecaca;color:#991b1b;padding:2px 8px;border-radius:4px;font-size:12px;font-weight:600;">STALE</span></td><td>{% trans "Over 2 years since last update" %}</td><td>{% trans "Not updated recently; use with caution" %}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>{% trans "Badges are calculated automatically from the plugin's modify_date. For plugins in the Plugin Store, this comes from the GitHub repository's last commit. For installed plugins, it uses the meta.xml file modification time." %}</p>
|
||||
</div></div></div>
|
||||
|
||||
<div class="help-section" id="section-premium">
|
||||
<div class="help-section-header" data-section="premium"><h2 id="premium">{% trans "Premium/Paid Plugin Creation" %}</h2><span class="section-toggle"><i class="fas fa-chevron-down"></i></span></div>
|
||||
<div class="help-section-body"><div class="help-section-content">
|
||||
<p>{% trans "You can create premium (paid) plugins and implement your own verification system. This includes optional encryption between the plugin and your verification site to prevent unauthorized bypass." %}</p>
|
||||
|
||||
<h3>{% trans "1. Mark Your Plugin as Paid in meta.xml" %}</h3>
|
||||
<pre><code><paid>true</paid>
|
||||
<patreon_tier>Your Tier Name</patreon_tier>
|
||||
<patreon_url>https://www.patreon.com/your-page</patreon_url></code></pre>
|
||||
<p>{% trans "Set <paid>true</paid> to display the Premium badge and subscription prompts in the Plugin Store." %}</p>
|
||||
|
||||
<h3>{% trans "2. Build Your Own Verification System" %}</h3>
|
||||
<p>{% trans "Premium plugins typically verify access via a remote API. You can host this on your own site. Common verification methods:" %}</p>
|
||||
<ul>
|
||||
<li><strong>{% trans "Patreon" %}</strong>: {% trans "Verify membership via Patreon OAuth/API" %}</li>
|
||||
<li><strong>{% trans "PayPal" %}</strong>: {% trans "Verify one-time or recurring payments" %}</li>
|
||||
<li><strong>{% trans "Plugin Grants" %}</strong>: {% trans "Admin panel where you manually grant access by email, IP, or domain" %}</li>
|
||||
<li><strong>{% trans "Activation Keys" %}</strong>: {% trans "Generate unique keys when granting access; users enter the key in the plugin" %}</li>
|
||||
</ul>
|
||||
|
||||
<h3>{% trans "3. Optional: Encrypt Plugin–API Communication" %}</h3>
|
||||
<p>{% trans "To protect against users modifying your plugin to bypass verification, you can encrypt the communication between the plugin and your verification API using AES-256-CBC. This ensures:" %}</p>
|
||||
<ul>
|
||||
<li>{% trans "Verification requests cannot be easily intercepted or forged" %}</li>
|
||||
<li>{% trans "Responses cannot be tampered with" %}</li>
|
||||
<li>{% trans "Your verification logic remains on your server; the plugin only encrypts/decrypts with a shared key" %}</li>
|
||||
</ul>
|
||||
<p>{% trans "Implementation outline:" %}</p>
|
||||
<ol>
|
||||
<li>{% trans "Generate a 32-byte secret key and store it in your API config (e.g. config.php)" %}</li>
|
||||
<li>{% trans "In your plugin (Python), encrypt outgoing requests and decrypt responses using the same key" %}</li>
|
||||
<li>{% trans "On your API (PHP/Python), decrypt incoming requests and encrypt responses" %}</li>
|
||||
<li>{% trans "Use the X-Encrypted: 1 header to indicate encrypted payloads" %}</li>
|
||||
</ol>
|
||||
<p>{% trans "Both sides must use the same AES-256-CBC key. Keep the key secret and never commit it to public repositories. Store it in a protected config file outside the web root or in environment variables." %}</p>
|
||||
|
||||
<h3>{% trans "4. Typical Premium Plugin Flow" %}</h3>
|
||||
<ol>
|
||||
<li>{% trans "User installs your premium plugin" %}</li>
|
||||
<li>{% trans "Plugin shows an activation screen (Patreon/PayPal links, or activation key input)" %}</li>
|
||||
<li>{% trans "Plugin calls your verification API with user identifier (email, domain, IP) or activation key" %}</li>
|
||||
<li>{% trans "Your API checks: Plugin Grants, activation key, Patreon, or PayPal — in your preferred order" %}</li>
|
||||
<li>{% trans "If access is granted, API returns success; plugin unlocks features and optionally stores the key locally" %}</li>
|
||||
</ol>
|
||||
<div class="alert alert-info">
|
||||
<strong>{% trans "Tip" %}:</strong> {% trans "Provide multiple verification paths: Patreon for subscribers, PayPal for one-time purchasers, Plugin Grants for beta testers or sponsors, and activation keys for manual grants. Encryption is optional but recommended for paid plugins to deter bypass attempts." %}
|
||||
</div>
|
||||
</div></div></div>
|
||||
|
||||
<div class="help-section" id="section-components">
|
||||
<div class="help-section-header" data-section="components"><h2 id="components">{% trans "Core Components" %}</h2><span class="section-toggle"><i class="fas fa-chevron-down"></i></span></div>
|
||||
<div class="help-section-body"><div class="help-section-content">
|
||||
<h3>{% trans "1. Authentication & Security" %}</h3>
|
||||
<p>{% trans "Always use the cyberpanel_login_required decorator:" %}</p>
|
||||
<pre><code>@cyberpanel_login_required
|
||||
@@ -532,8 +761,11 @@ urlpatterns = [
|
||||
{% load static %}
|
||||
{% load i18n %}</code></pre>
|
||||
{% endverbatim %}
|
||||
</div></div></div>
|
||||
|
||||
<h2 id="advanced">{% trans "Advanced Features" %}</h2>
|
||||
<div class="help-section" id="section-advanced">
|
||||
<div class="help-section-header" data-section="advanced"><h2 id="advanced">{% trans "Advanced Features" %}</h2><span class="section-toggle"><i class="fas fa-chevron-down"></i></span></div>
|
||||
<div class="help-section-body"><div class="help-section-content">
|
||||
<h3>{% trans "Database Models" %}</h3>
|
||||
<pre><code>from django.db import models
|
||||
|
||||
@@ -558,8 +790,11 @@ class MyForm(forms.Form):
|
||||
def api_endpoint(request):
|
||||
data = {'status': 'success'}
|
||||
return JsonResponse(data)</code></pre>
|
||||
</div></div></div>
|
||||
|
||||
<h2 id="best-practices">{% trans "Best Practices" %}</h2>
|
||||
<div class="help-section" id="section-best-practices">
|
||||
<div class="help-section-header" data-section="best-practices"><h2 id="best-practices">{% trans "Best Practices" %}</h2><span class="section-toggle"><i class="fas fa-chevron-down"></i></span></div>
|
||||
<div class="help-section-body"><div class="help-section-content">
|
||||
<ul>
|
||||
<li>{% trans "Keep files under 500 lines - split into modules if needed" %}</li>
|
||||
<li>{% trans "Use descriptive names for functions and variables" %}</li>
|
||||
@@ -569,8 +804,11 @@ def api_endpoint(request):
|
||||
<li>{% trans "Use Django ORM instead of raw SQL" %}</li>
|
||||
<li>{% trans "Test your plugin thoroughly before distribution" %}</li>
|
||||
</ul>
|
||||
</div></div></div>
|
||||
|
||||
<h2 id="security">{% trans "Security Guidelines" %}</h2>
|
||||
<div class="help-section" id="section-security">
|
||||
<div class="help-section-header" data-section="security"><h2 id="security">{% trans "Security Guidelines" %}</h2><span class="section-toggle"><i class="fas fa-chevron-down"></i></span></div>
|
||||
<div class="help-section-body"><div class="help-section-content">
|
||||
<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>
|
||||
@@ -582,8 +820,11 @@ def api_endpoint(request):
|
||||
<li>{% trans "Use HTTPS for sensitive operations" %}</li>
|
||||
<li>{% trans "Implement rate limiting for API endpoints" %}</li>
|
||||
</ul>
|
||||
</div></div></div>
|
||||
|
||||
<h2 id="testing">{% trans "Testing & Debugging" %}</h2>
|
||||
<div class="help-section" id="section-testing">
|
||||
<div class="help-section-header" data-section="testing"><h2 id="testing">{% trans "Testing & Debugging" %}</h2><span class="section-toggle"><i class="fas fa-chevron-down"></i></span></div>
|
||||
<div class="help-section-body"><div class="help-section-content">
|
||||
<h3>{% trans "Common Issues" %}</h3>
|
||||
<ul>
|
||||
<li><strong>{% trans "Template Not Found" %}</strong>: {% trans "Check template path and name" %}</li>
|
||||
@@ -601,16 +842,22 @@ xmllint --noout meta.xml
|
||||
|
||||
# View logs
|
||||
tail -f /usr/local/lscp/logs/error.log</code></pre>
|
||||
</div></div></div>
|
||||
|
||||
<h2 id="packaging">{% trans "Packaging & Distribution" %}</h2>
|
||||
<div class="help-section" id="section-packaging">
|
||||
<div class="help-section-header" data-section="packaging"><h2 id="packaging">{% trans "Packaging & Distribution" %}</h2><span class="section-toggle"><i class="fas fa-chevron-down"></i></span></div>
|
||||
<div class="help-section-body"><div class="help-section-content">
|
||||
<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>
|
||||
</div></div></div>
|
||||
|
||||
<h2 id="troubleshooting">{% trans "Troubleshooting" %}</h2>
|
||||
<div class="help-section" id="section-troubleshooting">
|
||||
<div class="help-section-header" data-section="troubleshooting"><h2 id="troubleshooting">{% trans "Troubleshooting" %}</h2><span class="section-toggle"><i class="fas fa-chevron-down"></i></span></div>
|
||||
<div class="help-section-body"><div class="help-section-content">
|
||||
<h3>{% trans "Installation Issues" %}</h3>
|
||||
<ul>
|
||||
<li>{% trans "Check meta.xml format and validity" %}</li>
|
||||
@@ -626,8 +873,11 @@ zip -r myPlugin-v1.0.0.zip . \
|
||||
<li>{% trans "Review template paths" %}</li>
|
||||
<li>{% trans "Check for JavaScript errors" %}</li>
|
||||
</ul>
|
||||
</div></div></div>
|
||||
|
||||
<h2 id="examples">{% trans "Examples & References" %}</h2>
|
||||
<div class="help-section" id="section-examples">
|
||||
<div class="help-section-header" data-section="examples"><h2 id="examples">{% trans "Examples & References" %}</h2><span class="section-toggle"><i class="fas fa-chevron-down"></i></span></div>
|
||||
<div class="help-section-body"><div class="help-section-content">
|
||||
<h3>{% trans "Reference Plugins" %}</h3>
|
||||
<ul>
|
||||
<li><strong>examplePlugin</strong>: {% trans "Basic plugin structure" %}
|
||||
@@ -663,28 +913,40 @@ zip -r myPlugin-v1.0.0.zip . \
|
||||
<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></div></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
|
||||
<strong>{% trans "Version" %}:</strong> 2.1.0 |
|
||||
<strong>{% trans "Last Updated" %}:</strong> 2026-02-02
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Smooth scrolling for anchor links
|
||||
// Collapsible section toggle
|
||||
document.querySelectorAll('.help-section-header').forEach(header => {
|
||||
header.addEventListener('click', function () {
|
||||
const section = this.closest('.help-section');
|
||||
section.classList.toggle('expanded');
|
||||
});
|
||||
});
|
||||
// Smooth scrolling and expand on TOC link click
|
||||
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
||||
anchor.addEventListener('click', function (e) {
|
||||
const href = this.getAttribute('href');
|
||||
if (href === '#') return;
|
||||
e.preventDefault();
|
||||
const target = document.querySelector(this.getAttribute('href'));
|
||||
const id = href.slice(1);
|
||||
const target = document.getElementById(id);
|
||||
if (target) {
|
||||
target.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start'
|
||||
});
|
||||
const section = target.closest('.help-section') || document.getElementById('section-' + id);
|
||||
if (section) {
|
||||
section.classList.add('expanded');
|
||||
}
|
||||
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -200,6 +200,32 @@
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
|
||||
.freshness-badge-new, .freshness-badge-stable, .freshness-badge-stale {
|
||||
display: inline-block;
|
||||
padding: 3px 8px;
|
||||
border-radius: 10px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.freshness-badge-new {
|
||||
background: #fef08a;
|
||||
color: #854d0e;
|
||||
}
|
||||
.freshness-badge-stable {
|
||||
background: #bbf7d0;
|
||||
color: #166534;
|
||||
}
|
||||
.freshness-badge-stale {
|
||||
background: #fecaca;
|
||||
color: #991b1b;
|
||||
}
|
||||
.freshness-badge-unstable {
|
||||
background: #e5e7eb;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.plugin-pricing-badge.paid {
|
||||
background: #fff3cd;
|
||||
color: #856404;
|
||||
@@ -443,11 +469,62 @@
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* View Toggle */
|
||||
/* View Toggle and Bulk Actions */
|
||||
.view-toggle-wrapper {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.view-toggle {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.bulk-actions-header {
|
||||
display: flex !important;
|
||||
visibility: visible !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
.bulk-actions-header .btn-bulk {
|
||||
margin: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.btn-bulk {
|
||||
padding: 8px 14px;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
border: 1px solid transparent;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
.btn-activate-all {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
border-color: #218838;
|
||||
}
|
||||
.btn-activate-all:hover:not(:disabled) {
|
||||
background: #218838;
|
||||
}
|
||||
.btn-activate-all:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.btn-deactivate-all {
|
||||
background: #ffc107;
|
||||
color: #212529;
|
||||
border-color: #e0a800;
|
||||
}
|
||||
.btn-deactivate-all:hover:not(:disabled) {
|
||||
background: #e0a800;
|
||||
}
|
||||
.btn-deactivate-all:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.view-btn {
|
||||
@@ -592,11 +669,150 @@
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.category-filter {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-bottom: 15px;
|
||||
padding: 15px;
|
||||
background: var(--bg-secondary, #f8f9ff);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.category-btn {
|
||||
padding: 8px 16px;
|
||||
border: 1px solid var(--border-primary, #e8e9ff);
|
||||
background: var(--bg-primary, white);
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
color: var(--text-secondary, #64748b);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.category-btn:hover {
|
||||
background: var(--bg-hover, #f0f1ff);
|
||||
border-color: #5856d6;
|
||||
color: #5856d6;
|
||||
}
|
||||
|
||||
.category-btn.active {
|
||||
background: #5856d6;
|
||||
color: white;
|
||||
border-color: #5856d6;
|
||||
}
|
||||
|
||||
.category-btn i {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.store-search-bar {
|
||||
position: relative;
|
||||
margin-bottom: 15px;
|
||||
max-width: 480px;
|
||||
}
|
||||
|
||||
.store-search-icon {
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: #64748b;
|
||||
font-size: 16px;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.store-search-input {
|
||||
width: 100%;
|
||||
padding-left: 46px;
|
||||
padding-right: 40px;
|
||||
padding-top: 12px;
|
||||
padding-bottom: 12px;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
background: #f8fafc;
|
||||
color: #1e293b;
|
||||
transition: border-color 0.2s, box-shadow 0.2s, background 0.2s;
|
||||
}
|
||||
|
||||
.store-search-input:focus {
|
||||
outline: none;
|
||||
border-color: #5856d6;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 0 0 3px rgba(88, 86, 214, 0.15);
|
||||
}
|
||||
|
||||
.store-search-input::placeholder {
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.store-search-clear {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-muted, #94a3b8);
|
||||
cursor: pointer;
|
||||
padding: 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
transition: color 0.2s, background 0.2s;
|
||||
}
|
||||
|
||||
.store-search-clear:hover {
|
||||
color: #5856d6;
|
||||
background: var(--bg-secondary, #f8f9ff);
|
||||
}
|
||||
|
||||
.alphabet-filter-wrapper {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.alphabet-toggle-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
border: 1px solid var(--border-primary, #e8e9ff);
|
||||
background: var(--bg-primary, white);
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary, #64748b);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.alphabet-toggle-btn:hover {
|
||||
background: var(--bg-hover, #f8f9ff);
|
||||
border-color: #5856d6;
|
||||
color: #5856d6;
|
||||
}
|
||||
|
||||
.alphabet-toggle-btn[aria-expanded="true"] .alphabet-chevron {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.alphabet-chevron {
|
||||
font-size: 10px;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.alphabet-filter {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 20px;
|
||||
margin-top: 12px;
|
||||
padding: 15px;
|
||||
background: var(--bg-secondary, #f8f9ff);
|
||||
border-radius: 8px;
|
||||
@@ -937,7 +1153,7 @@
|
||||
<div class="plugins-container">
|
||||
<!-- Page Header -->
|
||||
<div class="page-header">
|
||||
<div style="display: flex; justify-content: space-between; align-items: flex-start; width: 100%;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: flex-start; width: 100%; flex-wrap: wrap; gap: 16px;">
|
||||
<div>
|
||||
<h1>
|
||||
<div class="icon">
|
||||
@@ -947,7 +1163,7 @@
|
||||
</h1>
|
||||
<p>{% trans "List of installed plugins on your CyberPanel" %}</p>
|
||||
</div>
|
||||
<div style="display: flex; gap: 20px; align-items: center; margin-top: 10px;">
|
||||
<div style="display: flex; gap: 20px; align-items: center; margin-top: 10px; flex-wrap: wrap;">
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<i class="fas fa-check-circle" style="color: #28a745; font-size: 18px;"></i>
|
||||
<span style="font-weight: 600; color: var(--text-primary, #2f3640);">
|
||||
@@ -960,6 +1176,16 @@
|
||||
{% trans "Active:" %} {{ active_count|default:0 }}
|
||||
</span>
|
||||
</div>
|
||||
{% if plugins %}
|
||||
<div class="bulk-actions-header" style="display: flex; gap: 10px; margin-left: 8px; padding-left: 16px; border-left: 2px solid #e2e8f0;">
|
||||
<button type="button" class="btn-bulk btn-activate-all" onclick="activateAllPlugins()" title="{% trans 'Activate all installed plugins' %}">
|
||||
<i class="fas fa-toggle-on"></i> {% trans "Activate All Plugins" %}
|
||||
</button>
|
||||
<button type="button" class="btn-bulk btn-deactivate-all" onclick="deactivateAllPlugins()" title="{% trans 'Deactivate all installed plugins' %}">
|
||||
<i class="fas fa-toggle-off"></i> {% trans "Deactivate All Plugins" %}
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -970,23 +1196,25 @@
|
||||
|
||||
{% if plugins %}
|
||||
<!-- View Toggle -->
|
||||
<div class="view-toggle">
|
||||
<button class="view-btn active" onclick="toggleView('grid')">
|
||||
<i class="fas fa-th-large"></i>
|
||||
{% trans "Grid View" %}
|
||||
</button>
|
||||
<button class="view-btn" onclick="toggleView('table')">
|
||||
<i class="fas fa-list"></i>
|
||||
{% trans "Table View" %}
|
||||
</button>
|
||||
<button class="view-btn" onclick="toggleView('store')">
|
||||
<i class="fas fa-store"></i>
|
||||
{% trans "CyberPanel Plugin Store" %}
|
||||
</button>
|
||||
<a href="/plugins/help/" class="view-btn" style="text-decoration: none;">
|
||||
<i class="fas fa-book"></i>
|
||||
{% trans "Plugin Development Guide" %}
|
||||
</a>
|
||||
<div class="view-toggle-wrapper">
|
||||
<div class="view-toggle">
|
||||
<button class="view-btn active" onclick="toggleView('grid', true)">
|
||||
<i class="fas fa-th-large"></i>
|
||||
{% trans "Grid View" %}
|
||||
</button>
|
||||
<button class="view-btn" onclick="toggleView('table', true)">
|
||||
<i class="fas fa-list"></i>
|
||||
{% trans "Table View" %}
|
||||
</button>
|
||||
<button class="view-btn" onclick="toggleView('store', true)">
|
||||
<i class="fas fa-store"></i>
|
||||
{% trans "CyberPanel Plugin Store" %}
|
||||
</button>
|
||||
<a href="/plugins/help/" class="view-btn" style="text-decoration: none;">
|
||||
<i class="fas fa-book"></i>
|
||||
{% trans "Plugin Development Guide" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Grid View -->
|
||||
@@ -995,14 +1223,24 @@
|
||||
<div class="plugin-card">
|
||||
<div class="plugin-header">
|
||||
<div class="plugin-icon">
|
||||
{% if plugin.type == "Security" %}
|
||||
{% if plugin.type|lower == "security" %}
|
||||
<i class="fas fa-shield-alt"></i>
|
||||
{% elif plugin.type == "Performance" %}
|
||||
{% elif plugin.type|lower == "performance" %}
|
||||
<i class="fas fa-rocket"></i>
|
||||
{% elif plugin.type == "Utility" %}
|
||||
{% elif plugin.type|lower == "utility" %}
|
||||
<i class="fas fa-tools"></i>
|
||||
{% elif plugin.type == "Backup" %}
|
||||
{% elif plugin.type|lower == "backup" %}
|
||||
<i class="fas fa-save"></i>
|
||||
{% elif plugin.type|lower == "monitoring" %}
|
||||
<i class="fas fa-heartbeat"></i>
|
||||
{% elif plugin.type|lower == "integration" %}
|
||||
<i class="fas fa-plug"></i>
|
||||
{% elif plugin.type|lower == "email" %}
|
||||
<i class="fas fa-envelope"></i>
|
||||
{% elif plugin.type|lower == "development" %}
|
||||
<i class="fas fa-code"></i>
|
||||
{% elif plugin.type|lower == "analytics" %}
|
||||
<i class="fas fa-chart-bar"></i>
|
||||
{% else %}
|
||||
<i class="fas fa-puzzle-piece"></i>
|
||||
{% endif %}
|
||||
@@ -1010,6 +1248,9 @@
|
||||
<div class="plugin-info">
|
||||
<h3 class="plugin-name">{{ plugin.name }}{% if plugin.is_paid|default:False|default_if_none:False %} <span class="paid-badge" title="{% trans 'Paid Plugin - Patreon Subscription Required' %}"><i class="fas fa-crown"></i> {% trans "Premium" %}</span>{% endif %}</h3>
|
||||
<div class="plugin-meta">
|
||||
{% if plugin.freshness_badge %}
|
||||
<span class="{{ plugin.freshness_badge.class }}" title="{{ plugin.freshness_badge.title }}">{{ plugin.freshness_badge.badge }}</span>
|
||||
{% endif %}
|
||||
<span class="plugin-type">{{ plugin.type }}</span>
|
||||
<span class="plugin-version-number">v{{ plugin.version }}</span>
|
||||
{% if plugin.is_paid|default:False|default_if_none:False %}
|
||||
@@ -1071,11 +1312,11 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if plugin.enabled %}
|
||||
<button class="btn-action btn-deactivate btn-small" onclick="deactivatePlugin('{{ plugin.plugin_dir }}')">
|
||||
<button class="btn-action btn-deactivate btn-small" data-plugin-dir="{{ plugin.plugin_dir }}" onclick="deactivatePlugin('{{ plugin.plugin_dir }}')">
|
||||
<i class="fas fa-toggle-on"></i> {% trans "Deactivate" %}
|
||||
</button>
|
||||
{% else %}
|
||||
<button class="btn-action btn-activate btn-small" onclick="activatePlugin('{{ plugin.plugin_dir }}')">
|
||||
<button class="btn-action btn-activate btn-small" data-plugin-dir="{{ plugin.plugin_dir }}" onclick="activatePlugin('{{ plugin.plugin_dir }}')">
|
||||
<i class="fas fa-toggle-off"></i> {% trans "Activate" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
@@ -1125,6 +1366,9 @@
|
||||
<tr>
|
||||
<td>
|
||||
<strong>{{ plugin.name }}</strong>
|
||||
{% if plugin.freshness_badge %}
|
||||
<br><span class="{{ plugin.freshness_badge.class }}" title="{{ plugin.freshness_badge.title }}">{{ plugin.freshness_badge.badge }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<span class="plugin-version-number">{{ plugin.version }}</span>
|
||||
@@ -1155,11 +1399,11 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if plugin.enabled %}
|
||||
<button class="btn-action btn-deactivate" onclick="deactivatePlugin('{{ plugin.plugin_dir }}')">
|
||||
<button class="btn-action btn-deactivate" data-plugin-dir="{{ plugin.plugin_dir }}" onclick="deactivatePlugin('{{ plugin.plugin_dir }}')">
|
||||
<i class="fas fa-toggle-on"></i> {% trans "Deactivate" %}
|
||||
</button>
|
||||
{% else %}
|
||||
<button class="btn-action btn-activate" onclick="activatePlugin('{{ plugin.plugin_dir }}')">
|
||||
<button class="btn-action btn-activate" data-plugin-dir="{{ plugin.plugin_dir }}" onclick="activatePlugin('{{ plugin.plugin_dir }}')">
|
||||
<i class="fas fa-toggle-off"></i> {% trans "Activate" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
@@ -1216,15 +1460,15 @@
|
||||
|
||||
<!-- View Toggle (only shown when no plugins installed) -->
|
||||
<div class="view-toggle" style="margin-top: 25px;">
|
||||
<button class="view-btn" onclick="toggleView('grid')">
|
||||
<button class="view-btn" onclick="toggleView('grid', true)">
|
||||
<i class="fas fa-th-large"></i>
|
||||
{% trans "Grid View" %}
|
||||
</button>
|
||||
<button class="view-btn" onclick="toggleView('table')">
|
||||
<button class="view-btn" onclick="toggleView('table', true)">
|
||||
<i class="fas fa-list"></i>
|
||||
{% trans "Table View" %}
|
||||
</button>
|
||||
<button class="view-btn active" onclick="toggleView('store')">
|
||||
<button class="view-btn active" onclick="toggleView('store', true)">
|
||||
<i class="fas fa-store"></i>
|
||||
{% trans "CyberPanel Plugin Store" %}
|
||||
</button>
|
||||
@@ -1274,35 +1518,83 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Store Loading Indicator -->
|
||||
<div id="storeLoading" class="store-loading" style="display: none;">
|
||||
<div class="loading-spinner"></div>
|
||||
<p>{% trans "Loading plugins from store..." %}</p>
|
||||
<!-- Search Bar (always visible in store view) -->
|
||||
<div class="store-search-bar">
|
||||
<i class="fas fa-search store-search-icon"></i>
|
||||
<input type="text" id="pluginSearchInput" class="store-search-input" placeholder="{% trans 'Search plugins by name or description...' %}" aria-label="{% trans 'Search plugins' %}">
|
||||
<button type="button" class="store-search-clear" id="pluginSearchClear" onclick="clearPluginSearch()" style="display: none;" aria-label="{% trans 'Clear search' %}">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Store Error -->
|
||||
<div id="storeError" class="alert alert-danger" style="display: none;">
|
||||
<i class="fas fa-exclamation-circle alert-icon"></i>
|
||||
<span id="storeErrorText"></span>
|
||||
</div>
|
||||
|
||||
<!-- Store Content -->
|
||||
<div id="storeContent" style="display: none;">
|
||||
<!-- Alphabetical Filter -->
|
||||
<div class="alphabet-filter">
|
||||
{% for letter in "ABCDEFGHIJKLMNOPQRSTUVWXYZ" %}
|
||||
<button class="alpha-btn" onclick="filterByLetter('{{ letter }}')">{{ letter }}</button>
|
||||
{% endfor %}
|
||||
<button class="alpha-btn active" onclick="filterByLetter('all')">{% trans "All" %}</button>
|
||||
<!-- Category Filter (always visible in store view) - v2026-02-01 no Plugin category -->
|
||||
<div class="category-filter">
|
||||
<button class="category-btn active" onclick="filterByCategory('all', event)" data-category="all">
|
||||
<i class="fas fa-th"></i>
|
||||
{% trans "All Categories" %}
|
||||
</button>
|
||||
<button class="category-btn" onclick="filterByCategory('Utility', event)" data-category="Utility">
|
||||
<i class="fas fa-wrench"></i>
|
||||
{% trans "Utility" %}
|
||||
</button>
|
||||
<button class="category-btn" onclick="filterByCategory('Security', event)" data-category="Security">
|
||||
<i class="fas fa-shield-alt"></i>
|
||||
{% trans "Security" %}
|
||||
</button>
|
||||
<button class="category-btn" onclick="filterByCategory('Backup', event)" data-category="Backup">
|
||||
<i class="fas fa-archive"></i>
|
||||
{% trans "Backup" %}
|
||||
</button>
|
||||
<button class="category-btn" onclick="filterByCategory('Performance', event)" data-category="Performance">
|
||||
<i class="fas fa-rocket"></i>
|
||||
{% trans "Performance" %}
|
||||
</button>
|
||||
<button class="category-btn" onclick="filterByCategory('Monitoring', event)" data-category="Monitoring">
|
||||
<i class="fas fa-heartbeat"></i>
|
||||
{% trans "Monitoring" %}
|
||||
</button>
|
||||
<button class="category-btn" onclick="filterByCategory('Integration', event)" data-category="Integration">
|
||||
<i class="fas fa-plug"></i>
|
||||
{% trans "Integration" %}
|
||||
</button>
|
||||
<button class="category-btn" onclick="filterByCategory('Email', event)" data-category="Email">
|
||||
<i class="fas fa-envelope"></i>
|
||||
{% trans "Email" %}
|
||||
</button>
|
||||
<button class="category-btn" onclick="filterByCategory('Development', event)" data-category="Development">
|
||||
<i class="fas fa-code"></i>
|
||||
{% trans "Development" %}
|
||||
</button>
|
||||
<button class="category-btn" onclick="filterByCategory('Analytics', event)" data-category="Analytics">
|
||||
<i class="fas fa-chart-bar"></i>
|
||||
{% trans "Analytics" %}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Store Table -->
|
||||
|
||||
<!-- Alphabetical Filter (collapsible, hidden by default) -->
|
||||
<div class="alphabet-filter-wrapper">
|
||||
<button type="button" class="alphabet-toggle-btn" id="alphabetToggleBtn" onclick="toggleAlphabetFilter()" aria-expanded="false">
|
||||
<i class="fas fa-sort-alpha-down alphabet-toggle-icon"></i>
|
||||
<span>{% trans "A-Å Filter" %}</span>
|
||||
<i class="fas fa-chevron-down alphabet-chevron"></i>
|
||||
</button>
|
||||
<div class="alphabet-filter" id="alphabetFilter" aria-hidden="true" style="display: none;">
|
||||
{% for letter in "ABCDEFGHIJKLMNOPQRSTUVWXYZ" %}
|
||||
<button class="alpha-btn" onclick="filterByLetter('{{ letter }}', event)">{{ letter }}</button>
|
||||
{% endfor %}
|
||||
<button class="alpha-btn active" onclick="filterByLetter('all', event)">{% trans "All" %}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Store Content (table area) -->
|
||||
<div id="storeContent" style="display: block;">
|
||||
<div class="store-table-wrapper">
|
||||
<table class="store-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Icon" %}</th>
|
||||
<th>{% trans "Plugin Name" %}</th>
|
||||
<th>{% trans "Category" %}</th>
|
||||
<th>{% trans "Version" %}</th>
|
||||
<th>{% trans "Pricing" %}</th>
|
||||
<th>{% trans "Modify Date" %}</th>
|
||||
@@ -1319,19 +1611,18 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-controller="listWebsites" id="listFail" class="alert alert-danger" style="display: none;">
|
||||
<i class="fas fa-exclamation-circle alert-icon"></i>
|
||||
<span>{% trans "Cannot list plugins. Error message:" %} {$ errorMessage $}</span>
|
||||
</div>
|
||||
<!-- Plugin store errors shown in storeError div; listWebsites controller removed - not applicable to plugins page -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Cache-busting version: 2026-01-25-v4 - Fixed is_paid normalization and ensured consistent rendering
|
||||
// Force browser to reload this script by changing version number
|
||||
// Cache-busting version: 2026-02-01-v1 - Fixed category filter, added search bar, collapsible A-Å
|
||||
let storePlugins = [];
|
||||
let currentFilter = 'all';
|
||||
let currentCategory = 'all';
|
||||
let currentSearchQuery = '';
|
||||
let isSettingHash = false; // Flag to prevent infinite loops
|
||||
|
||||
// Get CSRF cookie helper function
|
||||
function getCookie(name) {
|
||||
@@ -1349,7 +1640,7 @@ function getCookie(name) {
|
||||
return cookieValue;
|
||||
}
|
||||
|
||||
function toggleView(view) {
|
||||
function toggleView(view, updateHash = true) {
|
||||
const gridView = document.getElementById('gridView');
|
||||
const tableView = document.getElementById('tableView');
|
||||
const storeView = document.getElementById('storeView');
|
||||
@@ -1357,6 +1648,23 @@ function toggleView(view) {
|
||||
|
||||
viewBtns.forEach(btn => btn.classList.remove('active'));
|
||||
|
||||
// Update URL hash only if explicitly requested (user clicked a button, not initial load)
|
||||
if (updateHash) {
|
||||
isSettingHash = true;
|
||||
const hash = '#' + view;
|
||||
// Use replaceState to update URL - this updates the hash without triggering hashchange event
|
||||
if (window.history && window.history.replaceState) {
|
||||
// Get current pathname and preserve it, just update the hash
|
||||
const newUrl = window.location.pathname + window.location.search + hash;
|
||||
window.history.replaceState(null, null, newUrl);
|
||||
} else {
|
||||
// Fallback for older browsers - this will trigger hashchange but we have the flag
|
||||
window.location.hash = hash;
|
||||
}
|
||||
// Reset flag after a short delay
|
||||
setTimeout(() => { isSettingHash = false; }, 100);
|
||||
}
|
||||
|
||||
if (view === 'grid') {
|
||||
gridView.style.display = 'grid';
|
||||
tableView.style.display = 'none';
|
||||
@@ -1387,18 +1695,34 @@ function loadPluginStore() {
|
||||
const storeError = document.getElementById('storeError');
|
||||
const storeErrorText = document.getElementById('storeErrorText');
|
||||
const storeContent = document.getElementById('storeContent');
|
||||
const storeTableBody = document.getElementById('storeTableBody');
|
||||
if (!storeLoading || !storeError || !storeErrorText || !storeContent) return;
|
||||
|
||||
storeLoading.style.display = 'block';
|
||||
storeError.style.display = 'none';
|
||||
storeContent.style.display = 'none';
|
||||
storeContent.style.display = 'block';
|
||||
|
||||
fetch('/plugins/api/store/plugins/', {
|
||||
method: 'GET',
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
'X-CSRFToken': getCookie('csrftoken')
|
||||
'X-CSRFToken': getCookie('csrftoken'),
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(response => {
|
||||
const contentType = response.headers.get('content-type') || '';
|
||||
if (!contentType.includes('application/json')) {
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
throw new Error('Session expired or access denied. Please refresh the page and log in again.');
|
||||
}
|
||||
if (response.redirected || response.status >= 400) {
|
||||
throw new Error('Could not load plugin store. Your session may have expired. Please refresh the page and log in again.');
|
||||
}
|
||||
throw new Error('Server returned invalid response. Please refresh the page and try again.');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
storeLoading.style.display = 'none';
|
||||
|
||||
@@ -1413,8 +1737,12 @@ function loadPluginStore() {
|
||||
})
|
||||
.catch(error => {
|
||||
storeLoading.style.display = 'none';
|
||||
storeErrorText.textContent = 'Error loading plugin store: ' + error.message;
|
||||
storeErrorText.textContent = error.message || 'Error loading plugin store. Please refresh the page and try again.';
|
||||
storeError.style.display = 'block';
|
||||
storeContent.style.display = 'block';
|
||||
if (storeTableBody && storeTableBody.innerHTML === '') {
|
||||
storeTableBody.innerHTML = '<tr><td colspan="9" style="text-align: center; padding: 20px; color: var(--text-secondary, #64748b);">Unable to load plugins. Please check your connection and try again.</td></tr>';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1425,9 +1753,34 @@ function escapeHtml(text) {
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function getFreshnessBadgeHtml(freshnessFromApi, modifyDate) {
|
||||
// Use API data if available
|
||||
if (freshnessFromApi && freshnessFromApi.badge && freshnessFromApi.class) {
|
||||
return `<br><span class="${escapeHtml(freshnessFromApi.class)}" title="${escapeHtml(freshnessFromApi.title || '')}">${escapeHtml(freshnessFromApi.badge)}</span>`;
|
||||
}
|
||||
// Compute from modify_date (for cached data without freshness_badge)
|
||||
if (!modifyDate || modifyDate === 'N/A') return '';
|
||||
try {
|
||||
const m = modifyDate.match(/^(\d{4})-(\d{2})-(\d{2})\s+(\d{2}):(\d{2}):(\d{2})/);
|
||||
if (!m) return '';
|
||||
const d = new Date(parseInt(m[1]), parseInt(m[2]) - 1, parseInt(m[3]), parseInt(m[4]), parseInt(m[5]), parseInt(m[6]));
|
||||
const daysAgo = Math.floor((Date.now() - d.getTime()) / (24 * 60 * 60 * 1000));
|
||||
if (daysAgo <= 90) {
|
||||
return '<br><span class="freshness-badge-new" title="This plugin was released/updated within the last 3 months">NEW</span>';
|
||||
} else if (daysAgo <= 365) {
|
||||
return '<br><span class="freshness-badge-stable" title="This plugin was updated within the last year">Stable</span>';
|
||||
} else if (daysAgo < 730) {
|
||||
return '<br><span class="freshness-badge-unstable" title="This plugin has not been updated in over 1 year">Unstable</span>';
|
||||
} else {
|
||||
return '<br><span class="freshness-badge-stale" title="This plugin has not been updated in over 2 years">STALE</span>';
|
||||
}
|
||||
} catch (e) {}
|
||||
return '';
|
||||
}
|
||||
|
||||
function displayStorePlugins() {
|
||||
// Version: 2026-01-25-v4 - Store view: Removed Status column, always show Free/Paid badges
|
||||
// CRITICAL: This function MUST create exactly 7 columns (no Status, no Deactivate/Uninstall)
|
||||
// Version: 2026-01-27-v1 - Added category filtering support
|
||||
// CRITICAL: This function MUST create exactly 8 columns (Icon, Plugin Name, Version, Pricing, Modify Date, Action, Help, About)
|
||||
const tbody = document.getElementById('storeTableBody');
|
||||
if (!tbody) {
|
||||
console.error('storeTableBody not found!');
|
||||
@@ -1436,23 +1789,50 @@ function displayStorePlugins() {
|
||||
tbody.innerHTML = '';
|
||||
|
||||
if (!storePlugins || storePlugins.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="8" style="text-align: center; padding: 20px; color: var(--text-secondary, #64748b);">No plugins available in store</td></tr>';
|
||||
tbody.innerHTML = '<tr><td colspan="9" style="text-align: center; padding: 20px; color: var(--text-secondary, #64748b);">No plugins available in store</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
let filteredPlugins = storePlugins;
|
||||
|
||||
// Apply category filter (case-insensitive); plugins without category are excluded
|
||||
if (currentCategory !== 'all') {
|
||||
const categoryLower = currentCategory.toLowerCase();
|
||||
filteredPlugins = filteredPlugins.filter(plugin => {
|
||||
const pluginType = ((plugin.type || '') + '').trim().toLowerCase();
|
||||
return pluginType && pluginType === categoryLower;
|
||||
});
|
||||
}
|
||||
|
||||
// Apply search filter (name + description)
|
||||
if ((currentSearchQuery || '').trim()) {
|
||||
const searchLower = (currentSearchQuery || '').trim().toLowerCase();
|
||||
const searchTerms = searchLower.split(/\s+/).filter(t => t.length > 0);
|
||||
filteredPlugins = filteredPlugins.filter(plugin => {
|
||||
const name = (plugin.name || '').toLowerCase();
|
||||
const desc = (plugin.description || '').toLowerCase();
|
||||
const combined = name + ' ' + desc + ' ' + (plugin.plugin_dir || '');
|
||||
return searchTerms.every(term => combined.includes(term));
|
||||
});
|
||||
}
|
||||
|
||||
// Apply alphabetical filter
|
||||
if (currentFilter !== 'all') {
|
||||
filteredPlugins = storePlugins.filter(plugin =>
|
||||
plugin.name.charAt(0).toUpperCase() === currentFilter
|
||||
filteredPlugins = filteredPlugins.filter(plugin =>
|
||||
(plugin.name || '').charAt(0).toUpperCase() === currentFilter
|
||||
);
|
||||
}
|
||||
|
||||
filteredPlugins.forEach(plugin => {
|
||||
if (filteredPlugins.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="9" style="text-align: center; padding: 20px; color: var(--text-secondary, #64748b);">No plugins match your filters. Try adjusting the category, search, or letter filter.</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
filteredPlugins.forEach(plugin => {
|
||||
const row = document.createElement('tr');
|
||||
|
||||
// Plugin icon - based on plugin type (same logic as Grid/Table views)
|
||||
const pluginType = (plugin.type || 'Plugin').toLowerCase();
|
||||
const pluginType = ((plugin.type || '') + '').trim().toLowerCase() || 'utility';
|
||||
let iconClass = 'fas fa-puzzle-piece'; // Default icon
|
||||
if (pluginType.includes('security')) {
|
||||
iconClass = 'fas fa-shield-alt';
|
||||
@@ -1462,6 +1842,16 @@ function displayStorePlugins() {
|
||||
iconClass = 'fas fa-tools';
|
||||
} else if (pluginType.includes('backup')) {
|
||||
iconClass = 'fas fa-save';
|
||||
} else if (pluginType.includes('monitoring')) {
|
||||
iconClass = 'fas fa-heartbeat';
|
||||
} else if (pluginType.includes('integration')) {
|
||||
iconClass = 'fas fa-plug';
|
||||
} else if (pluginType.includes('email')) {
|
||||
iconClass = 'fas fa-envelope';
|
||||
} else if (pluginType.includes('development')) {
|
||||
iconClass = 'fas fa-code';
|
||||
} else if (pluginType.includes('analytics')) {
|
||||
iconClass = 'fas fa-chart-bar';
|
||||
}
|
||||
const iconHtml = `<div class="plugin-icon" style="width: 40px; height: 40px; background: var(--bg-secondary, #f8f9ff); border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 18px; color: #5856d6; margin: 0 auto;">
|
||||
<i class="${iconClass}"></i>
|
||||
@@ -1503,6 +1893,9 @@ function displayStorePlugins() {
|
||||
// Modify Date column - show N/A for store plugins (they're from GitHub, not local)
|
||||
const modifyDateHtml = plugin.modify_date ? `<small style="color: var(--text-secondary, #64748b);">${escapeHtml(plugin.modify_date)}</small>` : '<small style="color: var(--text-secondary, #64748b);">N/A</small>';
|
||||
|
||||
// Freshness badge (NEW/Stable/STALE) - use API data or compute from modify_date
|
||||
const freshnessBadgeHtml = getFreshnessBadgeHtml(plugin.freshness_badge || null, plugin.modify_date);
|
||||
|
||||
// Pricing badge - ALWAYS show a badge (default to Free if is_paid is missing/undefined/null)
|
||||
// Version: 2026-01-25-v4 - Normalize is_paid to handle all possible values
|
||||
let isPaid = false;
|
||||
@@ -1516,12 +1909,17 @@ function displayStorePlugins() {
|
||||
? '<span class="plugin-pricing-badge paid">Paid</span>'
|
||||
: '<span class="plugin-pricing-badge free">Free</span>';
|
||||
|
||||
// Version: 2026-01-25-v5 - Added plugin icons to Store view (8 columns: Icon, Plugin Name, Version, Pricing, Modify Date, Action, Help, About)
|
||||
// Version: 2026-01-27-v1 - Added Category column; category is required, no default
|
||||
const pluginCategory = ((plugin.type || '') + '').trim() || '—';
|
||||
const categoryBadge = `<span class="plugin-type">${escapeHtml(pluginCategory.toUpperCase())}</span>`;
|
||||
|
||||
row.innerHTML = `
|
||||
<td style="text-align: center;">${iconHtml}</td>
|
||||
<td>
|
||||
<strong>${escapeHtml(plugin.name)}</strong>
|
||||
${freshnessBadgeHtml}
|
||||
</td>
|
||||
<td>${categoryBadge}</td>
|
||||
<td><span class="plugin-version-number">${escapeHtml(plugin.version)}</span></td>
|
||||
<td>${pricingBadge}</td>
|
||||
<td>${modifyDateHtml}</td>
|
||||
@@ -1534,16 +1932,16 @@ function displayStorePlugins() {
|
||||
});
|
||||
}
|
||||
|
||||
function filterByLetter(letter) {
|
||||
currentFilter = letter;
|
||||
const alphaBtns = document.querySelectorAll('.alpha-btn');
|
||||
alphaBtns.forEach(btn => btn.classList.remove('active'));
|
||||
if (event && event.target) {
|
||||
event.target.classList.add('active');
|
||||
function filterByCategory(category, evt) {
|
||||
currentCategory = category;
|
||||
const categoryBtns = document.querySelectorAll('.category-btn');
|
||||
categoryBtns.forEach(btn => btn.classList.remove('active'));
|
||||
const clickedBtn = evt && (evt.currentTarget || (evt.target && evt.target.closest('.category-btn')));
|
||||
if (clickedBtn) {
|
||||
clickedBtn.classList.add('active');
|
||||
} else {
|
||||
// Find and activate the clicked button
|
||||
alphaBtns.forEach(btn => {
|
||||
if (btn.textContent.trim() === letter || (letter === 'all' && btn.textContent.trim() === 'All')) {
|
||||
categoryBtns.forEach(btn => {
|
||||
if (btn.getAttribute('data-category') === category) {
|
||||
btn.classList.add('active');
|
||||
}
|
||||
});
|
||||
@@ -1551,6 +1949,46 @@ function filterByLetter(letter) {
|
||||
displayStorePlugins();
|
||||
}
|
||||
|
||||
function filterByLetter(letter, evt) {
|
||||
currentFilter = letter;
|
||||
const alphaBtns = document.querySelectorAll('.alpha-btn');
|
||||
alphaBtns.forEach(btn => btn.classList.remove('active'));
|
||||
const clickedBtn = evt && (evt.currentTarget || (evt.target && evt.target.closest('.alpha-btn')));
|
||||
if (clickedBtn) {
|
||||
clickedBtn.classList.add('active');
|
||||
} else {
|
||||
const label = letter === 'all' ? 'All' : letter;
|
||||
alphaBtns.forEach(btn => {
|
||||
if (btn.textContent.trim() === label) {
|
||||
btn.classList.add('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
displayStorePlugins();
|
||||
}
|
||||
|
||||
function clearPluginSearch() {
|
||||
const input = document.getElementById('pluginSearchInput');
|
||||
const clearBtn = document.getElementById('pluginSearchClear');
|
||||
if (input) {
|
||||
input.value = '';
|
||||
currentSearchQuery = '';
|
||||
if (clearBtn) clearBtn.style.display = 'none';
|
||||
displayStorePlugins();
|
||||
input.focus();
|
||||
}
|
||||
}
|
||||
|
||||
function toggleAlphabetFilter() {
|
||||
const filter = document.getElementById('alphabetFilter');
|
||||
const toggleBtn = document.getElementById('alphabetToggleBtn');
|
||||
if (!filter || !toggleBtn) return;
|
||||
const isExpanded = toggleBtn.getAttribute('aria-expanded') === 'true';
|
||||
filter.style.display = isExpanded ? 'none' : 'flex';
|
||||
filter.setAttribute('aria-hidden', isExpanded ? 'true' : 'false');
|
||||
toggleBtn.setAttribute('aria-expanded', isExpanded ? 'false' : 'true');
|
||||
}
|
||||
|
||||
function upgradePlugin(pluginName, currentVersion, newVersion) {
|
||||
// Show confirmation dialog with backup warning
|
||||
const message = `⚠️ WARNING: Plugin Upgrade\n\n` +
|
||||
@@ -2126,6 +2564,91 @@ function deactivatePlugin(pluginName) {
|
||||
});
|
||||
}
|
||||
|
||||
function activateAllPlugins() {
|
||||
const btns = document.querySelectorAll('#gridView .btn-activate[data-plugin-dir], #tableView .btn-activate[data-plugin-dir]');
|
||||
const plugins = Array.from(btns).map(b => b.getAttribute('data-plugin-dir')).filter(Boolean);
|
||||
if (plugins.length === 0) {
|
||||
if (typeof PNotify !== 'undefined') {
|
||||
new PNotify({ title: 'Info', text: 'No inactive plugins to activate.', type: 'info' });
|
||||
} else {
|
||||
alert('No inactive plugins to activate.');
|
||||
}
|
||||
return;
|
||||
}
|
||||
const activateAllBtn = document.querySelector('.btn-activate-all');
|
||||
if (activateAllBtn) activateAllBtn.disabled = true;
|
||||
let done = 0;
|
||||
const run = () => {
|
||||
if (done >= plugins.length) {
|
||||
if (typeof PNotify !== 'undefined') {
|
||||
new PNotify({ title: 'Success!', text: `Activated ${plugins.length} plugin(s).`, type: 'success' });
|
||||
} else {
|
||||
alert(`Activated ${plugins.length} plugin(s).`);
|
||||
}
|
||||
setTimeout(() => location.reload(), 800);
|
||||
return;
|
||||
}
|
||||
const name = plugins[done];
|
||||
fetch(`/plugins/api/enable/${name}/`, {
|
||||
method: 'POST',
|
||||
headers: { 'X-CSRFToken': getCookie('csrftoken'), 'Content-Type': 'application/json' }
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (!data.success && typeof PNotify !== 'undefined') {
|
||||
new PNotify({ title: 'Warning', text: `${name}: ${data.error || 'failed'}`, type: 'error' });
|
||||
}
|
||||
done++;
|
||||
run();
|
||||
})
|
||||
.catch(() => { done++; run(); });
|
||||
};
|
||||
run();
|
||||
}
|
||||
|
||||
function deactivateAllPlugins() {
|
||||
const btns = document.querySelectorAll('#gridView .btn-deactivate[data-plugin-dir], #tableView .btn-deactivate[data-plugin-dir]');
|
||||
const plugins = Array.from(btns).map(b => b.getAttribute('data-plugin-dir')).filter(Boolean);
|
||||
if (plugins.length === 0) {
|
||||
if (typeof PNotify !== 'undefined') {
|
||||
new PNotify({ title: 'Info', text: 'No active plugins to deactivate.', type: 'info' });
|
||||
} else {
|
||||
alert('No active plugins to deactivate.');
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!confirm(`Deactivate ${plugins.length} plugin(s)? They will be disabled but remain installed.`)) return;
|
||||
const deactivateAllBtn = document.querySelector('.btn-deactivate-all');
|
||||
if (deactivateAllBtn) deactivateAllBtn.disabled = true;
|
||||
let done = 0;
|
||||
const run = () => {
|
||||
if (done >= plugins.length) {
|
||||
if (typeof PNotify !== 'undefined') {
|
||||
new PNotify({ title: 'Success!', text: `Deactivated ${plugins.length} plugin(s).`, type: 'success' });
|
||||
} else {
|
||||
alert(`Deactivated ${plugins.length} plugin(s).`);
|
||||
}
|
||||
setTimeout(() => location.reload(), 800);
|
||||
return;
|
||||
}
|
||||
const name = plugins[done];
|
||||
fetch(`/plugins/api/disable/${name}/`, {
|
||||
method: 'POST',
|
||||
headers: { 'X-CSRFToken': getCookie('csrftoken'), 'Content-Type': 'application/json' }
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (!data.success && typeof PNotify !== 'undefined') {
|
||||
new PNotify({ title: 'Warning', text: `${name}: ${data.error || 'failed'}`, type: 'error' });
|
||||
}
|
||||
done++;
|
||||
run();
|
||||
})
|
||||
.catch(() => { done++; run(); });
|
||||
};
|
||||
run();
|
||||
}
|
||||
|
||||
function dismissNotice() {
|
||||
const notice = document.getElementById('pluginStoreNotice');
|
||||
if (notice) {
|
||||
@@ -2206,16 +2729,46 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
// Update cache expiry time to local timezone
|
||||
updateCacheExpiryTime();
|
||||
|
||||
// Default to grid view if plugins exist, otherwise show store
|
||||
const gridView = document.getElementById('gridView');
|
||||
const storeView = document.getElementById('storeView');
|
||||
if (gridView && gridView.children.length > 0) {
|
||||
toggleView('grid');
|
||||
} else if (storeView) {
|
||||
// No plugins installed, show store by default
|
||||
toggleView('store');
|
||||
// Search input listener (debounced)
|
||||
const searchInput = document.getElementById('pluginSearchInput');
|
||||
const searchClearBtn = document.getElementById('pluginSearchClear');
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener('input', function() {
|
||||
currentSearchQuery = this.value;
|
||||
if (searchClearBtn) {
|
||||
searchClearBtn.style.display = currentSearchQuery.trim() ? 'block' : 'none';
|
||||
}
|
||||
displayStorePlugins();
|
||||
});
|
||||
searchInput.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') {
|
||||
clearPluginSearch();
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Check URL hash for view preference
|
||||
const hash = window.location.hash.substring(1); // Remove #
|
||||
const validViews = ['grid', 'table', 'store'];
|
||||
|
||||
let initialView = 'grid'; // Default
|
||||
if (validViews.includes(hash)) {
|
||||
initialView = hash;
|
||||
} else {
|
||||
// Default to grid view if plugins exist, otherwise show store
|
||||
const gridView = document.getElementById('gridView');
|
||||
if (gridView && gridView.children.length > 0) {
|
||||
initialView = 'grid';
|
||||
} else {
|
||||
initialView = 'store';
|
||||
}
|
||||
}
|
||||
|
||||
// Set initial view without updating hash (only update hash if there was already one)
|
||||
const hadHash = hash.length > 0;
|
||||
toggleView(initialView, hadHash);
|
||||
|
||||
// Load store plugins if store view is visible (either from toggleView or already displayed)
|
||||
setTimeout(function() {
|
||||
const storeViewCheck = document.getElementById('storeView');
|
||||
@@ -2224,5 +2777,21 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
}
|
||||
}, 100); // Small delay to ensure DOM is ready
|
||||
});
|
||||
|
||||
// Handle hash changes (back/forward browser buttons)
|
||||
window.addEventListener('hashchange', function() {
|
||||
// Prevent infinite loops when we programmatically set the hash
|
||||
if (isSettingHash) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hash = window.location.hash.substring(1);
|
||||
const validViews = ['grid', 'table', 'store'];
|
||||
|
||||
if (validViews.includes(hash)) {
|
||||
// Don't update hash again since it's already set (user navigated via browser)
|
||||
toggleView(hash, false);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -53,6 +53,33 @@ def _is_plugin_enabled(plugin_name):
|
||||
return True # Default to enabled if file read fails
|
||||
return True # Default to enabled if state file doesn't exist
|
||||
|
||||
|
||||
def _get_freshness_badge(modify_date):
|
||||
"""
|
||||
Return freshness badge (NEW/Stable/STALE) based on modify_date.
|
||||
modify_date format: 'YYYY-MM-DD HH:MM:SS' or 'N/A'
|
||||
- 0-90 days: NEW (yellow)
|
||||
- 90-365 days: Stable (green)
|
||||
- 730+ days: STALE (red)
|
||||
- 365-730 days: no badge
|
||||
"""
|
||||
if not modify_date or modify_date == 'N/A' or not isinstance(modify_date, str):
|
||||
return None
|
||||
try:
|
||||
dt = datetime.strptime(modify_date[:19], '%Y-%m-%d %H:%M:%S')
|
||||
days_ago = (datetime.now() - dt).days
|
||||
if days_ago <= 90:
|
||||
return {'badge': 'NEW', 'class': 'freshness-badge-new', 'title': 'This plugin was released/updated within the last 3 months'}
|
||||
elif days_ago <= 365:
|
||||
return {'badge': 'Stable', 'class': 'freshness-badge-stable', 'title': 'This plugin was updated within the last year'}
|
||||
elif days_ago < 730:
|
||||
return {'badge': 'Unstable', 'class': 'freshness-badge-unstable', 'title': 'This plugin has not been updated in over 1 year'}
|
||||
else:
|
||||
return {'badge': 'STALE', 'class': 'freshness-badge-stale', 'title': 'This plugin has not been updated in over 2 years'}
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
return None
|
||||
|
||||
def _set_plugin_state(plugin_name, enabled):
|
||||
"""Set plugin enabled/disabled state"""
|
||||
state_file = _get_plugin_state_file(plugin_name)
|
||||
@@ -115,20 +142,27 @@ def installed(request):
|
||||
desc_elem = root.find('description')
|
||||
version_elem = root.find('version')
|
||||
|
||||
# Type field is optional (testPlugin doesn't have it)
|
||||
if name_elem is None or desc_elem is None or version_elem is None:
|
||||
errorPlugins.append({'name': plugin, 'error': 'Missing required metadata fields (name, description, or version)'})
|
||||
# All fields required including type (category) - no default
|
||||
if name_elem is None or type_elem is None or desc_elem is None or version_elem is None:
|
||||
errorPlugins.append({'name': plugin, 'error': 'Missing required metadata fields (name, type/category, description, or version)'})
|
||||
logging.writeToFile(f"Plugin {plugin}: Missing required metadata fields in meta.xml")
|
||||
continue
|
||||
|
||||
# Check if text is None (empty elements)
|
||||
if name_elem.text is None or desc_elem.text is None or version_elem.text is None:
|
||||
errorPlugins.append({'name': plugin, 'error': 'Empty metadata fields'})
|
||||
# Check if text is None or empty (all required)
|
||||
type_text = type_elem.text.strip() if type_elem.text else ''
|
||||
if name_elem.text is None or desc_elem.text is None or version_elem.text is None or not type_text:
|
||||
errorPlugins.append({'name': plugin, 'error': 'Empty metadata fields (name, type/category, description, or version required)'})
|
||||
logging.writeToFile(f"Plugin {plugin}: Empty metadata fields in meta.xml")
|
||||
continue
|
||||
|
||||
# Valid categories only: Utility, Security, Backup, Performance (Plugin category removed)
|
||||
if type_text.lower() not in ('utility', 'security', 'backup', 'performance', 'monitoring', 'integration', 'email', 'development', 'analytics'):
|
||||
errorPlugins.append({'name': plugin, 'error': f'Invalid category "{type_text}". Use: Utility, Security, Backup, or Performance.'})
|
||||
logging.writeToFile(f"Plugin {plugin}: Invalid category '{type_text}'")
|
||||
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['type'] = type_text
|
||||
data['desc'] = desc_elem.text
|
||||
data['version'] = version_elem.text
|
||||
data['plugin_dir'] = plugin # Plugin directory name
|
||||
@@ -158,6 +192,7 @@ def installed(request):
|
||||
modify_date = 'N/A'
|
||||
|
||||
data['modify_date'] = modify_date
|
||||
data['freshness_badge'] = _get_freshness_badge(modify_date)
|
||||
|
||||
# Extract settings URL or main URL for "Manage" button
|
||||
settings_url_elem = root.find('settings_url')
|
||||
@@ -252,20 +287,28 @@ def installed(request):
|
||||
pluginMetaData = ElementTree.parse(metaXmlPath)
|
||||
root = pluginMetaData.getroot()
|
||||
|
||||
# Validate required fields
|
||||
# Validate required fields (including type/category - no default)
|
||||
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:
|
||||
if name_elem is None or type_elem is None or desc_elem is None or version_elem is None:
|
||||
errorPlugins.append({'name': plugin, 'error': 'Missing required metadata (name, type/category, description, or version)'})
|
||||
continue
|
||||
|
||||
if name_elem.text is None or desc_elem.text is None or version_elem.text is None:
|
||||
type_text = type_elem.text.strip() if type_elem.text else ''
|
||||
if name_elem.text is None or desc_elem.text is None or version_elem.text is None or not type_text:
|
||||
errorPlugins.append({'name': plugin, 'error': 'Empty metadata (type/category required)'})
|
||||
continue
|
||||
|
||||
# Valid categories only: Utility, Security, Backup, Performance (Plugin category removed)
|
||||
if type_text.lower() not in ('utility', 'security', 'backup', 'performance', 'monitoring', 'integration', 'email', 'development', 'analytics'):
|
||||
errorPlugins.append({'name': plugin, 'error': f'Invalid category "{type_text}". Use: Utility, Security, Backup, or Performance.'})
|
||||
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['type'] = type_text
|
||||
data['desc'] = desc_elem.text
|
||||
data['version'] = version_elem.text
|
||||
data['plugin_dir'] = plugin
|
||||
@@ -287,6 +330,7 @@ def installed(request):
|
||||
modify_date = 'N/A'
|
||||
|
||||
data['modify_date'] = modify_date
|
||||
data['freshness_badge'] = _get_freshness_badge(modify_date)
|
||||
|
||||
# Extract settings URL or main URL
|
||||
settings_url_elem = root.find('settings_url')
|
||||
@@ -943,6 +987,7 @@ def _enrich_store_plugins(plugins):
|
||||
elif 'is_paid' not in plugin or plugin.get('is_paid') is None:
|
||||
# Try to check from local meta.xml if available
|
||||
meta_path = None
|
||||
source_path = os.path.join(plugin_source_dir, plugin_dir)
|
||||
if os.path.exists(installed_path):
|
||||
meta_path = os.path.join(installed_path, 'meta.xml')
|
||||
elif os.path.exists(source_path):
|
||||
@@ -1050,10 +1095,21 @@ def _fetch_plugins_from_github():
|
||||
patreon_url_elem = root.find('patreon_url')
|
||||
patreon_url = patreon_url_elem.text if patreon_url_elem is not None else 'https://www.patreon.com/c/newstargeted/membership'
|
||||
|
||||
# Category (type) is required - valid: Utility, Security, Backup, Performance (Plugin removed)
|
||||
type_elem = root.find('type')
|
||||
if type_elem is None or not type_elem.text or not type_elem.text.strip():
|
||||
logging.writeToFile(f"Plugin {plugin_name}: Missing required type/category in meta.xml, skipping")
|
||||
continue
|
||||
type_text = type_elem.text.strip().lower()
|
||||
if type_text not in ('utility', 'security', 'backup', 'performance', 'monitoring', 'integration', 'email', 'development', 'analytics'):
|
||||
logging.writeToFile(f"Plugin {plugin_name}: Invalid category '{type_elem.text}', skipping (use Utility, Security, Backup, or Performance)")
|
||||
continue
|
||||
|
||||
freshness = _get_freshness_badge(modify_date)
|
||||
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',
|
||||
'type': type_elem.text.strip(),
|
||||
'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}/',
|
||||
@@ -1062,6 +1118,7 @@ def _fetch_plugins_from_github():
|
||||
'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,
|
||||
'freshness_badge': freshness,
|
||||
'is_paid': is_paid,
|
||||
'patreon_tier': patreon_tier,
|
||||
'patreon_url': patreon_url
|
||||
@@ -1110,21 +1167,29 @@ def _fetch_plugins_from_github():
|
||||
@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:
|
||||
mailUtilities.checkHome()
|
||||
except Exception as e:
|
||||
logging.writeToFile(f"fetch_plugin_store: checkHome failed: {str(e)}")
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': 'Authentication required. Please log in again.',
|
||||
'plugins': []
|
||||
}, status=401)
|
||||
|
||||
try:
|
||||
# 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
|
||||
plugins = _fetch_plugins_from_github()
|
||||
|
||||
# Enrich plugins with installed/enabled status
|
||||
@@ -1139,7 +1204,7 @@ def fetch_plugin_store(request):
|
||||
'plugins': enriched_plugins,
|
||||
'cached': False
|
||||
})
|
||||
|
||||
|
||||
except Exception as e:
|
||||
error_message = str(e)
|
||||
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# CyberPanel phpMyAdmin Access Control Rollback Script
|
||||
# This script reverts the phpMyAdmin access control changes
|
||||
|
||||
echo "=== CyberPanel phpMyAdmin Access Control Rollback ==="
|
||||
|
||||
# Check if running as root
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
echo "Please run this script as root"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Find the most recent backup
|
||||
LATEST_BACKUP=$(ls -t /usr/local/CyberCP/public/phpmyadmin/index.php.backup.* 2>/dev/null | head -n1)
|
||||
|
||||
if [ -z "$LATEST_BACKUP" ]; then
|
||||
echo "No backup found. Cannot rollback changes."
|
||||
echo "You may need to reinstall phpMyAdmin or restore from your own backup."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Found backup: $LATEST_BACKUP"
|
||||
echo "Restoring original phpMyAdmin index.php..."
|
||||
|
||||
# Restore the original index.php
|
||||
cp "$LATEST_BACKUP" /usr/local/CyberCP/public/phpmyadmin/index.php
|
||||
|
||||
# Remove the .htaccess file if it exists
|
||||
if [ -f "/usr/local/CyberCP/public/phpmyadmin/.htaccess" ]; then
|
||||
echo "Removing .htaccess file..."
|
||||
rm /usr/local/CyberCP/public/phpmyadmin/.htaccess
|
||||
fi
|
||||
|
||||
# Set proper permissions
|
||||
echo "Setting permissions..."
|
||||
chown lscpd:lscpd /usr/local/CyberCP/public/phpmyadmin/index.php
|
||||
chmod 644 /usr/local/CyberCP/public/phpmyadmin/index.php
|
||||
|
||||
# Restart LiteSpeed to ensure changes take effect
|
||||
echo "Restarting LiteSpeed..."
|
||||
systemctl restart lscpd
|
||||
|
||||
echo "=== Rollback Complete ==="
|
||||
echo ""
|
||||
echo "phpMyAdmin access control has been reverted!"
|
||||
echo "phpMyAdmin should now work as it did before the changes."
|
||||
echo ""
|
||||
echo "Backup file used: $LATEST_BACKUP"
|
||||
114
simple_install.sh
Normal file
114
simple_install.sh
Normal file
@@ -0,0 +1,114 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Simplified CyberPanel Installation Script
|
||||
# Based on 2.4.4 approach with AlmaLinux 9 fixes
|
||||
|
||||
OUTPUT=$(cat /etc/*release)
|
||||
|
||||
# Detect OS and set appropriate variables
|
||||
if echo $OUTPUT | grep -q "AlmaLinux 9" ; then
|
||||
echo -e "\nDetecting AlmaLinux 9...\n"
|
||||
SERVER_OS="AlmaLinux9"
|
||||
PKG_MGR="dnf"
|
||||
elif echo $OUTPUT | grep -q "AlmaLinux 8" ; then
|
||||
echo -e "\nDetecting AlmaLinux 8...\n"
|
||||
SERVER_OS="AlmaLinux8"
|
||||
PKG_MGR="yum"
|
||||
elif echo $OUTPUT | grep -q "Ubuntu 22.04" ; then
|
||||
echo -e "\nDetecting Ubuntu 22.04...\n"
|
||||
SERVER_OS="Ubuntu2204"
|
||||
PKG_MGR="apt"
|
||||
elif echo $OUTPUT | grep -q "Ubuntu 20.04" ; then
|
||||
echo -e "\nDetecting Ubuntu 20.04...\n"
|
||||
SERVER_OS="Ubuntu2004"
|
||||
PKG_MGR="apt"
|
||||
elif echo $OUTPUT | grep -q "CentOS Linux 8" ; then
|
||||
echo -e "\nDetecting CentOS 8...\n"
|
||||
SERVER_OS="CentOS8"
|
||||
PKG_MGR="yum"
|
||||
else
|
||||
echo -e "\nUnsupported OS detected. This script supports:\n"
|
||||
echo -e "AlmaLinux: 8, 9\n"
|
||||
echo -e "Ubuntu: 20.04, 22.04\n"
|
||||
echo -e "CentOS: 8\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Installing basic dependencies..."
|
||||
|
||||
# Install basic packages
|
||||
if [ "$PKG_MGR" = "dnf" ]; then
|
||||
dnf update -y
|
||||
dnf install -y epel-release
|
||||
dnf install -y wget curl unzip zip rsync firewalld git python3 python3-pip
|
||||
dnf install -y mariadb-server mariadb-client
|
||||
dnf install -y ImageMagick gd libicu oniguruma aspell libc-client
|
||||
elif [ "$PKG_MGR" = "yum" ]; then
|
||||
yum update -y
|
||||
yum install -y epel-release
|
||||
yum install -y wget curl unzip zip rsync firewalld git python3 python3-pip
|
||||
yum install -y mariadb-server mariadb-client
|
||||
yum install -y ImageMagick gd libicu oniguruma aspell libc-client
|
||||
elif [ "$PKG_MGR" = "apt" ]; then
|
||||
apt update -y
|
||||
apt install -y wget curl unzip zip rsync git python3 python3-pip
|
||||
apt install -y mariadb-server mariadb-client
|
||||
apt install -y imagemagick php-gd php-intl php-mbstring php-pspell
|
||||
fi
|
||||
|
||||
# Start and enable MariaDB
|
||||
echo "Starting MariaDB..."
|
||||
systemctl enable mariadb
|
||||
systemctl start mariadb
|
||||
|
||||
# Create MySQL password file
|
||||
echo "Setting up MySQL..."
|
||||
mkdir -p /etc/cyberpanel
|
||||
echo "cyberpanel123" > /etc/cyberpanel/mysqlPassword
|
||||
chmod 600 /etc/cyberpanel/mysqlPassword
|
||||
|
||||
# Secure MySQL installation
|
||||
mysql -u root -e "ALTER USER 'root'@'localhost' IDENTIFIED BY 'cyberpanel123';" 2>/dev/null || true
|
||||
mysql -u root -pcyberpanel123 -e "DELETE FROM mysql.user WHERE User='';" 2>/dev/null || true
|
||||
mysql -u root -pcyberpanel123 -e "DELETE FROM mysql.user WHERE User='root' AND Host NOT IN ('localhost', '127.0.0.1', '::1');" 2>/dev/null || true
|
||||
mysql -u root -pcyberpanel123 -e "DROP DATABASE IF EXISTS test;" 2>/dev/null || true
|
||||
mysql -u root -pcyberpanel123 -e "DELETE FROM mysql.db WHERE Db='test' OR Db='test\\_%';" 2>/dev/null || true
|
||||
mysql -u root -pcyberpanel123 -e "FLUSH PRIVILEGES;" 2>/dev/null || true
|
||||
|
||||
# Configure firewall
|
||||
echo "Configuring firewall..."
|
||||
if [ "$PKG_MGR" = "dnf" ] || [ "$PKG_MGR" = "yum" ]; then
|
||||
systemctl enable firewalld
|
||||
systemctl start firewalld
|
||||
firewall-cmd --permanent --add-port=8090/tcp
|
||||
firewall-cmd --permanent --add-port=7080/tcp
|
||||
firewall-cmd --permanent --add-port=80/tcp
|
||||
firewall-cmd --permanent --add-port=443/tcp
|
||||
firewall-cmd --permanent --add-port=21/tcp
|
||||
firewall-cmd --permanent --add-port=25/tcp
|
||||
firewall-cmd --permanent --add-port=587/tcp
|
||||
firewall-cmd --permanent --add-port=465/tcp
|
||||
firewall-cmd --permanent --add-port=110/tcp
|
||||
firewall-cmd --permanent --add-port=143/tcp
|
||||
firewall-cmd --permanent --add-port=993/tcp
|
||||
firewall-cmd --permanent --add-port=995/tcp
|
||||
firewall-cmd --permanent --add-port=53/tcp
|
||||
firewall-cmd --permanent --add-port=53/udp
|
||||
firewall-cmd --reload
|
||||
fi
|
||||
|
||||
# Download and install CyberPanel
|
||||
echo "Downloading CyberPanel..."
|
||||
rm -f cyberpanel.sh
|
||||
curl --silent -o cyberpanel.sh "https://cyberpanel.sh/?dl&$SERVER_OS" 2>/dev/null
|
||||
|
||||
if [ -f "cyberpanel.sh" ]; then
|
||||
echo "Installing CyberPanel..."
|
||||
chmod +x cyberpanel.sh
|
||||
./cyberpanel.sh
|
||||
else
|
||||
echo "Failed to download CyberPanel installer!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Installation completed!"
|
||||
88
to-do/MARIADB_INSTALLATION_FIXES.md
Normal file
88
to-do/MARIADB_INSTALLATION_FIXES.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# MariaDB Installation Fixes
|
||||
|
||||
## Issues Fixed
|
||||
|
||||
### 1. MariaDB-server-compat Package Conflict
|
||||
**Problem**: `MariaDB-server-compat-12.1.2-1.el9.noarch` was conflicting with MariaDB 10.11 installation, causing transaction test errors.
|
||||
|
||||
**Solution**:
|
||||
- Enhanced compat package removal with multiple aggressive removal attempts
|
||||
- Added `--allowerasing` flag to dnf remove commands
|
||||
- Added dnf exclude configuration to prevent compat package reinstallation
|
||||
- Verification step to ensure all compat packages are removed before installation
|
||||
|
||||
**Files Modified**:
|
||||
- `cyberpanel-repo/plogical/upgrade.py` - `fix_almalinux9_mariadb()` function
|
||||
- `cyberpanel-repo/install/install.py` - `installMySQL()` function
|
||||
|
||||
### 2. MySQL Command Not Found Error
|
||||
**Problem**: After MariaDB installation failed, the `changeMYSQLRootPassword()` function tried to use the `mysql` command which didn't exist, causing `FileNotFoundError`.
|
||||
|
||||
**Solution**:
|
||||
- Added verification that MariaDB binaries exist before attempting password change
|
||||
- Added check for mysql/mariadb command availability
|
||||
- Added MariaDB service status verification before password change
|
||||
- Added wait time for MariaDB to be ready after service start
|
||||
|
||||
**Files Modified**:
|
||||
- `cyberpanel-repo/install/install.py` - `changeMYSQLRootPassword()` function
|
||||
- `cyberpanel-repo/install/install.py` - `installMySQL()` function
|
||||
|
||||
### 3. MariaDB Installation Verification
|
||||
**Problem**: Installation was proceeding even when MariaDB wasn't actually installed successfully.
|
||||
|
||||
**Solution**:
|
||||
- Added binary existence check after installation
|
||||
- Added service status verification
|
||||
- Added proper error handling and return values
|
||||
- Installation now fails gracefully if MariaDB wasn't installed
|
||||
|
||||
**Files Modified**:
|
||||
- `cyberpanel-repo/plogical/upgrade.py` - `fix_almalinux9_mariadb()` function
|
||||
- `cyberpanel-repo/install/install.py` - `installMySQL()` function
|
||||
|
||||
## Changes Made
|
||||
|
||||
### upgrade.py
|
||||
1. **Enhanced compat package removal**:
|
||||
- Multiple removal attempts (dnf remove, rpm -e, individual package removal)
|
||||
- Added `--allowerasing` flag
|
||||
- Added dnf exclude configuration
|
||||
- Verification step
|
||||
|
||||
2. **Improved MariaDB installation**:
|
||||
- Added `--exclude='MariaDB-server-compat*'` to dnf install command
|
||||
- Added fallback with `--allowerasing` if conflicts occur
|
||||
- Added binary existence verification after installation
|
||||
- Proper error handling and return values
|
||||
|
||||
### install.py
|
||||
1. **Enhanced compat package removal** (same as upgrade.py)
|
||||
|
||||
2. **Improved installation verification**:
|
||||
- Check for MariaDB binaries after installation
|
||||
- Verify service is running before password change
|
||||
- Added wait time for service to be ready
|
||||
- Proper error handling
|
||||
|
||||
3. **Improved password change function**:
|
||||
- Verify mysql/mariadb command exists before attempting password change
|
||||
- Better error messages
|
||||
- Graceful failure handling
|
||||
|
||||
## Testing Recommendations
|
||||
|
||||
1. Test on clean AlmaLinux 9 system
|
||||
2. Test with existing MariaDB-server-compat package installed
|
||||
3. Test with MariaDB 10.x already installed
|
||||
4. Test with MariaDB 12.x already installed
|
||||
5. Verify MariaDB service starts correctly
|
||||
6. Verify mysql/mariadb commands are available
|
||||
7. Verify password change succeeds
|
||||
|
||||
## Notes
|
||||
|
||||
- The fixes maintain backward compatibility
|
||||
- All changes include proper error handling
|
||||
- Installation now fails gracefully with clear error messages
|
||||
- Compat package removal is more aggressive to handle edge cases
|
||||
132
to-do/OLD-REPO-CHECKLIST-BEFORE-REMOVAL.md
Normal file
132
to-do/OLD-REPO-CHECKLIST-BEFORE-REMOVAL.md
Normal file
@@ -0,0 +1,132 @@
|
||||
# What Was in the Old cyberpanel-fix Repo – Pre-Removal Checklist
|
||||
|
||||
Before removing `/home/cyberpanel-fix-backup-20260202`, verify the merged repo has everything you need.
|
||||
|
||||
---
|
||||
|
||||
## 1. Files ONLY in cyberpanel-repo (not in old fix) ✅
|
||||
|
||||
These are in the merged repo and were not in the old fix:
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `commit_and_push.sh`, `commit_changes.py`, `push_fix.py`, `push_fix.sh` | Dev/utility scripts |
|
||||
| `fix_todo_git.py`, `remove_todo.py`, `remove_todo_from_git.sh` | Git helpers |
|
||||
| `olves issue -1654: Hostname SSL setup...` | Patch file (typo in filename) |
|
||||
| `pluginHolder/patreon_verifier.py.bak`, `plugin_access.py.bak` | Backups |
|
||||
| `pluginHolder/templates/pluginHolder/plugins.html.backup` | Template backup |
|
||||
| `static/userManagment/modifyUser.html` | UI change |
|
||||
| `to-do/PLUGIN-DEFAULT-REMOVAL-2026-02-01.md` | Notes |
|
||||
| `to-do/REPO-MERGE-2026-02-02.md` | Merge notes |
|
||||
|
||||
**Action:** None. These are already in the merged repo.
|
||||
|
||||
---
|
||||
|
||||
## 2. Files COPIED from old fix into repo ✅
|
||||
|
||||
These were only in the old fix and were copied into repo during the merge:
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `cyberpanel_clean.sh` | Clean install script |
|
||||
| `cyberpanel_complete.sh` | Complete install script |
|
||||
| `cyberpanel_simple.sh` | Simple install script |
|
||||
| `cyberpanel_standalone.sh` | Standalone install script |
|
||||
| `fix_installation_issues.sh` | Installation fixes |
|
||||
| `install_phpmyadmin.sh` | phpMyAdmin installer |
|
||||
| `simple_install.sh` | Simple installer |
|
||||
| `INSTALLER_SUMMARY.md` | Installer docs |
|
||||
| `UNIVERSAL_OS_COMPATIBILITY.md` | OS compatibility docs |
|
||||
| `to-do/MARIADB_INSTALLATION_FIXES.md` | MariaDB fixes |
|
||||
|
||||
**Action:** Confirm these exist in `/home/cyberpanel-repo/`.
|
||||
|
||||
---
|
||||
|
||||
## 3. Files that DIFFER – repo is the intended version
|
||||
|
||||
The merged repo keeps the **cyberpanel-repo** versions. Old fix had older or different logic.
|
||||
|
||||
### CyberCP/settings.py
|
||||
- **Repo:** `emailMarketing` is commented out (install via Plugin Store)
|
||||
- **Old fix:** `emailMarketing` was in `INSTALLED_APPS`
|
||||
|
||||
**Check:** Plugin Store for emailMarketing works; no need for it in core install.
|
||||
|
||||
### CyberCP/urls.py
|
||||
- **Repo:** `path('emailMarketing/', ...)` is commented out
|
||||
- **Old fix:** `path('emailMarketing/', ...)` was active
|
||||
|
||||
**Check:** Same as above; emailMarketing via Plugin Store.
|
||||
|
||||
### plogical/mailUtilities.py
|
||||
- **Repo:** DNS fallback logic – falls back to **local DNS** when external API fails
|
||||
- **Old fix:** Returns empty `[]` when external API fails; no local fallback
|
||||
|
||||
**Check:** Hostname SSL / rDNS works when cyberpanel.net API is down or unreachable.
|
||||
|
||||
### emailMarketing/meta.xml
|
||||
- **Repo:** version `1.0.1`, category `Email`
|
||||
- **Old fix:** version `1.0.0`
|
||||
|
||||
### examplePlugin/meta.xml
|
||||
- **Repo:** version `1.0.1`, category `Utility`
|
||||
- **Old fix:** version `1.0.0`
|
||||
|
||||
**Check:** Plugin Store shows correct versions and categories.
|
||||
|
||||
---
|
||||
|
||||
## 4. PluginHolder / Plugin Store (in repo)
|
||||
|
||||
The merged repo has:
|
||||
|
||||
- Collapsible help sections
|
||||
- Freshness badges (NEW/Stable/Unstable/STALE)
|
||||
- Activate All / Deactivate All
|
||||
- Updated categories and premium docs
|
||||
- Version 2.1.0 in the help footer
|
||||
|
||||
**Check:** `/plugins/help/` and `/plugins/installed` behave as expected.
|
||||
|
||||
---
|
||||
|
||||
## 5. Quick verification commands
|
||||
|
||||
```bash
|
||||
# Copied files exist
|
||||
ls -la /home/cyberpanel-repo/cyberpanel_clean.sh \
|
||||
/home/cyberpanel-repo/fix_installation_issues.sh \
|
||||
/home/cyberpanel-repo/install_phpmyadmin.sh
|
||||
|
||||
# Symlink works
|
||||
ls -la /home/cyberpanel-fix
|
||||
# Should show: cyberpanel-fix -> cyberpanel-repo
|
||||
|
||||
# Live deployment
|
||||
ls -la /usr/local/CyberCP/pluginHolder/templates/pluginHolder/help.html
|
||||
# Should have collapsible sections and version 2.1.0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Safe to remove when
|
||||
|
||||
- [ ] Plugin Store loads and filters work
|
||||
- [ ] Plugin Development Guide (help) shows collapsible sections and 2.1.0
|
||||
- [ ] Hostname SSL / rDNS works (or you accept no local DNS fallback)
|
||||
- [ ] emailMarketing is installed via Plugin Store, not core (if used)
|
||||
- [ ] Install scripts (`cyberpanel_clean.sh`, etc.) are present and used as needed
|
||||
|
||||
---
|
||||
|
||||
## Remove backup
|
||||
|
||||
```bash
|
||||
rm -rf /home/cyberpanel-fix-backup-20260202
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Created:** 2026-02-02
|
||||
13
to-do/PLUGIN-DEFAULT-REMOVAL-2026-02-01.md
Normal file
13
to-do/PLUGIN-DEFAULT-REMOVAL-2026-02-01.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Plugin Default Removal - 2026-02-01
|
||||
|
||||
## Summary
|
||||
CyberPanel repository no longer requires any plugins by default. Plugins are installed by users from the [Plugin Store](https://github.com/master3395/cyberpanel-plugins) via the CyberPanel Plugin Manager.
|
||||
|
||||
## Changes
|
||||
- **settings.py**: Removed `emailMarketing` from `INSTALLED_APPS`
|
||||
- **urls.py**: Commented out `emailMarketing` route (plugin installer adds it when plugin is installed)
|
||||
|
||||
## Plugin Installation
|
||||
Users install plugins from: https://github.com/master3395/cyberpanel-plugins
|
||||
|
||||
The plugin installer adds apps to `INSTALLED_APPS` and URL routes when plugins are installed via the Plugin Store UI.
|
||||
38
to-do/REPO-MERGE-2026-02-02.md
Normal file
38
to-do/REPO-MERGE-2026-02-02.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# CyberPanel Repo Merge – 2026-02-02
|
||||
|
||||
## Summary
|
||||
|
||||
`cyberpanel-repo` and `cyberpanel-fix` have been merged into a single working directory.
|
||||
|
||||
## What Was Done
|
||||
|
||||
1. **Unique files copied from cyberpanel-fix into cyberpanel-repo:**
|
||||
- `cyberpanel_clean.sh`
|
||||
- `cyberpanel_complete.sh`
|
||||
- `cyberpanel_simple.sh`
|
||||
- `cyberpanel_standalone.sh`
|
||||
- `fix_installation_issues.sh`
|
||||
- `install_phpmyadmin.sh`
|
||||
- `simple_install.sh`
|
||||
- `INSTALLER_SUMMARY.md`
|
||||
- `UNIVERSAL_OS_COMPATIBILITY.md`
|
||||
- `to-do/MARIADB_INSTALLATION_FIXES.md`
|
||||
|
||||
2. **cyberpanel-fix backup:** Renamed to `cyberpanel-fix-backup-20260202`
|
||||
|
||||
3. **Symlink created:** `cyberpanel-fix` → `cyberpanel-repo`
|
||||
- Paths like `/home/cyberpanel-fix/` now resolve to `/home/cyberpanel-repo/`
|
||||
|
||||
## Single Source of Truth
|
||||
|
||||
Use **`/home/cyberpanel-repo`** (or `/home/cyberpanel-fix` via symlink) for all CyberPanel development and deployment.
|
||||
|
||||
## Backup Location
|
||||
|
||||
The previous cyberpanel-fix tree is preserved at:
|
||||
`/home/cyberpanel-fix-backup-20260202`
|
||||
|
||||
You can remove it after confirming everything works:
|
||||
```bash
|
||||
rm -rf /home/cyberpanel-fix-backup-20260202
|
||||
```
|
||||
Reference in New Issue
Block a user