<?php
/**
* @version	$Id: session.php 12898 2009-11-11 19:11:30Z 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!');

/*
The session works the following way:

1. When a visitor loads a page from the site the script checks if cookies_on varibale has been passed to it as a cookie.
2. If it has been passed, the script tries to get Session ID (SID) from the request:
3. Depending on session mode the script is getting SID differently.

The following modes are available:
- AUTO
Automatic mode: if cookies are on at the client side, the script relays only on cookies and
ignore all other methods of passing SID. If cookies are off at the client side, the script relays on SID
passed through query string and referal passed by the client. THIS METHOD IS NOT 100% SECURE, as long as
attacker may get SID and substitude referal to gain access to user' session. One of the faults of this method
is that the session is only created when the visitor clicks the first link on the site, so there
is NO session at the first load of the page. (Actually there is a session, but it gets lost after
the first click because we do not use SID in query string while we are not sure if we need it)

- smCOOKIES_ONLY
Cookies only: in this mode the script relays solely on cookies passed from the browser and ignores
all other methods. In this mode there is no way to use sessions for clients without cookies support
or cookies support disabled. The cookies are stored with the full domain name and path to base-directory
of script installation.

- smGET_ONLY
GET only: the script will not set any cookies and will use only SID passed in query string using GET,
it will also check referal. The script will set SID at the first load of the page

- smCOOKIES_AND_GET
Combined mode: the script will use both cookies and GET right from the start. If client has cookies enabled,
the script will check SID stored in cookie and passed in query string, and will use this SID only if both
cookie and query string matches. However if cookies are disabled on the client side, the script will work
the same way as in GET_ONLY mode.

4. After the script has the SID it tries to load it from the Storage (default is database)

5. If such SID is found in the database, the script checks its expiration time. If session is not expired,
it updates its expiration, and resend the cookie (if applicable to session mode)

6. Then the script loads all the data (session variables) pertaining to the SID.


Usage:

$session = new Session(smAUTO); //smAUTO is default, you could just leave the brackets empty, or provide another mode

$session->SetCookieDomain('my.domain.com');
$session->SetCookiePath('/myscript');
$session->SetCookieName('my_sid_cookie');
$session->SetGETName('sid');
$session->InitSession();
...

//link output:

echo "<a href='index.php?'". ( $session->NeedQueryString() ? 'sid='.$session->SID : '' ) .">My Link</a>";

*/


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

	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;

	function Init($prefix,$special)
	{
		parent::Init($prefix,$special);
		$this->setTableName('sessions');
		$this->setIDField('sid');
		$this->TimestampField = 'expire';
		$this->SessionDataTable = 'SessionData';
		$this->DataValueField = 'value';
		$this->DataVarField = 'var';
	}

	function setSessionTimeout($new_timeout)
	{
		$this->SessionTimeout = $new_timeout;
	}

	/**
	 * 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 StoreSession(&$session, $additional_fields = Array())
	{
		if (defined('IS_INSTALL') && IS_INSTALL && !$this->Application->TableFound($this->TableName)) {
			return false;
		}

		$fields_hash = Array (
			$this->IDField			=>	$session->SID,
			$this->TimestampField	=>	$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 + additional values + values set during this script run
		$additional_fields = array_merge($additional_fields, $this->DirectVars); // used 2 times later
		$fields_hash = array_merge($fields_hash, $additional_fields);

		$this->Conn->doInsert($fields_hash, $this->TableName);

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

	function DeleteSession(&$session)
	{
		$query = ' DELETE FROM '.$this->TableName.' WHERE '.$this->IDField.' = '.$this->Conn->qstr($session->SID);
		$this->Conn->Query($query);

		$query = ' DELETE FROM '.$this->SessionDataTable.' WHERE '.$this->IDField.' = '.$this->Conn->qstr($session->SID);
		$this->Conn->Query($query);

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

	function UpdateSession(&$session, $timeout=0)
	{
		$this->SetField($session, $this->TimestampField, $session->Expiration);
		$query = ' UPDATE '.$this->TableName.' SET '.$this->TimestampField.' = '.$session->Expiration.' WHERE '.$this->IDField.' = '.$this->Conn->qstr($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())) {
			return false;
		}

		if ($this->Application->ConfigValue('SessionIPAddressCheck') && ($result['IpAddress'] != $_SERVER['REMOTE_ADDR'])) {
			// 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(&$session)
	{
		$query = 'SELECT '.$this->DataValueField.','.$this->DataVarField.' FROM '.$this->SessionDataTable.' WHERE '.$this->IDField.' = '.$this->Conn->qstr($session->SID);

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

	/**
	 * Enter description here...
	 *
	 * @param Session $session
	 * @param string $var_name
	 * @param mixed $default
	 */
	function GetField(&$session, $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($session->GetID()) );
	}

	function SetField(&$session, $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($session->GetID()) );
	}

	function SaveData(&$session)
	{
		if(!$session->SID) return false; // can't save without sid

		$ses_data = $session->Data->GetParams();

		$replace = '';
		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($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($session->GetID());
			$this->Conn->Query($query);
		}
	}

	function RemoveFromData(&$session, $var)
	{
		$query = 'DELETE FROM '.$this->SessionDataTable.' WHERE '.$this->IDField.' = '.$this->Conn->qstr($session->SID).
						 ' AND '.$this->DataVarField.' = '.$this->Conn->qstr($var);
		$this->Conn->Query($query);
		unset($this->OriginalData[$var]);
	}

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

	function GetExpiredSIDs()
	{
		$query = ' SELECT '.$this->IDField.' FROM '.$this->TableName.' WHERE '.$this->TimestampField.' > '.adodb_mktime();
		return $this->Conn->GetCol($query);
	}

	function DeleteExpired()
	{
		$expired_sids = $this->GetExpiredSIDs();
		if ($expired_sids) {
			$sessionlog_table = $this->Application->getUnitOption('session-log', 'TableName');
			$session_log_sql =
				'	UPDATE '.$sessionlog_table.'
					SET Status = 2, SessionEnd =
						(	SELECT '.$this->TimestampField.' - '.$this->SessionTimeout.'
							FROM '.$this->TableName.'
							WHERE '.$this->IDField.' = '.$sessionlog_table.'.SessionId
						)
					WHERE Status = 0 AND SessionId IN ('.join(',', $expired_sids).')';
			if ($sessionlog_table) {
				$this->Conn->Query($session_log_sql);
			}

			$where_clause = ' WHERE '.$this->IDField.' IN ("'.implode('","',$expired_sids).'")';
			$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 expired sessions
			foreach ($expired_sids as $expired_sid) {
				$debug_file = WRITEABLE . '/cache/debug_@' . $expired_sid . '@.txt';
				if (file_exists($debug_file)) {
					@unlink($debug_file);
				}
			}
		}
		return $expired_sids;
	}

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

	/**
	 * Stores variable to persistent session
	 *
	 * @param Session $session
	 * @param string $var_name
	 * @param mixed $var_value
	 */
	function StorePersistentVar(&$session, $var_name, $var_value)
	{
		$user_id = $session->RecallVar('user_id');
		if ($user_id == -2 || $user_id === false) {
			// -2 (when not logged in), false (when after u:OnLogout event)
			$session->StoreVar($var_name, $var_value);
			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.'PersistantSessionData
				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.'PersistantSessionData', $key_clause);
		}
		else {
			$this->Conn->doInsert($fields_hash, TABLE_PREFIX.'PersistantSessionData');
		}
	}

	/**
	 * Gets persistent variable
	 *
	 * @param Session $session
	 * @param string $var_name
	 * @param mixed $default
	 * @return mixed
	 */
	function RecallPersistentVar(&$session, $var_name, $default = false)
	{
		if ($session->RecallVar('user_id') == -2) {
			if ($default == ALLOW_DEFAULT_SETTINGS) {
				$default = null;
			}
			return $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 = -1;
			}
			$sql = 'SELECT VariableValue, VariableName
					FROM '.TABLE_PREFIX.'PersistantSessionData
					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($session, $var_name, $value); //storing it, so next time we don't load default user setting
			}
			return $value;
		}
		else {
			return $default;
		}
	}

	function RemovePersistentVar(&$session, $var_name)
	{
		unset($this->PersistentVars[$var_name]);

		$user_id = $session->RecallVar('user_id');

		if ($user_id != -2) {
			$sql = 'DELETE FROM '.TABLE_PREFIX.'PersistantSessionData
					WHERE PortalUserId = '.$user_id.' AND VariableName = '.$this->Conn->qstr($var_name);
			$this->Conn->Query($sql);
		}
	}
}

