<?php
/*
** Copyright (C) 2001-2025 Zabbix SIA
**
** This program is free software: you can redistribute it and/or modify it under the terms of
** the GNU Affero General Public License as published by the Free Software Foundation, version 3.
**
** This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
** without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
** See the GNU Affero General Public License for more details.
**
** You should have received a copy of the GNU Affero General Public License along with this program.
** If not, see <https://www.gnu.org/licenses/>.
**/


class CControllerPopupAcknowledgeCreate extends CController {

	/**
	 * @var bool
	 */
	private $close_problems;

	/**
	 * @var bool
	 */
	private $change_severity;

	/**
	 * @var bool
	 */
	private $unacknowledge;

	/**
	 * @var bool
	 */
	private $acknowledge;

	/**
	 * @var string
	 */
	private $new_severity;

	/**
	 * @var string
	 */
	private $message;

	/**
	 * @var bool
	 */
	private $suppress;

	/**
	 * @var bool
	 */
	private $unsuppress;

	/**
	 * @var int
	 */
	private $suppress_until;

	/**
	 * @var CRangeTimeParser
	 */
	private $suppress_until_time_parser;

	/**
	 * @var int
	 */
	private $change_rank;

	/**
	 * @var bool
	 */
	private $rank_change_to_cause;

	/**
	 * @var bool
	 */
	private $rank_change_to_symptom;

	protected function checkInput() {
		$fields = [
			'eventids' =>				'required|array_db acknowledges.eventid',
			'cause_eventid' =>			'db acknowledges.eventid',
			'message' =>				'db acknowledges.message|flags '.P_CRLF,
			'scope' =>					'in '.ZBX_ACKNOWLEDGE_SELECTED.','.ZBX_ACKNOWLEDGE_PROBLEM,
			'change_severity' =>		'db acknowledges.action|in '.ZBX_PROBLEM_UPDATE_NONE.','.ZBX_PROBLEM_UPDATE_SEVERITY,
			'severity' =>				'ge '.TRIGGER_SEVERITY_NOT_CLASSIFIED.'|le '.TRIGGER_SEVERITY_COUNT,
			'acknowledge_problem' =>	'db acknowledges.action|in '.ZBX_PROBLEM_UPDATE_NONE.','.ZBX_PROBLEM_UPDATE_ACKNOWLEDGE,
			'unacknowledge_problem' =>	'db acknowledges.action|in '.ZBX_PROBLEM_UPDATE_NONE.','.ZBX_PROBLEM_UPDATE_UNACKNOWLEDGE,
			'close_problem' =>			'db acknowledges.action|in '.ZBX_PROBLEM_UPDATE_NONE.','.ZBX_PROBLEM_UPDATE_CLOSE,
			'suppress_problem' =>		'db acknowledges.action|in '.ZBX_PROBLEM_UPDATE_NONE.','.ZBX_PROBLEM_UPDATE_SUPPRESS,
			'suppress_time_option' =>	'in '.ZBX_PROBLEM_SUPPRESS_TIME_INDEFINITE.','.ZBX_PROBLEM_SUPPRESS_TIME_DEFINITE,
			'suppress_until_problem' => 'range_time',
			'unsuppress_problem' =>		'db acknowledges.action|in '.ZBX_PROBLEM_UPDATE_NONE.','.ZBX_PROBLEM_UPDATE_UNSUPPRESS,
			'change_rank' => 			'db acknowledges.action|in '.ZBX_PROBLEM_UPDATE_NONE.','.ZBX_PROBLEM_UPDATE_RANK_TO_CAUSE.','.ZBX_PROBLEM_UPDATE_RANK_TO_SYMPTOM
		];

		$ret = $this->validateInput($fields);

		$suppress = $this->getInput('suppress_problem', ZBX_PROBLEM_UPDATE_NONE);
		$suppress_time = $this->getInput('suppress_time_option', ZBX_PROBLEM_SUPPRESS_TIME_INDEFINITE);

		if ($ret && $suppress == ZBX_PROBLEM_UPDATE_SUPPRESS && $suppress_time == ZBX_PROBLEM_SUPPRESS_TIME_DEFINITE) {
			$this->suppress_until_time_parser = new CRangeTimeParser();
			$this->suppress_until_time_parser->parse($this->getInput('suppress_until_problem', ''));
			$suppress_until = $this->suppress_until_time_parser->getDateTime(false)->getTimestamp();

			if (!validateUnixTime($suppress_until) || $suppress_until < time()) {
				error(_s('Incorrect value for field "%1$s": %2$s.', _('Suppress'), _('invalid time')));
				$ret = false;
			}
		}

		$this->change_rank = $this->getInput('change_rank', ZBX_PROBLEM_UPDATE_NONE);

		if ($ret && $this->change_rank == ZBX_PROBLEM_UPDATE_RANK_TO_SYMPTOM
				&& !$this->hasInput('cause_eventid')) {
			error(_s('Field "%1$s" is mandatory.', 'cause_eventid'));
			$ret = false;
		}

		if (!$ret) {
			$error_title = $this->hasInput('eventids')
				? _n('Cannot update event', 'Cannot update events', count($this->getInput('eventids', [])))
				: _('Cannot update events');

			$this->setResponse(
				new CControllerResponseData(['main_block' => json_encode([
					'error' => [
						'title' => $error_title,
						'messages' => array_column(get_and_clear_messages(), 'message')
					]
				])])
			);
		}

		return $ret;
	}

