// SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\File\Ini; use Icinga\Application\Config; use Icinga\Application\Logger; use Icinga\Data\ConfigObject; use Icinga\Exception\NotWritableError; use Icinga\Exception\ProgrammingError; use Icinga\File\Ini\Dom\Directive; use Icinga\File\Ini\Dom\Document; use Icinga\File\Ini\Dom\Section; /** * A INI file adapter that respects the file structure and the comments of already existing ini files */ class IniWriter { /** * Stores the options * * @var array */ protected $options; /** * The configuration object to write * * @var Config */ protected $config; /** * The mode to set on new files * * @var int */ protected $fileMode; /** * The path to write to * * @var string */ protected $filename; /** * Create a new INI writer * * @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(Config $config, $filename, $filemode = 0660, $options = array()) { $this->config = $config; $this->filename = $filename; $this->fileMode = $filemode; $this->options = $options; } /** * Render the Zend_Config into a config filestring * * @return string */ public function render() { if ($this->filename !== null && file_exists($this->filename)) { $oldconfig = Config::fromIni($this->filename); $content = trim(file_get_contents($this->filename)); } else { $oldconfig = Config::fromArray(array()); $content = ''; } $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 bool $exclusiveLock * * @throws NotWritableError */ public function write($filename = null, $exclusiveLock = false) { $filePath = isset($filename) ? $filename : $this->filename; $setMode = false === file_exists($filePath); if (@file_put_contents($filePath, $this->render(), $exclusiveLock ? LOCK_EX : 0) === false) { throw new NotWritableError('Could not write to file "' . $filePath . '"'); } if ($setMode) { // file was newly created $mode = $this->fileMode; if (is_int($this->fileMode) && false === @chmod($filePath, $this->fileMode)) { throw new NotWritableError(sprintf('Failed to set file mode "%o" on file "%s"', $mode, $filePath)); } } } /** * 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(Config $newconfig, Document $oldDoc) { $doc = new Document(); $dangling = $oldDoc->getCommentsDangling(); if (! empty($dangling)) { $doc->setCommentsDangling($dangling); } foreach ($newconfig->toArray() as $section => $directives) { $doc->addSection($oldDoc->getSection($section)); } return $doc; } /** * Search for created and updated properties and use the editor to create or update these entries * * @param Config $newconfig The config representing the state after the change * @param Document $doc * * @throws ProgrammingError */ 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 { $domSection = $doc->getSection($section); } foreach ($directives as $key => $value) { if ($value === null) { continue; } if ($value instanceof ConfigObject) { throw new ProgrammingError('Cannot diff recursive configs'); } if ($domSection->hasDirective($key)) { $domSection->getDirective($key)->setValue($value); } else { $dir = new Directive($key); $dir->setValue($value); $domSection->addDirective($dir); } } } } /** * Search for deleted properties and use the editor to delete these entries * * @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(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; } if ($newconfig->hasSection($section)) { $newSection = $newconfig->getSection($section); $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); } } } }