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