summaryrefslogtreecommitdiffstats
path: root/library/Icinga/File/Ini
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--library/Icinga/File/Ini/Dom/Comment.php37
-rw-r--r--library/Icinga/File/Ini/Dom/Directive.php166
-rw-r--r--library/Icinga/File/Ini/Dom/Document.php132
-rw-r--r--library/Icinga/File/Ini/Dom/Section.php190
-rw-r--r--library/Icinga/File/Ini/IniParser.php310
-rw-r--r--library/Icinga/File/Ini/IniWriter.php205
6 files changed, 1040 insertions, 0 deletions
diff --git a/library/Icinga/File/Ini/Dom/Comment.php b/library/Icinga/File/Ini/Dom/Comment.php
new file mode 100644
index 0000000..c202d0f
--- /dev/null
+++ b/library/Icinga/File/Ini/Dom/Comment.php
@@ -0,0 +1,37 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\File\Ini\Dom;
+
+/**
+ * A single comment-line in an INI file
+ */
+class Comment
+{
+ /**
+ * The comment text
+ *
+ * @var string
+ */
+ protected $content;
+
+ /**
+ * Set the text content of this comment
+ *
+ * @param $content
+ */
+ public function setContent($content)
+ {
+ $this->content = $content;
+ }
+
+ /**
+ * Render this comment into INI markup
+ *
+ * @return string
+ */
+ public function render()
+ {
+ return ';' . $this->content;
+ }
+}
diff --git a/library/Icinga/File/Ini/Dom/Directive.php b/library/Icinga/File/Ini/Dom/Directive.php
new file mode 100644
index 0000000..4279a5f
--- /dev/null
+++ b/library/Icinga/File/Ini/Dom/Directive.php
@@ -0,0 +1,166 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\File\Ini\Dom;
+
+use Icinga\Exception\ConfigurationError;
+
+/**
+ * A key value pair in a Section
+ */
+class Directive
+{
+ /**
+ * The value of this configuration directive
+ *
+ * @var string
+ */
+ protected $key;
+
+ /**
+ * The immutable name of this configuration directive
+ *
+ * @var string
+ */
+ protected $value;
+
+ /**
+ * Comments added one line before this directive
+ *
+ * @var Comment[] The comment lines
+ */
+ protected $commentsPre = null;
+
+ /**
+ * Comment added at the end of the same line
+ *
+ * @var Comment
+ */
+ protected $commentPost = null;
+
+ /**
+ * @param string $key The name of this configuration directive
+ *
+ * @throws ConfigurationError
+ */
+ public function __construct($key)
+ {
+ $this->key = trim($key);
+ if (strlen($this->key) < 1) {
+ throw new ConfigurationError(sprintf('Ini error: empty directive key.'));
+ }
+ }
+
+ /**
+ * Return the name of this directive
+ *
+ * @return string
+ */
+ public function getKey()
+ {
+ return $this->key;
+ }
+
+ /**
+ * Return the value of this configuration directive
+ *
+ * @return string
+ */
+ public function getValue()
+ {
+ return $this->value;
+ }
+
+ /**
+ * Set the value of this configuration directive
+ *
+ * @param string $value
+ */
+ public function setValue($value)
+ {
+ $this->value = trim($value);
+ }
+
+ /**
+ * Set the comments to be rendered on the line before this directive
+ *
+ * @param Comment[] $comments
+ */
+ public function setCommentsPre(array $comments)
+ {
+ $this->commentsPre = $comments;
+ }
+
+ /**
+ * Return the comments to be rendered on the line before this directive
+ *
+ * @return Comment[]
+ */
+ public function getCommentsPre()
+ {
+ return $this->commentsPre;
+ }
+
+ /**
+ * Set the comment rendered on the same line of this directive
+ *
+ * @param Comment $comment
+ */
+ public function setCommentPost(Comment $comment)
+ {
+ $this->commentPost = $comment;
+ }
+
+ /**
+ * Render this configuration directive into INI markup
+ *
+ * @return string
+ */
+ public function render()
+ {
+ $str = '';
+ if (! empty($this->commentsPre)) {
+ $comments = array();
+ foreach ($this->commentsPre as $comment) {
+ $comments[] = $comment->render();
+ }
+ $str = implode(PHP_EOL, $comments) . PHP_EOL;
+ }
+ $str .= sprintf('%s = "%s"', $this->sanitizeKey($this->key), $this->sanitizeValue($this->value));
+ if (isset($this->commentPost)) {
+ $str .= ' ' . $this->commentPost->render();
+ }
+ return $str;
+ }
+
+ /**
+ * Assure that the given identifier contains no newlines and pending or trailing whitespaces
+ *
+ * @param $str The string to sanitize
+ *
+ * @return string
+ */
+ protected function sanitizeKey($str)
+ {
+ return trim(str_replace(PHP_EOL, ' ', $str));
+ }
+
+ /**
+ * Escape the significant characters in directive values, normalize line breaks and assure that
+ * the character contains no linebreaks
+ *
+ * @param $str The string to sanitize
+ *
+ * @return mixed|string
+ */
+ protected function sanitizeValue($str)
+ {
+ $str = trim($str);
+ $str = str_replace('\\', '\\\\', $str);
+ $str = str_replace('"', '\"', $str);
+ $str = str_replace("\r", '\r', $str);
+ $str = str_replace("\n", '\n', $str);
+
+ return $str;
+ }
+}
diff --git a/library/Icinga/File/Ini/Dom/Document.php b/library/Icinga/File/Ini/Dom/Document.php
new file mode 100644
index 0000000..f38f33e
--- /dev/null
+++ b/library/Icinga/File/Ini/Dom/Document.php
@@ -0,0 +1,132 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\File\Ini\Dom;
+
+class Document
+{
+ /**
+ * The sections of this INI file
+ *
+ * @var Section[]
+ */
+ protected $sections = array();
+
+ /**
+ * The comemnts at file end that belong to no particular section
+ *
+ * @var Comment[]
+ */
+ protected $commentsDangling;
+
+ /**
+ * Append a section to the end of this INI file
+ *
+ * @param Section $section
+ */
+ public function addSection(Section $section)
+ {
+ $this->sections[$section->getName()] = $section;
+ }
+
+ /**
+ * Return whether this INI file has the section with the given key
+ *
+ * @param string $name
+ *
+ * @return bool
+ */
+ public function hasSection($name)
+ {
+ return isset($this->sections[trim($name)]);
+ }
+
+ /**
+ * Return the section with the given name
+ *
+ * @param string $name
+ *
+ * @return Section
+ */
+ public function getSection($name)
+ {
+ return $this->sections[trim($name)];
+ }
+
+ /**
+ * Set the section with the given name
+ *
+ * @param string $name
+ * @param Section $section
+ *
+ * @return Section
+ */
+ public function setSection($name, Section $section)
+ {
+ return $this->sections[trim($name)] = $section;
+ }
+
+ /**
+ * Remove the section with the given name
+ *
+ * @param string $name
+ */
+ public function removeSection($name)
+ {
+ unset($this->sections[trim($name)]);
+ }
+
+ /**
+ * Set the dangling comments at file end that belong to no particular directive
+ *
+ * @param Comment[] $comments
+ */
+ public function setCommentsDangling(array $comments)
+ {
+ $this->commentsDangling = $comments;
+ }
+
+ /**
+ * Get the dangling comments at file end that belong to no particular directive
+ *
+ * @return array
+ */
+ public function getCommentsDangling()
+ {
+ return $this->commentsDangling;
+ }
+
+ /**
+ * Render this document into the corresponding INI markup
+ *
+ * @return string
+ */
+ public function render()
+ {
+ $sections = array();
+ foreach ($this->sections as $section) {
+ $sections []= $section->render();
+ }
+ $str = implode(PHP_EOL, $sections);
+ if (! empty($this->commentsDangling)) {
+ foreach ($this->commentsDangling as $comment) {
+ $str .= PHP_EOL . $comment->render();
+ }
+ }
+ return $str;
+ }
+
+ /**
+ * Convert $this to an array
+ *
+ * @return array
+ */
+ public function toArray()
+ {
+ $a = array();
+ foreach ($this->sections as $section) {
+ $a[$section->getName()] = $section->toArray();
+ }
+ return $a;
+ }
+}
diff --git a/library/Icinga/File/Ini/Dom/Section.php b/library/Icinga/File/Ini/Dom/Section.php
new file mode 100644
index 0000000..f877c87
--- /dev/null
+++ b/library/Icinga/File/Ini/Dom/Section.php
@@ -0,0 +1,190 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\File\Ini\Dom;
+
+use Icinga\Exception\ConfigurationError;
+
+/**
+ * A section in an INI file
+ */
+class Section
+{
+ /**
+ * The immutable name of this section
+ *
+ * @var string
+ */
+ protected $name;
+
+ /**
+ * All configuration directives of this section
+ *
+ * @var Directive[]
+ */
+ protected $directives = array();
+
+ /**
+ * Comments added one line before this section
+ *
+ * @var Comment[]
+ */
+ protected $commentsPre;
+
+ /**
+ * Comment added at the end of the same line
+ *
+ * @var string
+ */
+ protected $commentPost;
+
+ /**
+ * @param string $name The immutable name of this section
+ *
+ * @throws ConfigurationError When the section name is empty or contains brackets
+ */
+ public function __construct($name)
+ {
+ $this->name = trim($name);
+ if (strlen($this->name) < 1) {
+ throw new ConfigurationError('Ini file error: empty section identifier');
+ } elseif (strpos($name, '[') !== false || strpos($name, ']') !== false) {
+ throw new ConfigurationError(
+ 'Ini file error: Section name "%s" must not contain any brackets ([, ])',
+ $name
+ );
+ }
+ }
+
+ /**
+ * Append a directive to the end of this section
+ *
+ * @param Directive $directive The directive to append
+ */
+ public function addDirective(Directive $directive)
+ {
+ $this->directives[$directive->getKey()] = $directive;
+ }
+
+ /**
+ * Remove the directive with the given name
+ *
+ * @param string $key They name of the directive to remove
+ */
+ public function removeDirective($key)
+ {
+ unset($this->directives[$key]);
+ }
+
+ /**
+ * Return whether this section has a directive with the given key
+ *
+ * @param string $key The name of the directive
+ *
+ * @return bool
+ */
+ public function hasDirective($key)
+ {
+ return isset($this->directives[$key]);
+ }
+
+ /**
+ * Get the directive with the given key
+ *
+ * @param $key string
+ *
+ * @return Directive
+ */
+ public function getDirective($key)
+ {
+ return $this->directives[$key];
+ }
+
+ /**
+ * Return the name of this section
+ *
+ * @return string The name
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ /**
+ * Set the comments to be rendered on the line before this section
+ *
+ * @param Comment[] $comments
+ */
+ public function setCommentsPre(array $comments)
+ {
+ $this->commentsPre = $comments;
+ }
+
+ /**
+ * Set the comment rendered on the same line of this section
+ *
+ * @param Comment $comment
+ */
+ public function setCommentPost(Comment $comment)
+ {
+ $this->commentPost = $comment;
+ }
+
+ /**
+ * Render this section into INI markup
+ *
+ * @return string
+ */
+ public function render()
+ {
+ $dirs = '';
+ $i = 0;
+ foreach ($this->directives as $directive) {
+ $comments = $directive->getCommentsPre();
+ $dirs .= (($i++ > 0 && ! empty($comments)) ? PHP_EOL : '')
+ . $directive->render() . PHP_EOL;
+ }
+ $cms = '';
+ if (! empty($this->commentsPre)) {
+ foreach ($this->commentsPre as $comment) {
+ $comments[] = $comment->render();
+ }
+ $cms = implode(PHP_EOL, $comments) . PHP_EOL;
+ }
+ $post = '';
+ if (isset($this->commentPost)) {
+ $post = ' ' . $this->commentPost->render();
+ }
+ return $cms . sprintf('[%s]', $this->sanitize($this->name)) . $post . PHP_EOL . $dirs;
+ }
+
+ /**
+ * Escape the significant characters in sections and normalize line breaks
+ *
+ * @param $str The string to sanitize
+ *
+ * @return mixed
+ */
+ protected function sanitize($str)
+ {
+ $str = trim($str);
+ $str = str_replace('\\', '\\\\', $str);
+ $str = str_replace('"', '\\"', $str);
+ $str = str_replace(';', '\\;', $str);
+ return str_replace(PHP_EOL, ' ', $str);
+ }
+
+ /**
+ * Convert $this to an array
+ *
+ * @return array
+ */
+ public function toArray()
+ {
+ $a = array();
+ foreach ($this->directives as $directive) {
+ $a[$directive->getKey()] = $directive->getValue();
+ }
+ return $a;
+ }
+}
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);
+ }
+}
diff --git a/library/Icinga/File/Ini/IniWriter.php b/library/Icinga/File/Ini/IniWriter.php
new file mode 100644
index 0000000..f00040e
--- /dev/null
+++ b/library/Icinga/File/Ini/IniWriter.php
@@ -0,0 +1,205 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\File\Ini;
+
+use Icinga\Application\Logger;
+use Icinga\Data\ConfigObject;
+use Icinga\Exception\ProgrammingError;
+use Icinga\File\Ini\Dom\Directive;
+use Icinga\File\Ini\Dom\Document;
+use Icinga\File\Ini\Dom\Section;
+use Zend_Config_Exception;
+use Icinga\Application\Config;
+
+/**
+ * A INI file adapter that respects the file structure and the comments of already existing ini files
+ */
+class IniWriter
+{
+ /**
+ * Stores the options
+ *
+ * @var array
+ */
+ protected $options;
+
+ /**
+ * The configuration object to write
+ *
+ * @var Config
+ */
+ protected $config;
+
+ /**
+ * The mode to set on new files
+ *
+ * @var int
+ */
+ protected $fileMode;
+
+ /**
+ * The path to write to
+ *
+ * @var string
+ */
+ protected $filename;
+
+ /**
+ * Create a new INI writer
+ *
+ * @param Config $config The configuration to write
+ * @param string $filename The file name to write to
+ * @param int $filemode Octal file persmissions
+ *
+ * @link http://framework.zend.com/apidoc/1.12/files/Config.Writer.html#\Zend_Config_Writer
+ */
+ public function __construct(Config $config, $filename, $filemode = 0660, $options = array())
+ {
+ $this->config = $config;
+ $this->filename = $filename;
+ $this->fileMode = $filemode;
+ $this->options = $options;
+ }
+
+ /**
+ * Render the Zend_Config into a config filestring
+ *
+ * @return string
+ */
+ public function render()
+ {
+ if (file_exists($this->filename)) {
+ $oldconfig = Config::fromIni($this->filename);
+ $content = trim(file_get_contents($this->filename));
+ } else {
+ $oldconfig = Config::fromArray(array());
+ $content = '';
+ }
+ $doc = IniParser::parseIni($content);
+ $this->diffPropertyUpdates($this->config, $doc);
+ $this->diffPropertyDeletions($oldconfig, $this->config, $doc);
+ $doc = $this->updateSectionOrder($this->config, $doc);
+ return $doc->render();
+ }
+
+ /**
+ * Write configuration to file and set file mode in case it does not exist yet
+ *
+ * @param string $filename
+ * @param bool $exclusiveLock
+ *
+ * @throws Zend_Config_Exception
+ */
+ public function write($filename = null, $exclusiveLock = false)
+ {
+ $filePath = isset($filename) ? $filename : $this->filename;
+ $setMode = false === file_exists($filePath);
+
+ if (file_put_contents($filePath, $this->render(), $exclusiveLock ? LOCK_EX : 0) === false) {
+ throw new Zend_Config_Exception('Could not write to file "' . $filePath . '"');
+ }
+
+ if ($setMode) {
+ // file was newly created
+ $mode = $this->fileMode;
+ if (is_int($this->fileMode) && false === @chmod($filePath, $this->fileMode)) {
+ throw new Zend_Config_Exception(sprintf('Failed to set file mode "%o" on file "%s"', $mode, $filePath));
+ }
+ }
+ }
+
+ /**
+ * Update the order of the sections in the ini file to match the order of the new config
+ *
+ * @return Document A new document with the changed section order applied
+ */
+ protected function updateSectionOrder(Config $newconfig, Document $oldDoc)
+ {
+ $doc = new Document();
+ $dangling = $oldDoc->getCommentsDangling();
+ if (isset($dangling)) {
+ $doc->setCommentsDangling($dangling);
+ }
+ foreach ($newconfig->toArray() as $section => $directives) {
+ $doc->addSection($oldDoc->getSection($section));
+ }
+ return $doc;
+ }
+
+ /**
+ * Search for created and updated properties and use the editor to create or update these entries
+ *
+ * @param Config $newconfig The config representing the state after the change
+ * @param Document $doc
+ *
+ * @throws ProgrammingError
+ */
+ protected function diffPropertyUpdates(Config $newconfig, Document $doc)
+ {
+ foreach ($newconfig->toArray() as $section => $directives) {
+ if (! is_array($directives)) {
+ Logger::warning('Section-less property ' . (string)$directives . ' was ignored.');
+ continue;
+ }
+ if (!$doc->hasSection($section)) {
+ $domSection = new Section($section);
+ $doc->addSection($domSection);
+ } else {
+ $domSection = $doc->getSection($section);
+ }
+ foreach ($directives as $key => $value) {
+ if ($value === null) {
+ continue;
+ }
+
+ if ($value instanceof ConfigObject) {
+ throw new ProgrammingError('Cannot diff recursive configs');
+ }
+ if ($domSection->hasDirective($key)) {
+ $domSection->getDirective($key)->setValue($value);
+ } else {
+ $dir = new Directive($key);
+ $dir->setValue($value);
+ $domSection->addDirective($dir);
+ }
+ }
+ }
+ }
+
+ /**
+ * Search for deleted properties and use the editor to delete these entries
+ *
+ * @param Config $oldconfig The config representing the state before the change
+ * @param Config $newconfig The config representing the state after the change
+ * @param Document $doc
+ *
+ * @throws ProgrammingError
+ */
+ protected function diffPropertyDeletions(Config $oldconfig, Config $newconfig, Document $doc)
+ {
+ // Iterate over all properties in the old configuration file and remove those that don't
+ // exist in the new config
+ foreach ($oldconfig->toArray() as $section => $directives) {
+ if (! is_array($directives)) {
+ Logger::warning('Section-less property ' . (string)$directives . ' was ignored.');
+ continue;
+ }
+
+ if ($newconfig->hasSection($section)) {
+ $newSection = $newconfig->getSection($section);
+ $oldDomSection = $doc->getSection($section);
+ foreach ($directives as $key => $value) {
+ if ($value instanceof ConfigObject) {
+ throw new ProgrammingError('Cannot diff recursive configs');
+ }
+ if (null === $newSection->get($key) && $oldDomSection->hasDirective($key)) {
+ $oldDomSection->removeDirective($key);
+ }
+ }
+ } else {
+ $doc->removeSection($section);
+ }
+ }
+ }
+}