feat: no plugins required by default; Plugin Store category updates

- Remove emailMarketing from default INSTALLED_APPS
- Comment out emailMarketing URL (plugin installer adds when installed)
- Bump emailMarketing, examplePlugin meta.xml to 1.0.1
- Plugin Holder: remove Plugin category, enforce Utility/Security/Backup/Performance
- Add to-do/PLUGIN-DEFAULT-REMOVAL-2026-02-01.md
This commit is contained in:
master3395
2026-02-01 23:46:48 +01:00
parent 6e935d64c7
commit 86b5ed6e0e
8 changed files with 473 additions and 100 deletions

View File

@@ -65,7 +65,8 @@ INSTALLED_APPS = [
# Apps with multiple or complex dependencies
'emailPremium',
'emailMarketing', # Depends on websiteFunctions and loginSystem
# Optional plugins (e.g. emailMarketing, discordWebhooks) - install via Plugin Store
# from https://github.com/master3395/cyberpanel-plugins - plugin installer adds them
'cloudAPI', # Depends on websiteFunctions
'containerization', # Depends on websiteFunctions
'IncBackups', # Depends on websiteFunctions and loginSystem

View File

@@ -51,7 +51,8 @@ urlpatterns = [
path('CloudLinux/', include('CLManager.urls')),
path('IncrementalBackups/', include('IncBackups.urls')),
path('aiscanner/', include('aiScanner.urls')),
path('emailMarketing/', include('emailMarketing.urls')),
# Optional plugin routes - added by plugin installer when plugins are installed from Plugin Store
# path('emailMarketing/', include('emailMarketing.urls')),
# path('Terminal/', include('WebTerminal.urls')),
path('', include('loginSystem.urls')),
]

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<cyberpanelPluginConfig>
<name>Email Marketing</name>
<type>plugin</type>
<type>Utility</type>
<description>Email Marketing plugin for CyberPanel.</description>
<version>1.0.0</version>
<version>1.0.1</version>
</cyberpanelPluginConfig>

View File

@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<cyberpanelPluginConfig>
<name>examplePlugin</name>
<type>plugin</type>
<type>Utility</type>
<description>This is an example plugin</description>
<version>1.0.0</version>
<version>1.0.1</version>
<author>usmannasir</author>
</cyberpanelPluginConfig>

View File

@@ -401,6 +401,10 @@ mkdir -p migrations</code></pre>
&lt;settings_url&gt;/plugins/myFirstPlugin/settings/&lt;/settings_url&gt;
&lt;/cyberpanelPluginConfig&gt;</code></pre>
<div class="alert alert-warning">
<strong>{% trans "Required: Category (type)" %}:</strong> {% trans "The &lt;type&gt; field is required. Valid categories: Utility, Security, Backup, Performance. Plugins without a valid category will not appear in the Plugin Store." %}
</div>
<h3>{% trans "Step 3: Create urls.py" %}</h3>
<pre><code>from django.urls import path
from . import views

View File

@@ -592,11 +592,150 @@
100% { transform: rotate(360deg); }
}
.category-filter {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 15px;
padding: 15px;
background: var(--bg-secondary, #f8f9ff);
border-radius: 8px;
}
.category-btn {
padding: 8px 16px;
border: 1px solid var(--border-primary, #e8e9ff);
background: var(--bg-primary, white);
border-radius: 8px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
color: var(--text-secondary, #64748b);
display: inline-flex;
align-items: center;
gap: 8px;
}
.category-btn:hover {
background: var(--bg-hover, #f0f1ff);
border-color: #5856d6;
color: #5856d6;
}
.category-btn.active {
background: #5856d6;
color: white;
border-color: #5856d6;
}
.category-btn i {
font-size: 14px;
}
.store-search-bar {
position: relative;
margin-bottom: 15px;
max-width: 480px;
}
.store-search-icon {
position: absolute;
left: 16px;
top: 50%;
transform: translateY(-50%);
color: #64748b;
font-size: 16px;
pointer-events: none;
z-index: 1;
width: 20px;
text-align: center;
}
.store-search-input {
width: 100%;
padding-left: 46px;
padding-right: 40px;
padding-top: 12px;
padding-bottom: 12px;
border: 1px solid #cbd5e1;
border-radius: 8px;
font-size: 14px;
background: #f8fafc;
color: #1e293b;
transition: border-color 0.2s, box-shadow 0.2s, background 0.2s;
}
.store-search-input:focus {
outline: none;
border-color: #5856d6;
background: #ffffff;
box-shadow: 0 0 0 3px rgba(88, 86, 214, 0.15);
}
.store-search-input::placeholder {
color: #64748b;
}
.store-search-clear {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
color: var(--text-muted, #94a3b8);
cursor: pointer;
padding: 6px;
border-radius: 4px;
font-size: 14px;
transition: color 0.2s, background 0.2s;
}
.store-search-clear:hover {
color: #5856d6;
background: var(--bg-secondary, #f8f9ff);
}
.alphabet-filter-wrapper {
margin-bottom: 20px;
}
.alphabet-toggle-btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
border: 1px solid var(--border-primary, #e8e9ff);
background: var(--bg-primary, white);
border-radius: 8px;
font-size: 13px;
font-weight: 600;
color: var(--text-secondary, #64748b);
cursor: pointer;
transition: all 0.2s;
}
.alphabet-toggle-btn:hover {
background: var(--bg-hover, #f8f9ff);
border-color: #5856d6;
color: #5856d6;
}
.alphabet-toggle-btn[aria-expanded="true"] .alphabet-chevron {
transform: rotate(180deg);
}
.alphabet-chevron {
font-size: 10px;
transition: transform 0.2s;
}
.alphabet-filter {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 20px;
margin-top: 12px;
padding: 15px;
background: var(--bg-secondary, #f8f9ff);
border-radius: 8px;
@@ -971,15 +1110,15 @@
{% if plugins %}
<!-- View Toggle -->
<div class="view-toggle">
<button class="view-btn active" onclick="toggleView('grid')">
<button class="view-btn active" onclick="toggleView('grid', true)">
<i class="fas fa-th-large"></i>
{% trans "Grid View" %}
</button>
<button class="view-btn" onclick="toggleView('table')">
<button class="view-btn" onclick="toggleView('table', true)">
<i class="fas fa-list"></i>
{% trans "Table View" %}
</button>
<button class="view-btn" onclick="toggleView('store')">
<button class="view-btn" onclick="toggleView('store', true)">
<i class="fas fa-store"></i>
{% trans "CyberPanel Plugin Store" %}
</button>
@@ -995,13 +1134,13 @@
<div class="plugin-card">
<div class="plugin-header">
<div class="plugin-icon">
{% if plugin.type == "Security" %}
{% if plugin.type|lower == "security" %}
<i class="fas fa-shield-alt"></i>
{% elif plugin.type == "Performance" %}
{% elif plugin.type|lower == "performance" %}
<i class="fas fa-rocket"></i>
{% elif plugin.type == "Utility" %}
{% elif plugin.type|lower == "utility" %}
<i class="fas fa-tools"></i>
{% elif plugin.type == "Backup" %}
{% elif plugin.type|lower == "backup" %}
<i class="fas fa-save"></i>
{% else %}
<i class="fas fa-puzzle-piece"></i>
@@ -1216,15 +1355,15 @@
<!-- View Toggle (only shown when no plugins installed) -->
<div class="view-toggle" style="margin-top: 25px;">
<button class="view-btn" onclick="toggleView('grid')">
<button class="view-btn" onclick="toggleView('grid', true)">
<i class="fas fa-th-large"></i>
{% trans "Grid View" %}
</button>
<button class="view-btn" onclick="toggleView('table')">
<button class="view-btn" onclick="toggleView('table', true)">
<i class="fas fa-list"></i>
{% trans "Table View" %}
</button>
<button class="view-btn active" onclick="toggleView('store')">
<button class="view-btn active" onclick="toggleView('store', true)">
<i class="fas fa-store"></i>
{% trans "CyberPanel Plugin Store" %}
</button>
@@ -1274,35 +1413,63 @@
</div>
</div>
<!-- Store Loading Indicator -->
<div id="storeLoading" class="store-loading" style="display: none;">
<div class="loading-spinner"></div>
<p>{% trans "Loading plugins from store..." %}</p>
<!-- Search Bar (always visible in store view) -->
<div class="store-search-bar">
<i class="fas fa-search store-search-icon"></i>
<input type="text" id="pluginSearchInput" class="store-search-input" placeholder="{% trans 'Search plugins by name or description...' %}" aria-label="{% trans 'Search plugins' %}">
<button type="button" class="store-search-clear" id="pluginSearchClear" onclick="clearPluginSearch()" style="display: none;" aria-label="{% trans 'Clear search' %}">
<i class="fas fa-times"></i>
</button>
</div>
<!-- Store Error -->
<div id="storeError" class="alert alert-danger" style="display: none;">
<i class="fas fa-exclamation-circle alert-icon"></i>
<span id="storeErrorText"></span>
</div>
<!-- Store Content -->
<div id="storeContent" style="display: none;">
<!-- Alphabetical Filter -->
<div class="alphabet-filter">
{% for letter in "ABCDEFGHIJKLMNOPQRSTUVWXYZ" %}
<button class="alpha-btn" onclick="filterByLetter('{{ letter }}')">{{ letter }}</button>
{% endfor %}
<button class="alpha-btn active" onclick="filterByLetter('all')">{% trans "All" %}</button>
<!-- Category Filter (always visible in store view) - v2026-02-01 no Plugin category -->
<div class="category-filter">
<button class="category-btn active" onclick="filterByCategory('all', event)" data-category="all">
<i class="fas fa-th"></i>
{% trans "All Categories" %}
</button>
<button class="category-btn" onclick="filterByCategory('Utility', event)" data-category="Utility">
<i class="fas fa-wrench"></i>
{% trans "Utility" %}
</button>
<button class="category-btn" onclick="filterByCategory('Security', event)" data-category="Security">
<i class="fas fa-shield-alt"></i>
{% trans "Security" %}
</button>
<button class="category-btn" onclick="filterByCategory('Backup', event)" data-category="Backup">
<i class="fas fa-archive"></i>
{% trans "Backup" %}
</button>
<button class="category-btn" onclick="filterByCategory('Performance', event)" data-category="Performance">
<i class="fas fa-rocket"></i>
{% trans "Performance" %}
</button>
</div>
<!-- Store Table -->
<!-- Alphabetical Filter (collapsible, hidden by default) -->
<div class="alphabet-filter-wrapper">
<button type="button" class="alphabet-toggle-btn" id="alphabetToggleBtn" onclick="toggleAlphabetFilter()" aria-expanded="false">
<i class="fas fa-sort-alpha-down alphabet-toggle-icon"></i>
<span>{% trans "A-Å Filter" %}</span>
<i class="fas fa-chevron-down alphabet-chevron"></i>
</button>
<div class="alphabet-filter" id="alphabetFilter" aria-hidden="true" style="display: none;">
{% for letter in "ABCDEFGHIJKLMNOPQRSTUVWXYZ" %}
<button class="alpha-btn" onclick="filterByLetter('{{ letter }}', event)">{{ letter }}</button>
{% endfor %}
<button class="alpha-btn active" onclick="filterByLetter('all', event)">{% trans "All" %}</button>
</div>
</div>
<!-- Store Content (table area) -->
<div id="storeContent" style="display: block;">
<div class="store-table-wrapper">
<table class="store-table">
<thead>
<tr>
<th>{% trans "Icon" %}</th>
<th>{% trans "Plugin Name" %}</th>
<th>{% trans "Category" %}</th>
<th>{% trans "Version" %}</th>
<th>{% trans "Pricing" %}</th>
<th>{% trans "Modify Date" %}</th>
@@ -1319,19 +1486,18 @@
</div>
</div>
<div ng-controller="listWebsites" id="listFail" class="alert alert-danger" style="display: none;">
<i class="fas fa-exclamation-circle alert-icon"></i>
<span>{% trans "Cannot list plugins. Error message:" %} {$ errorMessage $}</span>
</div>
<!-- Plugin store errors shown in storeError div; listWebsites controller removed - not applicable to plugins page -->
</div>
</div>
</div>
<script>
// Cache-busting version: 2026-01-25-v4 - Fixed is_paid normalization and ensured consistent rendering
// Force browser to reload this script by changing version number
// Cache-busting version: 2026-02-01-v1 - Fixed category filter, added search bar, collapsible A-Å
let storePlugins = [];
let currentFilter = 'all';
let currentCategory = 'all';
let currentSearchQuery = '';
let isSettingHash = false; // Flag to prevent infinite loops
// Get CSRF cookie helper function
function getCookie(name) {
@@ -1349,7 +1515,7 @@ function getCookie(name) {
return cookieValue;
}
function toggleView(view) {
function toggleView(view, updateHash = true) {
const gridView = document.getElementById('gridView');
const tableView = document.getElementById('tableView');
const storeView = document.getElementById('storeView');
@@ -1357,6 +1523,23 @@ function toggleView(view) {
viewBtns.forEach(btn => btn.classList.remove('active'));
// Update URL hash only if explicitly requested (user clicked a button, not initial load)
if (updateHash) {
isSettingHash = true;
const hash = '#' + view;
// Use replaceState to update URL - this updates the hash without triggering hashchange event
if (window.history && window.history.replaceState) {
// Get current pathname and preserve it, just update the hash
const newUrl = window.location.pathname + window.location.search + hash;
window.history.replaceState(null, null, newUrl);
} else {
// Fallback for older browsers - this will trigger hashchange but we have the flag
window.location.hash = hash;
}
// Reset flag after a short delay
setTimeout(() => { isSettingHash = false; }, 100);
}
if (view === 'grid') {
gridView.style.display = 'grid';
tableView.style.display = 'none';
@@ -1387,18 +1570,34 @@ function loadPluginStore() {
const storeError = document.getElementById('storeError');
const storeErrorText = document.getElementById('storeErrorText');
const storeContent = document.getElementById('storeContent');
const storeTableBody = document.getElementById('storeTableBody');
if (!storeLoading || !storeError || !storeErrorText || !storeContent) return;
storeLoading.style.display = 'block';
storeError.style.display = 'none';
storeContent.style.display = 'none';
storeContent.style.display = 'block';
fetch('/plugins/api/store/plugins/', {
method: 'GET',
credentials: 'same-origin',
headers: {
'X-CSRFToken': getCookie('csrftoken')
'X-CSRFToken': getCookie('csrftoken'),
'Accept': 'application/json'
}
})
.then(response => response.json())
.then(response => {
const contentType = response.headers.get('content-type') || '';
if (!contentType.includes('application/json')) {
if (response.status === 401 || response.status === 403) {
throw new Error('Session expired or access denied. Please refresh the page and log in again.');
}
if (response.redirected || response.status >= 400) {
throw new Error('Could not load plugin store. Your session may have expired. Please refresh the page and log in again.');
}
throw new Error('Server returned invalid response. Please refresh the page and try again.');
}
return response.json();
})
.then(data => {
storeLoading.style.display = 'none';
@@ -1413,8 +1612,12 @@ function loadPluginStore() {
})
.catch(error => {
storeLoading.style.display = 'none';
storeErrorText.textContent = 'Error loading plugin store: ' + error.message;
storeErrorText.textContent = error.message || 'Error loading plugin store. Please refresh the page and try again.';
storeError.style.display = 'block';
storeContent.style.display = 'block';
if (storeTableBody && storeTableBody.innerHTML === '') {
storeTableBody.innerHTML = '<tr><td colspan="9" style="text-align: center; padding: 20px; color: var(--text-secondary, #64748b);">Unable to load plugins. Please check your connection and try again.</td></tr>';
}
});
}
@@ -1426,8 +1629,8 @@ function escapeHtml(text) {
}
function displayStorePlugins() {
// Version: 2026-01-25-v4 - Store view: Removed Status column, always show Free/Paid badges
// CRITICAL: This function MUST create exactly 7 columns (no Status, no Deactivate/Uninstall)
// Version: 2026-01-27-v1 - Added category filtering support
// CRITICAL: This function MUST create exactly 8 columns (Icon, Plugin Name, Version, Pricing, Modify Date, Action, Help, About)
const tbody = document.getElementById('storeTableBody');
if (!tbody) {
console.error('storeTableBody not found!');
@@ -1436,23 +1639,50 @@ function displayStorePlugins() {
tbody.innerHTML = '';
if (!storePlugins || storePlugins.length === 0) {
tbody.innerHTML = '<tr><td colspan="8" style="text-align: center; padding: 20px; color: var(--text-secondary, #64748b);">No plugins available in store</td></tr>';
tbody.innerHTML = '<tr><td colspan="9" style="text-align: center; padding: 20px; color: var(--text-secondary, #64748b);">No plugins available in store</td></tr>';
return;
}
let filteredPlugins = storePlugins;
// Apply category filter (case-insensitive); plugins without category are excluded
if (currentCategory !== 'all') {
const categoryLower = currentCategory.toLowerCase();
filteredPlugins = filteredPlugins.filter(plugin => {
const pluginType = ((plugin.type || '') + '').trim().toLowerCase();
return pluginType && pluginType === categoryLower;
});
}
// Apply search filter (name + description)
if ((currentSearchQuery || '').trim()) {
const searchLower = (currentSearchQuery || '').trim().toLowerCase();
const searchTerms = searchLower.split(/\s+/).filter(t => t.length > 0);
filteredPlugins = filteredPlugins.filter(plugin => {
const name = (plugin.name || '').toLowerCase();
const desc = (plugin.description || '').toLowerCase();
const combined = name + ' ' + desc + ' ' + (plugin.plugin_dir || '');
return searchTerms.every(term => combined.includes(term));
});
}
// Apply alphabetical filter
if (currentFilter !== 'all') {
filteredPlugins = storePlugins.filter(plugin =>
plugin.name.charAt(0).toUpperCase() === currentFilter
filteredPlugins = filteredPlugins.filter(plugin =>
(plugin.name || '').charAt(0).toUpperCase() === currentFilter
);
}
filteredPlugins.forEach(plugin => {
if (filteredPlugins.length === 0) {
tbody.innerHTML = '<tr><td colspan="9" style="text-align: center; padding: 20px; color: var(--text-secondary, #64748b);">No plugins match your filters. Try adjusting the category, search, or letter filter.</td></tr>';
return;
}
filteredPlugins.forEach(plugin => {
const row = document.createElement('tr');
// Plugin icon - based on plugin type (same logic as Grid/Table views)
const pluginType = (plugin.type || 'Plugin').toLowerCase();
const pluginType = ((plugin.type || '') + '').trim().toLowerCase() || 'utility';
let iconClass = 'fas fa-puzzle-piece'; // Default icon
if (pluginType.includes('security')) {
iconClass = 'fas fa-shield-alt';
@@ -1516,12 +1746,16 @@ function displayStorePlugins() {
? '<span class="plugin-pricing-badge paid">Paid</span>'
: '<span class="plugin-pricing-badge free">Free</span>';
// Version: 2026-01-25-v5 - Added plugin icons to Store view (8 columns: Icon, Plugin Name, Version, Pricing, Modify Date, Action, Help, About)
// Version: 2026-01-27-v1 - Added Category column; category is required, no default
const pluginCategory = ((plugin.type || '') + '').trim() || '—';
const categoryBadge = `<span class="plugin-type">${escapeHtml(pluginCategory.toUpperCase())}</span>`;
row.innerHTML = `
<td style="text-align: center;">${iconHtml}</td>
<td>
<strong>${escapeHtml(plugin.name)}</strong>
</td>
<td>${categoryBadge}</td>
<td><span class="plugin-version-number">${escapeHtml(plugin.version)}</span></td>
<td>${pricingBadge}</td>
<td>${modifyDateHtml}</td>
@@ -1534,16 +1768,16 @@ function displayStorePlugins() {
});
}
function filterByLetter(letter) {
currentFilter = letter;
const alphaBtns = document.querySelectorAll('.alpha-btn');
alphaBtns.forEach(btn => btn.classList.remove('active'));
if (event && event.target) {
event.target.classList.add('active');
function filterByCategory(category, evt) {
currentCategory = category;
const categoryBtns = document.querySelectorAll('.category-btn');
categoryBtns.forEach(btn => btn.classList.remove('active'));
const clickedBtn = evt && (evt.currentTarget || (evt.target && evt.target.closest('.category-btn')));
if (clickedBtn) {
clickedBtn.classList.add('active');
} else {
// Find and activate the clicked button
alphaBtns.forEach(btn => {
if (btn.textContent.trim() === letter || (letter === 'all' && btn.textContent.trim() === 'All')) {
categoryBtns.forEach(btn => {
if (btn.getAttribute('data-category') === category) {
btn.classList.add('active');
}
});
@@ -1551,6 +1785,46 @@ function filterByLetter(letter) {
displayStorePlugins();
}
function filterByLetter(letter, evt) {
currentFilter = letter;
const alphaBtns = document.querySelectorAll('.alpha-btn');
alphaBtns.forEach(btn => btn.classList.remove('active'));
const clickedBtn = evt && (evt.currentTarget || (evt.target && evt.target.closest('.alpha-btn')));
if (clickedBtn) {
clickedBtn.classList.add('active');
} else {
const label = letter === 'all' ? 'All' : letter;
alphaBtns.forEach(btn => {
if (btn.textContent.trim() === label) {
btn.classList.add('active');
}
});
}
displayStorePlugins();
}
function clearPluginSearch() {
const input = document.getElementById('pluginSearchInput');
const clearBtn = document.getElementById('pluginSearchClear');
if (input) {
input.value = '';
currentSearchQuery = '';
if (clearBtn) clearBtn.style.display = 'none';
displayStorePlugins();
input.focus();
}
}
function toggleAlphabetFilter() {
const filter = document.getElementById('alphabetFilter');
const toggleBtn = document.getElementById('alphabetToggleBtn');
if (!filter || !toggleBtn) return;
const isExpanded = toggleBtn.getAttribute('aria-expanded') === 'true';
filter.style.display = isExpanded ? 'none' : 'flex';
filter.setAttribute('aria-hidden', isExpanded ? 'true' : 'false');
toggleBtn.setAttribute('aria-expanded', isExpanded ? 'false' : 'true');
}
function upgradePlugin(pluginName, currentVersion, newVersion) {
// Show confirmation dialog with backup warning
const message = `⚠️ WARNING: Plugin Upgrade\n\n` +
@@ -2206,16 +2480,46 @@ document.addEventListener('DOMContentLoaded', function() {
// Update cache expiry time to local timezone
updateCacheExpiryTime();
// Default to grid view if plugins exist, otherwise show store
const gridView = document.getElementById('gridView');
const storeView = document.getElementById('storeView');
if (gridView && gridView.children.length > 0) {
toggleView('grid');
} else if (storeView) {
// No plugins installed, show store by default
toggleView('store');
// Search input listener (debounced)
const searchInput = document.getElementById('pluginSearchInput');
const searchClearBtn = document.getElementById('pluginSearchClear');
if (searchInput) {
searchInput.addEventListener('input', function() {
currentSearchQuery = this.value;
if (searchClearBtn) {
searchClearBtn.style.display = currentSearchQuery.trim() ? 'block' : 'none';
}
displayStorePlugins();
});
searchInput.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
clearPluginSearch();
e.preventDefault();
}
});
}
// Check URL hash for view preference
const hash = window.location.hash.substring(1); // Remove #
const validViews = ['grid', 'table', 'store'];
let initialView = 'grid'; // Default
if (validViews.includes(hash)) {
initialView = hash;
} else {
// Default to grid view if plugins exist, otherwise show store
const gridView = document.getElementById('gridView');
if (gridView && gridView.children.length > 0) {
initialView = 'grid';
} else {
initialView = 'store';
}
}
// Set initial view without updating hash (only update hash if there was already one)
const hadHash = hash.length > 0;
toggleView(initialView, hadHash);
// Load store plugins if store view is visible (either from toggleView or already displayed)
setTimeout(function() {
const storeViewCheck = document.getElementById('storeView');
@@ -2224,5 +2528,21 @@ document.addEventListener('DOMContentLoaded', function() {
}
}, 100); // Small delay to ensure DOM is ready
});
// Handle hash changes (back/forward browser buttons)
window.addEventListener('hashchange', function() {
// Prevent infinite loops when we programmatically set the hash
if (isSettingHash) {
return;
}
const hash = window.location.hash.substring(1);
const validViews = ['grid', 'table', 'store'];
if (validViews.includes(hash)) {
// Don't update hash again since it's already set (user navigated via browser)
toggleView(hash, false);
}
});
</script>
{% endblock %}

View File

@@ -115,20 +115,27 @@ def installed(request):
desc_elem = root.find('description')
version_elem = root.find('version')
# Type field is optional (testPlugin doesn't have it)
if name_elem is None or desc_elem is None or version_elem is None:
errorPlugins.append({'name': plugin, 'error': 'Missing required metadata fields (name, description, or version)'})
# All fields required including type (category) - no default
if name_elem is None or type_elem is None or desc_elem is None or version_elem is None:
errorPlugins.append({'name': plugin, 'error': 'Missing required metadata fields (name, type/category, description, or version)'})
logging.writeToFile(f"Plugin {plugin}: Missing required metadata fields in meta.xml")
continue
# Check if text is None (empty elements)
if name_elem.text is None or desc_elem.text is None or version_elem.text is None:
errorPlugins.append({'name': plugin, 'error': 'Empty metadata fields'})
# Check if text is None or empty (all required)
type_text = type_elem.text.strip() if type_elem.text else ''
if name_elem.text is None or desc_elem.text is None or version_elem.text is None or not type_text:
errorPlugins.append({'name': plugin, 'error': 'Empty metadata fields (name, type/category, description, or version required)'})
logging.writeToFile(f"Plugin {plugin}: Empty metadata fields in meta.xml")
continue
# Valid categories only: Utility, Security, Backup, Performance (Plugin category removed)
if type_text.lower() not in ('utility', 'security', 'backup', 'performance'):
errorPlugins.append({'name': plugin, 'error': f'Invalid category "{type_text}". Use: Utility, Security, Backup, or Performance.'})
logging.writeToFile(f"Plugin {plugin}: Invalid category '{type_text}'")
continue
data['name'] = name_elem.text
data['type'] = type_elem.text if type_elem is not None and type_elem.text is not None else 'Plugin'
data['type'] = type_text
data['desc'] = desc_elem.text
data['version'] = version_elem.text
data['plugin_dir'] = plugin # Plugin directory name
@@ -252,20 +259,28 @@ def installed(request):
pluginMetaData = ElementTree.parse(metaXmlPath)
root = pluginMetaData.getroot()
# Validate required fields
# Validate required fields (including type/category - no default)
name_elem = root.find('name')
type_elem = root.find('type')
desc_elem = root.find('description')
version_elem = root.find('version')
if name_elem is None or desc_elem is None or version_elem is None:
if name_elem is None or type_elem is None or desc_elem is None or version_elem is None:
errorPlugins.append({'name': plugin, 'error': 'Missing required metadata (name, type/category, description, or version)'})
continue
if name_elem.text is None or desc_elem.text is None or version_elem.text is None:
type_text = type_elem.text.strip() if type_elem.text else ''
if name_elem.text is None or desc_elem.text is None or version_elem.text is None or not type_text:
errorPlugins.append({'name': plugin, 'error': 'Empty metadata (type/category required)'})
continue
# Valid categories only: Utility, Security, Backup, Performance (Plugin category removed)
if type_text.lower() not in ('utility', 'security', 'backup', 'performance'):
errorPlugins.append({'name': plugin, 'error': f'Invalid category "{type_text}". Use: Utility, Security, Backup, or Performance.'})
continue
data['name'] = name_elem.text
data['type'] = type_elem.text if type_elem is not None and type_elem.text is not None else 'Plugin'
data['type'] = type_text
data['desc'] = desc_elem.text
data['version'] = version_elem.text
data['plugin_dir'] = plugin
@@ -943,6 +958,7 @@ def _enrich_store_plugins(plugins):
elif 'is_paid' not in plugin or plugin.get('is_paid') is None:
# Try to check from local meta.xml if available
meta_path = None
source_path = os.path.join(plugin_source_dir, plugin_dir)
if os.path.exists(installed_path):
meta_path = os.path.join(installed_path, 'meta.xml')
elif os.path.exists(source_path):
@@ -1050,10 +1066,20 @@ def _fetch_plugins_from_github():
patreon_url_elem = root.find('patreon_url')
patreon_url = patreon_url_elem.text if patreon_url_elem is not None else 'https://www.patreon.com/c/newstargeted/membership'
# Category (type) is required - valid: Utility, Security, Backup, Performance (Plugin removed)
type_elem = root.find('type')
if type_elem is None or not type_elem.text or not type_elem.text.strip():
logging.writeToFile(f"Plugin {plugin_name}: Missing required type/category in meta.xml, skipping")
continue
type_text = type_elem.text.strip().lower()
if type_text not in ('utility', 'security', 'backup', 'performance'):
logging.writeToFile(f"Plugin {plugin_name}: Invalid category '{type_elem.text}', skipping (use Utility, Security, Backup, or Performance)")
continue
plugin_data = {
'plugin_dir': plugin_name,
'name': root.find('name').text if root.find('name') is not None else plugin_name,
'type': root.find('type').text if root.find('type') is not None else 'Plugin',
'type': type_elem.text.strip(),
'description': root.find('description').text if root.find('description') is not None else '',
'version': root.find('version').text if root.find('version') is not None else '1.0.0',
'url': root.find('url').text if root.find('url') is not None else f'/plugins/{plugin_name}/',
@@ -1110,21 +1136,29 @@ def _fetch_plugins_from_github():
@require_http_methods(["GET"])
def fetch_plugin_store(request):
"""Fetch plugins from the plugin store with caching"""
mailUtilities.checkHome()
# Try to get from cache first
cached_plugins = _get_cached_plugins()
if cached_plugins is not None:
# Enrich cached plugins with installed/enabled status
enriched_plugins = _enrich_store_plugins(cached_plugins)
return JsonResponse({
'success': True,
'plugins': enriched_plugins,
'cached': True
})
# Cache miss or expired - fetch from GitHub
try:
mailUtilities.checkHome()
except Exception as e:
logging.writeToFile(f"fetch_plugin_store: checkHome failed: {str(e)}")
return JsonResponse({
'success': False,
'error': 'Authentication required. Please log in again.',
'plugins': []
}, status=401)
try:
# Try to get from cache first
cached_plugins = _get_cached_plugins()
if cached_plugins is not None:
# Enrich cached plugins with installed/enabled status
enriched_plugins = _enrich_store_plugins(cached_plugins)
return JsonResponse({
'success': True,
'plugins': enriched_plugins,
'cached': True
})
# Cache miss or expired - fetch from GitHub
plugins = _fetch_plugins_from_github()
# Enrich plugins with installed/enabled status
@@ -1139,7 +1173,7 @@ def fetch_plugin_store(request):
'plugins': enriched_plugins,
'cached': False
})
except Exception as e:
error_message = str(e)

View File

@@ -0,0 +1,13 @@
# Plugin Default Removal - 2026-02-01
## Summary
CyberPanel repository no longer requires any plugins by default. Plugins are installed by users from the [Plugin Store](https://github.com/master3395/cyberpanel-plugins) via the CyberPanel Plugin Manager.
## Changes
- **settings.py**: Removed `emailMarketing` from `INSTALLED_APPS`
- **urls.py**: Commented out `emailMarketing` route (plugin installer adds it when plugin is installed)
## Plugin Installation
Users install plugins from: https://github.com/master3395/cyberpanel-plugins
The plugin installer adds apps to `INSTALLED_APPS` and URL routes when plugins are installed via the Plugin Store UI.