e107inc/e107

View on GitHub
e107_handlers/session_handler.php

Summary

Maintainability
A
40 mins
Test Coverage
F
22%
<?php
/*
 * e107 website system
 *
 * Copyright (C) 2008-2012 e107 Inc (e107.org)
 * Released under the terms and conditions of the
 * GNU General Public License (http://www.gnu.org/licenses/gpl.txt)
 *
 * Session handler
 *
 * $URL$
 * $Id$
 */

if (!defined('e107_INIT'))
{
    exit;
}

/**
 * @package e107
 * @subpackage    e107_handlers
 * @version $Id$
 * @author SecretR
 * 
 * Dependencies:
 * - direct: language handler
 * - indirect: system preferences (required by language handler)
 * 
 * What could break it?
 * If session is started before the first system session call (see class2.php
 * 'Start: Set User Language' phase), session config will not be applied!
 * This could happen if included $CLASS2_INCLUDE script (see class2.php)
 * calls session_start(). However, sessions will not be broken, just not secured
 * as per e_SECURITY_LEVEL setting.
 * 
 * Security levels:
 * - SECURITY_LEVEL_NONE [0]: security disabled - no token checks, all session validation settings dsiabled 
 * - SECURITY_LEVEL_BALANCED [5]: ValidateRemoteAddr, ValidateHttpXForwardedFor are on,
 * session token is created/checked, but not regenerated on every page load
 * - SECURITY_LEVEL_HIGH [7]: Same as above but ValidateHttpVia, ValidateHttpUserAgent are on. 
 * - SECURITY_LEVEL_PARANOID [9]: Same as SECURITY_LEVEL_HIGH except session token is regenerated on
 * every page load. 'httponly' is on, which means JS is unable to retrieve session cookie, this may cause
 * troubles with some browsers.
 * - SECURITY_LEVEL_INSANE [10]: Same as SECURITY_LEVEL_HIGH plus session id is regenerated at the end
 * of every page request. 
 * 
 * Session objects are created by namespace:
 * $_SESSION['e107'] is default namesapce auto created with
 * <code><?php e107::getSession();</code>
 * Session handler is validating corresponding session COOKIE 
 * (named as current session name, keeping the session id)
 * on regular basis (session lifetime/4). If validation
 * fails, corresponding cookie is destroyed (not the session itself).  
 * 
 * Initial system Session is started after language detection (see class2.php) to 
 * ensure proper session handling for sites using language sub-domains (e.g. fr.site.com)
 * 
 * Some important system session data will be kept outside of the object for now (e.g. user validation data) 
 * 
 */


class e_session
{
    /**
     * No protection, label 'Looking for trouble'
     * @var integer
     */
    const SECURITY_LEVEL_NONE = 0;


    const SECURITY_LEVEL_LOW = 3;
    /**
     * Default system protection, balanced for best user experience, 
     * label 'Safe mode - Balanced'
     * @var integer
     */
    const SECURITY_LEVEL_BALANCED = 5;
    
    /**
     * Adds more system security, but there is a chance (minimal) to break stuff,
     * label 'High Security'
     * @var integer
     */
    const SECURITY_LEVEL_HIGH = 7;
    
    /**
     * High system protection, session id is regenerated on every page request,
     * label 'Paranoid'
     * @var integer
     */
    const SECURITY_LEVEL_PARANOID = 9;
    
    /**
     * Highest system protection, session id and token values are regenerated on every page request,
     * label 'Insane'
     * @var int unknown_type
     */
    const SECURITY_LEVEL_INSANE = 10;
    
    /**
     * Session save path
     * @var string
     */
    protected $_sessionSavePath = false;

    /**
     * Session save method
     * @var string files|db
     */
    protected $_sessionSaveMethod = 'files';//'files';

    /**
     * Session cache limiter, ignored if empty
     * php.net/manual/en/function.session-cache-limiter.php
     * @var string public|private_no_expire|private|nocache
     */
    protected $_sessionCacheLimiter = '';
    
    protected $_namespace;
    protected $_name;
    protected static $_sessionStarted = false; // Fixes lost $_SESSION value problem.

    /**
     * Validation options
     * @var boolean
     */
    protected $_sessionValidateRemoteAddr = true;
    protected $_sessionValidateHttpVia = true;
    protected $_sessionValidateHttpXForwardedFor = true;
    protected $_sessionValidateHttpUserAgent = true;

    /**
     * Skip validation
     * @var array
     */
    protected $_sessionValidateRemoteAddrSkip = array();
    protected $_sessionValidateHttpViaSkip = array();
    protected $_sessionValidateHttpXForwardedForSkip = array();
    protected $_sessionValidateHttpUserAgentSkip = array();

