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