Skip to content

Commit 55ba384

Browse files
authored
Merge pull request #1724 from master3395/v2.5.5-dev
V2.5.5 dev
2 parents 9c55612 + a6102f9 commit 55ba384

File tree

4 files changed

+215
-100
lines changed

4 files changed

+215
-100
lines changed

pluginHolder/urls.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,10 +104,11 @@ def _get_installed_plugin_list():
104104
path('api/revert/<str:plugin_name>/', views.revert_plugin, name='revert_plugin'),
105105
path('api/debug-plugins/', views.debug_loaded_plugins, name='debug_loaded_plugins'),
106106
path('api/check-subscription/<str:plugin_name>/', views.check_plugin_subscription, name='check_plugin_subscription'),
107+
path('<str:plugin_name>/settings/', views.plugin_settings_proxy, name='plugin_settings_proxy'),
107108
path('<str:plugin_name>/help/', views.plugin_help, name='plugin_help'),
108109
]
109110

110-
# Include each installed plugin's URLs *before* the catch-all so /plugins/<name>/settings/ etc. match
111+
# Include each installed plugin's URLs *before* the catch-all so /plugins/<name>/... (other than settings/help) match
111112
_loaded_plugins = []
112113
_failed_plugins = {}
113114
for _plugin_name, _path_parent in _get_installed_plugin_list():

pluginHolder/views.py

Lines changed: 87 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,34 @@
5454
'websiteFunctions', 'aiScanner', 'dns', 'help', 'installed',
5555
])
5656

57+
def _find_plugin_prefix_in_archive(namelist, plugin_name):
58+
"""
59+
Find the path prefix for a plugin inside a GitHub archive (e.g. repo-main/pluginName/ or repo-main/Category/pluginName/).
60+
Returns (top_level, plugin_prefix) or (None, None) if not found.
61+
"""
62+
top_level = None
63+
for name in namelist:
64+
if '/' in name:
65+
top_level = name.split('/')[0]
66+
break
67+
if not top_level:
68+
return None, None
69+
plugin_name_lower = plugin_name.lower()
70+
# Check every path: find one that has a segment equal to plugin_name (e.g. .../pm2Manager/ or .../snappymailAdmin/)
71+
for name in namelist:
72+
if '/' not in name:
73+
continue
74+
parts = name.split('/')
75+
# parts[0] = top_level, then we need a segment that matches plugin_name
76+
for i in range(1, len(parts)):
77+
if parts[i].lower() == plugin_name_lower:
78+
# Plugin folder is at top_level/parts[1]/.../parts[i]/
79+
prefix_parts = [top_level] + parts[1:i + 1]
80+
plugin_prefix = '/'.join(prefix_parts) + '/'
81+
return top_level, plugin_prefix
82+
return top_level, None
83+
84+
5785
def _get_plugin_source_path(plugin_name):
5886
"""Return the full path to a plugin's source directory, or None if not found."""
5987
for base in PLUGIN_SOURCE_PATHS:
@@ -607,19 +635,16 @@ def install_plugin(request, plugin_name):
607635
'error': f'Failed to create zip file for {plugin_name}'
608636
}, status=500)
609637

610-
# Copy zip to current directory (pluginInstaller expects it in cwd)
638+
zip_path_abs = os.path.abspath(zip_path)
639+
if not os.path.exists(zip_path_abs):
640+
raise Exception(f'Zip file not found: {zip_path_abs}')
611641
original_cwd = os.getcwd()
612642
os.chdir(temp_dir)
613643

614644
try:
615-
# Verify zip file exists in current directory
616-
zip_file = plugin_name + '.zip'
617-
if not os.path.exists(zip_file):
618-
raise Exception(f'Zip file {zip_file} not found in temp directory')
619-
620-
# Install using pluginInstaller
645+
# Install using pluginInstaller with explicit zip path (avoids cwd races)
621646
try:
622-
pluginInstaller.installPlugin(plugin_name)
647+
pluginInstaller.installPlugin(plugin_name, zip_path=zip_path_abs)
623648
except Exception as install_error:
624649
# Log the full error for debugging
625650
error_msg = str(install_error)
@@ -638,8 +663,8 @@ def install_plugin(request, plugin_name):
638663
# Verify plugin was actually installed
639664
pluginInstalled = '/usr/local/CyberCP/' + plugin_name
640665
if not os.path.exists(pluginInstalled):
641-
# Check if files were extracted to root instead
642-
root_files = ['README.md', 'apps.py', 'meta.xml', 'urls.py', 'views.py']
666+
# Check if plugin files were extracted to root (exclude README.md - main repo has it at root)
667+
root_files = ['apps.py', 'meta.xml', 'urls.py', 'views.py']
643668
found_root_files = [f for f in root_files if os.path.exists(os.path.join('/usr/local/CyberCP', f))]
644669
if found_root_files:
645670
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):
14421467
repo_zip = zipfile.ZipFile(io.BytesIO(repo_zip_data))
14431468
namelist = repo_zip.namelist()
14441469

