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