<?php

// Includes. Will probably already be included.
require_once BASE_PATH . 'config.php';
require_once BASE_PATH . 'defaults.php';
require_once BASE_PATH . 'server/includes/util.php';
require_once BASE_PATH . 'server/includes/exceptions/class.JSONException.php';

/**
 * A class with helper functions for the PHP session.
 *
 * @singleton
 */
class WebAppSession {
	// Holds the instance of this class
	// Use public static method getInstance() to retrieve its value
	private static $instance;

	// Set to true when the the session has timed out
	// Use public method hasTimedOut() to retrieve its value
	private $timeout = false;

	/**
	 * Constructor.
	 */
	private function __construct() {
		$secure = useSecureCookies();
		if (defined('COOKIE_NAME')) {
			// Create a named session (otherwise use the PHP default, normally PHPSESSID)
			if ($secure) {
				// Create a named session and set secure cookies params.
				session_name('__Secure-' . COOKIE_NAME);
			}
			else {
				session_name(COOKIE_NAME);
			}
		}
		// Keycloak/OAuth requires SameSite=None so the session cookie
		// survives the cross-site redirect back from the identity provider.
		// Firefox strictly enforces SameSite=Lax on cross-site redirects
		// and would otherwise drop the session cookie, causing a login loop.
		$keycloakEnabled = !defined('DISABLE_KEYCLOAK') || !DISABLE_KEYCLOAK;
		$samesite = $keycloakEnabled && $secure ? 'None' : 'Lax';

		session_set_cookie_params([
			'lifetime' => (int) ini_get('session.cookie_lifetime'),
			'path'     => ini_get('session.cookie_path') ?: '/',
			'domain'   => ini_get('session.cookie_domain') ?: '',
			'secure'   => $secure,
			'httponly'  => true,
			'samesite' => $samesite,
		]);

		$this->start();

		// For grommunio.php, release the session file lock early
		// so parallel requests from the same browser session are
		// not serialized behind it.  Service requests (e.g.
		// fingerprint) still need a writable session, so skip
		// the early close for those — grommunio.php line 50
		// closes the session for services after they finish.
		$isGrommunioPhp = basename((string) $_SERVER['PHP_SELF']) === 'grommunio.php';
		if (!$isGrommunioPhp) {
			// We will only check for timeout in the grommunio.php page
			$this->setStartTime();
		}
		else {
			$this->checkForTimeout();
			if (!isset($_GET['service'])) {
				session_write_close();
			}
		}
	}

	/**
	 * returns the instance of this class. Creates one if it doesn't exist yet.
	 * To force this class to be used as singleton, the constructor is private
	 * and the single instance can be created/retrieved with this static method
	 * e.g.: $session = WebAppSession::createInstance().
	 *
	 * @return WebAppSession the only available instance of this class
	 */
	public static function getInstance() {
		if (!isset(WebAppSession::$instance)) {
			WebAppSession::$instance = new WebAppSession();
		}

		return WebAppSession::$instance;
	}

	/**
	 * The method getInstance() was first called createInstance(), so
	 * for backward compatibility we also define createInstance as an alias.
	 */
	public static function createInstance() {
		return WebAppSession::getInstance();
	}

	/**
	 * Starts the session.
	 */
	public function start() {
		session_start();
	}

	/**
	 * Closes the session.
	 */
	public function close() {
		session_write_close();
	}

	/**
	 * Destroy the session.
	 */
	public function destroy() {
		// Make sure the session is open
		if (session_status() !== PHP_SESSION_ACTIVE) {
			session_start();
		}

		// Destroy the session cookie
		if (isset($_COOKIE[session_name()])) {
			setcookie(session_name(), '', ['expires' => time() - 42000, 'path' => '/']);
		}

		// Destroy the session
		$_SESSION = [];
		session_destroy();
	}

	/**
	 * Sets the start time in the session to current timestamp.
	 * This is used to logout the user when CLIENT_TIMEOUT has
	 * been set in config.php.
	 */
	public function setStartTime() {
		$_SESSION['starttime'] = time();
	}

	/**
	 * gets the start time from the session. This is used to logout the user
	 * when CLIENT_TIMEOUT has been set in config.php.
	 *
	 * @return false|int The starttime (timestamp) when set, false otherwise
	 */
	public function getStartTime() {
		return $_SESSION['starttime'] ?? false;
	}

	/**
	 * Checks if the session should timeout and destroys the session if it should.
	 */
	private function checkForTimeout() {
		if (!defined('CLIENT_TIMEOUT') || !CLIENT_TIMEOUT) {
			// Timeout was not set in configuration, so do nothing
			return;
		}

		if (array_key_exists('HTTP_CONTENT_TYPE', $_SERVER) && !str_contains((string) $_SERVER['HTTP_CONTENT_TYPE'], 'application/json')) {
			return;
		}

		$starttime = $this->getStartTime();
		// let's add 5 seconds to the CLIENT_TIMEOUT to handle possible latency
		if ($starttime && (time() - $starttime > CLIENT_TIMEOUT + 5)) {
			$this->destroy();
			$this->timeout = true;
		}
		else {
			try {
				// decode JSON data
				$requestJsonString = readData();
				$requestJson = json_decode_data($requestJsonString, true);
			}
			catch (JsonException $e) {
				// Invalid json sent with the request
				// Log the error and do nothing is best option
				dump($e->getMessage());

				return;
			}

			$isReminderlistRequest = $this->isReminderListRequest($requestJson);
			$isDestroySessionRequest = $this->isDestroySessionRequest($requestJson);

			if (!$isReminderlistRequest && !$isDestroySessionRequest) {
				$this->setStartTime();
			}
			elseif ($isDestroySessionRequest) {
				// sessiondestroy is sent because of timeout at client side
				$this->timeout = true;
				$this->destroy();
			}
		}
	}

	/**
	 * Checks if the current request is a destroysession request.
	 *
	 * @param array $requestJson The JSON that was sent as the current request
	 *
	 * @return bool
	 */
	private function isDestroySessionRequest($requestJson) {
		$isDestroySession = false;

		if (isset($requestJson, $requestJson['zarafa'])) {
			if (isset($requestJson['zarafa']['hierarchymodule'])) {
				foreach ($requestJson['zarafa']['hierarchymodule'] as $requestId => $action) {
					if (isset($action, $action['destroysession'])) {
						$isDestroySession = true;
					}
				}
			}
		}

		return $isDestroySession;
	}

	/**
	 * Checks if the current request is a reminderlist request.
	 *
	 * @param array $requestJson The JSON that was sent as the current request
	 *
	 * @return bool
	 */
	private function isReminderListRequest($requestJson) {
		return isset($requestJson) && isset($requestJson['zarafa'], $requestJson['zarafa']['reminderlistmodule']);
	}

	/**
	 * Returns true if the current session has timed out, false otherwise.
	 *
	 * @return bool
	 */
	public function hasTimedOut() {
		return $this->timeout;
	}
}
