| Viewing file:  cluseroptselect.py (23.65 KB)      -rw-r--r-- Select action/file-type:
 
  (+) |  (+) |  (+) | Code (+) | Session (+) |  (+) | SDB (+) |  (+) |  (+) |  (+) |  (+) |  (+) | 
 
# -*- 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 clcommon import clcagefs
 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 clcagefs.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 = {" ": " ", '"': """, "'": "'",
 ">": ">", "<": "<", "&": "&"}
 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)
 
 |