Skip to content
Snippets Groups Projects
class.ostsession.php 9.43 KiB
Newer Older
Jared Hancock's avatar
Jared Hancock committed
<?php
/*********************************************************************
    class.ostsession.php

    Custom osTicket session handler.

    Peter Rotich <peter@osticket.com>
    Copyright (c)  2006-2013 osTicket
Jared Hancock's avatar
Jared Hancock committed
    http://www.osticket.com

    Released under the GNU General Public License WITHOUT ANY WARRANTY.
    See LICENSE.TXT for details.

    vim: expandtab sw=4 ts=4 sts=4:
**********************************************************************/

class osTicketSession {
    static $backends = array(
        'db'        => 'DbSessionBackend',
        'memcache'  => 'MemcacheSessionBackend',
        'system'    => 'FallbackSessionBackend',
    );
Jared Hancock's avatar
Jared Hancock committed

    var $ttl = SESSION_TTL;
    var $data = '';
    var $data_hash = '';
    var $id = '';
    function __construct($ttl=0){
        $this->ttl = $ttl ?: ini_get('session.gc_maxlifetime') ?: SESSION_TTL;
        // Set osTicket specific session name.
        session_name('OSTSESSID');

        // Forced cleanup on shutdown
        register_shutdown_function('session_write_close');

        // Set session cleanup time to match TTL
        ini_set('session.gc_maxlifetime', $ttl);

        if (OsticketConfig::getDBVersion())
            return session_start();

        # Cookies
        // Avoid setting a cookie domain without a dot, thanks
        // http://stackoverflow.com/a/1188145
        $domain = null;
        if (isset($_SERVER['HTTP_HOST'])
                && strpos($_SERVER['HTTP_HOST'], '.') !== false
                && !Validator::is_ip($_SERVER['HTTP_HOST']))
            // Remote port specification, as it will make an invalid domain
            list($domain) = explode(':', $_SERVER['HTTP_HOST']);

        session_set_cookie_params($ttl, ROOT_PATH, $domain,
JediKev's avatar
JediKev committed
            osTicket::is_https(), true);
        if (!defined('SESSION_BACKEND'))
            define('SESSION_BACKEND', 'db');

        try {
            $bk = SESSION_BACKEND;
            if (!class_exists(self::$backends[$bk]))
                $bk = 'db';
            $this->backend = new self::$backends[$bk]($this->ttl);
        }
        catch (Exception $x) {
            // Use the database for sessions
            trigger_error($x->getMessage(), E_USER_WARNING);
            $this->backend = new self::$backends['db']($this->ttl);
        }

        if ($this->backend instanceof SessionBackend) {
            // Set handlers.
            session_set_save_handler(
                array($this->backend, 'open'),
                array($this->backend, 'close'),
                array($this->backend, 'read'),
                array($this->backend, 'write'),
                array($this->backend, 'destroy'),
                array($this->backend, 'gc')
            );
        }

        // Start the session.
Jared Hancock's avatar
Jared Hancock committed
        session_start();
    }
Jared Hancock's avatar
Jared Hancock committed
    function regenerate_id(){
        $oldId = session_id();
        session_regenerate_id();
        $this->backend->destroy($oldId);
    static function destroyCookie() {
        setcookie(session_name(), 'deleted', 1,
            ini_get('session.cookie_path'),
            ini_get('session.cookie_domain'),
            ini_get('session.cookie_secure'),
            ini_get('session.cookie_httponly'));
    }

    static function renewCookie($baseTime=false, $window=false) {
        setcookie(session_name(), session_id(),
            ($baseTime ?: time()) + ($window ?: SESSION_TTL),
            ini_get('session.cookie_path'),
            ini_get('session.cookie_domain'),
            ini_get('session.cookie_secure'),
            ini_get('session.cookie_httponly'));
    }

    /* helper functions */

    function get_online_users($sec=0){
        $sql='SELECT user_id FROM '.SESSION_TABLE.' WHERE user_id>0 AND session_expire>NOW()';
        if($sec)
            $sql.=" AND TIME_TO_SEC(TIMEDIFF(NOW(),session_updated))<$sec";

        $users=array();
        if(($res=db_query($sql)) && db_num_rows($res)) {
            while(list($uid)=db_fetch_row($res))
                $users[] = $uid;
        }

        return $users;
    }

    /* ---------- static function ---------- */
    static function start($ttl=0) {
        return new static($ttl);
    }
}

abstract class SessionBackend {
    var $isnew = false;
    var $ttl;

