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
22/**
23 * A parser for relative time in "now[/<yMwdhm>][<+->N<yMwdhms>[/<yMwdhm>]]" format.
24 */
25class CRelativeTimeParser extends CParser {
26
27	const ZBX_TOKEN_PRECISION = 0;
28	const ZBX_TOKEN_OFFSET = 1;
29
30	/**
31	 * @var array $tokens  An array of tokens for relative date.
32	 */
33	private $tokens;
34
35	private $user_macro_parser;
36	private $lld_macro_parser;
37	private $lld_macro_function_parser;
38
39	/**
40	 * An options array.
41	 *
42	 * Supported options:
43	 *   'usermacros' => false  Enable user macros usage in the periods.
44	 *   'lldmacros' => false   Enable low-level discovery macros usage in the periods.
45	 *
46	 * @var array
47	 */
48	public $options = [
49		'usermacros' => false,
50		'lldmacros' => false
51	];
52
53	/**
54	 * @param array $options
55	 */
56	public function __construct(array $options = []) {
57		$this->options = $options + $this->options;
58
59		if ($this->options['usermacros']) {
60			$this->user_macro_parser = new CUserMacroParser();
61		}
62		if ($this->options['lldmacros']) {
63			$this->lld_macro_parser = new CLLDMacroParser();
64			$this->lld_macro_function_parser = new CLLDMacroFunctionParser();
65		}
66	}
67
68	/**
69	 * Parse the given period.
70	 *
71	 * @param string $source  Source string that needs to be parsed.
72	 * @param int    $pos     Position offset.
73	 */
74	public function parse($source, $pos = 0) {
75		$this->length = 0;
76		$this->match = '';
77		$this->tokens = [];
78
79		$p = $pos;
80
81		if (!$this->parseRelativeTime($source, $p) && !$this->parseMacros($source, $p)) {
82			return self::PARSE_FAIL;
83		}
84
85		$this->length = $p - $pos;
86		$this->match = substr($source, $pos, $this->length);
87
88		return isset($source[$p]) ? self::PARSE_SUCCESS_CONT : self::PARSE_SUCCESS;
89	}
90
91	/**
92	 * Parse relative time.
93	 *
94	 * @param string	$source
95	 * @param int		$pos
96	 *
97	 * @return bool
98	 */
99	private function parseRelativeTime($source, &$pos) {
100		if (strncmp(substr($source, $pos), 'now', 3) != 0) {
101			return false;
102		}
103
104		$pos += 3;
105
106		while ($this->parsePrecision($source, $pos) || $this->parseOffset($source, $pos)) {
107		}
108
109		return true;
110	}
111
112	/**
113	 * Parse precision.
114	 *
115	 * @param string	$source
116	 * @param int		$pos
117	 *
118	 * @return bool
119	 */
120	private function parsePrecision($source, &$pos) {
121		$p = $pos;
122
123		if (!isset($source[$p]) || $source[$p] !== '/') {
124			return false;
125		}
126
127		$p++;
128
129		if (preg_match('/^[yMwdhm]/', substr($source, $p), $matches)) {
130			$this->tokens[] = [
131				'type' => self::ZBX_TOKEN_PRECISION,
132				'suffix' => $matches[0]
133			];
134
135			$p++;
136		}
137		elseif (!$this->parseMacros($source, $p)) {
138			return false;
139		}
140
141		$pos = $p;
142
143		return true;
144	}
145
146	/**
147	 * Parse offset.
148	 *
149	 * @param string	$source
150	 * @param int		$pos
151	 *
152	 * @return bool
153	 */
154	private function parseOffset($source, &$pos) {
155		$p = $pos;
156
157		if (!preg_match('/^[+-]/', substr($source, $p), $sign_matches)) {
158			return false;
159		}
160
161		$p++;
162
163		if (preg_match('/^(?P<offset_value>[0-9]+)(?P<offset_suffix>[yMwdhms])?/', substr($source, $p), $matches)) {
164			$this->tokens[] = [
165				'type' => self::ZBX_TOKEN_OFFSET,
166				'sign' => $sign_matches[0],
167				'value' => $matches['offset_value'],
168				'suffix' => array_key_exists('offset_suffix', $matches) ? $matches['offset_suffix'] : 's'
169			];
170
171			$p += strlen($matches[0]);
172		}
173		elseif (!$this->parseMacros($source, $p)) {
174			return false;
175		}
176
177		$pos = $p;
178
179		return true;
180	}
181
182	/**
183	 * Parse macros.
184	 *
185	 * @param string	$source
186	 * @param int		$pos
187	 *
188	 * @return bool
189	 */
190	private function parseMacros($source, &$pos) {
191		if ($this->options['usermacros'] && $this->user_macro_parser->parse($source, $pos) !== CParser::PARSE_FAIL) {
192			$pos += $this->user_macro_parser->length;
193		}
194		elseif ($this->options['lldmacros'] && $this->lld_macro_parser->parse($source, $pos) !== CParser::PARSE_FAIL) {
195			$pos += $this->lld_macro_parser->length;
196		}
197		elseif ($this->options['lldmacros']
198				&& $this->lld_macro_function_parser->parse($source, $pos) !== CParser::PARSE_FAIL) {
199			$pos += $this->lld_macro_function_parser->length;
200		}
201		else {
202			return false;
203		}
204
205		return true;
206	}
207	/**
208	 * Returns an array of tokens.
209	 *
210	 * @return array
211	 */
212	public function getTokens() {
213		return $this->tokens;
214	}
215
216	/**
217	 * Timestamp is returned as initialized DateTime object. Returns null when timestamp is not valid.
218	 *
219	 * @param bool   $is_start  If set to true date will be modified to lowest value, example (now/w) will be returned
220	 *                          as Monday of this week. When set to false precisiion will modify date to highest value,
221	 *                          same example will return Sunday of this week.
222	 *
223	 * @return DateTime|null
224	 */
225	public function getDateTime($is_start) {
226		if ($this->match === '') {
227			return null;
228		}
229
230		$date = new DateTime('now');
231
232		foreach ($this->getTokens() as $token) {
233			switch ($token['type']) {
234				case CRelativeTimeParser::ZBX_TOKEN_PRECISION:
235					if ($token['suffix'] === 'm' || $token['suffix'] === 'h' || $token['suffix'] === 'd') {
236						$formats = $is_start
237							? [
238								'd' => 'Y-m-d 00:00:00',
239								'm' => 'Y-m-d H:i:00',
240								'h' => 'Y-m-d H:00:00'
241							]
242							: [
243								'd' => 'Y-m-d 23:59:59',
244								'm' => 'Y-m-d H:i:59',
245								'h' => 'Y-m-d H:59:59'
246							];
247
248						$date = new DateTime($date->format($formats[$token['suffix']]));
249					}
250					else {
251						$modifiers = $is_start
252							? [
253								'w' => 'Monday this week 00:00:00',
254								'M' => 'first day of this month 00:00:00',
255								'y' => 'first day of January this year 00:00:00'
256							]
257							: [
258								'w' => 'Sunday this week 23:59:59',
259								'M' => 'last day of this month 23:59:59',
260								'y' => 'last day of December this year 23:59:59'
261							];
262
263						$date->modify($modifiers[$token['suffix']]);
264					}
265					break;
266
267				case CRelativeTimeParser::ZBX_TOKEN_OFFSET:
268					$units = [
269						's' => 'second',
270						'm' => 'minute',
271						'h' => 'hour',
272						'd' => 'day',
273						'w' => 'week',
274						'M' => 'month',
275						'y' => 'year'
276					];
277
278					$date->modify($token['sign'].$token['value'].' '.$units[$token['suffix']]);
279					break;
280			}
281		}
282
283		return $date;
284	}
285}
286