Merge branch 'bugfix/creating-dashlet-with-parentheses-9530'

fixes #9530
This commit is contained in:
Matthias Jentsch 2015-08-05 18:22:29 +02:00
commit 60675979bc
11 changed files with 791 additions and 1337 deletions

View File

@ -77,7 +77,21 @@ class DashletForm extends Form
array(
'required' => true,
'label' => $this->translate('Dashlet Title'),
'description' => $this->translate('Enter a title for the dashlet.')
'description' => $this->translate('Enter a title for the dashlet.'),
'validators' => array(
array(
'Regex',
false,
array(
'pattern' => '/^[^\\[\\]]+$/',
'messages' => array(
'regexNotMatch' => $this->translate(
'The name cannot contain \'[\' or \']\'.'
)
)
)
)
)
)
);
$this->addElement(

View File

@ -359,11 +359,7 @@ class Config implements Countable, Iterator, Selectable
*/
protected function getIniWriter($filePath = null, $fileMode = null)
{
return new IniWriter(array(
'config' => $this,
'filename' => $filePath,
'filemode' => $fileMode
));
return new IniWriter($this, $filePath, $fileMode);
}
/**
@ -418,7 +414,6 @@ class Config implements Countable, Iterator, Selectable
static::resolvePath('modules/' . $modulename . '/' . $configname . '.ini')
);
}
return $moduleConfigs[$configname];
}

View File

@ -0,0 +1,20 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\File\Ini\Dom;
class Comment
{
/**
* @var string
*/
public $content;
/**
* @return string
*/
public function render()
{
return ';' . $this->content;
}
}

View File

@ -0,0 +1,76 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\File\Ini\Dom;
class Directive
{
/**
* @var string
*/
protected $key;
/**
* @var string
*/
protected $value;
/**
* @var array
*/
public $commentsPre;
/**
* @var string
*/
public $commentPost;
/**
* @param string $key
*
* @throws Exception
*/
public function __construct($key)
{
$this->key = trim(str_replace("\n", ' ', $key));
if (strlen($this->key) < 1) {
throw new Exception(sprintf('Ini parser error: empty key.'));
}
}
/**
* @return string
*/
public function getKey()
{
return $this->key;
}
/**
* @param string $value
*/
public function setValue($value)
{
$this->value = trim(str_replace("\n", ' ', $value));
}
/**
* @return string
*/
public function render()
{
$str = '';
if (! empty ($this->commentsPre)) {
$comments = array();
foreach ($this->commentsPre as $comment) {
$comments[] = $comment->render();
}
$str = implode(PHP_EOL, $comments) . PHP_EOL;
}
$str .= sprintf('%s = "%s"', $this->key, $this->value);
if (isset ($this->commentPost)) {
$str .= ' ' . $this->commentPost->render();
}
return $str;
}
}

View File

@ -0,0 +1,81 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\File\Ini\Dom;
class Document
{
/**
* @var array
*/
protected $sections = array();
/**
* @var array
*/
public $commentsDangling;
/**
* @param Section $section
*/
public function addSection(Section $section)
{
$this->sections[$section->getName()] = $section;
}
/**
* @param string $name
*
* @return bool
*/
public function hasSection($name)
{
return isset($this->sections[$name]);
}
/**
* @param string $name
*
* @return Section
*/
public function getSection($name)
{
return $this->sections[$name];
}
/**
* @param string $name
* @param Section $section
*
* @return Section
*/
public function setSection($name, Section $section)
{
return $this->sections[$name] = $section;
}
/**
* @param string $name
*/
public function removeSection($name)
{
unset ($this->sections[$name]);
}
/**
* @return string
*/
public function render()
{
foreach ($this->sections as $section) {
$sections []= $section->render();
}
$str = implode(PHP_EOL, $sections);
if (! empty($this->commentsDangling)) {
foreach ($this->commentsDangling as $comment) {
$str .= PHP_EOL . $comment->render();
}
}
return $str;
}
}

View File

@ -0,0 +1,108 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\File\Ini\Dom;
class Section
{
/**
* @var string
*
protected $name;
/**
* @var array
*/
protected $directives = array();
/**
* @var array
*/
public $commentsPre;
/**
* @var string
*/
public $commentPost;
/**
* @param string $name
*
* @throws Exception
*/
public function __construct($name)
{
$this->name = trim(str_replace("\n", ' ', $name));
if (strlen($this->name) < 1) {
throw new Exception(sprintf('Ini parser error: empty section identifier'));
}
}
/**
* @param Directive $directive
*/
public function addDirective(Directive $directive)
{
$this->directives[$directive->getKey()] = $directive;
}
/**
* @param string $key
*/
public function removeDirective($key)
{
unset ($this->directives[$key]);
}
/**
* @param string $key
*
* @return bool
*/
public function hasDirective($key)
{
return isset($this->directives[$key]);
}
/**
* @param $key string
*
* @return Directive
*/
public function getDirective($key)
{
return $this->directives[$key];
}
/**
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* @return string
*/
public function render()
{
$dirs = '';
$i = 0;
foreach ($this->directives as $directive) {
$dirs .= (($i++ > 0 && ! empty($directive->commentsPre)) ? PHP_EOL : '') . $directive->render() . PHP_EOL;
}
$cms = '';
if (! empty($this->commentsPre)) {
foreach ($this->commentsPre as $comment) {
$comments[] = $comment->render();
}
$cms = implode(PHP_EOL, $comments) . PHP_EOL;
}
$post = '';
if (isset($this->commentPost)) {
$post = ' ' . $this->commentPost->render();
}
return $cms . sprintf('[%s]', $this->name) . $post . PHP_EOL . $dirs;
}
}

View File

