Merge pull request #1724 from master3395/v2.5.5-dev

V2.5.5 dev
This commit is contained in:
Master3395
2026-03-06 21:14:43 +01:00
committed by GitHub
4 changed files with 215 additions and 100 deletions

View File

@@ -104,10 +104,11 @@ urlpatterns = [
path('api/revert/<str:plugin_name>/', views.revert_plugin, name='revert_plugin'),
path('api/debug-plugins/', views.debug_loaded_plugins, name='debug_loaded_plugins'),
path('api/check-subscription/<str:plugin_name>/', views.check_plugin_subscription, name='check_plugin_subscription'),
path('<str:plugin_name>/settings/', views.plugin_settings_proxy, name='plugin_settings_proxy'),
path('<str:plugin_name>/help/', views.plugin_help, name='plugin_help'),
]
# Include each installed plugin's URLs *before* the catch-all so /plugins/<name>/settings/ etc. match
# Include each installed plugin's URLs *before* the catch-all so /plugins/<name>/... (other than settings/help) match
_loaded_plugins = []
_failed_plugins = {}
for _plugin_name, _path_parent in _get_installed_plugin_list():

View File

@@ -54,6 +54,34 @@ RESERVED_PLUGIN_DIRS = frozenset([
'websiteFunctions', 'aiScanner', 'dns', 'help', 'installed',
])
def _find_plugin_prefix_in_archive(namelist, plugin_name):
"""
Find the path prefix for a plugin inside a GitHub archive (e.g. repo-main/pluginName/ or repo-main/Category/pluginName/).
Returns (top_level, plugin_prefix) or (None, None) if not found.
"""
top_level = None
for name in namelist:
if '/' in name:
top_level = name.split('/')[0]
break
if not top_level:
return None, None
plugin_name_lower = plugin_name.lower()
# Check every path: find one that has a segment equal to plugin_name (e.g. .../pm2Manager/ or .../snappymailAdmin/)
for name in namelist:
if '/' not in name:
continue
parts = name.split('/')
# parts[0] = top_level, then we need a segment that matches plugin_name
for i in range(1, len(parts)):
if parts[i].lower() == plugin_name_lower:
# Plugin folder is at top_level/parts[1]/.../parts[i]/
prefix_parts = [top_level] + parts[1:i + 1]
plugin_prefix = '/'.join(prefix_parts) + '/'
return top_level, plugin_prefix
return top_level, None
def _get_plugin_source_path(plugin_name):
"""Return the full path to a plugin's source directory, or None if not found."""
for base in PLUGIN_SOURCE_PATHS:
@@ -607,19 +635,16 @@ def install_plugin(request, plugin_name):
'error': f'Failed to create zip file for {plugin_name}'
}, status=500)
# Copy zip to current directory (pluginInstaller expects it in cwd)
zip_path_abs = os.path.abspath(zip_path)
if not os.path.exists(zip_path_abs):
raise Exception(f'Zip file not found: {zip_path_abs}')
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
# Install using pluginInstaller with explicit zip path (avoids cwd races)
try:
pluginInstaller.installPlugin(plugin_name)
pluginInstaller.installPlugin(plugin_name, zip_path=zip_path_abs)
except Exception as install_error:
# Log the full error for debugging
error_msg = str(install_error)
@@ -638,8 +663,8 @@ def install_plugin(request, plugin_name):
# 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']
# Check if plugin files were extracted to root (exclude README.md - main repo has it at root)
root_files = ['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}/')
@@ -1442,29 +1467,10 @@ def upgrade_plugin(request, plugin_name):
repo_zip = zipfile.ZipFile(io.BytesIO(repo_zip_data))
namelist = repo_zip.namelist()
# Discover top-level folder (GitHub uses repo-name-branch, e.g. cyberpanel-plugins-main)
top_level = None
for name in namelist:
if '/' in name:
top_level = name.split('/')[0]
break
elif name and not name.endswith('/'):
top_level = name
break
# Find plugin folder (supports flat repo or nested e.g. Category/pluginName)
top_level, plugin_prefix = _find_plugin_prefix_in_archive(namelist, plugin_name)
if not top_level:
raise Exception('GitHub archive has no recognizable structure')
# Find plugin folder in ZIP (case-insensitive: repo may have RedisManager vs redisManager)
plugin_prefix = None
plugin_name_lower = plugin_name.lower()
for name in namelist:
if '/' not in name:
continue
parts = name.split('/')
if len(parts) >= 2 and parts[0] == top_level and parts[1].lower() == plugin_name_lower:
# Use the actual casing from the ZIP for reading
plugin_prefix = f'{top_level}/{parts[1]}/'
break
if not plugin_prefix:
sample = namelist[:15] if len(namelist) > 15 else namelist
logging.writeToFile(f"Plugin {plugin_name} not in archive. Top-level={top_level}, sample paths: {sample}")
@@ -1495,20 +1501,18 @@ def upgrade_plugin(request, plugin_name):
logging.writeToFile(f"Created plugin ZIP: {zip_path}")
# Copy ZIP to current directory (pluginInstaller expects it in cwd)
zip_path_abs = os.path.abspath(zip_path)
if not os.path.exists(zip_path_abs):
raise Exception(f'Zip file not found: {zip_path_abs}')
original_cwd = os.getcwd()
os.chdir(temp_dir)
try:
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"Upgrading plugin using pluginInstaller (zip={zip_path_abs})")
logging.writeToFile(f"Upgrading plugin using pluginInstaller")
# Install using pluginInstaller (this will overwrite existing files)
# Install using pluginInstaller with explicit zip path (this will overwrite existing files)
try:
pluginInstaller.installPlugin(plugin_name)
pluginInstaller.installPlugin(plugin_name, zip_path=zip_path_abs)
except Exception as install_error:
error_msg = str(install_error)
logging.writeToFile(f"pluginInstaller.installPlugin raised exception: {error_msg}")
@@ -1702,23 +1706,10 @@ def install_from_store(request, plugin_name):
repo_zip = zipfile.ZipFile(io.BytesIO(repo_zip_data))
namelist = repo_zip.namelist()
# Discover top-level folder and find plugin (case-insensitive)
top_level = None
for name in namelist:
if '/' in name:
top_level = name.split('/')[0]
break
# Find plugin folder (supports flat repo or nested e.g. Category/pluginName)
top_level, plugin_prefix = _find_plugin_prefix_in_archive(namelist, plugin_name)
if not top_level:
raise Exception('GitHub archive has no recognizable structure')
plugin_prefix = None
plugin_name_lower = plugin_name.lower()
for name in namelist:
if '/' not in name:
continue
parts = name.split('/')
if len(parts) >= 2 and parts[0] == top_level and parts[1].lower() == plugin_name_lower:
plugin_prefix = f'{top_level}/{parts[1]}/'
break
if not plugin_prefix:
repo_zip.close()
logging.writeToFile(f"Plugin {plugin_name} not found in GitHub repository, trying local source")
@@ -1775,21 +1766,20 @@ def install_from_store(request, plugin_name):
logging.writeToFile(f"Created plugin ZIP: {zip_path}")
# Copy ZIP to current directory (pluginInstaller expects it in cwd)
if not os.path.exists(zip_path):
raise Exception(f'Zip file not found: {zip_path}')
# Pass absolute path so extraction does not depend on cwd (installPlugin may change cwd)
zip_path_abs = os.path.abspath(zip_path)
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 (zip={zip_path_abs})")
logging.writeToFile(f"Installing plugin using pluginInstaller")
# Install using pluginInstaller (direct call, not via command line)
# Install using pluginInstaller with explicit zip path (avoids cwd races)
try:
pluginInstaller.installPlugin(plugin_name)
pluginInstaller.installPlugin(plugin_name, zip_path=zip_path_abs)
except Exception as install_error:
# Log the full error for debugging
error_msg = str(install_error)
@@ -1808,8 +1798,8 @@ def install_from_store(request, plugin_name):
# 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']
# Exclude README.md - main CyberPanel repo has it at root
root_files = ['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}/')
@@ -1872,6 +1862,38 @@ def debug_loaded_plugins(request):
except Exception as e:
return JsonResponse({'success': False, 'error': str(e)}, status=500)
@require_http_methods(["GET", "POST"])
def plugin_settings_proxy(request, plugin_name):
"""
Proxy for /plugins/<plugin_name>/settings/ so plugin settings pages work even when
the plugin was installed after the worker started (dynamic URL list is built at import time).
"""
mailUtilities.checkHome()
plugin_path = '/usr/local/CyberCP/' + plugin_name
urls_py = os.path.join(plugin_path, 'urls.py')
if not plugin_name or not os.path.isdir(plugin_path) or not os.path.exists(urls_py):
from django.http import HttpResponseNotFound
return HttpResponseNotFound('Plugin not found or has no URL configuration.')
if plugin_name in RESERVED_PLUGIN_DIRS or plugin_name in (
'api', 'installed', 'help', 'emailMarketing', 'emailPremium', 'pluginHolder'
):
from django.http import HttpResponseNotFound
return HttpResponseNotFound('Invalid plugin.')
try:
import importlib
views_mod = importlib.import_module(plugin_name + '.views')
settings_view = getattr(views_mod, 'settings', None)
if not callable(settings_view):
from django.http import HttpResponseNotFound
return HttpResponseNotFound('Plugin has no settings view.')
return settings_view(request)
except Exception as e:
logging.writeToFile(f"plugin_settings_proxy for {plugin_name}: {str(e)}")
from django.http import HttpResponseServerError
return HttpResponseServerError(f'Plugin settings error: {str(e)}')
def plugin_help(request, plugin_name):
"""Plugin-specific help page - shows plugin information, version history, and help content"""
mailUtilities.checkHome()