define('smAUTO', 1);
define('smCOOKIES_ONLY', 2);
define('smGET_ONLY', 3);
define('smCOOKIES_AND_GET', 4);


class Session extends kBase {
	var $Checkers;

	var $Mode;
	var $OriginalMode = null;
	var $GETName = 'sid';

	var $CookiesEnabled = true;
	var $CookieName = 'sid';
	var $CookieDomain;
	var $CookiePath;
	var $CookieSecure = 0;

	var $SessionTimeout = 3600;
	var $Expiration;

	var $SID;

	var $CachedSID;

	var $SessionSet = false;

	/**
	 * Session ID is used from GET
	 *
	 * @var bool
	 */
	var $_fromGet = false;

	/**
	 * Enter description here...
	 *
	 * @var SessionStorage
	 */
	var $Storage;

	var $CachedNeedQueryString = null;

	/**
	 * Session Data array
	 *
	 * @var Params
	 */
	var $Data;

	/**
	 * Names of optional session keys with their optional values (which does not need to be always stored)
	 *
	 * @var Array
	 */
	var $OptionalData = Array ();

	/**
	 * Session expiration mark
	 *
	 * @var bool
	 */
	var $expired = false;

	function Session($mode = smAUTO)
	{
		parent::kBase();

		$this->SetMode($mode);
	}

