summaryrefslogtreecommitdiffstats
path: root/library/Icinga/Util
diff options
context:
space:
mode:
Diffstat (limited to 'library/Icinga/Util')
-rw-r--r--library/Icinga/Util/ASN1.php102
-rw-r--r--library/Icinga/Util/Color.php121
-rw-r--r--library/Icinga/Util/ConfigAwareFactory.php18
-rw-r--r--library/Icinga/Util/Csp.php107
-rw-r--r--library/Icinga/Util/Dimension.php123
-rw-r--r--library/Icinga/Util/DirectoryIterator.php214
-rw-r--r--library/Icinga/Util/EnumeratingFilterIterator.php30
-rw-r--r--library/Icinga/Util/Environment.php42
-rw-r--r--library/Icinga/Util/File.php195
-rw-r--r--library/Icinga/Util/Format.php197
-rw-r--r--library/Icinga/Util/GlobFilter.php182
-rw-r--r--library/Icinga/Util/Json.php151
-rw-r--r--library/Icinga/Util/LessParser.php15
-rw-r--r--library/Icinga/Util/StringHelper.php184
-rw-r--r--library/Icinga/Util/TimezoneDetect.php107
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;
+ }
+}