summaryrefslogtreecommitdiffstats
path: root/library/Icinga/File/Ini/IniParser.php
diff options
context:
space:
mode:
Diffstat (limited to 'library/Icinga/File/Ini/IniParser.php')
-rw-r--r--library/Icinga/File/Ini/IniParser.php310
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);
+ }
+}