<?php
# MantisBT - A PHP based bugtracking system

# MantisBT is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# MantisBT 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with MantisBT.  If not, see <http://www.gnu.org/licenses/>.

/**
 * Session API
 *
 * Handles user/browser sessions in an extendable manner. New session handlers
 * can be added and configured without affecting how the API is used. Calls to
 * session_*() are appropriately directed at the session handler class as
 * chosen in config_inc.php.
 *
 * @package CoreAPI
 * @subpackage SessionAPI
 * @copyright Copyright 2000 - 2002  Kenzaburo Ito - kenito@300baud.org
 * @copyright Copyright 2002  MantisBT Team - mantisbt-dev@lists.sourceforge.net
 * @link http://www.mantisbt.org
 *
 * @uses config_api.php
 * @uses constant_inc.php
 * @uses error_api.php
 * @uses gpc_api.php
 * @uses php_api.php
 *
 * @noinspection PhpUnused
 */

require_api( 'config_api.php' );
require_api( 'constant_inc.php' );
require_api( 'error_api.php' );
require_api( 'gpc_api.php' );
require_api( 'php_api.php' );

$g_session = null;

/**
 * Abstract interface for a MantisBT session handler.
 */
abstract class MantisSession {
	/**
	 * Session ID
	 */
	protected $id;

	/**
	 * Constructor
	 */
	abstract function __construct();

	/**
	 * get session data
	 * @param string $p_name    The name of the value to set.
	 * @param mixed  $p_default The value to set.
	 * @return string
	 */
	abstract function get( $p_name, $p_default = null );

	/**
	 * set session data
	 * @param string $p_name  The name of the value to set.
	 * @param mixed  $p_value The value to set.
	 * @return void
	 */
	abstract function set( $p_name, $p_value );

	/**
	 * delete session data
	 * @param string $p_name The name of the value to set.
	 * @return void
	 */
	abstract function delete( $p_name );

	/**
	 * destroy session
	 * @return void
	 */
	abstract function destroy();
}

/**
 * MantisPHPSession Class.
 *
 * Implementation of the abstract MantisBT session interface using
 * standard PHP sessions stored on the server's filesystem according
 * to PHP's session.* settings in 'php.ini'.
 */
class MantisPHPSession extends MantisSession {
	/**
	 * Session key prefix.
	 * Used with {@see $g_crypto_master_salt} to build a unique Session key.
	 */
	const SESSION_KEY_PREFIX = 'session_key_v_2';

	/**
	 * @var string Session key
	 */
	protected $key;

	/**
	 * Constructor
	 * @param integer $p_session_id The session id.
	 */
	function __construct( $p_session_id = null ) {
		global $g_cookie_secure_flag_enabled;

		$this->key = hash( 'whirlpool',
			$this::SESSION_KEY_PREFIX . config_get_global( 'crypto_master_salt' )
		);

		# Save session information where specified or with PHP's default
		$t_session_save_path = config_get_global( 'session_save_path' );
		if( $t_session_save_path ) {
			session_save_path( $t_session_save_path );
		}

		# Handle session cookie and caching
		session_cache_limiter( 'private_no_expire' );
		$t_path = config_get_global( 'cookie_path' );
		$t_domain = config_get_global( 'cookie_domain' );
		$t_samesite = config_get_global( 'cookie_samesite' );
		$t_options = array(
			'lifetime' => 0,
			'path' => $t_path,
			'domain' => $t_domain,
			'samesite' => $t_samesite,
			'secure' => $g_cookie_secure_flag_enabled,
			'httponly' => true,
		);
		session_set_cookie_params( $t_options );

		# Handle existent session ID
		if( !is_null( $p_session_id ) ) {
			session_id( $p_session_id );
		}

		# Initialize the session
		session_start();
		$this->id = session_id();

		# Initialize the keyed session store
		if( !isset( $_SESSION[$this->key] ) ) {
			$_SESSION[$this->key] = array();
		}
	}

	/**
	 * get session data
	 *
	 * @param string $p_name    The name of the value to set.
	 * @param mixed  $p_default The value to set.
	 *
	 * @return string
	 * @noinspection PhpInconsistentReturnPointsInspection
	 */
	function get( $p_name, $p_default = null ) {
		if( isset( $_SESSION[$this->key][$p_name] ) ) {
			return $_SESSION[$this->key][$p_name];
		}

		if( func_num_args() > 1 ) {
			return $p_default;
		}

		error_parameters( $p_name );
		trigger_error( ERROR_SESSION_VAR_NOT_FOUND, ERROR );
	}

