Update pluginHolder with Free/Paid badges and Plugin Information support

- Added Free/Paid badges to Grid View, Table View, and Plugin Store
- Fixed intermittent badge display issues with robust boolean handling
- Updated plugin store to show plugin icons and proper pricing badges
- Removed Deactivate/Uninstall from Plugin Store (only Install/Installed)
- Fixed template syntax errors and duplicate navigation buttons
- Enhanced cache handling for plugin metadata (is_paid, patreon_url, etc.)
- Improved JavaScript cache-busting and isPaid normalization
This commit is contained in:
master3395
2026-01-25 20:55:56 +01:00
parent b4a9a0741f
commit 56cb95fadd
9 changed files with 1319 additions and 128 deletions

Binary file not shown.

BIN
pluginHolder/fail2ban.zip Normal file

Binary file not shown.

View File

@@ -0,0 +1,245 @@
# -*- coding: utf-8 -*-
"""
Patreon Verifier for CyberPanel Plugins
Verifies Patreon membership status for paid plugins
"""
import urllib.request
import urllib.error
import json
import os
import sys
# Patreon API configuration
PATREON_API_BASE = 'https://www.patreon.com/api/oauth2/v2'
PATREON_MEMBERSHIP_TIER = 'CyberPanel Paid Plugin' # The membership tier name to check
class PatreonVerifier:
"""
Verifies Patreon membership status for CyberPanel users
"""
def __init__(self):
"""Initialize Patreon verifier"""
# Try to import from Django settings first, then fallback to environment
try:
from django.conf import settings
self.client_id = getattr(settings, 'PATREON_CLIENT_ID', os.environ.get('PATREON_CLIENT_ID', ''))
self.client_secret = getattr(settings, 'PATREON_CLIENT_SECRET', os.environ.get('PATREON_CLIENT_SECRET', ''))
self.creator_id = getattr(settings, 'PATREON_CREATOR_ID', os.environ.get('PATREON_CREATOR_ID', ''))
self.membership_tier_id = getattr(settings, 'PATREON_MEMBERSHIP_TIER_ID', os.environ.get('PATREON_MEMBERSHIP_TIER_ID', '27789984'))
self.creator_access_token = getattr(settings, 'PATREON_CREATOR_ACCESS_TOKEN', os.environ.get('PATREON_CREATOR_ACCESS_TOKEN', ''))
except:
# Fallback to environment variables only
self.client_id = os.environ.get('PATREON_CLIENT_ID', '')
self.client_secret = os.environ.get('PATREON_CLIENT_SECRET', '')
self.creator_id = os.environ.get('PATREON_CREATOR_ID', '')
self.membership_tier_id = os.environ.get('PATREON_MEMBERSHIP_TIER_ID', '27789984')
self.creator_access_token = os.environ.get('PATREON_CREATOR_ACCESS_TOKEN', '')
# Cache for membership checks (to avoid excessive API calls)
self.cache_file = '/home/cyberpanel/patreon_cache.json'
self.cache_duration = 300 # Cache for 5 minutes
def get_user_patreon_token(self, user_email):
"""
Get stored Patreon access token for a user
This should be stored when user authorizes via Patreon OAuth
"""
# In a real implementation, you'd store this in a database
# For now, we'll check if there's a stored token file
token_file = f'/home/cyberpanel/patreon_tokens/{user_email}.token'
if os.path.exists(token_file):
try:
with open(token_file, 'r') as f:
token_data = json.load(f)
return token_data.get('access_token')
except:
return None
return None
def check_membership_cached(self, user_email):
"""
Check membership with caching
"""
import time
cache_data = self._load_cache()
cache_key = f"membership_{user_email}"
if cache_key in cache_data:
cached_result = cache_data[cache_key]
if time.time() - cached_result.get('timestamp', 0) < self.cache_duration:
return cached_result.get('has_membership', False)
# Check membership via API
has_membership = self.check_membership(user_email)
# Update cache
cache_data[cache_key] = {
'has_membership': has_membership,
'timestamp': time.time()
}
self._save_cache(cache_data)
return has_membership
def check_membership(self, user_email):
"""
Check if user has active Patreon membership for 'CyberPanel Paid Plugin'
Args:
user_email: User's email address
Returns:
bool: True if user has active membership, False otherwise
"""
access_token = self.get_user_patreon_token(user_email)
if not access_token:
return False
try:
# Get user's identity
user_info = self._get_user_identity(access_token)
if not user_info:
return False
member_id = user_info.get('id')
if not member_id:
return False
# Get user's memberships
memberships = self._get_memberships(access_token, member_id)
if not memberships:
return False
# Check if user has the required membership tier
# First try to match by tier ID (more accurate)
for membership in memberships:
tier_id = membership.get('id', '')
tier_name = membership.get('attributes', {}).get('title', '')
# Check by tier ID first (most accurate)
if tier_id == self.membership_tier_id:
# Check if membership is active
status = membership.get('attributes', {}).get('patron_status', '')
if status in ['active_patron', 'former_patron']:
# Check if currently entitled
entitled = membership.get('attributes', {}).get('currently_entitled_amount_cents', 0)
if entitled > 0:
return True
# Fallback: Check by tier name (for compatibility)
if PATREON_MEMBERSHIP_TIER.lower() in tier_name.lower():
# Check if membership is active
status = membership.get('attributes', {}).get('patron_status', '')
if status in ['active_patron', 'former_patron']:
# Check if currently entitled
entitled = membership.get('attributes', {}).get('currently_entitled_amount_cents', 0)
if entitled > 0:
return True
return False
except Exception as e:
import logging
logging.writeToFile(f"Error checking Patreon membership for {user_email}: {str(e)}")
return False
def _get_user_identity(self, access_token):
"""
Get user identity from Patreon API
"""
url = f"{PATREON_API_BASE}/identity"
try:
req = urllib.request.Request(url)
req.add_header('Authorization', f'Bearer {access_token}')
with urllib.request.urlopen(req) as response:
data = json.loads(response.read().decode())
return data.get('data', {})
except Exception as e:
import logging
logging.writeToFile(f"Error getting Patreon identity: {str(e)}")
return None
def _get_memberships(self, access_token, member_id):
"""
Get user's memberships from Patreon API
"""
url = f"{PATREON_API_BASE}/members/{member_id}?include=currently_entitled_tiers"
try:
req = urllib.request.Request(url)
req.add_header('Authorization', f'Bearer {access_token}')
with urllib.request.urlopen(req) as response:
data = json.loads(response.read().decode())
# Parse included tiers
memberships = []
included = data.get('included', [])
for item in included:
if item.get('type') == 'tier':
# Include tier ID in the membership data
tier_data = {
'id': item.get('id', ''),
'type': item.get('type', ''),
'attributes': item.get('attributes', {})
}
memberships.append(tier_data)
return memberships
except Exception as e:
import logging
logging.writeToFile(f"Error getting Patreon memberships: {str(e)}")
return []
def _load_cache(self):
"""Load cache from file"""
if os.path.exists(self.cache_file):
try:
with open(self.cache_file, 'r') as f:
return json.load(f)
except:
return {}
return {}
def _save_cache(self, cache_data):
"""Save cache to file"""
try:
os.makedirs(os.path.dirname(self.cache_file), exist_ok=True)
with open(self.cache_file, 'w') as f:
json.dump(cache_data, f)
except Exception as e:
import logging
logging.writeToFile(f"Error saving Patreon cache: {str(e)}")
def verify_plugin_access(self, user_email, plugin_name):
"""
Verify if user can access a paid plugin
Args:
user_email: User's email address
plugin_name: Name of the plugin to check
Returns:
dict: {
'has_access': bool,
'is_paid': bool,
'message': str
}
"""
# Check if plugin is paid (this will be checked from meta.xml)
# For now, we'll assume the plugin system will pass this info
# Check membership
has_membership = self.check_membership_cached(user_email)
return {
'has_access': has_membership,
'is_paid': True, # This will be determined by plugin metadata
'message': 'Access granted' if has_membership else 'Patreon subscription required'
}