    /**
     * Default session options
     * @var array
     */
    protected $_options = array(
        'lifetime'     => 3600 , // 1 hour
        'path'         => '',
        'domain'     => '',
        'secure'     => false,
        'httponly'     => true,
    );

    /**
     * Session data
     * @var array
     */
    protected $_data = array();
    
    /**
     * Set session options
     * @param string $key
     * @param mixed $value
     * @return e_session
     */
    public function setOption($key, $value)
    {
        $this->setOptions(array($key => $value));
        return $this;
    }

    /**
     * @return array
     */
    public function getOptions()
    {
        return $this->_options;
    }



    /**
     * Get session option
     * @param string $key
     * @param mixed $default
     * @return mixed value
     */
    public function getOption($key, $default = null)
    {
        return (isset($this->_options[$key]) ? $this->_options[$key] : $default);
    }
    
    /**
     * Set default settings/options based on the current security level
     * NOTE: new prefs 'session_save_path', 'session_save_method', 'session_lifetime' introduced, 
     * still not added to preference administration
     * @return e_session
     */
    public function setDefaultSystemConfig()
    {
        if ($this->getSessionId()) return $this;

        $config = array(
            'ValidateRemoteAddr' => (e_SECURITY_LEVEL >= self::SECURITY_LEVEL_BALANCED),
            'ValidateHttpVia' => (e_SECURITY_LEVEL >= self::SECURITY_LEVEL_HIGH),
            'ValidateHttpXForwardedFor' => (e_SECURITY_LEVEL >= self::SECURITY_LEVEL_LOW),
            'ValidateHttpUserAgent' => (e_SECURITY_LEVEL >= self::SECURITY_LEVEL_HIGH),
        );

        $options = array(
            //        'httponly' => (e_SECURITY_LEVEL >= self::SECURITY_LEVEL_PARANOID),
            'httponly' => true,
        );

        if (!defined('E107_INSTALL'))
        {
            $systemSaveMethod = ini_get('session.save_handler');

            $saveMethod = (!empty($systemSaveMethod)) ? $systemSaveMethod : 'files';

            $config['SavePath']     = e107::getPref('session_save_path', false); // FIXME - new pref
            $config['SaveMethod']   = e107::getPref('session_save_method', $saveMethod);
            $options['lifetime']    = (int) e107::getPref('session_lifetime', 86400);
            $options['path']        = e107::getPref('session_cookie_path', ''); // FIXME - new pref
            $options['secure']      = e107::getPref('ssl_enabled', false); //

            e107::getDebug()->log("Session Save Method: ".$config['SaveMethod']);

            if (!empty($options['secure']))
            {
                ini_set('session.cookie_secure', 1);
            }

            ini_set('session.gc_maxlifetime', $options['lifetime']);
        }

        if (defined('SESSION_SAVE_PATH')) // safer than a pref.
        {
            $config['SavePath'] = e_BASE . SESSION_SAVE_PATH;
        }

        $hashes = hash_algos();

    //    if ((e_SECURITY_LEVEL >= self::SECURITY_LEVEL_BALANCED) && in_array('sha512', $hashes))
        {

          //  ini_set('session.hash_function', 'sha512'); Removed in PHP 7.1
          //  ini_set('session.hash_bits_per_character', 5); Removed in PHP 7.1
        }

        $this->fixSessionFileGarbageCollection();

        $this->setConfig($config)
            ->setOptions($options);

        return $this;
    }

    /**
     * Modify PHP ini at runtime to enable session file garbage collection
     *
     * Takes no action if the garbage collector is already enabled.
     *
     * @see https://github.com/e107inc/e107/issues/4113
     * @return void
     */
    private function fixSessionFileGarbageCollection()
    {
        $gc_probability = ini_get('session.gc_probability');
        if ($gc_probability > 0) return;

        ini_set('session.gc_probability', 1);
        ini_set('session.gc_divisor', 100);
    }
    
    /**
     * Retrieve value from current session namespace
     * Equals to $_SESSION[NAMESPACE][$key]
     * @param string $key
     * @param boolean $clear unset key
     * @return mixed
     */
    public function get($key, $clear = false)
    {
        $ret = isset($this->_data[$key]) ? $this->_data[$key] : null;
        if($clear) $this->clear($key);
        return $ret;
    }
    
    /**
     * Retrieve value from current session namespace
     * If key is null, returns all current session namespace data
     * 
     * @param string|null $key
     * @param boolean $clear
     * @return mixed
     */
    public function getData($key = null, $clear = false)
    {
        if(null === $key)
        {
            $ret = $this->_data;
            if($clear) $this->clearData();
            return $ret;
        }
        return $this->get($key, $clear);
    }
    