View File

@@ -6,6 +6,8 @@ import argparse
import os
import shutil
import time
import tempfile
import zipfile
import django
from plogical.processUtilities import ProcessUtilities
@@ -58,16 +60,96 @@ class pluginInstaller:
### Functions Related to plugin installation.
@staticmethod
def extractPlugin(pluginName):
pathToPlugin = pluginName + '.zip'
command = 'unzip -o ' + pathToPlugin + ' -d /usr/local/CyberCP'
result = subprocess.run(shlex.split(command), capture_output=True, text=True)
if result.returncode != 0:
raise Exception(f"Failed to extract plugin {pluginName}: {result.stderr}")
# Verify extraction succeeded
def extractPlugin(pluginName, zip_path=None):
"""
Extract plugin zip so that all files end up in /usr/local/CyberCP/pluginName/.
Handles zips with: (1) top-level folder pluginName/, (2) top-level folder with
another name (e.g. repo-main/), or (3) files at root (no top-level folder).
If zip_path is given (absolute path), use it; otherwise use pluginName + '.zip' in cwd.
"""
if zip_path is not None:
pathToPlugin = os.path.abspath(zip_path)
else:
pathToPlugin = os.path.abspath(pluginName + '.zip')
if not os.path.exists(pathToPlugin):
raise Exception(f"Plugin zip not found: {pathToPlugin}")
pluginPath = '/usr/local/CyberCP/' + pluginName
if not os.path.exists(pluginPath):
raise Exception(f"Plugin extraction failed: {pluginPath} does not exist after extraction")
# Remove existing plugin dir so we start clean (e.g. from a previous failed install)
if os.path.exists(pluginPath):
shutil.rmtree(pluginPath)
extract_dir = tempfile.mkdtemp(prefix='cyberpanel_plugin_')
try:
with zipfile.ZipFile(pathToPlugin, 'r') as zf:
zf.extractall(extract_dir)
top_level = os.listdir(extract_dir)
plugin_name_lower = pluginName.lower()
# Prefer a top-level directory whose name matches pluginName (case-insensitive)
matching_dir = None
for name in top_level:
if os.path.isdir(os.path.join(extract_dir, name)) and name.lower() == plugin_name_lower:
matching_dir = name
break
if len(top_level) == 1:
single = os.path.join(extract_dir, top_level[0])
if os.path.isdir(single):
# One top-level directory
single_name = top_level[0]
# If it's the repo root (e.g. cyberpanel-plugins-main), check for plugin subdir
if single_name.lower() != plugin_name_lower:
plugin_subdir = os.path.join(single, pluginName)
if not os.path.isdir(plugin_subdir):
# Try case-insensitive subdir match
for entry in os.listdir(single):
if os.path.isdir(os.path.join(single, entry)) and entry.lower() == plugin_name_lower:
plugin_subdir = os.path.join(single, entry)
break
if os.path.isdir(plugin_subdir):
# Use the plugin subdir as the plugin content (avoid nesting repo root)
shutil.move(plugin_subdir, pluginPath)
shutil.rmtree(single, ignore_errors=True)
else:
shutil.move(single, pluginPath)
else:
shutil.move(single, pluginPath)
else:
# Single file at root
os.makedirs(pluginPath, exist_ok=True)
shutil.move(single, os.path.join(pluginPath, top_level[0]))
elif matching_dir:
# Multiple items: one is a dir matching pluginName - use it as plugin, put rest inside pluginPath
os.makedirs(pluginPath, exist_ok=True)
src_match = os.path.join(extract_dir, matching_dir)
# Move the matching plugin dir to pluginPath (replace if exists)
if os.path.exists(pluginPath):
shutil.rmtree(pluginPath)
shutil.move(src_match, pluginPath)
for name in top_level:
if name == matching_dir:
continue
src = os.path.join(extract_dir, name)
dst = os.path.join(pluginPath, name)
if os.path.exists(dst):
if os.path.isdir(dst):
shutil.rmtree(dst)
else:
os.remove(dst)
shutil.move(src, dst)
else:
# Multiple items or empty: place everything inside pluginName/
os.makedirs(pluginPath, exist_ok=True)
for name in top_level:
src = os.path.join(extract_dir, name)
dst = os.path.join(pluginPath, name)
if os.path.exists(dst):
if os.path.isdir(dst):
shutil.rmtree(dst)
else:
os.remove(dst)
shutil.move(src, dst)
if not os.path.exists(pluginPath):
raise Exception(f"Plugin extraction failed: {pluginPath} does not exist after extraction")
finally:
shutil.rmtree(extract_dir, ignore_errors=True)
@staticmethod
def upgradingSettingsFile(pluginName):
@@ -210,12 +292,12 @@ class pluginInstaller:
@staticmethod
def installPlugin(pluginName):
def installPlugin(pluginName, zip_path=None):
try:
##
pluginInstaller.stdOut('Extracting plugin..')
pluginInstaller.extractPlugin(pluginName)
pluginInstaller.extractPlugin(pluginName, zip_path=zip_path)
pluginInstaller.stdOut('Plugin extracted.')
##
@@ -385,41 +467,50 @@ class pluginInstaller:
@staticmethod
def removeFromSettings(pluginName):
data = open("/usr/local/CyberCP/CyberCP/settings.py", 'r', encoding='utf-8').readlines()
writeToFile = open("/usr/local/CyberCP/CyberCP/settings.py", 'w', encoding='utf-8')
settings_path = "/usr/local/CyberCP/CyberCP/settings.py"
try:
with open(settings_path, 'r', encoding='utf-8') as f:
data = f.readlines()
except (OSError, IOError) as e:
raise Exception(f'Cannot read {settings_path}: {e}. Ensure the panel user can read it.')
in_installed_apps = False
out_lines = []
for i, items in enumerate(data):
# Track if we're in INSTALLED_APPS section
if 'INSTALLED_APPS' in items and '=' in items:
in_installed_apps = True
elif in_installed_apps and items.strip().startswith(']'):
in_installed_apps = False
# More precise matching: look for plugin name in quotes (e.g., 'pluginName' or "pluginName")
# Only match if we're in INSTALLED_APPS section to prevent false positives
if in_installed_apps and (f"'{pluginName}'" in items or f'"{pluginName}"' in items):
continue
else:
writeToFile.writelines(items)
writeToFile.close()
out_lines.append(items)
try:
with open(settings_path, 'w', encoding='utf-8') as writeToFile:
writeToFile.writelines(out_lines)
except (OSError, IOError) as e:
raise Exception(
f'Cannot write {settings_path}: {e}. '
'Ensure the file is writable by the panel user (e.g. chgrp lscpd ... ; chmod g+w ...).'
)
@staticmethod
def removeFromURLs(pluginName):
data = open("/usr/local/CyberCP/CyberCP/urls.py", 'r', encoding='utf-8').readlines()
writeToFile = open("/usr/local/CyberCP/CyberCP/urls.py", 'w', encoding='utf-8')
urls_path = "/usr/local/CyberCP/CyberCP/urls.py"
try:
with open(urls_path, 'r', encoding='utf-8') as f:
data = f.readlines()
except (OSError, IOError) as e:
raise Exception(f'Cannot read {urls_path}: {e}.')
out_lines = []
for items in data:
# More precise matching: look for plugin name in path() or include() calls
# Match patterns like: path('plugins/pluginName/', include('pluginName.urls'))
# This prevents partial matches
if (f"plugins/{pluginName}/" in items or f"'{pluginName}.urls'" in items or f'"{pluginName}.urls"' in items or
if (f"plugins/{pluginName}/" in items or f"'{pluginName}.urls'" in items or f'"{pluginName}.urls"' in items or
f"include('{pluginName}.urls')" in items or f'include("{pluginName}.urls")' in items):
continue
else:
writeToFile.writelines(items)
writeToFile.close()
out_lines.append(items)
try:
with open(urls_path, 'w', encoding='utf-8') as f:
f.writelines(out_lines)
except (OSError, IOError) as e:
raise Exception(f'Cannot write {urls_path}: {e}. Ensure the file is writable by the panel user (chgrp lscpd; chmod g+w).')
@staticmethod
def informCyberPanelRemoval(pluginName):

View File

@@ -37,12 +37,13 @@ Pre_Upgrade_Required_Components() {
# Check if CyberCP directory exists but is incomplete/damaged
echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Checking CyberCP directory integrity..." | tee -a /var/log/cyberpanel_upgrade_debug.log
# Define essential CyberCP components
# Define essential CyberCP components (do not add /usr/local/CyberCP/manage - it does not exist; see usmannasir/cyberpanel#1721)
CYBERCP_ESSENTIAL_DIRS=(
"/usr/local/CyberCP/CyberCP"
"/usr/local/CyberCP/plogical"
"/usr/local/CyberCP/websiteFunctions"
"/usr/local/CyberCP/manage"
"/usr/local/CyberCP/pluginHolder"
"/usr/local/CyberCP/pluginInstaller"
)
CYBERCP_MISSING=0