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 * Controller class to maintain server-side notification generation tasks.
24 */
25class CControllerNotificationsGet extends CController {
26
27	protected function init() {
28		parent::init();
29
30		$this->notifications = [];
31		$this->settings = getMessageSettings();
32		$ok_timeout = (int) timeUnitToSeconds(CSettingsHelper::get(CSettingsHelper::OK_PERIOD));
33		$timeout = (int) timeUnitToSeconds($this->settings['timeout']);
34		$this->settings['timeout'] = $timeout;
35		$this->settings['ok_timeout'] = min([$timeout, $ok_timeout]);
36		$this->settings['show_recovered'] = (bool) $this->settings['triggers.recovery'];
37		$this->settings['show_suppressed'] = (bool) $this->settings['show_suppressed'];
38		if (!$this->settings['triggers.severities']) {
39			$this->settings['enabled'] = true;
40		}
41
42		$this->timeout_time = time() - $this->settings['timeout'];
43		$this->time_from = max([$this->settings['last.clock'], $this->timeout_time]);
44	}
45
46	protected function checkInput() {
47		$fields = [
48			'known_eventids' => 'array_db events.eventid'
49		];
50
51		$ret = $this->validateInput($fields);
52
53		if (!$ret) {
54			return $this->setResponse(
55				new CControllerResponseData([
56					'main_block' => json_encode(['error' => true])
57				])
58			);
59		}
60
61		return $ret;
62	}
63
64	protected function checkPermissions() {
65		return (!CWebUser::isGuest() && $this->getUserType() >= USER_TYPE_ZABBIX_USER);
66	}
67
68	protected function doAction() {
69		if (!$this->settings['enabled']) {
70			return $this->setResponse(new CControllerResponseData(['main_block' => $this->makeResponseData()]));
71		}
72
73		// Server returns only basic details for events already known by client-side.
74		$this->known_eventids = array_flip($this->getInput('known_eventids', []));
75		$this->loadNotifications();
76
77		$this->setResponse(new CControllerResponseData(['main_block' => $this->makeResponseData()]));
78	}
79
80	protected function loadNotifications() {
81		// Select problem events.
82		$options = [
83			'output' => ['eventid', 'r_eventid', 'objectid', 'severity', 'clock', 'r_clock', 'name'],
84			'source' => EVENT_SOURCE_TRIGGERS,
85			'object' => EVENT_OBJECT_TRIGGER,
86			'severities' => array_keys($this->settings['triggers.severities']),
87			'suppressed' => $this->settings['show_suppressed'] ? null : false,
88			'sortorder' => ZBX_SORT_DOWN,
89			'sortfield' => 'eventid',
90			'limit' => 15,
91			'preservekeys' => true
92		];
93
94		$options += $this->settings['show_recovered']
95			? ['recent' => true]
96			: ['time_from' => $this->time_from];
97
98		$events = API::Problem()->get($options);
99
100		// Select latest status for already known events that are no longer available in problems table.
101		$other_still_shown_eventids = $this->settings['show_recovered']
102			? array_diff(array_keys($this->known_eventids), array_keys($events))
103			: [];
104
105		if ($other_still_shown_eventids) {
106			$resolved_events = API::Event()->get([
107				'output' => ['eventid', 'r_eventid', 'clock', 'severity'],
108				'eventids' => $other_still_shown_eventids,
109				'sortfield' => 'clock',
110				'sortorder' => ZBX_SORT_DOWN,
111				'preservekeys' => true
112			]);
113
114			$r_eventids = [];
115
116			foreach ($resolved_events as $eventid => $resolved_event) {
117				if ($resolved_event['r_eventid'] != 0) {
118					$r_eventids[$eventid] = $resolved_event['r_eventid'];
119				}
120			}
121
122			if ($r_eventids) {
123				$r_clocks = API::Event()->get([
124					'output' => ['clock'],
125					'eventids' => array_values($r_eventids),
126					'sortfield' => 'clock',
127					'sortorder' => ZBX_SORT_DOWN,
128					'preservekeys' => true
129				]);
130
131				foreach ($r_eventids as $eventid => &$r_eventid) {
132					$resolved_events[$eventid]['r_clock'] = $r_clocks[$r_eventid]['clock'];
133				}
134				unset($r_eventid);
135			}
136
137			$events += $resolved_events;
138		}
139
140		// Append selected events to notifications array.
141		$problems_by_triggerid = [];
142
143		foreach ($events as $eventid => $event) {
144			if ($this->settings['show_recovered']) {
145				if (array_key_exists('r_clock', $event) && $event['r_clock'] >= $this->time_from) {
146					/*
147					 * This happens if trigger is recovered and is already removed from the list of known eventids.
148					 * Do nothing here. This statement is needed just to catch specific case before next IF statement.
149					 */
150				}
151				elseif (array_key_exists($event['eventid'], $this->known_eventids)
152						&& $event['clock'] < $this->timeout_time) {
153					/*
154					 * This exception is needed to add notifications that are delayed in front-end, for example, in case
155					 * if user has logged in between 30th and 60th second after event was generated. Since notification
156					 * is still in response, front-end will remove that message using client side timeout.
157					 */
158				}
159				// Filter by problem start time, because that is not done by API if show_recovered is enabled.
160				elseif ($event['clock'] < $this->time_from && !in_array($eventid, $other_still_shown_eventids)) {
161					continue;
162				}
163			}
164
165			// Trigger API is used to select hostname only for notifications that client cannot recover from cache.
166			if (!array_key_exists($event['eventid'], $this->known_eventids)) {
167				$problems_by_triggerid[$event['objectid']][] = $eventid;
168			}
169
170			$this->notifications[$eventid] = [
171				'eventid' => $event['eventid'],
172				'resolved' => (int) ($event['r_eventid'] != 0),
173				'severity' => (int) $event['severity'],
174				'clock' => ((int) $event['r_eventid'] == 0) ? $event['clock'] : $event['r_clock'],
175				'name' => array_key_exists('name', $event) ? $event['name'] : ''
176			];
177		}
178
179		// Add additional details newly discovered events.
180		if ($problems_by_triggerid) {
181			$triggers = API::Trigger()->get([
182				'output' => [],
183				'selectHosts' => ['hostid', 'name'],
184				'triggerids' => array_keys($problems_by_triggerid),
185				'lastChangeSince' => $this->time_from,
186				'preservekeys' => true
187			]);
188
189			foreach ($problems_by_triggerid as $triggerid => $notification_eventids) {
190				$trigger = $triggers[$triggerid];
191
192				$url_problems = (new CUrl('zabbix.php'))
193					->setArgument('action', 'problem.view')
194					->setArgument('filter_name', '')
195					->setArgument('hostids[]', $trigger['hosts'][0]['hostid'])
196					->getUrl();
197
198				$url_events = (new CUrl('zabbix.php'))
199					->setArgument('action', 'problem.view')
200					->setArgument('filter_name', '')
201					->setArgument('triggerids[]', $triggerid)
202					->getUrl();
203
204				$url_trigger_events_pt = (new CUrl('tr_events.php'))->setArgument('triggerid', $triggerid);
205
206				foreach ($notification_eventids as $eventid) {
207					$notification = &$this->notifications[$eventid];
208
209					$url_trigger_events = $url_trigger_events_pt
210						->setArgument('eventid', $notification['eventid'])
211						->getUrl();
212
213					$notification += [
214						'title' => sprintf('[url=%s]%s[/url]', $url_problems,
215							CHtml::encode($trigger['hosts'][0]['name'])
216						),
217						'body' => [
218							'[url='.$url_events.']'.CHtml::encode($notification['name']).'[/url]',
219							'[url='.$url_trigger_events.']'.
220								zbx_date2str(DATE_TIME_FORMAT_SECONDS, $notification['clock']).
221							'[/url]'
222						]
223					];
224				}
225			}
226		}
227
228		$this->notifications = array_values($this->notifications);
229	}
230
231	protected function makeResponseData() {
232		CArrayHelper::sort($this->notifications, [
233			['field' => 'clock', 'order' => ZBX_SORT_DOWN],
234			['field' => 'severity', 'order' => ZBX_SORT_DOWN],
235			['field' => 'eventid', 'order' => ZBX_SORT_DOWN]
236		]);
237
238		$this->notifications = array_values($this->notifications);
239
240		foreach ($this->notifications as &$notification) {
241			unset($notification['clock']);
242			unset($notification['name']);
243			if (!array_key_exists('title', $notification)) {
244				unset($notification['severity']);
245			}
246		}
247		unset($notification);
248
249		return json_encode([
250			'notifications' => $this->notifications,
251			'settings' => [
252				'enabled' => (bool) $this->settings['enabled'],
253				'alarm_timeout' => (int) $this->settings['sounds.repeat'],
254				'msg_recovery_timeout' => $this->settings['ok_timeout'],
255				'msg_timeout' => $this->settings['timeout'],
256				'muted' => (bool) $this->settings['sounds.mute'],
257				'severity_styles' => [
258					-1 => ZBX_STYLE_NORMAL_BG,
259					TRIGGER_SEVERITY_AVERAGE => ZBX_STYLE_AVERAGE_BG,
260					TRIGGER_SEVERITY_DISASTER => ZBX_STYLE_DISASTER_BG,
261					TRIGGER_SEVERITY_HIGH  => ZBX_STYLE_HIGH_BG,
262					TRIGGER_SEVERITY_INFORMATION => ZBX_STYLE_INFO_BG,
263					TRIGGER_SEVERITY_NOT_CLASSIFIED => ZBX_STYLE_NA_BG,
264					TRIGGER_SEVERITY_WARNING => ZBX_STYLE_WARNING_BG
265				],
266				'files' => [
267					-1 => $this->settings['sounds.recovery'],
268					TRIGGER_SEVERITY_AVERAGE => $this->settings['sounds.'.TRIGGER_SEVERITY_AVERAGE],
269					TRIGGER_SEVERITY_DISASTER => $this->settings['sounds.'.TRIGGER_SEVERITY_DISASTER],
270					TRIGGER_SEVERITY_HIGH => $this->settings['sounds.'.TRIGGER_SEVERITY_HIGH],
271					TRIGGER_SEVERITY_INFORMATION => $this->settings['sounds.'.TRIGGER_SEVERITY_INFORMATION],
272					TRIGGER_SEVERITY_NOT_CLASSIFIED => $this->settings['sounds.'.TRIGGER_SEVERITY_NOT_CLASSIFIED],
273					TRIGGER_SEVERITY_WARNING => $this->settings['sounds.'.TRIGGER_SEVERITY_WARNING]
274				]
275			]
276		]);
277	}
278}
279