pluginInstaller: privileged write for settings/urls, skip duplicate pluginHolder urls

- _write_lines_to_protected_file: temp file + ProcessUtilities cp when direct write fails
- upgradingSettingsFile: idempotent; fallback anchor pluginHolder; clearer warnings
- upgradingURLs: no-op when pluginHolder.urls present (avoids duplicate routes and root-only edits)
- add/removeInterfaceLink and removeFromSettings/URLs use protected writer
This commit is contained in:
master3395
2026-03-24 23:21:09 +01:00
parent bb61871b28
commit 1f06e8b090

View File

@@ -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/<name>/', ...). 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(
'<li><a href="{% url \'' + pluginName + '\' %}" title="{% trans \'' + pluginName + '\' %}"><span>{% trans "' + pluginName + '" %}</span></a></li>\n')
out.append(items)
out.append(" ")
out.append(
'<li><a href="{% url \'' + pluginName + '\' %}" title="{% trans \'' + pluginName + '\' %}"><span>{% trans "' + pluginName + '" %}</span></a></li>\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('<li>') > -1:
continue
else:
writeToFile.writelines(items)
writeToFile.close()
out.append(items)
pluginInstaller._write_lines_to_protected_file(path_html, out)
@staticmethod
def removeMigrations(pluginName):