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; ifnot, write to the Free Software
18** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19**/
20
21
22function getSeverityStyle($severity, $type = true) {
23	if (!$type) {
24		return ZBX_STYLE_NORMAL_BG;
25	}
26
27	switch ($severity) {
28		case TRIGGER_SEVERITY_DISASTER:
29			return ZBX_STYLE_DISASTER_BG;
30		case TRIGGER_SEVERITY_HIGH:
31			return ZBX_STYLE_HIGH_BG;
32		case TRIGGER_SEVERITY_AVERAGE:
33			return ZBX_STYLE_AVERAGE_BG;
34		case TRIGGER_SEVERITY_WARNING:
35			return ZBX_STYLE_WARNING_BG;
36		case TRIGGER_SEVERITY_INFORMATION:
37			return ZBX_STYLE_INFO_BG;
38		case TRIGGER_SEVERITY_NOT_CLASSIFIED:
39			return ZBX_STYLE_NA_BG;
40		default:
41			return null;
42	}
43}
44
45/**
46 *	Get trigger severity name by given state and configuration.
47 *
48 * @param int	 $severity		trigger severity
49 * @param array  $config		array containing configuration parameters containing severity names
50 *
51 * @return string
52 */
53function getSeverityName($severity, array $config) {
54	switch ($severity) {
55		case TRIGGER_SEVERITY_NOT_CLASSIFIED:
56			return _($config['severity_name_0']);
57		case TRIGGER_SEVERITY_INFORMATION:
58			return _($config['severity_name_1']);
59		case TRIGGER_SEVERITY_WARNING:
60			return _($config['severity_name_2']);
61		case TRIGGER_SEVERITY_AVERAGE:
62			return _($config['severity_name_3']);
63		case TRIGGER_SEVERITY_HIGH:
64			return _($config['severity_name_4']);
65		case TRIGGER_SEVERITY_DISASTER:
66			return _($config['severity_name_5']);
67		default:
68			return _('Unknown');
69	}
70}
71
72function getSeverityColor($severity, $value = TRIGGER_VALUE_TRUE) {
73	if ($value == TRIGGER_VALUE_FALSE) {
74		return 'AAFFAA';
75	}
76	$config = select_config();
77
78	switch ($severity) {
79		case TRIGGER_SEVERITY_DISASTER:
80			$color = $config['severity_color_5'];
81			break;
82		case TRIGGER_SEVERITY_HIGH:
83			$color = $config['severity_color_4'];
84			break;
85		case TRIGGER_SEVERITY_AVERAGE:
86			$color = $config['severity_color_3'];
87			break;
88		case TRIGGER_SEVERITY_WARNING:
89			$color = $config['severity_color_2'];
90			break;
91		case TRIGGER_SEVERITY_INFORMATION:
92			$color = $config['severity_color_1'];
93			break;
94		case TRIGGER_SEVERITY_NOT_CLASSIFIED:
95			$color = $config['severity_color_0'];
96			break;
97		default:
98			$color = $config['severity_color_0'];
99	}
100
101	return $color;
102}
103
104/**
105 * Returns HTML representation of trigger severity cell containing severity name and color.
106 *
107 * @param int	 $severity			trigger severity
108 * @param array  $config			array of configuration parameters to get trigger severity name
109 * @param string $text				trigger severity name
110 * @param bool	 $forceNormal		true to return 'normal' class, false to return corresponding severity class
111 *
112 * @return CCol
113 */
114function getSeverityCell($severity, $config, $text = null, $forceNormal = false) {
115	if ($text === null) {
116		$text = CHtml::encode(getSeverityName($severity, $config));
117	}
118
119	return (new CCol($text))->addClass(getSeverityStyle($severity, !$forceNormal));
120}
121
122/**
123 * Add color style and blinking to an object like CSpan or CDiv depending on trigger status
124 * Settings and colors are kept in 'config' database table
125 *
126 * @param mixed $object             object like CSpan, CDiv, etc.
127 * @param int $triggerValue         TRIGGER_VALUE_FALSE or TRIGGER_VALUE_TRUE
128 * @param int $triggerLastChange
129 * @param bool $isAcknowledged
130 * @return void
131 */
132function addTriggerValueStyle($object, $triggerValue, $triggerLastChange, $isAcknowledged) {
133	$config = select_config();
134
135	// color of text and blinking depends on trigger value and whether event is acknowledged
136	if ($triggerValue == TRIGGER_VALUE_TRUE && !$isAcknowledged) {
137		$color = $config['problem_unack_color'];
138		$blinks = $config['problem_unack_style'];
139	}
140	elseif ($triggerValue == TRIGGER_VALUE_TRUE && $isAcknowledged) {
141		$color = $config['problem_ack_color'];
142		$blinks = $config['problem_ack_style'];
143	}
144	elseif ($triggerValue == TRIGGER_VALUE_FALSE && !$isAcknowledged) {
145		$color = $config['ok_unack_color'];
146		$blinks = $config['ok_unack_style'];
147	}
148	elseif ($triggerValue == TRIGGER_VALUE_FALSE && $isAcknowledged) {
149		$color = $config['ok_ack_color'];
150		$blinks = $config['ok_ack_style'];
151	}
152	if (isset($color) && isset($blinks)) {
153		// color
154		$object->addStyle('color: #'.$color);
155
156		// blinking
157		$timeSinceLastChange = time() - $triggerLastChange;
158		if ($blinks && $timeSinceLastChange < $config['blink_period']) {
159			$object->addClass('blink'); // elements with this class will blink
160			$object->setAttribute('data-time-to-blink', $config['blink_period'] - $timeSinceLastChange);
161		}
162	}
163	else {
164		$object->addClass(ZBX_STYLE_GREY);
165	}
166}
167
168function trigger_value2str($value = null) {
169	$triggerValues = [
170		TRIGGER_VALUE_FALSE => _('OK'),
171		TRIGGER_VALUE_TRUE => _('PROBLEM')
172	];
173
174	if ($value === null) {
175		return $triggerValues;
176	}
177	elseif (isset($triggerValues[$value])) {
178		return $triggerValues[$value];
179	}
180	else {
181		return _('Unknown');
182	}
183}
184
185function discovery_value($val = null) {
186	$array = [
187		DOBJECT_STATUS_UP => _('UP'),
188		DOBJECT_STATUS_DOWN => _('DOWN'),
189		DOBJECT_STATUS_DISCOVER => _('DISCOVERED'),
190		DOBJECT_STATUS_LOST => _('LOST')
191	];
192
193	if (is_null($val)) {
194		return $array;
195	}
196	elseif (isset($array[$val])) {
197		return $array[$val];
198	}
199	else {
200		return _('Unknown');
201	}
202}
203
204function discovery_value_style($val) {
205	switch ($val) {
206		case DOBJECT_STATUS_UP:
207			$style = ZBX_STYLE_GREEN;
208			break;
209		case DOBJECT_STATUS_DOWN:
210			$style = ZBX_STYLE_RED;
211			break;
212		case DOBJECT_STATUS_DISCOVER:
213			$style = ZBX_STYLE_GREEN;
214			break;
215		case DOBJECT_STATUS_LOST:
216			$style = ZBX_STYLE_GREY;
217			break;
218		default:
219			$style = '';
220	}
221
222	return $style;
223}
224
225function getParentHostsByTriggers($triggers) {
226	$hosts = [];
227	$triggerParent = [];
228
229	while (!empty($triggers)) {
230		foreach ($triggers as $tnum => $trigger) {
231			if ($trigger['templateid'] == 0) {
232				if (isset($triggerParent[$trigger['triggerid']])) {
233					foreach ($triggerParent[$trigger['triggerid']] as $triggerid => $state) {
234						$hosts[$triggerid] = $trigger['hosts'];
235					}
236				}
237				else {
238					$hosts[$trigger['triggerid']] = $trigger['hosts'];
239				}
240				unset($triggers[$tnum]);
241			}
242			else {
243				if (isset($triggerParent[$trigger['triggerid']])) {
244					if (!isset($triggerParent[$trigger['templateid']])) {
245						$triggerParent[$trigger['templateid']] = [];
246					}
247					$triggerParent[$trigger['templateid']][$trigger['triggerid']] = 1;
248					$triggerParent[$trigger['templateid']] += $triggerParent[$trigger['triggerid']];
249				}
250				else {
251					if (!isset($triggerParent[$trigger['templateid']])) {
252						$triggerParent[$trigger['templateid']] = [];
253					}
254					$triggerParent[$trigger['templateid']][$trigger['triggerid']] = 1;
255				}
256			}
257		}
258		$triggers = API::Trigger()->get([
259			'triggerids' => zbx_objectValues($triggers, 'templateid'),
260			'selectHosts' => ['hostid', 'host', 'name', 'status'],
261			'output' => ['triggerid', 'templateid'],
262			'filter' => ['flags' => null]
263		]);
264	}
265
266	return $hosts;
267}
268
269function get_trigger_by_triggerid($triggerid) {
270	$db_trigger = DBfetch(DBselect('SELECT t.* FROM triggers t WHERE t.triggerid='.zbx_dbstr($triggerid)));
271	if (!empty($db_trigger)) {
272		return $db_trigger;
273	}
274	error(_s('No trigger with triggerid "%1$s".', $triggerid));
275
276	return false;
277}
278
279function get_hosts_by_triggerid($triggerids) {
280	zbx_value2array($triggerids);
281
282	return DBselect(
283		'SELECT DISTINCT h.*'.
284		' FROM hosts h,functions f,items i'.
285		' WHERE i.itemid=f.itemid'.
286			' AND h.hostid=i.hostid'.
287			' AND '.dbConditionInt('f.triggerid', $triggerids)
288	);
289}
290
291function get_triggers_by_hostid($hostid) {
292	return DBselect(
293		'SELECT DISTINCT t.*'.
294		' FROM triggers t,functions f,items i'.
295		' WHERE i.hostid='.zbx_dbstr($hostid).
296			' AND f.itemid=i.itemid'.
297			' AND f.triggerid=t.triggerid'
298	);
299}
300
301// unescape Raw URL
302function utf8RawUrlDecode($source) {
303	$decodedStr = '';
304	$pos = 0;
305	$len = strlen($source);
306	while ($pos < $len) {
307		$charAt = substr($source, $pos, 1);
308		if ($charAt == '%') {
309			$pos++;
310			$charAt = substr($source, $pos, 1);
311			if ($charAt == 'u') {
312				// we got a unicode character
313				$pos++;
314				$unicodeHexVal = substr($source, $pos, 4);
315				$unicode = hexdec($unicodeHexVal);
316				$entity = "&#".$unicode.';';
317				$decodedStr .= html_entity_decode(utf8_encode($entity), ENT_COMPAT, 'UTF-8');
318				$pos += 4;
319			}
320			else {
321				$decodedStr .= substr($source, $pos-1, 1);
322			}
323		}
324		else {
325			$decodedStr .= $charAt;
326			$pos++;
327		}
328	}
329
330	return $decodedStr;
331}
332
333/**
334 * Copies the given triggers to the given hosts or templates.
335 *
336 * Without the $srcHostId parameter it will only be able to copy triggers that belong to only one host. If the
337 * $srcHostId parameter is not passed, and a trigger has multiple hosts, it will throw an error. If the
338 * $srcHostId parameter is passed, the given host will be replaced with the destination host.
339 *
340 * This function takes care of copied trigger dependencies.
341 * If trigger is copied alongside with trigger on which it depends, then dependencies is replaced directly using new ids,
342 * If there is target host within dependency trigger, algorithm will search for potential matching trigger in target host,
343 * if matching trigger is found, then id from this trigger is used, if not rise exception,
344 * otherwise original dependency will be left.
345 *
346 *
347 * @param int|array $srcTriggerIds triggers which will be copied to $dstHostIds
348 * @param int|array $dstHostIds hosts and templates to whom add triggers, ids not present in DB (host table) will be ignored
349 * @param int $srcHostId host id in which context trigger with multiple hosts will be treated
350 *
351 * @return bool
352 */
353function copyTriggersToHosts($srcTriggerIds, $dstHostIds, $srcHostId = null) {
354	$options = [
355		'triggerids' => $srcTriggerIds,
356		'output' => ['triggerid', 'expression', 'description', 'url', 'status', 'priority', 'comments', 'type'],
357		'filter' => ['flags' => ZBX_FLAG_DISCOVERY_NORMAL],
358		'selectDependencies' => ['triggerid']
359	];
360	if ($srcHostId) {
361		$srcHost = API::Host()->get([
362			'output' => ['host'],
363			'hostids' => $srcHostId,
364			'preservekeys' => true,
365			'templated_hosts' => true
366		]);
367
368		// If provided $srcHostId doesn't match any record in DB, return false.
369		if (!($srcHost = reset($srcHost))) {
370			return false;
371		}
372	}
373	// If no $srcHostId provided we will need trigger host 'host'.
374	else {
375		$options['selectHosts'] = ['host'];
376	}
377	$dbSrcTriggers = API::Trigger()->get($options);
378
379	$dbSrcTriggers = CMacrosResolverHelper::resolveTriggerExpressions($dbSrcTriggers);
380
381	$dbDstHosts = API::Host()->get([
382		'output' => ['hostid', 'host'],
383		'hostids' => $dstHostIds,
384		'preservekeys' => true,
385		'templated_hosts' => true
386	]);
387
388	$newTriggers = [];
389
390	// Create each trigger for each host.
391	foreach ($dbDstHosts as $dstHost) {
392		foreach ($dbSrcTriggers as $srcTrigger) {
393			// If $srcHostId provided, get host 'host' for triggerExpressionReplaceHost().
394			if ($srcHostId != 0) {
395				$host = $srcHost['host'];
396				$srcTriggerContextHostId = $srcHostId;
397			}
398			// If $srcHostId not provided, use source trigger first host 'host'.
399			else {
400
401				/*
402				 * If we have multiple hosts in trigger expression and we haven't pointed ($srcHostId) which host to
403				 * replace, call error.
404				 */
405				if (count($srcTrigger['hosts']) > 1) {
406					error(_s('Cannot copy trigger "%1$s:%2$s", because it has multiple hosts in the expression.',
407						$srcTrigger['description'], $srcTrigger['expression']
408					));
409
410					return false;
411				}
412				$host = $srcTrigger['hosts'][0]['host'];
413				$srcTriggerContextHostId = $srcTrigger['hosts'][0]['hostid'];
414			}
415
416			$srcTrigger['expression'] = triggerExpressionReplaceHost($srcTrigger['expression'], $host,
417				$dstHost['host']
418			);
419
420			// The dependddencies must be added after all triggers are created.
421			$result = API::Trigger()->create([[
422				'description' => $srcTrigger['description'],
423				'expression' => $srcTrigger['expression'],
424				'url' => $srcTrigger['url'],
425				'status' => $srcTrigger['status'],
426				'priority' => $srcTrigger['priority'],
427				'comments' => $srcTrigger['comments'],
428				'type' => $srcTrigger['type']
429			]]);
430
431			if (!$result) {
432				return false;
433			}
434
435			$newTriggers[$srcTrigger['triggerid']][] = [
436				'newTriggerId' => reset($result['triggerids']),
437				'newTriggerExpression' => $srcTrigger['expression'],
438				'newTriggerHostId' => $dstHost['hostid'],
439				'newTriggerHost' => $dstHost['host'],
440				'srcTriggerContextHostId' => $srcTriggerContextHostId,
441				'srcTriggerContextHost' => $host
442			];
443		}
444	}
445
446	$depids = [];
447	foreach ($dbSrcTriggers as $srcTrigger) {
448		foreach ($srcTrigger['dependencies'] as $depTrigger) {
449			$depids[] = $depTrigger['triggerid'];
450		}
451	}
452	$depTriggers = API::Trigger()->get([
453		'triggerids' => $depids,
454		'output' => ['description', 'expression'],
455		'selectHosts' => ['hostid'],
456		'preservekeys' => true
457	]);
458
459	$depTriggers = CMacrosResolverHelper::resolveTriggerExpressions($depTriggers);
460
461	// Map dependencies to the new trigger IDs and save.
462	if ($newTriggers) {
463		$dependencies = [];
464
465		foreach ($dbSrcTriggers as $srcTrigger) {
466			if ($srcTrigger['dependencies']) {
467				// Get corresponding created triggers.
468				$dst_triggers = $newTriggers[$srcTrigger['triggerid']];
469
470				foreach ($dst_triggers as $dst_trigger) {
471					foreach ($srcTrigger['dependencies'] as $depTrigger) {
472						/*
473						 * We have added $depTrigger trigger and we know corresponding trigger ID for newly
474						 * created trigger.
475						 */
476						if (array_key_exists($depTrigger['triggerid'], $newTriggers)) {
477							$dst_dep_triggers = $newTriggers[$depTrigger['triggerid']];
478
479							foreach ($dst_dep_triggers as $dst_dep_trigger) {
480								/*
481								 * Dependency is within same host according to $srcHostId parameter or dep trigger has
482								 * single host.
483								 */
484								if ($dst_trigger['srcTriggerContextHostId'] == $dst_dep_trigger['srcTriggerContextHostId']
485										&& $dst_dep_trigger['newTriggerHostId'] == $dst_trigger['newTriggerHostId']) {
486									$depTriggerId = $dst_dep_trigger['newTriggerId'];
487									break;
488								}
489								// Dependency is to trigger from another host.
490								else {
491									$depTriggerId = $depTrigger['triggerid'];
492								}
493							}
494						}
495						// We need to search for $depTrigger trigger if target host is within dependency hosts.
496						elseif (in_array(['hostid' => $dst_trigger['srcTriggerContextHostId']],
497								$depTriggers[$depTrigger['triggerid']]['hosts'])) {
498							// Get all possible $depTrigger matching triggers by description.
499							$targetHostTriggersByDescription = API::Trigger()->get([
500								'hostids' => $dst_trigger['newTriggerHostId'],
501								'output' => ['hosts', 'triggerid', 'expression'],
502								'filter' => ['description' => $depTriggers[$depTrigger['triggerid']]['description']],
503								'preservekeys' => true
504							]);
505
506							$targetHostTriggersByDescription =
507								CMacrosResolverHelper::resolveTriggerExpressions($targetHostTriggersByDescription);
508
509							// Compare exploded expressions for exact match.
510							$expr1 = $depTriggers[$depTrigger['triggerid']]['expression'];
511							$depTriggerId = null;
512
513							foreach ($targetHostTriggersByDescription as $potentialTargetTrigger) {
514								$expr2 = triggerExpressionReplaceHost($potentialTargetTrigger['expression'],
515									$dst_trigger['newTriggerHost'], $dst_trigger['srcTriggerContextHost']
516								);
517
518								if ($expr2 == $expr1) {
519									// Matching trigger has been found.
520									$depTriggerId = $potentialTargetTrigger['triggerid'];
521									break;
522								}
523							}
524
525							// If matching trigger wasn't found raise exception.
526							if ($depTriggerId === null) {
527								$expr2 = triggerExpressionReplaceHost($expr1, $dst_trigger['srcTriggerContextHost'],
528									$dst_trigger['newTriggerHost']
529								);
530
531								error(_s(
532									'Cannot add dependency from trigger "%1$s:%2$s" to non existing trigger "%3$s:%4$s".',
533									$srcTrigger['description'], $dst_trigger['newTriggerExpression'],
534									$depTriggers[$depTrigger['triggerid']]['description'], $expr2
535								));
536
537								return false;
538							}
539						}
540						// Leave original dependency.
541						else {
542							$depTriggerId = $depTrigger['triggerid'];
543						}
544
545						$dependencies[] = [
546							'triggerid' => $dst_trigger['newTriggerId'],
547							'dependsOnTriggerid' => $depTriggerId
548						];
549					}
550				}
551			}
552		}
553
554		if ($dependencies) {
555			if (!API::Trigger()->addDependencies($dependencies)) {
556				return false;
557			}
558		}
559	}
560
561	return true;
562}
563
564/**
565 * Purpose: Replaces host in trigger expression.
566 * {localhost:agent.ping.nodata(5m)}  =>  {localhost6:agent.ping.nodata(5m)}
567 *
568 * @param string $expression	full expression with host names and item keys
569 * @param string $src_host
570 * @param string $dst_host
571 *
572 * @return string
573 */
574function triggerExpressionReplaceHost($expression, $src_host, $dst_host) {
575	$new_expression = '';
576
577	$function_macro_parser = new CFunctionMacroParser();
578	$user_macro_parser = new CUserMacroParser();
579	$macro_parser = new CMacroParser(['{TRIGGER.VALUE}']);
580	$lld_macro_parser = new CLLDMacroParser();
581
582	for ($pos = 0, $pos_left = 0; isset($expression[$pos]); $pos++) {
583		if ($function_macro_parser->parse($expression, $pos) != CParser::PARSE_FAIL) {
584			$host = $function_macro_parser->getHost();
585			$item = $function_macro_parser->getItem();
586			$function = $function_macro_parser->getFunction();
587
588			if ($host === $src_host) {
589				$host = $dst_host;
590			}
591
592			$new_expression .= substr($expression, $pos_left, $pos - $pos_left);
593			$new_expression .= '{'.$host.':'.$item.'.'.$function.'}';
594			$pos_left = $pos + $function_macro_parser->getLength();
595
596			$pos += $function_macro_parser->getLength() - 1;
597		}
598		elseif ($user_macro_parser->parse($expression, $pos) != CParser::PARSE_FAIL) {
599			$pos += $user_macro_parser->getLength() - 1;
600		}
601		elseif ($macro_parser->parse($expression, $pos) != CParser::PARSE_FAIL) {
602			$pos += $macro_parser->getLength() - 1;
603		}
604		elseif ($lld_macro_parser->parse($expression, $pos) != CParser::PARSE_FAIL) {
605			$pos += $lld_macro_parser->getLength() - 1;
606		}
607	}
608
609	$new_expression .= substr($expression, $pos_left, $pos - $pos_left);
610
611	return $new_expression;
612}
613
614/**
615 * Implodes expression, replaces names and keys with IDs.
616 *
617 * For example: localhost:system.cpu.load.last(0)>10 will be translated to {12}>10 and created database representation.
618 *
619 * @throws Exception if error occurred
620 *
621 * @param string $expression Full expression with host names and item keys
622 * @param numeric $triggerid
623 * @param array optional $hostnames Reference to array which will be filled with unique visible host names.
624 *
625 * @return string Imploded expression (names and keys replaced by IDs)
626 */
627function implode_exp($expression, $triggerId, &$hostnames = []) {
628	$expressionData = new CTriggerExpression();
629	if (!$expressionData->parse($expression)) {
630		throw new Exception($expressionData->error);
631	}
632
633	$newFunctions = [];
634	$functions = [];
635	$items = [];
636	$triggerFunctionValidator = new CFunctionValidator();
637
638	foreach ($expressionData->expressions as $exprPart) {
639		if (isset($newFunctions[$exprPart['expression']])) {
640			continue;
641		}
642
643		if (!isset($items[$exprPart['host']][$exprPart['item']])) {
644			$result = DBselect(
645				'SELECT i.itemid,i.value_type,h.name'.
646				' FROM items i,hosts h'.
647				' WHERE i.key_='.zbx_dbstr($exprPart['item']).
648					' AND '.dbConditionInt('i.flags', [ZBX_FLAG_DISCOVERY_NORMAL, ZBX_FLAG_DISCOVERY_CREATED, ZBX_FLAG_DISCOVERY_PROTOTYPE]).
649					' AND h.host='.zbx_dbstr($exprPart['host']).
650					' AND h.hostid=i.hostid'
651			);
652			if ($row = DBfetch($result)) {
653				$hostnames[] = $row['name'];
654				$items[$exprPart['host']][$exprPart['item']] = [
655					'itemid' => $row['itemid'],
656					'valueType' => $row['value_type']
657				];
658			}
659			else {
660				throw new Exception(_s('Incorrect item key "%1$s" provided for trigger expression on "%2$s".',
661						$exprPart['item'], $exprPart['host']));
662			}
663		}
664
665		if (!$triggerFunctionValidator->validate([
666				'function' => $exprPart['function'],
667				'functionName' => $exprPart['functionName'],
668				'functionParamList' => $exprPart['functionParamList'],
669				'valueType' => $items[$exprPart['host']][$exprPart['item']]['valueType']])) {
670			throw new Exception($triggerFunctionValidator->getError());
671		}
672
673		$newFunctions[$exprPart['expression']] = 0;
674
675		$functions[] = [
676			'itemid' => $items[$exprPart['host']][$exprPart['item']]['itemid'],
677			'triggerid' => $triggerId,
678			'function' => $exprPart['functionName'],
679			'parameter' => $exprPart['functionParam']
680		];
681	}
682
683	$functionIds = DB::insert('functions', $functions);
684
685	$num = 0;
686	foreach ($newFunctions as &$newFunction) {
687		$newFunction = $functionIds[$num++];
688	}
689	unset($newFunction);
690
691	$exprPart = end($expressionData->expressions);
692	do {
693		$expression = substr_replace($expression, '{'.$newFunctions[$exprPart['expression']].'}',
694				$exprPart['pos'], strlen($exprPart['expression']));
695	}
696	while ($exprPart = prev($expressionData->expressions));
697
698	$hostnames = array_unique($hostnames);
699
700	return $expression;
701}
702
703function check_right_on_trigger_by_expression($permission, $expression) {
704	$expressionData = new CTriggerExpression();
705	if (!$expressionData->parse($expression)) {
706		error($expressionData->error);
707		return false;
708	}
709	$expressionHosts = $expressionData->getHosts();
710
711	$hosts = API::Host()->get([
712		'filter' => ['host' => $expressionHosts],
713		'editable' => ($permission == PERM_READ_WRITE),
714		'output' => ['hostid', 'host'],
715		'templated_hosts' => true,
716		'preservekeys' => true
717	]);
718	$hosts = zbx_toHash($hosts, 'host');
719
720	foreach ($expressionHosts as $host) {
721		if (!isset($hosts[$host])) {
722			error(_s('Incorrect trigger expression. Host "%1$s" does not exist or you have no access to this host.', $host));
723			return false;
724		}
725	}
726
727	return true;
728}
729
730function replace_template_dependencies($deps, $hostid) {
731	foreach ($deps as $id => $val) {
732		$sql = 'SELECT t.triggerid'.
733				' FROM triggers t,functions f,items i'.
734				' WHERE t.triggerid=f.triggerid'.
735					' AND f.itemid=i.itemid'.
736					' AND t.templateid='.zbx_dbstr($val).
737					' AND i.hostid='.zbx_dbstr($hostid);
738		if ($db_new_dep = DBfetch(DBselect($sql))) {
739			$deps[$id] = $db_new_dep['triggerid'];
740		}
741	}
742
743	return $deps;
744}
745
746/**
747 * Creates and returns the trigger overview table for the given hosts.
748 *
749 * @param array  	$hosts							an array of hosts with host IDs as keys
750 * @param string 	$hosts[hostid][name]
751 * @param string 	$hosts[hostid][hostid]
752 * @param array		$triggers
753 * @param string	$triggers[][triggerid]
754 * @param string	$triggers[][description]
755 * @param string	$triggers[][expression]
756 * @param int		$triggers[][value]
757 * @param int		$triggers[][lastchange]
758 * @param int		$triggers[][flags]
759 * @param array		$triggers[][url]
760 * @param int		$triggers[][priority]
761 * @param array		$triggers[][hosts]
762 * @param string	$triggers[][hosts][][hostid]
763 * @param string	$triggers[][hosts][][name]
764 * @param string 	$pageFile						the page where the element is displayed
765 * @param int    	$viewMode						table display style: either hosts on top, or host on the left side
766 * @param string 	$screenId						the ID of the screen, that contains the trigger overview table
767 *
768 * @return CTableInfo
769 */
770function getTriggersOverview(array $hosts, array $triggers, $pageFile, $viewMode = null, $screenId = null) {
771	$data = [];
772	$hostNames = [];
773	$trcounter = [];
774
775	$triggers = CMacrosResolverHelper::resolveTriggerNames($triggers, true);
776
777	foreach ($triggers as $trigger) {
778		$trigger_name = $trigger['description'];
779
780		foreach ($trigger['hosts'] as $host) {
781			// triggers may belong to hosts that are filtered out and shouldn't be displayed, skip them
782			if (!isset($hosts[$host['hostid']])) {
783				continue;
784			}
785
786			$hostNames[$host['hostid']] = $host['name'];
787
788			if (!array_key_exists($host['name'], $trcounter)) {
789				$trcounter[$host['name']] = [];
790			}
791
792			if (!array_key_exists($trigger_name, $trcounter[$host['name']])) {
793				$trcounter[$host['name']][$trigger_name] = 0;
794			}
795
796			$data[$trigger_name][$trcounter[$host['name']][$trigger_name]][$host['name']] = [
797				'groupid' => $trigger['groupid'],
798				'hostid' => $host['hostid'],
799				'triggerid' => $trigger['triggerid'],
800				'value' => $trigger['value'],
801				'lastchange' => $trigger['lastchange'],
802				'priority' => $trigger['priority'],
803				'flags' => $trigger['flags'],
804				'url' => $trigger['url'],
805				'hosts' => $trigger['hosts'],
806				'items' => $trigger['items']
807			];
808			$trcounter[$host['name']][$trigger_name]++;
809		}
810	}
811
812	$triggerTable = new CTableInfo();
813
814	if (empty($hostNames)) {
815		return $triggerTable;
816	}
817
818	$triggerTable->makeVerticalRotation();
819
820	order_result($hostNames);
821
822	if ($viewMode == STYLE_TOP) {
823		// header
824		$header = [_('Triggers')];
825
826		foreach ($hostNames as $hostName) {
827			$header[] = (new CColHeader($hostName))->addClass('vertical_rotation');
828		}
829		$triggerTable->setHeader($header);
830
831		// data
832		foreach ($data as $trigger_name => $trigger_data) {
833			foreach ($trigger_data as $trigger_hosts) {
834				$columns = [nbsp($trigger_name)];
835
836				foreach ($hostNames as $hostName) {
837					$columns[] = getTriggerOverviewCells(
838						isset($trigger_hosts[$hostName]) ? $trigger_hosts[$hostName] : null,
839						$pageFile,
840						$screenId
841					);
842				}
843				$triggerTable->addRow($columns);
844			}
845		}
846	}
847	else {
848		// header
849		$header = [_('Host')];
850
851		foreach ($data as $trigger_name => $trigger_data) {
852			foreach ($trigger_data as $trigger_hosts) {
853				$header[] = (new CColHeader($trigger_name))->addClass('vertical_rotation');
854			}
855		}
856
857		$triggerTable->setHeader($header);
858
859		// data
860		$scripts = API::Script()->getScriptsByHosts(zbx_objectValues($hosts, 'hostid'));
861
862		foreach ($hostNames as $hostId => $hostName) {
863			$name = (new CSpan($hostName))->addClass(ZBX_STYLE_LINK_ACTION);
864			$name->setMenuPopup(CMenuPopupHelper::getHost($hosts[$hostId], $scripts[$hostId]));
865
866			$columns = [(new CCol($name))->addClass(ZBX_STYLE_NOWRAP)];
867			foreach ($data as $trigger_data) {
868				foreach ($trigger_data as $trigger_hosts) {
869					$columns[] = getTriggerOverviewCells(
870						isset($trigger_hosts[$hostName]) ? $trigger_hosts[$hostName] : null,
871						$pageFile,
872						$screenId
873					);
874				}
875			}
876
877			$triggerTable->addRow($columns);
878		}
879	}
880
881	return $triggerTable;
882}
883
884/**
885 * Creates and returns a trigger status cell for the trigger overview table.
886 *
887 * @see getTriggersOverview()
888 *
889 * @param array  $trigger
890 * @param string $pageFile		the page where the element is displayed
891 * @param string $screenid
892 *
893 * @return CCol
894 */
895function getTriggerOverviewCells($trigger, $pageFile, $screenid = null) {
896	$ack = null;
897	$css = null;
898	$desc = [];
899	$acknowledge = [];
900
901	// for how long triggers should blink on status change (set by user in administration->general)
902	$config = select_config();
903
904	if ($trigger) {
905		$css = getSeverityStyle($trigger['priority'], $trigger['value'] == TRIGGER_VALUE_TRUE);
906
907		// problem trigger
908		if ($trigger['value'] == TRIGGER_VALUE_TRUE) {
909			$ack = null;
910
911			if ($config['event_ack_enable']) {
912				if ($event = get_last_event_by_triggerid($trigger['triggerid'])) {
913					if ($screenid !== null) {
914						$acknowledge = [
915							'eventid' => $event['eventid'],
916							'backurl' => $pageFile.'?screenid='.$screenid
917						];
918					}
919					else {
920						$acknowledge = [
921							'eventid' => $event['eventid'],
922							'backurl' => $pageFile
923						];
924					}
925
926					if ($event['acknowledged'] == 1) {
927						$ack = (new CSpan())->addClass(ZBX_STYLE_ICON_ACKN);
928					}
929				}
930			}
931		}
932
933		// dependency: triggers on which depends this
934		$triggerId = empty($trigger['triggerid']) ? 0 : $trigger['triggerid'];
935
936		// trigger dependency DOWN
937		$dependencyTable = (new CTableInfo())
938			->setAttribute('style', 'width: 200px;')
939			->addRow(bold(_('Depends on').':'));
940
941		$isDependencyFound = false;
942		$dbDependencies = DBselect('SELECT td.* FROM trigger_depends td WHERE td.triggerid_down='.zbx_dbstr($triggerId));
943		while ($dbDependency = DBfetch($dbDependencies)) {
944			$dependencyTable->addRow(SPACE.'-'.SPACE.CMacrosResolverHelper::resolveTriggerNameById($dbDependency['triggerid_up']));
945			$isDependencyFound = true;
946		}
947
948		if ($isDependencyFound) {
949			$desc[] = (new CSpan())
950				->addClass(ZBX_STYLE_ICON_DEPEND_DOWN)
951				->setHint($dependencyTable, '', false);
952		}
953
954		// trigger dependency UP
955		$dependencyTable = (new CTableInfo())
956			->setAttribute('style', 'width: 200px;')
957			->addRow(bold(_('Dependent').':'));
958
959		$isDependencyFound = false;
960		$dbDependencies = DBselect('SELECT td.* FROM trigger_depends td WHERE td.triggerid_up='.zbx_dbstr($triggerId));
961		while ($dbDependency = DBfetch($dbDependencies)) {
962			$dependencyTable->addRow(SPACE.'-'.SPACE.CMacrosResolverHelper::resolveTriggerNameById($dbDependency['triggerid_down']));
963			$isDependencyFound = true;
964		}
965
966		if ($isDependencyFound) {
967			$desc[] = (new CSpan())
968				->addClass(ZBX_STYLE_ICON_DEPEND_UP)
969				->setHint($dependencyTable, '', false);
970		}
971	}
972
973	$column = new CCol([$desc, $ack]);
974
975	if ($css !== null) {
976		$column
977			->addClass($css)
978			->addClass(ZBX_STYLE_CURSOR_POINTER);
979	}
980
981	if ($trigger && $config['blink_period'] > 0 && time() - $trigger['lastchange'] < $config['blink_period']) {
982		$column->addClass('blink');
983		$column->setAttribute('data-toggle-class', $css);
984	}
985
986	if ($trigger) {
987		$column->setMenuPopup(CMenuPopupHelper::getTrigger($trigger, $acknowledge));
988	}
989
990	return $column;
991}
992
993/**
994 * Calculate trigger availability.
995 *
996 * @param int $triggerId		trigger id
997 * @param int $startTime		begin period
998 * @param int $endTime			end period
999 *
1000 * @return array
1001 */
1002function calculateAvailability($triggerId, $startTime, $endTime) {
1003	$startValue = TRIGGER_VALUE_FALSE;
1004
1005	if ($startTime > 0 && $startTime <= time()) {
1006		$sql = 'SELECT e.eventid,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<'.zbx_dbstr($startTime).
1012				' ORDER BY e.eventid DESC';
1013		if ($row = DBfetch(DBselect($sql, 1))) {
1014			$startValue = $row['value'];
1015		}
1016
1017		$min = $startTime;
1018	}
1019
1020	$sql = 'SELECT COUNT(e.eventid) AS cnt,MIN(e.clock) AS min_clock,MAX(e.clock) AS max_clock'.
1021			' FROM events e'.
1022			' WHERE e.objectid='.zbx_dbstr($triggerId).
1023				' AND e.source='.EVENT_SOURCE_TRIGGERS.
1024				' AND e.object='.EVENT_OBJECT_TRIGGER;
1025	if ($startTime) {
1026		$sql .= ' AND e.clock>='.zbx_dbstr($startTime);
1027	}
1028	if ($endTime) {
1029		$sql .= ' AND e.clock<='.zbx_dbstr($endTime);
1030	}
1031
1032	$dbEvents = DBfetch(DBselect($sql));
1033	if ($dbEvents['cnt'] > 0) {
1034		if (!isset($min)) {
1035			$min = $dbEvents['min_clock'];
1036		}
1037		$max = $dbEvents['max_clock'];
1038	}
1039	else {
1040		if ($startTime == 0 && $endTime == 0) {
1041			$max = time();
1042			$min = $max - SEC_PER_DAY;
1043		}
1044		else {
1045			$ret['true_time'] = 0;
1046			$ret['false_time'] = 0;
1047			$ret['true'] = (TRIGGER_VALUE_TRUE == $startValue) ? 100 : 0;
1048			$ret['false'] = (TRIGGER_VALUE_FALSE == $startValue) ? 100 : 0;
1049			return $ret;
1050		}
1051	}
1052
1053	$state = $startValue;
1054	$true_time = 0;
1055	$false_time = 0;
1056	$time = $min;
1057	if ($startTime == 0 && $endTime == 0) {
1058		$max = time();
1059	}
1060	if ($endTime == 0) {
1061		$endTime = $max;
1062	}
1063
1064	$rows = 0;
1065	$dbEvents = DBselect(
1066		'SELECT e.eventid,e.clock,e.value'.
1067		' FROM events e'.
1068		' WHERE e.objectid='.zbx_dbstr($triggerId).
1069			' AND e.source='.EVENT_SOURCE_TRIGGERS.
1070			' AND e.object='.EVENT_OBJECT_TRIGGER.
1071			' AND e.clock BETWEEN '.$min.' AND '.$max.
1072		' ORDER BY e.eventid'
1073	);
1074	while ($row = DBfetch($dbEvents)) {
1075		$clock = $row['clock'];
1076		$value = $row['value'];
1077
1078		$diff = max($clock - $time, 0);
1079		$time = $clock;
1080
1081		if ($state == 0) {
1082			$false_time += $diff;
1083			$state = $value;
1084		}
1085		elseif ($state == 1) {
1086			$true_time += $diff;
1087			$state = $value;
1088		}
1089		$rows++;
1090	}
1091
1092	if ($rows == 0) {
1093		$trigger = get_trigger_by_triggerid($triggerId);
1094		$state = $trigger['value'];
1095	}
1096
1097	if ($state == TRIGGER_VALUE_FALSE) {
1098		$false_time = $false_time + $endTime - $time;
1099	}
1100	elseif ($state == TRIGGER_VALUE_TRUE) {
1101		$true_time = $true_time + $endTime - $time;
1102	}
1103	$total_time = $true_time + $false_time;
1104
1105	if ($total_time == 0) {
1106		$ret['true_time'] = 0;
1107		$ret['false_time'] = 0;
1108		$ret['true'] = 0;
1109		$ret['false'] = 0;
1110	}
1111	else {
1112		$ret['true_time'] = $true_time;
1113		$ret['false_time'] = $false_time;
1114		$ret['true'] = (100 * $true_time) / $total_time;
1115		$ret['false'] = (100 * $false_time) / $total_time;
1116	}
1117
1118	return $ret;
1119}
1120
1121function get_triggers_unacknowledged($db_element, $count_problems = null, $ack = false) {
1122	$elements = [
1123		'hosts' => [],
1124		'hosts_groups' => [],
1125		'triggers' => []
1126	];
1127
1128	get_map_elements($db_element, $elements);
1129	if (empty($elements['hosts_groups']) && empty($elements['hosts']) && empty($elements['triggers'])) {
1130		return 0;
1131	}
1132
1133	$config = select_config();
1134
1135	$options = [
1136		'monitored' => true,
1137		'countOutput' => true,
1138		'filter' => [],
1139		'limit' => $config['search_limit'] + 1
1140	];
1141
1142	if ($ack) {
1143		$options['withAcknowledgedEvents'] = 1;
1144	}
1145	else {
1146		$options['withUnacknowledgedEvents'] = 1;
1147	}
1148
1149	if ($count_problems) {
1150		$options['filter']['value'] = TRIGGER_VALUE_TRUE;
1151	}
1152	if (!empty($elements['hosts_groups'])) {
1153		$options['groupids'] = array_unique($elements['hosts_groups']);
1154	}
1155	if (!empty($elements['hosts'])) {
1156		$options['hostids'] = array_unique($elements['hosts']);
1157	}
1158	if (!empty($elements['triggers'])) {
1159		$options['triggerids'] = array_unique($elements['triggers']);
1160	}
1161
1162	return API::Trigger()->get($options);
1163}
1164
1165function make_trigger_details($trigger) {
1166	$hostNames = [];
1167
1168	$config = select_config();
1169
1170	$hostIds = zbx_objectValues($trigger['hosts'], 'hostid');
1171
1172	$hosts = API::Host()->get([
1173		'output' => ['name', 'hostid', 'status'],
1174		'hostids' => $hostIds,
1175		'selectScreens' => API_OUTPUT_COUNT,
1176		'selectGraphs' => API_OUTPUT_COUNT
1177	]);
1178
1179	if (count($hosts) > 1) {
1180		order_result($hosts, 'name', ZBX_SORT_UP);
1181	}
1182
1183	$scripts = API::Script()->getScriptsByHosts($hostIds);
1184
1185	foreach ($hosts as $host) {
1186		$hostNames[] = (new CSpan($host['name']))
1187			->setMenuPopup(CMenuPopupHelper::getHost($host, $scripts[$host['hostid']]))
1188			->addClass(ZBX_STYLE_LINK_ACTION);
1189		$hostNames[] = ', ';
1190	}
1191	array_pop($hostNames);
1192
1193	$expression = CMacrosResolverHelper::resolveTriggerExpression($trigger['expression'],
1194		['html' => true, 'resolve_usermacros' => true, 'resolve_macros' => true]);
1195
1196	$table = (new CTableInfo())
1197		->addRow([
1198			new CCol(_n('Host', 'Hosts', count($hosts))),
1199			new CCol($hostNames)
1200		])
1201		->addRow([
1202			new CCol(_('Trigger')),
1203			new CCol(CMacrosResolverHelper::resolveTriggerName($trigger))
1204		])
1205		->addRow([
1206			_('Severity'),
1207			getSeverityCell($trigger['priority'], $config)
1208		])
1209		->addRow([
1210			new CCol(_('Expression')),
1211			new CCol($expression)
1212		])
1213		->addRow([_('Event generation'), _('Normal').((TRIGGER_MULT_EVENT_ENABLED == $trigger['type'])
1214			? SPACE.'+'.SPACE._('Multiple PROBLEM events') : '')])
1215		->addRow([_('Enabled'), (($trigger['status'] == TRIGGER_STATUS_ENABLED)
1216			? (new CCol(_('Yes')))->addClass(ZBX_STYLE_GREEN) : (new CCol(_('No')))->addClass(ZBX_STYLE_RED))
1217		]);
1218
1219	return $table;
1220}
1221
1222/**
1223 * Analyze an expression and returns expression html tree
1224 *
1225 * @param string $expression
1226 *
1227 * @return array
1228 */
1229function analyzeExpression($expression) {
1230	if (empty($expression)) {
1231		return ['', null];
1232	}
1233
1234	$expressionData = new CTriggerExpression();
1235	if (!$expressionData->parse($expression)) {
1236		error($expressionData->error);
1237		return false;
1238	}
1239
1240	$expressionTree[] = getExpressionTree($expressionData, 0, strlen($expressionData->expression) - 1);
1241
1242	$next = [];
1243	$letterNum = 0;
1244	return buildExpressionHtmlTree($expressionTree, $next, $letterNum);
1245}
1246
1247/**
1248 * Builds expression html tree
1249 *
1250 * @param array 	$expressionTree 	output of getExpressionTree() function
1251 * @param array 	$next           	parameter only for recursive call; should be empty array
1252 * @param int 		$letterNum      	parameter only for recursive call; should be 0
1253 * @param int 		$level          	parameter only for recursive call
1254 * @param string 	$operator       	parameter only for recursive call
1255 *
1256 * @return array	array containing the trigger expression formula as the first element and an array describing the
1257 *					expression tree as the second
1258 */
1259function buildExpressionHtmlTree(array $expressionTree, array &$next, &$letterNum, $level = 0, $operator = null) {
1260	$treeList = [];
1261	$outline = '';
1262
1263	end($expressionTree);
1264	$lastKey = key($expressionTree);
1265
1266	foreach ($expressionTree as $key => $element) {
1267		switch ($element['type']) {
1268			case 'operator':
1269				$next[$level] = ($key != $lastKey);
1270				$expr = expressionLevelDraw($next, $level);
1271				$expr[] = SPACE;
1272				$expr[] = ($element['operator'] === 'and') ? _('And') : _('Or');
1273				$levelDetails = [
1274					'list' => $expr,
1275					'id' => $element['id'],
1276					'expression' => [
1277						'value' => $element['expression']
1278					]
1279				];
1280
1281				$levelErrors = expressionHighLevelErrors($element['expression']);
1282				if (count($levelErrors) > 0) {
1283					$levelDetails['expression']['levelErrors'] = $levelErrors;
1284				}
1285				$treeList[] = $levelDetails;
1286
1287				list($subOutline, $subTreeList) = buildExpressionHtmlTree($element['elements'], $next, $letterNum,
1288						$level + 1, $element['operator']);
1289				$treeList = array_merge($treeList, $subTreeList);
1290
1291				$outline .= ($level == 0) ? $subOutline : '('.$subOutline.')';
1292				if ($operator !== null && $next[$level]) {
1293					$outline .= ' '.$operator.' ';
1294				}
1295				break;
1296			case 'expression':
1297				$next[$level] = ($key != $lastKey);
1298
1299				$letter = num2letter($letterNum++);
1300				$outline .= $letter;
1301				if ($operator !== null && $next[$level]) {
1302					$outline .= ' '.$operator.' ';
1303				}
1304
1305				if (defined('NO_LINK_IN_TESTING')) {
1306					$url = $element['expression'];
1307				}
1308				else {
1309					$expressionId = 'expr_'.$element['id'];
1310
1311					$url = (new CSpan($element['expression']))
1312						->addClass(ZBX_STYLE_LINK_ACTION)
1313						->setId($expressionId)
1314						->onClick('javascript: copy_expression("'.$expressionId.'");');
1315				}
1316				$expr = expressionLevelDraw($next, $level);
1317				$expr[] = SPACE;
1318				$expr[] = bold($letter);
1319				$expr[] = SPACE;
1320				$expr[] = $url;
1321
1322				$levelDetails = [
1323					'list' => $expr,
1324					'id' => $element['id'],
1325					'expression' => [
1326						'value' => $element['expression']
1327					]
1328				];
1329
1330				$levelErrors = expressionHighLevelErrors($element['expression']);
1331				if (count($levelErrors) > 0) {
1332					$levelDetails['expression']['levelErrors'] = $levelErrors;
1333				}
1334				$treeList[] = $levelDetails;
1335				break;
1336		}
1337	}
1338	return [$outline, $treeList];
1339}
1340
1341function expressionHighLevelErrors($expression) {
1342	static $errors, $definedErrorPhrases;
1343
1344	if (!isset($errors)) {
1345		$definedErrorPhrases = [
1346			EXPRESSION_HOST_UNKNOWN => _('Unknown host, no such host present in system'),
1347			EXPRESSION_HOST_ITEM_UNKNOWN => _('Unknown host item, no such item in selected host'),
1348			EXPRESSION_NOT_A_MACRO_ERROR => _('Given expression is not a macro'),
1349			EXPRESSION_FUNCTION_UNKNOWN => _('Incorrect function is used'),
1350			EXPRESSION_UNSUPPORTED_VALUE_TYPE => _('Incorrect item value type')
1351		];
1352		$errors = [];
1353	}
1354
1355	if (!isset($errors[$expression])) {
1356		$errors[$expression] = [];
1357		$expressionData = new CTriggerExpression();
1358		if ($expressionData->parse($expression)) {
1359			foreach ($expressionData->expressions as $exprPart) {
1360				$info = get_item_function_info($exprPart['expression']);
1361
1362				if (!is_array($info) && isset($definedErrorPhrases[$info])) {
1363					if (!isset($errors[$expression][$exprPart['expression']])) {
1364						$errors[$expression][$exprPart['expression']] = $definedErrorPhrases[$info];
1365					}
1366				}
1367			}
1368		}
1369	}
1370
1371	$ret = [];
1372	if (count($errors[$expression]) == 0) {
1373		return $ret;
1374	}
1375
1376	$expressionData = new CTriggerExpression();
1377	if ($expressionData->parse($expression)) {
1378		foreach ($expressionData->expressions as $exprPart) {
1379			if (isset($errors[$expression][$exprPart['expression']])) {
1380				$ret[$exprPart['expression']] = $errors[$expression][$exprPart['expression']];
1381			}
1382		}
1383	}
1384	return $ret;
1385}
1386
1387/**
1388 * Draw level for trigger expression builder tree
1389 *
1390 * @param array $next
1391 * @param int $level
1392 *
1393 * @return array
1394 */
1395function expressionLevelDraw(array $next, $level) {
1396	$expr = [];
1397	for ($i = 1; $i <= $level; $i++) {
1398		if ($i == $level) {
1399			$image = $next[$i] ? 'top_right_bottom' : 'top_right';
1400		}
1401		else {
1402			$image = $next[$i] ? 'top_bottom' : 'space';
1403		}
1404		$expr[] = new CImg('images/general/tr_'.$image.'.gif', 'tr', 12, 12);
1405	}
1406	return $expr;
1407}
1408
1409/**
1410 * Makes tree of expression elements
1411 *
1412 * Expression:
1413 *   "{host1:system.cpu.util[,iowait].last(0)} > 50 and {host2:system.cpu.util[,iowait].last(0)} > 50"
1414 * Result:
1415 *   array(
1416 *     [0] => array(
1417 *       'id' => '0_94',
1418 *       'type' => 'operator',
1419 *       'operator' => 'and',
1420 *       'elements' => array(
1421 *         [0] => array(
1422 *           'id' => '0_44',
1423 *           'type' => 'expression',
1424 *           'expression' => '{host1:system.cpu.util[,iowait].last(0)} > 50'
1425 *         ),
1426 *         [1] => array(
1427 *           'id' => '50_94',
1428 *           'type' => 'expression',
1429 *           'expression' => '{host2:system.cpu.util[,iowait].last(0)} > 50'
1430 *         )
1431 *       )
1432 *     )
1433 *   )
1434 *
1435 * @param CTriggerExpression $expressionData
1436 * @param int $start
1437 * @param int $end
1438 *
1439 * @return array
1440 */
1441function getExpressionTree(CTriggerExpression $expressionData, $start, $end) {
1442	$blankSymbols = [' ', "\r", "\n", "\t"];
1443
1444	$expressionTree = [];
1445	foreach (['or', 'and'] as $operator) {
1446		$operatorFound = false;
1447		$lParentheses = -1;
1448		$rParentheses = -1;
1449		$expressions = [];
1450		$openSymbolNum = $start;
1451		$operatorPos = 0;
1452		$operatorToken = '';
1453
1454		for ($i = $start, $level = 0; $i <= $end; $i++) {
1455			switch ($expressionData->expression[$i]) {
1456				case ' ':
1457				case "\r":
1458				case "\n":
1459				case "\t":
1460					if ($openSymbolNum == $i) {
1461						$openSymbolNum++;
1462					}
1463					break;
1464				case '(':
1465					if ($level == 0) {
1466						$lParentheses = $i;
1467					}
1468					$level++;
1469					break;
1470				case ')':
1471					$level--;
1472					if ($level == 0) {
1473						$rParentheses = $i;
1474					}
1475					break;
1476				case '{':
1477					foreach ($expressionData->expressions as $exprPart) {
1478						if ($exprPart['pos'] == $i) {
1479							$i += strlen($exprPart['expression']) - 1;
1480							break;
1481						}
1482					}
1483					break;
1484				default:
1485					// try to parse an operator
1486					if ($operator[$operatorPos] === $expressionData->expression[$i]) {
1487						$operatorPos++;
1488						$operatorToken .= $expressionData->expression[$i];
1489
1490						// operator found
1491						if ($operatorToken === $operator) {
1492							// we've reached the end of a complete expression, parse the expression on the left side of
1493							// the operator
1494							if ($level == 0) {
1495								// find the last symbol of the expression before the operator
1496								$closeSymbolNum = $i - strlen($operator);
1497
1498								// trim blank symbols after the expression
1499								while (in_array($expressionData->expression[$closeSymbolNum], $blankSymbols)) {
1500									$closeSymbolNum--;
1501								}
1502
1503								$expressions[] = getExpressionTree($expressionData, $openSymbolNum, $closeSymbolNum);
1504								$openSymbolNum = $i + 1;
1505								$operatorFound = true;
1506							}
1507							$operatorPos = 0;
1508							$operatorToken = '';
1509						}
1510					}
1511			}
1512		}
1513
1514		// trim blank symbols in the end of the trigger expression
1515		$closeSymbolNum = $end;
1516		while (in_array($expressionData->expression[$closeSymbolNum], $blankSymbols)) {
1517			$closeSymbolNum--;
1518		}
1519
1520		// we've found a whole expression and parsed the expression on the left side of the operator,
1521		// parse the expression on the right
1522		if ($operatorFound) {
1523			$expressions[] = getExpressionTree($expressionData, $openSymbolNum, $closeSymbolNum);
1524
1525			// trim blank symbols in the beginning of the trigger expression
1526			$openSymbolNum = $start;
1527			while (in_array($expressionData->expression[$openSymbolNum], $blankSymbols)) {
1528				$openSymbolNum++;
1529			}
1530
1531			// trim blank symbols in the end of the trigger expression
1532			$closeSymbolNum = $end;
1533			while (in_array($expressionData->expression[$closeSymbolNum], $blankSymbols)) {
1534				$closeSymbolNum--;
1535			}
1536
1537			$expressionTree = [
1538				'id' => $openSymbolNum.'_'.$closeSymbolNum,
1539				'expression' => substr($expressionData->expression, $openSymbolNum, $closeSymbolNum - $openSymbolNum + 1),
1540				'type' => 'operator',
1541				'operator' => $operator,
1542				'elements' => $expressions
1543			];
1544			break;
1545		}
1546		// if we've tried both operators and didn't find anything, it means there's only one expression
1547		// return the result
1548		elseif ($operator === 'and') {
1549			// trim extra parentheses
1550			if ($openSymbolNum == $lParentheses && $closeSymbolNum == $rParentheses) {
1551				$openSymbolNum++;
1552				$closeSymbolNum--;
1553
1554				$expressionTree = getExpressionTree($expressionData, $openSymbolNum, $closeSymbolNum);
1555			}
1556			// no extra parentheses remain, return the result
1557			else {
1558				$expressionTree = [
1559					'id' => $openSymbolNum.'_'.$closeSymbolNum,
1560					'expression' => substr($expressionData->expression, $openSymbolNum, $closeSymbolNum - $openSymbolNum + 1),
1561					'type' => 'expression'
1562				];
1563			}
1564		}
1565	}
1566
1567	return $expressionTree;
1568}
1569
1570/**
1571 * Recreate an expression depending on action.
1572 *
1573 * Supported action values:
1574 * - and	- add an expression using "and";
1575 * - or		- add an expression using "or";
1576 * - r 		- replace;
1577 * - R		- remove.
1578 *
1579 * @param string $expression
1580 * @param string $expressionId  element identifier like "0_55"
1581 * @param string $action        action to perform
1582 * @param string $newExpression expression for AND, OR or replace actions
1583 *
1584 * @return bool                 returns new expression or false if expression is incorrect
1585 */
1586function remakeExpression($expression, $expressionId, $action, $newExpression) {
1587	if (empty($expression)) {
1588		return false;
1589	}
1590
1591	$expressionData = new CTriggerExpression();
1592	if ($action != 'R' && !$expressionData->parse($newExpression)) {
1593		error($expressionData->error);
1594		return false;
1595	}
1596
1597	if (!$expressionData->parse($expression)) {
1598		error($expressionData->error);
1599		return false;
1600	}
1601
1602	$expressionTree[] = getExpressionTree($expressionData, 0, strlen($expressionData->expression) - 1);
1603
1604	if (rebuildExpressionTree($expressionTree, $expressionId, $action, $newExpression)) {
1605		$expression = makeExpression($expressionTree);
1606	}
1607	return $expression;
1608}
1609
1610/**
1611 * Rebuild expression depending on action.
1612 *
1613 * Supported action values:
1614 * - and	- add an expression using "and";
1615 * - or		- add an expression using "or";
1616 * - r 		- replace;
1617 * - R		- remove.
1618 *
1619 * Example:
1620 *   $expressionTree = array(
1621 *     [0] => array(
1622 *       'id' => '0_94',
1623 *       'type' => 'operator',
1624 *       'operator' => 'and',
1625 *       'elements' => array(
1626 *         [0] => array(
1627 *           'id' => '0_44',
1628 *           'type' => 'expression',
1629 *           'expression' => '{host1:system.cpu.util[,iowait].last(0)} > 50'
1630 *         ),
1631 *         [1] => array(
1632 *           'id' => '50_94',
1633 *           'type' => 'expression',
1634 *           'expression' => '{host2:system.cpu.util[,iowait].last(0)} > 50'
1635 *         )
1636 *       )
1637 *     )
1638 *   )
1639 *   $action = 'R'
1640 *   $expressionId = '50_94'
1641 *
1642 * Result:
1643 *   $expressionTree = array(
1644 *     [0] => array(
1645 *       'id' => '0_44',
1646 *       'type' => 'expression',
1647 *       'expression' => '{host1:system.cpu.util[,iowait].last(0)} > 50'
1648 *     )
1649 *   )
1650 *
1651 * @param array 	$expressionTree
1652 * @param string 	$expressionId  		element identifier like "0_55"
1653 * @param string 	$action        		action to perform
1654 * @param string 	$newExpression 		expression for AND, OR or replace actions
1655 * @param string 	$operator       	parameter only for recursive call
1656 *
1657 * @return bool                 returns true if element is found, false - otherwise
1658 */
1659function rebuildExpressionTree(array &$expressionTree, $expressionId, $action, $newExpression, $operator = null) {
1660	foreach ($expressionTree as $key => $expression) {
1661		if ($expressionId == $expressionTree[$key]['id']) {
1662			switch ($action) {
1663				case 'and':
1664				case 'or':
1665					switch ($expressionTree[$key]['type']) {
1666						case 'operator':
1667							if ($expressionTree[$key]['operator'] == $action) {
1668								$expressionTree[$key]['elements'][] = [
1669									'expression' => $newExpression,
1670									'type' => 'expression'
1671								];
1672							}
1673							else {
1674								$element = [
1675									'type' => 'operator',
1676									'operator' => $action,
1677									'elements' => [
1678										$expressionTree[$key],
1679										[
1680											'expression' => $newExpression,
1681											'type' => 'expression'
1682										]
1683									]
1684								];
1685								$expressionTree[$key] = $element;
1686							}
1687							break;
1688						case 'expression':
1689							if (!$operator || $operator != $action) {
1690								$element = [
1691									'type' => 'operator',
1692									'operator' => $action,
1693									'elements' => [
1694										$expressionTree[$key],
1695										[
1696											'expression' => $newExpression,
1697											'type' => 'expression'
1698										]
1699									]
1700								];
1701								$expressionTree[$key] = $element;
1702							}
1703							else {
1704								$expressionTree[] = [
1705									'expression' => $newExpression,
1706									'type' => 'expression'
1707								];
1708							}
1709							break;
1710					}
1711					break;
1712				// replace
1713				case 'r':
1714					$expressionTree[$key]['expression'] = $newExpression;
1715					if ($expressionTree[$key]['type'] == 'operator') {
1716						$expressionTree[$key]['type'] = 'expression';
1717						unset($expressionTree[$key]['operator'], $expressionTree[$key]['elements']);
1718					}
1719					break;
1720				// remove
1721				case 'R':
1722					unset($expressionTree[$key]);
1723					break;
1724			}
1725			return true;
1726		}
1727
1728		if ($expressionTree[$key]['type'] == 'operator') {
1729			if (rebuildExpressionTree($expressionTree[$key]['elements'], $expressionId, $action, $newExpression,
1730					$expressionTree[$key]['operator'])) {
1731				return true;
1732			}
1733		}
1734	}
1735
1736	return false;
1737}
1738
1739/**
1740 * Makes expression by expression tree
1741 *
1742 * Example:
1743 *   $expressionTree = array(
1744 *     [0] => array(
1745 *       'type' => 'operator',
1746 *       'operator' => 'and',
1747 *       'elements' => array(
1748 *         [0] => array(
1749 *           'type' => 'expression',
1750 *           'expression' => '{host1:system.cpu.util[,iowait].last(0)} > 50'
1751 *         ),
1752 *         [1] => array(
1753 *           'type' => 'expression',
1754 *           'expression' => '{host2:system.cpu.util[,iowait].last(0)} > 50'
1755 *         )
1756 *       )
1757 *     )
1758 *   )
1759 *
1760 * Result:
1761 *   "{host1:system.cpu.util[,iowait].last(0)} > 50 and {host2:system.cpu.util[,iowait].last(0)} > 50"
1762 *
1763 * @param array  $expressionTree
1764 * @param int    $level				parameter only for recursive call
1765 * @param string $operator			parameter only for recursive call
1766 *
1767 * @return string
1768 */
1769function makeExpression(array $expressionTree, $level = 0, $operator = null) {
1770	$expression = '';
1771
1772	end($expressionTree);
1773	$lastKey = key($expressionTree);
1774
1775	foreach ($expressionTree as $key => $element) {
1776		switch ($element['type']) {
1777			case 'operator':
1778				$subExpression = makeExpression($element['elements'], $level + 1, $element['operator']);
1779
1780				$expression .= ($level == 0) ? $subExpression : '('.$subExpression.')';
1781				break;
1782			case 'expression':
1783				$expression .= $element['expression'];
1784				break;
1785		}
1786		if ($operator !== null && $key != $lastKey) {
1787			$expression .= ' '.$operator.' ';
1788		}
1789	}
1790
1791	return $expression;
1792}
1793
1794function get_item_function_info($expr) {
1795	$rule_float = [_('Numeric (float)'), 'preg_match("/^'.ZBX_PREG_NUMBER.'$/", {})'];
1796	$rule_int = [_('Numeric (integer)'), 'preg_match("/^'.ZBX_PREG_INT.'$/", {})'];
1797	$rule_0or1 = [_('0 or 1'), IN('0,1')];
1798	$rules = [
1799		// Every nested array should have two elements: label, validation.
1800		'integer' => [
1801			ITEM_VALUE_TYPE_UINT64 => $rule_int
1802		],
1803		'numeric' => [
1804			ITEM_VALUE_TYPE_UINT64 => $rule_int,
1805			ITEM_VALUE_TYPE_FLOAT => $rule_float
1806		],
1807		'numeric_as_float' => [
1808			ITEM_VALUE_TYPE_UINT64 => $rule_float,
1809			ITEM_VALUE_TYPE_FLOAT => $rule_float
1810		],
1811		'numeric_as_uint' => [
1812			ITEM_VALUE_TYPE_UINT64 => $rule_int,
1813			ITEM_VALUE_TYPE_FLOAT => $rule_int
1814		],
1815		'numeric_as_0or1' => [
1816			ITEM_VALUE_TYPE_UINT64 => $rule_0or1,
1817			ITEM_VALUE_TYPE_FLOAT => $rule_0or1
1818		],
1819		'string_as_0or1' => [
1820			ITEM_VALUE_TYPE_TEXT => $rule_0or1,
1821			ITEM_VALUE_TYPE_STR => $rule_0or1,
1822			ITEM_VALUE_TYPE_LOG => $rule_0or1
1823		],
1824		'string_as_uint' => [
1825			ITEM_VALUE_TYPE_TEXT => $rule_int,
1826			ITEM_VALUE_TYPE_STR => $rule_int,
1827			ITEM_VALUE_TYPE_LOG => $rule_int
1828		],
1829		'string_as_float' => [
1830			ITEM_VALUE_TYPE_TEXT => $rule_float,
1831			ITEM_VALUE_TYPE_STR => $rule_float,
1832			ITEM_VALUE_TYPE_LOG => $rule_float
1833		],
1834		'log_as_uint' => [
1835			ITEM_VALUE_TYPE_LOG => $rule_int
1836		],
1837		'log_as_0or1' => [
1838			ITEM_VALUE_TYPE_LOG => $rule_0or1
1839		],
1840		'date' => [
1841			'any' => ['YYYYMMDD', '{}>=19700101&&{}<=99991231']
1842		],
1843		'time' => [
1844			'any' => ['HHMMSS', 'preg_match("/^([01]?\d|2[0-3])([0-5]?\d)([0-5]?\d)$/", {})']
1845		],
1846		'day_of_month' => [
1847			'any' => ['1-31', '{}>=1&&{}<=31']
1848		],
1849		'day_of_week' => [
1850			'any' => ['1-7', IN('1,2,3,4,5,6,7')]
1851		]
1852	];
1853
1854	$functions = [
1855		'abschange' => $rules['numeric'] + $rules['string_as_0or1'],
1856		'avg' => $rules['numeric_as_float'],
1857		'band' => $rules['integer'],
1858		'change' => $rules['numeric'] + $rules['string_as_0or1'],
1859		'count' => $rules['numeric_as_uint'] + $rules['string_as_uint'],
1860		'date' => $rules['date'],
1861		'dayofmonth' => $rules['day_of_month'],
1862		'dayofweek' => $rules['day_of_week'],
1863		'delta' => $rules['numeric'],
1864		'diff' => $rules['numeric_as_0or1'] + $rules['string_as_0or1'],
1865		'forecast' => $rules['numeric_as_float'],
1866		'fuzzytime' => $rules['numeric_as_0or1'],
1867		'iregexp' => $rules['string_as_0or1'],
1868		'last' => $rules['numeric'] + $rules['string_as_float'],
1869		'logeventid' => $rules['log_as_0or1'],
1870		'logseverity' => $rules['log_as_uint'],
1871		'logsource' => $rules['log_as_0or1'],
1872		'max' => $rules['numeric'],
1873		'min' => $rules['numeric'],
1874		'nodata' => $rules['numeric_as_0or1'] + $rules['string_as_0or1'],
1875		'now' => $rules['numeric_as_uint'] + $rules['string_as_uint'],
1876		'percentile' => $rules['numeric'],
1877		'prev' => $rules['numeric'] + $rules['string_as_float'],
1878		'regexp' => $rules['string_as_0or1'],
1879		'str' => $rules['string_as_0or1'],
1880		'strlen' => $rules['string_as_uint'],
1881		'sum' => $rules['numeric'],
1882		'time' => $rules['time'],
1883		'timeleft' => $rules['numeric_as_float']
1884	];
1885
1886	$expr_data = new CTriggerExpression();
1887	$expression = $expr_data->parse($expr);
1888
1889	if (!$expression) {
1890		return EXPRESSION_NOT_A_MACRO_ERROR;
1891	}
1892
1893	switch (true) {
1894		case ($expression->hasTokenOfType(CTriggerExpressionParserResult::TOKEN_TYPE_MACRO)):
1895			$result = [
1896				'type' => T_ZBX_STR,
1897				'value_type' => $rule_0or1[0],
1898				'validation' => $rule_0or1[1]
1899			];
1900			break;
1901
1902		case ($expression->hasTokenOfType(CTriggerExpressionParserResult::TOKEN_TYPE_USER_MACRO)):
1903		case ($expression->hasTokenOfType(CTriggerExpressionParserResult::TOKEN_TYPE_LLD_MACRO)):
1904			$result = [
1905				'type' => T_ZBX_STR,
1906				'value_type' => $rule_float[0],
1907				'validation' => $rule_float[1]
1908			];
1909			break;
1910
1911		case ($expression->hasTokenOfType(CTriggerExpressionParserResult::TOKEN_TYPE_FUNCTION_MACRO)):
1912			$expr_part = reset($expr_data->expressions);
1913
1914			if (!array_key_exists($expr_part['functionName'], $functions)) {
1915				$result = EXPRESSION_FUNCTION_UNKNOWN;
1916				break;
1917			}
1918
1919			$host = API::Host()->get([
1920				'output' => ['hostid'],
1921				'filter' => ['host' => [$expr_part['host']]],
1922				'templated_hosts' => true
1923			]);
1924
1925			if (!$host) {
1926				$result = EXPRESSION_HOST_UNKNOWN;
1927				break;
1928			}
1929
1930			$item = API::Item()->get([
1931				'output' => ['value_type'],
1932				'hostids' => $host[0]['hostid'],
1933				'filter' => [
1934					'key_' => [$expr_part['item']]
1935				],
1936				'webitems' => true
1937			]);
1938
1939			if (!$item) {
1940				$item = API::ItemPrototype()->get([
1941					'output' => ['value_type'],
1942					'hostids' => $host[0]['hostid'],
1943					'filter' => [
1944						'key_' => [$expr_part['item']]
1945					]
1946				]);
1947			}
1948
1949			if (!$item) {
1950				$result = EXPRESSION_HOST_ITEM_UNKNOWN;
1951				break;
1952			}
1953
1954			$function = $functions[$expr_part['functionName']];
1955			$value_type = $item[0]['value_type'];
1956
1957			if (array_key_exists('any', $function)) {
1958				$value_type = 'any';
1959			}
1960			elseif (!array_key_exists($value_type, $function)) {
1961				$result = EXPRESSION_UNSUPPORTED_VALUE_TYPE;
1962				break;
1963			}
1964
1965			$result = [
1966				'type' => T_ZBX_STR,
1967				'value_type' => $function[$value_type][0],
1968				'validation' => $function[$value_type][1]
1969			];
1970			break;
1971
1972		default:
1973			$result = EXPRESSION_NOT_A_MACRO_ERROR;
1974			break;
1975	}
1976
1977	return $result;
1978}
1979
1980/**
1981 * Substitute macros in the expression with the given values and evaluate its result.
1982 *
1983 * @param string $expression                a trigger expression
1984 * @param array  $replaceFunctionMacros     an array of macro - value pairs
1985 *
1986 * @return bool     the calculated value of the expression
1987 */
1988function evalExpressionData($expression, $replaceFunctionMacros) {
1989	// Sort by longest array key which in this case contains macros.
1990	uksort($replaceFunctionMacros, function ($key1, $key2) {
1991		$s1 = strlen($key1);
1992		$s2 = strlen($key2);
1993
1994		if ($s1 == $s2) {
1995			return 0;
1996		}
1997
1998		return ($s1 > $s2) ? -1 : 1;
1999	});
2000
2001	// replace function macros with their values
2002	$expression = str_replace(array_keys($replaceFunctionMacros), array_values($replaceFunctionMacros), $expression);
2003
2004	$parser = new CTriggerExpression();
2005	$parseResult = $parser->parse($expression);
2006
2007	// The $replaceFunctionMacros array may contain string values which after substitution
2008	// will result in an invalid expression. In such cases we should just return false.
2009	if (!$parseResult) {
2010		return false;
2011	}
2012
2013	// turn the expression into valid PHP code
2014	$evStr = '';
2015	$replaceOperators = ['not' => '!', '=' => '=='];
2016	foreach ($parseResult->getTokens() as $token) {
2017		$value = $token['value'];
2018
2019		switch ($token['type']) {
2020			case CTriggerExpressionParserResult::TOKEN_TYPE_OPERATOR:
2021				// replace specific operators with their PHP analogues
2022				if (isset($replaceOperators[$token['value']])) {
2023					$value = $replaceOperators[$token['value']];
2024				}
2025
2026				break;
2027			case CTriggerExpressionParserResult::TOKEN_TYPE_NUMBER:
2028				// convert numeric values with suffixes
2029				if ($token['data']['suffix'] !== null) {
2030					$value = convert($value);
2031				}
2032
2033				$value = '((float) "'.$value.'")';
2034
2035				break;
2036		}
2037
2038		$evStr .= ' '.$value;
2039	}
2040
2041	// execute expression
2042	eval('$result = ('.trim($evStr).');');
2043
2044	return $result;
2045}
2046
2047function convert($value) {
2048	$value = trim($value);
2049
2050	if (!preg_match('/(?P<value>[\-+]?([.][0-9]+|[0-9]+[.]?[0-9]*))(?P<mult>['.ZBX_BYTE_SUFFIXES.ZBX_TIME_SUFFIXES.']?)/',
2051			$value, $arr)) {
2052		return $value;
2053	}
2054
2055	$value = $arr['value'];
2056	switch ($arr['mult']) {
2057		case 'T':
2058			$value *= 1024 * 1024 * 1024 * 1024;
2059			break;
2060		case 'G':
2061			$value *= 1024 * 1024 * 1024;
2062			break;
2063		case 'M':
2064			$value *= 1024 * 1024;
2065			break;
2066		case 'K':
2067			$value *= 1024;
2068			break;
2069		case 'm':
2070			$value *= 60;
2071			break;
2072		case 'h':
2073			$value *= 60 * 60;
2074			break;
2075		case 'd':
2076			$value *= 60 * 60 * 24;
2077			break;
2078		case 'w':
2079			$value *= 60 * 60 * 24 * 7;
2080			break;
2081	}
2082
2083	return $value;
2084}
2085
2086/**
2087 * Quoting $param if it contains special characters.
2088 *
2089 * @param string $param
2090 * @param bool   $forced
2091 *
2092 * @return string
2093 */
2094function quoteFunctionParam($param, $forced = false) {
2095	if (!$forced) {
2096		if (!isset($param[0]) || ($param[0] != '"' && false === strpbrk($param, ',)'))) {
2097			return $param;
2098		}
2099	}
2100
2101	return '"'.str_replace('"', '\\"', $param).'"';
2102}
2103
2104/**
2105 * Returns the text indicating the trigger's status and state. If the $state parameter is not given, only the status of
2106 * the trigger will be taken into account.
2107 *
2108 * @param int $status
2109 * @param int $state
2110 *
2111 * @return string
2112 */
2113function triggerIndicator($status, $state = null) {
2114	if ($status == TRIGGER_STATUS_ENABLED) {
2115		return ($state == TRIGGER_STATE_UNKNOWN) ? _('Unknown') : _('Enabled');
2116	}
2117	elseif ($status == TRIGGER_STATUS_DISABLED) {
2118		return _('Disabled');
2119	}
2120
2121	return _('Unknown');
2122}
2123
2124/**
2125 * Returns the CSS class for the trigger's status and state indicator. If the $state parameter is not given, only the
2126 * status of the trigger will be taken into account.
2127 *
2128 * @param int $status
2129 * @param int $state
2130 *
2131 * @return string
2132 */
2133function triggerIndicatorStyle($status, $state = null) {
2134	if ($status == TRIGGER_STATUS_ENABLED) {
2135		return ($state == TRIGGER_STATE_UNKNOWN) ?
2136			ZBX_STYLE_GREY :
2137			ZBX_STYLE_GREEN;
2138	}
2139	elseif ($status == TRIGGER_STATUS_DISABLED) {
2140		return ZBX_STYLE_RED;
2141	}
2142
2143	return ZBX_STYLE_GREY;
2144}
2145
2146/**
2147 * Orders triggers by both status and state. Triggers are sorted in the following order: enabled, disabled, unknown.
2148 *
2149 * Keep in sync with orderItemsByStatus().
2150 *
2151 * @param array  $triggers
2152 * @param string $sortorder
2153 */
2154function orderTriggersByStatus(array &$triggers, $sortorder = ZBX_SORT_UP) {
2155	$sort = [];
2156
2157	foreach ($triggers as $key => $trigger) {
2158		if ($trigger['status'] == TRIGGER_STATUS_ENABLED) {
2159			$statusOrder = ($trigger['state'] == TRIGGER_STATE_UNKNOWN) ? 2 : 0;
2160		}
2161		elseif ($trigger['status'] == TRIGGER_STATUS_DISABLED) {
2162			$statusOrder = 1;
2163		}
2164
2165		$sort[$key] = $statusOrder;
2166	}
2167
2168	if ($sortorder == ZBX_SORT_UP) {
2169		asort($sort);
2170	}
2171	else {
2172		arsort($sort);
2173	}
2174
2175	$sortedTriggers = [];
2176	foreach ($sort as $key => $val) {
2177		$sortedTriggers[$key] = $triggers[$key];
2178	}
2179	$triggers = $sortedTriggers;
2180}
2181