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+
5785def _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+
18751897def plugin_help (request , plugin_name ):
18761898 """Plugin-specific help page - shows plugin information, version history, and help content"""
18771899 mailUtilities .checkHome ()
0 commit comments