Merge branch 'bugfix/preserving-ini-writer-order-4610-4615'

resolves #4610
resolves #4615
This commit is contained in:
Jannis Moßhammer 2013-08-27 18:52:26 +02:00
commit 92aa27aba1
4 changed files with 286 additions and 67 deletions

View File

@ -61,7 +61,6 @@ class ConfigController extends BaseConfigController
'url' => Url::fromPath('/config')
)
),
'authentication' => new Tab(
array(
'name' => 'auth',
@ -208,9 +207,6 @@ class ConfigController extends BaseConfigController
/**
* Write changes to an authentication file
*
* This uses the Zend_Config_Writer_Ini implementation for now, as the Preserving ini writer can't
* handle ordering
*
* @param array $config The configuration changes
*
* @return bool True when persisting succeeded, otherwise false
@ -218,13 +214,7 @@ class ConfigController extends BaseConfigController
* @see writeConfigFile()
*/
private function writeAuthenticationFile($config) {
$writer = new Zend_Config_Writer_Ini(
array(
'config' => new Zend_Config($config),
'filename' => IcingaConfig::app('authentication')->getConfigFile()
)
);
return $this->writeConfigFile($config, 'authentication', $writer);
return $this->writeConfigFile($config, 'authentication');
}
/**

View File

@ -47,14 +47,47 @@ class IniEditor
*/
private $nestSeparator = '.';
/**
* The indentation level of the comments
*
* @var string
*/
private $commentIndentation;
/**
* The indentation level of the values
*
* @var string
*/
private $valueIndentation;
/**
* The number of new lines between sections
*
* @var number
*/
private $sectionSeparators;
/**
* Create a new IniEditor
*
* @param string $content The content of the ini as string
* @param array $options Optional formatting options used when changing the ini file
* * valueIndentation: The indentation level of the values
* * commentIndentation: The indentation level of the comments
* * sectionSeparators: The amount of newlines between sections
*/
public function __construct($content)
{
$this->text = explode("\n", $content);
public function __construct(
$content,
array $options = array()
) {
$this->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'] : 2;
}
/**
@ -103,6 +136,9 @@ class IniEditor
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);
@ -180,6 +216,92 @@ class IniEditor
}
}
/**
* 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
*/
private function getSectionFromDeclaration($declaration)
{
$tmp = preg_split('/(\[|\]|:)/', $declaration);
return trim($tmp[1]);
}
/**
* Remove a section declaration
*
@ -215,7 +337,7 @@ class IniEditor
public function getText()
{
$this->cleanUpWhitespaces();
return implode("\n", $this->text);
return implode(PHP_EOL, $this->text);
}
/**
@ -223,7 +345,6 @@ class IniEditor
*/
private function cleanUpWhitespaces()
{
$i = count($this->text) - 1;
for (; $i > 0; $i--) {
$line = $this->text[$i];
@ -233,7 +354,7 @@ class IniEditor
/*
* Ignore comments that are glued to the section declaration
*/
while ($i > 0 && preg_match('/^\s*;/', $line) === 1) {
while ($i > 0 && $this->isComment($line)) {
$i--;
$line = $this->text[$i];
}
@ -246,10 +367,10 @@ class IniEditor
$line = $this->text[$i];
}
/*
* Add a single whitespace
* Refresh section separators
*/
if ($i !== 0) {
$this->insertAtLine($i + 1, '');
if ($i !== 0 && $this->sectionSeparators > 0) {
$this->insertAtLine($i + 1, str_repeat(PHP_EOL, $this->sectionSeparators - 1));
}
}
}
@ -275,10 +396,12 @@ class IniEditor
private function updateLine($lineNr, $content)
{
$comment = $this->getComment($this->text[$lineNr]);
$comment = trim($comment);
if (strlen($comment) > 0) {
$comment = ' ; ' . trim($comment);
$comment = ' ; ' . $comment;
$content = str_pad($content, $this->commentIndentation) . $comment;
}
$this->text[$lineNr] = str_pad($content, 43) . $comment;
$this->text[$lineNr] = $content;
}
/**
@ -320,7 +443,7 @@ class IniEditor
*/
private function formatKeyValuePair(array $key, $value)
{
return str_pad($this->formatKey($key), 19) . ' = ' . $this->formatValue($value);
return str_pad($this->formatKey($key), $this->valueIndentation) . ' = ' . $this->formatValue($value);
}
/**
@ -355,7 +478,7 @@ class IniEditor
* ignore all comments 'glued' to the next section, to allow section
* comments in front of sections
*/
while ($i > 0 && preg_match('/^\s*;/', $this->text[$i - 1]) === 1) {
while ($i > 0 && $this->isComment($this->text[$i - 1])) {
$i--;
}
return $i;
@ -370,6 +493,14 @@ class IniEditor
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
*

View File

@ -39,6 +39,31 @@ use \Icinga\Config\IniEditor;
*/
class PreservingIniWriter extends Zend_Config_Writer_FileAbstract
{
/**
* Stores the options
*
* @var array
*/
private $options;
/**
* Create a new PreservingIniWriter
*
* @param array $options Contains the options that should be used for the ConfigWriter
* in an associative array. Supports all options of Zend_Config_Writer and additional
* options for setting the formatting for the internal IniEditor:
* * valueIndentation: The indentation level of the values
* * commentIndentation: The indentation level of the comments
* * sectionSeparators: The amount of newlines between sections
*
* @link http://framework.zend.com/apidoc/1.12/files/Config.Writer.html#\Zend_Config_Writer
*/
public function __construct(array $options = null)
{
$this->options = $options;
parent::__construct($options);
}
/**
* Render the Zend_Config into a config file string
*
@ -48,8 +73,9 @@ class PreservingIniWriter extends Zend_Config_Writer_FileAbstract
{
$oldconfig = new Zend_Config_Ini($this->_filename);
$newconfig = $this->_config;
$editor = new IniEditor(file_get_contents($this->_filename));
$editor = new IniEditor(file_get_contents($this->_filename), $this->options);
$this->diffConfigs($oldconfig, $newconfig, $editor);
$this->updateSectionOrder($newconfig, $editor);
return $editor->getText();
}
@ -71,6 +97,23 @@ class PreservingIniWriter extends Zend_Config_Writer_FileAbstract
$this->diffPropertyDeletions($oldconfig, $newconfig, $editor, $parents);
}
/**
* Update the order of the sections in the ini file to match
* the order of the new config
*/
private function updateSectionOrder(
Zend_Config $newconfig,
IniEditor $editor
) {
$order = array();
foreach ($newconfig as $key => $value) {
if ($value instanceof Zend_Config) {
array_push($order, $key);
}
}
$editor->refreshSectionOrder($order);
}
/**
* Search for created and updated properties and use the editor to create or update these entries
*

View File

@ -36,6 +36,7 @@ require_once('../../library/Icinga/Config/IniEditor.php');
require_once('../../library/Icinga/Config/PreservingIniWriter.php');
use Icinga\Config\PreservingIniWriter;
use Zend_Config;
class PreservingIniWriterTest extends \PHPUnit_Framework_TestCase {
@ -120,8 +121,8 @@ prop2="5"
/**
* Write a string to a temporary file
*
* @param $name The name of the temporary file
* @param $content The content
* @param string $name The name of the temporary file
* @param string $content The content
*/
private function writeToTmp($name, $content)
{
@ -183,10 +184,62 @@ prop2="5"
}
/**
* Change the test config and write the changes to the temporary
* file $tmpFile
* Test if the order of sections is correctly changed in the config.
*/
public function testSectionOrderChange()
{
$original = '
;1
[section2]
;3
;4
[section3]
;5
;2
[section1]
property = "something" ; comment
';
$this->writeToTmp('section-order',$original);
$config = new Zend_Config(
array(
'section1' => array(
'property' => 'something'
),
'section2' => array(),
'section3' => array()
)
);
$writer = new PreservingIniWriter(
array('config' => $config, 'filename' => $this->tmpfiles['section-order'])
);
$writer->write();
$changed = new \Zend_Config_Ini(
$this->tmpfiles['section-order'],
null,
array('allowModifications' => true)
);
$this->assertEquals($config->section1->property, $changed->section1->property);
/*
* IniWriter should move the sections, so that comments
* are now in the right order
*/
$this->checkConfigComments(
$this->tmpfiles['section-order'],
5,
'Sections re-ordered correctly'
);
}
/**
* Change the test config, write the changes to the temporary
* file $tmpFile and save the path to the file in the array tmpfiles
*
* @param $tmpFile
* @param string $tmpFile The name that should be given to the temporary file
*/
private function changeConfigAndWriteToFile($tmpFile)
{
@ -202,9 +255,11 @@ prop2="5"
/**
* Check if all comments are present
*
* @param $file
* @param String $file The file to check
* @param Number $count The amount of comments that should be present
* @param String $assertion The assertion message that will be displayed on errors
*/
private function checkConfigComments($file)
private function checkConfigComments($file,$count = 10,$assertion = 'Comment unchanged')
{
$i = 0;
foreach (explode("\n",file_get_contents($file)) as $line) {
@ -212,17 +267,17 @@ prop2="5"
$i++;
$this->assertEquals(
$i,intval(substr($line,1)),
'Comment unchanged'
$assertion
);
}
}
$this->assertEquals(10,$i,'All comments exist');
$this->assertEquals($count, $i, 'All comments exist');
}
/**
* Test if all configuration properties are set correctly
*
* @param $config
* @param mixed $config The configuration to check
*/
private function checkConfigProperties($config)
{
@ -289,9 +344,9 @@ prop2="5"
}
/**
* Change the content of a Zend_Config
* Change the content of a Zend_Config for testing purposes
*
* @param Zend_Config $config
* @param Zend_Config $config The configuration that should be changed
*/
private function alterConfig(\Zend_Config $config)
{