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.

879 lines
22 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 CExpressionParser extends CParser {
// For parsing of expressions.
private const STATE_AFTER_OPEN_BRACE = 1;
private const STATE_AFTER_BINARY_OPERATOR = 2;
private const STATE_AFTER_LOGICAL_OPERATOR = 3;
private const STATE_AFTER_NOT_OPERATOR = 4;
private const STATE_AFTER_UNARY_MINUS = 5;
private const STATE_AFTER_CLOSE_BRACE = 6;
private const STATE_AFTER_CONSTANT = 7;
// For parsing of math function parameters.
private const STATE_NEW = 1;
private const STATE_END = 2;
private const STATE_END_OF_PARAMS = 3;
private const MAX_MATH_FUNCTION_DEPTH = 32;
/**
* An error message if trigger expression is not valid
*
* @var string
*/
private $error = '';
/**
* An options array.
*
* Supported options:
* 'usermacros' => false Enable user macros usage in expression.
* 'lldmacros' => false Enable low-level discovery macros usage in expression.
* 'collapsed_expression' => false Short trigger expression.
* For example: {439} > {$MAX_THRESHOLD} or {439} < {$MIN_THRESHOLD}
* 'calculated' => false Parse calculated item formula instead of trigger expression.
* 'host_macro' => false Allow {HOST.HOST} macro as host name part in the query.
* 'host_macro_n' => false Allow {HOST.HOST} and {HOST.HOST<1-9>} macros as host name part in the query.
* 'empty_host' => false Allow empty hostname in the query string.
*
* @var array
*/
private $options = [
'usermacros' => false,
'lldmacros' => false,
'collapsed_expression' => false,
'calculated' => false,
'host_macro' => false,
'host_macro_n' => false,
'empty_host' => false
];
/**
* Object containing the results of parsing.
*
* @var null|CExpressionParserResult
*/
private $result;
/**
* Chars that should be treated as spaces.
*/
public const WHITESPACES = " \r\n\t";
/**
* @param array $options
*/
public function __construct(array $options = []) {
$this->options = $options + $this->options;
if ($this->options['collapsed_expression']
&& ($this->options['host_macro'] || $this->options['host_macro_n'])) {
exit('Incompatible options.');
}
}
/**
* Parse an expression and set public variables $this->error, $this->result
*
* Examples:
* last(/Zabbix server/agent.ping,0) = 1 and {TRIGGER.VALUE} = {$MACRO}
*
* @param string $source
* @param int $pos
*
* @return int
*/
public function parse($source, $pos = 0) {
// initializing local variables
$this->error = '';
$this->match = '';
$this->length = 0;
$p = $pos;
$tokens = [];
$parsed_pos = 0;
if (self::parseExpression($source, $p, $tokens, $this->options, $parsed_pos)) {
// Including trailing whitespaces as part of the expression.
if (preg_match('/^['.self::WHITESPACES.']+$/', substr($source, $p), $matches)) {
$p += strlen($matches[0]);
}
$len = $p - $pos;
$this->length = $len;
$this->match = substr($source, $pos, $len);
$this->result = new CExpressionParserResult();
$this->result->addTokens($tokens);
$this->result->pos = $pos;
if (isset($source[$p])) {
$this->error = _s('incorrect expression starting from "%1$s"', substr($source, $parsed_pos));
return self::PARSE_SUCCESS_CONT;
}
return self::PARSE_SUCCESS;
}
$this->error = _s('incorrect expression starting from "%1$s"', substr($source, $parsed_pos));
return self::PARSE_FAIL;
}
/**
* Parses an expression.
*
* @param string $source
* @param int $pos
* @param array $tokens
* @param array $options
* @param int $parsed_pos
* @param int $depth
*
* @return bool Returns true if parsed successfully, false otherwise.
*/
private static function parseExpression(string $source, int &$pos, array &$tokens, array $options,
int &$parsed_pos = null, int $depth = 0): bool {
$binary_operator_parser = new CSetParser(['<', '>', '<=', '>=', '+', '-', '/', '*', '=', '<>']);
$logical_operator_parser = new CSetParser(['and', 'or']);
if ($depth++ > self::MAX_MATH_FUNCTION_DEPTH) {
return false;
}
$state = self::STATE_AFTER_OPEN_BRACE;
$after_space = false;
$level = 0;
$p = $pos;
$_tokens = [];
while (isset($source[$p])) {
$char = $source[$p];
if (strpos(self::WHITESPACES, $char) !== false) {
$after_space = true;
$p++;
continue;
}
switch ($state) {
case self::STATE_AFTER_OPEN_BRACE:
switch ($char) {
case '-':
$state = self::STATE_AFTER_UNARY_MINUS;
$_tokens[] = [
'type' => CExpressionParserResult::TOKEN_TYPE_OPERATOR,
'pos' => $p,
'match' => $char,
'length' => 1
];
break;
case '(':
$level++;
$_tokens[] = [
'type' => CExpressionParserResult::TOKEN_TYPE_OPEN_BRACE,
'pos' => $p,
'match' => $char,
'length' => 1
];
break;
default:
if (self::parseNot($source, $p, $_tokens)) {
$state = self::STATE_AFTER_NOT_OPERATOR;
}
elseif (self::parseConstant($source, $p, $_tokens, $options, $depth)) {
$state = self::STATE_AFTER_CONSTANT;
if ($level == 0) {
$pos = $p + 1;
$tokens = $_tokens;
}
}
else {
break 3;
}
}
break;
case self::STATE_AFTER_BINARY_OPERATOR:
switch ($char) {
case '-':
$state = self::STATE_AFTER_UNARY_MINUS;
$_tokens[] = [
'type' => CExpressionParserResult::TOKEN_TYPE_OPERATOR,
'pos' => $p,
'match' => $char,
'length' => 1
];
break;
case '(':
$level++;
$state = self::STATE_AFTER_OPEN_BRACE;
$_tokens[] = [
'type' => CExpressionParserResult::TOKEN_TYPE_OPEN_BRACE,
'pos' => $p,
'match' => $char,
'length' => 1
];
break;
default:
if (self::parseConstant($source, $p, $_tokens, $options, $depth)) {
$state = self::STATE_AFTER_CONSTANT;
if ($level == 0) {
$pos = $p + 1;
$tokens = $_tokens;
}
break;
}
if ($after_space && self::parseNot($source, $p, $_tokens)) {
$state = self::STATE_AFTER_NOT_OPERATOR;
}
else {
break 3;
}
}
break;
case self::STATE_AFTER_LOGICAL_OPERATOR:
switch ($char) {
case '-':
if (!$after_space) {
break 3;
}
$state = self::STATE_AFTER_UNARY_MINUS;
$_tokens[] = [
'type' => CExpressionParserResult::TOKEN_TYPE_OPERATOR,
'pos' => $p,
'match' => $char,
'length' => 1
];
break;
case '(':
$level++;
$state = self::STATE_AFTER_OPEN_BRACE;
$_tokens[] = [
'type' => CExpressionParserResult::TOKEN_TYPE_OPEN_BRACE,
'pos' => $p,
'match' => $char,
'length' => 1
];
break;
default:
if (!$after_space) {
break 3;
}
if (self::parseNot($source, $p, $_tokens)) {
$state = self::STATE_AFTER_NOT_OPERATOR;
}
elseif (self::parseConstant($source, $p, $_tokens, $options, $depth)) {
$state = self::STATE_AFTER_CONSTANT;
if ($level == 0) {
$pos = $p + 1;
$tokens = $_tokens;
}
}
else {
break 3;
}
}
break;
case self::STATE_AFTER_CLOSE_BRACE:
switch ($char) {
case ')':
if ($level == 0) {
break 3;
}
$level--;
$_tokens[] = [
'type' => CExpressionParserResult::TOKEN_TYPE_CLOSE_BRACE,
'pos' => $p,
'match' => $char,
'length' => 1
];
if ($level == 0) {
$pos = $p + 1;
$tokens = $_tokens;
}
break;
default:
if (self::parseUsing($binary_operator_parser, $source, $p, $_tokens,
CExpressionParserResult::TOKEN_TYPE_OPERATOR)) {
$state = self::STATE_AFTER_BINARY_OPERATOR;
break;
}
if (self::parseUsing($logical_operator_parser, $source, $p, $_tokens,
CExpressionParserResult::TOKEN_TYPE_OPERATOR)) {
$state = self::STATE_AFTER_LOGICAL_OPERATOR;
break;
}
break 3;
}
break;
case self::STATE_AFTER_CONSTANT:
switch ($char) {
case ')':
if ($level == 0) {
break 3;
}
$level--;
$state = self::STATE_AFTER_CLOSE_BRACE;
$_tokens[] = [
'type' => CExpressionParserResult::TOKEN_TYPE_CLOSE_BRACE,
'pos' => $p,
'match' => $char,
'length' => 1
];
if ($level == 0) {
$pos = $p + 1;
$tokens = $_tokens;
}
break;
default:
if (self::parseUsing($binary_operator_parser, $source, $p, $_tokens,
CExpressionParserResult::TOKEN_TYPE_OPERATOR)) {
$state = self::STATE_AFTER_BINARY_OPERATOR;
break;
}
if ($after_space && self::parseUsing($logical_operator_parser, $source, $p, $_tokens,
CExpressionParserResult::TOKEN_TYPE_OPERATOR)) {
$state = self::STATE_AFTER_LOGICAL_OPERATOR;
}
else {
break 3;
}
}
break;
case self::STATE_AFTER_NOT_OPERATOR:
switch ($char) {
case '-':
if (!$after_space) {
break 3;
}
$state = self::STATE_AFTER_UNARY_MINUS;
$_tokens[] = [
'type' => CExpressionParserResult::TOKEN_TYPE_OPERATOR,
'pos' => $p,
'match' => $char,
'length' => 1
];
break;
case '(':
$level++;
$state = self::STATE_AFTER_OPEN_BRACE;
$_tokens[] = [
'type' => CExpressionParserResult::TOKEN_TYPE_OPEN_BRACE,
'pos' => $p,
'match' => $char,
'length' => 1
];
break;
default:
if (!$after_space) {
break 3;
}
if (self::parseConstant($source, $p, $_tokens, $options, $depth)) {
$state = self::STATE_AFTER_CONSTANT;
if ($level == 0) {
$pos = $p + 1;
$tokens = $_tokens;
}
}
else {
break 3;
}
}
break;
case self::STATE_AFTER_UNARY_MINUS:
switch ($char) {
case '(':
$level++;
$state = self::STATE_AFTER_OPEN_BRACE;
$_tokens[] = [
'type' => CExpressionParserResult::TOKEN_TYPE_OPEN_BRACE,
'pos' => $p,
'match' => $char,
'length' => 1
];
break;
default:
if (self::parseConstant($source, $p, $_tokens, $options, $depth)) {
$state = self::STATE_AFTER_CONSTANT;
if ($level == 0) {
$pos = $p + 1;
$tokens = $_tokens;
}
}
else {
break 3;
}
}
break;
}
$after_space = false;
$p++;
}
$parsed_pos = $p;
return (bool) $tokens;
}
/**
* Parse unary "not".
*
* @param string $source
* @param int $pos
* @param array $tokens
*
* @return bool
*/
private static function parseNot(string $source, int &$pos, array &$tokens): bool {
if (substr($source, $pos, 3) !== 'not' || !isset($source[$pos + 3])
|| strpos(self::WHITESPACES.'(', $source[$pos + 3]) === false) {
return false;
}
$tokens[] = [
'type' => CExpressionParserResult::TOKEN_TYPE_OPERATOR,
'pos' => $pos,
'match' => 'not',
'length' => 3
];
$pos += 2;
return true;
}
/**
* Parse the string using the given parser. If a match has been found, move the cursor to the last symbol of the
* matched string.
*
* @param CParser $parser
* @param string $source
* @param int $pos
* @param array $tokens
* @param int $token_type
*
* @return bool
*/
private static function parseUsing(CParser $parser, string $source, int &$pos, array &$tokens,
int $token_type): bool {
if ($parser->parse($source, $pos) == CParser::PARSE_FAIL) {
return false;
}
$tokens[] = [
'type' => $token_type,
'pos' => $pos,
'match' => $parser->getMatch(),
'length' => $parser->getLength()
];
$pos += $parser->getLength() - 1;
return true;
}
/**
* Parses a constant in the expression.
*
* The constant can be (depending on options):
* - function like func(<expression>)
* - function like func(/host/item,<params>)
* - floating point number; can be with suffix [KMGTsmhdw]
* - string
* - macro like {TRIGGER.VALUE}
* - user macro like {$MACRO}
* - LLD macro like {#LLD}
* - LLD macro with function like {{#LLD}.func())}
*
* @param string $source
* @param int $pos
* @param array $tokens
* @param array $options
* @param int $depth
*
* @return bool Returns true if parsed successfully, false otherwise.
*/
private static function parseConstant(string $source, int &$pos, array &$tokens, array $options, int $depth): bool {
if (self::parseNumber($source, $pos, $tokens) || self::parseString($source, $pos, $tokens)) {
return true;
}
if (!$options['calculated']) {
$macro_parser = new CMacroParser(['macros' => ['{TRIGGER.VALUE}']]);
if (self::parseUsing($macro_parser, $source, $pos, $tokens, CExpressionParserResult::TOKEN_TYPE_MACRO)) {
return true;
}
}
if ($options['collapsed_expression']) {
$functionid_parser = new CFunctionIdParser();
if (self::parseUsing($functionid_parser, $source, $pos, $tokens,
CExpressionParserResult::TOKEN_TYPE_FUNCTIONID_MACRO)) {
return true;
}
}
elseif (self::parseHistFunction($source, $pos, $tokens, $options)) {
return true;
}
if (self::parseMathFunction($source, $pos, $tokens, $options, $depth)) {
return true;
}
if ($options['usermacros']) {
$user_macro_parser = new CUserMacroParser();
if (self::parseUsing($user_macro_parser, $source, $pos, $tokens,
CExpressionParserResult::TOKEN_TYPE_USER_MACRO)) {
return true;
}
}
if ($options['lldmacros']) {
$lld_macro_parser = new CLLDMacroParser();
if (self::parseUsing($lld_macro_parser, $source, $pos, $tokens,
CExpressionParserResult::TOKEN_TYPE_LLD_MACRO)) {
return true;
}
$lld_macro_function_parser = new CLLDMacroFunctionParser();
if (self::parseUsing($lld_macro_function_parser, $source, $pos, $tokens,
CExpressionParserResult::TOKEN_TYPE_LLD_MACRO)) {
return true;
}
}
return false;
}
/**
* Parses a historical function constant in the expression.
*
* @param string $source
* @param int $pos
* @param array $tokens
* @param array $options
*
* @return bool Returns true if parsed successfully, false otherwise.
*/
private static function parseHistFunction(string $source, int &$pos, array &$tokens, array $options): bool {
$hist_function_parser = new CHistFunctionParser([
'usermacros' => $options['usermacros'],
'lldmacros' => $options['lldmacros'],
'calculated' => $options['calculated'],
'host_macro' => $options['host_macro'],
'host_macro_n' => $options['host_macro_n'],
'empty_host' => $options['empty_host']
]);
if ($hist_function_parser->parse($source, $pos) == CParser::PARSE_FAIL) {
return false;
}
$len = $hist_function_parser->getLength();
$tokens[] = [
'type' => CExpressionParserResult::TOKEN_TYPE_HIST_FUNCTION,
'pos' => $pos,
'match' => $hist_function_parser->getMatch(),
'length' => $len,
'data' => [
'function' => $hist_function_parser->getFunction(),
'parameters' => $hist_function_parser->getParameters()
]
];
$pos += $len - 1;
return true;
}
/**
* Parses a math function constant in the expression.
*
* @param string $source
* @param int $pos
* @param array $tokens
* @param array $options
* @param int $depth
*
* @return bool Returns true if parsed successfully, false otherwise.
*/
private static function parseMathFunction(string $source, int &$pos, array &$tokens, array $options,
int $depth): bool {
$p = $pos;
if (!preg_match('/^([a-z0-9_]+)\(/', substr($source, $p), $matches)) {
return false;
}
$p += strlen($matches[0]);
$p2 = $p - 1;
$_tokens = [];
$state = self::STATE_NEW;
while (isset($source[$p])) {
switch ($state) {
case self::STATE_NEW:
switch ($source[$p]) {
case ' ':
break;
case ')':
if (!$_tokens) {
$state = self::STATE_END_OF_PARAMS;
break;
}
break 3;
default:
$_p = $p;
$expression_tokens = [];
$parsed_pos = 0;
if (!self::parseExpression($source, $_p, $expression_tokens, $options, $parsed_pos,
$depth)) {
break 3;
}
$len = $_p - $p;
$_tokens[] = [
'type' => CExpressionParserResult::TOKEN_TYPE_EXPRESSION,
'pos' => $p,
'match' => substr($source, $p, $len),
'length' => $len,
'data' => [
'tokens' => $expression_tokens
]
];
$p = $_p - 1;
$state = self::STATE_END;
}
break;
case self::STATE_END:
switch ($source[$p]) {
case ' ':
break;
case ')':
$state = self::STATE_END_OF_PARAMS;
break;
case ',':
$state = self::STATE_NEW;
break;
default:
break 3;
}
break;
case self::STATE_END_OF_PARAMS:
break 2;
}
$p++;
}
if ($state != self::STATE_END_OF_PARAMS) {
return false;
}
$len = $p - $pos;
$tokens[] = [
'type' => CExpressionParserResult::TOKEN_TYPE_MATH_FUNCTION,
'pos' => $pos,
'match' => substr($source, $pos, $len),
'length' => $len,
'data' => [
'function' => $matches[1],
'parameters' => $_tokens
]
];
$pos += $len - 1;
return true;
}
/**
* Parses a number constant in the expression and moves a current position on a last symbol of the number.
*
* @param string $source
* @param int $pos
* @param array $tokens
*
* @return bool returns true if parsed successfully, false otherwise
*/
private static function parseNumber(string $source, int &$pos, array &$tokens): bool {
$number_parser = new CNumberParser([
'with_minus' => false,
'with_size_suffix' => true,
'with_time_suffix' => true
]);
if ($number_parser->parse($source, $pos) == CParser::PARSE_FAIL) {
return false;
}
$value = $number_parser->calcValue();
if (abs($value) == INF) {
return false;
}
$len = $number_parser->getLength();
$tokens[] = [
'type' => CExpressionParserResult::TOKEN_TYPE_NUMBER,
'pos' => $pos,
'match' => $number_parser->getMatch(),
'length' => $len,
'data' => ['suffix' => $number_parser->getSuffix()]
];
$pos += $len - 1;
return true;
}
/**
* Parses a quoted string constant in the expression.
*
* @param string $source
* @param int $pos
* @param array $tokens
*
* @return bool returns true if parsed successfully, false otherwise
*/
private static function parseString(string $source, int &$pos, array &$tokens): bool {
if (!preg_match('/^"([^"\\\\]|\\\\["\\\\])*"/', substr($source, $pos), $matches)) {
return false;
}
$len = strlen($matches[0]);
$tokens[] = [
'type' => CExpressionParserResult::TOKEN_TYPE_STRING,
'pos' => $pos,
'match' => $matches[0],
'length' => $len
];
$pos += $len - 1;
return true;
}
/**
* Unquoting quoted string $value.
*
* @param string $value
*
* @return string
*/
public static function unquoteString(string $value): string {
return strtr(substr($value, 1, -1), ['\\"' => '"', '\\\\' => '\\']);
}
/**
* Quoting $value if it contains a non numeric value.
*
* @param string $value
* @param bool $allow_macros
* @param bool $force
*
* @return string
*/
public static function quoteString(string $value, bool $allow_macros = true, bool $force = false): string {
if (!$force) {
$number_parser = new CNumberParser(['with_size_suffix' => true, 'with_time_suffix' => true]);
if ($number_parser->parse($value) == CParser::PARSE_SUCCESS) {
return $value;
}
if ($allow_macros) {
$user_macro_parser = new CUserMacroParser();
$macro_parser = new CMacroParser(['macros' => ['{TRIGGER.VALUE}']]);
$lld_macro_parser = new CLLDMacroParser();
$lld_macro_function_parser = new CLLDMacroFunctionParser;
if ($user_macro_parser->parse($value) == CParser::PARSE_SUCCESS
|| $macro_parser->parse($value) == CParser::PARSE_SUCCESS
|| $lld_macro_parser->parse($value) == CParser::PARSE_SUCCESS
|| $lld_macro_function_parser->parse($value) == CParser::PARSE_SUCCESS) {
return $value;
}
}
}
return '"'.strtr($value, ['\\' => '\\\\', '"' => '\\"']).'"';
}
/**
* Returns an expression parser result.
*
* @return null|CExpressionParserResult
*/
public function getResult(): ?CExpressionParserResult {
return $this->result;
}
/**
* Returns a friendly error message or empty string if expression was parsed successfully.
*
* @return string
*/
public function getError(): string {
return $this->error;
}
}