<?php

/**
* @version	$Id: session_storage.php 15789 2013-06-06 08:40:39Z alex $
* @package	In-Portal
* @copyright	Copyright (C) 1997 - 2009 Intechnic. All rights reserved.
* @license      GNU/GPL
* In-Portal is Open Source software.
* This means that this software may have been modified pursuant
* the GNU General Public License, and as distributed it includes
* or is derivative of works licensed under the GNU General Public License
* or other free or open source software licenses.
* See http://www.in-portal.org/license for copyright notices and details.
*/

defined('FULL_PATH') or die('restricted access!');

/**
 * Implements Session Store in the Database
 *
 */
class SessionStorage extends kDBBase {

	/**
	 * Reference to session
	 *
	 * @var Session
	 * @access protected
	 */
	protected $Session = null;

	var $Expiration;
	var $SessionTimeout = 0;

	var $DirectVars = Array ();
	var $ChangedDirectVars = Array ();

	var $PersistentVars = Array ();

	var $OriginalData = Array ();

	var $TimestampField;
	var $SessionDataTable;
	var $DataValueField;
	var $DataVarField;

	public function Init($prefix, $special)
	{
		parent::Init($prefix, $special);

		$this->TableName = 'sessions';
		$this->IDField = 'sid';
		$this->TimestampField = 'expire';
		$this->SessionDataTable = 'SessionData';
		$this->DataValueField = 'value';
		$this->DataVarField = 'var';
	}

	/**
	 * Sets reference to session
	 *
	 * @param Session $session
	 */
	public function setSession(&$session)
	{
		$this->Session =& $session;
		$this->SessionTimeout = $session->SessionTimeout;
	}

	/**
	 * Calculates browser signature
	 *
	 * @return string
	 */
	function _getBrowserSignature()
	{
		$signature_parts = Array(
			'HTTP_USER_AGENT', 'SERVER_PROTOCOL',
			'HTTP_ACCEPT_CHARSET', 'HTTP_ACCEPT_ENCODING', 'HTTP_ACCEPT_LANGUAGE'
		);

		$ret = '';

		foreach ($signature_parts as $signature_part) {
			if (array_key_exists($signature_part, $_SERVER)) {
				$ret .= '&|&' . $_SERVER[$signature_part];
			}
		}

		return md5( substr($ret, 3) );
	}

	function GetSessionDefaults()
	{
		$fields_hash = Array (
			$this->IDField			=>	$this->Session->SID,
			$this->TimestampField	=>	$this->Session->Expiration,
		);

		if (!defined('IS_INSTALL') || !IS_INSTALL) {
			// this column was added only in 5.0.1 version,
			// so accessing it while database is not upgraded
			// will result in admin's inability to login inside
			// installator
			$fields_hash['BrowserSignature'] = $this->_getBrowserSignature();
		}

		// default values + values set during this script run

		return array_merge($fields_hash, $this->DirectVars);
	}

	/**
	 * Stores session to database
	 *
	 * @param bool $to_database
	 *
	 * @return void
	 * @access public
	 */
	public function StoreSession($to_database = true)
	{
		if ( defined('IS_INSTALL') && IS_INSTALL && $to_database && !$this->Application->TableFound($this->TableName, true) ) {
			return;
		}

		$fields_hash = $this->GetSessionDefaults();

		if ( $to_database ) {
			$this->Conn->doInsert($fields_hash, $this->TableName);
		}

		foreach ($fields_hash as $field_name => $field_value) {
			$this->SetField($field_name, $field_value);
		}

		// ensure user groups are stored in a way, that kPermissionsHelper::CheckUserPermission can understand
		$this->Session->StoreVar('UserGroups', $this->GetField('GroupList'), !$to_database);
	}

	function DeleteSession()
	{
		$this->DeleteSessions( Array ($this->Session->SID), SESSION_LOG_LOGGED_OUT );

		$this->DirectVars = $this->ChangedDirectVars = $this->OriginalData = Array();
	}

