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