	protected function checkPermissions() {
		if (!$this->checkAccess(CRoleHelper::ACTIONS_ACKNOWLEDGE_PROBLEMS)
				&& !$this->checkAccess(CRoleHelper::ACTIONS_CLOSE_PROBLEMS)
				&& !$this->checkAccess(CRoleHelper::ACTIONS_CHANGE_SEVERITY)
				&& !$this->checkAccess(CRoleHelper::ACTIONS_ADD_PROBLEM_COMMENTS)
				&& !$this->checkAccess(CRoleHelper::ACTIONS_SUPPRESS_PROBLEMS)
				&& !$this->checkAccess(CRoleHelper::ACTIONS_CHANGE_PROBLEM_RANKING)) {
			return false;
		}

		$eventids = array_flip($this->getInput('eventids'));

		if ($this->hasInput('cause_eventid')) {
			$eventids[$this->getInput('cause_eventid')] = true;
		}

		$events = API::Event()->get([
			'countOutput' => true,
			'eventids' => array_keys($eventids),
			'source' => EVENT_SOURCE_TRIGGERS,
			'object' => EVENT_OBJECT_TRIGGER
		]);

		return $events == count($eventids);
	}

	protected function doAction() {
		$updated_events_count = 0;
		$result = false;
		$data = null;

		$this->close_problems = $this->checkAccess(CRoleHelper::ACTIONS_CLOSE_PROBLEMS)
			? ($this->getInput('close_problem', ZBX_PROBLEM_UPDATE_NONE) == ZBX_PROBLEM_UPDATE_CLOSE)
			: false;
		$this->change_severity = $this->checkAccess(CRoleHelper::ACTIONS_CHANGE_SEVERITY)
			? ($this->getInput('change_severity', ZBX_PROBLEM_UPDATE_NONE) == ZBX_PROBLEM_UPDATE_SEVERITY)
			: false;
		$this->acknowledge = $this->checkAccess(CRoleHelper::ACTIONS_ACKNOWLEDGE_PROBLEMS)
			? ($this->getInput('acknowledge_problem', ZBX_PROBLEM_UPDATE_NONE) == ZBX_PROBLEM_UPDATE_ACKNOWLEDGE)
			: false;
		$this->unacknowledge = $this->checkAccess(CRoleHelper::ACTIONS_ACKNOWLEDGE_PROBLEMS)
			? ($this->getInput('unacknowledge_problem', ZBX_PROBLEM_UPDATE_NONE) == ZBX_PROBLEM_UPDATE_UNACKNOWLEDGE)
			: false;
		$this->new_severity = $this->getInput('severity', '');
		$this->message = $this->checkAccess(CRoleHelper::ACTIONS_ADD_PROBLEM_COMMENTS)
			? $this->getInput('message', '')
			: '';
		$this->suppress = $this->checkAccess(CRoleHelper::ACTIONS_SUPPRESS_PROBLEMS)
			? ($this->getInput('suppress_problem', ZBX_PROBLEM_UPDATE_NONE) == ZBX_PROBLEM_UPDATE_SUPPRESS)
			: false;
		$this->unsuppress = $this->checkAccess(CRoleHelper::ACTIONS_SUPPRESS_PROBLEMS)
			? ($this->getInput('unsuppress_problem', ZBX_PROBLEM_UPDATE_NONE) == ZBX_PROBLEM_UPDATE_UNSUPPRESS)
			: false;
		$this->rank_change_to_cause = $this->checkAccess(CRoleHelper::ACTIONS_CHANGE_PROBLEM_RANKING)
			? ($this->change_rank == ZBX_PROBLEM_UPDATE_RANK_TO_CAUSE)
			: false;
		$this->rank_change_to_symptom = $this->checkAccess(CRoleHelper::ACTIONS_CHANGE_PROBLEM_RANKING)
			? ($this->change_rank == ZBX_PROBLEM_UPDATE_RANK_TO_SYMPTOM)
			: false;

		$eventids = array_flip($this->getInput('eventids'));

		// Process suppress until time.
		if ($this->checkAccess(CRoleHelper::ACTIONS_SUPPRESS_PROBLEMS)
				&& $this->getInput('suppress_problem', ZBX_PROBLEM_UPDATE_NONE) == ZBX_PROBLEM_UPDATE_SUPPRESS) {
			// Check if suppress time option was definite or indefinite.
			if ($this->getInput('suppress_time_option') == ZBX_PROBLEM_SUPPRESS_TIME_DEFINITE) {
				// Convert suppress until problem input to Unix timestamp.
				$this->suppress_until = $this->suppress_until_time_parser->getDateTime(false)->getTimestamp();
				// Save if inserted was relative time.
				if ($this->suppress_until_time_parser->getTimeType() == CRangeTimeParser::ZBX_TIME_RELATIVE) {
					$suppress_until_time = $this->getInput('suppress_until_problem');
					CProfile::update('web.problem_suppress_action_time_until', $suppress_until_time, PROFILE_TYPE_STR);
				}
			}
			else {
				// Save suppress until time for indefinite option.
				$this->suppress_until = ZBX_PROBLEM_SUPPRESS_TIME_INDEFINITE;
			}
		}

		// Select events that are created from the same trigger if ZBX_ACKNOWLEDGE_PROBLEM is selected.
		if ($this->getInput('scope', ZBX_ACKNOWLEDGE_SELECTED) == ZBX_ACKNOWLEDGE_PROBLEM) {
			$eventids += array_flip($this->getRelatedProblemids($eventids));
		}

		// Select data about all affected events and triggers involved.
		[$events, $editable_triggers] = $this->getEventDetails(array_keys($eventids));
		unset($eventids);

		// Group events by actions user is allowed to perform.
		$eventid_groups = $this->groupEventsByActionsAllowed($events, $editable_triggers);

		// Update selected events.
		while ($eventid_groups['readable']) {
			$data = $this->getAcknowledgeOptions($eventid_groups);
			/*
			 * No actions to perform. This can happen only if user has selected an action they do not have permission to
			 * do for any of selected events. This can happen, when user performs one action on multiple problems,
			 * where only some of these problems can perform this action (ex. close problem).
			 */
			if ($data['action'] == ZBX_PROBLEM_UPDATE_NONE) {
				break;
			}

			if ($data['eventids']) {
				$eventid_chunks = array_chunk($data['eventids'], ZBX_DB_MAX_INSERTS);
				foreach ($eventid_chunks as $eventid_chunk) {
					$data['eventids'] = $eventid_chunk;
					$result = API::Event()->acknowledge($data);

					// Do not continue if event.acknowledge validation fails.
					if (!$result) {
						break 2;
					}

					$updated_events_count += count($data['eventids']);
				}
			}
		}

		$output = [];

		if ($result) {
			$success = ['title' => _n('Event updated', 'Events updated', $updated_events_count)];

			if ($messages = get_and_clear_messages()) {
				$success['messages'] = array_column($messages, 'message');
			}

			$output['success'] = $success;
		}
		else {
			if ($data && $data['action'] == ZBX_PROBLEM_UPDATE_NONE) {
				error(_('At least one update operation or message is mandatory'));
			}

			$output['error'] = [
				'title' => _n('Cannot update event', 'Cannot update events', count($this->getInput('eventids'))),
				'messages' => array_column(get_and_clear_messages(), 'message')
			];
		}

		$this->setResponse(new CControllerResponseData(['main_block' => json_encode($output)]));
	}

