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