	function SetMode($mode)
	{
		$this->Mode = $mode;
		$this->CachedNeedQueryString = null;
		$this->CachedSID = null;
	}

	function SetCookiePath($path)
	{
		$this->CookiePath = str_replace(' ', '%20', $path);
	}

	/**
	 * Setting cookie domain. Set false for local domains, because they don't contain dots in their names.
	 *
	 * @param string $domain
	 */
	function SetCookieDomain($domain)
	{
		$this->CookieDomain = substr_count($domain, '.') ? '.'.ltrim($domain, '.') : false;
	}

	function SetGETName($get_name)
	{
		$this->GETName = $get_name;
	}

	function SetCookieName($cookie_name)
	{
		$this->CookieName = $cookie_name;
	}

	function InitStorage($special)
	{
		$this->Storage =& $this->Application->recallObject('SessionStorage.'.$special);
		$this->Storage->setSessionTimeout($this->SessionTimeout);
	}

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

		$this->CheckIfCookiesAreOn();
		if ($this->CookiesEnabled) $_COOKIE['cookies_on'] = 1;

		$this->Checkers = Array();
		$this->InitStorage($special);
		$this->Data = new Params();

		$tmp_sid = $this->GetPassedSIDValue();

		$check = $this->Check();

		if ($this->Application->isAdmin) {
			// 1. Front-End session may not be created (SID is present, but no data in database).
			// Check expiration LATER from kApplication::Init, because template, used in session
			// expiration redirect should be retrieved from mod-rewrite url first.

			// 2. Admin sessions are always created, so case when SID is present,
			// but session in database isn't is 100% session expired. Check expiration
			// HERE because Session::SetSession will create missing session in database
			// and when Session::ValidateExpired will be called later from kApplication::Init
			// it won't consider such session as expired !!!
			$this->ValidateExpired();
		}

		if ($check) {
			$this->SID = $this->GetPassedSIDValue();
			$this->Refresh();
			$this->LoadData();
		}
		else {
			$this->SetSession();
		}

