<?php
/**
* @version	$Id: session.php 16587 2017-07-18 11:46:31Z 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:
- Session::smAUTO
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)

- Session::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.

- Session::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

- Session::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(Session::smAUTO); //Session::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>";

*/

class Session extends kBase {

	const smAUTO = 1;
	const smCOOKIES_ONLY = 2;
	const smGET_ONLY = 3;
	const smCOOKIES_AND_GET = 4;

	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
	 * @access protected
	 */
	protected $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;

	/**
	 * Creates session
	 *
	 * @param int $mode
	 * @access public
	 */
	public function __construct($mode = self::smAUTO)
	{
		parent::__construct();

		$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)
	{
		// 1. localhost or other like it without "." in domain name
		if (!substr_count($domain, '.')) {
			// don't use cookie domain at all
			$this->CookieDomain = false;
			return ;
		}

		// 2. match using predefined cookie domains from configuration
		$cookie_domains = $this->Application->ConfigValue('SessionCookieDomains');

		if ($cookie_domains) {
			$cookie_domains = array_map('trim', explode("\n", $cookie_domains));

			foreach ($cookie_domains as $cookie_domain) {
				if (ltrim($cookie_domain, '.') == $domain) {
					$this->CookieDomain = $cookie_domain; // as defined in configuration
					return ;
				}
			}
		}

		// 3. only will execute, when none of domains were matched at previous step
		$this->CookieDomain = $this->_autoGuessDomain($domain);
	}

