From edebbf93ab850dcabfa3c0e8d4f13db8c8645ae0 Mon Sep 17 00:00:00 2001 From: Matthias Jentsch Date: Thu, 1 Aug 2013 09:18:14 +0200 Subject: [PATCH 1/5] Add an IniWriter that preserves the comments in already existing files Add a function to create diffs between two object, add a FileEditor to edit files line-by-line and add the class PropertyDiff to describe a single property change. refs #4352 --- .../Icinga/{Application => Config}/Config.php | 3 +- library/Icinga/Config/PreservingIniWriter.php | 311 ++++++++++++++++++ 2 files changed, 312 insertions(+), 2 deletions(-) rename library/Icinga/{Application => Config}/Config.php (97%) create mode 100644 library/Icinga/Config/PreservingIniWriter.php diff --git a/library/Icinga/Application/Config.php b/library/Icinga/Config/Config.php similarity index 97% rename from library/Icinga/Application/Config.php rename to library/Icinga/Config/Config.php index b23bc268b..7eeef1ce0 100755 --- a/library/Icinga/Application/Config.php +++ b/library/Icinga/Config/Config.php @@ -28,7 +28,6 @@ namespace Icinga\Application; -use Icinga\Protocol\Ldap\Exception; use Zend_Config_Ini; /** @@ -71,7 +70,7 @@ class Config extends Zend_Config_Ini public function __construct($filename) { if (!@is_readable($filename)) { - throw new Exception('Cannot read config file: ' . $filename); + throw new \Exception('Cannot read config file: ' . $filename); }; $this->configFile = $filename; $section = null; diff --git a/library/Icinga/Config/PreservingIniWriter.php b/library/Icinga/Config/PreservingIniWriter.php new file mode 100644 index 000000000..5605d1c28 --- /dev/null +++ b/library/Icinga/Config/PreservingIniWriter.php @@ -0,0 +1,311 @@ + + * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2 + * @author Icinga Development Team + */ +// {{{ICINGA_LICENSE_HEADER}}} + +namespace Icinga\Config; + +/** + * A ini file adapter that preserves comments in the existing ini file, when writing changes to it + */ +class PreservingIniWriter extends \Zend_Config_Writer +{ + /** + * The file that is written to + * + * @var string + */ + private $filename; + + public function setFilename($filename) + { + $this->filename = $filename; + } + + function __construct(array $options) + { + parent::__construct($options); + } + + /** + * Write the config to the file + */ + public function write() + { + if (empty($this->filename)) { + throw new Exception('No filename for configuration provided'); + } + $oldConfig = parse_ini_file($this->filename); + $newConfig = $this->_config; + $diff = $this->createPropertyDiff($oldConfig,$newConfig); + } + + /** + * Create a diff between the properties of two Zend_Config objects + * + * @param \Zend_Config $oldConfig + * @param \Zend_Config $newConfig + */ + private function createConfigDiff(\Zend_Config $oldConfig, \Zend_Config $newConfig) + { + // TODO: Find deleted sections in old + } + + private function updateIniWithDiff($fileDiff) + { + $iniReader = new IniKeyPositionReader(); + $editor = new FileEditor($this->filename); + foreach ($fileDiff as $key => $diff) { + + } + } + + private function unwrapKeys($parents,$diffs) + { + + } +} + +/** + * Can read information about the position of ini-file keys and values + * from + */ +class IniKeyPositionReader +{ + public function getKeyStart(array $parents,String $key) + { + // return line + } + + public function getKeyContainerFirstEmpty(String $section) + { + // return line + } + + public function getKeyLine(String $section,String $key) + { + // return line + } +} + +/** + * Edit a file line by line + * + * The functions delete, insert and update can be applied to certain lines + * of the file and are written to it, once applyChanges is called. Line inserts and deletes + * are handled automatically and the changes in line numbers don't need to be respected when + * calling the edit functions. + */ +class FileEditor +{ + /** + * @var String + */ + private $filename; + + /** + * The symbol that delimits a comment. + * + * @var string + */ + private $commentDelimiter = ''; + + /** + * Set a new comment delimiter + */ + public function setCommentDelimiter(String $delimiter) + { + $this->commentDelimiter = $delimiter; + return $this; + } + + /** + * Get the current comment delimiter + * + * @return string The comment delimiter + */ + public function getCommentDelimiter() + { + return $this->commentDelimiter; + } + + /** + * Create a new FileEditor + * + * @param $filename The file that should be edited. + */ + public function __constructor($filename) + { + $this->filename = $filename; + } + + /** + * Delete a line + * + * @param $line The line + */ + public function delete($line) + { + + } + + /** + * Insert a text into the file + * + * @param $line The line where the text should be inserted + * @param $text The text + */ + public function insert($line,$text) + { + + } + + /** + * Update the given line and insert $text + * + * Update the line but ignore text separated by a comment delimiter. + * + * @param $line The line number + * @param $text The text that will be inserted + */ + public function update($line,$text) + { + + } + + /** + * Write changes to the file + */ + public function applyChanges() + { + + } +} + +/** + * A diff that describes the change of an object property + */ +class PropertyDiff { + + /** + * Create the property diff between two objects + * + * @param stdClass $oldObject The object representing the state before the change + * @param stdClass $newObject The object representing the state after the change + * + * @return array An associative array mapping all changed properties to a property diff + * describing the change + */ + public static function createObjectDiff(stdClass $oldObject,stdClass $newObject) + { + $diffs = array(); + /* + * Search inserted or updated properties + */ + foreach ($newObject as $key => $value) { + $newProperty = $value; + $oldProperty = $oldObject->{$key}; + if (is_array($newProperty)) { + if (empty($oldProperty)) { + $diffs[$key] = new PropertyDiff( + PropertyDiff::ACTION_INSERT, + PropertyDiff::createObjectDiff(new \stdClass(),$newObject)); + } else { + $diffs[$key] = new PropertyDiff( + PropertyDiff::ACTION_NONE, + PropertyDiff::createObjectDiff($oldObject,$newObject) + ); + } + } else { + if (empty($oldProperty)) { + $diffs[$key] = + new PropertyDiff(PropertyDiff::ACTION_INSERT,$newProperty); + } elseif (strcasecmp($newProperty,$oldProperty) != 0) { + $diffs[$key] = + new PropertyDiff(PropertyDiff::ACTION_UPDATE,$newProperty); + } + } + } + /* + * Search deleted properties + */ + foreach ($oldObject as $key => $value) { + if (empty($newObject->{$key})) { + $oldProperty = $value; + if (is_array($oldProperty)){ + $diffs[key] = new PropertyDiff( + PropertyDiff::ACTION_DELETE, + PropertyDiff::createObjectDiff($oldObject,new \stdClass()) + ); + } else { + $diffs[$key] = + new PropertyDiff(PropertyDiff::ACTION_DELETE,null); + } + } + } + } + + /** + * The available action types + */ + const ACTION_INSERT = 0; + const ACTION_UPDATE = 1; + const ACTION_DELETE = 2; + const ACTION_NONE = 3; + + /** + * The action described by this diff + * + * @var String + */ + public $action; + + /** + * The value after the change + * + * @var StdClass + */ + public $value; + + /** + * Create a new PropertyDiff + * + * @param int $action The action described by this diff + * @param string $value The value after the change + */ + public function Diff($action, $value) + { + if (action != ACTION_CREATE && + action != ACTION_UPDATE && + action != ACTION_DELETE) { + throw new \Exception('Invalid action code: '.$action); + } + $this->action = $action; + $this->value = $value; + } +} + From 56e47fd08415c51e38a0c33c99875081afafac4f Mon Sep 17 00:00:00 2001 From: Matthias Jentsch Date: Tue, 6 Aug 2013 16:28:26 +0200 Subject: [PATCH 2/5] Add an ini writer for configuration files Add an ini writer that respects the file structure and the comments that may be already present in the config file. Move Application/Config.php into Config/Config.php. refs #4352 --- .../forms/Authentication/SettingsForm.php | 2 +- doc/CONFIG.md | 4 +- .../Application/ApplicationBootstrap.php | 1 + library/Icinga/Application/Modules/Module.php | 2 +- .../Backend/LdapUserBackend.php | 2 +- library/Icinga/Authentication/Manager.php | 2 +- library/Icinga/Backend.php | 2 +- library/Icinga/Config/Config.php | 2 +- library/Icinga/Config/PreservingIniWriter.php | 685 ++++++++++++------ library/Icinga/Protocol/Ldap/Connection.php | 2 +- library/Icinga/Web/ActionController.php | 2 +- library/Icinga/Web/ModuleActionController.php | 2 +- library/Icinga/Web/Widget/Dashboard.php | 2 +- .../controllers/CommandController.php | 2 +- .../monitoring/library/Monitoring/Backend.php | 2 +- .../library/Monitoring/Environment.php | 2 +- .../controllers/IndexControllerTest.php | 2 +- .../Authentication/DbUserBackendTest.php | 4 +- .../{Application => Config}/ConfigTest.php | 4 +- .../Icinga/Config/PreservingIniWriterTest.php | 329 +++++++++ 20 files changed, 823 insertions(+), 232 deletions(-) rename test/php/library/Icinga/{Application => Config}/ConfigTest.php (95%) create mode 100644 test/php/library/Icinga/Config/PreservingIniWriterTest.php diff --git a/application/forms/Authentication/SettingsForm.php b/application/forms/Authentication/SettingsForm.php index 69e8db257..df87e8afe 100644 --- a/application/forms/Authentication/SettingsForm.php +++ b/application/forms/Authentication/SettingsForm.php @@ -28,7 +28,7 @@ namespace Icinga\Web\Form; use Icinga\Web\Form; use Icinga\Web\Session; use Icinga\Web\Notification; -use Icinga\Application\Config; +use Icinga\Config\Config; /** * Class SettingsForm diff --git a/doc/CONFIG.md b/doc/CONFIG.md index 5ebaa607b..1085c61ec 100644 --- a/doc/CONFIG.md +++ b/doc/CONFIG.md @@ -1,13 +1,13 @@ # Application and Module Configuration -The \Icinga\Application\Config class is a general purpose service to help you find, load and save +The \Icinga\Config\Config class is a general purpose service to help you find, load and save configuration data. It is used both by the Icinga 2 Web modules and the framework itself. With INI files as source it enables you to store configuration in a familiar format. Icinga 2 Web defines some configuration files for its own purposes. Please note that both modules and framework keep their main configuration in the INI file called config.ini. Here's some example code: global->get('defaultTimezone', 'Europe/Berlin'); diff --git a/library/Icinga/Application/ApplicationBootstrap.php b/library/Icinga/Application/ApplicationBootstrap.php index 7ebaa55bc..a5890aae6 100755 --- a/library/Icinga/Application/ApplicationBootstrap.php +++ b/library/Icinga/Application/ApplicationBootstrap.php @@ -31,6 +31,7 @@ namespace Icinga\Application; use Icinga\Application\Modules\Manager as ModuleManager; use Icinga\Application\Platform; use Icinga\Exception\ProgrammingError; +use Icinga\Config\Config; use Zend_Loader_Autoloader; use Icinga\Exception\ConfigurationError; diff --git a/library/Icinga/Application/Modules/Module.php b/library/Icinga/Application/Modules/Module.php index 2cac2b1a6..530c1ace4 100644 --- a/library/Icinga/Application/Modules/Module.php +++ b/library/Icinga/Application/Modules/Module.php @@ -29,7 +29,7 @@ namespace Icinga\Application\Modules; use Icinga\Application\ApplicationBootstrap; -use Icinga\Application\Config; +use Icinga\Config\Config; use Icinga\Application\Icinga; use Icinga\Web\Hook; use Zend_Controller_Router_Route as Route; diff --git a/library/Icinga/Authentication/Backend/LdapUserBackend.php b/library/Icinga/Authentication/Backend/LdapUserBackend.php index 01b8f0768..2892e1c57 100644 --- a/library/Icinga/Authentication/Backend/LdapUserBackend.php +++ b/library/Icinga/Authentication/Backend/LdapUserBackend.php @@ -32,7 +32,7 @@ use Icinga\User; use Icinga\Authentication\UserBackend; use Icinga\Authentication\Credentials; use Icinga\Protocol\Ldap; -use Icinga\Application\Config as IcingaConfig; +use Icinga\Config\Config as IcingaConfig; /** * User authentication backend (@see Icinga\Authentication\UserBackend) for diff --git a/library/Icinga/Authentication/Manager.php b/library/Icinga/Authentication/Manager.php index 3edc40a2f..f3ef103cd 100644 --- a/library/Icinga/Authentication/Manager.php +++ b/library/Icinga/Authentication/Manager.php @@ -29,7 +29,7 @@ namespace Icinga\Authentication; use Icinga\Application\Logger; -use Icinga\Application\Config as IcingaConfig; +use Icinga\Config\Config as IcingaConfig; use Icinga\Exception\ConfigurationError as ConfigError; use Icinga\User; diff --git a/library/Icinga/Backend.php b/library/Icinga/Backend.php index bf4f89eba..ef347915f 100755 --- a/library/Icinga/Backend.php +++ b/library/Icinga/Backend.php @@ -2,7 +2,7 @@ namespace Icinga; -use Icinga\Application\Config as IcingaConfig; +use Icinga\Config\Config as IcingaConfig; use Icinga\Authentication\Manager as AuthManager; class Backend diff --git a/library/Icinga/Config/Config.php b/library/Icinga/Config/Config.php index 7eeef1ce0..74dc64be9 100755 --- a/library/Icinga/Config/Config.php +++ b/library/Icinga/Config/Config.php @@ -26,7 +26,7 @@ */ // {{{ICINGA_LICENSE_HEADER}}} -namespace Icinga\Application; +namespace Icinga\Config; use Zend_Config_Ini; diff --git a/library/Icinga/Config/PreservingIniWriter.php b/library/Icinga/Config/PreservingIniWriter.php index 5605d1c28..045891fe4 100644 --- a/library/Icinga/Config/PreservingIniWriter.php +++ b/library/Icinga/Config/PreservingIniWriter.php @@ -1,5 +1,4 @@ filename = $filename; - } - function __construct(array $options) { parent::__construct($options); } /** - * Write the config to the file - */ - public function write() - { - if (empty($this->filename)) { - throw new Exception('No filename for configuration provided'); - } - $oldConfig = parse_ini_file($this->filename); - $newConfig = $this->_config; - $diff = $this->createPropertyDiff($oldConfig,$newConfig); - } - - /** - * Create a diff between the properties of two Zend_Config objects + * Render the Zend_Config into a config file string * - * @param \Zend_Config $oldConfig - * @param \Zend_Config $newConfig + * @return string */ - private function createConfigDiff(\Zend_Config $oldConfig, \Zend_Config $newConfig) + public function render() { - // TODO: Find deleted sections in old + $oldconfig = new \Zend_Config_Ini($this->_filename); + $newconfig = $this->_config; + $editor = new IniEditor(file_get_contents($this->_filename)); + $this->diffConfigs($oldconfig,$newconfig,$editor); + return $editor->getText(); } - private function updateIniWithDiff($fileDiff) + /** + * Create a property diff and apply the changes to the editor + * + * Compare two Zend_Config that represent the state change of an ini file and use the + * IniEditor to write the changes back to the config, while preserving the structure and + * the comments of the original file. + * + * @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 + */ + private function diffConfigs( + \Zend_Config $oldconfig, + \Zend_Config $newconfig, + IniEditor $editor, + array $parents = array()) { - $iniReader = new IniKeyPositionReader(); - $editor = new FileEditor($this->filename); - foreach ($fileDiff as $key => $diff) { - + foreach ($newconfig as $key => $value) { + $oldvalue = $oldconfig->get($key); + $fullKey = array_merge($parents,array($key)); + if ($value instanceof \Zend_Config) { + if (empty($parents)) { + $extends = $newconfig->getExtends(); + $extend = array_key_exists($key,$extends) ? $extends[$key] : null; + $editor->setSection($key,$extend); + } + if (!isset($oldvalue)) { + $this->diffConfigs(new \Zend_Config(array()),$value,$editor,$fullKey); + } else { + $this->diffConfigs($oldvalue,$value,$editor,$fullKey); + } + } else { + if (is_numeric($key)){ + $editor->setArrayEl($fullKey,$value); + } else { + $editor->set($fullKey,$value); + } + } + } + foreach ($oldconfig as $key => $value) { + $fullKey = array_merge($parents,array($key)); + $o = $newconfig->get($key); + if (!isset($o)) { + if ($value instanceof \Zend_Config) { + $this->diffConfigs( + $value,new \Zend_Config(array()),$editor,$fullKey + ); + $editor->removeSection($key); + } else { + if (is_numeric($key)) { + $editor->delArrayEl($fullKey); + } else { + $editor->reset($fullKey); + } + } + } } } - - private function unwrapKeys($parents,$diffs) - { - - } } -/** - * Can read information about the position of ini-file keys and values - * from - */ -class IniKeyPositionReader -{ - public function getKeyStart(array $parents,String $key) - { - // return line - } - - public function getKeyContainerFirstEmpty(String $section) - { - // return line - } - - public function getKeyLine(String $section,String $key) - { - // return line - } -} /** - * Edit a file line by line - * - * The functions delete, insert and update can be applied to certain lines - * of the file and are written to it, once applyChanges is called. Line inserts and deletes - * are handled automatically and the changes in line numbers don't need to be respected when - * calling the edit functions. + * Edit the sections and keys of an ini in-place */ -class FileEditor +class IniEditor { /** - * @var String - */ - private $filename; - - /** - * The symbol that delimits a comment. + * The text that is edited * * @var string */ - private $commentDelimiter = ''; + private $text; /** - * Set a new comment delimiter + * The symbol that is used + * + * @var string */ - public function setCommentDelimiter(String $delimiter) + private $nestSeparator = '.'; + + /** + * Get the nest separator + * + * @return string The nest separator + */ + public function getNestSeparator() { - $this->commentDelimiter = $delimiter; + return $this->nestSeparator; + } + + /** + * Set the nest separator + * + * @param $separator The nest separator + * @return mixed The current instance of IniReader + */ + public function setNestSeparator($separator) + { + $this->nestSeparator = $separator; return $this; } /** - * Get the current comment delimiter + * Create a new IniEditor * - * @return string The comment delimiter + * @param $content The content of the ini as string */ - public function getCommentDelimiter() + public function __construct($content) { - return $this->commentDelimiter; + $this->text = explode("\n",$content); } /** - * Create a new FileEditor + * Set the value of the given key. * - * @param $filename The file that should be edited. + * Update the key, if it already exists, otherwise create it. + * + * @param array $key + * @param $value */ - public function __constructor($filename) + public function set(array $key,$value) { - $this->filename = $filename; + $line = $this->getKeyLine($key); + if ($line === -1) { + $this->insert($key,$value); + return; + } + $content = $this->formatKeyValuePair( + $this->truncateSection($key),$value); + $this->updateLine($line,$content); + } + + public function delArrayEl(array $key) + { + $line = $this->getArrayEl($key); + if ($line !== -1) { + $this->deleteLine($line); + } + } + + public function setArrayEl(array $key,$value) + { + $line = $this->getArrayEl($key); + if (count($key) > 1) { + $ident = $this->truncateSection($key); + $section = $key[0]; + } else { + $ident = $key; + $section = null; + } + if ($line !== -1) { + if (count($ident) > 1){ + $this->updateLine($line,$this->formatKeyValuePair($ident,$value)); + } else { + // move into own section + $this->deleteLine($line); + $this->setSection($section); + $this->insert(array_merge(array($section),$ident),$value); + } + } else { + $e = $this->getSectionEnd($section); + $this->insertAtLine($e,$this->formatKeyValuePair($ident,$value)); + } } /** - * Delete a line + * Get the line of an array element * - * @param $line The line + * @param array $key The key of the property. + * @param $value The value */ - public function delete($line) + private function getArrayEl(array $key) { + $line = 0; + if (count($key) > 1) { + $line = $this->getSectionDeclLine($key[0]) + 1; + $validKey = array_slice($key,1,null,true); + } + $index = array_pop($validKey); + $formattedKey = explode('=',$this->formatKeyValuePair($validKey,'')); + $formattedKey = $formattedKey[0]; - } - - /** - * Insert a text into the file - * - * @param $line The line where the text should be inserted - * @param $text The text - */ - public function insert($line,$text) - { - - } - - /** - * Update the given line and insert $text - * - * Update the line but ignore text separated by a comment delimiter. - * - * @param $line The line number - * @param $text The text that will be inserted - */ - public function update($line,$text) - { - - } - - /** - * Write changes to the file - */ - public function applyChanges() - { - - } -} - -/** - * A diff that describes the change of an object property - */ -class PropertyDiff { - - /** - * Create the property diff between two objects - * - * @param stdClass $oldObject The object representing the state before the change - * @param stdClass $newObject The object representing the state after the change - * - * @return array An associative array mapping all changed properties to a property diff - * describing the change - */ - public static function createObjectDiff(stdClass $oldObject,stdClass $newObject) - { - $diffs = array(); - /* - * Search inserted or updated properties - */ - foreach ($newObject as $key => $value) { - $newProperty = $value; - $oldProperty = $oldObject->{$key}; - if (is_array($newProperty)) { - if (empty($oldProperty)) { - $diffs[$key] = new PropertyDiff( - PropertyDiff::ACTION_INSERT, - PropertyDiff::createObjectDiff(new \stdClass(),$newObject)); - } else { - $diffs[$key] = new PropertyDiff( - PropertyDiff::ACTION_NONE, - PropertyDiff::createObjectDiff($oldObject,$newObject) - ); + for (;$line < count($this->text);$line++) { + $l = $this->text[$line]; + if ($this->isSectionDecl($l)) { + return -1; + } + if (strlen($formattedKey) > 0) { + if (preg_match('/^'.$formattedKey.'\[\]/',$l) === 1 || + preg_match('/^'.$formattedKey.'.'.$index.'/',$l) === 1 ) { + return $line; } } else { - if (empty($oldProperty)) { - $diffs[$key] = - new PropertyDiff(PropertyDiff::ACTION_INSERT,$newProperty); - } elseif (strcasecmp($newProperty,$oldProperty) != 0) { - $diffs[$key] = - new PropertyDiff(PropertyDiff::ACTION_UPDATE,$newProperty); + if (preg_match('/^'.$index.'/',$l) === 1 ) { + return $line; } } } - /* - * Search deleted properties - */ - foreach ($oldObject as $key => $value) { - if (empty($newObject->{$key})) { - $oldProperty = $value; - if (is_array($oldProperty)){ - $diffs[key] = new PropertyDiff( - PropertyDiff::ACTION_DELETE, - PropertyDiff::createObjectDiff($oldObject,new \stdClass()) - ); - } else { - $diffs[$key] = - new PropertyDiff(PropertyDiff::ACTION_DELETE,null); - } - } + return -1; + } + + /** + * Reset the given key + * + * Set the key to null, if it already exists. Otherwise do nothing. + * + * @param array $key + */ + public function reset(array $key) + { + $line = $this->getKeyLine($key); + if ($line === -1) { + return; + } + $this->deleteLine($line); + } + + /** + * Change the extended section of $section + */ + public function setSection($section,$extend = null) + { + if (isset($extend)) { + $decl = '['.$section.' : '.$extend.']'; + } else { + $decl = '['.$section.']'; + } + $line = $this->getSectionDeclLine($section); + if ($line !== -1) { + $this->deleteLine($line); + $this->insertAtLine($line,$decl); + } else { + $line = $this->getLastLine(); + $this->insertAtLine($line,$decl); + $this->insertAtLine($line,""); } } /** - * The available action types + * Remove the section declarationa of $section */ - const ACTION_INSERT = 0; - const ACTION_UPDATE = 1; - const ACTION_DELETE = 2; - const ACTION_NONE = 3; - - /** - * The action described by this diff - * - * @var String - */ - public $action; - - /** - * The value after the change - * - * @var StdClass - */ - public $value; - - /** - * Create a new PropertyDiff - * - * @param int $action The action described by this diff - * @param string $value The value after the change - */ - public function Diff($action, $value) + public function removeSection($section) { - if (action != ACTION_CREATE && - action != ACTION_UPDATE && - action != ACTION_DELETE) { - throw new \Exception('Invalid action code: '.$action); + $line = $this->getSectionDeclLine($section); + if ($line !== -1) { + $this->deleteLine($line); + } + } + + /** + * Insert a key + * + * Insert the key at the end of the corresponding section. + * + * @param array $key The key to insert + * @param $value The value to insert + */ + private function insert(array $key,$value) + { + if (count($key) > 1) { + // insert into end of section + $line = $this->getSectionEnd($key[0]); + } else { + // insert into section-less space + $line = $this->getSectionEnd(); + } + $content = $this->formatKeyValuePair($this->truncateSection($key),$value); + $this->insertAtLine($line,$content); + } + + + /** + * Return the edited text + * + * @return string The edited text + */ + public function getText() + { + // clean up whitespaces + $i = count($this->text) - 1; + for (;$i >= 0; $i--) { + $line = $this->text[$i]; + if ($this->isSectionDecl($line)) { + $i--; + $line = $this->text[$i]; + while ($i >= 0 && preg_match('/^[\s]*$/',$line) === 1) { + $this->deleteLine($i); + $i--; + $line = $this->text[$i]; + } + if ($i !== 0) { + $this->insertAtLine($i + 1,''); + } + } + } + return implode("\n",$this->text); + } + + /** + * 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 $lineNr + * @param $toInsert The lineNr starting at 0 + */ + private function updateLine($lineNr,$toInsert) + { + $this->text[$lineNr] = $toInsert; + } + + /** + * 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 + * @param $value The value + * + * @return string The formatted key-value pair + */ + private function formatKeyValuePair(array $key,$value) + { + return implode($this->nestSeparator,$key).'='.$this->_prepareValue($value); + } + + /** + * Strip the section off of a key, when necessary. + * + * @param array $key + * @return array + */ + private function truncateSection(array $key) + { + if (count($key) > 1) { + unset($key[0]); + } + return $key; + } + + /** + * Get the first line after the given $section + * + * If section is empty, return the end of section-less + * space at the file start. + * + * @param $section The name of the section + * @return int + */ + private function getSectionEnd($section = null) + { + $i = 0; + $started = false; + if (!isset($section)) { + $started = true; + } + foreach ($this->text as $line) { + if ($started) { + if (preg_match('/^\[/',$line) === 1) { + return $i; + } + } elseif (preg_match('/^\['.$section.'.*\]/',$line) === 1) { + $started = true; + } + $i++; + } + if (!$started) { + return -1; + } + return $i; + } + + /** + * 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 + */ + private function isSectionDecl($lineContent,$section = "") + { + return preg_match('/^\[/'.$section,$lineContent) === 1; + } + + private function getSectionDeclLine($section) + { + $i = 0; + foreach ($this->text as $line) { + if (preg_match('/^\['.$section.'/',$line)) { + return $i; + } + $i++; + } + return -1; + } + + /** + * Return the line number of the given key + * + * When sections are active, return the first matching key in the key's + * section, otherwise return the first matching key. + * + * @param array $keys The key and its parents + */ + private function getKeyLine(array $keys) + { + // remove section + if (count($keys) > 1) { + // the key is in a section + $section = $keys[0]; + $key = implode($this->nestSeparator,array_slice($keys,1,null,true)); + $inSection = false; + } else { + // section-less key + $section = null; + $key = implode($this->nestSeparator,$keys); + $inSection = true; + } + $i = 0; + foreach ($this->text as $line) { + if ($inSection && preg_match('/^\[/',$line) === 1) { + return -1; + } + if ($inSection && preg_match('/^'.$key.'/',$line) === 1) { + return $i; + } + if (!$inSection && preg_match('/^\['.$section.'/',$line) === 1) { + $inSection = true; + } + $i++; + } + return -1; + } + + /** + * Get the last line number + * + * @return int The line nr. 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 + */ + 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 + */ + private function removeFromArray($array,$pos) + { + unset($array[$pos]); + return array_values($array); + } + + /** + * Prepare a value for INI + * + * @param $value + * @return string + * @throws Zend_Config_Exception + */ + protected function _prepareValue($value) + { + if (is_integer($value) || is_float($value)) { + return $value; + } elseif (is_bool($value)) { + return ($value ? 'true' : 'false'); + } elseif (strpos($value, '"') === false) { + return '"' . $value . '"'; + } else { + /** @see Zend_Config_Exception */ + require_once 'Zend/Config/Exception.php'; + throw new Zend_Config_Exception('Value can not contain double quotes "'); } - $this->action = $action; - $this->value = $value; } } diff --git a/library/Icinga/Protocol/Ldap/Connection.php b/library/Icinga/Protocol/Ldap/Connection.php index 8b3f15db1..9c6a73a69 100644 --- a/library/Icinga/Protocol/Ldap/Connection.php +++ b/library/Icinga/Protocol/Ldap/Connection.php @@ -6,7 +6,7 @@ namespace Icinga\Protocol\Ldap; use Icinga\Application\Platform; -use Icinga\Application\Config; +use Icinga\Config\Config; use Icinga\Application\Logger as Log; /** diff --git a/library/Icinga/Web/ActionController.php b/library/Icinga/Web/ActionController.php index 3767182c2..7d989373e 100755 --- a/library/Icinga/Web/ActionController.php +++ b/library/Icinga/Web/ActionController.php @@ -31,7 +31,7 @@ namespace Icinga\Web; use Icinga\Authentication\Manager as AuthManager; use Icinga\Application\Benchmark; use Icinga\Exception; -use Icinga\Application\Config; +use Icinga\Config\Config; use Icinga\Web\Notification; use Zend_Layout as ZfLayout; use Zend_Controller_Action as ZfController; diff --git a/library/Icinga/Web/ModuleActionController.php b/library/Icinga/Web/ModuleActionController.php index a83d95fe4..dd19af17b 100644 --- a/library/Icinga/Web/ModuleActionController.php +++ b/library/Icinga/Web/ModuleActionController.php @@ -28,7 +28,7 @@ */ namespace Icinga\Web; -use Icinga\Application\Config as IcingaConfig; +use Icinga\Config\Config as IcingaConfig; use Icinga\Application\Icinga; /** diff --git a/library/Icinga/Web/Widget/Dashboard.php b/library/Icinga/Web/Widget/Dashboard.php index f1ed0c867..91da715e5 100644 --- a/library/Icinga/Web/Widget/Dashboard.php +++ b/library/Icinga/Web/Widget/Dashboard.php @@ -3,7 +3,7 @@ namespace Icinga\Web\Widget; use Icinga\Application\Icinga; -use Icinga\Application\Config; +use Icinga\Config\Config; use Icinga\Web\Widget; use Icinga\Web\Widget\Dashboard\Pane; use Icinga\Web\Url; diff --git a/modules/monitoring/application/controllers/CommandController.php b/modules/monitoring/application/controllers/CommandController.php index 5d51b8b63..a18aa42ec 100644 --- a/modules/monitoring/application/controllers/CommandController.php +++ b/modules/monitoring/application/controllers/CommandController.php @@ -31,7 +31,7 @@ use Icinga\Application\Benchmark; use Icinga\Application\Icinga; use Icinga\Backend; -use Icinga\Application\Config; +use Icinga\Config\Config; use Icinga\Application\Logger; use Icinga\Authentication\Manager; use Icinga\Web\Form; diff --git a/modules/monitoring/library/Monitoring/Backend.php b/modules/monitoring/library/Monitoring/Backend.php index 5f8b8301c..29fcebbb7 100644 --- a/modules/monitoring/library/Monitoring/Backend.php +++ b/modules/monitoring/library/Monitoring/Backend.php @@ -2,7 +2,7 @@ namespace Monitoring; -use Icinga\Application\Config as IcingaConfig; +use Icinga\Config\Config as IcingaConfig; use Icinga\Authentication\Manager as AuthManager; use Exception; diff --git a/modules/monitoring/library/Monitoring/Environment.php b/modules/monitoring/library/Monitoring/Environment.php index 338b5b38b..b4a67b7c6 100644 --- a/modules/monitoring/library/Monitoring/Environment.php +++ b/modules/monitoring/library/Monitoring/Environment.php @@ -2,7 +2,7 @@ namespace Monitoring; -use Icinga\Application\Config; +use Icinga\Config\Config; use Icinga\Web\Session; use Exception; diff --git a/test/php/application/controllers/IndexControllerTest.php b/test/php/application/controllers/IndexControllerTest.php index 2b3504928..d418aa72e 100644 --- a/test/php/application/controllers/IndexControllerTest.php +++ b/test/php/application/controllers/IndexControllerTest.php @@ -10,7 +10,7 @@ require 'Zend/Controller/Action.php'; require '../../library/Icinga/Exception/ProgrammingError.php'; require '../../library/Icinga/Application/Benchmark.php'; -require '../../library/Icinga/Application/Config.php'; +require '../../library/Icinga/Config/Config.php'; require '../../library/Icinga/Application/Icinga.php'; require '../../library/Icinga/Web/ActionController.php'; require '../../library/Icinga/Web/Notification.php'; diff --git a/test/php/library/Icinga/Authentication/DbUserBackendTest.php b/test/php/library/Icinga/Authentication/DbUserBackendTest.php index fbd47f5d7..f1cee60a0 100644 --- a/test/php/library/Icinga/Authentication/DbUserBackendTest.php +++ b/test/php/library/Icinga/Authentication/DbUserBackendTest.php @@ -36,7 +36,7 @@ require_once('Zend/Config/Ini.php'); require_once('Zend/Db.php'); require_once('../../library/Icinga/Authentication/UserBackend.php'); require_once('../../library/Icinga/Protocol/Ldap/Exception.php'); -require_once('../../library/Icinga/Application/Config.php'); +require_once('../../library/Icinga/Config/Config.php'); require_once('../../library/Icinga/Authentication/Credentials.php'); require_once('../../library/Icinga/Authentication/Backend/DbUserBackend.php'); require_once('../../library/Icinga/User.php'); @@ -45,7 +45,7 @@ use Icinga\Authentication\Backend\DbUserBackend; use Icinga\Util\Crypto; use Icinga\Authentication\Credentials; use Icinga\User; -use Icinga\Application\Config; +use Icinga\Config\Config; /** * diff --git a/test/php/library/Icinga/Application/ConfigTest.php b/test/php/library/Icinga/Config/ConfigTest.php similarity index 95% rename from test/php/library/Icinga/Application/ConfigTest.php rename to test/php/library/Icinga/Config/ConfigTest.php index 5a48e753e..5c406a0b7 100644 --- a/test/php/library/Icinga/Application/ConfigTest.php +++ b/test/php/library/Icinga/Config/ConfigTest.php @@ -6,9 +6,9 @@ namespace Tests\Icinga\Application; require_once 'Zend/Config/Ini.php'; -require_once dirname(__FILE__) . '/../../../../../library/Icinga/Application/Config.php'; +require_once '../../library/Icinga/Config/Config.php'; -use Icinga\Application\Config as IcingaConfig; +use Icinga\Config\Config as IcingaConfig; class ConfigTest extends \PHPUnit_Framework_TestCase { diff --git a/test/php/library/Icinga/Config/PreservingIniWriterTest.php b/test/php/library/Icinga/Config/PreservingIniWriterTest.php new file mode 100644 index 000000000..ea19ab736 --- /dev/null +++ b/test/php/library/Icinga/Config/PreservingIniWriterTest.php @@ -0,0 +1,329 @@ + + * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2 + * @author Icinga Development Team + */ +// {{{ICINGA_LICENSE_HEADER}}} + + +namespace Tests\Icinga\PreservingIniWriterTest; + +require_once 'Zend/Config.php'; +require_once 'Zend/Config/Ini.php'; +require_once 'Zend/Config/Writer/Ini.php'; +require_once('../../library/Icinga/Config/PreservingIniWriter.php'); + +use Icinga\Config\PreservingIniWriter; + +class PreservingIniWriterTest extends \PHPUnit_Framework_TestCase { + + private $tmpfiles = array(); + + /** + * Set up the test fixture + */ + public function setUp() + { + $ini = +';1 +trailing1="wert" +arr[]="0" +arr[]="1" +arr[]="2" +arr[]="3" + +;2 +;3 +Trailing2= +[parent] +;4 +;5 +;6 +;7 +list[]="zero" +list[]="one" + +;8 +;9 +many.many.nests="value" +propOne="value1" +propTwo="2" +propThree= +propFour="true" + +Prop5="true" + +[child : parent] +PropOne="overwritten" +;10 +'; + $this->writeToTmp('orig',$ini); + + $this->writeToTmp('sonst',''); + + $emptyIni = " "; + $this->writeToTmp('empty',$emptyIni); + + $editedIni = +';1 +;2 +;3 +;4 +;5 +trailing1="1" + +[parent] +;6 +;7 +;8 +;9 +;10 +propOne="value1" + +[different] +prop1="1" +prop2="2" + +[nested : different] +prop2="5" +'; + $this->writeToTmp('edited',$editedIni); + } + + /** + * Write a string to a temporary file + * + * @param $name The name of the temporary file + * @param $content The content + */ + private function writeToTmp($name,$content) + { + $this->tmpfiles[$name] = + tempnam(dirname(__FILE__) . '/temp',$name); + $file = fopen($this->tmpfiles[$name],'w'); + fwrite($file,$content); + fflush($file); + fclose($file); + } + + /** + * Tear down the test fixture + */ + public function tearDown() + { + foreach ($this->tmpfiles as $filename) { + unlink($filename); + } + } + + /** + * Test if the IniWriter works correctly when writing the changes back to + * the same ini file + */ + public function testPropertyChangeSameConfig() + { + $this->changeConfigAndWriteToFile('orig'); + $config = new \Zend_Config_Ini( + $this->tmpfiles['orig'],null,array('allowModifications' => true) + ); + $this->checkConfigProperties($config); + $this->checkConfigComments($this->tmpfiles['orig']); + } + + /** + * Test if the IniWriter works correctly when writing to an empty file + */ + public function testPropertyChangeEmptyConfig() + { + $this->changeConfigAndWriteToFile('empty'); + $config = new \Zend_Config_Ini( + $this->tmpfiles['empty'],null,array('allowModifications' => true) + ); + $this->checkConfigProperties($config); + } + + /** + * Test if the IniWriter works correctly when writing to a file with changes + */ + public function testPropertyChangeEditedConfig() + { + $original = $this->changeConfigAndWriteToFile('edited'); + $config = new \Zend_Config_Ini( + $this->tmpfiles['edited'],null,array('allowModifications' => true) + ); + $this->checkConfigProperties($config); + $this->checkConfigComments($this->tmpfiles['edited']); + } + + /** + * Change the test config and write the changes to the temporary + * file $tmpFile + * + * @param $tmpFile + */ + private function changeConfigAndWriteToFile($tmpFile) + { + $config = $this->createTestConfig(); + $this->alterConfig($config); + $writer = new PreservingIniWriter( + array('config' => $config,'filename' => $this->tmpfiles[$tmpFile]) + ); + $writer->write(); + return $config; + } + + /** + * Check if all comments are present + * + * @param $file + */ + private function checkConfigComments($file) + { + $i = 0; + foreach (explode("\n",file_get_contents($file)) as $line) { + if (preg_match('/^;/',$line)) { + $i++; + $this->assertEquals( + $i,intval(substr($line,1)), + 'Comment unchanged' + ); + } + } + $this->assertEquals(10,$i,'All comments exist'); + } + + /** + * Test if all configuration properties are set correctly + * + * @param $config + */ + private function checkConfigProperties($config) + { + $this->assertEquals('val',$config->Trailing2, + 'Section-less property updated.'); + + $this->assertNull($config->trailing1, + 'Section-less property deleted.'); + + $this->assertEquals('value',$config->new, + 'Section-less property created.'); + + $this->assertEquals('0',$config->arr->{0}, + 'Value persisted in array'); + + $this->assertEquals('update',$config->arr->{2}, + 'Value changed in array'); + + $this->assertEquals('arrvalue',$config->arr->{4}, + 'Value added to array'); + + $this->assertEquals('',$config->parent->propOne, + 'Section property deleted.'); + + $this->assertEquals("2",$config->parent->propTwo, + 'Section property numerical unchanged.'); + + $this->assertEquals('update',$config->parent->propThree, + 'Section property updated.'); + + $this->assertEquals("true",$config->parent->propFour, + 'Section property boolean unchanged.'); + + $this->assertEquals("1",$config->parent->new, + 'Section property numerical created.'); + + $this->assertNull($config->parent->list->{0}, + 'Section array deleted' + ); + + $this->assertEquals('new',$config->parent->list->{1}, + 'Section array changed.'); + + $this->assertEquals('changed',$config->parent->many->many->nests, + 'Change strongy nested value.'); + + $this->assertEquals('new',$config->parent->many->many->new, + 'Ccreate strongy nested value.'); + + $this->assertEquals('overwritten',$config->child->PropOne, + 'Overridden inherited property unchanged.'); + + $this->assertEquals('somethingNew',$config->child->propTwo, + 'Inherited property changed.'); + + $this->assertEquals('test',$config->child->create, + 'Non-inherited property created.'); + + $this->assertInstanceOf('Zend_Config',$config->newsection, + 'New section created.'); + + $extends = $config->getExtends(); + $this->assertEquals('child',$extends['newsection'], + 'New inheritance created.'); + } + + /** + * Change the content of a Zend_Config + * + * @param Zend_Config $config + */ + private function alterConfig(\Zend_Config $config) + { + $config->Trailing2 = 'val'; + unset($config->trailing1); + $config->new = 'value'; + $config->arr->{2} = "update"; + $config->arr->{4} = "arrvalue"; + + $config->parent->propOne = null; + $config->parent->propThree = 'update'; + $config->parent->new = 1; + unset($config->parent->list->{0}); + $config->parent->list->{1} = 'new'; + + $config->parent->many->many->nests = "changed"; + $config->parent->many->many->new = "new"; + + $config->child->propTwo = 'somethingNew'; + $config->child->create = 'test'; + + $config->newsection = array(); + $config->newsection->p1 = "prop"; + $config->newsection->P2 = "prop"; + $config->setExtend('newsection','child'); + } + + /** + * Create the the configuration that will be used for the tests. + */ + private function createTestConfig() + { + return new \Zend_Config_Ini( + $this->tmpfiles['orig'], + null, + array('allowModifications' => true) + ); + } +} From c1338898a6c19802b6f4f78018fe3fef61974432 Mon Sep 17 00:00:00 2001 From: Matthias Jentsch Date: Tue, 6 Aug 2013 17:11:44 +0200 Subject: [PATCH 3/5] Fix errors in function comments and move IniEditor into single class file refs #4352 --- library/Icinga/Config/IniEditor.php | 448 +++++++++++++++ library/Icinga/Config/PreservingIniWriter.php | 511 ++---------------- test/php/library/Icinga/Config/ConfigTest.php | 2 +- .../Icinga/Config/PreservingIniWriterTest.php | 4 +- .../{Application => }/Config/files/config.ini | 0 .../{Application => }/Config/files/extra.ini | 0 .../Config/files/modules/amodule/config.ini | 0 .../Config/files/modules/amodule/extra.ini | 0 8 files changed, 484 insertions(+), 481 deletions(-) create mode 100644 library/Icinga/Config/IniEditor.php rename test/php/library/Icinga/{Application => }/Config/files/config.ini (100%) rename test/php/library/Icinga/{Application => }/Config/files/extra.ini (100%) rename test/php/library/Icinga/{Application => }/Config/files/modules/amodule/config.ini (100%) rename test/php/library/Icinga/{Application => }/Config/files/modules/amodule/extra.ini (100%) diff --git a/library/Icinga/Config/IniEditor.php b/library/Icinga/Config/IniEditor.php new file mode 100644 index 000000000..edc209e0e --- /dev/null +++ b/library/Icinga/Config/IniEditor.php @@ -0,0 +1,448 @@ + + * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2 + * @author Icinga Development Team + */ +// {{{ICINGA_LICENSE_HEADER}}} + +namespace Icinga\Config; + +use Icinga\Exception\ConfigurationError; + +/** + * Edit the sections and keys of an ini in-place + */ +class IniEditor +{ + /** + * The text that is edited + * + * @var array + */ + private $text; + + /** + * The symbol that is used to separate keys + * + * @var string + */ + private $nestSeparator = '.'; + + /** + * Create a new IniEditor + * + * @param $content The content of the ini as string + */ + public function __construct($content) + { + $this->text = explode("\n",$content); + } + + /** + * Set the value of the given key. + * + * @param array $key The key to set + * @param $value The value to set + * @param $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 $key The key of the array value + * @param $section The section of the array. + */ + public function resetArrayElement(array $key, $section = null) + { + $line = $this->getArrayEl($key, $section); + if ($line !== -1) { + $this->deleteLine($line); + } + } + + /** + * Set the value for an array element + * + * @param array $key The key of the property + * @param $value The value of the property + * @param $section The section to use + */ + public function setArrayElement(array $key,$value,$section = null) + { + $line = $this->getArrayEl($key,$section); + if ($line !== -1) { + if (isset($section)) { + $this->updateLine($line,$this->formatKeyValuePair($key,$value)); + } else { + $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 $value The value + * @param $section The section to use + * + * @return The line of the array element. + */ + private function getArrayEl(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 (strlen($formatted) > 0) { + if (preg_match('/^'.$formatted.'\[\]/',$l) === 1 || + preg_match( + '/^'.$formatted.$this->nestSeparator.$index.'/',$l) === 1){ + return $line; + } + } else { + if (preg_match('/^'.$index.'/',$l) === 1 ) { + return $line; + } + } + } + return -1; + } + + /** + * When it exists, set the key back to null + * + * @param array $key The key to reset + * @param $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 $section The section name + * @param $extend The section that should be extended by this section + */ + public function setSection($section,$extend = null) + { + if (isset($extend)) { + $decl = '['.$section.' : '.$extend.']'; + } else { + $decl = '['.$section.']'; + } + $line = $this->getSectionLine($section); + if ($line !== -1) { + $this->deleteLine($line); + $this->insertAtLine($line,$decl); + } else { + $line = $this->getLastLine(); + $this->insertAtLine($line,$decl); + } + } + + /** + * Remove a section declaration + * + * @param $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 $value The value to insert + * @param array $key 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->cleanUpWhitespaces(); + return implode("\n",$this->text); + } + + /** + * Remove all unneeded line breaks between sections + */ + private function cleanUpWhitespaces() + { + $i = count($this->text) - 1; + for (;$i >= 0; $i--) { + $line = $this->text[$i]; + if ($this->isSectionDeclaration($line)) { + $i--; + $line = $this->text[$i]; + while ($i > 0 && preg_match('/^[\s]*$/',$line) === 1) { + $this->deleteLine($i); + $i--; + $line = $this->text[$i]; + } + if ($i !== 0) { + $this->insertAtLine($i + 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 $lineNr The line number of the target line + * @param $toInsert The new line content + */ + private function updateLine($lineNr,$content) + { + $this->text[$lineNr] = $content; + } + + /** + * 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 + * @param $value The value + * + * @return string The formatted key-value pair + */ + private function formatKeyValuePair(array $key,$value) + { + return $this->formatKey($key).'='.$this->formatValue($value); + } + + /** + * Format a key to an INI key + */ + private function formatKey(array $key) + { + 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 && preg_match('/^\[/',$line) === 1) { + return $i; + } elseif (preg_match('/^\['.$section.'.*\]/',$line) === 1) { + $started = true; + } + $i++; + } + if (!$started) { + return -1; + } + return $i; + } + + /** + * 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 + */ + private function isSectionDeclaration($lineContent,$section = "") + { + return preg_match('/^\[/'.$section,$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 (preg_match('/^\['.$section.'/',$line)) { + 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 && preg_match('/^\[/',$line) === 1) { + return -1; + } + if ($inSection && preg_match('/^'.$key.'/',$line) === 1) { + return $i; + } + if (!$inSection && preg_match('/^\['.$section.'/',$line) === 1) { + $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 + */ + 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'); + } elseif (strpos($value, '"') === false) { + return '"' . $value . '"'; + } else { + throw new ConfigurationError('Value can not contain double quotes "'); + } + } +} diff --git a/library/Icinga/Config/PreservingIniWriter.php b/library/Icinga/Config/PreservingIniWriter.php index 045891fe4..dabfc4aea 100644 --- a/library/Icinga/Config/PreservingIniWriter.php +++ b/library/Icinga/Config/PreservingIniWriter.php @@ -26,9 +26,10 @@ */ // {{{ICINGA_LICENSE_HEADER}}} - namespace Icinga\Config; +use Zend_Config; +use Zend_Config_Ini; /** * A ini file adapter that respects the file structure and the comments of already @@ -53,7 +54,7 @@ class PreservingIniWriter extends \Zend_Config_Writer_FileAbstract */ public function render() { - $oldconfig = new \Zend_Config_Ini($this->_filename); + $oldconfig = new Zend_Config_Ini($this->_filename); $newconfig = $this->_config; $editor = new IniEditor(file_get_contents($this->_filename)); $this->diffConfigs($oldconfig,$newconfig,$editor); @@ -63,57 +64,60 @@ class PreservingIniWriter extends \Zend_Config_Writer_FileAbstract /** * Create a property diff and apply the changes to the editor * - * Compare two Zend_Config that represent the state change of an ini file and use the - * IniEditor to write the changes back to the config, while preserving the structure and - * the comments of the original file. - * - * @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 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 $eeditor 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 */ private function diffConfigs( - \Zend_Config $oldconfig, - \Zend_Config $newconfig, + Zend_Config $oldconfig, + Zend_Config $newconfig, IniEditor $editor, array $parents = array()) { + $section = empty($parents) ? null : $parents[0]; foreach ($newconfig as $key => $value) { $oldvalue = $oldconfig->get($key); - $fullKey = array_merge($parents,array($key)); - if ($value instanceof \Zend_Config) { - if (empty($parents)) { + $nextParents = array_merge($parents,array($key)); + $ident = empty($parents) ? + array($key) : array_slice($nextParents,1,null,true); + if ($value instanceof Zend_Config) { + if (!isset($section)) { $extends = $newconfig->getExtends(); - $extend = array_key_exists($key,$extends) ? $extends[$key] : null; + $extend = array_key_exists($key,$extends) ? + $extends[$key] : null; $editor->setSection($key,$extend); } if (!isset($oldvalue)) { - $this->diffConfigs(new \Zend_Config(array()),$value,$editor,$fullKey); - } else { - $this->diffConfigs($oldvalue,$value,$editor,$fullKey); + $oldvalue = new Zend_Config(array()); } + $this->diffConfigs($oldvalue,$value,$editor,$nextParents); } else { if (is_numeric($key)){ - $editor->setArrayEl($fullKey,$value); + $editor->setArrayElement($ident,$value,$section); } else { - $editor->set($fullKey,$value); + $editor->set($ident,$value,$section); } } } foreach ($oldconfig as $key => $value) { - $fullKey = array_merge($parents,array($key)); - $o = $newconfig->get($key); - if (!isset($o)) { - if ($value instanceof \Zend_Config) { + $nextParents = array_merge($parents,array($key)); + $newvalue = $newconfig->get($key); + $ident = empty($parents) ? + array($key) : array_slice($nextParents,1,null,true); + if (!isset($newvalue)) { + if ($value instanceof Zend_Config) { $this->diffConfigs( - $value,new \Zend_Config(array()),$editor,$fullKey + $value,new Zend_Config(array()),$editor,$nextParents ); - $editor->removeSection($key); + if (!isset($section)) { + $editor->removeSection($key); + } } else { if (is_numeric($key)) { - $editor->delArrayEl($fullKey); + $editor->resetArrayElement($ident,$section); } else { - $editor->reset($fullKey); + $editor->reset($ident,$section); } } } @@ -121,452 +125,3 @@ class PreservingIniWriter extends \Zend_Config_Writer_FileAbstract } } - -/** - * Edit the sections and keys of an ini in-place - */ -class IniEditor -{ - /** - * The text that is edited - * - * @var string - */ - private $text; - - /** - * The symbol that is used - * - * @var string - */ - private $nestSeparator = '.'; - - /** - * Get the nest separator - * - * @return string The nest separator - */ - public function getNestSeparator() - { - return $this->nestSeparator; - } - - /** - * Set the nest separator - * - * @param $separator The nest separator - * @return mixed The current instance of IniReader - */ - public function setNestSeparator($separator) - { - $this->nestSeparator = $separator; - return $this; - } - - /** - * Create a new IniEditor - * - * @param $content The content of the ini as string - */ - public function __construct($content) - { - $this->text = explode("\n",$content); - } - - /** - * Set the value of the given key. - * - * Update the key, if it already exists, otherwise create it. - * - * @param array $key - * @param $value - */ - public function set(array $key,$value) - { - $line = $this->getKeyLine($key); - if ($line === -1) { - $this->insert($key,$value); - return; - } - $content = $this->formatKeyValuePair( - $this->truncateSection($key),$value); - $this->updateLine($line,$content); - } - - public function delArrayEl(array $key) - { - $line = $this->getArrayEl($key); - if ($line !== -1) { - $this->deleteLine($line); - } - } - - public function setArrayEl(array $key,$value) - { - $line = $this->getArrayEl($key); - if (count($key) > 1) { - $ident = $this->truncateSection($key); - $section = $key[0]; - } else { - $ident = $key; - $section = null; - } - if ($line !== -1) { - if (count($ident) > 1){ - $this->updateLine($line,$this->formatKeyValuePair($ident,$value)); - } else { - // move into own section - $this->deleteLine($line); - $this->setSection($section); - $this->insert(array_merge(array($section),$ident),$value); - } - } else { - $e = $this->getSectionEnd($section); - $this->insertAtLine($e,$this->formatKeyValuePair($ident,$value)); - } - } - - /** - * Get the line of an array element - * - * @param array $key The key of the property. - * @param $value The value - */ - private function getArrayEl(array $key) - { - $line = 0; - if (count($key) > 1) { - $line = $this->getSectionDeclLine($key[0]) + 1; - $validKey = array_slice($key,1,null,true); - } - $index = array_pop($validKey); - $formattedKey = explode('=',$this->formatKeyValuePair($validKey,'')); - $formattedKey = $formattedKey[0]; - - for (;$line < count($this->text);$line++) { - $l = $this->text[$line]; - if ($this->isSectionDecl($l)) { - return -1; - } - if (strlen($formattedKey) > 0) { - if (preg_match('/^'.$formattedKey.'\[\]/',$l) === 1 || - preg_match('/^'.$formattedKey.'.'.$index.'/',$l) === 1 ) { - return $line; - } - } else { - if (preg_match('/^'.$index.'/',$l) === 1 ) { - return $line; - } - } - } - return -1; - } - - /** - * Reset the given key - * - * Set the key to null, if it already exists. Otherwise do nothing. - * - * @param array $key - */ - public function reset(array $key) - { - $line = $this->getKeyLine($key); - if ($line === -1) { - return; - } - $this->deleteLine($line); - } - - /** - * Change the extended section of $section - */ - public function setSection($section,$extend = null) - { - if (isset($extend)) { - $decl = '['.$section.' : '.$extend.']'; - } else { - $decl = '['.$section.']'; - } - $line = $this->getSectionDeclLine($section); - if ($line !== -1) { - $this->deleteLine($line); - $this->insertAtLine($line,$decl); - } else { - $line = $this->getLastLine(); - $this->insertAtLine($line,$decl); - $this->insertAtLine($line,""); - } - } - - /** - * Remove the section declarationa of $section - */ - public function removeSection($section) - { - $line = $this->getSectionDeclLine($section); - if ($line !== -1) { - $this->deleteLine($line); - } - } - - /** - * Insert a key - * - * Insert the key at the end of the corresponding section. - * - * @param array $key The key to insert - * @param $value The value to insert - */ - private function insert(array $key,$value) - { - if (count($key) > 1) { - // insert into end of section - $line = $this->getSectionEnd($key[0]); - } else { - // insert into section-less space - $line = $this->getSectionEnd(); - } - $content = $this->formatKeyValuePair($this->truncateSection($key),$value); - $this->insertAtLine($line,$content); - } - - - /** - * Return the edited text - * - * @return string The edited text - */ - public function getText() - { - // clean up whitespaces - $i = count($this->text) - 1; - for (;$i >= 0; $i--) { - $line = $this->text[$i]; - if ($this->isSectionDecl($line)) { - $i--; - $line = $this->text[$i]; - while ($i >= 0 && preg_match('/^[\s]*$/',$line) === 1) { - $this->deleteLine($i); - $i--; - $line = $this->text[$i]; - } - if ($i !== 0) { - $this->insertAtLine($i + 1,''); - } - } - } - return implode("\n",$this->text); - } - - /** - * 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 $lineNr - * @param $toInsert The lineNr starting at 0 - */ - private function updateLine($lineNr,$toInsert) - { - $this->text[$lineNr] = $toInsert; - } - - /** - * 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 - * @param $value The value - * - * @return string The formatted key-value pair - */ - private function formatKeyValuePair(array $key,$value) - { - return implode($this->nestSeparator,$key).'='.$this->_prepareValue($value); - } - - /** - * Strip the section off of a key, when necessary. - * - * @param array $key - * @return array - */ - private function truncateSection(array $key) - { - if (count($key) > 1) { - unset($key[0]); - } - return $key; - } - - /** - * Get the first line after the given $section - * - * If section is empty, return the end of section-less - * space at the file start. - * - * @param $section The name of the section - * @return int - */ - private function getSectionEnd($section = null) - { - $i = 0; - $started = false; - if (!isset($section)) { - $started = true; - } - foreach ($this->text as $line) { - if ($started) { - if (preg_match('/^\[/',$line) === 1) { - return $i; - } - } elseif (preg_match('/^\['.$section.'.*\]/',$line) === 1) { - $started = true; - } - $i++; - } - if (!$started) { - return -1; - } - return $i; - } - - /** - * 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 - */ - private function isSectionDecl($lineContent,$section = "") - { - return preg_match('/^\[/'.$section,$lineContent) === 1; - } - - private function getSectionDeclLine($section) - { - $i = 0; - foreach ($this->text as $line) { - if (preg_match('/^\['.$section.'/',$line)) { - return $i; - } - $i++; - } - return -1; - } - - /** - * Return the line number of the given key - * - * When sections are active, return the first matching key in the key's - * section, otherwise return the first matching key. - * - * @param array $keys The key and its parents - */ - private function getKeyLine(array $keys) - { - // remove section - if (count($keys) > 1) { - // the key is in a section - $section = $keys[0]; - $key = implode($this->nestSeparator,array_slice($keys,1,null,true)); - $inSection = false; - } else { - // section-less key - $section = null; - $key = implode($this->nestSeparator,$keys); - $inSection = true; - } - $i = 0; - foreach ($this->text as $line) { - if ($inSection && preg_match('/^\[/',$line) === 1) { - return -1; - } - if ($inSection && preg_match('/^'.$key.'/',$line) === 1) { - return $i; - } - if (!$inSection && preg_match('/^\['.$section.'/',$line) === 1) { - $inSection = true; - } - $i++; - } - return -1; - } - - /** - * Get the last line number - * - * @return int The line nr. 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 - */ - 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 - */ - private function removeFromArray($array,$pos) - { - unset($array[$pos]); - return array_values($array); - } - - /** - * Prepare a value for INI - * - * @param $value - * @return string - * @throws Zend_Config_Exception - */ - protected function _prepareValue($value) - { - if (is_integer($value) || is_float($value)) { - return $value; - } elseif (is_bool($value)) { - return ($value ? 'true' : 'false'); - } elseif (strpos($value, '"') === false) { - return '"' . $value . '"'; - } else { - /** @see Zend_Config_Exception */ - require_once 'Zend/Config/Exception.php'; - throw new Zend_Config_Exception('Value can not contain double quotes "'); - } - } -} - diff --git a/test/php/library/Icinga/Config/ConfigTest.php b/test/php/library/Icinga/Config/ConfigTest.php index 5c406a0b7..026389667 100644 --- a/test/php/library/Icinga/Config/ConfigTest.php +++ b/test/php/library/Icinga/Config/ConfigTest.php @@ -14,7 +14,7 @@ class ConfigTest extends \PHPUnit_Framework_TestCase { public function setUp() { - IcingaConfig::$configDir = dirname(__FILE__) . '/Config/files'; + IcingaConfig::$configDir = dirname(__FILE__) . '/files'; } public function testAppConfig() diff --git a/test/php/library/Icinga/Config/PreservingIniWriterTest.php b/test/php/library/Icinga/Config/PreservingIniWriterTest.php index ea19ab736..96245a858 100644 --- a/test/php/library/Icinga/Config/PreservingIniWriterTest.php +++ b/test/php/library/Icinga/Config/PreservingIniWriterTest.php @@ -33,6 +33,7 @@ namespace Tests\Icinga\PreservingIniWriterTest; require_once 'Zend/Config.php'; require_once 'Zend/Config/Ini.php'; require_once 'Zend/Config/Writer/Ini.php'; +require_once('../../library/Icinga/Config/IniEditor.php'); require_once('../../library/Icinga/Config/PreservingIniWriter.php'); use Icinga\Config\PreservingIniWriter; @@ -255,8 +256,7 @@ prop2="5" 'Section property numerical created.'); $this->assertNull($config->parent->list->{0}, - 'Section array deleted' - ); + 'Section array deleted'); $this->assertEquals('new',$config->parent->list->{1}, 'Section array changed.'); diff --git a/test/php/library/Icinga/Application/Config/files/config.ini b/test/php/library/Icinga/Config/files/config.ini similarity index 100% rename from test/php/library/Icinga/Application/Config/files/config.ini rename to test/php/library/Icinga/Config/files/config.ini diff --git a/test/php/library/Icinga/Application/Config/files/extra.ini b/test/php/library/Icinga/Config/files/extra.ini similarity index 100% rename from test/php/library/Icinga/Application/Config/files/extra.ini rename to test/php/library/Icinga/Config/files/extra.ini diff --git a/test/php/library/Icinga/Application/Config/files/modules/amodule/config.ini b/test/php/library/Icinga/Config/files/modules/amodule/config.ini similarity index 100% rename from test/php/library/Icinga/Application/Config/files/modules/amodule/config.ini rename to test/php/library/Icinga/Config/files/modules/amodule/config.ini diff --git a/test/php/library/Icinga/Application/Config/files/modules/amodule/extra.ini b/test/php/library/Icinga/Config/files/modules/amodule/extra.ini similarity index 100% rename from test/php/library/Icinga/Application/Config/files/modules/amodule/extra.ini rename to test/php/library/Icinga/Config/files/modules/amodule/extra.ini From 76d7753860297f404aefcb2482f2b214f977f1d0 Mon Sep 17 00:00:00 2001 From: Matthias Jentsch Date: Wed, 7 Aug 2013 15:21:49 +0200 Subject: [PATCH 4/5] Split up functions in PreservingIniWriter for better readabillity and add better comment handling Split up the function diffPropertyUpdates into two single functions. Change the IniEditor to "glue" comments to section declarations. resolves #4352 --- library/Icinga/Config/IniEditor.php | 33 +++++-- library/Icinga/Config/PreservingIniWriter.php | 89 ++++++++++++++++--- 2 files changed, 106 insertions(+), 16 deletions(-) diff --git a/library/Icinga/Config/IniEditor.php b/library/Icinga/Config/IniEditor.php index edc209e0e..68e70a91a 100644 --- a/library/Icinga/Config/IniEditor.php +++ b/library/Icinga/Config/IniEditor.php @@ -136,13 +136,13 @@ class IniEditor return -1; } if (strlen($formatted) > 0) { - if (preg_match('/^'.$formatted.'\[\]/',$l) === 1 || + if (preg_match('/^'.$formatted.'\[\]=/',$l) === 1 || preg_match( - '/^'.$formatted.$this->nestSeparator.$index.'/',$l) === 1){ + '/^'.$formatted.$this->nestSeparator.$index.'=/',$l) === 1){ return $line; } } else { - if (preg_match('/^'.$index.'/',$l) === 1 ) { + if (preg_match('/^'.$index.'=/',$l) === 1 ) { return $line; } } @@ -237,11 +237,24 @@ class IniEditor if ($this->isSectionDeclaration($line)) { $i--; $line = $this->text[$i]; + /* + * Ignore comments that are glued to the section declaration + */ + while ($i > 0 && preg_match('/^;/',$line) === 1) { + $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]; } + /* + * Add a single whitespace + */ if ($i !== 0) { $this->insertAtLine($i + 1,''); } @@ -315,6 +328,16 @@ class IniEditor $started = isset($section) ? false: true; foreach ($this->text as $line) { if ($started && preg_match('/^\[/',$line) === 1) { + if ($i === 0) { + return $i; + } + /* + * ignore all comments 'glued' to the next section, to allow section + * comments in front of sections + */ + while (preg_match('/^;/',$this->text[$i - 1]) === 1) { + $i--; + } return $i; } elseif (preg_match('/^\['.$section.'.*\]/',$line) === 1) { $started = true; @@ -376,10 +399,10 @@ class IniEditor if ($inSection && preg_match('/^\[/',$line) === 1) { return -1; } - if ($inSection && preg_match('/^'.$key.'/',$line) === 1) { + if ($inSection && preg_match('/^'.$key.'=/',$line) === 1) { return $i; } - if (!$inSection && preg_match('/^\['.$section.'/',$line) === 1) { + if (!$inSection && preg_match('/^\[ *'.$section.' *[\]:]/',$line) === 1) { $inSection = true; } $i++; diff --git a/library/Icinga/Config/PreservingIniWriter.php b/library/Icinga/Config/PreservingIniWriter.php index dabfc4aea..b5664eb08 100644 --- a/library/Icinga/Config/PreservingIniWriter.php +++ b/library/Icinga/Config/PreservingIniWriter.php @@ -73,16 +73,49 @@ class PreservingIniWriter extends \Zend_Config_Writer_FileAbstract Zend_Config $oldconfig, Zend_Config $newconfig, IniEditor $editor, - array $parents = array()) - { + array $parents = array() + ) { + $this->diffPropertyUpdates($oldconfig,$newconfig,$editor,$parents); + $this->diffPropertyDeletions($oldconfig,$newconfig,$editor,$parents); + } + + /** + * 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 $eeditor 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 + */ + private 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)); - $ident = empty($parents) ? + $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 (!isset($section)) { + /* + * Update the section declaration + */ $extends = $newconfig->getExtends(); $extend = array_key_exists($key,$extends) ? $extends[$key] : null; @@ -93,31 +126,65 @@ class PreservingIniWriter extends \Zend_Config_Writer_FileAbstract } $this->diffConfigs($oldvalue,$value,$editor,$nextParents); } else { + /* + * The value is a plain value, use the editor to set it + */ if (is_numeric($key)){ - $editor->setArrayElement($ident,$value,$section); + $editor->setArrayElement($keyIdentifier,$value,$section); } else { - $editor->set($ident,$value,$section); + $editor->set($keyIdentifier,$value,$section); } } } + } + + /** + * 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 $eeditor 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 + */ + private 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) { $nextParents = array_merge($parents,array($key)); $newvalue = $newconfig->get($key); - $ident = empty($parents) ? + $keyIdentifier = empty($parents) ? array($key) : array_slice($nextParents,1,null,true); + if (!isset($newvalue)) { if ($value instanceof Zend_Config) { - $this->diffConfigs( - $value,new Zend_Config(array()),$editor,$nextParents - ); + /* + * The deleted value is a nested Zend_Config, handle it recursively + */ + $this->diffConfigs($value,new Zend_Config(array()),$editor,$nextParents); if (!isset($section)) { $editor->removeSection($key); } } else { + /* + * The deleted value is a plain value, use the editor to delete it + */ if (is_numeric($key)) { - $editor->resetArrayElement($ident,$section); + $editor->resetArrayElement($keyIdentifier,$section); } else { - $editor->reset($ident,$section); + $editor->reset($keyIdentifier,$section); } } } From 57257d1af318cc0e4a8caa9a7f4a2120ee2a3600 Mon Sep 17 00:00:00 2001 From: Matthias Jentsch Date: Wed, 7 Aug 2013 16:19:15 +0200 Subject: [PATCH 5/5] Fix coding standard violations resolves #4352 --- library/Icinga/Config/IniEditor.php | 33 ++++++++++--------- library/Icinga/Config/PreservingIniWriter.php | 12 +------ 2 files changed, 18 insertions(+), 27 deletions(-) diff --git a/library/Icinga/Config/IniEditor.php b/library/Icinga/Config/IniEditor.php index 68e70a91a..0338e6c5a 100644 --- a/library/Icinga/Config/IniEditor.php +++ b/library/Icinga/Config/IniEditor.php @@ -66,7 +66,7 @@ class IniEditor * @param $value The value to set * @param $section The section to insert to. */ - public function set(array $key,$value,$section = null) + public function set(array $key, $value, $section = null) { $line = $this->getKeyLine($key,$section); if ($line === -1) { @@ -98,7 +98,7 @@ class IniEditor * @param $value The value of the property * @param $section The section to use */ - public function setArrayElement(array $key,$value,$section = null) + public function setArrayElement(array $key, $value, $section = null) { $line = $this->getArrayEl($key,$section); if ($line !== -1) { @@ -125,12 +125,12 @@ class IniEditor * * @return The line of the array element. */ - private function getArrayEl(array $key,$section = null) + private function getArrayEl(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++) { + for (; $line < count($this->text); $line++) { $l = $this->text[$line]; if ($this->isSectionDeclaration($l)) { return -1; @@ -138,7 +138,7 @@ class IniEditor if (strlen($formatted) > 0) { if (preg_match('/^'.$formatted.'\[\]=/',$l) === 1 || preg_match( - '/^'.$formatted.$this->nestSeparator.$index.'=/',$l) === 1){ + '/^'.$formatted.$this->nestSeparator.$index.'=/',$l) === 1) { return $line; } } else { @@ -156,7 +156,7 @@ class IniEditor * @param array $key The key to reset * @param $section The section of the key */ - public function reset(array $key,$section = null) + public function reset(array $key, $section = null) { $line = $this->getKeyLine($key,$section); if ($line === -1) { @@ -171,7 +171,7 @@ class IniEditor * @param $section The section name * @param $extend The section that should be extended by this section */ - public function setSection($section,$extend = null) + public function setSection($section, $extend = null) { if (isset($extend)) { $decl = '['.$section.' : '.$extend.']'; @@ -208,7 +208,7 @@ class IniEditor * @param $value The value to insert * @param array $key The key to insert */ - private function insert(array $key,$value,$section = null) + private function insert(array $key, $value, $section = null) { $line = $this->getSectionEnd($section); $content = $this->formatKeyValuePair($key,$value); @@ -232,7 +232,7 @@ class IniEditor private function cleanUpWhitespaces() { $i = count($this->text) - 1; - for (;$i >= 0; $i--) { + for (; $i >= 0; $i--) { $line = $this->text[$i]; if ($this->isSectionDeclaration($line)) { $i--; @@ -268,7 +268,7 @@ class IniEditor * @param $lineNr The line nr the inserted line should have * @param $toInsert The text that will be inserted */ - private function insertAtLine($lineNr,$toInsert) + private function insertAtLine($lineNr, $toInsert) { $this->text = IniEditor::insertIntoArray($this->text,$lineNr,$toInsert); } @@ -279,7 +279,7 @@ class IniEditor * @param $lineNr The line number of the target line * @param $toInsert The new line content */ - private function updateLine($lineNr,$content) + private function updateLine($lineNr, $content) { $this->text[$lineNr] = $content; } @@ -302,7 +302,7 @@ class IniEditor * * @return string The formatted key-value pair */ - private function formatKeyValuePair(array $key,$value) + private function formatKeyValuePair(array $key, $value) { return $this->formatKey($key).'='.$this->formatValue($value); } @@ -358,7 +358,7 @@ class IniEditor * * @return bool */ - private function isSectionDeclaration($lineContent,$section = "") + private function isSectionDeclaration($lineContent, $section = "") { return preg_match('/^\[/'.$section,$lineContent) === 1; } @@ -390,7 +390,7 @@ class IniEditor * * @return int The line number */ - private function getKeyLine(array $keys,$section = null) + private function getKeyLine(array $keys, $section = null) { $key = implode($this->nestSeparator,$keys); $inSection = isset($section) ? false : true; @@ -429,7 +429,7 @@ class IniEditor * * @return array The changed array */ - private static function insertIntoArray($array,$pos,$element) + private static function insertIntoArray($array, $pos, $element) { array_splice($array, $pos, 0, $element); return $array; @@ -441,7 +441,7 @@ class IniEditor * @param $array The array to use * @param $pos The position to remove */ - private function removeFromArray($array,$pos) + private function removeFromArray($array, $pos) { unset($array[$pos]); return array_values($array); @@ -469,3 +469,4 @@ class IniEditor } } } + diff --git a/library/Icinga/Config/PreservingIniWriter.php b/library/Icinga/Config/PreservingIniWriter.php index b5664eb08..d4bee0591 100644 --- a/library/Icinga/Config/PreservingIniWriter.php +++ b/library/Icinga/Config/PreservingIniWriter.php @@ -37,16 +37,6 @@ use Zend_Config_Ini; */ class PreservingIniWriter extends \Zend_Config_Writer_FileAbstract { - /** - * Create a new instance of PreservingIniWriter - * - * @param array $options The options passed to the base class - */ - function __construct(array $options) - { - parent::__construct($options); - } - /** * Render the Zend_Config into a config file string * @@ -129,7 +119,7 @@ class PreservingIniWriter extends \Zend_Config_Writer_FileAbstract /* * The value is a plain value, use the editor to set it */ - if (is_numeric($key)){ + if (is_numeric($key)) { $editor->setArrayElement($keyIdentifier,$value,$section); } else { $editor->set($keyIdentifier,$value,$section);