1445-
# Discover top-level folder (GitHub uses repo-name-branch, e.g. cyberpanel-plugins-main)
1446-
top_level = None
1447-
for name in namelist:
1448-
if '/' in name:
1449-
top_level = name.split('/')[0]
1450-
break
1451-
elif name and not name.endswith('/'):
1452-
top_level = name
1453-
break
1470+
# Find plugin folder (supports flat repo or nested e.g. Category/pluginName)
1471+
top_level, plugin_prefix = _find_plugin_prefix_in_archive(namelist, plugin_name)
14541472
if not top_level:
14551473
raise Exception('GitHub archive has no recognizable structure')
1456-
1457-
# Find plugin folder in ZIP (case-insensitive: repo may have RedisManager vs redisManager)
1458-
plugin_prefix = None
1459-
plugin_name_lower = plugin_name.lower()
1460-
for name in namelist:
1461-
if '/' not in name:
1462-
continue
1463-
parts = name.split('/')
1464-
if len(parts) >= 2 and parts[0] == top_level and parts[1].lower() == plugin_name_lower:
1465-
# Use the actual casing from the ZIP for reading
1466-
plugin_prefix = f'{top_level}/{parts[1]}/'
1467-
break
14681474
if not plugin_prefix:
14691475
sample = namelist[:15] if len(namelist) > 15 else namelist
14701476
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):
14951501

14961502
logging.writeToFile(f"Created plugin ZIP: {zip_path}")
14971503

1498-
# Copy ZIP to current directory (pluginInstaller expects it in cwd)
1504+
zip_path_abs = os.path.abspath(zip_path)
1505+
if not os.path.exists(zip_path_abs):
1506+
raise Exception(f'Zip file not found: {zip_path_abs}')
14991507
original_cwd = os.getcwd()
15001508
os.chdir(temp_dir)
15011509

15021510
try:
1503-
zip_file = plugin_name + '.zip'
1504-
if not os.path.exists(zip_file):
1505-
raise Exception(f'Zip file {zip_file} not found in temp directory')
1506-
1507-
logging.writeToFile(f"Upgrading plugin using pluginInstaller")
1511+
logging.writeToFile(f"Upgrading plugin using pluginInstaller (zip={zip_path_abs})")
15081512

1509-
# Install using pluginInstaller (this will overwrite existing files)
1513+
# Install using pluginInstaller with explicit zip path (this will overwrite existing files)
15101514
try:
1511-
pluginInstaller.installPlugin(plugin_name)
1515+
pluginInstaller.installPlugin(plugin_name, zip_path=zip_path_abs)
15121516
except Exception as install_error:
15131517
error_msg = str(install_error)
15141518
logging.writeToFile(f"pluginInstaller.installPlugin raised exception: {error_msg}")
@@ -1702,23 +1706,10 @@ def install_from_store(request, plugin_name):
17021706
repo_zip = zipfile.ZipFile(io.BytesIO(repo_zip_data))
17031707
namelist = repo_zip.namelist()
17041708

1705-
# Discover top-level folder and find plugin (case-insensitive)
1706-
top_level = None
1707-
for name in namelist:
1708-
if '/' in name:
1709-
top_level = name.split('/')[0]
1710-
break
1709+
# Find plugin folder (supports flat repo or nested e.g. Category/pluginName)
1710+
top_level, plugin_prefix = _find_plugin_prefix_in_archive(namelist, plugin_name)
17111711
if not top_level:
17121712
raise Exception('GitHub archive has no recognizable structure')
1713-
plugin_prefix = None
1714-
plugin_name_lower = plugin_name.lower()
1715-
for name in namelist:
1716-
if '/' not in name:
1717-
continue
1718-
parts = name.split('/')
1719-
if len(parts) >= 2 and parts[0] == top_level and parts[1].lower() == plugin_name_lower:
1720-
plugin_prefix = f'{top_level}/{parts[1]}/'
1721-
break
17221713
if not plugin_prefix:
17231714
repo_zip.close()
17241715
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):
17751766

