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 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 {
@ -86,10 +87,10 @@ Prop5="true"
PropOne="overwritten"
;10
';
$this->writeToTmp('orig',$ini);
$this->writeToTmp('orig', $ini);
$emptyIni = " ";
$this->writeToTmp('empty',$emptyIni);
$this->writeToTmp('empty', $emptyIni);
$editedIni =
';1
@ -114,21 +115,21 @@ prop2="2"
[nested : different]
prop2="5"
';
$this->writeToTmp('edited',$editedIni);
$this->writeToTmp('edited', $editedIni);
}
/**
* 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)
private function writeToTmp($name, $content)
{
$this->tmpfiles[$name] =
tempnam(dirname(__FILE__) . '/temp',$name);
$file = fopen($this->tmpfiles[$name],'w');
fwrite($file,$content);
tempnam(dirname(__FILE__) . '/temp', $name);
$file = fopen($this->tmpfiles[$name], 'w');
fwrite($file, $content);
fflush($file);
fclose($file);
}
@ -151,7 +152,7 @@ prop2="5"
{
$this->changeConfigAndWriteToFile('orig');
$config = new \Zend_Config_Ini(
$this->tmpfiles['orig'],null,array('allowModifications' => true)
$this->tmpfiles['orig'], null, array('allowModifications' => true)
);
$this->checkConfigProperties($config);
$this->checkConfigComments($this->tmpfiles['orig']);
@ -164,7 +165,7 @@ prop2="5"
{
$this->changeConfigAndWriteToFile('empty');
$config = new \Zend_Config_Ini(
$this->tmpfiles['empty'],null,array('allowModifications' => true)
$this->tmpfiles['empty'], null, array('allowModifications' => true)
);
$this->checkConfigProperties($config);
}
@ -176,17 +177,69 @@ prop2="5"
{
$original = $this->changeConfigAndWriteToFile('edited');
$config = new \Zend_Config_Ini(
$this->tmpfiles['edited'],null,array('allowModifications' => true)
$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
* 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,86 +267,86 @@ 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)
{
$this->assertEquals('val',$config->Trailing2,
$this->assertEquals('val', $config->Trailing2,
'Section-less property updated.');
$this->assertNull($config->trailing1,
'Section-less property deleted.');
$this->assertEquals('value',$config->new,
$this->assertEquals('value', $config->new,
'Section-less property created.');
$this->assertEquals('0',$config->arr->{0},
$this->assertEquals('0', $config->arr->{0},
'Value persisted in array');
$this->assertEquals('update',$config->arr->{2},
$this->assertEquals('update', $config->arr->{2},
'Value changed in array');
$this->assertEquals('arrvalue',$config->arr->{4},
$this->assertEquals('arrvalue', $config->arr->{4},
'Value added to array');
$this->assertEquals('',$config->parent->propOne,
$this->assertEquals('', $config->parent->propOne,
'Section property deleted.');
$this->assertEquals("2",$config->parent->propTwo,
$this->assertEquals("2", $config->parent->propTwo,
'Section property numerical unchanged.');
$this->assertEquals('update',$config->parent->propThree,
$this->assertEquals('update', $config->parent->propThree,
'Section property updated.');
$this->assertEquals("true",$config->parent->propFour,
$this->assertEquals("true", $config->parent->propFour,
'Section property boolean unchanged.');
$this->assertEquals("1",$config->parent->new,
$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},
$this->assertEquals('new', $config->parent->list->{1},
'Section array changed.');
$this->assertEquals('changed',$config->parent->many->many->nests,
$this->assertEquals('changed', $config->parent->many->many->nests,
'Change strongly nested value.');
$this->assertEquals('new',$config->parent->many->many->new,
$this->assertEquals('new', $config->parent->many->many->new,
'Ccreate strongy nested value.');
$this->assertEquals('overwritten',$config->child->PropOne,
$this->assertEquals('overwritten', $config->child->PropOne,
'Overridden inherited property unchanged.');
$this->assertEquals('somethingNew',$config->child->propTwo,
$this->assertEquals('somethingNew', $config->child->propTwo,
'Inherited property changed.');
$this->assertEquals('test',$config->child->create,
$this->assertEquals('test', $config->child->create,
'Non-inherited property created.');
$this->assertInstanceOf('Zend_Config',$config->newsection,
$this->assertInstanceOf('Zend_Config', $config->newsection,
'New section created.');
$extends = $config->getExtends();
$this->assertEquals('child',$extends['newsection'],
$this->assertEquals('child', $extends['newsection'],
'New inheritance created.');
}
/**
* 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)
{
@ -320,7 +375,7 @@ prop2="5"
$config->newsection = array();
$config->newsection->p1 = "prop";
$config->newsection->P2 = "prop";
$config->setExtend('newsection','child');
$config->setExtend('newsection', 'child');
}
/**