		if (!is_null($this->OriginalMode)) $this->SetMode($this->OriginalMode);
	}

	function ValidateExpired()
	{
		if (defined('IS_INSTALL') && IS_INSTALL) {
			return ;
		}

		$this->DeleteExpired();

		if ($this->expired || ($this->CachedSID && !$this->_fromGet && !$this->SessionSet)) {
			$this->RemoveSessionCookie();
			// true was here to force new session creation, but I (kostja) used
			// RemoveCookie a line above, to avoid redirect loop with expired sid
			// not being removed setSession with true was used before, to set NEW
			// session cookie
			$this->SetSession();

			// case #1: I've OR other site visitor expired my session
			// case #2: I have no session in database, but SID is present
			$this->expired = false;
			$expire_event = new kEvent('u:OnSessionExpire');
			$this->Application->HandleEvent($expire_event);
		}
	}

	/**
	 * This is redirect from https to http or via versa
	 *
	 * @return bool
	 */
	function IsHTTPSRedirect()
	{
		$http_referer = array_key_exists('HTTP_REFERER', $_SERVER) ? $_SERVER['HTTP_REFERER'] : false;

		return (
					( PROTOCOL == 'https://' && preg_match('#http:\/\/#', $http_referer) )
					||
					( PROTOCOL == 'http://' && preg_match('#https:\/\/#', $http_referer) )
				);
	}

	/**
	 * Helper method for detecting cookie availability
	 *
	 * @return bool
	 */
	function _checkCookieReferer()
	{
		// removing /admin for compatability with in-portal (in-link/admin/add_link.php)
		$path = preg_replace('/admin[\/]{0,1}$/', '', $this->CookiePath);
		$reg = '#^'.preg_quote(PROTOCOL.ltrim($this->CookieDomain, '.').$path).'#';

		return preg_match($reg, getArrayValue($_SERVER, 'HTTP_REFERER') );
	}

	function CheckIfCookiesAreOn()
	{
		if ($this->Mode == smGET_ONLY) {
			//we don't need to bother checking if we would not use it
			$this->CookiesEnabled = false;
			return;
		}

		$http_query =& $this->Application->recallObject('HTTPQuery');
		$cookies_on = array_key_exists('cookies_on', $http_query->Cookie); // not good here

		$get_sid = getArrayValue($http_query->Get, $this->GETName);

		if ($this->IsHTTPSRedirect() && $get_sid) { // Redirect from http to https on different domain
			$this->OriginalMode = $this->Mode;
			$this->SetMode(smGET_ONLY);
		}

		if (!$cookies_on || $this->IsHTTPSRedirect()) {
			//If referer is our server, but we don't have our cookies_on, it's definetly off
			$is_install = defined('IS_INSTALL') && IS_INSTALL;
			if (!$is_install && $this->_checkCookieReferer() && !$this->Application->GetVar('admin') && !$this->IsHTTPSRedirect()) {
				$this->CookiesEnabled = false;
			}
			else {
				//Otherwise we still suppose cookies are on, because may be it's the first time user visits the site
				//So we send cookies on to get it next time (when referal will tell us if they are realy off
				$this->SetCookie('cookies_on', 1, adodb_mktime() + 31104000); //one year should be enough
			}
		}
		else {
			$this->CookiesEnabled = true;
		}

		return $this->CookiesEnabled;
	}

	/**
	 * Sets cookie for current site using path and domain
	 *
	 * @param string $name
	 * @param mixed $value
	 * @param int $expires
	 */
	function SetCookie($name, $value, $expires = null)
	{
		if (isset($expires) && $expires < adodb_mktime()) {
			unset($this->Application->HttpQuery->Cookie[$name]);
		}
		else {
			$this->Application->HttpQuery->Cookie[$name] = $value;
		}

		setcookie($name, $value, $expires, $this->CookiePath, $this->CookieDomain, $this->CookieSecure);
	}

	function Check()
	{
		// don't check referer here, because it doesn't provide any security option and can be easily falsified

		$sid = $this->GetPassedSIDValue();

		if (empty($sid)) {
			return false;
		}

		//try to load session by sid, if everything is fine
		$result = $this->LoadSession($sid);

		$this->SessionSet = $result; // fake front-end session will given "false" here

		return $result;
	}

	function LoadSession($sid)
	{
		if( $this->Storage->LocateSession($sid) ) {
			// if we have session with such SID - get its expiration
			$this->Expiration = $this->Storage->GetExpiration();

			// If session has expired
			if ($this->Expiration < adodb_mktime()) {
				// when expired session is loaded, then SID is
				// not assigned, but used in Destroy method
				$this->SID = $sid;
				$this->Destroy();

				$this->expired = true;

				// when Destory methods calls SetSession inside and new session get created
				return $this->SessionSet;
			}

			// Otherwise it's ok
			return true;
		}
		else {
			// fake or deleted due to expiration SID
			if (!$this->_fromGet) {
				$this->expired = true;
			}

			return false;
		}
	}

	function GetPassedSIDValue($use_cache = 1)
	{
		if (!empty($this->CachedSID) && $use_cache) {
			return $this->CachedSID;
		}

		$http_query =& $this->Application->recallObject('HTTPQuery');
		$get_sid = getArrayValue($http_query->Get, $this->GETName);
		$sid_from_get = $get_sid ? true : false;

		if ($this->Application->GetVar('admin') == 1 && $get_sid) {
			$sid = $get_sid;
		}
		else {
			switch ($this->Mode) {
				case smAUTO:
					//Cookies has the priority - we ignore everything else
					$sid = $this->CookiesEnabled ? $this->GetSessionCookie() : $get_sid;

					if ($this->CookiesEnabled) {
						$sid_from_get = false;
					}
					break;

				case smCOOKIES_ONLY:
					$sid = $this->GetSessionCookie();
					break;

				case smGET_ONLY:
					$sid = $get_sid;
					break;

				case smCOOKIES_AND_GET:
					$cookie_sid = $this->GetSessionCookie();
					//both sids should match if cookies are enabled
					if (!$this->CookiesEnabled || ($cookie_sid == $get_sid)) {
						$sid = $get_sid; //we use get here just in case cookies are disabled
					}
					else {
						$sid = '';
						$sid_from_get = false;
					}
					break;
			}
		}

		$this->CachedSID = $sid;
		$this->_fromGet = $sid_from_get;

		return $this->CachedSID;
	}

	/**
	 * Returns session id
	 *
	 * @return int
	 * @access public
	 */
	function GetID()
	{
		return $this->SID;
	}

	/**
	 * Generates new session id
	 *
	 * @return int
	 * @access private
	 */
	function GenerateSID()
	{
		list ($usec, $sec) = explode(' ', microtime());

		$sid_part_1 = substr($usec, 4, 4);
		$sid_part_2 = mt_rand(1, 9);
		$sid_part_3 = substr($sec, 6, 4);
		$digit_one = substr($sid_part_1, 0, 1);

		if ($digit_one == 0) {
			$digit_one = mt_rand(1, 9);
			$sid_part_1 = preg_replace('/^0/', '', $sid_part_1);
			$sid_part_1 = $digit_one . $sid_part_1;
		}

		$this->setSID($sid_part_1 . $sid_part_2 . $sid_part_3);

		return $this->SID;
	}

	/**
	 * Set's new session id
	 *
	 * @param int $new_sid
	 * @access private
	 */
	function setSID($new_sid)
	{
		$this->SID /*= $this->CachedSID*/ = $new_sid; // don't set cached sid here
		$this->Application->SetVar($this->GETName,$new_sid);
	}

	function NeedSession()
	{
		$data = $this->Data->GetParams();

		$data_keys = array_keys($data);
		$optional_keys = array_keys($this->OptionalData);
		$real_keys = array_diff($data_keys, $optional_keys);

		return $real_keys ? true : false;
	}

	function SetSession($force = false)
	{
		if ($this->SessionSet && !$force) {
			return true;
		}

		if (!$force && !($this->Application->isAdmin || $this->Application->GetVar('admin')) && !$this->NeedSession()) {
			// don't create session (in db) on Front-End, when sid is present (GPC), but data in db isn't
			if ($this->_fromGet) {
				// set sid, that was given in GET
				$this->setSID( $this->GetPassedSIDValue() );
			} else {
				// re-generate sid only, when cookies are used
				$this->GenerateSID();
			}
			return false;
		}

		if (!$this->SID || $force) {
			$this->GenerateSID();
		}

		$this->Expiration = adodb_mktime() + $this->SessionTimeout;

		switch ($this->Mode) {
			case smAUTO:
				if ($this->CookiesEnabled) {
					$this->SetSessionCookie();
				}
				break;

			case smGET_ONLY:
				break;

			case smCOOKIES_ONLY:
			case smCOOKIES_AND_GET:
				$this->SetSessionCookie();
				break;
		}

		$this->Storage->StoreSession($this);

		if ($this->Application->isAdmin || $this->Special == 'admin') {
			$this->StoreVar('admin', 1);
		}

		$this->SessionSet = true; // should be called before SaveData, because SaveData will try to SetSession again
		if ($this->Special != '') {
			// front-session called from admin or otherwise, then save it's data
			$this->SaveData();
		}

		$this->Application->resetCounters('UserSession');
		return true;
	}

	/**
	 * Returns SID from cookie.
	 *
	 * Use 2 cookies to have 2 expiration:
	 * - 1. for normal expiration when browser is not closed (30 minutes by default), configurable
	 * - 2. for advanced expiration when browser is closed
	 *
	 * @return int
	 */
	function GetSessionCookie()
	{
		$keep_session_on_browser_close = $this->Application->ConfigValue('KeepSessionOnBrowserClose');
		if (isset($this->Application->HttpQuery->Cookie[$this->CookieName]) &&
					( $keep_session_on_browser_close ||
						(
							!$keep_session_on_browser_close &&
							isset($this->Application->HttpQuery->Cookie[$this->CookieName.'_live'])
							&&
							$this->Application->HttpQuery->Cookie[$this->CookieName] == $this->Application->HttpQuery->Cookie[$this->CookieName.'_live']
						)
					)
				) {
			return $this->Application->HttpQuery->Cookie[$this->CookieName];
		}
		return false;
	}

	/**
	 * Updates SID in cookie with new value
	 *
	 */
	function SetSessionCookie()
	{
		$this->SetCookie($this->CookieName, $this->SID, $this->Expiration);
		$this->SetCookie($this->CookieName.'_live', $this->SID);
		$_COOKIE[$this->CookieName] = $this->SID;	// for compatibility with in-portal
	}

	function RemoveSessionCookie()
	{
		$this->SetCookie($this->CookieName, '');
		$this->SetCookie($this->CookieName.'_live', '');
		$_COOKIE[$this->CookieName] = null;	// for compatibility with in-portal
	}

	/**
	 * Refreshes session expiration time
	 *
	 * @access private
	 */
	function Refresh()
	{
		if ($this->Application->GetVar('skip_session_refresh')) {
			return ;
		}

		if ($this->CookiesEnabled) {
			// we need to refresh the cookie
			$this->SetSessionCookie();
		}
		$this->Storage->UpdateSession($this);
	}

	function Destroy()
	{
		$this->Storage->DeleteSession($this);
		$this->Data = new Params();
		$this->SID = $this->CachedSID = '';
		$this->SessionSet = false;

		if ($this->CookiesEnabled) {
			$this->SetSessionCookie(); //will remove the cookie due to value (sid) is empty
		}

		$this->SetSession(true); //will create a new session, true to force
	}

	function NeedQueryString($use_cache = 1)
	{
		if ($this->CachedNeedQueryString != null && $use_cache) {
			return $this->CachedNeedQueryString;
		}

		$result = false;
		switch ($this->Mode) {
			case smAUTO:
				if (!$this->CookiesEnabled) {
					$result = true;
				}
				break;

			/*case smCOOKIES_ONLY:
				break;*/

			case smGET_ONLY:
			case smCOOKIES_AND_GET:
				$result = true;
				break;
		}

		$this->CachedNeedQueryString = $result;

		return $result;
	}

	function LoadData()
	{
		$this->Data->AddParams($this->Storage->LoadData($this));
	}

	function PrintSession($comment = '')
	{
		if (defined('DEBUG_MODE') && $this->Application->isDebugMode() && constOn('DBG_SHOW_SESSIONDATA')) {
			// dump session data
			$this->Application->Debugger->appendHTML('SessionStorage [' . ($this->RecallVar('admin') == 1 ? 'Admin' : 'Front-End') . '] ('.$comment.'):');
			$session_data = $this->Data->GetParams();
			ksort($session_data);
			foreach ($session_data as $session_key => $session_value) {
				if (IsSerialized($session_value)) {
					$session_data[$session_key] = unserialize($session_value);
				}
			}
			$this->Application->Debugger->dumpVars($session_data);

			if (!$this->RecallVar('admin')) {
				// dump real keys (only for front-end)
				$data_keys = array_keys($session_data);
				$optional_keys = array_keys($this->OptionalData);
				$real_keys = array_diff($data_keys, $optional_keys);

				if ($real_keys) {
					$ret = '';
					foreach ($real_keys as $real_key) {
						$ret .= '[' . $real_key . '] = [' . $session_data[$real_key] . ']<br/>';
					}

					$this->Application->Debugger->appendHTML('Real Keys:<br/> ' . $ret);
				}
			}
		}

		if (defined('DEBUG_MODE') && $this->Application->isDebugMode() && constOn('DBG_SHOW_PERSISTENTDATA')) {
			// dump persistent session data
			if ($this->Storage->PersistentVars) {
				$this->Application->Debugger->appendHTML('Persistant Session:');
				$session_data = $this->Storage->PersistentVars;
				ksort($session_data);
				foreach ($session_data as $session_key => $session_value) {
					if (IsSerialized($session_value)) {
						$session_data[$session_key] = unserialize($session_value);
					}
				}
				$this->Application->Debugger->dumpVars($session_data);
			}
		}
	}

	function SaveData($params = Array ())
	{
		if (!$this->SetSession()) { // call it here - it may be not set before, because there was no need; if there is a need, it will be set here
			return;
		}

		if (!$this->Application->GetVar('skip_last_template') && $this->Application->GetVar('ajax') != 'yes') {
			$this->SaveLastTemplate( $this->Application->GetVar('t'), $params );
		}

		$this->PrintSession('after save');
		$this->Storage->SaveData($this);
	}

	/**
	 * Save last template
	 *
	 * @param string $t
	 * @param Array $params
	 */
	function SaveLastTemplate($t, $params = Array ())
	{
		$wid = $this->Application->GetVar('m_wid');

		$last_env = $this->getLastTemplateENV($t, Array('m_opener' => 'u'));
		$last_template = basename($_SERVER['PHP_SELF']).'|'.mb_substr($last_env, mb_strlen(ENV_VAR_NAME) + 1);
		$this->StoreVar(rtrim('last_template_'.$wid, '_'), $last_template);

		// prepare last_template for opener stack, module & session could be added later
		$last_env = $this->getLastTemplateENV($t, null, false);
		$last_template = basename($_SERVER['PHP_SELF']).'|'.mb_substr($last_env, mb_strlen(ENV_VAR_NAME) + 1);

		// save last_template in persistant session
		if (!$wid) {
			if ($this->Application->isAdmin) {
				// only for main window, not popups, not login template, not temp mode (used in adm:MainFrameLink tag)
				$temp_mode = false;
				$passed = explode(',', $this->Application->GetVar('passed'));
				foreach ($passed as $passed_prefix) {
					if ($this->Application->GetVar($passed_prefix.'_mode')) {
						$temp_mode = true;
						break;
					}
				}

				if (!$temp_mode) {
					if (isset($this->Application->HttpQuery->Get['section'])) {
						// check directly in GET, bacause LinkVar (session -> request) used on these vars
						$last_template .= '&section='.$this->Application->GetVar('section').'&module='.$this->Application->GetVar('module');
					}

					$this->StorePersistentVar('last_template_popup', $last_template);
				}
			}
			elseif ($this->Application->GetVar('admin')) {
				// admin checking by session data to prevent recursive session save
				static $admin_saved = null;

				if (!$this->RecallVar('admin') && !isset($admin_saved)) {
					// bug: we get recursion in this place, when cookies are disabled in browser and we are browsing
					// front-end in admin's frame (front-end session is initialized using admin's sid and they are
					// mixed together)

					$admin_saved = true;

					$admin_session =& $this->Application->recallObject('Session.admin');
					/* @var $admin_session Session */

					// save to admin last_template too, because when F5 is pressed in frameset Front-End frame should reload as well

					$admin_session->StoreVar('last_template_popup', '../' . $last_template);
					$admin_session->StorePersistentVar('last_template_popup', '../' . $last_template);
					$admin_session->SaveData( Array ('save_last_template' => false) );
				}
				else {
					// don't allow admin=1 & editing_mode=* to get in admin last_template
					$last_template = preg_replace('/&(admin|editing_mode)=[\d]/', '', $last_template);
				}
			}
		}

		// save other last... variables for mistical purposes (customizations may be)
		$this->StoreVar('last_url', $_SERVER['REQUEST_URI']); // needed by ord:StoreContinueShoppingLink
		$this->StoreVar('last_env', mb_substr($last_env, mb_strlen(ENV_VAR_NAME)+1));

		$save_last_template = array_key_exists('save_last_template', $params) ? $params['save_last_template'] : true;

		if ($save_last_template) {
			// save last template here, becase section & module could be added before
			$this->StoreVar(rtrim('last_template_popup_'.$wid, '_'), $last_template);
		}
	}

	function getLastTemplateENV($t, $params = null, $encode = true)
	{
		if (!isset($params)) {
			$params = Array ();
		}

		$params['__URLENCODE__'] = 1; // uses "&" instead of "&amp;" for url part concatenation + replaces "\" to "%5C" (works in HTML)


		if ($this->Application->GetVar('admin') && !array_key_exists('admin', $params) && !defined('EDITING_MODE')) {
			$params['editing_mode'] = ''; // used in kApplication::Run
		}

		$params = array_merge($this->Application->getPassThroughVariables($params), $params);
		$ret = $this->Application->BuildEnv($t, $params, 'all');

		if (!$encode) {
			// cancels 2nd part of replacements, that URLENCODE does
			$ret = str_replace('%5C', '\\', $ret);
		}
		return $ret;
	}

	function StoreVar($name, $value, $optional = false)
	{
		$this->Data->Set($name, $value);

		if ($optional) {
			// make variable optional, also remember optional value
			$this->OptionalData[$name] = $value;
		}
		elseif (!$optional && array_key_exists($name, $this->OptionalData)) {
			if ($this->OptionalData[$name] == $value) {
				// same value as optional -> don't remove optional mark
				return ;
			}

			// make variable non-optional
			unset($this->OptionalData[$name]);
		}
	}

	function StorePersistentVar($name, $value)
	{
		$this->Storage->StorePersistentVar($this, $name, $value);
	}

	function LoadPersistentVars()
	{
		$this->Storage->LoadPersistentVars($this);
	}

	function StoreVarDefault($name, $value, $optional=false)
	{
		$tmp = $this->RecallVar($name);
		if($tmp === false || $tmp == '')
		{
			$this->StoreVar($name, $value, $optional);
		}
	}

	function RecallVar($name, $default = false)
	{
		$ret = $this->Data->Get($name);
		return ($ret === false) ? $default : $ret;
	}

	function RecallPersistentVar($name, $default = false)
	{
		return $this->Storage->RecallPersistentVar($this, $name, $default);
	}


	function RemoveVar($name)
	{
		$this->Storage->RemoveFromData($this, $name);
		$this->Data->Remove($name);
	}

	function RemovePersistentVar($name)
	{
		return $this->Storage->RemovePersistentVar($this, $name);
	}

	/**
	 * Ignores session varible value set before
	 *
	 * @param string $name
	 */
	function RestoreVar($name)
	{
		$value = $this->Storage->GetFromData($this, $name, '__missing__');

		if ($value === '__missing__') {
			// there is nothing to restore (maybe session was not saved), look in optional variable values
			$value = array_key_exists($name, $this->OptionalData) ? $this->OptionalData[$name] : false;
		}

		return $this->StoreVar($name, $value);
	}

	function GetField($var_name, $default = false)
	{
		return $this->Storage->GetField($this, $var_name, $default);
	}

	function SetField($var_name, $value)
	{
		$this->Storage->SetField($this, $var_name, $value);
	}

	/**
	 * Deletes expired sessions
	 *
	 * @return Array	expired sids if any
	 * @access private
	 */
	function DeleteExpired()
	{
		return $this->Storage->DeleteExpired();
	}

	/**
	 * Allows to check if user in this session is logged in or not
	 *
	 * @return bool
	 */
	function LoggedIn()
	{
		$user_id = $this->RecallVar('user_id');

		$ret = $user_id > 0;
		if (($this->RecallVar('admin') == 1 || defined('ADMIN')) && ($user_id == -1)) {
			$ret = true;
		}
		return $ret;
	}

}