	/**
	 * Function returns array containing problem IDs generated by same trigger as event IDs passed as $eventids.
	 *
	 * @param array $eventids  Event IDs for which related problems must be selected.
	 *
	 * @return array
	 */
	protected function getRelatedProblemids(array $eventids): array {
		$events = API::Event()->get([
			'output' => ['objectid'],
			'eventids' => array_keys($eventids),
			'source' => EVENT_SOURCE_TRIGGERS,
			'object' => EVENT_OBJECT_TRIGGER
		]);

		if ($events) {
			$related_problems = API::Problem()->get([
				'output' => ['eventid'],
				'objectids' => array_column($events, 'objectid', 'objectid'),
				'preservekeys' => true
			]);

			return array_keys($related_problems);
		}

		return [];
	}

	/**
	 * Function returns array containing 2 sub-arrays:
	 *  - First sub-array contains details for all requested events based on user actions.
	 *  - Second sub-array contains all editable trigger IDs that has caused requested events.
	 *
	 * @param array $eventids
	 *
	 * @return array
	 */
	protected function getEventDetails(array $eventids): array {
		// Select details for all affected events.
		$events = API::Event()->get([
			'output' => ['eventid', 'objectid', 'acknowledged', 'r_eventid'],
			'selectAcknowledges' => $this->close_problems || $this->suppress || $this->unsuppress ? ['action'] : null,
			'selectSuppressionData' => $this->unsuppress ? ['maintenanceid'] : null,
			'eventids' => $eventids,
			'source' => EVENT_SOURCE_TRIGGERS,
			'object' => EVENT_OBJECT_TRIGGER,
			'preservekeys' => true
		]);

		// Select editable triggers.
		$editable_triggers = ($events && ($this->change_severity || $this->close_problems))
			? API::Trigger()->get([
				'output' => ['manual_close'],
				'triggerids' => array_column($events, 'objectid'),
				'editable' => true,
				'preservekeys' => true
			])
			: [];

		return [$events, $editable_triggers];
	}