@ -1,625 +0,0 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\File\Ini;
/**
* 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 = '.';
/**
* 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,
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'] : 1;
}
/**
* Set the value of the given key.
*
* @param array $key The key to set
* @param string $value The value to set
* @param array $section The section to insert to.
*/
public function set(array $key, $value, $section = null)
{
$line = $this->getKeyLine($key, $section);
if ($line === -1) {
$this->insert($key, $value, $section);
} else {
$content = $this->formatKeyValuePair($key, $value);
$this->updateLine($line, $content);
}
}
/**
* Reset the value of the given array element
*
* @param array $key The key of the array value
* @param array $section The section of the array.
*/
public function resetArrayElement(array $key, $section = null)
{
$line = $this->getArrayElement($key, $section);
if ($line !== -1) {
$this->deleteLine($line);
}
}
/**
* Set the value for an array element
*
* @param array $key The key of the property
* @param string $value The value of the property
* @param array $section The section to use
*/
public function setArrayElement(array $key, $value, $section = null)
{
$line = $this->getArrayElement($key, $section);
if ($line !== -1) {
if (isset($section)) {
$this->updateLine($line, $this->formatKeyValuePair($key, $value));
} else {
/*
* Move into new section to avoid ambiguous configurations
*/
$section = $key[0];
unset($key[0]);
$this->deleteLine($line);
$this->setSection($section);
$this->insert($key, $value, $section);
}
} else {
$this->insert($key, $value, $section);
}
}
/**
* Get the line of an array element
*
* @param array $key The key of the property.
* @param mixed $section The section to use
*
* @return int The line of the array element.
*/
private function getArrayElement(array $key, $section = null)
{
$line = isset($section) ? $this->getSectionLine($section) + 1 : 0;
$index = array_pop($key);
$formatted = $this->formatKey($key);
for (; $line < count($this->text); $line++) {
$l = $this->text[$line];
if ($this->isSectionDeclaration($l)) {
return -1;
}
if (preg_match('/^\s*' . $formatted . '\[\]\s*=/', $l) === 1) {
return $line;
}
if ($this->isPropertyDeclaration($l, array_merge($key, array($index)))) {
return $line;
}
}
return -1;
}
/**
* When it exists, set the key back to null
*
* @param array $key The key to reset
* @param array $section The section of the key
*/
public function reset(array $key, $section = null)
{
$line = $this->getKeyLine($key, $section);
if ($line === -1) {
return;
}
$this->deleteLine($line);
}
/**
* Create the section if it does not exist and set the properties
*
* @param string $section The section name
* @param array $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);
}
}
/**
* Refresh the section order of the ini file
*
* @param array $order An array containing the section names in the new order
* Example: array(0 => 'FirstSection', 1 => 'SecondSection')
*/
public function refreshSectionOrder(array $order)
{
$sections = $this->createSectionMap($this->text);
/*
* Move section-less properties to the start of the ordered text
*/
$orderedText = array();
foreach ($sections['[section-less]'] as $line) {
array_push($orderedText, $line);
}
/*
* Reorder the sections
*/
$len = count($order);
for ($i = 0; $i < $len; $i++) {
if (array_key_exists($i, $order)) {
/*
* Append the lines of the section to the end of the
* ordered text
*/
foreach ($sections[$order[$i]] as $line) {
array_push($orderedText, $line);
}
}
}
$this->text = $orderedText;
}
/**
* Create a map of sections to lines of a given ini file
*
* @param array $text The text split up in lines
*
* @return array $sectionMap A map containing all sections as arrays of lines. The array of section-less
* lines will be available using they key '[section-less]' which is no valid
* section declaration because it contains brackets.
*/
private function createSectionMap($text)
{
$sections = array('[section-less]' => array());
$section = '[section-less]';
$len = count($text);
for ($i = 0; $i < $len; $i++) {
if ($this->isSectionDeclaration($text[$i])) {
$newSection = $this->getSectionFromDeclaration($this->text[$i]);
$sections[$newSection] = array();
/*
* Remove comments 'glued' to the new section from the old
* section array and put them into the new section.
*/
$j = $i - 1;
$comments = array();
while ($j >= 0 && $this->isComment($this->text[$j])) {
array_push($comments, array_pop($sections[$section]));
$j--;
}
$comments = array_reverse($comments);
foreach ($comments as $comment) {
array_push($sections[$newSection], $comment);
}
$section = $newSection;
}
array_push($sections[$section], $this->text[$i]);
}
return $sections;
}
/**
* Extract the section name from a section declaration
*
* @param String $declaration The section declaration
*
* @return string The section name
*/
private function getSectionFromDeclaration($declaration)
{
$tmp = preg_split('/(\[|\]|:)/', $declaration);
return trim($tmp[1]);
}
/**
* Remove a section declaration
*
* @param string $section The section name
*/
public function removeSection($section)
{
$line = $this->getSectionLine($section);
if ($line !== -1) {
$this->deleteLine($line);
}
}
/**
* Insert the key at the end of the corresponding section
*
* @param array $key The key to insert
* @param mixed $value The value to insert
* @param array $section The key to insert
*/
private function insert(array $key, $value, $section = null)
{
$line = $this->getSectionEnd($section);
$content = $this->formatKeyValuePair($key, $value);
$this->insertAtLine($line, $content);
}
/**
* Get the edited text
*
* @return string The edited text
*/
public function getText()
{
$this->normalizeSectionSpacing();
// trim leading and trailing whitespaces from generated file
$txt = trim(implode(PHP_EOL, $this->text)) . PHP_EOL;
// replace linebreaks, unless they preceed a comment or a section
return preg_replace("/\n[\n]*([^;\[])/", "\n$1", $txt);
}
/**
* normalize section spacing according to the current settings
*/
private function normalizeSectionSpacing()
{
$i = count($this->text) - 1;
for (; $i > 0; $i--) {
$line = $this->text[$i];
if ($this->isSectionDeclaration($line) && $i > 0) {
$i--;
$line = $this->text[$i];
// ignore comments that are glued to the section declaration
while ($i > 0 && $this->isComment($line)) {
$i--;
$line = $this->text[$i];
}
// remove whitespaces between the sections
while ($i > 0 && preg_match('/^\s*$/', $line) === 1) {
$this->deleteLine($i);
$i--;
$line = $this->text[$i];
}
// refresh section separators
if ($i !== 0 && $this->sectionSeparators > 0) {
$this->insertAtLine($i + 1, str_repeat(PHP_EOL, $this->sectionSeparators - 1));
}
}
}
}
/**
* Insert the text at line $lineNr
*
* @param $lineNr The line nr the inserted line should have
* @param $toInsert The text that will be inserted
*/
private function insertAtLine($lineNr, $toInsert)
{
$this->text = IniEditor::insertIntoArray($this->text, $lineNr, $toInsert);
}
/**
* Update the line $lineNr
*
* @param int $lineNr The line number of the target line
* @param string $content The new line content
*/
private function updateLine($lineNr, $content)
{
$comment = $this->getComment($this->text[$lineNr]);
$comment = trim($comment);
if (strlen($comment) > 0) {
$comment = ' ; ' . $comment;
$content = str_pad($content, $this->commentIndentation) . $comment;
}
$this->text[$lineNr] = $content;
}
/**
* Get the comment from the given line
*
* @param $lineContent The content of the line
*
* @return string The extracted comment
*/
private function getComment($lineContent)
{
/*
* Remove all content in double quotes that is not behind a semicolon, recognizing
* escaped double quotes inside the string
*/
$cleaned = preg_replace('/^[^;"]*"[^"\\\\]*(?:\\\\.[^"\\\\]*)*"/s', '', $lineContent);
$matches = explode(';', $cleaned, 2);
return array_key_exists(1, $matches) ? $matches[1] : '';
}
/**
* Delete the line $lineNr
*
* @param $lineNr The lineNr starting at 0
*/
private function deleteLine($lineNr)
{
$this->text = $this->removeFromArray($this->text, $lineNr);
}
/**
* Format a key-value pair to an INI file-entry
*
* @param array $key The key to format
* @param string $value The value to format
*
* @return string The formatted key-value pair
*/
private function formatKeyValuePair(array $key, $value)
{
return str_pad($this->formatKey($key), $this->valueIndentation) . ' = ' . $this->formatValue($value);
}
/**
* Format a key to an INI key
*
* @param array $key the key array to format
*
* @return string
*/
private function formatKey(array $key)
{
foreach ($key as $i => $separator) {
$key[$i] = $this->sanitize($separator);
}
return implode($this->nestSeparator, $key);
}
/**
* Get the first line after the given $section
*
* @param $section The name of the section
*
* @return int The line number of the section
*/
private function getSectionEnd($section = null)
{
$i = 0;
$started = isset($section) ? false : true;
foreach ($this->text as $line) {
if ($started && $this->isSectionDeclaration($line)) {
if ($i === 0) {
return $i;
}
/*
* ignore all comments 'glued' to the next section, to allow section
* comments in front of sections
*/
while ($i > 0 && $this->isComment($this->text[$i - 1])) {
$i--;
}
return $i;
} elseif ($this->isSectionDeclaration($line, $section)) {
$started = true;
}
$i++;
}
if (!$started) {
return -1;
}
return $i;
}
/**
* Check if the given line contains only a comment
*/
private function isComment($line)
{
return preg_match('/^\s*;/', $line) === 1;
}
/**
* Check if the line contains the property declaration for a key
*
* @param string $lineContent The content of the line
* @param array $key The key this declaration is supposed to have
*
* @return boolean True, when the lineContent is a property declaration
*/
private function isPropertyDeclaration($lineContent, array $key)
{
return preg_match(
'/^\s*' . preg_quote($this->formatKey($key), '/') . '\s*=\s*/',
$lineContent
) === 1;
}
/**
* Check if the given line contains a section declaration
*
* @param $lineContent The content of the line
* @param string $section The optional section name that will be assumed
*
* @return bool True, when the lineContent is a section declaration
*/
private function isSectionDeclaration($lineContent, $section = null)
{
if (isset($section)) {
return preg_match('/^\s*\[\s*' . $section . '\s*[\]:]/', $lineContent) === 1;
} else {
return preg_match('/^\s*\[/', $lineContent) === 1;
}
}
/**
* Get the line where the section begins
*
* @param $section The section
*
* @return int The line number
*/
private function getSectionLine($section)
{
$i = 0;
foreach ($this->text as $line) {
if ($this->isSectionDeclaration($line, $section)) {
return $i;
}
$i++;
}
return -1;
}
/**
* Get the line number where the given key occurs
*
* @param array $keys The key and its parents
* @param $section The section of the key
*
* @return int The line number
*/
private function getKeyLine(array $keys, $section = null)
{
$key = implode($this->nestSeparator, $keys);
$inSection = isset($section) ? false : true;
$i = 0;
foreach ($this->text as $line) {
if ($inSection && $this->isSectionDeclaration($line)) {
return -1;
}
if ($inSection && $this->isPropertyDeclaration($line, $keys)) {
return $i;
}
if (!$inSection && $this->isSectionDeclaration($line, $section)) {
$inSection = true;
}
$i++;
}
return -1;
}
/**
* Get the last line number occurring in the text
*
* @return The line number of the last line
*/
private function getLastLine()
{
return count($this->text);
}
/**
* Insert a new element into a specific position of an array
*
* @param $array The array to use
* @param $pos The target position
* @param $element The element to insert
*
* @return array The changed array
*/
private static function insertIntoArray($array, $pos, $element)
{
array_splice($array, $pos, 0, $element);
return $array;
}
/**
* Remove an element from an array
*
* @param $array The array to use
* @param $pos The position to remove
*
* @return array The altered array
*/
private function removeFromArray($array, $pos)
{
unset($array[$pos]);
return array_values($array);
}
/**
* Prepare a value for INI
*
* @param $value The value of the string
*
* @return string The formatted value
*
* @throws Zend_Config_Exception
*/
private function formatValue($value)
{
if (is_integer($value) || is_float($value)) {
return $value;
} elseif (is_bool($value)) {
return ($value ? 'true' : 'false');
}
return '"' . str_replace('"', '\"', $this->sanitize($value)) . '"';
}
private function sanitize($value)
{
return str_replace("\n", '', $value);
}
}

