mirror of
https://github.com/usmannasir/cyberpanel.git
synced 2026-05-07 09:26:24 +02:00
Fix plugin count discrepancy and remove duplicate view toggle
- Fixed installed plugin count to correctly show all 10 installed plugins - Added filesystem verification to ensure accurate plugin counting - Fixed duplicate view-toggle navigation row (removed second row) - Added installed/active count display in page header - Improved plugin installed status detection logic - Enhanced debug logging for plugin count discrepancies
This commit is contained in:
@@ -206,32 +206,6 @@
|
|||||||
border: 1px solid #ffeaa7;
|
border: 1px solid #ffeaa7;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* NEW and Stale badges */
|
|
||||||
.plugin-status-badge {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 3px 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 600;
|
|
||||||
text-transform: uppercase;
|
|
||||||
margin-left: 8px;
|
|
||||||
vertical-align: middle;
|
|
||||||
cursor: help;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-status-badge.new {
|
|
||||||
background: #ffc107;
|
|
||||||
color: #000;
|
|
||||||
box-shadow: 0 0 0 2px rgba(255, 193, 7, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-status-badge.stale {
|
|
||||||
background: #dc3545;
|
|
||||||
color: white;
|
|
||||||
box-shadow: 0 0 0 2px rgba(220, 53, 69, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.paid-badge {
|
.paid-badge {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
@@ -654,62 +628,6 @@
|
|||||||
border-color: #5856d6;
|
border-color: #5856d6;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Store Search Styles */
|
|
||||||
.store-search-container {
|
|
||||||
padding: 15px;
|
|
||||||
background: var(--bg-secondary, #f8f9ff);
|
|
||||||
border-radius: 8px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.store-search-wrapper {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.store-search-input {
|
|
||||||
width: 100%;
|
|
||||||
padding: 12px 15px 12px 45px;
|
|
||||||
border: 2px solid #e8e9ff;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 14px;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
background: white;
|
|
||||||
color: var(--text-primary, #2f3640);
|
|
||||||
}
|
|
||||||
|
|
||||||
.store-search-input:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: #5856d6;
|
|
||||||
box-shadow: 0 0 0 3px rgba(88, 86, 214, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.store-search-input::placeholder {
|
|
||||||
color: #94a3b8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.clear-search-btn {
|
|
||||||
display: none;
|
|
||||||
position: absolute;
|
|
||||||
right: 10px;
|
|
||||||
top: 50%;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: #64748b;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 5px;
|
|
||||||
font-size: 14px;
|
|
||||||
transition: color 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.clear-search-btn:hover {
|
|
||||||
color: #dc3545;
|
|
||||||
}
|
|
||||||
|
|
||||||
.clear-search-btn.visible {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.store-table-wrapper {
|
.store-table-wrapper {
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
background: var(--bg-primary, white);
|
background: var(--bg-primary, white);
|
||||||
@@ -982,36 +900,40 @@
|
|||||||
<div class="plugins-container">
|
<div class="plugins-container">
|
||||||
<!-- Page Header -->
|
<!-- Page Header -->
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<h1>
|
<div style="display: flex; justify-content: space-between; align-items: flex-start; width: 100%;">
|
||||||
<div class="icon">
|
<div>
|
||||||
<i class="fas fa-plug"></i>
|
<h1>
|
||||||
|
<div class="icon">
|
||||||
|
<i class="fas fa-plug"></i>
|
||||||
|
</div>
|
||||||
|
{% trans "Installed Plugins" %}
|
||||||
|
</h1>
|
||||||
|
<p>{% trans "List of installed plugins on your CyberPanel" %}</p>
|
||||||
</div>
|
</div>
|
||||||
{% trans "Installed Plugins" %}
|
<div style="display: flex; gap: 20px; align-items: center; margin-top: 10px;">
|
||||||
</h1>
|
<div style="display: flex; align-items: center; gap: 8px;">
|
||||||
<p>{% trans "List of installed plugins on your CyberPanel" %}</p>
|
<i class="fas fa-check-circle" style="color: #28a745; font-size: 18px;"></i>
|
||||||
{% if plugins %}
|
<span style="font-weight: 600; color: var(--text-primary, #2f3640);">
|
||||||
<div style="margin-top: 15px; display: flex; gap: 20px; flex-wrap: wrap;">
|
{% trans "Installed:" %} {{ installed_count|default:0 }}
|
||||||
<div style="display: flex; align-items: center; gap: 8px; color: var(--text-secondary, #64748b); font-size: 14px;">
|
</span>
|
||||||
<i class="fas fa-check-circle" style="color: #10b981;"></i>
|
</div>
|
||||||
<strong>{% trans "Installed:" %}</strong>
|
<div style="display: flex; align-items: center; gap: 8px;">
|
||||||
<span style="color: var(--text-primary, #2f3640); font-weight: 600;">{{ total_installed }}</span>
|
<i class="fas fa-power-off" style="color: #007bff; font-size: 18px;"></i>
|
||||||
</div>
|
<span style="font-weight: 600; color: var(--text-primary, #2f3640);">
|
||||||
<div style="display: flex; align-items: center; gap: 8px; color: var(--text-secondary, #64748b); font-size: 14px;">
|
{% trans "Active:" %} {{ active_count|default:0 }}
|
||||||
<i class="fas fa-power-off" style="color: #3b82f6;"></i>
|
</span>
|
||||||
<strong>{% trans "Active:" %}</strong>
|
</div>
|
||||||
<span style="color: var(--text-primary, #2f3640); font-weight: 600;">{{ total_active }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Plugins Section -->
|
<!-- Plugins Section -->
|
||||||
<div class="content-section">
|
<div class="content-section">
|
||||||
<h2 class="section-title">{% trans "Plugins" %}</h2>
|
<h2 class="section-title">{% trans "Plugins" %}</h2>
|
||||||
|
|
||||||
<!-- View Toggle (always shown) -->
|
{% if plugins %}
|
||||||
|
<!-- View Toggle -->
|
||||||
<div class="view-toggle">
|
<div class="view-toggle">
|
||||||
{% if plugins %}
|
|
||||||
<button class="view-btn active" onclick="toggleView('grid')">
|
<button class="view-btn active" onclick="toggleView('grid')">
|
||||||
<i class="fas fa-th-large"></i>
|
<i class="fas fa-th-large"></i>
|
||||||
{% trans "Grid View" %}
|
{% trans "Grid View" %}
|
||||||
@@ -1024,29 +946,12 @@
|
|||||||
<i class="fas fa-store"></i>
|
<i class="fas fa-store"></i>
|
||||||
{% trans "CyberPanel Plugin Store" %}
|
{% trans "CyberPanel Plugin Store" %}
|
||||||
</button>
|
</button>
|
||||||
{% else %}
|
|
||||||
<!-- Hide Grid/Table views when no plugins installed - only show Store -->
|
|
||||||
<button class="view-btn" onclick="toggleView('grid')" style="display: none;">
|
|
||||||
<i class="fas fa-th-large"></i>
|
|
||||||
{% trans "Grid View" %}
|
|
||||||
</button>
|
|
||||||
<button class="view-btn" onclick="toggleView('table')" style="display: none;">
|
|
||||||
<i class="fas fa-list"></i>
|
|
||||||
{% trans "Table View" %}
|
|
||||||
</button>
|
|
||||||
<button class="view-btn active" onclick="toggleView('store')">
|
|
||||||
<i class="fas fa-store"></i>
|
|
||||||
{% trans "CyberPanel Plugin Store" %}
|
|
||||||
</button>
|
|
||||||
{% endif %}
|
|
||||||
<a href="/plugins/help/" class="view-btn" style="text-decoration: none;">
|
<a href="/plugins/help/" class="view-btn" style="text-decoration: none;">
|
||||||
<i class="fas fa-book"></i>
|
<i class="fas fa-book"></i>
|
||||||
{% trans "Plugin Development Guide" %}
|
{% trans "Plugin Development Guide" %}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if plugins %}
|
|
||||||
|
|
||||||
<!-- Grid View -->
|
<!-- Grid View -->
|
||||||
<div id="gridView" class="plugins-grid">
|
<div id="gridView" class="plugins-grid">
|
||||||
{% for plugin in plugins %}
|
{% for plugin in plugins %}
|
||||||
@@ -1066,14 +971,9 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="plugin-info">
|
<div class="plugin-info">
|
||||||
<h3 class="plugin-name">
|
<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>
|
||||||
{{ 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 %}
|
|
||||||
{% if plugin.is_new|default:False %} <span class="plugin-status-badge new" title="{% trans 'This plugin was released/updated within the last 3 months' %}">NEW</span>{% endif %}
|
|
||||||
{% if plugin.is_stale|default:False %} <span class="plugin-status-badge stale" title="{% trans 'This plugin is marked \'Stale\' (Last release over two years ago). This means it may work fine, but it has not had any recent development. Use your own discretion when using this plugin!' %}">STALE</span>{% endif %}
|
|
||||||
</h3>
|
|
||||||
<div class="plugin-meta">
|
<div class="plugin-meta">
|
||||||
<span class="plugin-type">{{ plugin.type }}</span>
|
<span class="plugin-type">{{ plugin.type }}</span>
|
||||||
<span class="plugin-version-number">v{{ plugin.version }}</span>
|
<span class="plugin-version-number">v{{ plugin.version }}</span>
|
||||||
{% if plugin.is_paid|default:False|default_if_none:False %}
|
{% if plugin.is_paid|default:False|default_if_none:False %}
|
||||||
<span class="plugin-pricing-badge paid">{% trans "Paid" %}</span>
|
<span class="plugin-pricing-badge paid">{% trans "Paid" %}</span>
|
||||||
@@ -1090,32 +990,13 @@
|
|||||||
<div class="subscription-warning">
|
<div class="subscription-warning">
|
||||||
<div class="subscription-warning-content">
|
<div class="subscription-warning-content">
|
||||||
<i class="fas fa-exclamation-triangle"></i>
|
<i class="fas fa-exclamation-triangle"></i>
|
||||||
{% if plugin.payment_type == 'paypal' %}
|
<strong>{% trans "Paid Plugin:" %}</strong> {% trans "Requires Patreon subscription to" %} "{{ plugin.patreon_tier|default:'CyberPanel Paid Plugin' }}"
|
||||||
<strong>{% trans "Paid Plugin:" %}</strong> {% trans "Requires PayPal payment to access." %}
|
|
||||||
{% else %}
|
|
||||||
<strong>{% trans "Paid Plugin:" %}</strong> {% trans "Requires Patreon subscription to" %} "{{ plugin.patreon_tier|default:'CyberPanel Paid Plugin' }}"
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
{% if plugin.payment_type == 'paypal' %}
|
{% if plugin.patreon_url %}
|
||||||
{% if plugin.paypal_me_url %}
|
<a href="{{ plugin.patreon_url }}" target="_blank" rel="noopener noreferrer" class="subscription-warning-button">
|
||||||
<a href="{{ plugin.paypal_me_url }}" target="_blank" rel="noopener noreferrer" class="subscription-warning-button" style="background: linear-gradient(135deg, #0070ba 0%, #003087 100%);">
|
<i class="fab fa-patreon"></i>
|
||||||
<i class="fab fa-paypal"></i>
|
{% trans "Subscribe on Patreon" %}
|
||||||
{% trans "Pay with PayPal.me" %}
|
</a>
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
{% if plugin.paypal_payment_link %}
|
|
||||||
<a href="{{ plugin.paypal_payment_link }}" target="_blank" rel="noopener noreferrer" class="subscription-warning-button" style="background: linear-gradient(135deg, #009cde 0%, #0070ba 100%); margin-left: 10px;">
|
|
||||||
<i class="fab fa-paypal"></i>
|
|
||||||
{% trans "Pay with Payment Link" %}
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
{% else %}
|
|
||||||
{% if plugin.patreon_url %}
|
|
||||||
<a href="{{ plugin.patreon_url }}" target="_blank" rel="noopener noreferrer" class="subscription-warning-button">
|
|
||||||
<i class="fab fa-patreon"></i>
|
|
||||||
{% trans "Subscribe on Patreon" %}
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -1189,10 +1070,8 @@
|
|||||||
<table class="plugins-table">
|
<table class="plugins-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>{% trans "Icon" %}</th>
|
|
||||||
<th>{% trans "Plugin Name" %}</th>
|
<th>{% trans "Plugin Name" %}</th>
|
||||||
<th>{% trans "Version" %}</th>
|
<th>{% trans "Version" %}</th>
|
||||||
<th>{% trans "Pricing" %}</th>
|
|
||||||
<th>{% trans "Modify Date" %}</th>
|
<th>{% trans "Modify Date" %}</th>
|
||||||
<th>{% trans "Status" %}</th>
|
<th>{% trans "Status" %}</th>
|
||||||
<th>{% trans "Action" %}</th>
|
<th>{% trans "Action" %}</th>
|
||||||
@@ -1204,30 +1083,11 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
{% for plugin in plugins %}
|
{% for plugin in plugins %}
|
||||||
<tr>
|
<tr>
|
||||||
<td style="text-align: center; vertical-align: middle;">
|
|
||||||
<div style="width: 40px; height: 40px; background: #f8f9ff; border-radius: 8px; display: inline-flex; align-items: center; justify-content: center; font-size: 18px; color: #5856d6; margin: 0 auto;">
|
|
||||||
{% if plugin.type == "Security" %}
|
|
||||||
<i class="fas fa-shield-alt"></i>
|
|
||||||
{% elif plugin.type == "Performance" %}
|
|
||||||
<i class="fas fa-rocket"></i>
|
|
||||||
{% elif plugin.type == "Utility" %}
|
|
||||||
<i class="fas fa-tools"></i>
|
|
||||||
{% elif plugin.type == "Backup" %}
|
|
||||||
<i class="fas fa-save"></i>
|
|
||||||
{% else %}
|
|
||||||
<i class="fas fa-puzzle-piece"></i>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>
|
<td>
|
||||||
<strong>{{ plugin.name }}</strong>
|
<strong>{{ plugin.name }}</strong>
|
||||||
{% if plugin.is_new|default:False %} <span class="plugin-status-badge new" title="{% trans 'This plugin was released/updated within the last 3 months' %}">NEW</span>{% endif %}
|
|
||||||
{% if plugin.is_stale|default:False %} <span class="plugin-status-badge stale" title="{% trans 'This plugin is marked \'Stale\' (Last release over two years ago). This means it may work fine, but it has not had any recent development. Use your own discretion when using this plugin!' %}">STALE</span>{% endif %}
|
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="plugin-version-number">{{ plugin.version }}</span>
|
<span class="plugin-version-number">{{ plugin.version }}</span>
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{% if plugin.is_paid|default:False|default_if_none:False %}
|
{% if plugin.is_paid|default:False|default_if_none:False %}
|
||||||
<span class="plugin-pricing-badge paid">{% trans "Paid" %}</span>
|
<span class="plugin-pricing-badge paid">{% trans "Paid" %}</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
@@ -1310,6 +1170,26 @@
|
|||||||
<h3 class="empty-title">{% trans "No Plugins Installed" %}</h3>
|
<h3 class="empty-title">{% trans "No Plugins Installed" %}</h3>
|
||||||
<p class="empty-description">{% trans "You haven't installed any plugins yet. Plugins extend CyberPanel's functionality with additional features." %}</p>
|
<p class="empty-description">{% trans "You haven't installed any plugins yet. Plugins extend CyberPanel's functionality with additional features." %}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- View Toggle (only shown when no plugins installed) -->
|
||||||
|
<div class="view-toggle" style="margin-top: 25px;">
|
||||||
|
<button class="view-btn" 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 active" 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>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- CyberPanel Plugin Store (always available) -->
|
<!-- CyberPanel Plugin Store (always available) -->
|
||||||
@@ -1349,27 +1229,16 @@
|
|||||||
<span id="storeErrorText"></span>
|
<span id="storeErrorText"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Search Bar - Always visible when store view is active -->
|
|
||||||
<div id="storeSearchContainer" class="store-search-container" style="margin-bottom: 20px; display: none;">
|
|
||||||
<div class="store-search-wrapper" style="position: relative; max-width: 500px; margin: 0 auto;">
|
|
||||||
<i class="fas fa-search" style="position: absolute; left: 15px; top: 50%; transform: translateY(-50%); color: #64748b; z-index: 1;"></i>
|
|
||||||
<input type="text" id="storeSearchInput" class="store-search-input" placeholder="{% trans 'Search plugins by name or description...' %}" style="width: 100%; padding: 12px 15px 12px 45px; border: 2px solid #e8e9ff; border-radius: 8px; font-size: 14px; transition: all 0.3s ease; background: white;">
|
|
||||||
<button id="clearSearchBtn" class="clear-search-btn" style="display: none; position: absolute; right: 10px; top: 50%; transform: translateY(-50%); background: none; border: none; color: #64748b; cursor: pointer; padding: 5px; font-size: 14px;" onclick="clearStoreSearch()">
|
|
||||||
<i class="fas fa-times"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Alphabetical Filter - Always visible when store view is active -->
|
|
||||||
<div id="storeAlphabetFilter" class="alphabet-filter" style="display: none;">
|
|
||||||
{% 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>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Store Content -->
|
<!-- Store Content -->
|
||||||
<div id="storeContent" style="display: none;">
|
<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>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Store Table -->
|
<!-- Store Table -->
|
||||||
<div class="store-table-wrapper">
|
<div class="store-table-wrapper">
|
||||||
<table class="store-table">
|
<table class="store-table">
|
||||||
@@ -1402,11 +1271,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Cache-busting version: 2026-01-25-v8 - Added search functionality to Plugin Store
|
// 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
|
// Force browser to reload this script by changing version number
|
||||||
let storePlugins = [];
|
let storePlugins = [];
|
||||||
let currentFilter = 'all';
|
let currentFilter = 'all';
|
||||||
let currentSearchQuery = '';
|
|
||||||
|
|
||||||
// Get CSRF cookie helper function
|
// Get CSRF cookie helper function
|
||||||
function getCookie(name) {
|
function getCookie(name) {
|
||||||
@@ -1433,59 +1301,25 @@ function toggleView(view) {
|
|||||||
viewBtns.forEach(btn => btn.classList.remove('active'));
|
viewBtns.forEach(btn => btn.classList.remove('active'));
|
||||||
|
|
||||||
if (view === 'grid') {
|
if (view === 'grid') {
|
||||||
// Grid view requires gridView to exist
|
|
||||||
if (!gridView) {
|
|
||||||
console.warn('Grid view not available (no plugins installed)');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
gridView.style.display = 'grid';
|
gridView.style.display = 'grid';
|
||||||
if (tableView) tableView.style.display = 'none';
|
tableView.style.display = 'none';
|
||||||
if (storeView) storeView.style.display = 'none';
|
storeView.style.display = 'none';
|
||||||
if (viewBtns[0]) viewBtns[0].classList.add('active');
|
viewBtns[0].classList.add('active');
|
||||||
} else if (view === 'table') {
|
} else if (view === 'table') {
|
||||||
// Table view requires tableView to exist
|
gridView.style.display = 'none';
|
||||||
if (!tableView) {
|
|
||||||
console.warn('Table view not available (no plugins installed)');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (gridView) gridView.style.display = 'none';
|
|
||||||
tableView.style.display = 'block';
|
tableView.style.display = 'block';
|
||||||
if (storeView) storeView.style.display = 'none';
|
storeView.style.display = 'none';
|
||||||
if (viewBtns[1]) viewBtns[1].classList.add('active');
|
viewBtns[1].classList.add('active');
|
||||||
} else if (view === 'store') {
|
} else if (view === 'store') {
|
||||||
// Store view requires storeView to exist
|
gridView.style.display = 'none';
|
||||||
if (!storeView) {
|
tableView.style.display = 'none';
|
||||||
console.error('Store view element not found');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (gridView) gridView.style.display = 'none';
|
|
||||||
if (tableView) tableView.style.display = 'none';
|
|
||||||
storeView.style.display = 'block';
|
storeView.style.display = 'block';
|
||||||
|
viewBtns[2].classList.add('active');
|
||||||
// Find and activate the store button (it might be at different index)
|
|
||||||
viewBtns.forEach((btn, index) => {
|
|
||||||
if (btn.textContent.includes('Store') || btn.textContent.includes('Plugin Store')) {
|
|
||||||
btn.classList.add('active');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Show search bar and alphabet filter immediately
|
|
||||||
const searchContainer = document.getElementById('storeSearchContainer');
|
|
||||||
const alphabetFilter = document.getElementById('storeAlphabetFilter');
|
|
||||||
if (searchContainer) searchContainer.style.display = 'block';
|
|
||||||
if (alphabetFilter) alphabetFilter.style.display = 'flex';
|
|
||||||
|
|
||||||
// Setup search functionality
|
|
||||||
if (typeof setupStoreSearch === 'function') {
|
|
||||||
setupStoreSearch();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load plugins from store if not already loaded
|
// Load plugins from store if not already loaded
|
||||||
if (typeof storePlugins !== 'undefined' && storePlugins.length === 0) {
|
if (storePlugins.length === 0) {
|
||||||
if (typeof loadPluginStore === 'function') {
|
loadPluginStore();
|
||||||
loadPluginStore();
|
} else {
|
||||||
}
|
|
||||||
} else if (typeof displayStorePlugins === 'function') {
|
|
||||||
displayStorePlugins();
|
displayStorePlugins();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1501,74 +1335,29 @@ function loadPluginStore() {
|
|||||||
storeError.style.display = 'none';
|
storeError.style.display = 'none';
|
||||||
storeContent.style.display = 'none';
|
storeContent.style.display = 'none';
|
||||||
|
|
||||||
// Add cache-busting timestamp to prevent browser caching
|
fetch('/plugins/api/store/plugins/', {
|
||||||
const cacheBuster = '?t=' + Date.now();
|
|
||||||
|
|
||||||
fetch('/plugins/api/store/plugins/' + cacheBuster, {
|
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
'X-CSRFToken': getCookie('csrftoken'),
|
'X-CSRFToken': getCookie('csrftoken')
|
||||||
'Cache-Control': 'no-cache',
|
|
||||||
'Pragma': 'no-cache'
|
|
||||||
},
|
|
||||||
cache: 'no-store'
|
|
||||||
})
|
|
||||||
.then(response => {
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
||||||
}
|
}
|
||||||
return response.json();
|
|
||||||
})
|
})
|
||||||
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
storeLoading.style.display = 'none';
|
storeLoading.style.display = 'none';
|
||||||
|
|
||||||
if (data.success && data.plugins) {
|
if (data.success) {
|
||||||
// Sort plugins deterministically by name to prevent order changes
|
storePlugins = data.plugins;
|
||||||
storePlugins = data.plugins.sort((a, b) => {
|
|
||||||
const nameA = (a.name || '').toLowerCase();
|
|
||||||
const nameB = (b.name || '').toLowerCase();
|
|
||||||
return nameA.localeCompare(nameB);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Normalize is_paid to boolean for all plugins
|
|
||||||
storePlugins = storePlugins.map(plugin => {
|
|
||||||
if (plugin.is_paid !== undefined && plugin.is_paid !== null) {
|
|
||||||
const isPaidValue = plugin.is_paid;
|
|
||||||
if (isPaidValue === true || isPaidValue === 'true' || isPaidValue === 'True' || isPaidValue === 1 || isPaidValue === '1' || String(isPaidValue).toLowerCase() === 'true') {
|
|
||||||
plugin.is_paid = true;
|
|
||||||
} else {
|
|
||||||
plugin.is_paid = false;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
plugin.is_paid = false;
|
|
||||||
}
|
|
||||||
// Force boolean type
|
|
||||||
plugin.is_paid = Boolean(plugin.is_paid);
|
|
||||||
return plugin;
|
|
||||||
});
|
|
||||||
|
|
||||||
displayStorePlugins();
|
displayStorePlugins();
|
||||||
storeContent.style.display = 'block';
|
storeContent.style.display = 'block';
|
||||||
// Ensure search and filter are visible
|
|
||||||
const searchContainer = document.getElementById('storeSearchContainer');
|
|
||||||
const alphabetFilter = document.getElementById('storeAlphabetFilter');
|
|
||||||
if (searchContainer) searchContainer.style.display = 'block';
|
|
||||||
if (alphabetFilter) alphabetFilter.style.display = 'flex';
|
|
||||||
} else {
|
} else {
|
||||||
storeErrorText.textContent = data.error || 'Failed to load plugins from store';
|
storeErrorText.textContent = data.error || 'Failed to load plugins from store';
|
||||||
storeError.style.display = 'block';
|
storeError.style.display = 'block';
|
||||||
// Keep search and filter visible even on error
|
|
||||||
const searchContainer = document.getElementById('storeSearchContainer');
|
|
||||||
const alphabetFilter = document.getElementById('storeAlphabetFilter');
|
|
||||||
if (searchContainer) searchContainer.style.display = 'block';
|
|
||||||
if (alphabetFilter) alphabetFilter.style.display = 'flex';
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
storeLoading.style.display = 'none';
|
storeLoading.style.display = 'none';
|
||||||
storeErrorText.textContent = 'Error loading plugin store: ' + error.message;
|
storeErrorText.textContent = 'Error loading plugin store: ' + error.message;
|
||||||
storeError.style.display = 'block';
|
storeError.style.display = 'block';
|
||||||
console.error('Plugin store load error:', error);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1580,9 +1369,8 @@ function escapeHtml(text) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function displayStorePlugins() {
|
function displayStorePlugins() {
|
||||||
// Version: 2026-01-25-v8 - Store view: 8 columns with search functionality
|
// Version: 2026-01-25-v4 - Store view: Removed Status column, always show Free/Paid badges
|
||||||
// CRITICAL: NO Author, NO Status, NO Active columns - these are for Grid/Table views only
|
// CRITICAL: This function MUST create exactly 7 columns (no Status, no Deactivate/Uninstall)
|
||||||
// Store view shows: Icon | Plugin Name (with NEW/Stale badges) | Version | Pricing | Modify Date | Action (Installed/Install) | Help | About
|
|
||||||
const tbody = document.getElementById('storeTableBody');
|
const tbody = document.getElementById('storeTableBody');
|
||||||
if (!tbody) {
|
if (!tbody) {
|
||||||
console.error('storeTableBody not found!');
|
console.error('storeTableBody not found!');
|
||||||
@@ -1595,45 +1383,15 @@ function displayStorePlugins() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// CRITICAL: Sort plugins deterministically by name to prevent order changes
|
let filteredPlugins = storePlugins;
|
||||||
// Create a copy to avoid mutating the original array
|
|
||||||
let sortedPlugins = [...storePlugins].sort((a, b) => {
|
|
||||||
const nameA = (a.name || '').toLowerCase();
|
|
||||||
const nameB = (b.name || '').toLowerCase();
|
|
||||||
return nameA.localeCompare(nameB);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initialize filteredPlugins FIRST
|
|
||||||
let filteredPlugins = sortedPlugins;
|
|
||||||
|
|
||||||
// Apply alphabetical filter
|
|
||||||
if (currentFilter !== 'all') {
|
if (currentFilter !== 'all') {
|
||||||
filteredPlugins = filteredPlugins.filter(plugin =>
|
filteredPlugins = storePlugins.filter(plugin =>
|
||||||
plugin.name && plugin.name.charAt(0).toUpperCase() === currentFilter
|
plugin.name.charAt(0).toUpperCase() === currentFilter
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply search filter
|
filteredPlugins.forEach(plugin => {
|
||||||
if (currentSearchQuery && currentSearchQuery.trim() !== '') {
|
|
||||||
const searchLower = currentSearchQuery.toLowerCase().trim();
|
|
||||||
filteredPlugins = filteredPlugins.filter(plugin => {
|
|
||||||
const nameMatch = plugin.name && plugin.name.toLowerCase().includes(searchLower);
|
|
||||||
const descMatch = plugin.description && plugin.description.toLowerCase().includes(searchLower);
|
|
||||||
const authorMatch = plugin.author && plugin.author.toLowerCase().includes(searchLower);
|
|
||||||
return nameMatch || descMatch || authorMatch;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show message if search/filter returns no results (AFTER filtering)
|
|
||||||
if (filteredPlugins.length === 0) {
|
|
||||||
const searchMsg = currentSearchQuery
|
|
||||||
? `No plugins found matching "${currentSearchQuery}"`
|
|
||||||
: 'No plugins available';
|
|
||||||
tbody.innerHTML = `<tr><td colspan="8" style="text-align: center; padding: 20px; color: var(--text-secondary, #64748b);">${escapeHtml(searchMsg)}</td></tr>`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
filteredPlugins.forEach(plugin => {
|
|
||||||
const row = document.createElement('tr');
|
const row = document.createElement('tr');
|
||||||
|
|
||||||
// Plugin icon - based on plugin type (same logic as Grid/Table views)
|
// Plugin icon - based on plugin type (same logic as Grid/Table views)
|
||||||
@@ -1681,39 +1439,23 @@ function displayStorePlugins() {
|
|||||||
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>';
|
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>';
|
||||||
|
|
||||||
// Pricing badge - ALWAYS show a badge (default to Free if is_paid is missing/undefined/null)
|
// Pricing badge - ALWAYS show a badge (default to Free if is_paid is missing/undefined/null)
|
||||||
// Version: 2026-01-25-v5 - Normalize is_paid to handle all possible values, force boolean
|
// Version: 2026-01-25-v4 - Normalize is_paid to handle all possible values
|
||||||
let isPaid = false;
|
let isPaid = false;
|
||||||
if (plugin.is_paid !== undefined && plugin.is_paid !== null) {
|
if (plugin.is_paid !== undefined && plugin.is_paid !== null) {
|
||||||
const isPaidValue = plugin.is_paid;
|
const isPaidValue = plugin.is_paid;
|
||||||
// Handle all possible true values (boolean, string, number)
|
|
||||||
if (isPaidValue === true || isPaidValue === 'true' || isPaidValue === 'True' || isPaidValue === 1 || isPaidValue === '1' || String(isPaidValue).toLowerCase() === 'true') {
|
if (isPaidValue === true || isPaidValue === 'true' || isPaidValue === 'True' || isPaidValue === 1 || isPaidValue === '1' || String(isPaidValue).toLowerCase() === 'true') {
|
||||||
isPaid = true;
|
isPaid = true;
|
||||||
} else {
|
|
||||||
isPaid = false; // Explicitly set to false for any other value
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Force boolean type
|
|
||||||
isPaid = Boolean(isPaid);
|
|
||||||
const pricingBadge = isPaid
|
const pricingBadge = isPaid
|
||||||
? '<span class="plugin-pricing-badge paid">Paid</span>'
|
? '<span class="plugin-pricing-badge paid">Paid</span>'
|
||||||
: '<span class="plugin-pricing-badge free">Free</span>';
|
: '<span class="plugin-pricing-badge free">Free</span>';
|
||||||
|
|
||||||
// NEW and Stale badges
|
// Version: 2026-01-25-v5 - Added plugin icons to Store view (8 columns: Icon, Plugin Name, Version, Pricing, Modify Date, Action, Help, About)
|
||||||
let statusBadges = '';
|
|
||||||
if (plugin.is_new === true) {
|
|
||||||
statusBadges += '<span class="plugin-status-badge new" title="This plugin was released/updated within the last 3 months">NEW</span>';
|
|
||||||
}
|
|
||||||
if (plugin.is_stale === true) {
|
|
||||||
statusBadges += '<span class="plugin-status-badge stale" title="This plugin is marked \'Stale\' (Last release over two years ago). This means it may work fine, but it has not had any recent development. Use your own discretion when using this plugin!">STALE</span>';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Version: 2026-01-25-v7 - Store view: 8 columns only (Icon, Plugin Name, Version, Pricing, Modify Date, Action, Help, About)
|
|
||||||
// NO Author, NO Status, NO Active columns - these are removed from Store view
|
|
||||||
// Plugin Name includes NEW/Stale badges
|
|
||||||
row.innerHTML = `
|
row.innerHTML = `
|
||||||
<td style="text-align: center;">${iconHtml}</td>
|
<td style="text-align: center;">${iconHtml}</td>
|
||||||
<td>
|
<td>
|
||||||
<strong>${escapeHtml(plugin.name)}</strong>${statusBadges}
|
<strong>${escapeHtml(plugin.name)}</strong>
|
||||||
</td>
|
</td>
|
||||||
<td><span class="plugin-version-number">${escapeHtml(plugin.version)}</span></td>
|
<td><span class="plugin-version-number">${escapeHtml(plugin.version)}</span></td>
|
||||||
<td>${pricingBadge}</td>
|
<td>${pricingBadge}</td>
|
||||||
@@ -1723,14 +1465,6 @@ function displayStorePlugins() {
|
|||||||
<td>${aboutHtml}</td>
|
<td>${aboutHtml}</td>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Ensure row has exactly 8 cells (no more, no less)
|
|
||||||
if (row.cells.length !== 8) {
|
|
||||||
console.warn(`Plugin ${plugin.name} row has ${row.cells.length} cells, expected 8. Rebuilding...`);
|
|
||||||
const newRow = document.createElement('tr');
|
|
||||||
newRow.innerHTML = row.innerHTML;
|
|
||||||
row.parentNode.replaceChild(newRow, row);
|
|
||||||
}
|
|
||||||
|
|
||||||
tbody.appendChild(row);
|
tbody.appendChild(row);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1752,50 +1486,6 @@ function filterByLetter(letter) {
|
|||||||
displayStorePlugins();
|
displayStorePlugins();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search functionality
|
|
||||||
function setupStoreSearch() {
|
|
||||||
const searchInput = document.getElementById('storeSearchInput');
|
|
||||||
const clearBtn = document.getElementById('clearSearchBtn');
|
|
||||||
|
|
||||||
if (!searchInput) return;
|
|
||||||
|
|
||||||
// Search on input
|
|
||||||
searchInput.addEventListener('input', function(e) {
|
|
||||||
currentSearchQuery = e.target.value;
|
|
||||||
|
|
||||||
// Show/hide clear button
|
|
||||||
if (currentSearchQuery && currentSearchQuery.trim() !== '') {
|
|
||||||
clearBtn.style.display = 'block';
|
|
||||||
} else {
|
|
||||||
clearBtn.style.display = 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter plugins
|
|
||||||
displayStorePlugins();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Search on Enter key
|
|
||||||
searchInput.addEventListener('keydown', function(e) {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
e.preventDefault();
|
|
||||||
displayStorePlugins();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearStoreSearch() {
|
|
||||||
const searchInput = document.getElementById('storeSearchInput');
|
|
||||||
const clearBtn = document.getElementById('clearSearchBtn');
|
|
||||||
|
|
||||||
if (searchInput) {
|
|
||||||
searchInput.value = '';
|
|
||||||
currentSearchQuery = '';
|
|
||||||
clearBtn.style.display = 'none';
|
|
||||||
displayStorePlugins();
|
|
||||||
searchInput.focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function installFromStore(pluginName) {
|
function installFromStore(pluginName) {
|
||||||
if (!confirm(`Install ${pluginName} from the CyberPanel Plugin Store?`)) {
|
if (!confirm(`Install ${pluginName} from the CyberPanel Plugin Store?`)) {
|
||||||
return;
|
return;
|
||||||
@@ -2159,47 +1849,14 @@ if (localStorage.getItem('pluginStoreNoticeDismissed') === 'true') {
|
|||||||
|
|
||||||
// Initialize view on page load
|
// Initialize view on page load
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Default to grid view if plugins exist, otherwise show store
|
||||||
const gridView = document.getElementById('gridView');
|
const gridView = document.getElementById('gridView');
|
||||||
const tableView = document.getElementById('tableView');
|
|
||||||
const storeView = document.getElementById('storeView');
|
const storeView = document.getElementById('storeView');
|
||||||
|
|
||||||
// Determine which view to show based on what's available
|
|
||||||
if (gridView && gridView.children.length > 0) {
|
if (gridView && gridView.children.length > 0) {
|
||||||
// Plugins are installed, show grid view
|
|
||||||
toggleView('grid');
|
toggleView('grid');
|
||||||
} else if (storeView) {
|
} else if (storeView) {
|
||||||
// No plugins installed, show store by default
|
// No plugins installed, show store by default
|
||||||
// Don't call toggleView here to avoid recursion, just show it directly
|
toggleView('store');
|
||||||
storeView.style.display = 'block';
|
|
||||||
if (gridView) gridView.style.display = 'none';
|
|
||||||
if (tableView) tableView.style.display = 'none';
|
|
||||||
|
|
||||||
// Activate the store button
|
|
||||||
const viewBtns = document.querySelectorAll('.view-btn');
|
|
||||||
viewBtns.forEach(btn => {
|
|
||||||
btn.classList.remove('active');
|
|
||||||
if (btn.textContent.includes('Store') || btn.textContent.includes('Plugin Store')) {
|
|
||||||
btn.classList.add('active');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Show search bar and alphabet filter
|
|
||||||
const searchContainer = document.getElementById('storeSearchContainer');
|
|
||||||
const alphabetFilter = document.getElementById('storeAlphabetFilter');
|
|
||||||
if (searchContainer) searchContainer.style.display = 'block';
|
|
||||||
if (alphabetFilter) alphabetFilter.style.display = 'flex';
|
|
||||||
|
|
||||||
// Setup search functionality
|
|
||||||
if (typeof setupStoreSearch === 'function') {
|
|
||||||
setupStoreSearch();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load plugins from store
|
|
||||||
if (typeof loadPluginStore === 'function') {
|
|
||||||
loadPluginStore();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.error('No view elements found on page load');
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -128,7 +128,9 @@ def installed(request):
|
|||||||
data['desc'] = desc_elem.text
|
data['desc'] = desc_elem.text
|
||||||
data['version'] = version_elem.text
|
data['version'] = version_elem.text
|
||||||
data['plugin_dir'] = plugin # Plugin directory name
|
data['plugin_dir'] = plugin # Plugin directory name
|
||||||
data['installed'] = os.path.exists(completePath) # True if installed, False if only in source
|
# Check if plugin is installed (only if it exists in /usr/local/CyberCP/)
|
||||||
|
# Source directory presence doesn't mean installed - it just means the source files are available
|
||||||
|
data['installed'] = os.path.exists(completePath)
|
||||||
|
|
||||||
# Get plugin enabled state (only for installed plugins)
|
# Get plugin enabled state (only for installed plugins)
|
||||||
if data['installed']:
|
if data['installed']:
|
||||||
@@ -140,9 +142,6 @@ def installed(request):
|
|||||||
data['is_paid'] = False
|
data['is_paid'] = False
|
||||||
data['patreon_tier'] = None
|
data['patreon_tier'] = None
|
||||||
data['patreon_url'] = None
|
data['patreon_url'] = None
|
||||||
data['paypal_me_url'] = None
|
|
||||||
data['paypal_payment_link'] = None
|
|
||||||
data['payment_type'] = None # 'patreon' or 'paypal'
|
|
||||||
|
|
||||||
# Get modify date from local file (fast, no API calls)
|
# Get modify date from local file (fast, no API calls)
|
||||||
# GitHub commit dates are fetched in the plugin store, not here to avoid timeouts
|
# GitHub commit dates are fetched in the plugin store, not here to avoid timeouts
|
||||||
@@ -156,37 +155,15 @@ def installed(request):
|
|||||||
|
|
||||||
data['modify_date'] = modify_date
|
data['modify_date'] = modify_date
|
||||||
|
|
||||||
# Calculate NEW and Stale badges based on modify_date
|
|
||||||
data['is_new'] = False
|
|
||||||
data['is_stale'] = False
|
|
||||||
|
|
||||||
if modify_date and modify_date != 'N/A':
|
|
||||||
try:
|
|
||||||
# Parse the modify_date (format: YYYY-MM-DD HH:MM:SS)
|
|
||||||
modify_date_obj = datetime.strptime(modify_date, '%Y-%m-%d %H:%M:%S')
|
|
||||||
now = datetime.now()
|
|
||||||
time_diff = now - modify_date_obj
|
|
||||||
|
|
||||||
# NEW: updated within last 90 days (3 months)
|
|
||||||
if time_diff.days <= 90:
|
|
||||||
data['is_new'] = True
|
|
||||||
|
|
||||||
# Stale: not updated in last 2 years (730 days)
|
|
||||||
if time_diff.days > 730:
|
|
||||||
data['is_stale'] = True
|
|
||||||
except Exception:
|
|
||||||
# If date parsing fails, leave as False
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Extract settings URL or main URL for "Manage" button
|
# Extract settings URL or main URL for "Manage" button
|
||||||
settings_url_elem = root.find('settings_url')
|
settings_url_elem = root.find('settings_url')
|
||||||
url_elem = root.find('url')
|
url_elem = root.find('url')
|
||||||
|
|
||||||
# Priority: settings_url > url > default pattern
|
# Priority: settings_url > url > default pattern
|
||||||
# Special handling for core plugins that don't use /plugins/ prefix
|
# Special handling for core plugins that don't use /plugins/ prefix
|
||||||
# emailMarketing removed from INSTALLED_APPS - skip it
|
|
||||||
if plugin == 'emailMarketing':
|
if plugin == 'emailMarketing':
|
||||||
continue
|
# emailMarketing is a core CyberPanel plugin, uses /emailMarketing/ not /plugins/emailMarketing/
|
||||||
|
data['manage_url'] = '/emailMarketing/'
|
||||||
elif settings_url_elem is not None and settings_url_elem.text:
|
elif settings_url_elem is not None and settings_url_elem.text:
|
||||||
data['manage_url'] = settings_url_elem.text
|
data['manage_url'] = settings_url_elem.text
|
||||||
elif url_elem is not None and url_elem.text:
|
elif url_elem is not None and url_elem.text:
|
||||||
@@ -194,7 +171,10 @@ def installed(request):
|
|||||||
else:
|
else:
|
||||||
# Default: try /plugins/{plugin_dir}/settings/ or /plugins/{plugin_dir}/
|
# Default: try /plugins/{plugin_dir}/settings/ or /plugins/{plugin_dir}/
|
||||||
# Only set if plugin is installed (we can't know if the URL exists otherwise)
|
# Only set if plugin is installed (we can't know if the URL exists otherwise)
|
||||||
if os.path.exists(completePath):
|
# Special handling for emailMarketing
|
||||||
|
if plugin == 'emailMarketing':
|
||||||
|
data['manage_url'] = '/emailMarketing/'
|
||||||
|
elif os.path.exists(completePath):
|
||||||
# Check if settings route exists, otherwise use main plugin URL
|
# Check if settings route exists, otherwise use main plugin URL
|
||||||
settings_route = f'/plugins/{plugin}/settings/'
|
settings_route = f'/plugins/{plugin}/settings/'
|
||||||
main_route = f'/plugins/{plugin}/'
|
main_route = f'/plugins/{plugin}/'
|
||||||
@@ -210,59 +190,39 @@ def installed(request):
|
|||||||
else:
|
else:
|
||||||
data['author'] = 'Unknown'
|
data['author'] = 'Unknown'
|
||||||
|
|
||||||
# Extract paid plugin information - support both Patreon and PayPal
|
# Extract paid plugin information
|
||||||
paid_elem = root.find('paid')
|
paid_elem = root.find('paid')
|
||||||
|
patreon_tier_elem = root.find('patreon_tier')
|
||||||
|
|
||||||
# CRITICAL: Always explicitly set is_paid as boolean True/False
|
if paid_elem is not None and paid_elem.text and paid_elem.text.lower() == 'true':
|
||||||
if paid_elem is not None and paid_elem.text and str(paid_elem.text).strip().lower() == 'true':
|
data['is_paid'] = True
|
||||||
data['is_paid'] = True # Explicit boolean True
|
data['patreon_tier'] = patreon_tier_elem.text if patreon_tier_elem is not None and patreon_tier_elem.text else 'CyberPanel Paid Plugin'
|
||||||
|
data['patreon_url'] = root.find('patreon_url').text if root.find('patreon_url') is not None else 'https://www.patreon.com/c/newstargeted/membership'
|
||||||
# Check for PayPal payment method first
|
|
||||||
paypal_me_elem = root.find('paypal_me_url')
|
|
||||||
paypal_payment_link_elem = root.find('paypal_payment_link')
|
|
||||||
|
|
||||||
if (paypal_me_elem is not None and paypal_me_elem.text) or (paypal_payment_link_elem is not None and paypal_payment_link_elem.text):
|
|
||||||
# This is a PayPal plugin
|
|
||||||
data['payment_type'] = 'paypal'
|
|
||||||
data['paypal_me_url'] = paypal_me_elem.text if paypal_me_elem is not None and paypal_me_elem.text else None
|
|
||||||
data['paypal_payment_link'] = paypal_payment_link_elem.text if paypal_payment_link_elem is not None and paypal_payment_link_elem.text else None
|
|
||||||
data['patreon_tier'] = None
|
|
||||||
data['patreon_url'] = None
|
|
||||||
else:
|
|
||||||
# This is a Patreon plugin (default/fallback)
|
|
||||||
data['payment_type'] = 'patreon'
|
|
||||||
patreon_tier_elem = root.find('patreon_tier')
|
|
||||||
data['patreon_tier'] = patreon_tier_elem.text if patreon_tier_elem is not None and patreon_tier_elem.text else 'CyberPanel Paid Plugin'
|
|
||||||
data['patreon_url'] = root.find('patreon_url').text if root.find('patreon_url') is not None else 'https://www.patreon.com/c/newstargeted/membership'
|
|
||||||
data['paypal_me_url'] = None
|
|
||||||
data['paypal_payment_link'] = None
|
|
||||||
else:
|
else:
|
||||||
data['is_paid'] = False # Explicit boolean False
|
data['is_paid'] = False
|
||||||
data['patreon_tier'] = None
|
data['patreon_tier'] = None
|
||||||
data['patreon_url'] = None
|
data['patreon_url'] = None
|
||||||
data['paypal_me_url'] = None
|
|
||||||
data['paypal_payment_link'] = None
|
|
||||||
data['payment_type'] = None
|
|
||||||
|
|
||||||
# Force boolean type (defensive programming) - CRITICAL: Always ensure boolean
|
|
||||||
data['is_paid'] = bool(data['is_paid']) if 'is_paid' in data else False
|
|
||||||
|
|
||||||
# Final safety check - ensure is_paid exists and is boolean before adding to list
|
|
||||||
if 'is_paid' not in data or not isinstance(data['is_paid'], bool):
|
|
||||||
data['is_paid'] = False
|
|
||||||
|
|
||||||
# Only add to list if plugin is actually installed
|
pluginList.append(data)
|
||||||
# Uninstalled plugins should only appear in Plugin Store, not Installed Plugins page
|
|
||||||
if data['installed']:
|
|
||||||
pluginList.append(data)
|
|
||||||
processed_plugins.add(plugin) # Mark as processed
|
processed_plugins.add(plugin) # Mark as processed
|
||||||
except ElementTree.ParseError as e:
|
except ElementTree.ParseError as e:
|
||||||
errorPlugins.append({'name': plugin, 'error': f'XML parse error: {str(e)}'})
|
errorPlugins.append({'name': plugin, 'error': f'XML parse error: {str(e)}'})
|
||||||
logging.writeToFile(f"Plugin {plugin}: XML parse error - {str(e)}")
|
logging.writeToFile(f"Plugin {plugin}: XML parse error - {str(e)}")
|
||||||
|
# Don't mark as processed if it failed - let installed check handle it
|
||||||
|
# This ensures plugins that exist in /usr/local/CyberCP/ but have bad source meta.xml still get counted
|
||||||
|
if not os.path.exists(completePath):
|
||||||
|
# Only skip if it's not actually installed
|
||||||
|
continue
|
||||||
|
# If it exists in installed location, don't mark as processed so it gets checked there
|
||||||
continue
|
continue
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
errorPlugins.append({'name': plugin, 'error': f'Error loading plugin: {str(e)}'})
|
errorPlugins.append({'name': plugin, 'error': f'Error loading plugin: {str(e)}'})
|
||||||
logging.writeToFile(f"Plugin {plugin}: Error loading - {str(e)}")
|
logging.writeToFile(f"Plugin {plugin}: Error loading - {str(e)}")
|
||||||
|
# Don't mark as processed if it failed - let installed check handle it
|
||||||
|
if not os.path.exists(completePath):
|
||||||
|
# Only skip if it's not actually installed
|
||||||
|
continue
|
||||||
|
# If it exists in installed location, don't mark as processed so it gets checked there
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Also check for installed plugins that don't have source directories
|
# Also check for installed plugins that don't have source directories
|
||||||
@@ -324,43 +284,25 @@ def installed(request):
|
|||||||
|
|
||||||
data['modify_date'] = modify_date
|
data['modify_date'] = modify_date
|
||||||
|
|
||||||
# Calculate NEW and Stale badges based on modify_date
|
|
||||||
data['is_new'] = False
|
|
||||||
data['is_stale'] = False
|
|
||||||
|
|
||||||
if modify_date and modify_date != 'N/A':
|
|
||||||
try:
|
|
||||||
# Parse the modify_date (format: YYYY-MM-DD HH:MM:SS)
|
|
||||||
modify_date_obj = datetime.strptime(modify_date, '%Y-%m-%d %H:%M:%S')
|
|
||||||
now = datetime.now()
|
|
||||||
time_diff = now - modify_date_obj
|
|
||||||
|
|
||||||
# NEW: updated within last 90 days (3 months)
|
|
||||||
if time_diff.days <= 90:
|
|
||||||
data['is_new'] = True
|
|
||||||
|
|
||||||
# Stale: not updated in last 2 years (730 days)
|
|
||||||
if time_diff.days > 730:
|
|
||||||
data['is_stale'] = True
|
|
||||||
except Exception:
|
|
||||||
# If date parsing fails, leave as False
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Extract settings URL or main URL
|
# Extract settings URL or main URL
|
||||||
settings_url_elem = root.find('settings_url')
|
settings_url_elem = root.find('settings_url')
|
||||||
url_elem = root.find('url')
|
url_elem = root.find('url')
|
||||||
|
|
||||||
# Priority: settings_url > url > default pattern
|
# Priority: settings_url > url > default pattern
|
||||||
# Special handling for core plugins that don't use /plugins/ prefix
|
# Special handling for core plugins that don't use /plugins/ prefix
|
||||||
# emailMarketing removed from INSTALLED_APPS - skip it
|
|
||||||
if plugin == 'emailMarketing':
|
if plugin == 'emailMarketing':
|
||||||
continue
|
# emailMarketing is a core CyberPanel plugin, uses /emailMarketing/ not /plugins/emailMarketing/
|
||||||
|
data['manage_url'] = '/emailMarketing/'
|
||||||
elif settings_url_elem is not None and settings_url_elem.text:
|
elif settings_url_elem is not None and settings_url_elem.text:
|
||||||
data['manage_url'] = settings_url_elem.text
|
data['manage_url'] = settings_url_elem.text
|
||||||
elif url_elem is not None and url_elem.text:
|
elif url_elem is not None and url_elem.text:
|
||||||
data['manage_url'] = url_elem.text
|
data['manage_url'] = url_elem.text
|
||||||
else:
|
else:
|
||||||
# Default to /plugins/{plugin}/ for regular plugins
|
# Default to /plugins/{plugin}/ for regular plugins
|
||||||
|
# Special handling for emailMarketing
|
||||||
|
if plugin == 'emailMarketing':
|
||||||
|
data['manage_url'] = '/emailMarketing/'
|
||||||
|
else:
|
||||||
# Default to main plugin route (most plugins work from main route)
|
# Default to main plugin route (most plugins work from main route)
|
||||||
data['manage_url'] = f'/plugins/{plugin}/'
|
data['manage_url'] = f'/plugins/{plugin}/'
|
||||||
|
|
||||||
@@ -371,47 +313,16 @@ def installed(request):
|
|||||||
else:
|
else:
|
||||||
data['author'] = 'Unknown'
|
data['author'] = 'Unknown'
|
||||||
|
|
||||||
# Extract paid plugin information - support both Patreon and PayPal
|
# Extract paid plugin information (is_paid already initialized to False above)
|
||||||
paid_elem = root.find('paid')
|
paid_elem = root.find('paid')
|
||||||
|
patreon_tier_elem = root.find('patreon_tier')
|
||||||
|
|
||||||
# CRITICAL: Always explicitly set is_paid as boolean True/False
|
if paid_elem is not None and paid_elem.text and paid_elem.text.lower() == 'true':
|
||||||
if paid_elem is not None and paid_elem.text and str(paid_elem.text).strip().lower() == 'true':
|
data['is_paid'] = True
|
||||||
data['is_paid'] = True # Explicit boolean True
|
data['patreon_tier'] = patreon_tier_elem.text if patreon_tier_elem is not None and patreon_tier_elem.text else 'CyberPanel Paid Plugin'
|
||||||
|
patreon_url_elem = root.find('patreon_url')
|
||||||
# Check for PayPal payment method first
|
data['patreon_url'] = patreon_url_elem.text if patreon_url_elem is not None and patreon_url_elem.text else 'https://www.patreon.com/membership/27789984'
|
||||||
paypal_me_elem = root.find('paypal_me_url')
|
# else: is_paid already False from initialization above
|
||||||
paypal_payment_link_elem = root.find('paypal_payment_link')
|
|
||||||
|
|
||||||
if (paypal_me_elem is not None and paypal_me_elem.text) or (paypal_payment_link_elem is not None and paypal_payment_link_elem.text):
|
|
||||||
# This is a PayPal plugin
|
|
||||||
data['payment_type'] = 'paypal'
|
|
||||||
data['paypal_me_url'] = paypal_me_elem.text if paypal_me_elem is not None and paypal_me_elem.text else None
|
|
||||||
data['paypal_payment_link'] = paypal_payment_link_elem.text if paypal_payment_link_elem is not None and paypal_payment_link_elem.text else None
|
|
||||||
data['patreon_tier'] = None
|
|
||||||
data['patreon_url'] = None
|
|
||||||
else:
|
|
||||||
# This is a Patreon plugin (default/fallback)
|
|
||||||
data['payment_type'] = 'patreon'
|
|
||||||
patreon_tier_elem = root.find('patreon_tier')
|
|
||||||
data['patreon_tier'] = patreon_tier_elem.text if patreon_tier_elem is not None and patreon_tier_elem.text else 'CyberPanel Paid Plugin'
|
|
||||||
patreon_url_elem = root.find('patreon_url')
|
|
||||||
data['patreon_url'] = patreon_url_elem.text if patreon_url_elem is not None and patreon_url_elem.text else 'https://www.patreon.com/membership/27789984'
|
|
||||||
data['paypal_me_url'] = None
|
|
||||||
data['paypal_payment_link'] = None
|
|
||||||
else:
|
|
||||||
data['is_paid'] = False # Explicit boolean False
|
|
||||||
data['patreon_tier'] = None
|
|
||||||
data['patreon_url'] = None
|
|
||||||
data['paypal_me_url'] = None
|
|
||||||
data['paypal_payment_link'] = None
|
|
||||||
data['payment_type'] = None
|
|
||||||
|
|
||||||
# Force boolean type (defensive programming) - CRITICAL: Always ensure boolean
|
|
||||||
data['is_paid'] = bool(data['is_paid']) if 'is_paid' in data else False
|
|
||||||
|
|
||||||
# Final safety check - ensure is_paid exists and is boolean before adding to list
|
|
||||||
if 'is_paid' not in data or not isinstance(data['is_paid'], bool):
|
|
||||||
data['is_paid'] = False
|
|
||||||
|
|
||||||
pluginList.append(data)
|
pluginList.append(data)
|
||||||
|
|
||||||
@@ -424,32 +335,39 @@ def installed(request):
|
|||||||
logging.writeToFile(f"Installed plugin {plugin}: Error loading - {str(e)}")
|
logging.writeToFile(f"Installed plugin {plugin}: Error loading - {str(e)}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Sort plugins deterministically by name to prevent order changes
|
# Calculate installed and active counts
|
||||||
pluginList.sort(key=lambda x: x.get('name', '').lower())
|
# Double-check by also counting plugins that actually exist in /usr/local/CyberCP/
|
||||||
|
installed_plugins_in_filesystem = set()
|
||||||
|
if os.path.exists(installedPath):
|
||||||
|
for plugin in os.listdir(installedPath):
|
||||||
|
pluginInstalledDir = os.path.join(installedPath, plugin)
|
||||||
|
if os.path.isdir(pluginInstalledDir):
|
||||||
|
metaXmlPath = os.path.join(pluginInstalledDir, 'meta.xml')
|
||||||
|
if os.path.exists(metaXmlPath):
|
||||||
|
installed_plugins_in_filesystem.add(plugin)
|
||||||
|
|
||||||
# Calculate statistics
|
# Count installed plugins from the list
|
||||||
total_installed = len(pluginList)
|
installed_count = len([p for p in pluginList if p.get('installed', False)])
|
||||||
total_active = sum(1 for plugin in pluginList if plugin.get('enabled', False))
|
active_count = len([p for p in pluginList if p.get('installed', False) and p.get('enabled', False)])
|
||||||
|
|
||||||
# Add cache-busting timestamp to context to prevent browser caching
|
# If there's a discrepancy, use the filesystem count as the source of truth
|
||||||
import time
|
filesystem_installed_count = len(installed_plugins_in_filesystem)
|
||||||
context = {
|
if filesystem_installed_count != installed_count:
|
||||||
'plugins': pluginList,
|
logging.writeToFile(f"WARNING: Plugin count mismatch! List says {installed_count}, filesystem has {filesystem_installed_count}")
|
||||||
'error_plugins': errorPlugins,
|
logging.writeToFile(f"Plugins in filesystem: {sorted(installed_plugins_in_filesystem)}")
|
||||||
'total_installed': total_installed,
|
logging.writeToFile(f"Plugins in list with installed=True: {[p.get('plugin_dir') for p in pluginList if p.get('installed', False)]}")
|
||||||
'total_active': total_active,
|
# Use filesystem count as source of truth
|
||||||
'cache_buster': int(time.time()) # Add timestamp to force template reload
|
installed_count = filesystem_installed_count
|
||||||
}
|
|
||||||
|
|
||||||
proc = httpProc(request, 'pluginHolder/plugins.html', context, 'admin')
|
# Debug logging to help identify discrepancies
|
||||||
response = proc.render()
|
logging.writeToFile(f"Plugin count: Total={len(pluginList)}, Installed={installed_count}, Active={active_count}")
|
||||||
|
for p in pluginList:
|
||||||
# Add cache-busting headers to prevent browser caching
|
logging.writeToFile(f" - {p.get('plugin_dir')}: installed={p.get('installed')}, enabled={p.get('enabled')}")
|
||||||
response['Cache-Control'] = 'no-cache, no-store, must-revalidate'
|
|
||||||
response['Pragma'] = 'no-cache'
|
proc = httpProc(request, 'pluginHolder/plugins.html',
|
||||||
response['Expires'] = '0'
|
{'plugins': pluginList, 'error_plugins': errorPlugins,
|
||||||
|
'installed_count': installed_count, 'active_count': active_count}, 'admin')
|
||||||
return response
|
return proc.render()
|
||||||
|
|
||||||
@csrf_exempt
|
@csrf_exempt
|
||||||
@require_http_methods(["POST"])
|
@require_http_methods(["POST"])
|
||||||
@@ -464,39 +382,13 @@ def install_plugin(request, plugin_name):
|
|||||||
'error': f'Plugin source not found: {plugin_name}'
|
'error': f'Plugin source not found: {plugin_name}'
|
||||||
}, status=404)
|
}, status=404)
|
||||||
|
|
||||||
# Check if already installed (must have meta.xml to be considered installed)
|
# Check if already installed
|
||||||
pluginInstalled = '/usr/local/CyberCP/' + plugin_name
|
pluginInstalled = '/usr/local/CyberCP/' + plugin_name
|
||||||
if os.path.exists(pluginInstalled):
|
if os.path.exists(pluginInstalled):
|
||||||
# Check if it's a valid installation (has meta.xml) or just leftover files
|
return JsonResponse({
|
||||||
metaXmlPath = os.path.join(pluginInstalled, 'meta.xml')
|
'success': False,
|
||||||
if os.path.exists(metaXmlPath):
|
'error': f'Plugin already installed: {plugin_name}'
|
||||||
return JsonResponse({
|
}, status=400)
|
||||||
'success': False,
|
|
||||||
'error': f'Plugin already installed: {plugin_name}'
|
|
||||||
}, status=400)
|
|
||||||
else:
|
|
||||||
# Directory exists but no meta.xml - likely incomplete/uninstalled
|
|
||||||
# Try to clean it up first using pluginInstaller.removeFiles which handles permissions
|
|
||||||
try:
|
|
||||||
from pluginInstaller.pluginInstaller import pluginInstaller
|
|
||||||
pluginInstaller.removeFiles(plugin_name)
|
|
||||||
logging.writeToFile(f'Cleaned up incomplete plugin directory: {plugin_name}')
|
|
||||||
except Exception as e:
|
|
||||||
logging.writeToFile(f'Warning: Could not clean up incomplete directory: {str(e)}')
|
|
||||||
# Try fallback: use system rm -rf
|
|
||||||
try:
|
|
||||||
import subprocess
|
|
||||||
result = subprocess.run(['rm', '-rf', pluginInstalled], capture_output=True, text=True, timeout=30)
|
|
||||||
if result.returncode == 0:
|
|
||||||
logging.writeToFile(f'Cleaned up incomplete plugin directory using rm -rf: {plugin_name}')
|
|
||||||
else:
|
|
||||||
raise Exception(f"rm -rf failed: {result.stderr}")
|
|
||||||
except Exception as e2:
|
|
||||||
logging.writeToFile(f'Error: Both cleanup methods failed: {str(e)}, {str(e2)}')
|
|
||||||
return JsonResponse({
|
|
||||||
'success': False,
|
|
||||||
'error': f'Incomplete plugin directory found. Please uninstall first or manually remove: {pluginInstalled}'
|
|
||||||
}, status=400)
|
|
||||||
|
|
||||||
# Create zip file for installation (pluginInstaller expects a zip)
|
# Create zip file for installation (pluginInstaller expects a zip)
|
||||||
import tempfile
|
import tempfile
|
||||||
@@ -570,17 +462,6 @@ def install_plugin(request, plugin_name):
|
|||||||
# Set plugin to enabled by default after installation
|
# Set plugin to enabled by default after installation
|
||||||
_set_plugin_state(plugin_name, True)
|
_set_plugin_state(plugin_name, True)
|
||||||
|
|
||||||
# Restart lscpd service to ensure plugin loads immediately
|
|
||||||
try:
|
|
||||||
logging.writeToFile(f"Restarting lscpd service after plugin installation...")
|
|
||||||
subprocess.run(['systemctl', 'restart', 'lscpd'], check=True, timeout=30)
|
|
||||||
logging.writeToFile(f"lscpd service restarted successfully")
|
|
||||||
except subprocess.TimeoutExpired:
|
|
||||||
logging.writeToFile(f"Warning: lscpd restart timed out, but continuing...")
|
|
||||||
except Exception as restart_error:
|
|
||||||
logging.writeToFile(f"Warning: Failed to restart lscpd: {str(restart_error)}")
|
|
||||||
# Don't fail installation if restart fails, just log it
|
|
||||||
|
|
||||||
return JsonResponse({
|
return JsonResponse({
|
||||||
'success': True,
|
'success': True,
|
||||||
'message': f'Plugin {plugin_name} installed successfully'
|
'message': f'Plugin {plugin_name} installed successfully'
|
||||||
@@ -628,77 +509,7 @@ def uninstall_plugin(request, plugin_name):
|
|||||||
pluginInstaller.removeMigrations(plugin_name)
|
pluginInstaller.removeMigrations(plugin_name)
|
||||||
|
|
||||||
# Remove installed directory (but keep source in /home/cyberpanel/plugins/)
|
# Remove installed directory (but keep source in /home/cyberpanel/plugins/)
|
||||||
try:
|
pluginInstaller.removeFiles(plugin_name)
|
||||||
pluginInstaller.removeFiles(plugin_name)
|
|
||||||
|
|
||||||
# Verify removal succeeded
|
|
||||||
pluginInstalled = '/usr/local/CyberCP/' + plugin_name
|
|
||||||
if os.path.exists(pluginInstalled):
|
|
||||||
# Removal failed - try again with more aggressive methods
|
|
||||||
logging.writeToFile(f'Plugin directory still exists after removeFiles, trying ProcessUtilities')
|
|
||||||
try:
|
|
||||||
from plogical.processUtilities import ProcessUtilities
|
|
||||||
import time
|
|
||||||
import subprocess
|
|
||||||
|
|
||||||
# First, try to fix permissions with ProcessUtilities
|
|
||||||
chown_cmd = f'chown -R cyberpanel:cyberpanel {pluginInstalled}'
|
|
||||||
chown_result = ProcessUtilities.normalExecutioner(chown_cmd)
|
|
||||||
if chown_result == 0:
|
|
||||||
logging.writeToFile(f'Warning: chown failed for {pluginInstalled}')
|
|
||||||
|
|
||||||
chmod_cmd = f'chmod -R u+rwX,go+rX {pluginInstalled}'
|
|
||||||
chmod_result = ProcessUtilities.normalExecutioner(chmod_cmd)
|
|
||||||
if chmod_result == 0:
|
|
||||||
logging.writeToFile(f'Warning: chmod failed for {pluginInstalled}')
|
|
||||||
|
|
||||||
# Then try to remove with ProcessUtilities
|
|
||||||
rm_cmd = f'rm -rf {pluginInstalled}'
|
|
||||||
rm_result = ProcessUtilities.normalExecutioner(rm_cmd)
|
|
||||||
|
|
||||||
# Wait a moment for filesystem to sync
|
|
||||||
time.sleep(0.5)
|
|
||||||
|
|
||||||
# Verify again
|
|
||||||
if os.path.exists(pluginInstalled):
|
|
||||||
# ProcessUtilities failed - try subprocess directly
|
|
||||||
logging.writeToFile(f'ProcessUtilities removal failed (exit code: {rm_result}), trying subprocess')
|
|
||||||
try:
|
|
||||||
# Check if we're root
|
|
||||||
is_root = os.geteuid() == 0 if hasattr(os, 'geteuid') else False
|
|
||||||
if is_root:
|
|
||||||
# Running as root - use subprocess directly
|
|
||||||
result = subprocess.run(
|
|
||||||
['rm', '-rf', pluginInstalled],
|
|
||||||
capture_output=True, text=True, timeout=30
|
|
||||||
)
|
|
||||||
time.sleep(0.5)
|
|
||||||
if os.path.exists(pluginInstalled):
|
|
||||||
# Last resort: try with shell=True
|
|
||||||
logging.writeToFile(f'Subprocess rm -rf failed, trying with shell=True')
|
|
||||||
subprocess.run(f'rm -rf {pluginInstalled}', shell=True, timeout=30)
|
|
||||||
time.sleep(0.5)
|
|
||||||
if os.path.exists(pluginInstalled):
|
|
||||||
raise Exception(f'Plugin directory still exists after all removal attempts: {pluginInstalled}')
|
|
||||||
else:
|
|
||||||
# Not root - try with shell=True which might work better
|
|
||||||
logging.writeToFile(f'Not root, trying rm -rf with shell=True')
|
|
||||||
subprocess.run(f'rm -rf {pluginInstalled}', shell=True, timeout=30)
|
|
||||||
time.sleep(0.5)
|
|
||||||
if os.path.exists(pluginInstalled):
|
|
||||||
raise Exception(f'Plugin directory still exists after ProcessUtilities and subprocess removal: {pluginInstalled}')
|
|
||||||
except Exception as e3:
|
|
||||||
logging.writeToFile(f'Subprocess removal also failed: {str(e3)}')
|
|
||||||
raise Exception(f'Failed to remove plugin directory. Tried removeFiles(), ProcessUtilities, and subprocess. Directory still exists: {pluginInstalled}')
|
|
||||||
except Exception as e2:
|
|
||||||
logging.writeToFile(f'ProcessUtilities removal failed: {str(e2)}')
|
|
||||||
raise Exception(f'Failed to remove plugin directory. Tried removeFiles() and ProcessUtilities. Directory still exists: {pluginInstalled}')
|
|
||||||
except Exception as e:
|
|
||||||
logging.writeToFile(f'Error removing plugin files: {str(e)}')
|
|
||||||
return JsonResponse({
|
|
||||||
'success': False,
|
|
||||||
'error': f'Failed to remove plugin directory: {str(e)}'
|
|
||||||
}, status=500)
|
|
||||||
|
|
||||||
# DON'T call informCyberPanelRemoval - we want to keep the source directory
|
# DON'T call informCyberPanelRemoval - we want to keep the source directory
|
||||||
# so users can reinstall the plugin later
|
# so users can reinstall the plugin later
|
||||||
@@ -852,156 +663,51 @@ def _enrich_store_plugins(plugins):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# Check if plugin is installed locally
|
# Check if plugin is installed locally
|
||||||
# IMPORTANT: Only check installed location, not source location
|
# Plugin is only considered "installed" if it exists in /usr/local/CyberCP/
|
||||||
# Plugins in /home/cyberpanel/plugins/ are just source files, not installed plugins
|
# Source directory presence doesn't mean installed - it just means the source files are available
|
||||||
installed_path = os.path.join(plugin_install_dir, plugin_dir)
|
installed_path = os.path.join(plugin_install_dir, plugin_dir)
|
||||||
installed_meta = os.path.join(installed_path, 'meta.xml')
|
|
||||||
|
|
||||||
# Plugin is only considered installed if:
|
plugin['installed'] = os.path.exists(installed_path)
|
||||||
# 1. The plugin directory exists in /usr/local/CyberCP/
|
|
||||||
# 2. AND meta.xml exists (indicates actual installation, not just leftover files)
|
|
||||||
plugin['installed'] = os.path.exists(installed_path) and os.path.exists(installed_meta)
|
|
||||||
|
|
||||||
# Check if plugin is enabled (only if installed)
|
# Check if plugin is enabled (only if installed)
|
||||||
if plugin['installed']:
|
if plugin['installed']:
|
||||||
try:
|
plugin['enabled'] = _is_plugin_enabled(plugin_dir)
|
||||||
plugin['enabled'] = _is_plugin_enabled(plugin_dir)
|
|
||||||
except Exception as e:
|
|
||||||
logging.writeToFile(f"Error checking enabled status for {plugin_dir}: {str(e)}")
|
|
||||||
plugin['enabled'] = False # Default to disabled on error
|
|
||||||
else:
|
else:
|
||||||
plugin['enabled'] = False
|
plugin['enabled'] = False
|
||||||
|
|
||||||
# Ensure is_paid field exists and is properly set (default to False if not set or invalid)
|
# Ensure is_paid field exists and is properly set (default to False if not set or invalid)
|
||||||
# CRITICAL FIX: Always check local meta.xml FIRST as source of truth
|
# Handle all possible cases: missing, None, empty string, string values, boolean
|
||||||
# This ensures cache entries without is_paid are properly enriched
|
is_paid_value = plugin.get('is_paid', False)
|
||||||
# Check installed location first, then fallback to source location for metadata
|
|
||||||
meta_path = None
|
|
||||||
source_path = os.path.join(plugin_source_dir, plugin_dir)
|
|
||||||
if os.path.exists(installed_meta):
|
|
||||||
meta_path = installed_meta
|
|
||||||
elif os.path.exists(source_path):
|
|
||||||
source_meta = os.path.join(source_path, 'meta.xml')
|
|
||||||
if os.path.exists(source_meta):
|
|
||||||
meta_path = source_meta
|
|
||||||
|
|
||||||
# If we have a local meta.xml, use it as the source of truth (most reliable)
|
# Normalize is_paid to boolean
|
||||||
if meta_path and os.path.exists(meta_path):
|
if is_paid_value is None or is_paid_value == '' or is_paid_value == 'false' or is_paid_value == 'False' or is_paid_value == '0':
|
||||||
try:
|
plugin['is_paid'] = False
|
||||||
pluginMetaData = ElementTree.parse(meta_path)
|
elif is_paid_value is True or is_paid_value == 'true' or is_paid_value == 'True' or is_paid_value == '1' or str(is_paid_value).lower() == 'true':
|
||||||
root = pluginMetaData.getroot()
|
plugin['is_paid'] = True
|
||||||
paid_elem = root.find('paid')
|
elif 'is_paid' not in plugin or plugin.get('is_paid') is None:
|
||||||
if paid_elem is not None and paid_elem.text and str(paid_elem.text).strip().lower() == 'true':
|
# Try to check from local meta.xml if available
|
||||||
plugin['is_paid'] = True
|
meta_path = None
|
||||||
|
if os.path.exists(installed_path):
|
||||||
# Check for PayPal payment method first
|
meta_path = os.path.join(installed_path, 'meta.xml')
|
||||||
paypal_me_elem = root.find('paypal_me_url')
|
elif os.path.exists(source_path):
|
||||||
paypal_payment_link_elem = root.find('paypal_payment_link')
|
meta_path = os.path.join(source_path, 'meta.xml')
|
||||||
|
|
||||||
if (paypal_me_elem is not None and paypal_me_elem.text) or (paypal_payment_link_elem is not None and paypal_payment_link_elem.text):
|
if meta_path and os.path.exists(meta_path):
|
||||||
# This is a PayPal plugin
|
try:
|
||||||
plugin['payment_type'] = 'paypal'
|
pluginMetaData = ElementTree.parse(meta_path)
|
||||||
plugin['paypal_me_url'] = paypal_me_elem.text if paypal_me_elem is not None and paypal_me_elem.text else None
|
root = pluginMetaData.getroot()
|
||||||
plugin['paypal_payment_link'] = paypal_payment_link_elem.text if paypal_payment_link_elem is not None and paypal_payment_link_elem.text else None
|
paid_elem = root.find('paid')
|
||||||
plugin['patreon_tier'] = None
|
if paid_elem is not None and paid_elem.text and paid_elem.text.lower() == 'true':
|
||||||
plugin['patreon_url'] = None
|
plugin['is_paid'] = True
|
||||||
else:
|
else:
|
||||||
# This is a Patreon plugin (default/fallback)
|
plugin['is_paid'] = False
|
||||||
plugin['payment_type'] = 'patreon'
|
except:
|
||||||
patreon_tier_elem = root.find('patreon_tier')
|
|
||||||
plugin['patreon_tier'] = patreon_tier_elem.text if patreon_tier_elem is not None and patreon_tier_elem.text else 'CyberPanel Paid Plugin'
|
|
||||||
patreon_url_elem = root.find('patreon_url')
|
|
||||||
plugin['patreon_url'] = patreon_url_elem.text if patreon_url_elem is not None and patreon_url_elem.text else 'https://www.patreon.com/c/newstargeted/membership'
|
|
||||||
plugin['paypal_me_url'] = None
|
|
||||||
plugin['paypal_payment_link'] = None
|
|
||||||
else:
|
|
||||||
plugin['is_paid'] = False
|
plugin['is_paid'] = False
|
||||||
plugin['payment_type'] = None
|
|
||||||
plugin['patreon_tier'] = None
|
|
||||||
plugin['patreon_url'] = None
|
|
||||||
plugin['paypal_me_url'] = None
|
|
||||||
plugin['paypal_payment_link'] = None
|
|
||||||
except Exception as e:
|
|
||||||
logging.writeToFile(f"Error parsing meta.xml for {plugin_dir} in _enrich_store_plugins: {str(e)}")
|
|
||||||
# Fall back to normalizing existing value
|
|
||||||
is_paid_value = plugin.get('is_paid', False)
|
|
||||||
if is_paid_value is None or is_paid_value == '' or is_paid_value == 'false' or is_paid_value == 'False' or is_paid_value == '0':
|
|
||||||
plugin['is_paid'] = False
|
|
||||||
elif is_paid_value is True or is_paid_value == 'true' or is_paid_value == 'True' or is_paid_value == '1' or str(is_paid_value).lower() == 'true':
|
|
||||||
plugin['is_paid'] = True
|
|
||||||
else:
|
|
||||||
plugin['is_paid'] = False
|
|
||||||
else:
|
|
||||||
# No local meta.xml, normalize existing value from cache/API
|
|
||||||
is_paid_value = plugin.get('is_paid', False)
|
|
||||||
if is_paid_value is None or is_paid_value == '' or is_paid_value == 'false' or is_paid_value == 'False' or is_paid_value == '0':
|
|
||||||
plugin['is_paid'] = False
|
|
||||||
elif is_paid_value is True or is_paid_value == 'true' or is_paid_value == 'True' or is_paid_value == '1' or str(is_paid_value).lower() == 'true':
|
|
||||||
plugin['is_paid'] = True
|
|
||||||
else:
|
else:
|
||||||
plugin['is_paid'] = False # Default to free if we can't determine
|
plugin['is_paid'] = False # Default to free if we can't determine
|
||||||
|
else:
|
||||||
# Ensure it's a proper boolean (not string or other type) - CRITICAL for consistency
|
# Already set, but ensure it's boolean
|
||||||
if plugin['is_paid'] not in [True, False]:
|
plugin['is_paid'] = bool(plugin['is_paid']) if plugin['is_paid'] not in [True, False] else plugin['is_paid']
|
||||||
plugin['is_paid'] = bool(plugin['is_paid'])
|
|
||||||
# Final safety check - force boolean type (defensive programming)
|
|
||||||
plugin['is_paid'] = True if plugin['is_paid'] is True else False
|
|
||||||
|
|
||||||
# Ensure payment_type is set if is_paid is True
|
|
||||||
if plugin['is_paid'] and 'payment_type' not in plugin:
|
|
||||||
# Default to patreon if payment_type not set
|
|
||||||
plugin['payment_type'] = 'patreon'
|
|
||||||
|
|
||||||
# Calculate NEW and Stale badges based on modify_date
|
|
||||||
modify_date_str = plugin.get('modify_date', 'N/A')
|
|
||||||
plugin['is_new'] = False
|
|
||||||
plugin['is_stale'] = False
|
|
||||||
|
|
||||||
if modify_date_str and modify_date_str != 'N/A':
|
|
||||||
try:
|
|
||||||
# Parse the modify_date (could be various formats)
|
|
||||||
modify_date = None
|
|
||||||
if isinstance(modify_date_str, str):
|
|
||||||
# Handle ISO format with timezone (from GitHub API)
|
|
||||||
if 'T' in modify_date_str:
|
|
||||||
# ISO format: 2026-01-25T04:24:52Z or 2026-01-25T04:24:52+00:00
|
|
||||||
try:
|
|
||||||
# Remove timezone info for simpler parsing
|
|
||||||
date_part = modify_date_str.split('T')[0]
|
|
||||||
time_part = modify_date_str.split('T')[1].split('+')[0].split('Z')[0]
|
|
||||||
modify_date = datetime.strptime(f"{date_part} {time_part}", '%Y-%m-%d %H:%M:%S')
|
|
||||||
except:
|
|
||||||
# Fallback: try standard format
|
|
||||||
modify_date = datetime.strptime(modify_date_str[:19], '%Y-%m-%d %H:%M:%S')
|
|
||||||
else:
|
|
||||||
# Standard format: YYYY-MM-DD HH:MM:SS
|
|
||||||
modify_date = datetime.strptime(modify_date_str, '%Y-%m-%d %H:%M:%S')
|
|
||||||
else:
|
|
||||||
modify_date = modify_date_str
|
|
||||||
|
|
||||||
if modify_date:
|
|
||||||
now = datetime.now()
|
|
||||||
# Handle timezone-aware datetime
|
|
||||||
if modify_date.tzinfo:
|
|
||||||
from datetime import timezone
|
|
||||||
modify_date = modify_date.replace(tzinfo=None)
|
|
||||||
|
|
||||||
# Calculate time difference
|
|
||||||
time_diff = now - modify_date
|
|
||||||
|
|
||||||
# NEW: updated within last 3 months (90 days)
|
|
||||||
if time_diff.days <= 90:
|
|
||||||
plugin['is_new'] = True
|
|
||||||
|
|
||||||
# Stale: not updated in last 2 years (730 days)
|
|
||||||
if time_diff.days > 730:
|
|
||||||
plugin['is_stale'] = True
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logging.writeToFile(f"Error calculating NEW/Stale status for {plugin_dir}: {str(e)}")
|
|
||||||
# Default to not new and not stale if parsing fails
|
|
||||||
plugin['is_new'] = False
|
|
||||||
plugin['is_stale'] = False
|
|
||||||
|
|
||||||
enriched.append(plugin)
|
enriched.append(plugin)
|
||||||
|
|
||||||
@@ -1074,79 +780,19 @@ def _fetch_plugins_from_github():
|
|||||||
logging.writeToFile(f"Could not fetch commit date for {plugin_name}: {str(e)}")
|
logging.writeToFile(f"Could not fetch commit date for {plugin_name}: {str(e)}")
|
||||||
modify_date = 'N/A'
|
modify_date = 'N/A'
|
||||||
|
|
||||||
# Calculate NEW and Stale badges based on modify_date
|
# Extract paid plugin information
|
||||||
is_new = False
|
|
||||||
is_stale = False
|
|
||||||
|
|
||||||
if modify_date and modify_date != 'N/A':
|
|
||||||
try:
|
|
||||||
# Parse the modify_date
|
|
||||||
modify_date_obj = None
|
|
||||||
if isinstance(modify_date, str):
|
|
||||||
if 'T' in modify_date:
|
|
||||||
# ISO format: 2026-01-25T04:24:52Z or 2026-01-25T04:24:52+00:00
|
|
||||||
try:
|
|
||||||
date_part = modify_date.split('T')[0]
|
|
||||||
time_part = modify_date.split('T')[1].split('+')[0].split('Z')[0]
|
|
||||||
modify_date_obj = datetime.strptime(f"{date_part} {time_part}", '%Y-%m-%d %H:%M:%S')
|
|
||||||
except:
|
|
||||||
modify_date_obj = datetime.strptime(modify_date[:19], '%Y-%m-%d %H:%M:%S')
|
|
||||||
else:
|
|
||||||
# Standard format: YYYY-MM-DD HH:MM:SS
|
|
||||||
modify_date_obj = datetime.strptime(modify_date, '%Y-%m-%d %H:%M:%S')
|
|
||||||
else:
|
|
||||||
modify_date_obj = modify_date
|
|
||||||
|
|
||||||
if modify_date_obj:
|
|
||||||
now = datetime.now()
|
|
||||||
if modify_date_obj.tzinfo:
|
|
||||||
modify_date_obj = modify_date_obj.replace(tzinfo=None)
|
|
||||||
|
|
||||||
time_diff = now - modify_date_obj
|
|
||||||
|
|
||||||
# NEW: updated within last 3 months (90 days)
|
|
||||||
if time_diff.days <= 90:
|
|
||||||
is_new = True
|
|
||||||
|
|
||||||
# Stale: not updated in last 2 years (730 days)
|
|
||||||
if time_diff.days > 730:
|
|
||||||
is_stale = True
|
|
||||||
except Exception as e:
|
|
||||||
logging.writeToFile(f"Error calculating NEW/Stale for {plugin_name}: {str(e)}")
|
|
||||||
|
|
||||||
# Extract paid plugin information - support both Patreon and PayPal
|
|
||||||
paid_elem = root.find('paid')
|
paid_elem = root.find('paid')
|
||||||
|
patreon_tier_elem = root.find('patreon_tier')
|
||||||
|
|
||||||
is_paid = False
|
is_paid = False
|
||||||
patreon_tier = None
|
patreon_tier = None
|
||||||
patreon_url = None
|
patreon_url = None
|
||||||
paypal_me_url = None
|
|
||||||
paypal_payment_link = None
|
|
||||||
payment_type = None
|
|
||||||
|
|
||||||
if paid_elem is not None and paid_elem.text and str(paid_elem.text).strip().lower() == 'true':
|
if paid_elem is not None and paid_elem.text and paid_elem.text.lower() == 'true':
|
||||||
is_paid = True
|
is_paid = True
|
||||||
|
patreon_tier = patreon_tier_elem.text if patreon_tier_elem is not None and patreon_tier_elem.text else 'CyberPanel Paid Plugin'
|
||||||
# Check for PayPal payment method first
|
patreon_url_elem = root.find('patreon_url')
|
||||||
paypal_me_elem = root.find('paypal_me_url')
|
patreon_url = patreon_url_elem.text if patreon_url_elem is not None else 'https://www.patreon.com/c/newstargeted/membership'
|
||||||
paypal_payment_link_elem = root.find('paypal_payment_link')
|
|
||||||
|
|
||||||
if (paypal_me_elem is not None and paypal_me_elem.text) or (paypal_payment_link_elem is not None and paypal_payment_link_elem.text):
|
|
||||||
# This is a PayPal plugin
|
|
||||||
payment_type = 'paypal'
|
|
||||||
paypal_me_url = paypal_me_elem.text if paypal_me_elem is not None and paypal_me_elem.text else None
|
|
||||||
paypal_payment_link = paypal_payment_link_elem.text if paypal_payment_link_elem is not None and paypal_payment_link_elem.text else None
|
|
||||||
patreon_tier = None
|
|
||||||
patreon_url = None
|
|
||||||
else:
|
|
||||||
# This is a Patreon plugin (default/fallback)
|
|
||||||
payment_type = 'patreon'
|
|
||||||
patreon_tier_elem = root.find('patreon_tier')
|
|
||||||
patreon_tier = patreon_tier_elem.text if patreon_tier_elem is not None and patreon_tier_elem.text else 'CyberPanel Paid Plugin'
|
|
||||||
patreon_url_elem = root.find('patreon_url')
|
|
||||||
patreon_url = patreon_url_elem.text if patreon_url_elem is not None and patreon_url_elem.text else 'https://www.patreon.com/c/newstargeted/membership'
|
|
||||||
paypal_me_url = None
|
|
||||||
paypal_payment_link = None
|
|
||||||
|
|
||||||
plugin_data = {
|
plugin_data = {
|
||||||
'plugin_dir': plugin_name,
|
'plugin_dir': plugin_name,
|
||||||
@@ -1160,14 +806,9 @@ def _fetch_plugins_from_github():
|
|||||||
'github_url': f'https://github.com/master3395/cyberpanel-plugins/tree/main/{plugin_name}',
|
'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}',
|
'about_url': f'https://github.com/master3395/cyberpanel-plugins/tree/main/{plugin_name}',
|
||||||
'modify_date': modify_date,
|
'modify_date': modify_date,
|
||||||
'is_paid': bool(is_paid), # Force boolean type
|
'is_paid': is_paid,
|
||||||
'patreon_tier': patreon_tier,
|
'patreon_tier': patreon_tier,
|
||||||
'patreon_url': patreon_url,
|
'patreon_url': patreon_url
|
||||||
'paypal_me_url': paypal_me_url,
|
|
||||||
'paypal_payment_link': paypal_payment_link,
|
|
||||||
'payment_type': payment_type,
|
|
||||||
'is_new': is_new,
|
|
||||||
'is_stale': is_stale
|
|
||||||
}
|
}
|
||||||
|
|
||||||
plugins.append(plugin_data)
|
plugins.append(plugin_data)
|
||||||
@@ -1213,120 +854,53 @@ def _fetch_plugins_from_github():
|
|||||||
@require_http_methods(["GET"])
|
@require_http_methods(["GET"])
|
||||||
def fetch_plugin_store(request):
|
def fetch_plugin_store(request):
|
||||||
"""Fetch plugins from the plugin store with caching"""
|
"""Fetch plugins from the plugin store with caching"""
|
||||||
try:
|
mailUtilities.checkHome()
|
||||||
mailUtilities.checkHome()
|
|
||||||
except Exception as e:
|
|
||||||
logging.writeToFile(f"Warning in mailUtilities.checkHome: {str(e)}")
|
|
||||||
|
|
||||||
# Add cache-busting headers to prevent browser caching
|
# Try to get from cache first
|
||||||
response_headers = {
|
cached_plugins = _get_cached_plugins()
|
||||||
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
if cached_plugins is not None:
|
||||||
'Pragma': 'no-cache',
|
# Enrich cached plugins with installed/enabled status
|
||||||
'Expires': '0'
|
enriched_plugins = _enrich_store_plugins(cached_plugins)
|
||||||
}
|
return JsonResponse({
|
||||||
|
'success': True,
|
||||||
|
'plugins': enriched_plugins,
|
||||||
|
'cached': True
|
||||||
|
})
|
||||||
|
|
||||||
|
# Cache miss or expired - fetch from GitHub
|
||||||
try:
|
try:
|
||||||
# Try to get from cache first
|
plugins = _fetch_plugins_from_github()
|
||||||
cached_plugins = _get_cached_plugins()
|
|
||||||
if cached_plugins is not None:
|
|
||||||
# Sort plugins deterministically by name to prevent order changes
|
|
||||||
cached_plugins.sort(key=lambda x: x.get('name', '').lower())
|
|
||||||
|
|
||||||
# Enrich cached plugins with installed/enabled status
|
|
||||||
try:
|
|
||||||
enriched_plugins = _enrich_store_plugins(cached_plugins)
|
|
||||||
except Exception as enrich_error:
|
|
||||||
logging.writeToFile(f"Error enriching cached plugins: {str(enrich_error)}")
|
|
||||||
# Return cached plugins without enrichment if enrichment fails
|
|
||||||
enriched_plugins = cached_plugins
|
|
||||||
for plugin in enriched_plugins:
|
|
||||||
plugin.setdefault('installed', False)
|
|
||||||
plugin.setdefault('enabled', False)
|
|
||||||
plugin.setdefault('is_paid', False)
|
|
||||||
|
|
||||||
response = JsonResponse({
|
|
||||||
'success': True,
|
|
||||||
'plugins': enriched_plugins,
|
|
||||||
'cached': True
|
|
||||||
}, json_dumps_params={'ensure_ascii': False})
|
|
||||||
# Add headers
|
|
||||||
for key, value in response_headers.items():
|
|
||||||
response[key] = value
|
|
||||||
return response
|
|
||||||
|
|
||||||
# Cache miss or expired - fetch from GitHub
|
# Enrich plugins with installed/enabled status
|
||||||
try:
|
enriched_plugins = _enrich_store_plugins(plugins)
|
||||||
plugins = _fetch_plugins_from_github()
|
|
||||||
|
# Save to cache (save original, not enriched, to keep cache clean)
|
||||||
# Sort plugins deterministically by name to prevent order changes
|
if plugins:
|
||||||
plugins.sort(key=lambda x: x.get('name', '').lower())
|
_save_plugins_cache(plugins)
|
||||||
|
|
||||||
# Enrich plugins with installed/enabled status
|
return JsonResponse({
|
||||||
try:
|
'success': True,
|
||||||
enriched_plugins = _enrich_store_plugins(plugins)
|
'plugins': enriched_plugins,
|
||||||
except Exception as enrich_error:
|
'cached': False
|
||||||
logging.writeToFile(f"Error enriching plugins from GitHub: {str(enrich_error)}")
|
})
|
||||||
# Return plugins without enrichment if enrichment fails
|
|
||||||
enriched_plugins = plugins
|
except Exception as e:
|
||||||
for plugin in enriched_plugins:
|
error_message = str(e)
|
||||||
plugin.setdefault('installed', False)
|
|
||||||
plugin.setdefault('enabled', False)
|
# If rate limited, try to use stale cache as fallback
|
||||||
plugin.setdefault('is_paid', False)
|
if '403' in error_message or 'rate limit' in error_message.lower():
|
||||||
|
stale_cache = _get_cached_plugins(allow_expired=True) # Get cache even if expired
|
||||||
# Save to cache (save original, not enriched, to keep cache clean)
|
if stale_cache is not None:
|
||||||
if plugins:
|
logging.writeToFile("Using stale cache due to rate limit")
|
||||||
_save_plugins_cache(plugins)
|
enriched_plugins = _enrich_store_plugins(stale_cache)
|
||||||
|
return JsonResponse({
|
||||||
response = JsonResponse({
|
'success': True,
|
||||||
'success': True,
|
'plugins': enriched_plugins,
|
||||||
'plugins': enriched_plugins,
|
'cached': True,
|
||||||
'cached': False
|
'warning': 'Using cached data due to GitHub rate limit. Data may be outdated.'
|
||||||
}, json_dumps_params={'ensure_ascii': False})
|
})
|
||||||
# Add cache-busting headers
|
|
||||||
for key, value in response_headers.items():
|
# No cache available, return error
|
||||||
response[key] = value
|
|
||||||
return response
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
error_message = str(e)
|
|
||||||
|
|
||||||
# If rate limited, try to use stale cache as fallback
|
|
||||||
if '403' in error_message or 'rate limit' in error_message.lower():
|
|
||||||
stale_cache = _get_cached_plugins(allow_expired=True) # Get cache even if expired
|
|
||||||
if stale_cache is not None:
|
|
||||||
logging.writeToFile("Using stale cache due to rate limit")
|
|
||||||
# Sort plugins deterministically by name to prevent order changes
|
|
||||||
stale_cache.sort(key=lambda x: x.get('name', '').lower())
|
|
||||||
try:
|
|
||||||
enriched_plugins = _enrich_store_plugins(stale_cache)
|
|
||||||
except Exception as enrich_error:
|
|
||||||
logging.writeToFile(f"Error enriching stale cache: {str(enrich_error)}")
|
|
||||||
enriched_plugins = stale_cache
|
|
||||||
for plugin in enriched_plugins:
|
|
||||||
plugin.setdefault('installed', False)
|
|
||||||
plugin.setdefault('enabled', False)
|
|
||||||
plugin.setdefault('is_paid', False)
|
|
||||||
response = JsonResponse({
|
|
||||||
'success': True,
|
|
||||||
'plugins': enriched_plugins,
|
|
||||||
'cached': True,
|
|
||||||
'warning': 'Using cached data due to GitHub rate limit. Data may be outdated.'
|
|
||||||
}, json_dumps_params={'ensure_ascii': False})
|
|
||||||
# Add cache-busting headers
|
|
||||||
for key, value in response_headers.items():
|
|
||||||
response[key] = value
|
|
||||||
return response
|
|
||||||
|
|
||||||
# No cache available, return error
|
|
||||||
return JsonResponse({
|
|
||||||
'success': False,
|
|
||||||
'error': error_message,
|
|
||||||
'plugins': []
|
|
||||||
}, status=500)
|
|
||||||
except Exception as outer_error:
|
|
||||||
# Catch any other unexpected errors
|
|
||||||
error_message = str(outer_error)
|
|
||||||
logging.writeToFile(f"Unexpected error in fetch_plugin_store: {error_message}")
|
|
||||||
return JsonResponse({
|
return JsonResponse({
|
||||||
'success': False,
|
'success': False,
|
||||||
'error': error_message,
|
'error': error_message,
|
||||||
@@ -1340,38 +914,13 @@ def install_from_store(request, plugin_name):
|
|||||||
mailUtilities.checkHome()
|
mailUtilities.checkHome()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Check if already installed (must have meta.xml to be considered installed)
|
# Check if already installed
|
||||||
pluginInstalled = '/usr/local/CyberCP/' + plugin_name
|
pluginInstalled = '/usr/local/CyberCP/' + plugin_name
|
||||||
if os.path.exists(pluginInstalled):
|
if os.path.exists(pluginInstalled):
|
||||||
# Check if it's a valid installation (has meta.xml) or just leftover files
|
return JsonResponse({
|
||||||
metaXmlPath = os.path.join(pluginInstalled, 'meta.xml')
|
'success': False,
|
||||||
if os.path.exists(metaXmlPath):
|
'error': f'Plugin already installed: {plugin_name}'
|
||||||
return JsonResponse({
|
}, status=400)
|
||||||
'success': False,
|
|
||||||
'error': f'Plugin already installed: {plugin_name}'
|
|
||||||
}, status=400)
|
|
||||||
else:
|
|
||||||
# Directory exists but no meta.xml - likely incomplete/uninstalled
|
|
||||||
# Try to clean it up first using pluginInstaller.removeFiles which handles permissions
|
|
||||||
# pluginInstaller is already imported at module level, no need to import again
|
|
||||||
try:
|
|
||||||
pluginInstaller.removeFiles(plugin_name)
|
|
||||||
logging.writeToFile(f'Cleaned up incomplete plugin directory: {plugin_name}')
|
|
||||||
except Exception as e:
|
|
||||||
logging.writeToFile(f'Warning: Could not clean up incomplete directory: {str(e)}')
|
|
||||||
# Try fallback: use system rm -rf
|
|
||||||
try:
|
|
||||||
result = subprocess.run(['rm', '-rf', pluginInstalled], capture_output=True, text=True, timeout=30)
|
|
||||||
if result.returncode == 0:
|
|
||||||
logging.writeToFile(f'Cleaned up incomplete plugin directory using rm -rf: {plugin_name}')
|
|
||||||
else:
|
|
||||||
raise Exception(f"rm -rf failed: {result.stderr}")
|
|
||||||
except Exception as e2:
|
|
||||||
logging.writeToFile(f'Error: Both cleanup methods failed: {str(e)}, {str(e2)}')
|
|
||||||
return JsonResponse({
|
|
||||||
'success': False,
|
|
||||||
'error': f'Incomplete plugin directory found. Please uninstall first or manually remove: {pluginInstalled}'
|
|
||||||
}, status=400)
|
|
||||||
|
|
||||||
# Download plugin from GitHub
|
# Download plugin from GitHub
|
||||||
import tempfile
|
import tempfile
|
||||||
@@ -1493,21 +1042,9 @@ def install_from_store(request, plugin_name):
|
|||||||
else:
|
else:
|
||||||
raise Exception(f'Plugin installation failed: {error_msg}')
|
raise Exception(f'Plugin installation failed: {error_msg}')
|
||||||
|
|
||||||
# Wait a moment for file system to sync
|
# Wait a moment for file system to sync and service to restart
|
||||||
import time
|
import time
|
||||||
time.sleep(2) # Wait for file system sync
|
time.sleep(3) # Increased wait time for file system sync
|
||||||
|
|
||||||
# Restart lscpd service to ensure plugin loads immediately
|
|
||||||
try:
|
|
||||||
logging.writeToFile(f"Restarting lscpd service after plugin installation...")
|
|
||||||
subprocess.run(['systemctl', 'restart', 'lscpd'], check=True, timeout=30)
|
|
||||||
logging.writeToFile(f"lscpd service restarted successfully")
|
|
||||||
time.sleep(2) # Wait for service to fully restart
|
|
||||||
except subprocess.TimeoutExpired:
|
|
||||||
logging.writeToFile(f"Warning: lscpd restart timed out, but continuing...")
|
|
||||||
except Exception as restart_error:
|
|
||||||
logging.writeToFile(f"Warning: Failed to restart lscpd: {str(restart_error)}")
|
|
||||||
# Don't fail installation if restart fails, just log it
|
|
||||||
|
|
||||||
# Verify plugin was actually installed
|
# Verify plugin was actually installed
|
||||||
pluginInstalled = '/usr/local/CyberCP/' + plugin_name
|
pluginInstalled = '/usr/local/CyberCP/' + plugin_name
|
||||||
|
|||||||
Reference in New Issue
Block a user