    /**
     * Set value in current session namespace
     * Equals to $_SESSION[NAMESPACE][$key] = $value
     * @param string $key Also accepts multi-dimensinal format. key1/key2
     * @param mixed $value
     * @return e_session
     */
    public function set($key, $value)
    {
        if(strpos($key,'/') !== false) // multi-dimensional
        {
            $keyArr = explode('/',$key);
            $count = count($keyArr);

            if($count === 2)
            {
                list($k1, $k2) = $keyArr;
                $this->_data[$k1][$k2] = $value;
            }
            elseif($count === 3)
            {
                list($k1, $k2, $k3) = $keyArr;
                $this->_data[$k1][$k2][$k3] = $value;
            }

        }
        else
        {
            $this->_data[$key] = $value;
        }

        return $this;
    }
    
    /**
     * Set value in current session namespace
     * If $key is array, the whole namespace array will be replaced with it,
     * $value will be ignored
     * @param string|null $key
     * @param mixed $value
     * @return e_session
     */
    public function setData($key, $value = null)
    {
        if(is_array($key))
        {
            $this->_data = $key;
            return $this;
        }
        return $this->set($key, $value);
    }
    
    /**
     * Check if given key is set in current session namespace
     * Equals to isset($_SESSION[NAMESPACE][$key])
     * @param string $key
     * @return boolean
     */
    public function is($key)
    {
        return isset($this->_data[$key]);
    }
    
    /**
     * Check if given key is set and not empty in current session namespace
     * Equals to !empty($_SESSION[NAMESPACE][$key]) check
     * @param string $key
     * @return boolean
     */
    public function has($key)
    {
        return (isset($this->_data[$key]) && $this->_data[$key]);
    }
    
    /**
     * Checks if current session namespace contains any data
     * Equals to !empty($_SESSION[NAMESPACE]) check
     * @return boolean
     */
    public function hasData()
    {
        return !empty($this->_data);
    }
    
    /**
     * Unset member of current session namespace array
     * Equals to unset($_SESSION[NAMESPACE][$key])
     * @param string $key
     * @return e_session
     */
    public function clear($key=null)
    {
        if($key == null) // clear all under this namespace.
        {
            $this->_data = array(); // must be set to array() not unset.
            return $this;
        }

        if(strpos($key,'/') !== false) // multi-dimensional
        {
            $keyArr = explode('/',$key);
            $count = count($keyArr);

            if($count === 2)
            {
                list($k1, $k2) = $keyArr;
                unset($this->_data[$k1][$k2]);
            }
            elseif($count === 3)
            {
                list($k1, $k2, $k3) = $keyArr;
                unset($this->_data[$k1][$k2][$k3]);
            }

        }
        else
        {
            unset($this->_data[$key]);
        }


        return $this;
    }
    
    /**
     * Reset current session namespace to empty array 
     * @return e_session
     */
    public function clearData()
    {
        $this->_data = array();
        return $this;
    }

    /**
     * Set protected class vars, prefixed with _session
     * @param array $config
     * @return e_session
     */
    public function setConfig($config)
    {
        foreach ($config as $k => $v)
        {
            $key = '_session'.$k;
            if (isset($this->$key)) $this->$key = $v;
        }
        return $this;
    }
    
    /**
     * Get registered namespace key
     * @return string
     */
    public function getNamespaceKey()
    {
        return $this->_namespace;
    }

    /**
     * Reset session options
     * @param array $options
     * @return e_session
     */
    public function setOptions($options)
    {
        if (empty($options) || !is_array($options)) return $this;
        foreach ($options as $k => $v)
        {
            switch ($k)
            {
                case 'lifetime':
                    $v = intval($v);
                break;

                case 'path':
                case 'domain':
                    $v = (string) $v;
                break;

                case 'secure':
                case 'httponly':
                    $v = $v ? true : false;
                break;

                default:
                    $v = null;
                break;
            }

            if($v !== null)
            {
                $this->_options[$k] = $v;
            }
        }
        return $this;
    }

    /**
     * @param $namespace
     * @param $sessionName
     * @return void
     */
    public function init($namespace, $sessionName = null)
    {
        $this->start($sessionName);

        if (!isset($_SESSION[$namespace]))
        {
            $_SESSION[$namespace] = array();
        }
        $this->_data =& $_SESSION[$namespace];
        $this->_namespace = $namespace;

        $this->validate();
        $this->validateSessionCookie();
    }