View File

@ -0,0 +1,217 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\File\Ini;
use Icinga\File\Ini\Dom\Section;
use Icinga\File\Ini\Dom\Comment;
use Icinga\File\Ini\Dom\Document;
use Icinga\File\Ini\Dom\Directive;
use Icinga\Application\Logger;
use Icinga\Exception\ConfigurationError;
class IniParser
{
const LINE_START = 0;
const SECTION = 1;
const DIRECTIVE_KEY = 4;
const DIRECTIVE_VALUE_START = 5;
const DIRECTIVE_VALUE = 6;
const DIRECTIVE_VALUE_QUOTED = 7;
const COMMENT = 8;
const COMMENT_END = 9;
const LINE_END = 10;
private static function throwParseError($message, $line)
{
throw new ConfigurationError(sprintf('Ini parser error: %s. (l. %d)', $message, $line));
}
/**
* Read the ini file contained in a string and return a mutable DOM that can be used
* to change the content of an INI file.
*
* @param $str A string containing the whole ini file
*
* @return Document The mutable DOM object.
* @throws ConfigurationError In case the file is not parseable
*/
public static function parseIni($str)
{
$doc = new Document();
$sec = null;
$dir = null;
$coms = array();
$state = self::LINE_START;
$token = '';
$line = 0;
for ($i = 0; $i < strlen($str); $i++) {
$s = $str[$i];
switch ($state) {
case self::LINE_START:
if (ctype_space($s)) {
continue;
}
switch ($s) {
case '[':
$state = self::SECTION;
break;
case ';':
$state = self::COMMENT;
break;
default:
$state = self::DIRECTIVE_KEY;
$token = $s;
break;
}
break;
case self::SECTION:
if ($s === "\n") {
self::throwParseError('Unterminated SECTION', $line);
} if ($s !== ']') {
$token .= $s;
} else {
$sec = new Section($token);
$sec->commentsPre = $coms;
$doc->addSection($sec);
$dir = null;
$coms = array();
$state = self::LINE_END;
$token = '';
}
break;
case self::DIRECTIVE_KEY:
if ($s !== '=') {
$token .= $s;
} else {
$dir = new Directive($token);
$dir->commentsPre = $coms;
if (isset($sec)) {
$sec->addDirective($dir);
} else {
Logger::warning(sprintf(
'Ini parser warning: section-less directive "%s" ignored. (l. %d)',
$token,
$line
));
}
$coms = array();
$state = self::DIRECTIVE_VALUE_START;
$token = '';
}
break;
case self::DIRECTIVE_VALUE_START:
if (ctype_space($s)) {
continue;
} elseif ($s === '"') {
$state = self::DIRECTIVE_VALUE_QUOTED;
} else {
$state = self::DIRECTIVE_VALUE;
$token = $s;
}
break;
case self::DIRECTIVE_VALUE:
if ($s === "\n" || $s === ";") {
$dir->setValue($token);
$token = '';
if ($s === "\n") {
$state = self::LINE_START;
$line ++;
} elseif ($s === ';') {
$state = self::COMMENT;
}
} else {
$token .= $s;
}
break;
case self::DIRECTIVE_VALUE_QUOTED:
if ($s === "\n") {
self::throwParseError('Unterminated DIRECTIVE_VALUE_QUOTED', $line);
} elseif ($s !== '"') {
$token .= $s;
} else {
$dir->setValue($token);
$token = '';
$state = self::LINE_END;
}
break;
case self::COMMENT:
case self::COMMENT_END:
if ($s !== "\n") {
$token .= $s;
} else {
$com = new Comment();
$com->content = $token;
$token = '';
// Comments at the line end belong to the current line's directive or section. Comments
// on empty lines belong to the next directive that shows up.
if ($state === self::COMMENT_END) {
if (isset($dir)) {
$dir->commentPost = $com;
} else {
$sec->commentPost = $com;
}
} else {
$coms[] = $com;
}
$state = self::LINE_START;
$line ++;
}
break;
case self::LINE_END:
if ($s === "\n") {
$state = self::LINE_START;
$line ++;
} elseif ($s === ';') {
$state = self::COMMENT_END;
}
break;
}
}
// process the last token
switch ($state) {
case self::COMMENT:
case self::COMMENT_END:
$com = new Comment();
$com->content = $token;
if ($state === self::COMMENT_END) {
if (isset($dir)) {
$dir->commentPost = $com;
} else {
$sec->commentPost = $com;
}
} else {
$coms[] = $com;
}
break;
case self::DIRECTIVE_VALUE:
$dir->setValue($token);
$sec->addDirective($dir);
break;
case self::DIRECTIVE_VALUE_QUOTED:
case self::DIRECTIVE_KEY:
case self::SECTION:
self::throwParseError('File ended in unterminated state ' . $state, $line);
}
if (! empty($coms)) {
$doc->commentsDangling = $coms;
}
return $doc;
}
}

