icingaweb2-module-director/library/Director/IcingaConfig/IcingaConfigHelper.php

431 lines
11 KiB
PHP

<?php
namespace Icinga\Module\Director\IcingaConfig;
use InvalidArgumentException;
use function ctype_digit;
use function explode;
use function floor;
use function implode;
use function preg_match;
use function preg_split;
use function sprintf;
use function strlen;
use function strpos;
use function substr;
class IcingaConfigHelper
{
/**
* Reserved words according to
* https://icinga.com/docs/icinga2/latest/doc/17-language-reference/#reserved-keywords
*/
protected static $reservedWords = [
'object',
'template',
'include',
'include_recursive',
'include_zones',
'library',
'null',
'true',
'false',
'const',
'var',
'this',
'globals',
'locals',
'use',
'default',
'ignore_on_error',
'current_filename',
'current_line',
'apply',
'to',
'where',
'import',
'assign',
'ignore',
'function',
'return',
'break',
'continue',
'for',
'if',
'else',
'while',
'throw',
'try',
'except',
'in',
'using',
'namespace',
];
public static function renderKeyValue($key, $value, $prefix = ' ')
{
return self::renderKeyOperatorValue($key, '=', $value, $prefix);
}
public static function renderKeyOperatorValue($key, $operator, $value, $prefix = ' ')
{
$string = sprintf(
"%s %s %s",
$key,
$operator,
$value
);
if ($prefix && strpos($string, "\n") !== false) {
return $prefix . implode("\n" . $prefix, explode("\n", $string)) . "\n";
}
return $prefix . $string . "\n";
}
public static function renderBoolean($value)
{
if ($value === 'y' || $value === true) {
return 'true';
}
if ($value === 'n' || $value === false) {
return 'false';
}
throw new InvalidArgumentException(sprintf(
'%s is not a valid boolean',
$value
));
}
protected static function renderInteger($value)
{
return (string) $value;
}
public static function renderFloat($value)
{
// Render .0000 floats as integers, mainly because of some JSON
// implementations:
if ((string) (int) $value === (string) $value) {
return static::renderInteger((int) $value);
}
return sprintf('%F', $value);
}
protected static function renderNull()
{
return 'null';
}
// TODO: Find out how to allow multiline {{{...}}} strings.
// Parameter? Dedicated method? Always if \n is found?
public static function renderString($string)
{
$special = [
'/\\\/',
'/"/',
'/\$/',
'/\t/',
'/\r/',
'/\n/',
// '/\b/', -> doesn't work
'/\f/',
];
$replace = [
'\\\\\\',
'\\"',
'\\$',
'\\t',
'\\r',
'\\n',
// '\\b',
'\\f',
];
$string = preg_replace($special, $replace, $string);
return '"' . $string . '"';
}
public static function renderPhpValue($value)
{
if (is_null($value)) {
return static::renderNull();
}
if (is_bool($value)) {
return static::renderBoolean($value);
}
if (is_int($value)) {
return static::renderInteger($value);
}
if (is_float($value)) {
return static::renderFloat($value);
}
// TODO:
// if (is_object($value) || static::isAssocArray($value)) {
// return static::renderHash($value, $prefix)
// TODO: also check array
if (is_array($value)) {
return static::renderArray($value);
}
if (is_string($value)) {
return static::renderString($value);
}
throw new InvalidArgumentException(sprintf(
'Unexpected type %s',
var_export($value, 1)
));
}
public static function renderDictionaryKey($key)
{
if (preg_match('/^[a-z_]+[a-z0-9_]*$/i', $key)) {
return static::escapeIfReserved($key);
}
return static::renderString($key);
}
// Requires an array
public static function renderArray($array)
{
$data = [];
foreach ($array as $entry) {
if ($entry instanceof IcingaConfigRenderer) {
$data[] = $entry;
} else {
$data[] = self::renderString($entry);
}
}
return static::renderEscapedArray($data);
}
public static function renderEscapedArray($array)
{
$str = '[ ' . implode(', ', $array) . ' ]';
if (strlen($str) < 60) {
return $str;
}
// Prefix for toConfigString?
return "[\n " . implode(",\n ", $array) . "\n]";
}
public static function renderDictionary($dictionary)
{
$values = [];
foreach ($dictionary as $key => $value) {
$values[$key] = rtrim(
self::renderKeyValue(
self::renderDictionaryKey($key),
$value
)
);
}
if (empty($values)) {
return '{}';
}
ksort($values, SORT_STRING);
// Prefix for toConfigString?
return "{\n" . implode("\n", $values) . "\n}";
}
public static function renderExpression($string)
{
return "{{\n " . $string . "\n}}";
}
public static function alreadyRendered($string)
{
return new IcingaConfigRendered($string);
}
public static function isReserved($string)
{
return in_array($string, self::$reservedWords, true);
}
public static function escapeIfReserved($string)
{
if (self::isReserved($string)) {
return '@' . $string;
}
return $string;
}
public static function isValidInterval($interval)
{
if (ctype_digit($interval)) {
return true;
}
$parts = preg_split('/\s+/', $interval, -1, PREG_SPLIT_NO_EMPTY);
foreach ($parts as $part) {
if (! preg_match('/^(\d+)([dhms]?)$/', $part)) {
return false;
}
}
return true;
}
public static function parseInterval($interval)
{
if ($interval === null || $interval === '') {
return null;
}
if (is_int($interval) || ctype_digit($interval)) {
return (int) $interval;
}
$parts = preg_split('/\s+/', $interval, -1, PREG_SPLIT_NO_EMPTY);
$value = 0;
foreach ($parts as $part) {
if (! preg_match('/^(\d+)([dhms]?)$/', $part, $m)) {
throw new InvalidArgumentException(sprintf(
'"%s" is not a valid time (duration) definition',
$interval
));
}
switch ($m[2]) {
case 'd':
$value += $m[1] * 86400;
break;
case 'h':
$value += $m[1] * 3600;
break;
case 'm':
$value += $m[1] * 60;
break;
default:
$value += (int) $m[1];
}
}
return $value;
}
public static function renderInterval($interval)
{
// TODO: compat only, do this at munge time. All db fields should be int
$seconds = self::parseInterval($interval);
if ($seconds === 0) {
return '0s';
}
$steps = [
'd' => 86400,
'h' => 3600,
'm' => 60,
];
foreach ($steps as $unit => $duration) {
if ($seconds % $duration === 0) {
return (int) floor($seconds / $duration) . $unit;
}
}
return $seconds . 's';
}
public static function stringHasMacro($string, $macroName = null)
{
$len = strlen($string);
$start = false;
// TODO: robust UTF8 support. It works, but it is not 100% correct
for ($i = 0; $i < $len; $i++) {
if ($string[$i] === '$') {
if ($start === false) {
$start = $i;
} else {
// Escaping, $$
if ($start + 1 === $i) {
$start = false;
} else {
if ($macroName === null) {
return true;
}
if ($macroName === substr($string, $start + 1, $i - $start - 1)) {
return true;
}
$start = false;
}
}
}
}
return false;
}
/**
* Hint: this isn't complete, but let's restrict ourselves right now
*
* @param $name
* @return bool
*/
public static function isValidMacroName($name)
{
return preg_match('/^[A-z_][A-z_.\d]+$/', $name)
&& ! preg_match('/\.$/', $name);
}
public static function renderStringWithVariables($string, array $whiteList = null)
{
$len = strlen($string);
$start = false;
$parts = [];
// TODO: UTF8...
$offset = 0;
for ($i = 0; $i < $len; $i++) {
if ($string[$i] === '$') {
if ($start === false) {
$start = $i;
} else {
// Ignore $$
if ($start + 1 === $i) {
$start = false;
} else {
// We got a macro
$macroName = substr($string, $start + 1, $i - $start - 1);
if (static::isValidMacroName($macroName)) {
if ($whiteList === null || in_array($macroName, $whiteList)) {
if ($start > $offset) {
$parts[] = static::renderString(
substr($string, $offset, $start - $offset)
);
}
$parts[] = $macroName;
$offset = $i + 1;
}
}
$start = false;
}
}
}
}
if ($offset < $i) {
$parts[] = static::renderString(substr($string, $offset, $i - $offset));
}
if (! empty($parts)) {
return implode(' + ', $parts);
}
return '""';
}
}