    /**
     * Conigure and start session
     *
     * @param string $sessionName optional session name
     * @return e_session
     */
    public function start($sessionName = null)
    {
    
        if (isset($_SESSION) && (self::$_sessionStarted === true))
        {
            return $this;
        }

        if (false !== $this->_sessionSavePath && is_writable($this->_sessionSavePath))
        {
            session_save_path($this->_sessionSavePath);
        }

        switch ($this->_sessionSaveMethod)
        {
            case 'db':
            //    ini_set('session.save_handler', 'user');

                $session = new e_session_db;
                session_set_save_handler(
                    [$session, 'open'],
                    [$session, 'close'],
                    [$session, 'read'],
                    [$session, 'write'],
                    [$session, 'destroy'],
                    [$session, 'gc']
                );
                $session->setSaveHandler();
            break;

            default:
                if(!isset($_SESSION))
                {
                    session_module_name($this->_sessionSaveMethod);
                }
            break;
        }

        if (empty($this->_options['domain']))
        {
            // MULTILANG_SUBDOMAIN set during initial language detection in language handler
            $doma = ((deftrue('e_SUBDOMAIN') || deftrue('MULTILANG_SUBDOMAIN')) && e_DOMAIN != FALSE) ? ".".e_DOMAIN : FALSE; // from v1.x
            $this->_options['domain'] = $doma;
        }

        if (empty($this->_options['path']))
        {
            if(defined('e_MULTISITE_MATCH')) // multisite support.
            {
                $this->_options['path'] = '/';
            }
            else
            {
                $this->_options['path'] = defined('e_HTTP') ? e_HTTP : '/';
            }
        }

        // session name before options - problems reported on php.net
        if (!empty($sessionName))
        {
            $this->setSessionName($sessionName);
        }
        
        // set session cookie params
        session_set_cookie_params($this->_options['lifetime'],
            $this->_options['path'],
            $this->_options['domain'],
            $this->_options['secure'],
            $this->_options['httponly']);

        if ($this->_sessionCacheLimiter)
        {
            session_cache_limiter((string) $this->_sessionCacheLimiter); //XXX Remove and have e_headers class handle it?
        }
        
    
        session_start();
        self::$_sessionStarted = true;
        return $this;
    }

    /**
     * Set session ID
     * @param string $sid
     * @return e_session
     */
    public function setSessionId($sid = null)
    {
        // comma and minus allowed since 5.0
        if (!empty($sid) && preg_match('#^[0-9a-zA-Z,-]+$#', $sid))
        {
            session_id($sid);
        }
        return $this;
    }

    /**
     * Retrieve current session id
     * @return string
     */
    public function getSessionId()
    {
        return session_id();
    }
    
    /**
     * Retrieve current session save method. 
     * @return string
     */
    public function getSaveMethod()
    {
        return $this->_sessionSaveMethod;    
    }

    /**
     * Set new session name
     * @param string $name alphanumeric characters only
     * @return false old session name or false on error
     */
    public function setSessionName($name)
    {
        if (!empty($name) && preg_match('#^[0-9a-z_]+$#i', $name))
        {
            $this->_name = $name;
        //    return session_name($name);
        }
        return false;
    }

    /**
     * Retrieve current session name
     * @return string
     */
    public function getSessionName()
    {
        return session_name();
    }

    /**
     * Reset session cookie lifetime
     * We reset session cookie on every (session_lifetime / 4) seconds 
     * It's done by all session handler instances, they all share
     * one and the same '_cookie_session_validate' variable (global session namespace)
     * @return e_session
     */
    public function validateSessionCookie()
    {
        if (!$this->_options['lifetime'])
        {
            return $this;
        }

        if (empty($_SESSION['_cookie_session_validate']))
        {
            $time = time() + round($this->_options['lifetime'] / 4);
            $_SESSION['_cookie_session_validate'] = $time;
        }
        elseif ($_SESSION['_cookie_session_validate'] < time())
        { 
            if (!headers_sent())
            {
                cookie(session_name(), session_id(), time() + $this->_options['lifetime'], $this->_options['path'], $this->_options['domain'], $this->_options['secure']);
                $time = time() + round($this->_options['lifetime'] / 4);
                $_SESSION['_cookie_session_validate'] = $time;
            }
        }

        return $this;
    }
    
    /**
     * Delete session cookie
     * @return e_session
     */
    public function cookieDelete()
    {
        cookie(session_name(), null, null, $this->_options['path'], $this->_options['domain'], $this->_options['secure']);
        return $this;
    }

