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