	function UpdateSession($timeout = 0)
	{
		$this->SetField($this->TimestampField, $this->Session->Expiration);
		$query = ' UPDATE '.$this->TableName.' SET '.$this->TimestampField.' = '.$this->Session->Expiration.' WHERE '.$this->IDField.' = '.$this->Conn->qstr($this->Session->SID);
		$this->Conn->Query($query);
	}

	function LocateSession($sid)
	{
		$sql = 'SELECT *
				FROM ' . $this->TableName . '
				WHERE ' . $this->IDField . ' = ' . $this->Conn->qstr($sid);
		$result = $this->Conn->GetRow($sql);

		if ($result === false) {
			return false;
		}

		// perform security checks to ensure, that session is used by it's creator
		if ($this->Application->ConfigValue('SessionBrowserSignatureCheck') && ($result['BrowserSignature'] != $this->_getBrowserSignature()) && $this->Application->GetVar('flashsid') === false) {
			return false;
		}

		if ($this->Application->ConfigValue('SessionIPAddressCheck') && ($result['IpAddress'] != $this->Application->getClientIp())) {
			// most secure, except for cases where NAT (Network Address Translation)
			// is used and two or more computers can have same IP address
			return false;
		}

		$this->DirectVars = $result;
		$this->Expiration = $result[$this->TimestampField];

		return true;
	}

	function GetExpiration()
	{
		return $this->Expiration;
	}

	function LoadData()
	{
		$query = 'SELECT '.$this->DataValueField.','.$this->DataVarField.' FROM '.$this->SessionDataTable.' WHERE '.$this->IDField.' = '.$this->Conn->qstr($this->Session->SID);

		$this->OriginalData = $this->Conn->GetCol($query, $this->DataVarField);
		return $this->OriginalData;
	}

	/**
	 * Enter description here...
	 *
	 * @param string $var_name
	 * @param mixed $default
	 * @return mixed
	 */
	function GetField($var_name, $default = false)
	{
		return isset($this->DirectVars[$var_name]) ? $this->DirectVars[$var_name] : $default;
		//return $this->Conn->GetOne('SELECT '.$var_name.' FROM '.$this->TableName.' WHERE `'.$this->IDField.'` = '.$this->Conn->qstr($this->Session->GetID()) );
	}

	function SetField($var_name, $value)
	{
		$value_changed = !isset($this->DirectVars[$var_name]) || ($this->DirectVars[$var_name] != $value);
		if ($value_changed) {
			$this->DirectVars[$var_name] = $value;
			$this->ChangedDirectVars[] = $var_name;
			$this->ChangedDirectVars = array_unique($this->ChangedDirectVars);
		}
		//return $this->Conn->Query('UPDATE '.$this->TableName.' SET '.$var_name.' = '.$this->Conn->qstr($value).' WHERE '.$this->IDField.' = '.$this->Conn->qstr($this->Session->GetID()) );
	}

	/**
	 * Saves changes in session to database using single REPLACE query
	 *
	 * @return void
	 * @access public
	 */
	public function SaveData()
	{
		if ( !$this->Session->SID ) {
			// can't save without sid
			return ;
		}

		$replace = '';
		$ses_data = $this->Session->Data->GetParams();

		foreach ($ses_data as $key => $value) {
			if ( isset($this->OriginalData[$key]) && $this->OriginalData[$key] == $value ) {
				continue; //skip unchanged session data
			}
			else {
				$replace .= sprintf("(%s, %s, %s),", $this->Conn->qstr($this->Session->SID), $this->Conn->qstr($key), $this->Conn->qstr($value));
			}
		}

		$replace = rtrim($replace, ',');

		if ( $replace != '' ) {
			$query = ' REPLACE INTO ' . $this->SessionDataTable . ' (' . $this->IDField . ', ' . $this->DataVarField . ', ' . $this->DataValueField . ') VALUES ' . $replace;
			$this->Conn->Query($query);
		}

		if ( $this->ChangedDirectVars ) {
			$changes = Array ();

			foreach ($this->ChangedDirectVars as $var) {
				$changes[] = $var . ' = ' . $this->Conn->qstr($this->DirectVars[$var]);
			}

			$query = '	UPDATE ' . $this->TableName . '
						SET ' . implode(',', $changes) . '
						WHERE ' . $this->IDField . ' = ' . $this->Conn->qstr($this->Session->GetID());
			$this->Conn->Query($query);
		}
	}

