diff --git a/library/Icinga/Application/Config.php b/library/Icinga/Application/Config.php index 02851b975..0538f01d4 100644 --- a/library/Icinga/Application/Config.php +++ b/library/Icinga/Application/Config.php @@ -359,11 +359,7 @@ class Config implements Countable, Iterator, Selectable */ protected function getIniWriter($filePath = null, $fileMode = null) { - return new IniWriter(array( - 'config' => $this, - 'filename' => $filePath, - 'filemode' => $fileMode - )); + return new IniWriter($this, $filePath, $fileMode); } /** @@ -418,7 +414,6 @@ class Config implements Countable, Iterator, Selectable static::resolvePath('modules/' . $modulename . '/' . $configname . '.ini') ); } - return $moduleConfigs[$configname]; } diff --git a/library/Icinga/File/Ini/Dom/Comment.php b/library/Icinga/File/Ini/Dom/Comment.php new file mode 100644 index 000000000..9cd9e416f --- /dev/null +++ b/library/Icinga/File/Ini/Dom/Comment.php @@ -0,0 +1,20 @@ +content; + } +} diff --git a/library/Icinga/File/Ini/Dom/Directive.php b/library/Icinga/File/Ini/Dom/Directive.php new file mode 100644 index 000000000..9d090ee63 --- /dev/null +++ b/library/Icinga/File/Ini/Dom/Directive.php @@ -0,0 +1,76 @@ +key = trim(str_replace("\n", ' ', $key)); + if (strlen($this->key) < 1) { + throw new Exception(sprintf('Ini parser error: empty key.')); + } + } + + /** + * @return string + */ + public function getKey() + { + return $this->key; + } + + /** + * @param string $value + */ + public function setValue($value) + { + $this->value = trim(str_replace("\n", ' ', $value)); + } + + /** + * @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->key, $this->value); + if (isset ($this->commentPost)) { + $str .= ' ' . $this->commentPost->render(); + } + 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 000000000..eca296f2a --- /dev/null +++ b/library/Icinga/File/Ini/Dom/Document.php @@ -0,0 +1,81 @@ +sections[$section->getName()] = $section; + } + + /** + * @param string $name + * + * @return bool + */ + public function hasSection($name) + { + return isset($this->sections[$name]); + } + + /** + * @param string $name + * + * @return Section + */ + public function getSection($name) + { + return $this->sections[$name]; + } + + /** + * @param string $name + * @param Section $section + * + * @return Section + */ + public function setSection($name, Section $section) + { + return $this->sections[$name] = $section; + } + + /** + * @param string $name + */ + public function removeSection($name) + { + unset ($this->sections[$name]); + } + + /** + * @return string + */ + public function render() + { + 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; + } +} diff --git a/library/Icinga/File/Ini/Dom/Section.php b/library/Icinga/File/Ini/Dom/Section.php new file mode 100644 index 000000000..0ae9b689c --- /dev/null +++ b/library/Icinga/File/Ini/Dom/Section.php @@ -0,0 +1,108 @@ +name = trim(str_replace("\n", ' ', $name)); + if (strlen($this->name) < 1) { + throw new Exception(sprintf('Ini parser error: empty section identifier')); + } + } + + /** + * @param Directive $directive + */ + public function addDirective(Directive $directive) + { + $this->directives[$directive->getKey()] = $directive; + } + + /** + * @param string $key + */ + public function removeDirective($key) + { + unset ($this->directives[$key]); + } + + /** + * @param string $key + * + * @return bool + */ + public function hasDirective($key) + { + return isset($this->directives[$key]); + } + + /** + * @param $key string + * + * @return Directive + */ + public function getDirective($key) + { + return $this->directives[$key]; + } + + /** + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * @return string + */ + public function render() + { + $dirs = ''; + $i = 0; + foreach ($this->directives as $directive) { + $dirs .= (($i++ > 0 && ! empty($directive->commentsPre)) ? 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->name) . $post . PHP_EOL . $dirs; + } +} diff --git a/library/Icinga/File/Ini/IniEditor.php b/library/Icinga/File/Ini/IniEditor.php deleted file mode 100644 index e48fef3d6..000000000 --- a/library/Icinga/File/Ini/IniEditor.php +++ /dev/null @@ -1,624 +0,0 @@ -text = explode(PHP_EOL, $content); - $this->valueIndentation = array_key_exists('valueIndentation', $options) - ? $options['valueIndentation'] : 19; - $this->commentIndentation = array_key_exists('commentIndentation', $options) - ? $options['commentIndentation'] : 43; - $this->sectionSeparators = array_key_exists('sectionSeparators', $options) - ? $options['sectionSeparators'] : 1; - } - - /** - * Set the value of the given key. - * - * @param array $key The key to set - * @param string $value The value to set - * @param array $section The section to insert to. - */ - public function set(array $key, $value, $section = null) - { - $line = $this->getKeyLine($key, $section); - if ($line === -1) { - $this->insert($key, $value, $section); - } else { - $content = $this->formatKeyValuePair($key, $value); - $this->updateLine($line, $content); - } - } - - /** - * Reset the value of the given array element - * - * @param array $key The key of the array value - * @param array $section The section of the array. - */ - public function resetArrayElement(array $key, $section = null) - { - $line = $this->getArrayElement($key, $section); - if ($line !== -1) { - $this->deleteLine($line); - } - } - - /** - * Set the value for an array element - * - * @param array $key The key of the property - * @param string $value The value of the property - * @param array $section The section to use - */ - public function setArrayElement(array $key, $value, $section = null) - { - $line = $this->getArrayElement($key, $section); - if ($line !== -1) { - if (isset($section)) { - $this->updateLine($line, $this->formatKeyValuePair($key, $value)); - } else { - /* - * Move into new section to avoid ambiguous configurations - */ - $section = $key[0]; - unset($key[0]); - $this->deleteLine($line); - $this->setSection($section); - $this->insert($key, $value, $section); - } - } else { - $this->insert($key, $value, $section); - } - } - - /** - * Get the line of an array element - * - * @param array $key The key of the property. - * @param mixed $section The section to use - * - * @return int The line of the array element. - */ - private function getArrayElement(array $key, $section = null) - { - $line = isset($section) ? $this->getSectionLine($section) + 1 : 0; - $index = array_pop($key); - $formatted = $this->formatKey($key); - for (; $line < count($this->text); $line++) { - $l = $this->text[$line]; - if ($this->isSectionDeclaration($l)) { - return -1; - } - if (preg_match('/^\s*' . preg_quote($formatted, '/') . '\[\]\s*=/', $l) === 1) { - return $line; - } - if ($this->isPropertyDeclaration($l, array_merge($key, array($index)))) { - return $line; - } - } - return -1; - } - - /** - * When it exists, set the key back to null - * - * @param array $key The key to reset - * @param array $section The section of the key - */ - public function reset(array $key, $section = null) - { - $line = $this->getKeyLine($key, $section); - if ($line === -1) { - return; - } - $this->deleteLine($line); - } - - /** - * Create the section if it does not exist and set the properties - * - * @param string $section The section name - */ - public function setSection($section) - { - if (false !== strpos($section, '[') || false !== strpos($section, ']')) { - throw new ConfigurationError('Brackets not allowed in section: %s', $section); - } - $decl = '[' . $section . ']'; - $line = $this->getSectionLine($section); - if ($line !== -1) { - $this->deleteLine($line); - $this->insertAtLine($line, $decl); - } else { - $line = $this->getLastLine(); - $this->insertAtLine($line, $decl); - } - } - - /** - * Refresh the section order of the ini file - * - * @param array $order An array containing the section names in the new order - * Example: array(0 => 'FirstSection', 1 => 'SecondSection') - */ - public function refreshSectionOrder(array $order) - { - $sections = $this->createSectionMap($this->text); - /* - * Move section-less properties to the start of the ordered text - */ - $orderedText = array(); - foreach ($sections['[section-less]'] as $line) { - array_push($orderedText, $line); - } - /* - * Reorder the sections - */ - $len = count($order); - for ($i = 0; $i < $len; $i++) { - if (array_key_exists($i, $order)) { - /* - * Append the lines of the section to the end of the - * ordered text - */ - foreach ($sections[$order[$i]] as $line) { - array_push($orderedText, $line); - } - } - } - $this->text = $orderedText; - } - - /** - * Create a map of sections to lines of a given ini file - * - * @param array $text The text split up in lines - * - * @return array $sectionMap A map containing all sections as arrays of lines. The array of section-less - * lines will be available using they key '[section-less]' which is no valid - * section declaration because it contains brackets. - */ - private function createSectionMap($text) - { - $sections = array('[section-less]' => array()); - $section = '[section-less]'; - $len = count($text); - for ($i = 0; $i < $len; $i++) { - if ($this->isSectionDeclaration($text[$i])) { - $newSection = $this->getSectionFromDeclaration($this->text[$i]); - $sections[$newSection] = array(); - - /* - * Remove comments 'glued' to the new section from the old - * section array and put them into the new section. - */ - $j = $i - 1; - $comments = array(); - while ($j >= 0 && $this->isComment($this->text[$j])) { - array_push($comments, array_pop($sections[$section])); - $j--; - } - $comments = array_reverse($comments); - foreach ($comments as $comment) { - array_push($sections[$newSection], $comment); - } - - $section = $newSection; - } - array_push($sections[$section], $this->text[$i]); - } - return $sections; - } - - /** - * Extract the section name from a section declaration - * - * @param String $declaration The section declaration - * - * @return string The section name - */ - private function getSectionFromDeclaration($declaration) - { - $tmp = preg_split('/(\[|\]|:)/', $declaration); - return trim($tmp[1]); - } - - /** - * Remove a section declaration - * - * @param string $section The section name - */ - public function removeSection($section) - { - $line = $this->getSectionLine($section); - if ($line !== -1) { - $this->deleteLine($line); - } - } - - /** - * Insert the key at the end of the corresponding section - * - * @param array $key The key to insert - * @param mixed $value The value to insert - * @param array $section The key to insert - */ - private function insert(array $key, $value, $section = null) - { - $line = $this->getSectionEnd($section); - $content = $this->formatKeyValuePair($key, $value); - $this->insertAtLine($line, $content); - } - - /** - * Get the edited text - * - * @return string The edited text - */ - public function getText() - { - $this->normalizeSectionSpacing(); - - // trim leading and trailing whitespaces from generated file - $txt = trim(implode(PHP_EOL, $this->text)) . PHP_EOL; - - // replace linebreaks, unless they preceed a comment or a section - return preg_replace("/\n[\n]*([^;\[])/", "\n$1", $txt); - } - - /** - * normalize section spacing according to the current settings - */ - private function normalizeSectionSpacing() - { - $i = count($this->text) - 1; - for (; $i > 0; $i--) { - $line = $this->text[$i]; - if ($this->isSectionDeclaration($line) && $i > 0) { - $i--; - $line = $this->text[$i]; - // ignore comments that are glued to the section declaration - while ($i > 0 && $this->isComment($line)) { - $i--; - $line = $this->text[$i]; - } - // remove whitespaces between the sections - while ($i > 0 && preg_match('/^\s*$/', $line) === 1) { - $this->deleteLine($i); - $i--; - $line = $this->text[$i]; - } - // refresh section separators - if ($i !== 0 && $this->sectionSeparators > 0) { - $this->insertAtLine($i + 1, str_repeat(PHP_EOL, $this->sectionSeparators - 1)); - } - } - } - } - - /** - * Insert the text at line $lineNr - * - * @param $lineNr The line nr the inserted line should have - * @param $toInsert The text that will be inserted - */ - private function insertAtLine($lineNr, $toInsert) - { - $this->text = IniEditor::insertIntoArray($this->text, $lineNr, $toInsert); - } - - /** - * Update the line $lineNr - * - * @param int $lineNr The line number of the target line - * @param string $content The new line content - */ - private function updateLine($lineNr, $content) - { - $comment = $this->getComment($this->text[$lineNr]); - $comment = trim($comment); - if (strlen($comment) > 0) { - $comment = ' ; ' . $comment; - $content = str_pad($content, $this->commentIndentation) . $comment; - } - $this->text[$lineNr] = $content; - } - - /** - * Get the comment from the given line - * - * @param $lineContent The content of the line - * - * @return string The extracted comment - */ - private function getComment($lineContent) - { - /* - * Remove all content in double quotes that is not behind a semicolon, recognizing - * escaped double quotes inside the string - */ - $cleaned = preg_replace('/^[^;"]*"[^"\\\\]*(?:\\\\.[^"\\\\]*)*"/s', '', $lineContent); - - $matches = explode(';', $cleaned, 2); - return array_key_exists(1, $matches) ? $matches[1] : ''; - } - - /** - * Delete the line $lineNr - * - * @param $lineNr The lineNr starting at 0 - */ - private function deleteLine($lineNr) - { - $this->text = $this->removeFromArray($this->text, $lineNr); - } - - /** - * Format a key-value pair to an INI file-entry - * - * @param array $key The key to format - * @param string $value The value to format - * - * @return string The formatted key-value pair - */ - private function formatKeyValuePair(array $key, $value) - { - return str_pad($this->formatKey($key), $this->valueIndentation) . ' = ' . $this->formatValue($value); - } - - /** - * Format a key to an INI key - * - * @param array $key the key array to format - * - * @return string - */ - private function formatKey(array $key) - { - foreach ($key as $i => $separator) { - $key[$i] = $this->sanitize($separator); - } - return implode($this->nestSeparator, $key); - } - - /** - * Get the first line after the given $section - * - * @param $section The name of the section - * - * @return int The line number of the section - */ - private function getSectionEnd($section = null) - { - $i = 0; - $started = isset($section) ? false : true; - foreach ($this->text as $line) { - if ($started && $this->isSectionDeclaration($line)) { - if ($i === 0) { - return $i; - } - /* - * ignore all comments 'glued' to the next section, to allow section - * comments in front of sections - */ - while ($i > 0 && $this->isComment($this->text[$i - 1])) { - $i--; - } - return $i; - } elseif ($this->isSectionDeclaration($line, $section)) { - $started = true; - } - $i++; - } - if (!$started) { - return -1; - } - return $i; - } - - /** - * Check if the given line contains only a comment - */ - private function isComment($line) - { - return preg_match('/^\s*;/', $line) === 1; - } - - /** - * Check if the line contains the property declaration for a key - * - * @param string $lineContent The content of the line - * @param array $key The key this declaration is supposed to have - * - * @return boolean True, when the lineContent is a property declaration - */ - private function isPropertyDeclaration($lineContent, array $key) - { - return preg_match( - '/^\s*' . preg_quote($this->formatKey($key), '/') . '\s*=\s*/', - $lineContent - ) === 1; - } - - /** - * Check if the given line contains a section declaration - * - * @param $lineContent The content of the line - * @param string $section The optional section name that will be assumed - * - * @return bool True, when the lineContent is a section declaration - */ - private function isSectionDeclaration($lineContent, $section = null) - { - if (isset($section)) { - return preg_match('/^\s*\[\s*' . preg_quote(trim($section), '/') . '\s*[\]:]/', $lineContent) === 1; - } else { - return preg_match('/^\s*\[/', $lineContent) === 1; - } - } - - /** - * Get the line where the section begins - * - * @param $section The section - * - * @return int The line number - */ - private function getSectionLine($section) - { - $i = 0; - foreach ($this->text as $line) { - if ($this->isSectionDeclaration($line, $section)) { - return $i; - } - $i++; - } - return -1; - } - - /** - * Get the line number where the given key occurs - * - * @param array $keys The key and its parents - * @param $section The section of the key - * - * @return int The line number - */ - private function getKeyLine(array $keys, $section = null) - { - $key = implode($this->nestSeparator, $keys); - $inSection = isset($section) ? false : true; - $i = 0; - foreach ($this->text as $line) { - if ($inSection && $this->isSectionDeclaration($line)) { - return -1; - } - if ($inSection && $this->isPropertyDeclaration($line, $keys)) { - return $i; - } - if (!$inSection && $this->isSectionDeclaration($line, $section)) { - $inSection = true; - } - $i++; - } - return -1; - } - - /** - * Get the last line number occurring in the text - * - * @return The line number of the last line - */ - private function getLastLine() - { - return count($this->text); - } - - /** - * Insert a new element into a specific position of an array - * - * @param $array The array to use - * @param $pos The target position - * @param $element The element to insert - * - * @return array The changed array - */ - private static function insertIntoArray($array, $pos, $element) - { - array_splice($array, $pos, 0, $element); - return $array; - } - - /** - * Remove an element from an array - * - * @param $array The array to use - * @param $pos The position to remove - * - * @return array The altered array - */ - private function removeFromArray($array, $pos) - { - unset($array[$pos]); - return array_values($array); - } - - /** - * Prepare a value for INI - * - * @param $value The value of the string - * - * @return string The formatted value - * - * @throws Zend_Config_Exception - */ - private function formatValue($value) - { - if (is_integer($value) || is_float($value)) { - return $value; - } elseif (is_bool($value)) { - return ($value ? 'true' : 'false'); - } - return '"' . str_replace('"', '\"', $this->sanitize($value)) . '"'; - } - - private function sanitize($value) - { - return str_replace("\n", '', $value); - } -} diff --git a/library/Icinga/File/Ini/IniParser.php b/library/Icinga/File/Ini/IniParser.php new file mode 100644 index 000000000..4f9813689 --- /dev/null +++ b/library/Icinga/File/Ini/IniParser.php @@ -0,0 +1,217 @@ +commentsPre = $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->commentsPre = $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; + } elseif ($s === '"') { + $state = self::DIRECTIVE_VALUE_QUOTED; + } else { + $state = self::DIRECTIVE_VALUE; + $token = $s; + } + break; + + case self::DIRECTIVE_VALUE: + 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 === "\n") { + self::throwParseError('Unterminated DIRECTIVE_VALUE_QUOTED', $line); + } 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->content = $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->commentPost = $com; + } else { + $sec->commentPost = $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->content = $token; + if ($state === self::COMMENT_END) { + if (isset($dir)) { + $dir->commentPost = $com; + } else { + $sec->commentPost = $com; + } + } else { + $coms[] = $com; + } + break; + + case self::DIRECTIVE_VALUE: + $dir->setValue($token); + $sec->addDirective($dir); + break; + + 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->commentsDangling = $coms; + } + return $doc; + } +} diff --git a/library/Icinga/File/Ini/IniWriter.php b/library/Icinga/File/Ini/IniWriter.php index f7df9b023..f6ad56680 100644 --- a/library/Icinga/File/Ini/IniWriter.php +++ b/library/Icinga/File/Ini/IniWriter.php @@ -3,16 +3,19 @@ namespace Icinga\File\Ini; -use Zend_Config; -use Zend_Config_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 Zend_Config_Writer_FileAbstract; use Icinga\Application\Config; /** * A INI file adapter that respects the file structure and the comments of already existing ini files */ -class IniWriter extends Zend_Config_Writer_FileAbstract +class IniWriter { /** * Stores the options @@ -21,148 +24,137 @@ class IniWriter extends Zend_Config_Writer_FileAbstract */ protected $options; + /** + * The configuration object to write + * + * @var Config + */ + protected $config; + /** * The mode to set on new files * * @var int */ - public static $fileMode = 0660; + protected $fileMode; + + /** + * The path to write to + * + * @var string + */ + protected $filename; /** * Create a new INI writer * - * @param array $options Supports all options of Zend_Config_Writer and additional options: - * * filemode: The mode to set on new files - * * valueIndentation: The indentation level of the values - * * commentIndentation: The indentation level of the comments - * * sectionSeparators: The amount of newlines between sections + * @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(array $options = null) + public function __construct(Config $config, $filename, $filemode = 0660, $options = array()) { - if (isset($options['config']) && $options['config'] instanceof Config) { - // As this class inherits from Zend_Config_Writer_FileAbstract we must - // not pass the config directly as it needs to be of type Zend_Config - $options['config'] = new Zend_Config($options['config']->toArray(), true); - } - + $this->config = $config; + $this->filename = $filename; + $this->fileMode = $filemode; $this->options = $options; - parent::__construct($options); } /** - * Render the Zend_Config into a config file string + * Render the Zend_Config into a config filestring * * @return string */ public function render() { - if (file_exists($this->_filename)) { - $oldconfig = new Zend_Config_Ini($this->_filename); - $content = trim(file_get_contents($this->_filename)); + if (file_exists($this->filename)) { + $oldconfig = Config::fromIni($this->filename); + $content = trim(file_get_contents($this->filename)); } else { - $oldconfig = new Zend_Config(array()); + $oldconfig = Config::fromArray(array()); $content = ''; } - - $newconfig = $this->_config; - $editor = new IniEditor($content, $this->options); - $this->diffConfigs($oldconfig, $newconfig, $editor); - $this->updateSectionOrder($newconfig, $editor); - return $editor->getText(); + $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 Zend_Config $config * @param bool $exclusiveLock + * + * @throws Zend_Config_Exception */ - public function write($filename = null, Zend_Config $config = null, $exclusiveLock = null) + public function write($filename = null, $exclusiveLock = false) { - $filePath = $filename !== null ? $filename : $this->_filename; + $filePath = isset($filename) ? $filename : $this->filename; $setMode = false === file_exists($filePath); - parent::write($filename, $config, $exclusiveLock); + 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) { - $mode = isset($this->options['filemode']) ? $this->options['filemode'] : static::$fileMode; - if (is_int($mode) && false === @chmod($filePath, $mode)) { + // 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)); } } } - /** - * Create a property diff and apply the changes to the editor - * - * @param Zend_Config $oldconfig The config representing the state before the change - * @param Zend_Config $newconfig The config representing the state after the change - * @param IniEditor $editor The editor that should be used to edit the old config file - * @param array $parents The parent keys that should be respected when editing the config - */ - protected function diffConfigs( - Zend_Config $oldconfig, - Zend_Config $newconfig, - IniEditor $editor, - array $parents = array() - ) { - $this->diffPropertyUpdates($oldconfig, $newconfig, $editor, $parents); - $this->diffPropertyDeletions($oldconfig, $newconfig, $editor, $parents); - } - /** * 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(Zend_Config $newconfig, IniEditor $editor) + protected function updateSectionOrder(Config $newconfig, Document $oldDoc) { - $order = array(); - foreach ($newconfig as $key => $value) { - if ($value instanceof Zend_Config) { - array_push($order, $key); - } + $doc = new Document(); + $doc->commentsDangling = $oldDoc->commentsDangling; + foreach ($newconfig->toArray() as $section => $directives) { + $doc->addSection($oldDoc->getSection($section)); } - $editor->refreshSectionOrder($order); + return $doc; } /** * Search for created and updated properties and use the editor to create or update these entries * - * @param Zend_Config $oldconfig The config representing the state before the change - * @param Zend_Config $newconfig The config representing the state after the change - * @param IniEditor $editor The editor that should be used to edit the old config file - * @param array $parents The parent keys that should be respected when editing the config + * @param Config $newconfig The config representing the state after the change + * @param Document $doc + * + * @throws ProgrammingError */ - protected function diffPropertyUpdates( - Zend_Config $oldconfig, - Zend_Config $newconfig, - IniEditor $editor, - array $parents = array() - ) { - // The current section. This value is null when processing the section-less root element - $section = empty($parents) ? null : $parents[0]; - // Iterate over all properties in the new configuration file and search for changes - foreach ($newconfig as $key => $value) { - $oldvalue = $oldconfig->get($key); - $nextParents = array_merge($parents, array($key)); - $keyIdentifier = empty($parents) ? array($key) : array_slice($nextParents, 1, null, true); - if ($value instanceof Zend_Config) { - // The value is a nested Zend_Config, handle it recursively - if ($section === null) { - $editor->setSection($key); - } - if ($oldvalue === null) { - $oldvalue = new Zend_Config(array()); - } - $this->diffConfigs($oldvalue, $value, $editor, $nextParents); + 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 { - // The value is a plain value, use the editor to set it - if (is_numeric($key)) { - $editor->setArrayElement($keyIdentifier, $value, $section); + $domSection = $doc->getSection($section); + } + foreach ($directives as $key => $value) { + if ($value instanceof ConfigObject) { + throw new ProgrammingError('Cannot diff recursive configs'); + } + if ($domSection->hasDirective($key)) { + $domSection->getDirective($key)->setValue($value); } else { - $editor->set($keyIdentifier, $value, $section); + $dir = new Directive($key); + $dir->setValue($value); + $domSection->addDirective($dir); } } } @@ -171,68 +163,35 @@ class IniWriter extends Zend_Config_Writer_FileAbstract /** * Search for deleted properties and use the editor to delete these entries * - * @param Zend_Config $oldconfig The config representing the state before the change - * @param Zend_Config $newconfig The config representing the state after the change - * @param IniEditor $editor The editor that should be used to edit the old config file - * @param array $parents The parent keys that should be respected when editing the config + * @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( - Zend_Config $oldconfig, - Zend_Config $newconfig, - IniEditor $editor, - array $parents = array() - ) { - // The current section. This value is null when processing the section-less root element - $section = empty($parents) ? null : $parents[0]; - - // Iterate over all properties in the old configuration file and search for deleted properties - foreach ($oldconfig as $key => $value) { - if ($newconfig->get($key) === null) { - $nextParents = array_merge($parents, array($key)); - $keyIdentifier = empty($parents) ? array($key) : array_slice($nextParents, 1, null, true); - foreach ($this->getPropertyIdentifiers($value, $keyIdentifier) as $propertyIdentifier) { - $editor->reset($propertyIdentifier, $section); + 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; + } + $newSection = $newconfig->getSection($section); + if (isset($newSection)) { + $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); } } } - - /** - * Return all possible combinations of property identifiers for the given value - * - * @param mixed $value The value to return all combinations for - * @param array $key The root property identifier, if any - * - * @return array All property combinations that are possible - * - * @todo Cannot handle array properties yet (e.g. a.b[]='c') - */ - protected function getPropertyIdentifiers($value, array $key = null) - { - $combinations = array(); - $rootProperty = $key !== null ? $key : array(); - - if ($value instanceof Zend_Config) { - foreach ($value as $subProperty => $subValue) { - $combinations = array_merge( - $combinations, - $this->getPropertyIdentifiers($subValue, array_merge($rootProperty, array($subProperty))) - ); - } - } elseif (is_string($value)) { - $combinations[] = $rootProperty; - } - - return $combinations; - } - - /** - * Getter for filename - * - * @return string - */ - public function getFilename() - { - return $this->_filename; - } } diff --git a/modules/doc/application/controllers/ModuleController.php b/modules/doc/application/controllers/ModuleController.php index 6faf7cc12..f9e9a2e2a 100644 --- a/modules/doc/application/controllers/ModuleController.php +++ b/modules/doc/application/controllers/ModuleController.php @@ -4,6 +4,7 @@ use Icinga\Application\Icinga; use Icinga\Module\Doc\DocController; use Icinga\Module\Doc\Exception\DocException; +use Icinga\File\Ini\Parser; class Doc_ModuleController extends DocController { diff --git a/test/php/library/Icinga/File/Ini/IniWriterTest.php b/test/php/library/Icinga/File/Ini/IniWriterTest.php index c83995715..a45e33e7b 100644 --- a/test/php/library/Icinga/File/Ini/IniWriterTest.php +++ b/test/php/library/Icinga/File/Ini/IniWriterTest.php @@ -31,86 +31,28 @@ class IniWriterTest extends BaseTestCase public function testWhetherPointInSectionIsNotNormalized() { $writer = new IniWriter( - array( - 'config' => Config::fromArray( - array( - 'section' => array( - 'foo.bar' => 1337 - ), - 'section.with.multiple.dots' => array( - 'some more' => array( - 'nested stuff' => 'With more values' - ) - ) - ) + Config::fromArray( + array( + 'section' => array( + 'foo.bar' => 1337 ), - 'filename' => $this->tempFile - ) + 'section.with.multiple.dots' => array( + 'some more.nested stuff' => 'With more values' + ) + ) + ), + $this->tempFile ); $writer->write(); $config = Config::fromIni($this->tempFile)->toArray(); $this->assertTrue(array_key_exists('section.with.multiple.dots', $config), 'Section names not normalized'); } - public function testWhetherSimplePropertiesAreInsertedInEmptyFiles() - { - $this->markTestSkipped('Implementation has changed. Section-less properties are not supported anymore'); - $target = $this->writeConfigToTemporaryFile(''); - $config = Config::fromArray(array('key' => 'value')); - $writer = new IniWriter(array('config' => $config, 'filename' => $target)); - $writer->write(); - - $newConfig = Config::fromIni($target); - $this->assertEquals('value', $newConfig->get('key'), 'IniWriter does not insert in empty files'); - } - - public function testWhetherSimplePropertiesAreInsertedInExistingFiles() - { - $this->markTestSkipped('Implementation has changed. Section-less properties are not supported anymore'); - $target = $this->writeConfigToTemporaryFile('key1 = "1"'); - $config = Config::fromArray(array('key2' => '2')); - $writer = new IniWriter(array('config' => $config, 'filename' => $target)); - $writer->write(); - - $newConfig = Config::fromIni($target); - $this->assertEquals('2', $newConfig->get('key2'), 'IniWriter does not insert in existing files'); - } - - /** - * @depends testWhetherSimplePropertiesAreInsertedInExistingFiles - */ - public function testWhetherSimplePropertiesAreUpdated() - { - $this->markTestSkipped('Implementation has changed. Section-less properties are not supported anymore'); - $target = $this->writeConfigToTemporaryFile('key = "value"'); - $config = Config::fromArray(array('key' => 'eulav')); - $writer = new IniWriter(array('config' => $config, 'filename' => $target)); - $writer->write(); - - $newConfig = Config::fromIni($target); - $this->assertEquals('eulav', $newConfig->get('key'), 'IniWriter does not update simple properties'); - } - - /** - * @depends testWhetherSimplePropertiesAreInsertedInExistingFiles - */ - public function testWhetherSimplePropertiesAreDeleted() - { - $this->markTestSkipped('Implementation has changed. Section-less properties are not supported anymore'); - $target = $this->writeConfigToTemporaryFile('key = "value"'); - $config = new Config(); - $writer = new IniWriter(array('config' => $config, 'filename' => $target)); - $writer->write(); - - $newConfig = Config::fromIni($target); - $this->assertNull($newConfig->get('key'), 'IniWriter does not delete simple properties'); - } - public function testWhetherNestedPropertiesAreInserted() { $target = $this->writeConfigToTemporaryFile(''); $config = Config::fromArray(array('a' => array('b' => 'c'))); - $writer = new IniWriter(array('config' => $config, 'filename' => $target)); + $writer = new IniWriter($config, $target); $writer->write(); $newConfig = Config::fromIni($target); @@ -126,236 +68,54 @@ class IniWriterTest extends BaseTestCase ); } - /** - * @depends testWhetherNestedPropertiesAreInserted - */ - public function testWhetherNestedPropertiesAreUpdated() - { - $this->markTestSkipped('Implementation has changed. Section-less properties are not supported anymore'); - $target = $this->writeConfigToTemporaryFile('a.b = "c"'); - $config = Config::fromArray(array('a' => array('b' => 'cc'))); - $writer = new IniWriter(array('config' => $config, 'filename' => $target)); - $writer->write(); - - $newConfig = Config::fromIni($target); - $this->assertInstanceOf( - get_class($newConfig), - $newConfig->get('a'), - 'IniWriter does not update nested properties' - ); - $this->assertEquals( - 'cc', - $newConfig->get('a')->get('b'), - 'IniWriter does not update nested properties' - ); - } - - /** - * @depends testWhetherNestedPropertiesAreInserted - */ - public function testWhetherNestedPropertiesAreDeleted() - { - $this->markTestSkipped('Implementation has changed. Section-less properties are not supported anymore'); - $target = $this->writeConfigToTemporaryFile('a.b = "c"'); - $config = new Config(); - $writer = new IniWriter(array('config' => $config, 'filename' => $target)); - $writer->write(); - - $newConfig = Config::fromIni($target); - $this->assertNull( - $newConfig->get('a'), - 'IniWriter does not delete nested properties' - ); - } - - public function testWhetherSimpleSectionPropertiesAreInserted() - { - $target = $this->writeConfigToTemporaryFile(''); - $config = Config::fromArray(array('section' => array('key' => 'value'))); - $writer = new IniWriter(array('config' => $config, 'filename' => $target)); - $writer->write(); - - $newConfig = Config::fromIni($target); - $this->assertInstanceOf( - 'Icinga\Data\ConfigObject', - $newConfig->getSection('section'), - 'IniWriter does not insert sections' - ); - $this->assertEquals( - 'value', - $newConfig->getSection('section')->get('key'), - 'IniWriter does not insert simple section properties' - ); - } - - /** - * @depends testWhetherSimpleSectionPropertiesAreInserted - */ - public function testWhetherSimpleSectionPropertiesAreUpdated() - { - $target = $this->writeConfigToTemporaryFile(<<<'EOD' -[section] -key = "value" -EOD - ); - $config = Config::fromArray(array('section' => array('key' => 'eulav'))); - $writer = new IniWriter(array('config' => $config, 'filename' => $target)); - $writer->write(); - - $newConfig = Config::fromIni($target); - $this->assertEquals( - 'eulav', - $newConfig->getSection('section')->get('key'), - 'IniWriter does not update simple section properties' - ); - } - - /** - * @depends testWhetherSimpleSectionPropertiesAreInserted - */ - public function testWhetherSimpleSectionPropertiesAreDeleted() - { - $target = $this->writeConfigToTemporaryFile(<<<'EOD' -[section] -key = "value" -EOD - ); - $config = Config::fromArray(array('section' => array())); - $writer = new IniWriter(array('config' => $config, 'filename' => $target)); - $writer->write(); - - $newConfig = Config::fromIni($target); - $this->assertNull( - $newConfig->getSection('section')->get('key'), - 'IniWriter does not delete simple section properties' - ); - } - - public function testWhetherNestedSectionPropertiesAreInserted() - { - $this->markTestSkipped('Implementation has changed. Config::fromIni cannot handle nested properties anymore'); - $target = $this->writeConfigToTemporaryFile(''); - $config = Config::fromArray(array('section' => array('a' => array('b' => 'c')))); - $writer = new IniWriter(array('config' => $config, 'filename' => $target)); - $writer->write(); - - $newConfig = Config::fromIni($target); - $this->assertInstanceOf( - get_class($newConfig), - $newConfig->get('section'), - 'IniWriter does not insert sections' - ); - $this->assertInstanceOf( - get_class($newConfig), - $newConfig->get('section')->get('a'), - 'IniWriter does not insert nested section properties' - ); - $this->assertEquals( - 'c', - $newConfig->get('section')->get('a')->get('b'), - 'IniWriter does not insert nested section properties' - ); - } - - /** - * @depends testWhetherNestedSectionPropertiesAreInserted - */ - public function testWhetherNestedSectionPropertiesAreUpdated() - { - $target = $this->writeConfigToTemporaryFile(<<<'EOD' -[section] -a.b = "c" -EOD - ); - $config = Config::fromArray(array('section' => array('a' => array('b' => 'cc')))); - $writer = new IniWriter(array('config' => $config, 'filename' => $target)); - $writer->write(); - - $newConfig = Config::fromIni($target); - $this->assertEquals( - 'cc', - $newConfig->get('section')->get('a')->get('b'), - 'IniWriter does not update nested section properties' - ); - } - - /** - * @depends testWhetherNestedSectionPropertiesAreInserted - */ - public function testWhetherNestedSectionPropertiesAreDeleted() - { - $target = $this->writeConfigToTemporaryFile(<<<'EOD' -[section] -a.b = "c" -EOD - ); - $config = Config::fromArray(array('section' => array())); - $writer = new IniWriter(array('config' => $config, 'filename' => $target)); - $writer->write(); - - $newConfig = Config::fromIni($target); - $this->assertNull( - $newConfig->get('section')->get('a'), - 'IniWriter does not delete nested section properties' - ); - } - public function testWhetherSectionOrderIsUpdated() { $config = <<<'EOD' [one] -key1 = "1" -key2 = "2" +key1 = "1" +key2 = "2" [two] -a.b = "c" -d.e = "f" +a.b = "c" +d.e = "f" [three] -key = "value" -foo.bar = "raboof" +key = "value" +foo.bar = "raboof" EOD; $reverted = <<<'EOD' [three] -key = "value" -foo.bar = "raboof" +key = "value" +foo.bar = "raboof" [two] -a.b = "c" -d.e = "f" +a.b = "c" +d.e = "f" [one] -key1 = "1" -key2 = "2" +key1 = "1" +key2 = "2" EOD; $target = $this->writeConfigToTemporaryFile($config); $writer = new IniWriter( - array( - 'config' => Config::fromArray( - array( - 'three' => array( - 'foo' => array( - 'bar' => 'raboof' - ), - 'key' => 'value' - ), - 'two' => array( - 'd' => array( - 'e' => 'f' - ), - 'a' => array( - 'b' => 'c' - ) - ), - 'one' => array( - 'key2' => '2', - 'key1' => '1' - ) + Config::fromArray( + array( + 'three' => array( + 'foo.bar' => 'raboof', + 'key' => 'value' + ), + 'two' => array( + 'd.e' => 'f', + 'a.b' => 'c' + ), + 'one' => array( + 'key2' => '2', + 'key1' => '1' ) - ), - 'filename' => $target - ) + ) + ), + $target ); $this->assertEquals( @@ -384,15 +144,13 @@ EOD; EOD; $target = $this->writeConfigToTemporaryFile($config); $writer = new IniWriter( - array( - 'config' => Config::fromArray( - array( - 'two' => array(), - 'one' => array() - ) - ), - 'filename' => $target - ) + Config::fromArray( + array( + 'two' => array(), + 'one' => array() + ) + ), + $target ); $this->assertEquals( @@ -407,14 +165,14 @@ EOD; { $config = <<<'EOD' ; some interesting comment -key = "value" -; another interesting comment +[blarg] +key = "value" + +; some dangling comment ; boring comment EOD; $target = $this->writeConfigToTemporaryFile($config); - $writer = new IniWriter( - array('config' => Config::fromArray(array('key' => 'value')), 'filename' => $target) - ); + $writer = new IniWriter(Config::fromArray(array('blarg' => array('key' => 'value'))), $target); $this->assertEquals( trim($config), @@ -426,26 +184,24 @@ EOD; public function testWhetherCommentsOnPropertyLinesArePreserved() { $config = <<<'EOD' -foo = 1337 ; I know what a " and a ' is -bar = 7331 ; I; tend; to; overact; !1!1!!11!111! ; -key = "value" ; some comment for a small sized property -xxl = "very loooooooooooooooooooooong" ; my value is very lo... +[blarg] +foo = "1337" ; I know what a " and a ' is +bar = "7331" ; I; tend; to; overact; !1!1!!11!111! ; +key = "value" ; some comment for a small sized property +xxl = "very loooooooooooooooooooooong" ; my value is very lo... EOD; $target = $this->writeConfigToTemporaryFile($config); $writer = new IniWriter( - array( - 'config' => Config::fromArray( - array( - 'foo' => 1337, - 'bar' => 7331, - 'key' => 'value', - 'xxl' => 'very loooooooooooooooooooooong' - ) - ), - 'filename' => $target - ) + Config::fromArray( + array('blarg' => array( + 'foo' => 1337, + 'bar' => 7331, + 'key' => 'value', + 'xxl' => 'very loooooooooooooooooooooong' + )) + ), + $target ); - $this->assertEquals( trim($config), trim($writer->render()), @@ -458,12 +214,10 @@ EOD; $config = <<<'EOD' [section] ; some interesting comment, in a section -key = "value" +key = "value" EOD; $target = $this->writeConfigToTemporaryFile($config); - $writer = new IniWriter( - array('config' => Config::fromArray(array('section' => array('key' => 'value'))), 'filename' => $target) - ); + $writer = new IniWriter(Config::fromArray(array('section' => array('key' => 'value'))), $target); $this->assertEquals( trim($config), @@ -476,26 +230,24 @@ EOD; { $config = <<<'EOD' [section] -foo = 1337 ; I know what a " and a ' is -bar = 7331 ; I; tend; to; overact; !1!1!!11!111! ; -key = "value" ; some comment for a small sized property -xxl = "very loooooooooooooooooooooong" ; my value is very lo... +foo = "1337" ; I know what a " and a ' is +bar = "7331" ; I; tend; to; overact; !1!1!!11!111! ; +key = "value" ; some comment for a small sized property +xxl = "very loooooooooooooooooooooong" ; my value is very lo... EOD; $target = $this->writeConfigToTemporaryFile($config); $writer = new IniWriter( - array( - 'config' => Config::fromArray( - array( - 'section' => array( - 'foo' => 1337, - 'bar' => 7331, - 'key' => 'value', - 'xxl' => 'very loooooooooooooooooooooong' - ) + Config::fromArray( + array( + 'section' => array( + 'foo' => 1337, + 'bar' => 7331, + 'key' => 'value', + 'xxl' => 'very loooooooooooooooooooooong' ) - ), - 'filename' => $target - ) + ) + ), + $target ); $this->assertEquals( @@ -509,19 +261,17 @@ EOD; { $target = $this->writeConfigToTemporaryFile(''); $writer = new IniWriter( - array( - 'config' => Config::fromArray( - array( - 'section' => array( - 'foo' => 'linebreak + Config::fromArray( + array( + 'section' => array( + 'foo' => 'linebreak in line', - 'linebreak + 'linebreak inkey' => 'blarg' - ) ) - ), - 'filename' => $target - ) + ) + ), + $target ); $rendered = $writer->render(); @@ -536,30 +286,28 @@ inkey' => 'blarg' { $config = <<<'EOD' [section 1] -foo = 1337 +foo = "1337" [section (with special chars)] -foo = "baz" +foo = "baz" [section/as/arbitrary/path] -foo = "nope" +foo = "nope" [section.with.dots.in.it] -foo = "bar" +foo = "bar" EOD; $target = $this->writeConfigToTemporaryFile($config); $writer = new IniWriter( - array( - 'config' => Config::fromArray( - array( - 'section 1' => array('foo' => 1337), - 'section (with special chars)' => array('foo' => 'baz'), - 'section/as/arbitrary/path' => array('foo' => 'nope'), - 'section.with.dots.in.it' => array('foo' => 'bar') - ) - ), - 'filename' => $target - ) + Config::fromArray( + array( + 'section 1' => array('foo' => 1337), + 'section (with special chars)' => array('foo' => 'baz'), + 'section/as/arbitrary/path' => array('foo' => 'nope'), + 'section.with.dots.in.it' => array('foo' => 'bar') + ) + ), + $target ); $this->assertEquals( @@ -569,6 +317,46 @@ EOD; ); } + public function testSectionDeleted() + { + $config = <<<'EOD' +[section 1] +guarg = "1" + +[section 2] +foo = "1337" +foo2 = "baz" +foo3 = "nope" +foo4 = "bar" + +[section 3] +guard = "2" +EOD; + $deleted = <<<'EOD' +[section 1] +guarg = "1" + +[section 3] +guard = "2" +EOD; + + $target = $this->writeConfigToTemporaryFile($config); + $writer = new IniWriter( + Config::fromArray(array( + 'section 1' => array('guarg' => 1), + 'section 3' => array('guard' => 2) + )), + $target + ); + + $this->assertEquals( + trim($deleted), + trim($writer->render()), + 'IniWriter does not delete sections properly' + ); + } + + /** * Write a INI-configuration string to a temporary file and return its path *