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 CWidgetFormSvgGraph extends CWidgetForm {
23
24	public function __construct($data) {
25		parent::__construct($data, WIDGET_SVG_GRAPH);
26
27		$this->data = self::convertDottedKeys($this->data);
28
29		// API doesn't guarantee fields to be retrieved in same order as stored.
30		foreach (['or', 'ds'] as $field) {
31			if (array_key_exists($field, $this->data)) {
32				ksort($this->data[$field]);
33			}
34		}
35
36		/**
37		 * Data set tab.
38		 *
39		 * Contains single CWidgetFieldGraphDataSet field for data sets definition and configuration.
40		 */
41		$field_ds = (new CWidgetFieldGraphDataSet('ds', _('Data set')))->setFlags(CWidgetField::FLAG_NOT_EMPTY);
42
43		if (array_key_exists('ds', $this->data)) {
44			$field_ds->setValue($this->data['ds']);
45		}
46
47		$this->fields[$field_ds->getName()] = $field_ds;
48
49		/**
50		 * Display options tab.
51		 *
52		 * Used to select either data are loaded from History or Trends or turning automatic mode on.
53		 */
54		$field_data_source = (new CWidgetFieldRadioButtonList('source', _('History data selection'), [
55			SVG_GRAPH_DATA_SOURCE_AUTO => _x('Auto', 'history source selection method'),
56			SVG_GRAPH_DATA_SOURCE_HISTORY => _('History'),
57			SVG_GRAPH_DATA_SOURCE_TRENDS => _('Trends')
58		]))
59			->setDefault(SVG_GRAPH_DATA_SOURCE_AUTO)
60			->setModern(true);
61
62		if (array_key_exists('source', $this->data)) {
63			$field_data_source->setValue($this->data['source']);
64		}
65
66		$this->fields[$field_data_source->getName()] = $field_data_source;
67
68		/**
69		 * Time period tab.
70		 *
71		 * Contains fields for specifying widget time options.
72		 */
73		// Checkbox to specify either relative dashboard time or widget's own time.
74		$field_graph_time = (new CWidgetFieldCheckBox('graph_time', _('Set custom time period')))
75			->setAction('jQuery("#time_from, #time_to, #time_from_calendar, #time_to_calendar")'.
76				'.prop("disabled", !jQuery(this).is(":checked"));'
77			);
78
79		if (array_key_exists('graph_time', $this->data)) {
80			$field_graph_time->setValue($this->data['graph_time']);
81		}
82
83		$this->fields[$field_graph_time->getName()] = $field_graph_time;
84
85		// Date from.
86		$field_time_from = (new CWidgetFieldDatePicker('time_from', _('From')))->setDefault('now-1h');
87
88		if ($field_graph_time->getValue() != SVG_GRAPH_CUSTOM_TIME) {
89			$field_time_from->setFlags(CWidgetField::FLAG_DISABLED);
90		}
91		elseif (array_key_exists('time_from', $this->data)) {
92			$field_time_from->setValue($this->data['time_from']);
93		}
94
95		$this->fields[$field_time_from->getName()] = $field_time_from;
96
97		// Time to.
98		$field_time_to = (new CWidgetFieldDatePicker('time_to', _('To')))->setDefault('now');
99
100		if ($field_graph_time->getValue() != SVG_GRAPH_CUSTOM_TIME) {
101			$field_time_to->setFlags(CWidgetField::FLAG_DISABLED);
102		}
103		elseif (array_key_exists('time_to', $this->data)) {
104			$field_time_to->setValue($this->data['time_to']);
105		}
106
107		$this->fields[$field_time_to->getName()] = $field_time_to;
108
109		/**
110		 * Axes tab.
111		 *
112		 * Contains fields to specify options for graph axes.
113		 */
114		// Show left Y axis.
115		$field_lefty = (new CWidgetFieldCheckBox('lefty', _('Left Y'), _('Show')))
116			->setDefault(SVG_GRAPH_AXIS_SHOW)
117			->setAction('onLeftYChange()');
118
119		if (array_key_exists('lefty', $this->data)) {
120			$field_lefty->setValue($this->data['lefty']);
121		}
122
123		$this->fields[$field_lefty->getName()] = $field_lefty;
124
125		// Min value on left Y axis.
126		$field_lefty_min = (new CWidgetFieldNumericBox('lefty_min', _('Min')))
127			->setPlaceholder(_('calculated'))
128			->setWidth(ZBX_TEXTAREA_SMALL_WIDTH);
129
130		if ($field_lefty->getValue() != SVG_GRAPH_AXIS_SHOW) {
131			$field_lefty_min->setFlags(CWidgetField::FLAG_DISABLED);
132		}
133		elseif (array_key_exists('lefty_min', $this->data)) {
134			$field_lefty_min->setValue($this->data['lefty_min']);
135		}
136
137		$this->fields[$field_lefty_min->getName()] = $field_lefty_min;
138
139		// Max value on left Y axis.
140		$field_lefty_max = (new CWidgetFieldNumericBox('lefty_max', _('Max')))
141			->setPlaceholder(_('calculated'))
142			->setWidth(ZBX_TEXTAREA_SMALL_WIDTH);
143
144		if ($field_lefty->getValue() != SVG_GRAPH_AXIS_SHOW) {
145			$field_lefty_max->setFlags(CWidgetField::FLAG_DISABLED);
146		}
147		elseif (array_key_exists('lefty_max', $this->data)) {
148			$field_lefty_max->setValue($this->data['lefty_max']);
149		}
150
151		$this->fields[$field_lefty_max->getName()] = $field_lefty_max;
152
153		// Specify the type of units on left Y axis.
154		$field_lefty_units = (new CWidgetFieldComboBox('lefty_units', _('Units'), [
155			SVG_GRAPH_AXIS_UNITS_AUTO => _x('Auto', 'history source selection method'),
156			SVG_GRAPH_AXIS_UNITS_STATIC => _x('Static', 'history source selection method')
157		]))
158			->setDefault(SVG_GRAPH_AXIS_UNITS_AUTO)
159			->setAction('jQuery("#lefty_static_units")'.
160							'.prop("disabled", (jQuery(this).val() != "'.SVG_GRAPH_AXIS_UNITS_STATIC.'"))'
161			);
162
163		if ($field_lefty->getValue() != SVG_GRAPH_AXIS_SHOW) {
164			$field_lefty_units->setFlags(CWidgetField::FLAG_DISABLED);
165		}
166		elseif (array_key_exists('lefty_units', $this->data)) {
167			$field_lefty_units->setValue($this->data['lefty_units']);
168		}
169
170		$this->fields[$field_lefty_units->getName()] = $field_lefty_units;
171
172		// Static units on left Y axis.
173		$field_lefty_static_units = (new CWidgetFieldTextBox('lefty_static_units', null))
174			->setPlaceholder(_('value'))
175			->setWidth(ZBX_TEXTAREA_TINY_WIDTH);
176
177		if ($field_lefty->getValue() != SVG_GRAPH_AXIS_SHOW
178				|| $field_lefty_units->getValue() != SVG_GRAPH_AXIS_UNITS_STATIC) {
179			$field_lefty_static_units->setFlags(CWidgetField::FLAG_DISABLED);
180		}
181		elseif (array_key_exists('lefty_static_units', $this->data)) {
182			$field_lefty_static_units->setValue($this->data['lefty_static_units']);
183		}
184
185		$this->fields[$field_lefty_static_units->getName()] = $field_lefty_static_units;
186
187		// Show right Y axis.
188		$field_righty = (new CWidgetFieldCheckBox('righty', _('Right Y'), _('Show')))
189			->setDefault(SVG_GRAPH_AXIS_SHOW)
190			->setAction('onRightYChange()');
191
192		if (array_key_exists('righty', $this->data)) {
193			$field_righty->setValue($this->data['righty']);
194		}
195
196		$this->fields[$field_righty->getName()] = $field_righty;
197
198		// Min value on right Y axis.
199		$field_righty_min = (new CWidgetFieldNumericBox('righty_min', _('Min')))
200			->setPlaceholder(_('calculated'))
201			->setWidth(ZBX_TEXTAREA_SMALL_WIDTH);
202
203		if ($field_righty->getValue() != SVG_GRAPH_AXIS_SHOW) {
204			$field_righty_min->setFlags(CWidgetField::FLAG_DISABLED);
205		}
206		elseif (array_key_exists('righty_min', $this->data)) {
207			$field_righty_min->setValue($this->data['righty_min']);
208		}
209
210		$this->fields[$field_righty_min->getName()] = $field_righty_min;
211
212		// Max value on right Y axis.
213		$field_righty_max = (new CWidgetFieldNumericBox('righty_max', _('Max')))
214			->setPlaceholder(_('calculated'))
215			->setWidth(ZBX_TEXTAREA_SMALL_WIDTH);
216
217		if ($field_righty->getValue() != SVG_GRAPH_AXIS_SHOW) {
218			$field_righty_max->setFlags(CWidgetField::FLAG_DISABLED);
219		}
220		elseif (array_key_exists('righty_max', $this->data)) {
221			$field_righty_max->setValue($this->data['righty_max']);
222		}
223
224		$this->fields[$field_righty_max->getName()] = $field_righty_max;
225
226		// Specify the type of units on right Y axis.
227		$field_righty_units = (new CWidgetFieldComboBox('righty_units', _('Units'), [
228			SVG_GRAPH_AXIS_UNITS_AUTO => _x('Auto', 'history source selection method'),
229			SVG_GRAPH_AXIS_UNITS_STATIC => _x('Static', 'history source selection method')
230		]))
231			->setDefault(SVG_GRAPH_AXIS_UNITS_AUTO)
232			->setAction('jQuery("#righty_static_units")'.
233							'.prop("disabled", (jQuery(this).val() != "'.SVG_GRAPH_AXIS_UNITS_STATIC.'"))'
234			);
235
236		if ($field_righty->getValue() != SVG_GRAPH_AXIS_SHOW) {
237			$field_righty_units->setFlags(CWidgetField::FLAG_DISABLED);
238		}
239		elseif (array_key_exists('righty_units', $this->data)) {
240			$field_righty_units->setValue($this->data['righty_units']);
241		}
242
243		$this->fields[$field_righty_units->getName()] = $field_righty_units;
244
245		// Static units on right Y axis.
246		$field_righty_static_units = (new CWidgetFieldTextBox('righty_static_units', null))
247			->setPlaceholder(_('value'))
248			->setWidth(ZBX_TEXTAREA_TINY_WIDTH);
249
250		if ($field_righty->getValue() != SVG_GRAPH_AXIS_SHOW
251				|| $field_righty_units->getValue() != SVG_GRAPH_AXIS_UNITS_STATIC) {
252			$field_righty_static_units->setFlags(CWidgetField::FLAG_DISABLED);
253		}
254		elseif (array_key_exists('righty_static_units', $this->data)) {
255			$field_righty_static_units->setValue($this->data['righty_static_units']);
256		}
257
258		$this->fields[$field_righty_static_units->getName()] = $field_righty_static_units;
259
260		// Show X axis.
261		$field_axisx = (new CWidgetFieldCheckBox('axisx', _('X-Axis'), _('Show')))->setDefault(SVG_GRAPH_AXIS_SHOW);
262
263		if (array_key_exists('axisx', $this->data)) {
264			$field_axisx->setValue($this->data['axisx']);
265		}
266
267		$this->fields[$field_axisx->getName()] = $field_axisx;
268
269		/**
270		 * Legend tab.
271		 *
272		 * Contains check-box field to show/hide legend and field to specify number of lines in which legend is shown.
273		 */
274		// Show legend.
275		$field_legend = (new CWidgetFieldCheckBox('legend', _('Show legend')))
276			->setAction('jQuery("[name=legend_lines]").rangeControl('.
277				'jQuery(this).is(":checked") ? "enable" : "disable"'.
278			');')
279			->setDefault(SVG_GRAPH_LEGEND_TYPE_SHORT);
280
281		if (array_key_exists('legend', $this->data)) {
282			$field_legend->setValue($this->data['legend']);
283		}
284
285		$this->fields[$field_legend->getName()] = $field_legend;
286
287		// Number of lines.
288		$field_legend_lines = (new CWidgetFieldRangeControl('legend_lines', _('Number of rows'),
289			SVG_GRAPH_LEGEND_LINES_MIN, SVG_GRAPH_LEGEND_LINES_MAX
290		))
291			->setDefault(SVG_GRAPH_LEGEND_LINES_MIN);
292
293		if ($field_legend->getValue() == SVG_GRAPH_LEGEND_TYPE_NONE) {
294			$field_legend_lines->setFlags(CWidgetField::FLAG_DISABLED);
295		}
296		if (array_key_exists('legend_lines', $this->data)) {
297			$field_legend_lines->setValue($this->data['legend_lines']);
298		}
299
300		$this->fields[$field_legend_lines->getName()] = $field_legend_lines;
301
302		/**
303		 * Problems tab.
304		 *
305		 * Contains fields to configure highlighted problem areas in graph.
306		 */
307		// Checkbox: Selected items only.
308		$field_show_problems = (new CWidgetFieldCheckBox('show_problems', _('Show problems')))
309			->setAction(
310				'var on = jQuery(this).is(":checked");'.
311				'jQuery("#graph_item_problems, #problemhosts, #problem_name, #problemhosts_select")'.
312					'.prop("disabled", !on);'.
313				'jQuery("[name=\"severities[]\"]").prop("disabled", !on);'.
314				'jQuery("[name=\"evaltype\"]").prop("disabled", !on);'.
315				'jQuery("input, button", jQuery("#tags_table_tags")).prop("disabled", !on);'
316			);
317
318		if (array_key_exists('show_problems', $this->data)) {
319			$field_show_problems->setValue($this->data['show_problems']);
320		}
321
322		$this->fields[$field_show_problems->getName()] = $field_show_problems;
323
324		// Checkbox: Selected items only.
325		$field_problems = (new CWidgetFieldCheckBox('graph_item_problems', _('Selected items only')))
326			->setDefault(SVG_GRAPH_SELECTED_ITEM_PROBLEMS);
327
328		if ($field_show_problems->getValue() != SVG_GRAPH_PROBLEMS_SHOW) {
329			$field_problems->setFlags(CWidgetField::FLAG_DISABLED);
330		}
331		elseif (array_key_exists('graph_item_problems', $this->data)) {
332			$field_problems->setValue($this->data['graph_item_problems']);
333		}
334
335		$this->fields[$field_problems->getName()] = $field_problems;
336
337		// Problem hosts.
338		$field_problemhosts = (new CWidgetFieldTextArea('problemhosts', _('Problem hosts')))
339			->setPlaceholder(_('host pattern'));
340
341		if ($field_show_problems->getValue() != SVG_GRAPH_PROBLEMS_SHOW) {
342			$field_problemhosts->setFlags(CWidgetField::FLAG_DISABLED);
343		}
344		elseif (array_key_exists('problemhosts', $this->data)) {
345			$field_problemhosts->setValue($this->data['problemhosts']);
346		}
347
348		$this->fields[$field_problemhosts->getName()] = $field_problemhosts;
349
350		// Severity checkboxes list.
351		$field_severities = (new CWidgetFieldSeverities('severities', _('Severity')))
352			->setOrientation(CWidgetFieldSeverities::ORIENTATION_HORIZONTAL);
353
354		if ($field_show_problems->getValue() != SVG_GRAPH_PROBLEMS_SHOW) {
355			$field_severities->setFlags(CWidgetField::FLAG_DISABLED);
356		}
357		elseif (array_key_exists('severities', $this->data)) {
358			$field_severities->setValue($this->data['severities']);
359		}
360
361		$this->fields[$field_severities->getName()] = $field_severities;
362
363		// Problem name input-text field.
364		$field_problem_name = (new CWidgetFieldTextBox('problem_name', _('Problem')))
365			->setPlaceholder(_('problem pattern'));
366
367		if ($field_show_problems->getValue() != SVG_GRAPH_PROBLEMS_SHOW) {
368			$field_problem_name->setFlags(CWidgetField::FLAG_DISABLED);
369		}
370		elseif (array_key_exists('problem_name', $this->data)) {
371			$field_problem_name->setValue($this->data['problem_name']);
372		}
373
374		$this->fields[$field_problem_name->getName()] = $field_problem_name;
375
376		// Problem tag evalype (And/Or).
377		$field_evaltype = (new CWidgetFieldRadioButtonList('evaltype', _('Tags'), [
378			TAG_EVAL_TYPE_AND_OR => _('And/Or'),
379			TAG_EVAL_TYPE_OR => _('Or')
380		]))
381			->setDefault(TAG_EVAL_TYPE_AND_OR)
382			->setModern(true);
383
384		if ($field_show_problems->getValue() != SVG_GRAPH_PROBLEMS_SHOW) {
385			$field_evaltype->setFlags(CWidgetField::FLAG_DISABLED);
386		}
387		elseif (array_key_exists('evaltype', $this->data)) {
388			$field_evaltype->setValue($this->data['evaltype']);
389		}
390
391		$this->fields[$field_evaltype->getName()] = $field_evaltype;
392
393		// Problem tags field.
394		$field_tags = new CWidgetFieldTags('tags', '');
395
396		if ($field_show_problems->getValue() != SVG_GRAPH_PROBLEMS_SHOW) {
397			$field_tags->setFlags(CWidgetField::FLAG_DISABLED);
398		}
399		elseif (array_key_exists('tags', $this->data)) {
400			$field_tags->setValue($this->data['tags']);
401		}
402
403		$this->fields[$field_tags->getName()] = $field_tags;
404
405		/**
406		 * Overrides tab.
407		 *
408		 * Contains single field for override configuration.
409		 */
410		$field_or = (new CWidgetFieldGraphOverride('or', _('Overrides')))->setFlags(CWidgetField::FLAG_NOT_EMPTY);
411
412		if (array_key_exists('or', $this->data)) {
413			$field_or->setValue($this->data['or']);
414		}
415
416		$this->fields[$field_or->getName()] = $field_or;
417	}
418
419	/**
420	 * Validate "from" and "to" parameters for allowed period.
421	 *
422	 * @param string $from
423	 * @param string $to
424	 *
425	 * @return array
426	 */
427	private static function validateTimeSelectorPeriod($from, $to) {
428		$errors = [];
429		$ts = [];
430		$range_time_parser = new CRangeTimeParser();
431
432		foreach (['from' => $from, 'to' => $to] as $field => $value) {
433			$range_time_parser->parse($value);
434			$ts[$field] = $range_time_parser->getDateTime($field === 'from')->getTimestamp();
435		}
436
437		$period = $ts['to'] - $ts['from'] + 1;
438
439		if ($period < ZBX_MIN_PERIOD) {
440			$errors[] = _n('Minimum time period to display is %1$s minute.',
441				'Minimum time period to display is %1$s minutes.', (int) (ZBX_MIN_PERIOD / SEC_PER_MIN)
442			);
443		}
444		elseif ($period > ZBX_MAX_PERIOD) {
445			$errors[] = _n('Maximum time period to display is %1$s day.',
446				'Maximum time period to display is %1$s days.', (int) (ZBX_MAX_PERIOD / SEC_PER_DAY)
447			);
448		}
449
450		return $errors;
451	}
452
453	/**
454	 * Validate form fields.
455	 *
456	 * @param bool $strict  Enables more strict validation of the form fields.
457	 *                      Must be enabled for validation of input parameters in the widget configuration form.
458	 *
459	 * @return bool
460	 */
461	public function validate($strict = false) {
462		$errors = parent::validate($strict);
463
464		// Test graph custom time period.
465		if ($this->fields['graph_time']->getValue() == SVG_GRAPH_CUSTOM_TIME) {
466			$errors = array_merge($errors, self::validateTimeSelectorPeriod($this->fields['time_from']->getValue(),
467				$this->fields['time_to']->getValue()
468			));
469		}
470
471		// Validate Min/Max values in Axes tab.
472		if ($this->fields['lefty']->getValue() == SVG_GRAPH_AXIS_SHOW) {
473			$lefty_min = $this->fields['lefty_min']->getValue();
474			$lefty_max = $this->fields['lefty_max']->getValue();
475			$lefty_min = ($lefty_min !== '') ? convertFunctionValue($lefty_min, ZBX_UNITS_ROUNDOFF_LOWER_LIMIT) : '';
476			$lefty_max = ($lefty_max !== '') ? convertFunctionValue($lefty_max, ZBX_UNITS_ROUNDOFF_LOWER_LIMIT) : '';
477			$compare = true;
478
479			if (strlen(substr(strrchr($lefty_min, '.'), 1)) > 4) {
480				$errors[] = _s('Invalid parameter "%1$s": %2$s.', _('Min'), _('too many decimal places'));
481				$compare = false;
482			}
483			if (strlen(substr(strrchr($lefty_max, '.'), 1))  > 4) {
484				$errors[] = _s('Invalid parameter "%1$s": %2$s.', _('Max'), _('too many decimal places'));
485				$compare = false;
486			}
487
488			if ($compare && $lefty_min !== '' && $lefty_max !== ''
489					&& bccomp($lefty_min, $lefty_max, 4) >= 0) {
490				$errors[] = _s('Invalid parameter "%1$s": %2$s.', _('Max'),
491					_('Y axis MAX value must be greater than Y axis MIN value')
492				);
493			}
494		}
495
496		if ($this->fields['righty']->getValue() == SVG_GRAPH_AXIS_SHOW) {
497			$righty_min = $this->fields['righty_min']->getValue();
498			$righty_max = $this->fields['righty_max']->getValue();
499			$righty_min = ($righty_min != '') ? convertFunctionValue($righty_min, ZBX_UNITS_ROUNDOFF_LOWER_LIMIT) : '';
500			$righty_max = ($righty_max != '') ? convertFunctionValue($righty_max, ZBX_UNITS_ROUNDOFF_LOWER_LIMIT) : '';
501			$compare = true;
502
503			if (strlen(substr(strrchr($righty_min, '.'), 1)) > 4) {
504				$errors[] = _s('Invalid parameter "%1$s": %2$s.', _('Min'), _('too many decimal places'));
505				$compare = false;
506			}
507			if (strlen(substr(strrchr($righty_max, '.'), 1))  > 4) {
508				$errors[] = _s('Invalid parameter "%1$s": %2$s.', _('Max'), _('too many decimal places'));
509				$compare = false;
510			}
511
512			if ($compare && $righty_min !== '' && $righty_max !== ''
513					&& bccomp($righty_min, $righty_max, 4) >= 0) {
514				$errors[] = _s('Invalid parameter "%1$s": %2$s.', _('Max'),
515					_('Y axis MAX value must be greater than Y axis MIN value')
516				);
517			}
518		}
519
520		return $errors;
521	}
522
523	/**
524	 * Check if widget configuration is set to use overridden time.
525	 *
526	 * @param array $fields                Widget configuration fields.
527	 * @param int   $fields['graph_time']  (optional)
528	 *
529	 * @return bool
530	 */
531	public static function hasOverrideTime($fields) {
532		return (array_key_exists('graph_time', $fields) && $fields['graph_time'] == SVG_GRAPH_CUSTOM_TIME);
533	}
534}
535