1<?php 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 is used to validate and parse a function. 24 */ 25class CHistFunctionParser extends CParser { 26 27 protected const STATE_NEW = 0; 28 protected const STATE_END = 1; 29 protected const STATE_QUOTED = 3; 30 protected const STATE_END_OF_PARAMS = 4; 31 32 public const PARAM_TYPE_QUERY = 0; 33 public const PARAM_TYPE_PERIOD = 1; 34 public const PARAM_TYPE_QUOTED = 2; 35 public const PARAM_TYPE_UNQUOTED = 3; 36 37 /** 38 * An options array. 39 * 40 * Supported options: 41 * 'usermacros' => false Enable user macros usage in function parameters. 42 * 'lldmacros' => false Enable low-level discovery macros usage in function parameters. 43 * 'host_macro' => false Allow {HOST.HOST} macro as host name part in the query. 44 * 'host_macro_n' => false Allow {HOST.HOST} and {HOST.HOST<1-9>} macros as host name part in the query. 45 * 'empty_host' => false Allow empty hostname in the query string. 46 * 47 * @var array 48 */ 49 private $options = [ 50 'usermacros' => false, 51 'lldmacros' => false, 52 'calculated' => false, 53 'host_macro' => false, 54 'host_macro_n' => false, 55 'empty_host' => false 56 ]; 57 58 private $query_parser; 59 private $period_parser; 60 private $user_macro_parser; 61 private $lld_macro_parser; 62 private $lld_macro_function_parser; 63 private $number_parser; 64 65 /** 66 * Parsed function name. 67 * 68 * @var string 69 */ 70 private $function = ''; 71 72 /** 73 * The list of the parsed function parameters. 74 * 75 * @var array 76 */ 77 private $parameters = []; 78 79 /** 80 * @param array $options 81 */ 82 public function __construct(array $options = []) { 83 $this->options = $options + $this->options; 84 85 $this->query_parser = new CQueryParser([ 86 'usermacros' => $this->options['usermacros'], 87 'lldmacros' => $this->options['lldmacros'], 88 'calculated' => $this->options['calculated'], 89 'host_macro' => $this->options['host_macro'], 90 'host_macro_n' => $this->options['host_macro_n'], 91 'empty_host' => $this->options['empty_host'] 92 ]); 93 $this->period_parser = new CPeriodParser([ 94 'usermacros' => $this->options['usermacros'], 95 'lldmacros' => $this->options['lldmacros'] 96 ]); 97 if ($this->options['usermacros']) { 98 $this->user_macro_parser = new CUserMacroParser(); 99 } 100 if ($this->options['lldmacros']) { 101 $this->lld_macro_parser = new CLLDMacroParser(); 102 $this->lld_macro_function_parser = new CLLDMacroFunctionParser(); 103 } 104 $this->number_parser = new CNumberParser([ 105 'with_minus' => true, 106 'with_suffix' => true 107 ]); 108 } 109 110 /** 111 * Parse a function and parameters and put them into $this->params_raw array. 112 * 113 * @param string $source 114 * @param int $pos 115 */ 116 public function parse($source, $pos = 0): int { 117 $this->length = 0; 118 $this->match = ''; 119 $this->function = ''; 120 121 $p = $pos; 122 123 if (!preg_match('/^([a-z_]+)\(/', substr($source, $p), $matches)) { 124 return self::PARSE_FAIL; 125 } 126 127 $p += strlen($matches[0]); 128 $p2 = $p - 1; 129 130 $parameters = []; 131 if (!$this->parseFunctionParameters($source, $p, $parameters)) { 132 return self::PARSE_FAIL; 133 } 134 135 $params_raw['raw'] = substr($source, $p2, $p - $p2); 136 137 $this->length = $p - $pos; 138 $this->match = substr($source, $pos, $this->length); 139 $this->function = $matches[1]; 140 $this->parameters = $parameters; 141 142 return isset($source[$p]) ? self::PARSE_SUCCESS_CONT : self::PARSE_SUCCESS; 143 } 144 145 /** 146 * @param string $source 147 * @param int $pos 148 * @param array $parameters 149 * 150 * @return bool 151 */ 152 protected function parseFunctionParameters(string $source, int &$pos, array &$parameters): bool { 153 $p = $pos; 154 155 $_parameters = []; 156 $state = self::STATE_NEW; 157 $num = 0; 158 159 // The list of parsers for unquoted parameters. 160 $parsers = [$this->number_parser]; 161 if ($this->options['usermacros']) { 162 $parsers[] = $this->user_macro_parser; 163 } 164 if ($this->options['lldmacros']) { 165 $parsers[] = $this->lld_macro_parser; 166 $parsers[] = $this->lld_macro_function_parser; 167 } 168 169 while (isset($source[$p])) { 170 switch ($state) { 171 // a new parameter started 172 case self::STATE_NEW: 173 if ($source[$p] !== ' ') { 174 if ($num == 0) { 175 if ($this->query_parser->parse($source, $p) != CParser::PARSE_FAIL) { 176 $_parameters[$num] = [ 177 'type' => self::PARAM_TYPE_QUERY, 178 'pos' => $p, 179 'match' => $this->query_parser->getMatch(), 180 'length' => $this->query_parser->getLength(), 181 'data' => [ 182 'host' => $this->query_parser->getHost(), 183 'item' => $this->query_parser->getItem(), 184 'filter' => $this->query_parser->getFilter() 185 ] 186 ]; 187 $p += $this->query_parser->getLength() - 1; 188 $state = self::STATE_END; 189 } 190 else { 191 break 2; 192 } 193 } 194 elseif ($num == 1) { 195 switch ($source[$p]) { 196 case ',': 197 $_parameters[$num++] = [ 198 'type' => self::PARAM_TYPE_UNQUOTED, 199 'pos' => $p, 200 'match' => '', 201 'length' => 0 202 ]; 203 break; 204 205 case ')': 206 $_parameters[$num] = [ 207 'type' => self::PARAM_TYPE_UNQUOTED, 208 'pos' => $p, 209 'match' => '', 210 'length' => 0 211 ]; 212 $state = self::STATE_END_OF_PARAMS; 213 break; 214 215 case '"': 216 $_parameters[$num] = [ 217 'type' => self::PARAM_TYPE_QUOTED, 218 'pos' => $p, 219 'match' => $source[$p], 220 'length' => 1 221 ]; 222 $state = self::STATE_QUOTED; 223 break; 224 225 default: 226 if ($this->period_parser->parse($source, $p) != CParser::PARSE_FAIL) { 227 $_parameters[$num] = [ 228 'type' => self::PARAM_TYPE_PERIOD, 229 'pos' => $p, 230 'match' => $this->period_parser->getMatch(), 231 'length' => $this->period_parser->getLength(), 232 'data' => [ 233 'sec_num' => $this->period_parser->getSecNum(), 234 'time_shift' => $this->period_parser->getTimeshift() 235 ] 236 ]; 237 $p += $this->period_parser->getLength() - 1; 238 $state = self::STATE_END; 239 } 240 else { 241 break 3; 242 } 243 } 244 } 245 else { 246 switch ($source[$p]) { 247 case ',': 248 $_parameters[$num++] = [ 249 'type' => self::PARAM_TYPE_UNQUOTED, 250 'pos' => $p, 251 'match' => '', 252 'length' => 0 253 ]; 254 break; 255 256 case ')': 257 $_parameters[$num] = [ 258 'type' => self::PARAM_TYPE_UNQUOTED, 259 'pos' => $p, 260 'match' => '', 261 'length' => 0 262 ]; 263 $state = self::STATE_END_OF_PARAMS; 264 break; 265 266 case '"': 267 $_parameters[$num] = [ 268 'type' => self::PARAM_TYPE_QUOTED, 269 'pos' => $p, 270 'match' => $source[$p], 271 'length' => 1 272 ]; 273 $state = self::STATE_QUOTED; 274 break; 275 276 default: 277 foreach ($parsers as $parser) { 278 if ($parser->parse($source, $p) != CParser::PARSE_FAIL) { 279 $_parameters[$num] = [ 280 'type' => self::PARAM_TYPE_UNQUOTED, 281 'pos' => $p, 282 'match' => $parser->getMatch(), 283 'length' => $parser->getLength() 284 ]; 285 286 $p += $parser->getLength() - 1; 287 $state = self::STATE_END; 288 } 289 } 290 291 if ($state != self::STATE_END) { 292 break 3; 293 } 294 } 295 } 296 } 297 break; 298 299 // end of parameter 300 case self::STATE_END: 301 switch ($source[$p]) { 302 case ' ': 303 break; 304 305 case ',': 306 $state = self::STATE_NEW; 307 $num++; 308 break; 309 310 case ')': 311 $state = self::STATE_END_OF_PARAMS; 312 break; 313 314 default: 315 break 3; 316 } 317 break; 318 319 // a quoted parameter 320 case self::STATE_QUOTED: 321 $_parameters[$num]['match'] .= $source[$p]; 322 $_parameters[$num]['length']++; 323 324 if ($source[$p] === '"') { 325 $state = self::STATE_END; 326 } 327 elseif ($source[$p] === '\\' && isset($source[$p + 1]) 328 && ($source[$p + 1] === '"' || $source[$p + 1] === '\\')) { 329 $_parameters[$num]['match'] .= $source[$p + 1]; 330 $_parameters[$num]['length']++; 331 $p++; 332 } 333 break; 334 335 // end of parameters 336 case self::STATE_END_OF_PARAMS: 337 break 2; 338 } 339 340 $p++; 341 } 342 343 if ($state == self::STATE_END_OF_PARAMS) { 344 $parameters = $_parameters; 345 $pos = $p; 346 347 return true; 348 } 349 350 return false; 351 } 352 353 /** 354 * Returns the left part of the function without parameters. 355 * 356 * @return string 357 */ 358 public function getFunction(): string { 359 return $this->function; 360 } 361 362 /** 363 * Returns the parameters of the function. 364 * 365 * @return array 366 */ 367 public function getParameters(): array { 368 return $this->parameters; 369 } 370 371 /* 372 * Unquotes special symbols in the parameter. 373 * 374 * @param string $param 375 * 376 * @return string 377 */ 378 public static function unquoteParam(string $param): string { 379 return strtr(substr($param, 1, -1), ['\\"' => '"', '\\\\' => '\\']); 380 } 381 382 /* 383 * @param string $param 384 * 385 * @return string 386 */ 387 public static function quoteParam(string $param): string { 388 return '"'.strtr($param, ['\\' => '\\\\', '"' => '\\"']).'"'; 389 } 390 391 /** 392 * Returns an unquoted parameter. 393 * 394 * @param int $n The number of the requested parameter. 395 * 396 * @return string|null 397 */ 398 public function getParam(int $num): ?string { 399 if (!array_key_exists($num, $this->parameters)) { 400 return null; 401 } 402 403 $param = $this->parameters[$num]; 404 405 return ($param['type'] == self::PARAM_TYPE_QUOTED) ? self::unquoteParam($param['match']) : $param['match']; 406 } 407} 408