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 * Get trigger severity full line height css style name.
24 *
25 * @param int $severity  Trigger severity.
26 *
27 * @return string|null
28 */
29function getSeverityFlhStyle($severity) {
30	switch ($severity) {
31		case TRIGGER_SEVERITY_DISASTER:
32			return ZBX_STYLE_FLH_DISASTER_BG;
33		case TRIGGER_SEVERITY_HIGH:
34			return ZBX_STYLE_FLH_HIGH_BG;
35		case TRIGGER_SEVERITY_AVERAGE:
36			return ZBX_STYLE_FLH_AVERAGE_BG;
37		case TRIGGER_SEVERITY_WARNING:
38			return ZBX_STYLE_FLH_WARNING_BG;
39		case TRIGGER_SEVERITY_INFORMATION:
40			return ZBX_STYLE_FLH_INFO_BG;
41		case TRIGGER_SEVERITY_NOT_CLASSIFIED:
42			return ZBX_STYLE_FLH_NA_BG;
43		default:
44			return null;
45	}
46}
47
48/**
49 * Get trigger severity status css style name.
50 *
51 * @param int $severity  Trigger severity.
52 *
53 * @return string|null
54 */
55function getSeverityStatusStyle($severity) {
56	switch ($severity) {
57		case TRIGGER_SEVERITY_DISASTER:
58			return ZBX_STYLE_STATUS_DISASTER_BG;
59		case TRIGGER_SEVERITY_HIGH:
60			return ZBX_STYLE_STATUS_HIGH_BG;
61		case TRIGGER_SEVERITY_AVERAGE:
62			return ZBX_STYLE_STATUS_AVERAGE_BG;
63		case TRIGGER_SEVERITY_WARNING:
64			return ZBX_STYLE_STATUS_WARNING_BG;
65		case TRIGGER_SEVERITY_INFORMATION:
66			return ZBX_STYLE_STATUS_INFO_BG;
67		case TRIGGER_SEVERITY_NOT_CLASSIFIED:
68			return ZBX_STYLE_STATUS_NA_BG;
69		default:
70			return null;
71	}
72}
73
74function getSeverityStyle($severity, $type = true) {
75	if (!$type) {
76		return ZBX_STYLE_NORMAL_BG;
77	}
78
79	switch ($severity) {
80		case TRIGGER_SEVERITY_DISASTER:
81			return ZBX_STYLE_DISASTER_BG;
82		case TRIGGER_SEVERITY_HIGH:
83			return ZBX_STYLE_HIGH_BG;
84		case TRIGGER_SEVERITY_AVERAGE:
85			return ZBX_STYLE_AVERAGE_BG;
86		case TRIGGER_SEVERITY_WARNING:
87			return ZBX_STYLE_WARNING_BG;
88		case TRIGGER_SEVERITY_INFORMATION:
89			return ZBX_STYLE_INFO_BG;
90		case TRIGGER_SEVERITY_NOT_CLASSIFIED:
91			return ZBX_STYLE_NA_BG;
92		default:
93			return null;
94	}
95}
96
97/**
98 * Get trigger severity name by given state and configuration.
99 *
100 * @param int   $severity  Trigger severity.
101 *
102 * @return string
103 */
104function getSeverityName($severity) {
105	switch ($severity) {
106		case TRIGGER_SEVERITY_NOT_CLASSIFIED:
107			return _(CSettingsHelper::get(CSettingsHelper::SEVERITY_NAME_0));
108		case TRIGGER_SEVERITY_INFORMATION:
109			return _(CSettingsHelper::get(CSettingsHelper::SEVERITY_NAME_1));
110		case TRIGGER_SEVERITY_WARNING:
111			return _(CSettingsHelper::get(CSettingsHelper::SEVERITY_NAME_2));
112		case TRIGGER_SEVERITY_AVERAGE:
113			return _(CSettingsHelper::get(CSettingsHelper::SEVERITY_NAME_3));
114		case TRIGGER_SEVERITY_HIGH:
115			return _(CSettingsHelper::get(CSettingsHelper::SEVERITY_NAME_4));
116		case TRIGGER_SEVERITY_DISASTER:
117			return _(CSettingsHelper::get(CSettingsHelper::SEVERITY_NAME_5));
118		default:
119			return _('Unknown');
120	}
121}
122
123function getSeverityColor($severity, $value = TRIGGER_VALUE_TRUE) {
124	if ($value == TRIGGER_VALUE_FALSE) {
125		return 'AAFFAA';
126	}
127
128	switch ($severity) {
129		case TRIGGER_SEVERITY_DISASTER:
130			$color = CSettingsHelper::get(CSettingsHelper::SEVERITY_COLOR_5);
131			break;
132		case TRIGGER_SEVERITY_HIGH:
133			$color = CSettingsHelper::get(CSettingsHelper::SEVERITY_COLOR_4);
134			break;
135		case TRIGGER_SEVERITY_AVERAGE:
136			$color = CSettingsHelper::get(CSettingsHelper::SEVERITY_COLOR_3);
137			break;
138		case TRIGGER_SEVERITY_WARNING:
139			$color = CSettingsHelper::get(CSettingsHelper::SEVERITY_COLOR_2);
140			break;
141		case TRIGGER_SEVERITY_INFORMATION:
142			$color = CSettingsHelper::get(CSettingsHelper::SEVERITY_COLOR_1);
143			break;
144		case TRIGGER_SEVERITY_NOT_CLASSIFIED:
145			$color = CSettingsHelper::get(CSettingsHelper::SEVERITY_COLOR_0);
146			break;
147		default:
148			$color = CSettingsHelper::get(CSettingsHelper::SEVERITY_COLOR_0);
149	}
150
151	return $color;
152}
153
154/**
155 * Generate array with severities options.
156 *
157 * @param int $min  Minimal severity.
158 * @param int $max  Maximum severity.
159 *
160 * @return array
161 */
162function getSeverities($min = TRIGGER_SEVERITY_NOT_CLASSIFIED, $max = TRIGGER_SEVERITY_COUNT - 1) {
163	$severities = [];
164
165	foreach (range($min, $max) as $severity) {
166		$severities[] = [
167			'name' => getSeverityName($severity),
168			'value' => $severity,
169			'style' => getSeverityStyle($severity)
170		];
171	}
172
173	return $severities;
174}
175
176/**
177 * Returns HTML representation of trigger severity cell containing severity name and color.
178 *
179 * @param int         $severity       Trigger, Event or Problem severity.
180 * @param string|null $text           Trigger severity name.
181 * @param bool        $force_normal   True to return 'normal' class, false to return corresponding severity class.
182 * @param bool        $return_as_div  True to return severity cell as DIV element.
183 *
184 * @return CDiv|CCol
185 */
186function getSeverityCell($severity, $text = null, $force_normal = false, $return_as_div = false) {
187	if ($text === null) {
188		$text = CHtml::encode(getSeverityName($severity));
189	}
190
191	if ($force_normal) {
192		return new CCol($text);
193	}
194
195	$return = $return_as_div ? new CDiv($text) : new CCol($text);
196	return $return->addClass(getSeverityStyle($severity));
197}
198
199/**
200 * Add color style and blinking to an object like CSpan or CDiv depending on trigger status.
201 * Settings and colors are kept in 'config' database table.
202 *
203 * @param mixed $object             object like CSpan, CDiv, etc.
204 * @param int $triggerValue         TRIGGER_VALUE_FALSE or TRIGGER_VALUE_TRUE
205 * @param int $triggerLastChange
206 * @param bool $isAcknowledged
207 */
208function addTriggerValueStyle($object, $triggerValue, $triggerLastChange, $isAcknowledged) {
209	$color_class = null;
210	$blinks = null;
211
212	// Color class for text and blinking depends on trigger value and whether event is acknowledged.
213	if ($triggerValue == TRIGGER_VALUE_TRUE && !$isAcknowledged) {
214		$color_class = ZBX_STYLE_PROBLEM_UNACK_FG;
215		$blinks = CSettingsHelper::get(CSettingsHelper::PROBLEM_UNACK_STYLE);
216	}
217	elseif ($triggerValue == TRIGGER_VALUE_TRUE && $isAcknowledged) {
218		$color_class = ZBX_STYLE_PROBLEM_ACK_FG;
219		$blinks = CSettingsHelper::get(CSettingsHelper::PROBLEM_ACK_STYLE);
220	}
221	elseif ($triggerValue == TRIGGER_VALUE_FALSE && !$isAcknowledged) {
222		$color_class = ZBX_STYLE_OK_UNACK_FG;
223		$blinks = CSettingsHelper::get(CSettingsHelper::OK_UNACK_STYLE);
224	}
225	elseif ($triggerValue == TRIGGER_VALUE_FALSE && $isAcknowledged) {
226		$color_class = ZBX_STYLE_OK_ACK_FG;
227		$blinks = CSettingsHelper::get(CSettingsHelper::OK_ACK_STYLE);
228	}
229
230	if ($color_class != null && $blinks != null) {
231		$object->addClass($color_class);
232
233		// blinking
234		$timeSinceLastChange = time() - $triggerLastChange;
235		$blink_period = timeUnitToSeconds(CSettingsHelper::get(CSettingsHelper::BLINK_PERIOD));
236
237		if ($blinks && $timeSinceLastChange < $blink_period) {
238			$object->addClass('blink'); // elements with this class will blink
239			$object->setAttribute('data-time-to-blink', $blink_period - $timeSinceLastChange);
240		}
241	}
242	else {
243		$object->addClass(ZBX_STYLE_GREY);
244	}
245}
246
247function trigger_value2str($value = null) {
248	$triggerValues = [
249		TRIGGER_VALUE_FALSE => _('OK'),
250		TRIGGER_VALUE_TRUE => _('PROBLEM')
251	];
252
253	if ($value === null) {
254		return $triggerValues;
255	}
256	elseif (isset($triggerValues[$value])) {
257		return $triggerValues[$value];
258	}
259
260	return _('Unknown');
261}
262
263function get_trigger_by_triggerid($triggerid) {
264	$db_trigger = DBfetch(DBselect('SELECT t.* FROM triggers t WHERE t.triggerid='.zbx_dbstr($triggerid)));
265	if (!empty($db_trigger)) {
266		return $db_trigger;
267	}
268	error(_s('No trigger with triggerid "%1$s".', $triggerid));
269
270	return false;
271}
272
273function get_hosts_by_triggerid($triggerids) {
274	zbx_value2array($triggerids);
275
276	return DBselect(
277		'SELECT DISTINCT h.*'.
278		' FROM hosts h,functions f,items i'.
279		' WHERE i.itemid=f.itemid'.
280			' AND h.hostid=i.hostid'.
281			' AND '.dbConditionInt('f.triggerid', $triggerids)
282	);
283}
284
285function get_triggers_by_hostid($hostid) {
286	return DBselect(
287		'SELECT DISTINCT t.*'.
288		' FROM triggers t,functions f,items i'.
289		' WHERE i.hostid='.zbx_dbstr($hostid).
290			' AND f.itemid=i.itemid'.
291			' AND f.triggerid=t.triggerid'
292	);
293}
294
295// unescape Raw URL
296function utf8RawUrlDecode($source) {
297	$decodedStr = '';
298	$pos = 0;
299	$len = strlen($source);
300	while ($pos < $len) {
301		$charAt = substr($source, $pos, 1);
302		if ($charAt == '%') {
303			$pos++;
304			$charAt = substr($source, $pos, 1);
305			if ($charAt == 'u') {
306				// we got a unicode character
307				$pos++;
308				$unicodeHexVal = substr($source, $pos, 4);
309				$unicode = hexdec($unicodeHexVal);
310				$entity = "&#".$unicode.';';
311				$decodedStr .= html_entity_decode(utf8_encode($entity), ENT_COMPAT, 'UTF-8');
312				$pos += 4;
313			}
314			else {
315				$decodedStr .= substr($source, $pos-1, 1);
316			}
317		}
318		else {
319			$decodedStr .= $charAt;
320			$pos++;
321		}
322	}
323
324	return $decodedStr;
325}
326
327/**
328 * Copies the given triggers to the given hosts or templates.
329 *
330 * Without the $src_hostid parameter it will only be able to copy triggers that belong to only one host. If the
331 * $src_hostid parameter is not passed, and a trigger has multiple hosts, it will throw an error. If the
332 * $src_hostid parameter is passed, the given host will be replaced with the destination host.
333 *
334 * This function takes care of copied trigger dependencies.
335 * If trigger is copied alongside with trigger on which it depends, then dependencies is replaced directly using new ids,
336 * If there is target host within dependency trigger, algorithm will search for potential matching trigger in target host,
337 * if matching trigger is found, then id from this trigger is used, if not rise exception,
338 * otherwise original dependency will be left.
339 *
340 *
341 * @param array $src_triggerids		Triggers which will be copied to $dst_hostids
342 * @param array $dst_hostids		Hosts and templates to whom add triggers. IDs not present in DB (host table)
343 *									will be ignored.
344 * @param int	$src_hostid			Host ID in which context trigger with multiple hosts will be treated.
345 *
346 * @return bool
347 */
348function copyTriggersToHosts($src_triggerids, $dst_hostids, $src_hostid = null) {
349	$options = [
350		'output' => ['triggerid', 'expression', 'description', 'url', 'status', 'priority', 'comments', 'type',
351			'recovery_mode', 'recovery_expression', 'correlation_mode', 'correlation_tag', 'manual_close', 'opdata',
352			'event_name'
353		],
354		'selectDependencies' => ['triggerid'],
355		'selectTags' => ['tag', 'value'],
356		'triggerids' => $src_triggerids
357	];
358
359	if ($src_hostid) {
360		$srcHost = API::Host()->get([
361			'output' => ['host'],
362			'hostids' => $src_hostid,
363			'preservekeys' => true,
364			'templated_hosts' => true
365		]);
366
367		if (!$srcHost = reset($srcHost)) {
368			return false;
369		}
370	}
371	else {
372		// Select source trigger first host 'host'.
373		$options['selectHosts'] = ['host'];
374	}
375
376	$dbSrcTriggers = API::Trigger()->get($options);
377
378	$dbSrcTriggers = CMacrosResolverHelper::resolveTriggerExpressions($dbSrcTriggers,
379		['sources' => ['expression', 'recovery_expression']]
380	);
381
382	$dbDstHosts = API::Host()->get([
383		'output' => ['hostid', 'host'],
384		'hostids' => $dst_hostids,
385		'preservekeys' => true,
386		'templated_hosts' => true
387	]);
388
389	$newTriggers = [];
390
391	foreach ($dbDstHosts as $dstHost) {
392		// Create each trigger for each host.
393
394		foreach ($dbSrcTriggers as $srcTrigger) {
395			if ($src_hostid) {
396				// Get host 'host' for triggerExpressionReplaceHost().
397
398				$host = $srcHost['host'];
399				$srcTriggerContextHostId = $src_hostid;
400			}
401			else {
402				if (count($srcTrigger['hosts']) > 1) {
403					error(_s('Cannot copy trigger "%1$s:%2$s", because it has multiple hosts in the expression.',
404						$srcTrigger['description'], $srcTrigger['expression']
405					));
406
407					return false;
408				}
409
410				// Use source trigger first host 'host'.
411				$host = $srcTrigger['hosts'][0]['host'];
412				$srcTriggerContextHostId = $srcTrigger['hosts'][0]['hostid'];
413			}
414
415			$srcTrigger['expression'] = triggerExpressionReplaceHost($srcTrigger['expression'], $host,
416				$dstHost['host']
417			);
418
419			if ($srcTrigger['recovery_mode'] == ZBX_RECOVERY_MODE_RECOVERY_EXPRESSION) {
420				$srcTrigger['recovery_expression'] = triggerExpressionReplaceHost($srcTrigger['recovery_expression'],
421					$host, $dstHost['host']
422				);
423			}
424
425			// The dependencies must be added after all triggers are created.
426			$result = API::Trigger()->create([[
427				'description' => $srcTrigger['description'],
428				'event_name' => $srcTrigger['event_name'],
429				'opdata' => $srcTrigger['opdata'],
430				'expression' => $srcTrigger['expression'],
431				'url' => $srcTrigger['url'],
432				'status' => $srcTrigger['status'],
433				'priority' => $srcTrigger['priority'],
434				'comments' => $srcTrigger['comments'],
435				'type' => $srcTrigger['type'],
436				'recovery_mode' => $srcTrigger['recovery_mode'],
437				'recovery_expression' => $srcTrigger['recovery_expression'],
438				'correlation_mode' => $srcTrigger['correlation_mode'],
439				'correlation_tag' => $srcTrigger['correlation_tag'],
440				'tags' => $srcTrigger['tags'],
441				'manual_close' => $srcTrigger['manual_close']
442			]]);
443
444			if (!$result) {
445				return false;
446			}
447
448			$newTriggers[$srcTrigger['triggerid']][] = [
449				'newTriggerId' => reset($result['triggerids']),
450				'newTriggerExpression' => $srcTrigger['expression'],
451				'newTriggerHostId' => $dstHost['hostid'],
452				'newTriggerHost' => $dstHost['host'],
453				'srcTriggerContextHostId' => $srcTriggerContextHostId,
454				'srcTriggerContextHost' => $host
455			];
456		}
457	}
458
459	$depids = [];
460	foreach ($dbSrcTriggers as $srcTrigger) {
461		foreach ($srcTrigger['dependencies'] as $depTrigger) {
462			$depids[] = $depTrigger['triggerid'];
463		}
464	}
465	$depTriggers = API::Trigger()->get([
466		'triggerids' => $depids,
467		'output' => ['description', 'expression', 'recovery_mode', 'recovery_expression'],
468		'selectHosts' => ['hostid'],
469		'preservekeys' => true
470	]);
471
472	$depTriggers = CMacrosResolverHelper::resolveTriggerExpressions($depTriggers,
473		['sources' => ['expression', 'recovery_expression']]
474	);
475
476	if ($newTriggers) {
477		// Map dependencies to the new trigger IDs and save.
478
479		$dependencies = [];
480
481		foreach ($dbSrcTriggers as $srcTrigger) {
482			if ($srcTrigger['dependencies']) {
483				// Get corresponding created triggers.
484				$dst_triggers = $newTriggers[$srcTrigger['triggerid']];
485
486				foreach ($dst_triggers as $dst_trigger) {
487					foreach ($srcTrigger['dependencies'] as $depTrigger) {
488						/*
489						 * We have added $depTrigger trigger and we know corresponding trigger ID for newly
490						 * created trigger.
491						 */
492						if (array_key_exists($depTrigger['triggerid'], $newTriggers)) {
493							$dst_dep_triggers = $newTriggers[$depTrigger['triggerid']];
494
495							foreach ($dst_dep_triggers as $dst_dep_trigger) {
496								/*
497								 * Dependency is within same host according to $src_hostid parameter or dep trigger has
498								 * single host.
499								 */
500								if ($dst_trigger['srcTriggerContextHostId'] == $dst_dep_trigger['srcTriggerContextHostId']
501										&& $dst_dep_trigger['newTriggerHostId'] == $dst_trigger['newTriggerHostId']) {
502									$depTriggerId = $dst_dep_trigger['newTriggerId'];
503									break;
504								}
505								// Dependency is to trigger from another host.
506								else {
507									$depTriggerId = $depTrigger['triggerid'];
508								}
509							}
510						}
511						// We need to search for $depTrigger trigger if target host is within dependency hosts.
512						elseif (in_array(['hostid' => $dst_trigger['srcTriggerContextHostId']],
513								$depTriggers[$depTrigger['triggerid']]['hosts'])) {
514							// Get all possible $depTrigger matching triggers by description.
515							$targetHostTriggersByDescription = API::Trigger()->get([
516								'hostids' => $dst_trigger['newTriggerHostId'],
517								'output' => ['hosts', 'triggerid', 'expression'],
518								'filter' => ['description' => $depTriggers[$depTrigger['triggerid']]['description']],
519								'preservekeys' => true
520							]);
521
522							$targetHostTriggersByDescription =
523								CMacrosResolverHelper::resolveTriggerExpressions($targetHostTriggersByDescription);
524
525							// Compare exploded expressions for exact match.
526							$expr1 = $depTriggers[$depTrigger['triggerid']]['expression'];
527							$depTriggerId = null;
528
529							foreach ($targetHostTriggersByDescription as $potentialTargetTrigger) {
530								$expr2 = triggerExpressionReplaceHost($potentialTargetTrigger['expression'],
531									$dst_trigger['newTriggerHost'], $dst_trigger['srcTriggerContextHost']
532								);
533
534								if ($expr2 == $expr1) {
535									// Matching trigger has been found.
536									$depTriggerId = $potentialTargetTrigger['triggerid'];
537									break;
538								}
539							}
540
541							// If matching trigger wasn't found raise exception.
542							if ($depTriggerId === null) {
543								$expr2 = triggerExpressionReplaceHost($expr1, $dst_trigger['srcTriggerContextHost'],
544									$dst_trigger['newTriggerHost']
545								);
546
547								error(_s(
548									'Cannot add dependency from trigger "%1$s:%2$s" to non existing trigger "%3$s:%4$s".',
549									$srcTrigger['description'], $dst_trigger['newTriggerExpression'],
550									$depTriggers[$depTrigger['triggerid']]['description'], $expr2
551								));
552
553								return false;
554							}
555						}
556						else {
557							// Leave original dependency.
558
559							$depTriggerId = $depTrigger['triggerid'];
560						}
561
562						$dependencies[] = [
563							'triggerid' => $dst_trigger['newTriggerId'],
564							'dependsOnTriggerid' => $depTriggerId
565						];
566					}
567				}
568			}
569		}
570
571		if ($dependencies) {
572			if (!API::Trigger()->addDependencies($dependencies)) {
573				return false;
574			}
575		}
576	}
577
578	return true;
579}
580
581/**
582 * Purpose: Replaces host in trigger expression.
583 * nodata(/localhost/agent.ping, 5m)  =>  nodata(/localhost6/agent.ping, 5m)
584 *
585 * @param string $expression	full expression with host names and item keys
586 * @param string $src_host
587 * @param string $dst_host
588 *
589 * @return string
590 */
591function triggerExpressionReplaceHost(string $expression, string $src_host, string $dst_host): string {
592	$expression_parser = new CExpressionParser(['usermacros' => true, 'lldmacros' => true]);
593
594	if ($expression_parser->parse($expression) == CParser::PARSE_SUCCESS) {
595		$hist_functions = $expression_parser->getResult()->getTokensOfTypes(
596			[CExpressionParserResult::TOKEN_TYPE_HIST_FUNCTION]
597		);
598		$hist_function = end($hist_functions);
599		do {
600			$query_parameter = $hist_function['data']['parameters'][0];
601			if ($query_parameter['data']['host'] === $src_host) {
602				$expression = substr_replace($expression, '/'.$dst_host.'/'.$query_parameter['data']['item'],
603					$query_parameter['pos'], $query_parameter['length']
604				);
605			}
606		}
607		while ($hist_function = prev($hist_functions));
608	}
609
610	return $expression;
611}
612
613function replace_template_dependencies($deps, $hostid) {
614	foreach ($deps as $id => $val) {
615		$sql = 'SELECT t.triggerid'.
616				' FROM triggers t,functions f,items i'.
617				' WHERE t.triggerid=f.triggerid'.
618					' AND f.itemid=i.itemid'.
619					' AND t.templateid='.zbx_dbstr($val).
620					' AND i.hostid='.zbx_dbstr($hostid);
621		if ($db_new_dep = DBfetch(DBselect($sql))) {
622			$deps[$id] = $db_new_dep['triggerid'];
623		}
624	}
625
626	return $deps;
627}
628
629/**
630 * Prepare arrays containing only hosts and triggers that will be shown results table.
631 *
632 * @param array $db_hosts
633 * @param array $db_triggers
634 *
635 * @return array
636 */
637function getTriggersOverviewTableData(array $db_hosts, array $db_triggers): array {
638	// Prepare triggers to show in results table.
639	$triggers_by_name = [];
640	foreach ($db_triggers as $trigger) {
641		foreach ($trigger['hosts'] as $host) {
642			if (!array_key_exists($host['hostid'], $db_hosts)) {
643				continue;
644			}
645
646			$triggers_by_name[$trigger['description']][$host['hostid']] = $trigger['triggerid'];
647		}
648	}
649
650	$limit = (int) CSettingsHelper::get(CSettingsHelper::MAX_OVERVIEW_TABLE_SIZE);
651	$exceeded_trigs = (count($triggers_by_name) > $limit);
652	$triggers_by_name = array_slice($triggers_by_name, 0, $limit, true);
653	foreach ($triggers_by_name as $name => $triggers) {
654		$triggers_by_name[$name] = array_slice($triggers, 0, $limit, true);
655	}
656
657	// Prepare hosts to show in results table.
658	$exceeded_hosts = false;
659	$hosts_by_name = [];
660	foreach ($db_hosts as $host) {
661		if (count($hosts_by_name) >= $limit) {
662			$exceeded_hosts = true;
663			break;
664		}
665		else {
666			$hosts_by_name[$host['name']] = $host['hostid'];
667		}
668	}
669
670	return [$triggers_by_name, $hosts_by_name, ($exceeded_hosts || $exceeded_trigs)];
671}
672
673/**
674 * @param array   $groupids
675 * @param array   $host_options
676 * @param array   $trigger_options
677 * @param array   $problem_options
678 * @param int     $problem_options['min_severity']         (optional) Minimal problem severity.
679 * @param int     $problem_options['show_suppressed']      (optional) Whether to show triggers with suppressed problems.
680 * @param int     $problem_options['time_from']            (optional) The time starting from which the problems were created.
681 * @param array   $problem_options['tags']                 (optional)
682 * @param string  $problem_options['tags'][]['tag']        (optional)
683 * @param int     $problem_options['tags'][]['operation']  (optional)
684 * @param string  $problem_options['tags'][]['value']      (optional)
685 * @param int     $problem_options['evaltype']		       (optional)
686 *
687 * @return array
688 */
689function getTriggersOverviewData(array $groupids, array $host_options = [], array $trigger_options = [],
690		array $problem_options = []): array {
691
692	$host_options = [
693		'output' => ['hostid', 'name'],
694		'groupids' => $groupids ? $groupids : null,
695		'with_monitored_triggers' => true,
696		'preservekeys' => true
697	] + $host_options;
698
699	$trigger_options = [
700		'output' => ['triggerid', 'expression', 'description', 'value', 'priority', 'lastchange', 'flags', 'comments',
701			'manual_close'
702		],
703		'selectHosts' => ['hostid', 'name'],
704		'selectDependencies' => ['triggerid'],
705		'monitored' => true
706	] + $trigger_options;
707
708	$problem_options += [
709		'show_suppressed' => ZBX_PROBLEM_SUPPRESSED_FALSE
710	];
711
712	$limit = (int) CSettingsHelper::get(CSettingsHelper::MAX_OVERVIEW_TABLE_SIZE);
713
714	do {
715		$db_hosts = API::Host()->get(['limit' => $limit + 1] + $host_options);
716		$fetch_hosts = (count($db_hosts) > $limit);
717
718		$db_triggers = getTriggersWithActualSeverity([
719			'hostids' => array_keys($db_hosts)
720		] + $trigger_options, $problem_options);
721
722		if (!$db_triggers) {
723			$db_hosts = [];
724		}
725
726		// Unset hosts without having matching triggers.
727		$represented_hosts = [];
728		foreach ($db_triggers as $trigger) {
729			$hostids = array_column($trigger['hosts'], 'hostid');
730			$represented_hosts += array_combine($hostids, $hostids);
731		}
732
733		$db_hosts = array_intersect_key($db_hosts, $represented_hosts);
734
735		$fetch_hosts &= (count($db_hosts) < $limit);
736		$limit += (int) CSettingsHelper::get(CSettingsHelper::MAX_OVERVIEW_TABLE_SIZE);
737
738	} while ($fetch_hosts);
739
740	CArrayHelper::sort($db_hosts, [
741		['field' => 'name', 'order' => ZBX_SORT_UP]
742	]);
743
744	$db_triggers = CMacrosResolverHelper::resolveTriggerNames($db_triggers, true);
745	$dependencies = $db_triggers ? getTriggerDependencies($db_triggers) : [];
746
747	CArrayHelper::sort($db_triggers, [
748		['field' => 'description', 'order' => ZBX_SORT_UP]
749	]);
750
751	[$triggers_by_name, $hosts_by_name, $exceeded_limit] = getTriggersOverviewTableData($db_hosts, $db_triggers);
752
753	return [$db_hosts, $db_triggers, $dependencies, $triggers_by_name, $hosts_by_name, $exceeded_limit];
754}
755
756/**
757 * Get triggers data with priority set to highest priority of unresolved problems generated by this trigger.
758 *
759 * @param array $trigger_options                           API options. Array 'output' should contain 'value', option
760 *                                                         'preservekeys' should be set to true.
761 * @param array   $problem_options
762 * @param int     $problem_options['show_suppressed']      Whether to show triggers with suppressed problems.
763 * @param int     $problem_options['min_severity']         (optional) Minimal problem severity.
764 * @param int     $problem_options['time_from']            (optional) The time starting from which the problems were
765 *                                                         created.
766 * @param bool    $problem_options['acknowledged']         (optional) Whether to show triggers with acknowledged
767 *                                                         problems.
768 * @param array   $problem_options['tags']                 (optional)
769 * @param string  $problem_options['tags'][]['tag']        (optional)
770 * @param int     $problem_options['tags'][]['operation']  (optional)
771 * @param string  $problem_options['tags'][]['value']      (optional)
772 * @param int     $problem_options['evaltype']		       (optional)
773 *
774 * @return array
775 */
776function getTriggersWithActualSeverity(array $trigger_options, array $problem_options) {
777	$problem_options += [
778		'min_severity' => TRIGGER_SEVERITY_NOT_CLASSIFIED,
779		'show_suppressed' => null,
780		'show_recent' => null,
781		'time_from' => null,
782		'acknowledged' => null
783	];
784
785	$triggers = API::Trigger()->get(['preservekeys' => true] + $trigger_options);
786
787	$nondependent_trigger_options = [
788		'output' => [],
789		'triggerids' => array_keys($triggers),
790		'skipDependent' => true,
791		'preservekeys' => true
792	];
793
794	$nondependent_triggers = API::Trigger()->get($nondependent_trigger_options);
795
796	CArrayHelper::sort($triggers, ['description']);
797
798	if ($triggers) {
799		$problem_stats = [];
800
801		foreach ($triggers as $triggerid => &$trigger) {
802			$trigger['priority'] = TRIGGER_SEVERITY_NOT_CLASSIFIED;
803			$trigger['resolved'] = true;
804
805			$problem_stats[$triggerid] = [
806				'has_resolved' => false,
807				'has_unresolved' => false,
808				'has_resolved_unacknowledged' => false,
809				'has_unresolved_unacknowledged' => false
810			];
811
812			if ($trigger['value'] == TRIGGER_VALUE_TRUE && !array_key_exists($triggerid, $nondependent_triggers)) {
813				$trigger['value'] = TRIGGER_VALUE_FALSE;
814			}
815		}
816		unset($trigger);
817
818		$problems = API::Problem()->get([
819			'output' => ['eventid', 'acknowledged', 'objectid', 'severity', 'r_eventid'],
820			'objectids' => array_keys($triggers),
821			'suppressed' => ($problem_options['show_suppressed'] == ZBX_PROBLEM_SUPPRESSED_FALSE) ? false : null,
822			'recent' => $problem_options['show_recent'],
823			'acknowledged' => $problem_options['acknowledged'],
824			'time_from' => $problem_options['time_from'],
825			'tags' => array_key_exists('tags', $problem_options) ? $problem_options['tags'] : null,
826			'evaltype' => array_key_exists('evaltype', $problem_options)
827				? $problem_options['evaltype']
828				: TAG_EVAL_TYPE_AND_OR
829		]);
830
831		foreach ($problems as $problem) {
832			$triggerid = $problem['objectid'];
833
834			if ($problem['r_eventid'] == 0 && array_key_exists($triggerid, $nondependent_triggers)) {
835				$triggers[$triggerid]['resolved'] = false;
836			}
837
838			$triggers[$triggerid]['problem']['eventid'] = $problem['eventid'];
839
840			if ($triggers[$triggerid]['priority'] < $problem['severity']) {
841				$triggers[$triggerid]['priority'] = $problem['severity'];
842			}
843
844			if ($problem['r_eventid'] == 0) {
845				$problem_stats[$triggerid]['has_unresolved'] = true;
846				if ($problem['acknowledged'] == 0 && $problem['severity'] >= $problem_options['min_severity']) {
847					$problem_stats[$triggerid]['has_unresolved_unacknowledged'] = true;
848				}
849			}
850			else {
851				$problem_stats[$triggerid]['has_resolved'] = true;
852				if ($problem['acknowledged'] == 0 && $problem['severity'] >= $problem_options['min_severity']) {
853					$problem_stats[$triggerid]['has_resolved_unacknowledged'] = true;
854				}
855			}
856		}
857
858		foreach ($triggers as $triggerid => &$trigger) {
859			$stats = $problem_stats[$triggerid];
860
861			$trigger['problem']['acknowledged'] = (
862				// Trigger has only resolved problems, all acknowledged.
863				($stats['has_resolved'] && !$stats['has_resolved_unacknowledged'] && !$stats['has_unresolved'])
864					// Trigger has unresolved problems, all acknowledged.
865					|| ($stats['has_unresolved'] && !$stats['has_unresolved_unacknowledged'])
866			) ? 1 : 0;
867
868			$trigger['value'] = ($triggers[$triggerid]['resolved'] === true)
869				? TRIGGER_VALUE_FALSE
870				: TRIGGER_VALUE_TRUE;
871
872			if (($stats['has_resolved'] || $stats['has_unresolved'])
873					&& $trigger['priority'] >= $problem_options['min_severity']) {
874				continue;
875			}
876
877			if (!array_key_exists('only_true', $trigger_options)
878					|| ($trigger_options['only_true'] === null && $trigger_options['filter']['value'] === null)) {
879				// Overview type = 'Data', Maps, Dasboard or Overview 'show any' mode.
880				$trigger['value'] = TRIGGER_VALUE_FALSE;
881			}
882			else {
883				unset($triggers[$triggerid]);
884			}
885		}
886		unset($trigger);
887	}
888
889	return $triggers;
890}
891
892/**
893 * Creates and returns a trigger status cell for the trigger overview table.
894 *
895 * @param array  $trigger
896 * @param array  $dependencies  The list of trigger dependencies, prepared by getTriggerDependencies() function.
897 *
898 * @return CCol
899 */
900function getTriggerOverviewCell(array $trigger, array $dependencies): CCol {
901	$ack = $trigger['problem']['acknowledged'] == 1 ? (new CSpan())->addClass(ZBX_STYLE_ICON_ACKN) : null;
902	$desc = array_key_exists($trigger['triggerid'], $dependencies)
903		? makeTriggerDependencies($dependencies[$trigger['triggerid']], false)
904		: [];
905
906	$column = (new CCol([$desc, $ack]))
907		->addClass(getSeverityStyle($trigger['priority'], $trigger['value'] == TRIGGER_VALUE_TRUE))
908		->addClass(ZBX_STYLE_CURSOR_POINTER);
909
910	$eventid = 0;
911	$blink_period = timeUnitToSeconds(CSettingsHelper::get(CSettingsHelper::BLINK_PERIOD));
912	$duration = time() - $trigger['lastchange'];
913
914	if ($blink_period > 0 && $duration < $blink_period) {
915		$column->addClass('blink');
916		$column->setAttribute('data-time-to-blink', $blink_period - $duration);
917		$column->setAttribute('data-toggle-class', ZBX_STYLE_BLINK_HIDDEN);
918	}
919
920	if ($trigger['value'] == TRIGGER_VALUE_TRUE) {
921		$eventid = $trigger['problem']['eventid'];
922		$acknowledge = true;
923	}
924	else {
925		$acknowledge = false;
926	}
927
928	$column->setMenuPopup(CMenuPopupHelper::getTrigger($trigger['triggerid'], $eventid, $acknowledge));
929
930	return $column;
931}
932
933/**
934 * Calculate trigger availability.
935 *
936 * @param int $triggerId		trigger id
937 * @param int $startTime		begin period
938 * @param int $endTime			end period
939 *
940 * @return array
941 */
942function calculateAvailability($triggerId, $startTime, $endTime) {
943	$startValue = TRIGGER_VALUE_FALSE;
944
945	if ($startTime > 0 && $startTime <= time()) {
946		$sql = 'SELECT e.eventid,e.value'.
947				' FROM events e'.
948				' WHERE e.objectid='.zbx_dbstr($triggerId).
949					' AND e.source='.EVENT_SOURCE_TRIGGERS.
950					' AND e.object='.EVENT_OBJECT_TRIGGER.
951					' AND e.clock<'.zbx_dbstr($startTime).
952				' ORDER BY e.eventid DESC';
953		if ($row = DBfetch(DBselect($sql, 1))) {
954			$startValue = $row['value'];
955		}
956
957		$min = $startTime;
958	}
959
960	$sql = 'SELECT COUNT(e.eventid) AS cnt,MIN(e.clock) AS min_clock,MAX(e.clock) AS max_clock'.
961			' FROM events e'.
962			' WHERE e.objectid='.zbx_dbstr($triggerId).
963				' AND e.source='.EVENT_SOURCE_TRIGGERS.
964				' AND e.object='.EVENT_OBJECT_TRIGGER;
965	if ($startTime) {
966		$sql .= ' AND e.clock>='.zbx_dbstr($startTime);
967	}
968	if ($endTime) {
969		$sql .= ' AND e.clock<='.zbx_dbstr($endTime);
970	}
971
972	$dbEvents = DBfetch(DBselect($sql));
973	if ($dbEvents['cnt'] > 0) {
974		if (!isset($min)) {
975			$min = $dbEvents['min_clock'];
976		}
977		$max = $dbEvents['max_clock'];
978	}
979	else {
980		if ($startTime == 0 && $endTime == 0) {
981			$max = time();
982			$min = $max - SEC_PER_DAY;
983		}
984		else {
985			$ret['true_time'] = 0;
986			$ret['false_time'] = 0;
987			$ret['true'] = (TRIGGER_VALUE_TRUE == $startValue) ? 100 : 0;
988			$ret['false'] = (TRIGGER_VALUE_FALSE == $startValue) ? 100 : 0;
989			return $ret;
990		}
991	}
992
993	$state = $startValue;
994	$true_time = 0;
995	$false_time = 0;
996	$time = $min;
997	if ($startTime == 0 && $endTime == 0) {
998		$max = time();
999	}
1000	if ($endTime == 0) {
1001		$endTime = $max;
1002	}
1003
1004	$rows = 0;
1005	$dbEvents = DBselect(
1006		'SELECT e.eventid,e.clock,e.value'.
1007		' FROM events e'.
1008		' WHERE e.objectid='.zbx_dbstr($triggerId).
1009			' AND e.source='.EVENT_SOURCE_TRIGGERS.
1010			' AND e.object='.EVENT_OBJECT_TRIGGER.
1011			' AND e.clock BETWEEN '.$min.' AND '.$max.
1012		' ORDER BY e.eventid'
1013	);
1014	while ($row = DBfetch($dbEvents)) {
1015		$clock = $row['clock'];
1016		$value = $row['value'];
1017
1018		$diff = max($clock - $time, 0);
1019		$time = $clock;
1020
1021		if ($state == 0) {
1022			$false_time += $diff;
1023			$state = $value;
1024		}
1025		elseif ($state == 1) {
1026			$true_time += $diff;
1027			$state = $value;
1028		}
1029		$rows++;
1030	}
1031
1032	if ($rows == 0) {
1033		$trigger = get_trigger_by_triggerid($triggerId);
1034		$state = $trigger['value'];
1035	}
1036
1037	if ($state == TRIGGER_VALUE_FALSE) {
1038		$false_time = $false_time + $endTime - $time;
1039	}
1040	elseif ($state == TRIGGER_VALUE_TRUE) {
1041		$true_time = $true_time + $endTime - $time;
1042	}
1043	$total_time = $true_time + $false_time;
1044
1045	if ($total_time == 0) {
1046		$ret['true_time'] = 0;
1047		$ret['false_time'] = 0;
1048		$ret['true'] = 0;
1049		$ret['false'] = 0;
1050	}
1051	else {
1052		$ret['true_time'] = $true_time;
1053		$ret['false_time'] = $false_time;
1054		$ret['true'] = (100 * $true_time) / $total_time;
1055		$ret['false'] = (100 * $false_time) / $total_time;
1056	}
1057
1058	return $ret;
1059}
1060
1061function get_triggers_unacknowledged($db_element, $count_problems = null, $ack = false) {
1062	$elements = [
1063		'hosts' => [],
1064		'hosts_groups' => [],
1065		'triggers' => []
1066	];
1067
1068	get_map_elements($db_element, $elements);
1069	if (empty($elements['hosts_groups']) && empty($elements['hosts']) && empty($elements['triggers'])) {
1070		return 0;
1071	}
1072
1073	$options = [
1074		'monitored' => true,
1075		'countOutput' => true,
1076		'filter' => [],
1077		'limit' => CSettingsHelper::get(CSettingsHelper::SEARCH_LIMIT) + 1
1078	];
1079
1080	if ($ack) {
1081		$options['withAcknowledgedEvents'] = 1;
1082	}
1083	else {
1084		$options['withUnacknowledgedEvents'] = 1;
1085	}
1086
1087	if ($count_problems) {
1088		$options['filter']['value'] = TRIGGER_VALUE_TRUE;
1089	}
1090	if (!empty($elements['hosts_groups'])) {
1091		$options['groupids'] = array_unique($elements['hosts_groups']);
1092	}
1093	if (!empty($elements['hosts'])) {
1094		$options['hostids'] = array_unique($elements['hosts']);
1095	}
1096	if (!empty($elements['triggers'])) {
1097		$options['triggerids'] = array_unique($elements['triggers']);
1098	}
1099
1100	return API::Trigger()->get($options);
1101}
1102
1103/**
1104 * Make trigger info block.
1105 *
1106 * @param array $trigger  Trigger described in info block.
1107 * @param array $eventid  Associated eventid.
1108 *
1109 * @return object
1110 */
1111function make_trigger_details($trigger, $eventid) {
1112	$hostNames = [];
1113
1114	$hostIds = zbx_objectValues($trigger['hosts'], 'hostid');
1115
1116	$hosts = API::Host()->get([
1117		'output' => ['name', 'hostid', 'status'],
1118		'hostids' => $hostIds
1119	]);
1120
1121	if (count($hosts) > 1) {
1122		order_result($hosts, 'name');
1123	}
1124
1125	foreach ($hosts as $host) {
1126		$hostNames[] = (new CLinkAction($host['name']))->setMenuPopup(CMenuPopupHelper::getHost($host['hostid']));
1127		$hostNames[] = ', ';
1128	}
1129	array_pop($hostNames);
1130
1131	$table = (new CTableInfo())
1132		->addRow([
1133			new CCol(_n('Host', 'Hosts', count($hosts))),
1134			new CCol($hostNames)
1135		])
1136		->addRow([
1137			new CCol(_('Trigger')),
1138			new CCol((new CLinkAction(CMacrosResolverHelper::resolveTriggerName($trigger)))
1139				->addClass(ZBX_STYLE_WORDWRAP)
1140				->setMenuPopup(CMenuPopupHelper::getTrigger($trigger['triggerid'], $eventid))
1141			)
1142		])
1143		->addRow([
1144			_('Severity'),
1145			getSeverityCell($trigger['priority'])
1146		]);
1147
1148	$trigger = CMacrosResolverHelper::resolveTriggerExpressions(zbx_toHash($trigger, 'triggerid'), [
1149		'html' => true,
1150		'resolve_usermacros' => true,
1151		'resolve_macros' => true,
1152		'sources' => ['expression', 'recovery_expression']
1153	]);
1154
1155	$trigger = reset($trigger);
1156
1157	$table
1158		->addRow([
1159			new CCol(_('Problem expression')),
1160			new CCol((new CDiv($trigger['expression']))->addClass(ZBX_STYLE_WORDWRAP))
1161		])
1162		->addRow([
1163			new CCol(_('Recovery expression')),
1164			new CCol((new CDiv($trigger['recovery_expression']))->addClass(ZBX_STYLE_WORDWRAP))
1165		])
1166		->addRow([_('Event generation'), _('Normal').((TRIGGER_MULT_EVENT_ENABLED == $trigger['type'])
1167			? SPACE.'+'.SPACE._('Multiple PROBLEM events')
1168			: '')
1169		]);
1170
1171	$table->addRow([_('Allow manual close'), ($trigger['manual_close'] == ZBX_TRIGGER_MANUAL_CLOSE_ALLOWED)
1172		? (new CCol(_('Yes')))->addClass(ZBX_STYLE_GREEN)
1173		: (new CCol(_('No')))->addClass(ZBX_STYLE_RED)
1174	]);
1175
1176	$table->addRow([_('Enabled'), ($trigger['status'] == TRIGGER_STATUS_ENABLED)
1177		? (new CCol(_('Yes')))->addClass(ZBX_STYLE_GREEN)
1178		: (new CCol(_('No')))->addClass(ZBX_STYLE_RED)
1179	]);
1180
1181	return $table;
1182}
1183
1184/**
1185 * Analyze an expression and returns expression html tree.
1186 *
1187 * @param string $expression  Trigger expression or recovery expression string.
1188 * @param int    $type        Type can be either TRIGGER_EXPRESSION or TRIGGER_RECOVERY_EXPRESSION.
1189 * @param string $error       [OUT] An error message.
1190 *
1191 * @return array|bool
1192 */
1193function analyzeExpression(string $expression, int $type, string &$error = null) {
1194	if ($expression === '') {
1195		return ['', null];
1196	}
1197
1198	$expression_parser = new CExpressionParser(['usermacros' => true, 'lldmacros' => true]);
1199
1200	if ($expression_parser->parse($expression) != CParser::PARSE_SUCCESS) {
1201		$error = $expression_parser->getError();
1202
1203		return false;
1204	}
1205
1206	$expression_tree[] = getExpressionTree($expression_parser, 0, $expression_parser->getLength() - 1);
1207
1208	$next = [];
1209	$letter_num = 0;
1210
1211	return buildExpressionHtmlTree($expression_tree, $next, $letter_num, 0, null, $type);
1212}
1213
1214/**
1215 * Builds expression HTML tree.
1216 *
1217 * @param array  $expressionTree  Output of getExpressionTree() function.
1218 * @param array  $next            Parameter only for recursive call; should be empty array.
1219 * @param int    $letterNum       Parameter only for recursive call; should be 0.
1220 * @param int    $level           Parameter only for recursive call.
1221 * @param string $operator        Parameter only for recursive call.
1222 * @param int    $type            Type can be either TRIGGER_EXPRESSION or TRIGGER_RECOVERY_EXPRESSION.
1223 *
1224 * @return array  Array containing the trigger expression formula as the first element and an array describing the
1225 *                expression tree as the second.
1226 */
1227function buildExpressionHtmlTree(array $expressionTree, array &$next, &$letterNum, $level = 0, $operator = null,
1228		$type) {
1229	$treeList = [];
1230	$outline = '';
1231
1232	end($expressionTree);
1233	$lastKey = key($expressionTree);
1234
1235	foreach ($expressionTree as $key => $element) {
1236		switch ($element['type']) {
1237			case 'operator':
1238				$next[$level] = ($key != $lastKey);
1239				$expr = expressionLevelDraw($next, $level);
1240				$expr[] = SPACE;
1241				$expr[] = ($element['operator'] === 'and') ? _('And') : _('Or');
1242				$levelDetails = [
1243					'list' => $expr,
1244					'id' => $element['id'],
1245					'expression' => [
1246						'value' => $element['expression']
1247					]
1248				];
1249
1250				$levelErrors = expressionHighLevelErrors($element['expression']);
1251				if ($levelErrors) {
1252					$levelDetails['expression']['levelErrors'] = $levelErrors;
1253				}
1254				$treeList[] = $levelDetails;
1255
1256				list($subOutline, $subTreeList) = buildExpressionHtmlTree($element['elements'], $next, $letterNum,
1257					$level + 1, $element['operator'], $type
1258				);
1259				$treeList = array_merge($treeList, $subTreeList);
1260
1261				$outline .= ($level == 0) ? $subOutline : '('.$subOutline.')';
1262				if ($operator !== null && $next[$level]) {
1263					$outline .= ' '.$operator.' ';
1264				}
1265				break;
1266
1267			case 'expression':
1268				$next[$level] = ($key != $lastKey);
1269
1270				$letter = num2letter($letterNum++);
1271				$outline .= $letter;
1272				if ($operator !== null && $next[$level]) {
1273					$outline .= ' '.$operator.' ';
1274				}
1275
1276				if (defined('NO_LINK_IN_TESTING')) {
1277					$url = $element['expression'];
1278				}
1279				else {
1280					if ($type == TRIGGER_EXPRESSION) {
1281						$expressionId = 'expr_'.$element['id'];
1282					}
1283					else {
1284						$expressionId = 'recovery_expr_'.$element['id'];
1285					}
1286
1287					$url = (new CLinkAction($element['expression']))
1288						->setId($expressionId)
1289						->onClick('javascript: copy_expression("'.$expressionId.'", '.$type.');');
1290				}
1291				$expr = expressionLevelDraw($next, $level);
1292				$expr[] = SPACE;
1293				$expr[] = bold($letter);
1294				$expr[] = SPACE;
1295				$expr[] = $url;
1296
1297				$levelDetails = [
1298					'list' => $expr,
1299					'id' => $element['id'],
1300					'expression' => [
1301						'value' => $element['expression']
1302					]
1303				];
1304
1305				$levelErrors = expressionHighLevelErrors($element['expression']);
1306				if ($levelErrors) {
1307					$levelDetails['expression']['levelErrors'] = $levelErrors;
1308				}
1309				$treeList[] = $levelDetails;
1310				break;
1311		}
1312	}
1313
1314	return [$outline, $treeList];
1315}
1316
1317function expressionHighLevelErrors($expression) {
1318	static $errors, $definedErrorPhrases;
1319
1320	if (!isset($errors)) {
1321		$definedErrorPhrases = [
1322			EXPRESSION_HOST_UNKNOWN => _('Unknown host, no such host present in system'),
1323			EXPRESSION_HOST_ITEM_UNKNOWN => _('Unknown host item, no such item in selected host'),
1324			EXPRESSION_NOT_A_MACRO_ERROR => _('Given expression is not a macro'),
1325			EXPRESSION_FUNCTION_UNKNOWN => _('Incorrect function is used'),
1326			EXPRESSION_UNSUPPORTED_VALUE_TYPE => _('Incorrect item value type')
1327		];
1328		$errors = [];
1329	}
1330
1331	if (!isset($errors[$expression])) {
1332		$errors[$expression] = [];
1333		$expression_parser = new CExpressionParser(['usermacros' => true, 'lldmacros' => true]);
1334		if ($expression_parser->parse($expression) == CParser::PARSE_SUCCESS) {
1335			$tokens = $expression_parser->getResult()->getTokensOfTypes([
1336				CExpressionParserResult::TOKEN_TYPE_MATH_FUNCTION,
1337				CExpressionParserResult::TOKEN_TYPE_HIST_FUNCTION
1338			]);
1339			foreach ($tokens as $token) {
1340				$info = get_item_function_info($token['match']);
1341
1342				if (!is_array($info) && isset($definedErrorPhrases[$info])) {
1343					if (!isset($errors[$expression][$token['match']])) {
1344						$errors[$expression][$token['match']] = $definedErrorPhrases[$info];
1345					}
1346				}
1347			}
1348		}
1349	}
1350
1351	$ret = [];
1352	if (!$errors[$expression]) {
1353		return $ret;
1354	}
1355
1356	$expression_parser = new CExpressionParser(['usermacros' => true, 'lldmacros' => true]);
1357	if ($expression_parser->parse($expression) == CParser::PARSE_SUCCESS) {
1358		$tokens = $expression_parser->getResult()->getTokensOfTypes([
1359			CExpressionParserResult::TOKEN_TYPE_MATH_FUNCTION,
1360			CExpressionParserResult::TOKEN_TYPE_HIST_FUNCTION
1361		]);
1362		foreach ($tokens as $token) {
1363			if (isset($errors[$expression][$token['match']])) {
1364				$ret[$token['match']] = $errors[$expression][$token['match']];
1365			}
1366		}
1367	}
1368
1369	return $ret;
1370}
1371
1372/**
1373 * Draw level for trigger expression builder tree.
1374 *
1375 * @param array $next
1376 * @param int   $level
1377 *
1378 * @return array
1379 */
1380function expressionLevelDraw(array $next, $level) {
1381	$expr = [];
1382	for ($i = 1; $i <= $level; $i++) {
1383		if ($i == $level) {
1384			$class_name = $next[$i] ? 'icon-tree-top-bottom-right' : 'icon-tree-top-right';
1385		}
1386		else {
1387			$class_name = $next[$i] ? 'icon-tree-top-bottom' : 'icon-tree-empty';
1388		}
1389
1390		$expr[] = (new CSpan(''))->addClass($class_name);
1391	}
1392	return $expr;
1393}
1394
1395/**
1396 * Makes tree of expression elements
1397 *
1398 * Expression:
1399 *   "last(/host1/system.cpu.util[,iowait], 0) > 50 and last(/host2/system.cpu.util[,iowait], 0) > 50"
1400 * Result:
1401 *   [
1402 *     [0] => [
1403 *       'id' => '0_94',
1404 *       'type' => 'operator',
1405 *       'operator' => 'and',
1406 *       'elements' => [
1407 *         [0] => [
1408 *           'id' => '0_44',
1409 *           'type' => 'expression',
1410 *           'expression' => 'last(/host1/system.cpu.util[,iowait], 0) > 50'
1411 *         ],
1412 *         [1] => [
1413 *           'id' => '50_94',
1414 *           'type' => 'expression',
1415 *           'expression' => 'last(/host2/system.cpu.util[,iowait], 0) > 50'
1416 *         ]
1417 *       ]
1418 *     ]
1419 *   ]
1420 *
1421 * @param CExpressionParser $expression_parser
1422 * @param int $start
1423 * @param int $end
1424 *
1425 * @return array
1426 */
1427function getExpressionTree(CExpressionParser $expression_parser, int $start, int $end) {
1428	$tokens = array_column($expression_parser->getResult()->getTokens(), null, 'pos');
1429	$expression = $expression_parser->getMatch();
1430
1431	$expressionTree = [];
1432	foreach (['or', 'and'] as $operator) {
1433		$operatorFound = false;
1434		$lParentheses = -1;
1435		$rParentheses = -1;
1436		$expressions = [];
1437		$openSymbolNum = $start;
1438
1439		for ($i = $start, $level = 0; $i <= $end; $i++) {
1440			switch ($expression[$i]) {
1441				case ' ':
1442				case "\r":
1443				case "\n":
1444				case "\t":
1445					if ($openSymbolNum == $i) {
1446						$openSymbolNum++;
1447					}
1448					break;
1449
1450				case '(':
1451					if ($level == 0) {
1452						$lParentheses = $i;
1453					}
1454					$level++;
1455					break;
1456
1457				case ')':
1458					$level--;
1459					if ($level == 0) {
1460						$rParentheses = $i;
1461					}
1462					break;
1463
1464				default:
1465					/*
1466					 * Once reached the end of a complete expression, parse the expression on the left side of the
1467					 * operator.
1468					 */
1469					if ($level == 0 && array_key_exists($i, $tokens)
1470							&& $tokens[$i]['type'] == CExpressionParserResult::TOKEN_TYPE_OPERATOR
1471							&& $tokens[$i]['match'] === $operator) {
1472						// Find the last symbol of the expression before the operator.
1473						$closeSymbolNum = $i - 1;
1474
1475						// Trim blank symbols after the expression.
1476						while (strpos(CExpressionParser::WHITESPACES, $expression[$closeSymbolNum]) !== false) {
1477							$closeSymbolNum--;
1478						}
1479
1480						$expressions[] = getExpressionTree($expression_parser, $openSymbolNum, $closeSymbolNum);
1481						$openSymbolNum = $i + $tokens[$i]['length'];
1482						$operatorFound = true;
1483					}
1484			}
1485		}
1486
1487		// Trim blank symbols in the end of the trigger expression.
1488		$closeSymbolNum = $end;
1489		while (strpos(CExpressionParser::WHITESPACES, $expression[$closeSymbolNum]) !== false) {
1490			$closeSymbolNum--;
1491		}
1492
1493		/*
1494		 * Once found a whole expression and parsed the expression on the left side of the operator, parse the
1495		 * expression on the right.
1496		 */
1497		if ($operatorFound) {
1498			$expressions[] = getExpressionTree($expression_parser, $openSymbolNum, $closeSymbolNum);
1499
1500			// Trim blank symbols in the beginning of the trigger expression.
1501			$openSymbolNum = $start;
1502			while (strpos(CExpressionParser::WHITESPACES, $expression[$openSymbolNum]) !== false) {
1503				$openSymbolNum++;
1504			}
1505
1506			// Trim blank symbols in the end of the trigger expression.
1507			$closeSymbolNum = $end;
1508			while (strpos(CExpressionParser::WHITESPACES, $expression[$closeSymbolNum]) !== false) {
1509				$closeSymbolNum--;
1510			}
1511
1512			$expressionTree = [
1513				'id' => $openSymbolNum.'_'.$closeSymbolNum,
1514				'expression' => substr($expression, $openSymbolNum, $closeSymbolNum - $openSymbolNum + 1),
1515				'type' => 'operator',
1516				'operator' => $operator,
1517				'elements' => $expressions
1518			];
1519			break;
1520		}
1521		// If finding both operators failed, it means there's only one expression return the result.
1522		elseif ($operator === 'and') {
1523			// Trim extra parentheses.
1524			if ($openSymbolNum == $lParentheses && $closeSymbolNum == $rParentheses) {
1525				$openSymbolNum++;
1526				$closeSymbolNum--;
1527
1528				$expressionTree = getExpressionTree($expression_parser, $openSymbolNum, $closeSymbolNum);
1529			}
1530			// No extra parentheses remain, return the result.
1531			else {
1532				$expressionTree = [
1533					'id' => $openSymbolNum.'_'.$closeSymbolNum,
1534					'expression' => substr($expression, $openSymbolNum, $closeSymbolNum - $openSymbolNum + 1),
1535					'type' => 'expression'
1536				];
1537			}
1538		}
1539	}
1540
1541	return $expressionTree;
1542}
1543
1544/**
1545 * Recreate an expression depending on action.
1546 *
1547 * Supported action values:
1548 * - and - add an expression using "and";
1549 * - or  - add an expression using "or";
1550 * - r   - replace;
1551 * - R   - remove.
1552 *
1553 * @param string $expression
1554 * @param string $expression_id   Element identifier like "0_55".
1555 * @param string $action          Action to perform.
1556 * @param string $new_expression  Expression for AND, OR or replace actions.
1557 * @param string $error           [OUT] An error message.
1558 *
1559 * @return bool|string  Returns new expression or false if expression is incorrect.
1560 */
1561function remakeExpression($expression, $expression_id, $action, $new_expression, string &$error = null) {
1562	if ($expression === '') {
1563		return false;
1564	}
1565
1566	$expression_parser = new CExpressionParser(['usermacros' => true, 'lldmacros' => true]);
1567	if ($action !== 'R' && $expression_parser->parse($new_expression) != CParser::PARSE_SUCCESS) {
1568		$error = $expression_parser->getError();
1569		return false;
1570	}
1571
1572	if ($expression_parser->parse($expression) != CParser::PARSE_SUCCESS) {
1573		$error = $expression_parser->getError();
1574		return false;
1575	}
1576
1577	$expression_tree[] = getExpressionTree($expression_parser, 0, $expression_parser->getLength() - 1);
1578
1579	if (rebuildExpressionTree($expression_tree, $expression_id, $action, $new_expression)) {
1580		$expression = makeExpression($expression_tree);
1581	}
1582
1583	return $expression;
1584}
1585
1586/**
1587 * Rebuild expression depending on action.
1588 *
1589 * Supported action values:
1590 * - and	- add an expression using "and";
1591 * - or		- add an expression using "or";
1592 * - r 		- replace;
1593 * - R		- remove.
1594 *
1595 * Example:
1596 *   $expressionTree = array(
1597 *     [0] => array(
1598 *       'id' => '0_94',
1599 *       'type' => 'operator',
1600 *       'operator' => 'and',
1601 *       'elements' => array(
1602 *         [0] => array(
1603 *           'id' => '0_44',
1604 *           'type' => 'expression',
1605 *           'expression' => '{host1:system.cpu.util[,iowait].last(0)} > 50'
1606 *         ),
1607 *         [1] => array(
1608 *           'id' => '50_94',
1609 *           'type' => 'expression',
1610 *           'expression' => '{host2:system.cpu.util[,iowait].last(0)} > 50'
1611 *         )
1612 *       )
1613 *     )
1614 *   )
1615 *   $action = 'R'
1616 *   $expressionId = '50_94'
1617 *
1618 * Result:
1619 *   $expressionTree = array(
1620 *     [0] => array(
1621 *       'id' => '0_44',
1622 *       'type' => 'expression',
1623 *       'expression' => '{host1:system.cpu.util[,iowait].last(0)} > 50'
1624 *     )
1625 *   )
1626 *
1627 * @param array 	$expressionTree
1628 * @param string 	$expressionId  		element identifier like "0_55"
1629 * @param string 	$action        		action to perform
1630 * @param string 	$newExpression 		expression for AND, OR or replace actions
1631 * @param string 	$operator       	parameter only for recursive call
1632 *
1633 * @return bool                 returns true if element is found, false - otherwise
1634 */
1635function rebuildExpressionTree(array &$expressionTree, $expressionId, $action, $newExpression, $operator = null) {
1636	foreach ($expressionTree as $key => $expression) {
1637		if ($expressionId == $expressionTree[$key]['id']) {
1638			switch ($action) {
1639				case 'and':
1640				case 'or':
1641					switch ($expressionTree[$key]['type']) {
1642						case 'operator':
1643							if ($expressionTree[$key]['operator'] == $action) {
1644								$expressionTree[$key]['elements'][] = [
1645									'expression' => $newExpression,
1646									'type' => 'expression'
1647								];
1648							}
1649							else {
1650								$element = [
1651									'type' => 'operator',
1652									'operator' => $action,
1653									'elements' => [
1654										$expressionTree[$key],
1655										[
1656											'expression' => $newExpression,
1657											'type' => 'expression'
1658										]
1659									]
1660								];
1661								$expressionTree[$key] = $element;
1662							}
1663							break;
1664						case 'expression':
1665							if (!$operator || $operator != $action) {
1666								$element = [
1667									'type' => 'operator',
1668									'operator' => $action,
1669									'elements' => [
1670										$expressionTree[$key],
1671										[
1672											'expression' => $newExpression,
1673											'type' => 'expression'
1674										]
1675									]
1676								];
1677								$expressionTree[$key] = $element;
1678							}
1679							else {
1680								$expressionTree[] = [
1681									'expression' => $newExpression,
1682									'type' => 'expression'
1683								];
1684							}
1685							break;
1686					}
1687					break;
1688				// replace
1689				case 'r':
1690					$expressionTree[$key]['expression'] = $newExpression;
1691					if ($expressionTree[$key]['type'] == 'operator') {
1692						$expressionTree[$key]['type'] = 'expression';
1693						unset($expressionTree[$key]['operator'], $expressionTree[$key]['elements']);
1694					}
1695					break;
1696				// remove
1697				case 'R':
1698					unset($expressionTree[$key]);
1699					break;
1700			}
1701			return true;
1702		}
1703
1704		if ($expressionTree[$key]['type'] == 'operator') {
1705			if (rebuildExpressionTree($expressionTree[$key]['elements'], $expressionId, $action, $newExpression,
1706					$expressionTree[$key]['operator'])) {
1707				return true;
1708			}
1709		}
1710	}
1711
1712	return false;
1713}
1714
1715/**
1716 * Makes expression by expression tree
1717 *
1718 * Example:
1719 *   $expressionTree = array(
1720 *     [0] => array(
1721 *       'type' => 'operator',
1722 *       'operator' => 'and',
1723 *       'elements' => array(
1724 *         [0] => array(
1725 *           'type' => 'expression',
1726 *           'expression' => '{host1:system.cpu.util[,iowait].last(0)} > 50'
1727 *         ),
1728 *         [1] => array(
1729 *           'type' => 'expression',
1730 *           'expression' => '{host2:system.cpu.util[,iowait].last(0)} > 50'
1731 *         )
1732 *       )
1733 *     )
1734 *   )
1735 *
1736 * Result:
1737 *   "{host1:system.cpu.util[,iowait].last(0)} > 50 and {host2:system.cpu.util[,iowait].last(0)} > 50"
1738 *
1739 * @param array  $expressionTree
1740 * @param int    $level				parameter only for recursive call
1741 * @param string $operator			parameter only for recursive call
1742 *
1743 * @return string
1744 */
1745function makeExpression(array $expressionTree, $level = 0, $operator = null) {
1746	$expression = '';
1747
1748	end($expressionTree);
1749	$lastKey = key($expressionTree);
1750
1751	foreach ($expressionTree as $key => $element) {
1752		switch ($element['type']) {
1753			case 'operator':
1754				$subExpression = makeExpression($element['elements'], $level + 1, $element['operator']);
1755
1756				$expression .= ($level == 0) ? $subExpression : '('.$subExpression.')';
1757				break;
1758			case 'expression':
1759				$expression .= $element['expression'];
1760				break;
1761		}
1762		if ($operator !== null && $key != $lastKey) {
1763			$expression .= ' '.$operator.' ';
1764		}
1765	}
1766
1767	return $expression;
1768}
1769
1770function get_item_function_info(string $expr) {
1771	$rule_float = ['value_type' => _('Numeric (float)'), 'values' => null];
1772	$rule_int = ['value_type' => _('Numeric (integer)'), 'values' => null];
1773	$rule_str = ['value_type' => _('String'), 'values' => null];
1774	$rule_any = ['value_type' => _('Any'), 'values' => null];
1775	$rule_0or1 = ['value_type' => _('0 or 1'), 'values' => [0 => 0, 1 => 1]];
1776	$rules = [
1777		// Every nested array should have two elements: label, values.
1778		'integer' => [
1779			ITEM_VALUE_TYPE_UINT64 => $rule_int
1780		],
1781		'numeric' => [
1782			ITEM_VALUE_TYPE_UINT64 => $rule_int,
1783			ITEM_VALUE_TYPE_FLOAT => $rule_float
1784		],
1785		'numeric_as_float' => [
1786			ITEM_VALUE_TYPE_UINT64 => $rule_float,
1787			ITEM_VALUE_TYPE_FLOAT => $rule_float
1788		],
1789		'numeric_as_uint' => [
1790			ITEM_VALUE_TYPE_UINT64 => $rule_int,
1791			ITEM_VALUE_TYPE_FLOAT => $rule_int
1792		],
1793		'numeric_as_0or1' => [
1794			ITEM_VALUE_TYPE_UINT64 => $rule_0or1,
1795			ITEM_VALUE_TYPE_FLOAT => $rule_0or1
1796		],
1797		'string_as_0or1' => [
1798			ITEM_VALUE_TYPE_TEXT => $rule_0or1,
1799			ITEM_VALUE_TYPE_STR => $rule_0or1,
1800			ITEM_VALUE_TYPE_LOG => $rule_0or1
1801		],
1802		'string_as_uint' => [
1803			ITEM_VALUE_TYPE_TEXT => $rule_int,
1804			ITEM_VALUE_TYPE_STR => $rule_int,
1805			ITEM_VALUE_TYPE_LOG => $rule_int
1806		],
1807		'string' => [
1808			ITEM_VALUE_TYPE_TEXT => $rule_str,
1809			ITEM_VALUE_TYPE_STR => $rule_str,
1810			ITEM_VALUE_TYPE_LOG => $rule_str
1811		],
1812		'log_as_uint' => [
1813			ITEM_VALUE_TYPE_LOG => $rule_int
1814		],
1815		'log_as_0or1' => [
1816			ITEM_VALUE_TYPE_LOG => $rule_0or1
1817		]
1818	];
1819
1820	$hist_functions = [
1821		'avg' => $rules['numeric_as_float'],
1822		'change' => $rules['numeric'] + $rules['string_as_0or1'],
1823		'count' => $rules['numeric_as_uint'] + $rules['string_as_uint'],
1824		'countunique' => $rules['numeric_as_uint'] + $rules['string_as_uint'],
1825		'find' => $rules['numeric_as_0or1'] + $rules['string_as_0or1'],
1826		'first' => $rules['numeric'] + $rules['string'],
1827		'forecast' => $rules['numeric_as_float'],
1828		'fuzzytime' => $rules['numeric_as_0or1'],
1829		'kurtosis' => $rules['numeric_as_float'],
1830		'last' => $rules['numeric'] + $rules['string'],
1831		'logeventid' => $rules['log_as_0or1'],
1832		'logseverity' => $rules['log_as_uint'],
1833		'logsource' => $rules['log_as_0or1'],
1834		'mad' => $rules['numeric_as_float'],
1835		'max' => $rules['numeric'],
1836		'min' => $rules['numeric'],
1837		'nodata' => $rules['numeric_as_0or1'] + $rules['string_as_0or1'],
1838		'percentile' => $rules['numeric'],
1839		'skewness' => $rules['numeric_as_float'],
1840		'stddevpop' => $rules['numeric_as_float'],
1841		'stddevsamp' => $rules['numeric_as_float'],
1842		'sum' => $rules['numeric'],
1843		'sumofsquares' => $rules['numeric_as_float'],
1844		'timeleft' => $rules['numeric_as_float'],
1845		'trendavg' => $rules['numeric'],
1846		'trendcount' => $rules['numeric'],
1847		'trendmax' => $rules['numeric'],
1848		'trendmin' => $rules['numeric'],
1849		'trendsum' => $rules['numeric'],
1850		'varpop' => $rules['numeric_as_float'],
1851		'varsamp' => $rules['numeric_as_float']
1852	];
1853
1854	$math_functions = [
1855		'abs' => ['any' => $rule_float],
1856		'acos' => ['any' => $rule_float],
1857		'ascii' => ['any' => $rule_int],
1858		'asin' => ['any' => $rule_float],
1859		'atan' => ['any' => $rule_float],
1860		'atan2' => ['any' => $rule_float],
1861		'avg' => ['any' => $rule_float],
1862		'between' => ['any' => $rule_0or1],
1863		'bitand' => ['any' => $rule_int],
1864		'bitlength' => ['any' => $rule_int],
1865		'bitlshift' => ['any' => $rule_int],
1866		'bitnot' => ['any' => $rule_int],
1867		'bitor' => ['any' => $rule_int],
1868		'bitrshift' => ['any' => $rule_int],
1869		'bitxor' => ['any' => $rule_int],
1870		'bytelength' => ['any' => $rule_int],
1871		'cbrt' => ['any' => $rule_float],
1872		'ceil' => ['any' => $rule_int],
1873		'char' => ['any' => $rule_str],
1874		'concat' => ['any' => $rule_str],
1875		'cos' => ['any' => $rule_float],
1876		'cosh' => ['any' => $rule_float],
1877		'cot' => ['any' => $rule_float],
1878		'date' => [
1879			'any' => ['value_type' => 'YYYYMMDD', 'values' => null]
1880		],
1881		'dayofmonth' => [
1882			'any' => ['value_type' => '1-31', 'values' => null]
1883		],
1884		'dayofweek' => [
1885			'any' => ['value_type' => '1-7', 'values' => [1 => 1, 2 => 2, 3 => 3, 4 => 4, 5 => 5, 6 => 6, 7 => 7]]
1886		],
1887		'degrees' => ['any' => $rule_float],
1888		'e' => ['any' => $rule_float],
1889		'exp' => ['any' => $rule_float],
1890		'expm1' => ['any' => $rule_float],
1891		'floor' => ['any' => $rule_int],
1892		'in' => ['any' => $rule_0or1],
1893		'insert' => ['any' => $rule_str],
1894		'left' => ['any' => $rule_str],
1895		'length' => ['any' => $rule_int],
1896		'log' => ['any' => $rule_float],
1897		'log10' => ['any' => $rule_float],
1898		'ltrim' => ['any' => $rule_str],
1899		'max' => ['any' => $rule_float],
1900		'mid' => ['any' => $rule_str],
1901		'min' => ['any' => $rule_float],
1902		'mod' => ['any' => $rule_float],
1903		'now' => ['any' => $rule_int],
1904		'pi' => ['any' => $rule_float],
1905		'power' => ['any' => $rule_float],
1906		'radians' => ['any' => $rule_float],
1907		'rand' => ['any' => $rule_int],
1908		'repeat' => ['any' => $rule_str],
1909		'replace' => ['any' => $rule_str],
1910		'right' => ['any' => $rule_str],
1911		'round' => ['any' => $rule_float],
1912		'rtrim' => ['any' => $rule_str],
1913		'signum' => ['any' => $rule_int],
1914		'sin' => ['any' => $rule_float],
1915		'sinh' => ['any' => $rule_float],
1916		'sqrt' => ['any' => $rule_float],
1917		'sum' => ['any' => $rule_float],
1918		'tan' => ['any' => $rule_float],
1919		'time' => [
1920			'any' => ['value_type' => 'HHMMSS', 'values' => null]
1921		],
1922		'trim' => ['any' => $rule_str],
1923		'truncate' => ['any' => $rule_float]
1924	];
1925
1926	$expression_parser = new CExpressionParser(['usermacros' => true, 'lldmacros' => true]);
1927	$expression_parser->parse($expr);
1928	$token = $expression_parser->getResult()->getTokens()[0];
1929
1930	switch ($token['type']) {
1931		case CExpressionParserResult::TOKEN_TYPE_MACRO:
1932			$result = $rule_0or1;
1933			break;
1934
1935		case CExpressionParserResult::TOKEN_TYPE_USER_MACRO:
1936		case CExpressionParserResult::TOKEN_TYPE_LLD_MACRO:
1937			$result = $rule_any;
1938			break;
1939
1940		case CExpressionParserResult::TOKEN_TYPE_HIST_FUNCTION:
1941			if (!array_key_exists($token['data']['function'], $hist_functions)) {
1942				$result = EXPRESSION_FUNCTION_UNKNOWN;
1943				break;
1944			}
1945
1946			$hosts = API::Host()->get([
1947				'output' => ['hostid'],
1948				'filter' => [
1949					'host' => $token['data']['parameters'][0]['data']['host']
1950				],
1951				'templated_hosts' => true
1952			]);
1953
1954			if (!$hosts) {
1955				$result = EXPRESSION_HOST_UNKNOWN;
1956				break;
1957			}
1958
1959			$items = API::Item()->get([
1960				'output' => ['value_type'],
1961				'hostids' => $hosts[0]['hostid'],
1962				'filter' => [
1963					'key_' => $token['data']['parameters'][0]['data']['item']
1964				],
1965				'webitems' => true
1966			]);
1967
1968			if (!$items) {
1969				$items = API::ItemPrototype()->get([
1970					'output' => ['value_type'],
1971					'hostids' => $hosts[0]['hostid'],
1972					'filter' => [
1973						'key_' => $token['data']['parameters'][0]['data']['item']
1974					]
1975				]);
1976			}
1977
1978			if (!$items) {
1979				$result = EXPRESSION_HOST_ITEM_UNKNOWN;
1980				break;
1981			}
1982
1983			$hist_function = $hist_functions[$token['data']['function']];
1984			$value_type = $items[0]['value_type'];
1985
1986			if (array_key_exists('any', $hist_function)) {
1987				$value_type = 'any';
1988			}
1989			elseif (!array_key_exists($value_type, $hist_function)) {
1990				$result = EXPRESSION_UNSUPPORTED_VALUE_TYPE;
1991				break;
1992			}
1993
1994			$result = $hist_function[$value_type];
1995			break;
1996
1997		case CExpressionParserResult::TOKEN_TYPE_MATH_FUNCTION:
1998			if (!array_key_exists($token['data']['function'], $math_functions)) {
1999				$result = EXPRESSION_FUNCTION_UNKNOWN;
2000				break;
2001			}
2002
2003			$result = $math_functions[$token['data']['function']]['any'];
2004			break;
2005
2006		default:
2007			$result = EXPRESSION_NOT_A_MACRO_ERROR;
2008	}
2009
2010	return $result;
2011}
2012
2013/**
2014 * Quoting $param if it contains special characters.
2015 *
2016 * @param string $param
2017 * @param bool   $forced
2018 *
2019 * @return string
2020 */
2021function quoteFunctionParam($param, $forced = false) {
2022	if (!$forced) {
2023		if (!isset($param[0]) || ($param[0] != '"' && false === strpbrk($param, ',)'))) {
2024			return $param;
2025		}
2026	}
2027
2028	return '"'.str_replace('"', '\\"', $param).'"';
2029}
2030
2031/**
2032 * Returns the text indicating the trigger's status and state. If the $state parameter is not given, only the status of
2033 * the trigger will be taken into account.
2034 *
2035 * @param int $status
2036 * @param int $state
2037 *
2038 * @return string
2039 */
2040function triggerIndicator($status, $state = null) {
2041	if ($status == TRIGGER_STATUS_ENABLED) {
2042		return ($state == TRIGGER_STATE_UNKNOWN) ? _('Unknown') : _('Enabled');
2043	}
2044
2045	return _('Disabled');
2046}
2047
2048/**
2049 * Returns the CSS class for the trigger's status and state indicator. If the $state parameter is not given, only the
2050 * status of the trigger will be taken into account.
2051 *
2052 * @param int $status
2053 * @param int $state
2054 *
2055 * @return string
2056 */
2057function triggerIndicatorStyle($status, $state = null) {
2058	if ($status == TRIGGER_STATUS_ENABLED) {
2059		return ($state == TRIGGER_STATE_UNKNOWN) ?
2060			ZBX_STYLE_GREY :
2061			ZBX_STYLE_GREEN;
2062	}
2063
2064	return ZBX_STYLE_RED;
2065}
2066
2067/**
2068 * Orders triggers by both status and state. Triggers are sorted in the following order: enabled, disabled, unknown.
2069 *
2070 * Keep in sync with orderItemsByStatus().
2071 *
2072 * @param array  $triggers
2073 * @param string $sortorder
2074 */
2075function orderTriggersByStatus(array &$triggers, $sortorder = ZBX_SORT_UP) {
2076	$sort = [];
2077
2078	foreach ($triggers as $key => $trigger) {
2079		if ($trigger['status'] == TRIGGER_STATUS_ENABLED) {
2080			$sort[$key] = ($trigger['state'] == TRIGGER_STATE_UNKNOWN) ? 2 : 0;
2081		}
2082		else {
2083			$sort[$key] = 1;
2084		}
2085	}
2086
2087	if ($sortorder == ZBX_SORT_UP) {
2088		asort($sort);
2089	}
2090	else {
2091		arsort($sort);
2092	}
2093
2094	$sortedTriggers = [];
2095	foreach ($sort as $key => $val) {
2096		$sortedTriggers[$key] = $triggers[$key];
2097	}
2098	$triggers = $sortedTriggers;
2099}
2100
2101/**
2102 * Create the list of hosts for each trigger.
2103 *
2104 * @param array  $triggers
2105 * @param string $triggers[]['triggerid']
2106 * @param array  $triggers[]['hosts']
2107 * @param string $triggers[]['hosts'][]['hostid']
2108 *
2109 * @return array
2110 */
2111function getTriggersHostsList(array $triggers) {
2112	$hostids = [];
2113
2114	foreach ($triggers as $trigger) {
2115		foreach ($trigger['hosts'] as $host) {
2116			$hostids[$host['hostid']] = true;
2117		}
2118	}
2119
2120	$db_hosts = $hostids
2121		? API::Host()->get([
2122			'output' => ['hostid', 'name', 'maintenanceid', 'maintenance_status', 'maintenance_type'],
2123			'hostids' => array_keys($hostids),
2124			'preservekeys' => true
2125		])
2126		: [];
2127
2128	$triggers_hosts = [];
2129	foreach ($triggers as $trigger) {
2130		$triggers_hosts[$trigger['triggerid']] = [];
2131
2132		foreach ($trigger['hosts'] as $host) {
2133			if (!array_key_exists($host['hostid'], $db_hosts)) {
2134				continue;
2135			}
2136
2137			$triggers_hosts[$trigger['triggerid']][] = $db_hosts[$host['hostid']];
2138		}
2139		order_result($triggers_hosts[$trigger['triggerid']], 'name');
2140	}
2141
2142	return $triggers_hosts;
2143}
2144
2145/**
2146 * Make the list of hosts for each trigger.
2147 *
2148 * @param array  $triggers_hosts
2149 * @param string $triggers_hosts[<triggerid>][]['hostid']
2150 * @param string $triggers_hosts[<triggerid>][]['name']
2151 * @param string $triggers_hosts[<triggerid>][]['maintenanceid']
2152 * @param int    $triggers_hosts[<triggerid>][]['maintenance_status']
2153 * @param int    $triggers_hosts[<triggerid>][]['maintenance_type']
2154 *
2155 * @return array
2156 */
2157function makeTriggersHostsList(array $triggers_hosts) {
2158	$db_maintenances = [];
2159
2160	$hostids = [];
2161	$maintenanceids = [];
2162
2163	foreach ($triggers_hosts as $hosts) {
2164		foreach ($hosts as $host) {
2165			$hostids[$host['hostid']] = true;
2166			if ($host['maintenance_status'] == HOST_MAINTENANCE_STATUS_ON) {
2167				$maintenanceids[$host['maintenanceid']] = true;
2168			}
2169		}
2170	}
2171
2172	if ($hostids) {
2173		if ($maintenanceids) {
2174			$db_maintenances = API::Maintenance()->get([
2175				'output' => ['name', 'description'],
2176				'maintenanceids' => array_keys($maintenanceids),
2177				'preservekeys' => true
2178			]);
2179		}
2180	}
2181
2182	foreach ($triggers_hosts as &$hosts) {
2183		$trigger_hosts = [];
2184
2185		foreach ($hosts as $host) {
2186			$host_name = (new CLinkAction($host['name']))
2187				->setMenuPopup(CMenuPopupHelper::getHost($host['hostid']));
2188
2189			if ($host['maintenance_status'] == HOST_MAINTENANCE_STATUS_ON) {
2190				if (array_key_exists($host['maintenanceid'], $db_maintenances)) {
2191					$maintenance = $db_maintenances[$host['maintenanceid']];
2192					$maintenance_icon = makeMaintenanceIcon($host['maintenance_type'], $maintenance['name'],
2193						$maintenance['description']
2194					);
2195				}
2196				else {
2197					$maintenance_icon = makeMaintenanceIcon($host['maintenance_type'], _('Inaccessible maintenance'),
2198						''
2199					);
2200				}
2201
2202				$host_name = (new CSpan([$host_name, $maintenance_icon]))->addClass(ZBX_STYLE_REL_CONTAINER);
2203			}
2204
2205			if ($trigger_hosts) {
2206				$trigger_hosts[] = (new CSpan(','))->addClass('separator');
2207			}
2208			$trigger_hosts[] = $host_name;
2209		}
2210
2211		$hosts = $trigger_hosts;
2212	}
2213	unset($hosts);
2214
2215	return $triggers_hosts;
2216}
2217
2218/**
2219 * Get parent templates for each given trigger.
2220 *
2221 * @param $array $triggers                  An array of triggers.
2222 * @param string $triggers[]['triggerid']   ID of a trigger.
2223 * @param string $triggers[]['templateid']  ID of parent template trigger.
2224 * @param int    $flag                      Origin of the trigger (ZBX_FLAG_DISCOVERY_NORMAL or
2225 *                                          ZBX_FLAG_DISCOVERY_PROTOTYPE).
2226 *
2227 * @return array
2228 */
2229function getTriggerParentTemplates(array $triggers, $flag) {
2230	$parent_triggerids = [];
2231	$data = [
2232		'links' => [],
2233		'templates' => []
2234	];
2235
2236	foreach ($triggers as $trigger) {
2237		if ($trigger['templateid'] != 0) {
2238			$parent_triggerids[$trigger['templateid']] = true;
2239			$data['links'][$trigger['triggerid']] = ['triggerid' => $trigger['templateid']];
2240		}
2241	}
2242
2243	if (!$parent_triggerids) {
2244		return $data;
2245	}
2246
2247	$all_parent_triggerids = [];
2248	$hostids = [];
2249	if ($flag == ZBX_FLAG_DISCOVERY_PROTOTYPE) {
2250		$lld_ruleids = [];
2251	}
2252
2253	do {
2254		if ($flag == ZBX_FLAG_DISCOVERY_PROTOTYPE) {
2255			$db_triggers = API::TriggerPrototype()->get([
2256				'output' => ['triggerid', 'templateid'],
2257				'selectHosts' => ['hostid'],
2258				'selectDiscoveryRule' => ['itemid'],
2259				'triggerids' => array_keys($parent_triggerids)
2260			]);
2261		}
2262		// ZBX_FLAG_DISCOVERY_NORMAL
2263		else {
2264			$db_triggers = API::Trigger()->get([
2265				'output' => ['triggerid', 'templateid'],
2266				'selectHosts' => ['hostid'],
2267				'triggerids' => array_keys($parent_triggerids)
2268			]);
2269		}
2270
2271		$all_parent_triggerids += $parent_triggerids;
2272		$parent_triggerids = [];
2273
2274		foreach ($db_triggers as $db_trigger) {
2275			foreach ($db_trigger['hosts'] as $host) {
2276				$data['templates'][$host['hostid']] = [];
2277				$hostids[$db_trigger['triggerid']][] = $host['hostid'];
2278			}
2279
2280			if ($flag == ZBX_FLAG_DISCOVERY_PROTOTYPE) {
2281				$lld_ruleids[$db_trigger['triggerid']] = $db_trigger['discoveryRule']['itemid'];
2282			}
2283
2284			if ($db_trigger['templateid'] != 0) {
2285				if (!array_key_exists($db_trigger['templateid'], $all_parent_triggerids)) {
2286					$parent_triggerids[$db_trigger['templateid']] = true;
2287				}
2288
2289				$data['links'][$db_trigger['triggerid']] = ['triggerid' => $db_trigger['templateid']];
2290			}
2291		}
2292	}
2293	while ($parent_triggerids);
2294
2295	foreach ($data['links'] as &$parent_trigger) {
2296		$parent_trigger['hostids'] = array_key_exists($parent_trigger['triggerid'], $hostids)
2297			? $hostids[$parent_trigger['triggerid']]
2298			: [0];
2299
2300		if ($flag == ZBX_FLAG_DISCOVERY_PROTOTYPE) {
2301			$parent_trigger['lld_ruleid'] = array_key_exists($parent_trigger['triggerid'], $lld_ruleids)
2302				? $lld_ruleids[$parent_trigger['triggerid']]
2303				: 0;
2304		}
2305	}
2306	unset($parent_trigger);
2307
2308	$db_templates = $data['templates']
2309		? API::Template()->get([
2310			'output' => ['name'],
2311			'templateids' => array_keys($data['templates']),
2312			'preservekeys' => true
2313		])
2314		: [];
2315
2316	$rw_templates = $db_templates
2317		? API::Template()->get([
2318			'output' => [],
2319			'templateids' => array_keys($db_templates),
2320			'editable' => true,
2321			'preservekeys' => true
2322		])
2323		: [];
2324
2325	$data['templates'][0] = [];
2326
2327	foreach ($data['templates'] as $hostid => &$template) {
2328		$template = array_key_exists($hostid, $db_templates)
2329			? [
2330				'hostid' => $hostid,
2331				'name' => $db_templates[$hostid]['name'],
2332				'permission' => array_key_exists($hostid, $rw_templates) ? PERM_READ_WRITE : PERM_READ
2333			]
2334			: [
2335				'hostid' => $hostid,
2336				'name' => _('Inaccessible template'),
2337				'permission' => PERM_DENY
2338			];
2339	}
2340	unset($template);
2341
2342	return $data;
2343}
2344
2345/**
2346 * Returns a template prefix for selected trigger.
2347 *
2348 * @param string $triggerid
2349 * @param array  $parent_templates  The list of the templates, prepared by getTriggerParentTemplates() function.
2350 * @param int    $flag              Origin of the trigger (ZBX_FLAG_DISCOVERY_NORMAL or ZBX_FLAG_DISCOVERY_PROTOTYPE).
2351 * @param bool   $provide_links     If this parameter is false, prefix will not contain links.
2352 *
2353 * @return array|null
2354 */
2355function makeTriggerTemplatePrefix($triggerid, array $parent_templates, $flag, bool $provide_links) {
2356	if (!array_key_exists($triggerid, $parent_templates['links'])) {
2357		return null;
2358	}
2359
2360	while (array_key_exists($parent_templates['links'][$triggerid]['triggerid'], $parent_templates['links'])) {
2361		$triggerid = $parent_templates['links'][$triggerid]['triggerid'];
2362	}
2363
2364	$templates = [];
2365	foreach ($parent_templates['links'][$triggerid]['hostids'] as $hostid) {
2366		$templates[] = $parent_templates['templates'][$hostid];
2367	}
2368
2369	CArrayHelper::sort($templates, ['name']);
2370
2371	$list = [];
2372
2373	foreach ($templates as $template) {
2374		if ($provide_links && $template['permission'] == PERM_READ_WRITE) {
2375			if ($flag == ZBX_FLAG_DISCOVERY_PROTOTYPE) {
2376				$url = (new CUrl('trigger_prototypes.php'))
2377					->setArgument('parent_discoveryid', $parent_templates['links'][$triggerid]['lld_ruleid'])
2378					->setArgument('context', 'template');
2379			}
2380			// ZBX_FLAG_DISCOVERY_NORMAL
2381			else {
2382				$url = (new CUrl('triggers.php'))
2383					->setArgument('filter_hostids', [$template['hostid']])
2384					->setArgument('filter_set', 1)
2385					->setArgument('context', 'template');
2386			}
2387
2388			$name = (new CLink(CHtml::encode($template['name']), $url))->addClass(ZBX_STYLE_LINK_ALT);
2389		}
2390		else {
2391			$name = new CSpan(CHtml::encode($template['name']));
2392		}
2393
2394		$list[] = $name->addClass(ZBX_STYLE_GREY);
2395		$list[] = ', ';
2396	}
2397
2398	array_pop($list);
2399	$list[] = NAME_DELIMITER;
2400
2401	return $list;
2402}
2403
2404/**
2405 * Returns a list of trigger templates.
2406 *
2407 * @param string $triggerid
2408 * @param array  $parent_templates  The list of the templates, prepared by getTriggerParentTemplates() function.
2409 * @param int    $flag              Origin of the trigger (ZBX_FLAG_DISCOVERY_NORMAL or ZBX_FLAG_DISCOVERY_PROTOTYPE).
2410 * @param bool   $provide_links     If this parameter is false, prefix will not contain links.
2411 *
2412 * @return array
2413 */
2414function makeTriggerTemplatesHtml($triggerid, array $parent_templates, $flag, bool $provide_links) {
2415	$list = [];
2416
2417	while (array_key_exists($triggerid, $parent_templates['links'])) {
2418		$list_item = [];
2419		$templates = [];
2420
2421		foreach ($parent_templates['links'][$triggerid]['hostids'] as $hostid) {
2422			$templates[] = $parent_templates['templates'][$hostid];
2423		}
2424
2425		$show_parentheses = (count($templates) > 1 && $list);
2426
2427		if ($show_parentheses) {
2428			CArrayHelper::sort($templates, ['name']);
2429			$list_item[] = '(';
2430		}
2431
2432		foreach ($templates as $template) {
2433			if ($provide_links && $template['permission'] == PERM_READ_WRITE) {
2434				if ($flag == ZBX_FLAG_DISCOVERY_PROTOTYPE) {
2435					$url = (new CUrl('trigger_prototypes.php'))
2436						->setArgument('form', 'update')
2437						->setArgument('triggerid', $parent_templates['links'][$triggerid]['triggerid'])
2438						->setArgument('parent_discoveryid', $parent_templates['links'][$triggerid]['lld_ruleid'])
2439						->setArgument('context', 'template');
2440				}
2441				// ZBX_FLAG_DISCOVERY_NORMAL
2442				else {
2443					$url = (new CUrl('triggers.php'))
2444						->setArgument('form', 'update')
2445						->setArgument('triggerid', $parent_templates['links'][$triggerid]['triggerid'])
2446						->setArgument('hostid', $template['hostid'])
2447						->setArgument('context', 'template');
2448				}
2449
2450				$name = new CLink(CHtml::encode($template['name']), $url);
2451			}
2452			else {
2453				$name = (new CSpan(CHtml::encode($template['name'])))->addClass(ZBX_STYLE_GREY);
2454			}
2455
2456			$list_item[] = $name;
2457			$list_item[] = ', ';
2458		}
2459		array_pop($list_item);
2460
2461		if ($show_parentheses) {
2462			$list_item[] = ')';
2463		}
2464
2465		array_unshift($list, $list_item, '&nbsp;&rArr;&nbsp;');
2466
2467		$triggerid = $parent_templates['links'][$triggerid]['triggerid'];
2468	}
2469
2470	if ($list) {
2471		array_pop($list);
2472	}
2473
2474	return $list;
2475}
2476
2477/**
2478 * Check if user has read permissions for triggers.
2479 *
2480 * @param $triggerids
2481 *
2482 * @return bool
2483 */
2484function isReadableTriggers(array $triggerids) {
2485	return count($triggerids) == API::Trigger()->get([
2486		'triggerids' => $triggerids,
2487		'countOutput' => true
2488	]);
2489}
2490
2491/**
2492 * Returns a list of the trigger dependencies.
2493 *
2494 * @param array  $triggers
2495 * @param array  $triggers[<triggerid>]['dependencies']
2496 * @param string $triggers[<triggerid>]['dependencies'][]['triggerid']
2497 *
2498 * @return array
2499 */
2500function getTriggerDependencies(array $triggers) {
2501	$triggerids = [];
2502	$triggerids_up = [];
2503	$triggerids_down = [];
2504
2505	// "Depends on" triggers.
2506	foreach ($triggers as $triggerid => $trigger) {
2507		foreach ($trigger['dependencies'] as $dependency) {
2508			$triggerids[$dependency['triggerid']] = true;
2509			$triggerids_up[$triggerid][] = $dependency['triggerid'];
2510		}
2511	}
2512
2513	// "Dependent" triggers.
2514	$db_trigger_depends = DBselect(
2515		'SELECT triggerid_down,triggerid_up'.
2516		' FROM trigger_depends'.
2517		' WHERE '.dbConditionInt('triggerid_up', array_keys($triggers))
2518	);
2519
2520	while ($row = DBfetch($db_trigger_depends)) {
2521		$triggerids[$row['triggerid_down']] = true;
2522		$triggerids_down[$row['triggerid_up']][] = $row['triggerid_down'];
2523	}
2524
2525	$dependencies = [];
2526
2527	if (!$triggerids) {
2528		return $dependencies;
2529	}
2530
2531	$db_triggers = API::Trigger()->get([
2532		'output' => ['expression', 'description'],
2533		'triggerids' => array_keys($triggerids),
2534		'preservekeys' => true
2535	]);
2536	$db_triggers = CMacrosResolverHelper::resolveTriggerNames($db_triggers);
2537
2538	foreach ($triggerids_up as $triggerid_up => $triggerids) {
2539		foreach ($triggerids as $triggerid) {
2540			$dependencies[$triggerid_up]['down'][] = array_key_exists($triggerid, $db_triggers)
2541				? $db_triggers[$triggerid]['description']
2542				: _('Inaccessible trigger');
2543		}
2544	}
2545
2546	foreach ($triggerids_down as $triggerid_down => $triggerids) {
2547		foreach ($triggerids as $triggerid) {
2548			$dependencies[$triggerid_down]['up'][] = array_key_exists($triggerid, $db_triggers)
2549				? $db_triggers[$triggerid]['description']
2550				: _('Inaccessible trigger');
2551		}
2552	}
2553
2554	return $dependencies;
2555}
2556
2557/**
2558 * Returns icons with tooltips for triggers with dependencies.
2559 *
2560 * @param array  $dependencies
2561 * @param array  $dependencies['up']    (optional) The list of "Dependent" triggers.
2562 * @param array  $dependencies['down']  (optional) The list of "Depeneds on" triggers.
2563 * @param bool   $freeze_on_click
2564 *
2565 * @return array
2566 */
2567function makeTriggerDependencies(array $dependencies, $freeze_on_click = true) {
2568	$result = [];
2569
2570	foreach (['down', 'up'] as $type) {
2571		if (array_key_exists($type, $dependencies)) {
2572			$header = ($type === 'down') ? _('Depends on') : _('Dependent');
2573			$class = ($type === 'down') ? ZBX_STYLE_ICON_DEPEND_DOWN : ZBX_STYLE_ICON_DEPEND_UP;
2574
2575			$table = (new CTableInfo())
2576				->setAttribute('style', 'max-width: '.ZBX_TEXTAREA_STANDARD_WIDTH.'px;')
2577				->setHeader([$header]);
2578
2579			foreach ($dependencies[$type] as $description) {
2580				$table->addRow($description);
2581			}
2582
2583			$result[] = (new CSpan())
2584				->addClass($class)
2585				->addClass(ZBX_STYLE_CURSOR_POINTER)
2586				->setHint($table, '', $freeze_on_click);
2587		}
2588	}
2589
2590	return $result;
2591}
2592
2593/**
2594 * Return list of functions that can be used without /host/key reference.
2595 *
2596 * @return array
2597 */
2598function getStandaloneFunctions(): array {
2599	return ['date', 'dayofmonth', 'dayofweek', 'time', 'now'];
2600}
2601
2602/**
2603 * Returns a list of functions that return a constant or random number.
2604 *
2605 * @return array
2606 */
2607function getFunctionsConstants(): array {
2608	return ['e', 'pi', 'rand'];
2609}
2610