Mini Shell

Direktori : /opt/sharedrads/cms_tools/
Upload File :
Current File : //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

Zerion Mini Shell 1.0