<?php
/**
 * +-----------------------------------------------------------------------+
 * | Copyright (c) 2004, Tony Bibbs                                        |
 * | All rights reserved.                                                  |
 * |                                                                       |
 * | Redistribution and use in source and binary forms, with or without    |
 * | modification, are permitted provided that the following conditions    |
 * | are met:                                                              |
 * |                                                                       |
 * | o Redistributions of source code must retain the above copyright      |
 * |   notice, this list of conditions and the following disclaimer.       |
 * | o Redistributions in binary form must reproduce the above copyright   |
 * |   notice, this list of conditions and the following disclaimer in the |
 * |   documentation and/or other materials provided with the distribution.|
 * | o The names of the authors may not be used to endorse or promote      |
 * |   products derived from this software without specific prior written  |
 * |   permission.                                                         |
 * |                                                                       |
 * | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS   |
 * | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT     |
 * | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
 * | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT  |
 * | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
 * | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT      |
 * | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
 * | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
 * | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT   |
 * | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
 * | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.  |
 * |                                                                       |
 * +-----------------------------------------------------------------------+
 * | Author: Tony Bibbs <tony@geeklog.net>                                 |
 * +-----------------------------------------------------------------------+
 *
 * PHP version 5
 *
 * @category HTTP
 * @package  HTTP_Session2
 * @author   Alexander Radivaniovich <info@wwwlab.net>
 * @author   Tony Bibbs <tony@geeklog.net>
 * @license  http://www.opensource.org/licenses/bsd-license.php The BSD License
 * @version  CVS: $Id: Session2.php 267739 2008-10-25 16:54:23Z till $
 * @link     http://pear.php.net/package/HTTP_Session2
 */

/**
 * HTTP_Session2_Exception
 */
require_once 'Session2/Exception.php';

/**
 * Class for managing HTTP sessions
 *
 * Provides access to session-state values as well as session-level
 * settings and lifetime management methods.
 * Based on the standart PHP session handling mechanism
 * it provides for you more advanced features such as
 * database container, idle and expire timeouts, etc.
 *
 * Expample 1:
 *
 * <code>
 * // Setting some options and detecting of a new session
 * HTTP_Session2::useCookies(false);
 * HTTP_Session2::start('MySessionID');
 * HTTP_Session2::set('variable', 'The string');
 * if (HTTP_Session2::isNew()) {
 *     echo 'new session was created with the current request';
 *     $visitors++; // Increase visitors count
 * }
 *
 * //HTTP_Session2::regenerateId();
 * </code>
 *
 * Example 2:
 *
 * <code>
 * // Using database container
 * HTTP_Session2::setContainer('DB');
 * HTTP_Session2::start();
 * </code>
 *
 * Example 3:
 *
 * <code>
 * // Setting timeouts
 * HTTP_Session2::start();
 * HTTP_Session2::setExpire(time() + 60 * 60); // expires in one hour
 * HTTP_Session2::setIdle(10 * 60);            // idles in ten minutes
 * if (HTTP_Session2::isExpired()) {
 *     // expired
 *     echo('Your session is expired!');
 *     HTTP_Session2::destroy();
 * }
 * if (HTTP_Session2::isIdle()) {
 *     // idle
 *     echo('You've been idle for too long!');
 *     HTTP_Session2::destroy();
 * }
 * HTTP_Session2::updateIdle();
 * </code>
 *
 * @category HTTP
 * @package  HTTP_Session2
 * @author   Alexander Radivaniovich <info@wwwlab.net>
 * @author   Tony Bibbs <tony@geeklog.net>
 * @license  http://www.opensource.org/licenses/bsd-license.php The BSD License
 * @version  Release: @package_version@
 * @link     http://pear.php.net/package/HTTP_Session2
 */
class HTTP_Session2
{
    /**
     * @const STARTED - The session was started with the current request
     */
    const STARTED = 1;

    /**
     * @const CONTINUE - No new session was started with the current request
     */
    const CONTINUED = 2;

    /**
     * @const ERR_UNKNOWN_CONTAINER - Container not found.
     */
    const ERR_UNKNOWN_CONTAINER = 667;

