# Copyright 1999-2017. Plesk International GmbH. All rights reserved. """ Collection of utilities for handling php.ini files. Following are excerpts from comments in stock php.ini file and php.net documentation that describe php.ini syntax and usage. PHP's initialization file, generally called php.ini, is responsible for configuring many of the aspects of PHP's behavior. The syntax of the file is extremely simple. Whitespace and Lines beginning with a semicolon are silently ignored (as you probably guessed). Section headers (e.g. [Foo]) are also silently ignored, even though they might mean something in the future. Directives following the section heading [PATH=/www/mysite] only apply to PHP files in the /www/mysite directory. Directives following the section heading [HOST=www.example.com] only apply to PHP files served from www.example.com. Directives set in these special sections cannot be overridden by user-defined INI files or at runtime. Currently, [PATH=] and [HOST=] sections only work under CGI/FastCGI. http://php.net/ini.sections Directives are specified using the following syntax: directive = value Directive names are *case sensitive* - foo=bar is different from FOO=bar. Directives are variables used to configure PHP or PHP extensions. There is no name validation. If PHP can't find an expected directive because it is not set or is mistyped, a default value will be used. The value can be a string, a number, a PHP constant (e.g. E_ALL or M_PI), one of the INI constants (On, Off, True, False, Yes, No and None) or an expression (e.g. E_ALL & ~E_NOTICE), a quoted string ("bar"), or a reference to a previously set variable or directive (e.g. ${foo}) Boolean flags can be turned on using the values 1, On, True or Yes. They can be turned off using the values 0, Off, False or No. An empty string can be denoted by simply not writing anything after the equal sign, or by using the None keyword: foo = ; sets foo to an empty string foo = None ; sets foo to an empty string foo = "None" ; sets foo to the string 'None' If you use constants in your value, and these constants belong to a dynamically loaded extension (either a PHP extension or a Zend extension), you may only use these constants *after* the line that loads the extension. ENV variables are also accessible in .ini files. As such it is possible to reference the home directory using ${LOGIN} and ${USER}. Environment variables may vary between Server APIs as those environments may be different. include_path = ".:${USER}/pear/php" """ import os import re from contextlib import closing try: from cStringIO import StringIO except ImportError: from StringIO import StringIO try: import plesk_log log = plesk_log.getLogger(__name__) except ImportError: # Avoid failing doctest in source tree import logging log = logging.getLogger(__name__) class PhpIniSyntaxError(Exception): """ Raised when a syntax error is encountered in php.ini file. """ pass class PhpIniFeatureNotSupportedError(PhpIniSyntaxError): """ Raised when encountered a certain feature in php.ini file that is not supported. """ pass class NoSectionError(Exception): """ Raised when a section with requested name was not found. """ def __init__(self, section): Exception.__init__(self, "No section: %s" % section) self.section = section class NoDirectiveError(Exception): """ Raised when a directive with requested name was not found. """ def __init__(self, directive, section): Exception.__init__(self, "No directive in section %s: %s" % (section, directive)) self.section = section self.directive = self.option = directive class PhpIniConfigParser(object): """ This is a parser for php.ini files. It works a lot like parsers from ConfigParser module, but was customized for PHP twisted logic. Important limitations and aspects to note: * array directives (like 'extension') are stored separately with their order preserved; * order of other directives is not guaranteed to be preserved; * hence variable interpolation (or substitution, or referencing, or whatever you may call it) is not supported and will trigger a syntax error; * all sections apart from special PATH= and HOST= ones are merged into a single one (by default its name is 'PHP'), this is governed by is_section_retained() method; * file parsing is fail-fast as opposed to ConfigParser implementation; * when parsing comments and blank lines are ignored. """ def __init__(self, default_section="PHP"): self._default_header = default_section self._sections = {} self._extensions = [] self._create_section(self._default_header) def sections(self): """ Returns a list of section names, including default one. >>> config.sections() ['PHP', 'HOST=www.example.com'] """ return self._sections.keys() def has_section(self, section): """ Check whether the named section is present in configuration. >>> config.has_section('PHP') True >>> config.has_section('Pdo') False """ return section in self._sections def items(self, section): """ Returns a list of (name, value) pairs for each option in the given section. For a default section extension directives also appear in this list. >>> config.items('HOST=www.example.com') [('display_startup_errors', 'True')] """ try: d = self._sections[section] except KeyError: if section != self._default_header: raise NoSectionError(section) d = {} if '__name__' in d: del d['__name__'] if section == self._default_header: return self._extensions + d.items() else: return d.items() def remove_section(self, section): """ Removes a configuration section. Returns previous existence status. >>> config = PhpIniConfigParser() >>> config.readstr(data1) >>> config.remove_section('HOST=www.example.com') True >>> config.remove_section('PHP') # default section is always present, ... # but this will delete its content True >>> config.sections() ['PHP'] >>> config.items('PHP') [] """ if section in self._sections: del self._sections[section] if section == self._default_header: log.debug("PhpIniConfigParser: default section '%s' was cleaned", section) self._extensions = [] self._create_section(self._default_header) return True else: return False def extensions(self): """ Returns list of (directive, path) pairs for extension directives in php.ini file preserving order. Following directives are recognised as extension ones: * extension * zend_extension * zend_extension_debug (prior to PHP 5.3.0) * zend_extension_debug_ts (prior to PHP 5.3.0) * zend_extension_ts (prior to PHP 5.3.0) >>> config.extensions() [('zend_extension_debug_ts', '/path/to/ioncube_loader_5.3.so'), ('extension', 'pdo.so')] """ return self._extensions[:] def get(self, section, option): """ Get an option value for named section or default one if not section. """ if not section: section = self._default_header if section not in self._sections: raise NoSectionError(section) elif option in self._sections[section]: return self._sections[section][option] elif section == self._default_header and self._is_extension_directive(option): return [ext[1] for ext in self._extensions if ext[0] == option] else: raise NoDirectiveError(section, option) def getbool(self, section, option): """ A convenience method that coerces the option value in the specified section to a boolean. """ return self._boolean_value_as_bool(self.get(section, option)) def is_section_retained(self, section): """ A predicate method that decides whether to retain a given section or merge its contents into a default one. Below is a tricky monkey-patching mumbo-jumbo example. It's usually better to just subclass, since monkey-patching is hard to debug. But I'm feeling fancy ;) >>> config = PhpIniConfigParser() >>> config.is_section_retained = type(PhpIniConfigParser.is_section_retained)( ... lambda self, section: ... PhpIniConfigParser.is_section_retained(self, section) or ... section in ('Pdo', 'Pdo_mysql'), ... config, PhpIniConfigParser) >>> config.readstr(data1) >>> config.readstr(data2) >>> config.sections() ['Pdo', 'Pdo_mysql', 'PATH=/www/mysite', 'PHP', 'HOST=www.example.com'] """ return section.startswith('PATH=') or section.startswith('HOST=') def _create_section(self, section): """ Returns section structure, optionally creating it. """ if section not in self._sections: log.debug("PhpIniConfigParser: created new section '%s'", section) self._sections[section] = {'__name__': section} return self._sections[section] def _has_variable_interpolation(self, value): """ Check whether a given string contains variable interpolation. E.g. ".:${USER}/pear/php" has one for USER variable. >>> config._has_variable_interpolation(".:${USER}/pear/php") True >>> config._has_variable_interpolation("abra}${cadabra") False """ return '${' in value and '}' in value and value.index('${') < value.rindex('}') BOOLEAN_TRUE_VALUES = ('1', 'on', 'true', 'yes') BOOLEAN_FALSE_VALUES = ('0', 'off', 'false', 'no') def _is_boolean_setting(self, name, value): """ Check whether a given setting is a boolean one. """ return value.lower() in self.BOOLEAN_TRUE_VALUES + self.BOOLEAN_FALSE_VALUES def _boolean_value_as_bool(self, value): """ Convert string boolean value to a bool. """ if value.lower() in self.BOOLEAN_TRUE_VALUES: return True elif value.lower() in self.BOOLEAN_FALSE_VALUES: return False else: raise PhpIniSyntaxError("Value is not a valid boolean one: '%s'" % value) _EXTENSION_RE = re.compile(r'^(zend_)?extension(_debug)?(_ts)?$') def _is_extension_directive(self, name): """ Check whether a given directive name requests loading extension. >>> dirs = ('extension', 'zend_extension', 'zend_extension_debug', 'zend_extension_ts', 'zend_extension_debug_ts') >>> for directive in dirs: ... if not config._is_extension_directive(directive): ... break ... else: ... print "OK" OK >>> config._is_extension_directive('display_errors') False """ return self._EXTENSION_RE.match(name) is not None _SECTION_HEADER_RE = re.compile(r'\[(?P
[^]]+)\]') _DIRECTIVE_RE = re.compile( r'^(?P