    /**
     * Validate current session
     * @return e_session
     */
    public function validate()
    {
        if (!isset($this->_data['_session_validate_data']))
        {
            $this->_data['_session_validate_data'] = $this->getValidateData();
        }
        elseif (!$this->_validate())
        { 
            $sessionData = $this->_data['_session_validate_data'];
            $validateData = $this->getValidateData();
            
            $details = 'USER INFORMATION: '.(isset($_COOKIE[e_COOKIE]) ? $_COOKIE[e_COOKIE] : (isset($_SESSION[e_COOKIE]) ? $_SESSION[e_COOKIE] : 'n/a'))."\n";
            $details .= "HOST: ".$_SERVER['HTTP_HOST']."\n";
            $details .= "REQUEST_URI: ".$_SERVER['REQUEST_URI']."\n";    
            $details .= "SESSION OPTIONS: ".print_r($this->_options, true)."\n";    
            $details .= "SESSION NAMESPACE: ".$this->_namespace."\n";    
            $details .= "SESSION VALIDATION DATA SAVED: ".print_r($sessionData, true)."\n";
            $details .= "SESSION VALIDATION DATA CURRENT: ".print_r($validateData, true)."\n";
            $details .= "CURRENT NAMESPACE SESSION DATA:\n";
            $this->clear('_session_validate_data'); // already logged
            $details .= print_r($this->_data, true);
            $this->close(false);
            $details .= "SESSION GLOBAL DATA:\n";
            $details .= print_r($_SESSION, true);
            
            // delete cookie, destroy session
            $this->cookieDelete()->destroy();
            
            // TODO event trigger
            
            // e107::getAdminLog()->add('Session validation failed!', $details, E_LOG_FATAL);
            // TODO session exception, handle it proper on live site
            // throw new Exception('');
            
            // just for now
            $msg = 'Session validation failed! <a href="'.strip_tags($_SERVER['REQUEST_URI']).'">Go Back</a>';
        //    die($msg); //FIXME not functioning as intended. 
        }

        return $this;
    }

    /**
     * Validate current session based on config options
     *
     * @return bool
     */
    protected function _validate()
    {
        $sessionData = $this->_data['_session_validate_data'];
        $validateData = $this->getValidateData();
        $keyvar = '_sessionValidate';
        
        foreach ($validateData as $vkey => $value) 
        {
            $var = $keyvar.$vkey;
            $varskip = $var.'Skip';
            if ($this->$var && $sessionData[$vkey] != $value && !in_array($value, $this->$varskip))
            {
                return false;
            }
        }

        return true;
    }

    /**
     * Retrieve data for validator
     * @return array
     */
    public function getValidateData()
    {
        $data = array(
            'RemoteAddr' => '',
            'HttpVia' => '',
            'HttpXForwardedFor' => '',
            'HttpUserAgent' => ''
        );

        // collect ip data
        if (isset($_SERVER['REMOTE_ADDR']))
        {
            $data['RemoteAddr'] = (string) $_SERVER['REMOTE_ADDR'];
        }
        if (isset($_ENV['HTTP_VIA']))
        {
            $data['HttpVia'] = (string) $_ENV['HTTP_VIA'];
        }
        if (isset($_ENV['HTTP_X_FORWARDED_FOR']))
        {
            $data['HttpXForwardedFor'] = (string) $_ENV['HTTP_X_FORWARDED_FOR'];
        }

        // collect user agent data
        if (isset($_SERVER['HTTP_USER_AGENT']))
        {
            $data['HttpUserAgent'] = (string) $_SERVER['HTTP_USER_AGENT'];
        }

        return $data;
    }

    /**
     * Retrieve (create if doesn't exist) XSF protection token
     * @param boolean $in_form if true (default) - value for forms, else raw session value
     * @return string
     */
    public function getFormToken($in_form = true)
    {
        if(!$this->has('__form_token') && !defined('e_TOKEN_DISABLE'))  // TODO FIXME: SEF URL of Error page causes e-token refresh.
        {
            $this->set('__form_token', uniqid(md5(rand()), true));
            if(deftrue('e_DEBUG_SESSION')) // XXX enable to troubleshoot "Unauthorized Access!" issues.
            {
                $message = date('r')."\t\t".e_REQUEST_URI."\n";
                file_put_contents(__DIR__.'/session.log', $message, FILE_APPEND);
            }
        }
        return ($in_form ? md5($this->get('__form_token')) : $this->get('__form_token'));
    }
    
    /**
     * Regenerate form token value
     * TODO - save old token
     * @return e_session
     */
    protected function _regenerateFormToken()
    {
        $this->set('__form_token', uniqid(md5(rand()), true));
        return $this;
    }

