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 host prototypes.
24 */
25class CHostPrototype extends CHostBase {
26
27	protected $sortColumns = ['hostid', 'host', 'name', 'status'];
28
29	public function __construct() {
30		parent::__construct();
31
32		$this->getOptions = array_merge($this->getOptions, [
33			'discoveryids'  		=> null,
34			'inherited'				=> null,
35			'selectDiscoveryRule' 	=> null,
36			'selectGroupLinks'		=> null,
37			'selectGroupPrototypes' => null,
38			'selectParentHost'		=> null,
39			'selectTemplates' 		=> null,
40			'selectInventory' 		=> null,
41			'editable'				=> false,
42			'nopermissions'			=> null,
43			'sortfield'    			=> '',
44			'sortorder'     		=> ''
45		]);
46	}
47
48	/**
49	 * Get host prototypes.
50	 *
51	 * @param array $options
52	 *
53	 * @return array
54	 */
55	public function get(array $options) {
56		$options = zbx_array_merge($this->getOptions, $options);
57		$options['filter']['flags'] = ZBX_FLAG_DISCOVERY_PROTOTYPE;
58
59		// build and execute query
60		$sql = $this->createSelectQuery($this->tableName(), $options);
61		$res = DBselect($sql, $options['limit']);
62
63		// fetch results
64		$result = [];
65		while ($row = DBfetch($res)) {
66			// a count query, return a single result
67			if ($options['countOutput']) {
68				if ($options['groupCount']) {
69					$result[] = $row;
70				}
71				else {
72					$result = $row['rowscount'];
73				}
74			}
75			// a normal select query
76			else {
77				$result[$row[$this->pk()]] = $row;
78			}
79		}
80
81		if ($options['countOutput']) {
82			return $result;
83		}
84
85		if ($result) {
86			$result = $this->addRelatedObjects($options, $result);
87			$result = $this->unsetExtraFields($result, ['triggerid'], $options['output']);
88		}
89
90		if (!$options['preservekeys']) {
91			$result = zbx_cleanHashes($result);
92		}
93
94		return $result;
95	}
96
97	/**
98	 * Check for duplicated names.
99	 *
100	 * @param string $field_name
101	 * @param array  $names_by_ruleid
102	 *
103	 * @throws APIException  if host prototype with same name already exists.
104	 */
105	private function checkDuplicates($field_name, array $names_by_ruleid) {
106		$sql_where = [];
107		foreach ($names_by_ruleid as $ruleid => $names) {
108			$sql_where[] = '(i.itemid='.$ruleid.' AND '.dbConditionString('h.'.$field_name, $names).')';
109		}
110
111		$db_host_prototypes = DBfetchArray(DBselect(
112				'SELECT i.name AS rule,h.'.$field_name.
113				' FROM items i,host_discovery hd,hosts h'.
114				' WHERE i.itemid=hd.parent_itemid'.
115					' AND hd.hostid=h.hostid'.
116					' AND ('.implode(' OR ', $sql_where).')',
117				1
118		));
119
120		if ($db_host_prototypes) {
121			$error = ($field_name === 'host')
122				? _('Host prototype with host name "%1$s" already exists in discovery rule "%2$s".')
123				: _('Host prototype with visible name "%1$s" already exists in discovery rule "%2$s".');
124
125			self::exception(ZBX_API_ERROR_PARAMETERS,
126				sprintf($error, $db_host_prototypes[0][$field_name], $db_host_prototypes[0]['rule'])
127			);
128		}
129	}
130
131	/**
132	 * Validates the input parameters for the create() method.
133	 *
134	 * @throws APIException if the input is invalid.
135	 *
136	 * @param array $host_prototypes
137	 */
138	protected function validateCreate(array &$host_prototypes) {
139		$api_input_rules = ['type' => API_OBJECTS, 'flags' => API_NOT_EMPTY | API_NORMALIZE, 'uniq' => [['ruleid', 'host'], ['ruleid', 'name']], 'fields' => [
140			'ruleid' =>				['type' => API_ID, 'flags' => API_REQUIRED],
141			'host' =>				['type' => API_H_NAME, 'flags' => API_REQUIRED | API_REQUIRED_LLD_MACRO, 'length' => DB::getFieldLength('hosts', 'host')],
142			'name' =>				['type' => API_STRING_UTF8, 'flags' => API_NOT_EMPTY, 'length' => DB::getFieldLength('hosts', 'name'), 'default_source' => 'host'],
143			'status' =>				['type' => API_INT32, 'in' => implode(',', [HOST_STATUS_MONITORED, HOST_STATUS_NOT_MONITORED])],
144			'groupLinks' =>			['type' => API_OBJECTS, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'uniq' => [['groupid']], 'fields' => [
145				'groupid' =>			['type' => API_ID, 'flags' => API_REQUIRED]
146			]],
147			'groupPrototypes' =>	['type' => API_OBJECTS, 'uniq' => [['name']], 'fields' => [
148				'name' =>				['type' => API_HG_NAME, 'flags' => API_REQUIRED | API_REQUIRED_LLD_MACRO, 'length' => DB::getFieldLength('hstgrp', 'name')]
149			]],
150			'inventory' =>			['type' => API_OBJECT, 'fields' => [
151				'inventory_mode' =>		['type' => API_INT32, 'flags' => API_REQUIRED, 'in' => implode(',', [HOST_INVENTORY_DISABLED, HOST_INVENTORY_MANUAL, HOST_INVENTORY_AUTOMATIC])]
152			]],
153			'templates' =>			['type' => API_OBJECTS, 'flags' => API_NORMALIZE, 'uniq' => [['templateid']], 'fields' => [
154				'templateid' =>			['type' => API_ID, 'flags' => API_REQUIRED]
155			]]
156		]];
157		if (!CApiInputValidator::validate($api_input_rules, $host_prototypes, '/', $error)) {
158			self::exception(ZBX_API_ERROR_PARAMETERS, $error);
159		}
160
161		$hosts_by_ruleid = [];
162		$names_by_ruleid = [];
163		$groupids = [];
164
165		foreach ($host_prototypes as $host_prototype) {
166			// Collect host group ID links for latter validation.
167			foreach ($host_prototype['groupLinks'] as $group_prototype) {
168				$groupids[$group_prototype['groupid']] = true;
169			}
170
171			$hosts_by_ruleid[$host_prototype['ruleid']][] = $host_prototype['host'];
172			$names_by_ruleid[$host_prototype['ruleid']][] = $host_prototype['name'];
173		}
174
175		$ruleids = array_unique(zbx_objectValues($host_prototypes, 'ruleid'));
176		$groupids = array_keys($groupids);
177
178		$this->checkDiscoveryRulePermissions($ruleids);
179		$this->checkHostGroupsPermissions($groupids);
180
181		// Check if the host is discovered.
182		$db_discovered_hosts = DBfetchArray(DBselect(
183			'SELECT h.host'.
184			' FROM items i,hosts h'.
185			' WHERE i.hostid=h.hostid'.
186				' AND '.dbConditionInt('i.itemid', $ruleids).
187				' AND h.flags='.ZBX_FLAG_DISCOVERY_CREATED,
188			1
189		));
190
191		if ($db_discovered_hosts) {
192			self::exception(ZBX_API_ERROR_PARAMETERS,
193				_s('Cannot create a host prototype on a discovered host "%1$s".', $db_discovered_hosts[0]['host'])
194			);
195		}
196
197		$this->checkDuplicates('host', $hosts_by_ruleid);
198		$this->checkDuplicates('name', $names_by_ruleid);
199	}
200
201	/**
202	 * Creates the given host prototypes.
203	 *
204	 * @param array $host_prototypes
205	 *
206	 * @return array
207	 */
208	public function create(array $host_prototypes) {
209		// 'templateid' validation happens during linkage.
210		$this->validateCreate($host_prototypes);
211
212		// Merge groups into group prototypes.
213		foreach ($host_prototypes as &$host_prototype) {
214			$host_prototype['groupPrototypes'] = array_merge(
215				array_key_exists('groupPrototypes', $host_prototype) ? $host_prototype['groupPrototypes'] : [],
216				$host_prototype['groupLinks']
217			);
218			unset($host_prototype['groupLinks']);
219		}
220		unset($host_prototype);
221
222		$host_prototypes = $this->createReal($host_prototypes);
223		$this->inherit($host_prototypes);
224
225		$this->addAuditBulk(AUDIT_ACTION_ADD, AUDIT_RESOURCE_HOST_PROTOTYPE, $host_prototypes);
226
227		return ['hostids' => zbx_objectValues($host_prototypes, 'hostid')];
228	}
229
230	/**
231	 * Creates the host prototypes and inherits them to linked hosts and templates.
232	 *
233	 * @param array $hostPrototypes
234	 *
235	 * @return array	an array of host prototypes with host IDs
236	 */
237	protected function createReal(array $hostPrototypes) {
238		foreach ($hostPrototypes as &$hostPrototype) {
239			$hostPrototype['flags'] = ZBX_FLAG_DISCOVERY_PROTOTYPE;
240		}
241		unset($hostPrototype);
242
243		// save the host prototypes
244		$hostPrototypeIds = DB::insert($this->tableName(), $hostPrototypes);
245
246		$groupPrototypes = [];
247		$hostPrototypeDiscoveryRules = [];
248		$hostPrototypeInventory = [];
249		foreach ($hostPrototypes as $key => $hostPrototype) {
250			$hostPrototypes[$key]['hostid'] = $hostPrototype['hostid'] = $hostPrototypeIds[$key];
251
252			// save group prototypes
253			foreach ($hostPrototype['groupPrototypes'] as $groupPrototype) {
254				$groupPrototype['hostid'] = $hostPrototype['hostid'];
255				$groupPrototypes[] = $groupPrototype;
256			}
257
258			// discovery rules
259			$hostPrototypeDiscoveryRules[] = [
260				'hostid' => $hostPrototype['hostid'],
261				'parent_itemid' => $hostPrototype['ruleid']
262			];
263
264			// inventory
265			if (isset($hostPrototype['inventory']['inventory_mode'])
266					&& ($hostPrototype['inventory']['inventory_mode'] == HOST_INVENTORY_MANUAL
267						|| $hostPrototype['inventory']['inventory_mode'] == HOST_INVENTORY_AUTOMATIC)) {
268				$hostPrototypeInventory[] = [
269					'hostid' => $hostPrototype['hostid'],
270					'inventory_mode' => $hostPrototype['inventory']['inventory_mode']
271				];
272			}
273		}
274
275		// save group prototypes
276		$groupPrototypes = DB::save('group_prototype', $groupPrototypes);
277		$i = 0;
278		foreach ($hostPrototypes as &$hostPrototype) {
279			foreach ($hostPrototype['groupPrototypes'] as &$groupPrototype) {
280				$groupPrototype['group_prototypeid'] = $groupPrototypes[$i]['group_prototypeid'];
281				$i++;
282			}
283			unset($groupPrototype);
284		}
285		unset($hostPrototype);
286
287		// link host prototypes to discovery rules
288		DB::insert('host_discovery', $hostPrototypeDiscoveryRules, false);
289
290		// save inventory
291		DB::insert('host_inventory', $hostPrototypeInventory, false);
292
293		// link templates
294		foreach ($hostPrototypes as $hostPrototype) {
295			if (isset($hostPrototype['templates']) && $hostPrototype['templates']) {
296				$this->link(zbx_objectValues($hostPrototype['templates'], 'templateid'), [$hostPrototype['hostid']]);
297			}
298		}
299
300		return $hostPrototypes;
301	}
302
303	/**
304	 * Validates the input parameters for the update() method.
305	 *
306	 * @throws APIException if the input is invalid.
307	 *
308	 * @param array $host_prototypes
309	 * @param array $db_host_prototypes
310	 */
311	protected function validateUpdate(array &$host_prototypes, array &$db_host_prototypes = null) {
312		$api_input_rules = ['type' => API_OBJECTS, 'flags' => API_NOT_EMPTY | API_NORMALIZE, 'uniq' => [['hostid']], 'fields' => [
313			'hostid' =>				['type' => API_ID, 'flags' => API_REQUIRED],
314			'host' =>				['type' => API_H_NAME, 'flags' => API_REQUIRED_LLD_MACRO, 'length' => DB::getFieldLength('hosts', 'host')],
315			'name' =>				['type' => API_STRING_UTF8, 'flags' => API_NOT_EMPTY, 'length' => DB::getFieldLength('hosts', 'name')],
316			'status' =>				['type' => API_INT32, 'in' => implode(',', [HOST_STATUS_MONITORED, HOST_STATUS_NOT_MONITORED])],
317			'groupLinks' =>			['type' => API_OBJECTS, 'flags' => API_NOT_EMPTY, 'uniq' => [['group_prototypeid'], ['groupid']], 'fields' => [
318				'group_prototypeid' =>	['type' => API_ID],
319				'groupid' =>			['type' => API_ID]
320			]],
321			'groupPrototypes' =>	['type' => API_OBJECTS, 'uniq' => [['group_prototypeid'], ['name']], 'fields' => [
322				'group_prototypeid' =>	['type' => API_ID],
323				'name' =>				['type' => API_HG_NAME, 'flags' => API_REQUIRED_LLD_MACRO, 'length' => DB::getFieldLength('hstgrp', 'name')]
324			]],
325			'inventory' =>			['type' => API_OBJECT, 'fields' => [
326				'inventory_mode' =>		['type' => API_INT32, 'flags' => API_REQUIRED, 'in' => implode(',', [HOST_INVENTORY_DISABLED, HOST_INVENTORY_MANUAL, HOST_INVENTORY_AUTOMATIC])]
327			]],
328			'templates' =>			['type' => API_OBJECTS, 'flags' => API_NORMALIZE, 'uniq' => [['templateid']], 'fields' => [
329				'templateid' =>			['type' => API_ID, 'flags' => API_REQUIRED]
330			]]
331		]];
332		if (!CApiInputValidator::validate($api_input_rules, $host_prototypes, '/', $error)) {
333			self::exception(ZBX_API_ERROR_PARAMETERS, $error);
334		}
335
336		$db_host_prototypes = $this->get([
337			'output' => ['hostid', 'host', 'name', 'status'],
338			'selectDiscoveryRule' => ['itemid'],
339			'selectGroupLinks' => ['group_prototypeid', 'groupid'],
340			'selectGroupPrototypes' => ['group_prototypeid', 'name'],
341			'hostids' => zbx_objectValues($host_prototypes, 'hostid'),
342			'editable' => true,
343			'preservekeys' => true
344		]);
345
346		$hosts_by_ruleid = [];
347		$names_by_ruleid = [];
348
349		foreach ($host_prototypes as &$host_prototype) {
350			// Check if this host prototype exists.
351			if (!array_key_exists($host_prototype['hostid'], $db_host_prototypes)) {
352				self::exception(ZBX_API_ERROR_PERMISSIONS,
353					_('No permissions to referred object or it does not exist!')
354				);
355			}
356
357			$db_host_prototype = $db_host_prototypes[$host_prototype['hostid']];
358			$host_prototype['ruleid'] = $db_host_prototype['discoveryRule']['itemid'];
359
360			if (array_key_exists('host', $host_prototype) && $host_prototype['host'] !== $db_host_prototype['host']) {
361				$hosts_by_ruleid[$host_prototype['ruleid']][] = $host_prototype['host'];
362			}
363
364			if (array_key_exists('name', $host_prototype) && $host_prototype['name'] !== $db_host_prototype['name']) {
365				$names_by_ruleid[$host_prototype['ruleid']][] = $host_prototype['name'];
366			}
367		}
368		unset($host_prototype);
369
370		$api_input_rules = ['type' => API_OBJECTS, 'uniq' => [['ruleid', 'host'], ['ruleid', 'name']]];
371		if (!CApiInputValidator::validateUniqueness($api_input_rules, $host_prototypes, '/', $error)) {
372			self::exception(ZBX_API_ERROR_PARAMETERS, $error);
373		}
374
375		$groupids = [];
376		$db_groupids = [];
377
378		foreach ($host_prototypes as $host_prototype) {
379			$db_host_prototype = $db_host_prototypes[$host_prototype['hostid']];
380
381			foreach ($db_host_prototype['groupLinks'] as $db_group_link) {
382				$db_groupids[$db_group_link['groupid']] = true;
383			}
384
385			$db_group_links = zbx_toHash($db_host_prototype['groupLinks'], 'group_prototypeid');
386			$db_group_prototypes = zbx_toHash($db_host_prototype['groupPrototypes'], 'group_prototypeid');
387
388			// Validate 'group_prototypeid' in 'groupLinks' property.
389			if (array_key_exists('groupLinks', $host_prototype)) {
390				foreach ($host_prototype['groupLinks'] as $group_link) {
391					if (!$group_link) {
392						self::exception(ZBX_API_ERROR_PARAMETERS, _('Incorrect arguments passed to function.'));
393					}
394
395					// Don't allow invalid 'group_prototypeid' parameters which do not belong to this 'hostid'.
396					if (array_key_exists('group_prototypeid', $group_link)
397							&& !array_key_exists($group_link['group_prototypeid'], $db_group_links)) {
398						self::exception(ZBX_API_ERROR_PERMISSIONS,
399							_('No permissions to referred object or it does not exist!')
400						);
401					}
402
403					if (array_key_exists('groupid', $group_link)) {
404						$groupids[$group_link['groupid']] = true;
405					}
406				}
407			}
408
409			// Validate 'group_prototypeid' in 'groupPrototypes' property.
410			if (array_key_exists('groupPrototypes', $host_prototype)) {
411				foreach ($host_prototype['groupPrototypes'] as $group_prototype) {
412					if (!$group_prototype) {
413						self::exception(ZBX_API_ERROR_PARAMETERS, _('Incorrect arguments passed to function.'));
414					}
415
416					// Don't allow invalid 'group_prototypeid' parameters which do not belong to this 'hostid'.
417					if (array_key_exists('group_prototypeid', $group_prototype)
418							&& !array_key_exists($group_prototype['group_prototypeid'], $db_group_prototypes)) {
419						self::exception(ZBX_API_ERROR_PERMISSIONS,
420							_('No permissions to referred object or it does not exist!')
421						);
422					}
423				}
424			}
425		}
426
427		// Collect only new given groupids for validation.
428		$groupids = array_diff_key($groupids, $db_groupids);
429
430		if ($groupids) {
431			$this->checkHostGroupsPermissions(array_keys($groupids));
432		}
433
434		$host_prototypes = $this->extendObjectsByKey($host_prototypes, $db_host_prototypes, 'hostid',
435			['host', 'name', 'groupLinks', 'groupPrototypes']
436		);
437
438		if ($hosts_by_ruleid) {
439			$this->checkDuplicates('host', $hosts_by_ruleid);
440		}
441		if ($names_by_ruleid) {
442			$this->checkDuplicates('name', $names_by_ruleid);
443		}
444	}
445
446	/**
447	 * Updates the given host prototypes.
448	 *
449	 * @param array $host_prototypes
450	 *
451	 * @return array
452	 */
453	public function update(array $host_prototypes) {
454		$this->validateUpdate($host_prototypes, $db_host_prototypes);
455
456		// merge group links into group prototypes
457		foreach ($host_prototypes as &$host_prototype) {
458			$host_prototype['groupPrototypes'] =
459				array_merge($host_prototype['groupPrototypes'], $host_prototype['groupLinks']);
460			unset($host_prototype['groupLinks']);
461		}
462		unset($host_prototype);
463
464		$host_prototypes = $this->updateReal($host_prototypes);
465		$this->inherit($host_prototypes);
466
467		foreach ($db_host_prototypes as &$db_host_prototype) {
468			unset($db_host_prototype['discoveryRule'], $db_host_prototype['groupLinks'],
469				$db_host_prototype['groupPrototypes']
470			);
471		}
472		unset($db_host_prototype);
473
474		$this->addAuditBulk(AUDIT_ACTION_UPDATE, AUDIT_RESOURCE_HOST_PROTOTYPE, $host_prototypes, $db_host_prototypes);
475
476		return ['hostids' => zbx_objectValues($host_prototypes, 'hostid')];
477	}
478
479	/**
480	 * Updates the host prototypes and propagates the changes to linked hosts and templates.
481	 *
482	 * @param array $hostPrototypes
483	 *
484	 * @return array
485	 */
486	protected function updateReal(array $hostPrototypes) {
487		// save the host prototypes
488		foreach ($hostPrototypes as $hostPrototype) {
489			DB::updateByPk($this->tableName(), $hostPrototype['hostid'], $hostPrototype);
490		}
491
492		$exHostPrototypes = $this->get([
493			'output' => ['hostid'],
494			'selectGroupLinks' => API_OUTPUT_EXTEND,
495			'selectGroupPrototypes' => API_OUTPUT_EXTEND,
496			'selectTemplates' => ['templateid'],
497			'selectInventory' => API_OUTPUT_EXTEND,
498			'hostids' => zbx_objectValues($hostPrototypes, 'hostid'),
499			'preservekeys' => true
500		]);
501
502		// update related objects
503		$inventoryCreate = [];
504		$inventoryDeleteIds = [];
505		foreach ($hostPrototypes as $key => $hostPrototype) {
506			$exHostPrototype = $exHostPrototypes[$hostPrototype['hostid']];
507
508			// group prototypes
509			if (isset($hostPrototype['groupPrototypes'])) {
510				foreach ($hostPrototype['groupPrototypes'] as &$groupPrototype) {
511					$groupPrototype['hostid'] = $hostPrototype['hostid'];
512				}
513				unset($groupPrototype);
514
515				// save group prototypes
516				$exGroupPrototypes = zbx_toHash(
517					array_merge($exHostPrototype['groupLinks'], $exHostPrototype['groupPrototypes']),
518					'group_prototypeid'
519				);
520				$modifiedGroupPrototypes = [];
521				foreach ($hostPrototype['groupPrototypes'] as $groupPrototype) {
522					if (isset($groupPrototype['group_prototypeid'])) {
523						unset($exGroupPrototypes[$groupPrototype['group_prototypeid']]);
524					}
525
526					$modifiedGroupPrototypes[] = $groupPrototype;
527				}
528				if ($exGroupPrototypes) {
529					$this->deleteGroupPrototypes(array_keys($exGroupPrototypes));
530				}
531				$hostPrototypes[$key]['groupPrototypes'] = DB::save('group_prototype', $modifiedGroupPrototypes);
532			}
533
534			// templates
535			if (isset($hostPrototype['templates'])) {
536				$existingTemplateIds = zbx_objectValues($exHostPrototype['templates'], 'templateid');
537				$newTemplateIds = zbx_objectValues($hostPrototype['templates'], 'templateid');
538				$this->unlink(array_diff($existingTemplateIds, $newTemplateIds), [$hostPrototype['hostid']]);
539				$this->link(array_diff($newTemplateIds, $existingTemplateIds), [$hostPrototype['hostid']]);
540			}
541
542			// inventory
543			if (isset($hostPrototype['inventory']) ) {
544				$inventory = zbx_array_mintersect(['inventory_mode'], $hostPrototype['inventory']);
545
546				if (array_key_exists('inventory_mode', $inventory)
547					&& ($inventory['inventory_mode'] == HOST_INVENTORY_MANUAL
548						|| $inventory['inventory_mode'] == HOST_INVENTORY_AUTOMATIC)) {
549
550					if ($exHostPrototype['inventory']['inventory_mode'] != HOST_INVENTORY_DISABLED) {
551						DB::update('host_inventory', [
552							'values' => $inventory,
553							'where' => ['hostid' => $hostPrototype['hostid']]
554						]);
555					}
556					else {
557						$inventoryCreate[] = $inventory + ['hostid' => $hostPrototype['hostid']];
558					}
559				}
560				else {
561					$inventoryDeleteIds[] = $hostPrototype['hostid'];
562				}
563			}
564		}
565
566		// save inventory
567		DB::insert('host_inventory', $inventoryCreate, false);
568		DB::delete('host_inventory', ['hostid' => $inventoryDeleteIds]);
569
570		return $hostPrototypes;
571	}
572
573	/**
574	 * Updates the children of the host prototypes on the given hosts and propagates the inheritance to the child hosts.
575	 *
576	 * @param array $hostPrototypes		array of host prototypes to inherit
577	 * @param array $hostids   			array of hosts to inherit to; if set to null, the children will be updated on all
578	 *                              	child hosts
579	 *
580	 * @return bool
581	 */
582	protected function inherit(array $hostPrototypes, array $hostids = null) {
583		if (empty($hostPrototypes)) {
584			return true;
585		}
586
587		// prepare the child host prototypes
588		$newHostPrototypes = $this->prepareInheritedObjects($hostPrototypes, $hostids);
589		if (!$newHostPrototypes) {
590			return true;
591		}
592
593		$insertHostPrototypes = [];
594		$updateHostPrototypes = [];
595		foreach ($newHostPrototypes as $newHostPrototype) {
596			if (isset($newHostPrototype['hostid'])) {
597				$updateHostPrototypes[] = $newHostPrototype;
598			}
599			else {
600				$insertHostPrototypes[] = $newHostPrototype;
601			}
602		}
603
604		// save the new host prototypes
605		if (!zbx_empty($insertHostPrototypes)) {
606			$insertHostPrototypes = $this->createReal($insertHostPrototypes);
607		}
608
609		if (!zbx_empty($updateHostPrototypes)) {
610			$updateHostPrototypes = $this->updateReal($updateHostPrototypes);
611		}
612
613		$host_prototypes = array_merge($updateHostPrototypes, $insertHostPrototypes);
614
615		if ($host_prototypes) {
616			$sql = 'SELECT hd.hostid'.
617					' FROM host_discovery hd,items i,hosts h'.
618					' WHERE hd.parent_itemid=i.itemid'.
619						' AND i.hostid=h.hostid'.
620						' AND h.status='.HOST_STATUS_TEMPLATE.
621						' AND '.dbConditionInt('hd.hostid', zbx_objectValues($host_prototypes, 'hostid'));
622			$valid_prototypes = DBfetchArrayAssoc(DBselect($sql), 'hostid');
623
624			foreach ($host_prototypes as $key => $host_prototype) {
625				if (!array_key_exists($host_prototype['hostid'], $valid_prototypes)) {
626					unset($host_prototypes[$key]);
627				}
628			}
629		}
630
631		// propagate the inheritance to the children
632		return $this->inherit($host_prototypes);
633	}
634
635
636	/**
637	 * Prepares and returns an array of child host prototypes, inherited from host prototypes $hostPrototypes
638	 * on the given hosts.
639	 *
640	 * Each host prototype must have the "ruleid" parameter set.
641	 *
642	 * @param array     $hostPrototypes
643	 * @param array		$hostIds
644	 *
645	 * @return array 	an array of unsaved child host prototypes
646	 */
647	protected function prepareInheritedObjects(array $hostPrototypes, array $hostIds = null) {
648		// fetch the related discovery rules with their hosts
649		$discoveryRules = API::DiscoveryRule()->get([
650			'output' => ['itemid', 'hostid'],
651			'selectHosts' => ['hostid'],
652			'itemids' => zbx_objectValues($hostPrototypes, 'ruleid'),
653			'templated' => true,
654			'nopermissions' => true,
655			'preservekeys' => true
656		]);
657
658		// fetch all child hosts to inherit to
659		// do not inherit host prototypes on discovered hosts
660		$chdHosts = API::Host()->get([
661			'output' => ['hostid', 'host', 'status'],
662			'selectParentTemplates' => ['templateid'],
663			'templateids' => zbx_objectValues($discoveryRules, 'hostid'),
664			'hostids' => $hostIds,
665			'nopermissions' => true,
666			'templated_hosts' => true,
667			'filter' => ['flags' => ZBX_FLAG_DISCOVERY_NORMAL]
668		]);
669		if (empty($chdHosts)) {
670			return [];
671		}
672
673		// fetch the child discovery rules
674		$childDiscoveryRules = API::DiscoveryRule()->get([
675			'output' => ['itemid', 'templateid', 'hostid'],
676			'preservekeys' => true,
677			'filter' => [
678				'templateid' => array_keys($discoveryRules)
679			]
680		]);
681
682		// fetch child host prototypes and group them by discovery rule
683		$childHostPrototypes = API::HostPrototype()->get([
684			'output' => ['hostid', 'host', 'templateid'],
685			'selectGroupLinks' => API_OUTPUT_EXTEND,
686			'selectGroupPrototypes' => API_OUTPUT_EXTEND,
687			'selectDiscoveryRule' => ['itemid'],
688			'discoveryids' => zbx_objectValues($childDiscoveryRules, 'itemid')
689		]);
690		foreach ($childDiscoveryRules as &$childDiscoveryRule) {
691			$childDiscoveryRule['hostPrototypes'] = [];
692		}
693		unset($childDiscoveryRule);
694		foreach ($childHostPrototypes as $childHostPrototype) {
695			$discoveryRuleId = $childHostPrototype['discoveryRule']['itemid'];
696			unset($childHostPrototype['discoveryRule']);
697
698			$childDiscoveryRules[$discoveryRuleId]['hostPrototypes'][] = $childHostPrototype;
699		}
700
701		// match each discovery that the parent host prototypes belong to to the child discovery rule for each host
702		$discoveryRuleChildren = [];
703		foreach ($childDiscoveryRules as $childRule) {
704			$discoveryRuleChildren[$childRule['templateid']][$childRule['hostid']] = $childRule['itemid'];
705		}
706
707		$newHostPrototypes = [];
708		foreach ($chdHosts as $host) {
709			$hostId = $host['hostid'];
710
711			// skip items not from parent templates of current host
712			$templateIds = zbx_toHash($host['parentTemplates'], 'templateid');
713			$parentHostPrototypes = [];
714			foreach ($hostPrototypes as $inum => $parentHostPrototype) {
715				$parentTemplateId = $discoveryRules[$parentHostPrototype['ruleid']]['hostid'];
716
717				if (isset($templateIds[$parentTemplateId])) {
718					$parentHostPrototypes[$inum] = $parentHostPrototype;
719				}
720			}
721
722			foreach ($parentHostPrototypes as $parentHostPrototype) {
723				$childDiscoveryRuleId = $discoveryRuleChildren[$parentHostPrototype['ruleid']][$hostId];
724				$exHostPrototype = null;
725
726				// check if the child discovery rule already has host prototypes
727				$exHostPrototypes = $childDiscoveryRules[$childDiscoveryRuleId]['hostPrototypes'];
728				if ($exHostPrototypes) {
729					$exHostPrototypesHosts = zbx_toHash($exHostPrototypes, 'host');
730					$exHostPrototypesTemplateIds = zbx_toHash($exHostPrototypes, 'templateid');
731
732					// look for an already created inherited host prototype
733					// if one exists - update it
734					if (isset($exHostPrototypesTemplateIds[$parentHostPrototype['hostid']])) {
735						$exHostPrototype = $exHostPrototypesTemplateIds[$parentHostPrototype['hostid']];
736
737						// check if there's a host prototype on the target host with the same host name but from a different template
738						// or no template
739						if (isset($exHostPrototypesHosts[$parentHostPrototype['host']])
740							&& !idcmp($exHostPrototypesHosts[$parentHostPrototype['host']]['templateid'], $parentHostPrototype['hostid'])) {
741
742							$discoveryRule = DBfetch(DBselect('SELECT i.name FROM items i WHERE i.itemid='.zbx_dbstr($exHostPrototype['discoveryRule']['itemid'])));
743							self::exception(ZBX_API_ERROR_PARAMETERS, _s('Host prototype "%1$s" already exists on "%2$s".', $parentHostPrototype['host'], $discoveryRule['name']));
744						}
745					}
746
747					// look for a host prototype with the same host name
748					// if one exists - convert it to an inherited host prototype
749					if (isset($exHostPrototypesHosts[$parentHostPrototype['host']])) {
750						$exHostPrototype = $exHostPrototypesHosts[$parentHostPrototype['host']];
751
752						// check that this host prototype is not inherited from a different template
753						if ($exHostPrototype['templateid'] > 0 && !idcmp($exHostPrototype['templateid'], $parentHostPrototype['hostid'])) {
754							$discoveryRule = DBfetch(DBselect('SELECT i.name FROM items i WHERE i.itemid='.zbx_dbstr($exHostPrototype['discoveryRule']['itemid'])));
755							self::exception(ZBX_API_ERROR_PARAMETERS, _s('Host prototype "%1$s" already exists on "%2$s", inherited from another template.', $parentHostPrototype['host'], $discoveryRule['name']));
756						}
757					}
758				}
759
760				// copy host prototype
761				$newHostPrototype = $parentHostPrototype;
762				$newHostPrototype['ruleid'] = $discoveryRuleChildren[$parentHostPrototype['ruleid']][$hostId];
763				$newHostPrototype['templateid'] = $parentHostPrototype['hostid'];
764
765				// update an existing inherited host prototype
766				if ($exHostPrototype) {
767					// look for existing group prototypes to update
768					$exGroupPrototypesByTemplateId = zbx_toHash($exHostPrototype['groupPrototypes'], 'templateid');
769					$exGroupPrototypesByName = zbx_toHash($exHostPrototype['groupPrototypes'], 'name');
770					$exGroupPrototypesByGroupId = zbx_toHash($exHostPrototype['groupLinks'], 'groupid');
771
772					// look for a group prototype that can be updated
773					foreach ($newHostPrototype['groupPrototypes'] as &$groupPrototype) {
774						// updated an inherited item prototype by templateid
775						if (isset($exGroupPrototypesByTemplateId[$groupPrototype['group_prototypeid']])) {
776							$groupPrototype['group_prototypeid'] = $exGroupPrototypesByTemplateId[$groupPrototype['group_prototypeid']]['group_prototypeid'];
777						}
778						// updated an inherited item prototype by name
779						elseif (isset($groupPrototype['name']) && !zbx_empty($groupPrototype['name'])
780								&& isset($exGroupPrototypesByName[$groupPrototype['name']])) {
781
782							$groupPrototype['templateid'] = $groupPrototype['group_prototypeid'];
783							$groupPrototype['group_prototypeid'] = $exGroupPrototypesByName[$groupPrototype['name']]['group_prototypeid'];
784						}
785						// updated an inherited item prototype by group ID
786						elseif (isset($groupPrototype['groupid']) && $groupPrototype['groupid']
787								&& isset($exGroupPrototypesByGroupId[$groupPrototype['groupid']])) {
788
789							$groupPrototype['templateid'] = $groupPrototype['group_prototypeid'];
790							$groupPrototype['group_prototypeid'] = $exGroupPrototypesByGroupId[$groupPrototype['groupid']]['group_prototypeid'];
791						}
792						// create a new child group prototype
793						else {
794							$groupPrototype['templateid'] = $groupPrototype['group_prototypeid'];
795							unset($groupPrototype['group_prototypeid']);
796						}
797
798						unset($groupPrototype['hostid']);
799					}
800					unset($groupPrototype);
801
802					$newHostPrototype['hostid'] = $exHostPrototype['hostid'];
803				}
804				// create a new inherited host prototype
805				else {
806					foreach ($newHostPrototype['groupPrototypes'] as &$groupPrototype) {
807						$groupPrototype['templateid'] = $groupPrototype['group_prototypeid'];
808						unset($groupPrototype['group_prototypeid'], $groupPrototype['hostid']);
809					}
810					unset($groupPrototype);
811
812					unset($newHostPrototype['hostid']);
813				}
814				$newHostPrototypes[] = $newHostPrototype;
815			}
816		}
817
818		return $newHostPrototypes;
819	}
820
821	/**
822	 * Inherits all host prototypes from the templates given in "templateids" to hosts or templates given in "hostids".
823	 *
824	 * @param array $data
825	 *
826	 * @return bool
827	 */
828	public function syncTemplates(array $data) {
829		$data['templateids'] = zbx_toArray($data['templateids']);
830		$data['hostids'] = zbx_toArray($data['hostids']);
831
832		$discoveryRules = API::DiscoveryRule()->get([
833			'output' => ['itemid'],
834			'hostids' => $data['templateids']
835		]);
836		$hostPrototypes = $this->get([
837			'discoveryids' => zbx_objectValues($discoveryRules, 'itemid'),
838			'preservekeys' => true,
839			'output' => API_OUTPUT_EXTEND,
840			'selectGroupLinks' => API_OUTPUT_EXTEND,
841			'selectGroupPrototypes' => API_OUTPUT_EXTEND,
842			'selectTemplates' => ['templateid'],
843			'selectDiscoveryRule' => ['itemid'],
844			'selectInventory' => ['inventory_mode']
845		]);
846
847		foreach ($hostPrototypes as &$hostPrototype) {
848			// merge group links into group prototypes
849			foreach ($hostPrototype['groupLinks'] as $group) {
850				$hostPrototype['groupPrototypes'][] = $group;
851			}
852			unset($hostPrototype['groupLinks']);
853
854			// the ID of the discovery rule must be passed in the "ruleid" parameter
855			$hostPrototype['ruleid'] = $hostPrototype['discoveryRule']['itemid'];
856			unset($hostPrototype['discoveryRule']);
857		}
858		unset($hostPrototype);
859
860		$this->inherit($hostPrototypes, $data['hostids']);
861
862		return true;
863	}
864
865	/**
866	 * Validates the input parameters for the delete() method.
867	 *
868	 * @throws APIException if the input is invalid
869	 *
870	 * @param array $hostPrototypeIds
871	 * @param bool 	$nopermissions
872	 */
873	protected function validateDelete($hostPrototypeIds, $nopermissions) {
874		if (!$hostPrototypeIds) {
875			self::exception(ZBX_API_ERROR_PARAMETERS, _('Empty input parameter.'));
876		}
877
878		if (!$nopermissions) {
879			$this->checkHostPrototypePermissions($hostPrototypeIds);
880			$this->checkNotInherited($hostPrototypeIds);
881		}
882	}
883
884	/**
885	 * Delete host prototypes.
886	 *
887	 * @param array 	$hostPrototypeIds
888	 * @param bool 		$nopermissions		if set to true, permission and template checks will be skipped
889	 *
890	 * @return array
891	 */
892	public function delete(array $hostPrototypeIds, $nopermissions = false) {
893		$this->validateDelete($hostPrototypeIds, $nopermissions);
894
895		// include child IDs
896		$parentHostPrototypeIds = $hostPrototypeIds;
897		$childHostPrototypeIds = [];
898		do {
899			$query = DBselect('SELECT h.hostid FROM hosts h WHERE '.dbConditionInt('h.templateid', $parentHostPrototypeIds));
900			$parentHostPrototypeIds = [];
901			while ($hostPrototype = DBfetch($query)) {
902				$parentHostPrototypeIds[] = $hostPrototype['hostid'];
903				$childHostPrototypeIds[] = $hostPrototype['hostid'];
904			}
905		} while (!empty($parentHostPrototypeIds));
906
907		$hostPrototypeIds = array_merge($hostPrototypeIds, $childHostPrototypeIds);
908
909		// Lock host prototypes before delete to prevent server from adding new LLD hosts.
910		DBselect(
911			'SELECT NULL'.
912			' FROM hosts h'.
913			' WHERE '.dbConditionInt('h.hostid', $hostPrototypeIds).
914			' FOR UPDATE'
915		);
916
917		$deleteHostPrototypes = $this->get([
918			'hostids' => $hostPrototypeIds,
919			'output' => ['host'],
920			'selectGroupPrototypes' => ['group_prototypeid'],
921			'selectParentHost' => ['host'],
922			'nopermissions' => true
923		]);
924
925		// delete discovered hosts
926		$discoveredHosts = DBfetchArray(DBselect(
927			'SELECT hostid FROM host_discovery WHERE '.dbConditionInt('parent_hostid', $hostPrototypeIds)
928		));
929		if ($discoveredHosts) {
930			API::Host()->delete(zbx_objectValues($discoveredHosts, 'hostid'), true);
931		}
932
933		// delete group prototypes and discovered groups
934		$groupPrototypeIds = [];
935		foreach ($deleteHostPrototypes as $groupPrototype) {
936			foreach ($groupPrototype['groupPrototypes'] as $groupPrototype) {
937				$groupPrototypeIds[] = $groupPrototype['group_prototypeid'];
938			}
939		}
940		$this->deleteGroupPrototypes($groupPrototypeIds);
941
942		// delete host prototypes
943		DB::delete($this->tableName(), ['hostid' => $hostPrototypeIds]);
944
945		// TODO: REMOVE info
946		foreach ($deleteHostPrototypes as $hostProtototype) {
947			info(_s('Deleted: Host prototype "%1$s" on "%2$s".', $hostProtototype['host'], $hostProtototype['parentHost']['host']));
948		}
949
950		return ['hostids' => $hostPrototypeIds];
951	}
952
953	protected function link(array $templateids, array $targetids) {
954		$this->checkHostPrototypePermissions($targetids);
955
956		$links = parent::link($templateids, $targetids);
957
958		foreach ($targetids as $targetid) {
959			$linked_templates = API::Template()->get([
960				'output' => [],
961				'hostids' => [$targetid],
962				'nopermissions' => true
963			]);
964
965			$result = DBselect(
966				'SELECT i.key_,count(*)'.
967				' FROM items i'.
968				' WHERE '.dbConditionInt('i.hostid', array_merge($templateids, array_keys($linked_templates))).
969				' GROUP BY i.key_'.
970				' HAVING count(*)>1',
971				1
972			);
973			if ($row = DBfetch($result)) {
974				$target_templates = API::HostPrototype()->get([
975					'output' => ['name'],
976					'hostids' => [$targetid],
977					'nopermissions' => true
978				]);
979
980				self::exception(ZBX_API_ERROR_PARAMETERS,
981					_s('Item "%1$s" already exists on "%2$s", inherited from another template.', $row['key_'],
982						$target_templates[0]['name']
983					)
984				);
985			}
986		}
987
988		return $links;
989	}
990
991	/**
992	 * Checks if the current user has access to the given LLD rules.
993	 *
994	 * @throws APIException if the user doesn't have write permissions for the given LLD rules
995	 *
996	 * @param array $ruleids
997	 */
998	protected function checkDiscoveryRulePermissions(array $ruleids) {
999		$count = API::DiscoveryRule()->get([
1000			'countOutput' => true,
1001			'itemids' => $ruleids,
1002			'editable' => true
1003		]);
1004
1005		if ($count != count($ruleids)) {
1006			self::exception(ZBX_API_ERROR_PERMISSIONS, _('No permissions to referred object or it does not exist!'));
1007		}
1008	}
1009
1010	/**
1011	 * Checks if the current user has access to the given host groups.
1012	 *
1013	 * @throws APIException if the user doesn't have write permissions for the given host groups
1014	 *
1015	 * @param array $groupids
1016	 */
1017	protected function checkHostGroupsPermissions(array $groupids) {
1018		$db_groups = API::HostGroup()->get([
1019			'output' => ['name', 'flags'],
1020			'groupids' => $groupids,
1021			'editable' => true,
1022			'preservekeys' => true
1023		]);
1024
1025		foreach ($groupids as $groupid) {
1026			if (!array_key_exists($groupid, $db_groups)) {
1027				self::exception(ZBX_API_ERROR_PERMISSIONS,
1028					_('No permissions to referred object or it does not exist!')
1029				);
1030			}
1031
1032			$db_group = $db_groups[$groupid];
1033
1034			// Check if group prototypes use discovered host groups.
1035			if ($db_group['flags'] == ZBX_FLAG_DISCOVERY_CREATED) {
1036				self::exception(ZBX_API_ERROR_PARAMETERS,
1037					_s('Group prototype cannot be based on a discovered host group "%1$s".', $db_group['name'])
1038				);
1039			}
1040		}
1041	}
1042
1043	/**
1044	 * Checks if the current user has access to the given host prototypes.
1045	 *
1046	 * @throws APIException if the user doesn't have write permissions for the host prototypes.
1047	 *
1048	 * @param array $hostPrototypeIds
1049	 */
1050	protected function checkHostPrototypePermissions(array $hostPrototypeIds) {
1051		if ($hostPrototypeIds) {
1052			$hostPrototypeIds = array_unique($hostPrototypeIds);
1053
1054			$count = $this->get([
1055				'countOutput' => true,
1056				'hostids' => $hostPrototypeIds,
1057				'editable' => true
1058			]);
1059
1060			if ($count != count($hostPrototypeIds)) {
1061				self::exception(ZBX_API_ERROR_PERMISSIONS,
1062					_('No permissions to referred object or it does not exist!')
1063				);
1064			}
1065		}
1066	}
1067
1068	/**
1069	 * Checks if the given host prototypes are not inherited from a template.
1070	 *
1071	 * @throws APIException 	if at least one host prototype is inherited
1072	 *
1073	 * @param array $hostPrototypeIds
1074	 */
1075	protected function checkNotInherited(array $hostPrototypeIds) {
1076		$query = DBSelect('SELECT hostid FROM hosts h WHERE h.templateid>0 AND '.dbConditionInt('h.hostid', $hostPrototypeIds), 1);
1077
1078		if ($hostPrototype = DBfetch($query)) {
1079			self::exception(ZBX_API_ERROR_PERMISSIONS, _('Cannot delete templated host prototype.'));
1080		}
1081	}
1082
1083	protected function applyQueryFilterOptions($tableName, $tableAlias, array $options, array $sqlParts) {
1084		$sqlParts = parent::applyQueryFilterOptions($tableName, $tableAlias, $options, $sqlParts);
1085
1086		// do not return host prototypes from discovered hosts
1087		$sqlParts['from'][] = 'host_discovery hd';
1088		$sqlParts['from'][] = 'items i';
1089		$sqlParts['from'][] = 'hosts ph';
1090		$sqlParts['where'][] = $this->fieldId('hostid').'=hd.hostid';
1091		$sqlParts['where'][] = 'hd.parent_itemid=i.itemid';
1092		$sqlParts['where'][] = 'i.hostid=ph.hostid';
1093		$sqlParts['where'][] = 'ph.flags='.ZBX_FLAG_DISCOVERY_NORMAL;
1094
1095		if (self::$userData['type'] != USER_TYPE_SUPER_ADMIN && !$options['nopermissions']) {
1096			$permission = $options['editable'] ? PERM_READ_WRITE : PERM_READ;
1097
1098			$sqlParts['where'][] = 'EXISTS ('.
1099				'SELECT NULL'.
1100				' FROM '.
1101					'host_discovery hd,items i,hosts_groups hgg'.
1102					' JOIN rights r'.
1103						' ON r.id=hgg.groupid'.
1104						' AND '.dbConditionInt('r.groupid', getUserGroupsByUserId(self::$userData['userid'])).
1105				' WHERE h.hostid=hd.hostid'.
1106					' AND hd.parent_itemid=i.itemid'.
1107					' AND i.hostid=hgg.hostid'.
1108				' GROUP BY hgg.hostid'.
1109				' HAVING MIN(r.permission)>'.PERM_DENY.
1110				' AND MAX(r.permission)>='.zbx_dbstr($permission).
1111				')';
1112		}
1113
1114		// discoveryids
1115		if ($options['discoveryids'] !== null) {
1116			$sqlParts['where'][] = dbConditionInt('hd.parent_itemid', (array) $options['discoveryids']);
1117
1118			if ($options['groupCount']) {
1119				$sqlParts['group']['hd'] = 'hd.parent_itemid';
1120			}
1121		}
1122
1123		// inherited
1124		if ($options['inherited'] !== null) {
1125			$sqlParts['where'][] = ($options['inherited']) ? 'h.templateid IS NOT NULL' : 'h.templateid IS NULL';
1126		}
1127
1128		return $sqlParts;
1129	}
1130
1131	/**
1132	 * Retrieves and adds additional requested data to the result set.
1133	 *
1134	 * @param array  $options
1135	 * @param array  $result
1136	 *
1137	 * @return array
1138	 */
1139	protected function addRelatedObjects(array $options, array $result) {
1140		$result = parent::addRelatedObjects($options, $result);
1141
1142		$hostPrototypeIds = array_keys($result);
1143
1144		// adding discovery rule
1145		if ($options['selectDiscoveryRule'] !== null && $options['selectDiscoveryRule'] != API_OUTPUT_COUNT) {
1146			$relationMap = $this->createRelationMap($result, 'hostid', 'parent_itemid', 'host_discovery');
1147			$discoveryRules = API::DiscoveryRule()->get([
1148				'output' => $options['selectDiscoveryRule'],
1149				'itemids' => $relationMap->getRelatedIds(),
1150				'nopermissions' => true,
1151				'preservekeys' => true
1152			]);
1153			$result = $relationMap->mapOne($result, $discoveryRules, 'discoveryRule');
1154		}
1155
1156		// adding group links
1157		if ($options['selectGroupLinks'] !== null && $options['selectGroupLinks'] != API_OUTPUT_COUNT) {
1158			$groupPrototypes = DBFetchArray(DBselect(
1159				'SELECT hg.group_prototypeid,hg.hostid'.
1160					' FROM group_prototype hg'.
1161					' WHERE '.dbConditionInt('hg.hostid', $hostPrototypeIds).
1162					' AND hg.groupid IS NOT NULL'
1163			));
1164			$relationMap = $this->createRelationMap($groupPrototypes, 'hostid', 'group_prototypeid');
1165			$groupPrototypes = API::getApiService()->select('group_prototype', [
1166				'output' => $options['selectGroupLinks'],
1167				'group_prototypeids' => $relationMap->getRelatedIds(),
1168				'preservekeys' => true
1169			]);
1170			foreach ($groupPrototypes as &$groupPrototype) {
1171				unset($groupPrototype['name']);
1172			}
1173			unset($groupPrototype);
1174			$result = $relationMap->mapMany($result, $groupPrototypes, 'groupLinks');
1175		}
1176
1177		// adding group prototypes
1178		if ($options['selectGroupPrototypes'] !== null && $options['selectGroupPrototypes'] != API_OUTPUT_COUNT) {
1179			$groupPrototypes = DBFetchArray(DBselect(
1180				'SELECT hg.group_prototypeid,hg.hostid'.
1181				' FROM group_prototype hg'.
1182				' WHERE '.dbConditionInt('hg.hostid', $hostPrototypeIds).
1183					' AND hg.groupid IS NULL'
1184			));
1185			$relationMap = $this->createRelationMap($groupPrototypes, 'hostid', 'group_prototypeid');
1186			$groupPrototypes = API::getApiService()->select('group_prototype', [
1187				'output' => $options['selectGroupPrototypes'],
1188				'group_prototypeids' => $relationMap->getRelatedIds(),
1189				'preservekeys' => true
1190			]);
1191			foreach ($groupPrototypes as &$groupPrototype) {
1192				unset($groupPrototype['groupid']);
1193			}
1194			unset($groupPrototype);
1195			$result = $relationMap->mapMany($result, $groupPrototypes, 'groupPrototypes');
1196		}
1197
1198		// adding host
1199		if ($options['selectParentHost'] !== null && $options['selectParentHost'] != API_OUTPUT_COUNT) {
1200			$relationMap = new CRelationMap();
1201			$dbRules = DBselect(
1202				'SELECT hd.hostid,i.hostid AS parent_hostid'.
1203					' FROM host_discovery hd,items i'.
1204					' WHERE '.dbConditionInt('hd.hostid', $hostPrototypeIds).
1205					' AND hd.parent_itemid=i.itemid'
1206			);
1207			while ($relation = DBfetch($dbRules)) {
1208				$relationMap->addRelation($relation['hostid'], $relation['parent_hostid']);
1209			}
1210
1211			$hosts = API::Host()->get([
1212				'output' => $options['selectParentHost'],
1213				'hostids' => $relationMap->getRelatedIds(),
1214				'templated_hosts' => true,
1215				'nopermissions' => true,
1216				'preservekeys' => true
1217			]);
1218			$result = $relationMap->mapOne($result, $hosts, 'parentHost');
1219		}
1220
1221		// adding templates
1222		if ($options['selectTemplates'] !== null) {
1223			if ($options['selectTemplates'] != API_OUTPUT_COUNT) {
1224				$relationMap = $this->createRelationMap($result, 'hostid', 'templateid', 'hosts_templates');
1225				$templates = API::Template()->get([
1226					'output' => $options['selectTemplates'],
1227					'templateids' => $relationMap->getRelatedIds(),
1228					'preservekeys' => true
1229				]);
1230				$result = $relationMap->mapMany($result, $templates, 'templates');
1231			}
1232			else {
1233				$templates = API::Template()->get([
1234					'hostids' => $hostPrototypeIds,
1235					'countOutput' => true,
1236					'groupCount' => true
1237				]);
1238				$templates = zbx_toHash($templates, 'hostid');
1239				foreach ($result as $hostid => $host) {
1240					$result[$hostid]['templates'] = array_key_exists($hostid, $templates)
1241						? $templates[$hostid]['rowscount']
1242						: '0';
1243				}
1244			}
1245		}
1246
1247		// adding inventory
1248		if ($options['selectInventory'] !== null) {
1249			$inventory = API::getApiService()->select('host_inventory', [
1250				'output' => ['hostid', 'inventory_mode'],
1251				'filter' => ['hostid' => $hostPrototypeIds],
1252				'preservekeys' => true
1253			]);
1254
1255			foreach ($hostPrototypeIds as $host_prototypeid) {
1256				// There is no DB record if inventory mode is HOST_INVENTORY_DISABLED.
1257				if (!array_key_exists($host_prototypeid, $inventory)) {
1258					$inventory[$host_prototypeid] = [
1259						'hostid' => (string) $host_prototypeid,
1260						'inventory_mode' => (string) HOST_INVENTORY_DISABLED
1261					];
1262				}
1263			}
1264
1265			$relation_map = $this->createRelationMap($result, 'hostid', 'hostid');
1266			$inventory = $this->unsetExtraFields($inventory, ['hostid', 'inventory_mode'], $options['selectInventory']);
1267			$result = $relation_map->mapOne($result, $inventory, 'inventory');
1268		}
1269
1270		return $result;
1271	}
1272
1273	/**
1274	 * Deletes the given group prototype and all discovered groups.
1275	 * Deletes also group prototype children.
1276	 *
1277	 * @param array $groupPrototypeIds
1278	 */
1279	protected function deleteGroupPrototypes(array $groupPrototypeIds) {
1280		// Lock group prototypes before delete to prevent server from adding new LLD elements.
1281		DBselect(
1282			'SELECT NULL'.
1283			' FROM group_prototype gp'.
1284			' WHERE '.dbConditionInt('gp.group_prototypeid', $groupPrototypeIds).
1285			' FOR UPDATE'
1286		);
1287
1288		// delete child group prototypes
1289		$groupPrototypeChildren = DBfetchArray(DBselect(
1290			'SELECT gp.group_prototypeid FROM group_prototype gp WHERE '.dbConditionInt('templateid', $groupPrototypeIds)
1291		));
1292		if ($groupPrototypeChildren) {
1293			$this->deleteGroupPrototypes(zbx_objectValues($groupPrototypeChildren, 'group_prototypeid'));
1294		}
1295
1296		// delete discovered groups
1297		$hostGroups = DBfetchArray(DBselect(
1298			'SELECT groupid FROM group_discovery WHERE '.dbConditionInt('parent_group_prototypeid', $groupPrototypeIds)
1299		));
1300		if ($hostGroups) {
1301			API::HostGroup()->delete(zbx_objectValues($hostGroups, 'groupid'), true);
1302		}
1303
1304		// delete group prototypes
1305		DB::delete('group_prototype', ['group_prototypeid' => $groupPrototypeIds]);
1306	}
1307}
1308