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