diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 11:46:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 11:46:43 +0000 |
commit | 3e02d5aff85babc3ffbfcf52313f2108e313aa23 (patch) | |
tree | b01f3923360c20a6a504aff42d45670c58af3ec5 /library/Icinga/File | |
parent | Initial commit. (diff) | |
download | icingaweb2-upstream.tar.xz icingaweb2-upstream.zip |
Adding upstream version 2.12.1.upstream/2.12.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'library/Icinga/File')
-rw-r--r-- | library/Icinga/File/Csv.php | 47 | ||||
-rw-r--r-- | library/Icinga/File/Ini/Dom/Comment.php | 37 | ||||
-rw-r--r-- | library/Icinga/File/Ini/Dom/Directive.php | 166 | ||||
-rw-r--r-- | library/Icinga/File/Ini/Dom/Document.php | 132 | ||||
-rw-r--r-- | library/Icinga/File/Ini/Dom/Section.php | 190 | ||||
-rw-r--r-- | library/Icinga/File/Ini/IniParser.php | 310 | ||||
-rw-r--r-- | library/Icinga/File/Ini/IniWriter.php | 205 | ||||
-rw-r--r-- | library/Icinga/File/Pdf.php | 81 | ||||
-rw-r--r-- | library/Icinga/File/Storage/LocalFileStorage.php | 164 | ||||
-rw-r--r-- | library/Icinga/File/Storage/StorageInterface.php | 94 | ||||
-rw-r--r-- | library/Icinga/File/Storage/TemporaryLocalFileStorage.php | 59 |
11 files changed, 1485 insertions, 0 deletions
diff --git a/library/Icinga/File/Csv.php b/library/Icinga/File/Csv.php new file mode 100644 index 0000000..56ee233 --- /dev/null +++ b/library/Icinga/File/Csv.php @@ -0,0 +1,47 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\File; + +use Traversable; + +class Csv +{ + protected $query; + + protected function __construct() + { + } + + public static function fromQuery(Traversable $query) + { + $csv = new static(); + $csv->query = $query; + return $csv; + } + + public function dump() + { + header('Content-type: text/csv'); + echo (string) $this; + } + + public function __toString() + { + $first = true; + $csv = ''; + foreach ($this->query as $row) { + if ($first) { + $csv .= implode(',', array_keys((array) $row)) . "\r\n"; + $first = false; + } + $out = array(); + foreach ($row as & $val) { + $out[] = '"' . str_replace('"', '""', $val) . '"'; + } + $csv .= implode(',', $out) . "\r\n"; + } + + return $csv; + } +} diff --git a/library/Icinga/File/Ini/Dom/Comment.php b/library/Icinga/File/Ini/Dom/Comment.php new file mode 100644 index 0000000..c202d0f --- /dev/null +++ b/library/Icinga/File/Ini/Dom/Comment.php @@ -0,0 +1,37 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\File\Ini\Dom; + +/** + * A single comment-line in an INI file + */ +class Comment +{ + /** + * The comment text + * + * @var string + */ + protected $content; + + /** + * Set the text content of this comment + * + * @param $content + */ + public function setContent($content) + { + $this->content = $content; + } + + /** + * Render this comment into INI markup + * + * @return string + */ + public function render() + { + return ';' . $this->content; + } +} diff --git a/library/Icinga/File/Ini/Dom/Directive.php b/library/Icinga/File/Ini/Dom/Directive.php new file mode 100644 index 0000000..4279a5f --- /dev/null +++ b/library/Icinga/File/Ini/Dom/Directive.php @@ -0,0 +1,166 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\File\Ini\Dom; + +use Icinga\Exception\ConfigurationError; + +/** + * A key value pair in a Section + */ +class Directive +{ + /** + * The value of this configuration directive + * + * @var string + */ + protected $key; + + /** + * The immutable name of this configuration directive + * + * @var string + */ + protected $value; + + /** + * Comments added one line before this directive + * + * @var Comment[] The comment lines + */ + protected $commentsPre = null; + + /** + * Comment added at the end of the same line + * + * @var Comment + */ + protected $commentPost = null; + + /** + * @param string $key The name of this configuration directive + * + * @throws ConfigurationError + */ + public function __construct($key) + { + $this->key = trim($key); + if (strlen($this->key) < 1) { + throw new ConfigurationError(sprintf('Ini error: empty directive key.')); + } + } + + /** + * Return the name of this directive + * + * @return string + */ + public function getKey() + { + return $this->key; + } + + /** + * Return the value of this configuration directive + * + * @return string + */ + public function getValue() + { + return $this->value; + } + + /** + * Set the value of this configuration directive + * + * @param string $value + */ + public function setValue($value) + { + $this->value = trim($value); + } + + /** + * Set the comments to be rendered on the line before this directive + * + * @param Comment[] $comments + */ + public function setCommentsPre(array $comments) + { + $this->commentsPre = $comments; + } + + /** + * Return the comments to be rendered on the line before this directive + * + * @return Comment[] + */ + public function getCommentsPre() + { + return $this->commentsPre; + } + + /** + * Set the comment rendered on the same line of this directive + * + * @param Comment $comment + */ + public function setCommentPost(Comment $comment) + { + $this->commentPost = $comment; + } + + /** + * Render this configuration directive into INI markup + * + * @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->sanitizeKey($this->key), $this->sanitizeValue($this->value)); + if (isset($this->commentPost)) { + $str .= ' ' . $this->commentPost->render(); + } + return $str; + } + + /** + * Assure that the given identifier contains no newlines and pending or trailing whitespaces + * + * @param $str The string to sanitize + * + * @return string + */ + protected function sanitizeKey($str) + { + return trim(str_replace(PHP_EOL, ' ', $str)); + } + + /** + * Escape the significant characters in directive values, normalize line breaks and assure that + * the character contains no linebreaks + * + * @param $str The string to sanitize + * + * @return mixed|string + */ + protected function sanitizeValue($str) + { + $str = trim($str); + $str = str_replace('\\', '\\\\', $str); + $str = str_replace('"', '\"', $str); + $str = str_replace("\r", '\r', $str); + $str = str_replace("\n", '\n', $str); + + return $str; + } +} diff --git a/library/Icinga/File/Ini/Dom/Document.php b/library/Icinga/File/Ini/Dom/Document.php new file mode 100644 index 0000000..f38f33e --- /dev/null +++ b/library/Icinga/File/Ini/Dom/Document.php @@ -0,0 +1,132 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\File\Ini\Dom; + +class Document +{ + /** + * The sections of this INI file + * + * @var Section[] + */ + protected $sections = array(); + + /** + * The comemnts at file end that belong to no particular section + * + * @var Comment[] + */ + protected $commentsDangling; + + /** + * Append a section to the end of this INI file + * + * @param Section $section + */ + public function addSection(Section $section) + { + $this->sections[$section->getName()] = $section; + } + + /** + * Return whether this INI file has the section with the given key + * + * @param string $name + * + * @return bool + */ + public function hasSection($name) + { + return isset($this->sections[trim($name)]); + } + + /** + * Return the section with the given name + * + * @param string $name + * + * @return Section + */ + public function getSection($name) + { + return $this->sections[trim($name)]; + } + + /** + * Set the section with the given name + * + * @param string $name + * @param Section $section + * + * @return Section + */ + public function setSection($name, Section $section) + { + return $this->sections[trim($name)] = $section; + } + + /** + * Remove the section with the given name + * + * @param string $name + */ + public function removeSection($name) + { + unset($this->sections[trim($name)]); + } + + /** + * Set the dangling comments at file end that belong to no particular directive + * + * @param Comment[] $comments + */ + public function setCommentsDangling(array $comments) + { + $this->commentsDangling = $comments; + } + + /** + * Get the dangling comments at file end that belong to no particular directive + * + * @return array + */ + public function getCommentsDangling() + { + return $this->commentsDangling; + } + + /** + * Render this document into the corresponding INI markup + * + * @return string + */ + public function render() + { + $sections = array(); + 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; + } + + /** + * Convert $this to an array + * + * @return array + */ + public function toArray() + { + $a = array(); + foreach ($this->sections as $section) { + $a[$section->getName()] = $section->toArray(); + } + return $a; + } +} diff --git a/library/Icinga/File/Ini/Dom/Section.php b/library/Icinga/File/Ini/Dom/Section.php new file mode 100644 index 0000000..5fac5ea --- /dev/null +++ b/library/Icinga/File/Ini/Dom/Section.php @@ -0,0 +1,190 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\File\Ini\Dom; + +use Icinga\Exception\ConfigurationError; + +/** + * A section in an INI file + */ +class Section +{ + /** + * The immutable name of this section + * + * @var string + */ + protected $name; + + /** + * All configuration directives of this section + * + * @var Directive[] + */ + protected $directives = array(); + + /** + * Comments added one line before this section + * + * @var Comment[] + */ + protected $commentsPre; + + /** + * Comment added at the end of the same line + * + * @var Comment + */ + protected $commentPost; + + /** + * @param string $name The immutable name of this section + * + * @throws ConfigurationError When the section name is empty or contains brackets + */ + public function __construct($name) + { + $this->name = trim($name); + if (strlen($this->name) < 1) { + throw new ConfigurationError('Ini file error: empty section identifier'); + } elseif (strpos($name, '[') !== false || strpos($name, ']') !== false) { + throw new ConfigurationError( + 'Ini file error: Section name "%s" must not contain any brackets ([, ])', + $name + ); + } + } + + /** + * Append a directive to the end of this section + * + * @param Directive $directive The directive to append + */ + public function addDirective(Directive $directive) + { + $this->directives[$directive->getKey()] = $directive; + } + + /** + * Remove the directive with the given name + * + * @param string $key They name of the directive to remove + */ + public function removeDirective($key) + { + unset($this->directives[$key]); + } + + /** + * Return whether this section has a directive with the given key + * + * @param string $key The name of the directive + * + * @return bool + */ + public function hasDirective($key) + { + return isset($this->directives[$key]); + } + + /** + * Get the directive with the given key + * + * @param $key string + * + * @return Directive + */ + public function getDirective($key) + { + return $this->directives[$key]; + } + + /** + * Return the name of this section + * + * @return string The name + */ + public function getName() + { + return $this->name; + } + + /** + * Set the comments to be rendered on the line before this section + * + * @param Comment[] $comments + */ + public function setCommentsPre(array $comments) + { + $this->commentsPre = $comments; + } + + /** + * Set the comment rendered on the same line of this section + * + * @param Comment $comment + */ + public function setCommentPost(Comment $comment) + { + $this->commentPost = $comment; + } + + /** + * Render this section into INI markup + * + * @return string + */ + public function render() + { + $dirs = ''; + $i = 0; + foreach ($this->directives as $directive) { + $comments = $directive->getCommentsPre(); + $dirs .= (($i++ > 0 && ! empty($comments)) ? 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->sanitize($this->name)) . $post . PHP_EOL . $dirs; + } + + /** + * Escape the significant characters in sections and normalize line breaks + * + * @param $str The string to sanitize + * + * @return mixed + */ + protected function sanitize($str) + { + $str = trim($str); + $str = str_replace('\\', '\\\\', $str); + $str = str_replace('"', '\\"', $str); + $str = str_replace(';', '\\;', $str); + return str_replace(PHP_EOL, ' ', $str); + } + + /** + * Convert $this to an array + * + * @return array + */ + public function toArray() + { + $a = array(); + foreach ($this->directives as $directive) { + $a[$directive->getKey()] = $directive->getValue(); + } + return $a; + } +} diff --git a/library/Icinga/File/Ini/IniParser.php b/library/Icinga/File/Ini/IniParser.php new file mode 100644 index 0000000..279aa45 --- /dev/null +++ b/library/Icinga/File/Ini/IniParser.php @@ -0,0 +1,310 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\File\Ini; + +use ErrorException; +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; +use Icinga\Exception\NotReadableError; +use Icinga\Application\Config; + +class IniParser +{ + const LINE_START = 0; + const SECTION = 1; + const ESCAPE = 2; + 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; + + /** + * Cancel the parsing with an error + * + * @param $message The error description + * @param $line The line in which the error occured + * + * @throws ConfigurationError + */ + 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; + $escaping = null; + $token = ''; + $line = 0; + + for ($i = 0; $i < strlen($str); $i++) { + $s = $str[$i]; + switch ($state) { + case self::LINE_START: + if (ctype_space($s)) { + continue 2; + } + switch ($s) { + case '[': + $state = self::SECTION; + break; + case ';': + $state = self::COMMENT; + break; + default: + $state = self::DIRECTIVE_KEY; + $token = $s; + break; + } + break; + + case self::ESCAPE: + $token .= $s; + $state = $escaping; + $escaping = null; + break; + + case self::SECTION: + if ($s === "\n") { + self::throwParseError('Unterminated SECTION', $line); + } elseif ($s === '\\') { + $state = self::ESCAPE; + $escaping = self::SECTION; + } elseif ($s !== ']') { + $token .= $s; + } else { + $sec = new Section($token); + $sec->setCommentsPre($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->setCommentsPre($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 2; + } elseif ($s === '"') { + $state = self::DIRECTIVE_VALUE_QUOTED; + } else { + $state = self::DIRECTIVE_VALUE; + $token = $s; + } + break; + + case self::DIRECTIVE_VALUE: + /* + Escaping non-quoted values is not supported by php_parse_ini, it might + be reasonable to include in case we are switching completely our own + parser implementation + */ + 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 === '\\') { + $state = self::ESCAPE; + $escaping = self::DIRECTIVE_VALUE_QUOTED; + } 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->setContent($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->setCommentPost($com); + } else { + $sec->setCommentPost($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->setContent($token); + if ($state === self::COMMENT_END) { + if (isset($dir)) { + $dir->setCommentPost($com); + } else { + $sec->setCommentPost($com); + } + } else { + $coms[] = $com; + } + break; + + case self::DIRECTIVE_VALUE: + $dir->setValue($token); + $sec->addDirective($dir); + break; + + case self::ESCAPE: + 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->setCommentsDangling($coms); + } + return $doc; + } + + /** + * Read the ini file and parse it with ::parseIni() + * + * @param string $file The ini file to read + * + * @return Config + * @throws NotReadableError When the file cannot be read + */ + public static function parseIniFile($file) + { + if (($path = realpath($file)) === false) { + throw new NotReadableError('Couldn\'t compute the absolute path of `%s\'', $file); + } + + if (($content = file_get_contents($path)) === false) { + throw new NotReadableError('Couldn\'t read the file `%s\'', $path); + } + + try { + $configArray = parse_ini_string($content, true, INI_SCANNER_RAW); + } catch (ErrorException $e) { + throw new ConfigurationError('Couldn\'t parse the INI file `%s\'', $path, $e); + } + + $unescaped = array(); + foreach ($configArray as $section => $options) { + $unescaped[self::unescapeSectionName($section)] = array_map([__CLASS__, 'unescapeOptionValue'], $options); + } + + return Config::fromArray($unescaped)->setConfigFile($file); + } + + /** + * Unescape significant characters in the given section name + * + * @param string $str + * + * @return string + */ + protected static function unescapeSectionName($str) + { + $str = str_replace('\"', '"', $str); + $str = str_replace('\;', ';', $str); + + return str_replace('\\\\', '\\', $str); + } + + /** + * Unescape significant characters in the given option value + * + * @param string $str + * + * @return string + */ + protected static function unescapeOptionValue($str) + { + $str = str_replace('\n', "\n", $str); + $str = str_replace('\r', "\r", $str); + $str = str_replace('\"', '"', $str); + $str = str_replace('\\\\', '\\', $str); + + // This replacement is a work-around for PHP bug #76965. Fixed with versions 7.1.24, 7.2.12 and 7.3.0. + return preg_replace('~^([\'"])(.*?)\1\s+$~', '$2', $str); + } +} diff --git a/library/Icinga/File/Ini/IniWriter.php b/library/Icinga/File/Ini/IniWriter.php new file mode 100644 index 0000000..1f470b0 --- /dev/null +++ b/library/Icinga/File/Ini/IniWriter.php @@ -0,0 +1,205 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\File\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 Icinga\Application\Config; + +/** + * 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 (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 Zend_Config_Exception + */ + 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 Zend_Config_Exception('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 Zend_Config_Exception(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); + } + } + } +} diff --git a/library/Icinga/File/Pdf.php b/library/Icinga/File/Pdf.php new file mode 100644 index 0000000..1b78424 --- /dev/null +++ b/library/Icinga/File/Pdf.php @@ -0,0 +1,81 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\File; + +use Dompdf\Dompdf; +use Dompdf\Options; +use Icinga\Application\Icinga; +use Icinga\Exception\ProgrammingError; +use Icinga\Util\Environment; +use Icinga\Web\Hook; +use Icinga\Web\Url; + +class Pdf +{ + protected function assertNoHeadersSent() + { + if (headers_sent()) { + throw new ProgrammingError( + 'Could not send pdf-response, content already written to output.' + ); + } + } + + public function renderControllerAction($controller) + { + $this->assertNoHeadersSent(); + + Environment::raiseMemoryLimit('512M'); + Environment::raiseExecutionTime(300); + + $viewRenderer = $controller->getHelper('viewRenderer'); + $viewRenderer->postDispatch(); + + $layoutHelper = $controller->getHelper('layout'); + $oldLayout = $layoutHelper->getLayout(); + $layout = $layoutHelper->setLayout('pdf'); + + $layout->content = $controller->getResponse(); + $html = $layout->render(); + + // Restore previous layout and reset content, to properly show errors + $controller->getResponse()->clearBody($viewRenderer->getResponseSegment()); + $layoutHelper->setLayout($oldLayout); + + $imgDir = Url::fromPath('img'); + $html = preg_replace( + '~src="' . $imgDir . '/~', + 'src="' . Icinga::app()->getBootstrapDirectory() . '/img/', + $html + ); + + $request = $controller->getRequest(); + + if (Hook::has('Pdfexport')) { + $pdfexport = Hook::first('Pdfexport'); + $pdfexport->streamPdfFromHtml($html, sprintf( + '%s-%s-%d', + $request->getControllerName(), + $request->getActionName(), + time() + )); + + return; + } + + $options = new Options(); + $options->set('defaultPaperSize', 'A4'); + $dompdf = new Dompdf($options); + $dompdf->loadHtml($html); + $dompdf->render(); + $dompdf->stream( + sprintf( + '%s-%s-%d', + $request->getControllerName(), + $request->getActionName(), + time() + ) + ); + } +} diff --git a/library/Icinga/File/Storage/LocalFileStorage.php b/library/Icinga/File/Storage/LocalFileStorage.php new file mode 100644 index 0000000..e1ed641 --- /dev/null +++ b/library/Icinga/File/Storage/LocalFileStorage.php @@ -0,0 +1,164 @@ +<?php +/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */ + +namespace Icinga\File\Storage; + +use ErrorException; +use Icinga\Exception\AlreadyExistsException; +use Icinga\Exception\NotFoundError; +use Icinga\Exception\NotReadableError; +use Icinga\Exception\NotWritableError; +use InvalidArgumentException; +use RecursiveDirectoryIterator; +use RecursiveIteratorIterator; +use Traversable; +use UnexpectedValueException; + +/** + * Stores files in the local file system + */ +class LocalFileStorage implements StorageInterface +{ + /** + * The root directory of this storage + * + * @var string + */ + protected $baseDir; + + /** + * Constructor + * + * @param string $baseDir The root directory of this storage + */ + public function __construct($baseDir) + { + $this->baseDir = rtrim($baseDir, DIRECTORY_SEPARATOR); + } + + public function getIterator(): Traversable + { + try { + return new RecursiveIteratorIterator( + new RecursiveDirectoryIterator( + $this->baseDir, + RecursiveDirectoryIterator::CURRENT_AS_FILEINFO + | RecursiveDirectoryIterator::KEY_AS_PATHNAME + | RecursiveDirectoryIterator::SKIP_DOTS + ) + ); + } catch (UnexpectedValueException $e) { + throw new NotReadableError('Couldn\'t read the directory "%s": %s', $this->baseDir, $e); + } + } + + public function has($path) + { + return is_file($this->resolvePath($path)); + } + + public function create($path, $content) + { + $resolvedPath = $this->resolvePath($path); + + $this->ensureDir(dirname($resolvedPath)); + + try { + $stream = fopen($resolvedPath, 'x'); + } catch (ErrorException $e) { + throw new AlreadyExistsException('Couldn\'t create the file "%s": %s', $path, $e); + } + + try { + fclose($stream); + chmod($resolvedPath, 0664); + file_put_contents($resolvedPath, $content); + } catch (ErrorException $e) { + throw new NotWritableError('Couldn\'t create the file "%s": %s', $path, $e); + } + + return $this; + } + + public function read($path) + { + $resolvedPath = $this->resolvePath($path, true); + + try { + return file_get_contents($resolvedPath); + } catch (ErrorException $e) { + throw new NotReadableError('Couldn\'t read the file "%s": %s', $path, $e); + } + } + + public function update($path, $content) + { + $resolvedPath = $this->resolvePath($path, true); + + try { + file_put_contents($resolvedPath, $content); + } catch (ErrorException $e) { + throw new NotWritableError('Couldn\'t update the file "%s": %s', $path, $e); + } + + return $this; + } + + public function delete($path) + { + $resolvedPath = $this->resolvePath($path, true); + + try { + unlink($resolvedPath); + } catch (ErrorException $e) { + throw new NotWritableError('Couldn\'t delete the file "%s": %s', $path, $e); + } + + return $this; + } + + public function resolvePath($path, $assertExistence = false) + { + if ($assertExistence && ! $this->has($path)) { + throw new NotFoundError('No such file: "%s"', $path); + } + + $steps = preg_split('~/~', $path, -1, PREG_SPLIT_NO_EMPTY); + for ($i = 0; $i < count($steps);) { + if ($steps[$i] === '.') { + array_splice($steps, $i, 1); + } elseif ($steps[$i] === '..' && $i > 0 && $steps[$i - 1] !== '..') { + array_splice($steps, $i - 1, 2); + --$i; + } else { + ++$i; + } + } + + if ($steps[0] === '..') { + throw new InvalidArgumentException('Paths above the base directory are not allowed'); + } + + return $this->baseDir . DIRECTORY_SEPARATOR . implode(DIRECTORY_SEPARATOR, $steps); + } + + /** + * Ensure that the given directory exists + * + * @param string $dir + * + * @throws NotWritableError + */ + protected function ensureDir($dir) + { + if (! is_dir($dir)) { + $this->ensureDir(dirname($dir)); + + try { + mkdir($dir, 02770); + } catch (ErrorException $e) { + throw new NotWritableError('Couldn\'t create the directory "%s": %s', $dir, $e); + } + } + } +} diff --git a/library/Icinga/File/Storage/StorageInterface.php b/library/Icinga/File/Storage/StorageInterface.php new file mode 100644 index 0000000..f416b00 --- /dev/null +++ b/library/Icinga/File/Storage/StorageInterface.php @@ -0,0 +1,94 @@ +<?php +/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */ + +namespace Icinga\File\Storage; + +use Icinga\Exception\AlreadyExistsException; +use Icinga\Exception\NotFoundError; +use Icinga\Exception\NotReadableError; +use Icinga\Exception\NotWritableError; +use IteratorAggregate; +use Traversable; + +interface StorageInterface extends IteratorAggregate +{ + /** + * Iterate over all existing files' paths + * + * @return Traversable + * + * @throws NotReadableError If the file list can't be read + */ + public function getIterator(): Traversable; + + /** + * Return whether the given file exists + * + * @param string $path + * + * @return bool + */ + public function has($path); + + /** + * Create the given file with the given content + * + * @param string $path + * @param mixed $content + * + * @return $this + * + * @throws AlreadyExistsException If the file already exists + * @throws NotWritableError If the file can't be written to + */ + public function create($path, $content); + + /** + * Load the content of the given file + * + * @param string $path + * + * @return mixed + * + * @throws NotFoundError If the file can't be found + * @throws NotReadableError If the file can't be read + */ + public function read($path); + + /** + * Overwrite the given file with the given content + * + * @param string $path + * @param mixed $content + * + * @return $this + * + * @throws NotFoundError If the file can't be found + * @throws NotWritableError If the file can't be written to + */ + public function update($path, $content); + + /** + * Delete the given file + * + * @param string $path + * + * @return $this + * + * @throws NotFoundError If the file can't be found + * @throws NotWritableError If the file can't be deleted + */ + public function delete($path); + + /** + * Get the absolute path to the given file + * + * @param string $path + * @param bool $assertExistence Whether to require that the given file exists + * + * @return string + * + * @throws NotFoundError If the file has to exist, but can't be found + */ + public function resolvePath($path, $assertExistence = false); +} diff --git a/library/Icinga/File/Storage/TemporaryLocalFileStorage.php b/library/Icinga/File/Storage/TemporaryLocalFileStorage.php new file mode 100644 index 0000000..faf91f5 --- /dev/null +++ b/library/Icinga/File/Storage/TemporaryLocalFileStorage.php @@ -0,0 +1,59 @@ +<?php +/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */ + +namespace Icinga\File\Storage; + +use ErrorException; +use RecursiveDirectoryIterator; +use RecursiveIteratorIterator; + +/** + * Stores files in a temporary directory + */ +class TemporaryLocalFileStorage extends LocalFileStorage +{ + /** + * Constructor + */ + public function __construct() + { + $path = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid(); + mkdir($path, 0700); + + parent::__construct($path); + } + + /** + * Destructor + */ + public function __destruct() + { + // Some classes may have cleaned up the tmp file, so we need to check this + // beforehand to prevent an unexpected crash. + if (! @realpath($this->baseDir)) { + return; + } + + $directoryIterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator( + $this->baseDir, + RecursiveDirectoryIterator::CURRENT_AS_FILEINFO + | RecursiveDirectoryIterator::KEY_AS_PATHNAME + | RecursiveDirectoryIterator::SKIP_DOTS + ), + RecursiveIteratorIterator::CHILD_FIRST + ); + + foreach ($directoryIterator as $path => $entry) { + /** @var \SplFileInfo $entry */ + + if ($entry->isDir() && ! $entry->isLink()) { + rmdir($path); + } else { + unlink($path); + } + } + + rmdir($this->baseDir); + } +} |