You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
426 lines
11 KiB
426 lines
11 KiB
<?php declare(strict_types = 0);
|
|
/*
|
|
** Zabbix
|
|
** Copyright (C) 2001-2023 Zabbix SIA
|
|
**
|
|
** This program is free software; you can redistribute it and/or modify
|
|
** it under the terms of the GNU General Public License as published by
|
|
** the Free Software Foundation; either version 2 of the License, or
|
|
** (at your option) any later version.
|
|
**
|
|
** This program is distributed in the hope that it will be useful,
|
|
** but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
** GNU General Public License for more details.
|
|
**
|
|
** You should have received a copy of the GNU General Public License
|
|
** along with this program; if not, write to the Free Software
|
|
** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
**/
|
|
|
|
|
|
/**
|
|
* Class for validating history functions.
|
|
*/
|
|
class CHistFunctionValidator extends CValidator {
|
|
|
|
/**
|
|
* An options array.
|
|
*
|
|
* Supported options:
|
|
* 'parameters' => [] Definition of parameters of known history functions.
|
|
* 'usermacros' => false Enable user macros usage in function parameters.
|
|
* 'lldmacros' => false Enable low-level discovery macros usage in function parameters.
|
|
* 'calculated' => false Validate history function as part of calculated item formula.
|
|
* 'aggregating' => false Validate as aggregating history function.
|
|
*
|
|
* @var array
|
|
*/
|
|
private $options = [
|
|
'parameters' => [],
|
|
'usermacros' => false,
|
|
'lldmacros' => false,
|
|
'calculated' => false,
|
|
'aggregating' => false
|
|
];
|
|
|
|
/**
|
|
* @param array $options
|
|
*/
|
|
public function __construct(array $options = []) {
|
|
$this->options = $options + $this->options;
|
|
}
|
|
|
|
/**
|
|
* Validate history function.
|
|
*
|
|
* @param array $token A token of CExpressionParserResult::TOKEN_TYPE_HIST_FUNCTION type.
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function validate($token) {
|
|
$invalid_param_messages = [
|
|
_('invalid first parameter in function "%1$s"'),
|
|
_('invalid second parameter in function "%1$s"'),
|
|
_('invalid third parameter in function "%1$s"'),
|
|
_('invalid fourth parameter in function "%1$s"'),
|
|
_('invalid fifth parameter in function "%1$s"'),
|
|
_('invalid sixth parameter in function "%1$s"'),
|
|
_('invalid seventh parameter in function "%1$s"')
|
|
];
|
|
|
|
if (!array_key_exists($token['data']['function'], $this->options['parameters'])) {
|
|
$this->setError(_s('unknown function "%1$s"', $token['data']['function']));
|
|
|
|
return false;
|
|
}
|
|
|
|
$params = $token['data']['parameters'];
|
|
$params_spec = $this->options['parameters'][$token['data']['function']];
|
|
|
|
if (count($params) > count($params_spec)) {
|
|
$this->setError(_s('invalid number of parameters in function "%1$s"', $token['data']['function']));
|
|
|
|
return false;
|
|
}
|
|
|
|
foreach ($params_spec as $index => $param_spec) {
|
|
$required = !array_key_exists('required', $param_spec) || $param_spec['required'];
|
|
|
|
if ($index >= count($params)) {
|
|
if ($required) {
|
|
$this->setError(
|
|
_s('mandatory parameter is missing in function "%1$s"', $token['data']['function'])
|
|
);
|
|
|
|
return false;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
$param = $params[$index];
|
|
|
|
if ($param['match'] === '') {
|
|
if ($required) {
|
|
$this->setError(_params($invalid_param_messages[$index], [$token['data']['function']]));
|
|
|
|
return false;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
switch ($param['type']) {
|
|
case CHistFunctionParser::PARAM_TYPE_PERIOD:
|
|
if (self::hasMacros($param['data']['sec_num'], $this->options)
|
|
&& $param['data']['time_shift'] === '') {
|
|
continue 2;
|
|
}
|
|
break;
|
|
|
|
case CHistFunctionParser::PARAM_TYPE_QUOTED:
|
|
if (self::hasMacros(CHistFunctionParser::unquoteParam($param['match']), $this->options)) {
|
|
continue 2;
|
|
}
|
|
break;
|
|
|
|
case CHistFunctionParser::PARAM_TYPE_UNQUOTED:
|
|
if (self::hasMacros($param['match'], $this->options)) {
|
|
continue 2;
|
|
}
|
|
break;
|
|
}
|
|
|
|
if (array_key_exists('rules', $param_spec)) {
|
|
$is_valid = self::validateRules($param, $param_spec['rules'], $this->options);
|
|
|
|
if (!$is_valid) {
|
|
$this->setError(_params($invalid_param_messages[$index], [$token['data']['function']]));
|
|
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Loose check if string value contains macros.
|
|
*
|
|
* @param string $value
|
|
* @param array $options
|
|
*
|
|
* @static
|
|
*
|
|
* @return bool
|
|
*/
|
|
private static function hasMacros(string $value, array $options): bool {
|
|
if (!$options['usermacros'] && !$options['lldmacros']) {
|
|
return false;
|
|
}
|
|
|
|
$macro_parsers = [];
|
|
|
|
if ($options['usermacros']) {
|
|
$macro_parsers[] = new CUserMacroParser();
|
|
}
|
|
if ($options['lldmacros']) {
|
|
$macro_parsers[] = new CLLDMacroParser();
|
|
$macro_parsers[] = new CLLDMacroFunctionParser();
|
|
}
|
|
|
|
for ($pos = strpos($value, '{'); $pos !== false; $pos = strpos($value, '{', $pos + 1)) {
|
|
foreach ($macro_parsers as $macro_parser) {
|
|
if ($macro_parser->parse($value, $pos) != CParser::PARSE_FAIL) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Validate function parameter token's compliance to the rules.
|
|
*
|
|
* @param array $param Function parameter token.
|
|
* @param array $rules
|
|
* @param array $options
|
|
*
|
|
* @static
|
|
*
|
|
* @return bool
|
|
*/
|
|
private static function validateRules(array $param, array $rules, array $options): bool {
|
|
$param_match_unquoted = ($param['type'] == CHistFunctionParser::PARAM_TYPE_QUOTED)
|
|
? CHistFunctionParser::unquoteParam($param['match'])
|
|
: $param['match'];
|
|
|
|
foreach ($rules as $rule) {
|
|
switch ($rule['type']) {
|
|
case 'query':
|
|
if ($param['type'] != CHistFunctionParser::PARAM_TYPE_QUERY) {
|
|
return false;
|
|
}
|
|
|
|
if (!self::validateQuery($param['data']['host'], $param['data']['item'], $param['data']['filter'],
|
|
$options)) {
|
|
return false;
|
|
}
|
|
|
|
break;
|
|
|
|
case 'period':
|
|
if ($param['type'] != CHistFunctionParser::PARAM_TYPE_PERIOD) {
|
|
return false;
|
|
}
|
|
|
|
if (!self::validatePeriod($param['data']['sec_num'], $param['data']['time_shift'], $rule['mode'],
|
|
$options)) {
|
|
return false;
|
|
}
|
|
|
|
// Make sure time shift uses units no less than one used in period.
|
|
if (array_key_exists('aligned_shift', $rule) && $rule['aligned_shift']) {
|
|
if (self::hasMacros($param['data']['sec_num'], $options)
|
|
|| self::hasMacros($param['data']['time_shift'], $options)) {
|
|
return true;
|
|
}
|
|
|
|
$period_parser = new CNumberParser([
|
|
'with_time_suffix' => true,
|
|
'with_year' => true
|
|
]);
|
|
|
|
if ($period_parser->parse($param['data']['sec_num']) != CParser::PARSE_SUCCESS) {
|
|
return false;
|
|
}
|
|
|
|
$period_unit_length = timeUnitToSeconds('1'.$period_parser->getSuffix(), true);
|
|
$shift_parser = new CRelativeTimeParser();
|
|
|
|
if ($shift_parser->parse($param['data']['time_shift']) != CParser::PARSE_SUCCESS) {
|
|
return false;
|
|
}
|
|
|
|
foreach ($shift_parser->getTokens() as $token) {
|
|
if (timeUnitToSeconds('1'.$token['suffix'], true) < $period_unit_length) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
break;
|
|
|
|
case 'number':
|
|
$with_suffix = (array_key_exists('with_suffix', $rule) && $rule['with_suffix']);
|
|
$with_float = true;
|
|
|
|
if (array_key_exists('with_float', $rule) && $rule['with_float'] === false) {
|
|
$with_float = false;
|
|
}
|
|
|
|
$parser = new CNumberParser([
|
|
'with_size_suffix' => $with_suffix,
|
|
'with_time_suffix' => $with_suffix,
|
|
'with_float' => $with_float
|
|
]);
|
|
|
|
if ($parser->parse($param_match_unquoted) != CParser::PARSE_SUCCESS) {
|
|
return false;
|
|
}
|
|
|
|
$value = $parser->calcValue();
|
|
|
|
if ((array_key_exists('min', $rule) && $value < $rule['min'])
|
|
|| array_key_exists('max', $rule) && $value > $rule['max']) {
|
|
return false;
|
|
}
|
|
|
|
break;
|
|
|
|
case 'regexp':
|
|
if (preg_match($rule['pattern'], $param_match_unquoted) != 1) {
|
|
return false;
|
|
}
|
|
|
|
break;
|
|
|
|
case 'time':
|
|
$parser = new CNumberParser([
|
|
'with_float' => false,
|
|
'with_time_suffix' => true,
|
|
'with_year' => (array_key_exists('with_year', $rule) && $rule['with_year'])
|
|
]);
|
|
|
|
if ($parser->parse($param_match_unquoted) != CParser::PARSE_SUCCESS) {
|
|
return false;
|
|
}
|
|
|
|
$sec = $parser->calcValue();
|
|
$min = array_key_exists('min', $rule) ? $rule['min'] : ZBX_MIN_INT32;
|
|
$max = array_key_exists('max', $rule) ? $rule['max'] : ZBX_MAX_INT32;
|
|
|
|
if ($sec < $min || $sec > $max) {
|
|
return false;
|
|
}
|
|
|
|
break;
|
|
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Validate function's query parameter.
|
|
*
|
|
* @param string $host
|
|
* @param string $item
|
|
* @param array $filter Filter token.
|
|
* @param array $options
|
|
*
|
|
* @static
|
|
*
|
|
* @return bool
|
|
*/
|
|
private static function validateQuery(string $host, string $item, array $filter, array $options): bool {
|
|
if ($options['calculated']) {
|
|
if ($options['aggregating']) {
|
|
if ($host === CQueryParser::HOST_ITEMKEY_WILDCARD && $item === CQueryParser::HOST_ITEMKEY_WILDCARD) {
|
|
return false;
|
|
}
|
|
}
|
|
else {
|
|
if ($filter['match'] !== '') {
|
|
return false;
|
|
}
|
|
|
|
if ($host === CQueryParser::HOST_ITEMKEY_WILDCARD || $item === CQueryParser::HOST_ITEMKEY_WILDCARD) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Validate function's period parameter.
|
|
*
|
|
* @param string $sec_num
|
|
* @param string $time_shift
|
|
* @param int $mode
|
|
* @param array $options
|
|
*
|
|
* @static
|
|
*
|
|
* @return bool
|
|
*/
|
|
private static function validatePeriod(string $sec_num, string $time_shift, int $mode, array $options): bool {
|
|
switch ($mode) {
|
|
case CHistFunctionData::PERIOD_MODE_DEFAULT:
|
|
if ($sec_num === '' || self::hasMacros($sec_num, $options)) {
|
|
return true;
|
|
}
|
|
|
|
$sec = timeUnitToSeconds($sec_num);
|
|
|
|
if ($sec !== null) {
|
|
return ($sec > 0 && $sec <= ZBX_MAX_INT32);
|
|
}
|
|
|
|
if (preg_match('/^#(?<num>\d+)$/', $sec_num, $matches) == 1) {
|
|
return ($matches['num'] > 0 && $matches['num'] <= ZBX_MAX_INT32);
|
|
}
|
|
|
|
break;
|
|
|
|
case CHistFunctionData::PERIOD_MODE_SEC:
|
|
case CHistFunctionData::PERIOD_MODE_SEC_ONLY:
|
|
if ($mode == CHistFunctionData::PERIOD_MODE_SEC_ONLY && $time_shift !== '') {
|
|
return false;
|
|
}
|
|
|
|
$sec = timeUnitToSeconds($sec_num);
|
|
|
|
if ($sec !== null) {
|
|
return ($sec > 0 && $sec <= ZBX_MAX_INT32);
|
|
}
|
|
|
|
break;
|
|
|
|
case CHistFunctionData::PERIOD_MODE_NUM_ONLY:
|
|
if (preg_match('/^#(?<num>\d+)$/', $sec_num, $matches) == 1) {
|
|
return ($matches['num'] > 0 && $matches['num'] <= ZBX_MAX_INT32);
|
|
}
|
|
|
|
break;
|
|
|
|
case CHistFunctionData::PERIOD_MODE_TREND:
|
|
if ($time_shift === '') {
|
|
return false;
|
|
}
|
|
|
|
if (self::hasMacros($sec_num, $options)) {
|
|
return true;
|
|
}
|
|
|
|
$sec = timeUnitToSeconds($sec_num, true);
|
|
|
|
if ($sec !== null) {
|
|
return ($sec > 0 && $sec <= ZBX_MAX_INT32 && $sec % SEC_PER_HOUR == 0);
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|