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 groups.
24 *
25 * @package API
26 */
27class CHostGroup extends CApiService {
28
29	protected $tableName = 'groups';
30	protected $tableAlias = 'g';
31	protected $sortColumns = ['groupid', 'name'];
32
33	/**
34	 * Get host groups.
35	 *
36	 * @param array $params
37	 *
38	 * @return array
39	 */
40	public function get($params) {
41		$result = [];
42
43		$sqlParts = [
44			'select'	=> ['groups' => 'g.groupid'],
45			'from'		=> ['groups' => 'groups g'],
46			'where'		=> [],
47			'order'		=> [],
48			'limit'		=> null
49		];
50
51		$defOptions = [
52			'groupids'					=> null,
53			'hostids'					=> null,
54			'templateids'				=> null,
55			'graphids'					=> null,
56			'triggerids'				=> null,
57			'maintenanceids'			=> null,
58			'monitored_hosts'			=> null,
59			'templated_hosts'			=> null,
60			'real_hosts'				=> null,
61			'with_hosts_and_templates'	=> null,
62			'with_items'				=> null,
63			'with_simple_graph_items'	=> null,
64			'with_monitored_items'		=> null,
65			'with_triggers'				=> null,
66			'with_monitored_triggers'	=> null,
67			'with_httptests'			=> null,
68			'with_monitored_httptests'	=> null,
69			'with_graphs'				=> null,
70			'with_applications'			=> null,
71			'editable'					=> false,
72			'nopermissions'				=> null,
73			// filter
74			'filter'					=> null,
75			'search'					=> null,
76			'searchByAny'				=> null,
77			'startSearch'				=> null,
78			'excludeSearch'				=> null,
79			'searchWildcardsEnabled'	=> null,
80			// output
81			'output'					=> API_OUTPUT_EXTEND,
82			'selectHosts'				=> null,
83			'selectTemplates'			=> null,
84			'selectGroupDiscovery'		=> null,
85			'selectDiscoveryRule'		=> null,
86			'countOutput'				=> null,
87			'groupCount'				=> null,
88			'preservekeys'				=> null,
89			'sortfield'					=> '',
90			'sortorder'					=> '',
91			'limit'						=> null,
92			'limitSelects'				=> null
93		];
94		$options = zbx_array_merge($defOptions, $params);
95
96		// editable + PERMISSION CHECK
97		if (self::$userData['type'] != USER_TYPE_SUPER_ADMIN && !$options['nopermissions']) {
98			$permission = $options['editable'] ? PERM_READ_WRITE : PERM_READ;
99			$userGroups = getUserGroupsByUserId(self::$userData['userid']);
100
101			$sqlParts['where'][] = 'EXISTS ('.
102				'SELECT NULL'.
103				' FROM rights r'.
104				' WHERE g.groupid=r.id'.
105					' AND '.dbConditionInt('r.groupid', $userGroups).
106				' GROUP BY r.id'.
107				' HAVING MIN(r.permission)>'.PERM_DENY.
108					' AND MAX(r.permission)>='.zbx_dbstr($permission).
109				')';
110		}
111
112		// groupids
113		if (!is_null($options['groupids'])) {
114			zbx_value2array($options['groupids']);
115			$sqlParts['where']['groupid'] = dbConditionInt('g.groupid', $options['groupids']);
116		}
117
118		// templateids
119		if (!is_null($options['templateids'])) {
120			zbx_value2array($options['templateids']);
121
122			if (!is_null($options['hostids'])) {
123				zbx_value2array($options['hostids']);
124				$options['hostids'] = array_merge($options['hostids'], $options['templateids']);
125			}
126			else {
127				$options['hostids'] = $options['templateids'];
128			}
129		}
130
131		// hostids
132		if (!is_null($options['hostids'])) {
133			zbx_value2array($options['hostids']);
134
135			$sqlParts['from']['hosts_groups'] = 'hosts_groups hg';
136			$sqlParts['where'][] = dbConditionInt('hg.hostid', $options['hostids']);
137			$sqlParts['where']['hgg'] = 'hg.groupid=g.groupid';
138		}
139
140		// triggerids
141		if (!is_null($options['triggerids'])) {
142			zbx_value2array($options['triggerids']);
143
144			$sqlParts['from']['hosts_groups'] = 'hosts_groups hg';
145			$sqlParts['from']['functions'] = 'functions f';
146			$sqlParts['from']['items'] = 'items i';
147			$sqlParts['where'][] = dbConditionInt('f.triggerid', $options['triggerids']);
148			$sqlParts['where']['fi'] = 'f.itemid=i.itemid';
149			$sqlParts['where']['hgi'] = 'hg.hostid=i.hostid';
150			$sqlParts['where']['hgg'] = 'hg.groupid=g.groupid';
151		}
152
153		// graphids
154		if (!is_null($options['graphids'])) {
155			zbx_value2array($options['graphids']);
156
157			$sqlParts['from']['gi'] = 'graphs_items gi';
158			$sqlParts['from']['i'] = 'items i';
159			$sqlParts['from']['hg'] = 'hosts_groups hg';
160			$sqlParts['where'][] = dbConditionInt('gi.graphid', $options['graphids']);
161			$sqlParts['where']['hgg'] = 'hg.groupid=g.groupid';
162			$sqlParts['where']['igi'] = 'i.itemid=gi.itemid';
163			$sqlParts['where']['hgi'] = 'hg.hostid=i.hostid';
164		}
165
166		// maintenanceids
167		if (!is_null($options['maintenanceids'])) {
168			zbx_value2array($options['maintenanceids']);
169
170			$sqlParts['from']['maintenances_groups'] = 'maintenances_groups mg';
171			$sqlParts['where'][] = dbConditionInt('mg.maintenanceid', $options['maintenanceids']);
172			$sqlParts['where']['hmh'] = 'g.groupid=mg.groupid';
173		}
174
175		$sub_sql_parts = array();
176
177		// monitored_hosts, real_hosts, templated_hosts, with_hosts_and_templates
178		if ($options['monitored_hosts'] !== null) {
179			$sub_sql_parts['from']['h'] = 'hosts h';
180			$sub_sql_parts['where']['hg-h'] = 'hg.hostid=h.hostid';
181			$sub_sql_parts['where'][] = dbConditionInt('h.status', array(HOST_STATUS_MONITORED));
182		}
183		elseif ($options['real_hosts'] !== null) {
184			$sub_sql_parts['from']['h'] = 'hosts h';
185			$sub_sql_parts['where']['hg-h'] = 'hg.hostid=h.hostid';
186			$sub_sql_parts['where'][] = dbConditionInt('h.status',
187				array(HOST_STATUS_MONITORED, HOST_STATUS_NOT_MONITORED)
188			);
189		}
190		elseif ($options['templated_hosts'] !== null) {
191			$sub_sql_parts['from']['h'] = 'hosts h';
192			$sub_sql_parts['where']['hg-h'] = 'hg.hostid=h.hostid';
193			$sub_sql_parts['where'][] = dbConditionInt('h.status', array(HOST_STATUS_TEMPLATE));
194		}
195		elseif ($options['with_hosts_and_templates'] !== null) {
196			$sub_sql_parts['from']['h'] = 'hosts h';
197			$sub_sql_parts['where']['hg-h'] = 'hg.hostid=h.hostid';
198			$sub_sql_parts['where'][] = dbConditionInt('h.status',
199				array(HOST_STATUS_MONITORED, HOST_STATUS_NOT_MONITORED, HOST_STATUS_TEMPLATE)
200			);
201		}
202
203		// with_items, with_monitored_items, with_simple_graph_items
204		if ($options['with_items'] !== null) {
205			$sub_sql_parts['from']['i'] = 'items i';
206			$sub_sql_parts['where']['hg-i'] = 'hg.hostid=i.hostid';
207			$sub_sql_parts['where'][] = dbConditionInt('i.flags',
208				array(ZBX_FLAG_DISCOVERY_NORMAL, ZBX_FLAG_DISCOVERY_CREATED)
209			);
210		}
211		elseif ($options['with_monitored_items'] !== null) {
212			$sub_sql_parts['from']['i'] = 'items i';
213			$sub_sql_parts['from']['h'] = 'hosts h';
214			$sub_sql_parts['where']['hg-i'] = 'hg.hostid=i.hostid';
215			$sub_sql_parts['where']['hg-h'] = 'hg.hostid=h.hostid';
216			$sub_sql_parts['where'][] = dbConditionInt('h.status', array(HOST_STATUS_MONITORED));
217			$sub_sql_parts['where'][] = dbConditionInt('i.status', array(ITEM_STATUS_ACTIVE));
218			$sub_sql_parts['where'][] = dbConditionInt('i.flags',
219				array(ZBX_FLAG_DISCOVERY_NORMAL, ZBX_FLAG_DISCOVERY_CREATED)
220			);
221		}
222		elseif ($options['with_simple_graph_items'] !== null) {
223			$sub_sql_parts['from']['i'] = 'items i';
224			$sub_sql_parts['where']['hg-i'] = 'hg.hostid=i.hostid';
225			$sub_sql_parts['where'][] = dbConditionInt('i.value_type',
226				array(ITEM_VALUE_TYPE_FLOAT, ITEM_VALUE_TYPE_UINT64)
227			);
228			$sub_sql_parts['where'][] = dbConditionInt('i.status', array(ITEM_STATUS_ACTIVE));
229			$sub_sql_parts['where'][] = dbConditionInt('i.flags',
230				array(ZBX_FLAG_DISCOVERY_NORMAL, ZBX_FLAG_DISCOVERY_CREATED)
231			);
232		}
233
234		// with_triggers, with_monitored_triggers
235		if ($options['with_triggers'] !== null) {
236			$sub_sql_parts['from']['i'] = 'items i';
237			$sub_sql_parts['from']['f'] = 'functions f';
238			$sub_sql_parts['from']['t'] = 'triggers t';
239			$sub_sql_parts['where']['hg-i'] = 'hg.hostid=i.hostid';
240			$sub_sql_parts['where']['i-f'] = 'i.itemid=f.itemid';
241			$sub_sql_parts['where']['f-t'] = 'f.triggerid=t.triggerid';
242			$sub_sql_parts['where'][] = dbConditionInt('t.flags',
243				array(ZBX_FLAG_DISCOVERY_NORMAL, ZBX_FLAG_DISCOVERY_CREATED)
244			);
245		}
246		elseif ($options['with_monitored_triggers'] !== null) {
247			$sub_sql_parts['from']['i'] = 'items i';
248			$sub_sql_parts['from']['h'] = 'hosts h';
249			$sub_sql_parts['from']['f'] = 'functions f';
250			$sub_sql_parts['from']['t'] = 'triggers t';
251			$sub_sql_parts['where']['hg-i'] = 'hg.hostid=i.hostid';
252			$sub_sql_parts['where']['hg-h'] = 'hg.hostid=h.hostid';
253			$sub_sql_parts['where']['i-f'] = 'i.itemid=f.itemid';
254			$sub_sql_parts['where']['f-t'] = 'f.triggerid=t.triggerid';
255			$sub_sql_parts['where'][] = dbConditionInt('h.status', array(HOST_STATUS_MONITORED));
256			$sub_sql_parts['where'][] = dbConditionInt('i.status', array(ITEM_STATUS_ACTIVE));
257			$sub_sql_parts['where'][] = dbConditionInt('t.status', array(TRIGGER_STATUS_ENABLED));
258			$sub_sql_parts['where'][] = dbConditionInt('t.flags',
259				array(ZBX_FLAG_DISCOVERY_NORMAL, ZBX_FLAG_DISCOVERY_CREATED)
260			);
261		}
262
263		// with_httptests, with_monitored_httptests
264		if ($options['with_httptests'] !== null) {
265			$sub_sql_parts['from']['ht'] = 'httptest ht';
266			$sub_sql_parts['where']['hg-ht'] = 'hg.hostid=ht.hostid';
267		}
268		elseif ($options['with_monitored_httptests'] !== null) {
269			$sub_sql_parts['from']['ht'] = 'httptest ht';
270			$sub_sql_parts['where']['hg-ht'] = 'hg.hostid=ht.hostid';
271			$sub_sql_parts['where'][] = dbConditionInt('ht.status', array(HTTPTEST_STATUS_ACTIVE));
272		}
273
274		// with_graphs
275		if ($options['with_graphs'] !== null) {
276			$sub_sql_parts['from']['i'] = 'items i';
277			$sub_sql_parts['from']['gi'] = 'graphs_items gi';
278			$sub_sql_parts['from']['gr'] = 'graphs gr';
279			$sub_sql_parts['where']['hg-i'] = 'hg.hostid=i.hostid';
280			$sub_sql_parts['where']['i-gi'] = 'i.itemid=gi.itemid';
281			$sub_sql_parts['where']['gi-gr'] = 'gi.graphid=gr.graphid';
282			$sub_sql_parts['where'][] = dbConditionInt('gr.flags',
283				array(ZBX_FLAG_DISCOVERY_NORMAL, ZBX_FLAG_DISCOVERY_CREATED)
284			);
285		}
286
287		// with_applications
288		if ($options['with_applications'] !== null) {
289			$sub_sql_parts['from']['a'] = 'applications a';
290			$sub_sql_parts['where']['hg-a'] = 'hg.hostid=a.hostid';
291		}
292
293		if ($sub_sql_parts) {
294			$sub_sql_parts['from']['hg'] = 'hosts_groups hg';
295			$sub_sql_parts['where']['g-hg'] = 'g.groupid=hg.groupid';
296
297			$sqlParts['where'][] = 'EXISTS ('.
298				'SELECT NULL'.
299				' FROM '.implode(',', $sub_sql_parts['from']).
300				' WHERE '.implode(' AND ', array_unique($sub_sql_parts['where'])).
301			')';
302		}
303
304		// filter
305		if (is_array($options['filter'])) {
306			$this->dbFilter('groups g', $options, $sqlParts);
307		}
308
309		// search
310		if (is_array($options['search'])) {
311			zbx_db_search('groups g', $options, $sqlParts);
312		}
313
314		// limit
315		if (zbx_ctype_digit($options['limit']) && $options['limit']) {
316			$sqlParts['limit'] = $options['limit'];
317		}
318
319		$sqlParts = $this->applyQueryOutputOptions($this->tableName(), $this->tableAlias(), $options, $sqlParts);
320		$sqlParts = $this->applyQuerySortOptions($this->tableName(), $this->tableAlias(), $options, $sqlParts);
321		$res = DBselect($this->createSelectQueryFromParts($sqlParts), $sqlParts['limit']);
322		while ($group = DBfetch($res)) {
323			if (!is_null($options['countOutput'])) {
324				if (!is_null($options['groupCount'])) {
325					$result[] = $group;
326				}
327				else {
328					$result = $group['rowscount'];
329				}
330			}
331			else {
332				$result[$group['groupid']] = $group;
333			}
334		}
335
336		if (!is_null($options['countOutput'])) {
337			return $result;
338		}
339
340		if ($result) {
341			$result = $this->addRelatedObjects($options, $result);
342		}
343
344		// removing keys (hash -> array)
345		if (is_null($options['preservekeys'])) {
346			$result = zbx_cleanHashes($result);
347		}
348		return $result;
349	}
350
351	/**
352	 * Create host groups.
353	 *
354	 * @param array $groups array with host group names
355	 * @param array $groups['name']
356	 *
357	 * @return array
358	 */
359	public function create(array $groups) {
360		$groups = zbx_toArray($groups);
361
362		if (USER_TYPE_SUPER_ADMIN != self::$userData['type']) {
363			self::exception(ZBX_API_ERROR_PERMISSIONS, _('Only Super Admins can create host groups.'));
364		}
365
366		foreach ($groups as $group) {
367			if (!isset($group['name']) || zbx_empty($group['name'])) {
368				self::exception(ZBX_API_ERROR_PARAMETERS, _('Host group name cannot be empty.'));
369			}
370			$this->checkNoParameters(
371				$group,
372				['internal'],
373				_('Cannot set "%1$s" for host group "%2$s".'),
374				$group['name']
375			);
376		}
377
378		// check host name duplicates in passed parameters
379		$collectionValidator = new CCollectionValidator([
380			'uniqueField' => 'name',
381			'messageDuplicate' => _('Host group "%1$s" already exists.')
382		]);
383		$this->checkValidator($groups, $collectionValidator);
384
385		// check host name duplicates in DB
386		$dbHostGroups = API::getApiService()->select($this->tableName(), [
387			'output' => ['name'],
388			'filter' => ['name' => zbx_objectValues($groups, 'name')],
389			'limit' => 1
390		]);
391
392		if ($dbHostGroups) {
393			$dbHostGroup = reset($dbHostGroups);
394			self::exception(ZBX_API_ERROR_PARAMETERS, _s('Host group "%1$s" already exists.', $dbHostGroup['name']));
395		}
396
397		$groupids = DB::insert('groups', $groups);
398
399		return ['groupids' => $groupids];
400	}
401
402	/**
403	 * Update host groups.
404	 *
405	 * @param array $groups
406	 * @param array $groups[0]['name'], ...
407	 * @param array $groups[0]['groupid'], ...
408	 *
409	 * @return boolean
410	 */
411	public function update(array $groups) {
412		$groups = zbx_toArray($groups);
413		$groupids = zbx_objectValues($groups, 'groupid');
414
415		if (empty($groups)) {
416			self::exception(ZBX_API_ERROR_PARAMETERS, _('Empty input parameter.'));
417		}
418
419		// permissions
420		$updGroups = $this->get([
421			'output' => ['groupid', 'flags', 'name'],
422			'groupids' => $groupids,
423			'editable' => true,
424			'preservekeys' => true
425		]);
426		foreach ($groups as $group) {
427			if (!isset($updGroups[$group['groupid']])) {
428				self::exception(ZBX_API_ERROR_PERMISSIONS,
429					_('No permissions to referred object or it does not exist!')
430				);
431			}
432			$this->checkNoParameters(
433				$group,
434				['internal'],
435				_('Cannot update "%1$s" for host group "%2$s".'),
436				isset($group['name']) ? $group['name'] : $updGroups[$group['groupid']]['name']
437			);
438		}
439
440		// name duplicate check
441		$groupsNames = $this->get([
442			'filter' => ['name' => zbx_objectValues($groups, 'name')],
443			'output' => ['groupid', 'name'],
444			'editable' => true,
445			'nopermissions' => true
446		]);
447		$groupsNames = zbx_toHash($groupsNames, 'name');
448
449		$updateDiscoveredValidator = new CUpdateDiscoveredValidator([
450			'messageAllowed' => _('Cannot update a discovered host group.')
451		]);
452
453		$update = [];
454		foreach ($groups as $group) {
455			if (isset($group['name'])) {
456				if (zbx_empty($group['name'])) {
457					self::exception(ZBX_API_ERROR_PARAMETERS, _('Host group name cannot be empty.'));
458				}
459
460				// cannot update discovered host groups
461				$this->checkPartialValidator($group, $updateDiscoveredValidator, $updGroups[$group['groupid']]);
462
463				if (isset($groupsNames[$group['name']])
464						&& !idcmp($groupsNames[$group['name']]['groupid'], $group['groupid'])) {
465					self::exception(ZBX_API_ERROR_PARAMETERS, _s('Host group "%1$s" already exists.', $group['name']));
466				}
467
468				$update[] = [
469					'values' => ['name' => $group['name']],
470					'where' => ['groupid' => $group['groupid']]
471				];
472			}
473
474			// prevents updating several groups with same name
475			$groupsNames[$group['name']] = ['groupid' => $group['groupid']];
476		}
477
478		DB::update('groups', $update);
479
480		return ['groupids' => $groupids];
481	}
482
483	/**
484	 * Delete host groups.
485	 *
486	 * @param array $groupids
487	 * @param bool 	$nopermissions
488	 *
489	 * @return array
490	 */
491	public function delete(array $groupids, $nopermissions = false) {
492		if (empty($groupids)) {
493			self::exception(ZBX_API_ERROR_PARAMETERS, _('Empty input parameter.'));
494		}
495		sort($groupids);
496
497		$delGroups = $this->get([
498			'groupids' => $groupids,
499			'editable' => true,
500			'output' => ['groupid', 'name', 'internal'],
501			'selectHosts' => ['hostid', 'host'],
502			'selectTemplates' => ['templateid', 'host'],
503			'preservekeys' => true,
504			'nopermissions' => $nopermissions
505		]);
506		foreach ($groupids as $groupid) {
507			if (!isset($delGroups[$groupid])) {
508				self::exception(ZBX_API_ERROR_PERMISSIONS,
509					_('No permissions to referred object or it does not exist!')
510				);
511			}
512			if ($delGroups[$groupid]['internal'] == ZBX_INTERNAL_GROUP) {
513				self::exception(ZBX_API_ERROR_PARAMETERS,
514					_s('Host group "%1$s" is internal and can not be deleted.', $delGroups[$groupid]['name']));
515			}
516		}
517
518		// check if a group is used in a group prototype
519		$groupPrototype = DBFetch(DBselect(
520			'SELECT groupid'.
521			' FROM group_prototype gp'.
522			' WHERE '.dbConditionInt('groupid', $groupids),
523			1
524		));
525		if ($groupPrototype) {
526			self::exception(ZBX_API_ERROR_PARAMETERS,
527				_s('Group "%1$s" cannot be deleted, because it is used by a host prototype.',
528					$delGroups[$groupPrototype['groupid']]['name']
529				)
530			);
531		}
532
533		$hosts_to_unlink = [];
534		$templates_to_unlink = [];
535
536		foreach ($delGroups as $group) {
537			foreach ($group['hosts'] as $host) {
538				$hosts_to_unlink[] = $host;
539			}
540
541			foreach ($group['templates'] as $template) {
542				$templates_to_unlink[] = $template;
543			}
544		}
545
546		$this->verifyHostsAndTemplatesAreUnlinkable($hosts_to_unlink, $templates_to_unlink, $groupids);
547
548		$dbScripts = API::Script()->get([
549			'groupids' => $groupids,
550			'output' => ['scriptid', 'groupid'],
551			'nopermissions' => true
552		]);
553
554		if (!empty($dbScripts)) {
555			foreach ($dbScripts as $script) {
556				if ($script['groupid'] == 0) {
557					continue;
558				}
559				self::exception(ZBX_API_ERROR_PARAMETERS,
560					_s('Host group "%1$s" cannot be deleted, because it is used in a global script.',
561						$delGroups[$script['groupid']]['name']
562					)
563				);
564			}
565		}
566
567		// delete screens items
568		$resources = [
569			SCREEN_RESOURCE_HOSTGROUP_TRIGGERS,
570			SCREEN_RESOURCE_HOSTS_INFO,
571			SCREEN_RESOURCE_TRIGGERS_INFO,
572			SCREEN_RESOURCE_TRIGGERS_OVERVIEW,
573			SCREEN_RESOURCE_DATA_OVERVIEW
574		];
575		DB::delete('screens_items', [
576			'resourceid' => $groupids,
577			'resourcetype' => $resources
578		]);
579
580		// delete sysmap element
581		if (!empty($groupids)) {
582			DB::delete('sysmaps_elements', ['elementtype' => SYSMAP_ELEMENT_TYPE_HOST_GROUP, 'elementid' => $groupids]);
583		}
584
585		// disable actions
586		// actions from conditions
587		$actionids = [];
588		$dbActions = DBselect(
589			'SELECT DISTINCT c.actionid'.
590			' FROM conditions c'.
591			' WHERE c.conditiontype='.CONDITION_TYPE_HOST_GROUP.
592				' AND '.dbConditionString('c.value', $groupids)
593		);
594		while ($dbAction = DBfetch($dbActions)) {
595			$actionids[$dbAction['actionid']] = $dbAction['actionid'];
596		}
597
598		// actions from operations
599		$dbActions = DBselect(
600			'SELECT o.actionid'.
601			' FROM operations o,opgroup og'.
602			' WHERE o.operationid=og.operationid AND '.dbConditionInt('og.groupid', $groupids).
603			' UNION'.
604			' SELECT o.actionid'.
605			' FROM operations o,opcommand_grp ocg'.
606			' WHERE o.operationid=ocg.operationid AND '.dbConditionInt('ocg.groupid', $groupids)
607		);
608		while ($dbAction = DBfetch($dbActions)) {
609			$actionids[$dbAction['actionid']] = $dbAction['actionid'];
610		}
611
612		if (!empty($actionids)) {
613			$update = [];
614			$update[] = [
615				'values' => ['status' => ACTION_STATUS_DISABLED],
616				'where' => ['actionid' => $actionids]
617			];
618			DB::update('actions', $update);
619		}
620
621		// delete action conditions
622		DB::delete('conditions', [
623			'conditiontype' => CONDITION_TYPE_HOST_GROUP,
624			'value' => $groupids
625		]);
626
627		// delete action operation groups
628		$operationids = [];
629		$dbOperations = DBselect(
630			'SELECT DISTINCT og.operationid'.
631			' FROM opgroup og'.
632			' WHERE '.dbConditionInt('og.groupid', $groupids)
633		);
634		while ($dbOperation = DBfetch($dbOperations)) {
635			$operationids[$dbOperation['operationid']] = $dbOperation['operationid'];
636		}
637		DB::delete('opgroup', [
638			'groupid' => $groupids
639		]);
640
641		// delete action operation commands
642		$dbOperations = DBselect(
643			'SELECT DISTINCT ocg.operationid'.
644			' FROM opcommand_grp ocg'.
645			' WHERE '.dbConditionInt('ocg.groupid', $groupids)
646		);
647		while ($dbOperation = DBfetch($dbOperations)) {
648			$operationids[$dbOperation['operationid']] = $dbOperation['operationid'];
649		}
650		DB::delete('opcommand_grp', [
651			'groupid' => $groupids
652		]);
653
654		// delete empty operations
655		$delOperationids = [];
656		$dbOperations = DBselect(
657			'SELECT DISTINCT o.operationid'.
658			' FROM operations o'.
659			' WHERE '.dbConditionInt('o.operationid', $operationids).
660				' AND NOT EXISTS (SELECT NULL FROM opgroup og WHERE o.operationid=og.operationid)'.
661				' AND NOT EXISTS (SELECT NULL FROM opcommand_grp ocg WHERE o.operationid=ocg.operationid)'
662		);
663		while ($dbOperation = DBfetch($dbOperations)) {
664			$delOperationids[$dbOperation['operationid']] = $dbOperation['operationid'];
665		}
666
667		DB::delete('operations', ['operationid' => $delOperationids]);
668
669		DB::delete('groups', ['groupid' => $groupids]);
670
671		DB::delete('profiles', [
672			'idx' => 'web.dashconf.groups.groupids',
673			'value_id' => $groupids
674		]);
675
676		DB::delete('profiles', [
677			'idx' => 'web.dashconf.groups.hide.groupids',
678			'value_id' => $groupids
679		]);
680
681		// TODO: remove audit
682		foreach ($groupids as $groupid) {
683			add_audit_ext(AUDIT_ACTION_DELETE, AUDIT_RESOURCE_HOST_GROUP, $groupid, $delGroups[$groupid]['name'], 'groups', null, null);
684		}
685
686		return ['groupids' => $groupids];
687	}
688
689	/**
690	 * Add hosts and templates to host groups. All given hosts and templates are added to all given host groups.
691	 *
692	 * @param array $data
693	 * @param array $data['groups']
694	 * @param array $data['hosts']
695	 * @param array $data['templates']
696	 *
697	 * @return array					returns array of group IDs that hosts and templates have been added to
698	 */
699	public function massAdd(array $data) {
700		$data['groups'] = zbx_toArray($data['groups']);
701		$data['hosts'] = isset($data['hosts']) ? zbx_toArray($data['hosts']) : [];
702		$data['templates'] = isset($data['templates']) ? zbx_toArray($data['templates']) : [];
703
704		$this->validateMassAdd($data);
705
706		$groupIds = zbx_objectValues($data['groups'], 'groupid');
707		$hostIds = zbx_objectValues($data['hosts'], 'hostid');
708		$templateIds = zbx_objectValues($data['templates'], 'templateid');
709
710		$objectIds = array_merge($hostIds, $templateIds);
711		$objectIds = array_keys(array_flip($objectIds));
712
713		$linked = [];
714		$linkedDb = DBselect(
715			'SELECT hg.hostid,hg.groupid'.
716			' FROM hosts_groups hg'.
717			' WHERE '.dbConditionInt('hg.hostid', $objectIds).
718				' AND '.dbConditionInt('hg.groupid', $groupIds)
719		);
720		while ($pair = DBfetch($linkedDb)) {
721			$linked[$pair['groupid']][$pair['hostid']] = 1;
722		}
723
724		$insert = [];
725		foreach ($groupIds as $groupId) {
726			foreach ($objectIds as $objectId) {
727				if (isset($linked[$groupId][$objectId])) {
728					continue;
729				}
730				$insert[] = ['hostid' => $objectId, 'groupid' => $groupId];
731			}
732		}
733
734		DB::insert('hosts_groups', $insert);
735
736		return ['groupids' => $groupIds];
737	}
738
739	/**
740	 * Remove hosts and templates from host groups. All given hosts and templates are removed from all given host groups.
741	 *
742	 * @param array $data
743	 * @param array $data['groupids']
744	 * @param array $data['hostids']
745	 * @param array $data['templateids']
746	 *
747	 * @return array				returns array of group IDs that hosts and templates have been removed from
748	 */
749	public function massRemove(array $data) {
750		$data['groupids'] = zbx_toArray($data['groupids'], 'groupid');
751		$data['hostids'] = isset($data['hostids']) ? zbx_toArray($data['hostids']) : [];
752		$data['templateids'] = isset($data['templateids']) ? zbx_toArray($data['templateids']) : [];
753
754		$this->validateMassRemove($data);
755
756		$objectIds = array_merge($data['hostids'], $data['templateids']);
757		$objectIds = array_keys(array_flip($objectIds));
758
759		DB::delete('hosts_groups', [
760			'hostid' => $objectIds,
761			'groupid' => $data['groupids']
762		]);
763
764		return ['groupids' => $data['groupids']];
765	}
766
767	/**
768	 * Update host groups with new hosts and templates.
769	 *
770	 * @param array $data
771	 * @param array $data['groups']
772	 * @param array $data['hosts']
773	 * @param array $data['templates']
774	 *
775	 * @return array				returns array of group IDs that hosts and templates have been added to and
776	 *								removed from
777	 */
778	public function massUpdate(array $data) {
779		if (!array_key_exists('groups', $data) || !is_array($data['groups'])) {
780			self::exception(ZBX_API_ERROR_PARAMETERS, _s('Field "%1$s" is mandatory.', 'groups'));
781		}
782
783		$data['groups'] = zbx_toArray($data['groups']);
784		$data['hosts'] = isset($data['hosts']) ? zbx_toArray($data['hosts']) : [];
785		$data['templates'] = isset($data['templates']) ? zbx_toArray($data['templates']) : [];
786
787		$this->validateMassUpdate($data);
788
789		$groupIds = zbx_objectValues($data['groups'], 'groupid');
790		$hostIds = zbx_objectValues($data['hosts'], 'hostid');
791		$templateIds = zbx_objectValues($data['templates'], 'templateid');
792
793		$objectIds = zbx_toHash(array_merge($hostIds, $templateIds));
794
795		// get old records and skip discovered hosts
796		$oldRecords = DBfetchArray(DBselect(
797			'SELECT hg.hostid,hg.groupid,hg.hostgroupid'.
798			' FROM hosts_groups hg,hosts h'.
799			' WHERE '.dbConditionInt('hg.groupid', $groupIds).
800				' AND hg.hostid=h.hostid'.
801				' AND h.flags='.ZBX_FLAG_DISCOVERY_NORMAL
802		));
803
804		// calculate new records
805		$replaceRecords = [];
806		$newRecords = [];
807
808		foreach ($groupIds as $groupId) {
809			$groupRecords = [];
810			foreach ($oldRecords as $oldRecord) {
811				if ($oldRecord['groupid'] == $groupId) {
812					$groupRecords[] = $oldRecord;
813				}
814			}
815
816			// find records for replace
817			foreach ($groupRecords as $groupRecord) {
818				if (isset($objectIds[$groupRecord['hostid']])) {
819					$replaceRecords[] = $groupRecord;
820				}
821			}
822
823			// find records for create
824			$groupHostIds = zbx_toHash(zbx_objectValues($groupRecords, 'hostid'));
825
826			$newHostIds = array_diff($objectIds, $groupHostIds);
827			foreach ($newHostIds as $newHostId) {
828				$newRecords[] = [
829					'groupid' => $groupId,
830					'hostid' => $newHostId
831				];
832			}
833		}
834
835		DB::replace('hosts_groups', $oldRecords, array_merge($replaceRecords, $newRecords));
836
837		return ['groupids' => $groupIds];
838	}
839
840	/**
841	 * Validate write permissions to host groups that are added to given hosts and templates.
842	 *
843	 * @param array $data
844	 * @param array $data['groups']
845	 * @param array $data['hosts']
846	 * @param array $data['templates']
847	 *
848	 * @throws APIException		if user has no write permissions to any of the given host groups
849	 */
850	protected function validateMassAdd(array $data) {
851		$groupIds = zbx_objectValues($data['groups'], 'groupid');
852		$hostIds = zbx_objectValues($data['hosts'], 'hostid');
853		$templateIds = zbx_objectValues($data['templates'], 'templateid');
854
855		$groupIdsToAdd = [];
856
857		if ($hostIds) {
858			$dbHosts = API::Host()->get([
859				'output' => ['hostid'],
860				'selectGroups' => ['groupid'],
861				'hostids' => $hostIds,
862				'editable' => true,
863				'preservekeys' => true
864			]);
865
866			$this->validateHostsPermissions($hostIds, $dbHosts);
867
868			$this->checkValidator($hostIds, new CHostNormalValidator([
869				'message' => _('Cannot update groups for discovered host "%1$s".')
870			]));
871
872			foreach ($dbHosts as $dbHost) {
873				$oldGroupIds = zbx_objectValues($dbHost['groups'], 'groupid');
874
875				foreach (array_diff($groupIds, $oldGroupIds) as $groupId) {
876					$groupIdsToAdd[$groupId] = $groupId;
877				}
878			}
879		}
880
881		if ($templateIds) {
882			$dbTemplates = API::Template()->get([
883				'output' => ['templateid'],
884				'selectGroups' => ['groupid'],
885				'templateids' => $templateIds,
886				'editable' => true,
887				'preservekeys' => true
888			]);
889
890			$this->validateHostsPermissions($templateIds, $dbTemplates);
891
892			foreach ($dbTemplates as $dbTemplate) {
893				$oldGroupIds = zbx_objectValues($dbTemplate['groups'], 'groupid');
894
895				foreach (array_diff($groupIds, $oldGroupIds) as $groupId) {
896					$groupIdsToAdd[$groupId] = $groupId;
897				}
898			}
899		}
900
901		if ($groupIdsToAdd && !$this->isWritable($groupIdsToAdd)) {
902			self::exception(ZBX_API_ERROR_PERMISSIONS,
903				_('No permissions to referred object or it does not exist!')
904			);
905		}
906	}
907
908	/**
909	 * Validate write permissions to host groups that are added and removed from given hosts and templates. Also check
910	 * if host and template has at least one host group left when removing host groups.
911	 *
912	 * @param array $data
913	 * @param array $data['groups']
914	 * @param array $data['hosts']
915	 * @param array $data['templates']
916	 *
917	 * @throws APIException		if user has no write permissions to any of the given host groups or one of the hosts and
918	 *							templates is left without a host group
919	 */
920	protected function validateMassUpdate(array $data) {
921		$groupIds = zbx_objectValues($data['groups'], 'groupid');
922		$hostIds = zbx_objectValues($data['hosts'], 'hostid');
923		$templateIds = zbx_objectValues($data['templates'], 'templateid');
924
925		$dbGroups = $this->get([
926			'output' => ['groupid'],
927			'groupids' => $groupIds,
928			'selectHosts' => ['hostid', 'host'],
929			'selectTemplates' => ['templateid', 'host']
930		]);
931
932		if (!$dbGroups) {
933			self::exception(ZBX_API_ERROR_PERMISSIONS, _('No permissions to referred object or it does not exist!'));
934		}
935
936		// Collect group IDs that will added to given hosts and templates.
937		$groupIdsToAdd = [];
938
939		// Collect group IDs that will removed from given hosts and templates.
940		$groupIdsToRemove = [];
941
942		/*
943		 * When given hosts or templates belong to other groups and those group IDs are not passed in parameters,
944		 * those groups will be removed from given hosts and templates. Collect those host and template IDs
945		 * from groups that will be removed.
946		 */
947		$objectIds = [];
948
949		/*
950		 * New or existing hosts have been passed in parameters. First check write permissions to hosts
951		 * and if hosts are not discovered. Then check if groups should be added and/or removed from given hosts.
952		 */
953		if ($hostIds) {
954			$dbHosts = API::Host()->get([
955				'output' => ['hostid'],
956				'selectGroups' => ['groupid'],
957				'hostids' => $hostIds,
958				'editable' => true,
959				'preservekeys' => true
960			]);
961
962			$this->validateHostsPermissions($hostIds, $dbHosts);
963
964			$this->checkValidator($hostIds, new CHostNormalValidator([
965				'message' => _('Cannot update groups for discovered host "%1$s".')
966			]));
967
968			foreach ($dbHosts as $dbHost) {
969				$oldGroupIds = zbx_objectValues($dbHost['groups'], 'groupid');
970
971				// Validate groups that are added for current host.
972				foreach (array_diff($groupIds, $oldGroupIds) as $groupId) {
973					$groupIdsToAdd[$groupId] = $groupId;
974				}
975
976				// Validate groups that are removed from current host.
977				foreach (array_diff($oldGroupIds, $groupIds) as $groupId) {
978					$groupIdsToRemove[$groupId] = $groupId;
979				}
980
981				if ($groupIdsToRemove) {
982					$objectIds[] = $dbHost['hostid'];
983				}
984			}
985		}
986
987		/*
988		 * New or existing templates have been passed in parameters. First check write permissions to templates.
989		 * Then check if groups should be added and/or removed from given templates.
990		 */
991		if ($templateIds) {
992			$dbTemplates = API::Template()->get([
993				'output' => ['templateid'],
994				'selectGroups' => ['groupid'],
995				'templateids' => $templateIds,
996				'editable' => true,
997				'preservekeys' => true
998			]);
999
1000			$this->validateHostsPermissions($templateIds, $dbTemplates);
1001
1002			foreach ($dbTemplates as $dbTemplate) {
1003				$oldGroupIds = zbx_objectValues($dbTemplate['groups'], 'groupid');
1004
1005				// Validate groups that are added for current template.
1006				foreach (array_diff($groupIds, $oldGroupIds) as $groupId) {
1007					$groupIdsToAdd[$groupId] = $groupId;
1008				}
1009
1010				// Validate groups that are removed from current template.
1011				foreach (array_diff($oldGroupIds, $groupIds) as $groupId) {
1012					$groupIdsToRemove[$groupId] = $groupId;
1013				}
1014
1015				if ($groupIdsToRemove) {
1016					$objectIds[] = $dbTemplate['templateid'];
1017				}
1018			}
1019		}
1020
1021		// Continue to check new, existing or removable groups for given hosts and templates.
1022		$groupIdsToUpdate = array_merge($groupIdsToAdd, $groupIdsToRemove);
1023
1024		// Validate write permissions only to changed (added/removed) groups for given hosts and templates.
1025		if ($groupIdsToUpdate && !$this->isWritable($groupIdsToUpdate)) {
1026			self::exception(ZBX_API_ERROR_PERMISSIONS,
1027				_('No permissions to referred object or it does not exist!')
1028			);
1029		}
1030
1031		// Check if groups can be removed from given hosts and templates. Only check if no groups are added.
1032		if (!$groupIdsToAdd && $groupIdsToRemove) {
1033			$unlinkableObjectIds = getUnlinkableHostIds($groupIdsToRemove, $objectIds);
1034
1035			if (count($objectIds) != count($unlinkableObjectIds)) {
1036				self::exception(ZBX_API_ERROR_PARAMETERS, _('One of the objects is left without a host group.'));
1037			}
1038		}
1039
1040		$hosts_to_unlink = [];
1041		$templates_to_unlink = [];
1042		$hostIds = array_flip($hostIds);
1043		$templateIds = array_flip($templateIds);
1044
1045		foreach ($dbGroups as $group) {
1046			foreach ($group['hosts'] as $host) {
1047				if (!array_key_exists($host['hostid'], $hostIds)) {
1048					$hosts_to_unlink[] = $host;
1049				}
1050			}
1051
1052			foreach ($group['templates'] as $template) {
1053				if (!array_key_exists($template['templateid'], $templateIds)) {
1054					$templates_to_unlink[] = $template;
1055				}
1056			}
1057		}
1058
1059		$this->verifyHostsAndTemplatesAreUnlinkable($hosts_to_unlink, $templates_to_unlink, $groupIds);
1060	}
1061
1062	/**
1063	 * Validate write permissions to host groups that are removed from given hosts and templates. Also check
1064	 * if host and template has at least one host group left.
1065	 *
1066	 * @param array $data
1067	 * @param array $data['groupids']
1068	 * @param array $data['hostids']
1069	 * @param array $data['templateids']
1070	 *
1071	 * @throws APIException		if user has no write permissions to any of the given host groups or one of the hosts and
1072	 *							templates is left without a host group
1073	 */
1074	protected function validateMassRemove(array $data) {
1075		$groupIdsToRemove = [];
1076		$hostIds = isset($data['hostids']) ? $data['hostids'] : [];
1077		$templateIds = isset($data['templateids']) ? $data['templateids'] : [];
1078		$hosts_to_unlink = [];
1079		$templates_to_unlink = [];
1080
1081		if ($hostIds) {
1082			$dbHosts = API::Host()->get([
1083				'output' => ['hostid', 'host'],
1084				'selectGroups' => ['groupid'],
1085				'hostids' => $hostIds,
1086				'editable' => true,
1087				'preservekeys' => true
1088			]);
1089
1090			$this->validateHostsPermissions($hostIds, $dbHosts);
1091
1092			$this->checkValidator($hostIds, new CHostNormalValidator([
1093				'message' => _('Cannot update groups for discovered host "%1$s".')
1094			]));
1095
1096			foreach ($dbHosts as $dbHost) {
1097				$oldGroupIds = zbx_objectValues($dbHost['groups'], 'groupid');
1098
1099				// check if host belongs to the removable host group
1100				$hostGroupIdsToRemove = array_intersect($data['groupids'], $oldGroupIds);
1101
1102				if ($hostGroupIdsToRemove) {
1103					$hosts_to_unlink[] = $dbHost;
1104
1105					foreach ($hostGroupIdsToRemove as $groupId) {
1106						$groupIdsToRemove[$groupId] = $groupId;
1107					}
1108				}
1109			}
1110		}
1111
1112		if ($templateIds) {
1113			$dbTemplates = API::Template()->get([
1114				'output' => ['templateid', 'host'],
1115				'selectGroups' => ['groupid'],
1116				'templateids' => $templateIds,
1117				'editable' => true,
1118				'preservekeys' => true
1119			]);
1120
1121			$this->validateHostsPermissions($templateIds, $dbTemplates);
1122
1123			foreach ($dbTemplates as $dbTemplate) {
1124				$oldGroupIds = zbx_objectValues($dbTemplate['groups'], 'groupid');
1125
1126				// check if template belongs to the removable host group
1127				$templateGroupIdsToRemove = array_intersect($data['groupids'], $oldGroupIds);
1128
1129				if ($templateGroupIdsToRemove) {
1130					$templates_to_unlink[] = $dbTemplate;
1131
1132					foreach ($templateGroupIdsToRemove as $groupId) {
1133						$groupIdsToRemove[$groupId] = $groupId;
1134					}
1135				}
1136			}
1137		}
1138
1139		if ($groupIdsToRemove && !$this->isWritable($groupIdsToRemove)) {
1140			self::exception(ZBX_API_ERROR_PERMISSIONS,
1141				_('No permissions to referred object or it does not exist!')
1142			);
1143		}
1144
1145		$this->verifyHostsAndTemplatesAreUnlinkable($hosts_to_unlink, $templates_to_unlink, $groupIdsToRemove);
1146	}
1147
1148	/**
1149	 * Validate write permissions to hosts or templates by given host or template IDs.
1150	 *
1151	 * @param array $hostIds		array of host IDs or template IDs
1152	 * @param array $dbHosts		array of allowed hosts or templates
1153	 *
1154	 * @throws APIException			if user has no write permissions to one of the hosts or templates
1155	 */
1156	protected function validateHostsPermissions(array $hostIds, array $dbHosts) {
1157		foreach ($hostIds as $hostId) {
1158			if (!isset($dbHosts[$hostId])) {
1159				self::exception(ZBX_API_ERROR_PERMISSIONS,
1160					_('No permissions to referred object or it does not exist!')
1161				);
1162			}
1163		}
1164	}
1165
1166	/**
1167	 * Check if user has read permissions for host groups.
1168	 *
1169	 * @param array $ids
1170	 *
1171	 * @return bool
1172	 */
1173	public function isReadable(array $ids) {
1174		if (!is_array($ids)) {
1175			return false;
1176		}
1177		if (empty($ids)) {
1178			return true;
1179		}
1180
1181		$ids = array_unique($ids);
1182
1183		$count = $this->get([
1184			'groupids' => $ids,
1185			'countOutput' => true
1186		]);
1187		return count($ids) == $count;
1188	}
1189
1190	/**
1191	 * Check if user has write permissions for host groups.
1192	 *
1193	 * @param array $ids
1194	 *
1195	 * @return bool
1196	 */
1197	public function isWritable(array $ids) {
1198		if (!is_array($ids)) {
1199			return false;
1200		}
1201		if (empty($ids)) {
1202			return true;
1203		}
1204
1205		$ids = array_unique($ids);
1206
1207		$count = $this->get([
1208			'groupids' => $ids,
1209			'editable' => true,
1210			'countOutput' => true
1211		]);
1212
1213		return count($ids) == $count;
1214	}
1215
1216	protected function addRelatedObjects(array $options, array $result) {
1217		$result = parent::addRelatedObjects($options, $result);
1218
1219		$groupIds = array_keys($result);
1220		sort($groupIds);
1221
1222		// adding hosts
1223		if ($options['selectHosts'] !== null) {
1224			if ($options['selectHosts'] !== API_OUTPUT_COUNT) {
1225				$relationMap = $this->createRelationMap($result, 'groupid', 'hostid', 'hosts_groups');
1226				$hosts = API::Host()->get([
1227					'output' => $options['selectHosts'],
1228					'hostids' => $relationMap->getRelatedIds(),
1229					'preservekeys' => true
1230				]);
1231				if (!is_null($options['limitSelects'])) {
1232					order_result($hosts, 'host');
1233				}
1234				$result = $relationMap->mapMany($result, $hosts, 'hosts', $options['limitSelects']);
1235			}
1236			else {
1237				$hosts = API::Host()->get([
1238					'groupids' => $groupIds,
1239					'countOutput' => true,
1240					'groupCount' => true
1241				]);
1242				$hosts = zbx_toHash($hosts, 'groupid');
1243				foreach ($result as $groupid => $group) {
1244					if (isset($hosts[$groupid])) {
1245						$result[$groupid]['hosts'] = $hosts[$groupid]['rowscount'];
1246					}
1247					else {
1248						$result[$groupid]['hosts'] = 0;
1249					}
1250				}
1251			}
1252		}
1253
1254		// adding templates
1255		if ($options['selectTemplates'] !== null) {
1256			if ($options['selectTemplates'] !== API_OUTPUT_COUNT) {
1257				$relationMap = $this->createRelationMap($result, 'groupid', 'hostid', 'hosts_groups');
1258				$hosts = API::Template()->get([
1259					'output' => $options['selectTemplates'],
1260					'templateids' => $relationMap->getRelatedIds(),
1261					'preservekeys' => true
1262				]);
1263				if (!is_null($options['limitSelects'])) {
1264					order_result($hosts, 'host');
1265				}
1266				$result = $relationMap->mapMany($result, $hosts, 'templates', $options['limitSelects']);
1267			}
1268			else {
1269				$hosts = API::Template()->get([
1270					'groupids' => $groupIds,
1271					'countOutput' => true,
1272					'groupCount' => true
1273				]);
1274				$hosts = zbx_toHash($hosts, 'groupid');
1275				foreach ($result as $groupid => $group) {
1276					if (isset($hosts[$groupid])) {
1277						$result[$groupid]['templates'] = $hosts[$groupid]['rowscount'];
1278					}
1279					else {
1280						$result[$groupid]['templates'] = 0;
1281					}
1282				}
1283			}
1284		}
1285
1286		// adding discovery rule
1287		if ($options['selectDiscoveryRule'] !== null && $options['selectDiscoveryRule'] != API_OUTPUT_COUNT) {
1288			// discovered items
1289			$discoveryRules = DBFetchArray(DBselect(
1290				'SELECT gd.groupid,hd.parent_itemid'.
1291					' FROM group_discovery gd,group_prototype gp,host_discovery hd'.
1292					' WHERE '.dbConditionInt('gd.groupid', $groupIds).
1293					' AND gd.parent_group_prototypeid=gp.group_prototypeid'.
1294					' AND gp.hostid=hd.hostid'
1295			));
1296			$relationMap = $this->createRelationMap($discoveryRules, 'groupid', 'parent_itemid');
1297
1298			$discoveryRules = API::DiscoveryRule()->get([
1299				'output' => $options['selectDiscoveryRule'],
1300				'itemids' => $relationMap->getRelatedIds(),
1301				'preservekeys' => true
1302			]);
1303			$result = $relationMap->mapOne($result, $discoveryRules, 'discoveryRule');
1304		}
1305
1306		// adding group discovery
1307		if ($options['selectGroupDiscovery'] !== null) {
1308			$groupDiscoveries = API::getApiService()->select('group_discovery', [
1309				'output' => $this->outputExtend($options['selectGroupDiscovery'], ['groupid']),
1310				'filter' => ['groupid' => $groupIds],
1311				'preservekeys' => true
1312			]);
1313			$relationMap = $this->createRelationMap($groupDiscoveries, 'groupid', 'groupid');
1314
1315			$groupDiscoveries = $this->unsetExtraFields($groupDiscoveries, ['groupid'],
1316				$options['selectGroupDiscovery']
1317			);
1318			$result = $relationMap->mapOne($result, $groupDiscoveries, 'groupDiscovery');
1319		}
1320
1321		return $result;
1322	}
1323
1324	/**
1325	 * Verify that hosts and templates are unlinkable from groups.
1326	 *
1327	 * @param array     $hosts
1328	 * @param integer   $hosts[]['hostid']
1329	 * @param string    $hosts[]['host']
1330	 * @param array     $templates
1331	 * @param integer   $templates[]['templateid']
1332	 * @param string    $templates[]['host']
1333	 * @param array     $groupids
1334	 */
1335	protected function verifyHostsAndTemplatesAreUnlinkable(array $hosts, array $templates, array $groupids) {
1336		$objectids = [];
1337		$host_names = [];
1338		$template_names = [];
1339
1340		foreach ($hosts as $host) {
1341			$objectids[] = $host['hostid'];
1342			$host_names[$host['hostid']] = $host['host'];
1343		}
1344
1345		foreach ($templates as $template) {
1346			$objectids[] = $template['templateid'];
1347			$template_names[$template['templateid']] = $template['host'];
1348		}
1349
1350		if ($objectids && $groupids) {
1351			$not_unlinkable_objectids = array_diff($objectids, getUnlinkableHostIds($groupids, $objectids));
1352
1353			if ($not_unlinkable_objectids) {
1354				$objectid = reset($not_unlinkable_objectids);
1355
1356				if (array_key_exists($objectid, $host_names)) {
1357					self::exception(ZBX_API_ERROR_PARAMETERS,
1358						_s('Host "%1$s" cannot be without host group.', $host_names[$objectid])
1359					);
1360				}
1361
1362				self::exception(ZBX_API_ERROR_PARAMETERS,
1363					_s('Template "%1$s" cannot be without host group.', $template_names[$objectid])
1364				);
1365			}
1366		}
1367	}
1368}
1369