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