summaryrefslogtreecommitdiffstats
path: root/library/Icinga/File
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 11:46:43 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 11:46:43 +0000
commit3e02d5aff85babc3ffbfcf52313f2108e313aa23 (patch)
treeb01f3923360c20a6a504aff42d45670c58af3ec5 /library/Icinga/File
parentInitial commit. (diff)
downloadicingaweb2-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.php47
-rw-r--r--library/Icinga/File/Ini/Dom/Comment.php37
-rw-r--r--library/Icinga/File/Ini/Dom/Directive.php166
-rw-r--r--library/Icinga/File/Ini/Dom/Document.php132
-rw-r--r--library/Icinga/File/Ini/Dom/Section.php190
-rw-r--r--library/Icinga/File/Ini/IniParser.php310
-rw-r--r--library/Icinga/File/Ini/IniWriter.php205
-rw-r--r--library/Icinga/File/Pdf.php81
-rw-r--r--library/Icinga/File/Storage/LocalFileStorage.php164
-rw-r--r--library/Icinga/File/Storage/StorageInterface.php94
-rw-r--r--library/Icinga/File/Storage/TemporaryLocalFileStorage.php59
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);
+ }
+}