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 class to display problems as a screen element.
24 */
25class CScreenProblem extends CScreenBase {
26
27	/**
28	 * Data
29	 *
30	 * @var array
31	 */
32	public $data;
33
34	/**
35	 * Init screen data.
36	 *
37	 * @param array $options
38	 * @param array $options['data']
39	 */
40	public function __construct(array $options = []) {
41		parent::__construct($options);
42		$this->data = array_key_exists('data', $options) ? $options['data'] : null;
43
44		if ($this->data['filter']['show'] == TRIGGERS_OPTION_ALL) {
45			$this->data['filter']['from'] = $this->timeline['from_ts'];
46			$this->data['filter']['to'] = $this->timeline['to_ts'];
47		}
48	}
49
50	/**
51	 * Get problems from "events" table.
52	 *
53	 * @param array       $options
54	 * @param array|null  $options['groupids']
55	 * @param array|null  $options['hostids']
56	 * @param array|null  $options['objectids']
57	 * @param string|null $options['eventid_till']
58	 * @param int|null    $options['time_from']
59	 * @param int|null    $options['time_till']
60	 * @param array       $options['severities']      (optional)
61	 * @param bool        $options['acknowledged']    (optional)
62	 * @param array       $options['tags']            (optional)
63	 * @param int         $options['limit']
64	 *
65	 * @static
66	 *
67	 * @return array
68	 */
69	private static function getDataEvents(array $options) {
70		return API::Event()->get([
71			'output' => ['eventid', 'objectid', 'clock', 'ns', 'name', 'severity'],
72			'source' => EVENT_SOURCE_TRIGGERS,
73			'object' => EVENT_OBJECT_TRIGGER,
74			'value' => TRIGGER_VALUE_TRUE,
75			'sortfield' => ['eventid'],
76			'sortorder' => ZBX_SORT_DOWN,
77			'preservekeys' => true
78		] + $options);
79	}
80
81	/**
82	 * Get problems from "problem" table.
83	 *
84	 * @param array       $options
85	 * @param array|null  $options['groupids']
86	 * @param array|null  $options['hostids']
87	 * @param array|null  $options['objectids']
88	 * @param string|null $options['eventid_till']
89	 * @param bool        $options['recent']
90	 * @param array       $options['severities']      (optional)
91	 * @param bool        $options['acknowledged']    (optional)
92	 * @param int         $options['time_from']       (optional)
93	 * @param array       $options['tags']            (optional)
94	 * @param int         $options['limit']
95	 *
96	 * @static
97	 *
98	 * @return array
99	 */
100	private static function getDataProblems(array $options) {
101		return API::Problem()->get([
102			'output' => ['eventid', 'objectid', 'clock', 'ns', 'name', 'severity'],
103			'source' => EVENT_SOURCE_TRIGGERS,
104			'object' => EVENT_OBJECT_TRIGGER,
105			'sortfield' => ['eventid'],
106			'sortorder' => ZBX_SORT_DOWN,
107			'preservekeys' => true
108		] + $options);
109	}
110
111	/**
112	 * Get problems from "problem" table. Return:
113	 * [
114	 *     'problems' => [...],
115	 *     'triggers' => [...]
116	 * ]
117	 *
118	 * @param array  $filter
119	 * @param array  $filter['groupids']              (optional)
120	 * @param array  $filter['exclude_groupids']      (optional)
121	 * @param array  $filter['hostids']               (optional)
122	 * @param array  $filter['triggerids']            (optional)
123	 * @param array  $filter['inventory']             (optional)
124	 * @param string $filter['inventory'][]['field']
125	 * @param string $filter['inventory'][]['value']
126	 * @param string $filter['name']                  (optional)
127	 * @param int    $filter['show']                  TRIGGERS_OPTION_*
128	 * @param int    $filter['from']                  (optional) usable together with 'to' and only for
129	 *                                                           TRIGGERS_OPTION_ALL, timestamp.
130	 * @param int    $filter['to']                    (optional) usable together with 'from' and only for
131	 *                                                           TRIGGERS_OPTION_ALL, timestamp.
132	 * @param int    $filter['age_state']             (optional) usable together with 'age' and only for
133	 *                                                           TRIGGERS_OPTION_(RECENT|IN)_PROBLEM
134	 * @param int    $filter['age']                   (optional) usable together with 'age_state' and only for
135	 *                                                           TRIGGERS_OPTION_(RECENT|IN)_PROBLEM
136	 * @param array  $filter['severities']            (optional)
137	 * @param int    $filter['unacknowledged']        (optional)
138	 * @param array  $filter['tags']                  (optional)
139	 * @param string $filter['tags'][]['tag']
140	 * @param string $filter['tags'][]['value']
141	 * @param int    $filter['show_suppressed']       (optional)
142	 * @param int    $filter['show_opdata']           (optional)
143	 * @param bool   $resolve_comments
144	 *
145	 * @static
146	 *
147	 * @return array
148	 */
149	public static function getData(array $filter, bool $resolve_comments = false) {
150		$filter_groupids = array_key_exists('groupids', $filter) && $filter['groupids']
151			? getSubGroups($filter['groupids'])
152			: null;
153		$filter_hostids = array_key_exists('hostids', $filter) && $filter['hostids'] ? $filter['hostids'] : null;
154		$filter_triggerids = array_key_exists('triggerids', $filter) && $filter['triggerids']
155			? $filter['triggerids']
156			: null;
157		$show_opdata = array_key_exists('show_opdata', $filter) && $filter['show_opdata'] != OPERATIONAL_DATA_SHOW_NONE;
158
159		if (array_key_exists('exclude_groupids', $filter) && $filter['exclude_groupids']) {
160			$exclude_groupids = getSubGroups($filter['exclude_groupids']);
161
162			if ($filter_hostids === null) {
163				// get all groups if no selected groups defined
164				if ($filter_groupids === null) {
165					$filter_groupids = array_keys(API::HostGroup()->get([
166						'output' => [],
167						'real_hosts' => true,
168						'preservekeys' => true
169					]));
170				}
171
172				$filter_groupids = array_diff($filter_groupids, $exclude_groupids);
173
174				// get available hosts
175				$filter_hostids = array_keys(API::Host()->get([
176					'output' => [],
177					'groupids' => $filter_groupids,
178					'preservekeys' => true
179				]));
180			}
181
182			$exclude_hostids = array_keys(API::Host()->get([
183				'output' => [],
184				'groupids' => $exclude_groupids,
185				'preservekeys' => true
186			]));
187
188			$filter_hostids = array_diff($filter_hostids, $exclude_hostids);
189		}
190
191		if (array_key_exists('inventory', $filter) && $filter['inventory']) {
192			$options = [
193				'output' => [],
194				'groupids' => $filter_groupids,
195				'hostids' => $filter_hostids,
196				'preservekeys' => true
197			];
198			foreach ($filter['inventory'] as $field) {
199				$options['searchInventory'][$field['field']][] = $field['value'];
200			}
201
202			$hostids = array_keys(API::Host()->get($options));
203
204			$filter_hostids = ($filter_hostids !== null) ? array_intersect($filter_hostids, $hostids) : $hostids;
205		}
206
207		$data = [
208			'problems' => [],
209			'triggers' => []
210		];
211
212		$seen_triggerids = [];
213		$eventid_till = null;
214
215		do {
216			$options = [
217				'groupids' => $filter_groupids,
218				'hostids' => $filter_hostids,
219				'objectids' => $filter_triggerids,
220				'eventid_till' => $eventid_till,
221				'suppressed' => false,
222				'limit' => CSettingsHelper::get(CSettingsHelper::SEARCH_LIMIT) + 1
223			];
224
225			if (array_key_exists('name', $filter) && $filter['name'] !== '') {
226				$options['search']['name'] = $filter['name'];
227			}
228
229			if ($filter['show'] == TRIGGERS_OPTION_ALL) {
230				if (array_key_exists('from', $filter) && array_key_exists('to', $filter)) {
231					$options['time_from'] = $filter['from'];
232					$options['time_till'] = $filter['to'];
233				}
234			}
235			else {
236				$options['recent'] = ($filter['show'] == TRIGGERS_OPTION_RECENT_PROBLEM);
237				if (array_key_exists('age_state', $filter) && array_key_exists('age', $filter)
238						&& $filter['age_state'] == 1) {
239					$options['time_from'] = time() - $filter['age'] * SEC_PER_DAY + 1;
240				}
241			}
242			if (array_key_exists('severities', $filter)) {
243				$filter_severities = implode(',', $filter['severities']);
244				$all_severities = implode(',', range(TRIGGER_SEVERITY_NOT_CLASSIFIED, TRIGGER_SEVERITY_COUNT - 1));
245
246				if ($filter_severities !== '' && $filter_severities !== $all_severities) {
247					$options['severities'] = $filter['severities'];
248				}
249			}
250			if (array_key_exists('unacknowledged', $filter) && $filter['unacknowledged']) {
251				$options['acknowledged'] = false;
252			}
253			if (array_key_exists('evaltype', $filter)) {
254				$options['evaltype'] = $filter['evaltype'];
255			}
256			if (array_key_exists('tags', $filter) && $filter['tags']) {
257				$options['tags'] = $filter['tags'];
258			}
259			if (array_key_exists('show_suppressed', $filter) && $filter['show_suppressed']) {
260				unset($options['suppressed']);
261				$options['selectSuppressionData'] = ['maintenanceid', 'suppress_until'];
262			}
263
264			$problems = ($filter['show'] == TRIGGERS_OPTION_ALL)
265				? self::getDataEvents($options)
266				: self::getDataProblems($options);
267
268			$end_of_data = (count($problems) < CSettingsHelper::get(CSettingsHelper::SEARCH_LIMIT) + 1);
269
270			if ($problems) {
271				$eventid_till = end($problems)['eventid'] - 1;
272				$triggerids = [];
273
274				if (array_key_exists('show_suppressed', $filter) && $filter['show_suppressed']) {
275					self::addMaintenanceNames($problems);
276				}
277
278				foreach ($problems as $problem) {
279					if (!array_key_exists($problem['objectid'], $seen_triggerids)) {
280						$triggerids[$problem['objectid']] = true;
281					}
282				}
283
284				if ($triggerids) {
285					$seen_triggerids += $triggerids;
286
287					$options = [
288						'output' => ['priority', 'manual_close'],
289						'selectHosts' => ['hostid'],
290						'triggerids' => array_keys($triggerids),
291						'monitored' => true,
292						'skipDependent' => ($filter['show'] == TRIGGERS_OPTION_ALL) ? null : true,
293						'preservekeys' => true
294					];
295
296					$details = (array_key_exists('details', $filter) && $filter['details'] == 1);
297
298					if ($show_opdata) {
299						$options['output'][] = 'opdata';
300						$options['selectFunctions'] = ['itemid'];
301					}
302
303					if ($resolve_comments || $show_opdata || $details) {
304						$options['output'][] = 'expression';
305					}
306
307					if ($show_opdata || $details) {
308						$options['output'] = array_merge($options['output'], ['recovery_mode', 'recovery_expression']);
309					}
310
311					if ($resolve_comments) {
312						$options['output'][] = 'comments';
313					}
314
315					$data['triggers'] += API::Trigger()->get($options);
316				}
317
318				foreach ($problems as $eventid => $problem) {
319					if (!array_key_exists($problem['objectid'], $data['triggers'])) {
320						unset($problems[$eventid]);
321					}
322				}
323
324				$data['problems'] += $problems;
325			}
326		}
327		while (count($data['problems']) < CSettingsHelper::get(CSettingsHelper::SEARCH_LIMIT) + 1 && !$end_of_data);
328
329		$data['problems'] = array_slice($data['problems'], 0, CSettingsHelper::get(CSettingsHelper::SEARCH_LIMIT) + 1,
330			true
331		);
332
333		if ($show_opdata && $data['triggers']) {
334			$items = API::Item()->get([
335				'output' => ['itemid', 'hostid', 'name', 'key_', 'value_type', 'units'],
336				'selectValueMap' => ['mappings'],
337				'triggerids' => array_keys($data['triggers']),
338				'webitems' => true,
339				'preservekeys' => true
340			]);
341
342			foreach ($data['triggers'] as &$trigger) {
343				foreach ($trigger['functions'] as $function) {
344					$trigger['items'][] = $items[$function['itemid']];
345				}
346				unset($trigger['functions']);
347			}
348			unset($trigger);
349		}
350
351		return $data;
352	}
353
354	/**
355	 * Adds maintenance names of suppressed problems.
356	 *
357	 * @param array $problems
358	 * @param array $problems[]['suppression_data']
359	 * @param int   $problems[]['suppression_data'][]['maintenanceid']
360	 *
361	 * @static
362	 */
363	public static function addMaintenanceNames(array &$problems) {
364		$maintenanceids = [];
365
366		foreach ($problems as $problem) {
367			if (array_key_exists('suppression_data', $problem) && $problem['suppression_data']) {
368				foreach ($problem['suppression_data'] as $data) {
369					$maintenanceids[] = $data['maintenanceid'];
370				}
371			}
372		}
373
374		if ($maintenanceids) {
375			$maintenances = API::Maintenance()->get([
376				'output' => ['name'],
377				'maintenanceids' => $maintenanceids,
378				'preservekeys' => true
379			]);
380
381			foreach ($problems as &$problem) {
382				if (array_key_exists('suppression_data', $problem) && $problem['suppression_data']) {
383					foreach ($problem['suppression_data'] as &$data) {
384						$data['maintenance_name'] = array_key_exists($data['maintenanceid'], $maintenances)
385							? $maintenances[$data['maintenanceid']]['name']
386							: _('Inaccessible maintenance');
387					}
388					unset($data);
389				}
390			}
391			unset($problem);
392		}
393	}
394
395	/**
396	 * @param array  $data
397	 * @param array  $data['problems']
398	 * @param array  $data['triggers']
399	 * @param string $sort
400	 * @param string $sortorder
401	 *
402	 * @static
403	 *
404	 * @return array
405	 */
406	public static function sortData(array $data, $sort, $sortorder) {
407		if (!$data['problems']) {
408			return $data;
409		}
410
411		$last_problem = end($data['problems']);
412		$data['problems'] = array_slice($data['problems'], 0, CSettingsHelper::get(CSettingsHelper::SEARCH_LIMIT),
413			true
414		);
415
416		switch ($sort) {
417			case 'host':
418				$triggers_hosts_list = [];
419				foreach (getTriggersHostsList($data['triggers']) as $triggerid => $trigger_hosts) {
420					$triggers_hosts_list[$triggerid] = implode(', ', zbx_objectValues($trigger_hosts, 'name'));
421				}
422
423				foreach ($data['problems'] as &$problem) {
424					$problem['host'] = $triggers_hosts_list[$problem['objectid']];
425				}
426				unset($problem);
427
428				$sort_fields = [
429					['field' => 'host', 'order' => $sortorder],
430					['field' => 'clock', 'order' => ZBX_SORT_DOWN],
431					['field' => 'ns', 'order' => ZBX_SORT_DOWN]
432				];
433				break;
434
435			case 'severity':
436				$sort_fields = [
437					['field' => 'severity', 'order' => $sortorder],
438					['field' => 'clock', 'order' => ZBX_SORT_DOWN],
439					['field' => 'ns', 'order' => ZBX_SORT_DOWN]
440				];
441				break;
442
443			case 'name':
444				$sort_fields = [
445					['field' => 'name', 'order' => $sortorder],
446					['field' => 'objectid', 'order' => $sortorder],
447					['field' => 'clock', 'order' => ZBX_SORT_DOWN],
448					['field' => 'ns', 'order' => ZBX_SORT_DOWN]
449				];
450				break;
451
452			default:
453				$sort_fields = [
454					['field' => 'clock', 'order' => $sortorder],
455					['field' => 'ns', 'order' => $sortorder]
456				];
457		}
458		CArrayHelper::sort($data['problems'], $sort_fields);
459
460		$data['problems'][$last_problem['eventid']] = $last_problem;
461
462		return $data;
463	}
464
465	/**
466	 * @param array $eventids
467	 *
468	 * @static
469	 *
470	 * @return array
471	 */
472	private static function getExDataEvents(array $eventids) {
473		$events = API::Event()->get([
474			'output' => ['eventid', 'r_eventid', 'acknowledged'],
475			'selectTags' => ['tag', 'value'],
476			'select_acknowledges' => ['userid', 'clock', 'message', 'action', 'old_severity', 'new_severity'],
477			'source' => EVENT_SOURCE_TRIGGERS,
478			'object' => EVENT_OBJECT_TRIGGER,
479			'eventids' => $eventids,
480			'preservekeys' => true
481		]);
482
483		$r_eventids = [];
484
485		foreach ($events as $event) {
486			$r_eventids[$event['r_eventid']] = true;
487		}
488		unset($r_eventids[0]);
489
490		$r_events = $r_eventids
491			? API::Event()->get([
492				'output' => ['clock', 'ns', 'correlationid', 'userid'],
493				'source' => EVENT_SOURCE_TRIGGERS,
494				'object' => EVENT_OBJECT_TRIGGER,
495				'eventids' => array_keys($r_eventids),
496				'preservekeys' => true
497			])
498			: [];
499
500		foreach ($events as &$event) {
501			if (array_key_exists($event['r_eventid'], $r_events)) {
502				$event['r_clock'] = $r_events[$event['r_eventid']]['clock'];
503				$event['r_ns'] = $r_events[$event['r_eventid']]['ns'];
504				$event['correlationid'] = $r_events[$event['r_eventid']]['correlationid'];
505				$event['userid'] = $r_events[$event['r_eventid']]['userid'];
506			}
507			else {
508				$event['r_clock'] = 0;
509				$event['r_ns'] = 0;
510				$event['correlationid'] = 0;
511				$event['userid'] = 0;
512			}
513		}
514		unset($event);
515
516		return $events;
517	}
518
519	/**
520	 * @param array $eventids
521	 *
522	 * @static
523	 *
524	 * @return array
525	 */
526	private static function getExDataProblems(array $eventids) {
527		return API::Problem()->get([
528			'output' => ['eventid', 'r_eventid', 'r_clock', 'r_ns', 'correlationid', 'userid', 'acknowledged'],
529			'selectTags' => ['tag', 'value'],
530			'selectAcknowledges' => ['userid', 'clock', 'message', 'action', 'old_severity', 'new_severity'],
531			'source' => EVENT_SOURCE_TRIGGERS,
532			'object' => EVENT_OBJECT_TRIGGER,
533			'eventids' => $eventids,
534			'recent' => true,
535			'preservekeys' => true
536		]);
537	}
538
539	/**
540	 * @param array $data
541	 * @param array $data['problems']
542	 * @param array $data['triggers']
543	 * @param array $filter
544	 * @param int   $filter['details']
545	 * @param int   $filter['show']
546	 * @param int   $filter['show_opdata']
547	 * @param bool  $resolve_comments
548	 *
549	 * @static
550	 *
551	 * @return array
552	 */
553	public static function makeData(array $data, array $filter, bool $resolve_comments = false) {
554		// unset unused triggers
555		$triggerids = [];
556
557		foreach ($data['problems'] as $problem) {
558			$triggerids[$problem['objectid']] = true;
559		}
560
561		foreach ($data['triggers'] as $triggerid => $trigger) {
562			if (!array_key_exists($triggerid, $triggerids)) {
563				unset($data['triggers'][$triggerid]);
564			}
565		}
566
567		if (!$data['problems']) {
568			return $data;
569		}
570
571		// resolve macros
572		if ($filter['details'] == 1 || $filter['show_opdata'] != OPERATIONAL_DATA_SHOW_NONE) {
573			foreach ($data['triggers'] as &$trigger) {
574				$trigger['expression_html'] = $trigger['expression'];
575				$trigger['recovery_expression_html'] = $trigger['recovery_expression'];
576			}
577			unset($trigger);
578
579			$data['triggers'] = CMacrosResolverHelper::resolveTriggerExpressions($data['triggers'], [
580				'html' => true,
581				'resolve_usermacros' => true,
582				'resolve_macros' => true,
583				'sources' => ['expression_html', 'recovery_expression_html']
584			]);
585
586			// Sort items.
587			if ($filter['show_opdata'] != OPERATIONAL_DATA_SHOW_NONE) {
588				$data['triggers'] = CMacrosResolverHelper::sortItemsByExpressionOrder($data['triggers']);
589
590				foreach ($data['triggers'] as &$trigger) {
591					$trigger['items'] = CMacrosResolverHelper::resolveItemNames($trigger['items']);
592				}
593				unset($trigger);
594			}
595		}
596
597		if ($resolve_comments) {
598			foreach ($data['problems'] as &$problem) {
599				$trigger = $data['triggers'][$problem['objectid']];
600				$problem['comments'] = CMacrosResolverHelper::resolveTriggerDescription(
601					[
602						'triggerid' => $problem['objectid'],
603						'expression' => $trigger['expression'],
604						'comments' => $trigger['comments'],
605						'clock' => $problem['clock'],
606						'ns' => $problem['ns']
607					],
608					['events' => true]
609				);
610			}
611			unset($problem);
612
613			foreach ($data['triggers'] as &$trigger) {
614				unset($trigger['comments']);
615			}
616			unset($trigger);
617		}
618
619		// get additional data
620		$eventids = array_keys($data['problems']);
621
622		$problems_data = ($filter['show'] == TRIGGERS_OPTION_ALL)
623			? self::getExDataEvents($eventids)
624			: self::getExDataProblems($eventids);
625
626		$correlationids = [];
627		$userids = [];
628
629		foreach ($data['problems'] as $eventid => &$problem) {
630			if (array_key_exists($eventid, $problems_data)) {
631				$problem_data = $problems_data[$eventid];
632
633				$problem['r_eventid'] = $problem_data['r_eventid'];
634				$problem['r_clock'] = $problem_data['r_clock'];
635				$problem['r_ns'] = $problem_data['r_ns'];
636				$problem['acknowledges'] = $problem_data['acknowledges'];
637				$problem['tags'] = $problem_data['tags'];
638				$problem['correlationid'] = $problem_data['correlationid'];
639				$problem['userid'] = $problem_data['userid'];
640				$problem['acknowledged'] = $problem_data['acknowledged'];
641
642				if ($problem['correlationid'] != 0) {
643					$correlationids[$problem['correlationid']] = true;
644				}
645				if ($problem['userid'] != 0) {
646					$userids[$problem['userid']] = true;
647				}
648			}
649			else {
650				unset($data['problems'][$eventid]);
651			}
652		}
653		unset($problem);
654
655		// Possible performance improvement: one API call may be saved, if r_clock for problem will be used.
656		$actions = getEventsActionsIconsData($data['problems'], $data['triggers']);
657		$data['actions'] = $actions['data'];
658
659		$data['correlations'] = $correlationids
660			? API::Correlation()->get([
661				'output' => ['name'],
662				'correlationids' => array_keys($correlationids),
663				'preservekeys' => true
664			])
665			: [];
666
667		$userids = $userids + $actions['userids'];
668		$data['users'] = $userids
669			? API::User()->get([
670				'output' => ['username', 'name', 'surname'],
671				'userids' => array_keys($userids + $actions['userids']),
672				'preservekeys' => true
673			])
674			: [];
675
676		return $data;
677	}
678
679	/**
680	 * Add timeline breakpoint to a table if needed.
681	 *
682	 * @param CTableInfo $table
683	 * @param int        $last_clock  timestamp of the previous record
684	 * @param int        $clock       timestamp of the current record
685	 * @param string     $sortorder
686	 *
687	 * @static
688	 */
689	public static function addTimelineBreakpoint(CTableInfo $table, $last_clock, $clock, $sortorder) {
690		if ($sortorder === ZBX_SORT_UP) {
691			list($clock, $last_clock) = [$last_clock, $clock];
692		}
693
694		$breakpoint = null;
695		$today = strtotime('today');
696		$yesterday = strtotime('yesterday');
697		$this_year = strtotime('first day of January '.date('Y', $today));
698
699		if ($last_clock >= $today) {
700			if ($clock < $today) {
701				$breakpoint = _('Today');
702			}
703			elseif (strftime('%H', $last_clock) != strftime('%H', $clock)) {
704				$breakpoint = strftime('%H:00', $last_clock);
705			}
706		}
707		elseif ($last_clock >= $yesterday) {
708			if ($clock < $yesterday) {
709				$breakpoint = _('Yesterday');
710			}
711		}
712		elseif ($last_clock >= $this_year && $clock < $this_year) {
713			$breakpoint = strftime('%Y', $last_clock);
714		}
715		elseif (strftime('%Y%m', $last_clock) != strftime('%Y%m', $clock)) {
716			$breakpoint = getMonthCaption(strftime('%m', $last_clock));
717		}
718
719		if ($breakpoint !== null) {
720			$table->addRow((new CRow([
721				(new CCol(new CTag('h4', true, $breakpoint)))->addClass(ZBX_STYLE_TIMELINE_DATE),
722				(new CCol())
723					->addClass(ZBX_STYLE_TIMELINE_AXIS)
724					->addClass(ZBX_STYLE_TIMELINE_DOT_BIG),
725				(new CCol())->addClass(ZBX_STYLE_TIMELINE_TD),
726				(new CCol())->setColSpan($table->getNumCols() - 3)
727			]))->addClass(ZBX_STYLE_HOVER_NOBG));
728		}
729	}
730
731	/**
732	 * Process screen.
733	 *
734	 * @return string|CDiv (screen inside container)
735	 */
736	public function get() {
737		$this->dataId = 'problem';
738
739		$url = (new CUrl('zabbix.php'))->setArgument('action', 'problem.view');
740		$args = [
741			'sort' => $this->data['sort'],
742			'sortorder' => $this->data['sortorder']
743		] + $this->data['filter'];
744
745		if ($this->data['filter']['show'] == TRIGGERS_OPTION_ALL) {
746			$args['from'] = $this->timeline['from'];
747			$args['to'] = $this->timeline['to'];
748		}
749
750		if (array_key_exists('severities', $args)) {
751			$args['severities'] = array_combine($args['severities'], $args['severities']);
752		}
753
754		array_map([$url, 'setArgument'], array_keys($args), $args);
755
756		$data = self::getData($this->data['filter'], true);
757		$data = self::sortData($data, $this->data['sort'], $this->data['sortorder']);
758
759		if ($this->data['action'] === 'problem.view') {
760			$paging = CPagerHelper::paginate($this->page, $data['problems'], ZBX_SORT_UP, $url);
761		}
762
763		$data = self::makeData($data, $this->data['filter'], true);
764
765		if ($data['triggers']) {
766			$triggerids = array_keys($data['triggers']);
767
768			$db_triggers = API::Trigger()->get([
769				'output' => [],
770				'selectDependencies' => ['triggerid'],
771				'triggerids' => $triggerids,
772				'preservekeys' => true
773			]);
774
775			foreach ($data['triggers'] as $triggerid => &$trigger) {
776				$trigger['dependencies'] = array_key_exists($triggerid, $db_triggers)
777					? $db_triggers[$triggerid]['dependencies']
778					: [];
779			}
780			unset($trigger);
781		}
782
783		if ($data['problems']) {
784			$triggers_hosts = getTriggersHostsList($data['triggers']);
785		}
786
787		$show_opdata = $this->data['filter']['compact_view']
788			? OPERATIONAL_DATA_SHOW_NONE
789			: $this->data['filter']['show_opdata'];
790
791		if ($this->data['action'] === 'problem.view') {
792			$form = (new CForm('post', 'zabbix.php'))
793				->setId('problem_form')
794				->setName('problem')
795				->cleanItems();
796
797			$header_check_box = (new CColHeader(
798				(new CCheckBox('all_eventids'))
799					->onClick("checkAll('".$form->getName()."', 'all_eventids', 'eventids');")
800			));
801
802			$this->data['filter']['compact_view']
803				? $header_check_box->addStyle('width: 20px;')
804				: $header_check_box->addClass(ZBX_STYLE_CELL_WIDTH);
805
806			$link = $url->getUrl();
807
808			$show_timeline = ($this->data['sort'] === 'clock' && !$this->data['filter']['compact_view']
809				&& $this->data['filter']['show_timeline']);
810
811			$show_recovery_data = in_array($this->data['filter']['show'], [
812				TRIGGERS_OPTION_RECENT_PROBLEM,
813				TRIGGERS_OPTION_ALL
814			]);
815			$header_clock =
816				make_sorting_header(_('Time'), 'clock', $this->data['sort'], $this->data['sortorder'], $link);
817
818			$this->data['filter']['compact_view']
819				? $header_clock->addStyle('width: 115px;')
820				: $header_clock->addClass(ZBX_STYLE_CELL_WIDTH);
821
822			if ($show_timeline) {
823				$header = [
824					$header_clock->addClass(ZBX_STYLE_RIGHT),
825					(new CColHeader())->addClass(ZBX_STYLE_TIMELINE_TH),
826					(new CColHeader())->addClass(ZBX_STYLE_TIMELINE_TH)
827				];
828			}
829			else {
830				$header = [$header_clock];
831			}
832
833			// Create table.
834			if ($this->data['filter']['compact_view']) {
835				if ($this->data['filter']['show_tags'] == PROBLEMS_SHOW_TAGS_NONE) {
836					$tags_header = null;
837				}
838				else {
839					$tags_header = (new CColHeader(_('Tags')));
840
841					switch ($this->data['filter']['show_tags']) {
842						case PROBLEMS_SHOW_TAGS_1:
843							$tags_header->addClass(ZBX_STYLE_COLUMN_TAGS_1);
844							break;
845						case PROBLEMS_SHOW_TAGS_2:
846							$tags_header->addClass(ZBX_STYLE_COLUMN_TAGS_2);
847							break;
848						case PROBLEMS_SHOW_TAGS_3:
849							$tags_header->addClass(ZBX_STYLE_COLUMN_TAGS_3);
850							break;
851					}
852				}
853
854				$table = (new CTableInfo())
855					->setHeader(array_merge($header, [
856						$header_check_box,
857						make_sorting_header(_('Severity'), 'severity', $this->data['sort'], $this->data['sortorder'],
858							$link
859						)->addStyle('width: 120px;'),
860						$show_recovery_data ? (new CColHeader(_('Recovery time')))->addStyle('width: 115px;') : null,
861						$show_recovery_data ? (new CColHeader(_('Status')))->addStyle('width: 70px;') : null,
862						(new CColHeader(_('Info')))->addStyle('width: 24px;'),
863						make_sorting_header(_('Host'), 'host', $this->data['sort'], $this->data['sortorder'], $link)
864							->addStyle('width: 42%;'),
865						make_sorting_header(_('Problem'), 'name', $this->data['sort'], $this->data['sortorder'], $link)
866							->addStyle('width: 58%;'),
867						(new CColHeader(_('Duration')))->addStyle('width: 73px;'),
868						(new CColHeader(_('Ack')))->addStyle('width: 36px;'),
869						(new CColHeader(_('Actions')))->addStyle('width: 64px;'),
870						$tags_header
871					]))
872						->addClass(ZBX_STYLE_COMPACT_VIEW)
873						->addClass(ZBX_STYLE_OVERFLOW_ELLIPSIS);
874			}
875			else {
876				$table = (new CTableInfo())
877					->setHeader(array_merge($header, [
878						$header_check_box,
879						make_sorting_header(_('Severity'), 'severity', $this->data['sort'], $this->data['sortorder'],
880							$link
881						),
882						$show_recovery_data
883							? (new CColHeader(_('Recovery time')))->addClass(ZBX_STYLE_CELL_WIDTH)
884							: null,
885						$show_recovery_data ? _('Status') : null,
886						_('Info'),
887						make_sorting_header(_('Host'), 'host', $this->data['sort'], $this->data['sortorder'], $link),
888						make_sorting_header(_('Problem'), 'name', $this->data['sort'], $this->data['sortorder'], $link),
889						($show_opdata == OPERATIONAL_DATA_SHOW_SEPARATELY)
890							? _('Operational data')
891							: null,
892						_('Duration'),
893						_('Ack'),
894						_('Actions'),
895						$this->data['filter']['show_tags'] ? _('Tags') : null
896					]));
897			}
898
899			if ($this->data['filter']['show_tags']) {
900				$tags = makeTags($data['problems'], true, 'eventid', $this->data['filter']['show_tags'],
901					array_key_exists('tags', $this->data['filter']) ? $this->data['filter']['tags'] : [],
902					$this->data['filter']['tag_name_format'], $this->data['filter']['tag_priority']
903				);
904			}
905
906			if ($data['problems']) {
907				$triggers_hosts = makeTriggersHostsList($triggers_hosts);
908			}
909
910			$last_clock = 0;
911			$today = strtotime('today');
912
913			// Make trigger dependencies.
914			if ($data['triggers']) {
915				$dependencies = getTriggerDependencies($data['triggers']);
916			}
917
918			$allowed = [
919				'add_comments' => CWebUser::checkAccess(CRoleHelper::ACTIONS_ADD_PROBLEM_COMMENTS),
920				'change_severity' => CWebUser::checkAccess(CRoleHelper::ACTIONS_CHANGE_SEVERITY),
921				'acknowledge' => CWebUser::checkAccess(CRoleHelper::ACTIONS_ACKNOWLEDGE_PROBLEMS),
922				'close' => CWebUser::checkAccess(CRoleHelper::ACTIONS_CLOSE_PROBLEMS)
923			];
924
925			// Add problems to table.
926			foreach ($data['problems'] as $eventid => $problem) {
927				$trigger = $data['triggers'][$problem['objectid']];
928
929				$cell_clock = ($problem['clock'] >= $today)
930					? zbx_date2str(TIME_FORMAT_SECONDS, $problem['clock'])
931					: zbx_date2str(DATE_TIME_FORMAT_SECONDS, $problem['clock']);
932				$cell_clock = new CCol(new CLink($cell_clock,
933					(new CUrl('tr_events.php'))
934						->setArgument('triggerid', $problem['objectid'])
935						->setArgument('eventid', $problem['eventid'])
936				));
937
938				if ($problem['r_eventid'] != 0) {
939					$cell_r_clock = ($problem['r_clock'] >= $today)
940						? zbx_date2str(TIME_FORMAT_SECONDS, $problem['r_clock'])
941						: zbx_date2str(DATE_TIME_FORMAT_SECONDS, $problem['r_clock']);
942					$cell_r_clock = (new CCol(new CLink($cell_r_clock,
943						(new CUrl('tr_events.php'))
944							->setArgument('triggerid', $problem['objectid'])
945							->setArgument('eventid', $problem['eventid'])
946					)))
947						->addClass(ZBX_STYLE_NOWRAP)
948						->addClass(ZBX_STYLE_RIGHT);
949				}
950				else {
951					$cell_r_clock = '';
952				}
953
954				$can_be_closed = ($trigger['manual_close'] == ZBX_TRIGGER_MANUAL_CLOSE_ALLOWED && $allowed['close']);
955
956				if ($problem['r_eventid'] != 0) {
957					$value = TRIGGER_VALUE_FALSE;
958					$value_str = _('RESOLVED');
959					$value_clock = $problem['r_clock'];
960					$can_be_closed = false;
961				}
962				else {
963					$in_closing = false;
964
965					foreach ($problem['acknowledges'] as $acknowledge) {
966						if (($acknowledge['action'] & ZBX_PROBLEM_UPDATE_CLOSE) == ZBX_PROBLEM_UPDATE_CLOSE) {
967							$in_closing = true;
968							$can_be_closed = false;
969							break;
970						}
971					}
972
973					$value = $in_closing ? TRIGGER_VALUE_FALSE : TRIGGER_VALUE_TRUE;
974					$value_str = $in_closing ? _('CLOSING') : _('PROBLEM');
975					$value_clock = $in_closing ? time() : $problem['clock'];
976				}
977
978				$is_acknowledged = ($problem['acknowledged'] == EVENT_ACKNOWLEDGED);
979				$cell_status = new CSpan($value_str);
980
981				// Add colors and blinking to span depending on configuration and trigger parameters.
982				addTriggerValueStyle($cell_status, $value, $value_clock, $is_acknowledged);
983
984				// Info.
985				$info_icons = [];
986				if ($problem['r_eventid'] != 0) {
987					if ($problem['correlationid'] != 0) {
988						$info_icons[] = makeInformationIcon(
989							array_key_exists($problem['correlationid'], $data['correlations'])
990								? _s('Resolved by correlation rule "%1$s".',
991									$data['correlations'][$problem['correlationid']]['name']
992								)
993								: _('Resolved by correlation rule.')
994						);
995					}
996					elseif ($problem['userid'] != 0) {
997						$info_icons[] = makeInformationIcon(
998							array_key_exists($problem['userid'], $data['users'])
999								? _s('Resolved by user "%1$s".', getUserFullname($data['users'][$problem['userid']]))
1000								: _('Resolved by inaccessible user.')
1001						);
1002					}
1003				}
1004
1005				if (array_key_exists('suppression_data', $problem) && $problem['suppression_data']) {
1006					$info_icons[] = makeSuppressedProblemIcon($problem['suppression_data']);
1007				}
1008
1009				$cell_info = ($this->data['filter']['compact_view'] && $this->data['filter']['show_suppressed']
1010						&& count($info_icons) > 1)
1011					? (new CSpan(
1012							(new CButton(null))
1013								->addClass(ZBX_STYLE_ICON_WZRD_ACTION)
1014								->addStyle('margin-left: -3px;')
1015								->setHint((new CDiv($info_icons))->addClass(ZBX_STYLE_REL_CONTAINER))
1016							))->addClass(ZBX_STYLE_REL_CONTAINER)
1017					: makeInformationList($info_icons);
1018
1019				$description = array_key_exists($trigger['triggerid'], $dependencies)
1020					? makeTriggerDependencies($dependencies[$trigger['triggerid']])
1021					: [];
1022				$description[] = (new CLinkAction($problem['name']))
1023					->addClass(ZBX_STYLE_WORDWRAP)
1024					->setMenuPopup(CMenuPopupHelper::getTrigger($trigger['triggerid'], $problem['eventid']));
1025
1026				$opdata = null;
1027
1028				if ($show_opdata != OPERATIONAL_DATA_SHOW_NONE) {
1029					if ($trigger['opdata'] === '') {
1030						if ($show_opdata == OPERATIONAL_DATA_SHOW_SEPARATELY) {
1031							$opdata = (new CCol(self::getLatestValues($trigger['items'])))->addClass('latest-values');
1032						}
1033					}
1034					else {
1035						$opdata = (new CSpan(CMacrosResolverHelper::resolveTriggerOpdata(
1036							[
1037								'triggerid' => $trigger['triggerid'],
1038								'expression' => $trigger['expression'],
1039								'opdata' => $trigger['opdata'],
1040								'clock' => ($problem['r_eventid'] != 0) ? $problem['r_clock'] : $problem['clock'],
1041								'ns' => ($problem['r_eventid'] != 0) ? $problem['r_ns'] : $problem['ns']
1042							],
1043							[
1044								'events' => true,
1045								'html' => true
1046							]
1047						)))
1048							->addClass('opdata')
1049							->addClass(ZBX_STYLE_WORDWRAP);
1050
1051						if ($show_opdata == OPERATIONAL_DATA_SHOW_WITH_PROBLEM) {
1052							$description[] = ' (';
1053							$description[] = $opdata;
1054							$description[] = ')';
1055						}
1056					}
1057				}
1058
1059				$description[] = ($problem['comments'] !== '') ? makeDescriptionIcon($problem['comments']) : null;
1060
1061				if ($this->data['filter']['details'] == 1) {
1062					$description[] = BR();
1063
1064					if ($trigger['recovery_mode'] == ZBX_RECOVERY_MODE_RECOVERY_EXPRESSION) {
1065						$description[] = [_('Problem'), ': ', (new CDiv($trigger['expression_html']))->addClass(ZBX_STYLE_WORDWRAP), BR()];
1066						$description[] = [_('Recovery'), ': ', (new CDiv($trigger['recovery_expression_html']))->addClass(ZBX_STYLE_WORDWRAP)];
1067					}
1068					else {
1069						$description[] = (new CDiv($trigger['expression_html']))->addClass(ZBX_STYLE_WORDWRAP);
1070					}
1071				}
1072
1073				if ($show_timeline) {
1074					if ($last_clock != 0) {
1075						self::addTimelineBreakpoint($table, $last_clock, $problem['clock'], $this->data['sortorder']);
1076					}
1077					$last_clock = $problem['clock'];
1078
1079					$row = [
1080						$cell_clock->addClass(ZBX_STYLE_TIMELINE_DATE),
1081						(new CCol())
1082							->addClass(ZBX_STYLE_TIMELINE_AXIS)
1083							->addClass(ZBX_STYLE_TIMELINE_DOT),
1084						(new CCol())->addClass(ZBX_STYLE_TIMELINE_TD)
1085					];
1086				}
1087				else {
1088					$row = [
1089						$cell_clock
1090							->addClass(ZBX_STYLE_NOWRAP)
1091							->addClass(ZBX_STYLE_RIGHT)
1092					];
1093				}
1094
1095				// Create acknowledge link.
1096				$problem_update_link = ($allowed['add_comments'] || $allowed['change_severity']
1097						|| $allowed['acknowledge'] || $can_be_closed)
1098					? (new CLink($is_acknowledged ? _('Yes') : _('No')))
1099						->addClass($is_acknowledged ? ZBX_STYLE_GREEN : ZBX_STYLE_RED)
1100						->addClass(ZBX_STYLE_LINK_ALT)
1101						->onClick('acknowledgePopUp('.json_encode(['eventids' => [$problem['eventid']]]).', this);')
1102					: (new CSpan($is_acknowledged ? _('Yes') : _('No')))->addClass(
1103						$is_acknowledged ? ZBX_STYLE_GREEN : ZBX_STYLE_RED
1104					);
1105
1106				// Add table row.
1107				$table->addRow(array_merge($row, [
1108					new CCheckBox('eventids['.$problem['eventid'].']', $problem['eventid']),
1109					getSeverityCell($problem['severity'], null, $value == TRIGGER_VALUE_FALSE),
1110					$show_recovery_data ? $cell_r_clock : null,
1111					$show_recovery_data ? $cell_status : null,
1112					$cell_info,
1113					$this->data['filter']['compact_view']
1114						? (new CDiv($triggers_hosts[$trigger['triggerid']]))->addClass('action-container')
1115						: $triggers_hosts[$trigger['triggerid']],
1116					$this->data['filter']['compact_view']
1117						? (new CDiv($description))->addClass('action-container')
1118						: $description,
1119					($show_opdata == OPERATIONAL_DATA_SHOW_SEPARATELY) ? $opdata : null,
1120					($problem['r_eventid'] != 0)
1121						? zbx_date2age($problem['clock'], $problem['r_clock'])
1122						: zbx_date2age($problem['clock']),
1123					$problem_update_link,
1124					makeEventActionsIcons($problem['eventid'], $data['actions'], $data['users']),
1125					$this->data['filter']['show_tags'] ? $tags[$problem['eventid']] : null
1126				]), ($this->data['filter']['highlight_row'] && $value == TRIGGER_VALUE_TRUE)
1127					? getSeverityFlhStyle($problem['severity'])
1128					: null
1129				);
1130			}
1131
1132			$footer = new CActionButtonList('action', 'eventids', [
1133				'popup.acknowledge.edit' => [
1134					'name' => _('Mass update'),
1135					'disabled' => !($allowed['add_comments'] || $allowed['change_severity'] || $allowed['acknowledge']
1136							|| $allowed['close']
1137					)
1138				]
1139			], 'problem');
1140
1141			return $this->getOutput($form->addItem([$table, $paging, $footer]), true, $this->data);
1142		}
1143
1144		/*
1145		 * Search limit performs +1 selection to know if limit was exceeded, this will assure that csv has
1146		 * "search_limit" records at most.
1147		 */
1148		array_splice($data['problems'], CSettingsHelper::get(CSettingsHelper::SEARCH_LIMIT));
1149
1150		$csv = [];
1151
1152		$csv[] = array_filter([
1153			_('Severity'),
1154			_('Time'),
1155			_('Recovery time'),
1156			_('Status'),
1157			_('Host'),
1158			_('Problem'),
1159			($show_opdata == OPERATIONAL_DATA_SHOW_SEPARATELY) ? _('Operational data') : null,
1160			_('Duration'),
1161			_('Ack'),
1162			_('Actions'),
1163			_('Tags')
1164		]);
1165
1166		$tags = makeTags($data['problems'], false);
1167
1168		foreach ($data['problems'] as $problem) {
1169			$trigger = $data['triggers'][$problem['objectid']];
1170
1171			if ($problem['r_eventid'] != 0) {
1172				$value_str = _('RESOLVED');
1173			}
1174			else {
1175				$in_closing = false;
1176
1177				foreach ($problem['acknowledges'] as $acknowledge) {
1178					if (($acknowledge['action'] & ZBX_PROBLEM_UPDATE_CLOSE) == ZBX_PROBLEM_UPDATE_CLOSE) {
1179						$in_closing = true;
1180						break;
1181					}
1182				}
1183
1184				$value_str = $in_closing ? _('CLOSING') : _('PROBLEM');
1185			}
1186
1187			$hosts = [];
1188			foreach ($triggers_hosts[$trigger['triggerid']] as $trigger_host) {
1189				$hosts[] = $trigger_host['name'];
1190			}
1191
1192			// operational data
1193			$opdata = null;
1194			if ($show_opdata != OPERATIONAL_DATA_SHOW_NONE) {
1195				if ($trigger['opdata'] === '') {
1196					if ($show_opdata == OPERATIONAL_DATA_SHOW_SEPARATELY) {
1197						$opdata = self::getLatestValues($trigger['items'], false);
1198					}
1199				}
1200				else {
1201					$opdata = CMacrosResolverHelper::resolveTriggerOpdata(
1202						[
1203							'triggerid' => $trigger['triggerid'],
1204							'expression' => $trigger['expression'],
1205							'opdata' => $trigger['opdata'],
1206							'clock' => ($problem['r_eventid'] != 0) ? $problem['r_clock'] : $problem['clock'],
1207							'ns' => ($problem['r_eventid'] != 0) ? $problem['r_ns'] : $problem['ns']
1208						],
1209						['events' => true]
1210					);
1211				}
1212			}
1213
1214			$actions_performed = [];
1215			if ($data['actions']['messages'][$problem['eventid']]['count'] > 0) {
1216				$actions_performed[] = _('Messages').
1217					' ('.$data['actions']['messages'][$problem['eventid']]['count'].')';
1218			}
1219			if ($data['actions']['severities'][$problem['eventid']]['count'] > 0) {
1220				$actions_performed[] = _('Severity changes');
1221			}
1222			if ($data['actions']['actions'][$problem['eventid']]['count'] > 0) {
1223				$actions_performed[] = _('Actions').' ('.$data['actions']['actions'][$problem['eventid']]['count'].')';
1224			}
1225
1226			$row = [];
1227
1228			$row[] = getSeverityName($problem['severity']);
1229			$row[] = zbx_date2str(DATE_TIME_FORMAT_SECONDS, $problem['clock']);
1230			$row[] = ($problem['r_eventid'] != 0) ? zbx_date2str(DATE_TIME_FORMAT_SECONDS, $problem['r_clock']) : '';
1231			$row[] = $value_str;
1232			$row[] = implode(', ', $hosts);
1233			$row[] = ($show_opdata == OPERATIONAL_DATA_SHOW_WITH_PROBLEM && $trigger['opdata'] !== '')
1234				? $problem['name'].' ('.$opdata.')'
1235				: $problem['name'];
1236
1237			if ($show_opdata == OPERATIONAL_DATA_SHOW_SEPARATELY) {
1238				$row[] = $opdata;
1239			}
1240
1241			$row[] = ($problem['r_eventid'] != 0)
1242				? zbx_date2age($problem['clock'], $problem['r_clock'])
1243				: zbx_date2age($problem['clock']);
1244			$row[] = ($problem['acknowledged'] == EVENT_ACKNOWLEDGED) ? _('Yes') : _('No');
1245			$row[] = implode(', ', $actions_performed);
1246			$row[] = implode(', ', $tags[$problem['eventid']]);
1247
1248			$csv[] = $row;
1249		}
1250
1251		return zbx_toCSV($csv);
1252	}
1253
1254	/**
1255	 * Get item latest values.
1256	 *
1257	 * @static
1258	 *
1259	 * @param array $items    An array of trigger items.
1260	 * @param bool  $html
1261	 *
1262	 * @return array|string
1263	 */
1264	public static function getLatestValues(array $items, bool $html = true) {
1265		$latest_values = [];
1266
1267		$items = zbx_toHash($items, 'itemid');
1268		$history_values = Manager::History()->getLastValues($items, 1, timeUnitToSeconds(CSettingsHelper::get(
1269			CSettingsHelper::HISTORY_PERIOD
1270		)));
1271
1272		if ($html) {
1273			$hint_table = (new CTable())->addClass('list-table');
1274		}
1275
1276		foreach ($items as $itemid => $item) {
1277			if (array_key_exists($itemid, $history_values)) {
1278				$last_value = reset($history_values[$itemid]);
1279				$last_value['value'] = formatHistoryValue(str_replace(["\r\n", "\n"], [" "], $last_value['value']),
1280					$item
1281				);
1282			}
1283			else {
1284				$last_value = [
1285					'itemid' => null,
1286					'clock' => null,
1287					'value' => UNRESOLVED_MACRO_STRING,
1288					'ns' => null
1289				];
1290			}
1291
1292			if ($html) {
1293				$hint_table->addRow([
1294					new CCol($item['name_expanded']),
1295					new CCol(
1296						($last_value['clock'] !== null)
1297							? zbx_date2str(DATE_TIME_FORMAT_SECONDS, $last_value['clock'])
1298							: UNRESOLVED_MACRO_STRING
1299					),
1300					new CCol($last_value['value']),
1301					new CCol(
1302						($item['value_type'] == ITEM_VALUE_TYPE_FLOAT || $item['value_type'] == ITEM_VALUE_TYPE_UINT64)
1303							? (CWebUser::checkAccess(CRoleHelper::UI_MONITORING_LATEST_DATA)
1304								? new CLink(_('Graph'), (new CUrl('history.php'))
1305									->setArgument('action', HISTORY_GRAPH)
1306									->setArgument('itemids[]', $itemid)
1307									->getUrl()
1308								)
1309								: _('Graph'))
1310							: (CWebUser::checkAccess(CRoleHelper::UI_MONITORING_LATEST_DATA)
1311								? new CLink(_('History'), (new CUrl('history.php'))
1312									->setArgument('action', HISTORY_VALUES)
1313									->setArgument('itemids[]', $itemid)
1314									->getUrl()
1315								)
1316								: _('History'))
1317					)
1318				]);
1319
1320				$latest_values[] = (new CLinkAction($last_value['value']))
1321					->addClass('hint-item')
1322					->setAttribute('data-hintbox', '1');
1323				$latest_values[] = ', ';
1324			}
1325			else {
1326				$latest_values[] = $last_value['value'];
1327			}
1328		}
1329
1330		if ($html) {
1331			array_pop($latest_values);
1332			array_unshift($latest_values, (new CDiv())
1333				->addClass('main-hint')
1334				->setHint($hint_table)
1335			);
1336
1337			return $latest_values;
1338		}
1339
1340		return implode(', ', $latest_values);
1341	}
1342}
1343