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/Util | |
parent | Initial commit. (diff) | |
download | icingaweb2-3e02d5aff85babc3ffbfcf52313f2108e313aa23.tar.xz icingaweb2-3e02d5aff85babc3ffbfcf52313f2108e313aa23.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/Util')
-rw-r--r-- | library/Icinga/Util/ASN1.php | 102 | ||||
-rw-r--r-- | library/Icinga/Util/Color.php | 121 | ||||
-rw-r--r-- | library/Icinga/Util/ConfigAwareFactory.php | 18 | ||||
-rw-r--r-- | library/Icinga/Util/Csp.php | 107 | ||||
-rw-r--r-- | library/Icinga/Util/Dimension.php | 123 | ||||
-rw-r--r-- | library/Icinga/Util/DirectoryIterator.php | 214 | ||||
-rw-r--r-- | library/Icinga/Util/EnumeratingFilterIterator.php | 30 | ||||
-rw-r--r-- | library/Icinga/Util/Environment.php | 42 | ||||
-rw-r--r-- | library/Icinga/Util/File.php | 195 | ||||
-rw-r--r-- | library/Icinga/Util/Format.php | 197 | ||||
-rw-r--r-- | library/Icinga/Util/GlobFilter.php | 182 | ||||
-rw-r--r-- | library/Icinga/Util/Json.php | 151 | ||||
-rw-r--r-- | library/Icinga/Util/LessParser.php | 15 | ||||
-rw-r--r-- | library/Icinga/Util/StringHelper.php | 184 | ||||
-rw-r--r-- | library/Icinga/Util/TimezoneDetect.php | 107 |
15 files changed, 1788 insertions, 0 deletions
diff --git a/library/Icinga/Util/ASN1.php b/library/Icinga/Util/ASN1.php new file mode 100644 index 0000000..9e00258 --- /dev/null +++ b/library/Icinga/Util/ASN1.php @@ -0,0 +1,102 @@ +<?php +/* Icinga Web 2 | (c) 2018 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Util; + +use DateInterval; +use DateTime; +use InvalidArgumentException; + +/** + * Parsers for ASN.1 types + */ +class ASN1 +{ + /** + * Parse the given value based on the "3.3.13. Generalized Time" syntax as specified by IETF RFC 4517 + * + * @param string $value + * + * @return DateTime + * + * @throws InvalidArgumentException + * + * @see https://tools.ietf.org/html/rfc4517#section-3.3.13 + */ + public static function parseGeneralizedTime($value) + { + $generalizedTimePattern = <<<EOD +/\A + (?P<YmdH> + [0-9]{4} # century year + (?:0[1-9]|1[0-2]) # month + (?:0[1-9]|[12][0-9]|3[0-1]) # day + (?:[01][0-9]|2[0-3]) # hour + ) + (?: + (?P<i>[0-5][0-9]) # minute + (?P<s>[0-5][0-9]|60)? # second or leap-second + )? + (?:[.,](?P<frac>[0-9]+))? # fraction + (?P<tz> # g-time-zone + Z + | + [-+] + (?:[01][0-9]|2[0-3]) # hour + (?:[0-5][0-9])? # minute + ) +\z/x +EOD; + + $matches = array(); + + if (preg_match($generalizedTimePattern, $value, $matches)) { + $dateTimeRaw = $matches['YmdH']; + $dateTimeFormat = 'YmdH'; + + if ($matches['i'] !== '') { + $dateTimeRaw .= $matches['i']; + $dateTimeFormat .= 'i'; + + if ($matches['s'] !== '') { + $dateTimeRaw .= $matches['s']; + $dateTimeFormat .= 's'; + $fractionOfSeconds = 1; + } else { + $fractionOfSeconds = 60; + } + } else { + $fractionOfSeconds = 3600; + } + + $dateTimeFormat .= 'O'; + + if ($matches['tz'] === 'Z') { + $dateTimeRaw .= '+0000'; + } else { + $dateTimeRaw .= $matches['tz']; + + if (strlen($matches['tz']) === 3) { + $dateTimeRaw .= '00'; + } + } + + $dateTime = DateTime::createFromFormat($dateTimeFormat, $dateTimeRaw); + + if ($dateTime !== false) { + if (isset($matches['frac'])) { + $dateTime->add(new DateInterval( + 'PT' . round((float) ('0.' . $matches['frac']) * $fractionOfSeconds) . 'S' + )); + } + + return $dateTime; + } + } + + throw new InvalidArgumentException(sprintf( + 'Failed to parse %s based on the ASN.1 standard (GeneralizedTime)', + var_export($value, true) + )); + } +} diff --git a/library/Icinga/Util/Color.php b/library/Icinga/Util/Color.php new file mode 100644 index 0000000..cf88f41 --- /dev/null +++ b/library/Icinga/Util/Color.php @@ -0,0 +1,121 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Util; + +/** + * Provide functions to change and convert colors. + */ +class Color +{ + /** + * Convert a given color string to an rgb-array containing + * each color as a decimal value. + * + * @param $color The color-string #RRGGBB + * + * @return array The converted rgb-array. + */ + public static function rgbAsArray($color) + { + if (substr($color, 0, 1) !== '#') { + $color = '#' . $color; + } + if (strlen($color) !== 7) { + return; + } + $r = (float)intval(substr($color, 1, 2), 16); + $g = (float)intval(substr($color, 3, 2), 16); + $b = (float)intval(substr($color, 5, 2), 16); + return array($r, $g, $b); + } + + /** + * Convert a rgb array to a color-string + * + * @param array $rgb The rgb-array + * + * @return string The color string #RRGGBB + */ + public static function arrayToRgb(array $rgb) + { + $r = (string)dechex($rgb[0]); + $g = (string)dechex($rgb[1]); + $b = (string)dechex($rgb[2]); + return '#' + . (strlen($r) > 1 ? $r : '0' . $r) + . (strlen($g) > 1 ? $g : '0' . $g) + . (strlen($b) > 1 ? $b : '0' . $b); + } + + /** + * Change the saturation for a given color. + * + * @param $color string The color to change + * @param $change float The change. + * 0.0 creates a black-and-white image. + * 0.5 reduces the color saturation by half. + * 1.0 causes no change. + * 2.0 doubles the color saturation. + * @return string + */ + public static function changeSaturation($color, $change) + { + return self::arrayToRgb(self::changeRgbSaturation(self::rgbAsArray($color), $change)); + } + + /** + * Change the brightness for a given color + * + * @param $color string The color to change + * @param $change float The change in percent + * + * @return string + */ + public static function changeBrightness($color, $change) + { + return self::arrayToRgb(self::changeRgbBrightness(self::rgbAsArray($color), $change)); + } + + /** + * @param $rgb array The rgb-array to change + * @param $change float The factor + * + * @return array The updated rgb-array + */ + private static function changeRgbSaturation(array $rgb, $change) + { + $pr = 0.499; // 0.299 + $pg = 0.387; // 0.587 + $pb = 0.114; // 0.114 + $r = $rgb[0]; + $g = $rgb[1]; + $b = $rgb[2]; + $p = sqrt( + $r * $r * $pr + + $g * $g * $pg + + $b * $b * $pb + ); + $rgb[0] = (int)($p + ($r - $p) * $change); + $rgb[1] = (int)($p + ($g - $p) * $change); + $rgb[2] = (int)($p + ($b - $p) * $change); + return $rgb; + } + + /** + * @param $rgb array The rgb-array to change + * @param $change float The factor + * + * @return array The updated rgb-array + */ + private static function changeRgbBrightness(array $rgb, $change) + { + $red = $rgb[0] + ($rgb[0] * $change); + $green = $rgb[1] + ($rgb[1] * $change); + $blue = $rgb[2] + ($rgb[2] * $change); + $rgb[0] = $red < 255 ? (int) $red : 255; + $rgb[1] = $green < 255 ? (int) $green : 255; + $rgb[2] = $blue < 255 ? (int) $blue : 255; + return $rgb; + } +} diff --git a/library/Icinga/Util/ConfigAwareFactory.php b/library/Icinga/Util/ConfigAwareFactory.php new file mode 100644 index 0000000..133887a --- /dev/null +++ b/library/Icinga/Util/ConfigAwareFactory.php @@ -0,0 +1,18 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Util; + +/** + * Interface defining a factory which is configured at runtime + */ +interface ConfigAwareFactory +{ + /** + * Set the factory's config + * + * @param mixed $config + * @throws \Icinga\Exception\ConfigurationError if the given config is not valid + */ + public static function setConfig($config); +} diff --git a/library/Icinga/Util/Csp.php b/library/Icinga/Util/Csp.php new file mode 100644 index 0000000..bd275c6 --- /dev/null +++ b/library/Icinga/Util/Csp.php @@ -0,0 +1,107 @@ +<?php + +/* Icinga Web 2 | (c) 2023 Icinga GmbH | GPLv2+ */ + +namespace Icinga\Util; + +use Icinga\Web\Response; +use Icinga\Web\Window; +use RuntimeException; + +use function ipl\Stdlib\get_php_type; + +/** + * Helper to enable strict content security policy (CSP) + * + * {@see static::addHeader()} adds a strict Content-Security-Policy header with a nonce to still support dynamic CSS + * securely. + * Note that {@see static::createNonce()} must be called first. + * Use {@see static::getStyleNonce()} to access the nonce for dynamic CSS. + * + * A nonce is not created for dynamic JS, + * and it is questionable whether this will ever be supported. + */ +class Csp +{ + /** @var static */ + protected static $instance; + + /** @var ?string */ + protected $styleNonce; + + /** Singleton */ + private function __construct() + { + } + + /** + * Add Content-Security-Policy header with a nonce for dynamic CSS + * + * Note that {@see static::createNonce()} must be called beforehand. + * + * @param Response $response + * + * @throws RuntimeException If no nonce set for CSS + */ + public static function addHeader(Response $response): void + { + $csp = static::getInstance(); + + if (empty($csp->styleNonce)) { + throw new RuntimeException('No nonce set for CSS'); + } + + $response->setHeader('Content-Security-Policy', "style-src 'self' 'nonce-$csp->styleNonce';", true); + } + + /** + * Set/recreate nonce for dynamic CSS + * + * Should always be called upon initial page loads or page reloads, + * as it sets/recreates a nonce for CSS and writes it to a window-aware session. + */ + public static function createNonce(): void + { + $csp = static::getInstance(); + $csp->styleNonce = base64_encode(random_bytes(16)); + + Window::getInstance()->getSessionNamespace('csp')->set('style_nonce', $csp->styleNonce); + } + + /** + * Get nonce for dynamic CSS + * + * @return ?string + */ + public static function getStyleNonce(): ?string + { + return static::getInstance()->styleNonce; + } + + /** + * Get the CSP instance + * + * @return self + */ + protected static function getInstance(): self + { + if (static::$instance === null) { + $csp = new static(); + $nonce = Window::getInstance()->getSessionNamespace('csp')->get('style_nonce'); + if ($nonce !== null && ! is_string($nonce)) { + throw new RuntimeException( + sprintf( + 'Nonce value is expected to be string, got %s instead', + get_php_type($nonce) + ) + ); + } + + $csp->styleNonce = $nonce; + + static::$instance = $csp; + } + + return static::$instance; + } +} diff --git a/library/Icinga/Util/Dimension.php b/library/Icinga/Util/Dimension.php new file mode 100644 index 0000000..6860fd8 --- /dev/null +++ b/library/Icinga/Util/Dimension.php @@ -0,0 +1,123 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Util; + +class Dimension +{ + /** + * Defines this dimension as nr of pixels + */ + const UNIT_PX = "px"; + + /** + * Defines this dimension as width of 'M' in current font + */ + const UNIT_EM = "em"; + + /** + * Defines this dimension as a percentage value + */ + const UNIT_PERCENT = "%"; + + /** + * Defines this dimension in points + */ + const UNIT_PT = "pt"; + + /** + * The current set value for this dimension + * + * @var int + */ + private $value = 0; + + /** + * The unit to interpret the value with + * + * @var string + */ + private $unit = self::UNIT_PX; + + /** + * Create a new Dimension object with the given size and unit + * + * @param int $value The new value + * @param string $unit The unit to use (default: px) + */ + public function __construct($value, $unit = self::UNIT_PX) + { + $this->setValue($value, $unit); + } + + /** + * Change the value and unit of this dimension + * + * @param int $value The new value + * @param string $unit The unit to use (default: px) + */ + public function setValue($value, $unit = self::UNIT_PX) + { + $this->value = intval($value); + $this->unit = $unit; + } + + /** + * Return true when the value is > 0 + * + * @return bool + */ + public function isDefined() + { + return $this->value > 0; + } + + /** + * Return the underlying value without unit information + * + * @return int + */ + public function getValue() + { + return $this->value; + } + + /** + * Return the unit used for the value + * + * @return string + */ + public function getUnit() + { + return $this->unit; + } + + /** + * Return this value with it's according unit as a string + * + * @return string + */ + public function __toString() + { + if (!$this->isDefined()) { + return ""; + } + return $this->value.$this->unit; + } + + /** + * Create a new Dimension object from a string containing the numeric value and the dimension (e.g. 200px, 20%) + * + * @param $string The string to parse + * + * @return Dimension + */ + public static function fromString($string) + { + $matches = array(); + if (!preg_match_all('/^ *([0-9]+)(px|pt|em|\%) */i', $string, $matches)) { + return new Dimension(0); + } + return new Dimension(intval($matches[1][0]), $matches[2][0]); + } +} diff --git a/library/Icinga/Util/DirectoryIterator.php b/library/Icinga/Util/DirectoryIterator.php new file mode 100644 index 0000000..cee37b6 --- /dev/null +++ b/library/Icinga/Util/DirectoryIterator.php @@ -0,0 +1,214 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Util; + +use ArrayIterator; +use InvalidArgumentException; +use RecursiveIterator; + +/** + * Iterator for traversing a directory + */ +class DirectoryIterator implements RecursiveIterator +{ + /** + * Iterate files first + * + * @var int + */ + const FILES_FIRST = 1; + + /** + * Current directory item + * + * @var string|false + */ + private $current; + + /** + * The file extension to filter for + * + * @var string + */ + protected $extension; + + /** + * Scanned files + * + * @var ArrayIterator + */ + private $files; + + /** + * Iterator flags + * + * @var int + */ + protected $flags; + + /** + * Current key + * + * @var string|false + */ + private $key; + + /** + * The path of the directory to traverse + * + * @var string + */ + protected $path; + + /** + * Directory queue if FILES_FIRST flag is set + * + * @var array + */ + private $queue; + + /** + * Whether to skip empty files + * + * Defaults to true. + * + * @var bool + */ + protected $skipEmpty = true; + + /** + * Whether to skip hidden files + * + * Defaults to true. + * + * @var bool + */ + protected $skipHidden = true; + + /** + * Create a new directory iterator from path + * + * The given path will not be validated whether it is readable. Use {@link isReadable()} before creating a new + * directory iterator instance. + * + * @param string $path The path of the directory to traverse + * @param string $extension The file extension to filter for. A leading dot is optional + * @param int $flags Iterator flags + */ + public function __construct($path, $extension = null, $flags = null) + { + if (empty($path)) { + throw new InvalidArgumentException('The path can\'t be empty'); + } + $this->path = $path; + if (! empty($extension)) { + $this->extension = '.' . ltrim($extension, '.'); + } + if ($flags !== null) { + $this->flags = $flags; + } + } + + /** + * Check whether the given path is a directory and is readable + * + * @param string $path The path of the directory + * + * @return bool + */ + public static function isReadable($path) + { + return is_dir($path) && is_readable($path); + } + + public function hasChildren(): bool + { + return static::isReadable($this->current); + } + + public function getChildren(): DirectoryIterator + { + return new static($this->current, $this->extension, $this->flags); + } + + #[\ReturnTypeWillChange] + public function current() + { + return $this->current; + } + + public function next(): void + { + $path = null; + do { + $this->files->next(); + $skip = false; + if (! $this->files->valid()) { + $file = false; + $path = false; + break; + } else { + $file = $this->files->current(); + do { + if ($this->skipHidden && $file[0] === '.') { + $skip = true; + break; + } + + $path = $this->path . '/' . $file; + + if (is_dir($path)) { + if ($this->flags & static::FILES_FIRST === static::FILES_FIRST) { + $this->queue[] = array($path, $file); + $skip = true; + } + break; + } + + if ($this->skipEmpty && ! filesize($path)) { + $skip = true; + break; + } + + if ($this->extension && ! StringHelper::endsWith($file, $this->extension)) { + $skip = true; + break; + } + } while (0); + } + } while ($skip); + + /** @noinspection PhpUndefinedVariableInspection */ + + if ($path === false && ! empty($this->queue)) { + list($path, $file) = array_shift($this->queue); + } + + $this->current = $path; + $this->key = $file; + } + + #[\ReturnTypeWillChange] + public function key() + { + return $this->key; + } + + public function valid(): bool + { + return $this->current !== false; + } + + public function rewind(): void + { + if ($this->files === null) { + $files = scandir($this->path); + natcasesort($files); + $this->files = new ArrayIterator($files); + } + $this->files->rewind(); + $this->queue = array(); + $this->next(); + } +} diff --git a/library/Icinga/Util/EnumeratingFilterIterator.php b/library/Icinga/Util/EnumeratingFilterIterator.php new file mode 100644 index 0000000..0659961 --- /dev/null +++ b/library/Icinga/Util/EnumeratingFilterIterator.php @@ -0,0 +1,30 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Util; + +use FilterIterator; + +/** + * Class EnumeratingFilterIterator + * + * FilterIterator with continuous numeric key (index) + */ +abstract class EnumeratingFilterIterator extends FilterIterator +{ + /** + * @var int + */ + private $index; + + public function rewind(): void + { + parent::rewind(); + $this->index = 0; + } + + public function key(): int + { + return $this->index++; + } +} diff --git a/library/Icinga/Util/Environment.php b/library/Icinga/Util/Environment.php new file mode 100644 index 0000000..8d47b84 --- /dev/null +++ b/library/Icinga/Util/Environment.php @@ -0,0 +1,42 @@ +<?php +/* Icinga Web 2 | (c) 2018 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Util; + +/** + * Helper for configuring the PHP environment + */ +class Environment +{ + /** + * Raise the PHP memory_limit + * + * Unless it is not already set to a higher limit + * + * @param string|int $minimum + */ + public static function raiseMemoryLimit($minimum = '512M') + { + if (is_string($minimum)) { + $minimum = Format::unpackShorthandBytes($minimum); + } + + if (Format::unpackShorthandBytes(ini_get('memory_limit')) < $minimum) { + ini_set('memory_limit', $minimum); + } + } + + /** + * Raise the PHP max_execution_time + * + * Unless it is not already configured to a higher value. + * + * @param int $minimum + */ + public static function raiseExecutionTime($minimum = 300) + { + if ((int) ini_get('max_execution_time') < $minimum) { + ini_set('max_execution_time', $minimum); + } + } +} diff --git a/library/Icinga/Util/File.php b/library/Icinga/Util/File.php new file mode 100644 index 0000000..dad332a --- /dev/null +++ b/library/Icinga/Util/File.php @@ -0,0 +1,195 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Util; + +use SplFileObject; +use ErrorException; +use RuntimeException; +use Icinga\Exception\NotWritableError; + +/** + * File + * + * A class to ease opening files and reading/writing to them. + */ +class File extends SplFileObject +{ + /** + * The mode used to open the file + * + * @var string + */ + protected $openMode; + + /** + * The access mode to use when creating directories + * + * @var int + */ + public static $dirMode = 1528; // 2770 + + /** + * @see SplFileObject::__construct() + */ + public function __construct($filename, $openMode = 'r', $useIncludePath = false, $context = null) + { + $this->openMode = $openMode; + if ($context === null) { + parent::__construct($filename, $openMode, $useIncludePath); + } else { + parent::__construct($filename, $openMode, $useIncludePath, $context); + } + } + + /** + * Create a file using the given access mode and return a instance of File open for writing + * + * @param string $path The path to the file + * @param int $accessMode The access mode to set + * @param bool $recursive Whether missing nested directories of the given path should be created + * + * @return File + * + * @throws RuntimeException In case the file cannot be created or the access mode cannot be set + * @throws NotWritableError In case the path's (existing) parent is not writable + */ + public static function create($path, $accessMode, $recursive = true) + { + $dirPath = dirname($path); + if ($recursive && !is_dir($dirPath)) { + static::createDirectories($dirPath); + } elseif (! is_writable($dirPath)) { + throw new NotWritableError(sprintf('Path "%s" is not writable', $dirPath)); + } + + $file = new static($path, 'x+'); + + if (! @chmod($path, $accessMode)) { + $error = error_get_last(); + throw new RuntimeException(sprintf( + 'Cannot set access mode "%s" on file "%s" (%s)', + decoct($accessMode), + $path, + $error['message'] + )); + } + + return $file; + } + + /** + * Create missing directories + * + * @param string $path + * + * @throws RuntimeException In case a directory cannot be created or the access mode cannot be set + */ + protected static function createDirectories($path) + { + $part = strpos($path, DIRECTORY_SEPARATOR) === 0 ? DIRECTORY_SEPARATOR : ''; + foreach (explode(DIRECTORY_SEPARATOR, ltrim($path, DIRECTORY_SEPARATOR)) as $dir) { + $part .= $dir . DIRECTORY_SEPARATOR; + + if (! is_dir($part)) { + if (! @mkdir($part, static::$dirMode)) { + $error = error_get_last(); + throw new RuntimeException(sprintf( + 'Failed to create missing directory "%s" (%s)', + $part, + $error['message'] + )); + } + + if (! @chmod($part, static::$dirMode)) { + $error = error_get_last(); + throw new RuntimeException(sprintf( + 'Failed to set access mode "%s" for directory "%s" (%s)', + decoct(static::$dirMode), + $part, + $error['message'] + )); + } + } + } + } + + #[\ReturnTypeWillChange] + public function fwrite($str, $length = null) + { + $this->assertOpenForWriting(); + $this->setupErrorHandler(); + $retVal = $length === null ? parent::fwrite($str) : parent::fwrite($str, $length); + restore_error_handler(); + return $retVal; + } + + public function ftruncate($size): bool + { + $this->assertOpenForWriting(); + $this->setupErrorHandler(); + $retVal = parent::ftruncate($size); + restore_error_handler(); + return $retVal; + } + + #[\ReturnTypeWillChange] + public function ftell() + { + $this->setupErrorHandler(); + $retVal = parent::ftell(); + restore_error_handler(); + return $retVal; + } + + public function flock($operation, &$wouldblock = null): bool + { + $this->setupErrorHandler(); + $retVal = parent::flock($operation, $wouldblock); + restore_error_handler(); + return $retVal; + } + + #[\ReturnTypeWillChange] + public function fgetc() + { + $this->setupErrorHandler(); + $retVal = parent::fgetc(); + restore_error_handler(); + return $retVal; + } + + public function fflush(): bool + { + $this->setupErrorHandler(); + $retVal = parent::fflush(); + restore_error_handler(); + return $retVal; + } + + /** + * Setup an error handler that throws a RuntimeException for every emitted E_WARNING + */ + protected function setupErrorHandler() + { + set_error_handler( + function ($errno, $errstr, $errfile, $errline) { + restore_error_handler(); + throw new ErrorException($errstr, 0, $errno, $errfile, $errline); + }, + E_WARNING + ); + } + + /** + * Assert that the file was opened for writing and throw an exception otherwise + * + * @throws NotWritableError In case the file was not opened for writing + */ + protected function assertOpenForWriting() + { + if (!preg_match('@w|a|\+@', $this->openMode)) { + throw new NotWritableError('File not open for writing'); + } + } +} diff --git a/library/Icinga/Util/Format.php b/library/Icinga/Util/Format.php new file mode 100644 index 0000000..1158208 --- /dev/null +++ b/library/Icinga/Util/Format.php @@ -0,0 +1,197 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Util; + +use DateTime; + +class Format +{ + const STANDARD_IEC = 0; + const STANDARD_SI = 1; + protected static $instance; + + protected static $bitPrefix = array( + array('bit', 'Kibit', 'Mibit', 'Gibit', 'Tibit', 'Pibit', 'Eibit', 'Zibit', 'Yibit'), + array('bit', 'kbit', 'Mbit', 'Gbit', 'Tbit', 'Pbit', 'Ebit', 'Zbit', 'Ybit'), + ); + protected static $bitBase = array(1024, 1000); + + protected static $bytePrefix = array( + array('B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'), + array('B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'), + ); + protected static $byteBase = array(1024, 1000); + + protected static $secondPrefix = array('s', 'ms', 'µs', 'ns', 'ps', 'fs', 'as'); + protected static $secondBase = 1000; + + public static function getInstance() + { + if (self::$instance === null) { + self::$instance = new Format; + } + return self::$instance; + } + + public static function bits($value, $standard = self::STANDARD_SI) + { + return self::formatForUnits( + $value, + self::$bitPrefix[$standard], + self::$bitBase[$standard] + ); + } + + public static function bytes($value, $standard = self::STANDARD_IEC) + { + return self::formatForUnits( + $value, + self::$bytePrefix[$standard], + self::$byteBase[$standard] + ); + } + + public static function seconds($value) + { + if ($value === null) { + return ''; + } + + $absValue = abs($value); + + if ($absValue < 60) { + return self::formatForUnits($value, self::$secondPrefix, self::$secondBase); + } elseif ($absValue < 3600) { + return sprintf('%0.2f m', $value / 60); + } elseif ($absValue < 86400) { + return sprintf('%0.2f h', $value / 3600); + } + + // TODO: Do we need weeks, months and years? + return sprintf('%0.2f d', $value / 86400); + } + + protected static function formatForUnits($value, &$units, $base) + { + if ($value === null) { + return ''; + } + + $sign = ''; + if ($value < 0) { + $value = abs($value); + $sign = '-'; + } + + if ($value == 0) { + $pow = $result = 0; + } else { + $pow = floor(log($value, $base)); + $result = $value / pow($base, $pow); + } + + // 1034.23 looks better than 1.03, but 2.03 is fine: + if ($pow > 0 && $result < 2) { + $result = $value / pow($base, --$pow); + } + + return sprintf( + '%s%0.2f %s', + $sign, + $result, + $units[abs($pow)] + ); + } + + /** + * Return the amount of seconds based on the given month + * + * @param DateTime|int $dateTimeOrTimestamp The date and time to use + * + * @return int + */ + public static function secondsByMonth($dateTimeOrTimestamp) + { + if ($dateTimeOrTimestamp === null) { + return 0; + } + + if (!($dt = $dateTimeOrTimestamp) instanceof DateTime) { + $dt = new DateTime(); + $dt->setTimestamp($dateTimeOrTimestamp); + } + + return (int) $dt->format('t') * 24 * 3600; + } + + /** + * Return the amount of seconds based on the given year + * + * @param DateTime|int $dateTimeOrTimestamp The date and time to use + * + * @return int + */ + public static function secondsByYear($dateTimeOrTimestamp) + { + if ($dateTimeOrTimestamp === null) { + return 0; + } + + return (self::isLeapYear($dateTimeOrTimestamp) ? 366 : 365) * 24 * 3600; + } + + /** + * Return whether the given year is a leap year + * + * @param DateTime|int $dateTimeOrTimestamp The date and time to use + * + * @return bool + */ + public static function isLeapYear($dateTimeOrTimestamp) + { + if ($dateTimeOrTimestamp === null) { + return false; + } + + if (!($dt = $dateTimeOrTimestamp) instanceof DateTime) { + $dt = new DateTime(); + $dt->setTimestamp($dateTimeOrTimestamp); + } + + return $dt->format('L') == 1; + } + + /** + * Unpack shorthand bytes PHP directives to bytes + * + * @param string $subject + * + * @return int + */ + public static function unpackShorthandBytes($subject) + { + $base = (int) $subject; + + if ($base <= -1) { + return INF; + } + + switch (strtoupper($subject[strlen($subject) - 1])) { + case 'K': + $multiplier = 1024; + break; + case 'M': + $multiplier = 1024 ** 2; + break; + case 'G': + $multiplier = 1024 ** 3; + break; + default: + $multiplier = 1; + break; + } + + return $base * $multiplier; + } +} diff --git a/library/Icinga/Util/GlobFilter.php b/library/Icinga/Util/GlobFilter.php new file mode 100644 index 0000000..ac0493a --- /dev/null +++ b/library/Icinga/Util/GlobFilter.php @@ -0,0 +1,182 @@ +<?php +/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Util; + +use stdClass; + +/** + * GLOB-like filter for simple data structures + * + * e.g. this filters: + * + * foo.bar.baz + * foo.b*r.baz + * **.baz + * + * match this one: + * + * array( + * 'foo' => array( + * 'bar' => array( + * 'baz' => 'deadbeef' // <--- + * ) + * ) + * ) + */ +class GlobFilter +{ + /** + * The prepared filters + * + * @var array + */ + protected $filters; + + /** + * Create a new filter from a comma-separated list of GLOB-like filters or an array of such lists. + * + * @param string|\Traversable|iterable $filters + */ + public function __construct($filters) + { + $patterns = array(array('')); + $lastIndex1 = $lastIndex2 = 0; + + foreach ((is_string($filters) ? array($filters) : $filters) as $rawPatterns) { + $escape = false; + + foreach (str_split($rawPatterns) as $c) { + if ($escape) { + $escape = false; + $patterns[$lastIndex1][$lastIndex2] .= preg_quote($c, '/'); + } else { + switch ($c) { + case '\\': + $escape = true; + break; + case ',': + $patterns[] = array(''); + ++$lastIndex1; + $lastIndex2 = 0; + break; + case '.': + $patterns[$lastIndex1][] = ''; + ++$lastIndex2; + break; + case '*': + $patterns[$lastIndex1][$lastIndex2] .= '.*'; + break; + default: + $patterns[$lastIndex1][$lastIndex2] .= preg_quote($c, '/'); + } + } + } + + if ($escape) { + $patterns[$lastIndex1][$lastIndex2] .= '\\\\'; + } + } + + $this->filters = array(); + + foreach ($patterns as $pattern) { + foreach ($pattern as $i => $subPattern) { + if ($subPattern === '') { + unset($pattern[$i]); + } elseif ($subPattern === '.*.*') { + $pattern[$i] = '**'; + } elseif ($subPattern === '.*') { + $pattern[$i] = '/^' . $subPattern . '$/'; + } else { + $pattern[$i] = '/^' . trim($subPattern) . '$/i'; + } + } + + if (! empty($pattern)) { + $found = false; + foreach ($pattern as $i => $v) { + if ($found) { + if ($v === '**') { + unset($pattern[$i]); + } else { + $found = false; + } + } elseif ($v === '**') { + $found = true; + } + } + + if (end($pattern) === '**') { + $pattern[] = '/^.*$/'; + } + + $this->filters[] = array_values($pattern); + } + } + } + + /** + * Remove all keys/attributes matching any of $this->filters from $dataStructure + * + * @param stdClass|array $dataStructure + * + * @return stdClass|array The modified copy of $dataStructure + */ + public function removeMatching($dataStructure) + { + foreach ($this->filters as $filter) { + $dataStructure = static::removeMatchingRecursive($dataStructure, $filter); + } + return $dataStructure; + } + + /** + * Helper method for removeMatching() + * + * @param stdClass|array $dataStructure + * @param array $filter + * + * @return stdClass|array + */ + protected static function removeMatchingRecursive($dataStructure, $filter) + { + $multiLevelPattern = $filter[0] === '**'; + if ($multiLevelPattern) { + $dataStructure = static::removeMatchingRecursive($dataStructure, array_slice($filter, 1)); + } + + $isObject = $dataStructure instanceof stdClass; + if ($isObject || is_array($dataStructure)) { + if ($isObject) { + $dataStructure = (array) $dataStructure; + } + + if ($multiLevelPattern) { + foreach ($dataStructure as $k => & $v) { + $v = static::removeMatchingRecursive($v, $filter); + unset($v); + } + } else { + $currentLevel = $filter[0]; + $nextLevels = count($filter) === 1 ? null : array_slice($filter, 1); + foreach ($dataStructure as $k => & $v) { + if (preg_match($currentLevel, (string) $k)) { + if ($nextLevels === null) { + unset($dataStructure[$k]); + } else { + $v = static::removeMatchingRecursive($v, $nextLevels); + } + } + unset($v); + } + } + + if ($isObject) { + $dataStructure = (object) $dataStructure; + } + } + + return $dataStructure; + } +} diff --git a/library/Icinga/Util/Json.php b/library/Icinga/Util/Json.php new file mode 100644 index 0000000..0b89dcc --- /dev/null +++ b/library/Icinga/Util/Json.php @@ -0,0 +1,151 @@ +<?php +/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Util; + +use Icinga\Exception\Json\JsonDecodeException; +use Icinga\Exception\Json\JsonEncodeException; + +/** + * Wrap {@link json_encode()} and {@link json_decode()} with error handling + */ +class Json +{ + /** + * {@link json_encode()} wrapper + * + * @param mixed $value + * @param int $options + * @param int $depth + * + * @return string + * @throws JsonEncodeException + */ + public static function encode($value, $options = 0, $depth = 512) + { + return static::encodeAndSanitize($value, $options, $depth, false); + } + + /** + * {@link json_encode()} wrapper, automatically sanitizes bad UTF-8 + * + * @param mixed $value + * @param int $options + * @param int $depth + * + * @return string + * @throws JsonEncodeException + */ + public static function sanitize($value, $options = 0, $depth = 512) + { + return static::encodeAndSanitize($value, $options, $depth, true); + } + + /** + * {@link json_encode()} wrapper, sanitizes bad UTF-8 + * + * @param mixed $value + * @param int $options + * @param int $depth + * @param bool $autoSanitize Automatically sanitize invalid UTF-8 (if any) + * + * @return string + * @throws JsonEncodeException + */ + protected static function encodeAndSanitize($value, $options, $depth, $autoSanitize) + { + $encoded = json_encode($value, $options, $depth); + + switch (json_last_error()) { + case JSON_ERROR_NONE: + return $encoded; + + /** @noinspection PhpMissingBreakStatementInspection */ + case JSON_ERROR_UTF8: + if ($autoSanitize) { + return static::encode(static::sanitizeUtf8Recursive($value), $options, $depth); + } + // Fallthrough + + default: + throw new JsonEncodeException('%s: %s', json_last_error_msg(), var_export($value, true)); + } + } + + /** + * {@link json_decode()} wrapper + * + * @param string $json + * @param bool $assoc + * @param int $depth + * @param int $options + * + * @return mixed + * @throws JsonDecodeException + */ + public static function decode($json, $assoc = false, $depth = 512, $options = 0) + { + $decoded = $json ? json_decode($json, $assoc, $depth, $options) : null; + + if (json_last_error() !== JSON_ERROR_NONE) { + throw new JsonDecodeException('%s: %s', json_last_error_msg(), var_export($json, true)); + } + return $decoded; + } + + /** + * Replace bad byte sequences in UTF-8 strings inside the given JSON-encodable structure with question marks + * + * @param mixed $value + * + * @return mixed + */ + protected static function sanitizeUtf8Recursive($value) + { + switch (gettype($value)) { + case 'string': + return static::sanitizeUtf8String($value); + + case 'array': + $sanitized = array(); + + foreach ($value as $key => $val) { + if (is_string($key)) { + $key = static::sanitizeUtf8String($key); + } + + $sanitized[$key] = static::sanitizeUtf8Recursive($val); + } + + return $sanitized; + + case 'object': + $sanitized = array(); + + foreach ($value as $key => $val) { + if (is_string($key)) { + $key = static::sanitizeUtf8String($key); + } + + $sanitized[$key] = static::sanitizeUtf8Recursive($val); + } + + return (object) $sanitized; + + default: + return $value; + } + } + + /** + * Replace bad byte sequences in the given UTF-8 string with question marks + * + * @param string $string + * + * @return string + */ + protected static function sanitizeUtf8String($string) + { + return mb_convert_encoding($string, 'UTF-8', 'UTF-8'); + } +} diff --git a/library/Icinga/Util/LessParser.php b/library/Icinga/Util/LessParser.php new file mode 100644 index 0000000..1e07aa9 --- /dev/null +++ b/library/Icinga/Util/LessParser.php @@ -0,0 +1,15 @@ +<?php +/* Icinga Web 2 | (c) 2021 Icinga GmbH | GPLv2+ */ + +namespace Icinga\Util; + +use Icinga\Less\Visitor; +use lessc; + +class LessParser extends lessc +{ + public function __construct() + { + $this->setOption('plugins', [new Visitor()]); + } +} diff --git a/library/Icinga/Util/StringHelper.php b/library/Icinga/Util/StringHelper.php new file mode 100644 index 0000000..67a836b --- /dev/null +++ b/library/Icinga/Util/StringHelper.php @@ -0,0 +1,184 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Util; + +/** + * Common string functions + */ +class StringHelper +{ + /** + * Split string into an array and trim spaces + * + * @param string $value + * @param string $delimiter + * @param int $limit + * + * @return array + */ + public static function trimSplit($value, $delimiter = ',', $limit = null) + { + if ($value === null) { + return []; + } + + if ($limit !== null) { + $exploded = explode($delimiter, $value, $limit); + } else { + $exploded = explode($delimiter, $value); + } + + return array_map('trim', $exploded); + } + + /** + * Uppercase the first character of each word in a string + * + * Converts 'first_name' to 'FirstName' for example. + * + * @param string $name + * @param string $separator Word separator + * + * @return string + */ + public static function cname($name, $separator = '_') + { + if ($name === null) { + return ''; + } + + return str_replace(' ', '', ucwords(str_replace($separator, ' ', strtolower($name)))); + } + + /** + * Add ellipsis when a string is longer than max length + * + * @param string $string + * @param int $maxLength + * @param string $ellipsis + * + * @return string + */ + public static function ellipsis($string, $maxLength, $ellipsis = '...') + { + if ($string === null) { + return ''; + } + + if (strlen($string) > $maxLength) { + return substr($string, 0, $maxLength - strlen($ellipsis)) . $ellipsis; + } + + return $string; + } + + /** + * Add ellipsis in the center of a string when a string is longer than max length + * + * @param string $string + * @param int $maxLength + * @param string $ellipsis + * + * @return string + */ + public static function ellipsisCenter($string, $maxLength, $ellipsis = '...') + { + if ($string === null) { + return ''; + } + + $start = ceil($maxLength / 2.0); + $end = floor($maxLength / 2.0); + if (strlen($string) > $maxLength) { + return substr($string, 0, $start - strlen($ellipsis)) . $ellipsis . substr($string, - $end); + } + + return $string; + } + + /** + * Find and return all similar strings in $possibilites matching $string with the given minimum $similarity + * + * @param string $string + * @param array $possibilities + * @param float $similarity + * + * @return array + */ + public static function findSimilar($string, array $possibilities, $similarity = 0.33) + { + if (empty($string)) { + return array(); + } + + $matches = array(); + foreach ($possibilities as $possibility) { + $distance = levenshtein($string, $possibility); + if ($distance / strlen($string) <= $similarity) { + $matches[] = $possibility; + } + } + + return $matches; + } + + /** + * Test whether the given string ends with the given suffix + * + * @param string $string The string to test + * @param string $suffix The suffix the string must end with + * + * @return bool + */ + public static function endsWith($string, $suffix) + { + if ($string === null) { + return false; + } + + $stringSuffix = substr($string, -strlen($suffix)); + return $stringSuffix !== false ? $stringSuffix === $suffix : false; + } + + /** + * Generates an array of strings that constitutes the cartesian product of all passed sets, with all + * string combinations concatenated using the passed join-operator. + * + * <pre> + * cartesianProduct( + * array(array('foo', 'bar'), array('mumble', 'grumble', null)), + * '_' + * ); + * => array('foo_mumble', 'foo_grumble', 'bar_mumble', 'bar_grumble', 'foo', 'bar') + * </pre> + * + * @param array $sets An array of arrays containing all sets for which the cartesian + * product should be calculated. + * @param string $glue The glue used to join the strings, defaults to ''. + * + * @returns array The cartesian product in one array of strings. + */ + public static function cartesianProduct(array $sets, $glue = '') + { + $product = null; + foreach ($sets as $set) { + if (! isset($product)) { + $product = $set; + } else { + $newProduct = array(); + foreach ($product as $strA) { + foreach ($set as $strB) { + if ($strB === null) { + $newProduct []= $strA; + } else { + $newProduct []= $strA . $glue . $strB; + } + } + } + $product = $newProduct; + } + } + return $product; + } +} diff --git a/library/Icinga/Util/TimezoneDetect.php b/library/Icinga/Util/TimezoneDetect.php new file mode 100644 index 0000000..4967c7f --- /dev/null +++ b/library/Icinga/Util/TimezoneDetect.php @@ -0,0 +1,107 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Util; + +/** + * Retrieve timezone information from cookie + */ +class TimezoneDetect +{ + /** + * If detection was successful + * + * @var bool + */ + private static $success; + + /** + * Timezone offset in minutes + * + * @var int + */ + private static $offset = 0; + + /** + * @var string + */ + private static $timezoneName; + + /** + * Cookie name + * + * @var string + */ + public static $cookieName = 'icingaweb2-tzo'; + + /** + * Timezone name + * + * @var string + */ + private static $timezone; + + /** + * Create new object and try to identify the timezone + */ + public function __construct() + { + if (self::$success !== null) { + return; + } + + if (array_key_exists(self::$cookieName, $_COOKIE)) { + $matches = array(); + if (preg_match('/\A(-?\d+)[\-,](\d+)\z/', $_COOKIE[self::$cookieName], $matches)) { + $offset = $matches[1]; + $timezoneName = timezone_name_from_abbr('', (int) $offset, (int) $matches[2]); + + self::$success = (bool) $timezoneName; + if (self::$success) { + self::$offset = $offset; + self::$timezoneName = $timezoneName; + } + } + } + } + + /** + * Get offset + * + * @return int + */ + public function getOffset() + { + return self::$offset; + } + + /** + * Get timezone name + * + * @return string + */ + public function getTimezoneName() + { + return self::$timezoneName; + } + + /** + * True on success + * + * @return bool + */ + public function success() + { + return self::$success; + } + + /** + * Reset object + */ + public function reset() + { + self::$success = null; + self::$timezoneName = null; + self::$offset = 0; + } +} |