Mini Shell
# -*- coding: utf-8 -*-
# Copyright © Cloud Linux GmbH & Cloud Linux Software, Inc 2010-2018 All Rights Reserved
#
# Licensed under CLOUD LINUX LICENSE AGREEMENT
# http://cloudlinux.com/docs/LICENSE.TXT
#
import copy
import re
class PhpConfBaseException(Exception):
"""Base class for all php.conf exceptions"""
def __init__(self, message):
super().__init__(message)
class PhpConfReadError(PhpConfBaseException):
def __init__(self, message=None):
if message is None:
message = "File open/read error"
super().__init__(message)
class PhpConfLoadException(PhpConfBaseException):
def __init__(self, line_num, reason):
message = f"Error at line {line_num}: {reason}. Please see " \
"http://docs.cloudlinux.com/index.html?custom_php_ini_options.html"
super().__init__(message)
class PhpConfNoSuchAlternativeException(PhpConfBaseException):
def __init__(self, version):
self.php_version = version
message = f'No such alternative version ({version})'
super().__init__(message)
# For unit tests
def _open(file_name):
return open(file_name, mode='rb')
class PhpConfReader:
"""
Class for read and parse /etc/cl.selector/php.conf
During read this file, its syntax check is performed
Contains methods for get its data for selectoctl and cagefsctl utilities in required formats
"""
DIRECTIVES_LIST = ['Directive', 'Default', 'Type', 'Comment', 'Range', 'Remark']
TYPES_LIST = ['value', 'list', 'bool']
def __init__(self, phpconf_path):
self.phpconf_path = phpconf_path
# /etc/cl.selector/php.conf contents as dictionary
# Example:
# { 'safe_mode': {'Default': 'Off', 'Comment': 'Enables PHP safe mode.',
# 'Remark': '<5.4.0', 'Type': 'bool'},
# 'file_uploads': {'Default': 'On', 'Comment': 'Allows uploading files over HTTP.',
# 'Type': 'bool'}
# }
self._php_conf = None
def _read_file(self):
"""
Reads file and returns its content
:return: List of filelines
"""
try:
f = _open(self.phpconf_path)
conf_lines = f.readlines()
f.close()
return conf_lines
except (OSError, IOError) as e:
# Open/read error
raise PhpConfReadError(e.strerror) from e
def _php_conf_load(self):
data = {}
conf_lines = self._read_file()
try:
directive = None
current_section = None
# Analyze lines
for line_num, line in enumerate(conf_lines, start=1):
# line example: 'Directive = upload_max_filesize'
line = line.strip().decode('utf-8', "ignore")
# pass comments
if line.startswith('#'):
continue
# Empty line - separator between directives
if len(line) == 0:
directive = None
current_section = None
continue
if '=' not in line:
# Separator = not found in line - error
raise PhpConfLoadException(line_num, "Required separator = not found")
line_parts = line.split('=', 1)
directive_name = line_parts[0].strip()
directive_value = line_parts[1].strip()
if directive_name not in self.DIRECTIVES_LIST:
# Invalid directive
raise PhpConfLoadException(line_num, f"Unknown directive '{directive_name}' found")
if not directive and directive_name != 'Directive':
# No current directive and some other directive found - error
raise PhpConfLoadException(line_num, f"'{directive_name}' found, but no 'Directive' before it")
if directive_name == 'Directive':
if directive:
# 'Directive' duplicate
raise PhpConfLoadException(line_num, f"Directive '{directive_name}' duplicate")
# Save current directive name and create placeholder for diirective data
directive = directive_value
current_section = directive_value
data[current_section] = {}
else:
if directive_name in data[current_section]:
# Directive already present
raise PhpConfLoadException(line_num, f"Directive '{directive_name}' duplicate")
if directive_name == 'Type' and directive_value not in self.TYPES_LIST:
raise PhpConfLoadException(line_num,
f"Directive is Type, but it value '{directive_value}' is invalid")
data[current_section][directive_name] = directive_value
# Successfull end
self._php_conf = data
except PhpConfLoadException as e:
# Parsing error
raise e
except Exception as e:
raise PhpConfBaseException(e) from e
def get_config_for_selectorctl(self, php_version, php_versions_map):
"""
Retrives php.conf for selectorctl needs
:param php_version: PHP version to filter options
:param php_versions_map: Short to full PHP version map. Example: {'4.4', '4.4.9'}
:return: dict
Example:
{'file_uploads': {'default': 'On',
'comment': 'Allows uploading files over HTTP.',
'type': 'bool'},
'max_execution_time': {'default': '30',
'comment': 'The maximum time in seconds a script is allowed to run '
'before it is terminated.',
'type': 'value'},
}
"""
# if config not loaded - load it
if not self._php_conf:
self._php_conf_load()
out_dict = {}
for directive_name, directive_data in self._php_conf.items():
# directive_data example:
# { 'Default': 'Off', 'Comment': 'Enables PHP safe mode.',
# 'Remark': '<5.4.0', 'Type': 'bool'}
# if directive has Remark attribute, compare version with supplied and pass if need
if 'Remark' in directive_data and not self._check_version(directive_data['Remark'],
php_version, php_versions_map):
continue
out_dict[directive_name] = {}
if 'Default' in directive_data:
out_dict[directive_name]['default'] = directive_data['Default']
if 'Comment' in directive_data:
out_dict[directive_name]['comment'] = directive_data['Comment']
if 'Type' in directive_data:
out_dict[directive_name]['type'] = directive_data['Type']
if 'Range' in directive_data:
out_dict[directive_name]['range'] = directive_data['Range']
return out_dict
def get_config_for_cagefsctl(self):
"""
Retrives php.conf for selectorctl needs
:return: dict
Example:
{'safe_mode': {'Remark': '<5.4.0', 'Type': 'bool'},
'file_uploads': {'Type': 'bool'}
}
"""
# if config not loaded - load it
if not self._php_conf:
self._php_conf_load()
out_dict = {}
for directive_name, directive_data in self._php_conf.items():
out_dict[directive_name] = copy.deepcopy(directive_data)
if 'Comment' in out_dict[directive_name]:
del out_dict[directive_name]['Comment']
return out_dict
@staticmethod
def _check_version(test, version, php_versions_map):
"""
Compares version in use and version required by PHP feature
and return true if PHP feature satisfies
:param test: Condition to filter from php.conf ('Remark' option value), such as <5.4.0
:param version: PHP Verson to check filter matching, such as 5.3
:param php_versions_map: Short to full PHP version map. Example: {'4.4': '4.4.9'}
:return: bool: true - condition true, false - else
"""
# 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 PhpConfNoSuchAlternativeException(test)
action = m.group(1)
test_int = PhpConfReader._full_version_string_to_int(m.group(2), action)
version_int = PhpConfReader._full_version_string_to_int(php_versions_map[version], action)
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'=':
if version_int == test_int:
return True
return False
@staticmethod
def _full_version_string_to_int(s_ver_full, action):
"""
Convert version string (such as '5.3.29') to int (10653)
:param s_ver_full: PHP version string. Only full, 3-section versions are allowed
:param action: Compare type. If None or '=' function will not use third section from version
:return: integer
"""
v_array = [int(x) for x in s_ver_full.split('.')]
ver_int = v_array[0] << 11 | v_array[1] << 7
if not action or action == r'=':
return ver_int
return ver_int | v_array[2]
Zerion Mini Shell 1.0