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	 * @param string|array  $options['selectInterfaces']  Return an "interfaces" property with host interfaces.
35	 *
36	 * @return array
37	 */
38	public function get(array $options) {
39		$hosts_fields = array_keys($this->getTableSchema('hosts')['fields']);
40		$output_fields = ['hostid', 'host', 'name', 'status', 'templateid', 'inventory_mode', 'discover',
41			'custom_interfaces', 'uuid'
42		];
43		$link_fields = ['group_prototypeid', 'groupid', 'hostid', 'templateid'];
44		$group_fields = ['group_prototypeid', 'name', 'hostid', 'templateid'];
45		$discovery_fields = array_keys($this->getTableSchema('items')['fields']);
46		$hostmacro_fields = array_keys($this->getTableSchema('hostmacro')['fields']);
47		$interface_fields = ['type', 'useip', 'ip', 'dns', 'port', 'main', 'details'];
48
49		$api_input_rules = ['type' => API_OBJECT, 'fields' => [
50			// filter
51			'hostids' =>				['type' => API_IDS, 'flags' => API_ALLOW_NULL | API_NORMALIZE, 'default' => null],
52			'discoveryids' =>			['type' => API_IDS, 'flags' => API_ALLOW_NULL | API_NORMALIZE, 'default' => null],
53			'filter' =>					['type' => API_OBJECT, 'flags' => API_ALLOW_NULL, 'default' => null, 'fields' => [
54				'hostid' =>					['type' => API_IDS, 'flags' => API_ALLOW_NULL | API_NORMALIZE],
55				'host' =>					['type' => API_STRINGS_UTF8, 'flags' => API_ALLOW_NULL | API_NORMALIZE],
56				'name' =>					['type' => API_STRINGS_UTF8, 'flags' => API_ALLOW_NULL | API_NORMALIZE],
57				'status' =>					['type' => API_INTS32, 'flags' => API_ALLOW_NULL | API_NORMALIZE, 'in' => implode(',', [HOST_STATUS_MONITORED, HOST_STATUS_NOT_MONITORED])],
58				'templateid' =>				['type' => API_IDS, 'flags' => API_ALLOW_NULL | API_NORMALIZE],
59				'inventory_mode' =>			['type' => API_INTS32, 'flags' => API_ALLOW_NULL | API_NORMALIZE, 'in' => implode(',', [HOST_INVENTORY_DISABLED, HOST_INVENTORY_MANUAL, HOST_INVENTORY_AUTOMATIC])]
60			]],
61			'search' =>					['type' => API_OBJECT, 'flags' => API_ALLOW_NULL, 'default' => null, 'fields' => [
62				'host' =>					['type' => API_STRINGS_UTF8, 'flags' => API_ALLOW_NULL | API_NORMALIZE],
63				'name' =>					['type' => API_STRINGS_UTF8, 'flags' => API_ALLOW_NULL | API_NORMALIZE]
64			]],
65			'searchByAny' =>			['type' => API_BOOLEAN, 'default' => false],
66			'startSearch' =>			['type' => API_FLAG, 'default' => false],
67			'excludeSearch' =>			['type' => API_FLAG, 'default' => false],
68			'searchWildcardsEnabled' =>	['type' => API_BOOLEAN, 'default' => false],
69			// output
70			'output' =>					['type' => API_OUTPUT, 'in' => 'inventory_mode,'.implode(',', $output_fields), 'default' => $output_fields],
71			'countOutput' =>			['type' => API_FLAG, 'default' => false],
72			'groupCount' =>				['type' => API_FLAG, 'default' => false],
73			'selectGroupLinks' =>		['type' => API_OUTPUT, 'flags' => API_ALLOW_NULL, 'in' => implode(',', $link_fields), 'default' => null],
74			'selectGroupPrototypes' =>	['type' => API_OUTPUT, 'flags' => API_ALLOW_NULL, 'in' => implode(',', $group_fields), 'default' => null],
75			'selectDiscoveryRule' =>	['type' => API_OUTPUT, 'flags' => API_ALLOW_NULL, 'in' => implode(',', $discovery_fields), 'default' => null],
76			'selectParentHost' =>		['type' => API_OUTPUT, 'flags' => API_ALLOW_NULL, 'in' => implode(',', $hosts_fields), 'default' => null],
77			'selectInterfaces' =>		['type' => API_OUTPUT, 'flags' => API_ALLOW_NULL, 'in' => implode(',', $interface_fields), 'default' => null],
78			'selectTemplates' =>		['type' => API_OUTPUT, 'flags' => API_ALLOW_NULL | API_ALLOW_COUNT, 'in' => implode(',', $hosts_fields), 'default' => null],
79			'selectMacros' =>			['type' => API_OUTPUT, 'flags' => API_ALLOW_NULL, 'in' => implode(',', $hostmacro_fields), 'default' => null],
80			'selectTags' =>				['type' => API_OUTPUT, 'flags' => API_ALLOW_NULL, 'in' => implode(',', ['tag', 'value']), 'default' => null],
81			// sort and limit
82			'sortfield' =>				['type' => API_STRINGS_UTF8, 'flags' => API_NORMALIZE, 'in' => implode(',', $this->sortColumns), 'uniq' => true, 'default' => []],
83			'sortorder' =>				['type' => API_SORTORDER, 'default' => []],
84			'limit' =>					['type' => API_INT32, 'flags' => API_ALLOW_NULL, 'in' => '1:'.ZBX_MAX_INT32, 'default' => null],
85			// flags
86			'inherited'	=>				['type' => API_BOOLEAN, 'flags' => API_ALLOW_NULL, 'default' => null],
87			'editable' =>				['type' => API_BOOLEAN, 'default' => false],
88			'preservekeys' =>			['type' => API_BOOLEAN, 'default' => false],
89			'nopermissions' =>			['type' => API_BOOLEAN, 'default' => false]	// TODO: This property and frontend usage SHOULD BE removed.
90		]];
91		if (!CApiInputValidator::validate($api_input_rules, $options, '/', $error)) {
92			self::exception(ZBX_API_ERROR_PARAMETERS, $error);
93		}
94
95		$options['filter']['flags'] = ZBX_FLAG_DISCOVERY_PROTOTYPE;
96
97		if ($options['output'] === API_OUTPUT_EXTEND) {
98			$options['output'] = $output_fields;
99		}
100
101		// build and execute query
102		$sql = $this->createSelectQuery($this->tableName(), $options);
103		$res = DBselect($sql, $options['limit']);
104
105		// fetch results
106		$result = [];
107		while ($row = DBfetch($res)) {
108			// a count query, return a single result
109			if ($options['countOutput']) {
110				if ($options['groupCount']) {
111					$result[] = $row;
112				}
113				else {
114					$result = $row['rowscount'];
115				}
116			}
117			// a normal select query
118			else {
119				$result[$row[$this->pk()]] = $row;
120			}
121		}
122
123		if ($options['countOutput']) {
124			return $result;
125		}
126
127		if ($result) {
128			$result = $this->addRelatedObjects($options, $result);
129			$result = $this->unsetExtraFields($result, ['triggerid'], $options['output']);
130		}
131
132		if (!$options['preservekeys']) {
133			$result = zbx_cleanHashes($result);
134		}
135
136		return $result;
137	}
138
139	protected function applyQueryOutputOptions($tableName, $tableAlias, array $options, array $sqlParts) {
140		$sqlParts = parent::applyQueryOutputOptions($tableName, $tableAlias, $options, $sqlParts);
141
142		if (!$options['countOutput'] && $this->outputIsRequested('inventory_mode', $options['output'])) {
143			$sqlParts['select']['inventory_mode'] =
144				dbConditionCoalesce('hinv.inventory_mode', HOST_INVENTORY_DISABLED, 'inventory_mode');
145		}
146
147		if ((!$options['countOutput'] && $this->outputIsRequested('inventory_mode', $options['output']))
148				|| ($options['filter'] && array_key_exists('inventory_mode', $options['filter']))) {
149			$sqlParts['left_join'][] = ['alias' => 'hinv', 'table' => 'host_inventory', 'using' => 'hostid'];
150			$sqlParts['left_table'] = ['alias' => $this->tableAlias, 'table' => $this->tableName];
151		}
152
153		return $sqlParts;
154	}
155
156
157	/**
158	 * Check for duplicated names.
159	 *
160	 * @param string $field_name
161	 * @param array  $names_by_ruleid
162	 *
163	 * @throws APIException  if host prototype with same name already exists.
164	 */
165	private function checkDuplicates($field_name, array $names_by_ruleid) {
166		$sql_where = [];
167		foreach ($names_by_ruleid as $ruleid => $names) {
168			$sql_where[] = '(i.itemid='.$ruleid.' AND '.dbConditionString('h.'.$field_name, $names).')';
169		}
170
171		$db_host_prototypes = DBfetchArray(DBselect(
172				'SELECT i.name AS rule,h.'.$field_name.
173				' FROM items i,host_discovery hd,hosts h'.
174				' WHERE i.itemid=hd.parent_itemid'.
175					' AND hd.hostid=h.hostid'.
176					' AND ('.implode(' OR ', $sql_where).')',
177				1
178		));
179
180		if ($db_host_prototypes) {
181			$error = ($field_name === 'host')
182				? _('Host prototype with host name "%1$s" already exists in discovery rule "%2$s".')
183				: _('Host prototype with visible name "%1$s" already exists in discovery rule "%2$s".');
184
185			self::exception(ZBX_API_ERROR_PARAMETERS,
186				sprintf($error, $db_host_prototypes[0][$field_name], $db_host_prototypes[0]['rule'])
187			);
188		}
189	}
190
191	/**
192	 * Validates the input parameters for the create() method.
193	 *
194	 * @param array $host_prototypes
195	 *
196	 * @throws APIException if the input is invalid.
197	 */
198	protected function validateCreate(array &$host_prototypes) {
199		$api_input_rules = ['type' => API_OBJECTS, 'flags' => API_NOT_EMPTY | API_NORMALIZE, 'uniq' => [['uuid'], ['ruleid', 'host'], ['ruleid', 'name']], 'fields' => [
200			'uuid' =>				['type' => API_UUID],
201			'ruleid' =>				['type' => API_ID, 'flags' => API_REQUIRED],
202			'host' =>				['type' => API_H_NAME, 'flags' => API_REQUIRED | API_REQUIRED_LLD_MACRO, 'length' => DB::getFieldLength('hosts', 'host')],
203			'name' =>				['type' => API_STRING_UTF8, 'flags' => API_NOT_EMPTY, 'length' => DB::getFieldLength('hosts', 'name'), 'default_source' => 'host'],
204			'custom_interfaces' =>	['type' => API_INT32, 'in' => implode(',', [HOST_PROT_INTERFACES_INHERIT, HOST_PROT_INTERFACES_CUSTOM]), 'default' => HOST_PROT_INTERFACES_INHERIT],
205			'status' =>				['type' => API_INT32, 'in' => implode(',', [HOST_STATUS_MONITORED, HOST_STATUS_NOT_MONITORED])],
206			'discover' =>			['type' => API_INT32, 'in' => implode(',', [HOST_DISCOVER, HOST_NO_DISCOVER])],
207			'interfaces' =>			['type' => API_OBJECTS, 'flags' => API_NORMALIZE, 'fields' => [
208				'type' =>				['type' => API_INT32, 'flags' => API_REQUIRED, 'in' => implode(',', [INTERFACE_TYPE_AGENT, INTERFACE_TYPE_SNMP, INTERFACE_TYPE_IPMI, INTERFACE_TYPE_JMX])],
209				'useip' => 				['type' => API_INT32, 'flags' => API_REQUIRED, 'in' => implode(',', [INTERFACE_USE_DNS, INTERFACE_USE_IP])],
210				'ip' => 				['type' => API_IP, 'flags' => API_ALLOW_USER_MACRO | API_ALLOW_LLD_MACRO | API_ALLOW_MACRO, 'length' => DB::getFieldLength('interface', 'ip')],
211				'dns' =>				['type' => API_DNS, 'flags' => API_ALLOW_USER_MACRO | API_ALLOW_LLD_MACRO | API_ALLOW_MACRO, 'length' => DB::getFieldLength('interface', 'dns')],
212				'port' =>				['type' => API_PORT, 'flags' => API_REQUIRED | API_NOT_EMPTY | API_ALLOW_USER_MACRO | API_ALLOW_LLD_MACRO, 'length' => DB::getFieldLength('interface', 'port')],
213				'main' => 				['type' => API_INT32, 'flags' => API_REQUIRED, 'in' => implode(',', [INTERFACE_SECONDARY, INTERFACE_PRIMARY])],
214				'details' =>			['type' => API_MULTIPLE, 'rules' => [
215											['if' => ['field' => 'type', 'in' => (string) INTERFACE_TYPE_SNMP], 'type' => API_OBJECT, 'fields' => [
216					'version' =>				['type' => API_INT32, 'flags' => API_REQUIRED, 'in' => implode(',', [SNMP_V1, SNMP_V2C, SNMP_V3])],
217					'bulk' =>					['type' => API_INT32, 'in' => implode(',', [SNMP_BULK_DISABLED, SNMP_BULK_ENABLED])],
218					'community' =>				['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('interface_snmp', 'community')],
219					'securityname' =>			['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('interface_snmp', 'securityname')],
220					'securitylevel' =>			['type' => API_INT32, 'in' => implode(',', [ITEM_SNMPV3_SECURITYLEVEL_NOAUTHNOPRIV, ITEM_SNMPV3_SECURITYLEVEL_AUTHNOPRIV, ITEM_SNMPV3_SECURITYLEVEL_AUTHPRIV])],
221					'authpassphrase' =>			['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('interface_snmp', 'authpassphrase')],
222					'privpassphrase' =>			['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('interface_snmp', 'privpassphrase')],
223					'authprotocol' =>			['type' => API_INT32, 'in' => implode(',', array_keys(getSnmpV3AuthProtocols()))],
224					'privprotocol' =>			['type' => API_INT32, 'in' => implode(',', array_keys(getSnmpV3PrivProtocols()))],
225					'contextname' =>			['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('interface_snmp', 'contextname')]
226											]],
227											['if' => ['field' => 'type', 'in' => implode(',', [INTERFACE_TYPE_AGENT, INTERFACE_TYPE_IPMI, INTERFACE_TYPE_JMX])], 'type' => API_OBJECT, 'fields' => []]
228				]]
229			]],
230			'groupLinks' =>			['type' => API_OBJECTS, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'uniq' => [['groupid']], 'fields' => [
231				'groupid' =>			['type' => API_ID, 'flags' => API_REQUIRED]
232			]],
233			'groupPrototypes' =>	['type' => API_OBJECTS, 'uniq' => [['name']], 'fields' => [
234				'name' =>				['type' => API_HG_NAME, 'flags' => API_REQUIRED | API_REQUIRED_LLD_MACRO, 'length' => DB::getFieldLength('hstgrp', 'name')]
235			]],
236			'templates' =>			['type' => API_OBJECTS, 'flags' => API_NORMALIZE, 'uniq' => [['templateid']], 'fields' => [
237				'templateid' =>			['type' => API_ID, 'flags' => API_REQUIRED]
238			]],
239			'tags' =>				['type' => API_OBJECTS, 'flags' => API_NORMALIZE, 'uniq' => [['tag', 'value']], 'fields' => [
240				'tag' =>				['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('host_tag', 'tag')],
241				'value' =>				['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('host_tag', 'value'), 'default' => DB::getDefault('host_tag', 'value')]
242			]],
243			'macros' =>				['type' => API_OBJECTS, 'flags' => API_NORMALIZE, 'uniq' => [['macro']], 'fields' => [
244				'macro' =>			['type' => API_USER_MACRO, 'flags' => API_REQUIRED, 'length' => DB::getFieldLength('hostmacro', 'macro')],
245				'type' =>			['type' => API_INT32, 'in' => implode(',', [ZBX_MACRO_TYPE_TEXT, ZBX_MACRO_TYPE_SECRET, ZBX_MACRO_TYPE_VAULT]), 'default' => ZBX_MACRO_TYPE_TEXT],
246				'value' =>			['type' => API_MULTIPLE, 'flags' => API_REQUIRED, 'rules' => [
247										['if' => ['field' => 'type', 'in' => implode(',', [ZBX_MACRO_TYPE_TEXT, ZBX_MACRO_TYPE_SECRET])], 'type' => API_STRING_UTF8, 'length' => DB::getFieldLength('hostmacro', 'value')],
248										['if' => ['field' => 'type', 'in' => implode(',', [ZBX_MACRO_TYPE_VAULT])], 'type' => API_VAULT_SECRET, 'length' => DB::getFieldLength('hostmacro', 'value')]
249				]],
250				'description' =>	['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('hostmacro', 'description')]
251			]],
252			'inventory_mode' =>		['type' => API_INT32, 'in' => implode(',', [HOST_INVENTORY_DISABLED, HOST_INVENTORY_MANUAL, HOST_INVENTORY_AUTOMATIC])]
253		]];
254
255		if (!CApiInputValidator::validate($api_input_rules, $host_prototypes, '/', $error)) {
256			self::exception(ZBX_API_ERROR_PARAMETERS, $error);
257		}
258
259		$hosts_by_ruleid = [];
260		$names_by_ruleid = [];
261		$groupids = [];
262
263		foreach ($host_prototypes as $host_prototype) {
264			// Collect host group ID links for latter validation.
265			foreach ($host_prototype['groupLinks'] as $group_prototype) {
266				$groupids[$group_prototype['groupid']] = true;
267			}
268
269			$hosts_by_ruleid[$host_prototype['ruleid']][] = $host_prototype['host'];
270			$names_by_ruleid[$host_prototype['ruleid']][] = $host_prototype['name'];
271		}
272
273		$ruleids = array_unique(zbx_objectValues($host_prototypes, 'ruleid'));
274		$groupids = array_keys($groupids);
275
276		$this->checkDiscoveryRulePermissions($ruleids);
277		$this->checkHostGroupsPermissions($groupids);
278
279		// Check if the host is discovered.
280		$db_discovered_hosts = DBfetchArray(DBselect(
281			'SELECT h.host'.
282			' FROM items i,hosts h'.
283			' WHERE i.hostid=h.hostid'.
284				' AND '.dbConditionInt('i.itemid', $ruleids).
285				' AND h.flags='.ZBX_FLAG_DISCOVERY_CREATED,
286			1
287		));
288
289		if ($db_discovered_hosts) {
290			self::exception(ZBX_API_ERROR_PARAMETERS,
291				_s('Cannot create a host prototype on a discovered host "%1$s".', $db_discovered_hosts[0]['host'])
292			);
293		}
294
295		$this->validateInterfaces($host_prototypes);
296
297		$this->checkAndAddUuid($host_prototypes);
298		$this->checkDuplicates('host', $hosts_by_ruleid);
299		$this->checkDuplicates('name', $names_by_ruleid);
300	}
301
302	/**
303	 * Check that only host prototypes on templates have UUID. Add UUID to all host prototypes on templates,
304	 *   if it doesn't exist.
305	 *
306	 * @param array $host_prototypes_to_create
307	 *
308	 * @throws APIException
309	 */
310	protected function checkAndAddUuid(array &$host_prototypes_to_create): void {
311		$discovery_ruleids = array_flip(array_column($host_prototypes_to_create, 'ruleid'));
312
313		$db_templated_rules = DBfetchArrayAssoc(DBselect(
314			'SELECT i.itemid, h.status'.
315			' FROM items i, hosts h'.
316			' WHERE '.dbConditionInt('i.itemid', array_keys($discovery_ruleids)).
317			' AND i.hostid=h.hostid'.
318			' AND h.status = ' . HOST_STATUS_TEMPLATE
319		), 'itemid');
320
321		foreach ($host_prototypes_to_create as $index => &$host_prototype) {
322			if (!array_key_exists($host_prototype['ruleid'], $db_templated_rules)
323					&& array_key_exists('uuid', $host_prototype)) {
324				self::exception(ZBX_API_ERROR_PARAMETERS,
325					_s('Invalid parameter "%1$s": %2$s.', '/' . ($index + 1), _s('unexpected parameter "%1$s"', 'uuid'))
326				);
327			}
328
329			if (array_key_exists($host_prototype['ruleid'], $db_templated_rules)
330					&& !array_key_exists('uuid', $host_prototype)) {
331				$host_prototype['uuid'] = generateUuidV4();
332			}
333		}
334		unset($host_prototype);
335
336		$db_uuid = DB::select('hosts', [
337			'output' => ['uuid'],
338			'filter' => ['uuid' => array_column($host_prototypes_to_create, 'uuid')],
339			'limit' => 1
340		]);
341
342		if ($db_uuid) {
343			self::exception(ZBX_API_ERROR_PARAMETERS,
344				_s('Entry with UUID "%1$s" already exists.', $db_uuid[0]['uuid'])
345			);
346		}
347	}
348
349	/**
350	 * Creates the given host prototypes.
351	 *
352	 * @param array $host_prototypes
353	 *
354	 * @return array
355	 */
356	public function create(array $host_prototypes) {
357		// 'templateid' validation happens during linkage.
358		$this->validateCreate($host_prototypes);
359
360		// Merge groups into group prototypes.
361		foreach ($host_prototypes as &$host_prototype) {
362			$host_prototype['groupPrototypes'] = array_merge(
363				array_key_exists('groupPrototypes', $host_prototype) ? $host_prototype['groupPrototypes'] : [],
364				$host_prototype['groupLinks']
365			);
366			unset($host_prototype['groupLinks']);
367		}
368		unset($host_prototype);
369
370		$this->createReal($host_prototypes);
371		$this->inherit($host_prototypes);
372
373		$this->addAuditBulk(AUDIT_ACTION_ADD, AUDIT_RESOURCE_HOST_PROTOTYPE, $host_prototypes);
374
375		return ['hostids' => zbx_objectValues($host_prototypes, 'hostid')];
376	}
377
378	/**
379	 * Creates the host prototypes and inherits them to linked hosts and templates.
380	 *
381	 * @param array $hostPrototypes
382	 */
383	protected function createReal(array &$hostPrototypes) {
384		foreach ($hostPrototypes as &$hostPrototype) {
385			$hostPrototype['flags'] = ZBX_FLAG_DISCOVERY_PROTOTYPE;
386		}
387		unset($hostPrototype);
388
389		// save the host prototypes
390		$hostPrototypeIds = DB::insert($this->tableName(), $hostPrototypes);
391
392		$groupPrototypes = [];
393		$hostPrototypeDiscoveryRules = [];
394		$hostPrototypeInventory = [];
395		$hosts_tags = [];
396		foreach ($hostPrototypes as $key => &$hostPrototype) {
397			$hostPrototype['hostid'] = $hostPrototypeIds[$key];
398
399			// save group prototypes
400			foreach ($hostPrototype['groupPrototypes'] as $groupPrototype) {
401				$groupPrototype['hostid'] = $hostPrototype['hostid'];
402				$groupPrototypes[] = $groupPrototype;
403			}
404
405			// discovery rules
406			$hostPrototypeDiscoveryRules[] = [
407				'hostid' => $hostPrototype['hostid'],
408				'parent_itemid' => $hostPrototype['ruleid']
409			];
410
411			// inventory
412			if (array_key_exists('inventory_mode', $hostPrototype)
413					&& $hostPrototype['inventory_mode'] != HOST_INVENTORY_DISABLED) {
414				$hostPrototypeInventory[] = [
415					'hostid' => $hostPrototype['hostid'],
416					'inventory_mode' => $hostPrototype['inventory_mode']
417				];
418			}
419
420			// tags
421			if (array_key_exists('tags', $hostPrototype)) {
422				foreach (zbx_toArray($hostPrototype['tags']) as $tag) {
423					$hosts_tags[] = ['hostid' => $hostPrototype['hostid']] + $tag;
424				}
425			}
426		}
427		unset($hostPrototype);
428
429		// save group prototypes
430		$groupPrototypes = DB::save('group_prototype', $groupPrototypes);
431		$i = 0;
432		foreach ($hostPrototypes as &$hostPrototype) {
433			foreach ($hostPrototype['groupPrototypes'] as &$groupPrototype) {
434				$groupPrototype['group_prototypeid'] = $groupPrototypes[$i]['group_prototypeid'];
435				$i++;
436			}
437			unset($groupPrototype);
438		}
439		unset($hostPrototype);
440
441		// link host prototypes to discovery rules
442		DB::insert('host_discovery', $hostPrototypeDiscoveryRules, false);
443
444		// save inventory
445		DB::insertBatch('host_inventory', $hostPrototypeInventory, false);
446
447		// save tags
448		if ($hosts_tags) {
449			DB::insert('host_tag', $hosts_tags);
450		}
451
452		// link templates
453		foreach ($hostPrototypes as $hostPrototype) {
454			if (isset($hostPrototype['templates']) && $hostPrototype['templates']) {
455				$this->link(zbx_objectValues($hostPrototype['templates'], 'templateid'), [$hostPrototype['hostid']]);
456			}
457		}
458
459		$this->createInterfaces($hostPrototypes);
460		$this->createHostMacros($hostPrototypes);
461	}
462
463	/**
464	 * Validates the input parameters for the update() method.
465	 *
466	 * @param array $host_prototypes
467	 * @param array $db_host_prototypes
468	 *
469	 * @throws APIException if the input is invalid.
470	 */
471	protected function validateUpdate(array &$host_prototypes, array &$db_host_prototypes = null) {
472		$api_input_rules = ['type' => API_OBJECTS, 'flags' => API_NOT_EMPTY | API_NORMALIZE, 'uniq' => [['hostid']], 'fields' => [
473			'hostid' =>				['type' => API_ID, 'flags' => API_REQUIRED],
474			'host' =>				['type' => API_H_NAME, 'flags' => API_REQUIRED_LLD_MACRO, 'length' => DB::getFieldLength('hosts', 'host')],
475			'name' =>				['type' => API_STRING_UTF8, 'flags' => API_NOT_EMPTY, 'length' => DB::getFieldLength('hosts', 'name')],
476			'custom_interfaces' =>	['type' => API_INT32, 'in' => implode(',', [HOST_PROT_INTERFACES_INHERIT, HOST_PROT_INTERFACES_CUSTOM])],
477			'status' =>				['type' => API_INT32, 'in' => implode(',', [HOST_STATUS_MONITORED, HOST_STATUS_NOT_MONITORED])],
478			'discover' =>			['type' => API_INT32, 'in' => implode(',', [ZBX_PROTOTYPE_DISCOVER, ZBX_PROTOTYPE_NO_DISCOVER])],
479			'interfaces' =>			['type' => API_OBJECTS, 'flags' => API_NORMALIZE, 'fields' => [
480				'type' =>				['type' => API_INT32, 'flags' => API_REQUIRED, 'in' => implode(',', [INTERFACE_TYPE_AGENT, INTERFACE_TYPE_SNMP, INTERFACE_TYPE_IPMI, INTERFACE_TYPE_JMX])],
481				'useip' => 				['type' => API_INT32, 'flags' => API_REQUIRED, 'in' => implode(',', [INTERFACE_USE_DNS, INTERFACE_USE_IP])],
482				'ip' => 				['type' => API_IP, 'flags' => API_ALLOW_USER_MACRO | API_ALLOW_LLD_MACRO | API_ALLOW_MACRO, 'length' => DB::getFieldLength('interface', 'ip')],
483				'dns' =>				['type' => API_DNS, 'flags' => API_ALLOW_USER_MACRO | API_ALLOW_LLD_MACRO | API_ALLOW_MACRO, 'length' => DB::getFieldLength('interface', 'dns')],
484				'port' =>				['type' => API_PORT, 'flags' => API_REQUIRED | API_NOT_EMPTY | API_ALLOW_USER_MACRO | API_ALLOW_LLD_MACRO, 'length' => DB::getFieldLength('interface', 'port')],
485				'main' => 				['type' => API_INT32, 'flags' => API_REQUIRED, 'in' => implode(',', [INTERFACE_SECONDARY, INTERFACE_PRIMARY])],
486				'details' =>			['type' => API_MULTIPLE, 'rules' => [
487											['if' => ['field' => 'type', 'in' => (string) INTERFACE_TYPE_SNMP], 'type' => API_OBJECT, 'fields' => [
488					'version' =>				['type' => API_INT32, 'flags' => API_REQUIRED, 'in' => implode(',', [SNMP_V1, SNMP_V2C, SNMP_V3])],
489					'bulk' =>					['type' => API_INT32, 'in' => implode(',', [SNMP_BULK_DISABLED, SNMP_BULK_ENABLED])],
490					'community' =>				['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('interface_snmp', 'community')],
491					'securityname' =>			['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('interface_snmp', 'securityname')],
492					'securitylevel' =>			['type' => API_INT32, 'in' => implode(',', [ITEM_SNMPV3_SECURITYLEVEL_NOAUTHNOPRIV, ITEM_SNMPV3_SECURITYLEVEL_AUTHNOPRIV, ITEM_SNMPV3_SECURITYLEVEL_AUTHPRIV])],
493					'authpassphrase' =>			['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('interface_snmp', 'authpassphrase')],
494					'privpassphrase' =>			['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('interface_snmp', 'privpassphrase')],
495					'authprotocol' =>			['type' => API_INT32, 'in' => implode(',', array_keys(getSnmpV3AuthProtocols()))],
496					'privprotocol' =>			['type' => API_INT32, 'in' => implode(',', array_keys(getSnmpV3PrivProtocols()))],
497					'contextname' =>			['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('interface_snmp', 'contextname')]
498											]],
499											['if' => ['field' => 'type', 'in' => implode(',', [INTERFACE_TYPE_AGENT, INTERFACE_TYPE_IPMI, INTERFACE_TYPE_JMX])], 'type' => API_OBJECT, 'fields' => []]
500				]]
501			]],
502			'groupLinks' =>			['type' => API_OBJECTS, 'flags' => API_NOT_EMPTY, 'uniq' => [['group_prototypeid'], ['groupid']], 'fields' => [
503				'group_prototypeid' =>	['type' => API_ID],
504				'groupid' =>			['type' => API_ID]
505			]],
506			'groupPrototypes' =>	['type' => API_OBJECTS, 'uniq' => [['group_prototypeid'], ['name']], 'fields' => [
507				'group_prototypeid' =>	['type' => API_ID],
508				'name' =>				['type' => API_HG_NAME, 'flags' => API_REQUIRED_LLD_MACRO, 'length' => DB::getFieldLength('hstgrp', 'name')]
509			]],
510			'templates' =>			['type' => API_OBJECTS, 'flags' => API_NORMALIZE, 'uniq' => [['templateid']], 'fields' => [
511				'templateid' =>			['type' => API_ID, 'flags' => API_REQUIRED]
512			]],
513			'tags' =>				['type' => API_OBJECTS, 'flags' => API_NORMALIZE, 'uniq' => [['tag', 'value']], 'fields' => [
514				'tag' =>				['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('host_tag', 'tag')],
515				'value' =>				['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('host_tag', 'value'), 'default' => DB::getDefault('host_tag', 'value')]
516			]],
517			'macros'  =>			['type' => API_OBJECTS, 'flags' => API_NORMALIZE, 'uniq' => [['hostmacroid']], 'fields' => [
518				'hostmacroid' =>		['type' => API_ID],
519				'macro' =>				['type' => API_USER_MACRO, 'length' => DB::getFieldLength('hostmacro', 'macro')],
520				'type' =>				['type' => API_INT32, 'in' => implode(',', [ZBX_MACRO_TYPE_TEXT, ZBX_MACRO_TYPE_SECRET, ZBX_MACRO_TYPE_VAULT])],
521				'value' =>				['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('hostmacro', 'value')],
522				'description' =>		['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('hostmacro', 'description')]
523			]],
524			'inventory_mode' =>		['type' => API_INT32, 'in' => implode(',', [HOST_INVENTORY_DISABLED, HOST_INVENTORY_MANUAL, HOST_INVENTORY_AUTOMATIC])]
525		]];
526
527		if (!CApiInputValidator::validate($api_input_rules, $host_prototypes, '/', $error)) {
528			self::exception(ZBX_API_ERROR_PARAMETERS, $error);
529		}
530
531		$db_host_prototypes = $this->get([
532			'output' => ['hostid', 'host', 'name', 'custom_interfaces', 'status'],
533			'selectDiscoveryRule' => ['itemid'],
534			'selectGroupLinks' => ['group_prototypeid', 'groupid'],
535			'selectGroupPrototypes' => ['group_prototypeid', 'name'],
536			'selectInterfaces' => ['type', 'useip', 'ip', 'dns', 'port', 'main', 'details'],
537			'hostids' => array_column($host_prototypes, 'hostid'),
538			'editable' => true,
539			'preservekeys' => true
540		]);
541
542		if (count($host_prototypes) != count($db_host_prototypes)) {
543			self::exception(ZBX_API_ERROR_PERMISSIONS, _('No permissions to referred object or it does not exist!'));
544		}
545
546		if (array_column($host_prototypes, 'macros')) {
547			$db_host_prototypes = $this->getHostMacros($db_host_prototypes);
548			$host_prototypes = $this->validateHostMacros($host_prototypes, $db_host_prototypes);
549		}
550
551		$hosts_by_ruleid = [];
552		$names_by_ruleid = [];
553
554		foreach ($host_prototypes as &$host_prototype) {
555			$db_host_prototype = $db_host_prototypes[$host_prototype['hostid']];
556			$host_prototype['ruleid'] = $db_host_prototype['discoveryRule']['itemid'];
557
558			if (array_key_exists('host', $host_prototype) && $host_prototype['host'] !== $db_host_prototype['host']) {
559				$hosts_by_ruleid[$host_prototype['ruleid']][] = $host_prototype['host'];
560			}
561
562			if (array_key_exists('name', $host_prototype) && $host_prototype['name'] !== $db_host_prototype['name']) {
563				$names_by_ruleid[$host_prototype['ruleid']][] = $host_prototype['name'];
564			}
565		}
566		unset($host_prototype);
567
568		$api_input_rules = ['type' => API_OBJECTS, 'uniq' => [['ruleid', 'host'], ['ruleid', 'name']], 'fields' => [
569			'ruleid' =>	['type' => API_ID],
570			'host' =>	['type' => API_H_NAME],
571			'name' =>	['type' => API_STRING_UTF8]
572		]];
573
574		if (!CApiInputValidator::validateUniqueness($api_input_rules, $host_prototypes, '/', $error)) {
575			self::exception(ZBX_API_ERROR_PARAMETERS, $error);
576		}
577
578		$groupids = [];
579		$db_groupids = [];
580
581		foreach ($host_prototypes as $host_prototype) {
582			$db_host_prototype = $db_host_prototypes[$host_prototype['hostid']];
583
584			foreach ($db_host_prototype['groupLinks'] as $db_group_link) {
585				$db_groupids[$db_group_link['groupid']] = true;
586			}
587
588			$db_group_links = zbx_toHash($db_host_prototype['groupLinks'], 'group_prototypeid');
589			$db_group_prototypes = zbx_toHash($db_host_prototype['groupPrototypes'], 'group_prototypeid');
590
591			// Validate 'group_prototypeid' in 'groupLinks' property.
592			if (array_key_exists('groupLinks', $host_prototype)) {
593				foreach ($host_prototype['groupLinks'] as $group_link) {
594					if (!$group_link) {
595						self::exception(ZBX_API_ERROR_PARAMETERS, _('Incorrect arguments passed to function.'));
596					}
597
598					// Don't allow invalid 'group_prototypeid' parameters which do not belong to this 'hostid'.
599					if (array_key_exists('group_prototypeid', $group_link)
600							&& !array_key_exists($group_link['group_prototypeid'], $db_group_links)) {
601						self::exception(ZBX_API_ERROR_PERMISSIONS,
602							_('No permissions to referred object or it does not exist!')
603						);
604					}
605
606					if (array_key_exists('groupid', $group_link)) {
607						$groupids[$group_link['groupid']] = true;
608					}
609				}
610			}
611
612			// Validate 'group_prototypeid' in 'groupPrototypes' property.
613			if (array_key_exists('groupPrototypes', $host_prototype)) {
614				foreach ($host_prototype['groupPrototypes'] as $group_prototype) {
615					if (!$group_prototype) {
616						self::exception(ZBX_API_ERROR_PARAMETERS, _('Incorrect arguments passed to function.'));
617					}
618
619					// Don't allow invalid 'group_prototypeid' parameters which do not belong to this 'hostid'.
620					if (array_key_exists('group_prototypeid', $group_prototype)
621							&& !array_key_exists($group_prototype['group_prototypeid'], $db_group_prototypes)) {
622						self::exception(ZBX_API_ERROR_PERMISSIONS,
623							_('No permissions to referred object or it does not exist!')
624						);
625					}
626				}
627			}
628		}
629
630		// Collect only new given groupids for validation.
631		$groupids = array_diff_key($groupids, $db_groupids);
632
633		if ($groupids) {
634			$this->checkHostGroupsPermissions(array_keys($groupids));
635		}
636
637		$host_prototypes = $this->extendObjectsByKey($host_prototypes, $db_host_prototypes, 'hostid',
638			['host', 'name', 'custom_interfaces', 'groupLinks', 'groupPrototypes']
639		);
640
641		$this->validateInterfaces($host_prototypes);
642
643		if ($hosts_by_ruleid) {
644			$this->checkDuplicates('host', $hosts_by_ruleid);
645		}
646		if ($names_by_ruleid) {
647			$this->checkDuplicates('name', $names_by_ruleid);
648		}
649	}
650
651	/**
652	 * Updates the given host prototypes.
653	 *
654	 * @param array $host_prototypes
655	 *
656	 * @return array
657	 */
658	public function update(array $host_prototypes) {
659		$this->validateUpdate($host_prototypes, $db_host_prototypes);
660
661		// merge group links into group prototypes
662		foreach ($host_prototypes as &$host_prototype) {
663			$host_prototype['groupPrototypes'] =
664				array_merge($host_prototype['groupPrototypes'], $host_prototype['groupLinks']);
665			unset($host_prototype['groupLinks']);
666		}
667		unset($host_prototype);
668
669		$this->updateHostMacros($host_prototypes, $db_host_prototypes);
670
671		$host_prototypes = $this->updateReal($host_prototypes);
672		$this->updateInterfaces($host_prototypes);
673		$this->inherit($host_prototypes);
674
675		foreach ($db_host_prototypes as &$db_host_prototype) {
676			unset($db_host_prototype['discoveryRule'], $db_host_prototype['groupLinks'],
677				$db_host_prototype['groupPrototypes']
678			);
679		}
680		unset($db_host_prototype);
681
682		$this->addAuditBulk(AUDIT_ACTION_UPDATE, AUDIT_RESOURCE_HOST_PROTOTYPE, $host_prototypes, $db_host_prototypes);
683
684		return ['hostids' => zbx_objectValues($host_prototypes, 'hostid')];
685	}
686
687	/**
688	 * Updates the host prototypes and propagates the changes to linked hosts and templates.
689	 *
690	 * @param array $host_prototypes
691	 *
692	 * @return array
693	 */
694	protected function updateReal(array $host_prototypes) {
695		// save the host prototypes
696		foreach ($host_prototypes as $host_prototype) {
697			DB::updateByPk($this->tableName(), $host_prototype['hostid'], $host_prototype);
698		}
699
700		$ex_host_prototypes = $this->get([
701			'output' => ['hostid', 'inventory_mode'],
702			'selectGroupLinks' => API_OUTPUT_EXTEND,
703			'selectGroupPrototypes' => API_OUTPUT_EXTEND,
704			'selectTemplates' => ['templateid'],
705			'hostids' => zbx_objectValues($host_prototypes, 'hostid'),
706			'preservekeys' => true
707		]);
708
709		// update related objects
710		$inventory_create = [];
711		$inventory_deleteids = [];
712		foreach ($host_prototypes as $key => $host_prototype) {
713			$ex_host_prototype = $ex_host_prototypes[$host_prototype['hostid']];
714
715			// group prototypes
716			if (isset($host_prototype['groupPrototypes'])) {
717				foreach ($host_prototype['groupPrototypes'] as &$group_prototype) {
718					$group_prototype['hostid'] = $host_prototype['hostid'];
719				}
720				unset($group_prototype);
721
722				// save group prototypes
723				$ex_group_prototypes = zbx_toHash(
724					array_merge($ex_host_prototype['groupLinks'], $ex_host_prototype['groupPrototypes']),
725					'group_prototypeid'
726				);
727				$modified_group_prototypes = [];
728				foreach ($host_prototype['groupPrototypes'] as $group_prototype) {
729					if (isset($group_prototype['group_prototypeid'])) {
730						unset($ex_group_prototypes[$group_prototype['group_prototypeid']]);
731					}
732
733					$modified_group_prototypes[] = $group_prototype;
734				}
735				if ($ex_group_prototypes) {
736					$this->deleteGroupPrototypes(array_keys($ex_group_prototypes));
737				}
738				$host_prototypes[$key]['groupPrototypes'] = DB::save('group_prototype', $modified_group_prototypes);
739			}
740
741			// templates
742			if (isset($host_prototype['templates'])) {
743				$existing_templateids = zbx_objectValues($ex_host_prototype['templates'], 'templateid');
744				$new_templateids = zbx_objectValues($host_prototype['templates'], 'templateid');
745				$this->unlink(array_diff($existing_templateids, $new_templateids), [$host_prototype['hostid']]);
746				$this->link(array_diff($new_templateids, $existing_templateids), [$host_prototype['hostid']]);
747			}
748
749			// inventory
750			if (array_key_exists('inventory_mode', $host_prototype)) {
751				if ($host_prototype['inventory_mode'] == HOST_INVENTORY_DISABLED) {
752					$inventory_deleteids[] = $host_prototype['hostid'];
753				}
754				else {
755					$inventory = ['inventory_mode' => $host_prototype['inventory_mode']];
756
757					if ($ex_host_prototype['inventory_mode'] != HOST_INVENTORY_DISABLED) {
758						if ($host_prototype['inventory_mode'] != $ex_host_prototype['inventory_mode']) {
759							DB::update('host_inventory', [
760								'values' => $inventory,
761								'where' => ['hostid' => $host_prototype['hostid']]
762							]);
763						}
764					}
765					else {
766						$inventory_create[] = $inventory + ['hostid' => $host_prototype['hostid']];
767					}
768				}
769			}
770		}
771
772		// save inventory
773		DB::insertBatch('host_inventory', $inventory_create, false);
774		DB::delete('host_inventory', ['hostid' => $inventory_deleteids]);
775
776		$this->updateTags(array_column($host_prototypes, 'tags', 'hostid'));
777
778		return $host_prototypes;
779	}
780
781	/**
782	 * Updates the children of the host prototypes on the given hosts and propagates the inheritance to the child hosts.
783	 *
784	 * @param array $hostPrototypes		array of host prototypes to inherit
785	 * @param array $hostids   			array of hosts to inherit to; if set to null, the children will be updated on all
786	 *                              	child hosts
787	 *
788	 * @return bool
789	 */
790	protected function inherit(array $hostPrototypes, array $hostids = null) {
791		if (empty($hostPrototypes)) {
792			return true;
793		}
794
795		// prepare the child host prototypes
796		$newHostPrototypes = $this->prepareInheritedObjects($hostPrototypes, $hostids);
797		if (!$newHostPrototypes) {
798			return true;
799		}
800
801		$insertHostPrototypes = [];
802		$updateHostPrototypes = [];
803		foreach ($newHostPrototypes as $newHostPrototype) {
804			if (isset($newHostPrototype['hostid'])) {
805				$updateHostPrototypes[] = $newHostPrototype;
806			}
807			else {
808				$insertHostPrototypes[] = $newHostPrototype;
809			}
810		}
811
812		// save the new host prototypes
813		if ($insertHostPrototypes) {
814			$this->createReal($insertHostPrototypes);
815		}
816
817		if ($updateHostPrototypes) {
818			$updateHostPrototypes = $this->updateReal($updateHostPrototypes);
819			$this->updateInterfaces($updateHostPrototypes);
820			$macros = array_column($updateHostPrototypes, 'macros', 'hostid');
821
822			if ($macros) {
823				$this->updateMacros($macros);
824			}
825		}
826
827		$host_prototypes = array_merge($updateHostPrototypes, $insertHostPrototypes);
828
829		if ($host_prototypes) {
830			$sql = 'SELECT hd.hostid'.
831					' FROM host_discovery hd,items i,hosts h'.
832					' WHERE hd.parent_itemid=i.itemid'.
833						' AND i.hostid=h.hostid'.
834						' AND h.status='.HOST_STATUS_TEMPLATE.
835						' AND '.dbConditionInt('hd.hostid', zbx_objectValues($host_prototypes, 'hostid'));
836			$valid_prototypes = DBfetchArrayAssoc(DBselect($sql), 'hostid');
837
838			foreach ($host_prototypes as $key => $host_prototype) {
839				if (!array_key_exists($host_prototype['hostid'], $valid_prototypes)) {
840					unset($host_prototypes[$key]);
841				}
842			}
843		}
844
845		// propagate the inheritance to the children
846		return $this->inherit($host_prototypes);
847	}
848
849
850	/**
851	 * Prepares and returns an array of child host prototypes, inherited from host prototypes $host_prototypes
852	 * on the given hosts.
853	 *
854	 * Each host prototype must have the "ruleid" parameter set.
855	 *
856	 * @param array     $host_prototypes
857	 * @param array		$hostIds
858	 *
859	 * @return array 	an array of unsaved child host prototypes
860	 */
861	protected function prepareInheritedObjects(array $host_prototypes, array $hostIds = null) {
862		// Fetch the related discovery rules with their hosts.
863		$discoveryRules = API::DiscoveryRule()->get([
864			'output' => ['itemid', 'hostid'],
865			'selectHosts' => ['hostid'],
866			'itemids' => array_column($host_prototypes, 'ruleid'),
867			'templated' => true,
868			'nopermissions' => true,
869			'preservekeys' => true
870		]);
871
872		// Remove host prototypes which don't belong to templates, so they cannot be inherited.
873		$host_prototypes = array_filter($host_prototypes, function ($host_prototype) use ($discoveryRules) {
874			return array_key_exists($host_prototype['ruleid'], $discoveryRules);
875		});
876
877		// Fetch all child hosts to inherit to. Do not inherit host prototypes on discovered hosts.
878		$chdHosts = API::Host()->get([
879			'output' => ['hostid', 'host', 'status'],
880			'selectParentTemplates' => ['templateid'],
881			'templateids' => zbx_objectValues($discoveryRules, 'hostid'),
882			'hostids' => $hostIds,
883			'nopermissions' => true,
884			'templated_hosts' => true,
885			'filter' => ['flags' => ZBX_FLAG_DISCOVERY_NORMAL]
886		]);
887		if (empty($chdHosts)) {
888			return [];
889		}
890
891		// Fetch the child discovery rules.
892		$childDiscoveryRules = API::DiscoveryRule()->get([
893			'output' => ['itemid', 'templateid', 'hostid'],
894			'filter' => [
895				'templateid' => array_keys($discoveryRules)
896			],
897			'nopermissions' => true,
898			'preservekeys' => true
899		]);
900
901		/*
902		 * Fetch child host prototypes and group them by discovery rule. "selectInterfaces" is not required, because
903		 * all child are rewritten when updating parents.
904		 */
905		$childHostPrototypes = API::HostPrototype()->get([
906			'output' => ['hostid', 'host', 'templateid'],
907			'selectGroupLinks' => API_OUTPUT_EXTEND,
908			'selectGroupPrototypes' => API_OUTPUT_EXTEND,
909			'selectDiscoveryRule' => ['itemid'],
910			'discoveryids' => zbx_objectValues($childDiscoveryRules, 'itemid'),
911			'nopermissions' => true
912		]);
913		foreach ($childDiscoveryRules as &$childDiscoveryRule) {
914			$childDiscoveryRule['hostPrototypes'] = [];
915		}
916		unset($childDiscoveryRule);
917		foreach ($childHostPrototypes as $childHostPrototype) {
918			$discoveryRuleId = $childHostPrototype['discoveryRule']['itemid'];
919			unset($childHostPrototype['discoveryRule']);
920
921			$childDiscoveryRules[$discoveryRuleId]['hostPrototypes'][] = $childHostPrototype;
922		}
923
924		// match each discovery that the parent host prototypes belong to to the child discovery rule for each host
925		$discoveryRuleChildren = [];
926		foreach ($childDiscoveryRules as $childRule) {
927			$discoveryRuleChildren[$childRule['templateid']][$childRule['hostid']] = $childRule['itemid'];
928		}
929
930		$newHostPrototypes = [];
931		foreach ($chdHosts as $host) {
932			$hostId = $host['hostid'];
933
934			// skip items not from parent templates of current host
935			$templateIds = zbx_toHash($host['parentTemplates'], 'templateid');
936			$parentHostPrototypes = [];
937			foreach ($host_prototypes as $inum => $parentHostPrototype) {
938				$parentTemplateId = $discoveryRules[$parentHostPrototype['ruleid']]['hostid'];
939
940				if (isset($templateIds[$parentTemplateId])) {
941					$parentHostPrototypes[$inum] = $parentHostPrototype;
942				}
943			}
944
945			foreach ($parentHostPrototypes as $parentHostPrototype) {
946				$childDiscoveryRuleId = $discoveryRuleChildren[$parentHostPrototype['ruleid']][$hostId];
947				$exHostPrototype = null;
948
949				// check if the child discovery rule already has host prototypes
950				$exHostPrototypes = $childDiscoveryRules[$childDiscoveryRuleId]['hostPrototypes'];
951				if ($exHostPrototypes) {
952					$exHostPrototypesHosts = zbx_toHash($exHostPrototypes, 'host');
953					$exHostPrototypesTemplateIds = zbx_toHash($exHostPrototypes, 'templateid');
954
955					// look for an already created inherited host prototype
956					// if one exists - update it
957					if (isset($exHostPrototypesTemplateIds[$parentHostPrototype['hostid']])) {
958						$exHostPrototype = $exHostPrototypesTemplateIds[$parentHostPrototype['hostid']];
959
960						// check if there's a host prototype on the target host with the same host name but from a different template
961						// or no template
962						if (isset($exHostPrototypesHosts[$parentHostPrototype['host']])
963							&& !idcmp($exHostPrototypesHosts[$parentHostPrototype['host']]['templateid'], $parentHostPrototype['hostid'])) {
964
965							$discoveryRule = DBfetch(DBselect('SELECT i.name FROM items i WHERE i.itemid='.zbx_dbstr($exHostPrototype['discoveryRule']['itemid'])));
966							self::exception(ZBX_API_ERROR_PARAMETERS, _s('Host prototype "%1$s" already exists on "%2$s".', $parentHostPrototype['host'], $discoveryRule['name']));
967						}
968					}
969
970					// look for a host prototype with the same host name
971					// if one exists - convert it to an inherited host prototype
972					if (isset($exHostPrototypesHosts[$parentHostPrototype['host']])) {
973						$exHostPrototype = $exHostPrototypesHosts[$parentHostPrototype['host']];
974
975						// check that this host prototype is not inherited from a different template
976						if ($exHostPrototype['templateid'] > 0 && !idcmp($exHostPrototype['templateid'], $parentHostPrototype['hostid'])) {
977							$discoveryRule = DBfetch(DBselect('SELECT i.name FROM items i WHERE i.itemid='.zbx_dbstr($exHostPrototype['discoveryRule']['itemid'])));
978							self::exception(ZBX_API_ERROR_PARAMETERS, _s('Host prototype "%1$s" already exists on "%2$s", inherited from another template.', $parentHostPrototype['host'], $discoveryRule['name']));
979						}
980					}
981				}
982
983				// copy host prototype
984				$newHostPrototype = $parentHostPrototype;
985				$newHostPrototype['uuid'] = '';
986				$newHostPrototype['ruleid'] = $discoveryRuleChildren[$parentHostPrototype['ruleid']][$hostId];
987				$newHostPrototype['templateid'] = $parentHostPrototype['hostid'];
988
989				if (array_key_exists('macros', $newHostPrototype)) {
990					foreach ($newHostPrototype['macros'] as &$hostmacro) {
991						unset($hostmacro['hostmacroid']);
992					}
993					unset($hostmacro);
994				}
995
996				// update an existing inherited host prototype
997				if ($exHostPrototype) {
998					// look for existing group prototypes to update
999					$exGroupPrototypesByTemplateId = zbx_toHash($exHostPrototype['groupPrototypes'], 'templateid');
1000					$exGroupPrototypesByName = zbx_toHash($exHostPrototype['groupPrototypes'], 'name');
1001					$exGroupPrototypesByGroupId = zbx_toHash($exHostPrototype['groupLinks'], 'groupid');
1002
1003					// look for a group prototype that can be updated
1004					foreach ($newHostPrototype['groupPrototypes'] as &$groupPrototype) {
1005						// updated an inherited item prototype by templateid
1006						if (isset($exGroupPrototypesByTemplateId[$groupPrototype['group_prototypeid']])) {
1007							$groupPrototype['group_prototypeid'] = $exGroupPrototypesByTemplateId[$groupPrototype['group_prototypeid']]['group_prototypeid'];
1008						}
1009						// updated an inherited item prototype by name
1010						elseif (isset($groupPrototype['name']) && !zbx_empty($groupPrototype['name'])
1011								&& isset($exGroupPrototypesByName[$groupPrototype['name']])) {
1012
1013							$groupPrototype['templateid'] = $groupPrototype['group_prototypeid'];
1014							$groupPrototype['group_prototypeid'] = $exGroupPrototypesByName[$groupPrototype['name']]['group_prototypeid'];
1015						}
1016						// updated an inherited item prototype by group ID
1017						elseif (isset($groupPrototype['groupid']) && $groupPrototype['groupid']
1018								&& isset($exGroupPrototypesByGroupId[$groupPrototype['groupid']])) {
1019
1020							$groupPrototype['templateid'] = $groupPrototype['group_prototypeid'];
1021							$groupPrototype['group_prototypeid'] = $exGroupPrototypesByGroupId[$groupPrototype['groupid']]['group_prototypeid'];
1022						}
1023						// create a new child group prototype
1024						else {
1025							$groupPrototype['templateid'] = $groupPrototype['group_prototypeid'];
1026							unset($groupPrototype['group_prototypeid']);
1027						}
1028
1029						unset($groupPrototype['hostid']);
1030					}
1031					unset($groupPrototype);
1032
1033					$newHostPrototype['hostid'] = $exHostPrototype['hostid'];
1034				}
1035				// create a new inherited host prototype
1036				else {
1037					foreach ($newHostPrototype['groupPrototypes'] as &$groupPrototype) {
1038						$groupPrototype['templateid'] = $groupPrototype['group_prototypeid'];
1039						unset($groupPrototype['group_prototypeid'], $groupPrototype['hostid']);
1040					}
1041					unset($groupPrototype);
1042
1043					unset($newHostPrototype['hostid']);
1044				}
1045				$newHostPrototypes[] = $newHostPrototype;
1046			}
1047		}
1048
1049		return $newHostPrototypes;
1050	}
1051
1052	/**
1053	 * Inherits all host prototypes from the templates given in "templateids" to hosts or templates given in "hostids".
1054	 *
1055	 * @param array $data
1056	 *
1057	 * @return bool
1058	 */
1059	public function syncTemplates(array $data) {
1060		$data['templateids'] = zbx_toArray($data['templateids']);
1061		$data['hostids'] = zbx_toArray($data['hostids']);
1062
1063		$discoveryRules = API::DiscoveryRule()->get([
1064			'output' => ['itemid'],
1065			'hostids' => $data['templateids']
1066		]);
1067		$hostPrototypes = $this->get([
1068			'discoveryids' => zbx_objectValues($discoveryRules, 'itemid'),
1069			'preservekeys' => true,
1070			'output' => API_OUTPUT_EXTEND,
1071			'selectGroupLinks' => API_OUTPUT_EXTEND,
1072			'selectGroupPrototypes' => API_OUTPUT_EXTEND,
1073			'selectTags' => ['tag', 'value'],
1074			'selectTemplates' => ['templateid'],
1075			'selectDiscoveryRule' => ['itemid'],
1076			'selectInterfaces' => ['main', 'type', 'useip', 'ip', 'dns', 'port', 'details']
1077		]);
1078
1079		$hostPrototypes = $this->getHostMacros($hostPrototypes);
1080
1081		foreach ($hostPrototypes as &$hostPrototype) {
1082			// merge group links into group prototypes
1083			foreach ($hostPrototype['groupLinks'] as $group) {
1084				$hostPrototype['groupPrototypes'][] = $group;
1085			}
1086			unset($hostPrototype['groupLinks']);
1087
1088			// the ID of the discovery rule must be passed in the "ruleid" parameter
1089			$hostPrototype['ruleid'] = $hostPrototype['discoveryRule']['itemid'];
1090			unset($hostPrototype['discoveryRule']);
1091		}
1092		unset($hostPrototype);
1093
1094		$this->inherit($hostPrototypes, $data['hostids']);
1095
1096		return true;
1097	}
1098
1099	/**
1100	 * Validates the input parameters for the delete() method.
1101	 *
1102	 * @throws APIException if the input is invalid
1103	 *
1104	 * @param array $hostPrototypeIds
1105	 * @param bool 	$nopermissions
1106	 */
1107	protected function validateDelete($hostPrototypeIds, $nopermissions) {
1108		if (!$hostPrototypeIds) {
1109			self::exception(ZBX_API_ERROR_PARAMETERS, _('Empty input parameter.'));
1110		}
1111
1112		if (!$nopermissions) {
1113			$this->checkHostPrototypePermissions($hostPrototypeIds);
1114			$this->checkNotInherited($hostPrototypeIds);
1115		}
1116	}
1117
1118	/**
1119	 * Delete host prototypes.
1120	 *
1121	 * @param array 	$hostPrototypeIds
1122	 * @param bool 		$nopermissions		if set to true, permission and template checks will be skipped
1123	 *
1124	 * @return array
1125	 */
1126	public function delete(array $hostPrototypeIds, $nopermissions = false) {
1127		$this->validateDelete($hostPrototypeIds, $nopermissions);
1128
1129		// include child IDs
1130		$parentHostPrototypeIds = $hostPrototypeIds;
1131		$childHostPrototypeIds = [];
1132		do {
1133			$query = DBselect('SELECT h.hostid FROM hosts h WHERE '.dbConditionInt('h.templateid', $parentHostPrototypeIds));
1134			$parentHostPrototypeIds = [];
1135			while ($hostPrototype = DBfetch($query)) {
1136				$parentHostPrototypeIds[] = $hostPrototype['hostid'];
1137				$childHostPrototypeIds[] = $hostPrototype['hostid'];
1138			}
1139		} while (!empty($parentHostPrototypeIds));
1140
1141		$hostPrototypeIds = array_merge($hostPrototypeIds, $childHostPrototypeIds);
1142
1143		// Lock host prototypes before delete to prevent server from adding new LLD hosts.
1144		DBselect(
1145			'SELECT NULL'.
1146			' FROM hosts h'.
1147			' WHERE '.dbConditionInt('h.hostid', $hostPrototypeIds).
1148			' FOR UPDATE'
1149		);
1150
1151		$deleteHostPrototypes = $this->get([
1152			'hostids' => $hostPrototypeIds,
1153			'output' => ['host'],
1154			'selectGroupPrototypes' => ['group_prototypeid'],
1155			'selectParentHost' => ['host'],
1156			'nopermissions' => true
1157		]);
1158
1159		// delete discovered hosts
1160		$discoveredHosts = DBfetchArray(DBselect(
1161			'SELECT hostid FROM host_discovery WHERE '.dbConditionInt('parent_hostid', $hostPrototypeIds)
1162		));
1163		if ($discoveredHosts) {
1164			API::Host()->delete(zbx_objectValues($discoveredHosts, 'hostid'), true);
1165		}
1166
1167		// delete group prototypes and discovered groups
1168		$groupPrototypeIds = [];
1169		foreach ($deleteHostPrototypes as $groupPrototype) {
1170			foreach ($groupPrototype['groupPrototypes'] as $groupPrototype) {
1171				$groupPrototypeIds[] = $groupPrototype['group_prototypeid'];
1172			}
1173		}
1174		$this->deleteGroupPrototypes($groupPrototypeIds);
1175
1176		// delete host prototypes
1177		DB::delete($this->tableName(), ['hostid' => $hostPrototypeIds]);
1178
1179		// TODO: REMOVE info
1180		foreach ($deleteHostPrototypes as $hostProtototype) {
1181			info(_s('Deleted: Host prototype "%1$s" on "%2$s".', $hostProtototype['host'], $hostProtototype['parentHost']['host']));
1182		}
1183
1184		return ['hostids' => $hostPrototypeIds];
1185	}
1186
1187	protected function link(array $templateids, array $targetids) {
1188		$this->checkHostPrototypePermissions($targetids);
1189
1190		$links = parent::link($templateids, $targetids);
1191
1192		foreach ($targetids as $targetid) {
1193			$linked_templates = API::Template()->get([
1194				'output' => [],
1195				'hostids' => [$targetid],
1196				'nopermissions' => true
1197			]);
1198
1199			$result = DBselect(
1200				'SELECT i.key_,count(*)'.
1201				' FROM items i'.
1202				' WHERE '.dbConditionInt('i.hostid', array_merge($templateids, array_keys($linked_templates))).
1203				' GROUP BY i.key_'.
1204				' HAVING count(*)>1',
1205				1
1206			);
1207			if ($row = DBfetch($result)) {
1208				$target_templates = API::HostPrototype()->get([
1209					'output' => ['name'],
1210					'hostids' => [$targetid],
1211					'nopermissions' => true
1212				]);
1213
1214				self::exception(ZBX_API_ERROR_PARAMETERS,
1215					_s('Item "%1$s" already exists on "%2$s", inherited from another template.', $row['key_'],
1216						$target_templates[0]['name']
1217					)
1218				);
1219			}
1220		}
1221
1222		return $links;
1223	}
1224
1225	/**
1226	 * Checks if the current user has access to the given LLD rules.
1227	 *
1228	 * @throws APIException if the user doesn't have write permissions for the given LLD rules
1229	 *
1230	 * @param array $ruleids
1231	 */
1232	protected function checkDiscoveryRulePermissions(array $ruleids) {
1233		$count = API::DiscoveryRule()->get([
1234			'countOutput' => true,
1235			'itemids' => $ruleids,
1236			'editable' => true
1237		]);
1238
1239		if ($count != count($ruleids)) {
1240			self::exception(ZBX_API_ERROR_PERMISSIONS, _('No permissions to referred object or it does not exist!'));
1241		}
1242	}
1243
1244	/**
1245	 * Checks if the current user has access to the given host groups.
1246	 *
1247	 * @throws APIException if the user doesn't have write permissions for the given host groups
1248	 *
1249	 * @param array $groupids
1250	 */
1251	protected function checkHostGroupsPermissions(array $groupids) {
1252		$db_groups = API::HostGroup()->get([
1253			'output' => ['name', 'flags'],
1254			'groupids' => $groupids,
1255			'editable' => true,
1256			'preservekeys' => true
1257		]);
1258
1259		foreach ($groupids as $groupid) {
1260			if (!array_key_exists($groupid, $db_groups)) {
1261				self::exception(ZBX_API_ERROR_PERMISSIONS,
1262					_('No permissions to referred object or it does not exist!')
1263				);
1264			}
1265
1266			$db_group = $db_groups[$groupid];
1267
1268			// Check if group prototypes use discovered host groups.
1269			if ($db_group['flags'] == ZBX_FLAG_DISCOVERY_CREATED) {
1270				self::exception(ZBX_API_ERROR_PARAMETERS,
1271					_s('Group prototype cannot be based on a discovered host group "%1$s".', $db_group['name'])
1272				);
1273			}
1274		}
1275	}
1276
1277	/**
1278	 * Checks if the current user has access to the given host prototypes.
1279	 *
1280	 * @throws APIException if the user doesn't have write permissions for the host prototypes.
1281	 *
1282	 * @param array $hostPrototypeIds
1283	 */
1284	protected function checkHostPrototypePermissions(array $hostPrototypeIds) {
1285		if ($hostPrototypeIds) {
1286			$hostPrototypeIds = array_unique($hostPrototypeIds);
1287
1288			$count = $this->get([
1289				'countOutput' => true,
1290				'hostids' => $hostPrototypeIds,
1291				'editable' => true
1292			]);
1293
1294			if ($count != count($hostPrototypeIds)) {
1295				self::exception(ZBX_API_ERROR_PERMISSIONS,
1296					_('No permissions to referred object or it does not exist!')
1297				);
1298			}
1299		}
1300	}
1301
1302	/**
1303	 * Checks if the given host prototypes are not inherited from a template.
1304	 *
1305	 * @throws APIException 	if at least one host prototype is inherited
1306	 *
1307	 * @param array $hostPrototypeIds
1308	 */
1309	protected function checkNotInherited(array $hostPrototypeIds) {
1310		$query = DBSelect('SELECT hostid FROM hosts h WHERE h.templateid>0 AND '.dbConditionInt('h.hostid', $hostPrototypeIds), 1);
1311
1312		if ($hostPrototype = DBfetch($query)) {
1313			self::exception(ZBX_API_ERROR_PERMISSIONS, _('Cannot delete templated host prototype.'));
1314		}
1315	}
1316
1317	protected function applyQueryFilterOptions($tableName, $tableAlias, array $options, array $sqlParts) {
1318		$sqlParts = parent::applyQueryFilterOptions($tableName, $tableAlias, $options, $sqlParts);
1319
1320		// do not return host prototypes from discovered hosts
1321		$sqlParts['from'][] = 'host_discovery hd';
1322		$sqlParts['from'][] = 'items i';
1323		$sqlParts['from'][] = 'hosts ph';
1324		$sqlParts['where'][] = $this->fieldId('hostid').'=hd.hostid';
1325		$sqlParts['where'][] = 'hd.parent_itemid=i.itemid';
1326		$sqlParts['where'][] = 'i.hostid=ph.hostid';
1327		$sqlParts['where'][] = 'ph.flags='.ZBX_FLAG_DISCOVERY_NORMAL;
1328
1329		if (self::$userData['type'] != USER_TYPE_SUPER_ADMIN && !$options['nopermissions']) {
1330			$permission = $options['editable'] ? PERM_READ_WRITE : PERM_READ;
1331
1332			$sqlParts['where'][] = 'EXISTS ('.
1333				'SELECT NULL'.
1334				' FROM '.
1335					'host_discovery hd,items i,hosts_groups hgg'.
1336					' JOIN rights r'.
1337						' ON r.id=hgg.groupid'.
1338						' AND '.dbConditionInt('r.groupid', getUserGroupsByUserId(self::$userData['userid'])).
1339				' WHERE h.hostid=hd.hostid'.
1340					' AND hd.parent_itemid=i.itemid'.
1341					' AND i.hostid=hgg.hostid'.
1342				' GROUP BY hgg.hostid'.
1343				' HAVING MIN(r.permission)>'.PERM_DENY.
1344				' AND MAX(r.permission)>='.zbx_dbstr($permission).
1345				')';
1346		}
1347
1348		// discoveryids
1349		if ($options['discoveryids'] !== null) {
1350			$sqlParts['where'][] = dbConditionInt('hd.parent_itemid', (array) $options['discoveryids']);
1351
1352			if ($options['groupCount']) {
1353				$sqlParts['group']['hd'] = 'hd.parent_itemid';
1354			}
1355		}
1356
1357		// inherited
1358		if ($options['inherited'] !== null) {
1359			$sqlParts['where'][] = ($options['inherited']) ? 'h.templateid IS NOT NULL' : 'h.templateid IS NULL';
1360		}
1361
1362		if ($options['filter'] && array_key_exists('inventory_mode', $options['filter'])) {
1363			if ($options['filter']['inventory_mode'] !== null) {
1364				$inventory_mode_query = (array) $options['filter']['inventory_mode'];
1365
1366				$inventory_mode_where = [];
1367				$null_position = array_search(HOST_INVENTORY_DISABLED, $inventory_mode_query);
1368
1369				if ($null_position !== false) {
1370					unset($inventory_mode_query[$null_position]);
1371					$inventory_mode_where[] = 'hinv.inventory_mode IS NULL';
1372				}
1373
1374				if ($null_position === false || $inventory_mode_query) {
1375					$inventory_mode_where[] = dbConditionInt('hinv.inventory_mode', $inventory_mode_query);
1376				}
1377
1378				$sqlParts['where'][] = (count($inventory_mode_where) > 1)
1379					? '('.implode(' OR ', $inventory_mode_where).')'
1380					: $inventory_mode_where[0];
1381			}
1382		}
1383
1384		return $sqlParts;
1385	}
1386
1387	/**
1388	 * Retrieves and adds additional requested data to the result set.
1389	 *
1390	 * @param array  $options
1391	 * @param array  $result
1392	 *
1393	 * @return array
1394	 */
1395	protected function addRelatedObjects(array $options, array $result) {
1396		$result = parent::addRelatedObjects($options, $result);
1397
1398		$hostPrototypeIds = array_keys($result);
1399
1400		// adding discovery rule
1401		if ($options['selectDiscoveryRule'] !== null && $options['selectDiscoveryRule'] != API_OUTPUT_COUNT) {
1402			$relationMap = $this->createRelationMap($result, 'hostid', 'parent_itemid', 'host_discovery');
1403			$discoveryRules = API::DiscoveryRule()->get([
1404				'output' => $options['selectDiscoveryRule'],
1405				'itemids' => $relationMap->getRelatedIds(),
1406				'nopermissions' => true,
1407				'preservekeys' => true
1408			]);
1409			$result = $relationMap->mapOne($result, $discoveryRules, 'discoveryRule');
1410		}
1411
1412		// adding group links
1413		if ($options['selectGroupLinks'] !== null && $options['selectGroupLinks'] != API_OUTPUT_COUNT) {
1414			$groupPrototypes = DBFetchArray(DBselect(
1415				'SELECT hg.group_prototypeid,hg.hostid'.
1416					' FROM group_prototype hg'.
1417					' WHERE '.dbConditionInt('hg.hostid', $hostPrototypeIds).
1418					' AND hg.groupid IS NOT NULL'
1419			));
1420			$relationMap = $this->createRelationMap($groupPrototypes, 'hostid', 'group_prototypeid');
1421			$groupPrototypes = API::getApiService()->select('group_prototype', [
1422				'output' => $options['selectGroupLinks'],
1423				'group_prototypeids' => $relationMap->getRelatedIds(),
1424				'preservekeys' => true
1425			]);
1426			foreach ($groupPrototypes as &$groupPrototype) {
1427				unset($groupPrototype['name']);
1428			}
1429			unset($groupPrototype);
1430			$result = $relationMap->mapMany($result, $groupPrototypes, 'groupLinks');
1431		}
1432
1433		// adding group prototypes
1434		if ($options['selectGroupPrototypes'] !== null && $options['selectGroupPrototypes'] != API_OUTPUT_COUNT) {
1435			$groupPrototypes = DBFetchArray(DBselect(
1436				'SELECT hg.group_prototypeid,hg.hostid'.
1437				' FROM group_prototype hg'.
1438				' WHERE '.dbConditionInt('hg.hostid', $hostPrototypeIds).
1439					' AND hg.groupid IS NULL'
1440			));
1441			$relationMap = $this->createRelationMap($groupPrototypes, 'hostid', 'group_prototypeid');
1442			$groupPrototypes = API::getApiService()->select('group_prototype', [
1443				'output' => $options['selectGroupPrototypes'],
1444				'group_prototypeids' => $relationMap->getRelatedIds(),
1445				'preservekeys' => true
1446			]);
1447			foreach ($groupPrototypes as &$groupPrototype) {
1448				unset($groupPrototype['groupid']);
1449			}
1450			unset($groupPrototype);
1451			$result = $relationMap->mapMany($result, $groupPrototypes, 'groupPrototypes');
1452		}
1453
1454		// adding host
1455		if ($options['selectParentHost'] !== null && $options['selectParentHost'] != API_OUTPUT_COUNT) {
1456			$hosts = [];
1457			$relationMap = new CRelationMap();
1458			$dbRules = DBselect(
1459				'SELECT hd.hostid,i.hostid AS parent_hostid'.
1460					' FROM host_discovery hd,items i'.
1461					' WHERE '.dbConditionInt('hd.hostid', $hostPrototypeIds).
1462					' AND hd.parent_itemid=i.itemid'
1463			);
1464			while ($relation = DBfetch($dbRules)) {
1465				$relationMap->addRelation($relation['hostid'], $relation['parent_hostid']);
1466			}
1467
1468			$related_ids = $relationMap->getRelatedIds();
1469
1470			if ($related_ids) {
1471				$hosts = API::Host()->get([
1472					'output' => $options['selectParentHost'],
1473					'hostids' => $related_ids,
1474					'templated_hosts' => true,
1475					'nopermissions' => true,
1476					'preservekeys' => true
1477				]);
1478			}
1479
1480			$result = $relationMap->mapOne($result, $hosts, 'parentHost');
1481		}
1482
1483		// adding templates
1484		if ($options['selectTemplates'] !== null) {
1485			if ($options['selectTemplates'] != API_OUTPUT_COUNT) {
1486				$templates = [];
1487				$relationMap = $this->createRelationMap($result, 'hostid', 'templateid', 'hosts_templates');
1488				$related_ids = $relationMap->getRelatedIds();
1489
1490				if ($related_ids) {
1491					$templates = API::Template()->get([
1492						'output' => $options['selectTemplates'],
1493						'templateids' => $related_ids,
1494						'preservekeys' => true
1495					]);
1496				}
1497
1498				$result = $relationMap->mapMany($result, $templates, 'templates');
1499			}
1500			else {
1501				$templates = API::Template()->get([
1502					'hostids' => $hostPrototypeIds,
1503					'countOutput' => true,
1504					'groupCount' => true
1505				]);
1506				$templates = zbx_toHash($templates, 'hostid');
1507				foreach ($result as $hostid => $host) {
1508					$result[$hostid]['templates'] = array_key_exists($hostid, $templates)
1509						? $templates[$hostid]['rowscount']
1510						: '0';
1511				}
1512			}
1513		}
1514
1515		// adding tags
1516		if ($options['selectTags'] !== null && $options['selectTags'] !== API_OUTPUT_COUNT) {
1517			$tags = API::getApiService()->select('host_tag', [
1518				'output' => $this->outputExtend($options['selectTags'], ['hostid', 'hosttagid']),
1519				'filter' => ['hostid' => $hostPrototypeIds],
1520				'preservekeys' => true
1521			]);
1522
1523			$relation_map = $this->createRelationMap($tags, 'hostid', 'hosttagid');
1524			$tags = $this->unsetExtraFields($tags, ['hostid', 'hosttagid'], []);
1525			$result = $relation_map->mapMany($result, $tags, 'tags');
1526		}
1527
1528		if ($options['selectInterfaces'] !== null && $options['selectInterfaces'] != API_OUTPUT_COUNT) {
1529			$interfaces = API::HostInterface()->get([
1530				'output' => $this->outputExtend($options['selectInterfaces'], ['hostid', 'interfaceid']),
1531				'hostids' => $hostPrototypeIds,
1532				'sortfield' => 'interfaceid',
1533				'nopermissions' => true,
1534				'preservekeys' => true
1535			]);
1536
1537			foreach (array_keys($result) as $hostid) {
1538				$result[$hostid]['interfaces'] = [];
1539			}
1540
1541			foreach ($interfaces as $interface) {
1542				$hostid = $interface['hostid'];
1543				unset($interface['hostid'], $interface['interfaceid']);
1544				$result[$hostid]['interfaces'][] = $interface;
1545			}
1546		}
1547
1548		return $result;
1549	}
1550
1551	/**
1552	 * Deletes the given group prototype and all discovered groups.
1553	 * Deletes also group prototype children.
1554	 *
1555	 * @param array $groupPrototypeIds
1556	 */
1557	protected function deleteGroupPrototypes(array $groupPrototypeIds) {
1558		// Lock group prototypes before delete to prevent server from adding new LLD elements.
1559		DBselect(
1560			'SELECT NULL'.
1561			' FROM group_prototype gp'.
1562			' WHERE '.dbConditionInt('gp.group_prototypeid', $groupPrototypeIds).
1563			' FOR UPDATE'
1564		);
1565
1566		// delete child group prototypes
1567		$groupPrototypeChildren = DBfetchArray(DBselect(
1568			'SELECT gp.group_prototypeid FROM group_prototype gp WHERE '.dbConditionInt('templateid', $groupPrototypeIds)
1569		));
1570		if ($groupPrototypeChildren) {
1571			$this->deleteGroupPrototypes(zbx_objectValues($groupPrototypeChildren, 'group_prototypeid'));
1572		}
1573
1574		// delete discovered groups
1575		$hostGroups = DBfetchArray(DBselect(
1576			'SELECT groupid FROM group_discovery WHERE '.dbConditionInt('parent_group_prototypeid', $groupPrototypeIds)
1577		));
1578		if ($hostGroups) {
1579			API::HostGroup()->delete(zbx_objectValues($hostGroups, 'groupid'), true);
1580		}
1581
1582		// delete group prototypes
1583		DB::delete('group_prototype', ['group_prototypeid' => $groupPrototypeIds]);
1584	}
1585
1586	/**
1587	 * Update host prototype macros, key is host prototype id and value is array of arrays with macro objects.
1588	 *
1589	 * @param array $update_macros  Array with macros objects.
1590	 */
1591	protected function updateMacros(array $update_macros): void {
1592		$ins_hostmacros = [];
1593		$upd_hostmacros = [];
1594
1595		$db_hostmacros = DB::select('hostmacro', [
1596			'output' => ['hostid', 'macro', 'type', 'value', 'description'],
1597			'filter' => ['hostid' => array_keys($update_macros)],
1598			'preservekeys' => true
1599		]);
1600		$host_macros = array_fill_keys(array_keys($update_macros), []);
1601
1602		foreach ($db_hostmacros as $hostmacroid => $db_hostmacro) {
1603			$host_macros[$db_hostmacro['hostid']][$db_hostmacro['macro']] = $hostmacroid;
1604		}
1605
1606		foreach ($update_macros as $hostid => $hostmacros) {
1607			foreach ($hostmacros as $hostmacro) {
1608				if (array_key_exists($hostmacro['macro'], $host_macros[$hostid])) {
1609					$hostmacroid = $host_macros[$hostid][$hostmacro['macro']];
1610
1611					$upd_hostmacro = DB::getUpdatedValues('hostmacro', $hostmacro, $db_hostmacro);
1612
1613					if ($upd_hostmacro) {
1614						$upd_hostmacros[] = [
1615							'values' => $upd_hostmacro,
1616							'where' => ['hostmacroid' => $hostmacroid]
1617						];
1618					}
1619
1620					unset($db_hostmacros[$hostmacroid], $host_macros[$hostid][$hostmacro['macro']]);
1621				}
1622				else {
1623					$ins_hostmacros[] = ['hostid' => $hostid] + $hostmacro;
1624				}
1625			}
1626		}
1627
1628		if ($db_hostmacros) {
1629			DB::delete('hostmacro', ['hostmacroid' => array_keys($db_hostmacros)]);
1630		}
1631
1632		if ($upd_hostmacros) {
1633			DB::update('hostmacro', $upd_hostmacros);
1634		}
1635
1636		if ($ins_hostmacros) {
1637			DB::insert('hostmacro', $ins_hostmacros);
1638		}
1639	}
1640
1641	/**
1642	 * Validate host prototype interfaces on create and update.
1643	 *
1644	 * @param array $host_prototypes                           Array of host prototype data.
1645	 * @param array $host_prototype[]['interfaces']            Host prototype interfaces.
1646	 * @param int   $host_prototype[]['custom_interfaces']     Use custom or inherited interfaces.
1647	 *
1648	 * @throws APIException if the interfaces input is invalid.
1649	 */
1650	private function validateInterfaces(array $host_prototypes): void {
1651		foreach ($host_prototypes as $hp_idx => $host_prototype) {
1652			if ($host_prototype['custom_interfaces'] == HOST_PROT_INTERFACES_CUSTOM) {
1653				if (array_key_exists('interfaces', $host_prototype) && $host_prototype['interfaces']) {
1654					foreach ($host_prototype['interfaces'] as $if_idx => $interface) {
1655						$path = '/'.($hp_idx + 1).'/interfaces/'.($if_idx + 1);
1656
1657						if ($interface['useip'] == INTERFACE_USE_DNS) {
1658							if (!array_key_exists('dns', $interface)) {
1659								self::exception(ZBX_API_ERROR_PARAMETERS, _s('Invalid parameter "%1$s": %2$s.', $path,
1660									_s('the parameter "%1$s" is missing', 'dns')
1661								));
1662							}
1663							elseif ($interface['dns'] === '') {
1664								self::exception(ZBX_API_ERROR_PARAMETERS, _s('Invalid parameter "%1$s": %2$s.',
1665									$path.'/dns', _('cannot be empty')
1666								));
1667							}
1668						}
1669						else {
1670							if (!array_key_exists('ip', $interface)) {
1671								self::exception(ZBX_API_ERROR_PARAMETERS, _s('Invalid parameter "%1$s": %2$s.', $path,
1672									_s('the parameter "%1$s" is missing', 'ip')
1673								));
1674							}
1675							elseif ($interface['ip'] === '') {
1676								self::exception(ZBX_API_ERROR_PARAMETERS, _s('Invalid parameter "%1$s": %2$s.',
1677									$path.'/ip', _('cannot be empty')
1678								));
1679							}
1680						}
1681
1682						if ($interface['type'] == INTERFACE_TYPE_SNMP) {
1683							if (array_key_exists('details', $interface)) {
1684								if ($interface['details']['version'] == SNMP_V1
1685										|| $interface['details']['version'] == SNMP_V2C) {
1686									if (!array_key_exists('community', $interface['details'])) {
1687										self::exception(ZBX_API_ERROR_PARAMETERS, _s('Invalid parameter "%1$s": %2$s.',
1688											$path.'/details', _s('the parameter "%1$s" is missing', 'community')
1689										));
1690									}
1691									elseif ($interface['details']['community'] === '') {
1692										self::exception(ZBX_API_ERROR_PARAMETERS, _s('Invalid parameter "%1$s": %2$s.',
1693											$path.'/details/community', _('cannot be empty')
1694										));
1695									}
1696								}
1697							}
1698							else {
1699								self::exception(ZBX_API_ERROR_PARAMETERS, _s('Invalid parameter "%1$s": %2$s.', $path,
1700									_s('the parameter "%1$s" is missing', 'details')
1701								));
1702							}
1703						}
1704						elseif (array_key_exists('details', $interface) && $interface['details']) {
1705							self::exception(ZBX_API_ERROR_PARAMETERS, _s('Invalid parameter "%1$s": %2$s.', $path,
1706								_s('unexpected parameter "%1$s"', 'details')
1707							));
1708						}
1709					}
1710
1711					$this->checkMainInterfaces($host_prototype, $host_prototype['interfaces']);
1712				}
1713			}
1714			elseif (array_key_exists('interfaces', $host_prototype) && $host_prototype['interfaces']) {
1715				self::exception(ZBX_API_ERROR_PARAMETERS, _s('Invalid parameter "%1$s": %2$s.',
1716					'/'.($hp_idx + 1).'/interfaces', _('should be empty')
1717				));
1718			}
1719		}
1720	}
1721
1722	/**
1723	 * Check if main interfaces are correctly set for every interface type. Each host must either have only one main
1724	 * interface for each interface type, or have no interface of that type at all.
1725	 *
1726	 * @param array  $host_prototype          Host prototype object.
1727	 * @param string $host_prototype['name']  Host prototype name.
1728	 * @param array  $interfaces              All single host prototype interfaces including existing ones in DB.
1729	 * @param int    $interfaces[]['type']    Interface type.
1730	 * @param int    $interfaces[]['main']    If interface type is main.
1731	 *
1732	 * @throws APIException if two main or no main interfaces are given.
1733	 */
1734	private function checkMainInterfaces(array $host_prototype, array $interfaces): void {
1735		$interface_types = [];
1736
1737		foreach ($interfaces as $interface) {
1738			if (!array_key_exists($interface['type'], $interface_types)) {
1739				$interface_types[$interface['type']] = ['main' => 0, 'all' => 0];
1740			}
1741
1742			if ($interface['main'] == INTERFACE_PRIMARY) {
1743				$interface_types[$interface['type']]['main']++;
1744			}
1745			else {
1746				$interface_types[$interface['type']]['all']++;
1747			}
1748		}
1749
1750		foreach ($interface_types as $type => $counters) {
1751			if ($counters['all'] && !$counters['main']) {
1752				self::exception(ZBX_API_ERROR_PARAMETERS, _s('No default interface for "%1$s" type on "%2$s".',
1753					hostInterfaceTypeNumToName($type), $host_prototype['name']
1754				));
1755			}
1756
1757			if ($counters['main'] > 1) {
1758				self::exception(ZBX_API_ERROR_PARAMETERS,
1759					_('Host prototype cannot have more than one default interface of the same type.')
1760				);
1761			}
1762		}
1763	}
1764
1765	/**
1766	 * Create host prototype interfaces.
1767	 *
1768	 * @param array $host_prototypes                  Array of host prototypes.
1769	 * @param array $host_prototypes[]['hostid']      Host prototype ID.
1770	 * @param array $host_prototypes[]['interfaces']  Host prototype interfaces data.
1771	 */
1772	private function createInterfaces(array $host_prototypes): void {
1773		$interfaces = [];
1774		foreach ($host_prototypes as $host_prototype) {
1775			if (array_key_exists('interfaces', $host_prototype)) {
1776				foreach ($host_prototype['interfaces'] as $interface) {
1777					$interface['hostid'] = $host_prototype['hostid'];
1778					$interfaces[] = $interface;
1779				}
1780			}
1781		}
1782
1783		if ($interfaces) {
1784			$this->createInterfacesReal($interfaces);
1785		}
1786	}
1787
1788	/**
1789	 * Update host prototype interfaces.
1790	 *
1791	 * @param array $host_prototypes                     Array of host prototypes.
1792	 * @param array $host_prototypes[]['hostid']         Host prototype ID.
1793	 * @param array $host_prototypes[]['interfaces']     Host prototype interfaces data.
1794	 * @param array $db_host_prototypes                  Array of host prototypes from DB.
1795	 * @param array $db_host_prototypes[]['interfaces']  Host prototype interfaces data from DB.
1796	 */
1797	private function updateInterfaces(array $host_prototypes): void {
1798		// We need to get interfaces with their interfaceid's.
1799		$interfaces = API::HostInterface()->get([
1800			'output' => ['hostid', 'interfaceid', 'type', 'useip', 'ip', 'dns', 'port', 'main', 'details'],
1801			'hostids' => array_column($host_prototypes, 'hostid'),
1802			'nopermissions' => true
1803		]);
1804
1805		$db_host_prototype_interfaces = [];
1806		foreach($interfaces as $interface) {
1807			$db_host_prototype_interfaces[$interface['hostid']][] = $interface;
1808		}
1809
1810		$interfaces_to_create = [];
1811		$interfaces_to_update = [];
1812		$interfaceids_to_delete = [];
1813
1814		foreach ($host_prototypes as $host_prototype) {
1815			$db_interfaces = array_key_exists($host_prototype['hostid'], $db_host_prototype_interfaces)
1816				? $db_host_prototype_interfaces[$host_prototype['hostid']]
1817				: [];
1818
1819			if ($host_prototype['custom_interfaces'] == HOST_PROT_INTERFACES_CUSTOM) {
1820				if (array_key_exists('interfaces', $host_prototype)) {
1821					if ($host_prototype['interfaces']) {
1822						CArrayHelper::sort($host_prototype['interfaces'], ['type', 'ip', 'dns']);
1823						CArrayHelper::sort($db_interfaces, ['type', 'ip', 'dns']);
1824						$host_prototype['interfaces'] = array_values($host_prototype['interfaces']);
1825						$db_interfaces = array_values($db_interfaces);
1826
1827						foreach ($host_prototype['interfaces'] as $index => $interface) {
1828							if (array_key_exists($index, $db_interfaces)) {
1829								if (!$this->compareInterface($interface, $db_interfaces[$index])) {
1830									$interface['interfaceid'] = $db_interfaces[$index]['interfaceid'];
1831									$interface['hostid'] = $host_prototype['hostid'];
1832									$interfaces_to_update[] = $interface;
1833								}
1834
1835								unset($db_interfaces[$index]);
1836							}
1837							else {
1838								// All remaining interfaces should be created.
1839								$interface['hostid'] = $host_prototype['hostid'];
1840								$interfaces_to_create[] = $interface;
1841							}
1842						}
1843					}
1844				}
1845				else {
1846					// Interfaces have not changed and should not be deleted;
1847					$db_interfaces = [];
1848				}
1849			}
1850
1851			$interfaceids_to_delete += array_flip(array_column($db_interfaces, 'interfaceid'));
1852		}
1853
1854		if ($interfaceids_to_delete) {
1855			$interfaceids_to_delete = array_flip($interfaceids_to_delete);
1856			DB::delete('interface_snmp', ['interfaceid' => $interfaceids_to_delete]);
1857			DB::delete('interface', ['interfaceid' => $interfaceids_to_delete]);
1858		}
1859
1860		if ($interfaces_to_update) {
1861			$this->updateInterfacesReal($interfaces_to_update);
1862		}
1863
1864		if ($interfaces_to_create) {
1865			$this->createInterfacesReal($interfaces_to_create);
1866		}
1867	}
1868
1869	/**
1870	 * Compare two interface. Retun true if they are same, return false otherwise.
1871	 *
1872	 * @param array $host_interface
1873	 * @param array $db_interface
1874	 *
1875	 * @return boolean
1876	 */
1877	private function compareInterface(array $host_interface, array $db_interface): bool {
1878		$interface_fields = ['type', 'ip', 'dns', 'useip', 'port', 'main'];
1879		$snmp_fields = ['version', 'community', 'bulk', 'securityname', 'securitylevel', 'authpassphrase',
1880			'privpassphrase', 'authprotocol', 'privprotocol', 'contextname'
1881		];
1882
1883		foreach ($interface_fields as $field) {
1884			if (array_key_exists($field, $db_interface)
1885					&& (!array_key_exists($field, $host_interface)
1886						|| $host_interface[$field] != $db_interface[$field])) {
1887				return false;
1888			}
1889		}
1890
1891		if ($db_interface['type'] == INTERFACE_TYPE_SNMP) {
1892			foreach ($snmp_fields as $field) {
1893				if (array_key_exists($field, $db_interface['details'])
1894						&& (!array_key_exists($field, $host_interface['details'])
1895							|| $host_interface['details'][$field] != $db_interface['details'][$field])) {
1896					return false;
1897				}
1898			}
1899		}
1900
1901		return true;
1902	}
1903
1904	/**
1905	 * Insert host prototype interfaces into DB.
1906	 */
1907	private function createInterfacesReal(array $interfaces): void {
1908		$interfaceids = DB::insert('interface', $interfaces);
1909
1910		$snmp_interfaces = [];
1911		foreach ($interfaceids as $key => $id) {
1912			if ($interfaces[$key]['type'] == INTERFACE_TYPE_SNMP) {
1913				$snmp_interfaces[] = ['interfaceid' => $id] + $interfaces[$key]['details'];
1914			}
1915		}
1916
1917		$this->createSnmpInterfaceDetails($snmp_interfaces);
1918	}
1919
1920	/**
1921	 * Update host prototype interfaces in DB.
1922	 */
1923	private function updateInterfacesReal(array $interfaces): void {
1924		DB::delete('interface_snmp', ['interfaceid' => array_column($interfaces, 'interfaceid')]);
1925
1926		$data = [];
1927		$snmp_interfaces = [];
1928
1929		foreach ($interfaces as $interface) {
1930			if ($interface['type'] == INTERFACE_TYPE_SNMP) {
1931				$snmp_interfaces[] = ['interfaceid' => $interface['interfaceid']] + $interface['details'];
1932			}
1933
1934			unset($interface['details']);
1935
1936			$data[] = [
1937				'values' => $interface,
1938				'where' => ['interfaceid' => $interface['interfaceid']]
1939			];
1940		}
1941
1942		DB::update('interface', $data);
1943		$this->createSnmpInterfaceDetails($snmp_interfaces);
1944	}
1945
1946	/**
1947	 * Create host prototype SNMP interface details.
1948	 *
1949	 * @param array $snmp_interfaces                   Array of host prototype interface details.
1950	 * @param int   $snmp_interfaces[]['interfaceid']  Interface id.
1951	 */
1952	private function createSnmpInterfaceDetails(array $snmp_interfaces): void {
1953		if ($snmp_interfaces) {
1954			$snmp_interfaces = $this->sanitizeSnmpFields($snmp_interfaces);
1955			DB::insert('interface_snmp', $snmp_interfaces, false);
1956		}
1957	}
1958
1959	/**
1960	 * Sanitize SNMP fields by version.
1961	 *
1962	 * @param array $interfaces
1963	 *
1964	 * @return array
1965	 */
1966	private function sanitizeSnmpFields(array $interfaces): array {
1967		$default_fields = [
1968			'community' => '',
1969			'securityname' => '',
1970			'securitylevel' => DB::getDefault('interface_snmp', 'securitylevel'),
1971			'authpassphrase' => '',
1972			'privpassphrase' => '',
1973			'authprotocol' => DB::getDefault('interface_snmp', 'authprotocol'),
1974			'privprotocol' => DB::getDefault('interface_snmp', 'privprotocol'),
1975			'contextname' => ''
1976		];
1977
1978		foreach ($interfaces as &$interface) {
1979			if ($interface['version'] == SNMP_V1 || $interface['version'] == SNMP_V2C) {
1980				unset($interface['securityname'], $interface['securitylevel'], $interface['authpassphrase'],
1981					$interface['privpassphrase'], $interface['authprotocol'], $interface['privprotocol'],
1982					$interface['contextname']
1983				);
1984			}
1985			else {
1986				unset($interface['community']);
1987			}
1988
1989			$interface = $interface + $default_fields;
1990		}
1991
1992		return $interfaces;
1993	}
1994}
1995