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