· 6 years ago · Apr 24, 2019, 02:16 PM
1
2 <?php
3 class Zebra_Session extends SessionHandler
4 {
5 private $flashdata;
6 private $flashdata_varname;
7 private $session_lifetime;
8 private $lock_timeout;
9 private $lock_to_ip;
10 private $lock_to_user_agent;
11 private $db = null;
12 private $new_sessions = [];
13
14 /**
15 * Constructor of class. Initializes the class and automatically calls
16 * {@link http://php.net/manual/en/function.session-start.php start_session()}.
17 *
18 * <code>
19 * // first, connect to a database containing the sessions table
20 *
21 * // include the class
22 * require 'path/to/Zebra_Session.php';
23 *
24 * // start the session
25 * // where $link is a connection link returned by mysqli_connect
26 * $session = new Zebra_Session($link, 'sEcUr1tY_c0dE');
27 * </code>
28 *
29 * By default, the cookie used by PHP to propagate session data across multiple pages ('PHPSESSID') uses the
30 * current top-level domain and subdomain in the cookie declaration.
31 *
32 * Example: www.domain.com
33
34 * <code>
35 * // takes the domain and removes the subdomain
36 * // blog.domain.com becoming .domain.com
37 * ini_set(
38 * 'session.cookie_domain',
39 * substr($_SERVER['SERVER_NAME'], strpos($_SERVER['SERVER_NAME'], '.'))
40 * );
41 * </code>
42 * @param resource $link An object representing the connection to a MySQL Server, as returned
43 * by calling {@link http://www.php.net/manual/en/mysqli.construct.php mysqli_connect}.
44 *
45 * If you use {@link http://stefangabos.ro/php-libraries/zebra-database/ Zebra_Database}
46 * to connect to the database, you can get the connection to the MySQL server
47 * via Zebra_Database's {@link http://stefangabos.ro/wp-content/docs/Zebra_Database/Zebra_Database/Zebra_Database.html#methodget_link get_link}
48 * method
49 * @param string $security_code The value of this argument is appended to the string created by
50 * concatenating the user's User Agent (browser) string (or an empty string
51 * if "lock_to_user_agent" is FALSE) and to the user's IP address (or an
52 * empty string if "lock_to_ip" is FALSE), before creating an MD5 hash out
53 * of it and storing it in the database.
54 *
55 * On each call this value will be generated again and compared to the
56 * value stored in the database ensuring that the session is correctly linked
57 * with the user who initiated the session thus preventing session hijacking.
58 *
59 * <samp>To prevent session hijacking, make sure you choose a string around
60 * 12 characters long containing upper- and lowercase letters, as well as
61 * digits. To simplify the process, use {@link https://www.random.org/passwords/?num=1&len=12&format=html&rnd=new this}
62 * link to generate such a random string.</samp>
63 * @param int $session_lifetime (Optional) The number of seconds after which a session will be considered
64 * as <i>expired</i>.
65 *
66 * Expired sessions are cleaned up from the database whenever the <i>garbage
67 * collection routine</i> is run. The probability of the <i>garbage collection
68 * routine</i> to be executed is given by the values of <i>$gc_probability</i>
69 * and <i>$gc_divisor</i>. See below.
70 *
71 * Default is the value of <i>session.gc_maxlifetime</i> as set in in php.ini.
72 * Read more at {@link http://www.php.net/manual/en/session.configuration.php}
73 *
74 * To clear any confusions that may arise: in reality, <i>session.gc_maxlifetime</i>
75 * does not represent a session's lifetime but the number of seconds after
76 * which a session is seen as <i>garbage</i> and is deleted by the <i>garbage
77 * collection routine</i>. The PHP setting that sets a session's lifetime is
78 * <i>session.cookie_lifetime</i> and is usually set to "0" - indicating that
79 * a session is active until the browser/browser tab is closed. When this class
80 * is used, a session is active until the browser/browser tab is closed and/or
81 * a session has been inactive for more than the number of seconds specified
82 * by <i>session.gc_maxlifetime</i>.
83 *
84 * To see the actual value of <i>session.gc_maxlifetime</i> for your
85 * environment, use the {@link get_settings()} method.
86 *
87 * Pass an empty string to keep default value
88 * @param bool $lock_to_user_agent (Optional) Whether to restrict the session to the same User Agent (or
89 * browser) as when the session was first opened.
90 *
91 * <i>The user agent check only adds minor security, since an attacker that
92 * hijacks the session cookie will most likely have the same user agent.</i>
93 *
94 * In certain scenarios involving Internet Explorer, the browser will randomly
95 * change the user agent string from one page to the next by automatically
96 * switching into compatibility mode. So, on the first load you would have
97 * something like:
98 *
99 * <code>Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; etc...</code>
100 *
101 * and reloading the page you would have
102 *
103 * <code> Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Trident/4.0; etc...</code>
104 *
105 * So, if the situation asks for this, change this value to FALSE.
106 *
107 * Default is TRUE
108 * @param bool $lock_to_ip (Optional) Whether to restrict the session to the same IP as when the
109 * session was first opened.
110 *
111 * Use this with caution as many users have dynamic IP addresses which may
112 * change over time, or may come through proxies.
113 *
114 * This is mostly useful if your know that all your users come from static IPs.
115 *
116 * Default is FALSE
117 * @param int $gc_probability (Optional) Used in conjunction with <i>$gc_divisor</i>. It defines the
118 * probability that the <i>garbage collection routine</i> is started.
119 *
120 * The probability is expressed by the formula:
121 *
122 * <code>
123 * $probability = $gc_probability / $gc_divisor;
124 * </code>
125 *
126 * So, if <i>$gc_probability</i> is 1 and <i>$gc_divisor</i> is 100, it means
127 * that there is a 1% chance the the <i>garbage collection routine</i> will
128 * be called on each request.
129 *
130 * Default is the value of <i>session.gc_probability</i> as set in php.ini.
131 * Read more at {@link http://www.php.net/manual/en/session.configuration.php}
132 *
133 * To see the actual value of <i>session.gc_probability</i> for your
134 * environment, and the computed <i>probability</i>, use the
135 * {@link get_settings()} method.
136 *
137 * Pass an empty string to keep default value
138 * @param int $gc_divisor (Optional) Used in conjunction with <i>$gc_probability</i>. It defines the
139 * probability that the <i>garbage collection routine</i> is started.
140 *
141 * The probability is expressed by the formula:
142 *
143 * <code>
144 * $probability = $gc_probability / $gc_divisor;
145 * </code>
146 *
147 * So, if <i>$gc_probability</i> is 1 and <i>$gc_divisor</i> is 100, it means
148 * that there is a 1% chance the the <i>garbage collection routine</i> will
149 * be called on each request.
150 *
151 * Default is the value of <i>session.gc_divisor</i> as set in php.ini.
152 * Read more at {@link http://www.php.net/manual/en/session.configuration.php}
153 *
154 * To see the actual value of <i>session.gc_divisor</i> for your
155 * environment, and the computed <i>probability</i>, use the
156 * {@link get_settings()} method.
157 *
158 * Default is <i>session_data</i>
159 * @param string $lock_timeout (Optional) The maximum amount of time (in seconds) for which a lock on
160 * the session data can be kept.
161 *
162 * <i>This must be lower than the maximum execution time of the script!</i>
163 *
164 * Session locking is a way to ensure that data is correctly handled in a
165 * scenario with multiple concurrent AJAX requests.
166 *
167 * Read more about it at
168 * {@link http://thwartedefforts.org/2006/11/11/race-conditions-with-ajax-and-php-sessions/}
169 *
170 * Default is <i>60</i>
171 */
172 public function __construct(&$db, $security_code, $session_lifetime = '', $lock_to_user_agent = true, $lock_to_ip = false, $gc_probability = '', $gc_divisor = '', $lock_timeout = 60)
173 {
174 // store the connection link
175 $this->db = $db;
176 // make sure session cookies never expire so that session lifetime
177 // will depend only on the value of $session_lifetime
178 ini_set('session.cookie_lifetime', 0);
179 // if $session_lifetime is specified and is an integer number
180 if ('' != $session_lifetime && is_integer($session_lifetime)) {
181 // set the new value
182 ini_set('session.gc_maxlifetime', (int) $session_lifetime);
183 }
184 // if $gc_probability is specified and is an integer number
185 if ('' != $gc_probability && is_integer($gc_probability)) {
186 // set the new value
187 ini_set('session.gc_probability', $gc_probability);
188 }
189 // if $gc_divisor is specified and is an integer number
190 if ('' != $gc_divisor && is_integer($gc_divisor)) {
191 // set the new value
192 ini_set('session.gc_divisor', $gc_divisor);
193 }
194 // get session lifetime
195 $this->session_lifetime = ini_get('session.gc_maxlifetime');
196 // we'll use this later on in order to try to prevent HTTP_USER_AGENT spoofing
197 $this->security_code = $security_code;
198 // some other defaults
199 $this->lock_to_user_agent = $lock_to_user_agent;
200 $this->lock_to_ip = $lock_to_ip;
201 // the maximum amount of time (in seconds) for which a process can lock the session
202 $this->lock_timeout = $lock_timeout;
203 // set parameters
204 session_set_cookie_params($session_lifetime, session_get_cookie_params()['path'], $_SERVER['HTTP_HOST'], true, true);
205 // register the new handler
206 session_set_save_handler([&$this, 'open'], [&$this, 'close'], [&$this, 'read'], [&$this, 'write'], [&$this, 'destroy'], [&$this, 'gc']);
207 // name the session
208 session_name(SESSION_NAME);
209 // start the session
210 session_start();
211 // the name for the session variable that will be created upon script execution
212 // and destroyed when instantiating this library, and which will hold information
213 // about flashdata session variables
214 $this->flashdata_varname = '_zebra_session_flashdata_ec3asbuiad';
215 // assume no flashdata
216 $this->flashdata = [];
217 // if there are any flashdata variables that need to be handled
218 if (isset($_SESSION[$this->flashdata_varname])) {
219 // store them
220 $this->flashdata = unserialize($_SESSION[$this->flashdata_varname]);
221 // and destroy the temporary session variable
222 unset($_SESSION[$this->flashdata_varname]);
223 }
224 // handle flashdata after script execution
225 register_shutdown_function([$this, '_manage_flashdata']);
226
227 return true;
228 }
229
230 /**
231 * Get the number of active sessions - sessions that have not expired.
232 *
233 * <i>The returned value does not represent the exact number of active users as some sessions may be unused
234 * although they haven't expired.</i>
235 *
236 * <code>
237 * // first, connect to a database containing the sessions table
238 *
239 * // include the class
240 * require 'path/to/Zebra_Session.php';
241 *
242 * // start the session
243 * // where $link is a connection link returned by mysqli_connect
244 * $session = new Zebra_Session($link, 'sEcUr1tY_c0dE');
245 *
246 * // get the (approximate) number of active sessions
247 * $active_sessions = $session->get_active_sessions();
248 * </code>
249 *
250 * @return int Returns the number of active (not expired) sessions
251 */
252 public function get_active_sessions()
253 {
254 // call the garbage collector
255 $this->gc();
256 $this->db->query('SELECT COUNT(session_id) FROM sessions');
257 $this->db->execute();
258
259 return $this->db->result();
260 }
261
262 /**
263 * Queries the system for the values of <i>session.gc_maxlifetime</i>, <i>session.gc_probability</i> and <i>session.gc_divisor</i>
264 * and returns them as an associative array.
265 *
266 * To view the result in a human-readable format use:
267 * <code>
268 * // include the class
269 * require 'path/to/Zebra_Session.php';
270 *
271 * // instantiate the class
272 * $session = new Zebra_Session();
273 *
274 * // get default settings
275 * print_r('<pre>');
276 * print_r($session->get_settings());
277 *
278 * // would output something similar to (depending on your actual settings)
279 * // Array
280 * // (
281 * // [session.gc_maxlifetime] => 1440 seconds (24 minutes)
282 * // [session.gc_probability] => 1
283 * // [session.gc_divisor] => 1000
284 * // [probability] => 0.1%
285 * // )
286 * </code>
287 *
288 * @since 1.0.8
289 *
290 * @return array Returns the values of <i>session.gc_maxlifetime</i>, <i>session.gc_probability</i> and <i>session.gc_divisor</i>
291 * as an associative array
292 */
293 public function get_settings()
294 {
295 // get the settings
296 $gc_maxlifetime = ini_get('session.gc_maxlifetime');
297 $gc_probability = ini_get('session.gc_probability');
298 $gc_divisor = ini_get('session.gc_divisor');
299 // return them as an array
300 return [
301 'session.gc_maxlifetime' => $gc_maxlifetime.' seconds ('.round($gc_maxlifetime / 60).' minutes)',
302 'session.gc_probability' => $gc_probability,
303 'session.gc_divisor' => $gc_divisor,
304 'probability' => $gc_probability / $gc_divisor * 100 .'%',
305 ];
306 }
307
308 /**
309 * Regenerates the session id.
310 *
311 * <b>Call this method whenever you do a privilege change in order to prevent session hijacking!</b>
312 *
313 * <code>
314 * // first, connect to a database containing the sessions table
315 *
316 * // include the class
317 * require 'path/to/Zebra_Session.php';
318 *
319 * // start the session
320 * // where $link is a connection link returned by mysqli_connect
321 * $session = new Zebra_Session($link, 'sEcUr1tY_c0dE');
322 *
323 * // regenerate the session's ID
324 * $session->regenerate_id();
325 * </code>
326 */
327 public function regenerate_id()
328 {
329 // regenerates the id (create a new session with a new id and containing the data from the old session)
330 // also, delete the old session
331 session_regenerate_id(false);
332 }
333
334 /**
335 * Sets a "flashdata" session variable which will only be available for the next server request, and which will be
336 * automatically deleted afterwards.
337 *
338 * Typically used for informational or status messages (for example: "data has been successfully updated").
339 *
340 * <code>
341 * // first, connect to a database containing the sessions table
342 *
343 * // include the library
344 * require 'path/to/Zebra_Session.php';
345 *
346 * // start the session
347 * // where $link is a connection link returned by mysqli_connect
348 * $session = new Zebra_Session($link, 'sEcUr1tY_c0dE');
349 *
350 * // set "myvar" which will only be available
351 * // for the next server request and will be
352 * // automatically deleted afterwards
353 * $session->set_flashdata('myvar', 'myval');
354 * </code>
355 *
356 * Flashdata session variables can be retrieved as any other session variable:
357 *
358 * <code>
359 * if (isset($_SESSION['myvar'])) {
360 * // do something here but remember that the
361 * // flashdata session variable is available
362 * // for a single server request after it has
363 * // been set!
364 * }
365 * </code>
366 *
367 * @param string $name The name of the session variable
368 * @param string $value The value of the session variable
369 */
370 public function set_flashdata($name, $value)
371 {
372 // set session variable
373 $_SESSION[$name] = $value;
374 // initialize the counter for this flashdata
375 $this->flashdata[$name] = 0;
376
377 return true;
378 }
379
380 /**
381 * Deletes all data related to the session.
382 *
383 * <code>
384 * // first, connect to a database containing the sessions table
385 *
386 * // include the class
387 * require 'path/to/Zebra_Session.php';
388 *
389 * // start the session
390 * // where $link is a connection link returned by mysqli_connect
391 * $session = new Zebra_Session($link, 'sEcUr1tY_c0dE');
392 *
393 * // end current session
394 * $session->stop();
395 * </code>
396 *
397 * @since 1.0.1
398 */
399 public function stop($session_id = null)
400 {
401 if ($this->mustRegenerate($session_id)) {
402 $this->regenerate_id();
403 }
404 session_unset();
405 session_destroy();
406
407 return true;
408 }
409
410 /**
411 * Custom close() function.
412 */
413 public function close()
414 {
415 // release the lock associated with the current session
416 $this->db->query('SELECT RELEASE_LOCK(?)');
417 $this->db->execute([$this->session_lock]);
418
419 return true;
420 }
421
422 /**
423 * Custom destroy() function.
424 */
425 public function destroy($session_id)
426 {
427 // deletes the current session id from the database
428 $this->db->query('DELETE FROM sessions WHERE session_id = ?');
429 $this->db->execute([$session_id]);
430
431 return (bool) $this->db->affected();
432 }
433
434 /**
435 * Custom gc() function (garbage collector).
436 */
437 public function gc($maxlifetime)
438 {
439 // deletes expired sessions from database
440 $this->db->query('DELETE FROM sessions WHERE session_expire < ?');
441 $this->db->execute([time()]);
442
443 return true;
444 }
445
446 /**
447 * Custom open() function.
448 */
449 public function open($save_path, $session_name)
450 {
451 return true;
452 }
453
454 /**
455 * Custom read() function.
456 */
457 public function read($session_id)
458 {
459 // setcookie(SESSION_NAME, $session_id);
460 // get the lock name, associated with the current session
461 $this->session_lock = 'session_'.$session_id;
462 // try to obtain a lock with the given name and timeout
463 $this->db->query('SELECT GET_LOCK(?, ?)');
464 $this->db->execute([$this->session_lock, $this->lock_timeout]);
465 $cnt = $this->db->count();
466 if (!$cnt) {
467 exit('Invalid lock status');
468 }
469 // reads session data associated with a session id, but only if
470 // - the session ID exists;
471 // - the session has not expired;
472 // - if lock_to_user_agent is TRUE and the HTTP_USER_AGENT is the same as the one who had previously been associated with this particular session;
473 // - if lock_to_ip is TRUE and the host is the same as the one who had previously been associated with this particular session;
474 $hash = '';
475 // if we need to identify sessions by also checking the user agent
476 if ($this->lock_to_user_agent && isset($_SERVER['HTTP_USER_AGENT'])) {
477 $hash .= $_SERVER['HTTP_USER_AGENT'];
478 }
479 // if we need to identify sessions by also checking the host
480 if ($this->lock_to_ip && isset($_SERVER['REMOTE_ADDR'])) {
481 $hash .= $_SERVER['REMOTE_ADDR'];
482 }
483 // append this to the end
484 $hash .= $this->security_code;
485 $this->db->query('SELECT session_data FROM sessions WHERE session_id = ? AND session_expire > ? AND hash = ?');
486 $this->db->execute([$session_id, time(), md5($hash)]);
487 // if anything was found
488 if ($this->db->count()) {
489 // return found data
490 // don't bother with the unserialization - PHP handles this automatically
491 return $this->db->result();
492 }
493 if (!$this->mustRegenerate($session_id)) {
494 if (isset($_SESSION[SESSION_NAME])) {
495 $session_id = $this->regenerate_id();
496 setcookie(SESSION_NAME, $session_id);
497
498 return true;
499 }
500 }
501
502 }
503
504 /**
505 * Custom write() function.
506 */
507 public function write($session_id, $session_data)
508 {
509 // insert OR update session's data - this is how it works:
510 // first it tries to insert a new row in the database BUT if session_id is already in the database then just
511 // update session_data and session_expire for that specific session_id
512 // read more here http://dev.mysql.com/doc/refman/4.1/en/insert-on-duplicate.html
513 $this->db->query('INSERT INTO sessions (session_id, hash, session_data, session_expire, userid) VALUES (?, ?, ?, ?, ?)
514 ON DUPLICATE KEY UPDATE session_data = ?, session_expire = ?, userid = ?
515 ');
516 $result = $this->db->execute([$session_id, md5(($this->lock_to_user_agent && isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '').($this->lock_to_ip && isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '').$this->security_code), $session_data, time() + $this->session_lifetime, !empty($_SESSION['id']) ? $_SESSION['id'] : 0, $session_data, time() + $this->session_lifetime, !empty($_SESSION['id']) ? $_SESSION['id'] : 0], true);
517 // if anything happened
518 if ($result) {
519 if ($this->db->affected()) { // if the row was inserted
520 return true;
521 }
522 }
523
524 return '';
525 }
526
527 /**
528 * Manages flashdata behind the scenes.
529 */
530 public function _manage_flashdata()
531 {
532 // if there is flashdata to be handled
533 if (!empty($this->flashdata)) {
534 // iterate through all the entries
535 foreach ($this->flashdata as $variable => $counter) {
536 // increment counter representing server requests
537 ++$this->flashdata[$variable];
538 // if we're past the first server request
539 if ($this->flashdata[$variable] > 1) {
540 // unset the session variable
541 unset($_SESSION[$variable]);
542 // stop tracking
543 unset($this->flashdata[$variable]);
544 }
545 }
546 // if there is any flashdata left to be handled
547 if (!empty($this->flashdata)) {
548 // store data in a temporary session variable
549 $_SESSION[$this->flashdata_varname] = serialize($this->flashdata);
550 }
551 }
552
553 return true;
554 }
555
556 private function mustRegenerate($session_id)
557 {
558 if (!$session_id) {
559 return false;
560 }
561 $this->db->query('SELECT COUNT(session_id) FROM sessions WHERE session_id = ?');
562 $this->db->execute([$session_id]);
563
564 return (bool) $this->db->result();
565 }
566 }
567 if (SQL_SESSIONS != false) {
568 if (!isset($db)) {
569 require_once __DIR__.'/pdo.class.php';
570 $db = database::getInstance();
571 }
572 $session = new Zebra_Session($db, 'hObhYAt4rq');
573 } else {
574 session_name(SESSION_NAME);
575 session_start();
576 }