    /**
     * Do a check against passed token
     * @param string $token
     * @return boolean
     */
    public function checkFormToken($token)
    {
        $utoken = $this->getFormToken(false);
        return ($token === md5($utoken));
    }
    
    /**
     * Clear and Unset current namespace, unregister session singleton
     * e107::getSession('namespace') if needed.
     * @param boolean $unregister if true (default) - unregister Singleton, destroy namespace, 
     *                                 else alias of self::clearData()
     * @return void
     */
    public function close($unregister = true)
    {
        $this->clearData();
        if($unregister) 
        {
            unset($_SESSION[$this->_namespace]);
            e107::setRegistry('core/e107/session/'.$this->_namespace, null);
        }
    }
    
    /**
     * Save session data to disk, end session.
     * Sessions can't be used after this point.
     * Method should be called before every header redirect.
     * @return void
     */
    public function end()
    {
        session_write_close();
    }
    
    /**
     * Destroy all session data
     * @return e_session
     */
    public function destroy()
    {
        $this->cookieDelete()->close();
        //unset($_SESSION);
        
        // cleanup
        cookie(e_COOKIE, null, null); // remove user auth cookie
        // unset($_SESSION['_cookie_session_validate']);
        
        session_destroy();
        return $this;
    }

    /**
     * @return void
     */
    public function replaceRegistry()
    {
        e107::setRegistry('core/e107/session/'.$this->_namespace, $this, true);
    }
}


/**
 *
 */
class e_core_session extends e_session
{
    /**
     * Constructor
     * 3rd party code and/or other system areas are 
     * able to extend the base e_session class and 
     * add more or override the implemented functionality, has their own
     * namespace, add more session security etc.
     * @param array $data session config data
     */
    public function __construct($data = array())
    {    
        // default system configuration
        $this->setDefaultSystemConfig();

        $namespace = 'e107sess'; // Quick Fix for Fatal Error "Cannot use object of type e107 as array" on line 550
        $name = (isset($data['name']) && !empty($data['name']) ? $data['name'] : deftrue('e_COOKIE', 'e107')).'SID';
        if(isset($data['namespace']) && !empty($data['namespace'])) $namespace = $data['namespace'];

        // create $_SESSION['e107'] namespace by default
        $this->init($namespace, $name);
    }
    
    /**
     * Session shutdown - called at the top of footer_default.php by default
     * @return void
     */
    public function shutdown()
    {
        if(!session_id()) // someone closed the session?
        {
            $this->init($this->_namespace, $this->_name); // restart
        }
        
        // give 3rd party code a way to prevent token re-generation
        if(e_SECURITY_LEVEL >= e_session::SECURITY_LEVEL_PARANOID && !deftrue('e_TOKEN_FREEZE'))
        {    
            if(e_SECURITY_LEVEL == e_session::SECURITY_LEVEL_INSANE)
            {
                // regenerate SID
                $oldSID = session_id(); // old SID
                $oldSData = $_SESSION; // old session data
                session_regenerate_id(false); // true don't work on php4 - so time to move on people!    
                $newSID = session_id(); // new SID
                
                // Clean
                session_id($oldSID); // switch to the old session
                session_destroy(); // destroy it
                
                // set new ID, reopen the session, set saved data
                session_id($newSID);
                session_start();
                $_SESSION = $oldSData;
            }
            $this->set('__form_token_regenerate', time()); // check() needs it to re-create token on the next request
        }
        // write session data
        $this->end();
    }

