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