mirror of
https://github.com/usmannasir/cyberpanel.git
synced 2026-07-25 21:50:42 +02:00
Plugins: fix installed/active counts, 404s, metadata sync, install fallback
- pluginHolder/views: use dir+meta.xml for installed count; exclude core apps; repair pass to restore meta.xml from source or GitHub; ensure_plugin_meta_xml falls back to GitHub when source missing; cap active <= installed - pluginHolder/urls: include plugin routes for all on-disk plugins (not only INSTALLED_APPS) so /plugins/<name>/settings/ works after install - pluginHolder/plugins.html: Install button tries local then store (GitHub) - CyberCP/settings: sync INSTALLED_APPS with plugin dirs on disk (meta.xml+urls.py) Author: master3395
This commit is contained in:
@@ -209,6 +209,9 @@ STATIC_ROOT = os.path.join(BASE_DIR, "static/")
|
|||||||
|
|
||||||
STATIC_URL = '/static/'
|
STATIC_URL = '/static/'
|
||||||
|
|
||||||
|
# Panel public directory (SnappyMail, phpMyAdmin, etc.) – served so /snappymail/ and /phpmyadmin/ work when panel is behind Django
|
||||||
|
PUBLIC_ROOT = os.path.join(BASE_DIR, 'public')
|
||||||
|
|
||||||
LOCALE_PATHS = (
|
LOCALE_PATHS = (
|
||||||
os.path.join(BASE_DIR, 'locale'),
|
os.path.join(BASE_DIR, 'locale'),
|
||||||
)
|
)
|
||||||
@@ -247,4 +250,25 @@ LOGIN_REDIRECT_URL = '/'
|
|||||||
|
|
||||||
# Default primary key field type
|
# Default primary key field type
|
||||||
# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field
|
# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field
|
||||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||||
|
|
||||||
|
# Sync INSTALLED_APPS with plugins on disk so /plugins/<name>/ and /plugins/<name>/settings/ work.
|
||||||
|
# Plugins installed under /usr/local/CyberCP/ (or BASE_DIR) are added here if they have meta.xml + urls.py.
|
||||||
|
_cybercp_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
if os.path.isdir(_cybercp_root):
|
||||||
|
try:
|
||||||
|
_existing_apps = set(INSTALLED_APPS)
|
||||||
|
for _name in os.listdir(_cybercp_root):
|
||||||
|
if _name.startswith('.'):
|
||||||
|
continue
|
||||||
|
_plugin_dir = os.path.join(_cybercp_root, _name)
|
||||||
|
if not os.path.isdir(_plugin_dir):
|
||||||
|
continue
|
||||||
|
if _name in _existing_apps:
|
||||||
|
continue
|
||||||
|
if (os.path.exists(os.path.join(_plugin_dir, 'meta.xml')) and
|
||||||
|
os.path.exists(os.path.join(_plugin_dir, 'urls.py'))):
|
||||||
|
INSTALLED_APPS.append(_name)
|
||||||
|
_existing_apps.add(_name)
|
||||||
|
except (OSError, IOError):
|
||||||
|
pass
|
||||||
@@ -430,13 +430,20 @@
|
|||||||
.installed-sort-filter-bar {
|
.installed-sort-filter-bar {
|
||||||
flex-basis: 100%;
|
flex-basis: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: flex-start;
|
||||||
gap: 12px;
|
gap: 10px;
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
padding: 12px 0;
|
padding: 12px 0;
|
||||||
border-top: 1px solid var(--border-primary, #e2e8f0);
|
border-top: 1px solid var(--border-primary, #e2e8f0);
|
||||||
}
|
}
|
||||||
|
.installed-sort-filter-bar .installed-filter-row,
|
||||||
|
.installed-sort-filter-bar .installed-sort-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
.installed-sort-filter-bar .sort-label {
|
.installed-sort-filter-bar .sort-label {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@@ -1224,10 +1231,25 @@
|
|||||||
{% trans "Plugin Development Guide" %}
|
{% trans "Plugin Development Guide" %}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<!-- Sort bar for Grid and Table view (visible when grid or table is active) -->
|
<!-- Filter + Sort bar for Grid and Table view (visible when grid or table is active) -->
|
||||||
<div id="installedSortFilterBar" class="installed-sort-filter-bar" style="display: none;"
|
<div id="installedSortFilterBar" class="installed-sort-filter-bar" style="display: none;"
|
||||||
data-trans-name-asc="{% trans 'Name A-Å' %}" data-trans-name-desc="{% trans 'Name Å-A' %}"
|
data-trans-name-asc="{% trans 'Name A-Å' %}" data-trans-name-desc="{% trans 'Name Å-A' %}"
|
||||||
data-trans-date-newest="{% trans 'Date (newest)' %}" data-trans-date-oldest="{% trans 'Date (oldest)' %}">
|
data-trans-date-newest="{% trans 'Date (newest)' %}" data-trans-date-oldest="{% trans 'Date (oldest)' }}">
|
||||||
|
<div class="installed-filter-row">
|
||||||
|
<span class="sort-label">{% trans "Show:" %}</span>
|
||||||
|
<div class="filter-btns sort-btns">
|
||||||
|
<button type="button" class="sort-btn filter-btn active" data-filter="all" id="installedFilterBtnAll" onclick="setInstalledFilter('all')" title="{% trans 'Show all plugins' %}">
|
||||||
|
<i class="fas fa-th-list"></i> <span class="filter-btn-label">{% trans "All" %}</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="sort-btn filter-btn" data-filter="installed" id="installedFilterBtnInstalled" onclick="setInstalledFilter('installed')" title="{% trans 'Show only installed plugins' %}">
|
||||||
|
<i class="fas fa-check-circle"></i> <span class="filter-btn-label">{% trans "Installed only" %}</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="sort-btn filter-btn" data-filter="active" id="installedFilterBtnActive" onclick="setInstalledFilter('active')" title="{% trans 'Show only active (enabled) plugins' %}">
|
||||||
|
<i class="fas fa-power-off"></i> <span class="filter-btn-label">{% trans "Active only" %}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="installed-sort-row">
|
||||||
<span class="sort-label">{% trans "Sort by:" %}</span>
|
<span class="sort-label">{% trans "Sort by:" %}</span>
|
||||||
<div class="sort-btns">
|
<div class="sort-btns">
|
||||||
<button type="button" class="sort-btn active" data-sort-field="name" id="installedSortBtnName" onclick="toggleInstalledSort('name')" title="{% trans 'Click to toggle A-Å / Å-A' %}">
|
<button type="button" class="sort-btn active" data-sort-field="name" id="installedSortBtnName" onclick="toggleInstalledSort('name')" title="{% trans 'Click to toggle A-Å / Å-A' %}">
|
||||||
@@ -1240,13 +1262,14 @@
|
|||||||
<i class="fas fa-calendar-alt"></i> <span class="sort-btn-label">{% trans "Date (newest)" %}</span>
|
<i class="fas fa-calendar-alt"></i> <span class="sort-btn-label">{% trans "Date (newest)" %}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Grid View -->
|
<!-- Grid View -->
|
||||||
<div id="gridView" class="plugins-grid">
|
<div id="gridView" class="plugins-grid">
|
||||||
{% for plugin in plugins %}
|
{% for plugin in plugins %}
|
||||||
<div class="plugin-card" data-plugin-name="{{ plugin.name }}" data-plugin-desc="{{ plugin.desc }}" data-plugin-type="{{ plugin.type }}" data-modify-date="{{ plugin.modify_date|default:'0000-00-00 00:00:00' }}">
|
<div class="plugin-card" data-plugin-name="{{ plugin.name }}" data-plugin-desc="{{ plugin.desc }}" data-plugin-type="{{ plugin.type }}" data-modify-date="{{ plugin.modify_date|default:'0000-00-00 00:00:00' }}" data-installed="{{ plugin.installed|yesno:'true,false' }}" data-enabled="{{ plugin.enabled|yesno:'true,false' }}">
|
||||||
<div class="plugin-header">
|
<div class="plugin-header">
|
||||||
<div class="plugin-icon">
|
<div class="plugin-icon">
|
||||||
{% if plugin.type|lower == "security" %}
|
{% if plugin.type|lower == "security" %}
|
||||||
@@ -1388,7 +1411,7 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for plugin in plugins %}
|
{% for plugin in plugins %}
|
||||||
<tr data-plugin-name="{{ plugin.name }}" data-plugin-desc="{{ plugin.desc }}" data-plugin-type="{{ plugin.type }}" data-modify-date="{{ plugin.modify_date|default:'0000-00-00 00:00:00' }}">
|
<tr data-plugin-name="{{ plugin.name }}" data-plugin-desc="{{ plugin.desc }}" data-plugin-type="{{ plugin.type }}" data-modify-date="{{ plugin.modify_date|default:'0000-00-00 00:00:00' }}" data-installed="{{ plugin.installed|yesno:'true,false' }}" data-enabled="{{ plugin.enabled|yesno:'true,false' }}">
|
||||||
<td>
|
<td>
|
||||||
<strong>{{ plugin.name }}</strong>
|
<strong>{{ plugin.name }}</strong>
|
||||||
{% if plugin.freshness_badge %}
|
{% if plugin.freshness_badge %}
|
||||||
@@ -1658,6 +1681,7 @@ let currentCategory = 'all';
|
|||||||
let currentSearchQuery = '';
|
let currentSearchQuery = '';
|
||||||
let isSettingHash = false; // Flag to prevent infinite loops
|
let isSettingHash = false; // Flag to prevent infinite loops
|
||||||
let currentInstalledSort = 'name-asc'; // name-asc, name-desc, type, date-desc, date-asc
|
let currentInstalledSort = 'name-asc'; // name-asc, name-desc, type, date-desc, date-asc
|
||||||
|
let currentInstalledFilter = 'all'; // all, installed, active
|
||||||
|
|
||||||
// Get CSRF cookie helper function
|
// Get CSRF cookie helper function
|
||||||
function getCookie(name) {
|
function getCookie(name) {
|
||||||
@@ -2035,14 +2059,37 @@ function clearPluginSearch() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setInstalledFilter(filter) {
|
||||||
|
currentInstalledFilter = filter;
|
||||||
|
var bar = document.getElementById('installedSortFilterBar');
|
||||||
|
if (bar) {
|
||||||
|
try {
|
||||||
|
bar.querySelectorAll('.filter-btn').forEach(function(btn) {
|
||||||
|
btn.classList.toggle('active', (btn.getAttribute('data-filter') || '') === filter);
|
||||||
|
});
|
||||||
|
} catch (e) { console.warn('setInstalledFilter: filter buttons', e); }
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
filterInstalledPlugins();
|
||||||
|
} catch (e) { console.warn('setInstalledFilter: filterInstalledPlugins', e); }
|
||||||
|
}
|
||||||
|
|
||||||
function filterInstalledPlugins() {
|
function filterInstalledPlugins() {
|
||||||
const query = (document.getElementById('installedPluginSearchInput') && document.getElementById('installedPluginSearchInput').value) || '';
|
const query = (document.getElementById('installedPluginSearchInput') && document.getElementById('installedPluginSearchInput').value) || '';
|
||||||
const terms = query.trim().toLowerCase().split(/\s+/).filter(function(t) { return t.length > 0; });
|
const terms = query.trim().toLowerCase().split(/\s+/).filter(function(t) { return t.length > 0; });
|
||||||
|
const filter = currentInstalledFilter || 'all';
|
||||||
const gridView = document.getElementById('gridView');
|
const gridView = document.getElementById('gridView');
|
||||||
const tableView = document.getElementById('tableView');
|
const tableView = document.getElementById('tableView');
|
||||||
const noResultsGrid = document.getElementById('installedPluginsNoResultsGrid');
|
const noResultsGrid = document.getElementById('installedPluginsNoResultsGrid');
|
||||||
const noResultsTable = document.getElementById('installedPluginsNoResultsTable');
|
const noResultsTable = document.getElementById('installedPluginsNoResultsTable');
|
||||||
|
if (!gridView && !tableView) return;
|
||||||
var visibleCount = 0;
|
var visibleCount = 0;
|
||||||
|
function matchesFilter(installed, enabled) {
|
||||||
|
if (filter === 'all') return true;
|
||||||
|
if (filter === 'installed') return installed === 'true';
|
||||||
|
if (filter === 'active') return installed === 'true' && enabled === 'true';
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (gridView) {
|
if (gridView) {
|
||||||
var cards = gridView.querySelectorAll('.plugin-card');
|
var cards = gridView.querySelectorAll('.plugin-card');
|
||||||
cards.forEach(function(card) {
|
cards.forEach(function(card) {
|
||||||
@@ -2050,7 +2097,11 @@ function filterInstalledPlugins() {
|
|||||||
var desc = (card.getAttribute('data-plugin-desc') || '').toLowerCase();
|
var desc = (card.getAttribute('data-plugin-desc') || '').toLowerCase();
|
||||||
var type = (card.getAttribute('data-plugin-type') || '').toLowerCase();
|
var type = (card.getAttribute('data-plugin-type') || '').toLowerCase();
|
||||||
var combined = name + ' ' + desc + ' ' + type;
|
var combined = name + ' ' + desc + ' ' + type;
|
||||||
var show = terms.length === 0 || terms.every(function(term) { return combined.indexOf(term) !== -1; });
|
var searchMatch = terms.length === 0 || terms.every(function(term) { return combined.indexOf(term) !== -1; });
|
||||||
|
var installed = card.getAttribute('data-installed') || 'false';
|
||||||
|
var enabled = card.getAttribute('data-enabled') || 'false';
|
||||||
|
var filterMatch = matchesFilter(installed, enabled);
|
||||||
|
var show = searchMatch && filterMatch;
|
||||||
card.style.display = show ? '' : 'none';
|
card.style.display = show ? '' : 'none';
|
||||||
if (show) visibleCount++;
|
if (show) visibleCount++;
|
||||||
});
|
});
|
||||||
@@ -2065,17 +2116,22 @@ function filterInstalledPlugins() {
|
|||||||
var desc = (row.getAttribute('data-plugin-desc') || '').toLowerCase();
|
var desc = (row.getAttribute('data-plugin-desc') || '').toLowerCase();
|
||||||
var type = (row.getAttribute('data-plugin-type') || '').toLowerCase();
|
var type = (row.getAttribute('data-plugin-type') || '').toLowerCase();
|
||||||
var combined = name + ' ' + desc + ' ' + type;
|
var combined = name + ' ' + desc + ' ' + type;
|
||||||
var show = terms.length === 0 || terms.every(function(term) { return combined.indexOf(term) !== -1; });
|
var searchMatch = terms.length === 0 || terms.every(function(term) { return combined.indexOf(term) !== -1; });
|
||||||
|
var installed = row.getAttribute('data-installed') || 'false';
|
||||||
|
var enabled = row.getAttribute('data-enabled') || 'false';
|
||||||
|
var filterMatch = matchesFilter(installed, enabled);
|
||||||
|
var show = searchMatch && filterMatch;
|
||||||
row.style.display = show ? '' : 'none';
|
row.style.display = show ? '' : 'none';
|
||||||
if (show) visibleCount++;
|
if (show) visibleCount++;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
var hasFilterOrSearch = terms.length > 0 || (filter !== 'all');
|
||||||
if (noResultsGrid) {
|
if (noResultsGrid) {
|
||||||
noResultsGrid.style.display = (terms.length > 0 && visibleCount === 0) ? 'block' : 'none';
|
noResultsGrid.style.display = (hasFilterOrSearch && visibleCount === 0) ? 'block' : 'none';
|
||||||
}
|
}
|
||||||
if (noResultsTable) {
|
if (noResultsTable) {
|
||||||
noResultsTable.style.display = (terms.length > 0 && visibleCount === 0) ? 'table-row' : 'none';
|
noResultsTable.style.display = (hasFilterOrSearch && visibleCount === 0) ? 'table-row' : 'none';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2386,43 +2442,54 @@ function installPlugin(pluginName) {
|
|||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Installing...';
|
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Installing...';
|
||||||
|
|
||||||
fetch(`/plugins/api/install/${pluginName}/`, {
|
const headers = {
|
||||||
method: 'POST',
|
'X-CSRFToken': getCookie('csrftoken'),
|
||||||
headers: {
|
'Content-Type': 'application/json'
|
||||||
'X-CSRFToken': getCookie('csrftoken'),
|
};
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
function showError(msg) {
|
||||||
})
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
if (data.success) {
|
|
||||||
location.reload();
|
|
||||||
} else {
|
|
||||||
if (typeof PNotify !== 'undefined') {
|
|
||||||
new PNotify({
|
|
||||||
title: 'Installation Failed!',
|
|
||||||
text: data.error || 'Failed to install plugin',
|
|
||||||
type: 'error'
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
alert('Error: ' + (data.error || 'Failed to install plugin'));
|
|
||||||
}
|
|
||||||
btn.disabled = false;
|
|
||||||
btn.innerHTML = originalText;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
if (typeof PNotify !== 'undefined') {
|
if (typeof PNotify !== 'undefined') {
|
||||||
new PNotify({
|
new PNotify({ title: 'Installation Failed!', text: msg || 'Failed to install plugin', type: 'error' });
|
||||||
title: 'Error!',
|
|
||||||
text: 'Failed to install plugin: ' + error.message,
|
|
||||||
type: 'error'
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
alert('Error: Failed to install plugin - ' + error.message);
|
alert('Error: ' + (msg || 'Failed to install plugin'));
|
||||||
}
|
}
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btn.innerHTML = originalText;
|
btn.innerHTML = originalText;
|
||||||
|
}
|
||||||
|
|
||||||
|
function tryLocalInstall() {
|
||||||
|
return fetch(`/plugins/api/install/${pluginName}/`, { method: 'POST', headers: headers })
|
||||||
|
.then(function(r) { return r.json().then(function(data) { return { response: r, data: data }; }); });
|
||||||
|
}
|
||||||
|
|
||||||
|
function tryStoreInstall() {
|
||||||
|
return fetch(`/plugins/api/store/install/${pluginName}/`, { method: 'POST', headers: headers })
|
||||||
|
.then(function(r) { return r.json().then(function(data) { return { response: r, data: data }; }); });
|
||||||
|
}
|
||||||
|
|
||||||
|
tryLocalInstall()
|
||||||
|
.then(function(result) {
|
||||||
|
if (result.data.success) {
|
||||||
|
location.reload();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var err = result.data.error || '';
|
||||||
|
if (result.response.status === 404 || (err && err.indexOf('Plugin source not found') !== -1)) {
|
||||||
|
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Installing from store...';
|
||||||
|
return tryStoreInstall();
|
||||||
|
}
|
||||||
|
showError(err);
|
||||||
|
})
|
||||||
|
.then(function(result) {
|
||||||
|
if (!result || !result.data) return;
|
||||||
|
if (result.data.success) {
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
showError(result.data.error || 'Failed to install plugin');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function(error) {
|
||||||
|
showError(error && error.message ? error.message : 'Failed to install plugin');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2978,7 +3045,14 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
|
|
||||||
// Set initial view without updating hash (only update hash if there was already one)
|
// Set initial view without updating hash (only update hash if there was already one)
|
||||||
const hadHash = hash.length > 0;
|
const hadHash = hash.length > 0;
|
||||||
toggleView(initialView, hadHash);
|
try {
|
||||||
|
toggleView(initialView, hadHash);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('plugins: toggleView on load failed', e);
|
||||||
|
if (storeView) storeView.style.display = 'block';
|
||||||
|
if (gridView) gridView.style.display = 'none';
|
||||||
|
if (tableView) tableView.style.display = 'none';
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Elements don't exist (no plugins installed), just show store view directly
|
// Elements don't exist (no plugins installed), just show store view directly
|
||||||
if (storeView) {
|
if (storeView) {
|
||||||
|
|||||||
@@ -98,16 +98,11 @@ urlpatterns = [
|
|||||||
path('<str:plugin_name>/help/', views.plugin_help, name='plugin_help'),
|
path('<str:plugin_name>/help/', views.plugin_help, name='plugin_help'),
|
||||||
]
|
]
|
||||||
|
|
||||||
# Dynamically include each installed plugin's URLs so /plugins/<plugin_name>/settings/ etc. work
|
# Dynamically include each installed plugin's URLs so /plugins/<plugin_name>/settings/ etc. work.
|
||||||
# Only include plugins that are in INSTALLED_APPS so Django can load their models.
|
# Include every plugin found on disk (INSTALLED_PLUGINS_PATH or PLUGIN_SOURCE_PATHS) so plugin
|
||||||
from django.conf import settings
|
# pages work even if the app was not added to INSTALLED_APPS (e.g. after git pull overwrote settings).
|
||||||
_installed_apps = getattr(settings, 'INSTALLED_APPS', ())
|
|
||||||
|
|
||||||
for _plugin_name, _path_parent in _get_installed_plugin_list():
|
for _plugin_name, _path_parent in _get_installed_plugin_list():
|
||||||
if _plugin_name not in _installed_apps:
|
|
||||||
continue
|
|
||||||
try:
|
try:
|
||||||
# If plugin is from a source path, ensure it is on sys.path so import works
|
|
||||||
if _path_parent not in sys.path:
|
if _path_parent not in sys.path:
|
||||||
sys.path.insert(0, _path_parent)
|
sys.path.insert(0, _path_parent)
|
||||||
__import__(_plugin_name + '.urls')
|
__import__(_plugin_name + '.urls')
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from django.views.decorators.csrf import csrf_exempt
|
|||||||
from django.views.decorators.http import require_http_methods
|
from django.views.decorators.http import require_http_methods
|
||||||
from plogical.mailUtilities import mailUtilities
|
from plogical.mailUtilities import mailUtilities
|
||||||
import os
|
import os
|
||||||
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import shlex
|
import shlex
|
||||||
import json
|
import json
|
||||||
@@ -42,6 +43,17 @@ PLUGIN_SOURCE_PATHS = ['/home/cyberpanel/plugins', '/home/cyberpanel-plugins']
|
|||||||
# These plugins show "Built-in" badge and only Settings button (no Deactivate/Uninstall)
|
# These plugins show "Built-in" badge and only Settings button (no Deactivate/Uninstall)
|
||||||
BUILTIN_PLUGINS = frozenset(['emailMarketing', 'emailPremium'])
|
BUILTIN_PLUGINS = frozenset(['emailMarketing', 'emailPremium'])
|
||||||
|
|
||||||
|
# Core CyberPanel app dirs under /usr/local/CyberCP that must not be counted as "installed plugins"
|
||||||
|
# (matches pluginHolder.urls so Installed count = store/plugin dirs only, not core apps)
|
||||||
|
RESERVED_PLUGIN_DIRS = frozenset([
|
||||||
|
'api', 'backup', 'baseTemplate', 'cloudAPI', 'CLManager', 'containerization', 'CyberCP',
|
||||||
|
'databases', 'dns', 'dockerManager', 'emailMarketing', 'emailPremium', 'filemanager',
|
||||||
|
'firewall', 'ftp', 'highAvailability', 'IncBackups', 'loginSystem', 'mailServer',
|
||||||
|
'managePHP', 'manageSSL', 'manageServices', 'packages', 'pluginHolder', 'plogical',
|
||||||
|
'pluginInstaller', 'serverLogs', 'serverStatus', 's3Backups', 'tuning', 'userManagment',
|
||||||
|
'websiteFunctions', 'aiScanner', 'dns', 'help', 'installed',
|
||||||
|
])
|
||||||
|
|
||||||
def _get_plugin_source_path(plugin_name):
|
def _get_plugin_source_path(plugin_name):
|
||||||
"""Return the full path to a plugin's source directory, or None if not found."""
|
"""Return the full path to a plugin's source directory, or None if not found."""
|
||||||
for base in PLUGIN_SOURCE_PATHS:
|
for base in PLUGIN_SOURCE_PATHS:
|
||||||
@@ -51,6 +63,30 @@ def _get_plugin_source_path(plugin_name):
|
|||||||
return path
|
return path
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def _ensure_plugin_meta_xml(plugin_name):
|
||||||
|
"""
|
||||||
|
If plugin is installed (directory exists) but meta.xml is missing,
|
||||||
|
restore it from source or from GitHub so the grid and version checks work.
|
||||||
|
"""
|
||||||
|
installed_dir = os.path.join('/usr/local/CyberCP', plugin_name)
|
||||||
|
installed_meta = os.path.join(installed_dir, 'meta.xml')
|
||||||
|
if not os.path.isdir(installed_dir) or os.path.exists(installed_meta):
|
||||||
|
return
|
||||||
|
source_path = _get_plugin_source_path(plugin_name)
|
||||||
|
if source_path:
|
||||||
|
source_meta = os.path.join(source_path, 'meta.xml')
|
||||||
|
if os.path.exists(source_meta):
|
||||||
|
try:
|
||||||
|
shutil.copy2(source_meta, installed_meta)
|
||||||
|
logging.writeToFile(f"Restored meta.xml for {plugin_name} from source")
|
||||||
|
except Exception as e:
|
||||||
|
logging.writeToFile(f"Could not restore meta.xml for {plugin_name}: {e}")
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
_sync_meta_xml_from_github(plugin_name)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
def _get_plugin_state_file(plugin_name):
|
def _get_plugin_state_file(plugin_name):
|
||||||
"""Get the path to the plugin state file"""
|
"""Get the path to the plugin state file"""
|
||||||
if not os.path.exists(PLUGIN_STATE_DIR):
|
if not os.path.exists(PLUGIN_STATE_DIR):
|
||||||
@@ -121,6 +157,15 @@ def installed(request):
|
|||||||
errorPlugins = []
|
errorPlugins = []
|
||||||
processed_plugins = set() # Track which plugins we've already processed
|
processed_plugins = set() # Track which plugins we've already processed
|
||||||
|
|
||||||
|
# Repair pass: ensure every installed plugin dir has meta.xml (from source or GitHub) so counts and grid are correct
|
||||||
|
if os.path.exists(installedPath):
|
||||||
|
for plugin in os.listdir(installedPath):
|
||||||
|
if plugin.startswith('.') or plugin in RESERVED_PLUGIN_DIRS:
|
||||||
|
continue
|
||||||
|
plugin_dir = os.path.join(installedPath, plugin)
|
||||||
|
if os.path.isdir(plugin_dir):
|
||||||
|
_ensure_plugin_meta_xml(plugin)
|
||||||
|
|
||||||
# First, process plugins from source directories (multiple paths: /home/cyberpanel/plugins, /home/cyberpanel-plugins)
|
# First, process plugins from source directories (multiple paths: /home/cyberpanel/plugins, /home/cyberpanel-plugins)
|
||||||
# BUT: Skip plugins that are already installed - we'll process those from the installed location instead
|
# BUT: Skip plugins that are already installed - we'll process those from the installed location instead
|
||||||
for pluginPath in PLUGIN_SOURCE_PATHS:
|
for pluginPath in PLUGIN_SOURCE_PATHS:
|
||||||
@@ -134,20 +179,20 @@ def installed(request):
|
|||||||
for plugin in os.listdir(pluginPath):
|
for plugin in os.listdir(pluginPath):
|
||||||
if plugin in processed_plugins:
|
if plugin in processed_plugins:
|
||||||
continue
|
continue
|
||||||
# Skip if plugin is already installed - we'll process it from installed location instead
|
|
||||||
completePath = installedPath + '/' + plugin + '/meta.xml'
|
|
||||||
if os.path.exists(completePath):
|
|
||||||
# Plugin is installed, skip source path - DON'T mark as processed yet
|
|
||||||
# The installed location loop will handle it and mark it as processed
|
|
||||||
continue
|
|
||||||
# Skip files (like .zip files) - only process directories
|
# Skip files (like .zip files) - only process directories
|
||||||
pluginDir = os.path.join(pluginPath, plugin)
|
pluginDir = os.path.join(pluginPath, plugin)
|
||||||
if not os.path.isdir(pluginDir):
|
if not os.path.isdir(pluginDir):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Use same "installed" criterion as install endpoint: plugin directory in /usr/local/CyberCP/
|
||||||
|
installed_dir = os.path.join(installedPath, plugin)
|
||||||
|
completePath = os.path.join(installedPath, plugin, 'meta.xml')
|
||||||
|
if os.path.exists(completePath):
|
||||||
|
# Plugin is fully installed (dir + meta.xml), skip - second loop will add it
|
||||||
|
continue
|
||||||
|
|
||||||
data = {}
|
data = {}
|
||||||
# Try installed location first, then fallback to source location
|
# Try installed location first, then fallback to source location
|
||||||
completePath = installedPath + '/' + plugin + '/meta.xml'
|
|
||||||
sourcePath = os.path.join(pluginDir, 'meta.xml')
|
sourcePath = os.path.join(pluginDir, 'meta.xml')
|
||||||
|
|
||||||
# Determine which meta.xml to use
|
# Determine which meta.xml to use
|
||||||
@@ -200,9 +245,9 @@ def installed(request):
|
|||||||
data['plugin_dir'] = plugin # Plugin directory name
|
data['plugin_dir'] = plugin # Plugin directory name
|
||||||
# Set builtin flag (core CyberPanel plugins vs user-installable plugins)
|
# Set builtin flag (core CyberPanel plugins vs user-installable plugins)
|
||||||
data['builtin'] = plugin in BUILTIN_PLUGINS
|
data['builtin'] = plugin in BUILTIN_PLUGINS
|
||||||
# Check if plugin is installed (only if it exists in /usr/local/CyberCP/)
|
# Installed = plugin directory exists (must match install endpoint which uses directory existence)
|
||||||
# Source directory presence doesn't mean installed - it just means the source files are available
|
# Fixes grid showing "Not Installed" when directory exists but meta.xml is missing
|
||||||
data['installed'] = os.path.exists(completePath)
|
data['installed'] = os.path.isdir(installed_dir)
|
||||||
|
|
||||||
# Get plugin enabled state (only for installed plugins)
|
# Get plugin enabled state (only for installed plugins)
|
||||||
if data['installed']:
|
if data['installed']:
|
||||||
@@ -247,11 +292,9 @@ def installed(request):
|
|||||||
# Special handling for emailMarketing
|
# Special handling for emailMarketing
|
||||||
if plugin == 'emailMarketing':
|
if plugin == 'emailMarketing':
|
||||||
data['manage_url'] = '/emailMarketing/'
|
data['manage_url'] = '/emailMarketing/'
|
||||||
elif os.path.exists(completePath):
|
elif data['installed']:
|
||||||
# Check if settings route exists, otherwise use main plugin URL
|
# Plugin directory exists; use main plugin URL
|
||||||
settings_route = f'/plugins/{plugin}/settings/'
|
|
||||||
main_route = f'/plugins/{plugin}/'
|
main_route = f'/plugins/{plugin}/'
|
||||||
# Default to main route - most plugins have a main route even if no settings
|
|
||||||
data['manage_url'] = main_route
|
data['manage_url'] = main_route
|
||||||
else:
|
else:
|
||||||
data['manage_url'] = None
|
data['manage_url'] = None
|
||||||
@@ -282,20 +325,14 @@ def installed(request):
|
|||||||
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
|
# 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.isdir(installed_dir):
|
||||||
if not os.path.exists(completePath):
|
|
||||||
# Only skip if it's not actually installed
|
|
||||||
continue
|
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.isdir(installed_dir):
|
||||||
if not os.path.exists(completePath):
|
|
||||||
# Only skip if it's not actually installed
|
|
||||||
continue
|
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
|
||||||
@@ -311,6 +348,7 @@ def installed(request):
|
|||||||
if not os.path.isdir(pluginInstalledDir):
|
if not os.path.isdir(pluginInstalledDir):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
_ensure_plugin_meta_xml(plugin)
|
||||||
metaXmlPath = os.path.join(pluginInstalledDir, 'meta.xml')
|
metaXmlPath = os.path.join(pluginInstalledDir, 'meta.xml')
|
||||||
if not os.path.exists(metaXmlPath):
|
if not os.path.exists(metaXmlPath):
|
||||||
continue
|
continue
|
||||||
@@ -475,29 +513,30 @@ def installed(request):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.writeToFile(f"Plugin {plugin_name} fallback load error: {str(e)}")
|
logging.writeToFile(f"Plugin {plugin_name} fallback load error: {str(e)}")
|
||||||
|
|
||||||
# Calculate installed and active counts
|
# Calculate installed and active counts: only count real plugins (have meta.xml, not core apps)
|
||||||
# Double-check by also counting plugins that actually exist in /usr/local/CyberCP/
|
|
||||||
installed_plugins_in_filesystem = set()
|
installed_plugins_in_filesystem = set()
|
||||||
if os.path.exists(installedPath):
|
if os.path.exists(installedPath):
|
||||||
for plugin in os.listdir(installedPath):
|
for plugin in os.listdir(installedPath):
|
||||||
|
if plugin.startswith('.') or plugin in RESERVED_PLUGIN_DIRS:
|
||||||
|
continue
|
||||||
pluginInstalledDir = os.path.join(installedPath, plugin)
|
pluginInstalledDir = os.path.join(installedPath, plugin)
|
||||||
if os.path.isdir(pluginInstalledDir):
|
if not os.path.isdir(pluginInstalledDir):
|
||||||
metaXmlPath = os.path.join(pluginInstalledDir, 'meta.xml')
|
continue
|
||||||
if os.path.exists(metaXmlPath):
|
if not os.path.exists(os.path.join(pluginInstalledDir, 'meta.xml')):
|
||||||
installed_plugins_in_filesystem.add(plugin)
|
continue
|
||||||
|
installed_plugins_in_filesystem.add(plugin)
|
||||||
|
|
||||||
# Count installed plugins from the list
|
|
||||||
installed_count = len([p for p in pluginList if p.get('installed', False)])
|
installed_count = len([p for p in pluginList if p.get('installed', False)])
|
||||||
active_count = len([p for p in pluginList if p.get('installed', False) and p.get('enabled', False)])
|
active_count = len([p for p in pluginList if p.get('installed', False) and p.get('enabled', False)])
|
||||||
|
|
||||||
# If there's a discrepancy, use the filesystem count as the source of truth
|
# Use the larger of list count and filesystem count so header never shows less than grid
|
||||||
filesystem_installed_count = len(installed_plugins_in_filesystem)
|
filesystem_installed_count = len(installed_plugins_in_filesystem)
|
||||||
if filesystem_installed_count != installed_count:
|
list_installed_count = len([p for p in pluginList if p.get('installed', False)])
|
||||||
logging.writeToFile(f"WARNING: Plugin count mismatch! List says {installed_count}, filesystem has {filesystem_installed_count}")
|
if filesystem_installed_count != list_installed_count:
|
||||||
logging.writeToFile(f"Plugins in filesystem: {sorted(installed_plugins_in_filesystem)}")
|
logging.writeToFile(f"Plugin count: list installed={list_installed_count}, filesystem with meta.xml={filesystem_installed_count}")
|
||||||
logging.writeToFile(f"Plugins in list with installed=True: {[p.get('plugin_dir') for p in pluginList if p.get('installed', False)]}")
|
installed_count = max(list_installed_count, filesystem_installed_count)
|
||||||
# Use filesystem count as source of truth
|
if active_count > installed_count:
|
||||||
installed_count = filesystem_installed_count
|
active_count = installed_count
|
||||||
|
|
||||||
# Debug logging to help identify discrepancies
|
# Debug logging to help identify discrepancies
|
||||||
logging.writeToFile(f"Plugin count: Total={len(pluginList)}, Installed={installed_count}, Active={active_count}")
|
logging.writeToFile(f"Plugin count: Total={len(pluginList)}, Installed={installed_count}, Active={active_count}")
|
||||||
@@ -609,6 +648,7 @@ 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)
|
||||||
|
|
||||||
|
_ensure_plugin_meta_xml(plugin_name)
|
||||||
logging.writeToFile(f"Plugin {plugin_name} installed successfully (upload)")
|
logging.writeToFile(f"Plugin {plugin_name} installed successfully (upload)")
|
||||||
return JsonResponse({
|
return JsonResponse({
|
||||||
'success': True,
|
'success': True,
|
||||||
@@ -1783,6 +1823,7 @@ def install_from_store(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)
|
||||||
|
|
||||||
|
_ensure_plugin_meta_xml(plugin_name)
|
||||||
return JsonResponse({
|
return JsonResponse({
|
||||||
'success': True,
|
'success': True,
|
||||||
'message': f'Plugin {plugin_name} installed successfully from store'
|
'message': f'Plugin {plugin_name} installed successfully from store'
|
||||||
|
|||||||
Reference in New Issue
Block a user