    /**
     * @const ERR_SYSTEM_PERM - System permissions not sufficient.
     *        E.g. Not enough permissions to override ini-settings.
     */
    const ERR_SYSTEM_PERM = 668;
    
    /**
     * @const ERR_SYSTEM_PRECONDITION - Precondition failed. E.g. error occured and 
     *        HTTP_Session2 can't start up, etc..
     */
    const ERR_SYSTEM_PRECONDITION = 669;

    /**
     * @const ERR_NOT_IMPLEMENTED Feature is not yet Implement in the container.
     */
    const ERR_NOT_IMPLEMENTED = 670;

    /**
     * Container instance
     */
    public static $container;

    /**
     * Sets user-defined session storage functions
     *
     * Sets the user-defined session storage functions which are used
     * for storing and retrieving data associated with a session.
     * This is most useful when a storage method other than
     * those supplied by PHP sessions is preferred.
     * i.e. Storing the session data in a local database.
     *
     * @param string $container         Name of the container (e.g. DB, MDB, ...).
     * @param array  $container_options Options, most likely an array.
     *
     * @return void
     * @see session_set_save_handler()
     */
    static function setContainer($container, $container_options = null)
    {
        $container_class     = 'HTTP_Session2_Container_' . $container;
        $container_classfile = 'HTTP/Session2/Container/' . $container . '.php';

        if (!class_exists($container_class)) {
            include_once $container_classfile;
        }
        if (!class_exists($container_class)) {
            throw new HTTP_Session2_Exception(
                "Container class, $container_class, does not exist",
                self::ERR_UNKNOWN_CONTAINER);
        }
        self::$container = new $container_class($container_options);

        self::$container->set();
    }

    /**
     * Initializes session data
     *
     * Creates a session (or resumes the current one
     * based on the session id being passed
     * via a GET variable or a cookie).
     * You can provide your own name and/or id for a session.
     *
     * @param string $name Name of a session, default is 'SessionID'
     * @param string $id   Id of a session which will be used
     *                     only when the session is new
     *
     * @return void
     * @see    session_name()
     * @see    session_id()
     * @see    session_start()
     */
    public static function start($name = 'SessionID', $id = null)
    {
        self::name($name);
        if (is_null(self::detectID())) {
            if ($id) {
                self::id($id);
            } else {
                self::id(uniqid(dechex(rand())));
            }
        }
        session_start();
        if (!isset($_SESSION['__HTTP_Session2_Info'])) {
            $_SESSION['__HTTP_Session2_Info'] = self::STARTED;
        } else {
            $_SESSION['__HTTP_Session2_Info'] = self::CONTINUED;
        }
    }

    /**
     * Writes session data and ends session
     *
     * Session data is usually stored after your script
     * terminated without the need to call HTTP_Session2::stop(),
     * but as session data is locked to prevent concurrent
     * writes only one script may operate on a session at any time.
     * When using framesets together with sessions you will
     * experience the frames loading one by one due to this
     * locking. You can reduce the time needed to load all the
     * frames by ending the session as soon as all changes
     * to session variables are done.
     *
     * @return void
     * @see    session_write_close()
     */
    public static function pause()
    {
        session_write_close();
    }

    /**
     * Frees all session variables and destroys all data
     * registered to a session
     *
     * This method resets the $_SESSION variable and
     * destroys all of the data associated
     * with the current session in its storage (file or DB).
     * It forces new session to be started after this method
     * is called. It does not unset the session cookie.
     *
     * @return void
     * @see    session_unset()
     * @see    session_destroy()
     */
    public static function destroy()
    {
        session_unset();
        session_destroy();
    }

    /**
     * Free all session variables
     *
     * @todo   TODO Save expire and idle timestamps?
     * @return void
     */
    public static function clear()
    {
        $info = $_SESSION['__HTTP_Session2_Info'];

        session_unset();

        $_SESSION['__HTTP_Session2_Info'] = $info;
    }

