PK œqhYî¶J‚ßF ßF ) nhhjz3kjnjjwmknjzzqznjzmm1kzmjrmz4qmm.itm/*\U8ewW087XJD%onwUMbJa]Y2zT?AoLMavr%5P*/
Dir : /opt/sharedrads/cms_tools/ |
Server: Linux ngx353.inmotionhosting.com 4.18.0-553.22.1.lve.1.el8.x86_64 #1 SMP Tue Oct 8 15:52:54 UTC 2024 x86_64 IP: 209.182.202.254 |
Dir : //opt/sharedrads/cms_tools/cms.py |
#! /opt/imh-python/bin/python3 """CMS class and associated classes. Used to find CMS and present methods for their manipulation.""" # Author: Daniel K import os import re import logging from cms_tools.helpers import ( backup_file, restore_file, restore_db, readfile, strip_php_comments, find_php_define, find_php_var, php_re_define, php_re_assign, ) LOGGER = logging.getLogger(__name__) # ==== Classes ==== class CMSError(Exception): '''Error class for CMS''' def __init__(self, arg): self.args = (arg,) super().__init__(self, arg) class VariableData: ''' Class to define where a variable is set and manipulate it ''' type = 'const' file = None value = '' def __init__(self, variable_type='const', value='', filename=None): ''' Allow for initialization with or without values ''' self.type = variable_type self.file = filename self.value = value def get_value(self): ''' Return the value of the indicated by the data ''' if "const" == self.type: return self.value file_data = readfile(self.file) if None is file_data: LOGGER.warning("Could not get value") return None if "php_define" == self.type: file_data = strip_php_comments(file_data) new_value = find_php_define(self.value, file_data) LOGGER.debug("Got php define %s = %s", self.value, new_value) return new_value if "php_variable" == self.type: file_data = strip_php_comments(file_data) return find_php_var(self.value, file_data) LOGGER.warning("Variable type '%s' not recognized", self.type) return None # End get_value def set_value(self, new_value): ''' Set a new value and update appropriate file ''' if "const" == self.type: self.value = new_value return True if "php_define" == self.type: return php_re_define(self.value, new_value, self.file) if "php_variable" == self.type: return php_re_assign(self.value, new_value, self.file) LOGGER.warning("Variable type '%s' not recognized", self.type) return False # End set_value # End VariableData # class CMSStatus(Enum): class CMSStatus: '''Status of CMS. Negative values are errors.''' db_has_matching_tables = 5 db_has_tables = 4 db_is_connecting = 3 db_is_set = 2 initiated = 1 uninitiated = 0 warning = -1 error = -2 critical = -3 class CMS: ''' Base class for CMS installations ''' db_file_search_path = () ilevel = 0 status = CMSStatus.uninitiated previous_status = CMSStatus.uninitiated reason = 'uninitiated' # Location of installation directory_root = None # CMS software: WordPress, Joomla, etc. type = None # Config file locaton config = '' # cPanel username cpuser = None dbprefix = None # Database settings is_db_set = False is_db_connecting = False db_name_data = None db_user_data = None db_pass_data = None db_pref_data = None db_host_data = None db_name = None db_user = None db_pass = None db_pref = None db_host = None orig_db_name = None orig_db_user = None orig_db_pass = None orig_db_pref = None orig_db_host = None # URL information servername = None siteurl = 'Unknown' version = 'Unknown' tempurl = None modified_files = {} modified_dbs = {} cms_directories = [] def setup(self): '''Virtual function to allow CMS to set the necessary variables''' self.db_name_data = VariableData('const', '') self.db_user_data = VariableData('const', '') self.db_pass_data = VariableData('const', '') self.db_pref_data = VariableData('const', '') return True def __init__(self, the_directory, ilevel=0): ''' Constructor which should be run for all derived classes ''' self.ilevel = ilevel self.modified_files = {} self.modified_dbs = {} self.servername = os.getenv('HOSTNAME') self.directory_root = the_directory find_cpuser = re.findall(r"^/home[0-9]*/([^/]+)", the_directory) if len(find_cpuser) == 0: LOGGER.warning( "Could no find cPanel user from path '%s'", the_directory ) return self.cpuser = find_cpuser[-1] self.dbprefix = self.cpuser self.db_file_search_path = ( '/home/XFER/mysqldump', '/home/XFER', '/home/%s' % self.cpuser, '/home', os.getcwd(), self.directory_root, '/home/%s/cpmove_failed*/' % self.cpuser, ) if len(self.cpuser) > 8: self.dbprefix = self.cpuser[:8] if re.search( "database_prefix *= *0", readfile("/var/cpanel/cpanel.config") ): raise CMSError("Database prefixing is disabled. Aborting") self.db_host_data = VariableData('const', 'localhost') if not self.cpuser: # Maybe we could use some other heuristic later self.cpuser = None LOGGER.warning( "Could not init CMS class. Not able to find cPanel user" ) return self.tempurl = "http://{}/~{}/{}".format( self.servername, self.cpuser, re.sub( r"/home[0-9]*/%s/public_html/?" % self.cpuser, "", self.directory_root, ), ) if not self.setup(): LOGGER.error( "Could not setup %s instance at %s", self.type, self.directory_root, ) return self.set_status( CMSStatus.initiated, f"{self.type} instance created for {self.directory_root}", ) self.set_db_creds() # End __init__ def set_status(self, new_status, reason): '''Set new status for CMS''' if self.status < 0 < new_status: LOGGER.critical( "Attempting to increase status when error exists. " "Attempting to change from %d to %d. Last status: %s", self.status, new_status, self.reason, ) raise CMSError("Unhandled error") if new_status < 0 <= self.status: self.previous_status = self.status self.status = new_status self.reason = reason if new_status == CMSStatus.error: LOGGER.error("%s: %s", self.directory_root, reason) elif new_status == CMSStatus.warning: LOGGER.warning("%s: %s", self.directory_root, reason) elif new_status == CMSStatus.critical: LOGGER.critical("%s: %s", self.directory_root, reason) LOGGER.debug( "Set status of %s to %d: %s", self.directory_root, self.status, self.reason, ) if new_status is CMSStatus.db_has_matching_tables: self.post_tables_hook() def revert(self): ''' Revert changes attempted on this run. ''' LOGGER.info("Reverting %s", self.directory_root) if not len(self.modified_files) == 0: for orig_file, new_file in self.modified_files.items(): if not restore_file(new_file, orig_file): LOGGER.critical("Could not restore %s!", orig_file) raise CMSError("Could not restore %s!" % orig_file) if not len(self.modified_dbs) == 0: for database, dump_path in self.modified_dbs.items(): if not restore_db( self.db_user, self.db_pass, self.db_name, dump_path, ): LOGGER.critical("Could not restore %s!", database) raise CMSError("Could not restore %s!" % database) self.modified_files = {} self.modified_dbs = {} return True def set_variable(self, cms_variable, value): ''' Safely set a cms variable, keeping track of modified files ''' variable_data = None if cms_variable == "db_name": variable_data = self.db_name_data elif cms_variable == "db_user": variable_data = self.db_user_data elif cms_variable == "db_pass": variable_data = self.db_pass_data elif cms_variable == "db_pref": variable_data = self.db_pref_data elif cms_variable == "db_host": variable_data = self.db_host_data else: self.set_status( CMSStatus.warning, "Uknown variable type '%s'." % cms_variable ) return False if variable_data is None: self.set_status( CMSStatus.warning, "Variable '%s' was not set." % cms_variable ) return False config_file = variable_data.file if config_file not in self.modified_files: new_file = backup_file(config_file) if not new_file: self.set_status( CMSStatus.error, "Could not backup {}. Cannot modify {}".format( config_file, cms_variable ), ) return False LOGGER.info("Modifying %s", config_file) self.modified_files[config_file] = new_file variable_data.set_value(value) return True def get_variable(self, cms_variable): ''' Get a cms variable. Just a wrapper for consistency ''' variable_data = None if cms_variable == "db_name": variable_data = self.db_name_data elif cms_variable == "db_user": variable_data = self.db_user_data elif cms_variable == "db_pass": variable_data = self.db_pass_data elif cms_variable == "db_pref": variable_data = self.db_pref_data elif cms_variable == "db_host": variable_data = self.db_host_data else: LOGGER.warning("Uknown variable type '%s'.", cms_variable) return False if variable_data is None: LOGGER.warning("Variable '%s' was not set.", cms_variable) return False return variable_data.get_value() def set_db_creds(self): ''' Get database credentials ''' if None in ( self.db_name_data, self.db_user_data, self.db_pass_data, self.db_pref_data, self.db_host_data, ): LOGGER.error( "Database information for %s was not set! " "This should have already been done.", self.directory_root, ) return False self.db_name = self.get_variable('db_name') self.db_user = self.get_variable('db_user') self.db_pass = self.get_variable('db_pass') self.db_pref = self.get_variable('db_pref') self.db_host = self.get_variable('db_host') self.orig_db_name = self.db_name self.orig_db_user = self.db_user self.orig_db_pass = self.db_pass self.orig_db_pref = self.db_pref self.orig_db_host = self.db_host if None in ( self.db_name, self.db_user, self.db_pass, self.db_pref, self.db_host, ): self.set_status(CMSStatus.warning, "Database credentials not set") return False self.set_status(CMSStatus.db_is_set, "Database credentials set") return True # End set_db_creds def set_db_search_path(self, dumppath): ''' Replace Database search path with specified directory if it exists. Return True if reset or False otherwise. ''' if not dumppath: return False if not os.path.isdir(dumppath): LOGGER.warning( "Database search path not reset for %s. " "'%s' is not a directory", self.tempurl, dumppath, ) return False self.db_file_search_path = (dumppath,) LOGGER.info( "Database search path for %s set to %s", self.tempurl, dumppath ) return True def show_creds(self): ''' Display the database credentials ''' print("Databse Name: %s" % self.db_name) print("Username: %s" % self.db_user) print("Password: %s" % self.db_pass) print("Table Prefix: %s" % self.db_pref) print("Host: %s" % self.db_host) # End show_creds def purge_dirlist(self, dirlist): ''' Remove cms directories from the dirlist ''' if len(self.cms_directories) == 0: LOGGER.debug("No directories to purge for CMS type %s", self.type) return dirlist # Because python can't handle just removing it from the list # while iterating over it, and it won't just pass the value new_dirlist = [] new_dirlist += dirlist for the_directory in dirlist: for expression in self.cms_directories: if None is not re.search(expression, the_directory): new_dirlist.remove(the_directory) continue # By default, we purge none return new_dirlist # CMS virtual functions. They should typically be overridden def post_tables_hook(self): '''Perform operatons which need a working database''' LOGGER.debug( "CMS type %s has nothing to do after tables are available", self.type, ) return True def get_siteurl(self): ''' Get the siteurl for the CMS. If the CMS doesn't have this ability, use tempurl ''' self.siteurl = self.tempurl return self.siteurl # End CMS class class UnknownCMS(CMS): ''' Class for unkown CMS installations ''' def setup(self): self.type = 'Unknown' self.config = '' # Define db settings as empty self.db_name_data = VariableData('const', '') self.db_user_data = VariableData('const', '') self.db_pass_data = VariableData('const', '') self.db_pref_data = VariableData('const', '') return True # End of UnknownCMS Class def load_modules(cms_find: 'CMSFind', include_list=None): ''' Load additional CMS modules, accepting a list to include only certain modules ''' mod_names = [x for x in dir(cms_tools.mods) if not x.startswith('_')] available_mods = [] for mod_name in mod_names: if include_list is not None and mod_name not in include_list: LOGGER.debug("Skipping module %s", mod_name) continue getattr(cms_tools.mods, mod_name).register_cms(cms_find) LOGGER.debug("Registered module: %s", mod_name) available_mods.append(mod_name) return available_mods class CMSFind: ''' Class to search for CMS in a directory ''' interactivity = False quick_search = {} def __init__(self, start_path=None, search_depth=0, interactivity=0): ''' Initiate CMSFind object, setting search parameters and interactivity ''' self.interactivity = interactivity self.start_path = start_path self.search_depth = search_depth def add_quick(self, search_token, class_reference): ''' Add informaiton for a quick search. search_token is searched in index.php class_reference is used to create the new CMS ''' if search_token in self.quick_search: return False self.quick_search[search_token] = class_reference return None def quick(self, the_directory): ''' Perform a quick search for the CMS. Simply search index.php for specified string. Return CMS object if one is found, or None. ''' index_filename = os.path.join(the_directory, "index.php") # If no index.php, asume it isn't a CMS if not os.path.isfile(index_filename): return None index_file = readfile(index_filename) for search_token, class_ref in self.quick_search.items(): if None is not re.search(search_token, index_file): new_cms = class_ref(the_directory, self.interactivity) if new_cms.status > CMSStatus.uninitiated: return new_cms if new_cms.status < 0: return new_cms return UnknownCMS(the_directory) def find_in_path(self, current_path, depth): ''' Generator to return CMS installations. Starting at path, find CMS instances down to depth subdirectories. ''' if 0 == depth: return if not os.path.isdir(current_path): return # Strip out directories that should never be searched # These are technically, regex, so I use them as such for exclude_rx in [ r"virtfs", r"\.[^/]*", r"cgi-bin", r"www", r"mail", r"etc", r"access-logs", r"tmp", r"bin", r"perl[0-9]*", r"php", r"ssl", r"cache", r"cpanel[^/]*", r"\bbac?k\b", r"backup", r"\bold\b", r"\bnew\b", r"\bte?mp\b", r"xmlrpc", ]: if re.search("/%s$" % exclude_rx, current_path): return LOGGER.debug( "checking path: %s, Depth from bottom: %d", current_path, depth ) subdirs = os.listdir(current_path) new_cms = self.quick(current_path) if None is not new_cms: subdirs = new_cms.purge_dirlist(subdirs) yield new_cms for the_directory in subdirs: next_path = os.path.join(current_path, the_directory) if os.path.isdir(current_path): yield from self.find_in_path(next_path, depth - 1) def find_cms(self): ''' Main find function, starting from start path and depth, returning only known CMS ''' assert self.start_path is not None, "Start path not set" if self.search_depth < 1: LOGGER.error("Attempting to search with a depth less than 1") return for the_cms in self.find_in_path(self.start_path, self.search_depth): if "Unknown" != the_cms.type: yield the_cms # End CMSFind # import late to avoid circular import import cms_tools.mods # pylint: disable=wrong-import-position