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 trigger expressions and calculated item formulas.
24 */
25class CExpressionValidator extends CValidator {
26
27	/**
28	 * An options array.
29	 *
30	 * Supported options:
31	 *   'usermacros' => false  Enable user macros usage in function parameters.
32	 *   'lldmacros' => false   Enable low-level discovery macros usage in function parameters.
33	 *   'calculated' => false  Validate expression as part of calculated item formula.
34	 *   'partial' => false     Validate partial expression (relaxed requirements).
35	 *
36	 * @var array
37	 */
38	private $options = [
39		'usermacros' => false,
40		'lldmacros' => false,
41		'calculated' => false,
42		'partial' => false
43	];
44
45	/**
46	 * Provider of information on math functions.
47	 *
48	 * @var CMathFunctionData
49	 */
50	private $math_function_data;
51
52	/**
53	 * Known math functions along with number or range of required parameters.
54	 *
55	 * @var array
56	 */
57	private $math_function_parameters = [];
58
59	/**
60	 * Provider of information on history functions.
61	 *
62	 * @var CHistFunctionData
63	 */
64	private $hist_function_data;
65
66	/**
67	 * Known history functions along with definition of parameters.
68	 *
69	 * @var array
70	 */
71	private $hist_function_parameters = [];
72
73	/**
74	 * @param array $options
75	 */
76	public function __construct(array $options = []) {
77		$this->options = $options + $this->options;
78
79		$this->math_function_data = new CMathFunctionData();
80		$this->math_function_parameters = $this->math_function_data->getParameters();
81
82		$this->hist_function_data = new CHistFunctionData(['calculated' => $this->options['calculated']]);
83		$this->hist_function_parameters = $this->hist_function_data->getParameters();
84	}
85
86	/**
87	 * Validate expression.
88	 *
89	 * @param array $tokens  A hierarchy of tokens of parsed expression.
90	 *
91	 * @return bool
92	 */
93	public function validate($tokens) {
94		if (!$this->validateRecursively($tokens, null)) {
95			return false;
96		}
97
98		if (!$this->options['calculated']) {
99			if (!$this->options['partial'] && !self::hasHistoryFunctions($tokens)) {
100				$this->setError(_('trigger expression must contain at least one /host/key reference'));
101
102				return false;
103			}
104		}
105
106		return true;
107	}
108
109	/**
110	 * Validate expression (recursive helper).
111	 *
112	 * @param array $tokens             A hierarchy of tokens.
113	 * @param array|null $parent_token  Parent token containing the hierarchy of tokens.
114	 *
115	 * @return bool
116	 */
117	private function validateRecursively(array $tokens, ?array $parent_token): bool {
118		foreach ($tokens as $token) {
119			switch ($token['type']) {
120				case CExpressionParserResult::TOKEN_TYPE_MATH_FUNCTION:
121					if (!$this->math_function_data->isKnownFunction($token['data']['function'])
122							&& $this->hist_function_data->isKnownFunction($token['data']['function'])) {
123						$this->setError(_s('incorrect usage of function "%1$s"', $token['data']['function']));
124
125						return false;
126					}
127
128					$math_function_validator = new CMathFunctionValidator([
129						'parameters' => $this->math_function_parameters
130					]);
131
132					if (!$math_function_validator->validate($token)) {
133						$this->setError($math_function_validator->getError());
134
135						return false;
136					}
137
138					foreach ($token['data']['parameters'] as $parameter) {
139						if (!$this->validateRecursively($parameter['data']['tokens'], $token)) {
140							return false;
141						}
142					}
143
144					break;
145
146				case CExpressionParserResult::TOKEN_TYPE_HIST_FUNCTION:
147					if (!$this->hist_function_data->isKnownFunction($token['data']['function'])
148							&& $this->math_function_data->isKnownFunction($token['data']['function'])) {
149						$this->setError(_s('incorrect usage of function "%1$s"', $token['data']['function']));
150
151						return false;
152					}
153
154					$options = [
155						'parameters' => $this->hist_function_parameters,
156						'usermacros' => $this->options['usermacros'],
157						'lldmacros' => $this->options['lldmacros'],
158						'calculated' => $this->options['calculated']
159					];
160
161					if ($this->options['calculated']) {
162						$options['aggregating'] = CHistFunctionData::isAggregating($token['data']['function']);
163					}
164
165					$hist_function_validator = new CHistFunctionValidator($options);
166
167					if (!$hist_function_validator->validate($token)) {
168						$this->setError($hist_function_validator->getError());
169
170						return false;
171					}
172
173					if ($options['calculated'] && $options['aggregating']) {
174						if ($parent_token === null
175								|| $parent_token['type'] != CExpressionParserResult::TOKEN_TYPE_MATH_FUNCTION
176								|| !CMathFunctionData::isAggregating($parent_token['data']['function'])
177								|| count($parent_token['data']['parameters']) != 1
178								|| count($parent_token['data']['parameters'][0]['data']['tokens']) != 1) {
179							$this->setError(_s('incorrect usage of function "%1$s"', $token['data']['function']));
180
181							return false;
182						}
183					}
184
185					break;
186
187				case CExpressionParserResult::TOKEN_TYPE_EXPRESSION:
188					if (!$this->validateRecursively($token['data']['tokens'], null)) {
189						return false;
190					}
191
192					break;
193			}
194		}
195
196		return true;
197	}
198
199	/**
200	 * Check if there are history function tokens within the hierarchy of given tokens.
201	 *
202	 * @param array $tokens
203	 *
204	 * @static
205	 *
206	 * @return bool
207	 */
208	private static function hasHistoryFunctions(array $tokens): bool {
209		foreach ($tokens as $token) {
210			switch ($token['type']) {
211				case CExpressionParserResult::TOKEN_TYPE_MATH_FUNCTION:
212					foreach ($token['data']['parameters'] as $parameter) {
213						if (self::hasHistoryFunctions($parameter['data']['tokens'])) {
214							return true;
215						}
216					}
217
218					break;
219
220				case CExpressionParserResult::TOKEN_TYPE_HIST_FUNCTION:
221					return true;
222
223				case CExpressionParserResult::TOKEN_TYPE_EXPRESSION:
224					return self::hasHistoryFunctions($token['data']['tokens']);
225			}
226		}
227
228		return false;
229	}
230}
231