	/**
	 * Auto-guess cookie domain based on $_SERVER['HTTP_HOST']
	 *
	 * @param $domain
	 * @return string
	 */
	function _autoGuessDomain($domain)
	{
		static $cache = Array ();

		if (!array_key_exists($domain, $cache)) {
			switch ( substr_count($domain, '.') ) {
				case 2:
					// 3rd level domain (3 parts)
					$cache[$domain] = substr($domain, strpos($domain, '.')); // with leading "."
					break;

				case 1:
					// 2rd level domain (2 parts)
					$cache[$domain] = '.' . $domain; // with leading "."
					break;

				default:
					// more then 3rd level
					$cache[$domain] = ltrim($domain, '.'); // without leading "."
					break;
			}
		}

		return $cache[$domain];
	}

	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->setSession($this);
	}

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

		if ( php_sapi_name() == 'cli' ) {
			$this->SetMode(self::smGET_ONLY);
		}

		$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(); // called from u:OnDeleteExpiredSessions scheduled task now

		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;
			$this->Application->HandleEvent(new kEvent('u:OnSessionExpire'));
		}
	}

	/**
	 * 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 == self::smGET_ONLY ) {
			//we don't need to bother checking if we would not use it
			$this->CookiesEnabled = false;
			return false;
		}

		/** @var kHTTPQuery $http_query */
		$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->Application->HttpQuery->IsHTTPSRedirect() && $get_sid) || $this->getFlashSID() ) { // Redirect from http to https on different domain OR flash uploader
			$this->OriginalMode = $this->Mode;
			$this->SetMode(self::smGET_ONLY);
		}

		if ( !$cookies_on || $this->Application->HttpQuery->IsHTTPSRedirect() || $this->getFlashSID() ) {
			//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->Application->HttpQuery->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;
		}

		$old_style_domains = Array (
			// domain like in pre 5.1.0 versions
			'.' . SERVER_NAME,

			// auto-guessed domain (when user specified other domain in configuration variable)
			$this->_autoGuessDomain(SERVER_NAME)
		);

		foreach ($old_style_domains as $old_style_domain) {
			if ($this->CookieDomain != $old_style_domain) {
				// new style cookie domain -> delete old style cookie to prevent infinite redirect
				setcookie($name, $value, adodb_mktime() - 3600, $this->CookiePath, $old_style_domain, $this->CookieSecure, true);

			}
		}

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

	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 getFlashSID()
	{
		/** @var kHTTPQuery $http_query */
		$http_query = $this->Application->recallObject('HTTPQuery');

		return getArrayValue($http_query->Post, 'flashsid');
	}

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

		// flash sid overrides regular sid
		$get_sid = $this->getFlashSID();

		if (!$get_sid) {
			/** @var kHTTPQuery $http_query */
			$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 self::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 self::smCOOKIES_ONLY:
					$sid = $this->GetSessionCookie();
					break;

				case self::smGET_ONLY:
					$sid = $get_sid;
					break;

				case self::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()
	{
		$this->setSID(kUtil::generateId());

		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;
		}

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

		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();
			}

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

			return false;
		}

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

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

			case self::smGET_ONLY:
				break;

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

		$this->Storage->StoreSession();

		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('UserSessions');
		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();
	}

	function Destroy()
	{
		$this->Storage->DeleteSession();
		$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 self::smAUTO:
				if ( !$this->CookiesEnabled && PHP_SAPI !== 'cli' ) {
					$result = true;
				}
				break;

			/*case self::smCOOKIES_ONLY:
				break;*/

			case self::smGET_ONLY:
			case self::smCOOKIES_AND_GET:
				if ( PHP_SAPI !== 'cli' ) {
					$result = true;
				}
				break;
		}

		$this->CachedNeedQueryString = $result;

		return $result;
	}

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

	/**
	 * Returns information about session contents
	 *
	 * @param bool $include_optional
	 * @return array
	 * @access public
	 */
	public function getSessionData($include_optional = true)
	{
		$session_data = $this->Data->GetParams();
		ksort($session_data);

		foreach ($session_data as $session_key => $session_value) {
			if ( kUtil::IsSerialized($session_value) ) {
				$session_data[$session_key] = unserialize($session_value);
			}
		}

		if ( !$include_optional ) {
			$optional_keys = array_keys($this->OptionalData);

			foreach ($session_data as $session_key => $session_value) {
				if ( in_array($session_key, $optional_keys) ) {
					unset($session_data[$session_key]);
				}
			}
		}

		return $session_data;
	}

	/**
	 * Returns real session data, that was saved
	 *
	 * @param Array $session_data
	 * @return Array
	 * @access protected
	 */
	protected function _getRealSessionData($session_data)
	{
		$data_keys = array_keys($session_data);
		$optional_keys = array_keys($this->OptionalData);
		$real_keys = array_diff($data_keys, $optional_keys);

		if ( !$real_keys ) {
			return Array ();
		}

		$ret = Array ();

		foreach ($real_keys as $real_key) {
			$ret[$real_key] = $session_data[$real_key];
		}

		return $ret;
	}

	function PrintSession($comment = '')
	{
		if ( defined('DEBUG_MODE') && $this->Application->isDebugMode() && kUtil::constOn('DBG_SHOW_SESSIONDATA') ) {
			// dump session data
			$this->Application->Debugger->appendHTML('SessionStorage [' . ($this->RecallVar('admin') == 1 ? 'Admin' : 'Front-End') . '] (' . $comment . '):');

			$session_data = $this->getSessionData();
			$this->Application->Debugger->dumpVars($session_data);

			if ( !$this->RecallVar('admin') ) {
				// dump real keys (only for front-end)
				$real_session_data = $this->_getRealSessionData($session_data);

				if ( $real_session_data ) {
					$this->Application->Debugger->appendHTML('Real Keys:');
					$this->Application->Debugger->dumpVars($real_session_data);
				}
			}
		}

		if ( defined('DEBUG_MODE') && $this->Application->isDebugMode() && kUtil::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 ( kUtil::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();
	}

	/**
	 * 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']) . '|' . $last_env;
		$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);
		$last_template = basename($_SERVER['PHP_SELF']) . '|' . $last_env;

		// save last_template in persistent 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 ( $this->Application->GetVarDirect('section', 'Get') !== false ) {
						// check directly in GET, because 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;

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

					// 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 mystical purposes (customizations may be)
		$this->StoreVar('last_url', $_SERVER['REQUEST_URI']); // needed by ord:StoreContinueShoppingLink
		$this->StoreVar('last_env', $last_env);

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

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

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

		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);

		return $this->Application->BuildEnv($t, $params, 'all', false, false);
	}

	/**
	 * Stores variable $val in session under name $var
	 *
	 * Use this method to store variable in session. Later this variable could be recalled.
	 *
	 * @param string $name Variable name
	 * @param mixed $value Variable value
	 * @param bool $optional
	 * @return void
	 * @access public
	 * @see Session::RecallVar()
	 */
	public 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]);
		}
	}

	/**
	 * Stores variable to persistent session
	 *
	 * @param string $name
	 * @param mixed $value
	 * @param bool $optional
	 * @return void
	 * @access public
	 */
	public function StorePersistentVar($name, $value, $optional = false)
	{
		$this->Storage->StorePersistentVar($name, $value, $optional);
	}

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

	/**
	 * Stores default value for session variable
	 *
	 * @param string $name
	 * @param string $value
	 * @param bool $optional
	 * @return void
	 * @access public
	 * @see Session::RecallVar()
	 * @see Session::StoreVar()
	 */
	public function StoreVarDefault($name, $value, $optional = false)
	{
		$tmp = $this->RecallVar($name);

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

	/**
	 * Returns session variable value
	 *
	 * Return value of $var variable stored in Session. An optional default value could be passed as second parameter.
	 *
	 * @param string $name Variable name
	 * @param mixed $default Default value to return if no $var variable found in session
	 * @return mixed
	 * @access public
	 */
	public function RecallVar($name, $default = false)
	{
		$ret = $this->Data->Get($name);

		return ($ret === false) ? $default : $ret;
	}

	/**
	 * Returns variable value from persistent session
	 *
	 * @param string $name
	 * @param mixed $default
	 * @return mixed
	 * @access public
	 */
	public function RecallPersistentVar($name, $default = false)
	{
		return $this->Storage->RecallPersistentVar($name, $default);
	}

	/**
	 * Deletes Session variable
	 *
	 * @param string $var
	 * @return void
	 * @access public
	 */
	public function RemoveVar($name)
	{
		$this->Storage->RemoveFromData($name);
		$this->Data->Remove($name);
	}

	/**
	 * Removes variable from persistent session
	 *
	 * @param string $name
	 * @return void
	 * @access public
	 */
	public function RemovePersistentVar($name)
	{
		$this->Storage->RemovePersistentVar($name);
	}

	/**
	 * Ignores session variable value set before
	 *
	 * @param string $name
	 * @return void
	 * @access public
	 */
	public function RestoreVar($name)
	{
		$value = $this->Storage->GetFromData($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;
		}

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

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

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

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

	/**
	 * Deletes given sessions
	 *
	 * @param $session_ids
	 * @param int $delete_reason
	 * @return void
	 */
	function DeleteSessions($session_ids, $delete_reason = SESSION_LOG_EXPIRED)
	{
		$this->Storage->DeleteSessions($session_ids, $delete_reason);
	}

	/**
	 * 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 == USER_ROOT)) {
			$ret = true;
		}
		return $ret;
	}

}
