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 CFilter extends CDiv {
23
24	// Filter form object.
25	private $form;
26	// Filter form object name and id attribute.
27	private $name = 'zbx_filter';
28	// Visibility of 'Apply', 'Reset' form buttons. Visibility is set to all tabs.
29	private $show_buttons = true;
30
31	/**
32	 * Filter page URL.
33	 *
34	 * @var object
35	 */
36	private $url;
37
38	// Array of filter tab headers. Every header is mapped to it content via href(header) and id(content) attribute.
39	protected $headers = [];
40	// Array of filter tab content.
41	protected $tabs = [];
42	// jQuery.tabs initialization options.
43	protected $tabs_options = [
44		'collapsible' => true,
45		'active' => false
46	];
47	// Profile data associated with filter object.
48	protected $idx = null;
49	protected $idx2 = 0;
50
51	/**
52	 * List of predefined time ranges.
53	 */
54	protected $time_ranges = [
55		[
56			['now-2d', 'now'],
57			['now-7d', 'now'],
58			['now-30d', 'now'],
59			['now-3M', 'now'],
60			['now-6M', 'now'],
61			['now-1y', 'now'],
62			['now-2y', 'now']
63		],
64		[
65			['now-1d/d', 'now-1d/d'],
66			['now-2d/d', 'now-2d/d'],
67			['now-1w/d', 'now-1w/d'],
68			['now-1w/w', 'now-1w/w'],
69			['now-1M/M', 'now-1M/M'],
70			['now-1y/y', 'now-1y/y']
71		],
72		[
73			['now/d', 'now/d'],
74			['now/d', 'now'],
75			['now/w', 'now/w'],
76			['now/w', 'now'],
77			['now/M', 'now/M'],
78			['now/M', 'now'],
79			['now/y', 'now/y'],
80			['now/y', 'now']
81		],
82		[
83			['now-5m', 'now'],
84			['now-15m', 'now'],
85			['now-30m', 'now'],
86			['now-1h', 'now'],
87			['now-3h', 'now'],
88			['now-6h', 'now'],
89			['now-12h', 'now'],
90			['now-24h', 'now']
91		]
92	];
93
94	public function __construct(CUrl $url) {
95		parent::__construct();
96
97		$this->url = $url;
98
99		$this
100			->setAttribute('data-accessible', 1)
101			->addClass('filter-space')
102			->setId(uniqid('filter_'));
103
104		$this->form = (new CForm('get'))
105			->cleanItems()
106			->setAttribute('name', $this->name);
107	}
108
109	public function getName() {
110		return $this->name;
111	}
112
113	/**
114	 * Add variable to filter form.
115	 *
116	 * @param string $name      Variable name.
117	 * @param string $value     Variable value.
118	 *
119	 * @return CFilter
120	 */
121	public function addVar($name, $value) {
122		$this->form->addVar($name, $value);
123
124		return $this;
125	}
126
127	/**
128	 * Hide filter tab buttons. Should be called before addFilterTab.
129	 */
130	public function hideFilterButtons() {
131		$this->show_buttons = false;
132
133		return $this;
134	}
135
136	/**
137	 * Set profile 'idx' and 'idx2' data.
138	 *
139	 * @param string $idx     Profile 'idx' string.
140	 * @param int    $idx2    Profile 'idx2' identifier, default 0.
141	 *
142	 * @return CFilter
143	 */
144	public function setProfile($idx, $idx2 = 0) {
145		$this->idx = $idx;
146		$this->idx2 = $idx2;
147
148		$this->setAttribute('data-profile-idx', $idx);
149		$this->setAttribute('data-profile-idx2', $idx2);
150
151		return $this;
152	}
153
154	/**
155	 * Adds an item inside the form object.
156	 *
157	 * @param mixed $item  An item to add inside the form object.
158	 *
159	 * @return CFilter
160	 */
161	public function addFormItem($item) {
162		$this->form->addItem($item);
163
164		return $this;
165	}
166
167	/**
168	 * Set active tab.
169	 *
170	 * @param int $tab  1 based index of active tab. If set to 0 all tabs will be collapsed.
171	 *
172	 * @return CFilter
173	 */
174	public function setActiveTab($tab) {
175		$this->tabs_options['active'] = $tab > 0 ? $tab - 1 : false;
176
177		return $this;
178	}
179
180	/**
181	 * Add tab with filter form.
182	 *
183	 * @param string $header    Tab header title string.
184	 * @param array  $columns   Array of filter columns markup.
185	 * @param array  $footer    Additional markup objects for filter tab, default null.
186	 *
187	 * @return CFilter
188	 */
189	public function addFilterTab($header, $columns, $footer = null) {
190		$row = (new CDiv())->addClass(ZBX_STYLE_ROW);
191		$body = [];
192		$anchor = 'tab_'.count($this->tabs);
193
194		foreach ($columns as $column) {
195			$row->addItem((new CDiv($column))->addClass(ZBX_STYLE_CELL));
196		}
197
198		$body[] = (new CDiv())
199			->addClass(ZBX_STYLE_TABLE)
200			->addClass(ZBX_STYLE_FILTER_FORMS)
201			->addItem($row);
202
203		if ($this->show_buttons) {
204			$body[] = (new CDiv())
205				->addClass(ZBX_STYLE_FILTER_FORMS)
206				->addItem(
207					(new CSubmitButton(_('Apply'), 'filter_set', 1))
208						->onClick('javascript: chkbxRange.clearSelectedOnFilterChange();')
209				)
210				->addItem(
211					(new CRedirectButton(_('Reset'),
212						$this->url
213							->setArgument('filter_rst', 1)
214							->getUrl()
215					))
216						->addClass(ZBX_STYLE_BTN_ALT)
217						->onClick('javascript: chkbxRange.clearSelectedOnFilterChange();')
218				);
219		}
220
221		if ($footer !== null) {
222			$body[] = $footer;
223		}
224
225		return $this->addTab(
226			(new CLink($header, '#'.$anchor))->addClass(ZBX_STYLE_FILTER_TRIGGER),
227			(new CDiv($body))
228				->addClass(ZBX_STYLE_FILTER_CONTAINER)
229				->setId($anchor)
230		);
231	}
232
233	/**
234	 * Add time selector specific tab. Should be called before any tab is added. Adds two tabs:
235	 * - time selector range change buttons: back, zoom out, forward.
236	 * - time selector range change form with predefined ranges.
237	 *
238	 * @param string $from    Start date. (can be in relative time format, example: now-1w)
239	 * @param string $to      End date. (can be in relative time format, example: now-1w)
240	 * @param bool   $visible Either to make time selector visible or hidden.
241	 * @param string $format  Date and time format used in CDateSelector.
242	 *
243	 * @return CFilter
244	 */
245	public function addTimeSelector($from, $to, $visible = true, $format = ZBX_FULL_DATE_TIME) {
246		$header = relativeDateToText($from, $to);
247
248		if ($visible) {
249			$this->addTab(new CDiv([
250				(new CSimpleButton())->addClass(ZBX_STYLE_BTN_TIME_LEFT),
251				(new CSimpleButton(_('Zoom out')))->addClass(ZBX_STYLE_BTN_TIME_OUT),
252				(new CSimpleButton())->addClass(ZBX_STYLE_BTN_TIME_RIGHT)
253			]), null);
254
255			$predefined_ranges = [];
256
257			foreach ($this->time_ranges as $column_ranges) {
258				$column = (new CList())->addClass(ZBX_STYLE_TIME_QUICK);
259
260				foreach ($column_ranges as $range) {
261					$label = relativeDateToText($range[0], $range[1]);
262					$is_selected = ($header === $label);
263
264					$column->addItem((new CLink($label))
265						->setAttribute('data-from', $range[0])
266						->setAttribute('data-to', $range[1])
267						->setAttribute('data-label', $label)
268						->addClass($is_selected ? ZBX_STYLE_SELECTED : null)
269					);
270				}
271
272				$predefined_ranges[] = (new CDiv($column))->addClass(ZBX_STYLE_CELL);
273			}
274
275			$anchor = 'tab_'.count($this->tabs);
276
277			$this->addTab(
278				(new CLink($header, '#'.$anchor))->addClass(ZBX_STYLE_BTN_TIME),
279				(new CDiv([
280					(new CDiv([
281						new CList([
282							new CLabel(_('From'), 'from'),
283							(new CDateSelector('from', $from))->setDateFormat($format)
284						]),
285						(new CList([(new CListItem(''))->addClass(ZBX_STYLE_RED)]))
286							->setAttribute('data-error-for', 'from')
287							->addClass(ZBX_STYLE_TIME_INPUT_ERROR)
288							->addStyle('display: none'),
289						new CList([
290							new CLabel(_('To'), 'to'),
291							(new CDateSelector('to', $to))->setDateFormat($format)
292						]),
293						(new CList([(new CListItem(''))->addClass(ZBX_STYLE_RED)]))
294							->setAttribute('data-error-for', 'to')
295							->addClass(ZBX_STYLE_TIME_INPUT_ERROR)
296							->addStyle('display: none'),
297						new CList([
298							new CButton('apply', _('Apply'))
299						])
300					]))->addClass(ZBX_STYLE_TIME_INPUT),
301					(new CDiv($predefined_ranges))->addClass(ZBX_STYLE_TIME_QUICK_RANGE)
302				]))
303					->addClass(ZBX_STYLE_FILTER_CONTAINER)
304					->addClass(ZBX_STYLE_TIME_SELECTION_CONTAINER)
305					->setId($anchor)
306			);
307		}
308		else {
309			$this
310				->setAttribute('data-accessible', 0)
311				->addTab(null, (new CDiv([
312					new CVar('from', $from),
313					new CVar('to', $to)
314				])));
315		}
316
317		return $this;
318	}
319
320	/**
321	 * Add tab.
322	 *
323	 * @param string|CTag $header    Tab header title string or CTag container.
324	 * @param array       $body      Array of body elements.
325	 *
326	 * @return CFilter
327	 */
328	public function addTab($header, $body) {
329		$this->headers[] = $header;
330		$this->tabs[] = $body;
331
332		return $this;
333	}
334
335	/**
336	 * Return javascript code for jquery-ui initialization.
337	 *
338	 * @return string
339	 */
340	private function getJS() {
341		$id = '#'.$this->getId();
342
343		$js = 'jQuery("'.$id.'").tabs('.json_encode($this->tabs_options).').show();';
344
345		// Set the focus to a field with autofocus after the filter becomes visible.
346		$js .= 'jQuery("[autofocus=autofocus]", jQuery("'.$id.'")).filter(":visible").focus();';
347
348		if ($this->idx !== null && $this->idx !== '') {
349			$js .= 'jQuery("'.$id.'").on("tabsactivate", function(e, ui) {'.
350				'var active = ui.newPanel.length ? jQuery(this).tabs("option", "active") + 1 : 0;'.
351				'updateUserProfile("'.$this->idx.'.active", active, []);'.
352
353				'if (active) {'.
354					'jQuery("[autofocus=autofocus]", ui.newPanel).focus();'.
355				'}'.
356			'});';
357		}
358
359		return $js;
360	}
361
362	/**
363	 * Render current CFilter object as HTML string.
364	 *
365	 * @return string
366	 */
367	public function toString($destroy = true) {
368		$headers = (new CList())->addClass(ZBX_STYLE_FILTER_BTN_CONTAINER);
369		$headers_cnt = 0;
370
371		if ($this->tabs_options['active'] !== false
372				&& !array_key_exists($this->tabs_options['active'], $this->headers)) {
373			$this->tabs_options['active'] = 0;
374		}
375
376		foreach ($this->headers as $index => $header) {
377			if ($header) {
378				$headers->addItem($header);
379				$headers_cnt++;
380			}
381
382			if ($this->tabs[$index] !== null && $index !== $this->tabs_options['active']) {
383				$this->tabs[$index]->addStyle('display: none');
384			}
385		}
386
387		$this
388			->addStyle('display:none')
389			->form->addItem($this->tabs);
390
391		if ($headers_cnt) {
392			$this
393				->addItem($headers)
394				->setAttribute('aria-label', _('Filter'));
395		}
396
397		$this->addItem($this->form);
398
399		return parent::toString($destroy).($headers_cnt ? get_js($this->getJS()) : '');
400	}
401}
402