	/**
	 * Function groups eventids according the actions user can perform for each of event.
	 * Following groups of eventids are made:
	 *  - closable events (events are writable + configured to be closed manually + not closed before);
	 *  - editable events (events are writable);
	 *  - acknowledgeable (events are not yet acknowledged);
	 *  - unacknowledgeable (events are not yet unacknowledged);
	 *  - readable events (events that user has at least read permissions).
	 *
	 * @param array   $events
	 * @param string  $events[]['eventid']                             Event ID.
	 * @param string  $events[]['objectid']                            Trigger ID that has generated particular event.
	 * @param string  $events[]['r_eventid']                           Recovery event ID.
	 * @param string  $events[]['acknowledged']                        Indicates if event is acknowledged.
	 * @param array   $editable_triggers[<triggerid>]                  Arrays containing editable trigger IDs as keys.
	 * @param int     $editable_triggers[<triggerid>]['manual_close']  Trigger's manual_close configuration.
	 *
	 * @param array
	 */
	protected function groupEventsByActionsAllowed(array $events, array $editable_triggers): array {
		$eventid_groups = [
			'closable' => [],
			'editable' => [],
			'acknowledgeable' => [],
			'unacknowledgeable' => [],
			'suppressible' => [],
			'unsuppressible' => [],
			'readable' => []
		];

		foreach ($events as $event) {
			if ($this->close_problems && $this->isEventClosable($event, $editable_triggers)) {
				$eventid_groups['closable'][] = $event['eventid'];
			}

			if ($this->change_severity && array_key_exists($event['objectid'], $editable_triggers)) {
				$eventid_groups['editable'][] = $event['eventid'];
			}

			if ($this->acknowledge && $event['acknowledged'] == EVENT_NOT_ACKNOWLEDGED) {
				$eventid_groups['acknowledgeable'][] = $event['eventid'];
			}

			if ($this->unacknowledge && $event['acknowledged'] == EVENT_ACKNOWLEDGED) {
				$eventid_groups['unacknowledgeable'][] = $event['eventid'];
			}

			if ($this->suppress && !isEventClosed($event)) {
				$eventid_groups['suppressible'][] = $event['eventid'];
			}

			if ($this->unsuppress && !isEventClosed($event) && $this->isEventSuppressed($event)) {
				$eventid_groups['unsuppressible'][] = $event['eventid'];
			}

			$eventid_groups['readable'][] = $event['eventid'];
		}

		return $eventid_groups;
	}

