Merge pull request #1669 from master3395/v2.5.5-dev

V2.5.5 dev
This commit is contained in:
Master3395
2026-02-02 02:57:19 +01:00
committed by GitHub
15 changed files with 1439 additions and 272 deletions

View File

@@ -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

View File

@@ -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')),
]

View File

@@ -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):

View File

@@ -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"

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>
&lt;settings_url&gt;/plugins/myFirstPlugin/settings/&lt;/settings_url&gt;
&lt;/cyberpanelPluginConfig&gt;</code></pre>
<div class="alert alert-warning">
<strong>{% trans "Required: Category (type)" %}:</strong> {% trans "The &lt;type&gt; 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>&lt;version&gt;1.0.0&lt;/version&gt;</code></pre>
<p>{% trans "Never use formats like '1.0' or 'v1.0'. Always use the full semantic version: '1.0.0'" %}</p>
</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 &lt;type&gt; 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 "12 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>&lt;paid&gt;true&lt;/paid&gt;
&lt;patreon_tier&gt;Your Tier Name&lt;/patreon_tier&gt;
&lt;patreon_url&gt;https://www.patreon.com/your-page&lt;/patreon_url&gt;</code></pre>
<p>{% trans "Set &lt;paid&gt;true&lt;/paid&gt; 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 PluginAPI 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' });
}
});
});

View File

@@ -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 %}

View File

@@ -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)

View File

@@ -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
View 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!"

View 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

View 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

View 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.

View 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
```