    /**
     * @param $status
     * @param $type
     * @return void|null
     */
    private function log($status, $type=E_LOG_FATAL)
    {

        if(!deftrue('e_DEBUG_SESSION'))
        {
            return null;
        }


    //    $details = "USER: ".USERNAME."\n";
        $details = "HOST: ".$_SERVER['HTTP_HOST']."\n";
        $details .= "REQUEST_URI: ".$_SERVER['REQUEST_URI']."\n";

        $details .= ($_POST['e-token']) ? "e-token (POST): ".$_POST['e-token']."\n" : "";
        $details .= ($_GET['e-token']) ? "e-token (GET): ".$_GET['e-token']."\n" : "";
        $details .= ($_POST['e_token']) ? "AJAX e_token (POST): ".$_POST['e_token']."\n" : "";
/*
        $utoken = $this->getFormToken(false);
        $details .= "raw token: ".$utoken."\n";
        $details .= "checkFormToken (e-token should match this): ".md5($utoken)."\n";
        $details .= "md5(e-token): ".md5($_POST['e-token'])."\n";*/
/*
        $regenerate = $this->get('__form_token_regenerate');
        $details .= "Regenerate after: ".date('r', $regenerate)." (".$regenerate.")\n";
*/

        $details .= "has __form_token: ";
        $hasToken = $this->has('__form_token');
        $details .= empty($hasToken) ? 'false' : 'true';
        $details .= "\n";

        $details .= "_SESSION:\n";
        $details .= print_r($_SESSION,true);

        /*    if($pref['plug_installed'])
            {
                $details .= "\nPlugins:\n";
                $details .= print_r($pref['plug_installed'],true);
            }*/

        $details .= $status."\n\n---------------------------------\n\n";

        $log = e107::getLog();
        $log->addDebug($details);

        if(deftrue('e_DEBUG_SESSION'))
        {
            $log->toFile('Unauthorized_access','Unauthorized access Log', true);
        }

        $log->add($status, $details, $type);


    }
    /**
     * Core CSF protection, see class2.php
     * Could be adopted by plugins for their own (different) protection logic
     * @param boolean $die
     * @return boolean
     */
    public function check($die = true)
    {
        // define('e_TOKEN_NAME', 'e107_token_'.md5($_SERVER['HTTP_HOST'].e_HTTP));
        // TODO e-token required for all system forms?
        
        // only if not disabled and not in 'cli' mod
        if(e_SECURITY_LEVEL < e_session::SECURITY_LEVEL_LOW || e107::getE107('cli')) return true;
        
        if($this->getSessionId())
        {

            if((isset($_POST['e-token']) && !$this->checkFormToken($_POST['e-token']))
            || (isset($_GET['e-token']) && !$this->checkFormToken($_GET['e-token']))
            || (isset($_POST['e_token']) && !$this->checkFormToken($_POST['e_token']))) // '-' is not allowed in jquery. b
            {
                $this->log('Unauthorized access!');
                // do not redirect, prevent dead loop, save server resources
                if($die == true)
                {
                     die('Unauthorized access!');
                }
                
                return false;
            }

            $this->log('Session Token Okay!', defset('E_LOG_NOTICE', 1));

        }
        
        if(!defined('e_TOKEN'))
        {
            // FREEZE token regeneration if minimal, ajax or iframe (ajax and iframe not implemented yet) request
            $_toFreeze = (e107::getE107('minimal') || e107::getE107('ajax') || e107::getE107('iframe'));
            if(!defined('e_TOKEN_FREEZE') && $_toFreeze)
            {
                define('e_TOKEN_FREEZE', true);
            }
            // __form_token_regenerate set in footer, so if footer is not called, token will be never regenerated!
            if(e_SECURITY_LEVEL == e_session::SECURITY_LEVEL_INSANE && !deftrue('e_TOKEN_FREEZE') && $this->has('__form_token_regenerate')) 
            {
                $this->_regenerateFormToken()
                    ->clear('__form_token_regenerate');
            }
            define('e_TOKEN', $this->getFormToken());
        }
        
        return true;
    }


    
    /**
     * Manually Reset the Token. 
     * @see e107forum::ajaxQuickReply();
     */
    public function reset()
    {
        $this->_regenerateFormToken()->clear('__form_token_regenerate');
    }
    
    
    /**
     * Make sure there is unique challenge string for CHAP login
     * @see class2.php
     * @return e_core_session
     
     @TODO: Remove debug code
     */
    public function challenge()
    {
        if (!$this->is('challenge'))        // TODO: Eliminate need for this
        {
            $this->set('challenge', sha1(time().rand().$this->getSessionId()));        // New challenge for next time
        }
        if ($this->is('challenge'))
        {    
            $this->set('prevprevchallenge', $this->get('prevchallenge'));        // Purely for debug
            $this->set('prevchallenge', $this->get('challenge'));                // Need to check user login against this
        }
        else
        {
            $this->set('prevchallenge', '');                                    // Dummy value
            $this->set('prevprevchallenge', '');                                // Dummy value
        }
        //$this->set('challenge', sha1(time().rand().$this->getSessionId()));        // Temporarily disabled
        // FIXME - session id will be regenerated if e_SECURITY_LEVEL is 'paranoid|insane' - generate  (might be OK as long as values retained)
        
        //$extra_text = 'C: '.$this->get('challenge').' PC: '.$this->get('prevchallenge').' PPC: '.$this->get('prevprevchallenge');
        //$logfp = fopen(e_LOG.'authlog.txt', 'a+'); fwrite($logfp, strftime('%H:%M:%S').' CHAP start: '.$extra_text."\n"); fclose($logfp);

        // could go, see _validate()
        $user_agent = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '';
        $ubrowser = md5('E107'.$user_agent);
        if (!$this->is('ubrowser'))
        {
            $this->set('ubrowser', $ubrowser);
        }
        return $this;
    }
}


