1<?php declare(strict_types = 1); 2/* 3** Zabbix 4** Copyright (C) 2001-2021 Zabbix SIA 5** 6** This program is free software; you can redistribute it and/or modify 7** it under the terms of the GNU General Public License as published by 8** the Free Software Foundation; either version 2 of the License, or 9** (at your option) any later version. 10** 11** This program is distributed in the hope that it will be useful, 12** but WITHOUT ANY WARRANTY; without even the implied warranty of 13** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14** GNU General Public License for more details. 15** 16** You should have received a copy of the GNU General Public License 17** along with this program; if not, write to the Free Software 18** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19**/ 20 21 22/** 23 * Class for validating history functions. 24 */ 25class CHistFunctionValidator extends CValidator { 26 27 /** 28 * An options array. 29 * 30 * Supported options: 31 * 'parameters' => [] Definition of parameters of known history functions. 32 * 'usermacros' => false Enable user macros usage in function parameters. 33 * 'lldmacros' => false Enable low-level discovery macros usage in function parameters. 34 * 'calculated' => false Validate history function as part of calculated item formula. 35 * 'aggregating' => false Validate as aggregating history function. 36 * 37 * @var array 38 */ 39 private $options = [ 40 'parameters' => [], 41 'usermacros' => false, 42 'lldmacros' => false, 43 'calculated' => false, 44 'aggregating' => false 45 ]; 46 47 /** 48 * @param array $options 49 */ 50 public function __construct(array $options = []) { 51 $this->options = $options + $this->options; 52 } 53 54 /** 55 * Validate history function. 56 * 57 * @param array $token A token of CExpressionParserResult::TOKEN_TYPE_HIST_FUNCTION type. 58 * 59 * @return bool 60 */ 61 public function validate($token) { 62 $invalid_param_messages = [ 63 _('invalid first parameter in function "%1$s"'), 64 _('invalid second parameter in function "%1$s"'), 65 _('invalid third parameter in function "%1$s"'), 66 _('invalid fourth parameter in function "%1$s"'), 67 _('invalid fifth parameter in function "%1$s"') 68 ]; 69 70 if (!array_key_exists($token['data']['function'], $this->options['parameters'])) { 71 $this->setError(_s('unknown function "%1$s"', $token['data']['function'])); 72 73 return false; 74 } 75 76 $params = $token['data']['parameters']; 77 $params_spec = $this->options['parameters'][$token['data']['function']]; 78 79 if (count($params) > count($params_spec)) { 80 $this->setError(_s('invalid number of parameters in function "%1$s"', $token['data']['function'])); 81 82 return false; 83 } 84 85 foreach ($params_spec as $index => $param_spec) { 86 $required = !array_key_exists('required', $param_spec) || $param_spec['required']; 87 88 if ($index >= count($params)) { 89 if ($required) { 90 $this->setError( 91 _s('mandatory parameter is missing in function "%1$s"', $token['data']['function']) 92 ); 93 94 return false; 95 } 96 97 continue; 98 } 99 100 $param = $params[$index]; 101 102 if ($param['match'] === '') { 103 if ($required) { 104 $this->setError(_params($invalid_param_messages[$index], [$token['data']['function']])); 105 106 return false; 107 } 108 109 continue; 110 } 111 112 switch ($param['type']) { 113 case CHistFunctionParser::PARAM_TYPE_PERIOD: 114 if (self::hasMacros($param['data']['sec_num'], $this->options) 115 && $param['data']['time_shift'] === '') { 116 continue 2; 117 } 118 break; 119 120 case CHistFunctionParser::PARAM_TYPE_QUOTED: 121 if (self::hasMacros(CHistFunctionParser::unquoteParam($param['match']), $this->options)) { 122 continue 2; 123 } 124 break; 125 126 case CHistFunctionParser::PARAM_TYPE_UNQUOTED: 127 if (self::hasMacros($param['match'], $this->options)) { 128 continue 2; 129 } 130 break; 131 } 132 133 if (array_key_exists('rules', $param_spec)) { 134 $is_valid = self::validateRules($param, $param_spec['rules'], $this->options); 135 136 if (!$is_valid) { 137 $this->setError(_params($invalid_param_messages[$index], [$token['data']['function']])); 138 139 return false; 140 } 141 } 142 } 143 144 return true; 145 } 146 147 /** 148 * Loose check if string value contains macros. 149 * 150 * @param string $value 151 * @param array $options 152 * 153 * @static 154 * 155 * @return bool 156 */ 157 private static function hasMacros(string $value, array $options): bool { 158 if (!$options['usermacros'] && !$options['lldmacros']) { 159 return false; 160 } 161 162 $macro_parsers = []; 163 164 if ($options['usermacros']) { 165 $macro_parsers[] = new CUserMacroParser(); 166 } 167 if ($options['lldmacros']) { 168 $macro_parsers[] = new CLLDMacroParser(); 169 $macro_parsers[] = new CLLDMacroFunctionParser(); 170 } 171 172 for ($pos = strpos($value, '{'); $pos !== false; $pos = strpos($value, '{', $pos + 1)) { 173 foreach ($macro_parsers as $macro_parser) { 174 if ($macro_parser->parse($value, $pos) != CParser::PARSE_FAIL) { 175 return true; 176 } 177 } 178 } 179 180 return false; 181 } 182 183 /** 184 * Validate function parameter token's compliance to the rules. 185 * 186 * @param array $param Function parameter token. 187 * @param array $rules 188 * @param array $options 189 * 190 * @static 191 * 192 * @return bool 193 */ 194 private static function validateRules(array $param, array $rules, array $options): bool { 195 $param_match_unquoted = ($param['type'] == CHistFunctionParser::PARAM_TYPE_QUOTED) 196 ? CHistFunctionParser::unquoteParam($param['match']) 197 : $param['match']; 198 199 foreach ($rules as $rule) { 200 switch ($rule['type']) { 201 case 'query': 202 if ($param['type'] != CHistFunctionParser::PARAM_TYPE_QUERY) { 203 return false; 204 } 205 206 if (!self::validateQuery($param['data']['host'], $param['data']['item'], $param['data']['filter'], 207 $options)) { 208 return false; 209 } 210 211 break; 212 213 case 'period': 214 if ($param['type'] != CHistFunctionParser::PARAM_TYPE_PERIOD) { 215 return false; 216 } 217 218 if (!self::validatePeriod($param['data']['sec_num'], $param['data']['time_shift'], $rule['mode'], 219 $options)) { 220 return false; 221 } 222 223 break; 224 225 case 'number': 226 $with_suffix = array_key_exists('with_suffix', $rule) && $rule['with_suffix']; 227 228 $parser = new CNumberParser(['with_minus' => true, 'with_suffix' => $with_suffix]); 229 230 if ($parser->parse($param_match_unquoted) != CParser::PARSE_SUCCESS) { 231 return false; 232 } 233 234 $value = $parser->calcValue(); 235 236 if ((array_key_exists('min', $rule) && $value < $rule['min']) 237 || array_key_exists('max', $rule) && $value > $rule['max']) { 238 return false; 239 } 240 241 break; 242 243 case 'regexp': 244 if (preg_match($rule['pattern'], $param_match_unquoted) != 1) { 245 return false; 246 } 247 248 break; 249 250 case 'time': 251 $with_year = array_key_exists('with_year', $rule) && $rule['with_year']; 252 $min = array_key_exists('min', $rule) ? $rule['min'] : ZBX_MIN_INT32; 253 $max = array_key_exists('max', $rule) ? $rule['max'] : ZBX_MAX_INT32; 254 255 $sec = timeUnitToSeconds($param_match_unquoted, $with_year); 256 257 if ($sec === null || $sec < $min || $sec > $max) { 258 return false; 259 } 260 261 break; 262 263 default: 264 return false; 265 } 266 } 267 268 return true; 269 } 270 271 /** 272 * Validate function's query parameter. 273 * 274 * @param string $host 275 * @param string $item 276 * @param array $filter Filter token. 277 * @param array $options 278 * 279 * @static 280 * 281 * @return bool 282 */ 283 private static function validateQuery(string $host, string $item, array $filter, array $options): bool { 284 if ($options['calculated']) { 285 if ($options['aggregating']) { 286 if ($host === CQueryParser::HOST_ITEMKEY_WILDCARD && $item === CQueryParser::HOST_ITEMKEY_WILDCARD) { 287 return false; 288 } 289 } 290 else { 291 if ($filter['match'] !== '') { 292 return false; 293 } 294 295 if ($host === CQueryParser::HOST_ITEMKEY_WILDCARD || $item === CQueryParser::HOST_ITEMKEY_WILDCARD) { 296 return false; 297 } 298 } 299 } 300 301 return true; 302 } 303 304 /** 305 * Validate function's period parameter. 306 * 307 * @param string $sec_num 308 * @param string $time_shift 309 * @param int $mode 310 * @param array $options 311 * 312 * @static 313 * 314 * @return bool 315 */ 316 private static function validatePeriod(string $sec_num, string $time_shift, int $mode, array $options): bool { 317 switch ($mode) { 318 case CHistFunctionData::PERIOD_MODE_DEFAULT: 319 if ($sec_num === '' || self::hasMacros($sec_num, $options)) { 320 return true; 321 } 322 323 $sec = timeUnitToSeconds($sec_num); 324 325 if ($sec !== null) { 326 return ($sec > 0 && $sec <= ZBX_MAX_INT32); 327 } 328 329 if (preg_match('/^#(?<num>\d+)$/', $sec_num, $matches) == 1) { 330 return ($matches['num'] > 0 && $matches['num'] <= ZBX_MAX_INT32); 331 } 332 333 return false; 334 335 case CHistFunctionData::PERIOD_MODE_SEC: 336 case CHistFunctionData::PERIOD_MODE_SEC_ONLY: 337 if ($mode == CHistFunctionData::PERIOD_MODE_SEC_ONLY && $time_shift !== '') { 338 return false; 339 } 340 341 $sec = timeUnitToSeconds($sec_num); 342 343 if ($sec !== null) { 344 return ($sec > 0 && $sec <= ZBX_MAX_INT32); 345 } 346 347 return false; 348 349 case CHistFunctionData::PERIOD_MODE_NUM_ONLY: 350 if (preg_match('/^#(?<num>\d+)$/', $sec_num, $matches) == 1) { 351 return ($matches['num'] > 0 && $matches['num'] <= ZBX_MAX_INT32); 352 } 353 354 return false; 355 356 case CHistFunctionData::PERIOD_MODE_TREND: 357 if ($time_shift === '') { 358 return false; 359 } 360 361 if (self::hasMacros($sec_num, $options)) { 362 return true; 363 } 364 365 $sec = timeUnitToSeconds($sec_num, true); 366 367 if ($sec !== null) { 368 return ($sec > 0 && $sec <= ZBX_MAX_INT32 && $sec % SEC_PER_HOUR == 0); 369 } 370 371 return false; 372 373 default: 374 return false; 375 } 376 377 return false; 378 } 379} 380