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
22class CUserMacroParser extends CParser {
23
24	const STATE_NEW = 0;
25	const STATE_END = 1;
26	const STATE_UNQUOTED = 2;
27	const STATE_QUOTED = 3;
28	const STATE_END_OF_MACRO = 4;
29	public const REGEX_PREFIX = 'regex:';
30
31	private $macro = '';
32	private $context = null;
33	private $context_quoted = false;
34	private $regex = null;
35
36	public function __construct() {
37		$this->error_msgs['empty'] = _('macro is empty');
38		$this->error_msgs['unexpected_end'] = _('unexpected end of macro');
39	}
40
41	/**
42	 * @inheritDoc
43	 */
44	public function parse($source, $pos = 0) {
45		$this->length = 0;
46		$this->match = '';
47		$this->macro = '';
48		$this->context = null;
49		$this->context_quoted = false;
50		$this->errorClear();
51		$this->regex = null;
52		$has_regex = false;
53
54		$p = $pos;
55
56		if (!isset($source[$p]) || $source[$p] != '{') {
57			$this->errorPos(substr($source, $pos), $p - $pos);
58
59			return self::PARSE_FAIL;
60		}
61		$p++;
62
63		if (!isset($source[$p]) || $source[$p] != '$') {
64			$this->errorPos(substr($source, $pos), $p - $pos);
65
66			return self::PARSE_FAIL;
67		}
68		$p++;
69
70		for (; isset($source[$p]) && $this->isMacroChar($source[$p]); $p++)
71			;
72
73		if ($p == $pos + 2 || !isset($source[$p])) {
74			$this->errorPos(substr($source, $pos), $p - $pos);
75
76			return self::PARSE_FAIL;
77		}
78
79		$this->macro = substr($source, $pos + 2, $p - $pos - 2);
80
81		if ($source[$p] == '}') {
82			$p++;
83			$this->length = $p - $pos;
84			$this->match = substr($source, $pos, $this->length);
85
86			if (isset($source[$p])) {
87				$this->errorPos(substr($source, $pos), $p - $pos);
88
89				return self::PARSE_SUCCESS_CONT;
90			}
91
92			return self::PARSE_SUCCESS;
93		}
94
95		if ($source[$p] != ':') {
96			$this->macro = '';
97			$this->errorPos(substr($source, $pos), $p - $pos);
98
99			return self::PARSE_FAIL;
100		}
101		$p++;
102
103		if (preg_match("/^\s*".self::REGEX_PREFIX."/", substr($source, $p)) === 1) {
104			$has_regex = true;
105			$p += strpos(substr($source, $p), self::REGEX_PREFIX) + strlen(self::REGEX_PREFIX);
106		}
107
108		$this->context = '';
109		$this->context_quoted = false;
110		$state = self::STATE_NEW;
111
112		for (; isset($source[$p]); $p++) {
113			switch ($state) {
114				case self::STATE_NEW:
115					switch ($source[$p]) {
116						case ' ':
117							break;
118
119						case '}':
120							$state = self::STATE_END_OF_MACRO;
121							break;
122
123						case '"':
124							$this->context .= $source[$p];
125							$this->context_quoted = true;
126							$state = self::STATE_QUOTED;
127							break;
128
129						default:
130							$this->context .= $source[$p];
131							$this->context_quoted = false;
132							$state = self::STATE_UNQUOTED;
133							break;
134					}
135					break;
136
137				case self::STATE_QUOTED:
138					$this->context .= $source[$p];
139					if ($source[$p] == '"' && $source[$p - 1] != '\\') {
140						$state = self::STATE_END;
141					}
142					break;
143
144				case self::STATE_UNQUOTED:
145					switch ($source[$p]) {
146						case '}':
147							$state = self::STATE_END_OF_MACRO;
148							break;
149
150						default:
151							$this->context .= $source[$p];
152							break;
153					}
154					break;
155
156				case self::STATE_END:
157					switch ($source[$p]) {
158						case ' ':
159							break;
160
161						case '}':
162							$state = self::STATE_END_OF_MACRO;
163							break;
164
165						default:
166							break 3;
167					}
168					break;
169
170				case self::STATE_END_OF_MACRO:
171					break 2;
172			}
173		}
174
175		if ($state != self::STATE_END_OF_MACRO) {
176			$this->macro = '';
177			$this->context = null;
178			$this->context_quoted = false;
179			$this->errorPos(substr($source, $pos), $p - $pos);
180
181			return self::PARSE_FAIL;
182		}
183
184		if ($has_regex) {
185			$this->regex = $this->context;
186			$this->context = null;
187		}
188
189		$this->length = $p - $pos;
190		$this->match = substr($source, $pos, $this->length);
191
192		if (isset($source[$p])) {
193			$this->errorPos(substr($source, $pos), $p - $pos);
194
195			return self::PARSE_SUCCESS_CONT;
196		}
197
198		return self::PARSE_SUCCESS;
199	}
200
201	/**
202	 * Returns true if the char is allowed in the macro, false otherwise.
203	 *
204	 * @param string $c
205	 *
206	 * @return bool
207	 */
208	private function isMacroChar(string $c): bool {
209		return (($c >= 'A' && $c <= 'Z') || $c == '.' || $c == '_' || ($c >= '0' && $c <= '9'));
210	}
211
212	/*
213	 * Unquotes special symbols in context
214	 *
215	 * @param string $context
216	 *
217	 * @return string
218	 */
219	private function unquoteContext(string $context): string {
220		$unquoted = '';
221
222		for ($p = 1; isset($context[$p]); $p++) {
223			if ('\\' == $context[$p] && '"' == $context[$p + 1]) {
224				continue;
225			}
226
227			$unquoted .= $context[$p];
228		}
229
230		return substr($unquoted, 0, -1);
231	}
232
233	/**
234	 * Returns parsed macro name.
235	 *
236	 * @return string
237	 */
238	public function getMacro(): string {
239		return $this->macro;
240	}
241
242	/**
243	 * Returns parsed macro context.
244	 *
245	 * @return string|null
246	 */
247	public function getContext(): ?string {
248		return ($this->context !== null && $this->context_quoted)
249			? $this->unquoteContext($this->context)
250			: $this->context;
251	}
252
253	/**
254	 * Returns parsed regex string.
255	 *
256	 * @return string|null
257	 */
258	public function getRegex(): ?string {
259		return ($this->regex !== null && $this->context_quoted) ? $this->unquoteContext($this->regex) : $this->regex;
260	}
261}
262