<?php

require_once __DIR__ . '/../exceptions/class.SearchException.php';

/**
 * Module
 * Superclass of every module. Many default functions are defined in this class.
 */
class Module {
	/**
	 * @var string entryid, which will be registered by the bus object
	 */
	public $entryid;

	/**
	 * @var array list of the results, which is send to the client
	 */
	public $responseData;

	/**
	 * @var array list of all the errors occurred
	 */
	public $errors;

	/**
	 * @var State The state object which refers to the statefile
	 */
	public $sessionState;

	/**
	 * @var array data stored in session for this module
	 */
	public $sessionData;

	/**
	 * @var array list of the properties necessary for the module
	 */
	public $properties;

	/**
	 * @var list of the folder list properties
	 */
	public $list_properties;

	/**
	 * @var bool whether the session state is shared
	 */
	public $_sharedSessionState;

	/**
	 * Constructor.
	 *
	 * @param int   $id   unique id
	 * @param array $data list of all actions
	 */
	public function __construct(public $id, public $data) {
		$this->errors = [];
		$this->responseData = [];
		$this->sessionState = false;
		$this->sessionData = false;

		$this->createNotifiers();

		// Get the store from $data and set it to properties class.
		// It is requires for multi server environment where namespace differs.
		// e.g. 'categories' => -2062020578, 'categories' => -2062610402,
		if (isset($GLOBALS['properties'])) {
			$GLOBALS['properties']->setStore($this->getActionStore($this->getActionData($this->data)));
		}
	}

	/**
	 * Creates the notifiers for this module,
	 * and register them to the Bus.
	 */
	public function createNotifiers() {}

	/**
	 * Executes all the actions in the $data variable.
	 */
	public function execute() {
		// you must implement this function for each module
	}

	/**
	 * This will call $handleException of updating the MAPIException based in the module data.
	 * When this is done, $sendFeedback will be called to send the message to the client.
	 *
	 * @param object     $e             exception object
	 * @param string     $actionType    the action type, sent by the client
	 * @param MAPIobject $store         store object of the store
	 * @param string     $parententryid parent entryid of the message
	 * @param string     $entryid       entryid of the message/folder
	 * @param array      $action        the action data, sent by the client
	 */
	public function processException(&$e, $actionType = null, $store = null, $parententryid = null, $entryid = null, $action = null) {
		$this->handleException($e, $actionType, $store, $parententryid, $entryid, $action);
		$this->sendFeedback(false, $this->errorDetailsFromException($e));
	}