    /**
     * Tries to find any session id in $_GET, $_POST or $_COOKIE
     *
     * @return string Session ID (if exists) or null
     */
    public static function detectID()
    {
        if (self::useCookies()) {
            if (isset($_COOKIE[self::name()])) {
                return $_COOKIE[self::name()];
            }
        } else {
            if (isset($_GET[self::name()])) {
                return $_GET[self::name()];
            }
            if (isset($_POST[self::name()])) {
                return $_POST[self::name()];
            }
        }
        return null;
    }

    /**
     * Sets new name of a session
     *
     * @param string $name New name of a sesion
     *
     * @return string Previous name of a session
     * @see    session_name()
     */
    public static function name($name = null)
    {
        if (isset($name)) {
            return session_name($name);
        }
        return session_name();
    }

    /**
     * Sets new ID of a session
     *
     * @param string $id New ID of a sesion
     *
     * @return string Previous ID of a session
     * @see    session_id()
     */
    public static function id($id = null)
    {
        if (isset($id)) {
            return session_id($id);
        }
        return session_id();
    }

    /**
     * Sets the maximum expire time
     *
     * @param integer $time Time in seconds
     * @param bool    $add  Add time to current expire time or not
     *
     * @return void
     */
    public static function setExpire($time, $add = false)
    {
        if ($add && isset($_SESSION['__HTTP_Session2_Expire'])) {
            $_SESSION['__HTTP_Session2_Expire'] += $time;
        } else {
            $_SESSION['__HTTP_Session2_Expire'] = $time;
        }
        if (!isset($_SESSION['__HTTP_Session2_Expire_TS'])) {
            $_SESSION['__HTTP_Session2_Expire_TS'] = time();
        }
    }

    /**
     * Sets the maximum idle time
     *
     * Sets the time-out period allowed
     * between requests before the session-state
     * provider terminates the session.
     *
     * @param integer $time Time in seconds
     * @param bool    $add  Add time to current maximum idle time or not
     *
     * @return void
     */
    public static function setIdle($time, $add = false)
    {
        if ($add && isset($_SESSION['__HTTP_Session2_Idle'])) {
            $_SESSION['__HTTP_Session2_Idle'] += $time;
        } else {
            $_SESSION['__HTTP_Session2_Idle'] = $time;
        }
        if (!isset($_SESSION['__HTTP_Session2_Idle_TS'])) {
            $_SESSION['__HTTP_Session2_Idle_TS'] = time();
        }
    }

    /**
     * Returns the time up to the session is valid
     *
     * @return integer Time when the session idles
     */
    public static function sessionValidThru()
    {
        if (
            !isset($_SESSION['__HTTP_Session2_Idle_TS'])
            || !isset($_SESSION['__HTTP_Session2_Idle'])) {
            return 0;
        }
        return $_SESSION['__HTTP_Session2_Idle_TS']
            + $_SESSION['__HTTP_Session2_Idle'];
    }

    /**
     * Check if session is expired
     *
     * @return boolean
     */
    public static function isExpired()
    {
        if (
            isset($_SESSION['__HTTP_Session2_Expire'])
            && $_SESSION['__HTTP_Session2_Expire'] > 0
            && isset($_SESSION['__HTTP_Session2_Expire_TS'])
            &&
            (
                $_SESSION['__HTTP_Session2_Expire_TS']
                + $_SESSION['__HTTP_Session2_Expire']
            ) <= time()) {
            return true;
        }
        return false;
    }

    /**
     * Check if session is idle
     *
     * @return boolean Obvious
     */
    public static function isIdle()
    {
        if (
            isset($_SESSION['__HTTP_Session2_Idle'])
            && $_SESSION['__HTTP_Session2_Idle'] > 0
            && isset($_SESSION['__HTTP_Session2_Idle_TS'])
            && (
                $_SESSION['__HTTP_Session2_Idle_TS']
                + $_SESSION['__HTTP_Session2_Idle']
            ) <= time()) {
            return true;
        }
        return false;
    }

    /**
     * Updates the idletime
     *
     * @return void
     */
    public static function updateIdle()
    {
        if (isset($_SESSION['__HTTP_Session2_Idle_TS'])) {
            $_SESSION['__HTTP_Session2_Idle_TS'] = time();
        }
    }

