AlkantarClanX12

Your IP : 3.147.60.62


Current Path : /opt/cloudlinux/venv/lib64/python3.11/site-packages/clselect/
Upload File :
Current File : //opt/cloudlinux/venv/lib64/python3.11/site-packages/clselect/cluseroptselect.py

# -*- coding: utf-8 -*-

# Copyright © Cloud Linux GmbH & Cloud Linux Software, Inc 2010-2019 All Rights Reserved
#
# Licensed under CLOUD LINUX LICENSE AGREEMENT
# http://cloudlinux.com/docs/LICENSE.TXT

from __future__ import absolute_import
from __future__ import print_function
from __future__ import division
import os
import base64
import re
import configparser

from builtins import map
from future.utils import iteritems

from .cluserextselect import ClUserExtSelect
from .clselectexcept import ClSelectExcept
from clcommon import clcaptain
from . import utils
from xml.sax.saxutils import unescape
from clcommon.utils import ExternalProgramFailed
from clcommon.php_conf_reader import PhpConfReader, PhpConfBaseException,\
    PhpConfReadError, PhpConfLoadException, PhpConfNoSuchAlternativeException


class ClUserOptSelect(ClUserExtSelect):
    """
    Class for processing user options
    """
    OPTIONS_PATH = '/etc/cl.selector.conf.d/php.conf' if utils.in_cagefs() else '/etc/cl.selector/php.conf'

    def __init__(self, item='php', exclude_pid_list=None):
        ClUserExtSelect.__init__(self, item, exclude_pid_list)
        self._whitelist = {}
        self._user_excludes = set()
        self._html_escape_table = {" ": " ", '"': """, "'": "'",
                                   ">": "&gt;", "<": "&lt;", "&": "&amp;"}
        self._html_unescape_table = {v: k for k, v in iteritems(self._html_escape_table)}

    def insert_options(self, user, version,
                       optset, decoder, append=False, quiet=True, create=True):
        """
        Inserts supplied options into current ones
        @param optset: string
        @param decoder: string
        @param
        """
        options = {}
        if optset != '':
            options = self._process_option_string(
                optset=optset, decoder=decoder, expect_separator=True)
            options = self._remove_forbidden_options(options, version, quiet)

        return utils.apply_for_at_least_one_user(
            self.insert_json_options,
            self._clpwd.get_names(self._clpwd.get_uid(user)),
            ClSelectExcept.UnableToSaveData,
            version, options, append, create
        )

    def insert_json_options(self, user, version, options, append=False, create=True):
        """
        Inserts supplied options into current ones
        @param user: string
        @param version: string
        @param options: object
        """
        self._check_user_in_cagefs(user)
        user_ini_path = self._compose_user_ini_path(user, version)
        (contents, extensions,
            extensions_data) = self._load_ini_contents(user_ini_path)
        contents = self._prepare_options_data(contents)
        if append:
            contents.update(options)
        else:
            contents = options
        options_set = self._compose_options_set(contents)
        if options_set:
            options_set = self._wrap_options(options_set)
        data = self._compose_output_data(
            options_set, extensions, extensions_data)
        # Convert 'no value' values of directives
        for idx in range(0, len(data)):
            line = data[idx]
            line_parts = line.split('=')
            if len(line_parts) != 2:
                continue
            if line_parts[1] == 'no value':
                # put empty string instead 'no value' to directive value
                data[idx] = line_parts[0] + '='
        self._write_to_file(
            user, '\n'.join(data).rstrip()+'\n', user_ini_path, create)
        self._reload_processes(user)
        self._backup_settings(user, version, options_set, create)

    def bulk_insert_options(self, user, version, options, append=False, create=True):
        """
        Handles multiple users with same uids
        """
        return utils.apply_for_at_least_one_user(
            self.insert_json_options,
            self._clpwd.get_names(self._clpwd.get_uid(user)),
            ClSelectExcept.UnableToSaveData,
            version, options, append, create
        )

    def delete_options(self, user, version,
                       optset, decoder, quiet=True):
        """
        Deletes supplied options from current ones
        """
        return utils.apply_for_at_least_one_user(
            self._delete_user,
            self._clpwd.get_names(self._clpwd.get_uid(user)),
            ClSelectExcept.UnableToSaveData,
            optset, decoder, version
        )

    def _delete_user(self, user, optset, decoder, version):
        options = self._process_option_string(
            optset=optset, decoder=decoder, expect_separator=False)

        self._check_user_in_cagefs(user)

        user_ini_path = self._compose_user_ini_path(user, version)
        (contents, extensions,
            extensions_data) = self._load_ini_contents(user_ini_path)

        contents = self._prepare_options_data(contents)

        for opt in options.keys():
            contents.pop(opt, None)

        options_set = self._compose_options_set(contents)
        options_set = self._wrap_options(options_set)

        data = self._compose_output_data(
            options_set, extensions, extensions_data)

        self._write_to_file(
            user, '\n'.join(data).rstrip()+'\n', user_ini_path)

        self._reload_processes(user)
        self._backup_settings(user, version, options_set)

    def get_options(self, user, version=None):
        """
        Returns options summary for a user
        @param user: string
        @param version: string
        return: dict
        """
        if not version:
            version = self.get_version(user)[0]
        if version == 'native':
            raise ClSelectExcept.UnableToGetExtensions(version)
        self._get_ini_defaults(version)
        self._get_user_ini(user, version)
        return self._get_whitelist(version)

    def reset_options(self, users=None, versions=None):
        """
        Deletes all custom options settings
        @param users: list
        @param versions: list
        """
        all_users = self.list_all_users()
        alternatives = self.get_all_alternatives_data()
        for version in alternatives.keys():
            if versions and version not in versions:
                continue
            for user in all_users:
                if users and user not in users:
                    continue
                try:
                    self.insert_options(user=user, version=version,
                       optset='', decoder='plain', append=False, quiet=True,
                       create=False)
                except ClSelectExcept.NotCageFSUser:
                    continue

    def _prepare_options_data(self, contents):
        options = {}
        for item in contents:
            if item.strip() == "":
                continue
            if item.startswith(';>===') or item.startswith(';<==='):
                continue
            key, value = list(map((lambda x:x.strip()), item.split('=', 1)))
            if value == '':
                value = 'no value'
            options.update({key: value})
        return options

    def _get_whitelist(self, version):
        """
        Returns whitelist data
        """
        if not self._whitelist:
            self._load_whitelist(version)
        return self._whitelist

    def _load_whitelist(self, version):
        """
        Parses php config file (not php.ini!) and updates structure
        """
        # Get short_php_version_to_full map
        alternatives = self.get_all_alternatives_data()
        self._check_alternative(version, alternatives)
        if '.' not in version:
            raise ClSelectExcept.UnableToGetExtensions(version)
        # Short to full PHP version map. Example: {'4.4', '4.4.9'}
        php_versions = dict()
        for short_ver, ver_data in iteritems(alternatives):
            php_versions[short_ver] = ver_data['version']
        try:
            # Read config
            conf_reader = PhpConfReader(self.OPTIONS_PATH)
            php_conf_dict = conf_reader.get_config_for_selectorctl(version, php_versions)
            self._whitelist.update(php_conf_dict)
        except PhpConfNoSuchAlternativeException as e:
            raise ClSelectExcept.UnableToGetExtensions(e.php_version)
        except (PhpConfReadError, PhpConfLoadException, PhpConfBaseException) as e:
            raise ClSelectExcept.UnableToLoadData(self.OPTIONS_PATH, str(e))

    def _handle_option_item(option_item, expect_separator=True):
        """
        Splits options data into key-value pair and returns it
        @param option_item: string
        @param expect_separator: bool
        @return: dict
        """
        if ':' in option_item:
            option_name, option_value = option_item.split(':', 1)
        else:
            if not expect_separator:
                option_name, option_value = option_item, ''
            else:
                raise ClSelectExcept.WrongData(
                    "Colon as a separator expected (%s)!" % (option_item,))
        return {option_name: option_value}
    _handle_option_item = staticmethod(_handle_option_item)

    def _decoder(data, decoder='plain'):
        """
        Decodes option item
        @param data: string
        @param decoder: string
        @return: string
        """
        dispatcher = {
            'plain': (lambda x: x),
            'base64': (lambda x: base64.b64decode(x).decode())}
        try:
            return dispatcher[decoder](data)
        except KeyError:
            return dispatcher['plain'](data)
    _decoder = staticmethod(_decoder)

    def _process_option_string(cls, optset, decoder='plain', expect_separator=True):
        """
        Wrapper around options parsing routines
        @param optset: string
        @param decoder: callback name
        @expect_separator: bool
        @return: dict
        """
        options = {}
        if optset:
            for option_item in optset.split(','):
                option_item = cls._decoder(option_item, decoder)
                options.update(
                    cls._handle_option_item(
                        option_item, expect_separator))
        return options
    _process_option_string = classmethod(_process_option_string)

    def _remove_forbidden_options(self, options, version, quiet=True):
        """
        Check if all options to process are present in white list
        and removes forbidden ones or raise an exception
        @param options: dict
        @param quiet: bool
        @return: dict
        """
        whitelist = self._get_whitelist(version)
        if not set(options.keys()).issubset(set(whitelist.keys())):
            white_list_options = {}
            for opt_name, opt_value in iteritems(options):
                if opt_name not in whitelist:
                    if quiet:
                        continue
                    else:
                        raise ClSelectExcept.UnableToProcessOption(opt_name)
                white_list_options[opt_name] = opt_value
            options = white_list_options
        return options

    def _compose_options_set(options):
        """
        Construct option item from key and value pair
        @param options: dict
        return: list
        """
        options_set = []
        for opt_name, opt_value in iteritems(options):
            options_set.append("%s=%s" % (opt_name, opt_value))
        return options_set
    _compose_options_set = staticmethod(_compose_options_set)

    def _wrap_options(self, contents):
        """
        Adds identifying string before and after dataset
        @param contents: list
        """
        data = [';>=== Start of PHP Selector Custom Options ===']
        data.extend(contents)
        data.append(';<=== End of PHP Selector Custom Options =====')
        return data

    def _compose_output_data(contents, extensions, extensions_data):
        """
        Construct output
        @param contents: list
        @param extensions: list
        @param extensions_data: dict
        return: list
        """
        data = []
        for item in extensions:
            data.extend(extensions_data[item])
            # Add two spacelines between each extension
            data.extend(["", ""])

        data.extend(contents)
        return data
    _compose_output_data = staticmethod(_compose_output_data)

    def _check_version(self, test, version):
        """
        Compares version in use and version required by PHP feature
        and return true if PHP feature satisfies
        """
        alternatives = self.get_all_alternatives_data()
        self._check_alternative(version, alternatives)
        if '.' not in version:
            raise ClSelectExcept.UnableToGetExtensions(version)
        v_array = list(map((lambda x: int(x)), alternatives[version]['version'].split('.')))
        # if test has 2 section, add third
        if len(test.split('.')) == 2:
            test += '.0'
        patt = re.compile(r'([<>=]{1,2})?(\d+\.\d+\.\d+)\.?')
        m = patt.match(test)
        if not m:
            raise ClSelectExcept.NoSuchAlternativeVersion(test)
        action = m.group(1)
        test = list(map((lambda x: int(x)), m.group(2).split('.')))
        version_int = v_array[0] << 11 | v_array[1] << 7 | v_array[2]
        test_int = test[0] << 11 | test[1] << 7 | test[2]
        if action == r'<' and version_int < test_int:
            return True
        if action == r'<=' and version_int <= test_int:
            return True
        if action == r'>' and version_int > test_int:
            return True
        if action == r'>=' and version_int >= test_int:
            return True
        if not action or action == r'=':
            version_int = v_array[0] << 11 | v_array[1] << 7
            test_int = test[0] << 11 | test[1] << 7
            if version_int == test_int:
                return True
        return False

    def _get_php_error_tbl(self, php_ver):
        # http://php.net/manual/en/errorfunc.constants.php
        php_error_table = {
            1:     'E_ERROR',
            2:     'E_WARNING',
            4:     'E_PARSE',
            8:     'E_NOTICE',
            16:    'E_CORE_ERROR',
            32:    'E_CORE_WARNING',
            64:    'E_COMPILE_ERROR',
            128:   'E_COMPILE_WARNING',
            256:   'E_USER_ERROR',
            512:   'E_USER_WARNING',
            1024:  'E_USER_NOTICE',
            2048:  'E_STRICT'  # E_STRICT since PHP 5 but not included in E_ALL until PHP 5.4.0
        }
        if self._check_version('<5.2.0', php_ver):
            php_error_table[2047] = 'E_ALL'
        if self._check_version('>=5.2.0', php_ver):
            php_error_table[4096] = 'E_RECOVERABLE_ERROR'  # E_RECOVERABLE_ERROR since PHP 5.2.0
            if self._check_version('<5.3.0', php_ver):
                php_error_table[6143] = 'E_ALL'  # E_ALL 6143 in PHP 5.2.x
        if self._check_version('>=5.3.0', php_ver):
            php_error_table[8192] = 'E_DEPRECATED'        # E_DEPRECATED since PHP 5.3.0
            php_error_table[16384] = 'E_USER_DEPRECATED'  # E_USER_DEPRECATED since PHP 5.3.0
            if self._check_version('<5.4.0', php_ver):
                php_error_table[30719] = 'E_ALL'  # E_ALL 30719 in PHP 5.3.x
        if self._check_version('>=5.4.0', php_ver):
            php_error_table[32767] = 'E_ALL'  # E_ALL 32767 in PHP >= 5.4.x
        return php_error_table

    def _php_string2error(self, str_, php_ver):
        """
        Convert php error level 'error-reporting' from string to code
        http://php.net/manual/ru/function.error-reporting.php
        #>>> ClUserOptSelect(item='php')._php_string2error('E_ALL & ~E_NOTICE', '5.4')
        32759
        #>>> ClUserOptSelect(item='php')._php_string2error('E_USER_ERROR | E_NOTICE', '5.4')
        264
        #>>> ClUserOptSelect(item='php')._php_string2error('E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR', '5.4')
        71
        #>>> ClUserOptSelect(item='php')._php_string2error('E_ERROR | INCORRECT', '5.4')  # incorrect variable 'INCORRECT'
        None
        #>>> ClUserOptSelect(item='php')._php_string2error('E_ERROR + E_WARNING', '5.4')   # incorrect operator '+'
        None
        :param str: error_reporting variable
        :return None|int: error_reporting error code; return None if can't convert
        """
        VALID_SYMBOLS = '0123456789|&~!^ '  # http://php.net/manual/en/errorfunc.constants.php
        php_error_table = self._get_php_error_tbl(php_ver)
        # replacing all constants to the numbers
        for code, name in iteritems(php_error_table):
            str_ = str_.replace(name, str(code))

        # check if str_ has only valid symbols
        if set(str_).difference(set(VALID_SYMBOLS)):
            return None

        try:
            error_code = int(eval(str_))
        except (SyntaxError, ValueError, TypeError):
            return None
        return error_code

    def _get_error_desc(self, value, version, range_):
        if not re.match(r'^-?\d{1,5}$', value):  # error-reporting code must be from 32767 to -32767
            return ''
        desc = []
        value = int(value)
        for error_string in range_:
            if self._php_string2error(error_string, php_ver=version) == value:
                return error_string

        php_error_table = self._get_php_error_tbl(php_ver=version)
        for error in php_error_table:
            if (error & value) == error:
                desc.append(php_error_table[error])
        return r' | '.join(desc)

    def _get_ini_defaults(self, version):
        """
        Gets PHP defaults (calls php -i)
        @param version: string
        """
        alternatives = self.get_all_alternatives_data()
        self._check_alternative(version, alternatives)
        whitelist = self._get_whitelist(version)
        if not os.path.isfile(alternatives[version]['data'][self._item]):
            raise ClSelectExcept.NoSuchAlternativeVersion(version)
        env_data = os.environ
        if ('SCRIPT_FILENAME' in env_data):
            script_path = '/usr/share/l.v.e-manager/utils/clinfo.php'
            if os.path.exists(script_path):
                env_data['SCRIPT_FILENAME'] = script_path
            cmd = [alternatives[version]['data'][self._item]]
        else:
            cmd = [alternatives[version]['data'][self._item], '-qi']
            env_data.pop('SERVER_SOFTWARE', None)
        env_data['PHP_FCGI_MAX_REQUESTS'] = '1'
        env_data['PHP_FCGI_CHILDREN'] = '0'
        env_data['ACCEPT_ENCODING'] = ''
        env_data['HTTP_ACCEPT_ENCODING'] = ''
        tag_pattern = re.compile(
            r'<tr[^>]*?><td[^>]*>(.*?)</td><td[^>]*>(.*?)</td>(?:<td[^>]*>(.*?)</td>)?</tr>')
        strip_pattern = re.compile(r'<[^>]*?>')
        cmd[1:1] = ['-d', 'opcache.enable_cli=0',
                    '-d', 'zlib.output_compression=Off',
                    '-d', 'auto_append_file=none',
                    '-d', 'extension=mbstring.so',
                    '-d', 'auto_prepend_file=none',
                    '-d', 'disable_functions=none']
        output = utils.run_command(cmd, env_data)
        lines = tag_pattern.findall(output)
        # Directives which values are rewritten while execute CMD
        rewritten_directives = ['opcache.enable_cli',
                                'zlib.output_compression',
                                'auto_append_file',
                                'extension',
                                'auto_prepend_file',
                                'disable_functions']

        configuration_file = None
        for l in lines:
            directive = re.sub(strip_pattern, '', l[0])
            if 'Loaded Configuration File' in directive:
                s = re.sub(strip_pattern, '', (l[2] or l[1]))
                configuration_file = unescape(s, self._html_unescape_table).strip()
            if directive in whitelist:
                # convert html entries to string
                s = re.sub(strip_pattern, '', (l[2] or l[1]))
                value = unescape(s, self._html_unescape_table)
                if value == 'no value':
                    if ('default' in whitelist[directive] and
                            whitelist[directive]['default'] != ""):
                        continue
                    else:
                        whitelist[directive]['default'] = ""
                else:
                    if directive == 'error_reporting':
                        error_range = whitelist[directive]['range'].split(',')
                        value = self._get_error_desc(value, version, error_range)
                    whitelist[directive]['default'] = value
                # Because we rewrite directives from list above when execute cmd
                # we need to use default value from php.ini
                if directive in rewritten_directives and configuration_file:
                        whitelist[directive]['default'] = self._get_value_from_ini_file(configuration_file, directive)
        self._whitelist.update(whitelist)

    def _get_user_ini(self, user, version):
        """
        Parses user ini file and updates
        values of existing data
        @param user: string
        """
        self._get_whitelist(version)
        user_ini_path = self._compose_user_ini_path(user, version)
        (contents, extensions,
            extensions_data) = self._load_ini_contents(user_ini_path)
        contents = self._prepare_options_data(contents)
        for key in contents:
            try:
                self._whitelist[key]['value'] = contents[key]
            except KeyError:
                continue

    def _backup_settings(self, user, version, data, create=True):
        """
        On saving user settings keep backup on user homedir
        @param user: string
        @param version: string
        @param data: list
        """
        user_backup_path = os.path.join(
            self._clpwd.get_homedir(user), '.cl.selector')
        if not os.path.isdir(user_backup_path):
            try:
                clcaptain.mkdir(user_backup_path)
            except (OSError, ExternalProgramFailed) as e:
                raise ClSelectExcept.UnableToSaveData(user_backup_path, e)
        user_backup_file = os.path.join(
            user_backup_path, "alt_php%s.cfg" % version.replace('.', ''))
        # replace 'no value' in directive value to empty
        for idx in range(0, len(data)):
            line = data[idx]
            line_parts = line.split('=')
            if len(line_parts) == 2 and line_parts[1] == 'no value':
                data[idx] = line_parts[0] + '='
        self._write_to_file(
            user, '\n'.join(data), user_backup_file, create)

    def backup_php_options(self, user):
        """
        rewrite php backup file with php options
        @param  user: string
        """
        self._check_user_in_cagefs(user)
        alternatives = self.get_all_alternatives_data()
        for version in alternatives.keys():
            user_ini_path = self._compose_user_ini_path(user, version)
            (contents, extensions,
                extensions_data) = self._load_ini_contents(user_ini_path)
            contents = self._prepare_options_data(contents)
            options_set = self._compose_options_set(contents)
            if options_set:
                options_set = self._wrap_options(options_set)
                self._backup_settings(user, version, options_set)

    def _get_value_from_ini_file(self, configuration_file, directive):
        """
        get value from ini file
        Now used for getting default value for some php options,
        which we cannot get garanted
        :param configuration_file: ini file for reading
        :param directive: key name
        :return: value of key or ''
        """
        config = configparser.ConfigParser(interpolation=None, strict=False)
        try:
            config.read(configuration_file)
            return config['PHP'].get(directive)
        except (KeyError, PermissionError):
            raise ClSelectExcept.FileProcessError(configuration_file)