1<?php declare(strict_types = 1);
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 scheduled reports.
24 */
25class CReport extends CApiService {
26
27	public const ACCESS_RULES = [
28		'get' => [
29			'min_user_type' => USER_TYPE_ZABBIX_ADMIN
30		],
31		'create' => [
32			'min_user_type' => USER_TYPE_ZABBIX_ADMIN,
33			'action' => CRoleHelper::ACTIONS_MANAGE_SCHEDULED_REPORTS
34		],
35		'update' => [
36			'min_user_type' => USER_TYPE_ZABBIX_ADMIN,
37			'action' => CRoleHelper::ACTIONS_MANAGE_SCHEDULED_REPORTS
38		],
39		'delete' => [
40			'min_user_type' => USER_TYPE_ZABBIX_ADMIN,
41			'action' => CRoleHelper::ACTIONS_MANAGE_SCHEDULED_REPORTS
42		]
43	];
44
45	protected $tableName = 'report';
46	protected $tableAlias = 'r';
47	protected $sortColumns = ['reportid', 'name', 'status'];
48
49	protected $output_fields = ['reportid', 'userid', 'name', 'description', 'status', 'dashboardid', 'period', 'cycle',
50		'weekdays', 'start_time', 'active_since', 'active_till', 'state', 'lastsent', 'info', 'subject', 'message'
51	];
52	protected $user_output_fields = ['userid', 'access_userid', 'exclude'];
53	protected $usrgrp_output_fields = ['usrgrpid', 'access_userid'];
54
55	/**
56	 * @param array $options
57	 *
58	 * @throws APIException if the input is invalid.
59	 *
60	 * @return array|string
61	 */
62	public function get(array $options = []) {
63		$api_input_rules = ['type' => API_OBJECT, 'fields' => [
64			// filter
65			'reportids' =>				['type' => API_IDS, 'flags' => API_ALLOW_NULL | API_NORMALIZE, 'default' => null],
66			'expired' =>				['type' => API_BOOLEAN, 'flags' => API_ALLOW_NULL, 'default' => null],
67			'filter' =>					['type' => API_OBJECT, 'flags' => API_ALLOW_NULL, 'default' => null, 'fields' => [
68				'reportid' =>				['type' => API_IDS, 'flags' => API_ALLOW_NULL | API_NORMALIZE],
69				'userid' =>					['type' => API_IDS, 'flags' => API_ALLOW_NULL | API_NORMALIZE],
70				'name' =>					['type' => API_STRINGS_UTF8, 'flags' => API_ALLOW_NULL | API_NORMALIZE],
71				'dashboardid' =>			['type' => API_IDS, 'flags' => API_ALLOW_NULL | API_NORMALIZE],
72				'status' =>					['type' => API_INTS32, 'flags' => API_ALLOW_NULL | API_NORMALIZE, 'in' => ZBX_REPORT_STATUS_DISABLED.','.ZBX_REPORT_STATUS_ENABLED],
73				'state' =>					['type' => API_INTS32, 'flags' => API_ALLOW_NULL | API_NORMALIZE, 'in' => implode(',', [ZBX_REPORT_STATE_UNKNOWN, ZBX_REPORT_STATE_SENT, ZBX_REPORT_STATE_ERROR, ZBX_REPORT_STATE_SUCCESS_INFO])]
74			]],
75			'search' =>					['type' => API_OBJECT, 'flags' => API_ALLOW_NULL, 'default' => null, 'fields' => [
76				'name' =>					['type' => API_STRINGS_UTF8, 'flags' => API_ALLOW_NULL | API_NORMALIZE]
77			]],
78			'searchByAny' =>			['type' => API_BOOLEAN, 'default' => false],
79			'startSearch' =>			['type' => API_FLAG, 'default' => false],
80			'excludeSearch' =>			['type' => API_FLAG, 'default' => false],
81			'searchWildcardsEnabled' =>	['type' => API_BOOLEAN, 'default' => false],
82			// output
83			'output' =>					['type' => API_OUTPUT, 'in' => implode(',', $this->output_fields), 'default' => API_OUTPUT_EXTEND],
84			'countOutput' =>			['type' => API_FLAG, 'default' => false],
85			'selectUsers' =>			['type' => API_OUTPUT, 'flags' => API_ALLOW_NULL, 'in' => implode(',', $this->user_output_fields), 'default' => null],
86			'selectUserGroups' =>		['type' => API_OUTPUT, 'flags' => API_ALLOW_NULL, 'in' => implode(',', $this->usrgrp_output_fields), 'default' => null],
87			// sort and limit
88			'sortfield' =>				['type' => API_STRINGS_UTF8, 'flags' => API_NORMALIZE, 'in' => implode(',', $this->sortColumns), 'uniq' => true, 'default' => []],
89			'sortorder' =>				['type' => API_SORTORDER, 'default' => []],
90			'limit' =>					['type' => API_INT32, 'flags' => API_ALLOW_NULL, 'in' => '1:'.ZBX_MAX_INT32, 'default' => null],
91			// flags
92			'preservekeys' =>			['type' => API_BOOLEAN, 'default' => false]
93		]];
94		if (!CApiInputValidator::validate($api_input_rules, $options, '/', $error)) {
95			self::exception(ZBX_API_ERROR_PARAMETERS, $error);
96		}
97
98		$sql_parts = $this->createSelectQueryParts($this->tableName(), $this->tableAlias(), $options);
99
100		// expired
101		if ($options['expired'] !== null) {
102			$sql_parts['where'][] = $options['expired']
103				? '(r.active_till>0 AND r.active_till<'.time().')'
104				: '(r.active_till=0 OR r.active_till>='.time().')';
105		}
106
107		$result = DBselect(self::createSelectQueryFromParts($sql_parts), $options['limit']);
108
109		$db_reports = [];
110		while ($row = DBfetch($result)) {
111			if ($options['countOutput']) {
112				return $row['rowscount'];
113			}
114
115			$db_reports[$row['reportid']] = $row;
116		}
117
118		if ($db_reports) {
119			$db_reports = $this->addRelatedObjects($options, $db_reports);
120			$db_reports = $this->unsetExtraFields($db_reports, ['reportid'], $options['output']);
121
122			if (!$options['preservekeys']) {
123				$db_reports = array_values($db_reports);
124			}
125		}
126
127		return $db_reports;
128	}
129
130	/**
131	 * @param array $reports
132	 *
133	 * @return array
134	 */
135	public function create(array $reports): array {
136		$this->validateCreate($reports);
137
138		$ins_reports = [];
139
140		foreach ($reports as $report) {
141			unset($report['subject'], $report['message'], $report['users'], $report['user_groups']);
142			$ins_reports[] = $report;
143		}
144
145		$reportids = DB::insert('report', $ins_reports);
146
147		foreach ($reports as $index => &$report) {
148			$report['reportid'] = $reportids[$index];
149		}
150		unset($report);
151
152		$this->updateParams($reports, __FUNCTION__);
153		$this->updateUsers($reports, __FUNCTION__);
154		$this->updateUserGroups($reports, __FUNCTION__);
155
156		$this->addAuditBulk(AUDIT_ACTION_ADD, AUDIT_RESOURCE_SCHEDULED_REPORT, $reports);
157
158		return ['reportids' => $reportids];
159	}
160
161	/**
162	 * @param array $reports
163	 *
164	 * @throws APIException if no permissions or the input is invalid.
165	 */
166	protected function validateCreate(array &$reports): void {
167		$api_input_rules = ['type' => API_OBJECTS, 'flags' => API_NOT_EMPTY | API_NORMALIZE, 'uniq' => [['name']], 'fields' => [
168			'userid' =>				['type' => API_ID, 'default' => self::$userData['userid']],
169			'name' =>				['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('report', 'name')],
170			'description' =>		['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('report', 'description')],
171			'status' =>				['type' => API_INT32, 'in' => ZBX_REPORT_STATUS_DISABLED.','.ZBX_REPORT_STATUS_ENABLED],
172			'dashboardid' =>		['type' => API_ID, 'flags' => API_REQUIRED],
173			'period' =>				['type' => API_INT32, 'in' => implode(',', [ZBX_REPORT_PERIOD_DAY, ZBX_REPORT_PERIOD_WEEK, ZBX_REPORT_PERIOD_MONTH, ZBX_REPORT_PERIOD_YEAR])],
174			'cycle' =>				['type' => API_INT32, 'in' => implode(',', [ZBX_REPORT_CYCLE_DAILY, ZBX_REPORT_CYCLE_WEEKLY, ZBX_REPORT_CYCLE_MONTHLY, ZBX_REPORT_CYCLE_YEARLY]), 'default' => DB::getDefault('report', 'cycle')],
175			'weekdays' =>			['type' => API_INT32],
176			'start_time' =>			['type' => API_INT32, 'in' => '0:86340'],
177			'active_since' =>		['type' => API_DATE, 'default' => ''],
178			'active_till' =>		['type' => API_DATE, 'default' => ''],
179			// The length of the "report.subject" and "media_type_message.subject" fields should match.
180			'subject' =>			['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('media_type_message', 'subject')],
181			'message' =>			['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('report_param', 'value')],
182			'users' =>				['type' => API_OBJECTS, 'flags' => API_NORMALIZE, 'uniq' => [['userid']], 'default' => [], 'fields' => [
183				'userid' =>				['type' => API_ID, 'flags' => API_REQUIRED],
184				'access_userid' =>		['type' => API_ID],
185				'exclude' =>			['type' => API_INT32, 'in' => ZBX_REPORT_EXCLUDE_USER_FALSE.','.ZBX_REPORT_EXCLUDE_USER_TRUE, 'default' => DB::getDefault('report_user', 'exclude')]
186			]],
187			'user_groups' =>		['type' => API_OBJECTS, 'flags' => API_NORMALIZE, 'uniq' => [['usrgrpid']], 'default' => [], 'fields' => [
188				'usrgrpid' =>			['type' => API_ID, 'flags' => API_REQUIRED],
189				'access_userid' =>		['type' => API_ID]
190			]]
191		]];
192		if (!CApiInputValidator::validate($api_input_rules, $reports, '/', $error)) {
193			self::exception(ZBX_API_ERROR_PARAMETERS, $error);
194		}
195
196		$rnum = 0;
197		foreach ($reports as &$report) {
198			$rnum++;
199
200			if ($report['cycle'] == ZBX_REPORT_CYCLE_WEEKLY) {
201				if (!array_key_exists('weekdays', $report)) {
202					self::exception(ZBX_API_ERROR_PARAMETERS, _s('Invalid parameter "%1$s": %2$s.', '/'.$rnum,
203						_s('the parameter "%1$s" is missing', 'weekdays')
204					));
205				}
206
207				if ($report['weekdays'] < 1 || $report['weekdays'] > 127) {
208					self::exception(ZBX_API_ERROR_PARAMETERS, _s('Invalid parameter "%1$s": %2$s.',
209						'/'.$rnum.'/weekdays', _s('value must be one of %1$s', '1-127')
210					));
211				}
212			}
213			elseif (array_key_exists('weekdays', $report) && $report['weekdays'] != 0) {
214				self::exception(ZBX_API_ERROR_PARAMETERS, _s('Invalid parameter "%1$s": %2$s.',
215					'/'.$rnum.'/weekdays', _s('value must be %1$s', '0')
216				));
217			}
218
219			$report['active_since'] = ($report['active_since'] !== '')
220				? $report['active_since'] = (DateTime::createFromFormat(ZBX_DATE, $report['active_since'],
221					new DateTimeZone('UTC')
222				))
223					->setTime(0, 0)
224					->getTimestamp()
225				: 0;
226
227			$report['active_till'] = ($report['active_till'] !== '')
228				? $report['active_till'] = (DateTime::createFromFormat(ZBX_DATE, $report['active_till'],
229					new DateTimeZone('UTC')
230				))
231					->setTime(23, 59, 59)
232					->getTimestamp()
233				: 0;
234
235			if ($report['active_till'] > 0 && $report['active_since'] > $report['active_till']) {
236				self::exception(ZBX_API_ERROR_PARAMETERS,
237					_s('"%1$s" must be an empty string or greater than "%2$s".', 'active_till', 'active_since')
238				);
239			}
240		}
241		unset($report);
242
243		$this->checkDuplicates(array_column($reports, 'name'));
244		$this->checkDashboards(array_unique(array_column($reports, 'dashboardid')));
245		$this->checkUsers($reports);
246		$this->checkUserGroups($reports);
247	}
248
249	/**
250	 * Check for duplicated reports.
251	 *
252	 * @param array $names
253	 *
254	 * @throws APIException if report already exists.
255	 */
256	protected function checkDuplicates(array $names): void {
257		$db_reports = DB::select('report', [
258			'output' => ['name'],
259			'filter' => ['name' => $names],
260			'limit' => 1
261		]);
262
263		if ($db_reports) {
264			self::exception(ZBX_API_ERROR_PARAMETERS, _s('Report "%1$s" already exists.', $db_reports[0]['name']));
265		}
266	}
267
268	/**
269	 * Check for valid dashboards.
270	 *
271	 * @param array $dashboardids
272	 *
273	 * @throws APIException if dashboard is not valid.
274	 */
275	protected function checkDashboards(array $dashboardids): void {
276		$db_dashboards = API::Dashboard()->get([
277			'output' => [],
278			'dashboardids' => $dashboardids,
279			'preservekeys' => true
280		]);
281
282		foreach ($dashboardids as $dashboardid) {
283			if (!array_key_exists($dashboardid, $db_dashboards)) {
284				self::exception(ZBX_API_ERROR_PARAMETERS,
285					_s('Dashboard with ID "%1$s" is not available.', $dashboardid)
286				);
287			}
288		}
289	}
290
291	/**
292	 * Check for valid users.
293	 *
294	 * @param array  $reports
295	 * @param string $reports[]['userid']                          (optional)
296	 * @param string $reports[]['dashboardid']                     (optional)
297	 * @param array  $reports[]['users']                           (optional)
298	 * @param string $reports[]['users'][]['userid']
299	 * @param string $reports[]['users'][]['access_userid']        (optional)
300	 * @param string $reports[]['users'][]['exclude']
301	 * @param array  $reports[]['user_groups']                     (optional)
302	 * @param string $reports[]['user_groups'][]['access_userid']  (optional)
303	 * @param array  $db_reports                                   (optional)
304	 * @param string $db_reports[]['reportid']
305	 * @param string $db_reports[]['userid']
306	 * @param string $db_reports[]['dashboardid']
307	 * @param array  $db_reports[]['users']
308	 * @param string $db_reports[]['users'][]['userid']
309	 * @param string $db_reports[]['users'][]['access_userid']
310	 * @param string $db_reports[]['users'][]['exclude']
311	 * @param array  $db_reports[]['user_groups']
312	 * @param string $db_reports[]['user_groups'][]['access_userid']
313	 *
314	 * @throws APIException if user is not valid.
315	 */
316	protected function checkUsers(array $reports, array $db_reports = []): void {
317		$userids = [];
318
319		foreach ($reports as $report) {
320			$db_report = [];
321			$dashboardid_has_changed = false;
322			$users = array_key_exists('users', $report) ? $report['users'] : [];
323			$user_groups = array_key_exists('user_groups', $report) ? $report['user_groups'] : [];
324
325			if ($db_reports) {
326				$db_report = $db_reports[$report['reportid']];
327				$dashboardid_has_changed = (array_key_exists('dashboardid', $report)
328					&& $report['dashboardid'] != $db_report['dashboardid']
329				);
330				if (!array_key_exists('users', $report)) {
331					$users = $db_report['users'];
332				}
333				if (!array_key_exists('user_groups', $report)) {
334					$user_groups = $db_report['user_groups'];
335				}
336			}
337
338			if (array_key_exists('userid', $report) && (!$db_report || $report['userid'] != $db_report['userid'])) {
339				if (self::$userData['type'] != USER_TYPE_SUPER_ADMIN) {
340					if ((!$db_report && $report['userid'] != self::$userData['userid'])
341							|| ($db_report && $report['userid'] != $db_report['userid'])) {
342						self::exception(ZBX_API_ERROR_PARAMETERS, _('Only super admins can set report owner.'));
343					}
344				}
345
346				$userids[$report['userid']] = true;
347			}
348
349			if (!$user_groups) {
350				if (!$users) {
351					self::exception(ZBX_API_ERROR_PARAMETERS, _('At least one user or user group must be specified.'));
352				}
353
354				if (!array_key_exists(ZBX_REPORT_EXCLUDE_USER_FALSE, array_column($users, 'exclude', 'exclude'))) {
355					self::exception(ZBX_API_ERROR_PARAMETERS,
356						_('If no user groups are specified, at least one user must be included in the mailing list.')
357					);
358				}
359			}
360
361			if (array_key_exists('users', $report) && $report['users']) {
362				$db_userids = [];
363				$db_access_userids = [];
364				if ($db_report) {
365					$db_userids = array_flip(array_column($db_report['users'], 'userid'));
366					$db_access_userids = array_flip(array_column($db_report['users'], 'access_userid'));
367				}
368
369				foreach ($report['users'] as $user) {
370					if ($dashboardid_has_changed || !array_key_exists($user['userid'], $db_userids)) {
371						$userids[$user['userid']] = true;
372					}
373
374					if (array_key_exists('access_userid', $user) && $user['access_userid'] != 0
375							&& ($dashboardid_has_changed
376								|| !array_key_exists($user['access_userid'], $db_access_userids))) {
377						$userids[$user['access_userid']] = true;
378					}
379				}
380			}
381
382			if (array_key_exists('user_groups', $report) && $report['user_groups']) {
383				$db_access_userids = $db_report
384					? array_flip(array_column($db_report['user_groups'], 'access_userid'))
385					: [];
386
387				foreach ($report['user_groups'] as $usrgrp) {
388					if (array_key_exists('access_userid', $usrgrp) && $usrgrp['access_userid'] != 0
389							&& ($dashboardid_has_changed
390								|| !array_key_exists($usrgrp['access_userid'], $db_access_userids))) {
391						$userids[$usrgrp['access_userid']] = true;
392					}
393				}
394			}
395		}
396
397		unset($userids[self::$userData['userid']]);
398
399		if (!$userids) {
400			return;
401		}
402
403		$userids = array_keys($userids);
404
405		$db_users = API::User()->get([
406			'output' => [],
407			'userids' => $userids,
408			'preservekeys' => true
409		]);
410
411		foreach ($userids as $userid) {
412			if (!array_key_exists($userid, $db_users)) {
413				self::exception(ZBX_API_ERROR_PARAMETERS, _s('User with ID "%1$s" is not available.', $userid));
414			}
415		}
416	}
417
418	/**
419	 * Check for valid user groups.
420	 *
421	 * @param array  $reports
422	 * @param string $reports[]['dashboarid']                    (optional)
423	 * @param array  $reports[]['user_groups']                   (optional)
424	 * @param string $reports[]['user_groups'][]['usrgrpid']
425	 * @param array  $db_reports                                 (optional)
426	 * @param string $db_reports[]['reportid']
427	 * @param string $db_reports[]['dashboarid']
428	 * @param array  $db_reports[]['user_groups']
429	 * @param string $db_reports[]['user_groups'][]['usrgrpid']
430	 *
431	 * @throws APIException if user group is not valid.
432	 */
433	protected function checkUserGroups(array $reports, array $db_reports = []): void {
434		$usrgrpids = [];
435
436		foreach ($reports as $report) {
437			if (array_key_exists('user_groups', $report) && $report['user_groups']) {
438				$db_usrgrpids = [];
439				$dashboardid_has_changed = false;
440
441				if ($db_reports) {
442					$db_report = $db_reports[$report['reportid']];
443					$db_usrgrpids = array_flip(array_column($db_report['user_groups'], 'usrgrpid'));
444					$dashboardid_has_changed = (array_key_exists('dashboarid', $report)
445						&& $report['dashboarid'] != $db_report['dashboarid']
446					);
447				}
448
449				foreach ($report['user_groups'] as $usrgrp) {
450					if ($dashboardid_has_changed || !array_key_exists($usrgrp['usrgrpid'], $db_usrgrpids)) {
451						$usrgrpids[$usrgrp['usrgrpid']] = true;
452					}
453				}
454			}
455		}
456
457		if (!$usrgrpids) {
458			return;
459		}
460
461		$usrgrpids = array_keys($usrgrpids);
462
463		$db_usrgrps = API::UserGroup()->get([
464			'output' => [],
465			'usrgrpids' => $usrgrpids,
466			'preservekeys' => true
467		]);
468
469		foreach ($usrgrpids as $usrgrpid) {
470			if (!array_key_exists($usrgrpid, $db_usrgrps)) {
471				self::exception(ZBX_API_ERROR_PARAMETERS, _s('User group with ID "%1$s" is not available.', $usrgrpid));
472			}
473		}
474	}
475
476	/**
477	 * @param array $reports
478	 *
479	 * @return array
480	 */
481	public function update(array $reports): array {
482		$this->validateUpdate($reports, $db_reports);
483
484		$upd_reports = [];
485
486		foreach ($reports as $report) {
487			$db_report = $db_reports[$report['reportid']];
488
489			$upd_report = [];
490
491			foreach (['userid', 'status', 'dashboardid', 'period', 'cycle', 'weekdays', 'start_time', 'active_since',
492					'active_till'] as $field_name) {
493				if (array_key_exists($field_name, $report) && $report[$field_name] != $db_report[$field_name]) {
494					$upd_report[$field_name] = $report[$field_name];
495				}
496			}
497
498			foreach (['name', 'description'] as $field_name) {
499				if (array_key_exists($field_name, $report) && $report[$field_name] !== $db_report[$field_name]) {
500					$upd_report[$field_name] = $report[$field_name];
501				}
502			}
503
504			if ($upd_report) {
505				$upd_reports[] = [
506					'values' => $upd_report,
507					'where' => ['reportid' => $report['reportid']]
508				];
509			}
510		}
511
512		if ($upd_reports) {
513			DB::update('report', $upd_reports);
514		}
515
516		$this->updateParams($reports, __FUNCTION__);
517		$this->updateUsers($reports, __FUNCTION__);
518		$this->updateUserGroups($reports, __FUNCTION__);
519
520		$this->addAuditBulk(AUDIT_ACTION_UPDATE, AUDIT_RESOURCE_SCHEDULED_REPORT, $reports, $db_reports);
521
522		return ['reportids' => array_column($reports, 'reportid')];
523	}
524
525	/**
526	 * @param array      $reports
527	 * @param array|null $db_reports
528	 *
529	 * @throws APIException if no permissions or the input is invalid.
530	 */
531	protected function validateUpdate(array &$reports, ?array &$db_reports = null): void {
532		$api_input_rules = ['type' => API_OBJECTS, 'flags' => API_NOT_EMPTY | API_NORMALIZE, 'uniq' => [['name']], 'fields' => [
533			'reportid' =>			['type' => API_ID, 'flags' => API_REQUIRED],
534			'userid' =>				['type' => API_ID],
535			'name' =>				['type' => API_STRING_UTF8, 'flags' => API_NOT_EMPTY, 'length' => DB::getFieldLength('report', 'name')],
536			'description' =>		['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('report', 'description')],
537			'status' =>				['type' => API_INT32, 'in' => ZBX_REPORT_STATUS_DISABLED.','.ZBX_REPORT_STATUS_ENABLED],
538			'dashboardid' =>		['type' => API_ID],
539			'period' =>				['type' => API_INT32, 'in' => implode(',', [ZBX_REPORT_PERIOD_DAY, ZBX_REPORT_PERIOD_WEEK, ZBX_REPORT_PERIOD_MONTH, ZBX_REPORT_PERIOD_YEAR])],
540			'cycle' =>				['type' => API_INT32, 'in' => implode(',', [ZBX_REPORT_CYCLE_DAILY, ZBX_REPORT_CYCLE_WEEKLY, ZBX_REPORT_CYCLE_MONTHLY, ZBX_REPORT_CYCLE_YEARLY])],
541			'weekdays' =>			['type' => API_INT32],
542			'start_time' =>			['type' => API_INT32, 'in' => '0:86340'],
543			'active_since' =>		['type' => API_DATE],
544			'active_till' =>		['type' => API_DATE],
545			// The length of the "report.subject" and "media_type_message.subject" fields should match.
546			'subject' =>			['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('media_type_message', 'subject')],
547			'message' =>			['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('report_param', 'value')],
548			'users' =>				['type' => API_OBJECTS, 'flags' => API_NORMALIZE, 'uniq' => [['userid']], 'fields' => [
549				'userid' =>				['type' => API_ID, 'flags' => API_REQUIRED],
550				'access_userid' =>		['type' => API_ID],
551				'exclude' =>			['type' => API_INT32, 'in' => ZBX_REPORT_EXCLUDE_USER_FALSE.','.ZBX_REPORT_EXCLUDE_USER_TRUE, 'default' => DB::getDefault('report_user', 'exclude')]
552			]],
553			'user_groups' =>		['type' => API_OBJECTS, 'flags' => API_NORMALIZE, 'uniq' => [['usrgrpid']], 'fields' => [
554				'usrgrpid' =>			['type' => API_ID, 'flags' => API_REQUIRED],
555				'access_userid' =>		['type' => API_ID]
556			]]
557		]];
558		if (!CApiInputValidator::validate($api_input_rules, $reports, '/', $error)) {
559			self::exception(ZBX_API_ERROR_PARAMETERS, $error);
560		}
561
562		$db_reports = $this->get([
563			'output' => $this->output_fields,
564			'selectUsers' => $this->user_output_fields,
565			'selectUserGroups' => $this->usrgrp_output_fields,
566			'reportids' => array_column($reports, 'reportid'),
567			'preservekeys' => true
568		]);
569
570		$names = [];
571		$dashboardids = [];
572
573		$rnum = 0;
574		foreach ($reports as &$report) {
575			$rnum++;
576
577			if (!array_key_exists($report['reportid'], $db_reports)) {
578				self::exception(ZBX_API_ERROR_PARAMETERS,
579					_s('Report with ID "%1$s" is not available.', $report['reportid'])
580				);
581			}
582
583			$db_report = $db_reports[$report['reportid']];
584
585			if (array_key_exists('name', $report) && $report['name'] !== $db_report['name']) {
586				$names[] = $report['name'];
587			}
588
589			if (array_key_exists('dashboardid', $report) && $report['dashboardid'] != $db_report['dashboardid']) {
590				if (!array_key_exists('users', $report)) {
591					self::exception(ZBX_API_ERROR_PARAMETERS, _s('Invalid parameter "%1$s": %2$s.', '/'.$rnum,
592						_s('the parameter "%1$s" is missing', 'users')
593					));
594				}
595
596				if (!array_key_exists('user_groups', $report)) {
597					self::exception(ZBX_API_ERROR_PARAMETERS, _s('Invalid parameter "%1$s": %2$s.', '/'.$rnum,
598						_s('the parameter "%1$s" is missing', 'user_groups')
599					));
600				}
601
602				$dashboardids[$report['dashboardid']] = true;
603			}
604
605			if (array_key_exists('cycle', $report) || array_key_exists('weekdays', $report)) {
606				$cycle = array_key_exists('cycle', $report) ? $report['cycle'] : $db_report['cycle'];
607				$weekdays = array_key_exists('weekdays', $report) ? $report['weekdays'] : $db_report['weekdays'];
608
609				if ($cycle == ZBX_REPORT_CYCLE_WEEKLY) {
610					if ($weekdays < 1 || $weekdays > 127) {
611						self::exception(ZBX_API_ERROR_PARAMETERS, _s('Invalid parameter "%1$s": %2$s.',
612							'/'.$rnum.'/weekdays', _s('value must be one of %1$s', '1-127')
613						));
614					}
615				}
616				elseif ($weekdays != 0) {
617					self::exception(ZBX_API_ERROR_PARAMETERS, _s('Invalid parameter "%1$s": %2$s.',
618						'/'.$rnum.'/weekdays', _s('value must be %1$s', '0')
619					));
620				}
621			}
622
623			if (array_key_exists('active_since', $report) || array_key_exists('active_till', $report)) {
624				$active_since = $db_report['active_since'];
625				$active_till = $db_report['active_till'];
626
627				if (array_key_exists('active_since', $report)) {
628					$active_since = ($report['active_since'] !== '')
629						? (DateTime::createFromFormat(ZBX_DATE, $report['active_since'],
630							new DateTimeZone('UTC')
631						))
632							->setTime(0, 0)
633							->getTimestamp()
634						: 0;
635					$report['active_since'] = $active_since;
636				}
637
638				if (array_key_exists('active_till', $report)) {
639					$active_till = ($report['active_till'] !== '')
640						? (DateTime::createFromFormat(ZBX_DATE, $report['active_till'],
641							new DateTimeZone('UTC')
642						))
643							->setTime(23, 59, 59)
644							->getTimestamp()
645						: 0;
646					$report['active_till'] = $active_till;
647				}
648
649				if ($active_till > 0 && $active_since > $active_till) {
650					self::exception(ZBX_API_ERROR_PARAMETERS,
651						_s('"%1$s" must be an empty string or greater than "%2$s".', 'active_till', 'active_since')
652					);
653				}
654			}
655		}
656		unset($report);
657
658		if ($names) {
659			$this->checkDuplicates($names);
660		}
661		if ($dashboardids) {
662			$this->checkDashboards(array_keys($dashboardids));
663		}
664		$this->checkUsers($reports, $db_reports);
665		$this->checkUserGroups($reports, $db_reports);
666	}
667
668	/**
669	 * Update table "report_param".
670	 *
671	 * @param array  $reports
672	 * @param string $method
673	 */
674	protected function updateParams(array $reports, string $method): void {
675		$params_by_name = [
676			'subject' => 'subject',
677			'body' => 'message'
678		];
679		$report_params = [];
680
681		foreach ($reports as $report) {
682			foreach ($params_by_name as $name => $param) {
683				if (!array_key_exists($param, $report)) {
684					continue;
685				}
686
687				$report_params[$report['reportid']][$name] = [
688					'name' => $name,
689					'value' => $report[$param]
690				];
691			}
692		}
693
694		if (!$report_params) {
695			return;
696		}
697
698		$db_report_params = ($method === 'update')
699			? DB::select('report_param', [
700				'output' => ['reportparamid', 'reportid', 'name', 'value'],
701				'filter' => ['reportid' => array_keys($report_params)]
702			])
703			: [];
704
705		$ins_report_params = [];
706		$upd_report_params = [];
707		$del_reportparamids = [];
708
709		foreach ($db_report_params as $db_report_param) {
710			$reportid = $db_report_param['reportid'];
711			$name = $db_report_param['name'];
712
713			if (array_key_exists($name, $report_params[$reportid])) {
714				$report_param = $report_params[$reportid][$name];
715				unset($report_params[$reportid][$name]);
716
717				if ($report_param['value'] === '') {
718					$del_reportparamids[] = $db_report_param['reportparamid'];
719				}
720				else {
721					$upd_report_param = DB::getUpdatedValues('report_param', $report_param, $db_report_param);
722
723					if ($upd_report_param) {
724						$upd_report_params[] = [
725							'values' => $upd_report_param,
726							'where' => ['reportparamid' => $db_report_param['reportparamid']]
727						];
728					}
729				}
730			}
731		}
732
733		foreach ($report_params as $reportid => $report_param) {
734			foreach ($report_param as $param) {
735				if ($param['value'] !== '') {
736					$ins_report_params[] = ['reportid' => $reportid] + $param;
737				}
738			}
739		}
740
741		if ($ins_report_params) {
742			DB::insertBatch('report_param', $ins_report_params);
743		}
744
745		if ($upd_report_params) {
746			DB::update('report_param', $upd_report_params);
747		}
748
749		if ($del_reportparamids) {
750			DB::delete('report_param', ['reportparamid' => $del_reportparamids]);
751		}
752	}
753
754	/**
755	 * Update table "report_user".
756	 *
757	 * @param array  $reports
758	 * @param string $method
759	 */
760	protected function updateUsers(array $reports, string $method): void {
761		$report_users = [];
762
763		foreach ($reports as $report) {
764			if (array_key_exists('users', $report)) {
765				$report_users[$report['reportid']] = array_column($report['users'], null, 'userid');
766			}
767		}
768
769		if (!$report_users) {
770			return;
771		}
772
773		$db_report_users = ($method === 'update')
774			? DB::select('report_user', [
775				'output' => ['reportuserid', 'reportid', 'userid', 'access_userid', 'exclude'],
776				'filter' => ['reportid' => array_keys($report_users)]
777			])
778			: [];
779
780		$ins_report_users = [];
781		$upd_report_users = [];
782		$del_reportuserids = [];
783
784		foreach ($db_report_users as $db_report_user) {
785			if (array_key_exists($db_report_user['userid'], $report_users[$db_report_user['reportid']])) {
786				$report_user = $report_users[$db_report_user['reportid']][$db_report_user['userid']];
787				unset($report_users[$db_report_user['reportid']][$db_report_user['userid']]);
788
789				$upd_report_user = DB::getUpdatedValues('report_user', $report_user, $db_report_user);
790
791				if ($upd_report_user) {
792					$upd_report_users[] = [
793						'values' => $upd_report_user,
794						'where' => ['reportuserid' => $db_report_user['reportuserid']]
795					];
796				}
797			}
798			else {
799				$del_reportuserids[] = $db_report_user['reportuserid'];
800			}
801		}
802
803		foreach ($report_users as $reportid => $users) {
804			foreach ($users as $user) {
805				$ins_report_users[] = ['reportid' => $reportid] + $user;
806			}
807		}
808
809		if ($ins_report_users) {
810			DB::insert('report_user', $ins_report_users);
811		}
812
813		if ($upd_report_users) {
814			DB::update('report_user', $upd_report_users);
815		}
816
817		if ($del_reportuserids) {
818			DB::delete('report_user', ['reportuserid' => $del_reportuserids]);
819		}
820	}
821
822	/**
823	 * Update table "report_usrgrp".
824	 *
825	 * @param array  $reports
826	 * @param string $method
827	 */
828	protected function updateUserGroups(array $reports, string $method): void {
829		$report_usrgrps = [];
830
831		foreach ($reports as $report) {
832			if (array_key_exists('user_groups', $report)) {
833				$report_usrgrps[$report['reportid']] = array_column($report['user_groups'], null, 'usrgrpid');
834			}
835		}
836
837		if (!$report_usrgrps) {
838			return;
839		}
840
841		$db_report_usrgrps = ($method === 'update')
842			? DB::select('report_usrgrp', [
843				'output' => ['reportusrgrpid', 'reportid', 'usrgrpid', 'access_userid'],
844				'filter' => ['reportid' => array_keys($report_usrgrps)]
845			])
846			: [];
847
848		$ins_report_usrgrps = [];
849		$upd_report_usrgrps = [];
850		$del_reportusrgrpids = [];
851
852		foreach ($db_report_usrgrps as $db_report_usrgrp) {
853			if (array_key_exists($db_report_usrgrp['usrgrpid'], $report_usrgrps[$db_report_usrgrp['reportid']])) {
854				$report_usrgrp = $report_usrgrps[$db_report_usrgrp['reportid']][$db_report_usrgrp['usrgrpid']];
855				unset($report_usrgrps[$db_report_usrgrp['reportid']][$db_report_usrgrp['usrgrpid']]);
856
857				$upd_report_usrgrp = DB::getUpdatedValues('report_usrgrp', $report_usrgrp, $db_report_usrgrp);
858
859				if ($upd_report_usrgrp) {
860					$upd_report_usrgrps[] = [
861						'values' => $upd_report_usrgrp,
862						'where' => ['reportusrgrpid' => $db_report_usrgrp['reportusrgrpid']]
863					];
864				}
865			}
866			else {
867				$del_reportusrgrpids[] = $db_report_usrgrp['reportusrgrpid'];
868			}
869		}
870
871		foreach ($report_usrgrps as $reportid => $usrgrps) {
872			foreach ($usrgrps as $usrgrp) {
873				$ins_report_usrgrps[] = ['reportid' => $reportid] + $usrgrp;
874			}
875		}
876
877		if ($ins_report_usrgrps) {
878			DB::insert('report_usrgrp', $ins_report_usrgrps);
879		}
880
881		if ($upd_report_usrgrps) {
882			DB::update('report_usrgrp', $upd_report_usrgrps);
883		}
884
885		if ($del_reportusrgrpids) {
886			DB::delete('report_usrgrp', ['reportusrgrpid' => $del_reportusrgrpids]);
887		}
888	}
889
890	/**
891	 * @param array $reportids
892	 *
893	 * @return array
894	 */
895	public function delete(array $reportids): array {
896		$api_input_rules = ['type' => API_IDS, 'flags' => API_NOT_EMPTY, 'uniq' => true];
897		if (!CApiInputValidator::validate($api_input_rules, $reportids, '/', $error)) {
898			self::exception(ZBX_API_ERROR_PARAMETERS, $error);
899		}
900
901		$db_reports = $this->get([
902			'output' => ['reportid', 'name'],
903			'reportids' => $reportids,
904			'preservekeys' => true
905		]);
906
907		foreach ($reportids as $reportid) {
908			if (!array_key_exists($reportid, $db_reports)) {
909				self::exception(ZBX_API_ERROR_PERMISSIONS,
910					_('No permissions to referred object or it does not exist!')
911				);
912			}
913		}
914
915		DB::delete('report', ['reportid' => $reportids]);
916
917		$this->addAuditBulk(AUDIT_ACTION_DELETE, AUDIT_RESOURCE_SCHEDULED_REPORT, $db_reports);
918
919		return ['reportids' => $reportids];
920	}
921
922	protected function addRelatedObjects(array $options, array $result): array {
923		$reportids = array_keys($result);
924
925		// If requested, convert 'active_since' and 'active_till' from timestamp to date.
926		if ($this->outputIsRequested('active_since', $options['output'])
927				|| $this->outputIsRequested('active_till', $options['output'])) {
928			foreach ($result as &$report) {
929				if (array_key_exists('active_since', $report)) {
930					$report['active_since'] = ($report['active_since'] != 0)
931						? (new DateTime('@'.$report['active_since']))->format(ZBX_DATE)
932						: '';
933				}
934				if (array_key_exists('active_till', $report)) {
935					$report['active_till'] = ($report['active_till'] != 0)
936						? (new DateTime('@'.$report['active_till']))->format(ZBX_DATE)
937						: '';
938				}
939			}
940			unset($report);
941		}
942
943		// Adding email subject and message.
944		$fields_by_name = [];
945		if ($this->outputIsRequested('subject', $options['output'])) {
946			$fields_by_name['subject'] = 'subject';
947		}
948		if ($this->outputIsRequested('message', $options['output'])) {
949			$fields_by_name['body'] = 'message';
950		}
951
952		if ($fields_by_name) {
953			foreach ($result as &$report) {
954				foreach ($fields_by_name as $field) {
955					$report[$field] = '';
956				}
957			}
958			unset($report);
959
960			$params = DBselect(
961				'SELECT rp.reportid,rp.name,rp.value'.
962				' FROM report_param rp'.
963				' WHERE '.dbConditionInt('rp.reportid', $reportids)
964			);
965			while ($param = DBfetch($params)) {
966				if (array_key_exists($param['name'], $fields_by_name)) {
967					$result[$param['reportid']][$fields_by_name[$param['name']]] = $param['value'];
968				}
969			}
970		}
971
972		// Adding users.
973		if ($options['selectUsers'] !== null && $options['selectUsers'] !== API_OUTPUT_COUNT) {
974			if ($options['selectUsers'] === API_OUTPUT_EXTEND) {
975				$options['selectUsers'] = $this->user_output_fields;
976			}
977
978			foreach ($result as &$report) {
979				$report['users'] = [];
980			}
981			unset($report);
982
983			if ($options['selectUsers']) {
984				$output_fields = [
985					$this->fieldId('reportid', 'ru')
986				];
987				foreach ($options['selectUsers'] as $field) {
988					$output_fields[$field] = $this->fieldId($field, 'ru');
989				}
990
991				$users = DBselect(
992					'SELECT '.implode(',', $output_fields).
993					' FROM report_user ru'.
994					' WHERE '.dbConditionInt('reportid', $reportids)
995				);
996
997				while ($user = DBfetch($users)) {
998					$reportid = $user['reportid'];
999					unset($user['reportid']);
1000
1001					$result[$reportid]['users'][] = $user;
1002				}
1003			}
1004		}
1005
1006		// Adding user groups.
1007		if ($options['selectUserGroups'] !== null && $options['selectUserGroups'] !== API_OUTPUT_COUNT) {
1008			if ($options['selectUserGroups'] === API_OUTPUT_EXTEND) {
1009				$options['selectUserGroups'] = $this->usrgrp_output_fields;
1010			}
1011
1012			foreach ($result as &$report) {
1013				$report['user_groups'] = [];
1014			}
1015			unset($report);
1016
1017			if ($options['selectUserGroups']) {
1018				$output_fields = [
1019					$this->fieldId('reportid', 'rug')
1020				];
1021				foreach ($options['selectUserGroups'] as $field) {
1022					$output_fields[$field] = $this->fieldId($field, 'rug');
1023				}
1024
1025				$user_groups = DBselect(
1026					'SELECT '.implode(',', $output_fields).
1027					' FROM report_usrgrp rug'.
1028					' WHERE '.dbConditionInt('reportid', $reportids)
1029				);
1030
1031				while ($user_group = DBfetch($user_groups)) {
1032					$reportid = $user_group['reportid'];
1033					unset($user_group['reportid']);
1034
1035					$result[$reportid]['user_groups'][] = $user_group;
1036				}
1037			}
1038		}
1039
1040		return $result;
1041	}
1042}
1043