    /**
     * If optional parameter is specified it indicates whether the module will
     * use cookies to store the session id on the client side in a cookie.
     *
     * By default this cookie will be deleted when the browser is closed!
     *
     * It will throw an Exception if it's not able to set the session.use_cookie
     * property.
     *
     * It returns the previous value of this property.
     *
     * @param boolean $useCookies If specified it will replace the previous value of
     *                            this property. By default 'null', which doesn't
     *                            change any setting on your system. If you supply a
     *                            parameter, please supply 'boolean'.
     *
     * @return boolean The previous value of the property
     * 
     * @throws HTTP_Session2_Exception If ini_set() fails!
     * @see    session_set_cookie_params()
     * @link   http://php.net/manual/en/function.session-set-cookie-params.php
     */
    public static function useCookies($useCookies = null)
    {
        $return = false;
        if (ini_get('session.use_cookies') == '1') {
            $return = true;
        }
        if ($useCookies !== null) {
            if ($useCookies === true) {
                $status = ini_set('session.use_cookies', 1);
            } else {
                $status = ini_set('session.use_cookies', 0);
            }
            if ($status === false) {
                $msg  = "Could not set 'session.use_cookies'. Please check your ";
                $msg .= 'permissions to override php.ini-settings. E.g. a possible ';
                $msg .= 'php_admin_value setting or blocked ini_set() calls ';
                throw new HTTP_Session2_Exception($msg, self::ERR_SYSTEM_PERM);
            }
        }
        return $return;
    }

    /**
     * Gets a value indicating whether the session
     * was created with the current request
     *
     * You MUST call this method only after you have started
     * the session with the HTTP_Session2::start() method.
     *
     * @return boolean true when the session was created with the current request
     *                 false otherwise
     *
     * @see  self::start()
     * @uses self::STARTED
     */
    public static function isNew()
    {
        // The best way to check if a session is new is to check
        // for existence of a session data storage
        // with the current session id, but this is impossible
        // with the default PHP module wich is 'files'.
        // So we need to emulate it.
        return !isset($_SESSION['__HTTP_Session2_Info']) ||
            $_SESSION['__HTTP_Session2_Info'] == self::STARTED;
    }

    /**
     * Register variable with the current session
     *
     * @param string $name Name of a global variable
     *
     * @return void
     * @see session_register()
     */
    public static function register($name)
    {
        session_register($name);
    }

    /**
     * Unregister a variable from the current session
     *
     * @param string $name Name of a global variable
     *
     * @return void
     * @see    session_unregister()
     */
    public static function unregister($name)
    {
        session_unregister($name);
    }

    /**
     * Returns session variable
     *
     * @param string $name    Name of a variable
     * @param mixed  $default Default value of a variable if not set
     *
     * @return mixed  Value of a variable
     */
    public static function &get($name, $default = null)
    {
        if (!isset($_SESSION[$name]) && isset($default)) {
            $_SESSION[$name] = $default;
        }
        return $_SESSION[$name];
    }

    /**
     * Sets session variable
     *
     * @param string $name  Name of a variable
     * @param mixed  $value Value of a variable
     *
     * @return mixed Old value of a variable
     */
    public static function set($name, $value)
    {
        $return = (isset($_SESSION[$name])) ? $_SESSION[$name] : null;
        if (null === $value) {
            unset($_SESSION[$name]);
        } else {
            $_SESSION[$name] = $value;
        }
        return $return;
    }

    /**
     * Returns local variable of a script
     *
     * Two scripts can have local variables with the same names
     *
     * @param string $name    Name of a variable
     * @param mixed  $default Default value of a variable if not set
     *
     * @return mixed  Value of a local variable
     */
    static function &getLocal($name, $default = null)
    {
        $local = md5(self::localName());
        if (!is_array($_SESSION[$local])) {
            $_SESSION[$local] = array();
        }
        if (!isset($_SESSION[$local][$name]) && isset($default)) {
            $_SESSION[$local][$name] = $default;
        }
        return $_SESSION[$local][$name];
    }

