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 * Class containing common methods for operations with triggers.
24 */
25abstract class CTriggerGeneral extends CApiService {
26
27	/**
28	 * @abstract
29	 *
30	 * @param array $options
31	 *
32	 * @return array
33	 */
34	abstract public function get(array $options = []);
35
36	/**
37	 * Prepares and returns an array of child triggers, inherited from triggers $tpl_triggers on the given hosts.
38	 *
39	 * @param array  $tpl_triggers
40	 * @param string $tpl_triggers[<tnum>]['triggerid']
41	 */
42	private function prepareInheritedTriggers(array $tpl_triggers, array $hostids = null, array &$ins_triggers = null,
43			array &$upd_triggers = null, array &$db_triggers = null) {
44		$ins_triggers = [];
45		$upd_triggers = [];
46		$db_triggers = [];
47
48		$result = DBselect(
49			'SELECT DISTINCT t.triggerid,h.hostid'.
50			' FROM triggers t,functions f,items i,hosts h'.
51			' WHERE t.triggerid=f.triggerid'.
52				' AND f.itemid=i.itemid'.
53				' AND i.hostid=h.hostid'.
54				' AND '.dbConditionInt('t.triggerid', zbx_objectValues($tpl_triggers, 'triggerid')).
55				' AND '.dbConditionInt('h.status', [HOST_STATUS_TEMPLATE])
56		);
57
58		$tpl_hostids_by_triggerid = [];
59		$tpl_hostids = [];
60
61		while ($row = DBfetch($result)) {
62			$tpl_hostids_by_triggerid[$row['triggerid']][] = $row['hostid'];
63			$tpl_hostids[$row['hostid']] = true;
64		}
65
66		// Unset host-level triggers.
67		foreach ($tpl_triggers as $tnum => $tpl_trigger) {
68			if (!array_key_exists($tpl_trigger['triggerid'], $tpl_hostids_by_triggerid)) {
69				unset($tpl_triggers[$tnum]);
70			}
71		}
72
73		if (!$tpl_triggers) {
74			// Nothing to inherit, just exit.
75			return;
76		}
77
78		$hosts_by_tpl_hostid = self::getLinkedHosts(array_keys($tpl_hostids), $hostids);
79		$chd_triggers_tpl = $this->getHostTriggersByTemplateId(array_keys($tpl_hostids_by_triggerid), $hostids);
80		$tpl_triggers_by_description = [];
81
82		// Preparing list of missing triggers on linked hosts.
83		foreach ($tpl_triggers as $tpl_trigger) {
84			$hostids = [];
85
86			foreach ($tpl_hostids_by_triggerid[$tpl_trigger['triggerid']] as $tpl_hostid) {
87				if (array_key_exists($tpl_hostid, $hosts_by_tpl_hostid)) {
88					foreach ($hosts_by_tpl_hostid[$tpl_hostid] as $host) {
89						if (array_key_exists($host['hostid'], $chd_triggers_tpl)
90								&& array_key_exists($tpl_trigger['triggerid'], $chd_triggers_tpl[$host['hostid']])) {
91							continue;
92						}
93
94						$hostids[$host['hostid']] = true;
95					}
96				}
97			}
98
99			if ($hostids) {
100				$tpl_triggers_by_description[$tpl_trigger['description']][] = [
101					'triggerid' => $tpl_trigger['triggerid'],
102					'expression' => $tpl_trigger['expression'],
103					'recovery_mode' => $tpl_trigger['recovery_mode'],
104					'recovery_expression' => $tpl_trigger['recovery_expression'],
105					'hostids' => $hostids
106				];
107			}
108		}
109
110		$chd_triggers_all = array_replace_recursive($chd_triggers_tpl,
111			$this->getHostTriggersByDescription($tpl_triggers_by_description)
112		);
113
114		$expression_parser = new CExpressionParser([
115			'usermacros' => true,
116			'lldmacros' => $this instanceof CTriggerPrototype
117		]);
118
119		$recovery_expression_parser = new CExpressionParser([
120			'usermacros' => true,
121			'lldmacros' => $this instanceof CTriggerPrototype
122		]);
123
124		// List of triggers to check for duplicates. Grouped by description.
125		$descriptions = [];
126		$triggerids = [];
127
128		$output = ['url', 'status', 'priority', 'comments', 'type', 'correlation_mode', 'correlation_tag',
129			'manual_close', 'opdata', 'event_name'
130		];
131		if ($this instanceof CTriggerPrototype) {
132			$output[] = 'discover';
133		}
134
135		$db_tpl_triggers = DB::select('triggers', [
136			'output' => $output,
137			'triggerids' => array_keys($tpl_hostids_by_triggerid),
138			'preservekeys' => true
139		]);
140
141		foreach ($tpl_triggers as $tpl_trigger) {
142			$db_tpl_trigger = $db_tpl_triggers[$tpl_trigger['triggerid']];
143
144			$tpl_hostid = $tpl_hostids_by_triggerid[$tpl_trigger['triggerid']][0];
145
146			// expression: func(/template/item) => func(/host/item)
147			if ($expression_parser->parse($tpl_trigger['expression']) != CParser::PARSE_SUCCESS) {
148				self::exception(ZBX_API_ERROR_PARAMETERS, _s('Incorrect value for field "%1$s": %2$s.',
149					'expression', $expression_parser->getError()
150				));
151			}
152
153			// recovery_expression: func(/template/item) => func(/host/item)
154			if ($tpl_trigger['recovery_mode'] == ZBX_RECOVERY_MODE_RECOVERY_EXPRESSION) {
155				if ($recovery_expression_parser->parse($tpl_trigger['recovery_expression']) != CParser::PARSE_SUCCESS) {
156					self::exception(ZBX_API_ERROR_PARAMETERS, _s('Incorrect value for field "%1$s": %2$s.',
157						'recovery_expression', $recovery_expression_parser->getError()
158					));
159				}
160			}
161
162			$new_trigger = $tpl_trigger;
163			$new_trigger['uuid'] = '';
164			unset($new_trigger['triggerid'], $new_trigger['templateid']);
165
166			if (array_key_exists($tpl_hostid, $hosts_by_tpl_hostid)) {
167				foreach ($hosts_by_tpl_hostid[$tpl_hostid] as $host) {
168					$new_trigger['expression'] = $tpl_trigger['expression'];
169					$hist_functions = $expression_parser->getResult()->getTokensOfTypes(
170						[CExpressionParserResult::TOKEN_TYPE_HIST_FUNCTION]
171					);
172					$hist_function = end($hist_functions);
173					do {
174						$query_parameter = $hist_function['data']['parameters'][0];
175						$new_trigger['expression'] = substr_replace($new_trigger['expression'],
176							'/'.$host['host'].'/'.$query_parameter['data']['item'], $query_parameter['pos'],
177							$query_parameter['length']
178						);
179					}
180					while ($hist_function = prev($hist_functions));
181
182					if ($tpl_trigger['recovery_mode'] == ZBX_RECOVERY_MODE_RECOVERY_EXPRESSION) {
183						$new_trigger['recovery_expression'] = $tpl_trigger['recovery_expression'];
184						$hist_functions = $recovery_expression_parser->getResult()->getTokensOfTypes(
185							[CExpressionParserResult::TOKEN_TYPE_HIST_FUNCTION]
186						);
187						$hist_function = end($hist_functions);
188						do {
189							$query_parameter = $hist_function['data']['parameters'][0];
190							$new_trigger['recovery_expression'] = substr_replace($new_trigger['recovery_expression'],
191								'/'.$host['host'].'/'.$query_parameter['data']['item'], $query_parameter['pos'],
192								$query_parameter['length']
193							);
194						}
195						while ($hist_function = prev($hist_functions));
196					}
197
198					if (array_key_exists($host['hostid'], $chd_triggers_all)
199							&& array_key_exists($tpl_trigger['triggerid'], $chd_triggers_all[$host['hostid']])) {
200						$chd_trigger = $chd_triggers_all[$host['hostid']][$tpl_trigger['triggerid']];
201
202						$upd_triggers[] = $new_trigger + [
203							'triggerid' => $chd_trigger['triggerid'],
204							'templateid' => $tpl_trigger['triggerid']
205						];
206						$db_triggers[] = $chd_trigger;
207						$triggerids[] = $chd_trigger['triggerid'];
208
209						$check_duplicates = ($chd_trigger['description'] !== $new_trigger['description']
210							|| $chd_trigger['expression'] !== $new_trigger['expression']
211							|| $chd_trigger['recovery_expression'] !== $new_trigger['recovery_expression']);
212					}
213					else {
214						$ins_triggers[] = $new_trigger + $db_tpl_trigger + ['templateid' => $tpl_trigger['triggerid']];
215						$check_duplicates = true;
216					}
217
218					if ($check_duplicates) {
219						$descriptions[$new_trigger['description']][] = [
220							'expression' => $new_trigger['expression'],
221							'recovery_expression' => $new_trigger['recovery_expression'],
222							'host' => [
223								'hostid' => $host['hostid'],
224								'status' => $host['status']
225							]
226						];
227					}
228				}
229			}
230		}
231
232		if ($triggerids) {
233			// Add trigger tags.
234			$result = DBselect(
235				'SELECT tt.triggertagid,tt.triggerid,tt.tag,tt.value'.
236				' FROM trigger_tag tt'.
237				' WHERE '.dbConditionInt('tt.triggerid', $triggerids)
238			);
239
240			$trigger_tags = [];
241
242			while ($row = DBfetch($result)) {
243				$trigger_tags[$row['triggerid']][] = [
244					'triggertagid' => $row['triggertagid'],
245					'tag' => $row['tag'],
246					'value' => $row['value']
247				];
248			}
249
250			foreach ($db_triggers as $tnum => $db_trigger) {
251				$db_triggers[$tnum]['tags'] = array_key_exists($db_trigger['triggerid'], $trigger_tags)
252					? $trigger_tags[$db_trigger['triggerid']]
253					: [];
254			}
255
256			// Add discovery rule IDs.
257			if ($this instanceof CTriggerPrototype) {
258				$result = DBselect(
259					'SELECT id.parent_itemid,f.triggerid'.
260						' FROM item_discovery id,functions f'.
261						' WHERE '.dbConditionInt('f.triggerid', $triggerids).
262						' AND f.itemid=id.itemid'
263				);
264
265				$drule_by_triggerid = [];
266
267				while ($row = DBfetch($result)) {
268					$drule_by_triggerid[$row['triggerid']] = $row['parent_itemid'];
269				}
270
271				foreach ($db_triggers as $tnum => $db_trigger) {
272					$db_triggers[$tnum]['discoveryRule']['itemid'] = $drule_by_triggerid[$db_trigger['triggerid']];
273				}
274			}
275		}
276
277		$this->checkDuplicates($descriptions);
278	}
279
280	/**
281	 * Returns list of linked hosts.
282	 *
283	 * Output format:
284	 *   [
285	 *     <tpl_hostid> => [
286	 *       [
287	 *         'hostid' => <hostid>,
288	 *         'host' => <host>
289	 *       ],
290	 *       ...
291	 *     ],
292	 *     ...
293	 *   ]
294	 *
295	 * @param array  $tpl_hostids
296	 * @param array  $hostids      The function will return a list of all linked hosts if no hostids are specified.
297	 *
298	 * @return array
299	 */
300	private static function getLinkedHosts(array $tpl_hostids, array $hostids = null) {
301		// Fetch all child hosts and templates
302		$sql = 'SELECT ht.hostid,ht.templateid,h.host,h.status'.
303			' FROM hosts_templates ht,hosts h'.
304			' WHERE ht.hostid=h.hostid'.
305				' AND '.dbConditionInt('ht.templateid', $tpl_hostids).
306				' AND '.dbConditionInt('h.flags', [ZBX_FLAG_DISCOVERY_NORMAL, ZBX_FLAG_DISCOVERY_CREATED]);
307		if ($hostids !== null) {
308			$sql .= ' AND '.dbConditionInt('ht.hostid', $hostids);
309		}
310		$result = DBselect($sql);
311
312		$hosts_by_tpl_hostid = [];
313
314		while ($row = DBfetch($result)) {
315			$hosts_by_tpl_hostid[$row['templateid']][] = [
316				'hostid' => $row['hostid'],
317				'host' => $row['host'],
318				'status' => $row['status']
319			];
320		}
321
322		return $hosts_by_tpl_hostid;
323	}
324
325	/**
326	 * Returns list of already linked triggers.
327	 *
328	 * Output format:
329	 *   [
330	 *     <hostid> => [
331	 *       <tpl_triggerid> => ['triggerid' => <triggerid>],
332	 *       ...
333	 *     ],
334	 *     ...
335	 *   ]
336	 *
337	 * @param array  $tpl_triggerids
338	 * @param array  $hostids         The function will return a list of all linked triggers if no hosts are specified.
339	 *
340	 * @return array
341	 */
342	private function getHostTriggersByTemplateId(array $tpl_triggerids, array $hostids = null) {
343		$output = 't.triggerid,t.expression,t.description,t.url,t.status,t.priority,t.comments,t.type,t.recovery_mode,'.
344			't.recovery_expression,t.correlation_mode,t.correlation_tag,t.manual_close,t.opdata,t.templateid,'.
345			't.event_name,i.hostid';
346		if ($this instanceof CTriggerPrototype) {
347			$output .= ',t.discover';
348		}
349
350		// Preparing list of triggers by templateid.
351		$sql = 'SELECT DISTINCT '.$output.
352			' FROM triggers t,functions f,items i'.
353			' WHERE t.triggerid=f.triggerid'.
354				' AND f.itemid=i.itemid'.
355				' AND '.dbConditionInt('t.templateid', $tpl_triggerids);
356		if ($hostids !== null) {
357			$sql .= ' AND '.dbConditionInt('i.hostid', $hostids);
358		}
359
360		$chd_triggers = DBfetchArray(DBselect($sql));
361		$chd_triggers = CMacrosResolverHelper::resolveTriggerExpressions($chd_triggers,
362			['sources' => ['expression', 'recovery_expression']]
363		);
364
365		$chd_triggers_tpl = [];
366
367		foreach ($chd_triggers as $chd_trigger) {
368			$hostid = $chd_trigger['hostid'];
369			unset($chd_trigger['hostid']);
370
371			$chd_triggers_tpl[$hostid][$chd_trigger['templateid']] = $chd_trigger;
372		}
373
374		return $chd_triggers_tpl;
375	}
376
377	/**
378	 * Returns list of not inherited triggers with same name and expression.
379	 *
380	 * Output format:
381	 *   [
382	 *     <hostid> => [
383	 *       <tpl_triggerid> => ['triggerid' => <triggerid>],
384	 *       ...
385	 *     ],
386	 *     ...
387	 *   ]
388	 *
389	 * @param array $tpl_triggers_by_description  The list of hostids, grouped by trigger description and expression.
390	 *
391	 * @return array
392	 */
393	private function getHostTriggersByDescription(array $tpl_triggers_by_description) {
394		$chd_triggers_description = [];
395
396		$expression_parser = new CExpressionParser([
397			'usermacros' => true,
398			'lldmacros' => $this instanceof CTriggerPrototype
399		]);
400
401		$recovery_expression_parser = new CExpressionParser([
402			'usermacros' => true,
403			'lldmacros' => $this instanceof CTriggerPrototype
404		]);
405
406		$output = 't.triggerid,t.expression,t.description,t.url,t.status,t.priority,t.comments,t.type,t.recovery_mode,'.
407			't.recovery_expression,t.correlation_mode,t.correlation_tag,t.manual_close,t.opdata,t.event_name,i.hostid,'.
408			'h.host';
409		if ($this instanceof CTriggerPrototype) {
410			$output .= ',t.discover';
411		}
412
413		foreach ($tpl_triggers_by_description as $description => $tpl_triggers) {
414			$hostids = [];
415
416			foreach ($tpl_triggers as $tpl_trigger) {
417				$hostids += $tpl_trigger['hostids'];
418			}
419
420			$chd_triggers = DBfetchArray(DBselect(
421				'SELECT DISTINCT '.$output.
422				' FROM triggers t,functions f,items i,hosts h'.
423				' WHERE t.triggerid=f.triggerid'.
424					' AND f.itemid=i.itemid'.
425					' AND i.hostid=h.hostid'.
426					' AND '.dbConditionString('t.description', [$description]).
427					' AND '.dbConditionInt('i.hostid', array_keys($hostids))
428			));
429
430			$chd_triggers = CMacrosResolverHelper::resolveTriggerExpressions($chd_triggers,
431				['sources' => ['expression', 'recovery_expression']]
432			);
433
434			foreach ($tpl_triggers as $tpl_trigger) {
435				// expression: func(/template/item) => func(/host/item)
436				if ($expression_parser->parse($tpl_trigger['expression']) != CParser::PARSE_SUCCESS) {
437					self::exception(ZBX_API_ERROR_PARAMETERS, _s('Incorrect value for field "%1$s": %2$s.',
438						'expression', $expression_parser->getError()
439					));
440				}
441
442				// recovery_expression: func(/template/item) => func(/host/item)
443				if ($tpl_trigger['recovery_mode'] == ZBX_RECOVERY_MODE_RECOVERY_EXPRESSION) {
444					if ($recovery_expression_parser->parse($tpl_trigger['recovery_expression']) !=
445							CParser::PARSE_SUCCESS) {
446						self::exception(ZBX_API_ERROR_PARAMETERS, _s('Incorrect value for field "%1$s": %2$s.',
447							'recovery_expression', $recovery_expression_parser->getError()
448						));
449					}
450				}
451
452				foreach ($chd_triggers as $chd_trigger) {
453					if (!array_key_exists($chd_trigger['hostid'], $tpl_trigger['hostids'])) {
454						continue;
455					}
456
457					if ($chd_trigger['recovery_mode'] != $tpl_trigger['recovery_mode']) {
458						continue;
459					}
460
461					// Replace template name in /host/key reference to target host name.
462					$expression = $tpl_trigger['expression'];
463					$hist_functions = $expression_parser->getResult()->getTokensOfTypes(
464						[CExpressionParserResult::TOKEN_TYPE_HIST_FUNCTION]
465					);
466					$hist_function = end($hist_functions);
467					do {
468						$query_parameter = $hist_function['data']['parameters'][0];
469						$expression = substr_replace($expression,
470							'/'.$chd_trigger['host'].'/'.$query_parameter['data']['item'], $query_parameter['pos'],
471							$query_parameter['length']
472						);
473					}
474					while ($hist_function = prev($hist_functions));
475
476					if ($chd_trigger['expression'] !== $expression) {
477						continue;
478					}
479
480					if ($tpl_trigger['recovery_mode'] == ZBX_RECOVERY_MODE_RECOVERY_EXPRESSION) {
481						$recovery_expression = $tpl_trigger['recovery_expression'];
482						$hist_functions = $recovery_expression_parser->getResult()->getTokensOfTypes(
483							[CExpressionParserResult::TOKEN_TYPE_HIST_FUNCTION]
484						);
485						$hist_function = end($hist_functions);
486						do {
487							$query_parameter = $hist_function['data']['parameters'][0];
488							$recovery_expression = substr_replace($recovery_expression,
489								'/'.$chd_trigger['host'].'/'.$query_parameter['data']['item'], $query_parameter['pos'],
490								$query_parameter['length']
491							);
492						}
493						while ($hist_function = prev($hist_functions));
494
495						if ($chd_trigger['recovery_expression'] !== $recovery_expression) {
496							continue;
497						}
498					}
499
500					$hostid = $chd_trigger['hostid'];
501					unset($chd_trigger['hostid'], $chd_trigger['host']);
502					$chd_triggers_description[$hostid][$tpl_trigger['triggerid']] = $chd_trigger + ['templateid' => 0];
503				}
504			}
505		}
506
507		return $chd_triggers_description;
508	}
509
510	/**
511	 * Updates the children of the triggers on the given hosts and propagates the inheritance to all child hosts.
512	 * If the given triggers was assigned to a different template or a host, all of the child triggers, that became
513	 * obsolete will be deleted.
514	 *
515	 * @param array  $triggers
516	 * @param string $triggers[]['triggerid']
517	 * @param string $triggers[]['description']
518	 * @param string $triggers[]['expression']
519	 * @param int    $triggers[]['recovery mode']
520	 * @param string $triggers[]['recovery_expression']
521	 * @param array  $hostids
522	 */
523	protected function inherit(array $triggers, array $hostids = null) {
524		$this->prepareInheritedTriggers($triggers, $hostids, $ins_triggers, $upd_triggers, $db_triggers);
525
526		if ($ins_triggers) {
527			$this->createReal($ins_triggers, true);
528		}
529
530		if ($upd_triggers) {
531			$this->updateReal($upd_triggers, $db_triggers, true);
532		}
533
534		if ($ins_triggers || $upd_triggers) {
535			$this->inherit(array_merge($ins_triggers + $upd_triggers));
536		}
537	}
538
539	/**
540	 * Populate an array by "hostid" keys.
541	 *
542	 * @param array  $descriptions
543	 * @param string $descriptions[<description>][]['expression']
544	 *
545	 * @throws APIException  If host or template does not exists.
546	 *
547	 * @return array
548	 */
549	protected function populateHostIds($descriptions) {
550		$expression_parser = new CExpressionParser([
551			'usermacros' => true,
552			'lldmacros' => $this instanceof CTriggerPrototype
553		]);
554
555		$hosts = [];
556
557		foreach ($descriptions as $description => $triggers) {
558			foreach ($triggers as $index => $trigger) {
559				$expression_parser->parse($trigger['expression']);
560				$hosts[$expression_parser->getResult()->getHosts()[0]][$description][] = $index;
561			}
562		}
563
564		$db_hosts = DBselect(
565			'SELECT h.hostid,h.host,h.status'.
566			' FROM hosts h'.
567			' WHERE '.dbConditionString('h.host', array_keys($hosts)).
568				' AND '.dbConditionInt('h.status',
569					[HOST_STATUS_MONITORED, HOST_STATUS_NOT_MONITORED, HOST_STATUS_TEMPLATE]
570				)
571		);
572
573		while ($db_host = DBfetch($db_hosts)) {
574			foreach ($hosts[$db_host['host']] as $description => $indexes) {
575				foreach ($indexes as $index) {
576					$descriptions[$description][$index]['host'] = [
577						'hostid' => $db_host['hostid'],
578						'status' => $db_host['status']
579					];
580				}
581			}
582			unset($hosts[$db_host['host']]);
583		}
584
585		if ($hosts) {
586			$error_wrong_host = ($this instanceof CTrigger)
587				? _('Incorrect trigger expression. Host "%1$s" does not exist or you have no access to this host.')
588				: _('Incorrect trigger prototype expression. Host "%1$s" does not exist or you have no access to this host.');
589			self::exception(ZBX_API_ERROR_PARAMETERS, _params($error_wrong_host, [key($hosts)]));
590		}
591
592		return $descriptions;
593	}
594
595	/**
596	 * Checks triggers for duplicates.
597	 *
598	 * @param array  $descriptions
599	 * @param string $descriptions[<description>][]['expression']
600	 * @param string $descriptions[<description>][]['recovery_expression']
601	 * @param string $descriptions[<description>][]['hostid']
602	 *
603	 * @throws APIException if at least one trigger exists
604	 */
605	protected function checkDuplicates(array $descriptions) {
606		foreach ($descriptions as $description => $triggers) {
607			$hostids = [];
608			$expressions = [];
609
610			foreach ($triggers as $trigger) {
611				$hostids[$trigger['host']['hostid']] = true;
612				$expressions[$trigger['expression']][$trigger['recovery_expression']] = $trigger['host']['hostid'];
613			}
614
615			$db_triggers = DBfetchArray(DBselect(
616				'SELECT DISTINCT t.expression,t.recovery_expression'.
617				' FROM triggers t,functions f,items i,hosts h'.
618				' WHERE t.triggerid=f.triggerid'.
619					' AND f.itemid=i.itemid'.
620					' AND i.hostid=h.hostid'.
621					' AND '.dbConditionString('t.description', [$description]).
622					' AND '.dbConditionInt('i.hostid', array_keys($hostids))
623			));
624
625			$db_triggers = CMacrosResolverHelper::resolveTriggerExpressions($db_triggers,
626				['sources' => ['expression', 'recovery_expression']]
627			);
628
629			foreach ($db_triggers as $db_trigger) {
630				$expression = $db_trigger['expression'];
631				$recovery_expression = $db_trigger['recovery_expression'];
632
633				if (array_key_exists($expression, $expressions)
634						&& array_key_exists($recovery_expression, $expressions[$expression])) {
635					$error_already_exists = ($this instanceof CTrigger)
636						? _('Trigger "%1$s" already exists on "%2$s".')
637						: _('Trigger prototype "%1$s" already exists on "%2$s".');
638
639					$db_hosts = DB::select('hosts', [
640						'output' => ['name'],
641						'hostids' => $expressions[$expression][$recovery_expression]
642					]);
643
644					self::exception(ZBX_API_ERROR_PARAMETERS,
645						_params($error_already_exists, [$description, $db_hosts[0]['name']])
646					);
647				}
648			}
649		}
650	}
651
652	/**
653	 * Check that only triggers on templates have UUID. Add UUID to all triggers on templates, if it does not exists.
654	 *
655	 * @param array $triggers_to_create
656	 * @param array $descriptions
657	 *
658	 * @throws APIException
659	 */
660	protected function checkAndAddUuid(array &$triggers_to_create, array $descriptions): void {
661		foreach ($descriptions as $triggers) {
662			foreach ($triggers as $trigger) {
663				if ($trigger['host']['status'] != HOST_STATUS_TEMPLATE && $trigger['uuid'] !== null) {
664					self::exception(ZBX_API_ERROR_PARAMETERS,
665						_s('Invalid parameter "%1$s": %2$s.', '/'.($trigger['index'] + 1),
666							_s('unexpected parameter "%1$s"', 'uuid')
667						)
668					);
669				}
670
671				if ($trigger['host']['status'] == HOST_STATUS_TEMPLATE && $trigger['uuid'] === null) {
672					$triggers_to_create[$trigger['index']]['uuid'] = generateUuidV4();
673				}
674			}
675		}
676
677		$db_uuid = DB::select('triggers', [
678			'output' => ['uuid'],
679			'filter' => ['uuid' => array_column($triggers_to_create, 'uuid')],
680			'limit' => 1
681		]);
682
683		if ($db_uuid) {
684			self::exception(ZBX_API_ERROR_PARAMETERS,
685				_s('Entry with UUID "%1$s" already exists.', $db_uuid[0]['uuid'])
686			);
687		}
688	}
689
690	protected function addRelatedObjects(array $options, array $result) {
691		$result = parent::addRelatedObjects($options, $result);
692
693		$triggerids = array_keys($result);
694
695		// adding groups
696		if ($options['selectGroups'] !== null && $options['selectGroups'] != API_OUTPUT_COUNT) {
697			$res = DBselect(
698				'SELECT f.triggerid,hg.groupid'.
699					' FROM functions f,items i,hosts_groups hg'.
700					' WHERE '.dbConditionInt('f.triggerid', $triggerids).
701					' AND f.itemid=i.itemid'.
702					' AND i.hostid=hg.hostid'
703			);
704			$relationMap = new CRelationMap();
705			while ($relation = DBfetch($res)) {
706				$relationMap->addRelation($relation['triggerid'], $relation['groupid']);
707			}
708
709			$groups = API::HostGroup()->get([
710				'output' => $options['selectGroups'],
711				'groupids' => $relationMap->getRelatedIds(),
712				'preservekeys' => true
713			]);
714			$result = $relationMap->mapMany($result, $groups, 'groups');
715		}
716
717		// adding hosts
718		if ($options['selectHosts'] !== null && $options['selectHosts'] != API_OUTPUT_COUNT) {
719			$res = DBselect(
720				'SELECT f.triggerid,i.hostid'.
721					' FROM functions f,items i'.
722					' WHERE '.dbConditionInt('f.triggerid', $triggerids).
723					' AND f.itemid=i.itemid'
724			);
725			$relationMap = new CRelationMap();
726			while ($relation = DBfetch($res)) {
727				$relationMap->addRelation($relation['triggerid'], $relation['hostid']);
728			}
729
730			$hosts = API::Host()->get([
731				'output' => $options['selectHosts'],
732				'hostids' => $relationMap->getRelatedIds(),
733				'templated_hosts' => true,
734				'nopermissions' => true,
735				'preservekeys' => true
736			]);
737			if (!is_null($options['limitSelects'])) {
738				order_result($hosts, 'host');
739			}
740			$result = $relationMap->mapMany($result, $hosts, 'hosts', $options['limitSelects']);
741		}
742
743		// adding functions
744		if ($options['selectFunctions'] !== null && $options['selectFunctions'] != API_OUTPUT_COUNT) {
745			$functions = API::getApiService()->select('functions', [
746				'output' => $this->outputExtend($options['selectFunctions'], ['triggerid', 'functionid']),
747				'filter' => ['triggerid' => $triggerids],
748				'preservekeys' => true
749			]);
750
751			// Rename column 'name' to 'function'.
752			$function = reset($functions);
753			if ($function && array_key_exists('name', $function)) {
754				$functions = CArrayHelper::renameObjectsKeys($functions, ['name' => 'function']);
755			}
756
757			$relationMap = $this->createRelationMap($functions, 'triggerid', 'functionid');
758
759			$functions = $this->unsetExtraFields($functions, ['triggerid', 'functionid'], $options['selectFunctions']);
760			$result = $relationMap->mapMany($result, $functions, 'functions');
761		}
762
763		// Adding trigger tags.
764		if ($options['selectTags'] !== null && $options['selectTags'] != API_OUTPUT_COUNT) {
765			$tags = API::getApiService()->select('trigger_tag', [
766				'output' => $this->outputExtend($options['selectTags'], ['triggerid']),
767				'filter' => ['triggerid' => $triggerids],
768				'preservekeys' => true
769			]);
770
771			$relationMap = $this->createRelationMap($tags, 'triggerid', 'triggertagid');
772			$tags = $this->unsetExtraFields($tags, ['triggertagid', 'triggerid'], []);
773			$result = $relationMap->mapMany($result, $tags, 'tags');
774		}
775
776		return $result;
777	}
778
779	/**
780	 * Validate integrity of trigger recovery properties.
781	 *
782	 * @static
783	 *
784	 * @param array  $trigger
785	 * @param int    $trigger['recovery_mode']
786	 * @param string $trigger['recovery_expression']
787	 *
788	 * @throws APIException if validation failed.
789	 */
790	private static function checkTriggerRecoveryMode(array $trigger) {
791		if ($trigger['recovery_mode'] == ZBX_RECOVERY_MODE_RECOVERY_EXPRESSION) {
792			if ($trigger['recovery_expression'] === '') {
793				self::exception(ZBX_API_ERROR_PARAMETERS,
794					_s('Incorrect value for field "%1$s": %2$s.', 'recovery_expression', _('cannot be empty'))
795				);
796			}
797		}
798		elseif ($trigger['recovery_expression'] !== '') {
799			self::exception(ZBX_API_ERROR_PARAMETERS,
800				_s('Incorrect value for field "%1$s": %2$s.', 'recovery_expression', _('should be empty'))
801			);
802		}
803	}
804
805	/**
806	 * Validate trigger correlation mode and related properties.
807	 *
808	 * @static
809	 *
810	 * @param array  $trigger
811	 * @param int    $trigger['correlation_mode']
812	 * @param string $trigger['correlation_tag']
813	 * @param int    $trigger['recovery_mode']
814	 *
815	 * @throws APIException if validation failed.
816	 */
817	private static function checkTriggerCorrelationMode(array $trigger) {
818		if ($trigger['correlation_mode'] == ZBX_TRIGGER_CORRELATION_TAG) {
819			if ($trigger['recovery_mode'] == ZBX_RECOVERY_MODE_NONE) {
820				self::exception(ZBX_API_ERROR_PARAMETERS, _s('Incorrect value for field "%1$s": %2$s.',
821					'correlation_mode', _s('unexpected value "%1$s"', $trigger['correlation_mode'])
822				));
823			}
824
825			if ($trigger['correlation_tag'] === '') {
826				self::exception(ZBX_API_ERROR_PARAMETERS,
827					_s('Incorrect value for field "%1$s": %2$s.', 'correlation_tag', _('cannot be empty'))
828				);
829			}
830		}
831		elseif ($trigger['correlation_tag'] !== '') {
832			self::exception(ZBX_API_ERROR_PARAMETERS,
833				_s('Incorrect value for field "%1$s": %2$s.', 'correlation_tag', _('should be empty'))
834			);
835		}
836	}
837
838	/**
839	 * Validate trigger to be created.
840	 *
841	 * @param array  $triggers                                   [IN/OUT]
842	 * @param array  $triggers[]['description']                  [IN]
843	 * @param string $triggers[]['expression']                   [IN]
844	 * @param string $triggers[]['opdata']                       [IN]
845	 * @param string $triggers[]['event_name']                   [IN]
846	 * @param string $triggers[]['comments']                     [IN] (optional)
847	 * @param int    $triggers[]['priority']                     [IN] (optional)
848	 * @param int    $triggers[]['status']                       [IN] (optional)
849	 * @param int    $triggers[]['type']                         [IN] (optional)
850	 * @param string $triggers[]['url']                          [IN] (optional)
851	 * @param int    $triggers[]['recovery_mode']                [IN/OUT] (optional)
852	 * @param string $triggers[]['recovery_expression']          [IN/OUT] (optional)
853	 * @param int    $triggers[]['correlation_mode']             [IN/OUT] (optional)
854	 * @param string $triggers[]['correlation_tag']              [IN/OUT] (optional)
855	 * @param int    $triggers[]['manual_close']                 [IN] (optional)
856	 * @param int    $triggers[]['discover']                     [IN] (optional) for trigger prototypes only
857	 * @param array  $triggers[]['tags']                         [IN] (optional)
858	 * @param string $triggers[]['tags'][]['tag']                [IN]
859	 * @param string $triggers[]['tags'][]['value']              [IN/OUT] (optional)
860	 * @param array  $triggers[]['dependencies']                 [IN] (optional)
861	 * @param string $triggers[]['dependencies'][]['triggerid']  [IN]
862	 *
863	 * @throws APIException if validation failed.
864	 */
865	protected function validateCreate(array &$triggers) {
866		$api_input_rules = ['type' => API_OBJECTS, 'flags' => API_NOT_EMPTY | API_NORMALIZE, 'uniq' => [['uuid'], ['description', 'expression']], 'fields' => [
867			'uuid' =>					['type' => API_UUID],
868			'description' =>			['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('triggers', 'description')],
869			'expression' =>				['type' => API_TRIGGER_EXPRESSION, 'flags' => API_REQUIRED | API_NOT_EMPTY | API_ALLOW_LLD_MACRO],
870			'event_name' =>				['type' => API_EVENT_NAME, 'length' => DB::getFieldLength('triggers', 'event_name')],
871			'opdata' =>					['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('triggers', 'opdata')],
872			'comments' =>				['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('triggers', 'comments')],
873			'priority' =>				['type' => API_INT32, 'in' => implode(',', range(TRIGGER_SEVERITY_NOT_CLASSIFIED, TRIGGER_SEVERITY_COUNT - 1))],
874			'status' =>					['type' => API_INT32, 'in' => implode(',', [TRIGGER_STATUS_ENABLED, TRIGGER_STATUS_DISABLED])],
875			'type' =>					['type' => API_INT32, 'in' => implode(',', [TRIGGER_MULT_EVENT_DISABLED, TRIGGER_MULT_EVENT_ENABLED])],
876			'url' =>					['type' => API_URL, 'flags' => API_ALLOW_USER_MACRO, 'length' => DB::getFieldLength('triggers', 'url')],
877			'recovery_mode' =>			['type' => API_INT32, 'in' => implode(',', [ZBX_RECOVERY_MODE_EXPRESSION, ZBX_RECOVERY_MODE_RECOVERY_EXPRESSION, ZBX_RECOVERY_MODE_NONE]), 'default' => DB::getDefault('triggers', 'recovery_mode')],
878			'recovery_expression' =>	['type' => API_TRIGGER_EXPRESSION, 'flags' => API_ALLOW_LLD_MACRO, 'default' => DB::getDefault('triggers', 'recovery_expression')],
879			'correlation_mode' =>		['type' => API_INT32, 'in' => implode(',', [ZBX_TRIGGER_CORRELATION_NONE, ZBX_TRIGGER_CORRELATION_TAG]), 'default' => DB::getDefault('triggers', 'correlation_mode')],
880			'correlation_tag' =>		['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('triggers', 'correlation_tag'), 'default' => DB::getDefault('triggers', 'correlation_tag')],
881			'manual_close' =>			['type' => API_INT32, 'in' => implode(',', [ZBX_TRIGGER_MANUAL_CLOSE_NOT_ALLOWED, ZBX_TRIGGER_MANUAL_CLOSE_ALLOWED])],
882			'tags' =>					['type' => API_OBJECTS, 'uniq' => [['tag', 'value']], 'fields' => [
883				'tag' =>					['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('trigger_tag', 'tag')],
884				'value' =>					['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('trigger_tag', 'value'), 'default' => DB::getDefault('trigger_tag', 'value')]
885			]],
886			'dependencies' =>			['type' => API_OBJECTS, 'uniq' => [['triggerid']], 'fields'=> [
887				'triggerid' =>				['type' => API_ID, 'flags' => API_REQUIRED]
888			]]
889		]];
890		if ($this instanceof CTriggerPrototype) {
891			$api_input_rules['fields']['discover'] = ['type' => API_INT32, 'in' => implode(',', [TRIGGER_DISCOVER, TRIGGER_NO_DISCOVER])];
892		}
893		else {
894			$api_input_rules['fields']['expression']['flags'] &= ~API_ALLOW_LLD_MACRO;
895			$api_input_rules['fields']['recovery_expression']['flags'] &= ~API_ALLOW_LLD_MACRO;
896		}
897		if (!CApiInputValidator::validate($api_input_rules, $triggers, '/', $error)) {
898			self::exception(ZBX_API_ERROR_PARAMETERS, $error);
899		}
900
901		$descriptions = [];
902		foreach ($triggers as $index => $trigger) {
903			self::checkTriggerRecoveryMode($trigger);
904			self::checkTriggerCorrelationMode($trigger);
905
906			$descriptions[$trigger['description']][] = [
907				'index' => $index,
908				'uuid' => array_key_exists('uuid', $trigger) ? $trigger['uuid'] : null,
909				'expression' => $trigger['expression'],
910				'recovery_expression' => $trigger['recovery_expression']
911			];
912		}
913
914		$descriptions = $this->populateHostIds($descriptions);
915		$this->checkAndAddUuid($triggers, $descriptions);
916		$this->checkDuplicates($descriptions);
917	}
918
919	/**
920	 * Validate trigger to be updated.
921	 *
922	 * @param array  $triggers                                   [IN/OUT]
923	 * @param array  $triggers[]['triggerid']                    [IN]
924	 * @param array  $triggers[]['description']                  [IN/OUT] (optional)
925	 * @param string $triggers[]['expression']                   [IN/OUT] (optional)
926	 * @param string $triggers[]['event_name']                   [IN] (optional)
927	 * @param string $triggers[]['opdata']                       [IN] (optional)
928	 * @param string $triggers[]['comments']                     [IN] (optional)
929	 * @param int    $triggers[]['priority']                     [IN] (optional)
930	 * @param int    $triggers[]['status']                       [IN] (optional)
931	 * @param int    $triggers[]['type']                         [IN] (optional)
932	 * @param string $triggers[]['url']                          [IN] (optional)
933	 * @param int    $triggers[]['recovery_mode']                [IN/OUT] (optional)
934	 * @param string $triggers[]['recovery_expression']          [IN/OUT] (optional)
935	 * @param int    $triggers[]['correlation_mode']             [IN/OUT] (optional)
936	 * @param string $triggers[]['correlation_tag']              [IN/OUT] (optional)
937	 * @param int    $triggers[]['manual_close']                 [IN] (optional)
938	 * @param int    $triggers[]['discover']                     [IN] (optional) for trigger prototypes only
939	 * @param array  $triggers[]['tags']                         [IN] (optional)
940	 * @param string $triggers[]['tags'][]['tag']                [IN]
941	 * @param string $triggers[]['tags'][]['value']              [IN/OUT] (optional)
942	 * @param array  $triggers[]['dependencies']                 [IN] (optional)
943	 * @param string $triggers[]['dependencies'][]['triggerid']  [IN]
944	 * @param array  $db_triggers                                [OUT]
945	 * @param array  $db_triggers[<tnum>]['triggerid']           [OUT]
946	 * @param array  $db_triggers[<tnum>]['description']         [OUT]
947	 * @param string $db_triggers[<tnum>]['expression']          [OUT]
948	 * @param string $db_triggers[<tnum>]['event_name']          [OUT]
949	 * @param string $db_triggers[<tnum>]['opdata']              [OUT]
950	 * @param int    $db_triggers[<tnum>]['recovery_mode']       [OUT]
951	 * @param string $db_triggers[<tnum>]['recovery_expression'] [OUT]
952	 * @param string $db_triggers[<tnum>]['url']                 [OUT]
953	 * @param int    $db_triggers[<tnum>]['status']              [OUT]
954	 * @param int    $db_triggers[<tnum>]['discover']            [OUT]
955	 * @param int    $db_triggers[<tnum>]['priority']            [OUT]
956	 * @param string $db_triggers[<tnum>]['comments']            [OUT]
957	 * @param int    $db_triggers[<tnum>]['type']                [OUT]
958	 * @param string $db_triggers[<tnum>]['templateid']          [OUT]
959	 * @param int    $db_triggers[<tnum>]['correlation_mode']    [OUT]
960	 * @param string $db_triggers[<tnum>]['correlation_tag']     [OUT]
961	 * @param int    $db_triggers[<tnum>]['discover']            [OUT] for trigger prototypes only
962	 *
963	 * @throws APIException if validation failed.
964	 */
965	protected function validateUpdate(array &$triggers, array &$db_triggers = null) {
966		$db_triggers = [];
967
968		$api_input_rules = ['type' => API_OBJECTS, 'flags' => API_NOT_EMPTY | API_NORMALIZE, 'uniq' => [['description', 'expression']], 'fields' => [
969			'triggerid' =>				['type' => API_ID, 'flags' => API_REQUIRED],
970			'description' =>			['type' => API_STRING_UTF8, 'flags' => API_NOT_EMPTY, 'length' => DB::getFieldLength('triggers', 'description')],
971			'expression' =>				['type' => API_TRIGGER_EXPRESSION, 'flags' => API_NOT_EMPTY | API_ALLOW_LLD_MACRO],
972			'event_name' =>				['type' => API_EVENT_NAME, 'length' => DB::getFieldLength('triggers', 'event_name')],
973			'opdata' =>					['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('triggers', 'opdata')],
974			'comments' =>				['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('triggers', 'comments')],
975			'priority' =>				['type' => API_INT32, 'in' => implode(',', range(TRIGGER_SEVERITY_NOT_CLASSIFIED, TRIGGER_SEVERITY_COUNT - 1))],
976			'status' =>					['type' => API_INT32, 'in' => implode(',', [TRIGGER_STATUS_ENABLED, TRIGGER_STATUS_DISABLED])],
977			'type' =>					['type' => API_INT32, 'in' => implode(',', [TRIGGER_MULT_EVENT_DISABLED, TRIGGER_MULT_EVENT_ENABLED])],
978			'url' =>					['type' => API_URL, 'flags' => API_ALLOW_USER_MACRO, 'length' => DB::getFieldLength('triggers', 'url')],
979			'recovery_mode' =>			['type' => API_INT32, 'in' => implode(',', [ZBX_RECOVERY_MODE_EXPRESSION, ZBX_RECOVERY_MODE_RECOVERY_EXPRESSION, ZBX_RECOVERY_MODE_NONE])],
980			'recovery_expression' =>	['type' => API_TRIGGER_EXPRESSION, 'flags' => API_ALLOW_LLD_MACRO],
981			'correlation_mode' =>		['type' => API_INT32, 'in' => implode(',', [ZBX_TRIGGER_CORRELATION_NONE, ZBX_TRIGGER_CORRELATION_TAG])],
982			'correlation_tag' =>		['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('triggers', 'correlation_tag')],
983			'manual_close' =>			['type' => API_INT32, 'in' => implode(',', [ZBX_TRIGGER_MANUAL_CLOSE_NOT_ALLOWED, ZBX_TRIGGER_MANUAL_CLOSE_ALLOWED])],
984			'tags' =>					['type' => API_OBJECTS, 'uniq' => [['tag', 'value']], 'fields' => [
985				'tag' =>					['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('trigger_tag', 'tag')],
986				'value' =>					['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('trigger_tag', 'value'), 'default' => DB::getDefault('trigger_tag', 'value')]
987			]],
988			'dependencies' =>			['type' => API_OBJECTS, 'uniq' => [['triggerid']], 'fields'=> [
989				'triggerid' =>				['type' => API_ID, 'flags' => API_REQUIRED]
990			]]
991		]];
992		if ($this instanceof CTriggerPrototype) {
993			$api_input_rules['fields']['discover'] = ['type' => API_INT32, 'in' => implode(',', [TRIGGER_DISCOVER, TRIGGER_NO_DISCOVER])];
994		}
995		else {
996			$api_input_rules['fields']['expression']['flags'] &= ~API_ALLOW_LLD_MACRO;
997			$api_input_rules['fields']['recovery_expression']['flags'] &= ~API_ALLOW_LLD_MACRO;
998		}
999		if (!CApiInputValidator::validate($api_input_rules, $triggers, '/', $error)) {
1000			self::exception(ZBX_API_ERROR_PARAMETERS, $error);
1001		}
1002
1003		$options = [
1004			'output' => ['triggerid', 'description', 'expression', 'url', 'status', 'priority', 'comments', 'type',
1005				'templateid', 'recovery_mode', 'recovery_expression', 'correlation_mode', 'correlation_tag',
1006				'manual_close', 'opdata', 'event_name'
1007			],
1008			'selectDependencies' => ['triggerid'],
1009			'triggerids' => zbx_objectValues($triggers, 'triggerid'),
1010			'editable' => true,
1011			'preservekeys' => true
1012		];
1013
1014		$class = get_class($this);
1015
1016		switch ($class) {
1017			case 'CTrigger':
1018				$error_cannot_update = _('Cannot update "%1$s" for templated trigger "%2$s".');
1019				$options['output'][] = 'flags';
1020
1021				// Discovered fields, except status, cannot be updated.
1022				$update_discovered_validator = new CUpdateDiscoveredValidator([
1023					'allowed' => ['triggerid', 'status'],
1024					'messageAllowedField' => _('Cannot update "%2$s" for a discovered trigger "%1$s".')
1025				]);
1026				break;
1027
1028			case 'CTriggerPrototype':
1029				$error_cannot_update = _('Cannot update "%1$s" for templated trigger prototype "%2$s".');
1030				$options['output'][] = 'discover';
1031				$options['selectDiscoveryRule'] = ['itemid'];
1032				break;
1033
1034			default:
1035				self::exception(ZBX_API_ERROR_INTERNAL, _('Internal error.'));
1036		}
1037
1038		$_db_triggers = CMacrosResolverHelper::resolveTriggerExpressions($this->get($options),
1039			['sources' => ['expression', 'recovery_expression']]
1040		);
1041
1042		$db_trigger_tags = $_db_triggers
1043			? DB::select('trigger_tag', [
1044				'output' => ['triggertagid', 'triggerid', 'tag', 'value'],
1045				'filter' => ['triggerid' => array_keys($_db_triggers)],
1046				'preservekeys' => true
1047			])
1048			: [];
1049
1050		$_db_triggers = $this
1051			->createRelationMap($db_trigger_tags, 'triggerid', 'triggertagid')
1052			->mapMany($_db_triggers, $db_trigger_tags, 'tags');
1053
1054		$read_only_fields = ['description', 'expression', 'recovery_mode', 'recovery_expression', 'correlation_mode',
1055			'correlation_tag', 'manual_close'
1056		];
1057
1058		$descriptions = [];
1059
1060		foreach ($triggers as $key => &$trigger) {
1061			if (!array_key_exists($trigger['triggerid'], $_db_triggers)) {
1062				self::exception(ZBX_API_ERROR_PARAMETERS, _('No permissions to referred object or it does not exist!'));
1063			}
1064
1065			$db_trigger = $_db_triggers[$trigger['triggerid']];
1066			$description = array_key_exists('description', $trigger)
1067				? $trigger['description']
1068				: $db_trigger['description'];
1069
1070			if ($class === 'CTrigger') {
1071				$update_discovered_validator->setObjectName($description);
1072				$this->checkPartialValidator($trigger, $update_discovered_validator, $db_trigger);
1073			}
1074
1075			if ($db_trigger['templateid'] != 0) {
1076				$this->checkNoParameters($trigger, $read_only_fields, $error_cannot_update, $description);
1077			}
1078
1079			$field_names = ['description', 'expression', 'recovery_mode', 'manual_close'];
1080			foreach ($field_names as $field_name) {
1081				if (!array_key_exists($field_name, $trigger)) {
1082					$trigger[$field_name] = $db_trigger[$field_name];
1083				}
1084			}
1085
1086			if (!array_key_exists('recovery_expression', $trigger)) {
1087				$trigger['recovery_expression'] = ($trigger['recovery_mode'] == ZBX_RECOVERY_MODE_RECOVERY_EXPRESSION)
1088					? $db_trigger['recovery_expression']
1089					: '';
1090			}
1091			if (!array_key_exists('correlation_mode', $trigger)) {
1092				$trigger['correlation_mode'] = ($trigger['recovery_mode'] != ZBX_RECOVERY_MODE_NONE)
1093					? $db_trigger['correlation_mode']
1094					: ZBX_TRIGGER_CORRELATION_NONE;
1095			}
1096			if (!array_key_exists('correlation_tag', $trigger)) {
1097				$trigger['correlation_tag'] = ($trigger['correlation_mode'] == ZBX_TRIGGER_CORRELATION_TAG)
1098					? $db_trigger['correlation_tag']
1099					: '';
1100			}
1101
1102			self::checkTriggerRecoveryMode($trigger);
1103			self::checkTriggerCorrelationMode($trigger);
1104
1105			if ($trigger['expression'] !== $db_trigger['expression']
1106					|| $trigger['recovery_expression'] !== $db_trigger['recovery_expression']
1107					|| $trigger['description'] !== $db_trigger['description']) {
1108				$descriptions[$trigger['description']][] = [
1109					'expression' => $trigger['expression'],
1110					'recovery_expression' => $trigger['recovery_expression']
1111				];
1112			}
1113
1114			$db_triggers[$key] = $db_trigger;
1115		}
1116		unset($trigger);
1117
1118		if ($descriptions) {
1119			$descriptions = $this->populateHostIds($descriptions);
1120			$this->checkDuplicates($descriptions);
1121		}
1122	}
1123
1124	/**
1125	 * Inserts trigger or trigger prototypes records into the database.
1126	 *
1127	 * @param array  $triggers                          [IN/OUT]
1128	 * @param array  $triggers[]['triggerid']           [OUT]
1129	 * @param array  $triggers[]['description']         [IN]
1130	 * @param string $triggers[]['expression']          [IN]
1131	 * @param int    $triggers[]['recovery_mode']       [IN]
1132	 * @param string $triggers[]['recovery_expression'] [IN]
1133	 * @param string $triggers[]['url']                 [IN] (optional)
1134	 * @param int    $triggers[]['status']              [IN] (optional)
1135	 * @param int    $triggers[]['priority']            [IN] (optional)
1136	 * @param string $triggers[]['comments']            [IN] (optional)
1137	 * @param int    $triggers[]['type']                [IN] (optional)
1138	 * @param string $triggers[]['templateid']          [IN] (optional)
1139	 * @param array  $triggers[]['tags']                [IN] (optional)
1140	 * @param string $triggers[]['tags'][]['tag']       [IN]
1141	 * @param string $triggers[]['tags'][]['value']     [IN]
1142	 * @param int    $triggers[]['correlation_mode']    [IN] (optional)
1143	 * @param string $triggers[]['correlation_tag']     [IN] (optional)
1144	 * @param bool   $inherited                         [IN] (optional)  If set to true, trigger will be created for
1145	 *                                                                   non-editable host/template.
1146	 *
1147	 * @throws APIException
1148	 */
1149	protected function createReal(array &$triggers, $inherited = false) {
1150		$class = get_class($this);
1151
1152		switch ($class) {
1153			case 'CTrigger':
1154				$resource = AUDIT_RESOURCE_TRIGGER;
1155				break;
1156
1157			case 'CTriggerPrototype':
1158				$resource = AUDIT_RESOURCE_TRIGGER_PROTOTYPE;
1159				break;
1160
1161			default:
1162				self::exception(ZBX_API_ERROR_INTERNAL, _('Internal error.'));
1163		}
1164
1165		$new_triggers = $triggers;
1166		$new_functions = [];
1167		$triggers_functions = [];
1168		$new_tags = [];
1169		$this->implode_expressions($new_triggers, null, $triggers_functions, $inherited);
1170
1171		$triggerid = DB::reserveIds('triggers', count($new_triggers));
1172
1173		foreach ($new_triggers as $tnum => &$new_trigger) {
1174			$new_trigger['triggerid'] = $triggerid;
1175			$triggers[$tnum]['triggerid'] = $triggerid;
1176
1177			foreach ($triggers_functions[$tnum] as $trigger_function) {
1178				$trigger_function['triggerid'] = $triggerid;
1179				$new_functions[] = $trigger_function;
1180			}
1181
1182			if ($class === 'CTriggerPrototype') {
1183				$new_trigger['flags'] = ZBX_FLAG_DISCOVERY_PROTOTYPE;
1184			}
1185
1186			if (array_key_exists('tags', $new_trigger)) {
1187				foreach ($new_trigger['tags'] as $tag) {
1188					$tag['triggerid'] = $triggerid;
1189					$new_tags[] = $tag;
1190				}
1191			}
1192
1193			$triggerid = bcadd($triggerid, 1, 0);
1194		}
1195		unset($new_trigger);
1196
1197		DB::insert('triggers', $new_triggers, false);
1198		DB::insertBatch('functions', $new_functions, false);
1199
1200		if ($new_tags) {
1201			DB::insert('trigger_tag', $new_tags);
1202		}
1203
1204		if (!$inherited) {
1205			$this->addAuditBulk(AUDIT_ACTION_ADD, $resource, $triggers);
1206		}
1207	}
1208
1209	/**
1210	 * Update trigger or trigger prototypes records in the database.
1211	 *
1212	 * @param array  $triggers                                       [IN] list of triggers to be updated
1213	 * @param array  $triggers[<tnum>]['triggerid']                  [IN]
1214	 * @param array  $triggers[<tnum>]['description']                [IN]
1215	 * @param string $triggers[<tnum>]['expression']                 [IN]
1216	 * @param int    $triggers[<tnum>]['recovery_mode']              [IN]
1217	 * @param string $triggers[<tnum>]['recovery_expression']        [IN]
1218	 * @param string $triggers[<tnum>]['url']                        [IN] (optional)
1219	 * @param int    $triggers[<tnum>]['status']                     [IN] (optional)
1220	 * @param int    $triggers[<tnum>]['priority']                   [IN] (optional)
1221	 * @param string $triggers[<tnum>]['comments']                   [IN] (optional)
1222	 * @param int    $triggers[<tnum>]['type']                       [IN] (optional)
1223	 * @param string $triggers[<tnum>]['templateid']                 [IN] (optional)
1224	 * @param array  $triggers[<tnum>]['tags']                       [IN]
1225	 * @param string $triggers[<tnum>]['tags'][]['tag']              [IN]
1226	 * @param string $triggers[<tnum>]['tags'][]['value']            [IN]
1227	 * @param int    $triggers[<tnum>]['correlation_mode']           [IN]
1228	 * @param string $triggers[<tnum>]['correlation_tag']            [IN]
1229	 * @param array  $db_triggers                                    [IN]
1230	 * @param array  $db_triggers[<tnum>]['triggerid']               [IN]
1231	 * @param array  $db_triggers[<tnum>]['description']             [IN]
1232	 * @param string $db_triggers[<tnum>]['expression']              [IN]
1233	 * @param int    $db_triggers[<tnum>]['recovery_mode']           [IN]
1234	 * @param string $db_triggers[<tnum>]['recovery_expression']     [IN]
1235	 * @param string $db_triggers[<tnum>]['url']                     [IN]
1236	 * @param int    $db_triggers[<tnum>]['status']                  [IN]
1237	 * @param int    $db_triggers[<tnum>]['priority']                [IN]
1238	 * @param string $db_triggers[<tnum>]['comments']                [IN]
1239	 * @param int    $db_triggers[<tnum>]['type']                    [IN]
1240	 * @param string $db_triggers[<tnum>]['templateid']              [IN]
1241	 * @param array  $db_triggers[<tnum>]['discoveryRule']           [IN] For trigger prototypes only.
1242	 * @param string $db_triggers[<tnum>]['discoveryRule']['itemid'] [IN]
1243	 * @param array  $db_triggers[<tnum>]['tags']                    [IN]
1244	 * @param string $db_triggers[<tnum>]['tags'][]['tag']           [IN]
1245	 * @param string $db_triggers[<tnum>]['tags'][]['value']         [IN]
1246	 * @param int    $db_triggers[<tnum>]['correlation_mode']        [IN]
1247	 * @param string $db_triggers[<tnum>]['correlation_tag']         [IN]
1248	 * @param bool   $inherited                                      [IN] (optional)  If set to true, trigger will be
1249	 *                                                                                created for non-editable
1250	 *                                                                                host/template.
1251	 *
1252	 * @throws APIException
1253	 */
1254	protected function updateReal(array $triggers, array $db_triggers, $inherited = false) {
1255		$class = get_class($this);
1256
1257		switch ($class) {
1258			case 'CTrigger':
1259				$resource = AUDIT_RESOURCE_TRIGGER;
1260				break;
1261
1262			case 'CTriggerPrototype':
1263				$resource = AUDIT_RESOURCE_TRIGGER_PROTOTYPE;
1264				break;
1265
1266			default:
1267				self::exception(ZBX_API_ERROR_INTERNAL, _('Internal error.'));
1268		}
1269
1270		$upd_triggers = [];
1271		$new_functions = [];
1272		$del_functions_triggerids = [];
1273		$triggers_functions = [];
1274		$new_tags = [];
1275		$del_triggertagids = [];
1276		$save_triggers = $triggers;
1277		$this->implode_expressions($triggers, $db_triggers, $triggers_functions, $inherited);
1278
1279		if ($class === 'CTrigger') {
1280			// The list of the triggers with changed priority.
1281			$changed_priority_triggerids = [];
1282		}
1283
1284		foreach ($triggers as $tnum => $trigger) {
1285			$db_trigger = $db_triggers[$tnum];
1286			$upd_trigger = ['values' => [], 'where' => ['triggerid' => $trigger['triggerid']]];
1287
1288			if (array_key_exists($tnum, $triggers_functions)) {
1289				$del_functions_triggerids[] = $trigger['triggerid'];
1290
1291				foreach ($triggers_functions[$tnum] as $trigger_function) {
1292					$trigger_function['triggerid'] = $trigger['triggerid'];
1293					$new_functions[] = $trigger_function;
1294				}
1295
1296				$upd_trigger['values']['expression'] = $trigger['expression'];
1297				$upd_trigger['values']['recovery_expression'] = $trigger['recovery_expression'];
1298			}
1299
1300			if (array_key_exists('uuid', $trigger)) {
1301				$upd_trigger['values']['uuid'] = $trigger['uuid'];
1302			}
1303			if ($trigger['description'] !== $db_trigger['description']) {
1304				$upd_trigger['values']['description'] = $trigger['description'];
1305			}
1306			if (array_key_exists('event_name', $trigger) && $trigger['event_name'] !== $db_trigger['event_name']) {
1307				$upd_trigger['values']['event_name'] = $trigger['event_name'];
1308			}
1309			if (array_key_exists('opdata', $trigger) && $trigger['opdata'] !== $db_trigger['opdata']) {
1310				$upd_trigger['values']['opdata'] = $trigger['opdata'];
1311			}
1312			if ($trigger['recovery_mode'] != $db_trigger['recovery_mode']) {
1313				$upd_trigger['values']['recovery_mode'] = $trigger['recovery_mode'];
1314			}
1315			if (array_key_exists('url', $trigger) && $trigger['url'] !== $db_trigger['url']) {
1316				$upd_trigger['values']['url'] = $trigger['url'];
1317			}
1318			if (array_key_exists('status', $trigger) && $trigger['status'] != $db_trigger['status']) {
1319				$upd_trigger['values']['status'] = $trigger['status'];
1320			}
1321			if ($class === 'CTriggerPrototype'
1322					&& array_key_exists('discover', $trigger) && $trigger['discover'] != $db_trigger['discover']) {
1323				$upd_trigger['values']['discover'] = $trigger['discover'];
1324			}
1325			if (array_key_exists('priority', $trigger) && $trigger['priority'] != $db_trigger['priority']) {
1326				$upd_trigger['values']['priority'] = $trigger['priority'];
1327
1328				if ($class === 'CTrigger') {
1329					$changed_priority_triggerids[] = $trigger['triggerid'];
1330				}
1331			}
1332			if (array_key_exists('comments', $trigger) && $trigger['comments'] !== $db_trigger['comments']) {
1333				$upd_trigger['values']['comments'] = $trigger['comments'];
1334			}
1335			if (array_key_exists('type', $trigger) && $trigger['type'] != $db_trigger['type']) {
1336				$upd_trigger['values']['type'] = $trigger['type'];
1337			}
1338			if (array_key_exists('templateid', $trigger) && $trigger['templateid'] != $db_trigger['templateid']) {
1339				$upd_trigger['values']['templateid'] = $trigger['templateid'];
1340			}
1341			if ($trigger['correlation_mode'] != $db_trigger['correlation_mode']) {
1342				$upd_trigger['values']['correlation_mode'] = $trigger['correlation_mode'];
1343			}
1344			if ($trigger['correlation_tag'] !== $db_trigger['correlation_tag']) {
1345				$upd_trigger['values']['correlation_tag'] = $trigger['correlation_tag'];
1346			}
1347			if ($trigger['manual_close'] != $db_trigger['manual_close']) {
1348				$upd_trigger['values']['manual_close'] = $trigger['manual_close'];
1349			}
1350
1351			if ($upd_trigger['values']) {
1352				$upd_triggers[] = $upd_trigger;
1353			}
1354
1355			if (array_key_exists('tags', $trigger)) {
1356				// Add new trigger tags and replace changed ones.
1357
1358				CArrayHelper::sort($db_trigger['tags'], ['tag', 'value']);
1359				CArrayHelper::sort($trigger['tags'], ['tag', 'value']);
1360
1361				$tags_delete = $db_trigger['tags'];
1362				$tags_add = $trigger['tags'];
1363
1364				foreach ($tags_delete as $dt_key => $tag_delete) {
1365					foreach ($tags_add as $nt_key => $tag_add) {
1366						if ($tag_delete['tag'] === $tag_add['tag'] && $tag_delete['value'] === $tag_add['value']) {
1367							unset($tags_delete[$dt_key], $tags_add[$nt_key]);
1368							continue 2;
1369						}
1370					}
1371				}
1372
1373				foreach ($tags_delete as $tag_delete) {
1374					$del_triggertagids[] = $tag_delete['triggertagid'];
1375				}
1376
1377				foreach ($tags_add as $tag_add) {
1378					$tag_add['triggerid'] = $trigger['triggerid'];
1379					$new_tags[] = $tag_add;
1380				}
1381			}
1382		}
1383
1384		if ($upd_triggers) {
1385			DB::update('triggers', $upd_triggers);
1386		}
1387		if ($del_functions_triggerids) {
1388			DB::delete('functions', ['triggerid' => $del_functions_triggerids]);
1389		}
1390		if ($new_functions) {
1391			DB::insertBatch('functions', $new_functions, false);
1392		}
1393		if ($del_triggertagids) {
1394			DB::delete('trigger_tag', ['triggertagid' => $del_triggertagids]);
1395		}
1396		if ($new_tags) {
1397			DB::insert('trigger_tag', $new_tags);
1398		}
1399
1400		if ($class === 'CTrigger' && $changed_priority_triggerids
1401				&& CTriggerManager::usedInItServices($changed_priority_triggerids)) {
1402			updateItServices();
1403		}
1404
1405		if (!$inherited) {
1406			$this->addAuditBulk(AUDIT_ACTION_UPDATE, $resource, $save_triggers, zbx_toHash($db_triggers, 'triggerid'));
1407		}
1408	}
1409
1410	/**
1411	 * Implodes expression and recovery_expression for each trigger. Also returns array of functions and
1412	 * array of hostnames for each trigger.
1413	 *
1414	 * For example: last(/host/system.cpu.load)>10 will be translated to {12}>10 and created database representation.
1415	 *
1416	 * Note: All expressions must be already validated and exploded.
1417	 *
1418	 * @param array      $triggers                                   [IN]
1419	 * @param string     $triggers[<tnum>]['description']            [IN]
1420	 * @param string     $triggers[<tnum>]['expression']             [IN/OUT]
1421	 * @param int        $triggers[<tnum>]['recovery_mode']          [IN]
1422	 * @param string     $triggers[<tnum>]['recovery_expression']    [IN/OUT]
1423	 * @param array|null $db_triggers                                [IN]
1424	 * @param string     $db_triggers[<tnum>]['triggerid']           [IN]
1425	 * @param string     $db_triggers[<tnum>]['expression']          [IN]
1426	 * @param string     $db_triggers[<tnum>]['recovery_expression'] [IN]
1427	 * @param array      $triggers_functions                         [OUT] array of the new functions which must be
1428	 *                                                                     inserted into DB
1429	 * @param string     $triggers_functions[<tnum>][]['functionid'] [OUT]
1430	 * @param null       $triggers_functions[<tnum>][]['triggerid']  [OUT] must be initialized before insertion into DB
1431	 * @param string     $triggers_functions[<tnum>][]['itemid']     [OUT]
1432	 * @param string     $triggers_functions[<tnum>][]['name']       [OUT]
1433	 * @param string     $triggers_functions[<tnum>][]['parameter']  [OUT]
1434	 * @param bool       $inherited                                  [IN] (optional)  If set to true, triggers will be
1435	 *                                                                                created for non-editable
1436	 *                                                                                hosts/templates.
1437	 *
1438	 * @throws APIException if error occurred
1439	 */
1440	private function implode_expressions(array &$triggers, array $db_triggers = null, array &$triggers_functions,
1441			$inherited = false) {
1442		$class = get_class($this);
1443
1444		switch ($class) {
1445			case 'CTrigger':
1446				$expression_parser = new CExpressionParser(['usermacros' => true]);
1447				$error_wrong_host = _('Incorrect trigger expression. Host "%1$s" does not exist or you have no access to this host.');
1448				$error_host_and_template = _('Incorrect trigger expression. Trigger expression elements should not belong to a template and a host simultaneously.');
1449				break;
1450
1451			case 'CTriggerPrototype':
1452				$expression_parser = new CExpressionParser(['usermacros' => true, 'lldmacros' => true]);
1453				$error_wrong_host = _('Incorrect trigger prototype expression. Host "%1$s" does not exist or you have no access to this host.');
1454				$error_host_and_template = _('Incorrect trigger prototype expression. Trigger prototype expression elements should not belong to a template and a host simultaneously.');
1455				break;
1456
1457			default:
1458				self::exception(ZBX_API_ERROR_INTERNAL, _('Internal error.'));
1459		}
1460
1461		$hist_function_value_types = (new CHistFunctionData())->getValueTypes();
1462
1463		/*
1464		 * [
1465		 *     <host> => [
1466		 *         'hostid' => <hostid>,
1467		 *         'host' => <host>,
1468		 *         'status' => <status>,
1469		 *         'keys' => [
1470		 *             <key> => [
1471		 *                 'itemid' => <itemid>,
1472		 *                 'key' => <key>,
1473		 *                 'value_type' => <value_type>,
1474		 *                 'flags' => <flags>,
1475		 *                 'lld_ruleid' => <itemid> (CTriggerProrotype only)
1476		 *             ]
1477		 *         ]
1478		 *     ]
1479		 * ]
1480		 */
1481		$hosts_keys = [];
1482		$functions_num = 0;
1483
1484		foreach ($triggers as $tnum => $trigger) {
1485			$expressions_changed = ($db_triggers === null
1486				|| ($trigger['expression'] !== $db_triggers[$tnum]['expression']
1487				|| $trigger['recovery_expression'] !== $db_triggers[$tnum]['recovery_expression']));
1488
1489			if (!$expressions_changed) {
1490				continue;
1491			}
1492
1493			$expression_parser->parse($trigger['expression']);
1494			$hist_functions = $expression_parser->getResult()->getTokensOfTypes(
1495				[CExpressionParserResult::TOKEN_TYPE_HIST_FUNCTION]
1496			);
1497
1498			if ($trigger['recovery_mode'] == ZBX_RECOVERY_MODE_RECOVERY_EXPRESSION) {
1499				$expression_parser->parse($trigger['recovery_expression']);
1500				$hist_functions = array_merge($hist_functions, $expression_parser->getResult()->getTokensOfTypes(
1501					[CExpressionParserResult::TOKEN_TYPE_HIST_FUNCTION]
1502				));
1503			}
1504
1505			foreach ($hist_functions as $hist_function) {
1506				$host = $hist_function['data']['parameters'][0]['data']['host'];
1507				$item = $hist_function['data']['parameters'][0]['data']['item'];
1508
1509				if (!array_key_exists($host, $hosts_keys)) {
1510					$hosts_keys[$host] = [
1511						'hostid' => null,
1512						'host' => $host,
1513						'status' => null,
1514						'keys' => []
1515					];
1516				}
1517
1518				$hosts_keys[$host]['keys'][$item] = [
1519					'itemid' => null,
1520					'key' => $item,
1521					'value_type' => null,
1522					'flags' => null
1523				];
1524			}
1525		}
1526
1527		if (!$hosts_keys) {
1528			return;
1529		}
1530
1531		$permission_check = $inherited
1532			? ['nopermissions' => true]
1533			: ['editable' => true];
1534
1535		$_db_hosts = API::Host()->get([
1536			'output' => ['hostid', 'host', 'status'],
1537			'filter' => ['host' => array_keys($hosts_keys)]
1538		] + $permission_check);
1539
1540		if (count($hosts_keys) != count($_db_hosts)) {
1541			$_db_templates = API::Template()->get([
1542				'output' => ['templateid', 'host', 'status'],
1543				'filter' => ['host' => array_keys($hosts_keys)]
1544			] + $permission_check);
1545
1546			foreach ($_db_templates as &$_db_template) {
1547				$_db_template['hostid'] = $_db_template['templateid'];
1548				unset($_db_template['templateid']);
1549			}
1550			unset($_db_template);
1551
1552			$_db_hosts = array_merge($_db_hosts, $_db_templates);
1553		}
1554
1555		foreach ($_db_hosts as $_db_host) {
1556			$host_keys = &$hosts_keys[$_db_host['host']];
1557
1558			$host_keys['hostid'] = $_db_host['hostid'];
1559			$host_keys['status'] = $_db_host['status'];
1560
1561			if ($class === 'CTriggerPrototype') {
1562				$sql = 'SELECT i.itemid,i.key_,i.value_type,i.flags,id.parent_itemid'.
1563					' FROM items i'.
1564						' LEFT JOIN item_discovery id ON i.itemid=id.itemid'.
1565					' WHERE i.hostid='.$host_keys['hostid'].
1566						' AND '.dbConditionString('i.key_', array_keys($host_keys['keys'])).
1567						' AND '.dbConditionInt('i.flags',
1568							[ZBX_FLAG_DISCOVERY_NORMAL, ZBX_FLAG_DISCOVERY_PROTOTYPE, ZBX_FLAG_DISCOVERY_CREATED]
1569						);
1570			}
1571			else {
1572				$sql = 'SELECT i.itemid,i.key_,i.value_type,i.flags'.
1573					' FROM items i'.
1574					' WHERE i.hostid='.$host_keys['hostid'].
1575						' AND '.dbConditionString('i.key_', array_keys($host_keys['keys'])).
1576						' AND '.dbConditionInt('i.flags', [ZBX_FLAG_DISCOVERY_NORMAL, ZBX_FLAG_DISCOVERY_CREATED]);
1577			}
1578
1579			$_db_items = DBselect($sql);
1580
1581			while ($_db_item = DBfetch($_db_items)) {
1582				$host_keys['keys'][$_db_item['key_']]['itemid'] = $_db_item['itemid'];
1583				$host_keys['keys'][$_db_item['key_']]['value_type'] = $_db_item['value_type'];
1584				$host_keys['keys'][$_db_item['key_']]['flags'] = $_db_item['flags'];
1585
1586				if ($class === 'CTriggerPrototype' && $_db_item['flags'] == ZBX_FLAG_DISCOVERY_PROTOTYPE) {
1587					$host_keys['keys'][$_db_item['key_']]['lld_ruleid'] = $_db_item['parent_itemid'];
1588				}
1589			}
1590
1591			unset($host_keys);
1592		}
1593
1594		/*
1595		 * The list of triggers with multiple templates.
1596		 *
1597		 * [
1598		 *     [
1599		 *         'description' => <description>,
1600		 *         'templateids' => [<templateid>, ...]
1601		 *     ],
1602		 *     ...
1603		 * ]
1604		 */
1605		$mt_triggers = [];
1606
1607		if ($class === 'CTrigger') {
1608			/*
1609			 * The list of triggers which are moved from one host or template to another.
1610			 *
1611			 * [
1612			 *     <triggerid> => [
1613			 *         'description' => <description>
1614			 *     ],
1615			 *     ...
1616			 * ]
1617			 */
1618			$moved_triggers = [];
1619		}
1620
1621		foreach ($triggers as $tnum => &$trigger) {
1622			$expressions_changed = ($db_triggers === null
1623				|| ($trigger['expression'] !== $db_triggers[$tnum]['expression']
1624				|| $trigger['recovery_expression'] !== $db_triggers[$tnum]['recovery_expression']));
1625
1626			if (!$expressions_changed) {
1627				continue;
1628			}
1629
1630			$expression_parser->parse($trigger['expression']);
1631
1632			$hist_functions1 = $expression_parser->getResult()->getTokensOfTypes(
1633				[CExpressionParserResult::TOKEN_TYPE_HIST_FUNCTION]
1634			);
1635
1636			$hist_functions2 = [];
1637
1638			if ($trigger['recovery_mode'] == ZBX_RECOVERY_MODE_RECOVERY_EXPRESSION) {
1639				$expression_parser->parse($trigger['recovery_expression']);
1640
1641				$hist_functions2 = $expression_parser->getResult()->getTokensOfTypes(
1642					[CExpressionParserResult::TOKEN_TYPE_HIST_FUNCTION]
1643				);
1644			}
1645
1646			$triggers_functions[$tnum] = [];
1647			if ($class === 'CTriggerPrototype') {
1648				$lld_ruleids = [];
1649			}
1650
1651			/*
1652			 * 0x01 - with templates
1653			 * 0x02 - with hosts
1654			 */
1655			$status_mask = 0x00;
1656			// The lists of hostids and hosts which are used in the current trigger.
1657			$hostids = [];
1658			$hosts = [];
1659
1660			// Common checks.
1661			foreach (array_merge($hist_functions1, $hist_functions2) as $hist_function) {
1662				$host = $hist_function['data']['parameters'][0]['data']['host'];
1663				$item = $hist_function['data']['parameters'][0]['data']['item'];
1664
1665				$host_keys = $hosts_keys[$host];
1666				$key = $host_keys['keys'][$item];
1667
1668				if ($host_keys['hostid'] === null) {
1669					self::exception(ZBX_API_ERROR_PARAMETERS, _params($error_wrong_host, [$host_keys['host']]));
1670				}
1671
1672				if ($key['itemid'] === null) {
1673					self::exception(ZBX_API_ERROR_PARAMETERS, _s(
1674						'Incorrect item key "%1$s" provided for trigger expression on "%2$s".', $key['key'],
1675						$host_keys['host']
1676					));
1677				}
1678
1679				if (!in_array($key['value_type'], $hist_function_value_types[$hist_function['data']['function']])) {
1680					self::exception(ZBX_API_ERROR_PARAMETERS, _s(
1681						'Incorrect item value type "%1$s" provided for trigger function "%2$s".',
1682						itemValueTypeString($key['value_type']), $hist_function['data']['function']
1683					));
1684				}
1685
1686				if (!array_key_exists($hist_function['match'], $triggers_functions[$tnum])) {
1687					$query_parameter = $hist_function['data']['parameters'][0];
1688					$parameter = substr_replace($hist_function['match'], TRIGGER_QUERY_PLACEHOLDER,
1689						$query_parameter['pos'] - $hist_function['pos'], $query_parameter['length']
1690					);
1691					$triggers_functions[$tnum][$hist_function['match']] = [
1692						'functionid' => null,
1693						'triggerid' => null,
1694						'itemid' => $key['itemid'],
1695						'name' => $hist_function['data']['function'],
1696						'parameter' => substr($parameter, strlen($hist_function['data']['function']) + 1, -1)
1697					];
1698					$functions_num++;
1699				}
1700
1701				if ($class === 'CTriggerPrototype' && $key['flags'] == ZBX_FLAG_DISCOVERY_PROTOTYPE) {
1702					$lld_ruleids[$key['lld_ruleid']] = true;
1703				}
1704
1705				$status_mask |= ($host_keys['status'] == HOST_STATUS_TEMPLATE ? 0x01 : 0x02);
1706				$hostids[$host_keys['hostid']] = true;
1707				$hosts[$host] = true;
1708			}
1709
1710			// When both templates and hosts are referenced in expressions.
1711			if ($status_mask == 0x03) {
1712				self::exception(ZBX_API_ERROR_PARAMETERS, $error_host_and_template);
1713			}
1714
1715			// Triggers with children cannot be moved from one template to another host or template.
1716			if ($class === 'CTrigger' && $db_triggers !== null && $expressions_changed) {
1717				$expression_parser->parse($db_triggers[$tnum]['expression']);
1718				$old_hosts1 = $expression_parser->getResult()->getHosts();
1719				$old_hosts2 = [];
1720
1721				if ($trigger['recovery_mode'] == ZBX_RECOVERY_MODE_RECOVERY_EXPRESSION) {
1722					$expression_parser->parse($db_triggers[$tnum]['recovery_expression']);
1723					$old_hosts2 = $expression_parser->getResult()->getHosts();
1724				}
1725
1726				$is_moved = true;
1727				foreach (array_merge($old_hosts1, $old_hosts2) as $old_host) {
1728					if (array_key_exists($old_host, $hosts)) {
1729						$is_moved = false;
1730						break;
1731					}
1732				}
1733
1734				if ($is_moved) {
1735					$moved_triggers[$db_triggers[$tnum]['triggerid']] = ['description' => $trigger['description']];
1736				}
1737			}
1738
1739			// The trigger with multiple templates.
1740			if ($status_mask == 0x01 && count($hostids) > 1) {
1741				$mt_triggers[] = [
1742					'description' => $trigger['description'],
1743					'templateids' => array_keys($hostids)
1744				];
1745			}
1746
1747			if ($class === 'CTriggerPrototype') {
1748				$lld_ruleids = array_keys($lld_ruleids);
1749
1750				if (!$lld_ruleids) {
1751					self::exception(ZBX_API_ERROR_PARAMETERS, _s(
1752						'Trigger prototype "%1$s" must contain at least one item prototype.', $trigger['description']
1753					));
1754				}
1755				elseif (count($lld_ruleids) > 1) {
1756					self::exception(ZBX_API_ERROR_PARAMETERS, _s(
1757						'Trigger prototype "%1$s" contains item prototypes from multiple discovery rules.',
1758						$trigger['description']
1759					));
1760				}
1761				elseif ($db_triggers !== null
1762						&& !idcmp($lld_ruleids[0], $db_triggers[$tnum]['discoveryRule']['itemid'])) {
1763					self::exception(ZBX_API_ERROR_PARAMETERS, _s('Cannot update trigger prototype "%1$s": %2$s.',
1764						$trigger['description'], _('trigger prototype cannot be moved to another template or host')
1765					));
1766				}
1767			}
1768		}
1769		unset($trigger);
1770
1771		if ($mt_triggers) {
1772			$this->validateTriggersWithMultipleTemplates($mt_triggers);
1773		}
1774
1775		if ($class === 'CTrigger' && $moved_triggers) {
1776			$this->validateMovedTriggers($moved_triggers);
1777		}
1778
1779		$functionid = DB::reserveIds('functions', $functions_num);
1780
1781		$expression_max_length = DB::getFieldLength('triggers', 'expression');
1782		$recovery_expression_max_length = DB::getFieldLength('triggers', 'recovery_expression');
1783
1784		// Replace func(/host/item) macros with {<functionid>}.
1785		foreach ($triggers as $tnum => &$trigger) {
1786			$expressions_changed = ($db_triggers === null
1787				|| ($trigger['expression'] !== $db_triggers[$tnum]['expression']
1788				|| $trigger['recovery_expression'] !== $db_triggers[$tnum]['recovery_expression']));
1789
1790			if (!$expressions_changed) {
1791				continue;
1792			}
1793
1794			foreach ($triggers_functions[$tnum] as &$trigger_function) {
1795				$trigger_function['functionid'] = $functionid;
1796				$functionid = bcadd($functionid, 1, 0);
1797			}
1798			unset($trigger_function);
1799
1800			$expression_parser->parse($trigger['expression']);
1801			$hist_functions = $expression_parser->getResult()->getTokensOfTypes(
1802				[CExpressionParserResult::TOKEN_TYPE_HIST_FUNCTION]
1803			);
1804			$hist_function = end($hist_functions);
1805			do {
1806				$trigger['expression'] = substr_replace($trigger['expression'],
1807					'{'.$triggers_functions[$tnum][$hist_function['match']]['functionid'].'}',
1808					$hist_function['pos'], $hist_function['length']
1809				);
1810			}
1811			while ($hist_function = prev($hist_functions));
1812
1813			if (mb_strlen($trigger['expression']) > $expression_max_length) {
1814				self::exception(ZBX_API_ERROR_PARAMETERS, _s(
1815					'Invalid parameter "%1$s": %2$s.', '/'.($tnum + 1).'/expression', _('value is too long')
1816				));
1817			}
1818
1819			if ($trigger['recovery_mode'] == ZBX_RECOVERY_MODE_RECOVERY_EXPRESSION) {
1820				$expression_parser->parse($trigger['recovery_expression']);
1821				$hist_functions = $expression_parser->getResult()->getTokensOfTypes(
1822					[CExpressionParserResult::TOKEN_TYPE_HIST_FUNCTION]
1823				);
1824				$hist_function = end($hist_functions);
1825				do {
1826					$trigger['recovery_expression'] = substr_replace($trigger['recovery_expression'],
1827						'{'.$triggers_functions[$tnum][$hist_function['match']]['functionid'].'}',
1828						$hist_function['pos'], $hist_function['length']
1829					);
1830				}
1831				while ($hist_function = prev($hist_functions));
1832
1833				if (mb_strlen($trigger['recovery_expression']) > $recovery_expression_max_length) {
1834					self::exception(ZBX_API_ERROR_PARAMETERS, _s('Invalid parameter "%1$s": %2$s.',
1835						'/'.($tnum + 1).'/recovery_expression', _('value is too long')
1836					));
1837				}
1838			}
1839		}
1840		unset($trigger);
1841	}
1842
1843	/**
1844	 * Check if all templates trigger belongs to are linked to same hosts.
1845	 *
1846	 * @param array  $mt_triggers
1847	 * @param string $mt_triggers[]['description']
1848	 * @param array  $mt_triggers[]['templateids']
1849	 *
1850	 * @throws APIException
1851	 */
1852	protected function validateTriggersWithMultipleTemplates(array $mt_triggers) {
1853		switch (get_class($this)) {
1854			case 'CTrigger':
1855				$error_different_linkages = _('Trigger "%1$s" belongs to templates with different linkages.');
1856				break;
1857
1858			case 'CTriggerPrototype':
1859				$error_different_linkages = _('Trigger prototype "%1$s" belongs to templates with different linkages.');
1860				break;
1861
1862			default:
1863				self::exception(ZBX_API_ERROR_INTERNAL, _('Internal error.'));
1864		}
1865
1866		$templateids = [];
1867
1868		foreach ($mt_triggers as $mt_trigger) {
1869			foreach ($mt_trigger['templateids'] as $templateid) {
1870				$templateids[$templateid] = true;
1871			}
1872		}
1873
1874		$templates = API::Template()->get([
1875			'output' => [],
1876			'selectHosts' => ['hostid'],
1877			'selectTemplates' => ['templateid'],
1878			'templateids' => array_keys($templateids),
1879			'nopermissions' => true,
1880			'preservekeys' => true
1881		]);
1882
1883		foreach ($templates as &$template) {
1884			$template = array_merge(
1885				zbx_objectValues($template['hosts'], 'hostid'),
1886				zbx_objectValues($template['templates'], 'templateid')
1887			);
1888		}
1889		unset($template);
1890
1891		foreach ($mt_triggers as $mt_trigger) {
1892			$compare_links = null;
1893
1894			foreach ($mt_trigger['templateids'] as $templateid) {
1895				if ($compare_links === null) {
1896					$compare_links = $templates[$templateid];
1897					continue;
1898				}
1899
1900				$linked_to = $templates[$templateid];
1901
1902				if (array_diff($compare_links, $linked_to) || array_diff($linked_to, $compare_links)) {
1903					self::exception(ZBX_API_ERROR_PARAMETERS,
1904						_params($error_different_linkages, [$mt_trigger['description']])
1905					);
1906				}
1907			}
1908		}
1909	}
1910
1911	/**
1912	 * Check if moved triggers does not have children.
1913	 *
1914	 * @param array  $moved_triggers
1915	 * @param string $moved_triggers[<triggerid>]['description']
1916	 *
1917	 * @throws APIException
1918	 */
1919	protected function validateMovedTriggers(array $moved_triggers) {
1920		$_db_triggers = DBselect(
1921			'SELECT t.templateid'.
1922			' FROM triggers t'.
1923			' WHERE '.dbConditionInt('t.templateid', array_keys($moved_triggers)),
1924			1
1925		);
1926
1927		if ($_db_trigger = DBfetch($_db_triggers)) {
1928			self::exception(ZBX_API_ERROR_PARAMETERS, _s('Cannot update trigger "%1$s": %2$s.',
1929				$moved_triggers[$_db_trigger['templateid']]['description'],
1930				_('trigger with linkages cannot be moved to another template or host')
1931			));
1932		}
1933	}
1934
1935	/**
1936	 * Adds triggers and trigger prototypes from template to hosts.
1937	 *
1938	 * @param array $data
1939	 */
1940	public function syncTemplates(array $data) {
1941		$data['templateids'] = zbx_toArray($data['templateids']);
1942		$data['hostids'] = zbx_toArray($data['hostids']);
1943
1944		$output = ['triggerid', 'description', 'expression', 'recovery_mode', 'recovery_expression', 'url', 'status',
1945			'priority', 'comments', 'type', 'correlation_mode', 'correlation_tag', 'manual_close', 'opdata',
1946			'event_name'
1947		];
1948		if ($this instanceof CTriggerPrototype) {
1949			$output[] = 'discover';
1950		}
1951
1952		$triggers = $this->get([
1953			'output' => $output,
1954			'selectTags' => ['tag', 'value'],
1955			'hostids' => $data['templateids'],
1956			'preservekeys' => true
1957		]);
1958
1959		$triggers = CMacrosResolverHelper::resolveTriggerExpressions($triggers,
1960			['sources' => ['expression', 'recovery_expression']]
1961		);
1962
1963		$this->inherit($triggers, $data['hostids']);
1964	}
1965}
1966