View File

@@ -0,0 +1,130 @@
# -*- coding: utf-8 -*-
"""
Plugin Access Control
Checks if user has access to paid plugins
"""
from .patreon_verifier import PatreonVerifier
import logging
def check_plugin_access(request, plugin_name, plugin_meta=None):
"""
Check if user has access to a plugin
Args:
request: Django request object
plugin_name: Name of the plugin
plugin_meta: Plugin metadata dict (optional, will be loaded if not provided)
Returns:
dict: {
'has_access': bool,
'is_paid': bool,
'message': str,
'patreon_url': str or None
}
"""
# Default response for free plugins
default_response = {
'has_access': True,
'is_paid': False,
'message': 'Access granted',
'patreon_url': None
}
# If plugin_meta not provided, try to load it
if plugin_meta is None:
plugin_meta = _load_plugin_meta(plugin_name)
# Check if plugin is paid
if not plugin_meta or not plugin_meta.get('is_paid', False):
return default_response
# Plugin is paid - check Patreon membership
if not request.user or not request.user.is_authenticated:
return {
'has_access': False,
'is_paid': True,
'message': 'Please log in to access this plugin',
'patreon_url': plugin_meta.get('patreon_url')
}
# Get user email
user_email = getattr(request.user, 'email', None)
if not user_email:
# Try to get from username or other fields
user_email = getattr(request.user, 'username', '')
if not user_email:
return {
'has_access': False,
'is_paid': True,
'message': 'Unable to verify user identity',
'patreon_url': plugin_meta.get('patreon_url')
}
# Check Patreon membership
verifier = PatreonVerifier()
has_membership = verifier.check_membership_cached(user_email)
if has_membership:
return {
'has_access': True,
'is_paid': True,
'message': 'Access granted',
'patreon_url': None
}
else:
return {
'has_access': False,
'is_paid': True,
'message': f'This plugin requires a Patreon subscription to "{plugin_meta.get("patreon_tier", "CyberPanel Paid Plugin")}"',
'patreon_url': plugin_meta.get('patreon_url', 'https://www.patreon.com/c/newstargeted/membership')
}
def _load_plugin_meta(plugin_name):
"""
Load plugin metadata from meta.xml
Args:
plugin_name: Name of the plugin
Returns:
dict: Plugin metadata or None
"""
import os
from xml.etree import ElementTree
installed_path = f'/usr/local/CyberCP/{plugin_name}/meta.xml'
source_path = f'/home/cyberpanel/plugins/{plugin_name}/meta.xml'
meta_path = None
if os.path.exists(installed_path):
meta_path = installed_path
elif os.path.exists(source_path):
meta_path = source_path
if not meta_path:
return None
try:
tree = ElementTree.parse(meta_path)
root = tree.getroot()
# Extract paid plugin information
paid_elem = root.find('paid')
patreon_tier_elem = root.find('patreon_tier')
patreon_url_elem = root.find('patreon_url')
is_paid = False
if paid_elem is not None and paid_elem.text and paid_elem.text.lower() == 'true':
is_paid = True
return {
'is_paid': is_paid,
'patreon_tier': patreon_tier_elem.text if patreon_tier_elem is not None and patreon_tier_elem.text else 'CyberPanel Paid Plugin',
'patreon_url': patreon_url_elem.text if patreon_url_elem is not None else 'https://www.patreon.com/c/newstargeted/membership'
}
except Exception as e:
logging.writeToFile(f"Error loading plugin meta for {plugin_name}: {str(e)}")
return None

View File