    function __construct($ttl=SESSION_TTL) {
    function open($save_path, $session_name) {
        return true;
    function close() {
        return true;
    }

    function getTTL() {
        return $this->ttl;
    }

    function write($id, $data) {
        // Last chance session update
        $i = new ArrayObject(array('touched' => false));
        Signal::send('session.close', null, $i);
        return $this->update($id, $i['touched'] ? session_encode() : $data);
    }

JediKev's avatar
JediKev committed
    function cleanup() {
        $this->gc(0);
    }

    abstract function read($id);
    abstract function update($id, $data);
    abstract function destroy($id);
    abstract function gc($maxlife);
}

class SessionData
extends VerySimpleModel {
    static $meta = array(
        'table' => SESSION_TABLE,
        'pk' => array('session_id'),
    );
}

class DbSessionBackend
extends SessionBackend {
    function read($id) {
        try {
            $this->data = SessionData::objects()->filter([
                'session_id' => $id,
                'session_expire__gt' => SqlFunction::NOW(),
            ])->one();
            $this->id = $id;
        }
        catch (DoesNotExist $e) {
            $this->data = new SessionData(['session_id' => $id]);
        }
        catch (OrmException $e) {
            return false;
        }
        return $this->data->session_data;
    function update($id, $data){
        if (defined('DISABLE_SESSION') && $this->data->__new__)
            return true;
        $ttl = $this && method_exists($this, 'getTTL')
            ? $this->getTTL() : SESSION_TTL;

        // Create a session data obj if not loaded.
        if (!isset($this->data))
            $this->data = new SessionData(['session_id' => $id]);

        $this->data->session_data = $data;
        $this->data->session_expire =
            SqlFunction::NOW()->plus(SqlInterval::SECOND($ttl));
        $this->data->user_id = $thisstaff ? $thisstaff->getId() : 0;
        $this->data->user_ip = $_SERVER['REMOTE_ADDR'];
        $this->data->user_agent = $_SERVER['HTTP_USER_AGENT'];
        return $this->data->save();
Jared Hancock's avatar
Jared Hancock committed
    }

    function destroy($id){
        return SessionData::objects()->filter(['session_id' => $id])->delete();
JediKev's avatar
JediKev committed
    function cleanup() {
        self::gc(0);
    }

Jared Hancock's avatar
Jared Hancock committed
    function gc($maxlife){
        SessionData::objects()->filter([
            'session_expire__lte' => SqlFunction::NOW()
        ])->delete();
class MemcacheSessionBackend
extends SessionBackend {
    var $memcache;
    var $servers = array();
    function __construct($ttl) {
        parent::__construct($ttl);

        if (!extension_loaded('memcache'))
            throw new Exception('Memcached extension is missing');
        if (!defined('MEMCACHE_SERVERS'))
            throw new Exception('MEMCACHE_SERVERS must be defined');

        $servers = explode(',', MEMCACHE_SERVERS);
        $this->memcache = new Memcache();

        foreach ($servers as $S) {
            @list($host, $port) = explode(':', $S);
            if (strpos($host, '/') !== false)
                // Use port '0' for unix sockets
                $port = 0;
            else
                $port = $port ?: ini_get('memcache.default_port') ?: 11211;
            $this->servers[] = array(trim($host), (int) trim($port));
            // FIXME: Crash or warn if invalid $host or $port
        }
    function getKey($id) {
        return sha1($id.SECRET_SALT);
    }
    function read($id) {
        $key = $this->getKey($id);

        // Try distributed read first
        foreach ($this->servers as $S) {
            list($host, $port) = $S;
            $this->memcache->addServer($host, $port);
        }
        $data = $this->memcache->get($key);

        // Read from other servers on failure
        if ($data === false && count($this->servers) > 1) {
            foreach ($this->servers as $S) {
                list($host, $port) = $S;
                $this->memcache->pconnect($host, $port);
                if ($data = $this->memcache->get($key))
                    break;
            }
        // No session data on record -- new session
        $this->isnew = $data === false;

        return $data;
    function update($id, $data) {
        if (defined('DISABLE_SESSION') && $this->isnew)
            return;

        $key = $this->getKey($id);
        foreach ($this->servers as $S) {
            list($host, $port) = $S;
            $this->memcache->pconnect($host, $port);
            if (!$this->memcache->replace($key, $data, 0, $this->getTTL()));
                $this->memcache->set($key, $data, 0, $this->getTTL());
        }
    }

    function destroy($id) {
        $key = $this->getKey($id);
        foreach ($this->servers as $S) {
            list($host, $port) = $S;
            $this->memcache->pconnect($host, $port);
            $this->memcache->replace($key, '', 0, 1);
            $this->memcache->delete($key, 0);
        }
    }

    function gc($maxlife) {
        // Memcache does this automatically

class FallbackSessionBackend {
    // Use default PHP settings, with some edits for best experience
    function __construct() {
        // FIXME: Consider extra possible security tweaks such as adjusting
        // the session.save_path
    }
}