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):