	/**
	 * set session data
	 * @param string $p_name  The name of the value to set.
	 * @param mixed  $p_value The value to set.
	 * @return void
	 */
	function set( $p_name, $p_value ) {
		$_SESSION[$this->key][$p_name] = $p_value;
	}

	/**
	 * delete session data
	 * @param string $p_name The name of the value to set.
	 * @return void
	 */
	function delete( $p_name ) {
		unset( $_SESSION[$this->key][$p_name] );
	}

	/**
	 * destroy session
	 * @return void
	 */
	function destroy() {
		if( isset( $_COOKIE[session_name()] ) && !headers_sent() ) {
			gpc_set_cookie( session_name(), '', time() - 42000 );
		}

		unset( $_SESSION[$this->key] );
	}
}

/**
 * Initialize the appropriate session handler.
 * @param string $p_session_id Session ID.
 * @return void
 */
function session_init( $p_session_id = null ) {
	global $g_session;

	$g_session = new MantisPHPSession( $p_session_id );

	if( ON == config_get_global( 'session_validation' ) && session_get( 'secure_session', false ) ) {
		session_validate( $g_session );
	}
}

/**
 * Validate the legitimacy of a session.
 * Checks may include last-known IP address, or more.
 * Triggers an error when the session is invalid.
 * @param object $p_session Session object.
 * @return void
 */
function session_validate( $p_session ) {
	$t_user_ip = '';
	if( isset( $_SERVER['REMOTE_ADDR'] ) ) {
		$t_user_ip = trim( $_SERVER['REMOTE_ADDR'] );
	}

	if( is_null( $t_last_ip = $p_session->get( 'last_ip', null ) ) ) {
		# First session usage
		$p_session->set( 'last_ip', $t_user_ip );

	} else {
		# Check a continued session request
		if( $t_user_ip != $t_last_ip ) {
			session_clean();

			trigger_error( ERROR_SESSION_NOT_VALID, WARNING );

			$t_url = config_get_global( 'path' ) . config_get_global( 'default_home_page' );
			echo "\t", '<meta http-equiv="Refresh" content="4; URL=' . $t_url . '" />', "\n";

			die();
		}
	}
}

/**
 * Get arbitrary data from the session.
 * @param string $p_name    Session variable name.
 * @param mixed  $p_default Default value.
 * @return mixed Session variable
 */
function session_get( $p_name, $p_default = null ) {
	global $g_session;

	$t_args = func_get_args();
	return call_user_func_array( array( $g_session, 'get' ), $t_args );
}

/**
 * Get an integer from the session.
 * @param string       $p_name    Session variable name.
 * @param integer|null $p_default Default value.
 * @return integer Session variable
 */
function session_get_int( $p_name, $p_default = null ) {
	$t_args = func_get_args();
	return (int)call_user_func_array( 'session_get', $t_args );
}

/**
 * Get a boolean from the session.
 * @param string       $p_name    Session variable name.
 * @param boolean|null $p_default Default value.
 * @return boolean Session variable
 */
function session_get_bool( $p_name, $p_default = null ) {
	$t_args = func_get_args();
	return (bool)call_user_func_array( 'session_get', $t_args );
}

/**
 * Get a string from the session.
 * @param string      $p_name    Session variable name.
 * @param string|null $p_default Default value.
 * @return string Session variable
 */
function session_get_string( $p_name, $p_default = null ) {
	$t_args = func_get_args();
	return (string)call_user_func_array( 'session_get', $t_args );
}

/**
 * Set a session variable.
 * @param string $p_name  Session variable name.
 * @param mixed  $p_value Variable value.
 * @return void
 */
function session_set( $p_name, $p_value ) {
	global $g_session;
	$g_session->set( $p_name, $p_value );
}

/**
 * Delete a session variable.
 * @param string $p_name Session variable name.
 * @return void
 */
function session_delete( $p_name ) {
	global $g_session;
	$g_session->delete( $p_name );
}

/**
 * Destroy the session entirely.
 * @return void
 */
function session_clean() {
	global $g_session;
	$g_session->destroy();
}

# Initialize the session
if( PHP_CGI == php_mode() ) {
	$t_session_id = gpc_get_string( 'session_id', '' );

	if( empty( $t_session_id ) ) {
		session_init();
	} else {
		session_init( $t_session_id );
	}
}