View File

@ -3,16 +3,19 @@
namespace Icinga\File\Ini;
use Zend_Config;
use Zend_Config_Ini;
use Icinga\Application\Logger;
use Icinga\Data\ConfigObject;
use Icinga\Exception\ProgrammingError;
use Icinga\File\Ini\Dom\Directive;
use Icinga\File\Ini\Dom\Document;
use Icinga\File\Ini\Dom\Section;
use Zend_Config_Exception;
use Zend_Config_Writer_FileAbstract;
use Icinga\Application\Config;
/**
* A INI file adapter that respects the file structure and the comments of already existing ini files
*/
class IniWriter extends Zend_Config_Writer_FileAbstract
class IniWriter
{
/**
* Stores the options
@ -21,151 +24,137 @@ class IniWriter extends Zend_Config_Writer_FileAbstract
*/
protected $options;
/**
* The configuration object to write
*
* @var Config
*/
protected $config;
/**
* The mode to set on new files
*
* @var int
*/
public static $fileMode = 0660;
protected $fileMode;
/**
* The path to write to
*
* @var string
*/
protected $filename;
/**
* Create a new INI writer
*
* @param array $options Supports all options of Zend_Config_Writer and additional options:
* * filemode: The mode to set on new files
* * valueIndentation: The indentation level of the values
* * commentIndentation: The indentation level of the comments
* * sectionSeparators: The amount of newlines between sections
* @param Config $config The configuration to write
* @param string $filename The file name to write to
* @param int $filemode Octal file persmissions
*
* @link http://framework.zend.com/apidoc/1.12/files/Config.Writer.html#\Zend_Config_Writer
*/
public function __construct(array $options = null)
public function __construct(Config $config, $filename, $filemode = 0660, $options = array())
{
if (isset($options['config']) && $options['config'] instanceof Config) {
// As this class inherits from Zend_Config_Writer_FileAbstract we must
// not pass the config directly as it needs to be of type Zend_Config
$options['config'] = new Zend_Config($options['config']->toArray(), true);
}
$this->config = $config;
$this->filename = $filename;
$this->fileMode = $filemode;
$this->options = $options;
parent::__construct($options);
}
/**
* Render the Zend_Config into a config file string
* Render the Zend_Config into a config filestring
*
* @return string
*/
public function render()
{
if (file_exists($this->_filename)) {
$oldconfig = new Zend_Config_Ini($this->_filename);
$content = trim(file_get_contents($this->_filename));
if (file_exists($this->filename)) {
$oldconfig = Config::fromIni($this->filename);
$content = trim(file_get_contents($this->filename));
} else {
$oldconfig = new Zend_Config(array());
$oldconfig = Config::fromArray(array());
$content = '';
}
$newconfig = $this->_config;
$editor = new IniEditor($content, $this->options);
$this->diffConfigs($oldconfig, $newconfig, $editor);
$this->updateSectionOrder($newconfig, $editor);
return $editor->getText();
$doc = IniParser::parseIni($content);
$this->diffPropertyUpdates($this->config, $doc);
$this->diffPropertyDeletions($oldconfig, $this->config, $doc);
$doc = $this->updateSectionOrder($this->config, $doc);
return $doc->render();
}
/**
* Write configuration to file and set file mode in case it does not exist yet
*
* @param string $filename
* @param Zend_Config $config
* @param bool $exclusiveLock
*
* @throws Zend_Config_Exception
*/
public function write($filename = null, Zend_Config $config = null, $exclusiveLock = null)
public function write($filename = null, $exclusiveLock = false)
{
$filePath = $filename !== null ? $filename : $this->_filename;
$filePath = isset($filename) ? $filename : $this->filename;
$setMode = false === file_exists($filePath);
parent::write($filename, $config, $exclusiveLock);
if (file_put_contents($filePath, $this->render(), $exclusiveLock ? LOCK_EX : 0) === false) {
throw new Zend_Config_Exception('Could not write to file "' . $filePath . '"');
}
if ($setMode) {
$mode = isset($this->options['filemode']) ? $this->options['filemode'] : static::$fileMode;
if (is_int($mode) && false === @chmod($filePath, $mode)) {
// file was newly created
$mode = $this->fileMode;
if (is_int($this->fileMode) && false === @chmod($filePath, $this->fileMode)) {
throw new Zend_Config_Exception(sprintf('Failed to set file mode "%o" on file "%s"', $mode, $filePath));
}
}
}
/**
* Create a property diff and apply the changes to the editor
*
* @param Zend_Config $oldconfig The config representing the state before the change
* @param Zend_Config $newconfig The config representing the state after the change
* @param IniEditor $editor The editor that should be used to edit the old config file
* @param array $parents The parent keys that should be respected when editing the config
*/
protected function diffConfigs(
Zend_Config $oldconfig,
Zend_Config $newconfig,
IniEditor $editor,
array $parents = array()
) {
$this->diffPropertyUpdates($oldconfig, $newconfig, $editor, $parents);
$this->diffPropertyDeletions($oldconfig, $newconfig, $editor, $parents);
}
/**
* Update the order of the sections in the ini file to match the order of the new config
*
* @return Document A new document with the changed section order applied
*/
protected function updateSectionOrder(Zend_Config $newconfig, IniEditor $editor)
protected function updateSectionOrder(Config $newconfig, Document $oldDoc)
{
$order = array();
foreach ($newconfig as $key => $value) {
if ($value instanceof Zend_Config) {
array_push($order, $key);
}
$doc = new Document();
$doc->commentsDangling = $oldDoc->commentsDangling;
foreach ($newconfig->toArray() as $section => $directives) {
$doc->addSection($oldDoc->getSection($section));
}
$editor->refreshSectionOrder($order);
return $doc;
}
/**
* Search for created and updated properties and use the editor to create or update these entries
*
* @param Zend_Config $oldconfig The config representing the state before the change
* @param Zend_Config $newconfig The config representing the state after the change
* @param IniEditor $editor The editor that should be used to edit the old config file
* @param array $parents The parent keys that should be respected when editing the config
* @param Config $newconfig The config representing the state after the change
* @param Document $doc
*
* @throws ProgrammingError
*/
protected function diffPropertyUpdates(
Zend_Config $oldconfig,
Zend_Config $newconfig,
IniEditor $editor,
array $parents = array()
) {
// The current section. This value is null when processing the section-less root element
$section = empty($parents) ? null : $parents[0];
// Iterate over all properties in the new configuration file and search for changes
foreach ($newconfig as $key => $value) {
$oldvalue = $oldconfig->get($key);
$nextParents = array_merge($parents, array($key));
$keyIdentifier = empty($parents) ? array($key) : array_slice($nextParents, 1, null, true);
if ($value instanceof Zend_Config) {
// The value is a nested Zend_Config, handle it recursively
if ($section === null) {
// Update the section declaration
$extends = $newconfig->getExtends();
$extend = array_key_exists($key, $extends) ? $extends[$key] : null;
$editor->setSection($key, $extend);
}
if ($oldvalue === null) {
$oldvalue = new Zend_Config(array());
}
$this->diffConfigs($oldvalue, $value, $editor, $nextParents);
protected function diffPropertyUpdates(Config $newconfig, Document $doc)
{
foreach ($newconfig->toArray() as $section => $directives) {
if (! is_array($directives)) {
Logger::warning('Section-less property ' . (string)$directives . ' was ignored.');
continue;
}
if (!$doc->hasSection($section)) {
$domSection = new Section($section);
$doc->addSection($domSection);
} else {
// The value is a plain value, use the editor to set it
if (is_numeric($key)) {
$editor->setArrayElement($keyIdentifier, $value, $section);
$domSection = $doc->getSection($section);
}
foreach ($directives as $key => $value) {
if ($value instanceof ConfigObject) {
throw new ProgrammingError('Cannot diff recursive configs');
}
if ($domSection->hasDirective($key)) {
$domSection->getDirective($key)->setValue($value);
} else {
$editor->set($keyIdentifier, $value, $section);
$dir = new Directive($key);
$dir->setValue($value);
$domSection->addDirective($dir);
}
}
}
@ -174,68 +163,35 @@ class IniWriter extends Zend_Config_Writer_FileAbstract
/**
* Search for deleted properties and use the editor to delete these entries
*
* @param Zend_Config $oldconfig The config representing the state before the change
* @param Zend_Config $newconfig The config representing the state after the change
* @param IniEditor $editor The editor that should be used to edit the old config file
* @param array $parents The parent keys that should be respected when editing the config
* @param Config $oldconfig The config representing the state before the change
* @param Config $newconfig The config representing the state after the change
* @param Document $doc
*
* @throws ProgrammingError
*/
protected function diffPropertyDeletions(
Zend_Config $oldconfig,
Zend_Config $newconfig,
IniEditor $editor,
array $parents = array()
) {
// The current section. This value is null when processing the section-less root element
$section = empty($parents) ? null : $parents[0];
// Iterate over all properties in the old configuration file and search for deleted properties
foreach ($oldconfig as $key => $value) {
if ($newconfig->get($key) === null) {
$nextParents = array_merge($parents, array($key));
$keyIdentifier = empty($parents) ? array($key) : array_slice($nextParents, 1, null, true);
foreach ($this->getPropertyIdentifiers($value, $keyIdentifier) as $propertyIdentifier) {
$editor->reset($propertyIdentifier, $section);
protected function diffPropertyDeletions(Config $oldconfig, Config $newconfig, Document $doc)
{
// Iterate over all properties in the old configuration file and remove those that don't
// exist in the new config
foreach ($oldconfig->toArray() as $section => $directives) {
if (! is_array($directives)) {
Logger::warning('Section-less property ' . (string)$directives . ' was ignored.');
continue;
}
$newSection = $newconfig->getSection($section);
if (isset($newSection)) {
$oldDomSection = $doc->getSection($section);
foreach ($directives as $key => $value) {
if ($value instanceof ConfigObject) {
throw new ProgrammingError('Cannot diff recursive configs');
}
if (null === $newSection->get($key) && $oldDomSection->hasDirective($key)) {
$oldDomSection->removeDirective($key);
}
}
} else {
$doc->removeSection($section);
}
}
}
/**
* Return all possible combinations of property identifiers for the given value
*
* @param mixed $value The value to return all combinations for
* @param array $key The root property identifier, if any
*
* @return array All property combinations that are possible
*
* @todo Cannot handle array properties yet (e.g. a.b[]='c')
*/
protected function getPropertyIdentifiers($value, array $key = null)
{
$combinations = array();
$rootProperty = $key !== null ? $key : array();
if ($value instanceof Zend_Config) {
foreach ($value as $subProperty => $subValue) {
$combinations = array_merge(
$combinations,
$this->getPropertyIdentifiers($subValue, array_merge($rootProperty, array($subProperty)))
);
}
} elseif (is_string($value)) {
$combinations[] = $rootProperty;
}
return $combinations;
}
/**
* Getter for filename
*
* @return string
*/
public function getFilename()
{
return $this->_filename;
}
}

View File

@ -4,6 +4,7 @@
use Icinga\Application\Icinga;
use Icinga\Module\Doc\DocController;
use Icinga\Module\Doc\Exception\DocException;
use Icinga\File\Ini\Parser;
class Doc_ModuleController extends DocController
{

View File

@ -31,86 +31,28 @@ class IniWriterTest extends BaseTestCase
public function testWhetherPointInSectionIsNotNormalized()
{
$writer = new IniWriter(
array(
'config' => Config::fromArray(
array(
'section' => array(
'foo.bar' => 1337
),
'section.with.multiple.dots' => array(
'some more' => array(
'nested stuff' => 'With more values'
)
)
)
Config::fromArray(
array(
'section' => array(
'foo.bar' => 1337
),
'filename' => $this->tempFile
)
'section.with.multiple.dots' => array(
'some more.nested stuff' => 'With more values'
)
)
),
$this->tempFile
);
$writer->write();
$config = Config::fromIni($this->tempFile)->toArray();
$this->assertTrue(array_key_exists('section.with.multiple.dots', $config), 'Section names not normalized');
}
public function testWhetherSimplePropertiesAreInsertedInEmptyFiles()
{
$this->markTestSkipped('Implementation has changed. Section-less properties are not supported anymore');
$target = $this->writeConfigToTemporaryFile('');
$config = Config::fromArray(array('key' => 'value'));
$writer = new IniWriter(array('config' => $config, 'filename' => $target));
$writer->write();
$newConfig = Config::fromIni($target);
$this->assertEquals('value', $newConfig->get('key'), 'IniWriter does not insert in empty files');
}
public function testWhetherSimplePropertiesAreInsertedInExistingFiles()
{
$this->markTestSkipped('Implementation has changed. Section-less properties are not supported anymore');
$target = $this->writeConfigToTemporaryFile('key1 = "1"');
$config = Config::fromArray(array('key2' => '2'));
$writer = new IniWriter(array('config' => $config, 'filename' => $target));
$writer->write();
$newConfig = Config::fromIni($target);
$this->assertEquals('2', $newConfig->get('key2'), 'IniWriter does not insert in existing files');
}
/**
* @depends testWhetherSimplePropertiesAreInsertedInExistingFiles
*/
public function testWhetherSimplePropertiesAreUpdated()
{
$this->markTestSkipped('Implementation has changed. Section-less properties are not supported anymore');
$target = $this->writeConfigToTemporaryFile('key = "value"');
$config = Config::fromArray(array('key' => 'eulav'));
$writer = new IniWriter(array('config' => $config, 'filename' => $target));
$writer->write();
$newConfig = Config::fromIni($target);
$this->assertEquals('eulav', $newConfig->get('key'), 'IniWriter does not update simple properties');
}
/**
* @depends testWhetherSimplePropertiesAreInsertedInExistingFiles
*/
public function testWhetherSimplePropertiesAreDeleted()
{
$this->markTestSkipped('Implementation has changed. Section-less properties are not supported anymore');
$target = $this->writeConfigToTemporaryFile('key = "value"');
$config = new Config();
$writer = new IniWriter(array('config' => $config, 'filename' => $target));
$writer->write();
$newConfig = Config::fromIni($target);
$this->assertNull($newConfig->get('key'), 'IniWriter does not delete simple properties');
}
public function testWhetherNestedPropertiesAreInserted()
{
$target = $this->writeConfigToTemporaryFile('');
$config = Config::fromArray(array('a' => array('b' => 'c')));
$writer = new IniWriter(array('config' => $config, 'filename' => $target));
$writer = new IniWriter($config, $target);
$writer->write();
$newConfig = Config::fromIni($target);
@ -126,450 +68,54 @@ class IniWriterTest extends BaseTestCase
);
}
/**
* @depends testWhetherNestedPropertiesAreInserted
*/
public function testWhetherNestedPropertiesAreUpdated()
{
$this->markTestSkipped('Implementation has changed. Section-less properties are not supported anymore');
$target = $this->writeConfigToTemporaryFile('a.b = "c"');
$config = Config::fromArray(array('a' => array('b' => 'cc')));
$writer = new IniWriter(array('config' => $config, 'filename' => $target));
$writer->write();
$newConfig = Config::fromIni($target);
$this->assertInstanceOf(
get_class($newConfig),
$newConfig->get('a'),
'IniWriter does not update nested properties'
);
$this->assertEquals(
'cc',
$newConfig->get('a')->get('b'),
'IniWriter does not update nested properties'
);
}
/**
* @depends testWhetherNestedPropertiesAreInserted
*/
public function testWhetherNestedPropertiesAreDeleted()
{
$this->markTestSkipped('Implementation has changed. Section-less properties are not supported anymore');
$target = $this->writeConfigToTemporaryFile('a.b = "c"');
$config = new Config();
$writer = new IniWriter(array('config' => $config, 'filename' => $target));
$writer->write();
$newConfig = Config::fromIni($target);
$this->assertNull(
$newConfig->get('a'),
'IniWriter does not delete nested properties'
);
}
public function testWhetherSimpleSectionPropertiesAreInserted()
{
$target = $this->writeConfigToTemporaryFile('');
$config = Config::fromArray(array('section' => array('key' => 'value')));
$writer = new IniWriter(array('config' => $config, 'filename' => $target));
$writer->write();
$newConfig = Config::fromIni($target);
$this->assertInstanceOf(
'Icinga\Data\ConfigObject',
$newConfig->getSection('section'),
'IniWriter does not insert sections'
);
$this->assertEquals(
'value',
$newConfig->getSection('section')->get('key'),
'IniWriter does not insert simple section properties'
);
}
/**
* @depends testWhetherSimpleSectionPropertiesAreInserted
*/
public function testWhetherSimpleSectionPropertiesAreUpdated()
{
$target = $this->writeConfigToTemporaryFile(<<<'EOD'
[section]
key = "value"
EOD
);
$config = Config::fromArray(array('section' => array('key' => 'eulav')));
$writer = new IniWriter(array('config' => $config, 'filename' => $target));
$writer->write();
$newConfig = Config::fromIni($target);
$this->assertEquals(
'eulav',
$newConfig->getSection('section')->get('key'),
'IniWriter does not update simple section properties'
);
}
/**
* @depends testWhetherSimpleSectionPropertiesAreInserted
*/
public function testWhetherSimpleSectionPropertiesAreDeleted()
{
$target = $this->writeConfigToTemporaryFile(<<<'EOD'
[section]
key = "value"
EOD
);
$config = Config::fromArray(array('section' => array()));
$writer = new IniWriter(array('config' => $config, 'filename' => $target));
$writer->write();
$newConfig = Config::fromIni($target);
$this->assertNull(
$newConfig->getSection('section')->get('key'),
'IniWriter does not delete simple section properties'
);
}
public function testWhetherNestedSectionPropertiesAreInserted()
{
$this->markTestSkipped('Implementation has changed. Config::fromIni cannot handle nested properties anymore');
$target = $this->writeConfigToTemporaryFile('');
$config = Config::fromArray(array('section' => array('a' => array('b' => 'c'))));
$writer = new IniWriter(array('config' => $config, 'filename' => $target));
$writer->write();
$newConfig = Config::fromIni($target);
$this->assertInstanceOf(
get_class($newConfig),
$newConfig->get('section'),
'IniWriter does not insert sections'
);
$this->assertInstanceOf(
get_class($newConfig),
$newConfig->get('section')->get('a'),
'IniWriter does not insert nested section properties'
);
$this->assertEquals(
'c',
$newConfig->get('section')->get('a')->get('b'),
'IniWriter does not insert nested section properties'
);
}
/**
* @depends testWhetherNestedSectionPropertiesAreInserted
*/
public function testWhetherNestedSectionPropertiesAreUpdated()
{
$target = $this->writeConfigToTemporaryFile(<<<'EOD'
[section]
a.b = "c"
EOD
);
$config = Config::fromArray(array('section' => array('a' => array('b' => 'cc'))));
$writer = new IniWriter(array('config' => $config, 'filename' => $target));
$writer->write();
$newConfig = Config::fromIni($target);
$this->assertEquals(
'cc',
$newConfig->get('section')->get('a')->get('b'),
'IniWriter does not update nested section properties'
);
}
/**
* @depends testWhetherNestedSectionPropertiesAreInserted
*/
public function testWhetherNestedSectionPropertiesAreDeleted()
{
$target = $this->writeConfigToTemporaryFile(<<<'EOD'
[section]
a.b = "c"
EOD
);
$config = Config::fromArray(array('section' => array()));
$writer = new IniWriter(array('config' => $config, 'filename' => $target));
$writer->write();
$newConfig = Config::fromIni($target);
$this->assertNull(
$newConfig->get('section')->get('a'),
'IniWriter does not delete nested section properties'
);
}
public function testWhetherSimplePropertiesOfExtendingSectionsAreInserted()
{
$this->markTestSkipped(
'Implementation has changed. There is no "Extend" functionality anymore in our Config object'
);
$target = $this->writeConfigToTemporaryFile('');
$config = Config::fromArray(
array(
'foo' => array('key1' => '1'),
'bar' => array('key2' => '2')
)
);
$config->setExtend('bar', 'foo');
$writer = new IniWriter(array('config' => $config, 'filename' => $target));
$writer->write();
$newConfig = Config::fromIni($target);
$this->assertInstanceOf(
get_class($newConfig),
$newConfig->get('foo'),
'IniWriter does not insert extended sections'
);
$this->assertInstanceOf(
get_class($newConfig),
$newConfig->get('bar'),
'IniWriter does not insert extending sections'
);
$this->assertEquals(
'2',
$newConfig->get('bar')->get('key2'),
'IniWriter does not insert simple properties into extending sections'
);
$this->assertEquals(
'1',
$newConfig->get('foo')->get('key1'),
'IniWriter does not properly define extending sections'
);
}
/**
* @depends testWhetherSimplePropertiesOfExtendingSectionsAreInserted
*/
public function testWhetherSimplePropertiesOfExtendingSectionsAreUpdated()
{
$this->markTestSkipped(
'Implementation has changed. There is no "Extend" functionality anymore in our Config object'
);
$target = $this->writeConfigToTemporaryFile(<<<'EOD'
[foo]
key1 = "1"
[bar : foo]
key2 = "2"
EOD
);
$config = Config::fromArray(
array(
'foo' => array('key1' => '1'),
'bar' => array('key2' => '22')
)
);
$config->setExtend('bar', 'foo');
$writer = new IniWriter(array('config' => $config, 'filename' => $target));
$writer->write();
$newConfig = Config::fromIni($target);
$this->assertEquals(
'22',
$newConfig->get('bar')->get('key2'),
'IniWriter does not update simple properties of extending sections'
);
}
/**
* @depends testWhetherSimplePropertiesOfExtendingSectionsAreInserted
*/
public function testWhetherSimplePropertiesOfExtendingSectionsAreDeleted()
{
$this->markTestSkipped(
'Implementation has changed. There is no "Extend" functionality anymore in our Config object'
);
$target = $this->writeConfigToTemporaryFile(<<<'EOD'
[foo]
key1 = "1"
[bar : foo]
key2 = "2"
EOD
);
$config = Config::fromArray(
array(
'foo' => array('key1' => '1'),
'bar' => array()
)
);
$config->setExtend('bar', 'foo');
$writer = new IniWriter(array('config' => $config, 'filename' => $target));
$writer->write();
$newConfig = Config::fromIni($target);
$this->assertNull(
$newConfig->get('bar')->get('key2'),
'IniWriter does not delete simple properties of extending sections'
);
}
public function testWhetherNestedPropertiesOfExtendingSectionsAreInserted()
{
$this->markTestSkipped(
'Implementation has changed. There is no "Extend" functionality anymore in our Config object'
);
$target = $this->writeConfigToTemporaryFile('');
$config = Config::fromArray(
array(
'foo' => array('a' => array('b' => 'c')),
'bar' => array('d' => array('e' => 'f'))
)
);
$config->setExtend('bar', 'foo');
$writer = new IniWriter(array('config' => $config, 'filename' => $target));
$writer->write();
$newConfig = Config::fromIni($target);
$this->assertInstanceOf(
get_class($newConfig),
$newConfig->get('foo'),
'IniWriter does not insert extended sections'
);
$this->assertInstanceOf(
get_class($newConfig),
$newConfig->get('bar'),
'IniWriter does not insert extending sections'
);
$this->assertInstanceOf(
get_class($newConfig),
$newConfig->get('bar')->get('d'),
'IniWriter does not insert nested properties into extending sections'
);
$this->assertEquals(
'f',
$newConfig->get('bar')->get('d')->get('e'),
'IniWriter does not insert nested properties into extending sections'
);
$this->assertEquals(
'c',
$newConfig->get('bar')->get('a')->get('b'),
'IniWriter does not properly define extending sections with nested properties'
);
}
/**
* @depends testWhetherNestedPropertiesOfExtendingSectionsAreInserted
*/
public function testWhetherNestedPropertiesOfExtendingSectionsAreUpdated()
{
$target = $this->writeConfigToTemporaryFile(<<<'EOD'
[foo]
a.b = "c"
[bar : foo]
d.e = "f"
EOD
);
$config = Config::fromArray(
array(
'foo' => array('a' => array('b' => 'c')),
'bar' => array('d' => array('e' => 'ff'))
)
);
$config->setExtend('bar', 'foo');
$writer = new IniWriter(array('config' => $config, 'filename' => $target));
$writer->write();
$newConfig = Config::fromIni($target);
$this->assertEquals(
'ff',
$newConfig->get('bar')->get('d')->get('e'),
'IniWriter does not update nested properties of extending sections'
);
}
/**
* @depends testWhetherNestedPropertiesOfExtendingSectionsAreInserted
*/
public function testWhetherNestedPropertiesOfExtendingSectionsAreDeleted()
{
$this->markTestSkipped(
'Implementation has changed. There is no "Extend" functionality anymore in our Config object'
);
$target = $this->writeConfigToTemporaryFile(<<<'EOD'
[foo]
a.b = "c"
[bar : foo]
d.e = "f"
EOD
);
$config = Config::fromArray(
array(
'foo' => array('a' => array('b' => 'c')),
'bar' => array()
)
);
$config->setExtend('bar', 'foo');
$writer = new IniWriter(array('config' => $config, 'filename' => $target));
$writer->write();
$newConfig = Config::fromIni($target);
$this->assertNull(
$newConfig->get('bar')->get('d'),
'IniWriter does not delete nested properties of extending sections'
);
}
public function testWhetherSectionOrderIsUpdated()
{
$config = <<<'EOD'
[one]
key1 = "1"
key2 = "2"
key1 = "1"
key2 = "2"
[two]
a.b = "c"
d.e = "f"
a.b = "c"
d.e = "f"
[three]
key = "value"
foo.bar = "raboof"
key = "value"
foo.bar = "raboof"
EOD;
$reverted = <<<'EOD'
[three]
key = "value"
foo.bar = "raboof"
key = "value"
foo.bar = "raboof"
[two]
a.b = "c"
d.e = "f"
a.b = "c"
d.e = "f"
[one]
key1 = "1"
key2 = "2"
key1 = "1"
key2 = "2"
EOD;
$target = $this->writeConfigToTemporaryFile($config);
$writer = new IniWriter(
array(
'config' => Config::fromArray(
array(
'three' => array(
'foo' => array(
'bar' => 'raboof'
),
'key' => 'value'
),
'two' => array(
'd' => array(
'e' => 'f'
),
'a' => array(
'b' => 'c'
)
),
'one' => array(
'key2' => '2',
'key1' => '1'
)
Config::fromArray(
array(
'three' => array(
'foo.bar' => 'raboof',
'key' => 'value'
),
'two' => array(
'd.e' => 'f',
'a.b' => 'c'
),
'one' => array(
'key2' => '2',
'key1' => '1'
)
),
'filename' => $target
)
)
),
$target
);
$this->assertEquals(
@ -598,15 +144,13 @@ EOD;
EOD;
$target = $this->writeConfigToTemporaryFile($config);
$writer = new IniWriter(
array(
'config' => Config::fromArray(
array(
'two' => array(),
'one' => array()
)
),
'filename' => $target
)
Config::fromArray(
array(
'two' => array(),
'one' => array()
)
),
$target
);
$this->assertEquals(
@ -621,14 +165,14 @@ EOD;
{
$config = <<<'EOD'
; some interesting comment
key = "value"
; another interesting comment
[blarg]
key = "value"
; some dangling comment
; boring comment
EOD;
$target = $this->writeConfigToTemporaryFile($config);
$writer = new IniWriter(
array('config' => Config::fromArray(array('key' => 'value')), 'filename' => $target)
);
$writer = new IniWriter(Config::fromArray(array('blarg' => array('key' => 'value'))), $target);
$this->assertEquals(
trim($config),
@ -640,26 +184,24 @@ EOD;
public function testWhetherCommentsOnPropertyLinesArePreserved()
{
$config = <<<'EOD'
foo = 1337 ; I know what a " and a ' is
bar = 7331 ; I; tend; to; overact; !1!1!!11!111! ;
key = "value" ; some comment for a small sized property
xxl = "very loooooooooooooooooooooong" ; my value is very lo...
[blarg]
foo = "1337" ; I know what a " and a ' is
bar = "7331" ; I; tend; to; overact; !1!1!!11!111! ;
key = "value" ; some comment for a small sized property
xxl = "very loooooooooooooooooooooong" ; my value is very lo...
EOD;
$target = $this->writeConfigToTemporaryFile($config);
$writer = new IniWriter(
array(
'config' => Config::fromArray(
array(
'foo' => 1337,
'bar' => 7331,
'key' => 'value',
'xxl' => 'very loooooooooooooooooooooong'
)
),
'filename' => $target
)
Config::fromArray(
array('blarg' => array(
'foo' => 1337,
'bar' => 7331,
'key' => 'value',
'xxl' => 'very loooooooooooooooooooooong'
))
),
$target
);
$this->assertEquals(
trim($config),
trim($writer->render()),
@ -672,12 +214,10 @@ EOD;
$config = <<<'EOD'
[section]
; some interesting comment, in a section
key = "value"
key = "value"
EOD;
$target = $this->writeConfigToTemporaryFile($config);
$writer = new IniWriter(
array('config' => Config::fromArray(array('section' => array('key' => 'value'))), 'filename' => $target)
);
$writer = new IniWriter(Config::fromArray(array('section' => array('key' => 'value'))), $target);
$this->assertEquals(
trim($config),
@ -690,26 +230,24 @@ EOD;
{
$config = <<<'EOD'
[section]
foo = 1337 ; I know what a " and a ' is
bar = 7331 ; I; tend; to; overact; !1!1!!11!111! ;
key = "value" ; some comment for a small sized property
xxl = "very loooooooooooooooooooooong" ; my value is very lo...
foo = "1337" ; I know what a " and a ' is
bar = "7331" ; I; tend; to; overact; !1!1!!11!111! ;
key = "value" ; some comment for a small sized property
xxl = "very loooooooooooooooooooooong" ; my value is very lo...
EOD;
$target = $this->writeConfigToTemporaryFile($config);
$writer = new IniWriter(
array(
'config' => Config::fromArray(
array(
'section' => array(
'foo' => 1337,
'bar' => 7331,
'key' => 'value',
'xxl' => 'very loooooooooooooooooooooong'
)
Config::fromArray(
array(
'section' => array(
'foo' => 1337,
'bar' => 7331,
'key' => 'value',
'xxl' => 'very loooooooooooooooooooooong'
)
),
'filename' => $target
)
)
),
$target
);
$this->assertEquals(
@ -723,19 +261,17 @@ EOD;
{
$target = $this->writeConfigToTemporaryFile('');
$writer = new IniWriter(
array(
'config' => Config::fromArray(
array(
'section' => array(
'foo' => 'linebreak
Config::fromArray(
array(
'section' => array(
'foo' => 'linebreak
in line',
'linebreak
'linebreak
inkey' => 'blarg'
)
)
),
'filename' => $target
)
)
),
$target
);
$rendered = $writer->render();
@ -746,6 +282,81 @@ inkey' => 'blarg'
);
}
public function testSectionNameEscaping()
{
$config = <<<'EOD'
[section 1]
foo = "1337"
[section (with special chars)]
foo = "baz"
[section/as/arbitrary/path]
foo = "nope"
[section.with.dots.in.it]
foo = "bar"
EOD;
$target = $this->writeConfigToTemporaryFile($config);
$writer = new IniWriter(
Config::fromArray(
array(
'section 1' => array('foo' => 1337),
'section (with special chars)' => array('foo' => 'baz'),
'section/as/arbitrary/path' => array('foo' => 'nope'),
'section.with.dots.in.it' => array('foo' => 'bar')
)
),
$target
);
$this->assertEquals(
trim($config),
trim($writer->render()),
'IniWriter does not handle special chars in section names properly.'
);
}
public function testSectionDeleted()
{
$config = <<<'EOD'
[section 1]
guarg = "1"
[section 2]
foo = "1337"
foo2 = "baz"
foo3 = "nope"
foo4 = "bar"
[section 3]
guard = "2"
EOD;
$deleted = <<<'EOD'
[section 1]
guarg = "1"
[section 3]
guard = "2"
EOD;
$target = $this->writeConfigToTemporaryFile($config);
$writer = new IniWriter(
Config::fromArray(array(
'section 1' => array('guarg' => 1),
'section 3' => array('guard' => 2)
)),
$target
);
$this->assertEquals(
trim($deleted),
trim($writer->render()),
'IniWriter does not delete sections properly'
);
}
/**
* Write a INI-configuration string to a temporary file and return its path
*