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 hosts.
24 */
25abstract class CHostGeneral extends CHostBase {
26
27	/**
28	 * Checks if the current user has access to the given hosts and templates. Assumes the "hostid" field is valid.
29	 *
30	 * @param array $hostids    an array of host or template IDs
31	 *
32	 * @throws APIException if the user doesn't have write permissions for the given hosts.
33	 */
34	protected function checkHostPermissions(array $hostids) {
35		if ($hostids) {
36			$hostids = array_unique($hostids);
37
38			$count = API::Host()->get([
39				'countOutput' => true,
40				'hostids' => $hostids,
41				'editable' => true
42			]);
43
44			if ($count == count($hostids)) {
45				return;
46			}
47
48			$count += API::Template()->get([
49				'countOutput' => true,
50				'templateids' => $hostids,
51				'editable' => true
52			]);
53
54			if ($count != count($hostids)) {
55				self::exception(ZBX_API_ERROR_PERMISSIONS,
56					_('No permissions to referred object or it does not exist!')
57				);
58			}
59		}
60	}
61
62	/**
63	 * Allows to:
64	 * - add hosts to groups;
65	 * - link templates to hosts;
66	 * - add new macros to hosts.
67	 *
68	 * Supported $data parameters are:
69	 * - hosts          - an array of hosts to be updated
70	 * - templates      - an array of templates to be updated
71	 * - groups         - an array of host groups to add the host to
72	 * - templates_link - an array of templates to link to the hosts
73	 * - macros         - an array of macros to create on the host
74	 *
75	 * @param array $data
76	 *
77	 * @return array
78	 */
79	public function massAdd(array $data) {
80		$hostIds = zbx_objectValues($data['hosts'], 'hostid');
81		$templateIds = zbx_objectValues($data['templates'], 'templateid');
82
83		$allHostIds = array_merge($hostIds, $templateIds);
84
85		// add groups
86		if (!empty($data['groups'])) {
87			API::HostGroup()->massAdd([
88				'hosts' => $data['hosts'],
89				'templates' => $data['templates'],
90				'groups' => $data['groups']
91			]);
92		}
93
94		// link templates
95		if (!empty($data['templates_link'])) {
96			$this->checkHostPermissions($allHostIds);
97
98			$this->link(zbx_objectValues(zbx_toArray($data['templates_link']), 'templateid'), $allHostIds);
99		}
100
101		// create macros
102		if (!empty($data['macros'])) {
103			$data['macros'] = zbx_toArray($data['macros']);
104
105			$hostMacrosToAdd = [];
106			foreach ($data['macros'] as $hostMacro) {
107				foreach ($allHostIds as $hostid) {
108					$hostMacro['hostid'] = $hostid;
109					$hostMacrosToAdd[] = $hostMacro;
110				}
111			}
112
113			API::UserMacro()->create($hostMacrosToAdd);
114		}
115
116		$ids = ['hostids' => $hostIds, 'templateids' => $templateIds];
117
118		return [$this->pkOption() => $ids[$this->pkOption()]];
119	}
120
121	/**
122	 * Allows to:
123	 * - remove hosts from groups;
124	 * - unlink and clear templates from hosts;
125	 * - remove macros from hosts.
126	 *
127	 * Supported $data parameters are:
128	 * - hostids            - an array of host IDs to be updated
129	 * - templateids        - an array of template IDs to be updated
130	 * - groupids           - an array of host group IDs the hosts should be removed from
131	 * - templateids_link   - an array of template IDs to unlink from the hosts
132	 * - templateids_clear  - an array of template IDs to unlink and clear from the hosts
133	 * - macros             - an array of macros to delete from the hosts
134	 *
135	 * @param array $data
136	 *
137	 * @return array
138	 */
139	public function massRemove(array $data) {
140		$allHostIds = array_merge($data['hostids'], $data['templateids']);
141
142		$this->checkHostPermissions($allHostIds);
143
144		if (!empty($data['templateids_link'])) {
145			$this->unlink(zbx_toArray($data['templateids_link']), $allHostIds);
146		}
147
148		if (isset($data['templateids_clear'])) {
149			$this->unlink(zbx_toArray($data['templateids_clear']), $allHostIds, true);
150		}
151
152		if (array_key_exists('macros', $data)) {
153			if (!$data['macros']) {
154				self::exception(ZBX_API_ERROR_PARAMETERS, _('Empty input parameter.'));
155			}
156
157			$hostMacros = API::UserMacro()->get([
158				'output' => ['hostmacroid'],
159				'hostids' => $allHostIds,
160				'filter' => [
161					'macro' => $data['macros']
162				]
163			]);
164			$hostMacroIds = zbx_objectValues($hostMacros, 'hostmacroid');
165			if ($hostMacroIds) {
166				API::UserMacro()->delete($hostMacroIds);
167			}
168		}
169
170		if (isset($data['groupids'])) {
171			API::HostGroup()->massRemove($data);
172		}
173
174		return [$this->pkOption() => $data[$this->pkOption()]];
175	}
176
177	protected function link(array $templateIds, array $targetIds) {
178		$hosts_linkage_inserts = parent::link($templateIds, $targetIds);
179		$templates_hostids = [];
180		$link_requests = [];
181
182		foreach ($hosts_linkage_inserts as $host_tpl_ids) {
183			$templates_hostids[$host_tpl_ids['templateid']][] = $host_tpl_ids['hostid'];
184		}
185
186		foreach ($templates_hostids as $templateid => $hostids) {
187			Manager::Application()->link($templateid, $hostids);
188
189			// Fist link web items, so that later regular items can use web item as their master item.
190			Manager::HttpTest()->link($templateid, $hostids);
191		}
192
193		while ($templates_hostids) {
194			$templateid = key($templates_hostids);
195			$link_request = [
196				'hostids' => reset($templates_hostids),
197				'templateids' => [$templateid]
198			];
199			unset($templates_hostids[$templateid]);
200
201			foreach ($templates_hostids as $templateid => $hostids) {
202				if ($link_request['hostids'] === $hostids) {
203					$link_request['templateids'][] = $templateid;
204					unset($templates_hostids[$templateid]);
205				}
206			}
207
208			$link_requests[] = $link_request;
209		}
210
211		foreach ($link_requests as $link_request) {
212			API::Item()->syncTemplates($link_request);
213			API::DiscoveryRule()->syncTemplates($link_request);
214			API::ItemPrototype()->syncTemplates($link_request);
215			API::HostPrototype()->syncTemplates($link_request);
216		}
217
218		// we do linkage in two separate loops because for triggers you need all items already created on host
219		foreach ($link_requests as $link_request){
220			API::Trigger()->syncTemplates($link_request);
221			API::TriggerPrototype()->syncTemplates($link_request);
222			API::GraphPrototype()->syncTemplates($link_request);
223			API::Graph()->syncTemplates($link_request);
224		}
225
226		foreach ($link_requests as $link_request){
227			API::Trigger()->syncTemplateDependencies($link_request);
228			API::TriggerPrototype()->syncTemplateDependencies($link_request);
229		}
230
231		return $hosts_linkage_inserts;
232	}
233
234	/**
235	 * Unlinks the templates from the given hosts. If $targetids is set to null, the templates will be unlinked from
236	 * all hosts.
237	 *
238	 * @param array      $templateids
239	 * @param null|array $targetids		the IDs of the hosts to unlink the templates from
240	 * @param bool       $clear			delete all of the inherited objects from the hosts
241	 */
242	protected function unlink($templateids, $targetids = null, $clear = false) {
243		$flags = ($clear)
244			? [ZBX_FLAG_DISCOVERY_NORMAL, ZBX_FLAG_DISCOVERY_RULE]
245			: [ZBX_FLAG_DISCOVERY_NORMAL, ZBX_FLAG_DISCOVERY_RULE, ZBX_FLAG_DISCOVERY_PROTOTYPE];
246
247		// check that all triggers on templates that we unlink, don't have items from another templates
248		$sql = 'SELECT DISTINCT t.description'.
249			' FROM triggers t,functions f,items i'.
250			' WHERE t.triggerid=f.triggerid'.
251			' AND f.itemid=i.itemid'.
252			' AND '.dbConditionInt('i.hostid', $templateids).
253			' AND EXISTS ('.
254			'SELECT ff.triggerid'.
255			' FROM functions ff,items ii'.
256			' WHERE ff.itemid=ii.itemid'.
257			' AND ff.triggerid=t.triggerid'.
258			' AND '.dbConditionInt('ii.hostid', $templateids, true).
259			')'.
260			' AND t.flags='.ZBX_FLAG_DISCOVERY_NORMAL;
261		if ($dbTrigger = DBfetch(DBSelect($sql, 1))) {
262			self::exception(ZBX_API_ERROR_PARAMETERS,
263				_s('Cannot unlink trigger "%1$s", it has items from template that is left linked to host.',
264					$dbTrigger['description']
265				)
266			);
267		}
268
269		$templ_triggerids = [];
270
271		$db_triggers = DBselect(
272			'SELECT DISTINCT f.triggerid'.
273			' FROM functions f,items i'.
274			' WHERE f.itemid=i.itemid'.
275				' AND '.dbConditionInt('i.hostid', $templateids)
276		);
277
278		while ($db_trigger = DBfetch($db_triggers)) {
279			$templ_triggerids[] = $db_trigger['triggerid'];
280		}
281
282		$triggerids = [ZBX_FLAG_DISCOVERY_NORMAL => [], ZBX_FLAG_DISCOVERY_PROTOTYPE => []];
283
284		if ($templ_triggerids) {
285			$sql_distinct = ($targetids !== null) ? ' DISTINCT' : '';
286			$sql_from = ($targetids !== null) ? ',functions f,items i' : '';
287			$sql_where = ($targetids !== null)
288				? ' AND t.triggerid=f.triggerid'.
289					' AND f.itemid=i.itemid'.
290					' AND '.dbConditionInt('i.hostid', $targetids)
291				: '';
292
293			$db_triggers = DBSelect(
294				'SELECT'.$sql_distinct.' t.triggerid,t.flags'.
295				' FROM triggers t'.$sql_from.
296				' WHERE '.dbConditionInt('t.templateid', $templ_triggerids).
297					' AND '.dbConditionInt('t.flags', $flags).
298					$sql_where
299			);
300
301			while ($db_trigger = DBfetch($db_triggers)) {
302				$triggerids[$db_trigger['flags']][] = $db_trigger['triggerid'];
303			}
304		}
305
306		if ($triggerids[ZBX_FLAG_DISCOVERY_NORMAL]) {
307			if ($clear) {
308				CTriggerManager::delete($triggerids[ZBX_FLAG_DISCOVERY_NORMAL]);
309			}
310			else {
311				DB::update('triggers', [
312					'values' => ['templateid' => 0],
313					'where' => ['triggerid' => $triggerids[ZBX_FLAG_DISCOVERY_NORMAL]]
314				]);
315			}
316		}
317
318		if ($triggerids[ZBX_FLAG_DISCOVERY_PROTOTYPE]) {
319			if ($clear) {
320				CTriggerPrototypeManager::delete($triggerids[ZBX_FLAG_DISCOVERY_PROTOTYPE]);
321			}
322			else {
323				DB::update('triggers', [
324					'values' => ['templateid' => 0],
325					'where' => ['triggerid' => $triggerids[ZBX_FLAG_DISCOVERY_PROTOTYPE]]
326				]);
327			}
328		}
329
330		/* GRAPHS {{{ */
331		$db_tpl_graphs = DBselect(
332			'SELECT DISTINCT g.graphid'.
333			' FROM graphs g,graphs_items gi,items i'.
334			' WHERE g.graphid=gi.graphid'.
335				' AND gi.itemid=i.itemid'.
336				' AND '.dbConditionInt('i.hostid', $templateids).
337				' AND '.dbConditionInt('g.flags', $flags)
338		);
339
340		$tpl_graphids = [];
341
342		while ($db_tpl_graph = DBfetch($db_tpl_graphs)) {
343			$tpl_graphids[] = $db_tpl_graph['graphid'];
344		}
345
346		if ($tpl_graphids) {
347			$sql = ($targetids !== null)
348				? 'SELECT DISTINCT g.graphid,g.flags'.
349					' FROM graphs g,graphs_items gi,items i'.
350					' WHERE g.graphid=gi.graphid'.
351						' AND gi.itemid=i.itemid'.
352						' AND '.dbConditionInt('g.templateid', $tpl_graphids).
353						' AND '.dbConditionInt('i.hostid', $targetids)
354				: 'SELECT g.graphid,g.flags'.
355					' FROM graphs g'.
356					' WHERE '.dbConditionInt('g.templateid', $tpl_graphids);
357
358			$db_graphs = DBSelect($sql);
359
360			$graphs = [
361				ZBX_FLAG_DISCOVERY_NORMAL => [],
362				ZBX_FLAG_DISCOVERY_PROTOTYPE => []
363			];
364			while ($db_graph = DBfetch($db_graphs)) {
365				$graphs[$db_graph['flags']][] = $db_graph['graphid'];
366			}
367
368			if ($graphs[ZBX_FLAG_DISCOVERY_PROTOTYPE]) {
369				if ($clear) {
370					CGraphPrototypeManager::delete($graphs[ZBX_FLAG_DISCOVERY_PROTOTYPE]);
371				}
372				else {
373					DB::update('graphs', [
374						'values' => ['templateid' => 0],
375						'where' => ['graphid' => $graphs[ZBX_FLAG_DISCOVERY_PROTOTYPE]]
376					]);
377				}
378			}
379
380			if ($graphs[ZBX_FLAG_DISCOVERY_NORMAL]) {
381				if ($clear) {
382					CGraphManager::delete($graphs[ZBX_FLAG_DISCOVERY_NORMAL]);
383				}
384				else {
385					DB::update('graphs', [
386						'values' => ['templateid' => 0],
387						'where' => ['graphid' => $graphs[ZBX_FLAG_DISCOVERY_NORMAL]]
388					]);
389				}
390			}
391		}
392		/* }}} GRAPHS */
393
394		/* ITEMS, DISCOVERY RULES {{{ */
395		$sqlFrom = ' items i1,items i2,hosts h';
396		$sqlWhere = ' i2.itemid=i1.templateid'.
397			' AND '.dbConditionInt('i2.hostid', $templateids).
398			' AND '.dbConditionInt('i1.flags', $flags).
399			' AND h.hostid=i1.hostid';
400
401		if (!is_null($targetids)) {
402			$sqlWhere .= ' AND '.dbConditionInt('i1.hostid', $targetids);
403		}
404		$sql = 'SELECT DISTINCT i1.itemid,i1.flags,i1.name,i1.hostid,h.name as host'.
405			' FROM '.$sqlFrom.
406			' WHERE '.$sqlWhere;
407		$dbItems = DBSelect($sql);
408		$items = [
409			ZBX_FLAG_DISCOVERY_NORMAL => [],
410			ZBX_FLAG_DISCOVERY_RULE => [],
411			ZBX_FLAG_DISCOVERY_PROTOTYPE => []
412		];
413		while ($item = DBfetch($dbItems)) {
414			$items[$item['flags']][$item['itemid']] = [
415				'name' => $item['name'],
416				'host' => $item['host']
417			];
418		}
419
420		if (!empty($items[ZBX_FLAG_DISCOVERY_RULE])) {
421			if ($clear) {
422				CDiscoveryRuleManager::delete(array_keys($items[ZBX_FLAG_DISCOVERY_RULE]));
423			}
424			else{
425				DB::update('items', [
426					'values' => ['templateid' => 0],
427					'where' => ['itemid' => array_keys($items[ZBX_FLAG_DISCOVERY_RULE])]
428				]);
429
430				foreach ($items[ZBX_FLAG_DISCOVERY_RULE] as $discoveryRule) {
431					info(_s('Unlinked: Discovery rule "%1$s" on "%2$s".', $discoveryRule['name'], $discoveryRule['host']));
432				}
433			}
434		}
435
436		if (!empty($items[ZBX_FLAG_DISCOVERY_NORMAL])) {
437			if ($clear) {
438				CItemManager::delete(array_keys($items[ZBX_FLAG_DISCOVERY_NORMAL]));
439			}
440			else{
441				DB::update('items', [
442					'values' => ['templateid' => 0],
443					'where' => ['itemid' => array_keys($items[ZBX_FLAG_DISCOVERY_NORMAL])]
444				]);
445
446				foreach ($items[ZBX_FLAG_DISCOVERY_NORMAL] as $item) {
447					info(_s('Unlinked: Item "%1$s" on "%2$s".', $item['name'], $item['host']));
448				}
449			}
450		}
451
452		if (!empty($items[ZBX_FLAG_DISCOVERY_PROTOTYPE])) {
453			$item_prototypeids = array_keys($items[ZBX_FLAG_DISCOVERY_PROTOTYPE]);
454
455			if ($clear) {
456				// This will include deletion of linked application prototypes.
457				CItemPrototypeManager::delete($item_prototypeids);
458			}
459			else {
460				DB::update('items', [
461					'values' => ['templateid' => 0],
462					'where' => ['itemid' => $item_prototypeids]
463				]);
464
465				foreach ($items[ZBX_FLAG_DISCOVERY_PROTOTYPE] as $item) {
466					info(_s('Unlinked: Item prototype "%1$s" on "%2$s".', $item['name'], $item['host']));
467				}
468
469				/*
470				 * Convert templated application prototypes to normal application prototypes
471				 * who are linked to these item prototypes.
472				 */
473				$application_prototypes = DBfetchArray(DBselect(
474					'SELECT ap.application_prototypeid'.
475					' FROM application_prototype ap'.
476					' WHERE EXISTS ('.
477						'SELECT NULL'.
478						' FROM item_application_prototype iap'.
479						' WHERE '.dbConditionInt('iap.itemid', $item_prototypeids).
480							' AND iap.application_prototypeid=ap.application_prototypeid'.
481					')'
482				));
483
484				if ($application_prototypes) {
485					$application_prototypeids = zbx_objectValues($application_prototypes, 'application_prototypeid');
486
487					DB::update('application_prototype', [
488						'values' => ['templateid' => 0],
489						'where' => ['application_prototypeid' => $application_prototypeids]
490					]);
491				}
492			}
493		}
494		/* }}} ITEMS, DISCOVERY RULES */
495
496		// host prototypes
497		// we need only to unlink host prototypes. in case of unlink and clear they will be deleted together with LLD rules.
498		if (!$clear && isset($items[ZBX_FLAG_DISCOVERY_RULE])) {
499			$discoveryRuleIds = array_keys($items[ZBX_FLAG_DISCOVERY_RULE]);
500
501			$hostPrototypes = DBfetchArrayAssoc(DBSelect(
502				'SELECT DISTINCT h.hostid,h.host,h3.host AS parent_host'.
503				' FROM hosts h'.
504					' INNER JOIN host_discovery hd ON h.hostid=hd.hostid'.
505					' INNER JOIN hosts h2 ON h.templateid=h2.hostid'.
506					' INNER JOIN host_discovery hd2 ON h.hostid=hd.hostid'.
507					' INNER JOIN items i ON hd.parent_itemid=i.itemid'.
508					' INNER JOIN hosts h3 ON i.hostid=h3.hostid'.
509				' WHERE '.dbConditionInt('hd.parent_itemid', $discoveryRuleIds)
510			), 'hostid');
511			if ($hostPrototypes) {
512				DB::update('hosts', [
513					'values' => ['templateid' => 0],
514					'where' => ['hostid' => array_keys($hostPrototypes)]
515				]);
516				DB::update('group_prototype', [
517					'values' => ['templateid' => 0],
518					'where' => ['hostid' => array_keys($hostPrototypes)]
519				]);
520				foreach ($hostPrototypes as $hostPrototype) {
521					info(_s('Unlinked: Host prototype "%1$s" on "%2$s".', $hostPrototype['host'], $hostPrototype['parent_host']));
522				}
523			}
524		}
525
526		// http tests
527		$sqlWhere = '';
528		if (!is_null($targetids)) {
529			$sqlWhere = ' AND '.dbConditionInt('ht1.hostid', $targetids);
530		}
531		$sql = 'SELECT DISTINCT ht1.httptestid,ht1.name,h.name as host'.
532				' FROM httptest ht1'.
533				' INNER JOIN httptest ht2 ON ht2.httptestid=ht1.templateid'.
534				' INNER JOIN hosts h ON h.hostid=ht1.hostid'.
535				' WHERE '.dbConditionInt('ht2.hostid', $templateids).
536				$sqlWhere;
537		$dbHttpTests = DBSelect($sql);
538		$httpTests = [];
539		while ($httpTest = DBfetch($dbHttpTests)) {
540			$httpTests[$httpTest['httptestid']] = [
541				'name' => $httpTest['name'],
542				'host' => $httpTest['host']
543			];
544		}
545
546		if (!empty($httpTests)) {
547			if ($clear) {
548				$result = API::HttpTest()->delete(array_keys($httpTests), true);
549				if (!$result) {
550					self::exception(ZBX_API_ERROR_INTERNAL, _('Cannot unlink and clear Web scenarios.'));
551				}
552			}
553			else {
554				DB::update('httptest', [
555					'values' => ['templateid' => 0],
556					'where' => ['httptestid' => array_keys($httpTests)]
557				]);
558				foreach ($httpTests as $httpTest) {
559					info(_s('Unlinked: Web scenario "%1$s" on "%2$s".', $httpTest['name'], $httpTest['host']));
560				}
561			}
562		}
563
564		/* APPLICATIONS {{{ */
565		$sql = 'SELECT at.application_templateid,at.applicationid,h.name,h.host,h.hostid'.
566			' FROM applications a1,application_template at,applications a2,hosts h'.
567			' WHERE a1.applicationid=at.applicationid'.
568				' AND at.templateid=a2.applicationid'.
569				' AND '.dbConditionInt('a2.hostid', $templateids).
570				' AND a1.hostid=h.hostid';
571		if ($targetids) {
572			$sql .= ' AND '.dbConditionInt('a1.hostid', $targetids);
573		}
574		$query = DBselect($sql);
575		$applicationTemplates = [];
576		while ($applicationTemplate = DBfetch($query)) {
577			$applicationTemplates[] = [
578				'applicationid' => $applicationTemplate['applicationid'],
579				'application_templateid' => $applicationTemplate['application_templateid'],
580				'name' => $applicationTemplate['name'],
581				'hostid' => $applicationTemplate['hostid'],
582				'host' => $applicationTemplate['host']
583			];
584		}
585
586		if ($applicationTemplates) {
587			// unlink applications from templates
588			DB::delete('application_template', [
589				'application_templateid' => zbx_objectValues($applicationTemplates, 'application_templateid')
590			]);
591
592			if ($clear) {
593				// Delete inherited applications that are no longer linked to any templates and items.
594				$applicationids = zbx_objectValues($applicationTemplates, 'applicationid');
595
596				$applications = DBfetchArray(DBselect(
597					'SELECT a.applicationid'.
598					' FROM applications a'.
599						' LEFT JOIN application_template at ON a.applicationid=at.applicationid'.
600					' WHERE '.dbConditionInt('a.applicationid', $applicationids).
601						' AND at.applicationid IS NULL'.
602						' AND a.applicationid NOT IN ('.
603							'SELECT ia.applicationid'.
604							' FROM items_applications ia'.
605							' WHERE '.dbConditionInt('ia.applicationid', $applicationids).
606						')'
607				));
608				if ($applications) {
609					$result = API::Application()->delete(zbx_objectValues($applications, 'applicationid'), true);
610					if (!$result) {
611						self::exception(ZBX_API_ERROR_INTERNAL, _('Cannot unlink and clear applications.'));
612					}
613				}
614			}
615			else {
616				foreach ($applicationTemplates as $application) {
617					info(_s('Unlinked: Application "%1$s" on "%2$s".', $application['name'], $application['host']));
618				}
619			}
620		}
621
622		/*
623		 * Process discovered applications when parent is a host, not template.
624		 * If a discovered application has no longer linked items, remove them.
625		 */
626		if ($targetids) {
627			$discovered_applications = API::Application()->get([
628				'output' => ['applicationid'],
629				'hostids' => $targetids,
630				'filter' => ['flags' => ZBX_FLAG_DISCOVERY_CREATED],
631				'preservekeys' => true
632			]);
633
634			if ($discovered_applications) {
635				$discovered_applications = API::Application()->get([
636					'output' => ['applicationid'],
637					'selectItems' => ['itemid'],
638					'applicationids' => array_keys($discovered_applications),
639					'filter' => ['flags' => ZBX_FLAG_DISCOVERY_CREATED]
640				]);
641
642				$applications_to_delete = [];
643
644				foreach ($discovered_applications as $discovered_application) {
645					if (!$discovered_application['items']) {
646						$applications_to_delete[$discovered_application['applicationid']] = true;
647					}
648				}
649
650				if ($applications_to_delete) {
651					API::Application()->delete(array_keys($applications_to_delete), true);
652				}
653			}
654		}
655		/* }}} APPLICATIONS */
656
657		parent::unlink($templateids, $targetids);
658	}
659
660	protected function addRelatedObjects(array $options, array $result) {
661		$result = parent::addRelatedObjects($options, $result);
662
663		$hostids = array_keys($result);
664
665		// adding groups
666		if ($options['selectGroups'] !== null && $options['selectGroups'] != API_OUTPUT_COUNT) {
667			$relationMap = $this->createRelationMap($result, 'hostid', 'groupid', 'hosts_groups');
668			$groups = API::HostGroup()->get([
669				'output' => $options['selectGroups'],
670				'groupids' => $relationMap->getRelatedIds(),
671				'preservekeys' => true
672			]);
673			$result = $relationMap->mapMany($result, $groups, 'groups');
674		}
675
676		// adding templates
677		if ($options['selectParentTemplates'] !== null) {
678			if ($options['selectParentTemplates'] != API_OUTPUT_COUNT) {
679				$templates = [];
680				$relationMap = $this->createRelationMap($result, 'hostid', 'templateid', 'hosts_templates');
681				$related_ids = $relationMap->getRelatedIds();
682
683				if ($related_ids) {
684					$templates = API::Template()->get([
685						'output' => $options['selectParentTemplates'],
686						'templateids' => $related_ids,
687						'nopermissions' => $options['nopermissions'],
688						'preservekeys' => true
689					]);
690					if (!is_null($options['limitSelects'])) {
691						order_result($templates, 'host');
692					}
693				}
694
695				$result = $relationMap->mapMany($result, $templates, 'parentTemplates', $options['limitSelects']);
696			}
697			else {
698				$templates = API::Template()->get([
699					'hostids' => $hostids,
700					'countOutput' => true,
701					'groupCount' => true
702				]);
703				$templates = zbx_toHash($templates, 'hostid');
704				foreach ($result as $hostid => $host) {
705					$result[$hostid]['parentTemplates'] = array_key_exists($hostid, $templates)
706						? $templates[$hostid]['rowscount']
707						: '0';
708				}
709			}
710		}
711
712		// adding items
713		if ($options['selectItems'] !== null) {
714			if ($options['selectItems'] != API_OUTPUT_COUNT) {
715				$items = API::Item()->get([
716					'output' => $this->outputExtend($options['selectItems'], ['hostid', 'itemid']),
717					'hostids' => $hostids,
718					'nopermissions' => true,
719					'preservekeys' => true
720				]);
721
722				if (!is_null($options['limitSelects'])) {
723					order_result($items, 'name');
724				}
725
726				$relationMap = $this->createRelationMap($items, 'hostid', 'itemid');
727
728				$items = $this->unsetExtraFields($items, ['hostid', 'itemid'], $options['selectItems']);
729				$result = $relationMap->mapMany($result, $items, 'items', $options['limitSelects']);
730			}
731			else {
732				$items = API::Item()->get([
733					'hostids' => $hostids,
734					'nopermissions' => true,
735					'countOutput' => true,
736					'groupCount' => true
737				]);
738				$items = zbx_toHash($items, 'hostid');
739				foreach ($result as $hostid => $host) {
740					$result[$hostid]['items'] = array_key_exists($hostid, $items) ? $items[$hostid]['rowscount'] : '0';
741				}
742			}
743		}
744
745		// adding discoveries
746		if ($options['selectDiscoveries'] !== null) {
747			if ($options['selectDiscoveries'] != API_OUTPUT_COUNT) {
748				$items = API::DiscoveryRule()->get([
749					'output' => $this->outputExtend($options['selectDiscoveries'], ['hostid', 'itemid']),
750					'hostids' => $hostids,
751					'nopermissions' => true,
752					'preservekeys' => true
753				]);
754
755				if (!is_null($options['limitSelects'])) {
756					order_result($items, 'name');
757				}
758
759				$relationMap = $this->createRelationMap($items, 'hostid', 'itemid');
760
761				$items = $this->unsetExtraFields($items, ['hostid', 'itemid'], $options['selectDiscoveries']);
762				$result = $relationMap->mapMany($result, $items, 'discoveries', $options['limitSelects']);
763			}
764			else {
765				$items = API::DiscoveryRule()->get([
766					'hostids' => $hostids,
767					'nopermissions' => true,
768					'countOutput' => true,
769					'groupCount' => true
770				]);
771				$items = zbx_toHash($items, 'hostid');
772				foreach ($result as $hostid => $host) {
773					$result[$hostid]['discoveries'] = array_key_exists($hostid, $items)
774						? $items[$hostid]['rowscount']
775						: '0';
776				}
777			}
778		}
779
780		// adding triggers
781		if ($options['selectTriggers'] !== null) {
782			if ($options['selectTriggers'] != API_OUTPUT_COUNT) {
783				$triggers = [];
784				$relationMap = new CRelationMap();
785				// discovered items
786				$res = DBselect(
787					'SELECT i.hostid,f.triggerid'.
788						' FROM items i,functions f'.
789						' WHERE '.dbConditionInt('i.hostid', $hostids).
790						' AND i.itemid=f.itemid'
791				);
792				while ($relation = DBfetch($res)) {
793					$relationMap->addRelation($relation['hostid'], $relation['triggerid']);
794				}
795
796				$related_ids = $relationMap->getRelatedIds();
797
798				if ($related_ids) {
799					$triggers = API::Trigger()->get([
800						'output' => $options['selectTriggers'],
801						'triggerids' => $related_ids,
802						'preservekeys' => true
803					]);
804					if (!is_null($options['limitSelects'])) {
805						order_result($triggers, 'description');
806					}
807				}
808
809				$result = $relationMap->mapMany($result, $triggers, 'triggers', $options['limitSelects']);
810			}
811			else {
812				$triggers = API::Trigger()->get([
813					'hostids' => $hostids,
814					'countOutput' => true,
815					'groupCount' => true
816				]);
817				$triggers = zbx_toHash($triggers, 'hostid');
818
819				foreach ($result as $hostid => $host) {
820					$result[$hostid]['triggers'] = array_key_exists($hostid, $triggers)
821						? $triggers[$hostid]['rowscount']
822						: '0';
823				}
824			}
825		}
826
827		// adding graphs
828		if ($options['selectGraphs'] !== null) {
829			if ($options['selectGraphs'] != API_OUTPUT_COUNT) {
830				$graphs = [];
831				$relationMap = new CRelationMap();
832				// discovered items
833				$res = DBselect(
834					'SELECT i.hostid,gi.graphid'.
835						' FROM items i,graphs_items gi'.
836						' WHERE '.dbConditionInt('i.hostid', $hostids).
837						' AND i.itemid=gi.itemid'
838				);
839				while ($relation = DBfetch($res)) {
840					$relationMap->addRelation($relation['hostid'], $relation['graphid']);
841				}
842
843				$related_ids = $relationMap->getRelatedIds();
844
845				if ($related_ids) {
846					$graphs = API::Graph()->get([
847						'output' => $options['selectGraphs'],
848						'graphids' => $related_ids,
849						'preservekeys' => true
850					]);
851					if (!is_null($options['limitSelects'])) {
852						order_result($graphs, 'name');
853					}
854				}
855
856				$result = $relationMap->mapMany($result, $graphs, 'graphs', $options['limitSelects']);
857			}
858			else {
859				$graphs = API::Graph()->get([
860					'hostids' => $hostids,
861					'countOutput' => true,
862					'groupCount' => true
863				]);
864				$graphs = zbx_toHash($graphs, 'hostid');
865				foreach ($result as $hostid => $host) {
866					$result[$hostid]['graphs'] = array_key_exists($hostid, $graphs)
867						? $graphs[$hostid]['rowscount']
868						: '0';
869				}
870			}
871		}
872
873		// adding http tests
874		if ($options['selectHttpTests'] !== null) {
875			if ($options['selectHttpTests'] != API_OUTPUT_COUNT) {
876				$httpTests = API::HttpTest()->get([
877					'output' => $this->outputExtend($options['selectHttpTests'], ['hostid', 'httptestid']),
878					'hostids' => $hostids,
879					'nopermissions' => true,
880					'preservekeys' => true
881				]);
882
883				if (!is_null($options['limitSelects'])) {
884					order_result($httpTests, 'name');
885				}
886
887				$relationMap = $this->createRelationMap($httpTests, 'hostid', 'httptestid');
888
889				$httpTests = $this->unsetExtraFields($httpTests, ['hostid', 'httptestid'], $options['selectHttpTests']);
890				$result = $relationMap->mapMany($result, $httpTests, 'httpTests', $options['limitSelects']);
891			}
892			else {
893				$httpTests = API::HttpTest()->get([
894					'hostids' => $hostids,
895					'nopermissions' => true,
896					'countOutput' => true,
897					'groupCount' => true
898				]);
899				$httpTests = zbx_toHash($httpTests, 'hostid');
900				foreach ($result as $hostid => $host) {
901					$result[$hostid]['httpTests'] = array_key_exists($hostid, $httpTests)
902						? $httpTests[$hostid]['rowscount']
903						: '0';
904				}
905			}
906		}
907
908		// adding applications
909		if ($options['selectApplications'] !== null) {
910			if ($options['selectApplications'] != API_OUTPUT_COUNT) {
911				$applications = API::Application()->get([
912					'output' => $this->outputExtend($options['selectApplications'], ['hostid', 'applicationid']),
913					'hostids' => $hostids,
914					'nopermissions' => true,
915					'preservekeys' => true
916				]);
917
918				if (!is_null($options['limitSelects'])) {
919					order_result($applications, 'name');
920				}
921
922				$relationMap = $this->createRelationMap($applications, 'hostid', 'applicationid');
923
924				$applications = $this->unsetExtraFields($applications, ['hostid', 'applicationid'],
925					$options['selectApplications']
926				);
927				$result = $relationMap->mapMany($result, $applications, 'applications', $options['limitSelects']);
928			}
929			else {
930				$applications = API::Application()->get([
931					'output' => $options['selectApplications'],
932					'hostids' => $hostids,
933					'nopermissions' => true,
934					'countOutput' => true,
935					'groupCount' => true
936				]);
937
938				$applications = zbx_toHash($applications, 'hostid');
939				foreach ($result as $hostid => $host) {
940					$result[$hostid]['applications'] = array_key_exists($hostid, $applications)
941						? $applications[$hostid]['rowscount']
942						: '0';
943				}
944			}
945		}
946
947		// adding macros
948		if ($options['selectMacros'] !== null && $options['selectMacros'] != API_OUTPUT_COUNT) {
949			$macros = API::UserMacro()->get([
950				'output' => $this->outputExtend($options['selectMacros'], ['hostid', 'hostmacroid']),
951				'hostids' => $hostids,
952				'preservekeys' => true
953			]);
954
955			$relationMap = $this->createRelationMap($macros, 'hostid', 'hostmacroid');
956
957			$macros = $this->unsetExtraFields($macros, ['hostid', 'hostmacroid'], $options['selectMacros']);
958			$result = $relationMap->mapMany($result, $macros, 'macros', $options['limitSelects']);
959		}
960
961		// adding tags
962		if ($options['selectTags'] !== null && $options['selectTags'] != API_OUTPUT_COUNT) {
963			if ($options['selectTags'] === API_OUTPUT_EXTEND) {
964				$options['selectTags'] = ['tag', 'value'];
965			}
966
967			$tags_options = [
968				'output' => $this->outputExtend($options['selectTags'], ['hostid']),
969				'filter' => ['hostid' => $hostids]
970			];
971
972			foreach ($result as &$host) {
973				$host['tags'] = [];
974			}
975			unset($host);
976
977			$tags = DBselect(DB::makeSql('host_tag', $tags_options));
978
979			while ($tag = DBfetch($tags)) {
980				$hostid = $tag['hostid'];
981				unset($tag['hosttagid'], $tag['hostid']);
982				$result[$hostid]['tags'][] = $tag;
983			}
984		}
985
986		return $result;
987	}
988
989	/**
990	 * Compares input tags with tags stored in the database and performs tag deleting and inserting.
991	 *
992	 * @param array  $hosts
993	 * @param int    $hosts[]['hostid']
994	 * @param int    $hosts[]['templateid']
995	 * @param array  $hosts[]['tags']
996	 * @param string $hosts[]['tags'][]['tag']
997	 * @param string $hosts[]['tags'][]['value']
998	 * @param string $id_field
999	 */
1000	protected function updateTags(array $hosts, $id_field) {
1001		$hostids = [];
1002		foreach ($hosts as $index => $host) {
1003			if (!array_key_exists('tags', $host)) {
1004				unset($host[$index]);
1005				continue;
1006			}
1007			$hostids[] = $host[$id_field];
1008		}
1009
1010		if (!$hostids) {
1011			return;
1012		}
1013
1014		$options = [
1015			'output' => ['hosttagid', 'hostid', 'tag', 'value'],
1016			'filter' => ['hostid' => $hostids]
1017		];
1018
1019		$db_tags = DBselect(DB::makeSql('host_tag', $options));
1020		$db_hosts = [];
1021		$del_hosttagids = [];
1022
1023		while ($db_tag = DBfetch($db_tags)) {
1024			$db_hosts[$db_tag['hostid']]['tags'][] = $db_tag;
1025			$del_hosttagids[$db_tag['hosttagid']] = true;
1026		}
1027
1028		$ins_tags = [];
1029		foreach ($hosts as $host) {
1030			foreach ($host['tags'] as $tag) {
1031				$tag += ['value' => ''];
1032
1033				if (array_key_exists($host[$id_field], $db_hosts)) {
1034					foreach ($db_hosts[$host[$id_field]]['tags'] as $db_tag) {
1035						if ($tag['tag'] === $db_tag['tag'] && $tag['value'] === $db_tag['value']) {
1036							unset($del_hosttagids[$db_tag['hosttagid']]);
1037							$tag = null;
1038							break;
1039						}
1040					}
1041				}
1042
1043				if ($tag !== null) {
1044					$ins_tags[] = ['hostid' => $host[$id_field]] + $tag;
1045				}
1046			}
1047		}
1048
1049		if ($del_hosttagids) {
1050			DB::delete('host_tag', ['hosttagid' => array_keys($del_hosttagids)]);
1051		}
1052
1053		if ($ins_tags) {
1054			DB::insert('host_tag', $ins_tags);
1055		}
1056	}
1057
1058	/**
1059	 * Validates tags.
1060	 *
1061	 * @param array  $host
1062	 * @param int    $host['evaltype']
1063	 * @param array  $host['tags']
1064	 * @param string $host['tags'][]['tag']
1065	 * @param string $host['tags'][]['value']
1066	 *
1067	 * @throws APIException if the input is invalid.
1068	 */
1069	protected function validateTags(array $host) {
1070		$api_input_rules = ['type' => API_OBJECT, 'fields' => [
1071			'evaltype'	=> ['type' => API_INT32, 'in' => implode(',', [TAG_EVAL_TYPE_AND_OR, TAG_EVAL_TYPE_OR])],
1072			'tags'		=> ['type' => API_OBJECTS, 'flags' => API_NORMALIZE, 'uniq' => [['tag', 'value']], 'fields' => [
1073				'tag'		=> ['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('host_tag', 'tag')],
1074				'value'		=> ['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('host_tag', 'value'), 'default' => DB::getDefault('host_tag', 'value')]
1075			]]
1076		]];
1077
1078		// Keep values only for fields with defined validation rules.
1079		$host = array_intersect_key($host, $api_input_rules['fields']);
1080
1081		if (!CApiInputValidator::validate($api_input_rules, $host, '/', $error)) {
1082			self::exception(ZBX_API_ERROR_PARAMETERS, $error);
1083		}
1084	}
1085}
1086