	/**
	 * Function does customization of MAPIException based on module data.
	 * like, here it will generate display message based on actionType
	 * for particular exception.
	 *
	 * @param object     $e             exception object
	 * @param string     $actionType    the action type, sent by the client
	 * @param MAPIobject $store         store object of the message
	 * @param string     $parententryid parent entryid of the message
	 * @param string     $entryid       entryid of the message/folder
	 * @param array      $action        the action data, sent by the client
	 */
	public function handleException(&$e, $actionType = null, $store = null, $parententryid = null, $entryid = null, $action = null) {
		if (is_null($e->displayMessage)) {
			switch ($actionType) {
				case "save":
					if ($e->getCode() == MAPI_E_NO_ACCESS) {
						$e->setDisplayMessage(_("You have insufficient privileges to save this message."));
					}
					else {
						$e->setDisplayMessage(_("Could not save message."));
					}
					$e->allowToShowDetailsMessage = true;
					break;

				case "delete":
					if ($e->getCode() == MAPI_E_NO_ACCESS) {
						$e->setDisplayMessage(_("You have insufficient privileges to delete this message."));
					}
					else {
						$e->setDisplayMessage(_("Could not delete message."));
					}
					break;

				case "cancelMeetingRequest":
					if ($e->getCode() == MAPI_E_NO_ACCESS) {
						$e->setDisplayMessage(_("You have insufficient privileges to cancel this Meeting Request."));
					}
					else {
						$e->setDisplayMessage(_("Could not cancel Meeting Request."));
					}
					break;

				case "declineMeetingRequest":
					if ($e->getCode() == MAPI_E_NO_ACCESS) {
						$e->setDisplayMessage(_("You have insufficient privileges to decline this Meeting Request."));
					}
					else {
						$e->setDisplayMessage(_("Could not decline Meeting Request."));
					}
					break;

				case "acceptMeetingRequest":
					if ($e->getCode() == MAPI_E_NO_ACCESS) {
						$e->setDisplayMessage(_("You have insufficient privileges to accept this Meeting Request."));
					}
					else {
						$e->setDisplayMessage(_("Could not accept Meeting Request."));
					}
					break;

				case "cancelInvitation":
					if ($e->getCode() == MAPI_E_NO_ACCESS) {
						$e->setDisplayMessage(_("You have insufficient privileges to cancel Meeting Request invitation."));
					}
					else {
						$e->setDisplayMessage(_("Could not cancel Meeting Request invitations."));
					}
					break;

				case "updatesearch":
				case "stopsearch":
				case "search":
					if ($e->getCode() == MAPI_E_NOT_INITIALIZED) {
						$e->setDisplayMessage(_("You can not continue search operation on this folder."));
					}
					else {
						$e->setDisplayMessage(_("Error in search, please try again."));
					}
					break;

				case "expand":
					$e->setDisplayMessage(_("Error in distribution list expansion."));
					break;
			}
			Log::Write(
				LOGLEVEL_ERROR,
				"Module::handleException():" . $actionType . ": " . $e->displayMessage
			);
		}
	}

	/**
	 * Get quota information of user store and check for over qouta restrictions,
	 * if any qouta (softquota/hardquota) limit is exceeded then it will simply
	 * return appropriate message string according to quota type(hardquota/softquota).
	 *
	 * @param mixed $store Store object of the store
	 *
	 * @return string short message according to quota type(hardquota/softquota) or a blank string
	 */
	public function getOverQuotaMessage($store) {
		if ($store === false) {
			$store = $GLOBALS['mapisession']->getDefaultMessageStore();
		}

		$storeProps = mapi_getprops($store, [PR_QUOTA_WARNING_THRESHOLD, PR_QUOTA_SEND_THRESHOLD, PR_QUOTA_RECEIVE_THRESHOLD, PR_MESSAGE_SIZE_EXTENDED]);

		$quotaDetails = [
			'store_size' => round($storeProps[PR_MESSAGE_SIZE_EXTENDED] / 1024),
			'quota_warning' => $storeProps[PR_QUOTA_WARNING_THRESHOLD],
			'quota_soft' => $storeProps[PR_QUOTA_SEND_THRESHOLD],
			'quota_hard' => $storeProps[PR_QUOTA_RECEIVE_THRESHOLD],
		];

		if ($quotaDetails['quota_hard'] !== 0 && $quotaDetails['store_size'] > $quotaDetails['quota_hard']) {
			return _('The message store has exceeded its hard quota limit.') . '<br/>' .
					_('To reduce the amount of data in this message store, select some items that you no longer need, delete them and cleanup your Deleted Items folder.');
		}

		// if hard quota limit doesn't restrict the operation then check for soft qouta limit
		if ($quotaDetails['quota_soft'] !== 0 && $quotaDetails['store_size'] > $quotaDetails['quota_soft']) {
			return _('The message store has exceeded its soft quota limit.') . '<br/> ' .
					_('To reduce the amount of data in this message store, select some items that you no longer need, delete them and cleanup your Deleted Items folder.');
		}

		return '';
	}

