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 problems.
24 */
25class CProblem extends CApiService {
26
27	protected $tableName = 'problem';
28	protected $tableAlias = 'p';
29	protected $sortColumns = ['eventid'];
30
31	/**
32	 * Get problem data.
33	 *
34	 * @param array $options
35	 *
36	 * @return array|int item data as array or false if error
37	 */
38	public function get($options = []) {
39		$result = [];
40		$userType = self::$userData['type'];
41
42		$sqlParts = [
43			'select'	=> [$this->fieldId('eventid')],
44			'from'		=> ['p' => 'problem p'],
45			'where'		=> [],
46			'order'		=> [],
47			'group'		=> [],
48			'limit'		=> null
49		];
50
51		$defOptions = [
52			'eventids'					=> null,
53			'groupids'					=> null,
54			'hostids'					=> null,
55			'applicationids'			=> null,
56			'objectids'					=> null,
57
58			'editable'					=> false,
59			'source'					=> EVENT_SOURCE_TRIGGERS,
60			'object'					=> EVENT_OBJECT_TRIGGER,
61			'severities'				=> null,
62			'nopermissions'				=> null,
63			// filter
64			'time_from'					=> null,
65			'time_till'					=> null,
66			'eventid_from'				=> null,
67			'eventid_till'				=> null,
68			'acknowledged'				=> null,
69			'suppressed'				=> null,
70			'recent'					=> null,
71			'any'						=> null,	// (internal) true if need not filtred by r_eventid
72			'evaltype'					=> TAG_EVAL_TYPE_AND_OR,
73			'tags'						=> null,
74			'filter'					=> null,
75			'search'					=> null,
76			'searchByAny'				=> null,
77			'startSearch'				=> false,
78			'excludeSearch'				=> false,
79			'searchWildcardsEnabled'	=> null,
80			// output
81			'output'					=> API_OUTPUT_EXTEND,
82			'selectAcknowledges'		=> null,
83			'selectSuppressionData'		=> null,
84			'selectTags'				=> null,
85			'countOutput'				=> false,
86			'preservekeys'				=> false,
87			'sortfield'					=> '',
88			'sortorder'					=> '',
89			'limit'						=> null
90		];
91		$options = zbx_array_merge($defOptions, $options);
92
93		$this->validateGet($options);
94
95		// source and object
96		$sqlParts['where'][] = 'p.source='.zbx_dbstr($options['source']);
97		$sqlParts['where'][] = 'p.object='.zbx_dbstr($options['object']);
98
99		// editable + PERMISSION CHECK
100		if ($userType != USER_TYPE_SUPER_ADMIN && !$options['nopermissions']) {
101			// triggers
102			if ($options['object'] == EVENT_OBJECT_TRIGGER) {
103				$user_groups = getUserGroupsByUserId(self::$userData['userid']);
104
105				// specific triggers
106				if ($options['objectids'] !== null) {
107					$options['objectids'] = array_keys(API::Trigger()->get([
108						'output' => [],
109						'triggerids' => $options['objectids'],
110						'editable' => $options['editable'],
111						'preservekeys' => true
112					]));
113				}
114				// all triggers
115				else {
116					$sqlParts['where'][] = 'NOT EXISTS ('.
117						'SELECT NULL'.
118						' FROM functions f,items i,hosts_groups hgg'.
119							' LEFT JOIN rights r'.
120								' ON r.id=hgg.groupid'.
121									' AND '.dbConditionInt('r.groupid', $user_groups).
122						' WHERE p.objectid=f.triggerid'.
123							' AND f.itemid=i.itemid'.
124							' AND i.hostid=hgg.hostid'.
125						' GROUP BY i.hostid'.
126						' HAVING MAX(permission)<'.($options['editable'] ? PERM_READ_WRITE : PERM_READ).
127							' OR MIN(permission) IS NULL'.
128							' OR MIN(permission)='.PERM_DENY.
129					')';
130				}
131
132				if ($options['source'] == EVENT_SOURCE_TRIGGERS) {
133					$sqlParts = self::addTagFilterSqlParts($user_groups, $sqlParts);
134				}
135			}
136			elseif ($options['object'] == EVENT_OBJECT_ITEM || $options['object'] == EVENT_OBJECT_LLDRULE) {
137				// specific items or lld rules
138				if ($options['objectids'] !== null) {
139					if ($options['object'] == EVENT_OBJECT_ITEM) {
140						$items = API::Item()->get([
141							'output' => [],
142							'itemids' => $options['objectids'],
143							'editable' => $options['editable'],
144							'preservekeys' => true
145						]);
146						$options['objectids'] = array_keys($items);
147					}
148					elseif ($options['object'] == EVENT_OBJECT_LLDRULE) {
149						$items = API::DiscoveryRule()->get([
150							'output' => [],
151							'itemids' => $options['objectids'],
152							'editable' => $options['editable'],
153							'preservekeys' => true
154						]);
155						$options['objectids'] = array_keys($items);
156					}
157				}
158				// all items or lld rules
159				else {
160					$user_groups = getUserGroupsByUserId(self::$userData['userid']);
161
162					$sqlParts['where'][] = 'EXISTS ('.
163						'SELECT NULL'.
164						' FROM items i,hosts_groups hgg'.
165							' JOIN rights r'.
166								' ON r.id=hgg.groupid'.
167									' AND '.dbConditionInt('r.groupid', $user_groups).
168						' WHERE p.objectid=i.itemid'.
169							' AND i.hostid=hgg.hostid'.
170						' GROUP BY hgg.hostid'.
171						' HAVING MIN(r.permission)>'.PERM_DENY.
172							' AND MAX(r.permission)>='.($options['editable'] ? PERM_READ_WRITE : PERM_READ).
173					')';
174				}
175			}
176		}
177
178		// eventids
179		if ($options['eventids'] !== null) {
180			zbx_value2array($options['eventids']);
181			$sqlParts['where'][] = dbConditionInt('p.eventid', $options['eventids']);
182		}
183
184		// objectids
185		if ($options['objectids'] !== null) {
186			zbx_value2array($options['objectids']);
187			$sqlParts['where'][] = dbConditionInt('p.objectid', $options['objectids']);
188		}
189
190		// groupids
191		if ($options['groupids'] !== null) {
192			zbx_value2array($options['groupids']);
193
194			// triggers
195			if ($options['object'] == EVENT_OBJECT_TRIGGER) {
196				$sqlParts['from']['f'] = 'functions f';
197				$sqlParts['from']['i'] = 'items i';
198				$sqlParts['from']['hg'] = 'hosts_groups hg';
199				$sqlParts['where']['p-f'] = 'p.objectid=f.triggerid';
200				$sqlParts['where']['f-i'] = 'f.itemid=i.itemid';
201				$sqlParts['where']['i-hg'] = 'i.hostid=hg.hostid';
202				$sqlParts['where']['hg'] = dbConditionInt('hg.groupid', $options['groupids']);
203			}
204			// lld rules and items
205			elseif ($options['object'] == EVENT_OBJECT_LLDRULE || $options['object'] == EVENT_OBJECT_ITEM) {
206				$sqlParts['from']['i'] = 'items i';
207				$sqlParts['from']['hg'] = 'hosts_groups hg';
208				$sqlParts['where']['p-i'] = 'p.objectid=i.itemid';
209				$sqlParts['where']['i-hg'] = 'i.hostid=hg.hostid';
210				$sqlParts['where']['hg'] = dbConditionInt('hg.groupid', $options['groupids']);
211			}
212		}
213
214		// hostids
215		if ($options['hostids'] !== null) {
216			zbx_value2array($options['hostids']);
217
218			// triggers
219			if ($options['object'] == EVENT_OBJECT_TRIGGER) {
220				$sqlParts['from']['f'] = 'functions f';
221				$sqlParts['from']['i'] = 'items i';
222				$sqlParts['where']['p-f'] = 'p.objectid=f.triggerid';
223				$sqlParts['where']['f-i'] = 'f.itemid=i.itemid';
224				$sqlParts['where']['i'] = dbConditionInt('i.hostid', $options['hostids']);
225			}
226			// lld rules and items
227			elseif ($options['object'] == EVENT_OBJECT_LLDRULE || $options['object'] == EVENT_OBJECT_ITEM) {
228				$sqlParts['from']['i'] = 'items i';
229				$sqlParts['where']['p-i'] = 'p.objectid=i.itemid';
230				$sqlParts['where']['i'] = dbConditionInt('i.hostid', $options['hostids']);
231			}
232		}
233
234		// applicationids
235		if ($options['applicationids'] !== null) {
236			zbx_value2array($options['applicationids']);
237
238			// triggers
239			if ($options['object'] == EVENT_OBJECT_TRIGGER) {
240				$sqlParts['from']['f'] = 'functions f';
241				$sqlParts['from']['ia'] = 'items_applications ia';
242				$sqlParts['where']['p-f'] = 'p.objectid=f.triggerid';
243				$sqlParts['where']['f-ia'] = 'f.itemid=ia.itemid';
244				$sqlParts['where']['ia'] = dbConditionInt('ia.applicationid', $options['applicationids']);
245			}
246			// items
247			elseif ($options['object'] == EVENT_OBJECT_ITEM) {
248				$sqlParts['from']['ia'] = 'items_applications ia';
249				$sqlParts['where']['p-ia'] = 'p.objectid=ia.itemid';
250				$sqlParts['where']['ia'] = dbConditionInt('ia.applicationid', $options['applicationids']);
251			}
252			// ignore this filter for lld rules
253		}
254
255		// severities
256		if ($options['severities'] !== null) {
257			// triggers
258			if ($options['object'] == EVENT_OBJECT_TRIGGER) {
259				zbx_value2array($options['severities']);
260				$sqlParts['where'][] = dbConditionInt('p.severity', $options['severities']);
261			}
262			// ignore this filter for items and lld rules
263		}
264
265		// acknowledged
266		if ($options['acknowledged'] !== null) {
267			$acknowledged = $options['acknowledged'] ? EVENT_ACKNOWLEDGED : EVENT_NOT_ACKNOWLEDGED;
268			$sqlParts['where'][] = 'p.acknowledged='.$acknowledged;
269		}
270
271		// suppressed
272		if ($options['suppressed'] !== null) {
273			$sqlParts['where'][] = (!$options['suppressed'] ? 'NOT ' : '').
274					'EXISTS ('.
275						'SELECT NULL'.
276						' FROM event_suppress es'.
277						' WHERE es.eventid=p.eventid'.
278					')';
279		}
280
281		// tags
282		if ($options['tags'] !== null && $options['tags']) {
283			$sqlParts['where'][] = CApiTagHelper::addWhereCondition($options['tags'], $options['evaltype'], 'p',
284				'problem_tag', 'eventid'
285			);
286		}
287
288		// recent
289		if ($options['recent'] !== null && $options['recent']) {
290			$config = select_config();
291			$ok_events_from = time() - timeUnitToSeconds($config['ok_period']);
292
293			$sqlParts['where'][] = '(p.r_eventid IS NULL OR p.r_clock>'.$ok_events_from.')';
294		}
295		else {
296			$sqlParts['where'][] = 'p.r_eventid IS NULL';
297		}
298
299		// time_from
300		if ($options['time_from'] !== null) {
301			$sqlParts['where'][] = 'p.clock>='.zbx_dbstr($options['time_from']);
302		}
303
304		// time_till
305		if ($options['time_till'] !== null) {
306			$sqlParts['where'][] = 'p.clock<='.zbx_dbstr($options['time_till']);
307		}
308
309		// eventid_from
310		if ($options['eventid_from'] !== null) {
311			$sqlParts['where'][] = 'p.eventid>='.zbx_dbstr($options['eventid_from']);
312		}
313
314		// eventid_till
315		if ($options['eventid_till'] !== null) {
316			$sqlParts['where'][] = 'p.eventid<='.zbx_dbstr($options['eventid_till']);
317		}
318
319		// search
320		if (is_array($options['search'])) {
321			zbx_db_search('problem p', $options, $sqlParts);
322		}
323
324		// filter
325		if (is_array($options['filter'])) {
326			$this->dbFilter('problem p', $options, $sqlParts);
327		}
328
329		// limit
330		if (zbx_ctype_digit($options['limit']) && $options['limit']) {
331			$sqlParts['limit'] = $options['limit'];
332		}
333
334		$sqlParts = $this->applyQueryOutputOptions($this->tableName(), $this->tableAlias(), $options, $sqlParts);
335		$sqlParts = $this->applyQuerySortOptions($this->tableName(), $this->tableAlias(), $options, $sqlParts);
336		$res = DBselect(self::createSelectQueryFromParts($sqlParts), $sqlParts['limit']);
337		while ($event = DBfetch($res)) {
338			if ($options['countOutput']) {
339				$result = $event['rowscount'];
340			}
341			else {
342				$result[$event['eventid']] = $event;
343			}
344		}
345
346		if ($options['countOutput']) {
347			return $result;
348		}
349
350		if ($result) {
351			$result = $this->addRelatedObjects($options, $result);
352			$result = $this->unsetExtraFields($result, ['object', 'objectid'], $options['output']);
353		}
354
355		// removing keys (hash -> array)
356		if (!$options['preservekeys']) {
357			$result = zbx_cleanHashes($result);
358		}
359
360		return $result;
361	}
362
363	/**
364	 * Validates the input parameters for the get() method.
365	 *
366	 * @throws APIException  if the input is invalid
367	 *
368	 * @param array $options
369	 */
370	protected function validateGet(array $options) {
371		$sourceValidator = new CLimitedSetValidator([
372			'values' => [EVENT_SOURCE_TRIGGERS, EVENT_SOURCE_INTERNAL]
373		]);
374		if (!$sourceValidator->validate($options['source'])) {
375			self::exception(ZBX_API_ERROR_PARAMETERS, _('Incorrect source value.'));
376		}
377
378		$objectValidator = new CLimitedSetValidator([
379			'values' => [EVENT_OBJECT_TRIGGER, EVENT_OBJECT_ITEM, EVENT_OBJECT_LLDRULE]
380		]);
381		if (!$objectValidator->validate($options['object'])) {
382			self::exception(ZBX_API_ERROR_PARAMETERS, _('Incorrect object value.'));
383		}
384
385		$sourceObjectValidator = new CEventSourceObjectValidator();
386		if (!$sourceObjectValidator->validate(['source' => $options['source'], 'object' => $options['object']])) {
387			self::exception(ZBX_API_ERROR_PARAMETERS, $sourceObjectValidator->getError());
388		}
389
390		$evaltype_validator = new CLimitedSetValidator([
391			'values' => [TAG_EVAL_TYPE_AND_OR, TAG_EVAL_TYPE_OR]
392		]);
393		if (!$evaltype_validator->validate($options['evaltype'])) {
394			self::exception(ZBX_API_ERROR_PARAMETERS, _('Incorrect evaltype value.'));
395		}
396	}
397
398	protected function addRelatedObjects(array $options, array $result) {
399		$result = parent::addRelatedObjects($options, $result);
400
401		$eventids = array_keys($result);
402
403		// Adding operational data.
404		if ($this->outputIsRequested('opdata', $options['output'])) {
405			$problems = DBFetchArrayAssoc(DBselect(
406				'SELECT p.eventid,p.clock,p.ns,t.triggerid,t.expression,t.opdata'.
407				' FROM problem p'.
408				' JOIN triggers t ON t.triggerid=p.objectid'.
409				' WHERE '.dbConditionInt('p.eventid', $eventids)
410			), 'eventid');
411
412			foreach ($result as $eventid => $problem) {
413				$result[$eventid]['opdata'] =
414					(array_key_exists($eventid, $problems) && $problems[$eventid]['opdata'] !== '')
415						? CMacrosResolverHelper::resolveTriggerOpdata($problems[$eventid], ['events' => true])
416						: '';
417			}
418		}
419
420		// adding acknowledges
421		if ($options['selectAcknowledges'] !== null) {
422			if ($options['selectAcknowledges'] != API_OUTPUT_COUNT) {
423				// create the base query
424				$acknowledges = API::getApiService()->select('acknowledges', [
425					'output' => $this->outputExtend($options['selectAcknowledges'],
426						['acknowledgeid', 'eventid']
427					),
428					'filter' => ['eventid' => $eventids],
429					'preservekeys' => true
430				]);
431
432				$relationMap = $this->createRelationMap($acknowledges, 'eventid', 'acknowledgeid');
433				$acknowledges = $this->unsetExtraFields($acknowledges, ['eventid', 'acknowledgeid'],
434					$options['selectAcknowledges']
435				);
436				$result = $relationMap->mapMany($result, $acknowledges, 'acknowledges');
437			}
438			else {
439				$acknowledges = DBFetchArrayAssoc(DBselect(
440					'SELECT a.eventid,COUNT(a.acknowledgeid) AS rowscount'.
441						' FROM acknowledges a'.
442						' WHERE '.dbConditionInt('a.eventid', $eventids).
443						' GROUP BY a.eventid'
444				), 'eventid');
445
446				foreach ($result as $eventid => $event) {
447					$result[$eventid]['acknowledges'] = array_key_exists($eventid, $acknowledges)
448						? $acknowledges[$eventid]['rowscount']
449						: '0';
450				}
451			}
452		}
453
454		// Adding suppression data.
455		if ($options['selectSuppressionData'] !== null && $options['selectSuppressionData'] != API_OUTPUT_COUNT) {
456			$suppression_data = API::getApiService()->select('event_suppress', [
457				'output' => $this->outputExtend($options['selectSuppressionData'], ['eventid', 'maintenanceid']),
458				'filter' => ['eventid' => $eventids],
459				'preservekeys' => true
460			]);
461			$relation_map = $this->createRelationMap($suppression_data, 'eventid', 'event_suppressid');
462			$suppression_data = $this->unsetExtraFields($suppression_data, ['event_suppressid', 'eventid'], []);
463			$result = $relation_map->mapMany($result, $suppression_data, 'suppression_data');
464		}
465
466		// Adding suppressed value.
467		if ($this->outputIsRequested('suppressed', $options['output'])) {
468			$suppressed_eventids = [];
469			foreach ($result as &$problem) {
470				if (array_key_exists('suppression_data', $problem)) {
471					$problem['suppressed'] = $problem['suppression_data']
472						? (string) ZBX_PROBLEM_SUPPRESSED_TRUE
473						: (string) ZBX_PROBLEM_SUPPRESSED_FALSE;
474				}
475				else {
476					$suppressed_eventids[] = $problem['eventid'];
477				}
478			}
479			unset($problem);
480
481			if ($suppressed_eventids) {
482				$suppressed_events = API::getApiService()->select('event_suppress', [
483					'output' => ['eventid'],
484					'filter' => ['eventid' => $suppressed_eventids]
485				]);
486				$suppressed_eventids = array_flip(zbx_objectValues($suppressed_events, 'eventid'));
487				foreach ($result as &$problem) {
488					$problem['suppressed'] = array_key_exists($problem['eventid'], $suppressed_eventids)
489						? (string) ZBX_PROBLEM_SUPPRESSED_TRUE
490						: (string) ZBX_PROBLEM_SUPPRESSED_FALSE;
491				}
492				unset($problem);
493			}
494		}
495
496		// Remove "maintenanceid" field if it's not requested.
497		if ($options['selectSuppressionData'] !== null && $options['selectSuppressionData'] != API_OUTPUT_COUNT
498				&& !$this->outputIsRequested('maintenanceid', $options['selectSuppressionData'])) {
499			foreach ($result as &$row) {
500				$row['suppression_data'] = $this->unsetExtraFields($row['suppression_data'], ['maintenanceid'], []);
501			}
502			unset($row);
503		}
504
505		// Resolve webhook urls.
506		if ($this->outputIsRequested('urls', $options['output'])) {
507			$tags_options = [
508				'output' => ['eventid', 'tag', 'value'],
509				'filter' => ['eventid' => $eventids]
510			];
511			$tags = DBselect(DB::makeSql('problem_tag', $tags_options));
512
513			$events = [];
514
515			foreach ($result as $event) {
516				$events[$event['eventid']]['tags'] = [];
517			}
518
519			while ($tag = DBfetch($tags)) {
520				$events[$tag['eventid']]['tags'][] = [
521					'tag' => $tag['tag'],
522					'value' => $tag['value']
523				];
524			}
525
526			$urls = DB::select('media_type', [
527				'output' => ['event_menu_url', 'event_menu_name'],
528				'filter' => [
529					'type' => MEDIA_TYPE_WEBHOOK,
530					'status' => MEDIA_TYPE_STATUS_ACTIVE,
531					'show_event_menu' => ZBX_EVENT_MENU_SHOW
532				]
533			]);
534
535			$events = CMacrosResolverHelper::resolveMediaTypeUrls($events, $urls);
536
537			foreach ($events as $eventid => $event) {
538				$result[$eventid]['urls'] = $event['urls'];
539			}
540		}
541
542		// Adding event tags.
543		if ($options['selectTags'] !== null && $options['selectTags'] != API_OUTPUT_COUNT) {
544			if ($options['selectTags'] === API_OUTPUT_EXTEND) {
545				$options['selectTags'] = ['tag', 'value'];
546			}
547
548			$tags_options = [
549				'output' => $this->outputExtend($options['selectTags'], ['eventid']),
550				'filter' => ['eventid' => $eventids]
551			];
552			$tags = DBselect(DB::makeSql('problem_tag', $tags_options));
553
554			foreach ($result as &$event) {
555				$event['tags'] = [];
556			}
557			unset($event);
558
559			while ($tag = DBfetch($tags)) {
560				$event = &$result[$tag['eventid']];
561
562				unset($tag['problemtagid'], $tag['eventid']);
563				$event['tags'][] = $tag;
564			}
565			unset($event);
566		}
567
568		return $result;
569	}
570
571	/**
572	 * Add sql parts related to tag-based permissions.
573	 *
574	 * @param array $usrgrpids
575	 * @param array $sqlParts
576	 *
577	 * @return array
578	 */
579	protected static function addTagFilterSqlParts(array $usrgrpids, array $sqlParts) {
580		$tag_filters = CEvent::getTagFilters($usrgrpids);
581
582		if (!$tag_filters) {
583			return $sqlParts;
584		}
585
586		$sqlParts['from']['f'] = 'functions f';
587		$sqlParts['from']['i'] = 'items i';
588		$sqlParts['from']['hg'] = 'hosts_groups hg';
589		$sqlParts['where']['p-f'] = 'p.objectid=f.triggerid';
590		$sqlParts['where']['f-i'] = 'f.itemid=i.itemid';
591		$sqlParts['where']['i-hg'] = 'i.hostid=hg.hostid';
592
593		$tag_conditions = [];
594		$full_access_groupids = [];
595
596		foreach ($tag_filters as $groupid => $filters) {
597			$tags = [];
598			$tag_values = [];
599
600			foreach ($filters as $filter) {
601				if ($filter['tag'] === '') {
602					$full_access_groupids[] = $groupid;
603					continue 2;
604				}
605				elseif ($filter['value'] === '') {
606					$tags[] = $filter['tag'];
607				}
608				else {
609					$tag_values[$filter['tag']][] = $filter['value'];
610				}
611			}
612
613			$conditions = [];
614
615			if ($tags) {
616				$conditions[] = dbConditionString('pt.tag', $tags);
617			}
618			$parenthesis = $tags || count($tag_values) > 1;
619
620			foreach ($tag_values as $tag => $values) {
621				$condition = 'pt.tag='.zbx_dbstr($tag).' AND '.dbConditionString('pt.value', $values);
622				$conditions[] = $parenthesis ? '('.$condition.')' : $condition;
623			}
624
625			$conditions = (count($conditions) > 1) ? '('.implode(' OR ', $conditions).')' : $conditions[0];
626
627			$tag_conditions[] = 'hg.groupid='.zbx_dbstr($groupid).' AND '.$conditions;
628		}
629
630		if ($tag_conditions) {
631			$sqlParts['from']['pt'] = 'problem_tag pt';
632			$sqlParts['where']['p-pt'] = 'p.eventid=pt.eventid';
633
634			if ($full_access_groupids || count($tag_conditions) > 1) {
635				foreach ($tag_conditions as &$tag_condition) {
636					$tag_condition = '('.$tag_condition.')';
637				}
638				unset($tag_condition);
639			}
640		}
641
642		if ($full_access_groupids) {
643			$tag_conditions[] = dbConditionInt('hg.groupid', $full_access_groupids);
644		}
645
646		$sqlParts['where'][] = (count($tag_conditions) > 1)
647			? '('.implode(' OR ', $tag_conditions).')'
648			: $tag_conditions[0];
649
650		return $sqlParts;
651	}
652}
653