1<?php 2/********************************************************************* 3 class.ostsession.php 4 5 Custom osTicket session handler. 6 7 Peter Rotich <peter@osticket.com> 8 Copyright (c) 2006-2013 osTicket 9 http://www.osticket.com 10 11 Released under the GNU General Public License WITHOUT ANY WARRANTY. 12 See LICENSE.TXT for details. 13 14 vim: expandtab sw=4 ts=4 sts=4: 15**********************************************************************/ 16 17class osTicketSession { 18 static $backends = array( 19 'db' => 'DbSessionBackend', 20 'memcache' => 'MemcacheSessionBackend', 21 'system' => 'FallbackSessionBackend', 22 ); 23 24 var $ttl = SESSION_TTL; 25 var $data = ''; 26 var $data_hash = ''; 27 var $id = ''; 28 var $backend; 29 30 function __construct($ttl=0){ 31 $this->ttl = $ttl ?: ini_get('session.gc_maxlifetime') ?: SESSION_TTL; 32 33 // Set osTicket specific session name. 34 session_name('OSTSESSID'); 35 36 // Forced cleanup on shutdown 37 register_shutdown_function('session_write_close'); 38 39 // Set session cleanup time to match TTL 40 ini_set('session.gc_maxlifetime', $ttl); 41 42 // Skip db version check if version is later than 1.7 43 if (!defined('MAJOR_VERSION') && OsticketConfig::getDBVersion()) 44 return session_start(); 45 46 # Cookies 47 // Avoid setting a cookie domain without a dot, thanks 48 // http://stackoverflow.com/a/1188145 49 $domain = null; 50 if (isset($_SERVER['HTTP_HOST']) 51 && strpos($_SERVER['HTTP_HOST'], '.') !== false 52 && !Validator::is_ip($_SERVER['HTTP_HOST'])) 53 // Remote port specification, as it will make an invalid domain 54 list($domain) = explode(':', $_SERVER['HTTP_HOST']); 55 56 session_set_cookie_params($ttl, ROOT_PATH, $domain, 57 osTicket::is_https(), true); 58 59 if (!defined('SESSION_BACKEND')) 60 define('SESSION_BACKEND', 'db'); 61 62 try { 63 $bk = SESSION_BACKEND; 64 if (!class_exists(self::$backends[$bk])) 65 $bk = 'db'; 66 $this->backend = new self::$backends[$bk]($this->ttl); 67 } 68 catch (Exception $x) { 69 // Use the database for sessions 70 trigger_error($x->getMessage(), E_USER_WARNING); 71 $this->backend = new self::$backends['db']($this->ttl); 72 } 73 74 if ($this->backend instanceof SessionBackend) { 75 // Set handlers. 76 session_set_save_handler( 77 array($this->backend, 'open'), 78 array($this->backend, 'close'), 79 array($this->backend, 'read'), 80 array($this->backend, 'write'), 81 array($this->backend, 'destroy'), 82 array($this->backend, 'gc') 83 ); 84 } 85 86 // Start the session. 87 session_start(); 88 } 89 90 function regenerate_id(){ 91 $oldId = session_id(); 92 session_regenerate_id(); 93 $this->backend->destroy($oldId); 94 } 95 96 static function destroyCookie() { 97 setcookie(session_name(), 'deleted', 1, 98 ini_get('session.cookie_path'), 99 ini_get('session.cookie_domain'), 100 ini_get('session.cookie_secure'), 101 ini_get('session.cookie_httponly')); 102 } 103 104 static function renewCookie($baseTime=false, $window=false) { 105 setcookie(session_name(), session_id(), 106 ($baseTime ?: time()) + ($window ?: SESSION_TTL), 107 ini_get('session.cookie_path'), 108 ini_get('session.cookie_domain'), 109 ini_get('session.cookie_secure'), 110 ini_get('session.cookie_httponly')); 111 } 112 113 /* helper functions */ 114 115 function get_online_users($sec=0){ 116 $sql='SELECT user_id FROM '.SESSION_TABLE.' WHERE user_id>0 AND session_expire>NOW()'; 117 if($sec) 118 $sql.=" AND TIME_TO_SEC(TIMEDIFF(NOW(),session_updated))<$sec"; 119 120 $users=array(); 121 if(($res=db_query($sql)) && db_num_rows($res)) { 122 while(list($uid)=db_fetch_row($res)) 123 $users[] = $uid; 124 } 125 126 return $users; 127 } 128 129 /* ---------- static function ---------- */ 130 static function start($ttl=0) { 131 return new static($ttl); 132 } 133} 134 135abstract class SessionBackend { 136 var $isnew = false; 137 var $ttl; 138 139 function __construct($ttl=SESSION_TTL) { 140 $this->ttl = $ttl; 141 } 142 143 function open($save_path, $session_name) { 144 return true; 145 } 146 147 function close() { 148 return true; 149 } 150 151 function getTTL() { 152 return $this->ttl; 153 } 154 155 function write($id, $data) { 156 // Last chance session update 157 $i = new ArrayObject(array('touched' => false)); 158 Signal::send('session.close', null, $i); 159 return $this->update($id, $i['touched'] ? session_encode() : $data); 160 } 161 162 function cleanup() { 163 $this->gc(0); 164 } 165 166 abstract function read($id); 167 abstract function update($id, $data); 168 abstract function destroy($id); 169 abstract function gc($maxlife); 170} 171 172class SessionData 173extends VerySimpleModel { 174 static $meta = array( 175 'table' => SESSION_TABLE, 176 'pk' => array('session_id'), 177 ); 178} 179 180class DbSessionBackend 181extends SessionBackend { 182 var $data = null; 183 184 function read($id) { 185 try { 186 $this->data = SessionData::objects() 187 ->filter(['session_id' => $id]) 188 ->annotate(array('is_expired' => 189 new SqlExpr(new Q(array('session_expire__lt' => SqlFunction::NOW()))))) 190 ->one(); 191 192 if ($this->data->is_expired > 0) { 193 // session_expire is in the past. Pretend it is expired and 194 // reset the data. This will assist with CSRF issues 195 $this->data->session_data=''; 196 } 197 $this->id = $id; 198 } 199 catch (DoesNotExist $e) { 200 $this->data = new SessionData(['session_id' => $id, 'session_data' => '']); 201 } 202 catch (OrmException $e) { 203 return false; 204 } 205 // Verify the User Agent string 206 if (isset($this->data->user_agent) 207 && (strcmp($_SERVER['HTTP_USER_AGENT'], $this->data->user_agent) !== 0)) { 208 $this->destroy($id); 209 return false; 210 } 211 return $this->data->session_data; 212 } 213 214 function update($id, $data){ 215 global $thisstaff; 216 217 if (defined('DISABLE_SESSION') && $this->data->__new__) 218 return true; 219 220 $ttl = $this && method_exists($this, 'getTTL') 221 ? $this->getTTL() : SESSION_TTL; 222 223 // Create a session data obj if not loaded. 224 if (!isset($this->data)) 225 $this->data = new SessionData(['session_id' => $id]); 226 227 $this->data->session_data = $data; 228 $this->data->session_expire = 229 SqlFunction::NOW()->plus(SqlInterval::SECOND($ttl)); 230 $this->data->user_id = $thisstaff ? $thisstaff->getId() : 0; 231 $this->data->user_ip = $_SERVER['REMOTE_ADDR']; 232 $this->data->user_agent = $_SERVER['HTTP_USER_AGENT']; 233 234 return $this->data->save(); 235 } 236 237 function destroy($id){ 238 return SessionData::objects()->filter(['session_id' => $id])->delete() ? true : false; 239 } 240 241 function cleanup() { 242 self::gc(0); 243 } 244 245 function gc($maxlife){ 246 SessionData::objects()->filter([ 247 'session_expire__lte' => SqlFunction::NOW() 248 ])->delete(); 249 } 250} 251 252class MemcacheSessionBackend 253extends SessionBackend { 254 var $memcache; 255 var $servers = array(); 256 257 function __construct($ttl) { 258 parent::__construct($ttl); 259 260 if (!extension_loaded('memcache')) 261 throw new Exception('Memcached extension is missing'); 262 if (!defined('MEMCACHE_SERVERS')) 263 throw new Exception('MEMCACHE_SERVERS must be defined'); 264 265 $servers = explode(',', MEMCACHE_SERVERS); 266 $this->memcache = new Memcache(); 267 268 foreach ($servers as $S) { 269 @list($host, $port) = explode(':', $S); 270 if (strpos($host, '/') !== false) 271 // Use port '0' for unix sockets 272 $port = 0; 273 else 274 $port = $port ?: ini_get('memcache.default_port') ?: 11211; 275 $this->servers[] = array(trim($host), (int) trim($port)); 276 // FIXME: Crash or warn if invalid $host or $port 277 } 278 } 279 280 function getKey($id) { 281 return sha1($id.SECRET_SALT); 282 } 283 284 function read($id) { 285 $key = $this->getKey($id); 286 287 // Try distributed read first 288 foreach ($this->servers as $S) { 289 list($host, $port) = $S; 290 $this->memcache->addServer($host, $port); 291 } 292 $data = $this->memcache->get($key); 293 294 // Read from other servers on failure 295 if ($data === false && count($this->servers) > 1) { 296 foreach ($this->servers as $S) { 297 list($host, $port) = $S; 298 $this->memcache->pconnect($host, $port); 299 if ($data = $this->memcache->get($key)) 300 break; 301 } 302 303 } 304 305 // No session data on record -- new session 306 $this->isnew = $data === false; 307 308 return $data ?: ''; 309 } 310 311 function update($id, $data) { 312 if (defined('DISABLE_SESSION') && $this->isnew) 313 return true; 314 315 $key = $this->getKey($id); 316 foreach ($this->servers as $S) { 317 list($host, $port) = $S; 318 $this->memcache->pconnect($host, $port); 319 if (!$this->memcache->replace($key, $data, 0, $this->getTTL())); 320 $this->memcache->set($key, $data, 0, $this->getTTL()); 321 } 322 323 return true; 324 325 } 326 327 function destroy($id) { 328 $key = $this->getKey($id); 329 foreach ($this->servers as $S) { 330 list($host, $port) = $S; 331 $this->memcache->pconnect($host, $port); 332 $this->memcache->replace($key, '', 0, 1); 333 $this->memcache->delete($key, 0); 334 } 335 336 return true; 337 } 338 339 function gc($maxlife) { 340 // Memcache does this automatically 341 } 342} 343 344class FallbackSessionBackend { 345 // Use default PHP settings, with some edits for best experience 346 function __construct() { 347 // FIXME: Consider extra possible security tweaks such as adjusting 348 // the session.save_path 349 } 350} 351 352?> 353