	/**
	 * Checks if events can be closed manually.
	 *
	 * @param array  $event                                           Event object.
	 * @param array  $event['r_eventid']                              OK event id. 0 if not resolved.
	 * @param array  $event['acknowledges']                           List of problem updates.
	 * @param string $event['acknowledges'][]['action']               Action performed in update.
	 * @param array  $editable_triggers[<triggerid>]                  List of editable triggers.
	 * @param int    $editable_triggers[<triggerid>]['manual_close']  Trigger's manual_close configuration.
	 *
	 * @return bool
	 */
	protected function isEventClosable(array $event, array $editable_triggers): bool {
		if (!array_key_exists($event['objectid'], $editable_triggers)
				|| $editable_triggers[$event['objectid']]['manual_close'] == ZBX_TRIGGER_MANUAL_CLOSE_NOT_ALLOWED
				|| isEventClosed($event)) {
			return false;
		}

		return true;
	}

	/**
	 * Checks if event is manually suppressed.
	 *
	 * @param array  $event                                         Event object.
	 * @param array  $event['suppression_data']                     List of problem suppression data.
	 * @param string $event['suppression_data'][]['maintenanceid']  Problem maintenanceid.
	 *
	 * @return bool
	 */
	protected function isEventSuppressed(array $event): bool {
		foreach ($event['suppression_data'] as $suppression) {
			if ($suppression['maintenanceid'] == 0) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Function returns an array for event.acknowledge API method, containing a list of eventids and specific 'action'
	 * flag to perform for list of eventids returned. Function will also clean utilized eventids from $eventids array.
	 *
	 * @param array $eventid_groups
	 * @param array $eventid_groups['closable']            Event IDs that user is allowed to close manually.
	 * @param array $eventid_groups['editable']            Event IDs that user is allowed to make changes.
	 * @param array $eventid_groups['acknowledgeable']     Event IDs that user is allowed to make acknowledgement.
	 * @param array $eventid_groups['unacknowledgeable']   Event IDs that user is allowed to make unacknowledgement.
	 * @param array $eventid_groups['readable']            Event IDs that user is allowed to read.
	 *
	 * @return array
	 */
	protected function getAcknowledgeOptions(array &$eventid_groups): array {
		$data = [
			'action' => ZBX_PROBLEM_UPDATE_NONE,
			'eventids' => []
		];

		if ($this->close_problems && $eventid_groups['closable']) {
			$data['action'] |= ZBX_PROBLEM_UPDATE_CLOSE;
			$data['eventids'] = $eventid_groups['closable'];
			$eventid_groups['closable'] = [];
		}

		if ($this->change_severity && $eventid_groups['editable']) {
			if (!$data['eventids']) {
				$data['eventids'] = $eventid_groups['editable'];
			}

			$data['action'] |= ZBX_PROBLEM_UPDATE_SEVERITY;
			$data['severity'] = $this->new_severity;
			$eventid_groups['editable'] = array_diff($eventid_groups['editable'], $data['eventids']);
		}

		if ($this->acknowledge && $eventid_groups['acknowledgeable']) {
			if (!$data['eventids']) {
				$data['eventids'] = $eventid_groups['acknowledgeable'];
			}

			$data['action'] |= ZBX_PROBLEM_UPDATE_ACKNOWLEDGE;
			$eventid_groups['acknowledgeable'] = array_diff($eventid_groups['acknowledgeable'], $data['eventids']);
		}

		if ($this->unacknowledge && $eventid_groups['unacknowledgeable']) {
			if (!$data['eventids']) {
				$data['eventids'] = $eventid_groups['unacknowledgeable'];
			}

			$data['action'] |= ZBX_PROBLEM_UPDATE_UNACKNOWLEDGE;
			$eventid_groups['unacknowledgeable'] = array_diff($eventid_groups['unacknowledgeable'], $data['eventids']);
		}

		if ($this->message !== '' && $eventid_groups['readable']) {
			if (!$data['eventids']) {
				$data['eventids'] = $eventid_groups['readable'];
			}

			$data['action'] |= ZBX_PROBLEM_UPDATE_MESSAGE;
			$data['message'] = $this->message;
		}

		if ($this->suppress && $eventid_groups['suppressible']) {
			if (!$data['eventids']) {
				$data['eventids'] = $eventid_groups['suppressible'];
			}

			$data['action'] |= ZBX_PROBLEM_UPDATE_SUPPRESS;
			$data['suppress_until'] = $this->suppress_until;
			$eventid_groups['suppressible'] = array_diff($eventid_groups['suppressible'], $data['eventids']);
		}

		if ($this->unsuppress && $eventid_groups['unsuppressible']) {
			if (!$data['eventids']) {
				$data['eventids'] = $eventid_groups['unsuppressible'];
			}

			$data['action'] |= ZBX_PROBLEM_UPDATE_UNSUPPRESS;
			$eventid_groups['unsuppressible'] = array_diff($eventid_groups['unsuppressible'], $data['eventids']);
		}

		if ($this->rank_change_to_cause) {
			if (!$data['eventids']) {
				$data['eventids'] = $eventid_groups['readable'];
			}

			$data['action'] |= ZBX_PROBLEM_UPDATE_RANK_TO_CAUSE;
		}

		if ($this->rank_change_to_symptom) {
			if (!$data['eventids']) {
				$data['eventids'] = $eventid_groups['readable'];
			}

			$data['action'] |= ZBX_PROBLEM_UPDATE_RANK_TO_SYMPTOM;
			$data['cause_eventid'] = $this->getInput('cause_eventid');
		}

		$eventid_groups['readable'] = array_diff($eventid_groups['readable'], $data['eventids']);
		$data['eventids'] = array_keys(array_flip($data['eventids']));

		return $data;
	}
}
