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