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 CScreenHistory extends CScreenBase {
23
24	/**
25	 * Type of graph to display.
26	 *
27	 * Supported values:
28	 * - GRAPH_TYPE_NORMAL
29	 * - GRAPH_TYPE_STACKED
30	 *
31	 * @var int
32	 */
33	protected $graphType;
34
35	/**
36	 * Search string
37	 *
38	 * @var string
39	 */
40	public $filter;
41
42	/**
43	 * Filter show/hide
44	 *
45	 * @var int
46	 */
47	public $filterTask;
48
49	/**
50	 * Filter highlight color
51	 *
52	 * @var string
53	 */
54	public $markColor;
55
56	/**
57	 * Is plain text displayed
58	 *
59	 * @var boolean
60	 */
61	public $plaintext;
62
63	/**
64	 * Items ids.
65	 *
66	 * @var array
67	 */
68	public $itemids;
69
70	/**
71	 * Graph id.
72	 *
73	 * @var int
74	 */
75	public $graphid = 0;
76
77	/**
78	 * String containing base URL for pager.
79	 *
80	 * @var string
81	 */
82	public $page_file;
83
84	/**
85	 * Init screen data.
86	 *
87	 * @param array		$options
88	 * @param string	$options['filter']
89	 * @param int		$options['filterTask']
90	 * @param int		$options['markColor']
91	 * @param boolean	$options['plaintext']
92	 * @param array		$options['itemids']
93	 * @param array     $options['graphid']     When set defines graph id where item.
94	 * @param string    $options['pageFile']    Current page file, is used for pagination links.
95	 */
96	public function __construct(array $options = []) {
97		parent::__construct($options);
98
99		$this->resourcetype = SCREEN_RESOURCE_HISTORY;
100
101		// mandatory
102		$this->filter = isset($options['filter']) ? $options['filter'] : '';
103		$this->filterTask = isset($options['filter_task']) ? $options['filter_task'] : null;
104		$this->markColor = isset($options['mark_color']) ? $options['mark_color'] : MARK_COLOR_RED;
105		$this->graphType = isset($options['graphtype']) ? $options['graphtype'] : GRAPH_TYPE_NORMAL;
106
107		// optional
108		$this->itemids = array_key_exists('itemids', $options) ?  $options['itemids'] : [];
109		$this->plaintext = isset($options['plaintext']) ? $options['plaintext'] : false;
110		$this->page_file = array_key_exists('pageFile', $options) ? $options['pageFile'] : null;
111
112		if (!$this->itemids && array_key_exists('graphid', $options)) {
113			$itemids = API::Item()->get([
114				'output' => ['itemid'],
115				'graphids' => [$options['graphid']]
116			]);
117			$this->itemids = zbx_objectValues($itemids, 'itemid');
118			$this->graphid = $options['graphid'];
119		}
120	}
121
122	/**
123	 * Process screen.
124	 *
125	 * @return CDiv (screen inside container)
126	 */
127	public function get() {
128		$output = [];
129
130		$items = API::Item()->get([
131			'output' => ['itemid', 'hostid', 'name', 'key_', 'value_type', 'history', 'trends'],
132			'selectHosts' => ['name'],
133			'selectValueMap' => ['mappings'],
134			'itemids' => $this->itemids,
135			'webitems' => true,
136			'preservekeys' => true
137		]);
138
139		if (!$items) {
140			show_error_message(_('No permissions to referred object or it does not exist!'));
141
142			return;
143		}
144
145		$items = CMacrosResolverHelper::resolveItemNames($items);
146
147		$iv_string = [
148			ITEM_VALUE_TYPE_LOG => 1,
149			ITEM_VALUE_TYPE_TEXT => 1
150		];
151
152		if ($this->action == HISTORY_VALUES || $this->action == HISTORY_LATEST) {
153			$options = [
154				'output' => API_OUTPUT_EXTEND,
155				'sortfield' => ['clock'],
156				'sortorder' => ZBX_SORT_DOWN
157			];
158
159			if ($this->action == HISTORY_LATEST) {
160				$options['limit'] = 500;
161			}
162			else {
163				$options += [
164					'time_from' => $this->timeline['from_ts'],
165					'time_till' => $this->timeline['to_ts'],
166					'limit' => CSettingsHelper::get(CSettingsHelper::SEARCH_LIMIT)
167				];
168			}
169
170			$is_many_items = (count($items) > 1);
171			$numeric_items = true;
172
173			foreach ($items as $item) {
174				$numeric_items = ($numeric_items && !array_key_exists($item['value_type'], $iv_string));
175				if (!$numeric_items) {
176					break;
177				}
178			}
179
180			/**
181			 * View type: As plain text.
182			 * Item type: numeric (unsigned, char), float, text, log.
183			 */
184			if ($this->plaintext) {
185				if (!$numeric_items && $this->filter !== ''
186						&& in_array($this->filterTask, [FILTER_TASK_SHOW, FILTER_TASK_HIDE])) {
187					$options['search'] = ['value' => $this->filter];
188
189					if ($this->filterTask == FILTER_TASK_HIDE) {
190						$options['excludeSearch'] = true;
191					}
192				}
193
194				$history_data = [];
195				$items_by_type = [];
196
197				foreach ($items as $item) {
198					$items_by_type[$item['value_type']][] = $item['itemid'];
199				}
200
201				foreach ($items_by_type as $value_type => $itemids) {
202					$options['history'] = $value_type;
203					$options['itemids'] = $itemids;
204
205					$item_data = API::History()->get($options);
206
207					if ($item_data) {
208						$history_data = array_merge($history_data, $item_data);
209					}
210				}
211
212				CArrayHelper::sort($history_data, [
213					['field' => 'clock', 'order' => ZBX_SORT_DOWN],
214					['field' => 'ns', 'order' => ZBX_SORT_DOWN]
215				]);
216
217				$history_data = array_slice($history_data, 0, $options['limit']);
218
219				foreach ($history_data as $history_row) {
220					$value = $history_row['value'];
221
222					if (in_array($items[$history_row['itemid']]['value_type'],
223							[ITEM_VALUE_TYPE_LOG, ITEM_VALUE_TYPE_STR, ITEM_VALUE_TYPE_TEXT])) {
224						$value = '"'.$value.'"';
225					}
226					elseif ($items[$history_row['itemid']]['value_type'] == ITEM_VALUE_TYPE_FLOAT) {
227						$value = formatFloat($value, null, ZBX_UNITS_ROUNDOFF_UNSUFFIXED);
228					}
229
230					$row = zbx_date2str(DATE_TIME_FORMAT_SECONDS, $history_row['clock']).' '.$history_row['clock'].
231						' '.$value;
232
233					if ($is_many_items) {
234						$row .= ' "'.$items[$history_row['itemid']]['hosts'][0]['name'].NAME_DELIMITER.
235							$items[$history_row['itemid']]['name_expanded'].'"';
236					}
237					$output[] = $row;
238				}
239
240				// Return values as array of formatted strings.
241				return $output;
242			}
243			/**
244			 * View type: Values, 500 latest values
245			 * Item type: text, log
246			 */
247			elseif (!$numeric_items) {
248				$use_log_item = false;
249				$use_eventlog_item = false;
250				$items_by_type = [];
251				$history_data = [];
252
253				foreach ($items as $item) {
254					$items_by_type[$item['value_type']][] = $item['itemid'];
255
256					if ($item['value_type'] == ITEM_VALUE_TYPE_LOG) {
257						$use_log_item = true;
258					}
259
260					if (strpos($item['key_'], 'eventlog[') === 0) {
261						$use_eventlog_item = true;
262					}
263				}
264
265				$history_table = (new CTableInfo())
266					->setHeader([
267						(new CColHeader(_('Timestamp')))->addClass(ZBX_STYLE_CELL_WIDTH),
268						$is_many_items ? _('Item') : null,
269						$use_log_item ? (new CColHeader(_('Local time')))->addClass(ZBX_STYLE_CELL_WIDTH) : null,
270						($use_eventlog_item && $use_log_item)
271							? (new CColHeader(_('Source')))->addClass(ZBX_STYLE_CELL_WIDTH)
272							: null,
273						($use_eventlog_item && $use_log_item)
274							? (new CColHeader(_('Severity')))->addClass(ZBX_STYLE_CELL_WIDTH)
275							: null,
276						($use_eventlog_item && $use_log_item)
277							? (new CColHeader(_('Event ID')))->addClass(ZBX_STYLE_CELL_WIDTH)
278							: null,
279						_('Value')
280					]);
281
282				if ($this->filter !== '' && in_array($this->filterTask, [FILTER_TASK_SHOW, FILTER_TASK_HIDE])) {
283					$options['search'] = ['value' => $this->filter];
284					if ($this->filterTask == FILTER_TASK_HIDE) {
285						$options['excludeSearch'] = true;
286					}
287				}
288
289				foreach ($items_by_type as $value_type => $itemids) {
290					$options['history'] = $value_type;
291					$options['itemids'] = $itemids;
292					$item_data = API::History()->get($options);
293
294					if ($item_data) {
295						$history_data = array_merge($history_data, $item_data);
296					}
297				}
298
299				CArrayHelper::sort($history_data, [
300					['field' => 'clock', 'order' => ZBX_SORT_DOWN],
301					['field' => 'ns', 'order' => ZBX_SORT_DOWN]
302				]);
303
304				// Array $history_data will be modified according page and rows on page.
305				$pagination = CPagerHelper::paginate($this->page, $history_data, ZBX_SORT_UP,
306					new CUrl($this->page_file)
307				);
308
309				foreach ($history_data as $data) {
310					$data['value'] = rtrim($data['value'], " \t\r\n");
311
312					$item = $items[$data['itemid']];
313					$host = reset($item['hosts']);
314					$color = null;
315
316					if ($this->filter !== '') {
317						$haystack = mb_strtolower($data['value']);
318						$needle = mb_strtolower($this->filter);
319						$pos = mb_strpos($haystack, $needle);
320
321						if ($pos !== false && $this->filterTask == FILTER_TASK_MARK) {
322							$color = $this->markColor;
323						}
324						elseif ($pos === false && $this->filterTask == FILTER_TASK_INVERT_MARK) {
325							$color = $this->markColor;
326						}
327
328						switch ($color) {
329							case MARK_COLOR_RED:
330								$color = ZBX_STYLE_RED;
331								break;
332							case MARK_COLOR_GREEN:
333								$color = ZBX_STYLE_GREEN;
334								break;
335							case MARK_COLOR_BLUE:
336								$color = ZBX_STYLE_BLUE;
337								break;
338						}
339					}
340
341					$row = [];
342
343					$row[] = (new CCol(zbx_date2str(DATE_TIME_FORMAT_SECONDS, $data['clock'])))
344						->addClass(ZBX_STYLE_NOWRAP)
345						->addClass($color);
346
347					if ($is_many_items) {
348						$row[] = (new CCol($host['name'].NAME_DELIMITER.$item['name_expanded']))
349							->addClass($color);
350					}
351
352					if ($use_log_item) {
353						$row[] = (array_key_exists('timestamp', $data) && $data['timestamp'] != 0)
354							? (new CCol(zbx_date2str(DATE_TIME_FORMAT_SECONDS, $data['timestamp'])))
355								->addClass(ZBX_STYLE_NOWRAP)
356								->addClass($color)
357							: '';
358
359						// If this is a eventLog item, showing additional info.
360						if ($use_eventlog_item) {
361							$row[] = array_key_exists('source', $data)
362								? (new CCol($data['source']))
363									->addClass(ZBX_STYLE_NOWRAP)
364									->addClass($color)
365								: '';
366							$row[] = (array_key_exists('severity', $data) && $data['severity'] != 0)
367								? (new CCol(get_item_logtype_description($data['severity'])))
368									->addClass(ZBX_STYLE_NOWRAP)
369									->addClass(get_item_logtype_style($data['severity']))
370								: '';
371							$row[] = array_key_exists('severity', $data)
372								? (new CCol($data['logeventid']))
373									->addClass(ZBX_STYLE_NOWRAP)
374									->addClass($color)
375								: '';
376						}
377					}
378
379					$row[] = (new CCol(new CPre(zbx_nl2br($data['value']))))->addClass($color);
380
381					$history_table->addRow($row);
382				}
383
384				$output[] = [$history_table, $pagination];
385			}
386			/**
387			 * View type: 500 latest values.
388			 * Item type: numeric (unsigned, char), float.
389			 */
390			elseif ($this->action === HISTORY_LATEST) {
391				$history_table = (new CTableInfo())
392					->makeVerticalRotation()
393					->setHeader([(new CColHeader(_('Timestamp')))->addClass(ZBX_STYLE_CELL_WIDTH), _('Value')]);
394
395				$items_by_type = [];
396				$history_data = [];
397
398				foreach ($items as $item) {
399					$items_by_type[$item['value_type']][] = $item['itemid'];
400				}
401
402				foreach ($items_by_type as $value_type => $itemids) {
403					$options['history'] = $value_type;
404					$options['itemids'] = $itemids;
405					$item_data = API::History()->get($options);
406
407					if ($item_data) {
408						$history_data = array_merge($history_data, $item_data);
409					}
410				}
411
412				CArrayHelper::sort($history_data, [
413					['field' => 'clock', 'order' => ZBX_SORT_DOWN],
414					['field' => 'ns', 'order' => ZBX_SORT_DOWN]
415				]);
416
417				$history_data = array_slice($history_data, 0, $options['limit']);
418
419				foreach ($history_data as $history_row) {
420					$item = $items[$history_row['itemid']];
421					$value = $history_row['value'];
422
423					if ($item['value_type'] == ITEM_VALUE_TYPE_FLOAT) {
424						$value = formatFloat($value, null, ZBX_UNITS_ROUNDOFF_UNSUFFIXED);
425					}
426
427					$value = CValueMapHelper::applyValueMap($item['value_type'], $value, $item['valuemap']);
428
429					$history_table->addRow([
430						(new CCol(zbx_date2str(DATE_TIME_FORMAT_SECONDS, $history_row['clock'])))
431							->addClass(ZBX_STYLE_NOWRAP),
432						new CPre(zbx_nl2br($value))
433					]);
434				}
435
436				$output[] = $history_table;
437			}
438			/**
439			 * View type: Values.
440			 * Item type: numeric (unsigned, char), float.
441			 */
442			else {
443				CArrayHelper::sort($items, [
444					['field' => 'name_expanded', 'order' => ZBX_SORT_UP]
445				]);
446				$table_header = [(new CColHeader(_('Timestamp')))->addClass(ZBX_STYLE_CELL_WIDTH)];
447				$history_data = [];
448
449				foreach ($items as $item) {
450					$options['itemids'] = [$item['itemid']];
451					$options['history'] = $item['value_type'];
452					$item_data = API::History()->get($options);
453
454					CArrayHelper::sort($item_data, [
455						['field' => 'clock', 'order' => ZBX_SORT_DOWN],
456						['field' => 'ns', 'order' => ZBX_SORT_DOWN]
457					]);
458
459					$table_header[] = (new CColHeader($item['name_expanded']))
460						->addClass('vertical_rotation')
461						->setTitle($item['name_expanded']);
462					$history_data_index = 0;
463
464					foreach ($item_data as $item_data_row) {
465						// Searching for starting 'insert before' index in results array.
466						while (array_key_exists($history_data_index, $history_data)) {
467							$history_row = $history_data[$history_data_index];
468
469							if ($history_row['clock'] <= $item_data_row['clock']
470									&& !array_key_exists($item['itemid'], $history_row['values'])) {
471								break;
472							}
473
474							++$history_data_index;
475						}
476
477						if (array_key_exists($history_data_index, $history_data)
478								&& !array_key_exists($item['itemid'], $history_row['values'])
479								&& $history_data[$history_data_index]['clock'] === $item_data_row['clock']) {
480							$history_data[$history_data_index]['values'][$item['itemid']] = $item_data_row['value'];
481						}
482						else {
483							array_splice($history_data, $history_data_index, 0, [[
484								'clock' => $item_data_row['clock'],
485								'values' => [$item['itemid'] => $item_data_row['value']]
486							]]);
487						}
488					}
489				}
490
491				// Array $history_data will be modified according page and rows on page.
492				$pagination = CPagerHelper::paginate($this->page, $history_data, ZBX_SORT_UP,
493					new CUrl($this->page_file)
494				);
495
496				$history_table = (new CTableInfo())->makeVerticalRotation()->setHeader($table_header);
497
498				foreach ($history_data as $history_data_row) {
499					$row = [(new CCol(zbx_date2str(DATE_TIME_FORMAT_SECONDS, $history_data_row['clock'])))
500						->addClass(ZBX_STYLE_NOWRAP)
501					];
502					$values = $history_data_row['values'];
503
504					foreach ($items as $item) {
505						$value = array_key_exists($item['itemid'], $values) ? $values[$item['itemid']] : '';
506
507						if ($item['value_type'] == ITEM_VALUE_TYPE_FLOAT && $value !== '') {
508							$value = formatFloat($value, null, ZBX_UNITS_ROUNDOFF_UNSUFFIXED);
509						}
510
511						$value = CValueMapHelper::applyValueMap($item['value_type'], $value, $item['valuemap']);
512
513						$row[] = ($value === '') ? '' : new CPre($value);
514					}
515
516					$history_table->addRow($row);
517				}
518
519				$output[] = [$history_table, $pagination];
520			}
521		}
522
523		// time control
524		if (str_in_array($this->action, [HISTORY_VALUES, HISTORY_GRAPH, HISTORY_BATCH_GRAPH])) {
525			$graphDims = getGraphDims();
526
527			$this->dataId = 'historyGraph';
528
529			$timeControlData = [];
530
531			if ($this->action == HISTORY_GRAPH || $this->action == HISTORY_BATCH_GRAPH) {
532				$containerId = 'graph_cont1';
533				$output[] = (new CDiv())
534					->addClass('center')
535					->setId($containerId);
536
537				$timeControlData['id'] = $this->getDataId();
538				$timeControlData['containerid'] = $containerId;
539				$timeControlData['src'] = $this->getGraphUrl($this->itemids);
540				$timeControlData['objDims'] = $graphDims;
541				$timeControlData['loadSBox'] = 1;
542				$timeControlData['loadImage'] = 1;
543				$timeControlData['dynamic'] = 1;
544			}
545			else {
546				$timeControlData['id'] = $this->getDataId();
547			}
548
549			if ($this->mode == SCREEN_MODE_JS) {
550				$timeControlData['dynamic'] = 0;
551
552				return 'timeControl.addObject("'.$this->getDataId().'", '.json_encode($this->timeline).', '.
553					json_encode($timeControlData).');';
554			}
555
556			zbx_add_post_js('timeControl.addObject("'.$this->getDataId().'", '.json_encode($this->timeline).', '.
557				json_encode($timeControlData).');'
558			);
559		}
560
561		if ($this->mode != SCREEN_MODE_JS) {
562			$flickerfreeData = [
563				'itemids' => $this->itemids,
564				'action' => ($this->action == HISTORY_BATCH_GRAPH) ? HISTORY_GRAPH : $this->action,
565				'filter' => $this->filter,
566				'filterTask' => $this->filterTask,
567				'markColor' => $this->markColor
568			];
569
570			if ($this->action == HISTORY_VALUES) {
571				$flickerfreeData['page'] = $this->page;
572			}
573
574			if ($this->graphid != 0) {
575				unset($flickerfreeData['itemids']);
576				$flickerfreeData['graphid'] = $this->graphid;
577			}
578
579			return $this->getOutput($output, true, $flickerfreeData);
580		}
581
582		return $output;
583	}
584
585	/**
586	 * Return the URL for the graph.
587	 *
588	 * @param array $itemIds
589	 *
590	 * @return string
591	 */
592	protected function getGraphUrl(array $itemIds) {
593		$url = (new CUrl('chart.php'))
594			->setArgument('from', $this->timeline['from'])
595			->setArgument('to', $this->timeline['to'])
596			->setArgument('itemids', $itemIds)
597			->setArgument('type', $this->graphType)
598			->setArgument('profileIdx', $this->profileIdx)
599			->setArgument('profileIdx2', $this->profileIdx2);
600
601		if ($this->action == HISTORY_BATCH_GRAPH) {
602			$url->setArgument('batch', 1);
603		}
604
605		return $url->getUrl();
606	}
607}
608