diff --git a/pluginInstaller/pluginInstaller.py b/pluginInstaller/pluginInstaller.py index c71e33e56..7ec9cfe62 100644 --- a/pluginInstaller/pluginInstaller.py +++ b/pluginInstaller/pluginInstaller.py @@ -57,6 +57,38 @@ class pluginInstaller: pluginHome = '/usr/local/CyberCP/' + pluginName return os.path.exists(pluginHome + '/enable_migrations') + @staticmethod + def _write_lines_to_protected_file(target_path, lines): + """ + Write UTF-8 lines to a file. Core panel files are often root:root 644; the panel + process may need a privileged copy (lscpd/sudo) to update them. + """ + try: + with open(target_path, 'w', encoding='utf-8') as wf: + wf.writelines(lines) + return + except (PermissionError, OSError) as e: + pluginInstaller.stdOut('Direct write failed for %s: %s' % (target_path, str(e))) + fd, tmp_path = tempfile.mkstemp(prefix='cpwr_', suffix='.txt', dir='/tmp') + try: + with os.fdopen(fd, 'w', encoding='utf-8') as wf: + wf.writelines(lines) + cmd = 'cp %s %s' % (shlex.quote(tmp_path), shlex.quote(target_path)) + if ProcessUtilities.executioner(cmd) == 1: + pluginInstaller.stdOut('Wrote %s via privileged copy' % target_path) + return + except Exception as ex: + pluginInstaller.stdOut('Privileged write failed: %s' % str(ex)) + finally: + try: + os.unlink(tmp_path) + except OSError: + pass + raise PermissionError( + 'Cannot write %s. As root: chgrp lscpd %s && chmod 664 %s' + % (target_path, target_path, target_path) + ) + ### Functions Related to plugin installation. @staticmethod @@ -153,53 +185,80 @@ class pluginInstaller: @staticmethod def upgradingSettingsFile(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" + with open(settings_path, 'r', encoding='utf-8') as rf: + data = rf.readlines() + line_plugin = " '" + pluginName + "',\n" + if any(line.strip() in ("'" + pluginName + "',", '"' + pluginName + '",') for line in data): + pluginInstaller.stdOut( + 'Plugin %s already listed in settings.py; skipping INSTALLED_APPS insert.' % pluginName + ) + return + out = [] + inserted = False for items in data: if items.find("'emailPremium',") > -1: - writeToFile.writelines(items) - writeToFile.writelines(" '" + pluginName + "',\n") + out.append(items) + out.append(line_plugin) + inserted = True else: - writeToFile.writelines(items) - - writeToFile.close() + out.append(items) + if not inserted: + out = [] + for items in data: + if "'pluginHolder'," in items or '"pluginHolder",' in items: + out.append(items) + out.append(line_plugin) + inserted = True + else: + out.append(items) + if not inserted: + pluginInstaller.stdOut( + 'Warning: no emailPremium or pluginHolder anchor in settings.py; ' + 'add %r to INSTALLED_APPS manually or upgrade CyberPanel (auto-sync plugins on disk).' + % pluginName + ) + return + pluginInstaller._write_lines_to_protected_file(settings_path, out) @staticmethod def upgradingURLs(pluginName): """ - Add plugin URL pattern to urls.py - Plugin URLs must be inserted BEFORE the generic 'plugins/' line - to ensure proper route matching (more specific routes first) + Legacy: add explicit path('plugins//', ...). Modern CyberPanel uses + pluginHolder.urls for all plugins — skip to avoid duplicate routes and root-only writes. """ - 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" + with open(urls_path, 'r', encoding='utf-8') as rf: + content = rf.read() + if "include('pluginHolder.urls')" in content or 'include("pluginHolder.urls")' in content: + pluginInstaller.stdOut( + 'pluginHolder.urls found; skipping per-plugin urls.py line for %s (dynamic routes).' % pluginName + ) + return + data = content.splitlines(keepends=True) + out = [] urlPatternAdded = False - for items in data: - # Insert plugin URL BEFORE the generic 'plugins/' line - # This ensures more specific routes are matched first - if items.find("path('plugins/', include('pluginHolder.urls'))") > -1 or items.find("path(\"plugins/\", include('pluginHolder.urls'))") > -1: + if items.find("path('plugins/', include('pluginHolder.urls'))") > -1 or items.find( + "path(\"plugins/\", include('pluginHolder.urls'))" + ) > -1: if not urlPatternAdded: - writeToFile.writelines(pluginInstaller.getUrlPattern(pluginName)) + out.append(pluginInstaller.getUrlPattern(pluginName)) urlPatternAdded = True - writeToFile.writelines(items) + out.append(items) else: - writeToFile.writelines(items) - - # Fallback: if 'plugins/' line not found, insert after 'manageservices' + out.append(items) if not urlPatternAdded: - pluginInstaller.stdOut(f"Warning: 'plugins/' line not found, using fallback insertion after 'manageservices'") - writeToFile.close() - writeToFile = open("/usr/local/CyberCP/CyberCP/urls.py", 'w', encoding='utf-8') + pluginInstaller.stdOut("Warning: 'plugins/' line not found, using fallback insertion after 'manageservices'") + out = [] for items in data: if items.find("manageservices") > -1: - writeToFile.writelines(items) - writeToFile.writelines(pluginInstaller.getUrlPattern(pluginName)) + out.append(items) + out.append(pluginInstaller.getUrlPattern(pluginName)) + urlPatternAdded = True else: - writeToFile.writelines(items) - - writeToFile.close() + out.append(items) + pluginInstaller._write_lines_to_protected_file(urls_path, out) @staticmethod def informCyberPanel(pluginName): @@ -214,19 +273,20 @@ class pluginInstaller: @staticmethod def addInterfaceLink(pluginName): - data = open("/usr/local/CyberCP/baseTemplate/templates/baseTemplate/index.html", 'r', encoding='utf-8').readlines() - writeToFile = open("/usr/local/CyberCP/baseTemplate/templates/baseTemplate/index.html", 'w', encoding='utf-8') - + path_html = "/usr/local/CyberCP/baseTemplate/templates/baseTemplate/index.html" + with open(path_html, 'r', encoding='utf-8') as rf: + data = rf.readlines() + out = [] for items in data: if items.find("{# pluginsList #}") > -1: - writeToFile.writelines(items) - writeToFile.writelines(" ") - writeToFile.writelines( - '
  • {% trans "' + pluginName + '" %}
  • \n') + out.append(items) + out.append(" ") + out.append( + '
  • {% trans "' + pluginName + '" %}
  • \n' + ) else: - writeToFile.writelines(items) - - writeToFile.close() + out.append(items) + pluginInstaller._write_lines_to_protected_file(path_html, out) @staticmethod def staticContent(): @@ -484,9 +544,8 @@ class pluginInstaller: continue out_lines.append(items) try: - with open(settings_path, 'w', encoding='utf-8') as writeToFile: - writeToFile.writelines(out_lines) - except (OSError, IOError) as e: + pluginInstaller._write_lines_to_protected_file(settings_path, out_lines) + except (OSError, IOError, PermissionError) 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 ...).' @@ -507,9 +566,8 @@ class pluginInstaller: continue out_lines.append(items) try: - with open(urls_path, 'w', encoding='utf-8') as f: - f.writelines(out_lines) - except (OSError, IOError) as e: + pluginInstaller._write_lines_to_protected_file(urls_path, out_lines) + except (OSError, IOError, PermissionError) 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 @@ -521,15 +579,15 @@ class pluginInstaller: @staticmethod def removeInterfaceLink(pluginName): - data = open("/usr/local/CyberCP/baseTemplate/templates/baseTemplate/index.html", 'r', encoding='utf-8').readlines() - writeToFile = open("/usr/local/CyberCP/baseTemplate/templates/baseTemplate/index.html", 'w', encoding='utf-8') - + path_html = "/usr/local/CyberCP/baseTemplate/templates/baseTemplate/index.html" + with open(path_html, 'r', encoding='utf-8') as rf: + data = rf.readlines() + out = [] for items in data: if items.find(pluginName) > -1 and items.find('
  • ') > -1: continue - else: - writeToFile.writelines(items) - writeToFile.close() + out.append(items) + pluginInstaller._write_lines_to_protected_file(path_html, out) @staticmethod def removeMigrations(pluginName):