	function RemoveFromData($var)
	{
		if ($this->Session->SessionSet) {
			// only, when session is stored in database
			$sql = 'DELETE FROM ' . $this->SessionDataTable . '
					WHERE ' . $this->IDField . ' = ' . $this->Conn->qstr($this->Session->SID) . ' AND ' . $this->DataVarField . ' = ' . $this->Conn->qstr($var);
			$this->Conn->Query($sql);
		}

		unset($this->OriginalData[$var]);
	}

	function GetFromData($var, $default = false)
	{
		return array_key_exists($var, $this->OriginalData) ? $this->OriginalData[$var] : $default;
	}

	function GetExpiredSIDs()
	{
		$sql = 'SELECT ' . $this->IDField . '
				FROM ' . $this->TableName . '
				WHERE ' . $this->TimestampField . ' > ' . adodb_mktime();

		return $this->Conn->GetCol($sql);
	}

	function DeleteExpired()
	{
		$expired_sids = $this->GetExpiredSIDs();

		$this->DeleteSessions($expired_sids);

		return $expired_sids;
	}

	function DeleteSessions($session_ids, $delete_reason = SESSION_LOG_EXPIRED)
	{
		if (!$session_ids) {
			return ;
		}

		$log_table = $this->Application->getUnitOption('session-log', 'TableName');

		if ($log_table) {
			// mark session with proper status
			$sub_sql = 'SELECT ' . $this->TimestampField . ' - ' . $this->SessionTimeout . '
						FROM ' . $this->TableName . '
						WHERE ' . $this->IDField . ' = ' . $log_table . '.SessionId';

			$sql = 'UPDATE ' . $log_table . '
					SET Status = ' . $delete_reason . ', SessionEnd = (' . $sub_sql . ')
					WHERE Status = ' . SESSION_LOG_ACTIVE . ' AND SessionId IN (' . implode(',', $session_ids) . ')';
 			$this->Conn->Query($sql);
		}

		$where_clause = ' WHERE ' . $this->IDField . ' IN (' . implode(',', $session_ids) . ')';
		$sql = 'DELETE FROM ' . $this->SessionDataTable . $where_clause;
		$this->Conn->Query($sql);

		$sql = 'DELETE FROM ' . $this->TableName . $where_clause;
		$this->Conn->Query($sql);

		// delete debugger ouputs left of deleted sessions
		foreach ($session_ids as $session_id) {
			$debug_file = (defined('RESTRICTED') ? RESTRICTED : WRITEABLE . '/cache') . '/debug_@' . $session_id . '@.txt';
			if (file_exists($debug_file)) {
				@unlink($debug_file);
 			}
 		}
 	}

	function LoadPersistentVars()
	{
		$user_id = $this->Session->RecallVar('user_id');
		if ($user_id != USER_GUEST) {
			// root & normal users
			$sql = 'SELECT VariableValue, VariableName
					FROM '.TABLE_PREFIX.'UserPersistentSessionData
					WHERE PortalUserId = '.$user_id;
			$this->PersistentVars = (array)$this->Conn->GetCol($sql, 'VariableName');
		}
		else {
			$this->PersistentVars = Array ();
		}
	}

	/**
	 * Stores variable to persistent session
	 *
	 * @param string $var_name
	 * @param mixed $var_value
	 * @param bool $optional
	 * @return void
	 * @access public
	 */
	public function StorePersistentVar($var_name, $var_value, $optional = false)
	{
		$user_id = $this->Session->RecallVar('user_id');
		if ( $user_id == USER_GUEST || $user_id === false ) {
			// -2 (when not logged in), false (when after u:OnLogout event)
			$this->Session->StoreVar($var_name, $var_value, $optional);
			return;
		}

		$this->PersistentVars[$var_name] = $var_value;

		$key_clause = 'PortalUserId = ' . $user_id . ' AND VariableName = ' . $this->Conn->qstr($var_name);

		$sql = 'SELECT VariableName
				FROM ' . TABLE_PREFIX . 'UserPersistentSessionData
				WHERE ' . $key_clause;
		$record_found = $this->Conn->GetOne($sql);

		$fields_hash = Array (
			'PortalUserId' => $user_id,
			'VariableName' => $var_name,
			'VariableValue' => $var_value,
		);

		if ( $record_found ) {
			$this->Conn->doUpdate($fields_hash, TABLE_PREFIX . 'UserPersistentSessionData', $key_clause);
		}
		else {
			$this->Conn->doInsert($fields_hash, TABLE_PREFIX . 'UserPersistentSessionData');
		}
	}

