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 item keys. 24 */ 25class CItemKey extends CParser { 26 27 const STATE_NEW = 0; 28 const STATE_END = 1; 29 const STATE_UNQUOTED = 2; 30 const STATE_QUOTED = 3; 31 const STATE_END_OF_PARAMS = 4; 32 33 const PARAM_ARRAY = 0; 34 const PARAM_UNQUOTED = 1; 35 const PARAM_QUOTED = 2; 36 37 private $key = ''; // main part of the key (for 'key[1, 2, 3]' key id would be 'key') 38 private $parameters = []; 39 private $error = ''; 40 41 /** 42 * An options array 43 * 44 * Supported options: 45 * '18_simple_checks' => true with support for old-style simple checks like "ftp,{$PORT}" 46 * 47 * @var array 48 */ 49 private $options = ['18_simple_checks' => false]; 50 51 /** 52 * @param array $options 53 */ 54 public function __construct($options = []) { 55 if (array_key_exists('18_simple_checks', $options)) { 56 $this->options['18_simple_checks'] = $options['18_simple_checks']; 57 } 58 } 59 60 /** 61 * Returns an error message depending on input parameters. 62 * 63 * @param string $key 64 * @param int $pos 65 * 66 * @return string 67 */ 68 private function errorMessage($key, $pos) { 69 for ($i = $pos, $chunk = '', $maxChunkSize = 50; isset($key[$i]); $i++) { 70 if (0x80 != (0xc0 & ord($key[$i])) && $maxChunkSize-- == 0) { 71 break; 72 } 73 $chunk .= $key[$i]; 74 } 75 76 if (isset($key[$i])) { 77 $chunk .= ' ...'; 78 } 79 80 return _s('incorrect syntax near "%1$s"', $chunk); 81 } 82 83 /** 84 * Check if given character is a valid key id char 85 * this function is a copy of is_key_char() from /src/libs/zbxcommon/misc.c 86 * don't forget to take look in there before changing anything. 87 * 88 * @param string $char 89 * @return bool 90 */ 91 function isKeyChar($char) { 92 return ( 93 ($char >= 'a' && $char <= 'z') 94 || $char == '.' || $char == '_' || $char == '-' 95 || ($char >= 'A' && $char <= 'Z') 96 || ($char >= '0' && $char <= '9') 97 ); 98 } 99 100 /** 101 * Parse key and parameters and put them into $this->parameters array. 102 * 103 * @param string $data 104 * @param int $offset 105 */ 106 public function parse($data, $offset = 0) { 107 $this->length = 0; 108 $this->match = ''; 109 $this->key = ''; 110 $this->parameters = []; 111 112 for ($p = $offset; isset($data[$p]) && $this->isKeyChar($data[$p]); $p++) { 113 // Code is not missing here. 114 } 115 116 // is key empty? 117 if ($p == $offset) { 118 $this->error = isset($data[$p]) 119 ? $this->errorMessage(substr($data, $offset), 0) 120 : _('key is empty'); 121 122 return self::PARSE_FAIL; 123 } 124 125 $_18_simple_check = false; 126 127 // old-style simple checks 128 if ($this->options['18_simple_checks'] && isset($data[$p]) && $data[$p] === ',') { 129 $p++; 130 131 $user_macro_parser = new CUserMacroParser(); 132 133 if ($user_macro_parser->parse($data, $p) != CParser::PARSE_FAIL) { 134 $p += $user_macro_parser->getLength(); 135 } 136 // numeric parameter or empty parameter 137 else { 138 for (; isset($data[$p]) && $data[$p] > '0' && $data[$p] < '9'; $p++) { 139 // Code is not missing here. 140 } 141 } 142 143 $_18_simple_check = true; 144 } 145 146 $this->key = substr($data, $offset, $p - $offset); 147 $p2 = $p; 148 149 if (!$_18_simple_check) { 150 // Zapcat compatibility. 151 for (; isset($data[$p2]) && $data[$p2] == '['; $p = $p2) { 152 $_parameters = [ 153 'type' => self::PARAM_ARRAY, 154 'raw' => '', 155 'pos' => $p2 - $offset, 156 'parameters' => [] 157 ]; 158 159 if (!$this->parseKeyParameters($data, $p2, $_parameters['parameters'])) { 160 break; 161 } 162 163 $_parameters['raw'] = substr($data, $p, $p2 - $p); 164 165 $this->parameters[] = $_parameters; 166 } 167 } 168 169 $this->length = $p - $offset; 170 $this->match = substr($data, $offset, $this->length); 171 172 if (!isset($data[$p])) { 173 $this->error = ''; 174 return self::PARSE_SUCCESS; 175 } 176 177 $this->error = !isset($data[$p2]) 178 ? _('unexpected end of key') 179 : $this->errorMessage(substr($data, $offset), $p2 - $offset); 180 181 return self::PARSE_SUCCESS_CONT; 182 } 183 184 private function parseKeyParameters($data, &$pos, array &$parameters) { 185 $state = self::STATE_NEW; 186 $num = 0; 187 188 for ($p = $pos + 1; isset($data[$p]); $p++) { 189 switch ($state) { 190 // a new parameter started 191 case self::STATE_NEW: 192 switch ($data[$p]) { 193 case ' ': 194 break; 195 196 case ',': 197 $parameters[$num++] = [ 198 'type' => self::PARAM_UNQUOTED, 199 'raw' => '', 200 'pos' => $p - $pos 201 ]; 202 break; 203 204 case '[': 205 $_p = $p; 206 $_parameters = [ 207 'type' => self::PARAM_ARRAY, 208 'raw' => '', 209 'pos' => $p - $pos, 210 'parameters' => [] 211 ]; 212 213 if (!$this->parseKeyParameters($data, $_p, $_parameters['parameters'])) { 214 break 3; 215 } 216 217 $_parameters['raw'] = substr($data, $p, $_p - $p); 218 $parameters[$num] = $_parameters; 219 220 $p = $_p - 1; 221 $state = self::STATE_END; 222 break; 223 224 case ']': 225 $parameters[$num] = [ 226 'type' => self::PARAM_UNQUOTED, 227 'raw' => '', 228 'pos' => $p - $pos 229 ]; 230 $state = self::STATE_END_OF_PARAMS; 231 break; 232 233 case '"': 234 $parameters[$num] = [ 235 'type' => self::PARAM_QUOTED, 236 'raw' => $data[$p], 237 'pos' => $p - $pos 238 ]; 239 $state = self::STATE_QUOTED; 240 break; 241 242 default: 243 $parameters[$num] = [ 244 'type' => self::PARAM_UNQUOTED, 245 'raw' => $data[$p], 246 'pos' => $p - $pos 247 ]; 248 $state = self::STATE_UNQUOTED; 249 } 250 break; 251 252 // end of parameter 253 case self::STATE_END: 254 switch ($data[$p]) { 255 case ' ': 256 break; 257 258 case ',': 259 $state = self::STATE_NEW; 260 $num++; 261 break; 262 263 case ']': 264 $state = self::STATE_END_OF_PARAMS; 265 break; 266 267 default: 268 break 3; 269 } 270 break; 271 272 // an unquoted parameter 273 case self::STATE_UNQUOTED: 274 switch ($data[$p]) { 275 case ']': 276 $state = self::STATE_END_OF_PARAMS; 277 break; 278 279 case ',': 280 $state = self::STATE_NEW; 281 $num++; 282 break; 283 284 default: 285 $parameters[$num]['raw'] .= $data[$p]; 286 } 287 break; 288 289 // a quoted parameter 290 case self::STATE_QUOTED: 291 $parameters[$num]['raw'] .= $data[$p]; 292 293 if ($data[$p] == '"' && $data[$p - 1] != '\\') { 294 $state = self::STATE_END; 295 } 296 break; 297 298 // end of parameters 299 case self::STATE_END_OF_PARAMS: 300 break 2; 301 } 302 } 303 304 $pos = $p; 305 306 return ($state == self::STATE_END_OF_PARAMS); 307 } 308 309 /** 310 * Returns the error message if key is invalid. 311 * 312 * @return string 313 */ 314 public function getError() { 315 return $this->error; 316 } 317 318 /** 319 * Returns the left part of key without parameters. 320 * 321 * @return string 322 */ 323 public function getKey() { 324 return $this->key; 325 } 326 327 /** 328 * Returns the list of key parameters. 329 * 330 * @return array 331 */ 332 public function getParamsRaw() { 333 return $this->parameters; 334 } 335 336 /** 337 * Returns the number of key parameters. 338 * 339 * @return int 340 */ 341 public function getParamsNum() { 342 $num = 0; 343 344 foreach ($this->parameters as $parameter) { 345 $num += count($parameter['parameters']); 346 } 347 348 return $num; 349 } 350 351 /* 352 * Unquotes special symbols in item key parameter 353 * 354 * @param string $param 355 * 356 * @return string 357 */ 358 public static function unquoteParam($param) { 359 $unquoted = ''; 360 361 for ($p = 1; isset($param[$p]); $p++) { 362 if ($param[$p] == '\\' && $param[$p + 1] == '"') { 363 continue; 364 } 365 366 $unquoted .= $param[$p]; 367 } 368 369 return substr($unquoted, 0, -1); 370 } 371 372 /* 373 * Quotes special symbols in item key parameter. 374 * 375 * @param string $param Item key parameter. 376 * @param bool $forced true - enclose parameter in " even if it does not contain any special characters. 377 * false - do nothing if the paramter does not contain any special characters. 378 * 379 * @return string|bool false - if parameter ends with backslash (cannot be quoted), string - otherwice. 380 */ 381 public static function quoteParam($param, $forced = false) { 382 if (!$forced) 383 { 384 if ($param === '') { 385 return $param; 386 } 387 388 if (strpos('" ', $param[0]) === false && strpos($param, ',') === false && strpos($param, ']') === false) { 389 return $param; 390 } 391 } 392 393 if ('\\' == substr($param, -1)) { 394 return false; 395 } 396 397 return '"'.str_replace ('"', '\\"', $param).'"'; 398 } 399 400 /** 401 * Returns an unquoted parameter. 402 * 403 * @param int $n the number of the requested parameter 404 * 405 * @return string|null 406 */ 407 public function getParam($n) { 408 $num = 0; 409 410 foreach ($this->parameters as $parameter) { 411 foreach ($parameter['parameters'] as $param) { 412 if ($num++ == $n) { 413 switch ($param['type']) { 414 case self::PARAM_ARRAY: 415 // return parameter without square brackets 416 return substr($param['raw'], 1, strlen($param['raw']) - 2); 417 case self::PARAM_UNQUOTED: 418 // return parameter without any changes 419 return $param['raw']; 420 case self::PARAM_QUOTED: 421 return $this->unquoteParam($param['raw']); 422 } 423 } 424 } 425 } 426 427 return null; 428 } 429} 430