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
21class CConditionFormula {
22
23	// possible parsing states
24	const STATE_AFTER_OPEN_BRACE = 0;
25	const STATE_AFTER_OPERATOR = 1;
26	const STATE_AFTER_CLOSE_BRACE = 2;
27	const STATE_AFTER_CONSTANT = 3;
28	const STATE_AFTER_NOT_OPERATOR = 4;
29
30	/**
31	 * Set to true of the formula is valid.
32	 *
33	 * @var bool
34	 */
35	public $isValid;
36
37	/**
38	 * Error message if the formula is invalid.
39	 *
40	 * @var string
41	 */
42	public $error;
43
44	/**
45	 * The parsed formula.
46	 *
47	 * @var string
48	 */
49	public $formula;
50
51	/**
52	 * Array of unique constants used in the formula.
53	 *
54	 * @var array
55	 */
56	public $constants = [];
57
58	/**
59	 * Array of supported operators.
60	 *
61	 * @var array
62	 */
63	protected $allowedOperators = ['and', 'or'];
64
65	/**
66	 * Current position on a parsed element.
67	 *
68	 * @var integer
69	 */
70	private $pos;
71
72	/**
73	 * Parses the given condition formula.
74	 *
75	 * @param string $formula
76	 *
77	 * @return bool		true if the formula is valid
78	 */
79	public function parse($formula) {
80		$this->isValid = true;
81		$this->error = '';
82		$this->constants = [];
83
84		$this->pos = 0;
85		$this->formula = $formula;
86
87		$state = self::STATE_AFTER_OPEN_BRACE;
88		$afterSpace = false;
89		$level = 0;
90
91		while (isset($this->formula[$this->pos])) {
92			if ($this->formula[$this->pos] === ' ') {
93				$afterSpace = true;
94				$this->pos++;
95
96				continue;
97			}
98
99			switch ($state) {
100				case self::STATE_AFTER_OPEN_BRACE:
101					switch ($this->formula[$this->pos]) {
102						case '(':
103							$state = self::STATE_AFTER_OPEN_BRACE;
104							$level++;
105							break;
106
107						default:
108							if ($this->parseConstant()) {
109								$state = self::STATE_AFTER_CONSTANT;
110							}
111							elseif ($this->parseNotOperator()) {
112								$state = self::STATE_AFTER_NOT_OPERATOR;
113							}
114							else {
115								break 3;
116							}
117					}
118					break;
119
120				case self::STATE_AFTER_OPERATOR:
121					switch ($this->formula[$this->pos]) {
122						case '(':
123							$state = self::STATE_AFTER_OPEN_BRACE;
124							$level++;
125							break;
126
127						default:
128							if (!$afterSpace) {
129								break 3;
130							}
131
132							if ($this->parseConstant()) {
133								$state = self::STATE_AFTER_CONSTANT;
134							}
135							elseif ($this->parseNotOperator()) {
136								$state = self::STATE_AFTER_NOT_OPERATOR;
137							}
138							else {
139								break 3;
140							}
141					}
142					break;
143
144				case self::STATE_AFTER_NOT_OPERATOR:
145					switch ($this->formula[$this->pos]) {
146						case '(':
147							$state = self::STATE_AFTER_OPEN_BRACE;
148							$level++;
149							break;
150
151						default:
152							if (!$afterSpace) {
153								break 3;
154							}
155
156							if ($this->parseConstant()) {
157								$state = self::STATE_AFTER_CONSTANT;
158							}
159							else {
160								break 3;
161							}
162					}
163					break;
164
165				case self::STATE_AFTER_CLOSE_BRACE:
166					switch ($this->formula[$this->pos]) {
167						case ')':
168							if ($level == 0) {
169								break 3;
170							}
171							$level--;
172							break;
173
174						default:
175							if ($this->parseOperator()) {
176								$state = self::STATE_AFTER_OPERATOR;
177							}
178							else {
179								break 3;
180							}
181					}
182					break;
183
184				case self::STATE_AFTER_CONSTANT:
185					switch ($this->formula[$this->pos]) {
186						case ')':
187							$state = self::STATE_AFTER_CLOSE_BRACE;
188							if ($level == 0) {
189								break 3;
190							}
191							$level--;
192							break;
193
194						default:
195							if (!$afterSpace) {
196								break 3;
197							}
198
199							if ($this->parseOperator()) {
200								$state = self::STATE_AFTER_OPERATOR;
201							}
202							else {
203								break 3;
204							}
205					}
206					break;
207			}
208
209			$afterSpace = false;
210			$this->pos++;
211		}
212
213		if ($this->pos == 0) {
214			$this->error = _('expression is empty');
215			$this->isValid = false;
216		}
217
218		if ($level != 0 || isset($this->formula[$this->pos]) || $state == self::STATE_AFTER_OPERATOR) {
219			$this->error = _s('check expression starting from "%1$s"',
220				substr($this->formula, $this->pos == 0 ? 0 : $this->pos - 1)
221			);
222			$this->isValid = false;
223		}
224
225		return $this->isValid;
226	}
227
228	/**
229	 * Parses a constant and advances the position to its last character.
230	 *
231	 * @return bool
232	 */
233	protected function parseConstant() {
234		$start = $this->pos;
235
236		while (isset($this->formula[$this->pos]) && $this->isConstantChar($this->formula[$this->pos])) {
237			$this->pos++;
238		}
239
240		// empty constant
241		if ($start == $this->pos) {
242			return false;
243		}
244
245		$constant = substr($this->formula, $start, $this->pos - $start);
246		$this->constants[] = [
247			'value' => $constant,
248			'pos' => $start
249		];
250
251		$this->pos--;
252
253		return true;
254	}
255
256	/**
257	 * Parses a keyword and advances the position to its last character.
258	 *
259	 * @return bool
260	 */
261	protected function parseNotOperator() {
262		if (substr($this->formula, $this->pos, 3) !== 'not') {
263			return false;
264		}
265
266		$this->pos += 2;
267
268		return true;
269	}
270
271	/**
272	 * Parses an operator and advances the position to its last character.
273	 *
274	 * @return bool
275	 */
276	protected function parseOperator() {
277		$start = $this->pos;
278
279		while (isset($this->formula[$this->pos]) && $this->isOperatorChar($this->formula[$this->pos])) {
280			$this->pos++;
281		}
282
283		// empty operator
284		if ($start == $this->pos) {
285			return false;
286		}
287
288		$operator = substr($this->formula, $start, $this->pos - $start);
289
290		// check if this is a valid operator
291		if (!in_array($operator, $this->allowedOperators)) {
292			$this->pos = $start;
293			return false;
294		}
295
296		$this->pos--;
297
298		return true;
299	}
300
301	/**
302	 * Returns true if the given character is a valid constant character.
303	 *
304	 * @param string $c
305	 *
306	 * @return bool
307	 */
308	protected function isConstantChar($c) {
309		return ($c >= 'A' && $c <= 'Z');
310	}
311
312	/**
313	 * Returns true if the given character is a valid operator character.
314	 *
315	 * @param string $c
316	 *
317	 * @return bool
318	 */
319	protected function isOperatorChar($c) {
320		return ($c >= 'a' && $c <= 'z');
321	}
322}
323