parser = new C10TriggerExpression(['allow_func_only' => true]); $this->standalone_functions = getStandaloneFunctions(); } /** * Converts trigger expression to new syntax. * * @param array $trigger_data * @param string $trigger_data['expression'] * @param string $trigger_data['host'] (optional) * @param string $trigger_data['item'] (optional) * * @return string */ public function convert($trigger_data) { $this->item = (array_key_exists('item', $trigger_data) && $trigger_data['item']) ? $trigger_data['item'] : ''; $this->host = (array_key_exists('host', $trigger_data) && $this->item) ? $trigger_data['host'] : ''; if ($this->parser->parse($trigger_data['expression']) !== false) { $functions = $this->parser->result->getTokensByType(C10TriggerExprParserResult::TOKEN_TYPE_FUNCTION_MACRO); $this->hanged_refs = $this->checkHangedFunctionsPerHost($functions); $extra_expressions = []; for ($i = count($functions) - 1; $i >= 0; $i--) { [$new_expr, $extra_expr] = $this->convertFunction($functions[$i]['data'], $this->host, $this->item); $trigger_data['expression'] = substr_replace($trigger_data['expression'], $new_expr, $functions[$i]['pos'], $functions[$i]['length'] ); if ($extra_expr !== null) { $extra_expressions[] = $extra_expr; } } if ($extra_expressions) { $extra_expressions = array_keys(array_flip($extra_expressions)); $trigger_data['expression'] = '('.$trigger_data['expression'].')'; $extra_expressions = array_reverse($extra_expressions); $trigger_data['expression'] .= ' or '.implode(' or ', $extra_expressions); } } return $trigger_data['expression']; } /** * Convert function to new syntax. * * @param array $fn Function to convert. * @param string $host_name Host name. * @param string $item_key Item key. * * @return array */ protected function convertFunction(array $fn, string $host_name, string $item_key): array { if ($fn['item'] === '' && $fn['host'] === '') { $query = sprintf('/%s/%s', $host_name, $item_key); $has_hanged_functions = $this->hanged_refs['']; } else { $query = sprintf('/%s/%s', $fn['host'], $fn['item']); $has_hanged_functions = array_key_exists($fn['host'], $this->hanged_refs) ? $this->hanged_refs[$fn['host']] : false; } $extra_expr = null; $parameters = [ 'unquotable' => array_filter($fn['functionParamsRaw']['parameters'], function ($param) { return ($param['type'] == C10FunctionParser::PARAM_UNQUOTED && $param['raw'] === ''); }), 'indicated' => array_filter($fn['functionParamsRaw']['parameters'], function ($param) { return ($param['type'] == C10FunctionParser::PARAM_QUOTED || $param['raw'] !== ''); }) ]; switch ($fn['functionName']) { case 'abschange': $new_expression = sprintf('abs(change(%1$s))', $query); break; case 'band': $params = self::convertParameters($fn['functionParams'], $parameters, $fn['functionName']); $timeshift = self::paramsToString([$params[0]]); $mask = self::paramsToString([$params[1]]); $new_expression = sprintf('bitand(last(%1$s%2$s)%3$s)', $query, $timeshift, $mask); break; case 'change': $new_expression = sprintf('change(%1$s)', $query); break; case 'delta': $params = self::convertParameters($fn['functionParams'], $parameters, $fn['functionName']); $params = self::paramsToString($params); $new_expression = sprintf('(max(%1$s%2$s)-min(%1$s%2$s))', $query, $params); break; case 'diff': $new_expression = sprintf('(last(%1$s,#1)<>last(%1$s,#2))', $query); break; case 'prev': $new_expression = sprintf('last(%1$s,#2)', $query); break; case 'trenddelta': $params = self::convertParameters($fn['functionParams'], $parameters, $fn['functionName']); $params = self::paramsToString($params); $new_expression = sprintf('(trendmax(%1$s%2$s)-trendmin(%1$s%2$s))', $query, $params); break; case 'iregexp': case 'regexp': case 'str': $params = self::convertParameters($fn['functionParams'], $parameters, $fn['functionName']); $params = self::paramsToString($params); $new_expression = sprintf('find(%1$s%2$s)', $query, $params); break; case 'strlen': $params = self::convertParameters($fn['functionParams'], $parameters, $fn['functionName']); $params = self::paramsToString($params); $new_expression = sprintf('length(last(%1$s%2$s))', $query, $params); break; case 'date': case 'dayofmonth': case 'dayofweek': case 'time': case 'now': $new_expression = $fn['functionName'].'()'; if (!$has_hanged_functions) { $extra_expr = sprintf('(last(%1$s)<>last(%1$s))', $query); } break; case 'logseverity': $new_expression = sprintf('logseverity(%1$s)', $query); break; default: $new_expression = sprintf('%s(%s%s)', $fn['functionName'], $query, self::paramsToString(self::convertParameters($fn['functionParams'], $parameters, $fn['functionName'] )) ); break; } return [$new_expression, $extra_expr]; } /** * Convert function parameters to new syntax. * * @param array $parameters List of parameters according to the previous syntax. * @param array $param_dets * @param array $param_dets['unquotable'] List of numeric indexes for parameters that don't need to be quoted. * @param array $param_dets['indicated'] List of numeric indexes for parameters that are especially indicated and * must be kept. * @param string $fn_name Function name. * * @return array */ private static function convertParameters(array $parameters, array $param_dets, string $fn_name): array { switch ($fn_name) { // (sec|#num,) case 'delta': case 'avg': case 'max': case 'min': case 'sum': // (sec|#num,,percentage) case 'percentile': $parameters += ['', '']; $parameters[0] = self::convertParamSec($parameters[0]); $parameters[1] = self::convertTimeshift($parameters[1]); $parameters[0] = ((string) $parameters[0] === '0') ? '#1' : $parameters[0]; if ($parameters[1] !== '') { $parameters[0] .= ':'.$parameters[1]; } unset($parameters[1], $param_dets['unquotable'][1], $param_dets['indicated'][1]); break; // (sec|#num,,threshold,) case 'timeleft': $parameters += ['', '', '', '']; $parameters[0] = self::convertParamSec($parameters[0]); $parameters[1] = self::convertTimeshift($parameters[1]); $parameters[0] = ((string) $parameters[0] === '0') ? '#1' : $parameters[0]; if ($parameters[1] !== '') { $parameters[0] .= ':'.$parameters[1]; } unset($parameters[1], $param_dets['unquotable'][1], $param_dets['indicated'][1]); if ($parameters[3] === '') { // Don't quote unspecified . $param_dets['unquotable'][3] = true; } break; // (<#num>,) case 'strlen': case 'last': $parameters += ['', '']; if (!self::isMacro($parameters[0]) && (substr($parameters[0], 0, 1) !== '#' || !ctype_digit(substr($parameters[0], 1)) || (int) substr($parameters[0], 1) === 0)) { $parameters[0] = ''; } $parameters[1] = self::convertTimeshift($parameters[1]); if ($parameters[1] !== '') { $parameters[0] = ($parameters[0] === '') ? '#1' : $parameters[0]; $parameters[0] .= ':'.$parameters[1]; } unset($parameters[1], $param_dets['unquotable'][1], $param_dets['indicated'][1]); break; // (sec|#num,,time,,) case 'forecast': $parameters += ['', '', '', '', '']; $parameters[0] = self::convertParamSec($parameters[0]); $parameters[1] = self::convertTimeshift($parameters[1]); $parameters[0] = ((string) $parameters[0] === '0') ? '#1' : $parameters[0]; if ($parameters[1] !== '') { $parameters[0] .= ':'.$parameters[1]; } unset($parameters[1], $param_dets['unquotable'][1], $param_dets['indicated'][1]); $parameters[2] = self::convertParamSec($parameters[2]); if ($parameters[3] === '') { // Don't quote unspecified . $param_dets['unquotable'][3] = true; } if ($parameters[4] === '') { // Don't quote unspecified . $param_dets['unquotable'][4] = true; } break; // (,mask,) case 'band': $parameters += ['', '', '']; if (!self::isMacro($parameters[0]) && (substr($parameters[0], 0, 1) !== '#' || !ctype_digit(substr($parameters[0], 1)) || (int) substr($parameters[0], 1) === 0)) { $parameters[0] = ''; } $parameters[2] = self::convertTimeshift($parameters[2]); if ($parameters[2] !== '') { $parameters[0] = ($parameters[0] === '') ? '#1' : $parameters[0]; $parameters[0] .= ':'.$parameters[2]; } unset($parameters[2], $param_dets['unquotable'][2], $param_dets['indicated'][2]); break; // (sec|#num,,,) case 'count': $parameters += ['', '', '', '']; $parameters[0] = self::convertParamSec($parameters[0]); $parameters[3] = self::convertTimeshift($parameters[3]); $parameters[0] = ((string) $parameters[0] === '0') ? '#1' : $parameters[0]; if ($parameters[3] !== '') { $parameters[0] .= ':'.$parameters[3]; } if ($parameters[2] === 'band') { $parameters[2] = 'bitand'; } elseif ($parameters[2] === '') { // Don't quote unspecified . $param_dets['unquotable'][2] = true; } $parameters[3] = $parameters[1]; unset($param_dets['unquotable'][3], $param_dets['indicated'][3], $parameters[1]); if (array_key_exists(1, $param_dets['unquotable'])) { $param_dets['unquotable'][3] = true; unset($param_dets['unquotable'][1]); } if (array_key_exists(1, $param_dets['indicated'])) { $param_dets['indicated'][3] = true; unset($param_dets['indicated'][1]); } break; // (sec,) case 'nodata': $parameters += ['', '']; $parameters[0] = self::convertParamSec($parameters[0]); if ($parameters[1] === '') { // Don't quote unspecified . $param_dets['unquotable'][1] = true; } break; // (sec) case 'fuzzytime': $parameters += ['']; $parameters[0] = self::convertParamSec($parameters[0]); break; // (,) case 'iregexp': case 'regexp': case 'str': $parameters += ['', '']; $parameters = [ self::convertParamSec($parameters[1]), ($fn_name === 'str') ? 'like' : $fn_name, $parameters[0] ]; unset($param_dets['unquotable'][1]); if (array_key_exists(0, $param_dets['indicated'])) { $param_dets['indicated'][2] = true; unset($param_dets['indicated'][0]); } break; // (period,period_shift) case 'trendavg': case 'trendcount': case 'trenddelta': case 'trendmax': case 'trendmin': case 'trendsum': $parameters += ['', '']; $parameters[0] = self::convertParamPeriod($parameters[0]); if ($parameters[1] !== '') { $parameters[0] .= ':'.$parameters[1]; } unset($parameters[1], $param_dets['unquotable'][1], $param_dets['indicated'][1]); break; case 'logeventid': case 'logsource': array_unshift($parameters, ''); if (array_key_exists(0, $param_dets['indicated'])) { $param_dets['indicated'][1] = true; unset($param_dets['indicated'][0]); } break; } // Keys in $parameters array to skip from quoting. $param_dets['unquotable'] = array_keys($param_dets['unquotable']); $param_dets['indicated'] = array_keys($param_dets['indicated']); $functions_with_period_parameter = ['delta', 'avg', 'max', 'min', 'sum', 'last', 'strlen', 'percentile', 'timeleft', 'forecast', 'band', 'count', 'fuzzytime', 'nodata', 'iregexp', 'regexp', 'str', 'trendavg', 'trendcount', 'trenddelta', 'trendmax', 'trendmin', 'trendsum', 'logeventid', 'logsource' ]; if (in_array($fn_name, $functions_with_period_parameter)) { $param_dets['unquotable'][] = 0; } if (in_array($fn_name, ['forecast', 'timeleft', 'percentile'])) { // Time parameter don't need to be quoted for forecast() function. $param_dets['unquotable'][] = 2; } elseif ($fn_name === 'band') { // Mask parameter don't need to be quoted for bitand() function. $param_dets['unquotable'][] = 1; } array_walk($parameters, function (&$param, $i) use ($param_dets) { if (in_array($i, $param_dets['unquotable'])) { return; } $param = CHistFunctionParser::quoteParam($param); }); // Remove empty parameters from the end of the parameters array. foreach (array_reverse(array_keys($parameters)) as $i) { if (in_array($i, $param_dets['indicated'])) { break; } if ($parameters[$i] !== '""' && $parameters[$i] !== '') { break; } unset($parameters[$i]); } return array_values($parameters); } /** * Convert seconds. * * @param string $param Parameter to convert. * * @return string */ private static function convertParamSec(string $param): string { return (preg_match('/^(?\d+)(?['.ZBX_TIME_SUFFIXES.']{0,1})$/', $param, $m) && $m['num'] > 0) ? $m['num'].($m['suffix'] !== '' ? $m['suffix'] : 's') : $param; } /** * Convert period. * * @param string $param Parameter to convert. * * @return string */ private static function convertParamPeriod(string $param): string { return (preg_match('/^(?\d+)(?[hdwMy]{0,1})$/', $param, $m) && $m['num'] > 0) ? $m['num'].($m['suffix'] !== '' ? $m['suffix'] : 's') : $param; } /** * Convert time shift. * * @param string $param Parameter to convert. * * @return string */ private static function convertTimeshift(string $param): string { $param = (preg_match('/^(?\d+)(?['.ZBX_TIME_SUFFIXES.']{0,1})$/', $param, $m) && $m['num'] > 0) ? $m['num'].($m['suffix'] !== '' ? $m['suffix'] : 's') : $param; return ($param !== '') ? 'now-'.$param : ''; } /** * Concatenate parameters into comma separated string. * * @param array $parameters Parameter to concatenate. * * @return string */ private static function paramsToString(array $parameters): string { $parameters = rtrim(implode(',', $parameters), ','); return ($parameters === '') ? '' : ','.$parameters; } /** * Check if each particular host reference would be linked through at least one functions according to the new * trigger expression syntax. * * @param array $tokens * * @return array */ protected function checkHangedFunctionsPerHost(array $tokens): array { $hanged_refs = []; foreach ($tokens as $token) { $fn = $token['data']; if (!array_key_exists($fn['host'], $hanged_refs)) { $hanged_refs[$fn['host']] = false; } if (!in_array($fn['functionName'], $this->standalone_functions)) { $hanged_refs[$fn['host']] = true; } } return $hanged_refs; } /** * Check if given string is valid user or lld macro. * * @param string $param * * @return bool */ private static function isMacro(string $param): bool { foreach ([new CUserMacroParser(), new CLLDMacroParser(), new CLLDMacroFunctionParser()] as $parser) { if ($parser->parse($param) == CParser::PARSE_SUCCESS) { return true; } } return false; } }