1<?php declare(strict_types = 1);
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 for validating history functions.
24 */
25class CHistFunctionValidator extends CValidator {
26
27	/**
28	 * An options array.
29	 *
30	 * Supported options:
31	 *   'parameters' => []      Definition of parameters of known history functions.
32	 *   'usermacros' => false   Enable user macros usage in function parameters.
33	 *   'lldmacros' => false    Enable low-level discovery macros usage in function parameters.
34	 *   'calculated' => false   Validate history function as part of calculated item formula.
35	 *   'aggregating' => false  Validate as aggregating history function.
36	 *
37	 * @var array
38	 */
39	private $options = [
40		'parameters' => [],
41		'usermacros' => false,
42		'lldmacros' => false,
43		'calculated' => false,
44		'aggregating' => false
45	];
46
47	/**
48	 * @param array $options
49	 */
50	public function __construct(array $options = []) {
51		$this->options = $options + $this->options;
52	}
53
54	/**
55	 * Validate history function.
56	 *
57	 * @param array $token  A token of CExpressionParserResult::TOKEN_TYPE_HIST_FUNCTION type.
58	 *
59	 * @return bool
60	 */
61	public function validate($token) {
62		$invalid_param_messages = [
63			_('invalid first parameter in function "%1$s"'),
64			_('invalid second parameter in function "%1$s"'),
65			_('invalid third parameter in function "%1$s"'),
66			_('invalid fourth parameter in function "%1$s"'),
67			_('invalid fifth parameter in function "%1$s"')
68		];
69
70		if (!array_key_exists($token['data']['function'], $this->options['parameters'])) {
71			$this->setError(_s('unknown function "%1$s"', $token['data']['function']));
72
73			return false;
74		}
75
76		$params = $token['data']['parameters'];
77		$params_spec = $this->options['parameters'][$token['data']['function']];
78
79		if (count($params) > count($params_spec)) {
80			$this->setError(_s('invalid number of parameters in function "%1$s"', $token['data']['function']));
81
82			return false;
83		}
84
85		foreach ($params_spec as $index => $param_spec) {
86			$required = !array_key_exists('required', $param_spec) || $param_spec['required'];
87
88			if ($index >= count($params)) {
89				if ($required) {
90					$this->setError(
91						_s('mandatory parameter is missing in function "%1$s"', $token['data']['function'])
92					);
93
94					return false;
95				}
96
97				continue;
98			}
99
100			$param = $params[$index];
101
102			if ($param['match'] === '') {
103				if ($required) {
104					$this->setError(_params($invalid_param_messages[$index], [$token['data']['function']]));
105
106					return false;
107				}
108
109				continue;
110			}
111
112			switch ($param['type']) {
113				case CHistFunctionParser::PARAM_TYPE_PERIOD:
114					if (self::hasMacros($param['data']['sec_num'], $this->options)
115							&& $param['data']['time_shift'] === '') {
116						continue 2;
117					}
118					break;
119
120				case CHistFunctionParser::PARAM_TYPE_QUOTED:
121					if (self::hasMacros(CHistFunctionParser::unquoteParam($param['match']), $this->options)) {
122						continue 2;
123					}
124					break;
125
126				case CHistFunctionParser::PARAM_TYPE_UNQUOTED:
127					if (self::hasMacros($param['match'], $this->options)) {
128						continue 2;
129					}
130					break;
131			}
132
133			if (array_key_exists('rules', $param_spec)) {
134				$is_valid = self::validateRules($param, $param_spec['rules'], $this->options);
135
136				if (!$is_valid) {
137					$this->setError(_params($invalid_param_messages[$index], [$token['data']['function']]));
138
139					return false;
140				}
141			}
142		}
143
144		return true;
145	}
146
147	/**
148	 * Loose check if string value contains macros.
149	 *
150	 * @param string $value
151	 * @param array  $options
152	 *
153	 * @static
154	 *
155	 * @return bool
156	 */
157	private static function hasMacros(string $value, array $options): bool {
158		if (!$options['usermacros'] && !$options['lldmacros']) {
159			return false;
160		}
161
162		$macro_parsers = [];
163
164		if ($options['usermacros']) {
165			$macro_parsers[] = new CUserMacroParser();
166		}
167		if ($options['lldmacros']) {
168			$macro_parsers[] = new CLLDMacroParser();
169			$macro_parsers[] = new CLLDMacroFunctionParser();
170		}
171
172		for ($pos = strpos($value, '{'); $pos !== false; $pos = strpos($value, '{', $pos + 1)) {
173			foreach ($macro_parsers as $macro_parser) {
174				if ($macro_parser->parse($value, $pos) != CParser::PARSE_FAIL) {
175					return true;
176				}
177			}
178		}
179
180		return false;
181	}
182
183	/**
184	 * Validate function parameter token's compliance to the rules.
185	 *
186	 * @param array $param    Function parameter token.
187	 * @param array $rules
188	 * @param array $options
189	 *
190	 * @static
191	 *
192	 * @return bool
193	 */
194	private static function validateRules(array $param, array $rules, array $options): bool {
195		$param_match_unquoted = ($param['type'] == CHistFunctionParser::PARAM_TYPE_QUOTED)
196			? CHistFunctionParser::unquoteParam($param['match'])
197			: $param['match'];
198
199		foreach ($rules as $rule) {
200			switch ($rule['type']) {
201				case 'query':
202					if ($param['type'] != CHistFunctionParser::PARAM_TYPE_QUERY) {
203						return false;
204					}
205
206					if (!self::validateQuery($param['data']['host'], $param['data']['item'], $param['data']['filter'],
207							$options)) {
208						return false;
209					}
210
211					break;
212
213				case 'period':
214					if ($param['type'] != CHistFunctionParser::PARAM_TYPE_PERIOD) {
215						return false;
216					}
217
218					if (!self::validatePeriod($param['data']['sec_num'], $param['data']['time_shift'], $rule['mode'],
219							$options)) {
220						return false;
221					}
222
223					break;
224
225				case 'number':
226					$with_suffix = array_key_exists('with_suffix', $rule) && $rule['with_suffix'];
227
228					$parser = new CNumberParser(['with_minus' => true, 'with_suffix' => $with_suffix]);
229
230					if ($parser->parse($param_match_unquoted) != CParser::PARSE_SUCCESS) {
231						return false;
232					}
233
234					$value = $parser->calcValue();
235
236					if ((array_key_exists('min', $rule) && $value < $rule['min'])
237							|| array_key_exists('max', $rule) && $value > $rule['max']) {
238						return false;
239					}
240
241					break;
242
243				case 'regexp':
244					if (preg_match($rule['pattern'], $param_match_unquoted) != 1) {
245						return false;
246					}
247
248					break;
249
250				case 'time':
251					$with_year = array_key_exists('with_year', $rule) && $rule['with_year'];
252					$min = array_key_exists('min', $rule) ? $rule['min'] : ZBX_MIN_INT32;
253					$max = array_key_exists('max', $rule) ? $rule['max'] : ZBX_MAX_INT32;
254
255					$sec = timeUnitToSeconds($param_match_unquoted, $with_year);
256
257					if ($sec === null || $sec < $min || $sec > $max) {
258						return false;
259					}
260
261					break;
262
263				default:
264					return false;
265			}
266		}
267
268		return true;
269	}
270
271	/**
272	 * Validate function's query parameter.
273	 *
274	 * @param string $host
275	 * @param string $item
276	 * @param array  $filter   Filter token.
277	 * @param array  $options
278	 *
279	 * @static
280	 *
281	 * @return bool
282	 */
283	private static function validateQuery(string $host, string $item, array $filter, array $options): bool {
284		if ($options['calculated']) {
285			if ($options['aggregating']) {
286				if ($host === CQueryParser::HOST_ITEMKEY_WILDCARD && $item === CQueryParser::HOST_ITEMKEY_WILDCARD) {
287					return false;
288				}
289			}
290			else {
291				if ($filter['match'] !== '') {
292					return false;
293				}
294
295				if ($host === CQueryParser::HOST_ITEMKEY_WILDCARD || $item === CQueryParser::HOST_ITEMKEY_WILDCARD) {
296					return false;
297				}
298			}
299		}
300
301		return true;
302	}
303
304	/**
305	 * Validate function's period parameter.
306	 *
307	 * @param string $sec_num
308	 * @param string $time_shift
309	 * @param int    $mode
310	 * @param array  $options
311	 *
312	 * @static
313	 *
314	 * @return bool
315	 */
316	private static function validatePeriod(string $sec_num, string $time_shift, int $mode, array $options): bool {
317		switch ($mode) {
318			case CHistFunctionData::PERIOD_MODE_DEFAULT:
319				if ($sec_num === '' || self::hasMacros($sec_num, $options)) {
320					return true;
321				}
322
323				$sec = timeUnitToSeconds($sec_num);
324
325				if ($sec !== null) {
326					return ($sec > 0 && $sec <= ZBX_MAX_INT32);
327				}
328
329				if (preg_match('/^#(?<num>\d+)$/', $sec_num, $matches) == 1) {
330					return ($matches['num'] > 0 && $matches['num'] <= ZBX_MAX_INT32);
331				}
332
333				return false;
334
335			case CHistFunctionData::PERIOD_MODE_SEC:
336			case CHistFunctionData::PERIOD_MODE_SEC_ONLY:
337				if ($mode == CHistFunctionData::PERIOD_MODE_SEC_ONLY && $time_shift !== '') {
338					return false;
339				}
340
341				$sec = timeUnitToSeconds($sec_num);
342
343				if ($sec !== null) {
344					return ($sec > 0 && $sec <= ZBX_MAX_INT32);
345				}
346
347				return false;
348
349			case CHistFunctionData::PERIOD_MODE_NUM_ONLY:
350				if (preg_match('/^#(?<num>\d+)$/', $sec_num, $matches) == 1) {
351					return ($matches['num'] > 0 && $matches['num'] <= ZBX_MAX_INT32);
352				}
353
354				return false;
355
356			case CHistFunctionData::PERIOD_MODE_TREND:
357				if ($time_shift === '') {
358					return false;
359				}
360
361				if (self::hasMacros($sec_num, $options)) {
362					return true;
363				}
364
365				$sec = timeUnitToSeconds($sec_num, true);
366
367				if ($sec !== null) {
368					return ($sec > 0 && $sec <= ZBX_MAX_INT32 && $sec % SEC_PER_HOUR == 0);
369				}
370
371				return false;
372
373			default:
374				return false;
375		}
376
377		return false;
378	}
379}
380