@@ -0,0 +1,351 @@
{% extends "baseTemplate/index.html" %}
{% load i18n %}
{% block title %}{% trans "Plugin Help - " %}{{ plugin_name }} - CyberPanel{% endblock %}
{% block header_scripts %}
<style>
/* Plugin Help Page Styles */
.plugin-help-wrapper {
background: transparent;
padding: 20px;
}
.plugin-help-container {
max-width: 900px;
margin: 0 auto;
}
/* Page Header */
.help-page-header {
background: var(--bg-primary, white);
border-radius: 12px;
padding: 30px;
margin-bottom: 25px;
box-shadow: var(--shadow-md, 0 2px 8px rgba(0,0,0,0.08));
border: 1px solid var(--border-primary, #e8e9ff);
border-left: 4px solid #5856d6;
}
.help-page-header h1 {
font-size: 32px;
font-weight: 700;
color: var(--text-primary, #2f3640);
margin: 0 0 15px 0;
display: flex;
align-items: center;
gap: 15px;
}
.help-page-header .help-icon {
width: 48px;
height: 48px;
background: #5856d6;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 24px;
box-shadow: 0 4px 12px rgba(88,86,214,0.3);
}
.help-page-header .plugin-title {
font-size: 24px;
font-weight: 600;
color: var(--text-primary, #2f3640);
margin-bottom: 10px;
}
.help-page-header .plugin-meta {
display: flex;
flex-wrap: wrap;
gap: 20px;
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid var(--border-primary, #e8e9ff);
font-size: 14px;
color: var(--text-secondary, #64748b);
}
.help-page-header .plugin-meta-item {
display: flex;
align-items: center;
gap: 8px;
}
.help-page-header .plugin-meta-item strong {
color: var(--text-primary, #2f3640);
}
/* Content Section */
.help-content-section {
background: var(--bg-primary, white);
border-radius: 12px;
padding: 35px;
margin-bottom: 25px;
box-shadow: var(--shadow-md, 0 2px 8px rgba(0,0,0,0.08));
border: 1px solid var(--border-primary, #e8e9ff);
}
.help-content {
line-height: 1.8;
color: var(--text-primary, #2f3640);
font-size: 15px;
}
/* Help content styling for HTML */
.help-content h1,
.help-content h2,
.help-content h3,
.help-content h4,
.help-content h5,
.help-content h6 {
color: var(--text-primary, #2f3640);
margin-top: 30px;
margin-bottom: 15px;
font-weight: 700;
}
.help-content h1 {
font-size: 28px;
border-bottom: 2px solid #5856d6;
padding-bottom: 10px;
}
.help-content h2 {
font-size: 24px;
color: #5856d6;
}
.help-content h3 {
font-size: 20px;
}
.help-content p {
margin-bottom: 15px;
}
.help-content ul,
.help-content ol {
margin-left: 25px;
margin-bottom: 15px;
}
.help-content li {
margin-bottom: 8px;
}
.help-content code {
background: var(--bg-secondary, #f8f9ff);
padding: 2px 6px;
border-radius: 4px;
font-family: 'Courier New', monospace;
font-size: 14px;
color: #5856d6;
}
.help-content pre {
background: var(--bg-secondary, #f8f9ff);
padding: 15px;
border-radius: 8px;
overflow-x: auto;
border-left: 4px solid #5856d6;
margin-bottom: 15px;
}
.help-content pre code {
background: none;
padding: 0;
color: var(--text-primary, #2f3640);
}
.help-content blockquote {
border-left: 4px solid #5856d6;
padding-left: 20px;
margin-left: 0;
margin-bottom: 15px;
color: var(--text-secondary, #64748b);
font-style: italic;
}
.help-content table {
width: 100%;
border-collapse: collapse;
margin-bottom: 15px;
}
.help-content table th,
.help-content table td {
padding: 12px;
border: 1px solid var(--border-primary, #e8e9ff);
text-align: left;
}
.help-content table th {
background: var(--bg-secondary, #f8f9ff);
font-weight: 600;
color: var(--text-primary, #2f3640);
}
.help-content a {
color: #5856d6;
text-decoration: none;
border-bottom: 1px dotted #5856d6;
}
.help-content a:hover {
color: #4a48c4;
border-bottom: 1px solid #4a48c4;
}
.help-content img {
max-width: 100%;
height: auto;
border-radius: 8px;
margin: 15px 0;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
/* Actions Section */
.help-actions {
background: var(--bg-primary, white);
border-radius: 12px;
padding: 25px;
box-shadow: var(--shadow-md, 0 2px 8px rgba(0,0,0,0.08));
border: 1px solid var(--border-primary, #e8e9ff);
display: flex;
gap: 15px;
flex-wrap: wrap;
}
.help-action-btn {
padding: 10px 20px;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 8px;
transition: all 0.3s ease;
}
.help-action-btn-primary {
background: #5856d6;
color: white;
}
.help-action-btn-primary:hover {
background: #4a48c4;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(88,86,214,0.3);
}
.help-action-btn-secondary {
background: var(--bg-secondary, #f8f9ff);
color: var(--text-secondary, #64748b);
border: 1px solid var(--border-primary, #e8e9ff);
}
.help-action-btn-secondary:hover {
background: var(--bg-hover, #f0f1ff);
color: #5856d6;
border-color: #5856d6;
}
/* Responsive */
@media (max-width: 768px) {
.plugin-help-wrapper {
padding: 15px;
}
.help-page-header {
padding: 20px;
}
.help-page-header h1 {
font-size: 24px;
flex-direction: column;
text-align: center;
}
.help-content-section {
padding: 20px;
}
.help-actions {
flex-direction: column;
}
.help-action-btn {
width: 100%;
justify-content: center;
}
}
</style>
{% endblock %}
{% block content %}
{% load static %}
<div class="plugin-help-wrapper">
<div class="plugin-help-container">
<!-- Page Header -->
<div class="help-page-header">
<h1>
<div class="help-icon">
<i class="fas fa-question-circle"></i>
</div>
{% trans "Module Help" %}
</h1>
<div class="plugin-title">{{ plugin_name }}</div>
{% if plugin_description %}
<p style="color: var(--text-secondary, #64748b); margin: 10px 0 0 0;">
{{ plugin_description }}
</p>
{% endif %}
<div class="plugin-meta">
<div class="plugin-meta-item">
<i class="fas fa-info-circle"></i>
<strong>{% trans "Version:" %}</strong> {{ plugin_version }}
</div>
<div class="plugin-meta-item">
<i class="fas fa-user"></i>
<strong>{% trans "Author:" %}</strong> {{ plugin_author }}
</div>
{% if installed %}
<div class="plugin-meta-item">
<i class="fas fa-check-circle"></i>
<strong>{% trans "Status:" %}</strong> <span style="color: #28a745;">{% trans "Installed" %}</span>
</div>
{% endif %}
</div>
</div>
<!-- Help Content -->
<div class="help-content-section">
<div class="help-content">
{{ help_content|safe }}
</div>
</div>
<!-- Actions -->
<div class="help-actions">
{% if installed %}
<a href="/plugins/{{ plugin_name_dir }}/" class="help-action-btn help-action-btn-primary">
<i class="fas fa-cog"></i>
{% trans "Plugin Settings" %}
</a>
{% endif %}
<a href="/plugins/installed" class="help-action-btn help-action-btn-secondary">
<i class="fas fa-arrow-left"></i>
{% trans "Back to Plugins" %}
</a>
<a href="https://github.com/master3395/cyberpanel-plugins/tree/main/{{ plugin_name_dir }}" target="_blank" class="help-action-btn help-action-btn-secondary">
<i class="fab fa-github"></i>
{% trans "View on GitHub" %}
</a>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,93 @@
{% extends "baseTemplate/index.html" %}
{% load i18n %}
{% block title %}{% trans "Plugin Not Found - CyberPanel" %}{% endblock %}
{% block header_scripts %}
<style>
.error-wrapper {
background: transparent;
padding: 20px;
}
.error-container {
max-width: 600px;
margin: 0 auto;
text-align: center;
padding: 60px 20px;
}
.error-icon {
font-size: 72px;
color: #dc3545;
margin-bottom: 20px;
}
.error-title {
font-size: 28px;
font-weight: 700;
color: var(--text-primary, #2f3640);
margin-bottom: 15px;
}
.error-message {
font-size: 16px;
color: var(--text-secondary, #64748b);
margin-bottom: 30px;
}
.error-actions {
display: flex;
gap: 15px;
justify-content: center;
flex-wrap: wrap;
}
.error-btn {
padding: 10px 20px;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 8px;
transition: all 0.3s ease;
}
.error-btn-primary {
background: #5856d6;
color: white;
}
.error-btn-primary:hover {
background: #4a48c4;
}
</style>
{% endblock %}
{% block content %}
<div class="error-wrapper">
<div class="error-container">
<div class="error-icon">
<i class="fas fa-exclamation-triangle"></i>
</div>
<h1 class="error-title">{% trans "Plugin Not Found" %}</h1>
<p class="error-message">
{% if plugin_name %}
{% trans "The plugin" %} "<strong>{{ plugin_name }}</strong>" {% trans "could not be found." %}
{% else %}
{% trans "The requested plugin could not be found." %}
{% endif %}
{% if error %}
<br><small style="color: #dc3545;">{{ error }}</small>
{% endif %}
</p>
<div class="error-actions">
<a href="/plugins/installed" class="error-btn error-btn-primary">
<i class="fas fa-arrow-left"></i>
{% trans "Back to Plugins" %}
</a>
</div>
</div>
</div>
{% endblock %}

View File

@@ -182,6 +182,103 @@
color: var(--text-secondary, #64748b);
}
.plugin-pricing-badge {
display: inline-block;
padding: 3px 8px;
border-radius: 4px;
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-left: 6px;
vertical-align: middle;
}
.plugin-pricing-badge.free {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.plugin-pricing-badge.paid {
background: #fff3cd;
color: #856404;
border: 1px solid #ffeaa7;
}
.paid-badge {
display: inline-block;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 4px 10px;
border-radius: 12px;
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-left: 8px;
vertical-align: middle;
}
.paid-badge i {
margin-right: 4px;
}
.subscription-warning {
background: #fff3cd;
border: 1px solid #ffc107;
border-radius: 8px;
padding: 12px;
margin-top: 10px;
font-size: 12px;
color: #856404;
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 10px;
}
.subscription-warning-content {
display: flex;
align-items: center;
flex: 1;
}
.subscription-warning i {
margin-right: 6px;
color: #ffc107;
}
.subscription-warning-button {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
background: linear-gradient(135deg, #f96854 0%, #f96854 100%);
color: white;
text-decoration: none;
border-radius: 6px;
font-size: 12px;
font-weight: 600;
transition: all 0.3s ease;
white-space: nowrap;
box-shadow: 0 2px 4px rgba(249, 104, 84, 0.3);
}
.subscription-warning-button:hover {
background: linear-gradient(135deg, #e55a47 0%, #e55a47 100%);
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(249, 104, 84, 0.4);
color: white;
text-decoration: none;
}
.subscription-warning-button i {
margin-right: 0;
color: white;
}
.plugin-description {
font-size: 13px;
color: var(--text-secondary, #64748b);
@@ -816,20 +913,35 @@
<div class="content-section">
<h2 class="section-title">{% trans "Plugins" %}</h2>
<!-- View Toggle -->
<div id="plugins-view-toggle" class="view-toggle" style="{% if not plugins %}margin-top: 0;{% endif %}">
<button class="view-btn {% if plugins %}active{% else %}{% endif %}" onclick="toggleView('grid')">
<!-- View Toggle (always shown) -->
<div class="view-toggle">
{% if plugins %}
<button class="view-btn active" onclick="toggleView('grid')">
<i class="fas fa-th-large"></i>
{% trans "Grid View" %}
</button>
<button class="view-btn {% if not plugins %}active{% endif %}" onclick="toggleView('table')">
<button class="view-btn" onclick="toggleView('table')">
<i class="fas fa-list"></i>
{% trans "Table View" %}
</button>
<button class="view-btn {% if not plugins %}active{% endif %}" onclick="toggleView('store')">
<button class="view-btn" onclick="toggleView('store')">
<i class="fas fa-store"></i>
{% trans "CyberPanel Plugin Store" %}
</button>
{% else %}
<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>
{% endif %}
<a href="/plugins/help/" class="view-btn" style="text-decoration: none;">
<i class="fas fa-book"></i>
{% trans "Plugin Development Guide" %}
@@ -837,6 +949,7 @@
</div>
{% if plugins %}
<!-- Grid View -->
<div id="gridView" class="plugins-grid">
{% for plugin in plugins %}
@@ -856,22 +969,35 @@
{% endif %}
</div>
<div class="plugin-info">
<h3 class="plugin-name">{{ plugin.name }}</h3>
<h3 class="plugin-name">{{ plugin.name }}{% if plugin.is_paid|default:False|default_if_none:False %} <span class="paid-badge" title="{% trans 'Paid Plugin - Patreon Subscription Required' %}"><i class="fas fa-crown"></i> {% trans "Premium" %}</span>{% endif %}</h3>
<div class="plugin-meta">
<span class="plugin-type">{{ plugin.type }}</span>
<span class="plugin-type">{{ plugin.type }}</span>
<span class="plugin-version-number">v{{ plugin.version }}</span>
{% if plugin.is_paid|default:False|default_if_none:False %}
<span class="plugin-pricing-badge paid">{% trans "Paid" %}</span>
{% else %}
<span class="plugin-pricing-badge free">{% trans "Free" %}</span>
{% endif %}
</div>
{% if plugin.author %}
<div class="plugin-author" style="margin-top: 8px; font-size: 13px; color: var(--text-secondary, #64748b);">
<i class="fas fa-user" style="margin-right: 5px;"></i>
<span>{% trans "Author:" %} {{ plugin.author }}</span>
</div>
{% endif %}
</div>
</div>
<div class="plugin-description">
{{ plugin.desc }}
{% if plugin.is_paid|default:False|default_if_none:False %}
<div class="subscription-warning">
<div class="subscription-warning-content">
<i class="fas fa-exclamation-triangle"></i>
<strong>{% trans "Paid Plugin:" %}</strong> {% trans "Requires Patreon subscription to" %} "{{ plugin.patreon_tier|default:'CyberPanel Paid Plugin' }}"
</div>
{% 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 %}
</div>
{% endif %}
</div>
<div class="plugin-status-section">
@@ -943,7 +1069,6 @@
<thead>
<tr>
<th>{% trans "Plugin Name" %}</th>
<th>{% trans "Author" %}</th>
<th>{% trans "Version" %}</th>
<th>{% trans "Modify Date" %}</th>
<th>{% trans "Status" %}</th>
@@ -959,13 +1084,13 @@
<td>
<strong>{{ plugin.name }}</strong>
</td>
<td>
<span style="color: var(--text-secondary, #64748b); font-size: 13px;">
{{ plugin.author|default:"Unknown" }}
</span>
</td>
<td>
<span class="plugin-version-number">{{ plugin.version }}</span>
{% if plugin.is_paid|default:False|default_if_none:False %}
<span class="plugin-pricing-badge paid">{% trans "Paid" %}</span>
{% else %}
<span class="plugin-pricing-badge free">{% trans "Free" %}</span>
{% endif %}
</td>
<td>
<small style="color: var(--text-secondary, #64748b);">
@@ -1097,13 +1222,12 @@
<table class="store-table">
<thead>
<tr>
<th>{% trans "Icon" %}</th>
<th>{% trans "Plugin Name" %}</th>
<th>{% trans "Author" %}</th>
<th>{% trans "Version" %}</th>
<th>{% trans "Pricing" %}</th>
<th>{% trans "Modify Date" %}</th>
<th>{% trans "Status" %}</th>
<th>{% trans "Action" %}</th>
<th>{% trans "Active" %}</th>
<th>{% trans "Help" %}</th>
<th>{% trans "About" %}</th>
</tr>
@@ -1125,6 +1249,8 @@
</div>
<script>
// Cache-busting version: 2026-01-25-v5 - Fixed is_paid boolean type enforcement and consistent rendering
// Force browser to reload this script by changing version number
let storePlugins = [];
let currentFilter = 'all';
@@ -1148,9 +1274,7 @@ function toggleView(view) {
const gridView = document.getElementById('gridView');
const tableView = document.getElementById('tableView');
const storeView = document.getElementById('storeView');
// Only select view buttons from the main view-toggle (prevent duplicates)
const viewToggle = document.getElementById('plugins-view-toggle');
const viewBtns = viewToggle ? viewToggle.querySelectorAll('.view-btn') : document.querySelectorAll('.view-btn');
const viewBtns = document.querySelectorAll('.view-btn');
viewBtns.forEach(btn => btn.classList.remove('active'));
@@ -1223,11 +1347,17 @@ function escapeHtml(text) {
}
function displayStorePlugins() {
// Version: 2026-01-25-v5 - Store view: Removed Status column, always show Free/Paid badges, fixed boolean handling
// CRITICAL: This function MUST create exactly 7 columns (no Status, no Deactivate/Uninstall)
const tbody = document.getElementById('storeTableBody');
if (!tbody) {
console.error('storeTableBody not found!');
return;
}
tbody.innerHTML = '';
if (!storePlugins || storePlugins.length === 0) {
tbody.innerHTML = '<tr><td colspan="9" style="text-align: center; padding: 20px; color: var(--text-secondary, #64748b);">No plugins available in store</td></tr>';
tbody.innerHTML = '<tr><td colspan="8" style="text-align: center; padding: 20px; color: var(--text-secondary, #64748b);">No plugins available in store</td></tr>';
return;
}
@@ -1239,57 +1369,38 @@ function displayStorePlugins() {
);
}
filteredPlugins.forEach(plugin => {
filteredPlugins.forEach(plugin => {
const row = document.createElement('tr');
// Status column
let statusHtml = '';
if (plugin.installed) {
statusHtml = '<span class="status-installed">Installed</span>';
} else {
statusHtml = '<span class="status-not-installed">Not Installed</span>';
// Plugin icon - based on plugin type (same logic as Grid/Table views)
const pluginType = (plugin.type || 'Plugin').toLowerCase();
let iconClass = 'fas fa-puzzle-piece'; // Default icon
if (pluginType.includes('security')) {
iconClass = 'fas fa-shield-alt';
} else if (pluginType.includes('performance')) {
iconClass = 'fas fa-rocket';
} else if (pluginType.includes('utility')) {
iconClass = 'fas fa-tools';
} else if (pluginType.includes('backup')) {
iconClass = 'fas fa-save';
}
const iconHtml = `<div class="plugin-icon" style="width: 40px; height: 40px; background: var(--bg-secondary, #f8f9ff); border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 18px; color: #5856d6; margin: 0 auto;">
<i class="${iconClass}"></i>
</div>`;
// Action column
// Action column - Store view only shows Install/Installed (no Deactivate/Uninstall)
// NOTE: Store view should NOT show Deactivate/Uninstall buttons - users manage from Grid/Table views
let actionHtml = '';
if (plugin.installed) {
if (plugin.enabled) {
actionHtml = `<div class="plugin-actions">
<button class="btn-action btn-deactivate" onclick="deactivatePlugin('${plugin.plugin_dir}')">
<i class="fas fa-toggle-on"></i> Deactivate
</button>
<button class="btn-action btn-uninstall" onclick="uninstallPluginFromStore('${plugin.plugin_dir}')">
<i class="fas fa-trash"></i> Uninstall
</button>
</div>`;
} else {
actionHtml = `<div class="plugin-actions">
<button class="btn-action btn-activate" onclick="activatePlugin('${plugin.plugin_dir}')">
<i class="fas fa-toggle-off"></i> Activate
</button>
<button class="btn-action btn-uninstall" onclick="uninstallPluginFromStore('${plugin.plugin_dir}')">
<i class="fas fa-trash"></i> Uninstall
</button>
</div>`;
}
// Show "Installed" text
actionHtml = '<span class="status-installed">Installed</span>';
} else {
// Show Install button
actionHtml = `<button class="btn-action btn-install" onclick="installFromStore('${plugin.plugin_dir}')">
<i class="fas fa-download"></i> Install
</button>`;
}
// Active column
let activeHtml = '';
if (plugin.installed) {
if (plugin.enabled) {
activeHtml = '<span class="active-status active-yes"><i class="fas fa-check-circle"></i></span>';
} else {
activeHtml = '<span class="active-status active-no"><i class="fas fa-times-circle"></i></span>';
}
} else {
activeHtml = '<span class="active-status active-na">-</span>';
}
// Help column - always use consistent help URL
const helpUrl = `/plugins/${plugin.plugin_dir}/help/`;
const helpHtml = `<a href="${helpUrl}" class="btn-link" title="Plugin Help">
@@ -1305,19 +1416,34 @@ function displayStorePlugins() {
// Modify Date column - show N/A for store plugins (they're from GitHub, not local)
const modifyDateHtml = plugin.modify_date ? `<small style="color: var(--text-secondary, #64748b);">${escapeHtml(plugin.modify_date)}</small>` : '<small style="color: var(--text-secondary, #64748b);">N/A</small>';
// Author column
const authorHtml = plugin.author ? `<span style="color: var(--text-secondary, #64748b); font-size: 13px;">${escapeHtml(plugin.author)}</span>` : '<span style="color: var(--text-secondary, #64748b); font-size: 13px;">Unknown</span>';
// 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
let isPaid = false;
if (plugin.is_paid !== undefined && plugin.is_paid !== null) {
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') {
isPaid = true;
} else {
isPaid = false; // Explicitly set to false for any other value
}
}
// Force boolean type
isPaid = Boolean(isPaid);
const pricingBadge = isPaid
? '<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)
row.innerHTML = `
<td style="text-align: center;">${iconHtml}</td>
<td>
<strong>${escapeHtml(plugin.name)}</strong>
</td>
<td>${authorHtml}</td>
<td>${escapeHtml(plugin.version)}</td>
<td><span class="plugin-version-number">${escapeHtml(plugin.version)}</span></td>
<td>${pricingBadge}</td>
<td>${modifyDateHtml}</td>
<td>${statusHtml}</td>
<td>${actionHtml}</td>
<td class="active-column">${activeHtml}</td>
<td>${helpHtml}</td>
<td>${aboutHtml}</td>
`;
@@ -1706,19 +1832,6 @@ if (localStorage.getItem('pluginStoreNoticeDismissed') === 'true') {
// Initialize view on page load
document.addEventListener('DOMContentLoaded', function() {
// Remove any duplicate view-toggle divs (keep only the one with id='plugins-view-toggle')
const mainViewToggle = document.getElementById('plugins-view-toggle');
if (mainViewToggle) {
const allViewToggles = document.querySelectorAll('.view-toggle');
allViewToggles.forEach(toggle => {
if (toggle !== mainViewToggle && toggle.id !== 'plugins-view-toggle') {
// This is a duplicate, remove it
console.log('Removing duplicate view-toggle div');
toggle.remove();
}
});
}
// Default to grid view if plugins exist, otherwise show store
const gridView = document.getElementById('gridView');
const storeView = document.getElementById('storeView');

BIN
pluginHolder/testPlugin.zip Normal file

Binary file not shown.

View File

@@ -18,6 +18,7 @@ import urllib.error
import time
sys.path.append('/usr/local/CyberCP')
from pluginInstaller.pluginInstaller import pluginInstaller
from .patreon_verifier import PatreonVerifier
# Plugin state file location
PLUGIN_STATE_DIR = '/home/cyberpanel/plugin_states'
@@ -135,6 +136,11 @@ def installed(request):
else:
data['enabled'] = False
# Initialize is_paid to False by default (will be set later if paid)
data['is_paid'] = False
data['patreon_tier'] = None
data['patreon_url'] = None
# Get modify date from local file (fast, no API calls)
# GitHub commit dates are fetched in the plugin store, not here to avoid timeouts
modify_date = 'N/A'
@@ -181,6 +187,27 @@ def installed(request):
data['author'] = author_elem.text
else:
data['author'] = 'Unknown'
# Extract paid plugin information
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 str(paid_elem.text).strip().lower() == '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'
else:
data['is_paid'] = False # Explicit boolean False
data['patreon_tier'] = None
data['patreon_url'] = 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)
processed_plugins.add(plugin) # Mark as processed
@@ -236,6 +263,11 @@ def installed(request):
data['installed'] = True # This is an installed plugin
data['enabled'] = _is_plugin_enabled(plugin)
# Initialize is_paid to False by default (will be set later if paid)
data['is_paid'] = False
data['patreon_tier'] = None
data['patreon_url'] = None
# Get modify date from installed location
modify_date = 'N/A'
try:
@@ -276,6 +308,26 @@ def installed(request):
else:
data['author'] = 'Unknown'
# Extract paid plugin information (is_paid already initialized to False above)
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 str(paid_elem.text).strip().lower() == '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')
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'
else:
data['is_paid'] = False # Explicit boolean False
# 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)
except ElementTree.ParseError as e:
@@ -315,11 +367,25 @@ def install_plugin(request, plugin_name):
# Create zip file for installation (pluginInstaller expects a zip)
import tempfile
import shutil
import zipfile
temp_dir = tempfile.mkdtemp()
zip_path = os.path.join(temp_dir, plugin_name + '.zip')
# Create zip from source directory
shutil.make_archive(os.path.join(temp_dir, plugin_name), 'zip', pluginSource)
# Create zip from source directory with correct structure
# The ZIP must contain plugin_name/ directory structure for proper extraction
plugin_zip = zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED)
# Walk through source directory and add files with plugin_name prefix
for root, dirs, files in os.walk(pluginSource):
for file in files:
file_path = os.path.join(root, file)
# Calculate relative path from plugin source
arcname = os.path.relpath(file_path, pluginSource)
# Add plugin_name prefix to maintain directory structure
arcname = os.path.join(plugin_name, arcname)
plugin_zip.write(file_path, arcname)
plugin_zip.close()
# Verify zip file was created
if not os.path.exists(zip_path):
@@ -340,11 +406,31 @@ def install_plugin(request, plugin_name):
raise Exception(f'Zip file {zip_file} not found in temp directory')
# Install using pluginInstaller
pluginInstaller.installPlugin(plugin_name)
try:
pluginInstaller.installPlugin(plugin_name)
except Exception as install_error:
# Log the full error for debugging
error_msg = str(install_error)
logging.writeToFile(f"pluginInstaller.installPlugin raised exception: {error_msg}")
# Check if plugin directory exists despite the error
pluginInstalled = '/usr/local/CyberCP/' + plugin_name
if os.path.exists(pluginInstalled):
logging.writeToFile(f"Plugin directory exists despite error, continuing...")
else:
raise Exception(f'Plugin installation failed: {error_msg}')
# Wait a moment for file system to sync
import time
time.sleep(2)
# Verify plugin was actually installed
pluginInstalled = '/usr/local/CyberCP/' + plugin_name
if not os.path.exists(pluginInstalled):
# Check if files were extracted to root instead
root_files = ['README.md', 'apps.py', 'meta.xml', 'urls.py', 'views.py']
found_root_files = [f for f in root_files if os.path.exists(os.path.join('/usr/local/CyberCP', f))]
if found_root_files:
raise Exception(f'Plugin installation failed: Files extracted to wrong location. Found {found_root_files} in /usr/local/CyberCP/ root instead of {pluginInstalled}/')
raise Exception(f'Plugin installation failed: {pluginInstalled} does not exist after installation')
# Set plugin to enabled by default after installation
@@ -562,6 +648,55 @@ def _enrich_store_plugins(plugins):
else:
plugin['enabled'] = False
# 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
# This ensures cache entries without is_paid are properly enriched
meta_path = None
if os.path.exists(installed_path):
meta_path = os.path.join(installed_path, 'meta.xml')
elif os.path.exists(source_path):
meta_path = os.path.join(source_path, 'meta.xml')
# If we have a local meta.xml, use it as the source of truth (most reliable)
if meta_path and os.path.exists(meta_path):
try:
pluginMetaData = ElementTree.parse(meta_path)
root = pluginMetaData.getroot()
paid_elem = root.find('paid')
if paid_elem is not None and paid_elem.text and paid_elem.text.lower() == 'true':
plugin['is_paid'] = True
# Also update patreon fields if available
patreon_tier_elem = root.find('patreon_tier')
if patreon_tier_elem is not None and patreon_tier_elem.text:
plugin['patreon_tier'] = patreon_tier_elem.text
patreon_url_elem = root.find('patreon_url')
if patreon_url_elem is not None and patreon_url_elem.text:
plugin['patreon_url'] = patreon_url_elem.text
else:
plugin['is_paid'] = False
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:
plugin['is_paid'] = False # Default to free if we can't determine
# Ensure it's a proper boolean (not string or other type)
plugin['is_paid'] = bool(plugin['is_paid']) if plugin['is_paid'] not in [True, False] else plugin['is_paid']
enriched.append(plugin)
return enriched
@@ -633,6 +768,20 @@ def _fetch_plugins_from_github():
logging.writeToFile(f"Could not fetch commit date for {plugin_name}: {str(e)}")
modify_date = 'N/A'
# Extract paid plugin information
paid_elem = root.find('paid')
patreon_tier_elem = root.find('patreon_tier')
is_paid = False
patreon_tier = None
patreon_url = None
if paid_elem is not None and paid_elem.text and paid_elem.text.lower() == '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'
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'
plugin_data = {
'plugin_dir': plugin_name,
'name': root.find('name').text if root.find('name') is not None else plugin_name,
@@ -644,7 +793,10 @@ def _fetch_plugins_from_github():
'author': root.find('author').text if root.find('author') is not None else 'Unknown',
'github_url': f'https://github.com/master3395/cyberpanel-plugins/tree/main/{plugin_name}',
'about_url': f'https://github.com/master3395/cyberpanel-plugins/tree/main/{plugin_name}',
'modify_date': modify_date
'modify_date': modify_date,
'is_paid': is_paid,
'patreon_tier': patreon_tier,
'patreon_url': patreon_url
}
plugins.append(plugin_data)
@@ -746,7 +898,7 @@ def fetch_plugin_store(request):
@csrf_exempt
@require_http_methods(["POST"])
def install_from_store(request, plugin_name):
"""Install plugin from GitHub store"""
"""Install plugin from GitHub store, with fallback to local source"""
mailUtilities.checkHome()
try:
@@ -771,44 +923,80 @@ def install_from_store(request, plugin_name):
zip_path = os.path.join(temp_dir, plugin_name + '.zip')
try:
# Download repository as ZIP
repo_zip_url = 'https://github.com/master3395/cyberpanel-plugins/archive/refs/heads/main.zip'
logging.writeToFile(f"Downloading plugin from: {repo_zip_url}")
# Try to download from GitHub first
use_local_fallback = False
try:
# Download repository as ZIP
repo_zip_url = 'https://github.com/master3395/cyberpanel-plugins/archive/refs/heads/main.zip'
logging.writeToFile(f"Downloading plugin from: {repo_zip_url}")
repo_req = urllib.request.Request(
repo_zip_url,
headers={
'User-Agent': 'CyberPanel-Plugin-Store/1.0',
'Accept': 'application/zip'
}
)
with urllib.request.urlopen(repo_req, timeout=30) as repo_response:
repo_zip_data = repo_response.read()
# Extract plugin directory from repository ZIP
repo_zip = zipfile.ZipFile(io.BytesIO(repo_zip_data))
# Find plugin directory in ZIP
plugin_prefix = f'cyberpanel-plugins-main/{plugin_name}/'
plugin_files = [f for f in repo_zip.namelist() if f.startswith(plugin_prefix)]
if not plugin_files:
logging.writeToFile(f"Plugin {plugin_name} not found in GitHub repository, trying local source")
use_local_fallback = True
else:
logging.writeToFile(f"Found {len(plugin_files)} files for plugin {plugin_name} in GitHub")
# Create plugin ZIP file from GitHub with correct structure
# The ZIP must contain plugin_name/ directory structure for proper extraction
plugin_zip = zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED)
for file_path in plugin_files:
# Remove the repository root prefix
relative_path = file_path[len(plugin_prefix):]
if relative_path: # Skip directories
file_data = repo_zip.read(file_path)
# Add plugin_name prefix to maintain directory structure
arcname = os.path.join(plugin_name, relative_path)
plugin_zip.writestr(arcname, file_data)
plugin_zip.close()
repo_zip.close()
except Exception as github_error:
logging.writeToFile(f"GitHub download failed for {plugin_name}: {str(github_error)}, trying local source")
use_local_fallback = True
repo_req = urllib.request.Request(
repo_zip_url,
headers={
'User-Agent': 'CyberPanel-Plugin-Store/1.0',
'Accept': 'application/zip'
}
)
with urllib.request.urlopen(repo_req, timeout=30) as repo_response:
repo_zip_data = repo_response.read()
# Extract plugin directory from repository ZIP
repo_zip = zipfile.ZipFile(io.BytesIO(repo_zip_data))
# Find plugin directory in ZIP
plugin_prefix = f'cyberpanel-plugins-main/{plugin_name}/'
plugin_files = [f for f in repo_zip.namelist() if f.startswith(plugin_prefix)]
if not plugin_files:
raise Exception(f'Plugin {plugin_name} not found in GitHub repository')
logging.writeToFile(f"Found {len(plugin_files)} files for plugin {plugin_name}")
# Create plugin ZIP file
plugin_zip = zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED)
for file_path in plugin_files:
# Remove the repository root prefix
relative_path = file_path[len(plugin_prefix):]
if relative_path: # Skip directories
file_data = repo_zip.read(file_path)
plugin_zip.writestr(relative_path, file_data)
plugin_zip.close()
# Fallback to local source if GitHub download failed
if use_local_fallback:
pluginSource = '/home/cyberpanel/plugins/' + plugin_name
if not os.path.exists(pluginSource):
raise Exception(f'Plugin {plugin_name} not found in GitHub repository and local source not found at {pluginSource}')
logging.writeToFile(f"Using local source for {plugin_name} from {pluginSource}")
# Create zip from local source directory with correct structure
# The ZIP must contain plugin_name/ directory structure for proper extraction
import zipfile
plugin_zip = zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED)
# Walk through source directory and add files with plugin_name prefix
for root, dirs, files in os.walk(pluginSource):
for file in files:
file_path = os.path.join(root, file)
# Calculate relative path from plugin source
arcname = os.path.relpath(file_path, pluginSource)
# Add plugin_name prefix to maintain directory structure
arcname = os.path.join(plugin_name, arcname)
plugin_zip.write(file_path, arcname)
plugin_zip.close()
# Verify ZIP was created
if not os.path.exists(zip_path):
@@ -829,15 +1017,31 @@ def install_from_store(request, plugin_name):
logging.writeToFile(f"Installing plugin using pluginInstaller")
# Install using pluginInstaller (direct call, not via command line)
pluginInstaller.installPlugin(plugin_name)
try:
pluginInstaller.installPlugin(plugin_name)
except Exception as install_error:
# Log the full error for debugging
error_msg = str(install_error)
logging.writeToFile(f"pluginInstaller.installPlugin raised exception: {error_msg}")
# Check if plugin directory exists despite the error
pluginInstalled = '/usr/local/CyberCP/' + plugin_name
if os.path.exists(pluginInstalled):
logging.writeToFile(f"Plugin directory exists despite error, continuing...")
else:
raise Exception(f'Plugin installation failed: {error_msg}')
# Wait a moment for file system to sync and service to restart
import time
time.sleep(2)
time.sleep(3) # Increased wait time for file system sync
# Verify plugin was actually installed
pluginInstalled = '/usr/local/CyberCP/' + plugin_name
if not os.path.exists(pluginInstalled):
# Check if files were extracted to root instead
root_files = ['README.md', 'apps.py', 'meta.xml', 'urls.py', 'views.py']
found_root_files = [f for f in root_files if os.path.exists(os.path.join('/usr/local/CyberCP', f))]
if found_root_files:
raise Exception(f'Plugin installation failed: Files extracted to wrong location. Found {found_root_files} in /usr/local/CyberCP/ root instead of {pluginInstalled}/')
raise Exception(f'Plugin installation failed: {pluginInstalled} does not exist after installation')
logging.writeToFile(f"Plugin {plugin_name} installed successfully")
@@ -1062,3 +1266,58 @@ def plugin_help(request, plugin_name):
proc = httpProc(request, 'pluginHolder/plugin_help.html', context, 'admin')
return proc.render()
@csrf_exempt
@require_http_methods(["GET"])
def check_plugin_subscription(request, plugin_name):
"""
API endpoint to check if user has Patreon subscription for a paid plugin
Args:
request: Django request object
plugin_name: Name of the plugin to check
Returns:
JsonResponse: {
'has_access': bool,
'is_paid': bool,
'message': str,
'patreon_url': str or None
}
"""
try:
# Check if user is authenticated
if not request.user or not request.user.is_authenticated:
return JsonResponse({
'success': False,
'has_access': False,
'is_paid': False,
'message': 'Please log in to check subscription status',
'patreon_url': None
}, status=401)
# Load plugin metadata
from .plugin_access import check_plugin_access, _load_plugin_meta
plugin_meta = _load_plugin_meta(plugin_name)
# Check access
access_result = check_plugin_access(request, plugin_name, plugin_meta)
return JsonResponse({
'success': True,
'has_access': access_result['has_access'],
'is_paid': access_result['is_paid'],
'message': access_result['message'],
'patreon_url': access_result.get('patreon_url')
})
except Exception as e:
logging.writeToFile(f"Error checking subscription for {plugin_name}: {str(e)}")
return JsonResponse({
'success': False,
'has_access': False,
'is_paid': False,
'message': f'Error checking subscription: {str(e)}',
'patreon_url': None
}, status=500)