	/**
	 * sends a success or error message to client based on parameters passed.
	 *
	 * @param bool  $success              operation completed successfully or not
	 * @param array $data                 the data array that will be send to the client as a response to success/failure
	 * @param bool  $addResponseDataToBus if true then data will be added to bus otherwise data
	 *                                    will be stored in module and later requests can add it to bus
	 */
	public function sendFeedback($success = false, $data = [], $addResponseDataToBus = true) {
		// Send success/error message to client
		$this->addActionData($success === true ? "success" : "error", $data);

		if ($addResponseDataToBus) {
			$GLOBALS["bus"]->addData($this->getResponseData());
		}
	}

	/**
	 * Function will retrieve error details from exception object based on exception type.
	 * it should also send type of exception with the data. so client can know which type
	 * of exception is generated.
	 *
	 * @param object $exception the exception object which is generated
	 *
	 * @return array error data
	 */
	public function errorDetailsFromException($exception) {
		if (!$exception->isHandled) {
			if ($exception instanceof MAPIException) {
				$exception->setHandled();

				return [
					"type" => ERROR_MAPI,
					"info" => [
						"hresult" => $exception->getCode(),
						"title" => $exception->getTitle(),
						"hresult_name" => get_mapi_error_name($exception->getCode()),
						"file" => $exception->getFileLine(),
						"display_message" => $exception->getDisplayMessage(),
						"details_message" => $exception->getDetailsMessage(),
					],
				];
			}
			if ($exception instanceof ZarafaException) {
				$exception->setHandled();

				return [
					"type" => ERROR_ZARAFA,
					"info" => [
						"file" => $exception->getFileLine(),
						"title" => $exception->getTitle(),
						"display_message" => $exception->getDisplayMessage(),
						"original_message" => $exception->getMessage(),
					],
				];
			}
		}

		return [];
	}

	/**
	 * Function which returns an entryid, which is used to register this module. It
	 * searches in the class variable $data for a ParentEntryID or an EntryID.
	 *
	 * @return string an entryid if found, false if entryid not found
	 */
	public function getEntryID() {
		$entryid = false;
		foreach ($this->data as $action) {
			if (isset($action["parent_entryid"]) && !empty($action["parent_entryid"])) {
				$entryid = $action["parent_entryid"];
			}
			elseif (isset($action["entryid"]) && !empty($action["entryid"])) {
				$entryid = $action["entryid"];
			}
		}

		return $entryid;
	}

	/**
	 * Returns all the errors, which occurred.
	 *
	 * @return array an array of all the errors, which occurred
	 */
	public function getErrors() {
		return $this->errors;
	}

	/**
	 * Returns the response data.
	 *
	 * @return array An array of the response data. This data is send to the client.
	 */
	public function getData() {
		return $this->responseData;
	}

	/**
	 * Sets the action data, which will be executed.
	 *
	 * @param array $data array of all the actions
	 */
	public function setData($data) {
		$this->data = $data;
	}

	/**
	 * Function which returns MAPI Message Store Object. It
	 * searches in the variable $action for a storeid.
	 *
	 * @param array $action the XML data retrieved from the client
	 *
	 * @return object MAPI Message Store Object, false if storeid is not found in the $action variable
	 */
	public function getActionStore($action) {
		$store = false;

		try {
			if (isset($action["store_entryid"]) && !empty($action["store_entryid"])) {
				if (is_array($action["store_entryid"])) {
					$store = [];
					foreach ($action["store_entryid"] as $store_id) {
						array_push($store, $GLOBALS["mapisession"]->openMessageStore(hex2bin((string) $store_id)));
					}
				}
				elseif (ctype_xdigit((string) $action["store_entryid"])) {
					$store = $GLOBALS["mapisession"]->openMessageStore(hex2bin((string) $action["store_entryid"]));
				}
			}
		}
		catch (Exception) {
		}

		return $store;
	}

