Copyright (c) 2006-2013 osTicket 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', ); var $ttl = SESSION_TTL; var $data = ''; var $data_hash = ''; var $id = ''; var $backend; 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); // Skip db version check if version is later than 1.7 if (!defined('MAJOR_VERSION') && 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, 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. session_start(); } 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) { $this->ttl = $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); } 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 { var $data = null; function read($id) { try { $this->data = SessionData::objects() ->filter(['session_id' => $id]) ->annotate(array('is_expired' => new SqlExpr(new Q(array('session_expire__lt' => SqlFunction::NOW()))))) ->one(); if ($this->data->is_expired > 0) { // session_expire is in the past. Pretend it is expired and // reset the data. This will assist with CSRF issues $this->data->session_data=''; } $this->id = $id; } catch (DoesNotExist $e) { $this->data = new SessionData(['session_id' => $id, 'session_data' => '']); } catch (OrmException $e) { return false; } // Verify the User Agent string if (isset($this->data->user_agent) && (strcmp($_SERVER['HTTP_USER_AGENT'], $this->data->user_agent) !== 0)) { $this->destroy($id); return false; } return $this->data->session_data; } function update($id, $data){ global $thisstaff; 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(); } function destroy($id){ return SessionData::objects()->filter(['session_id' => $id])->delete() ? true : false; } function cleanup() { self::gc(0); } 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 true; $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()); } return true; } 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); } return true; } 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 } } ?>