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 methods for operations with triggers.
24 */
25class CTrigger extends CTriggerGeneral {
26
27	public const ACCESS_RULES = [
28		'get' => ['min_user_type' => USER_TYPE_ZABBIX_USER],
29		'create' => ['min_user_type' => USER_TYPE_ZABBIX_ADMIN],
30		'update' => ['min_user_type' => USER_TYPE_ZABBIX_ADMIN],
31		'delete' => ['min_user_type' => USER_TYPE_ZABBIX_ADMIN],
32		'adddependencies' => ['min_user_type' => USER_TYPE_ZABBIX_ADMIN],
33		'deletedependencies' => ['min_user_type' => USER_TYPE_ZABBIX_ADMIN]
34	];
35
36	protected $tableName = 'triggers';
37	protected $tableAlias = 't';
38	protected $sortColumns = ['triggerid', 'description', 'status', 'priority', 'lastchange', 'hostname'];
39
40	/**
41	 * Get Triggers data.
42	 *
43	 * @param array $options
44	 * @param array $options['itemids']
45	 * @param array $options['hostids']
46	 * @param array $options['groupids']
47	 * @param array $options['triggerids']
48	 * @param array $options['status']
49	 * @param bool  $options['editable']
50	 * @param array $options['count']
51	 * @param array $options['pattern']
52	 * @param array $options['limit']
53	 * @param array $options['order']
54	 *
55	 * @return array|int item data as array or false if error
56	 */
57	public function get(array $options = []) {
58		$result = [];
59
60		$sqlParts = [
61			'select'	=> ['triggers' => 't.triggerid'],
62			'from'		=> ['t' => 'triggers t'],
63			'where'		=> [],
64			'group'		=> [],
65			'order'		=> [],
66			'limit'		=> null
67		];
68
69		$defOptions = [
70			'groupids'						=> null,
71			'templateids'					=> null,
72			'hostids'						=> null,
73			'triggerids'					=> null,
74			'itemids'						=> null,
75			'functions'						=> null,
76			'inherited'						=> null,
77			'dependent'						=> null,
78			'templated'						=> null,
79			'monitored'						=> null,
80			'active'						=> null,
81			'maintenance'					=> null,
82			'withUnacknowledgedEvents'		=> null,
83			'withAcknowledgedEvents'		=> null,
84			'withLastEventUnacknowledged'	=> null,
85			'skipDependent'					=> null,
86			'nopermissions'					=> null,
87			'editable'						=> false,
88			// timing
89			'lastChangeSince'				=> null,
90			'lastChangeTill'				=> null,
91			// filter
92			'group'							=> null,
93			'host'							=> null,
94			'only_true'						=> null,
95			'min_severity'					=> null,
96			'evaltype'						=> TAG_EVAL_TYPE_AND_OR,
97			'tags'							=> null,
98			'filter'						=> null,
99			'search'						=> null,
100			'searchByAny'					=> null,
101			'startSearch'					=> false,
102			'excludeSearch'					=> false,
103			'searchWildcardsEnabled'		=> null,
104			// output
105			'expandDescription'				=> null,
106			'expandComment'					=> null,
107			'expandExpression'				=> null,
108			'output'						=> API_OUTPUT_EXTEND,
109			'selectGroups'					=> null,
110			'selectHosts'					=> null,
111			'selectItems'					=> null,
112			'selectFunctions'				=> null,
113			'selectDependencies'			=> null,
114			'selectDiscoveryRule'			=> null,
115			'selectLastEvent'				=> null,
116			'selectTags'					=> null,
117			'selectTriggerDiscovery'		=> null,
118			'countOutput'					=> false,
119			'groupCount'					=> false,
120			'preservekeys'					=> false,
121			'sortfield'						=> '',
122			'sortorder'						=> '',
123			'limit'							=> null,
124			'limitSelects'					=> null
125		];
126		$options = zbx_array_merge($defOptions, $options);
127
128		// editable + PERMISSION CHECK
129		if (self::$userData['type'] != USER_TYPE_SUPER_ADMIN && !$options['nopermissions']) {
130			$permission = $options['editable'] ? PERM_READ_WRITE : PERM_READ;
131			$userGroups = getUserGroupsByUserId(self::$userData['userid']);
132
133			$sqlParts['where'][] = 'NOT EXISTS ('.
134				'SELECT NULL'.
135				' FROM functions f,items i,hosts_groups hgg'.
136					' LEFT JOIN rights r'.
137						' ON r.id=hgg.groupid'.
138							' AND '.dbConditionInt('r.groupid', $userGroups).
139				' WHERE t.triggerid=f.triggerid '.
140					' AND f.itemid=i.itemid'.
141					' AND i.hostid=hgg.hostid'.
142				' GROUP BY i.hostid'.
143				' HAVING MAX(permission)<'.zbx_dbstr($permission).
144					' OR MIN(permission) IS NULL'.
145					' OR MIN(permission)='.PERM_DENY.
146			')';
147		}
148
149		// groupids
150		if ($options['groupids'] !== null) {
151			zbx_value2array($options['groupids']);
152
153			sort($options['groupids']);
154
155			$sqlParts['from']['functions'] = 'functions f';
156			$sqlParts['from']['items'] = 'items i';
157			$sqlParts['from']['hosts_groups'] = 'hosts_groups hg';
158			$sqlParts['where']['hgi'] = 'hg.hostid=i.hostid';
159			$sqlParts['where']['ft'] = 'f.triggerid=t.triggerid';
160			$sqlParts['where']['fi'] = 'f.itemid=i.itemid';
161			$sqlParts['where']['groupid'] = dbConditionInt('hg.groupid', $options['groupids']);
162
163			if ($options['groupCount']) {
164				$sqlParts['group']['hg'] = 'hg.groupid';
165			}
166		}
167
168		// templateids
169		if ($options['templateids'] !== null) {
170			zbx_value2array($options['templateids']);
171
172			if ($options['hostids'] !== null) {
173				zbx_value2array($options['hostids']);
174				$options['hostids'] = array_merge($options['hostids'], $options['templateids']);
175			}
176			else {
177				$options['hostids'] = $options['templateids'];
178			}
179		}
180
181		// hostids
182		if ($options['hostids'] !== null) {
183			zbx_value2array($options['hostids']);
184
185			$sqlParts['from']['functions'] = 'functions f';
186			$sqlParts['from']['items'] = 'items i';
187			$sqlParts['where']['hostid'] = dbConditionInt('i.hostid', $options['hostids']);
188			$sqlParts['where']['ft'] = 'f.triggerid=t.triggerid';
189			$sqlParts['where']['fi'] = 'f.itemid=i.itemid';
190
191			if ($options['groupCount']) {
192				$sqlParts['group']['i'] = 'i.hostid';
193			}
194		}
195
196		// triggerids
197		if ($options['triggerids'] !== null) {
198			zbx_value2array($options['triggerids']);
199
200			$sqlParts['where']['triggerid'] = dbConditionInt('t.triggerid', $options['triggerids']);
201		}
202
203		// itemids
204		if ($options['itemids'] !== null) {
205			zbx_value2array($options['itemids']);
206
207			$sqlParts['from']['functions'] = 'functions f';
208			$sqlParts['where']['itemid'] = dbConditionInt('f.itemid', $options['itemids']);
209			$sqlParts['where']['ft'] = 'f.triggerid=t.triggerid';
210
211			if ($options['groupCount']) {
212				$sqlParts['group']['f'] = 'f.itemid';
213			}
214		}
215
216		// functions
217		if ($options['functions'] !== null) {
218			zbx_value2array($options['functions']);
219
220			$sqlParts['from']['functions'] = 'functions f';
221			$sqlParts['where']['ft'] = 'f.triggerid=t.triggerid';
222			$sqlParts['where'][] = dbConditionString('f.name', $options['functions']);
223		}
224
225		// monitored
226		if ($options['monitored'] !== null) {
227			$sqlParts['where']['monitored'] = 'NOT EXISTS ('.
228					'SELECT NULL'.
229					' FROM functions f,items i,hosts h'.
230					' WHERE t.triggerid=f.triggerid'.
231						' AND f.itemid=i.itemid'.
232						' AND i.hostid=h.hostid'.
233						' AND ('.
234							'i.status<>'.ITEM_STATUS_ACTIVE.
235							' OR h.status<>'.HOST_STATUS_MONITORED.
236						')'.
237					')';
238			$sqlParts['where']['status'] = 't.status='.TRIGGER_STATUS_ENABLED;
239		}
240
241		// active
242		if ($options['active'] !== null) {
243			$sqlParts['where']['active'] = 'NOT EXISTS ('.
244					'SELECT NULL'.
245					' FROM functions f,items i,hosts h'.
246					' WHERE t.triggerid=f.triggerid'.
247						' AND f.itemid=i.itemid'.
248						' AND i.hostid=h.hostid'.
249						' AND h.status<>'.HOST_STATUS_MONITORED.
250					')';
251			$sqlParts['where']['status'] = 't.status='.TRIGGER_STATUS_ENABLED;
252		}
253
254		// maintenance
255		if ($options['maintenance'] !== null) {
256			$sqlParts['where'][] = ($options['maintenance'] == 0 ? 'NOT ' : '').
257					'EXISTS ('.
258						'SELECT NULL'.
259						' FROM functions f,items i,hosts h'.
260						' WHERE t.triggerid=f.triggerid'.
261							' AND f.itemid=i.itemid'.
262							' AND i.hostid=h.hostid'.
263							' AND h.maintenance_status='.HOST_MAINTENANCE_STATUS_ON.
264					')';
265			$sqlParts['where'][] = 't.status='.TRIGGER_STATUS_ENABLED;
266		}
267
268		// lastChangeSince
269		if ($options['lastChangeSince'] !== null) {
270			$sqlParts['where']['lastchangesince'] = 't.lastchange>'.zbx_dbstr($options['lastChangeSince']);
271		}
272
273		// lastChangeTill
274		if ($options['lastChangeTill'] !== null) {
275			$sqlParts['where']['lastchangetill'] = 't.lastchange<'.zbx_dbstr($options['lastChangeTill']);
276		}
277
278		// withUnacknowledgedEvents
279		if ($options['withUnacknowledgedEvents'] !== null) {
280			$sqlParts['where']['unack'] = 'EXISTS ('.
281					'SELECT NULL'.
282					' FROM events e'.
283					' WHERE t.triggerid=e.objectid'.
284						' AND e.source='.EVENT_SOURCE_TRIGGERS.
285						' AND e.object='.EVENT_OBJECT_TRIGGER.
286						' AND e.value='.TRIGGER_VALUE_TRUE.
287						' AND e.acknowledged='.EVENT_NOT_ACKNOWLEDGED.
288					')';
289		}
290
291		// withAcknowledgedEvents
292		if ($options['withAcknowledgedEvents'] !== null) {
293			$sqlParts['where']['ack'] = 'NOT EXISTS ('.
294					'SELECT NULL'.
295					' FROM events e'.
296					' WHERE e.objectid=t.triggerid'.
297						' AND e.source='.EVENT_SOURCE_TRIGGERS.
298						' AND e.object='.EVENT_OBJECT_TRIGGER.
299						' AND e.value='.TRIGGER_VALUE_TRUE.
300						' AND e.acknowledged='.EVENT_NOT_ACKNOWLEDGED.
301					')';
302		}
303
304		// templated
305		if ($options['templated'] !== null) {
306			$sqlParts['from']['functions'] = 'functions f';
307			$sqlParts['from']['items'] = 'items i';
308			$sqlParts['from']['hosts'] = 'hosts h';
309			$sqlParts['where']['ft'] = 'f.triggerid=t.triggerid';
310			$sqlParts['where']['fi'] = 'f.itemid=i.itemid';
311			$sqlParts['where']['hi'] = 'h.hostid=i.hostid';
312
313			if ($options['templated']) {
314				$sqlParts['where'][] = 'h.status='.HOST_STATUS_TEMPLATE;
315			}
316			else {
317				$sqlParts['where'][] = 'h.status<>'.HOST_STATUS_TEMPLATE;
318			}
319		}
320
321		// inherited
322		if ($options['inherited'] !== null) {
323			if ($options['inherited']) {
324				$sqlParts['where'][] = 't.templateid IS NOT NULL';
325			}
326			else {
327				$sqlParts['where'][] = 't.templateid IS NULL';
328			}
329		}
330
331		// dependent
332		if ($options['dependent'] !== null) {
333			if ($options['dependent']) {
334				$sqlParts['where'][] = 'EXISTS ('.
335					'SELECT NULL'.
336					' FROM trigger_depends td'.
337					' WHERE td.triggerid_down=t.triggerid'.
338				')';
339			}
340			else {
341				$sqlParts['where'][] = 'NOT EXISTS ('.
342					'SELECT NULL'.
343					' FROM trigger_depends td'.
344					' WHERE td.triggerid_down=t.triggerid'.
345				')';
346			}
347		}
348
349		// search
350		if (is_array($options['search'])) {
351			zbx_db_search('triggers t', $options, $sqlParts);
352		}
353
354		// filter
355		if ($options['filter'] === null) {
356			$options['filter'] = [];
357		}
358
359		if (is_array($options['filter'])) {
360			if (!array_key_exists('flags', $options['filter'])) {
361				$options['filter']['flags'] = [
362					ZBX_FLAG_DISCOVERY_NORMAL,
363					ZBX_FLAG_DISCOVERY_CREATED
364				];
365			}
366
367			$this->dbFilter('triggers t', $options, $sqlParts);
368
369			if (array_key_exists('host', $options['filter']) && $options['filter']['host'] !== null) {
370				zbx_value2array($options['filter']['host']);
371
372				$sqlParts['from']['functions'] = 'functions f';
373				$sqlParts['from']['items'] = 'items i';
374				$sqlParts['where']['ft'] = 'f.triggerid=t.triggerid';
375				$sqlParts['where']['fi'] = 'f.itemid=i.itemid';
376				$sqlParts['from']['hosts'] = 'hosts h';
377				$sqlParts['where']['hi'] = 'h.hostid=i.hostid';
378				$sqlParts['where']['host'] = dbConditionString('h.host', $options['filter']['host']);
379			}
380
381			if (array_key_exists('hostid', $options['filter']) && $options['filter']['hostid'] !== null) {
382				zbx_value2array($options['filter']['hostid']);
383
384				$sqlParts['from']['functions'] = 'functions f';
385				$sqlParts['from']['items'] = 'items i';
386				$sqlParts['where']['ft'] = 'f.triggerid=t.triggerid';
387				$sqlParts['where']['fi'] = 'f.itemid=i.itemid';
388				$sqlParts['where']['hostid'] = dbConditionInt('i.hostid', $options['filter']['hostid']);
389			}
390		}
391
392		// group
393		if ($options['group'] !== null) {
394			$sqlParts['from']['functions'] = 'functions f';
395			$sqlParts['from']['items'] = 'items i';
396			$sqlParts['from']['hosts_groups'] = 'hosts_groups hg';
397			$sqlParts['from']['hstgrp'] = 'hstgrp g';
398			$sqlParts['where']['ft'] = 'f.triggerid=t.triggerid';
399			$sqlParts['where']['fi'] = 'f.itemid=i.itemid';
400			$sqlParts['where']['hgi'] = 'hg.hostid=i.hostid';
401			$sqlParts['where']['ghg'] = 'g.groupid = hg.groupid';
402			$sqlParts['where']['group'] = ' g.name='.zbx_dbstr($options['group']);
403		}
404
405		// host
406		if ($options['host'] !== null) {
407			$sqlParts['from']['functions'] = 'functions f';
408			$sqlParts['from']['items'] = 'items i';
409			$sqlParts['from']['hosts'] = 'hosts h';
410			$sqlParts['where']['ft'] = 'f.triggerid=t.triggerid';
411			$sqlParts['where']['fi'] = 'f.itemid=i.itemid';
412			$sqlParts['where']['hi'] = 'h.hostid=i.hostid';
413			$sqlParts['where']['host'] = ' h.host='.zbx_dbstr($options['host']);
414		}
415
416		// only_true
417		if ($options['only_true'] !== null) {
418			$sqlParts['where']['ot'] = '((t.value='.TRIGGER_VALUE_TRUE.')'.
419				' OR ((t.value='.TRIGGER_VALUE_FALSE.')'.
420					' AND (t.lastchange>'.
421					(time() - timeUnitToSeconds(CSettingsHelper::get(CSettingsHelper::OK_PERIOD))).
422				'))'.
423			')';
424		}
425
426		// min_severity
427		if ($options['min_severity'] !== null) {
428			$sqlParts['where'][] = 't.priority>='.zbx_dbstr($options['min_severity']);
429		}
430
431		// tags
432		if ($options['tags'] !== null && $options['tags']) {
433			$sqlParts['where'][] = CApiTagHelper::addWhereCondition($options['tags'], $options['evaltype'], 't',
434				'trigger_tag', 'triggerid'
435			);
436		}
437
438		// limit
439		if (!zbx_ctype_digit($options['limit']) || !$options['limit']) {
440			$options['limit'] = null;
441		}
442
443		$sqlParts = $this->applyQueryOutputOptions($this->tableName(), $this->tableAlias(), $options, $sqlParts);
444		$sqlParts = $this->applyQuerySortOptions($this->tableName(), $this->tableAlias(), $options, $sqlParts);
445
446		// return count or grouped counts via direct SQL count
447		if ($options['countOutput'] && !$this->requiresPostSqlFiltering($options)) {
448			$dbRes = DBselect(self::createSelectQueryFromParts($sqlParts), $options['limit']);
449			while ($trigger = DBfetch($dbRes)) {
450				if ($options['groupCount']) {
451					$result[] = $trigger;
452				}
453				else {
454					$result = $trigger['rowscount'];
455				}
456			}
457			return $result;
458		}
459
460		$result = zbx_toHash($this->customFetch(self::createSelectQueryFromParts($sqlParts), $options), 'triggerid');
461
462		// return count for post SQL filtered result sets
463		if ($options['countOutput']) {
464			return (string) count($result);
465		}
466
467		if ($result) {
468			$result = $this->addRelatedObjects($options, $result);
469		}
470
471		// expandDescription
472		if ($options['expandDescription'] !== null && $result && array_key_exists('description', reset($result))) {
473			$result = CMacrosResolverHelper::resolveTriggerNames($result);
474		}
475
476		// expandComment
477		if ($options['expandComment'] !== null && $result && array_key_exists('comments', reset($result))) {
478			$result = CMacrosResolverHelper::resolveTriggerDescriptions($result, ['sources' => ['comments']]);
479		}
480
481		// expand expressions
482		if ($options['expandExpression'] !== null && $result) {
483			$sources = [];
484			if (array_key_exists('expression', reset($result))) {
485				$sources[] = 'expression';
486			}
487			if (array_key_exists('recovery_expression', reset($result))) {
488				$sources[] = 'recovery_expression';
489			}
490
491			if ($sources) {
492				$result = CMacrosResolverHelper::resolveTriggerExpressions($result,
493					['resolve_usermacros' => true, 'resolve_macros' => true, 'sources' => $sources]
494				);
495			}
496		}
497
498		// removing keys (hash -> array)
499		if (!$options['preservekeys']) {
500			$result = zbx_cleanHashes($result);
501		}
502
503		$result = $this->unsetExtraFields($result, ['state', 'expression'], $options['output']);
504
505		// Triggers share table with trigger prototypes. Therefore remove trigger unrelated fields.
506		if ($this->outputIsRequested('discover', $options['output'])) {
507			foreach ($result as &$row) {
508				unset($row['discover']);
509			}
510			unset($row);
511		}
512
513		return $result;
514	}
515
516	/**
517	 * Add triggers.
518	 *
519	 * Trigger params: expression, description, type, priority, status, comments, url, templateid
520	 *
521	 * @param array $triggers
522	 *
523	 * @return array
524	 */
525	public function create(array $triggers) {
526		$this->validateCreate($triggers);
527		$this->createReal($triggers);
528		$this->inherit($triggers);
529
530		// Clear all dependencies on inherited triggers.
531		$this->deleteDependencies($triggers);
532
533		// Add new dependencies.
534		foreach ($triggers as $trigger) {
535			if (!array_key_exists('dependencies', $trigger) || !$trigger['dependencies']) {
536				continue;
537			}
538
539			$new_dependencies = [];
540			foreach ($trigger['dependencies'] as $dependency) {
541				$new_dependencies[] = [
542					'triggerid' => $trigger['triggerid'],
543					'dependsOnTriggerid' => $dependency['triggerid']
544				];
545			}
546			$this->addDependencies($new_dependencies);
547		}
548
549		return ['triggerids' => zbx_objectValues($triggers, 'triggerid')];
550	}
551
552	/**
553	 * Update triggers.
554	 *
555	 * If a trigger expression is passed in any of the triggers, it must be in it's exploded form.
556	 *
557	 * @param array $triggers
558	 *
559	 * @return array
560	 */
561	public function update(array $triggers) {
562		$this->validateUpdate($triggers, $db_triggers);
563
564		$validate_dependencies = [];
565		foreach ($triggers as $tnum => $trigger) {
566			$db_trigger = $db_triggers[$tnum];
567
568			$expressions_changed = ($trigger['expression'] !== $db_trigger['expression']
569					|| $trigger['recovery_expression'] !== $db_trigger['recovery_expression']);
570
571			if ($expressions_changed && $db_trigger['dependencies'] && !array_key_exists('dependencies', $trigger)) {
572				$validate_dependencies[] = [
573					'triggerid' => $trigger['triggerid'],
574					'dependencies' => zbx_objectValues($db_trigger['dependencies'], 'triggerid')
575				];
576			}
577		}
578
579		if ($validate_dependencies) {
580			$this->checkDependencies($validate_dependencies);
581			$this->checkDependencyParents($validate_dependencies);
582		}
583
584		$this->updateReal($triggers, $db_triggers);
585		$this->inherit($triggers);
586
587		foreach ($triggers as $trigger) {
588			// Replace dependencies.
589			if (array_key_exists('dependencies', $trigger)) {
590				$this->deleteDependencies($trigger);
591
592				if ($trigger['dependencies']) {
593					$new_dependencies = [];
594					foreach ($trigger['dependencies'] as $dependency) {
595						$new_dependencies[] = [
596							'triggerid' => $trigger['triggerid'],
597							'dependsOnTriggerid' => $dependency['triggerid']
598						];
599					}
600					$this->addDependencies($new_dependencies);
601				}
602			}
603		}
604
605		return ['triggerids' => zbx_objectValues($triggers, 'triggerid')];
606	}
607
608	/**
609	 * Delete triggers.
610	 *
611	 * @param array $triggerids
612	 *
613	 * @return array
614	 */
615	public function delete(array $triggerids) {
616		$this->validateDelete($triggerids, $db_triggers);
617
618		CTriggerManager::delete($triggerids);
619
620		$this->addAuditBulk(AUDIT_ACTION_DELETE, AUDIT_RESOURCE_TRIGGER, $db_triggers);
621
622		return ['triggerids' => $triggerids];
623	}
624
625	/**
626	 * Validates the input parameters for the delete() method.
627	 *
628	 * @param array $triggerids   [IN/OUT]
629	 * @param array $db_triggers  [OUT]
630	 *
631	 * @throws APIException if the input is invalid.
632	 */
633	protected function validateDelete(array &$triggerids, array &$db_triggers = null) {
634		$api_input_rules = ['type' => API_IDS, 'flags' => API_NOT_EMPTY, 'uniq' => true];
635		if (!CApiInputValidator::validate($api_input_rules, $triggerids, '/', $error)) {
636			self::exception(ZBX_API_ERROR_PARAMETERS, $error);
637		}
638
639		$db_triggers = $this->get([
640			'output' => ['triggerid', 'description', 'expression', 'templateid'],
641			'triggerids' => $triggerids,
642			'editable' => true,
643			'preservekeys' => true
644		]);
645
646		foreach ($triggerids as $triggerid) {
647			if (!array_key_exists($triggerid, $db_triggers)) {
648				self::exception(ZBX_API_ERROR_PERMISSIONS,
649					_('No permissions to referred object or it does not exist!')
650				);
651			}
652
653			$db_trigger = $db_triggers[$triggerid];
654
655			if ($db_trigger['templateid'] != 0) {
656				self::exception(ZBX_API_ERROR_PARAMETERS,
657					_s('Cannot delete templated trigger "%1$s:%2$s".', $db_trigger['description'],
658						CMacrosResolverHelper::resolveTriggerExpression($db_trigger['expression'])
659					)
660				);
661			}
662		}
663	}
664
665	/**
666	 * Validates the input for the addDependencies() method.
667	 *
668	 * @param array $triggers_data
669	 * @param bool  $inherited
670	 *
671	 * @throws APIException if the given dependencies are invalid.
672	 */
673	protected function validateAddDependencies(array &$triggers_data, $inherited = false) {
674		$api_input_rules = ['type' => API_OBJECTS, 'flags' => API_NOT_EMPTY | API_NORMALIZE, 'uniq' => [['triggerid', 'dependsOnTriggerid']], 'fields' => [
675			'triggerid' =>			['type' => API_ID, 'flags' => API_REQUIRED],
676			'dependsOnTriggerid' =>	['type' => API_ID, 'flags' => API_REQUIRED]
677		]];
678		if (!CApiInputValidator::validate($api_input_rules, $triggers_data, '/', $error)) {
679			self::exception(ZBX_API_ERROR_PARAMETERS, $error);
680		}
681
682		$triggerids = zbx_objectValues($triggers_data, 'triggerid');
683		$triggerids = array_keys(array_flip($triggerids));
684
685		$permission_check = $inherited
686			? ['nopermissions' => true]
687			: ['editable' => true];
688
689		$triggers = $this->get([
690			'output' => ['triggerid', 'description', 'flags'],
691			'triggerids' => $triggerids,
692			'preservekeys' => true
693		] + $permission_check);
694
695		if (count($triggerids) != count($triggers)) {
696			self::exception(ZBX_API_ERROR_PERMISSIONS, _('No permissions to referred object or it does not exist!'));
697		}
698
699		foreach ($triggers as $trigger) {
700			if ($trigger['flags'] == ZBX_FLAG_DISCOVERY_CREATED) {
701				self::exception(ZBX_API_ERROR_PERMISSIONS, _s('Cannot update "%2$s" for a discovered trigger "%1$s".',
702					$trigger['description'], 'dependencies'
703				));
704			}
705		}
706
707		$dep_triggerids = [];
708		$triggers = [];
709		foreach ($triggers_data as $dep) {
710			$triggerid = $dep['triggerid'];
711
712			if (!array_key_exists($dep['triggerid'], $triggers)) {
713				$triggers[$triggerid] = [
714					'triggerid' => $triggerid,
715					'dependencies' => []
716				];
717			}
718			$triggers[$triggerid]['dependencies'][] = $dep['dependsOnTriggerid'];
719			$dep_triggerids[$dep['dependsOnTriggerid']] = $dep['dependsOnTriggerid'];
720		}
721
722		if (!$inherited) {
723			$count = $this->get([
724				'countOutput' => true,
725				'triggerids' => $dep_triggerids
726			]);
727
728			if ($count != count($dep_triggerids)) {
729				self::exception(ZBX_API_ERROR_PERMISSIONS,
730					_('No permissions to referred object or it does not exist!')
731				);
732			}
733		}
734
735		$this->checkDependencies($triggers);
736		$this->checkDependencyParents($triggers);
737		$this->checkDependencyDuplicates($triggers);
738	}
739
740	/**
741	 * Add the given dependencies and inherit them on all child triggers.
742	 *
743	 * @param array $triggers_data  An array of trigger dependency pairs, each pair in the form of
744	 *                              ['triggerid' => 1, 'dependsOnTriggerid' => 2].
745	 * @param bool  $inherited      Determines either to check permissions for added dependencies. Permissions are not
746	 *                              validated for inherited triggers.
747	 *
748	 * @return array
749	 */
750	public function addDependencies(array $triggers_data, $inherited = false) {
751		$this->validateAddDependencies($triggers_data, $inherited);
752
753		foreach ($triggers_data as $dep) {
754			$triggerId = $dep['triggerid'];
755			$depTriggerId = $dep['dependsOnTriggerid'];
756
757			DB::insert('trigger_depends', [[
758				'triggerid_down' => $triggerId,
759				'triggerid_up' => $depTriggerId
760			]]);
761
762			// propagate the dependencies to the child triggers
763			$childTriggers = API::getApiService()->select($this->tableName(), [
764				'output' => ['triggerid'],
765				'filter' => [
766					'templateid' => $triggerId
767				]
768			]);
769			if ($childTriggers) {
770				foreach ($childTriggers as $childTrigger) {
771					$childHostsQuery = get_hosts_by_triggerid($childTrigger['triggerid']);
772					while ($childHost = DBfetch($childHostsQuery)) {
773						$newDep = [$childTrigger['triggerid'] => $depTriggerId];
774						$newDep = replace_template_dependencies($newDep, $childHost['hostid']);
775
776						$this->addDependencies([[
777							'triggerid' => $childTrigger['triggerid'],
778							'dependsOnTriggerid' => $newDep[$childTrigger['triggerid']]
779						]], true);
780					}
781				}
782			}
783		}
784
785		return ['triggerids' => array_unique(zbx_objectValues($triggers_data, 'triggerid'))];
786	}
787
788	/**
789	 * Validates the input for the deleteDependencies() method.
790	 *
791	 * @param array $triggers
792	 * @param bool  $inherited
793	 *
794	 * @throws APIException if the given input is invalid
795	 */
796	protected function validateDeleteDependencies(array $triggers, $inherited) {
797		if (!$triggers) {
798			self::exception(ZBX_API_ERROR_PARAMETERS, _('Empty input parameter.'));
799		}
800
801		foreach ($triggers as $trigger) {
802			if (!check_db_fields(['triggerid' => null], $trigger)) {
803				self::exception(ZBX_API_ERROR_PARAMETERS, _('Incorrect input parameters.'));
804			}
805		}
806
807		$triggerids = zbx_objectValues($triggers, 'triggerid');
808		$triggerids = array_keys(array_flip($triggerids));
809
810		$permission_check = $inherited
811			? ['nopermissions' => true]
812			: ['editable' => true];
813
814		$triggers = $this->get([
815			'output' => ['triggerid', 'description', 'flags'],
816			'triggerids' => $triggerids,
817			'preservekeys' => true
818		] + $permission_check);
819
820		foreach ($triggerids as $triggerid) {
821			if (!array_key_exists($triggerid, $triggers)) {
822				self::exception(ZBX_API_ERROR_PERMISSIONS,
823					_('No permissions to referred object or it does not exist!')
824				);
825			}
826		}
827
828		foreach ($triggers as $trigger) {
829			if ($trigger['flags'] == ZBX_FLAG_DISCOVERY_CREATED) {
830				self::exception(ZBX_API_ERROR_PERMISSIONS, _s('Cannot update "%2$s" for a discovered trigger "%1$s".',
831					$trigger['description'], 'dependencies'
832				));
833			}
834		}
835	}
836
837	/**
838	 * Deletes all trigger dependencies from the given triggers and their children.
839	 *
840	 * @param array $triggers   an array of triggers with the 'triggerid' field defined
841	 * @param bool  $inherited  Determines either to check permissions for deleted dependencies. Permissions are not
842	 *                          validated for inherited triggers.
843	 *
844	 * @return array
845	 */
846	public function deleteDependencies(array $triggers, $inherited = false) {
847		$triggers = zbx_toArray($triggers);
848
849		$this->validateDeleteDependencies($triggers, $inherited);
850
851		$triggerids = zbx_objectValues($triggers, 'triggerid');
852
853		try {
854			// delete the dependencies from the child triggers
855			$childTriggers = DB::select($this->tableName(), [
856				'output' => ['triggerid'],
857				'filter' => [
858					'templateid' => $triggerids
859				]
860			]);
861			if ($childTriggers) {
862				$this->deleteDependencies($childTriggers, true);
863			}
864
865			DB::delete('trigger_depends', [
866				'triggerid_down' => $triggerids
867			]);
868		}
869		catch (APIException $e) {
870			self::exception(ZBX_API_ERROR_PARAMETERS, _('Cannot delete dependency'));
871		}
872
873		return ['triggerids' => $triggerids];
874	}
875
876	/**
877	 * Synchronizes the templated trigger dependencies on the given hosts inherited from the given
878	 * templates.
879	 * Update dependencies, do it after all triggers that can be dependent were created/updated on
880	 * all child hosts/templates. Starting from highest level template triggers select triggers from
881	 * one level lower, then for each lower trigger look if it's parent has dependencies, if so
882	 * find this dependency trigger child on dependent trigger host and add new dependency.
883	 *
884	 * @param array $data
885	 */
886	public function syncTemplateDependencies(array $data) {
887		$templateIds = zbx_toArray($data['templateids']);
888		$hostIds = zbx_toArray($data['hostids']);
889
890		$parentTriggers = $this->get([
891			'output' => ['triggerid'],
892			'hostids' => $templateIds,
893			'preservekeys' => true,
894			'selectDependencies' => ['triggerid']
895		]);
896
897		if ($parentTriggers) {
898			$childTriggers = $this->get([
899				'output' => ['triggerid', 'templateid'],
900				'hostids' => ($hostIds) ? $hostIds : null,
901				'filter' => ['templateid' => array_keys($parentTriggers)],
902				'nopermissions' => true,
903				'preservekeys' => true,
904				'selectHosts' => ['hostid']
905			]);
906
907			if ($childTriggers) {
908				$newDependencies = [];
909				foreach ($childTriggers as $childTrigger) {
910					$parentDependencies = $parentTriggers[$childTrigger['templateid']]['dependencies'];
911					if ($parentDependencies) {
912						$dependencies = [];
913						foreach ($parentDependencies as $depTrigger) {
914							$dependencies[] = $depTrigger['triggerid'];
915						}
916						$host = reset($childTrigger['hosts']);
917						$dependencies = replace_template_dependencies($dependencies, $host['hostid']);
918						foreach ($dependencies as $depTriggerId) {
919							$newDependencies[] = [
920								'triggerid' => $childTrigger['triggerid'],
921								'dependsOnTriggerid' => $depTriggerId
922							];
923						}
924					}
925				}
926				$this->deleteDependencies($childTriggers);
927
928				if ($newDependencies) {
929					$this->addDependencies($newDependencies);
930				}
931			}
932		}
933	}
934
935	/**
936	 * Validates the dependencies of the given triggers.
937	 *
938	 * @param array $triggers list of triggers and corresponding dependencies
939	 * @param int $triggers[]['triggerid'] trigger id
940	 * @param array $triggers[]['dependencies'] list of trigger ids on which depends given trigger
941	 *
942	 * @trows APIException if any of the dependencies is invalid
943	 */
944	protected function checkDependencies(array $triggers) {
945		foreach ($triggers as $trigger) {
946			if (empty($trigger['dependencies'])) {
947				continue;
948			}
949
950			// trigger templates
951			$triggerTemplates = API::Template()->get([
952				'output' => ['status', 'hostid'],
953				'triggerids' => $trigger['triggerid'],
954				'nopermissions' => true
955			]);
956
957			// forbid dependencies from hosts to templates
958			if (!$triggerTemplates) {
959				$triggerDependencyTemplates = API::Template()->get([
960					'output' => ['templateid'],
961					'triggerids' => $trigger['dependencies'],
962					'nopermissions' => true,
963					'limit' => 1
964				]);
965				if ($triggerDependencyTemplates) {
966					self::exception(ZBX_API_ERROR_PARAMETERS, _('Cannot add dependency from a host to a template.'));
967				}
968			}
969
970			// the trigger can't depend on itself
971			if (in_array($trigger['triggerid'], $trigger['dependencies'])) {
972				self::exception(ZBX_API_ERROR_PARAMETERS, _('Cannot create dependency on trigger itself.'));
973			}
974
975			// check circular dependency
976			$downTriggerIds = [$trigger['triggerid']];
977			do {
978				// triggerid_down depends on triggerid_up
979				$res = DBselect(
980					'SELECT td.triggerid_up'.
981					' FROM trigger_depends td'.
982					' WHERE '.dbConditionInt('td.triggerid_down', $downTriggerIds)
983				);
984
985				// combine db dependencies with those to be added
986				$upTriggersIds = [];
987				while ($row = DBfetch($res)) {
988					$upTriggersIds[] = $row['triggerid_up'];
989				}
990				foreach ($downTriggerIds as $id) {
991					if (isset($triggers[$id]) && isset($triggers[$id]['dependencies'])) {
992						$upTriggersIds = array_merge($upTriggersIds, $triggers[$id]['dependencies']);
993					}
994				}
995
996				// if found trigger id is in dependent triggerids, there is a dependency loop
997				$downTriggerIds = [];
998				foreach ($upTriggersIds as $id) {
999					if (bccomp($id, $trigger['triggerid']) == 0) {
1000						self::exception(ZBX_API_ERROR_PARAMETERS, _('Cannot create circular dependencies.'));
1001					}
1002					$downTriggerIds[] = $id;
1003				}
1004			} while (!empty($downTriggerIds));
1005
1006			// fetch all templates that are used in dependencies
1007			$triggerDependencyTemplates = API::Template()->get([
1008				'output' => ['templateid'],
1009				'triggerids' => $trigger['dependencies'],
1010				'nopermissions' => true
1011			]);
1012			$depTemplateIds = zbx_toHash(zbx_objectValues($triggerDependencyTemplates, 'templateid'));
1013
1014			// run the check only if a templated trigger has dependencies on other templates
1015			$triggerTemplateIds = zbx_toHash(zbx_objectValues($triggerTemplates, 'templateid'));
1016			$tdiff = array_diff($depTemplateIds, $triggerTemplateIds);
1017			if (!empty($triggerTemplateIds) && !empty($depTemplateIds) && !empty($tdiff)) {
1018				$affectedTemplateIds = zbx_array_merge($triggerTemplateIds, $depTemplateIds);
1019
1020				// create a list of all hosts, that are children of the affected templates
1021				$dbLowlvltpl = DBselect(
1022					'SELECT DISTINCT ht.templateid,ht.hostid,h.host'.
1023					' FROM hosts_templates ht,hosts h'.
1024					' WHERE h.hostid=ht.hostid'.
1025						' AND '.dbConditionInt('ht.templateid', $affectedTemplateIds)
1026				);
1027				$map = [];
1028				while ($lowlvltpl = DBfetch($dbLowlvltpl)) {
1029					if (!isset($map[$lowlvltpl['hostid']])) {
1030						$map[$lowlvltpl['hostid']] = [];
1031					}
1032					$map[$lowlvltpl['hostid']][$lowlvltpl['templateid']] = $lowlvltpl['host'];
1033				}
1034
1035				// check that if some host is linked to the template, that the trigger belongs to,
1036				// the host must also be linked to all of the templates, that trigger dependencies point to
1037				foreach ($map as $templates) {
1038					foreach ($triggerTemplateIds as $triggerTemplateId) {
1039						// is the host linked to one of the trigger templates?
1040						if (isset($templates[$triggerTemplateId])) {
1041							// then make sure all of the dependency templates are also linked
1042							foreach ($depTemplateIds as $depTemplateId) {
1043								if (!isset($templates[$depTemplateId])) {
1044									self::exception(ZBX_API_ERROR_PARAMETERS,
1045										_s('Not all templates are linked to "%1$s".', reset($templates))
1046									);
1047								}
1048							}
1049							break;
1050						}
1051					}
1052				}
1053			}
1054		}
1055	}
1056
1057	/**
1058	 * Check that none of the triggers have dependencies on their children. Checks only one level of inheritance, but
1059	 * since it is called on each inheritance step, also works for multiple inheritance levels.
1060	 *
1061	 * @throws APIException     if at least one trigger is dependent on its child
1062	 *
1063	 * @param array $triggers
1064	 */
1065	protected function checkDependencyParents(array $triggers) {
1066		// fetch all templated dependency trigger parents
1067		$depTriggerIds = [];
1068		foreach ($triggers as $trigger) {
1069			foreach ($trigger['dependencies'] as $depTriggerId) {
1070				$depTriggerIds[$depTriggerId] = $depTriggerId;
1071			}
1072		}
1073		$parentDepTriggers = DBfetchArray(DBSelect(
1074			'SELECT templateid,triggerid'.
1075			' FROM triggers'.
1076			' WHERE templateid>0'.
1077				' AND '.dbConditionInt('triggerid', $depTriggerIds)
1078		));
1079		if ($parentDepTriggers) {
1080			$parentDepTriggers = zbx_toHash($parentDepTriggers, 'triggerid');
1081			foreach ($triggers as $trigger) {
1082				foreach ($trigger['dependencies'] as $depTriggerId) {
1083					// check if the current trigger is the parent of the dependency trigger
1084					if (isset($parentDepTriggers[$depTriggerId])
1085							&& $parentDepTriggers[$depTriggerId]['templateid'] == $trigger['triggerid']) {
1086
1087						self::exception(ZBX_API_ERROR_PARAMETERS,
1088							_s('Trigger cannot be dependent on a trigger that is inherited from it.')
1089						);
1090					}
1091				}
1092			}
1093		}
1094	}
1095
1096	/**
1097	 * Checks if the given dependencies contain duplicates.
1098	 *
1099	 * @throws APIException if the given dependencies contain duplicates
1100	 *
1101	 * @param array $triggers
1102	 */
1103	protected function checkDependencyDuplicates(array $triggers) {
1104		// check duplicates in array
1105		$uniqueTriggers = [];
1106		$duplicateTriggerId = null;
1107		foreach ($triggers as $trigger) {
1108			foreach ($trigger['dependencies'] as $dep) {
1109				if (isset($uniqueTriggers[$trigger['triggerid']][$dep])) {
1110					$duplicateTriggerId = $trigger['triggerid'];
1111					break 2;
1112				}
1113				else {
1114					$uniqueTriggers[$trigger['triggerid']][$dep] = 1;
1115				}
1116			}
1117		}
1118
1119		if ($duplicateTriggerId === null) {
1120			// check if dependency already exists in DB
1121			foreach ($triggers as $trigger) {
1122				$dbUpTriggers = DBselect(
1123					'SELECT td.triggerid_up'.
1124					' FROM trigger_depends td'.
1125					' WHERE '.dbConditionInt('td.triggerid_up', $trigger['dependencies']).
1126					' AND td.triggerid_down='.zbx_dbstr($trigger['triggerid'])
1127				, 1);
1128				if (DBfetch($dbUpTriggers)) {
1129					$duplicateTriggerId = $trigger['triggerid'];
1130					break;
1131				}
1132			}
1133		}
1134
1135		if ($duplicateTriggerId) {
1136			$dplTrigger = DBfetch(DBselect(
1137				'SELECT t.description'.
1138				' FROM triggers t'.
1139				' WHERE t.triggerid='.zbx_dbstr($duplicateTriggerId)
1140			));
1141			self::exception(ZBX_API_ERROR_PARAMETERS,
1142				_s('Duplicate dependencies in trigger "%1$s".', $dplTrigger['description'])
1143			);
1144		}
1145	}
1146
1147	protected function applyQueryOutputOptions($tableName, $tableAlias, array $options, array $sqlParts) {
1148		$sqlParts = parent::applyQueryOutputOptions($tableName, $tableAlias, $options, $sqlParts);
1149
1150		if (!$options['countOutput'] && $options['expandDescription'] !== null) {
1151			$sqlParts = $this->addQuerySelect($this->fieldId('expression'), $sqlParts);
1152		}
1153
1154		return $sqlParts;
1155	}
1156
1157	protected function addRelatedObjects(array $options, array $result) {
1158		$result = parent::addRelatedObjects($options, $result);
1159
1160		if (!$result) {
1161			return $result;
1162		}
1163
1164		$triggerids = array_keys($result);
1165
1166		// adding trigger dependencies
1167		if ($options['selectDependencies'] !== null && $options['selectDependencies'] != API_OUTPUT_COUNT) {
1168			$dependencies = [];
1169			$relationMap = new CRelationMap();
1170			$res = DBselect(
1171				'SELECT td.triggerid_up,td.triggerid_down'.
1172				' FROM trigger_depends td'.
1173				' WHERE '.dbConditionInt('td.triggerid_down', $triggerids)
1174			);
1175			while ($relation = DBfetch($res)) {
1176				$relationMap->addRelation($relation['triggerid_down'], $relation['triggerid_up']);
1177			}
1178
1179			$related_ids = $relationMap->getRelatedIds();
1180
1181			if ($related_ids) {
1182				$dependencies = $this->get([
1183					'output' => $options['selectDependencies'],
1184					'triggerids' => $related_ids,
1185					'preservekeys' => true
1186				]);
1187			}
1188
1189			$result = $relationMap->mapMany($result, $dependencies, 'dependencies');
1190		}
1191
1192		// adding items
1193		if ($options['selectItems'] !== null && $options['selectItems'] != API_OUTPUT_COUNT) {
1194			$relationMap = $this->createRelationMap($result, 'triggerid', 'itemid', 'functions');
1195			$items = API::Item()->get([
1196				'output' => $options['selectItems'],
1197				'itemids' => $relationMap->getRelatedIds(),
1198				'webitems' => true,
1199				'nopermissions' => true,
1200				'preservekeys' => true
1201			]);
1202			$result = $relationMap->mapMany($result, $items, 'items');
1203		}
1204
1205		// adding discoveryrule
1206		if ($options['selectDiscoveryRule'] !== null && $options['selectDiscoveryRule'] != API_OUTPUT_COUNT) {
1207			$discoveryRules = [];
1208			$relationMap = new CRelationMap();
1209			$dbRules = DBselect(
1210				'SELECT id.parent_itemid,td.triggerid'.
1211				' FROM trigger_discovery td,item_discovery id,functions f'.
1212				' WHERE '.dbConditionInt('td.triggerid', $triggerids).
1213					' AND td.parent_triggerid=f.triggerid'.
1214					' AND f.itemid=id.itemid'
1215			);
1216			while ($rule = DBfetch($dbRules)) {
1217				$relationMap->addRelation($rule['triggerid'], $rule['parent_itemid']);
1218			}
1219
1220			$related_ids = $relationMap->getRelatedIds();
1221
1222			if ($related_ids) {
1223				$discoveryRules = API::DiscoveryRule()->get([
1224					'output' => $options['selectDiscoveryRule'],
1225					'itemids' => $related_ids,
1226					'nopermissions' => true,
1227					'preservekeys' => true
1228				]);
1229			}
1230
1231			$result = $relationMap->mapOne($result, $discoveryRules, 'discoveryRule');
1232		}
1233
1234		// adding last event
1235		if ($options['selectLastEvent'] !== null) {
1236			foreach ($result as $triggerId => $trigger) {
1237				$result[$triggerId]['lastEvent'] = [];
1238			}
1239
1240			if (is_array($options['selectLastEvent'])) {
1241				$pkFieldId = $this->pk('events');
1242				$outputFields = [
1243					'objectid' => $this->fieldId('objectid', 'e'),
1244					'ns' => $this->fieldId('ns', 'e'),
1245					$pkFieldId => $this->fieldId($pkFieldId, 'e')
1246				];
1247
1248				foreach ($options['selectLastEvent'] as $field) {
1249					if ($this->hasField($field, 'events')) {
1250						$outputFields[$field] = $this->fieldId($field, 'e');
1251					}
1252				}
1253
1254				$outputFields = implode(',', $outputFields);
1255			}
1256			else {
1257				$outputFields = 'e.*';
1258			}
1259
1260			// Due to performance issues, avoid using 'ORDER BY' for outer SELECT.
1261			$dbEvents = DBselect(
1262				'SELECT '.$outputFields.
1263				' FROM events e'.
1264					' JOIN ('.
1265						'SELECT e2.source,e2.object,e2.objectid,MAX(clock) AS clock'.
1266						' FROM events e2'.
1267						' WHERE e2.source='.EVENT_SOURCE_TRIGGERS.
1268							' AND e2.object='.EVENT_OBJECT_TRIGGER.
1269							' AND '.dbConditionInt('e2.objectid', $triggerids).
1270						' GROUP BY e2.source,e2.object,e2.objectid'.
1271					') e3 ON e3.source=e.source'.
1272						' AND e3.object=e.object'.
1273						' AND e3.objectid=e.objectid'.
1274						' AND e3.clock=e.clock'
1275			);
1276
1277			// in case there are multiple records with same 'clock' for one trigger, we'll get different 'ns'
1278			$lastEvents = [];
1279
1280			while ($dbEvent = DBfetch($dbEvents)) {
1281				$triggerId = $dbEvent['objectid'];
1282				$ns = $dbEvent['ns'];
1283
1284				// unset fields, that were not requested
1285				if (is_array($options['selectLastEvent'])) {
1286					if (!in_array('objectid', $options['selectLastEvent'])) {
1287						unset($dbEvent['objectid']);
1288					}
1289					if (!in_array('ns', $options['selectLastEvent'])) {
1290						unset($dbEvent['ns']);
1291					}
1292				}
1293
1294				$lastEvents[$triggerId][$ns] = $dbEvent;
1295			}
1296
1297			foreach ($lastEvents as $triggerId => $events) {
1298				// find max 'ns' for each trigger and that will be the 'lastEvent'
1299				$maxNs = max(array_keys($events));
1300				$result[$triggerId]['lastEvent'] = $events[$maxNs];
1301			}
1302		}
1303
1304		// adding trigger discovery
1305		if ($options['selectTriggerDiscovery'] !== null && $options['selectTriggerDiscovery'] !== API_OUTPUT_COUNT) {
1306			foreach ($result as &$trigger) {
1307				$trigger['triggerDiscovery'] = [];
1308			}
1309			unset($trigger);
1310
1311			$sql_select = ['triggerid'];
1312			foreach (['parent_triggerid', 'ts_delete'] as $field) {
1313				if ($this->outputIsRequested($field, $options['selectTriggerDiscovery'])) {
1314					$sql_select[] = $field;
1315				}
1316			}
1317
1318			$trigger_discoveries = DBselect(
1319				'SELECT '.implode(',', $sql_select).
1320				' FROM trigger_discovery'.
1321				' WHERE '.dbConditionInt('triggerid', $triggerids)
1322			);
1323
1324			while ($trigger_discovery = DBfetch($trigger_discoveries)) {
1325				$triggerid = $trigger_discovery['triggerid'];
1326				unset($trigger_discovery['triggerid']);
1327
1328				$result[$triggerid]['triggerDiscovery'] = $trigger_discovery;
1329			}
1330		}
1331
1332		return $result;
1333	}
1334
1335	protected function applyQuerySortField($sortfield, $sortorder, $alias, array $sqlParts) {
1336		if ($sortfield === 'hostname') {
1337			$sqlParts['select']['hostname'] = 'h.name AS hostname';
1338			$sqlParts['from']['functions'] = 'functions f';
1339			$sqlParts['from']['items'] = 'items i';
1340			$sqlParts['from']['hosts'] = 'hosts h';
1341			$sqlParts['where'][] = 't.triggerid = f.triggerid';
1342			$sqlParts['where'][] = 'f.itemid = i.itemid';
1343			$sqlParts['where'][] = 'i.hostid = h.hostid';
1344			$sqlParts['order'][] = 'h.name '.$sortorder;
1345		}
1346		else {
1347			$sqlParts = parent::applyQuerySortField($sortfield, $sortorder, $alias, $sqlParts);
1348		}
1349
1350		return $sqlParts;
1351	}
1352
1353	protected function requiresPostSqlFiltering(array $options) {
1354		return $options['skipDependent'] !== null || $options['withLastEventUnacknowledged'] !== null;
1355	}
1356
1357	protected function applyPostSqlFiltering(array $triggers, array $options) {
1358		$triggers = zbx_toHash($triggers, 'triggerid');
1359
1360		// unset triggers which depend on at least one problem trigger upstream into dependency tree
1361		if ($options['skipDependent'] !== null) {
1362			// Result trigger IDs of all triggers in results.
1363			$resultTriggerIds = zbx_objectValues($triggers, 'triggerid');
1364
1365			// Will contain IDs of all triggers on which some other trigger depends.
1366			$allUpTriggerIds = [];
1367
1368			// Trigger dependency map.
1369			$downToUpTriggerIds = [];
1370
1371			// Values (state) of each "up" trigger ID is stored in here.
1372			$upTriggerValues = [];
1373
1374			// Will contain IDs of all triggers either disabled directly, or by having disabled item or disabled host.
1375			$disabledTriggerIds = [];
1376
1377			// First loop uses result trigger IDs.
1378			$triggerIds = $resultTriggerIds;
1379			do {
1380				// Fetch all dependency records where "down" trigger IDs are in current iteration trigger IDs.
1381				$dbResult = DBselect(
1382					'SELECT d.triggerid_down,d.triggerid_up,t.value'.
1383					' FROM trigger_depends d,triggers t'.
1384					' WHERE d.triggerid_up=t.triggerid'.
1385					' AND '.dbConditionInt('d.triggerid_down', $triggerIds)
1386				);
1387
1388				// Add trigger IDs as keys and empty arrays as values.
1389				$downToUpTriggerIds = $downToUpTriggerIds + array_fill_keys($triggerIds, []);
1390
1391				$triggerIds = [];
1392				while ($dependency = DBfetch($dbResult)) {
1393					// Trigger ID for "down" trigger, which has dependencies.
1394					$downTriggerId = $dependency['triggerid_down'];
1395
1396					// Trigger ID for "up" trigger, on which the other ("up") trigger depends.
1397					$upTriggerId = $dependency['triggerid_up'];
1398
1399					// Add "up" trigger ID to mapping. We also index by $upTrigger because later these arrays
1400					// are combined with + and this way indexes and values do not break.
1401					$downToUpTriggerIds[$downTriggerId][$upTriggerId] = $upTriggerId;
1402
1403					// Add ID of this "up" trigger to all known "up" triggers.
1404					$allUpTriggerIds[] = $upTriggerId;
1405
1406					// Remember value of this "up" trigger.
1407					$upTriggerValues[$upTriggerId] = $dependency['value'];
1408
1409					// Add ID of this "up" trigger to the list of trigger IDs which should be mapped.
1410					$triggerIds[] = $upTriggerId;
1411				}
1412			} while ($triggerIds);
1413
1414			// Fetch trigger IDs for triggers that are disabled, have disabled items or disabled item hosts.
1415			$dbResult = DBSelect(
1416				'SELECT t.triggerid'.
1417				' FROM triggers t,functions f,items i,hosts h'.
1418				' WHERE t.triggerid=f.triggerid'.
1419				' AND f.itemid=i.itemid'.
1420				' AND i.hostid=h.hostid'.
1421				' AND ('.
1422				'i.status='.ITEM_STATUS_DISABLED.
1423				' OR h.status='.HOST_STATUS_NOT_MONITORED.
1424				' OR t.status='.TRIGGER_STATUS_DISABLED.
1425				')'.
1426				' AND '.dbConditionInt('t.triggerid', $allUpTriggerIds)
1427			);
1428			while ($row = DBfetch($dbResult)) {
1429				$resultTriggerId = $row['triggerid'];
1430				$disabledTriggerIds[$resultTriggerId] = $resultTriggerId;
1431			}
1432
1433			// Now process all mapped dependencies and unset any disabled "up" triggers so they do not participate in
1434			// decisions regarding nesting resolution in next step.
1435			foreach ($downToUpTriggerIds as $downTriggerId => $upTriggerIds) {
1436				$upTriggerIdsToUnset = [];
1437				foreach ($upTriggerIds as $upTriggerId) {
1438					if (isset($disabledTriggerIds[$upTriggerId])) {
1439						unset($downToUpTriggerIds[$downTriggerId][$upTriggerId]);
1440					}
1441				}
1442			}
1443
1444			// Resolve dependencies for all result set triggers.
1445			foreach ($resultTriggerIds as $resultTriggerId) {
1446				// We start with result trigger.
1447				$triggerIds = [$resultTriggerId];
1448
1449				// This also is unrolled recursive function and is repeated until there are no more trigger IDs to
1450				// check, add and resolve.
1451				do {
1452					$nextTriggerIds = [];
1453					foreach ($triggerIds as $triggerId) {
1454						// Loop through all "up" triggers.
1455						foreach ($downToUpTriggerIds[$triggerId] as $upTriggerId) {
1456							if ($downToUpTriggerIds[$upTriggerId]) {
1457								// If there this "up" trigger has "up" triggers of it's own, merge them and proceed with recursion.
1458								$downToUpTriggerIds[$resultTriggerId] += $downToUpTriggerIds[$upTriggerId];
1459
1460								// Add trigger ID to be processed in next loop iteration.
1461								$nextTriggerIds[] = $upTriggerId;
1462							}
1463						}
1464					}
1465					$triggerIds = $nextTriggerIds;
1466				} while ($triggerIds);
1467			}
1468
1469			// Clean result set.
1470			foreach ($resultTriggerIds as $resultTriggerId) {
1471				foreach ($downToUpTriggerIds[$resultTriggerId] as $upTriggerId) {
1472					// If "up" trigger is in problem state, dependent trigger should not be returned and is removed
1473					// from results.
1474					if ($upTriggerValues[$upTriggerId] == TRIGGER_VALUE_TRUE) {
1475						unset($triggers[$resultTriggerId]);
1476					}
1477				}
1478
1479				// Check if result trigger is disabled and if so, remove from results.
1480				if (isset($disabledTriggerIds[$resultTriggerId])) {
1481					unset($triggers[$resultTriggerId]);
1482				}
1483			}
1484		}
1485
1486		// withLastEventUnacknowledged
1487		if ($options['withLastEventUnacknowledged'] !== null) {
1488			$triggerIds = zbx_objectValues($triggers, 'triggerid');
1489			$eventIds = [];
1490			$eventsDb = DBselect(
1491				'SELECT MAX(e.eventid) AS eventid,e.objectid'.
1492					' FROM events e'.
1493					' WHERE e.object='.EVENT_OBJECT_TRIGGER.
1494					' AND e.source='.EVENT_SOURCE_TRIGGERS.
1495					' AND '.dbConditionInt('e.objectid', $triggerIds).
1496					' AND '.dbConditionInt('e.value', [TRIGGER_VALUE_TRUE]).
1497					' GROUP BY e.objectid'
1498			);
1499			while ($event = DBfetch($eventsDb)) {
1500				$eventIds[] = $event['eventid'];
1501			}
1502
1503			$correctTriggerIds = DBfetchArrayAssoc(DBselect(
1504				'SELECT e.objectid'.
1505					' FROM events e '.
1506					' WHERE '.dbConditionInt('e.eventid', $eventIds).
1507					' AND e.acknowledged=0'
1508			), 'objectid');
1509
1510			foreach ($triggers as $triggerId => $trigger) {
1511				if (!isset($correctTriggerIds[$triggerId])) {
1512					unset($triggers[$triggerId]);
1513				}
1514			}
1515		}
1516
1517		return $triggers;
1518	}
1519}
1520