From 11d201747ab4fda1ff9fd8f89b5506af9cdd797f Mon Sep 17 00:00:00 2001 From: master3395 Date: Fri, 6 Mar 2026 20:16:21 +0100 Subject: [PATCH 1/5] Fix plugin installation: extract to plugin dir for any zip structure (install/upgrade/downgrade safe) --- pluginInstaller/pluginInstaller.py | 49 +++++++++++++++++++++++++----- 1 file changed, 42 insertions(+), 7 deletions(-) diff --git a/pluginInstaller/pluginInstaller.py b/pluginInstaller/pluginInstaller.py index 30288ccf5..b0c5070c3 100644 --- a/pluginInstaller/pluginInstaller.py +++ b/pluginInstaller/pluginInstaller.py @@ -6,6 +6,8 @@ import argparse import os import shutil import time +import tempfile +import zipfile import django from plogical.processUtilities import ProcessUtilities @@ -59,15 +61,48 @@ class pluginInstaller: @staticmethod def extractPlugin(pluginName): + """ + 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). + """ 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 + 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) + if len(top_level) == 1: + single = os.path.join(extract_dir, top_level[0]) + if os.path.isdir(single): + # One top-level directory: use it as the plugin dir (move or rename to pluginName) + 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])) + 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): From a6268d7f539ee76c7d8b5babd8b9075dd4104d32 Mon Sep 17 00:00:00 2001 From: master3395 Date: Fri, 6 Mar 2026 20:26:15 +0100 Subject: [PATCH 2/5] Plugin install: pass absolute zip path to avoid cwd races; extract always uses given path --- pluginHolder/views.py | 45 +++++++++++++----------------- pluginInstaller/pluginInstaller.py | 12 +++++--- 2 files changed, 27 insertions(+), 30 deletions(-) diff --git a/pluginHolder/views.py b/pluginHolder/views.py index 166a4f0f5..c3527e181 100644 --- a/pluginHolder/views.py +++ b/pluginHolder/views.py @@ -607,19 +607,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) @@ -1495,20 +1492,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}") @@ -1775,21 +1770,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,7 +1802,6 @@ 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'] found_root_files = [f for f in root_files if os.path.exists(os.path.join('/usr/local/CyberCP', f))] if found_root_files: diff --git a/pluginInstaller/pluginInstaller.py b/pluginInstaller/pluginInstaller.py index b0c5070c3..67d0c160e 100644 --- a/pluginInstaller/pluginInstaller.py +++ b/pluginInstaller/pluginInstaller.py @@ -60,13 +60,17 @@ class pluginInstaller: ### Functions Related to plugin installation. @staticmethod - def extractPlugin(pluginName): + 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. """ - pathToPlugin = pluginName + '.zip' + 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 @@ -245,12 +249,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.') ## From bbf0436c0db0c82a72ce113d01753e6e7ce2cae1 Mon Sep 17 00:00:00 2001 From: master3395 Date: Fri, 6 Mar 2026 20:40:32 +0100 Subject: [PATCH 3/5] Plugin store: fix install for all plugins (nested repo + extraction) - Find plugin in GitHub archive by any path segment (Category/pluginName) - extractPlugin: prefer top-level dir matching plugin name; handle repo-root single dir by using plugin subdir when present (avoids wrong location) - Add pluginHolder and pluginInstaller to upgrade recovery essential dirs Fixes: Files extracted to wrong location (e.g. pm2Manager, README.md in root) --- pluginHolder/views.py | 68 ++++++++++++++---------------- pluginInstaller/pluginInstaller.py | 47 ++++++++++++++++++++- upgrade_modules/06_components.sh | 5 ++- 3 files changed, 80 insertions(+), 40 deletions(-) diff --git a/pluginHolder/views.py b/pluginHolder/views.py index c3527e181..977098540 100644 --- a/pluginHolder/views.py +++ b/pluginHolder/views.py @@ -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: @@ -1439,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}") @@ -1697,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") diff --git a/pluginInstaller/pluginInstaller.py b/pluginInstaller/pluginInstaller.py index 67d0c160e..e29fc6010 100644 --- a/pluginInstaller/pluginInstaller.py +++ b/pluginInstaller/pluginInstaller.py @@ -82,15 +82,58 @@ class pluginInstaller: 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: use it as the plugin dir (move or rename to pluginName) - shutil.move(single, pluginPath) + # 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) diff --git a/upgrade_modules/06_components.sh b/upgrade_modules/06_components.sh index b76d62850..b7eab9ad5 100644 --- a/upgrade_modules/06_components.sh +++ b/upgrade_modules/06_components.sh @@ -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 From ecf3af09863aa364a2de5c1b2297d473f71c6aaf Mon Sep 17 00:00:00 2001 From: master3395 Date: Fri, 6 Mar 2026 20:51:45 +0100 Subject: [PATCH 4/5] Plugin install: exclude README.md from wrong-location check (main repo has it at root) Fixes false 'Files extracted to wrong location' when /usr/local/CyberCP/README.md exists. Ensure panel user can create plugin dirs (e.g. chgrp lscpd /usr/local/CyberCP, chmod g+w, and add panel user to group lscpd). --- pluginHolder/views.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pluginHolder/views.py b/pluginHolder/views.py index 977098540..f00fbf6ff 100644 --- a/pluginHolder/views.py +++ b/pluginHolder/views.py @@ -663,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}/') @@ -1798,7 +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): - 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}/') From a6102f9c86d93aabb8147f3f028ff11c79cb9c5e Mon Sep 17 00:00:00 2001 From: master3395 Date: Fri, 6 Mar 2026 21:00:33 +0100 Subject: [PATCH 5/5] Plugin settings 404 and uninstall permission fixes - Add plugin_settings_proxy for /plugins//settings/ so settings pages work for all installed plugins (handles plugins installed after worker start) - Add path('/settings/', plugin_settings_proxy) in pluginHolder.urls - removeFromSettings/removeFromURLs: use try/except for read/write, raise clear PermissionError message; ensure panel user can write (chgrp lscpd; chmod g+w) - Deploy: make CyberCP/settings.py, urls.py, baseTemplate index.html group- writable by lscpd so uninstall can update them --- pluginHolder/urls.py | 3 +- pluginHolder/views.py | 32 +++++++++++++++++++ pluginInstaller/pluginInstaller.py | 51 ++++++++++++++++++------------ 3 files changed, 64 insertions(+), 22 deletions(-) diff --git a/pluginHolder/urls.py b/pluginHolder/urls.py index b25be6087..f7876162a 100644 --- a/pluginHolder/urls.py +++ b/pluginHolder/urls.py @@ -104,10 +104,11 @@ urlpatterns = [ path('api/revert//', views.revert_plugin, name='revert_plugin'), path('api/debug-plugins/', views.debug_loaded_plugins, name='debug_loaded_plugins'), path('api/check-subscription//', views.check_plugin_subscription, name='check_plugin_subscription'), + path('/settings/', views.plugin_settings_proxy, name='plugin_settings_proxy'), path('/help/', views.plugin_help, name='plugin_help'), ] -# Include each installed plugin's URLs *before* the catch-all so /plugins//settings/ etc. match +# Include each installed plugin's URLs *before* the catch-all so /plugins//... (other than settings/help) match _loaded_plugins = [] _failed_plugins = {} for _plugin_name, _path_parent in _get_installed_plugin_list(): diff --git a/pluginHolder/views.py b/pluginHolder/views.py index f00fbf6ff..8e7d1dee1 100644 --- a/pluginHolder/views.py +++ b/pluginHolder/views.py @@ -1862,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//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() diff --git a/pluginInstaller/pluginInstaller.py b/pluginInstaller/pluginInstaller.py index e29fc6010..c71e33e56 100644 --- a/pluginInstaller/pluginInstaller.py +++ b/pluginInstaller/pluginInstaller.py @@ -467,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):