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 trigger prototypes.
24 */
25class CTriggerPrototype extends CTriggerGeneral {
26
27	protected $tableName = 'triggers';
28	protected $tableAlias = 't';
29	protected $sortColumns = ['triggerid', 'description', 'status', 'priority', 'discover'];
30
31	/**
32	 * Get trigger prototypes from database.
33	 *
34	 * @param array $options
35	 *
36	 * @return array|int
37	 */
38	public function get(array $options = []) {
39		$result = [];
40
41		$sqlParts = [
42			'select'	=> ['triggers' => 't.triggerid'],
43			'from'		=> ['t' => 'triggers t'],
44			'where'		=> ['t.flags='.ZBX_FLAG_DISCOVERY_PROTOTYPE],
45			'group'		=> [],
46			'order'		=> [],
47			'limit'		=> null
48		];
49
50		$defOptions = [
51			'groupids'						=> null,
52			'templateids'					=> null,
53			'hostids'						=> null,
54			'triggerids'					=> null,
55			'itemids'						=> null,
56			'applicationids'				=> null,
57			'discoveryids'					=> null,
58			'functions'						=> null,
59			'inherited'						=> null,
60			'templated'						=> null,
61			'monitored' 					=> null,
62			'active' 						=> null,
63			'maintenance'					=> null,
64			'nopermissions'					=> null,
65			'editable'						=> false,
66			// filter
67			'group'							=> null,
68			'host'							=> null,
69			'min_severity'					=> null,
70			'filter'						=> null,
71			'search'						=> null,
72			'searchByAny'					=> null,
73			'startSearch'					=> false,
74			'excludeSearch'					=> false,
75			'searchWildcardsEnabled'		=> null,
76			// output
77			'expandExpression'				=> null,
78			'output'						=> API_OUTPUT_EXTEND,
79			'selectGroups'					=> null,
80			'selectHosts'					=> null,
81			'selectItems'					=> null,
82			'selectFunctions'				=> null,
83			'selectDependencies'			=> null,
84			'selectDiscoveryRule'			=> null,
85			'selectTags'					=> null,
86			'countOutput'					=> false,
87			'groupCount'					=> false,
88			'preservekeys'					=> false,
89			'sortfield'						=> '',
90			'sortorder'						=> '',
91			'limit'							=> null,
92			'limitSelects'					=> null
93		];
94		$options = zbx_array_merge($defOptions, $options);
95
96		// editable + permission check
97		if (self::$userData['type'] != USER_TYPE_SUPER_ADMIN && !$options['nopermissions']) {
98			$permission = $options['editable'] ? PERM_READ_WRITE : PERM_READ;
99			$userGroups = getUserGroupsByUserId(self::$userData['userid']);
100
101			$sqlParts['where'][] = 'NOT EXISTS ('.
102				'SELECT NULL'.
103				' FROM functions f,items i,hosts_groups hgg'.
104					' LEFT JOIN rights r'.
105						' ON r.id=hgg.groupid'.
106							' AND '.dbConditionInt('r.groupid', $userGroups).
107				' WHERE t.triggerid=f.triggerid'.
108					' AND f.itemid=i.itemid'.
109					' AND i.hostid=hgg.hostid'.
110				' GROUP BY i.hostid'.
111				' HAVING MAX(permission)<'.zbx_dbstr($permission).
112					' OR MIN(permission) IS NULL'.
113					' OR MIN(permission)='.PERM_DENY.
114			')';
115		}
116
117		// groupids
118		if ($options['groupids'] !== null) {
119			zbx_value2array($options['groupids']);
120
121			$sqlParts['from']['functions'] = 'functions f';
122			$sqlParts['from']['items'] = 'items i';
123			$sqlParts['from']['hosts_groups'] = 'hosts_groups hg';
124			$sqlParts['where']['hgi'] = 'hg.hostid=i.hostid';
125			$sqlParts['where']['ft'] = 'f.triggerid=t.triggerid';
126			$sqlParts['where']['fi'] = 'f.itemid=i.itemid';
127			$sqlParts['where']['groupid'] = dbConditionInt('hg.groupid', $options['groupids']);
128
129			if ($options['groupCount']) {
130				$sqlParts['group']['hg'] = 'hg.groupid';
131			}
132		}
133
134		// templateids
135		if ($options['templateids'] !== null) {
136			zbx_value2array($options['templateids']);
137
138			if ($options['hostids'] !== null) {
139				zbx_value2array($options['hostids']);
140				$options['hostids'] = array_merge($options['hostids'], $options['templateids']);
141			}
142			else {
143				$options['hostids'] = $options['templateids'];
144			}
145		}
146
147		// hostids
148		if ($options['hostids'] !== null) {
149			zbx_value2array($options['hostids']);
150
151			$sqlParts['from']['functions'] = 'functions f';
152			$sqlParts['from']['items'] = 'items i';
153			$sqlParts['where']['hostid'] = dbConditionInt('i.hostid', $options['hostids']);
154			$sqlParts['where']['ft'] = 'f.triggerid=t.triggerid';
155			$sqlParts['where']['fi'] = 'f.itemid=i.itemid';
156
157			if ($options['groupCount']) {
158				$sqlParts['group']['i'] = 'i.hostid';
159			}
160		}
161
162		// triggerids
163		if ($options['triggerids'] !== null) {
164			zbx_value2array($options['triggerids']);
165
166			$sqlParts['where']['triggerid'] = dbConditionInt('t.triggerid', $options['triggerids']);
167		}
168
169		// itemids
170		if ($options['itemids'] !== null) {
171			zbx_value2array($options['itemids']);
172
173			$sqlParts['from']['functions'] = 'functions f';
174			$sqlParts['where']['itemid'] = dbConditionInt('f.itemid', $options['itemids']);
175			$sqlParts['where']['ft'] = 'f.triggerid=t.triggerid';
176
177			if ($options['groupCount']) {
178				$sqlParts['group']['f'] = 'f.itemid';
179			}
180		}
181
182		// applicationids
183		if ($options['applicationids'] !== null) {
184			zbx_value2array($options['applicationids']);
185
186			$sqlParts['from']['functions'] = 'functions f';
187			$sqlParts['from']['items'] = 'items i';
188			$sqlParts['from']['applications'] = 'applications a';
189			$sqlParts['where']['a'] = dbConditionInt('a.applicationid', $options['applicationids']);
190			$sqlParts['where']['ia'] = 'i.hostid=a.hostid';
191			$sqlParts['where']['ft'] = 'f.triggerid=t.triggerid';
192			$sqlParts['where']['fi'] = 'f.itemid=i.itemid';
193		}
194
195		// discoveryids
196		if ($options['discoveryids'] !== null) {
197			zbx_value2array($options['discoveryids']);
198
199			$sqlParts['from']['functions'] = 'functions f';
200			$sqlParts['from']['item_discovery'] = 'item_discovery id';
201			$sqlParts['where']['fid'] = 'f.itemid=id.itemid';
202			$sqlParts['where']['ft'] = 'f.triggerid=t.triggerid';
203			$sqlParts['where'][] = dbConditionInt('id.parent_itemid', $options['discoveryids']);
204
205			if ($options['groupCount']) {
206				$sqlParts['group']['id'] = 'id.parent_itemid';
207			}
208		}
209
210		// functions
211		if ($options['functions'] !== null) {
212			zbx_value2array($options['functions']);
213
214			$sqlParts['from']['functions'] = 'functions f';
215			$sqlParts['where']['ft'] = 'f.triggerid=t.triggerid';
216			$sqlParts['where'][] = dbConditionString('f.name', $options['functions']);
217		}
218
219		// monitored
220		if ($options['monitored'] !== null) {
221			$sqlParts['where']['monitored'] =
222				' NOT EXISTS ('.
223					' SELECT NULL'.
224					' FROM functions ff'.
225					' WHERE ff.triggerid=t.triggerid'.
226						' AND EXISTS ('.
227								' SELECT NULL'.
228								' FROM items ii,hosts hh'.
229								' WHERE ff.itemid=ii.itemid'.
230									' AND hh.hostid=ii.hostid'.
231									' AND ('.
232										' ii.status<>'.ITEM_STATUS_ACTIVE.
233										' OR hh.status<>'.HOST_STATUS_MONITORED.
234									' )'.
235						' )'.
236				' )';
237			$sqlParts['where']['status'] = 't.status='.TRIGGER_STATUS_ENABLED;
238		}
239
240		// active
241		if ($options['active'] !== null) {
242			$sqlParts['where']['active'] =
243				' NOT EXISTS ('.
244					' SELECT NULL'.
245					' FROM functions ff'.
246					' WHERE ff.triggerid=t.triggerid'.
247						' AND EXISTS ('.
248							' SELECT NULL'.
249							' FROM items ii,hosts hh'.
250							' WHERE ff.itemid=ii.itemid'.
251								' AND hh.hostid=ii.hostid'.
252								' AND  hh.status<>'.HOST_STATUS_MONITORED.
253						' )'.
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 ff'.
264					' WHERE ff.triggerid=t.triggerid'.
265						' AND EXISTS ('.
266								' SELECT NULL'.
267								' FROM items ii,hosts hh'.
268								' WHERE ff.itemid=ii.itemid'.
269									' AND hh.hostid=ii.hostid'.
270									' AND hh.maintenance_status=1'.
271						' )'.
272				' )';
273			$sqlParts['where'][] = 't.status='.TRIGGER_STATUS_ENABLED;
274		}
275
276		// templated
277		if ($options['templated'] !== null) {
278			$sqlParts['from']['functions'] = 'functions f';
279			$sqlParts['from']['items'] = 'items i';
280			$sqlParts['from']['hosts'] = 'hosts h';
281			$sqlParts['where']['ft'] = 'f.triggerid=t.triggerid';
282			$sqlParts['where']['fi'] = 'f.itemid=i.itemid';
283			$sqlParts['where']['hi'] = 'h.hostid=i.hostid';
284
285			if ($options['templated']) {
286				$sqlParts['where'][] = 'h.status='.HOST_STATUS_TEMPLATE;
287			}
288			else {
289				$sqlParts['where'][] = 'h.status<>'.HOST_STATUS_TEMPLATE;
290			}
291		}
292
293		// inherited
294		if ($options['inherited'] !== null) {
295			if ($options['inherited']) {
296				$sqlParts['where'][] = 't.templateid IS NOT NULL';
297			}
298			else {
299				$sqlParts['where'][] = 't.templateid IS NULL';
300			}
301		}
302
303		// search
304		if (is_array($options['search'])) {
305			zbx_db_search('triggers t', $options, $sqlParts);
306		}
307
308		// filter
309		if (is_array($options['filter'])) {
310			$this->dbFilter('triggers t', $options, $sqlParts);
311
312			if (isset($options['filter']['host']) && $options['filter']['host'] !== null) {
313				zbx_value2array($options['filter']['host']);
314
315				$sqlParts['from']['functions'] = 'functions f';
316				$sqlParts['from']['items'] = 'items i';
317				$sqlParts['where']['ft'] = 'f.triggerid=t.triggerid';
318				$sqlParts['where']['fi'] = 'f.itemid=i.itemid';
319
320				$sqlParts['from']['hosts'] = 'hosts h';
321				$sqlParts['where']['hi'] = 'h.hostid=i.hostid';
322				$sqlParts['where']['host'] = dbConditionString('h.host', $options['filter']['host']);
323			}
324
325			if (isset($options['filter']['hostid']) && $options['filter']['hostid'] !== null) {
326				zbx_value2array($options['filter']['hostid']);
327
328				$sqlParts['from']['functions'] = 'functions f';
329				$sqlParts['from']['items'] = 'items i';
330				$sqlParts['where']['ft'] = 'f.triggerid=t.triggerid';
331				$sqlParts['where']['fi'] = 'f.itemid=i.itemid';
332
333				$sqlParts['where']['hostid'] = dbConditionInt('i.hostid', $options['filter']['hostid']);
334			}
335		}
336
337		// group
338		if ($options['group'] !== null) {
339			$sqlParts['from']['functions'] = 'functions f';
340			$sqlParts['from']['items'] = 'items i';
341			$sqlParts['from']['hosts_groups'] = 'hosts_groups hg';
342			$sqlParts['from']['hstgrp'] = 'hstgrp g';
343			$sqlParts['where']['ft'] = 'f.triggerid=t.triggerid';
344			$sqlParts['where']['fi'] = 'f.itemid=i.itemid';
345			$sqlParts['where']['hgi'] = 'hg.hostid=i.hostid';
346			$sqlParts['where']['ghg'] = 'g.groupid=hg.groupid';
347			$sqlParts['where']['group'] = ' g.name='.zbx_dbstr($options['group']);
348		}
349
350		// host
351		if ($options['host'] !== null) {
352			$sqlParts['from']['functions'] = 'functions f';
353			$sqlParts['from']['items'] = 'items i';
354			$sqlParts['from']['hosts'] = 'hosts h';
355			$sqlParts['where']['i'] = dbConditionInt('i.hostid', $options['hostids']);
356			$sqlParts['where']['ft'] = 'f.triggerid=t.triggerid';
357			$sqlParts['where']['fi'] = 'f.itemid=i.itemid';
358			$sqlParts['where']['hi'] = 'h.hostid=i.hostid';
359			$sqlParts['where']['host'] = ' h.host='.zbx_dbstr($options['host']);
360		}
361
362		// min_severity
363		if ($options['min_severity'] !== null) {
364			$sqlParts['where'][] = 't.priority>='.zbx_dbstr($options['min_severity']);
365		}
366
367		// limit
368		if (zbx_ctype_digit($options['limit']) && $options['limit']) {
369			$sqlParts['limit'] = $options['limit'];
370		}
371
372		$sqlParts = $this->applyQueryOutputOptions($this->tableName(), $this->tableAlias(), $options, $sqlParts);
373		$sqlParts = $this->applyQuerySortOptions($this->tableName(), $this->tableAlias(), $options, $sqlParts);
374		$dbRes = DBselect(self::createSelectQueryFromParts($sqlParts), $sqlParts['limit']);
375		while ($triggerPrototype = DBfetch($dbRes)) {
376			if ($options['countOutput']) {
377				if ($options['groupCount']) {
378					$result[] = $triggerPrototype;
379				}
380				else {
381					$result = $triggerPrototype['rowscount'];
382				}
383			}
384			else {
385				$result[$triggerPrototype['triggerid']] = $triggerPrototype;
386			}
387		}
388
389		if ($options['countOutput']) {
390			return $result;
391		}
392
393		if ($result) {
394			$result = $this->addRelatedObjects($options, $result);
395		}
396
397		// expand expressions
398		if ($options['expandExpression'] !== null && $result) {
399			$sources = [];
400			if (array_key_exists('expression', reset($result))) {
401				$sources[] = 'expression';
402			}
403			if (array_key_exists('recovery_expression', reset($result))) {
404				$sources[] = 'recovery_expression';
405			}
406
407			if ($sources) {
408				$result = CMacrosResolverHelper::resolveTriggerExpressions($result,
409					['resolve_usermacros' => true, 'resolve_macros' => true, 'sources' => $sources]
410				);
411			}
412		}
413
414		// removing keys (hash -> array)
415		if (!$options['preservekeys']) {
416			$result = zbx_cleanHashes($result);
417		}
418
419		return $result;
420	}
421
422	/**
423	 * Create new trigger prototypes.
424	 *
425	 * @param array $trigger_prototypes
426	 *
427	 * @return array
428	 */
429	public function create(array $trigger_prototypes) {
430		$this->validateCreate($trigger_prototypes);
431		$this->createReal($trigger_prototypes);
432		$this->inherit($trigger_prototypes);
433
434		$addDependencies = false;
435
436		foreach ($trigger_prototypes as $trigger_prototype) {
437			if (isset($trigger_prototype['dependencies']) && is_array($trigger_prototype['dependencies'])
438					&& $trigger_prototype['dependencies']) {
439				$addDependencies = true;
440				break;
441			}
442		}
443
444		if ($addDependencies) {
445			$this->addDependencies($trigger_prototypes);
446		}
447
448		return ['triggerids' => zbx_objectValues($trigger_prototypes, 'triggerid')];
449	}
450
451	/**
452	 * Update existing trigger prototypes.
453	 *
454	 * @param array $trigger_prototypes
455	 *
456	 * @return array
457	 */
458	public function update(array $trigger_prototypes) {
459		$this->validateUpdate($trigger_prototypes, $db_triggers);
460		$this->updateReal($trigger_prototypes, $db_triggers);
461		$this->inherit($trigger_prototypes);
462
463		$updateDependencies = false;
464
465		foreach ($trigger_prototypes as $trigger_prototype) {
466			if (isset($trigger_prototype['dependencies']) && is_array($trigger_prototype['dependencies'])) {
467				$updateDependencies = true;
468				break;
469			}
470		}
471
472		if ($updateDependencies) {
473			$this->updateDependencies($trigger_prototypes);
474		}
475
476		return ['triggerids' => zbx_objectValues($trigger_prototypes, 'triggerid')];
477	}
478
479	/**
480	 * Delete existing trigger prototypes.
481	 *
482	 * @param array $triggerids
483	 *
484	 * @throws APIException
485	 *
486	 * @return array
487	 */
488	public function delete(array $triggerids) {
489		$this->validateDelete($triggerids, $db_triggers);
490
491		CTriggerPrototypeManager::delete($triggerids);
492
493		$this->addAuditBulk(AUDIT_ACTION_DELETE, AUDIT_RESOURCE_TRIGGER_PROTOTYPE, $db_triggers);
494
495		return ['triggerids' => $triggerids];
496	}
497
498	/**
499	 * Validates the input parameters for the delete() method.
500	 *
501	 * @param array $triggerids   [IN/OUT]
502	 * @param array $db_triggers  [OUT]
503	 *
504	 * @throws APIException if the input is invalid.
505	 */
506	protected function validateDelete(array &$triggerids, array &$db_triggers = null) {
507		$api_input_rules = ['type' => API_IDS, 'flags' => API_NOT_EMPTY, 'uniq' => true];
508		if (!CApiInputValidator::validate($api_input_rules, $triggerids, '/', $error)) {
509			self::exception(ZBX_API_ERROR_PARAMETERS, $error);
510		}
511
512		$db_triggers = $this->get([
513			'output' => ['triggerid', 'description', 'expression', 'templateid'],
514			'triggerids' => $triggerids,
515			'editable' => true,
516			'preservekeys' => true
517		]);
518
519		foreach ($triggerids as $triggerid) {
520			if (!array_key_exists($triggerid, $db_triggers)) {
521				self::exception(ZBX_API_ERROR_PERMISSIONS,
522					_('No permissions to referred object or it does not exist!')
523				);
524			}
525
526			$db_trigger = $db_triggers[$triggerid];
527
528			if ($db_trigger['templateid'] != 0) {
529				self::exception(ZBX_API_ERROR_PARAMETERS,
530					_s('Cannot delete templated trigger prototype "%1$s:%2$s".', $db_trigger['description'],
531						CMacrosResolverHelper::resolveTriggerExpression($db_trigger['expression'])
532					)
533				);
534			}
535		}
536	}
537
538	/**
539	 * Update the given dependencies and inherit them on all child triggers.
540	 *
541	 * @param array $triggerPrototypes
542	 */
543	protected function updateDependencies(array $triggerPrototypes) {
544		$this->deleteDependencies($triggerPrototypes);
545
546		$this->addDependencies($triggerPrototypes);
547	}
548
549	/**
550	 * Deletes all trigger and trigger prototype dependencies from the given trigger prototypes and their children.
551	 *
552	 * @param array  $triggerPrototypes
553	 * @param string $triggerPrototypes[]['triggerid']
554	 */
555	protected function deleteDependencies(array $triggerPrototypes) {
556		$triggerPrototypeIds = zbx_objectValues($triggerPrototypes, 'triggerid');
557
558		try {
559			// Delete the dependencies from the child trigger prototypes.
560
561			$childTriggerPrototypes = API::getApiService()->select($this->tableName(), [
562				'output' => ['triggerid'],
563				'filter' => [
564					'templateid' => $triggerPrototypeIds
565				]
566			]);
567
568			if ($childTriggerPrototypes) {
569				$this->deleteDependencies($childTriggerPrototypes);
570			}
571
572			DB::delete('trigger_depends', [
573				'triggerid_down' => $triggerPrototypeIds
574			]);
575		}
576		catch (APIException $e) {
577			self::exception(ZBX_API_ERROR_PARAMETERS, _('Cannot delete dependency'));
578		}
579	}
580
581	/**
582	 * Add the given dependencies and inherit them on all child triggers.
583	 *
584	 * @param array  $triggerPrototypes
585	 * @param string $triggerPrototypes[]['triggerid']
586	 * @param array  $triggerPrototypes[]['dependencies']
587	 * @param string $triggerPrototypes[]['dependencies'][]['triggerid']
588	 * @param bool   $inherited                                           Determines either to check permissions for
589	 *                                                                    added dependencies. Permissions are not
590	 *                                                                    validated for inherited triggers.
591	 */
592	public function addDependencies(array $triggerPrototypes, bool $inherited = false) {
593		$this->validateAddDependencies($triggerPrototypes, $inherited);
594
595		$insert = [];
596
597		foreach ($triggerPrototypes as $triggerPrototype) {
598			if (!array_key_exists('dependencies', $triggerPrototype)) {
599				continue;
600			}
601
602			foreach ($triggerPrototype['dependencies'] as $dependency) {
603				$insert[] = [
604					'triggerid_down' => $triggerPrototype['triggerid'],
605					'triggerid_up' => $dependency['triggerid']
606				];
607			}
608		}
609
610		DB::insertBatch('trigger_depends', $insert);
611
612		foreach ($triggerPrototypes as $triggerPrototype) {
613			// Propagate the dependencies to the child triggers.
614
615			$childTriggers = API::getApiService()->select($this->tableName(), [
616				'output' => ['triggerid'],
617				'filter' => [
618					'templateid' => $triggerPrototype['triggerid']
619				]
620			]);
621
622			if ($childTriggers) {
623				foreach ($childTriggers as &$childTrigger) {
624					$childTrigger['dependencies'] = [];
625					$childHostsQuery = get_hosts_by_triggerid($childTrigger['triggerid']);
626
627					while ($childHost = DBfetch($childHostsQuery)) {
628						foreach ($triggerPrototype['dependencies'] as $dependency) {
629							$newDependency = [$childTrigger['triggerid'] => $dependency['triggerid']];
630							$newDependency = replace_template_dependencies($newDependency, $childHost['hostid']);
631
632							$childTrigger['dependencies'][] = [
633								'triggerid' => $newDependency[$childTrigger['triggerid']]
634							];
635						}
636					}
637				}
638				unset($childTrigger);
639
640				$this->addDependencies($childTriggers, true);
641			}
642		}
643	}
644
645	/**
646	 * Validates the input for the addDependencies() method.
647	 *
648	 * @param array  $trigger_prototypes
649	 * @param string $trigger_prototypes[]['triggerid']
650	 * @param array  $trigger_prototypes[]['dependencies']
651	 * @param string $trigger_prototypes[]['dependencies'][]['triggerid']
652	 * @param bool   $inherited
653	 *
654	 * @throws APIException if the given dependencies are invalid.
655	 */
656	protected function validateAddDependencies(array $trigger_prototypes, bool $inherited = false): void {
657		$depTriggerIds = [];
658
659		foreach ($trigger_prototypes as $trigger_prototype) {
660			if (!array_key_exists('dependencies', $trigger_prototype)) {
661				continue;
662			}
663
664			foreach ($trigger_prototype['dependencies'] as $dependency) {
665				$depTriggerIds[$dependency['triggerid']] = $dependency['triggerid'];
666			}
667		}
668
669		if (!$depTriggerIds) {
670			return;
671		}
672
673		// Check if given IDs are actual trigger prototypes and get discovery rules if they are.
674		$depTriggerPrototypes = $this->get([
675			'output' => ['triggerid'],
676			'selectDiscoveryRule' => ['itemid'],
677			'triggerids' => $depTriggerIds,
678			'nopermissions' => $inherited ? true : null,
679			'preservekeys' => true
680		]);
681
682		$dep_triggerids = array_diff($depTriggerIds, array_keys($depTriggerPrototypes));
683
684		if ($depTriggerPrototypes) {
685			// Get current trigger prototype discovery rules.
686			$dRules = $this->get([
687				'output' => ['triggerid'],
688				'selectDiscoveryRule' => ['itemid'],
689				'triggerids' => zbx_objectValues($trigger_prototypes, 'triggerid'),
690				'nopermissions' => $inherited ? true : null,
691				'preservekeys' => true
692			]);
693
694			foreach ($trigger_prototypes as $trigger_prototype) {
695				if (!array_key_exists('dependencies', $trigger_prototype)) {
696					continue;
697				}
698
699				$dRuleId = $dRules[$trigger_prototype['triggerid']]['discoveryRule']['itemid'];
700
701				// Check if current trigger prototype rules match dependent trigger prototype rules.
702				foreach ($trigger_prototype['dependencies'] as $dependency) {
703					if (isset($depTriggerPrototypes[$dependency['triggerid']])) {
704						$depTriggerDRuleId = $depTriggerPrototypes[$dependency['triggerid']]['discoveryRule']['itemid'];
705
706						if (bccomp($depTriggerDRuleId, $dRuleId) != 0) {
707							self::exception(ZBX_API_ERROR_PERMISSIONS,
708								_('No permissions to referred object or it does not exist!')
709							);
710						}
711					}
712				}
713			}
714		}
715		elseif (!$dep_triggerids) {
716			self::exception(ZBX_API_ERROR_PERMISSIONS, _('No permissions to referred object or it does not exist!'));
717		}
718
719		if ($dep_triggerids && !$inherited) {
720			// Check other dependency IDs if those are normal triggers.
721			$count = API::Trigger()->get([
722				'countOutput' => true,
723				'triggerids' => $dep_triggerids,
724				'filter' => [
725					'flags' => [ZBX_FLAG_DISCOVERY_NORMAL]
726				]
727			]);
728
729			if ($count != count($dep_triggerids)) {
730				self::exception(ZBX_API_ERROR_PERMISSIONS,
731					_('No permissions to referred object or it does not exist!')
732				);
733			}
734		}
735
736		$this->checkDependencies($trigger_prototypes);
737		$this->checkDependencyParents($trigger_prototypes);
738		$this->checkDependencyDuplicates($trigger_prototypes);
739	}
740
741	/**
742	 * Check the dependencies of the given trigger prototypes.
743	 *
744	 * @param array  $triggerPrototypes
745	 * @param string $triggerPrototypes[]['triggerid']
746	 * @param array  $triggerPrototypes[]['dependencies']
747	 * @param string $triggerPrototypes[]['dependencies'][]['triggerid']
748	 *
749	 * @throws APIException if any of the dependencies are invalid.
750	 */
751	protected function checkDependencies(array $triggerPrototypes) {
752		$triggerPrototypes = zbx_toHash($triggerPrototypes, 'triggerid');
753
754		foreach ($triggerPrototypes as $triggerPrototype) {
755			if (!array_key_exists('dependencies', $triggerPrototype)) {
756				continue;
757			}
758
759			$triggerid_down = $triggerPrototype['triggerid'];
760			$triggerids_up = zbx_objectValues($triggerPrototype['dependencies'], 'triggerid');
761
762			foreach ($triggerids_up as $triggerid_up) {
763				if (bccomp($triggerid_down, $triggerid_up) == 0) {
764					self::exception(ZBX_API_ERROR_PARAMETERS,
765						_('Cannot create dependency on trigger prototype itself.')
766					);
767				}
768			}
769		}
770
771		foreach ($triggerPrototypes as $triggerPrototype) {
772			if (!array_key_exists('dependencies', $triggerPrototype)) {
773				continue;
774			}
775
776			$depTriggerIds = zbx_objectValues($triggerPrototype['dependencies'], 'triggerid');
777
778			$triggerTemplates = API::Template()->get([
779				'output' => ['hostid', 'status'],
780				'triggerids' => [$triggerPrototype['triggerid']],
781				'nopermissions' => true
782			]);
783
784			if (!$triggerTemplates) {
785				// Current trigger prototype belongs to a host, so forbid dependencies from a host to a template.
786
787				$triggerDepTemplates = API::Template()->get([
788					'output' => ['templateid'],
789					'triggerids' => $depTriggerIds,
790					'nopermissions' => true,
791					'limit' => 1
792				]);
793
794				if ($triggerDepTemplates) {
795					self::exception(ZBX_API_ERROR_PARAMETERS, _('Cannot add dependency from a host to a template.'));
796				}
797			}
798
799			// check circular dependency
800			$downTriggerIds = [$triggerPrototype['triggerid']];
801			do {
802				// triggerid_down depends on triggerid_up
803				$res = DBselect(
804					'SELECT td.triggerid_up'.
805					' FROM trigger_depends td'.
806					' WHERE '.dbConditionInt('td.triggerid_down', $downTriggerIds)
807				);
808
809				// combine db dependencies with those to be added
810				$upTriggersIds = [];
811				while ($row = DBfetch($res)) {
812					$upTriggersIds[] = $row['triggerid_up'];
813				}
814				foreach ($downTriggerIds as $id) {
815					if (isset($triggerPrototypes[$id]) && isset($triggerPrototypes[$id]['dependencies'])) {
816						$upTriggersIds = array_merge($upTriggersIds,
817							zbx_objectValues($triggerPrototypes[$id]['dependencies'], 'triggerid')
818						);
819					}
820				}
821
822				// if found trigger id is in dependent triggerids, there is a dependency loop
823				$downTriggerIds = [];
824				foreach ($upTriggersIds as $id) {
825					if (bccomp($id, $triggerPrototype['triggerid']) == 0) {
826						self::exception(ZBX_API_ERROR_PARAMETERS, _('Cannot create circular dependencies.'));
827					}
828					$downTriggerIds[] = $id;
829				}
830			} while (!empty($downTriggerIds));
831
832			// fetch all templates that are used in dependencies
833			$triggerDepTemplates = API::Template()->get([
834				'output' => ['templateid'],
835				'triggerids' => $depTriggerIds,
836				'nopermissions' => true,
837				'preservekeys' => true
838			]);
839
840			$depTemplateIds = array_keys($triggerDepTemplates);
841
842			// run the check only if a templated trigger has dependencies on other templates
843			$triggerTemplateIds = zbx_toHash(zbx_objectValues($triggerTemplates, 'templateid'));
844			$tdiff = array_diff($depTemplateIds, $triggerTemplateIds);
845
846			if (!empty($triggerTemplateIds) && !empty($depTemplateIds) && !empty($tdiff)) {
847				$affectedTemplateIds = zbx_array_merge($triggerTemplateIds, $depTemplateIds);
848
849				// create a list of all hosts, that are children of the affected templates
850				$dbLowlvltpl = DBselect(
851					'SELECT DISTINCT ht.templateid,ht.hostid,h.host'.
852					' FROM hosts_templates ht,hosts h'.
853					' WHERE h.hostid=ht.hostid'.
854						' AND '.dbConditionInt('ht.templateid', $affectedTemplateIds)
855				);
856				$map = [];
857				while ($lowlvltpl = DBfetch($dbLowlvltpl)) {
858					if (!isset($map[$lowlvltpl['hostid']])) {
859						$map[$lowlvltpl['hostid']] = [];
860					}
861					$map[$lowlvltpl['hostid']][$lowlvltpl['templateid']] = $lowlvltpl['host'];
862				}
863
864				// check that if some host is linked to the template, that the trigger belongs to,
865				// the host must also be linked to all of the templates, that trigger dependencies point to
866				foreach ($map as $templates) {
867					foreach ($triggerTemplateIds as $triggerTemplateId) {
868						// is the host linked to one of the trigger templates?
869						if (isset($templates[$triggerTemplateId])) {
870							// then make sure all of the dependency templates are also linked
871							foreach ($depTemplateIds as $depTemplateId) {
872								if (!isset($templates[$depTemplateId])) {
873									self::exception(ZBX_API_ERROR_PARAMETERS,
874										_s('Not all templates are linked to "%1$s".', reset($templates))
875									);
876								}
877							}
878							break;
879						}
880					}
881				}
882			}
883		}
884	}
885
886	/**
887	 * Check that none of the triggers have dependencies on their children. Checks only one level of inheritance, but
888	 * since it is called on each inheritance step, also works for multiple inheritance levels.
889	 *
890	 * @param array  $triggerPrototypes
891	 * @param string $triggerPrototypes[]['triggerid']
892	 * @param array  $triggerPrototypes[]['dependencies']
893	 * @param string $triggerPrototypes[]['dependencies'][]['triggerid']
894	 *
895	 * @throws APIException if at least one trigger is dependent on its child.
896	 */
897	protected function checkDependencyParents(array $triggerPrototypes) {
898		// fetch all templated dependency trigger parents
899		$depTriggerIds = [];
900
901		foreach ($triggerPrototypes as $triggerPrototype) {
902			if (!array_key_exists('dependencies', $triggerPrototype)) {
903				continue;
904			}
905
906			foreach ($triggerPrototype['dependencies'] as $dependency) {
907				$depTriggerIds[$dependency['triggerid']] = $dependency['triggerid'];
908			}
909		}
910
911		$parentDepTriggers = DBfetchArray(DBSelect(
912			'SELECT templateid,triggerid'.
913			' FROM triggers'.
914			' WHERE templateid>0'.
915				' AND '.dbConditionInt('triggerid', $depTriggerIds)
916		));
917
918		if ($parentDepTriggers) {
919			$parentDepTriggers = zbx_toHash($parentDepTriggers, 'triggerid');
920
921			foreach ($triggerPrototypes as $triggerPrototype) {
922				foreach ($triggerPrototype['dependencies'] as $dependency) {
923					// Check if the current trigger is the parent of the dependency trigger.
924
925					$depTriggerId = $dependency['triggerid'];
926
927					if (isset($parentDepTriggers[$depTriggerId])
928							&& $parentDepTriggers[$depTriggerId]['templateid'] == $triggerPrototype['triggerid']) {
929
930						self::exception(ZBX_API_ERROR_PARAMETERS,
931							_s('Trigger prototype cannot be dependent on a trigger that is inherited from it.')
932						);
933					}
934				}
935			}
936		}
937	}
938
939	/**
940	 * Checks if the given dependencies contain duplicates.
941	 *
942	 * @param array  $triggerPrototypes
943	 * @param string $triggerPrototypes[]['triggerid']
944	 * @param array  $triggerPrototypes[]['dependencies']
945	 * @param string $triggerPrototypes[]['dependencies'][]['triggerid']
946	 *
947	 * @throws APIException if the given dependencies contain duplicates.
948	 */
949	protected function checkDependencyDuplicates(array $triggerPrototypes) {
950		// check duplicates in array
951		$uniqueTriggers = [];
952		$depTriggerIds = [];
953		$duplicateTriggerId = null;
954
955		foreach ($triggerPrototypes as $triggerPrototype) {
956			if (!array_key_exists('dependencies', $triggerPrototype)) {
957				continue;
958			}
959
960			foreach ($triggerPrototype['dependencies'] as $dependency) {
961				$depTriggerIds[$dependency['triggerid']] = $dependency['triggerid'];
962
963				if (isset($uniqueTriggers[$triggerPrototype['triggerid']][$dependency['triggerid']])) {
964					$duplicateTriggerId = $triggerPrototype['triggerid'];
965					break 2;
966				}
967				else {
968					$uniqueTriggers[$triggerPrototype['triggerid']][$dependency['triggerid']] = 1;
969				}
970			}
971		}
972
973		if ($duplicateTriggerId === null) {
974			// check if dependency already exists in DB
975			foreach ($triggerPrototypes as $triggerPrototype) {
976				$dbUpTriggers = DBselect(
977					'SELECT td.triggerid_up'.
978					' FROM trigger_depends td'.
979					' WHERE '.dbConditionInt('td.triggerid_up', $depTriggerIds).
980					' AND td.triggerid_down='.zbx_dbstr($triggerPrototype['triggerid'])
981				, 1);
982				if (DBfetch($dbUpTriggers)) {
983					$duplicateTriggerId = $triggerPrototype['triggerid'];
984					break;
985				}
986			}
987		}
988
989		if ($duplicateTriggerId) {
990			$duplicateTrigger = DBfetch(DBselect(
991				'SELECT t.description'.
992				' FROM triggers t'.
993				' WHERE t.triggerid='.zbx_dbstr($duplicateTriggerId)
994			));
995			self::exception(ZBX_API_ERROR_PARAMETERS,
996				_s('Duplicate dependencies in trigger prototype "%1$s".', $duplicateTrigger['description'])
997			);
998		}
999	}
1000
1001	/**
1002	 * Synchronizes the templated trigger prototype dependencies on the given hosts inherited from the given templates.
1003	 * Update dependencies, do it after all triggers and trigger prototypes that can be dependent were created/updated
1004	 * on all child hosts/templates. Starting from highest level template trigger prototypes select trigger prototypes
1005	 * from one level lower, then for each lower trigger prototype look if it's parent has dependencies, if so
1006	 * find this dependency trigger prototype child on dependent trigger prototype host and add new dependency.
1007	 *
1008	 * @param array			$data
1009	 * @param array|string	$data['templateids']
1010	 * @param array|string	$data['hostids']
1011	 */
1012	public function syncTemplateDependencies(array $data) {
1013		$templateIds = zbx_toArray($data['templateids']);
1014		$hostIds = zbx_toArray($data['hostids']);
1015
1016		$parentTriggers = $this->get([
1017			'output' => ['triggerid'],
1018			'selectDependencies' => ['triggerid'],
1019			'hostids' => $templateIds,
1020			'preservekeys' => true
1021		]);
1022
1023		if ($parentTriggers) {
1024			$childTriggers = $this->get([
1025				'output' => ['triggerid', 'templateid'],
1026				'selectHosts' => ['hostid'],
1027				'hostids' => ($hostIds) ? $hostIds : null,
1028				'filter' => ['templateid' => array_keys($parentTriggers)],
1029				'nopermissions' => true,
1030				'preservekeys' => true
1031			]);
1032
1033			if ($childTriggers) {
1034				$newDependencies = [];
1035
1036				foreach ($childTriggers as $childTrigger) {
1037					$parentDependencies = $parentTriggers[$childTrigger['templateid']]['dependencies'];
1038
1039					if ($parentDependencies) {
1040						$newDependencies[$childTrigger['triggerid']] = [
1041							'triggerid' => $childTrigger['triggerid'],
1042							'dependencies' => []
1043						];
1044
1045						$dependencies = [];
1046						foreach ($parentDependencies as $depTrigger) {
1047							$dependencies[] = $depTrigger['triggerid'];
1048						}
1049
1050						$host = reset($childTrigger['hosts']);
1051						$dependencies = replace_template_dependencies($dependencies, $host['hostid']);
1052
1053						foreach ($dependencies as $depTriggerId) {
1054							$newDependencies[$childTrigger['triggerid']]['dependencies'][] = [
1055								'triggerid' => $depTriggerId
1056							];
1057						}
1058					}
1059				}
1060
1061				$this->deleteDependencies($childTriggers);
1062
1063				if ($newDependencies) {
1064					$this->addDependencies($newDependencies);
1065				}
1066			}
1067		}
1068	}
1069
1070	/**
1071	 * Retrieves and adds additional requested data (options 'selectHosts', 'selectGroups', etc.) to result set.
1072	 *
1073	 * @param array		$options
1074	 * @param array		$result
1075	 *
1076	 * @return array
1077	 */
1078	protected function addRelatedObjects(array $options, array $result) {
1079		$result = parent::addRelatedObjects($options, $result);
1080
1081		$triggerPrototypeIds = array_keys($result);
1082
1083		// Add trigger prototype dependencies.
1084		if ($options['selectDependencies'] !== null && $options['selectDependencies'] != API_OUTPUT_COUNT) {
1085			$dependencies = [];
1086			$relationMap = new CRelationMap();
1087			$res = DBselect(
1088				'SELECT td.triggerid_up,td.triggerid_down'.
1089				' FROM trigger_depends td'.
1090				' WHERE '.dbConditionInt('td.triggerid_down', $triggerPrototypeIds)
1091			);
1092
1093			while ($relation = DBfetch($res)) {
1094				$relationMap->addRelation($relation['triggerid_down'], $relation['triggerid_up']);
1095			}
1096
1097			$related_ids = $relationMap->getRelatedIds();
1098
1099			if ($related_ids) {
1100				$dependencies = API::getApiService()->select($this->tableName(), [
1101					'output' => $options['selectDependencies'],
1102					'triggerids' => $related_ids,
1103					'preservekeys' => true
1104				]);
1105			}
1106
1107			$result = $relationMap->mapMany($result, $dependencies, 'dependencies');
1108		}
1109
1110		// adding items
1111		if ($options['selectItems'] !== null && $options['selectItems'] != API_OUTPUT_COUNT) {
1112			$relationMap = $this->createRelationMap($result, 'triggerid', 'itemid', 'functions');
1113			$items = API::Item()->get([
1114				'output' => $options['selectItems'],
1115				'itemids' => $relationMap->getRelatedIds(),
1116				'webitems' => true,
1117				'nopermissions' => true,
1118				'preservekeys' => true,
1119				'filter' => ['flags' => null]
1120			]);
1121			$result = $relationMap->mapMany($result, $items, 'items');
1122		}
1123
1124		// adding discovery rule
1125		if ($options['selectDiscoveryRule'] !== null && $options['selectDiscoveryRule'] != API_OUTPUT_COUNT) {
1126			$dbRules = DBselect(
1127				'SELECT id.parent_itemid,f.triggerid'.
1128					' FROM item_discovery id,functions f'.
1129					' WHERE '.dbConditionInt('f.triggerid', $triggerPrototypeIds).
1130					' AND f.itemid=id.itemid'
1131			);
1132			$relationMap = new CRelationMap();
1133			while ($rule = DBfetch($dbRules)) {
1134				$relationMap->addRelation($rule['triggerid'], $rule['parent_itemid']);
1135			}
1136
1137			$discoveryRules = API::DiscoveryRule()->get([
1138				'output' => $options['selectDiscoveryRule'],
1139				'itemids' => $relationMap->getRelatedIds(),
1140				'nopermissions' => true,
1141				'preservekeys' => true
1142			]);
1143			$result = $relationMap->mapOne($result, $discoveryRules, 'discoveryRule');
1144		}
1145
1146		return $result;
1147	}
1148
1149}
1150