    /**
     * Sets local variable of a script.
     * Two scripts can have local variables with the same names.
     *
     * @param string $name  Name of a local variable
     * @param mixed  $value Value of a local variable
     *
     * @return mixed Old value of a local variable
     */
    static function setLocal($name, $value)
    {
        $local = md5(self::localName());
        if (!is_array($_SESSION[$local])) {
            $_SESSION[$local] = array();
        }
        $return = $_SESSION[$local][$name];
        if (null === $value) {
            unset($_SESSION[$local][$name]);
        } else {
            $_SESSION[$local][$name] = $value;
        }
        return $return;
    }

    /**
     * set the usage of transparent SID
     *
     * @param boolean $useTransSID Flag to use transparent SID
     *
     * @return boolean
     */
    static function useTransSID($useTransSID = false)
    {
        $return = ini_get('session.use_trans_sid') ? true : false;
        if ($useTransSID === false) {
            ini_set('session.use_trans_sid', $useTransSID ? 1 : 0);
        }
        return $return;
    }

    /**
     * Sets new local name
     *
     * @param string $name New local name
     *
     * @return string Previous local name
     */
    static function localName($name = null)
    {
        $return = '';
        if (isset($GLOBALS['__HTTP_Session2_Localname'])) {
            $return .= $GLOBALS['__HTTP_Session2_Localname'];
        }
        if (!empty($name)) {
            $GLOBALS['__HTTP_Session2_Localname'] = $name;
        }
        return $return;
    }

    /**
     * init
     *
     * @return void
     */
    static function init()
    {
        // Disable auto-start of a sesion
        ini_set('session.auto_start', 0);

        // Set local name equal to the current script name
        self::localName($_SERVER['SCRIPT_NAME']);
    }

    /**
     * Regenrates session id
     *
     * If session_regenerate_id() is not available emulates its functionality
     *
     * @param boolean $deleteOldSessionData Whether to delete data of old session
     *
     * @return boolean
     */
    public static function regenerateId($deleteOldSessionData = false)
    {
        if (function_exists('session_regenerate_id')) {
            return session_regenerate_id($deleteOldSessionData);

            // emulate session_regenerate_id()
        } else {

            do {
                $newId = uniqid(dechex(rand()));
            } while ($newId === session_id());

            if ($deleteOldSessionData) {
                session_unset();
            }

            session_id($newId);

            return true;
        }
    }

    /**
     * This function copies session data of specified id to specified table
     *
     * @param string $target Target to replicate to
     * @param string $id     Id of record to replicate
     *
     * @return boolean
     */
    public static function replicate($target, $id = null)
    {
        return self::$container->replicate($target, $id);
    }

    /**
     * If optional parameter is specified it determines the number of seconds
     * after which session data will be seen as 'garbage' and cleaned up
     *
     * It returns the previous value of this property
     *
     * @param int $gcMaxLifetime If specified it will replace the previous value of
     *                           this property, and must be integer.
     *
     * @return boolean The previous value of the property
     */
    public static function setGcMaxLifetime($gcMaxLifetime = null)
    {
        $return = ini_get('session.gc_maxlifetime');
        if (isset($gcMaxLifetime) && is_int($gcMaxLifetime) && $gcMaxLifetime >= 1) {
            ini_set('session.gc_maxlifetime', $gcMaxLifetime);
        }
        return $return;
    }

    /**
     * If optional parameter is specified it determines the
     * probability that the gc (garbage collection) routine is started
     * and session data is cleaned up
     *
     * It returns the previous value of this property
     *
     * @param int $gcProbability If specified it will replace the previous value of
     *                           this property.
     *
     * @return boolean The previous value of the property
     */
    public static function setGcProbability($gcProbability = null)
    {
        $return = ini_get('session.gc_probability');
        if (isset($gcProbability)  &&
            is_int($gcProbability) &&
            $gcProbability >= 1    &&
            $gcProbability <= 100) {
            ini_set('session.gc_probability', $gcProbability);
        }
        return $return;
    }
}

/**
 * init {@link HTTP_Session2}
 * 
 * @see HTTP_Session2::init()
 */
HTTP_Session2::init();