	/**
	 * Function which returns a parent entryid. It
	 * searches in the variable $action for a parententryid.
	 *
	 * @param array $action the XML data retrieved from the client
	 *
	 * @return object MAPI Message Store Object, false if parententryid is not found in the $action variable
	 */
	public function getActionParentEntryID($action) {
		$parententryid = false;

		if (isset($action["parent_entryid"]) && !empty($action["parent_entryid"])) {
			$parententryid = hex2bin((string) $action["parent_entryid"]);
		}

		return $parententryid;
	}

	/**
	 * Function which returns an entryid. It
	 * searches in the variable $action for an entryid.
	 *
	 * @param array $action the XML data retrieved from the client
	 *
	 * @return mixed MAPI Message Store Object, false if entryid is not found in the $action variable
	 */
	public function getActionEntryID($action) {
		$entryid = false;

		if (isset($action["entryid"]) && !empty($action["entryid"])) {
			if (is_array($action["entryid"])) {
				$entryid = [];
				foreach ($action["entryid"] as $action_entryid) {
					array_push($entryid, hex2bin((string) $action_entryid));
				}
			}
			else {
				$entryid = hex2bin((string) $action["entryid"]);
			}
		}

		return $entryid;
	}

	/**
	 * Helper function which used to get the action data from request.
	 *
	 * @param array $data list of all actions
	 *
	 * @return array $action the json data retrieved from the client
	 */
	public function getActionData($data) {
		$actionData = false;
		foreach ($data as $actionType => $action) {
			if (isset($actionType)) {
				$actionData = $action;
			}
		}

		return $actionData;
	}

	/**
	 * Function which adds action data to module, so later it can be retrieved to send.
	 *
	 * @param string $actionType type of action that response data corresponds
	 * @param mixed  $data
	 */
	public function addActionData($actionType, $data) {
		if (!isset($this->responseData[$actionType])) {
			$this->responseData[$actionType] = $data;
		}
	}

	/**
	 * Function which returns response data that will be sent to client. If there isn't any data added
	 * to response data then it will return a blank array.
	 *
	 * @return object response data
	 */
	public function getResponseData() {
		if (!empty($this->responseData)) {
			return
				[
					$this->getModuleName() => [
						$this->id => $this->responseData,
					],
				];
		}

		return [];
	}

	/**
	 * Function which returns name of the module class.
	 *
	 * @return string module name
	 */
	public function getModuleName() {
		return strtolower(static::class);
	}

	/**
	 * Function which will handle unknown action type for all modules.
	 *
	 * @param string $actionType action type
	 */
	public function handleUnknownActionType($actionType) {
		$this->sendFeedback(
			false,
			[
				"type" => ERROR_ZARAFA,
				"info" => [
					"display_message" => _("Could not process request data properly."),
					"original_message" => sprintf(_("Unknown action type specified - %s"), $actionType),
				],
			]
		);
		Log::Write(
			LOGLEVEL_ERROR,
			"Module::handleUnknownActionType(): ERROR_ZARAFA : " . _("Could not process request data properly.")
		);
	}

	/**
	 * Loads sessiondata of the module from state file on disk.
	 *
	 * @param State $sharedState optional shared State object so that
	 *                           multiple modules can reuse a single lock
	 *                           and avoid repeated open/serialize/close cycles.
	 */
	public function loadSessionData($sharedState = null) {
		if ($sharedState) {
			$this->sessionState = $sharedState;
			$this->_sharedSessionState = true;
		}
		else {
			$this->sessionState = new State('module_sessiondata');
			$this->sessionState->open();
			$this->_sharedSessionState = false;
		}
		$this->sessionData = $this->sessionState->read($this->getModuleName());
	}

	/**
	 * Saves sessiondata of the module to the state file on disk.
	 */
	public function saveSessionData() {
		if ($this->sessionData !== false) {
			// When using a shared state, defer flush to the caller.
			$this->sessionState->write($this->getModuleName(), $this->sessionData, false);
		}
		if (empty($this->_sharedSessionState)) {
			$this->sessionState->flush();
			$this->sessionState->close();
		}
	}
}
