2025-08-01 14:56:30 +05:00
# -*- coding: utf-8 -*-
2026-01-19 22:55:59 +01:00
from django . shortcuts import render , redirect
2026-01-04 21:04:51 +01:00
from django . http import JsonResponse
from django . views . decorators . csrf import csrf_exempt
from django . views . decorators . http import require_http_methods
2025-08-01 14:56:30 +05:00
from plogical . mailUtilities import mailUtilities
import os
2026-01-04 21:04:51 +01:00
import subprocess
import shlex
import json
2026-01-19 22:55:59 +01:00
from datetime import datetime , timedelta
2025-08-01 14:56:30 +05:00
from xml . etree import ElementTree
from plogical . httpProc import httpProc
2026-01-04 21:04:51 +01:00
from plogical . CyberCPLogFileWriter import CyberCPLogFileWriter as logging
import sys
2026-01-19 22:55:59 +01:00
import urllib . request
import urllib . error
import time
2026-01-04 21:04:51 +01:00
sys . path . append ( ' /usr/local/CyberCP ' )
from pluginInstaller . pluginInstaller import pluginInstaller
2026-01-25 20:55:56 +01:00
from . patreon_verifier import PatreonVerifier
2026-01-04 21:04:51 +01:00
# Plugin state file location
PLUGIN_STATE_DIR = ' /home/cyberpanel/plugin_states '
2026-01-19 22:55:59 +01:00
# Plugin store cache configuration
PLUGIN_STORE_CACHE_DIR = ' /home/cyberpanel/plugin_store_cache '
PLUGIN_STORE_CACHE_FILE = os . path . join ( PLUGIN_STORE_CACHE_DIR , ' plugins_cache.json ' )
PLUGIN_STORE_CACHE_DURATION = 3600 # Cache for 1 hour (3600 seconds)
GITHUB_REPO_API = ' https://api.github.com/repos/master3395/cyberpanel-plugins/contents '
GITHUB_RAW_BASE = ' https://raw.githubusercontent.com/master3395/cyberpanel-plugins/main '
GITHUB_COMMITS_API = ' https://api.github.com/repos/master3395/cyberpanel-plugins/commits '
2026-01-04 21:04:51 +01:00
def _get_plugin_state_file ( plugin_name ) :
""" Get the path to the plugin state file """
if not os . path . exists ( PLUGIN_STATE_DIR ) :
os . makedirs ( PLUGIN_STATE_DIR , mode = 0o755 )
return os . path . join ( PLUGIN_STATE_DIR , plugin_name + ' .state ' )
def _is_plugin_enabled ( plugin_name ) :
""" Check if a plugin is enabled """
state_file = _get_plugin_state_file ( plugin_name )
if os . path . exists ( state_file ) :
try :
with open ( state_file , ' r ' ) as f :
state = f . read ( ) . strip ( )
return state == ' enabled '
except :
return True # Default to enabled if file read fails
return True # Default to enabled if state file doesn't exist
def _set_plugin_state ( plugin_name , enabled ) :
""" Set plugin enabled/disabled state """
state_file = _get_plugin_state_file ( plugin_name )
try :
with open ( state_file , ' w ' ) as f :
f . write ( ' enabled ' if enabled else ' disabled ' )
os . chmod ( state_file , 0o644 )
return True
except Exception as e :
logging . writeToFile ( f " Error writing plugin state for { plugin_name } : { str ( e ) } " )
return False
2025-08-01 14:56:30 +05:00
2026-01-19 22:55:59 +01:00
def help_page ( request ) :
""" Display plugin development help page """
mailUtilities . checkHome ( )
proc = httpProc ( request , ' pluginHolder/help.html ' , { } , ' admin ' )
return proc . render ( )
2025-08-01 14:56:30 +05:00
def installed ( request ) :
mailUtilities . checkHome ( )
pluginPath = ' /home/cyberpanel/plugins '
2026-01-20 00:53:18 +01:00
installedPath = ' /usr/local/CyberCP '
2025-08-01 14:56:30 +05:00
pluginList = [ ]
2026-01-04 21:04:51 +01:00
errorPlugins = [ ]
2026-01-20 00:53:18 +01:00
processed_plugins = set ( ) # Track which plugins we've already processed
2025-08-01 14:56:30 +05:00
2026-01-20 00:53:18 +01:00
# First, process plugins from source directory
2025-08-01 14:56:30 +05:00
if os . path . exists ( pluginPath ) :
for plugin in os . listdir ( pluginPath ) :
2026-01-04 21:04:51 +01:00
# Skip files (like .zip files) - only process directories
pluginDir = os . path . join ( pluginPath , plugin )
if not os . path . isdir ( pluginDir ) :
continue
2025-08-01 14:56:30 +05:00
data = { }
2026-01-04 21:04:51 +01:00
# Try installed location first, then fallback to source location
2026-01-20 00:53:18 +01:00
completePath = installedPath + ' / ' + plugin + ' /meta.xml '
2026-01-04 21:04:51 +01:00
sourcePath = os . path . join ( pluginDir , ' meta.xml ' )
# Determine which meta.xml to use
metaXmlPath = None
if os . path . exists ( completePath ) :
metaXmlPath = completePath
elif os . path . exists ( sourcePath ) :
# Plugin not installed but has source meta.xml - use it
metaXmlPath = sourcePath
# Add error handling to prevent 500 errors
try :
if metaXmlPath is None :
# No meta.xml found in either location - skip silently
continue
pluginMetaData = ElementTree . parse ( metaXmlPath )
root = pluginMetaData . getroot ( )
# Validate required fields exist (handle both <plugin> and <cyberpanelPluginConfig> formats)
name_elem = root . find ( ' name ' )
type_elem = root . find ( ' type ' )
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) ' } )
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 ' } )
logging . writeToFile ( f " Plugin { plugin } : Empty metadata fields in meta.xml " )
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 [ ' desc ' ] = desc_elem . text
data [ ' version ' ] = version_elem . text
data [ ' plugin_dir ' ] = plugin # Plugin directory name
2026-01-26 17:45:36 +01:00
# Check if plugin is installed (only if it exists in /usr/local/CyberCP/)
# Source directory presence doesn't mean installed - it just means the source files are available
data [ ' installed ' ] = os . path . exists ( completePath )
2026-01-04 21:04:51 +01:00
# Get plugin enabled state (only for installed plugins)
if data [ ' installed ' ] :
data [ ' enabled ' ] = _is_plugin_enabled ( plugin )
else :
data [ ' enabled ' ] = False
2026-01-25 20:55:56 +01:00
# 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
2026-01-20 00:38:21 +01:00
# Get modify date from local file (fast, no API calls)
# GitHub commit dates are fetched in the plugin store, not here to avoid timeouts
2026-01-19 22:55:59 +01:00
modify_date = ' N/A '
try :
2026-01-20 00:08:46 +01:00
if os . path . exists ( metaXmlPath ) :
modify_time = os . path . getmtime ( metaXmlPath )
modify_date = datetime . fromtimestamp ( modify_time ) . strftime ( ' % Y- % m- %d % H: % M: % S ' )
except Exception :
modify_date = ' N/A '
2026-01-19 22:55:59 +01:00
data [ ' modify_date ' ] = modify_date
2026-01-04 21:04:51 +01:00
# Extract settings URL or main URL for "Manage" button
settings_url_elem = root . find ( ' settings_url ' )
url_elem = root . find ( ' url ' )
# Priority: settings_url > url > default pattern
2026-01-20 01:31:18 +01:00
# Special handling for core plugins that don't use /plugins/ prefix
if plugin == ' emailMarketing ' :
2026-01-26 17:45:36 +01:00
# emailMarketing is a core CyberPanel plugin, uses /emailMarketing/ not /plugins/emailMarketing/
data [ ' manage_url ' ] = ' /emailMarketing/ '
2026-01-20 01:31:18 +01:00
elif settings_url_elem is not None and settings_url_elem . text :
2026-01-04 21:04:51 +01:00
data [ ' manage_url ' ] = settings_url_elem . text
elif url_elem is not None and url_elem . text :
data [ ' manage_url ' ] = url_elem . text
else :
# Default: try /plugins/{plugin_dir}/settings/ or /plugins/{plugin_dir}/
# Only set if plugin is installed (we can't know if the URL exists otherwise)
2026-01-26 17:45:36 +01:00
# Special handling for emailMarketing
if plugin == ' emailMarketing ' :
data [ ' manage_url ' ] = ' /emailMarketing/ '
elif os . path . exists ( completePath ) :
2026-01-20 01:39:35 +01:00
# Check if settings route exists, otherwise use main plugin URL
settings_route = f ' /plugins/ { plugin } /settings/ '
main_route = f ' /plugins/ { plugin } / '
# Default to main route - most plugins have a main route even if no settings
data [ ' manage_url ' ] = main_route
2026-01-04 21:04:51 +01:00
else :
data [ ' manage_url ' ] = None
2026-01-20 01:31:18 +01:00
# Extract author information
author_elem = root . find ( ' author ' )
if author_elem is not None and author_elem . text :
data [ ' author ' ] = author_elem . text
else :
data [ ' author ' ] = ' Unknown '
2026-01-25 20:55:56 +01:00
2026-01-26 17:45:36 +01:00
# Extract paid plugin information
2026-01-25 20:55:56 +01:00
paid_elem = root . find ( ' paid ' )
2026-01-26 17:45:36 +01:00
patreon_tier_elem = root . find ( ' patreon_tier ' )
2026-01-25 20:55:56 +01:00
2026-01-26 17:45:36 +01:00
if paid_elem is not None and paid_elem . text and paid_elem . text . lower ( ) == ' true ' :
data [ ' is_paid ' ] = 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 '
2026-01-25 20:55:56 +01:00
else :
2026-01-26 17:45:36 +01:00
data [ ' is_paid ' ] = False
2026-01-25 20:55:56 +01:00
data [ ' patreon_tier ' ] = None
data [ ' patreon_url ' ] = None
2025-08-01 14:56:30 +05:00
2026-01-26 17:45:36 +01:00
pluginList . append ( data )
2026-01-20 00:55:24 +01:00
processed_plugins . add ( plugin ) # Mark as processed
2026-01-04 21:04:51 +01:00
except ElementTree . ParseError as e :
errorPlugins . append ( { ' name ' : plugin , ' error ' : f ' XML parse error: { str ( e ) } ' } )
logging . writeToFile ( f " Plugin { plugin } : XML parse error - { str ( e ) } " )
2026-01-26 17:45:36 +01:00
# Don't mark as processed if it failed - let installed check handle it
# This ensures plugins that exist in /usr/local/CyberCP/ but have bad source meta.xml still get counted
if not os . path . exists ( completePath ) :
# Only skip if it's not actually installed
continue
# If it exists in installed location, don't mark as processed so it gets checked there
2026-01-04 21:04:51 +01:00
continue
except Exception as e :
errorPlugins . append ( { ' name ' : plugin , ' error ' : f ' Error loading plugin: { str ( e ) } ' } )
logging . writeToFile ( f " Plugin { plugin } : Error loading - { str ( e ) } " )
2026-01-26 17:45:36 +01:00
# Don't mark as processed if it failed - let installed check handle it
if not os . path . exists ( completePath ) :
# Only skip if it's not actually installed
continue
# If it exists in installed location, don't mark as processed so it gets checked there
2026-01-04 21:04:51 +01:00
continue
2026-01-20 00:55:24 +01:00
# Also check for installed plugins that don't have source directories
# This handles plugins installed from the store that may not be in /home/cyberpanel/plugins/
if os . path . exists ( installedPath ) :
for plugin in os . listdir ( installedPath ) :
# Skip if already processed
if plugin in processed_plugins :
continue
# Only check directories that look like plugins (have meta.xml)
pluginInstalledDir = os . path . join ( installedPath , plugin )
if not os . path . isdir ( pluginInstalledDir ) :
continue
metaXmlPath = os . path . join ( pluginInstalledDir , ' meta.xml ' )
if not os . path . exists ( metaXmlPath ) :
continue
# This is an installed plugin without a source directory - process it
try :
data = { }
pluginMetaData = ElementTree . parse ( metaXmlPath )
root = pluginMetaData . getroot ( )
# Validate required fields
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 :
continue
if name_elem . text is None or desc_elem . text is None or version_elem . text is None :
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 [ ' desc ' ] = desc_elem . text
data [ ' version ' ] = version_elem . text
data [ ' plugin_dir ' ] = plugin
data [ ' installed ' ] = True # This is an installed plugin
data [ ' enabled ' ] = _is_plugin_enabled ( plugin )
2026-01-25 20:55:56 +01:00
# 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
2026-01-20 00:55:24 +01:00
# Get modify date from installed location
modify_date = ' N/A '
try :
if os . path . exists ( metaXmlPath ) :
modify_time = os . path . getmtime ( metaXmlPath )
modify_date = datetime . fromtimestamp ( modify_time ) . strftime ( ' % Y- % m- %d % H: % M: % S ' )
except Exception :
modify_date = ' N/A '
data [ ' modify_date ' ] = modify_date
# Extract settings URL or main URL
settings_url_elem = root . find ( ' settings_url ' )
url_elem = root . find ( ' url ' )
2026-01-20 01:31:18 +01:00
# Priority: settings_url > url > default pattern
# Special handling for core plugins that don't use /plugins/ prefix
if plugin == ' emailMarketing ' :
2026-01-26 17:45:36 +01:00
# emailMarketing is a core CyberPanel plugin, uses /emailMarketing/ not /plugins/emailMarketing/
data [ ' manage_url ' ] = ' /emailMarketing/ '
2026-01-20 01:31:18 +01:00
elif settings_url_elem is not None and settings_url_elem . text :
2026-01-20 00:55:24 +01:00
data [ ' manage_url ' ] = settings_url_elem . text
elif url_elem is not None and url_elem . text :
data [ ' manage_url ' ] = url_elem . text
else :
2026-01-20 01:37:49 +01:00
# Default to /plugins/{plugin}/ for regular plugins
2026-01-26 17:45:36 +01:00
# Special handling for emailMarketing
if plugin == ' emailMarketing ' :
data [ ' manage_url ' ] = ' /emailMarketing/ '
else :
2026-01-20 01:39:35 +01:00
# Default to main plugin route (most plugins work from main route)
data [ ' manage_url ' ] = f ' /plugins/ { plugin } / '
2026-01-20 00:55:24 +01:00
2026-01-20 01:31:18 +01:00
# Extract author information
author_elem = root . find ( ' author ' )
if author_elem is not None and author_elem . text :
data [ ' author ' ] = author_elem . text
else :
data [ ' author ' ] = ' Unknown '
2026-01-26 17:45:36 +01:00
# Extract paid plugin information (is_paid already initialized to False above)
2026-01-25 20:55:56 +01:00
paid_elem = root . find ( ' paid ' )
2026-01-26 17:45:36 +01:00
patreon_tier_elem = root . find ( ' patreon_tier ' )
2026-01-25 20:55:56 +01:00
2026-01-26 17:45:36 +01:00
if paid_elem is not None and paid_elem . text and paid_elem . text . lower ( ) == ' true ' :
data [ ' is_paid ' ] = 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: is_paid already False from initialization above
2026-01-25 20:55:56 +01:00
2026-01-20 00:55:24 +01:00
pluginList . append ( data )
except ElementTree . ParseError as e :
errorPlugins . append ( { ' name ' : plugin , ' error ' : f ' XML parse error: { str ( e ) } ' } )
logging . writeToFile ( f " Installed plugin { plugin } : XML parse error - { str ( e ) } " )
continue
except Exception as e :
errorPlugins . append ( { ' name ' : plugin , ' error ' : f ' Error loading installed plugin: { str ( e ) } ' } )
logging . writeToFile ( f " Installed plugin { plugin } : Error loading - { str ( e ) } " )
continue
2025-08-01 14:56:30 +05:00
2026-01-26 17:45:36 +01:00
# Calculate installed and active counts
# Double-check by also counting plugins that actually exist in /usr/local/CyberCP/
installed_plugins_in_filesystem = set ( )
if os . path . exists ( installedPath ) :
for plugin in os . listdir ( installedPath ) :
pluginInstalledDir = os . path . join ( installedPath , plugin )
if os . path . isdir ( pluginInstalledDir ) :
metaXmlPath = os . path . join ( pluginInstalledDir , ' meta.xml ' )
if os . path . exists ( metaXmlPath ) :
installed_plugins_in_filesystem . add ( plugin )
2026-01-26 02:53:24 +01:00
2026-01-26 17:45:36 +01:00
# Count installed plugins from the list
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 ) ] )
2026-01-26 02:53:24 +01:00
2026-01-26 17:45:36 +01:00
# If there's a discrepancy, use the filesystem count as the source of truth
filesystem_installed_count = len ( installed_plugins_in_filesystem )
if filesystem_installed_count != installed_count :
logging . writeToFile ( f " WARNING: Plugin count mismatch! List says { installed_count } , filesystem has { filesystem_installed_count } " )
logging . writeToFile ( f " Plugins in filesystem: { sorted ( installed_plugins_in_filesystem ) } " )
logging . writeToFile ( f " Plugins in list with installed=True: { [ p . get ( ' plugin_dir ' ) for p in pluginList if p . get ( ' installed ' , False ) ] } " )
# Use filesystem count as source of truth
installed_count = filesystem_installed_count
2026-01-26 02:53:24 +01:00
2026-01-26 17:45:36 +01:00
# Debug logging to help identify discrepancies
logging . writeToFile ( f " Plugin count: Total= { len ( pluginList ) } , Installed= { installed_count } , Active= { active_count } " )
for p in pluginList :
logging . writeToFile ( f " - { p . get ( ' plugin_dir ' ) } : installed= { p . get ( ' installed ' ) } , enabled= { p . get ( ' enabled ' ) } " )
proc = httpProc ( request , ' pluginHolder/plugins.html ' ,
{ ' plugins ' : pluginList , ' error_plugins ' : errorPlugins ,
' installed_count ' : installed_count , ' active_count ' : active_count } , ' admin ' )
return proc . render ( )
2026-01-04 21:04:51 +01:00
@csrf_exempt
@require_http_methods ( [ " POST " ] )
def install_plugin ( request , plugin_name ) :
""" Install a plugin """
try :
# Check if plugin source exists
pluginSource = ' /home/cyberpanel/plugins/ ' + plugin_name
if not os . path . exists ( pluginSource ) :
return JsonResponse ( {
' success ' : False ,
' error ' : f ' Plugin source not found: { plugin_name } '
} , status = 404 )
2026-01-26 17:45:36 +01:00
# Check if already installed
2026-01-04 21:04:51 +01:00
pluginInstalled = ' /usr/local/CyberCP/ ' + plugin_name
if os . path . exists ( pluginInstalled ) :
2026-01-26 17:45:36 +01:00
return JsonResponse ( {
' success ' : False ,
' error ' : f ' Plugin already installed: { plugin_name } '
} , status = 400 )
2026-01-04 21:04:51 +01:00
# Create zip file for installation (pluginInstaller expects a zip)
import tempfile
import shutil
2026-01-25 20:55:56 +01:00
import zipfile
2026-01-04 21:04:51 +01:00
temp_dir = tempfile . mkdtemp ( )
zip_path = os . path . join ( temp_dir , plugin_name + ' .zip ' )
2026-01-25 20:55:56 +01:00
# 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 ( )
2026-01-04 21:04:51 +01:00
# Verify zip file was created
if not os . path . exists ( zip_path ) :
shutil . rmtree ( temp_dir , ignore_errors = True )
return JsonResponse ( {
' success ' : False ,
' error ' : f ' Failed to create zip file for { plugin_name } '
} , status = 500 )
# Copy zip to current directory (pluginInstaller expects it in cwd)
original_cwd = os . getcwd ( )
os . chdir ( temp_dir )
try :
# Verify zip file exists in current directory
zip_file = plugin_name + ' .zip '
if not os . path . exists ( zip_file ) :
raise Exception ( f ' Zip file { zip_file } not found in temp directory ' )
# Install using pluginInstaller
2026-01-25 20:55:56 +01:00
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 )
2026-01-04 21:04:51 +01:00
# Verify plugin was actually installed
pluginInstalled = ' /usr/local/CyberCP/ ' + plugin_name
if not os . path . exists ( pluginInstalled ) :
2026-01-25 20:55:56 +01:00
# 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 } / ' )
2026-01-04 21:04:51 +01:00
raise Exception ( f ' Plugin installation failed: { pluginInstalled } does not exist after installation ' )
# Set plugin to enabled by default after installation
_set_plugin_state ( plugin_name , True )
return JsonResponse ( {
' success ' : True ,
' message ' : f ' Plugin { plugin_name } installed successfully '
} )
finally :
os . chdir ( original_cwd )
# Cleanup
shutil . rmtree ( temp_dir , ignore_errors = True )
except Exception as e :
logging . writeToFile ( f " Error installing plugin { plugin_name } : { str ( e ) } " )
return JsonResponse ( {
' success ' : False ,
' error ' : str ( e )
} , status = 500 )
@csrf_exempt
@require_http_methods ( [ " POST " ] )
def uninstall_plugin ( request , plugin_name ) :
""" Uninstall a plugin - but keep source files and settings """
try :
# Check if plugin is installed
pluginInstalled = ' /usr/local/CyberCP/ ' + plugin_name
if not os . path . exists ( pluginInstalled ) :
return JsonResponse ( {
' success ' : False ,
' error ' : f ' Plugin not installed: { plugin_name } '
} , status = 404 )
# Custom uninstall that keeps source files
# We need to remove from settings.py, urls.py, and remove installed directory
# but NOT remove from /home/cyberpanel/plugins/
# Remove from settings.py
pluginInstaller . removeFromSettings ( plugin_name )
# Remove from URLs
pluginInstaller . removeFromURLs ( plugin_name )
# Remove interface link
pluginInstaller . removeInterfaceLink ( plugin_name )
# Remove migrations if enabled
if pluginInstaller . migrationsEnabled ( plugin_name ) :
pluginInstaller . removeMigrations ( plugin_name )
# Remove installed directory (but keep source in /home/cyberpanel/plugins/)
2026-01-26 17:45:36 +01:00
pluginInstaller . removeFiles ( plugin_name )
2026-01-04 21:04:51 +01:00
# DON'T call informCyberPanelRemoval - we want to keep the source directory
# so users can reinstall the plugin later
# Restart service
pluginInstaller . restartGunicorn ( )
# Keep state file - we want to remember if it was enabled/disabled
# So user can reinstall and have same state
return JsonResponse ( {
' success ' : True ,
' message ' : f ' Plugin { plugin_name } uninstalled successfully (source files and settings preserved) '
} )
except Exception as e :
logging . writeToFile ( f " Error uninstalling plugin { plugin_name } : { str ( e ) } " )
return JsonResponse ( {
' success ' : False ,
' error ' : str ( e )
} , status = 500 )
@csrf_exempt
@require_http_methods ( [ " POST " ] )
def enable_plugin ( request , plugin_name ) :
""" Enable a plugin """
try :
# Check if plugin is installed
pluginInstalled = ' /usr/local/CyberCP/ ' + plugin_name
if not os . path . exists ( pluginInstalled ) :
return JsonResponse ( {
' success ' : False ,
' error ' : f ' Plugin not installed: { plugin_name } '
} , status = 404 )
# Set plugin state to enabled
if _set_plugin_state ( plugin_name , True ) :
return JsonResponse ( {
' success ' : True ,
' message ' : f ' Plugin { plugin_name } enabled successfully '
} )
else :
return JsonResponse ( {
' success ' : False ,
' error ' : ' Failed to update plugin state '
} , status = 500 )
except Exception as e :
logging . writeToFile ( f " Error enabling plugin { plugin_name } : { str ( e ) } " )
return JsonResponse ( {
' success ' : False ,
' error ' : str ( e )
} , status = 500 )
@csrf_exempt
@require_http_methods ( [ " POST " ] )
def disable_plugin ( request , plugin_name ) :
""" Disable a plugin """
try :
# Check if plugin is installed
pluginInstalled = ' /usr/local/CyberCP/ ' + plugin_name
if not os . path . exists ( pluginInstalled ) :
return JsonResponse ( {
' success ' : False ,
' error ' : f ' Plugin not installed: { plugin_name } '
} , status = 404 )
# Set plugin state to disabled
if _set_plugin_state ( plugin_name , False ) :
return JsonResponse ( {
' success ' : True ,
' message ' : f ' Plugin { plugin_name } disabled successfully '
} )
else :
return JsonResponse ( {
' success ' : False ,
' error ' : ' Failed to update plugin state '
} , status = 500 )
except Exception as e :
logging . writeToFile ( f " Error disabling plugin { plugin_name } : { str ( e ) } " )
return JsonResponse ( {
' success ' : False ,
' error ' : str ( e )
2026-01-19 22:55:59 +01:00
} , status = 500 )
def _ensure_cache_dir ( ) :
""" Ensure cache directory exists """
try :
if not os . path . exists ( PLUGIN_STORE_CACHE_DIR ) :
os . makedirs ( PLUGIN_STORE_CACHE_DIR , mode = 0o755 )
except Exception as e :
logging . writeToFile ( f " Error creating cache directory: { str ( e ) } " )
def _get_cached_plugins ( allow_expired = False ) :
""" Get plugins from cache if available and not expired
Args :
allow_expired : If True , return cache even if expired ( for fallback )
"""
try :
if not os . path . exists ( PLUGIN_STORE_CACHE_FILE ) :
return None
# Check if cache is expired
cache_mtime = os . path . getmtime ( PLUGIN_STORE_CACHE_FILE )
cache_age = time . time ( ) - cache_mtime
if cache_age > PLUGIN_STORE_CACHE_DURATION :
if not allow_expired :
logging . writeToFile ( f " Plugin store cache expired (age: { cache_age : .0f } s) " )
return None
else :
logging . writeToFile ( f " Using expired cache as fallback (age: { cache_age : .0f } s) " )
# Read cache file
with open ( PLUGIN_STORE_CACHE_FILE , ' r ' , encoding = ' utf-8 ' ) as f :
cache_data = json . load ( f )
if not allow_expired or cache_age < = PLUGIN_STORE_CACHE_DURATION :
logging . writeToFile ( f " Using cached plugin store data (age: { cache_age : .0f } s) " )
return cache_data . get ( ' plugins ' , [ ] )
except Exception as e :
logging . writeToFile ( f " Error reading plugin store cache: { str ( e ) } " )
return None
def _save_plugins_cache ( plugins ) :
""" Save plugins to cache """
try :
_ensure_cache_dir ( )
cache_data = {
' plugins ' : plugins ,
' cached_at ' : datetime . now ( ) . isoformat ( ) ,
' cache_duration ' : PLUGIN_STORE_CACHE_DURATION
}
with open ( PLUGIN_STORE_CACHE_FILE , ' w ' , encoding = ' utf-8 ' ) as f :
json . dump ( cache_data , f , indent = 2 , ensure_ascii = False )
logging . writeToFile ( " Plugin store cache saved successfully " )
except Exception as e :
logging . writeToFile ( f " Error saving plugin store cache: { str ( e ) } " )
def _enrich_store_plugins ( plugins ) :
""" Enrich store plugins with installed/enabled status from local system """
enriched = [ ]
plugin_source_dir = ' /home/cyberpanel/plugins '
plugin_install_dir = ' /usr/local/CyberCP '
for plugin in plugins :
plugin_dir = plugin . get ( ' plugin_dir ' , ' ' )
if not plugin_dir :
continue
# Check if plugin is installed locally
2026-01-26 17:45:36 +01:00
# Plugin is only considered "installed" if it exists in /usr/local/CyberCP/
# Source directory presence doesn't mean installed - it just means the source files are available
2026-01-19 22:55:59 +01:00
installed_path = os . path . join ( plugin_install_dir , plugin_dir )
2026-01-26 17:45:36 +01:00
plugin [ ' installed ' ] = os . path . exists ( installed_path )
2026-01-19 22:55:59 +01:00
# Check if plugin is enabled (only if installed)
if plugin [ ' installed ' ] :
2026-01-26 17:45:36 +01:00
plugin [ ' enabled ' ] = _is_plugin_enabled ( plugin_dir )
2026-01-19 22:55:59 +01:00
else :
plugin [ ' enabled ' ] = False
2026-01-25 20:55:56 +01:00
# Ensure is_paid field exists and is properly set (default to False if not set or invalid)
2026-01-26 17:45:36 +01:00
# Handle all possible cases: missing, None, empty string, string values, boolean
is_paid_value = plugin . get ( ' is_paid ' , False )
2026-01-25 20:55:56 +01:00
2026-01-26 17:45:36 +01:00
# Normalize is_paid to boolean
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
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
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 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
2026-01-26 02:53:24 +01:00
else :
2026-01-26 17:45:36 +01:00
plugin [ ' is_paid ' ] = False
except :
2026-01-25 20:55:56 +01:00
plugin [ ' is_paid ' ] = False
else :
plugin [ ' is_paid ' ] = False # Default to free if we can't determine
2026-01-26 17:45:36 +01:00
else :
# Already set, but ensure it's boolean
plugin [ ' is_paid ' ] = bool ( plugin [ ' is_paid ' ] ) if plugin [ ' is_paid ' ] not in [ True , False ] else plugin [ ' is_paid ' ]
2026-01-25 22:25:21 +01:00
2026-01-19 22:55:59 +01:00
enriched . append ( plugin )
return enriched
def _fetch_plugins_from_github ( ) :
""" Fetch plugins from GitHub repository """
plugins = [ ]
try :
# Fetch repository contents
req = urllib . request . Request (
GITHUB_REPO_API ,
headers = {
' User-Agent ' : ' CyberPanel-Plugin-Store/1.0 ' ,
' Accept ' : ' application/vnd.github.v3+json '
}
)
with urllib . request . urlopen ( req , timeout = 10 ) as response :
contents = json . loads ( response . read ( ) . decode ( ' utf-8 ' ) )
# Filter for directories (plugins)
plugin_dirs = [ item for item in contents if item . get ( ' type ' ) == ' dir ' and not item . get ( ' name ' , ' ' ) . startswith ( ' . ' ) ]
for plugin_dir in plugin_dirs :
plugin_name = plugin_dir . get ( ' name ' , ' ' )
if not plugin_name :
continue
try :
# Fetch meta.xml from raw GitHub
meta_xml_url = f " { GITHUB_RAW_BASE } / { plugin_name } /meta.xml "
meta_req = urllib . request . Request (
meta_xml_url ,
headers = { ' User-Agent ' : ' CyberPanel-Plugin-Store/1.0 ' }
)
with urllib . request . urlopen ( meta_req , timeout = 10 ) as meta_response :
meta_xml_content = meta_response . read ( ) . decode ( ' utf-8 ' )
# Parse meta.xml
root = ElementTree . fromstring ( meta_xml_content )
# Fetch last commit date for this plugin from GitHub
modify_date = ' N/A '
try :
commits_url = f " { GITHUB_COMMITS_API } ?path= { plugin_name } &per_page=1 "
commits_req = urllib . request . Request (
commits_url ,
headers = {
' User-Agent ' : ' CyberPanel-Plugin-Store/1.0 ' ,
' Accept ' : ' application/vnd.github.v3+json '
}
)
with urllib . request . urlopen ( commits_req , timeout = 10 ) as commits_response :
commits_data = json . loads ( commits_response . read ( ) . decode ( ' utf-8 ' ) )
if commits_data and len ( commits_data ) > 0 :
commit_date = commits_data [ 0 ] . get ( ' commit ' , { } ) . get ( ' author ' , { } ) . get ( ' date ' , ' ' )
if commit_date :
# Parse ISO 8601 date and format it
try :
from datetime import datetime
dt = datetime . fromisoformat ( commit_date . replace ( ' Z ' , ' +00:00 ' ) )
modify_date = dt . strftime ( ' % Y- % m- %d % H: % M: % S ' )
except Exception :
modify_date = commit_date [ : 19 ] . replace ( ' T ' , ' ' ) # Fallback formatting
except Exception as e :
logging . writeToFile ( f " Could not fetch commit date for { plugin_name } : { str ( e ) } " )
modify_date = ' N/A '
2026-01-26 17:45:36 +01:00
# Extract paid plugin information
2026-01-25 20:55:56 +01:00
paid_elem = root . find ( ' paid ' )
2026-01-26 17:45:36 +01:00
patreon_tier_elem = root . find ( ' patreon_tier ' )
2026-01-25 20:55:56 +01:00
is_paid = False
patreon_tier = None
patreon_url = None
2026-01-26 17:45:36 +01:00
if paid_elem is not None and paid_elem . text and paid_elem . text . lower ( ) == ' true ' :
2026-01-25 20:55:56 +01:00
is_paid = True
2026-01-26 17:45:36 +01:00
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 '
2026-01-25 20:55:56 +01:00
2026-01-19 22:55:59 +01:00
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 ' ,
' 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 } / ' ,
' settings_url ' : root . find ( ' settings_url ' ) . text if root . find ( ' settings_url ' ) is not None else f ' /plugins/ { plugin_name } /settings/ ' ,
' 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 } ' ,
2026-01-25 20:55:56 +01:00
' modify_date ' : modify_date ,
2026-01-26 17:45:36 +01:00
' is_paid ' : is_paid ,
2026-01-25 20:55:56 +01:00
' patreon_tier ' : patreon_tier ,
2026-01-26 17:45:36 +01:00
' patreon_url ' : patreon_url
2026-01-19 22:55:59 +01:00
}
plugins . append ( plugin_data )
logging . writeToFile ( f " Fetched plugin: { plugin_name } (last modified: { modify_date } ) " )
except urllib . error . HTTPError as e :
if e . code == 403 :
# Rate limit hit - log and break
logging . writeToFile ( f " GitHub API rate limit exceeded (403) for plugin { plugin_name } " )
raise # Re-raise to be caught by outer handler
elif e . code == 404 :
# meta.xml not found, skip this plugin
logging . writeToFile ( f " meta.xml not found for plugin { plugin_name } , skipping " )
continue
else :
logging . writeToFile ( f " HTTP error { e . code } fetching { plugin_name } : { str ( e ) } " )
continue
except Exception as e :
logging . writeToFile ( f " Error processing plugin { plugin_name } : { str ( e ) } " )
continue
return plugins
except urllib . error . HTTPError as e :
if e . code == 403 :
error_msg = " GitHub API rate limit exceeded. Using cached data if available. "
logging . writeToFile ( f " GitHub API 403 error: { error_msg } " )
raise Exception ( error_msg )
else :
error_msg = f " GitHub API error { e . code } : { str ( e ) } "
logging . writeToFile ( error_msg )
raise Exception ( error_msg )
except urllib . error . URLError as e :
error_msg = f " Network error fetching plugins: { str ( e ) } "
logging . writeToFile ( error_msg )
raise Exception ( error_msg )
except Exception as e :
error_msg = f " Error fetching plugins from GitHub: { str ( e ) } "
logging . writeToFile ( error_msg )
raise Exception ( error_msg )
@csrf_exempt
@require_http_methods ( [ " GET " ] )
def fetch_plugin_store ( request ) :
""" Fetch plugins from the plugin store with caching """
2026-01-26 17:45:36 +01:00
mailUtilities . checkHome ( )
2026-01-19 22:55:59 +01:00
2026-01-26 17:45:36 +01:00
# 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
} )
2026-01-26 02:53:24 +01:00
2026-01-26 17:45:36 +01:00
# Cache miss or expired - fetch from GitHub
2026-01-19 22:55:59 +01:00
try :
2026-01-26 17:45:36 +01:00
plugins = _fetch_plugins_from_github ( )
# Enrich plugins with installed/enabled status
enriched_plugins = _enrich_store_plugins ( plugins )
# Save to cache (save original, not enriched, to keep cache clean)
if plugins :
_save_plugins_cache ( plugins )
2026-01-19 22:55:59 +01:00
2026-01-26 17:45:36 +01:00
return JsonResponse ( {
' success ' : True ,
' plugins ' : enriched_plugins ,
' cached ' : False
} )
except Exception as e :
error_message = str ( e )
# If rate limited, try to use stale cache as fallback
if ' 403 ' in error_message or ' rate limit ' in error_message . lower ( ) :
stale_cache = _get_cached_plugins ( allow_expired = True ) # Get cache even if expired
if stale_cache is not None :
logging . writeToFile ( " Using stale cache due to rate limit " )
enriched_plugins = _enrich_store_plugins ( stale_cache )
return JsonResponse ( {
' success ' : True ,
' plugins ' : enriched_plugins ,
' cached ' : True ,
' warning ' : ' Using cached data due to GitHub rate limit. Data may be outdated. '
} )
# No cache available, return error
2026-01-26 03:45:29 +01:00
return JsonResponse ( {
' success ' : False ,
' error ' : error_message ,
' plugins ' : [ ]
} , status = 500 )
2026-01-19 22:55:59 +01:00
@csrf_exempt
@require_http_methods ( [ " POST " ] )
def install_from_store ( request , plugin_name ) :
2026-01-25 20:55:56 +01:00
""" Install plugin from GitHub store, with fallback to local source """
2026-01-19 22:55:59 +01:00
mailUtilities . checkHome ( )
2026-01-19 23:34:28 +01:00
try :
2026-01-26 17:45:36 +01:00
# Check if already installed
2026-01-19 23:34:28 +01:00
pluginInstalled = ' /usr/local/CyberCP/ ' + plugin_name
if os . path . exists ( pluginInstalled ) :
2026-01-26 17:45:36 +01:00
return JsonResponse ( {
' success ' : False ,
' error ' : f ' Plugin already installed: { plugin_name } '
} , status = 400 )
2026-01-19 23:34:28 +01:00
# Download plugin from GitHub
import tempfile
import shutil
import zipfile
import io
logging . writeToFile ( f " Starting installation of { plugin_name } from GitHub store " )
# Create temporary directory
temp_dir = tempfile . mkdtemp ( )
zip_path = os . path . join ( temp_dir , plugin_name + ' .zip ' )
try :
2026-01-25 20:55:56 +01:00
# 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
2026-01-19 23:34:28 +01:00
2026-01-25 20:55:56 +01:00
# 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 ( )
2026-01-19 23:34:28 +01:00
# Verify ZIP was created
if not os . path . exists ( zip_path ) :
raise Exception ( f ' Failed to create plugin ZIP file ' )
logging . writeToFile ( f " Created plugin ZIP: { zip_path } " )
# Copy ZIP to current directory (pluginInstaller expects it in cwd)
original_cwd = os . getcwd ( )
os . chdir ( temp_dir )
try :
# Verify zip file exists in current directory
zip_file = plugin_name + ' .zip '
if not os . path . exists ( zip_file ) :
raise Exception ( f ' Zip file { zip_file } not found in temp directory ' )
logging . writeToFile ( f " Installing plugin using pluginInstaller " )
2026-01-20 00:38:21 +01:00
# Install using pluginInstaller (direct call, not via command line)
2026-01-25 20:55:56 +01:00
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 } ' )
2026-01-19 23:34:28 +01:00
2026-01-26 17:45:36 +01:00
# Wait a moment for file system to sync and service to restart
2026-01-20 00:47:49 +01:00
import time
2026-01-26 17:45:36 +01:00
time . sleep ( 3 ) # Increased wait time for file system sync
2026-01-20 00:47:49 +01:00
2026-01-19 23:34:28 +01:00
# Verify plugin was actually installed
pluginInstalled = ' /usr/local/CyberCP/ ' + plugin_name
if not os . path . exists ( pluginInstalled ) :
2026-01-25 20:55:56 +01:00
# 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 } / ' )
2026-01-19 23:34:28 +01:00
raise Exception ( f ' Plugin installation failed: { pluginInstalled } does not exist after installation ' )
logging . writeToFile ( f " Plugin { plugin_name } installed successfully " )
# Set plugin to enabled by default after installation
_set_plugin_state ( plugin_name , True )
return JsonResponse ( {
' success ' : True ,
' message ' : f ' Plugin { plugin_name } installed successfully from store '
} )
finally :
os . chdir ( original_cwd )
finally :
# Cleanup
shutil . rmtree ( temp_dir , ignore_errors = True )
except urllib . error . HTTPError as e :
error_msg = f ' Failed to download plugin from GitHub: HTTP { e . code } '
if e . code == 404 :
error_msg = f ' Plugin { plugin_name } not found in GitHub repository '
logging . writeToFile ( f " Error installing { plugin_name } : { error_msg } " )
return JsonResponse ( {
' success ' : False ,
' error ' : error_msg
} , status = 500 )
except Exception as e :
logging . writeToFile ( f " Error installing plugin { plugin_name } : { str ( e ) } " )
import traceback
error_details = traceback . format_exc ( )
logging . writeToFile ( f " Traceback: { error_details } " )
return JsonResponse ( {
' success ' : False ,
' error ' : str ( e )
} , status = 500 )
2026-01-19 22:55:59 +01:00
def plugin_help ( request , plugin_name ) :
2026-01-20 01:19:14 +01:00
""" Plugin-specific help page - shows plugin information, version history, and help content """
2026-01-19 22:55:59 +01:00
mailUtilities . checkHome ( )
2026-01-20 01:19:14 +01:00
# Paths for the plugin
plugin_path = ' /usr/local/CyberCP/ ' + plugin_name
meta_xml_path = os . path . join ( plugin_path , ' meta.xml ' )
# Check if plugin exists
if not os . path . exists ( plugin_path ) or not os . path . exists ( meta_xml_path ) :
proc = httpProc ( request , ' pluginHolder/plugin_not_found.html ' , {
' plugin_name ' : plugin_name
} , ' admin ' )
return proc . render ( )
# Parse meta.xml
try :
plugin_meta = ElementTree . parse ( meta_xml_path )
root = plugin_meta . getroot ( )
# Extract plugin information
plugin_display_name = root . find ( ' name ' ) . text if root . find ( ' name ' ) is not None else plugin_name
plugin_description = root . find ( ' description ' ) . text if root . find ( ' description ' ) is not None else ' '
plugin_version = root . find ( ' version ' ) . text if root . find ( ' version ' ) is not None else ' Unknown '
plugin_author = root . find ( ' author ' ) . text if root . find ( ' author ' ) is not None else ' Unknown '
plugin_type = root . find ( ' type ' ) . text if root . find ( ' type ' ) is not None else ' Plugin '
# Check if plugin is installed
installed = os . path . exists ( plugin_path )
except Exception as e :
logging . writeToFile ( f " Error parsing meta.xml for { plugin_name } : { str ( e ) } " )
proc = httpProc ( request , ' pluginHolder/plugin_not_found.html ' , {
' plugin_name ' : plugin_name
} , ' admin ' )
return proc . render ( )
# Look for help content files (README.md, CHANGELOG.md, HELP.md, etc.)
help_content = ' '
changelog_content = ' '
# Check for README.md or HELP.md
help_files = [ ' HELP.md ' , ' README.md ' , ' docs/HELP.md ' , ' docs/README.md ' ]
help_file_path = None
for help_file in help_files :
potential_path = os . path . join ( plugin_path , help_file )
if os . path . exists ( potential_path ) :
help_file_path = potential_path
break
if help_file_path :
try :
with open ( help_file_path , ' r ' , encoding = ' utf-8 ' ) as f :
help_content = f . read ( )
except Exception as e :
logging . writeToFile ( f " Error reading help file for { plugin_name } : { str ( e ) } " )
help_content = ' '
# Check for CHANGELOG.md
changelog_paths = [ ' CHANGELOG.md ' , ' changelog.md ' , ' CHANGELOG.txt ' , ' docs/CHANGELOG.md ' ]
for changelog_file in changelog_paths :
potential_path = os . path . join ( plugin_path , changelog_file )
if os . path . exists ( potential_path ) :
try :
with open ( potential_path , ' r ' , encoding = ' utf-8 ' ) as f :
changelog_content = f . read ( )
break
except Exception as e :
logging . writeToFile ( f " Error reading changelog for { plugin_name } : { str ( e ) } " )
2026-01-20 01:22:03 +01:00
# If no local changelog, try fetching from GitHub (non-blocking)
if not changelog_content :
try :
github_changelog_url = f ' { GITHUB_RAW_BASE } / { plugin_name } /CHANGELOG.md '
try :
with urllib . request . urlopen ( github_changelog_url , timeout = 3 ) as response :
if response . getcode ( ) == 200 :
changelog_content = response . read ( ) . decode ( ' utf-8 ' )
logging . writeToFile ( f " Fetched CHANGELOG.md from GitHub for { plugin_name } " )
except ( urllib . error . HTTPError , urllib . error . URLError , Exception ) :
# Silently fail - GitHub fetch is optional
pass
except Exception :
# Silently fail - GitHub fetch is optional
pass
# If no help content and no local README, try fetching README.md from GitHub
if not help_content :
try :
github_readme_url = f ' { GITHUB_RAW_BASE } / { plugin_name } /README.md '
try :
with urllib . request . urlopen ( github_readme_url , timeout = 3 ) as response :
if response . getcode ( ) == 200 :
help_content = response . read ( ) . decode ( ' utf-8 ' )
logging . writeToFile ( f " Fetched README.md from GitHub for { plugin_name } " )
except ( urllib . error . HTTPError , urllib . error . URLError , Exception ) :
# Silently fail - GitHub fetch is optional
pass
except Exception :
# Silently fail - GitHub fetch is optional
pass
2026-01-20 01:19:14 +01:00
# If no help content found, create default content from meta.xml
if not help_content :
help_content = f """
< h2 > Plugin Information < / h2 >
< p > < strong > Name : < / strong > { plugin_display_name } < / p >
< p > < strong > Type : < / strong > { plugin_type } < / p >
< p > < strong > Version : < / strong > { plugin_version } < / p >
< p > < strong > Author : < / strong > { plugin_author } < / p >
< h2 > Description < / h2 >
< p > { plugin_description } < / p >
< h2 > Usage < / h2 >
< p > For detailed information about this plugin , please visit the GitHub repository or check the plugin ' s documentation.</p>
"""
else :
# Convert markdown to HTML (basic conversion)
import re
2026-01-20 01:23:44 +01:00
# Convert linked images first (badges): [](link_url)
help_content = re . sub (
r ' \ [! \ [([^ \ ]]*) \ ] \ (([^ \ )]+) \ ) \ ] \ (([^ \ )]+) \ ) ' ,
r ' <a href= " \ 3 " target= " _blank " rel= " noopener noreferrer " ><img src= " \ 2 " alt= " \ 1 " style= " display:inline-block;margin:0 4px;vertical-align:middle; " ></a> ' ,
help_content
)
# Convert regular images: 
help_content = re . sub (
r ' ! \ [([^ \ ]]*) \ ] \ (([^ \ )]+) \ ) ' ,
r ' <img src= " \ 2 " alt= " \ 1 " style= " display:inline-block;margin:4px 0;max-width:100 % ; " > ' ,
help_content
)
# Convert regular links: [text](url)
help_content = re . sub (
r ' \ [([^ \ ]]+) \ ] \ (([^ \ )]+) \ ) ' ,
r ' <a href= " \ 2 " target= " _blank " rel= " noopener noreferrer " > \ 1</a> ' ,
help_content
)
# Convert headings
2026-01-20 01:19:14 +01:00
help_content = re . sub ( r ' ^### (.*?)$ ' , r ' <h3> \ 1</h3> ' , help_content , flags = re . MULTILINE )
help_content = re . sub ( r ' ^## (.*?)$ ' , r ' <h2> \ 1</h2> ' , help_content , flags = re . MULTILINE )
help_content = re . sub ( r ' ^# (.*?)$ ' , r ' <h1> \ 1</h1> ' , help_content , flags = re . MULTILINE )
2026-01-20 01:23:44 +01:00
# Convert formatting
2026-01-20 01:19:14 +01:00
help_content = re . sub ( r ' \ * \ *(.*?) \ * \ * ' , r ' <strong> \ 1</strong> ' , help_content )
help_content = re . sub ( r ' \ *(.*?) \ * ' , r ' <em> \ 1</em> ' , help_content )
help_content = re . sub ( r ' `([^`]+)` ' , r ' <code> \ 1</code> ' , help_content )
2026-01-20 01:23:44 +01:00
# Convert lists
2026-01-20 01:19:14 +01:00
help_content = re . sub ( r ' ^ \ - (.*?)$ ' , r ' <li> \ 1</li> ' , help_content , flags = re . MULTILINE )
help_content = re . sub ( r ' ^( \ d+) \ . (.*?)$ ' , r ' <li> \ 2</li> ' , help_content , flags = re . MULTILINE )
2026-01-20 01:23:44 +01:00
# Wrap paragraphs (but preserve HTML tags and images)
2026-01-20 01:19:14 +01:00
lines = help_content . split ( ' \n ' )
processed_lines = [ ]
for line in lines :
line = line . strip ( )
2026-01-20 01:23:44 +01:00
if line and not line . startswith ( ' < ' ) and not line . startswith ( ' http ' ) and not ' <img ' in line and not ' <a ' in line :
2026-01-20 01:19:14 +01:00
processed_lines . append ( f ' <p> { line } </p> ' )
elif line :
processed_lines . append ( line )
help_content = ' \n ' . join ( processed_lines )
# Add changelog if available
if changelog_content :
# Convert changelog markdown to HTML
import re
changelog_html = changelog_content
changelog_html = re . sub ( r ' ^## (.*?)$ ' , r ' <h3> \ 1</h3> ' , changelog_html , flags = re . MULTILINE )
changelog_html = re . sub ( r ' ^### (.*?)$ ' , r ' <h4> \ 1</h4> ' , changelog_html , flags = re . MULTILINE )
changelog_html = re . sub ( r ' ^ \ - (.*?)$ ' , r ' <li> \ 1</li> ' , changelog_html , flags = re . MULTILINE )
changelog_html = re . sub ( r ' \ * \ *(.*?) \ * \ * ' , r ' <strong> \ 1</strong> ' , changelog_html )
# Wrap in pre for code-like formatting
changelog_html = f ' <div class= " changelog-content " ><h2>Version History</h2><pre> { changelog_html } </pre></div> '
help_content + = changelog_html
# Context for template
context = {
' plugin_name ' : plugin_display_name ,
' plugin_name_dir ' : plugin_name ,
' plugin_description ' : plugin_description ,
' plugin_version ' : plugin_version ,
' plugin_author ' : plugin_author ,
' plugin_type ' : plugin_type ,
' installed ' : installed ,
' help_content ' : help_content ,
}
proc = httpProc ( request , ' pluginHolder/plugin_help.html ' , context , ' admin ' )
return proc . render ( )
2026-01-25 20:55:56 +01:00
@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 )