	/**
	 * Gets persistent variable
	 *
	 * @param string $var_name
	 * @param mixed $default
	 * @return mixed
	 * @access public
	 */
	public function RecallPersistentVar($var_name, $default = false)
	{
		if ( $this->Session->RecallVar('user_id') == USER_GUEST ) {
			if ( $default == ALLOW_DEFAULT_SETTINGS ) {
				$default = null;
			}

			return $this->Session->RecallVar($var_name, $default);
		}

		if ( array_key_exists($var_name, $this->PersistentVars) ) {
			return $this->PersistentVars[$var_name];
		}
		elseif ( $default == ALLOW_DEFAULT_SETTINGS ) {
			$default_user_id = $this->Application->ConfigValue('DefaultSettingsUserId');

			if ( !$default_user_id ) {
				$default_user_id = USER_ROOT;
			}

			$sql = 'SELECT VariableValue, VariableName
					FROM ' . TABLE_PREFIX . 'UserPersistentSessionData
					WHERE VariableName = ' . $this->Conn->qstr($var_name) . ' AND PortalUserId = ' . $default_user_id;
			$value = $this->Conn->GetOne($sql);
			$this->PersistentVars[$var_name] = $value;

			if ( $value !== false ) {
				$this->StorePersistentVar($var_name, $value); //storing it, so next time we don't load default user setting
			}

			return $value;
		}

		return $default;
	}

	/**
	 * Removes variable from persistent session
	 *
	 * @param string $var_name
	 * @return void
	 * @access public
	 */
	function RemovePersistentVar($var_name)
	{
		unset($this->PersistentVars[$var_name]);

		$user_id = $this->Session->RecallVar('user_id');

		if ( $user_id == USER_GUEST || $user_id === false ) {
			// -2 (when not logged in), false (when after u:OnLogout event)
			$this->Session->RemoveVar($var_name);
		}
		else {
			$sql = 'DELETE FROM ' . TABLE_PREFIX . 'UserPersistentSessionData
					WHERE PortalUserId = ' . $user_id . ' AND VariableName = ' . $this->Conn->qstr($var_name);
			$this->Conn->Query($sql);
		}
	}

	/**
	 * Checks of object has given field
	 *
	 * @param string $name
	 *
	 * @return bool
	 * @access public
	 * @throws BadMethodCallException Always.
	 */
	public function HasField($name)
	{
		throw new BadMethodCallException('Unsupported');
	}

	/**
	 * Returns field values
	 *
	 * @return Array
	 * @access public
	 * @throws BadMethodCallException Always.
	 */
	public function GetFieldValues()
	{
		throw new BadMethodCallException('Unsupported');
	}

	/**
	 * Returns unformatted field value
	 *
	 * @param string $field
	 *
	 * @return string
	 * @access public
	 * @throws BadMethodCallException Always.
	 */
	public function GetDBField($field)
	{
		throw new BadMethodCallException('Unsupported');
	}

	/**
	 * Returns true, when list/item was queried/loaded
	 *
	 * @return bool
	 * @access public
	 * @throws BadMethodCallException Always.
	 */
	public function isLoaded()
	{
		throw new BadMethodCallException('Unsupported');
	}

	/**
	 * Returns specified field value from all selected rows.
	 * Don't affect current record index
	 *
	 * @param string $field
	 *
	 * @return Array
	 * @access public
	 * @throws BadMethodCallException Always.
	 */
	public function GetCol($field)
	{
		throw new BadMethodCallException('Unsupported');
	}

}