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 * A class for implementing conversions used by the trigger wizard.
24 */
25class CTextTriggerConstructor {
26
27	const EXPRESSION_TYPE_MATCH = 0;
28	const EXPRESSION_TYPE_NO_MATCH = 1;
29
30	/**
31	 * Parser used for parsing trigger expressions.
32	 *
33	 * @var CExpressionParser
34	 */
35	protected $expression_parser;
36
37	/**
38	 * @param CExpressionParser $expression_parser
39	 */
40	public function __construct(CExpressionParser $expression_parser) {
41		$this->expression_parser = $expression_parser;
42	}
43
44	/**
45	 * Create a trigger expression from the given expression parts.
46	 *
47	 * Most of this function was left unchanged to preserve the current behavior of the constructor.
48	 * Feel free to rewrite and correct it if necessary.
49	 *
50	 * @param string    $host                       host name
51	 * @param string    $item_key                    item key
52	 * @param array     $expressions                array of expression parts
53	 * @param string    $expressions[]['value']     expression string
54	 * @param int       $expressions[]['type']      whether the string should match the expression; supported values:
55	 *                                              self::EXPRESSION_TYPE_MATCH and self::EXPRESSION_TYPE_NO_MATCH
56	 *
57	 * @return bool|string
58	 */
59	public function getExpressionFromParts(string $host, string $item_key, array $expressions) {
60		$result = '';
61		$query = '/'.$host.'/'.$item_key;
62
63		if (empty($expressions)) {
64			error(_('Expression cannot be empty'));
65
66			return false;
67		}
68
69		// regexp used to split an expressions into tokens
70		$ZBX_PREG_EXPESSION_FUNC_FORMAT = '^(['.ZBX_PREG_PRINT.']*) (and|or) (not )?(-)? ?[(]*(([a-zA-Z_.\$]{6,7})(\\((['.ZBX_PREG_PRINT.']+?){0,1}\\)))(['.ZBX_PREG_PRINT.']*)$';
71		$functions = ['regexp' => 1, 'iregexp' => 1];
72		$expr_array = [];
73		$cexpor = 0;
74		$startpos = -1;
75
76		foreach ($expressions as $expression) {
77			if ($expression['type'] == self::EXPRESSION_TYPE_MATCH) {
78				if (!empty($result)) {
79					$result .= ' or ';
80				}
81				if ($cexpor == 0) {
82					$startpos = mb_strlen($result);
83				}
84				$cexpor++;
85				$eq_global = '<>0';
86			}
87			else {
88				if (($cexpor > 1) & ($startpos >= 0)) {
89					$head = mb_substr($result, 0, $startpos);
90					$tail = mb_substr($result, $startpos);
91					$result = $head.'('.$tail.')';
92				}
93				$cexpor = 0;
94				$eq_global = '=0';
95				if (!empty($result)) {
96					$result .= ' and ';
97				}
98			}
99
100			$expr = ' and '.$expression['value'];
101
102			// strip extra spaces around "and" and "or" operators
103			$expr = preg_replace('/\s+(and|or)\s+/U', ' $1 ', $expr);
104
105			$expr_array = [];
106			$sub_expr_count = 0;
107			$sub_expr = '';
108			$multi = preg_match('/.+(and|or).+/', $expr);
109
110			// split an expression into separate tokens
111			// start from the first part of the expression, then move to the next one
112			while (preg_match('/'.$ZBX_PREG_EXPESSION_FUNC_FORMAT.'/i', $expr, $arr)) {
113				$arr[6] = strtolower($arr[6]);
114				if (!isset($functions[$arr[6]])) {
115					error(_('Incorrect function is used').'. ['.$expression['value'].']');
116
117					return false;
118				}
119				$expr_array[$sub_expr_count]['eq'] = trim($arr[2]);
120				$expr_array[$sub_expr_count]['not'] = trim($arr[3]);
121				$expr_array[$sub_expr_count]['minus'] = trim($arr[4]);
122				$expr_array[$sub_expr_count]['func'] = $arr[6];
123				$expr_array[$sub_expr_count]['pattern'] = $arr[8];
124
125				$sub_expr_count++;
126				$expr = $arr[1];
127			}
128
129			if (empty($expr_array)) {
130				error(_('Incorrect trigger expression').'. ['.$expression['value'].']');
131
132				return false;
133			}
134
135			$expr_array[$sub_expr_count-1]['eq'] = '';
136
137			$sub_eq = '';
138			if ($multi > 0) {
139				$sub_eq = $eq_global;
140			}
141
142			foreach ($expr_array as $id => $expr) {
143				$eq = ($expr['eq'] === '') ? '' : ' '.$expr['eq'].' ';
144				$not = ($expr['not'] === '') ? '' : $expr['not'].' ';
145				$function = 'find('.$query.',,"'.$expr['func'].'","'.$expr['pattern'].'")';
146				if ($multi > 0) {
147					$sub_expr = $eq.'('.$not.$expr['minus'].$function.')'.$sub_eq.$sub_expr;
148				}
149				else {
150					$sub_expr = $eq.$expr['eq'].$not.$expr['minus'].$function.')'.$sub_eq.$sub_expr;
151				}
152			}
153
154			if ($multi > 0) {
155				$result .= '('.$sub_expr.')';
156			}
157			else {
158				$result .= '(('.$sub_expr.')'.$eq_global.')';
159			}
160		}
161
162		if (($cexpor > 1) & ($startpos >= 0)) {
163			$head = mb_substr($result, 0, $startpos);
164			$tail = mb_substr($result, $startpos);
165			$result = $head.'('.$tail.')';
166		}
167
168		return $result;
169	}
170
171	/**
172	 * Break a trigger expression generated by the constructor.
173	 *
174	 * To be successfully parsed, each item function macro must be wrapped in additional parentheses, for example,
175	 * ((find(item.item,,regex,param))=0)
176	 *
177	 * Most of this function was left unchanged to preserve the current behavior of the constructor.
178	 * Feel free to rewrite and correct it if necessary.
179	 *
180	 * @param string $expression    trigger expression
181	 *
182	 * @return array    an array of expression parts, see self::getExpressionFromParts() for the structure of the part
183	 *                  array
184	 */
185	public function getPartsFromExpression($expression) {
186		// strip extra parentheses
187		$expression = preg_replace('/\(\(\((.+?)\)\) and/i', '(($1) and', $expression);
188		$expression = preg_replace('/\(\(\((.+?)\)\)$/i', '(($1)', $expression);
189
190		$this->expression_parser->parse($expression);
191
192		$expressions = [];
193		$splitTokens = $this->splitTokensByFirstLevel($this->expression_parser->getResult()->getTokens());
194		foreach ($splitTokens as $key => $tokens) {
195			$expr = [];
196
197			// replace whole function macros with their functions
198			foreach ($tokens as $token) {
199				switch ($token['type']) {
200					case CExpressionParserResult::TOKEN_TYPE_OPERATOR:
201						$value = ($token['match'] === 'and' || $token['match'] === 'or' || $token['match'] === 'not')
202							? ' '.$token['match'].' '
203							: $token['match'];
204						break;
205
206					case CExpressionParserResult::TOKEN_TYPE_HIST_FUNCTION:
207						if ($token['data']['function'] === 'find' && count($token['data']['parameters']) == 4) {
208							$function = CExpressionParser::unquoteString($token['data']['parameters'][2]['match']);
209							$pattern = CExpressionParser::unquoteString($token['data']['parameters'][3]['match']);
210							$value = $function.'('.$pattern.')';
211							break;
212						}
213						// break; is not missing here
214
215					default:
216						$value = $token['match'];
217				}
218
219				$expr[] = $value;
220			}
221
222			$expr = implode($expr);
223
224			// trim surrounding parentheses
225			$expr = preg_replace('/^\((.*)\)$/u', '$1', $expr);
226
227			// trim parentheses around item function macros
228			$value = preg_replace('/\((.*)\)(=|<>)0/U', '$1', $expr);
229
230			// trim surrounding parentheses
231			$value = preg_replace('/^\((.*)\)$/u', '$1', $value);
232
233			$expressions[$key]['value'] = trim($value);
234			$expressions[$key]['type'] = (strpos($expr, '<>0', mb_strlen($expr) - 4) === false)
235				? self::EXPRESSION_TYPE_NO_MATCH
236				: self::EXPRESSION_TYPE_MATCH;
237		}
238
239		return $expressions;
240	}
241
242	/**
243	 * Split the trigger expression tokens into separate arrays.
244	 *
245	 * The tokens are split at the first occurrence of the "and" or "or" operators with respect to parentheses.
246	 *
247	 * @param array $tokens     an array of tokens from the CExpressionParserResult
248	 *
249	 * @return array    an array of token arrays grouped by expression
250	 */
251	protected function splitTokensByFirstLevel(array $tokens) {
252		$expressions = [];
253		$currentExpression = [];
254
255		$level = 0;
256		foreach ($tokens as $token) {
257			switch ($token['type']) {
258				case CExpressionParserResult::TOKEN_TYPE_OPERATOR:
259					// look for an "or" or "and" operator on the top parentheses level
260					// if such an expression is found, save all of the tokens before it as a separate expression
261					if ($level == 0 && ($token['match'] === 'or' || $token['match'] === 'and')) {
262						$expressions[] = $currentExpression;
263						$currentExpression = [];
264
265						// continue to the next token
266						continue 2;
267					}
268
269					break;
270				case CExpressionParserResult::TOKEN_TYPE_OPEN_BRACE:
271					$level++;
272
273					break;
274				case CExpressionParserResult::TOKEN_TYPE_CLOSE_BRACE:
275					$level--;
276
277					break;
278			}
279
280			$currentExpression[] = $token;
281		}
282
283		$expressions[] = $currentExpression;
284
285		return $expressions;
286	}
287
288}
289