From 0225f2f95a4535200a34487fd810ca891b30cc7d Mon Sep 17 00:00:00 2001 From: master3395 Date: Tue, 3 Feb 2026 18:46:05 +0100 Subject: [PATCH] Fix plugin settings 404: dynamic URL inclusion for all installed plugins - pluginHolder/urls.py: Discover plugins from /usr/local/CyberCP and source paths (/home/cyberpanel/plugins, /home/cyberpanel-plugins); dynamically include each plugin's urls so /plugins//settings/ works without hardcoding. Add source path to sys.path when loading from source. - CyberCP/urls.py: Remove hardcoded _plugin_routes; all plugin routes now served via pluginHolder dynamic inclusion. Fixes 404 on /plugins/contaboAutoSnapshot/settings/ and any installed plugin settings page. No per-plugin core changes required. --- CyberCP/urls.py | 7 ++- pluginHolder/urls.py | 102 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 106 insertions(+), 3 deletions(-) diff --git a/CyberCP/urls.py b/CyberCP/urls.py index db097f5a0..18467e668 100644 --- a/CyberCP/urls.py +++ b/CyberCP/urls.py @@ -20,6 +20,10 @@ from django.conf.urls.static import static from django.views.static import serve from firewall import views as firewall_views +# Plugin routes are no longer hardcoded here; pluginHolder.urls dynamically +# includes each installed plugin (under /plugins//) so settings and +# other plugin pages work for any installed plugin. + urlpatterns = [ # Serve static files first (before catch-all routes) re_path(r'^static/(?P.*)$', serve, {'document_root': settings.STATIC_ROOT}), @@ -43,6 +47,7 @@ urlpatterns = [ path('api/', include('api.urls')), path('filemanager/', include('filemanager.urls')), path('emailPremium/', include('emailPremium.urls')), + path('emailMarketing/', include('emailMarketing.urls')), # Default-installed (sidebar links to it) path('manageservices/', include('manageServices.urls')), path('plugins/', include('pluginHolder.urls')), path('cloudAPI/', include('cloudAPI.urls')), @@ -51,8 +56,6 @@ urlpatterns = [ path('CloudLinux/', include('CLManager.urls')), path('IncrementalBackups/', include('IncBackups.urls')), path('aiscanner/', include('aiScanner.urls')), - # Optional plugin routes - added by plugin installer when plugins are installed from Plugin Store - # path('emailMarketing/', include('emailMarketing.urls')), # path('Terminal/', include('WebTerminal.urls')), path('', include('loginSystem.urls')), ] diff --git a/pluginHolder/urls.py b/pluginHolder/urls.py index 7e4c547cb..5eefc03e6 100644 --- a/pluginHolder/urls.py +++ b/pluginHolder/urls.py @@ -1,6 +1,88 @@ -from django.urls import path +# -*- coding: utf-8 -*- +""" +PluginHolder URL configuration. +Static routes are defined first; then URLs for each installed plugin are +included dynamically so /plugins//... (e.g. settings/) works +without hardcoding plugin names in the main CyberCP urls.py. + +Discovery order: /usr/local/CyberCP first (installed), then source paths +(/home/cyberpanel/plugins, /home/cyberpanel-plugins) so settings work even +when the plugin is only present in source. +""" +from django.urls import path, include +import os +import sys + from . import views +# Installed plugins live under this path (must match pluginInstaller and pluginHolder.views) +INSTALLED_PLUGINS_PATH = '/usr/local/CyberCP' + +# Source paths for plugins (same as pluginHolder.views PLUGIN_SOURCE_PATHS) +# Checked when plugin is not under INSTALLED_PLUGINS_PATH so URLs still work +PLUGIN_SOURCE_PATHS = ['/home/cyberpanel/plugins', '/home/cyberpanel-plugins'] + +# Plugin directory names that must not be routed here (core apps or reserved paths) +RESERVED_PLUGIN_PATHS = frozenset([ + 'installed', 'help', 'api', # pluginHolder's own path segments + 'emailMarketing', 'emailPremium', 'pluginHolder', 'loginSystem', 'baseTemplate', + 'packages', 'websiteFunctions', 'userManagment', 'dns', 'databases', 'ftp', + 'filemanager', 'mailServer', 'cloudAPI', 'containerization', 'IncBackups', + 'CLManager', 's3Backups', 'dockerManager', 'aiScanner', 'firewall', 'tuning', + 'serverStatus', 'serverLogs', 'backup', 'managePHP', 'manageSSL', 'manageServices', + 'highAvailability', +]) + + +def _plugin_has_urls(plugin_dir): + """Return True if plugin_dir has meta.xml and urls.py.""" + if not os.path.isdir(plugin_dir): + return False + return (os.path.exists(os.path.join(plugin_dir, 'meta.xml')) and + os.path.exists(os.path.join(plugin_dir, 'urls.py'))) + + +def _get_installed_plugin_list(): + """ + Return sorted list of (plugin_name, path_parent) to mount at /plugins//. + path_parent is the directory that must be on sys.path to import the plugin + (e.g. /usr/local/CyberCP or /home/cyberpanel/plugins). + First discovers from INSTALLED_PLUGINS_PATH, then from PLUGIN_SOURCE_PATHS. + """ + seen = set() + result = [] # (name, path_parent) + + # 1) Installed location (canonical) + if os.path.isdir(INSTALLED_PLUGINS_PATH): + try: + for name in os.listdir(INSTALLED_PLUGINS_PATH): + if name in RESERVED_PLUGIN_PATHS or name.startswith('.'): + continue + plugin_dir = os.path.join(INSTALLED_PLUGINS_PATH, name) + if _plugin_has_urls(plugin_dir): + seen.add(name) + result.append((name, INSTALLED_PLUGINS_PATH)) + except (OSError, IOError): + pass + + # 2) Source paths (fallback so /plugins/PluginName/settings/ works even if not in CyberCP) + for base in PLUGIN_SOURCE_PATHS: + if not os.path.isdir(base): + continue + try: + for name in os.listdir(base): + if name in seen or name in RESERVED_PLUGIN_PATHS or name.startswith('.'): + continue + plugin_dir = os.path.join(base, name) + if _plugin_has_urls(plugin_dir): + seen.add(name) + result.append((name, base)) + except (OSError, IOError): + pass + + return sorted(result, key=lambda x: x[0]) + + urlpatterns = [ path('installed', views.installed, name='installed'), path('help/', views.help_page, name='help'), @@ -15,3 +97,21 @@ urlpatterns = [ path('api/revert//', views.revert_plugin, name='revert_plugin'), path('/help/', views.plugin_help, name='plugin_help'), ] + +# Dynamically include each installed plugin's URLs so /plugins//settings/ etc. work +for _plugin_name, _path_parent in _get_installed_plugin_list(): + try: + # If plugin is from a source path, ensure it is on sys.path so import works + if _path_parent not in sys.path: + sys.path.insert(0, _path_parent) + __import__(_plugin_name + '.urls') + urlpatterns.append(path(_plugin_name + '/', include(_plugin_name + '.urls'))) + except (ImportError, AttributeError) as e: + try: + from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter as _logging + _logging.writeToFile( + 'pluginHolder.urls: Skipping plugin "%s" (urls not loadable): %s' + % (_plugin_name, e) + ) + except Exception: + pass