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