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