/**
 * Database session handler
 *
 * @todo PHP 8.1 support with {@see SessionHandlerInterface}
 */
class e_session_db #implements SessionHandlerInterface
{
    /**
     * @var e_db
     */
    protected $_db = null;

    /**
     * Table name
     * @var string
     */
    protected $_table = 'session';
    
    /**
     * @var integer
     */
    protected $_lifetime = null;
    
    public function __construct()
    {
        $this->_db = e107::getDb('session');
    }

    /**
     *
     */
    public function __destruct()
    {
        session_write_close();
    }
    
    /**
     * @return string
     */
    public function getTable()
    {
        return $this->_table;
    }
    
    /**
     * @param string $table
     * @return e_session_db
     */
    public function setTable($table)
    {
        $this->_table = $table;
        return $this;
    }
    
    /**
     * @return integer
     */
    public function getLifetime()
    {
        if(null === $this->_lifetime)
        {
            $this->_lifetime = ini_get('session.gc_maxlifetime');
            if(!$this->_lifetime)
            {
                $this->_lifetime = 3600;
            }
        }
        return (integer) $this->_lifetime;
    }
    
    /**
     * @param integer $seconds
     * @return e_session_db
     */
    public function setLifetime($seconds = null)
    {
        $this->_lifetime = $seconds;
        return $this;
    }
    
    /**
     * Set session save handler
     * @return e_session_db
     */
    public function setSaveHandler()
    {
        session_set_save_handler(
            array($this, 'open'),
            array($this, 'close'),
            array($this, 'read'),
            array($this, 'write'),
            array($this, 'destroy'),
            array($this, 'gc')
        );
        return $this;
    }
    
    /**
     * Open session, parameters are ignored (see e_session handler)
     * @param string $save_path
     * @param string $sess_name
     * @return boolean
     */
    public function open($save_path, $sess_name)
    {
        return true;
    }
    
    /**
     * Close session
     * @return boolean
     */
    public function close()
    {
        $this->gc($this->getLifetime());
        return true;
    }
    
    /**
     * Get session data
     * @param string $session_id
     * @return string
     */
    public function read($session_id)
    {
        $data = false;
        $check = $this->_db->select($this->getTable(), 'session_data', "session_id='".$this->_sanitize($session_id)."' AND session_expires>".time());
        if($check)
        {
            $tmp = $this->_db->fetch();
            $data = base64_decode($tmp['session_data']);
        }
        elseif(false !== $check)
        {
            $data = '';
        }
        return $data;
    }
    
    /**
     * Write session data
     * @param string $session_id
     * @param string $session_data
     * @return boolean
     */
    public function write($session_id, $session_data)
    {
        $data = array(
            'data' => array(
                'session_expires' => time() + $this->getLifetime(),
                'session_data'       => base64_encode($session_data),
                'session_user'    => defset('USERID'),
            ),
            '_FIELD_TYPES' => array(
                'session_id'        => 'str',
                'session_expires'    => 'int',
                'session_user'      => 'int',
                'session_data'        => 'str'
            ),
            '_DEFAULT' => 'str'
        );
        if(!($session_id = $this->_sanitize($session_id)))
        {
            return false;
        }

        $check = $this->_db->select($this->getTable(), 'session_id', "`session_id`='{$session_id}'");
        
        if($check)
        {
            $data['WHERE'] = "`session_id`='{$session_id}'";
            if(false !== $this->_db->update($this->getTable(), $data))
            {
                return true;
            }
        }
        else
        {
            $data['data']['session_id'] = $session_id;
            if($this->_db->insert($this->getTable(), $data))
            {
                return true;
            }    
        }
        return false;
    }
    
    /**
     * Destroy session
     * @param string $session_id
     * @return boolean
     */
    public function destroy($session_id)
    {
        $session_id = $this->_sanitize($session_id);
        $this->_db->delete($this->getTable(), "`session_id`='{$session_id}'");
        return true;
    }
    
    /**
     * Garbage collection
     * @param integer $session_maxlf ignored - see write()
     * @return boolean
     */
    public function gc($session_maxlf)
    {
        $this->_db->delete($this->getTable(), '`session_expires`<'.time());
        return true;
    }
    
    /**
     * Allow only well formed session id string 
     * @param string $session_id
     * @return string
     */
    protected function _sanitize($session_id)
    {
        return preg_replace('#[^0-9a-zA-Z,-]#', '', $session_id);
    }
}