diff options
Diffstat (limited to 'library/Icinga/File/Ini/IniParser.php')
-rw-r--r-- | library/Icinga/File/Ini/IniParser.php | 310 |
1 files changed, 310 insertions, 0 deletions
diff --git a/library/Icinga/File/Ini/IniParser.php b/library/Icinga/File/Ini/IniParser.php new file mode 100644 index 0000000..279aa45 --- /dev/null +++ b/library/Icinga/File/Ini/IniParser.php @@ -0,0 +1,310 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\File\Ini; + +use ErrorException; +use Icinga\File\Ini\Dom\Section; +use Icinga\File\Ini\Dom\Comment; +use Icinga\File\Ini\Dom\Document; +use Icinga\File\Ini\Dom\Directive; +use Icinga\Application\Logger; +use Icinga\Exception\ConfigurationError; +use Icinga\Exception\NotReadableError; +use Icinga\Application\Config; + +class IniParser +{ + const LINE_START = 0; + const SECTION = 1; + const ESCAPE = 2; + const DIRECTIVE_KEY = 4; + const DIRECTIVE_VALUE_START = 5; + const DIRECTIVE_VALUE = 6; + const DIRECTIVE_VALUE_QUOTED = 7; + const COMMENT = 8; + const COMMENT_END = 9; + const LINE_END = 10; + + /** + * Cancel the parsing with an error + * + * @param $message The error description + * @param $line The line in which the error occured + * + * @throws ConfigurationError + */ + private static function throwParseError($message, $line) + { + throw new ConfigurationError(sprintf('Ini parser error: %s. (l. %d)', $message, $line)); + } + + /** + * Read the ini file contained in a string and return a mutable DOM that can be used + * to change the content of an INI file. + * + * @param $str A string containing the whole ini file + * + * @return Document The mutable DOM object. + * @throws ConfigurationError In case the file is not parseable + */ + public static function parseIni($str) + { + $doc = new Document(); + $sec = null; + $dir = null; + $coms = array(); + $state = self::LINE_START; + $escaping = null; + $token = ''; + $line = 0; + + for ($i = 0; $i < strlen($str); $i++) { + $s = $str[$i]; + switch ($state) { + case self::LINE_START: + if (ctype_space($s)) { + continue 2; + } + switch ($s) { + case '[': + $state = self::SECTION; + break; + case ';': + $state = self::COMMENT; + break; + default: + $state = self::DIRECTIVE_KEY; + $token = $s; + break; + } + break; + + case self::ESCAPE: + $token .= $s; + $state = $escaping; + $escaping = null; + break; + + case self::SECTION: + if ($s === "\n") { + self::throwParseError('Unterminated SECTION', $line); + } elseif ($s === '\\') { + $state = self::ESCAPE; + $escaping = self::SECTION; + } elseif ($s !== ']') { + $token .= $s; + } else { + $sec = new Section($token); + $sec->setCommentsPre($coms); + $doc->addSection($sec); + $dir = null; + $coms = array(); + + $state = self::LINE_END; + $token = ''; + } + break; + + case self::DIRECTIVE_KEY: + if ($s !== '=') { + $token .= $s; + } else { + $dir = new Directive($token); + $dir->setCommentsPre($coms); + if (isset($sec)) { + $sec->addDirective($dir); + } else { + Logger::warning(sprintf( + 'Ini parser warning: section-less directive "%s" ignored. (l. %d)', + $token, + $line + )); + } + + $coms = array(); + $state = self::DIRECTIVE_VALUE_START; + $token = ''; + } + break; + + case self::DIRECTIVE_VALUE_START: + if (ctype_space($s)) { + continue 2; + } elseif ($s === '"') { + $state = self::DIRECTIVE_VALUE_QUOTED; + } else { + $state = self::DIRECTIVE_VALUE; + $token = $s; + } + break; + + case self::DIRECTIVE_VALUE: + /* + Escaping non-quoted values is not supported by php_parse_ini, it might + be reasonable to include in case we are switching completely our own + parser implementation + */ + if ($s === "\n" || $s === ";") { + $dir->setValue($token); + $token = ''; + + if ($s === "\n") { + $state = self::LINE_START; + $line ++; + } elseif ($s === ';') { + $state = self::COMMENT; + } + } else { + $token .= $s; + } + break; + + case self::DIRECTIVE_VALUE_QUOTED: + if ($s === '\\') { + $state = self::ESCAPE; + $escaping = self::DIRECTIVE_VALUE_QUOTED; + } elseif ($s !== '"') { + $token .= $s; + } else { + $dir->setValue($token); + $token = ''; + $state = self::LINE_END; + } + break; + + case self::COMMENT: + case self::COMMENT_END: + if ($s !== "\n") { + $token .= $s; + } else { + $com = new Comment(); + $com->setContent($token); + $token = ''; + + // Comments at the line end belong to the current line's directive or section. Comments + // on empty lines belong to the next directive that shows up. + if ($state === self::COMMENT_END) { + if (isset($dir)) { + $dir->setCommentPost($com); + } else { + $sec->setCommentPost($com); + } + } else { + $coms[] = $com; + } + $state = self::LINE_START; + $line ++; + } + break; + + case self::LINE_END: + if ($s === "\n") { + $state = self::LINE_START; + $line ++; + } elseif ($s === ';') { + $state = self::COMMENT_END; + } + break; + } + } + + // process the last token + switch ($state) { + case self::COMMENT: + case self::COMMENT_END: + $com = new Comment(); + $com->setContent($token); + if ($state === self::COMMENT_END) { + if (isset($dir)) { + $dir->setCommentPost($com); + } else { + $sec->setCommentPost($com); + } + } else { + $coms[] = $com; + } + break; + + case self::DIRECTIVE_VALUE: + $dir->setValue($token); + $sec->addDirective($dir); + break; + + case self::ESCAPE: + case self::DIRECTIVE_VALUE_QUOTED: + case self::DIRECTIVE_KEY: + case self::SECTION: + self::throwParseError('File ended in unterminated state ' . $state, $line); + } + if (! empty($coms)) { + $doc->setCommentsDangling($coms); + } + return $doc; + } + + /** + * Read the ini file and parse it with ::parseIni() + * + * @param string $file The ini file to read + * + * @return Config + * @throws NotReadableError When the file cannot be read + */ + public static function parseIniFile($file) + { + if (($path = realpath($file)) === false) { + throw new NotReadableError('Couldn\'t compute the absolute path of `%s\'', $file); + } + + if (($content = file_get_contents($path)) === false) { + throw new NotReadableError('Couldn\'t read the file `%s\'', $path); + } + + try { + $configArray = parse_ini_string($content, true, INI_SCANNER_RAW); + } catch (ErrorException $e) { + throw new ConfigurationError('Couldn\'t parse the INI file `%s\'', $path, $e); + } + + $unescaped = array(); + foreach ($configArray as $section => $options) { + $unescaped[self::unescapeSectionName($section)] = array_map([__CLASS__, 'unescapeOptionValue'], $options); + } + + return Config::fromArray($unescaped)->setConfigFile($file); + } + + /** + * Unescape significant characters in the given section name + * + * @param string $str + * + * @return string + */ + protected static function unescapeSectionName($str) + { + $str = str_replace('\"', '"', $str); + $str = str_replace('\;', ';', $str); + + return str_replace('\\\\', '\\', $str); + } + + /** + * Unescape significant characters in the given option value + * + * @param string $str + * + * @return string + */ + protected static function unescapeOptionValue($str) + { + $str = str_replace('\n', "\n", $str); + $str = str_replace('\r', "\r", $str); + $str = str_replace('\"', '"', $str); + $str = str_replace('\\\\', '\\', $str); + + // This replacement is a work-around for PHP bug #76965. Fixed with versions 7.1.24, 7.2.12 and 7.3.0. + return preg_replace('~^([\'"])(.*?)\1\s+$~', '$2', $str); + } +} |