Files
CyberPanel/pluginInstaller/pluginInstaller.py

479 lines
18 KiB
Python
Raw Normal View History

2025-08-01 14:56:30 +05:00
import sys
sys.path.append('/usr/local/CyberCP')
import subprocess
import shlex
import argparse
import os
import shutil
import time
import django
2025-08-01 14:56:30 +05:00
from plogical.processUtilities import ProcessUtilities
class pluginInstaller:
installLogPath = "/home/cyberpanel/modSecInstallLog"
tempRulesFile = "/home/cyberpanel/tempModSecRules"
mirrorPath = "cyberpanel.net"
@staticmethod
def getUrlPattern(pluginName):
"""
Generate URL pattern compatible with both Django 2.x and 3.x+
Django 2.x uses url() with regex patterns
Django 3.x+ prefers path() with simpler patterns
Plugins are routed under /plugins/pluginName/ to match meta.xml URLs
"""
try:
django_version = django.get_version()
major_version = int(django_version.split('.')[0])
pluginInstaller.stdOut(f"Django version detected: {django_version}")
if major_version >= 3:
# Django 3.x+ - use path() syntax with /plugins/ prefix
pluginInstaller.stdOut(f"Using path() syntax for Django 3.x+ compatibility")
return " path('plugins/" + pluginName + "/',include('" + pluginName + ".urls')),\n"
else:
# Django 2.x - use url() syntax with regex and /plugins/ prefix
pluginInstaller.stdOut(f"Using url() syntax for Django 2.x compatibility")
return " url(r'^plugins/" + pluginName + "/',include('" + pluginName + ".urls')),\n"
except Exception as e:
# Fallback to modern path() syntax if version detection fails
pluginInstaller.stdOut(f"Django version detection failed: {str(e)}, using path() syntax as fallback")
return " path('plugins/" + pluginName + "/',include('" + pluginName + ".urls')),\n"
2025-08-01 14:56:30 +05:00
@staticmethod
def stdOut(message):
print("\n\n")
print(("[" + time.strftime(
"%m.%d.%Y_%H-%M-%S") + "] #########################################################################\n"))
print(("[" + time.strftime("%m.%d.%Y_%H-%M-%S") + "] " + message + "\n"))
print(("[" + time.strftime(
"%m.%d.%Y_%H-%M-%S") + "] #########################################################################\n"))
@staticmethod
def migrationsEnabled(pluginName: str) -> bool:
pluginHome = '/usr/local/CyberCP/' + pluginName
return os.path.exists(pluginHome + '/enable_migrations')
### Functions Related to plugin installation.
@staticmethod
def extractPlugin(pluginName):
pathToPlugin = pluginName + '.zip'
command = 'unzip -o ' + pathToPlugin + ' -d /usr/local/CyberCP'
result = subprocess.run(shlex.split(command), capture_output=True, text=True)
if result.returncode != 0:
raise Exception(f"Failed to extract plugin {pluginName}: {result.stderr}")
# Verify extraction succeeded
pluginPath = '/usr/local/CyberCP/' + pluginName
if not os.path.exists(pluginPath):
raise Exception(f"Plugin extraction failed: {pluginPath} does not exist after extraction")
2025-08-01 14:56:30 +05:00
@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')
2025-08-01 14:56:30 +05:00
for items in data:
if items.find("'emailPremium',") > -1:
writeToFile.writelines(items)
writeToFile.writelines(" '" + pluginName + "',\n")
else:
writeToFile.writelines(items)
writeToFile.close()
@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)
"""
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')
urlPatternAdded = False
2025-08-01 14:56:30 +05:00
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 not urlPatternAdded:
writeToFile.writelines(pluginInstaller.getUrlPattern(pluginName))
urlPatternAdded = True
2025-08-01 14:56:30 +05:00
writeToFile.writelines(items)
else:
writeToFile.writelines(items)
# Fallback: if 'plugins/' line not found, insert after 'manageservices'
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')
for items in data:
if items.find("manageservices") > -1:
writeToFile.writelines(items)
writeToFile.writelines(pluginInstaller.getUrlPattern(pluginName))
else:
writeToFile.writelines(items)
2025-08-01 14:56:30 +05:00
writeToFile.close()
@staticmethod
def informCyberPanel(pluginName):
pluginPath = '/home/cyberpanel/plugins'
if not os.path.exists(pluginPath):
os.mkdir(pluginPath)
pluginFile = pluginPath + '/' + pluginName
command = 'touch ' + pluginFile
subprocess.call(shlex.split(command))
@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')
2025-08-01 14:56:30 +05:00
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')
else:
writeToFile.writelines(items)
writeToFile.close()
@staticmethod
def staticContent():
currentDir = os.getcwd()
command = "rm -rf /usr/local/lscp/cyberpanel/static"
subprocess.call(shlex.split(command))
os.chdir('/usr/local/CyberCP')
command = "python3 /usr/local/CyberCP/manage.py collectstatic --noinput"
2025-08-01 14:56:30 +05:00
subprocess.call(shlex.split(command))
command = "mv /usr/local/CyberCP/static /usr/local/lscp/cyberpanel"
subprocess.call(shlex.split(command))
os.chdir(currentDir)
@staticmethod
def installMigrations(pluginName):
currentDir = os.getcwd()
os.chdir('/usr/local/CyberCP')
command = "python3 /usr/local/CyberCP/manage.py makemigrations %s" % pluginName
2025-08-01 14:56:30 +05:00
subprocess.call(shlex.split(command))
command = "python3 /usr/local/CyberCP/manage.py migrate %s" % pluginName
2025-08-01 14:56:30 +05:00
subprocess.call(shlex.split(command))
os.chdir(currentDir)
@staticmethod
def preInstallScript(pluginName):
pluginHome = '/usr/local/CyberCP/' + pluginName
if os.path.exists(pluginHome + '/pre_install'):
command = 'chmod +x ' + pluginHome + '/pre_install'
subprocess.call(shlex.split(command))
command = pluginHome + '/pre_install'
subprocess.call(shlex.split(command))
@staticmethod
def postInstallScript(pluginName):
pluginHome = '/usr/local/CyberCP/' + pluginName
if os.path.exists(pluginHome + '/post_install'):
command = 'chmod +x ' + pluginHome + '/post_install'
subprocess.call(shlex.split(command))
command = pluginHome + '/post_install'
subprocess.call(shlex.split(command))
@staticmethod
def preRemoveScript(pluginName):
pluginHome = '/usr/local/CyberCP/' + pluginName
if os.path.exists(pluginHome + '/pre_remove'):
command = 'chmod +x ' + pluginHome + '/pre_remove'
subprocess.call(shlex.split(command))
command = pluginHome + '/pre_remove'
subprocess.call(shlex.split(command))
@staticmethod
def installPlugin(pluginName):
try:
##
pluginInstaller.stdOut('Extracting plugin..')
pluginInstaller.extractPlugin(pluginName)
pluginInstaller.stdOut('Plugin extracted.')
##
pluginInstaller.stdOut('Executing pre_install script..')
pluginInstaller.preInstallScript(pluginName)
pluginInstaller.stdOut('pre_install executed.')
##
pluginInstaller.stdOut('Restoring settings file.')
pluginInstaller.upgradingSettingsFile(pluginName)
pluginInstaller.stdOut('Settings file restored.')
##
pluginInstaller.stdOut('Upgrading URLs')
pluginInstaller.upgradingURLs(pluginName)
pluginInstaller.stdOut('URLs upgraded.')
##
pluginInstaller.stdOut('Informing CyberPanel about plugin.')
pluginInstaller.informCyberPanel(pluginName)
pluginInstaller.stdOut('CyberPanel core informed about the plugin.')
##
pluginInstaller.stdOut('Adding interface link..')
pluginInstaller.addInterfaceLink(pluginName)
pluginInstaller.stdOut('Interface link added.')
##
pluginInstaller.stdOut('Upgrading static content..')
pluginInstaller.staticContent()
pluginInstaller.stdOut('Static content upgraded.')
##
if pluginInstaller.migrationsEnabled(pluginName):
pluginInstaller.stdOut('Running Migrations..')
pluginInstaller.installMigrations(pluginName)
pluginInstaller.stdOut('Migrations Completed..')
else:
pluginInstaller.stdOut('Migrations not enabled, add file \'enable_migrations\' to plugin to enable')
##
pluginInstaller.restartGunicorn()
##
pluginInstaller.stdOut('Executing post_install script..')
pluginInstaller.postInstallScript(pluginName)
pluginInstaller.stdOut('post_install executed.')
##
pluginInstaller.stdOut('Plugin successfully installed.')
except BaseException as msg:
pluginInstaller.stdOut(str(msg))
### Functions Related to plugin installation.
@staticmethod
def removeFiles(pluginName):
pluginPath = '/usr/local/CyberCP/' + pluginName
if os.path.exists(pluginPath):
try:
# Fix ownership and permissions before deletion to avoid permission errors
import stat
import pwd
import grp
# Get cyberpanel user/group IDs
try:
cyberpanel_uid = pwd.getpwnam('cyberpanel').pw_uid
cyberpanel_gid = grp.getgrnam('cyberpanel').gr_gid
except (KeyError, OSError):
# Fallback to root if cyberpanel user doesn't exist
cyberpanel_uid = 0
cyberpanel_gid = 0
# Recursively fix ownership and permissions
def fix_permissions(path):
for root, dirs, files in os.walk(path):
try:
os.chown(root, cyberpanel_uid, cyberpanel_gid)
os.chmod(root, stat.S_IRWXU | stat.S_IRWXG | stat.S_IROTH | stat.S_IXOTH)
except (OSError, PermissionError) as e:
pluginInstaller.stdOut(f"Warning: Could not fix permissions for {root}: {str(e)}")
for d in dirs:
dir_path = os.path.join(root, d)
try:
os.chown(dir_path, cyberpanel_uid, cyberpanel_gid)
os.chmod(dir_path, stat.S_IRWXU | stat.S_IRWXG | stat.S_IROTH | stat.S_IXOTH)
except (OSError, PermissionError) as e:
pluginInstaller.stdOut(f"Warning: Could not fix permissions for {dir_path}: {str(e)}")
for f in files:
file_path = os.path.join(root, f)
try:
os.chown(file_path, cyberpanel_uid, cyberpanel_gid)
os.chmod(file_path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH)
except (OSError, PermissionError) as e:
pluginInstaller.stdOut(f"Warning: Could not fix permissions for {file_path}: {str(e)}")
# Fix permissions before deletion
fix_permissions(pluginPath)
# Now remove the directory
shutil.rmtree(pluginPath)
except Exception as e:
pluginInstaller.stdOut(f"Error removing plugin files: {str(e)}")
# Try alternative: use system rm -rf as fallback
try:
import subprocess
result = subprocess.run(['rm', '-rf', pluginPath], capture_output=True, text=True, timeout=30)
if result.returncode != 0:
raise Exception(f"rm -rf failed: {result.stderr}")
except Exception as e2:
raise Exception(f"Failed to remove plugin directory: {str(e)} (fallback also failed: {str(e2)})")
2025-08-01 14:56:30 +05:00
@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')
2025-08-01 14:56:30 +05:00
for items in data:
if items.find(pluginName) > -1:
continue
else:
writeToFile.writelines(items)
writeToFile.close()
@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')
2025-08-01 14:56:30 +05:00
for items in data:
if items.find(pluginName) > -1:
continue
else:
writeToFile.writelines(items)
writeToFile.close()
@staticmethod
def informCyberPanelRemoval(pluginName):
pluginPath = '/home/cyberpanel/plugins'
pluginFile = pluginPath + '/' + pluginName
if os.path.exists(pluginFile):
os.remove(pluginFile)
@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')
2025-08-01 14:56:30 +05:00
for items in data:
if items.find(pluginName) > -1 and items.find('<li>') > -1:
continue
else:
writeToFile.writelines(items)
writeToFile.close()
@staticmethod
def removeMigrations(pluginName):
currentDir = os.getcwd()
os.chdir('/usr/local/CyberCP')
command = "python3 /usr/local/CyberCP/manage.py migrate %s zero" % pluginName
2025-08-01 14:56:30 +05:00
subprocess.call(shlex.split(command))
os.chdir(currentDir)
@staticmethod
def removePlugin(pluginName):
try:
##
pluginInstaller.stdOut('Executing pre_remove script..')
pluginInstaller.preRemoveScript(pluginName)
pluginInstaller.stdOut('pre_remove executed.')
##
if pluginInstaller.migrationsEnabled(pluginName):
pluginInstaller.stdOut('Removing migrations..')
pluginInstaller.removeMigrations(pluginName)
pluginInstaller.stdOut('Migrations removed..')
else:
pluginInstaller.stdOut('Migrations not enabled, add file \'enable_migrations\' to plugin to enable')
##
pluginInstaller.stdOut('Removing files..')
pluginInstaller.removeFiles(pluginName)
pluginInstaller.stdOut('Files removed..')
##
pluginInstaller.stdOut('Restoring settings file.')
pluginInstaller.removeFromSettings(pluginName)
pluginInstaller.stdOut('Settings file restored.')
###
pluginInstaller.stdOut('Upgrading URLs')
pluginInstaller.removeFromURLs(pluginName)
pluginInstaller.stdOut('URLs upgraded.')
##
pluginInstaller.stdOut('Informing CyberPanel about plugin removal.')
pluginInstaller.informCyberPanelRemoval(pluginName)
pluginInstaller.stdOut('CyberPanel core informed about the plugin removal.')
##
pluginInstaller.stdOut('Remove interface link..')
pluginInstaller.removeInterfaceLink(pluginName)
pluginInstaller.stdOut('Interface link removed.')
##
pluginInstaller.restartGunicorn()
pluginInstaller.stdOut('Plugin successfully removed.')
except BaseException as msg:
pluginInstaller.stdOut(str(msg))
####
@staticmethod
def restartGunicorn():
command = 'systemctl restart lscpd'
ProcessUtilities.normalExecutioner(command)
def main():
parser = argparse.ArgumentParser(description='CyberPanel Installer')
parser.add_argument('function', help='Specify a function to call!')
parser.add_argument('--pluginName', help='Temporary path to configurations data!')
args = parser.parse_args()
if args.function == 'install':
pluginInstaller.installPlugin(args.pluginName)
else:
pluginInstaller.removePlugin(args.pluginName)
if __name__ == "__main__":
main()