17761767
logging.writeToFile(f"Created plugin ZIP: {zip_path}")
17771768

1778-
# Copy ZIP to current directory (pluginInstaller expects it in cwd)
1769+
if not os.path.exists(zip_path):
1770+
raise Exception(f'Zip file not found: {zip_path}')
1771+
1772+
# Pass absolute path so extraction does not depend on cwd (installPlugin may change cwd)
1773+
zip_path_abs = os.path.abspath(zip_path)
17791774
original_cwd = os.getcwd()
17801775
os.chdir(temp_dir)
17811776

17821777
try:
1783-
# Verify zip file exists in current directory
1784-
zip_file = plugin_name + '.zip'
1785-
if not os.path.exists(zip_file):
1786-
raise Exception(f'Zip file {zip_file} not found in temp directory')
1787-
1788-
logging.writeToFile(f"Installing plugin using pluginInstaller")
1778+
logging.writeToFile(f"Installing plugin using pluginInstaller (zip={zip_path_abs})")
17891779

1790-
# Install using pluginInstaller (direct call, not via command line)
1780+
# Install using pluginInstaller with explicit zip path (avoids cwd races)
17911781
try:
1792-
pluginInstaller.installPlugin(plugin_name)
1782+
pluginInstaller.installPlugin(plugin_name, zip_path=zip_path_abs)
17931783
except Exception as install_error:
17941784
# Log the full error for debugging
17951785
error_msg = str(install_error)
@@ -1808,8 +1798,8 @@ def install_from_store(request, plugin_name):
18081798
# Verify plugin was actually installed
18091799
pluginInstalled = '/usr/local/CyberCP/' + plugin_name
18101800
if not os.path.exists(pluginInstalled):
1811-
# Check if files were extracted to root instead
1812-
root_files = ['README.md', 'apps.py', 'meta.xml', 'urls.py', 'views.py']
1801+
# Exclude README.md - main CyberPanel repo has it at root
1802+
root_files = ['apps.py', 'meta.xml', 'urls.py', 'views.py']
18131803
found_root_files = [f for f in root_files if os.path.exists(os.path.join('/usr/local/CyberCP', f))]
18141804
if found_root_files:
18151805
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):
18721862
except Exception as e:
18731863
return JsonResponse({'success': False, 'error': str(e)}, status=500)
18741864

1865+
1866+
@require_http_methods(["GET", "POST"])
1867+
def plugin_settings_proxy(request, plugin_name):
1868+
"""
1869+
Proxy for /plugins/<plugin_name>/settings/ so plugin settings pages work even when
1870+
the plugin was installed after the worker started (dynamic URL list is built at import time).
1871+
"""
1872+
mailUtilities.checkHome()
1873+
plugin_path = '/usr/local/CyberCP/' + plugin_name
1874+
urls_py = os.path.join(plugin_path, 'urls.py')
1875+
if not plugin_name or not os.path.isdir(plugin_path) or not os.path.exists(urls_py):
1876+
from django.http import HttpResponseNotFound
1877+
return HttpResponseNotFound('Plugin not found or has no URL configuration.')
1878+
if plugin_name in RESERVED_PLUGIN_DIRS or plugin_name in (
1879+
'api', 'installed', 'help', 'emailMarketing', 'emailPremium', 'pluginHolder'
1880+
):
1881+
from django.http import HttpResponseNotFound
1882+
return HttpResponseNotFound('Invalid plugin.')
1883+
try:
1884+
import importlib
1885+
views_mod = importlib.import_module(plugin_name + '.views')
1886+
settings_view = getattr(views_mod, 'settings', None)
1887+
if not callable(settings_view):
1888+
from django.http import HttpResponseNotFound
1889+
return HttpResponseNotFound('Plugin has no settings view.')
1890+
return settings_view(request)
1891+
except Exception as e:
1892+
logging.writeToFile(f"plugin_settings_proxy for {plugin_name}: {str(e)}")
1893+
from django.http import HttpResponseServerError
1894+
return HttpResponseServerError(f'Plugin settings error: {str(e)}')
1895+
1896+
18751897
def plugin_help(request, plugin_name):
18761898
"""Plugin-specific help page - shows plugin information, version history, and help content"""
18771899
mailUtilities.checkHome()

0 commit comments

Comments
 (0)