Plugin settings 404 and uninstall permission fixes

- Add plugin_settings_proxy for /plugins/<name>/settings/ so settings pages
  work for all installed plugins (handles plugins installed after worker start)
- Add path('<str:plugin_name>/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
This commit is contained in:
master3395
2026-03-06 21:00:33 +01:00
committed by KraoESPfan1n
parent ecf3af0986
commit a6102f9c86
3 changed files with 64 additions and 22 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

@@ -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/<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

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