├── run-tests.sh ├── install └── session_data.sql ├── examples ├── example.php └── example-pdo.php ├── LICENSE.md ├── README.md ├── CHANGELOG.md └── Zebra_Session.php /run-tests.sh: -------------------------------------------------------------------------------- 1 | vendor/bin/phpstan analyse; vendor/bin/phpcs --standard=coding-standards.xml -------------------------------------------------------------------------------- /install/session_data.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `session_data` ( 2 | `session_id` varchar(32) NOT NULL default '', 3 | `hash` varchar(32) NOT NULL default '', 4 | `session_data` blob NOT NULL, 5 | `session_expire` int(11) NOT NULL default '0', 6 | PRIMARY KEY (`session_id`) 7 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -------------------------------------------------------------------------------- /examples/example.php: -------------------------------------------------------------------------------- 1 | Current session settings:

'); 29 | print_r($session->get_settings()); 30 | print_r(''); 31 | 32 | // from now on, use sessions as you would normally 33 | // the only difference is that session data is no longer saved on the server 34 | // but in your database 35 | 36 | print_r(' 37 | The first time you run the script there should be an empty array (as there\'s nothing in the $_SESSION array)
38 | After you press "refresh" on your browser, you will se the values that were written in the $_SESSION array
39 | '); 40 | 41 | print_r('
');
42 |     print_r($_SESSION);
43 |     print_r('
'); 44 | 45 | // add some values to the session 46 | $_SESSION['value1'] = 'hello'; 47 | $_SESSION['value2'] = 'world'; 48 | 49 | // now check the table and see that there is data in it! 50 | 51 | // to completely delete a session un-comment the following line 52 | //$session->stop(); 53 | 54 | ?> -------------------------------------------------------------------------------- /examples/example-pdo.php: -------------------------------------------------------------------------------- 1 | PDO::ERRMODE_EXCEPTION, 22 | )); 23 | } catch (\PDOException $e) { 24 | throw new \PDOException($e->getMessage(), (int)$e->getCode()); 25 | } 26 | 27 | // include the Zebra_Session class 28 | require '../Zebra_Session.php'; 29 | 30 | // instantiate the class 31 | // note that you don't need to call the session_start() function 32 | // as it is called automatically when the object is instantiated 33 | // also note that we are passing the PDO instance as the first argument 34 | $session = new Zebra_Session($pdo, 'sEcUr1tY_c0dE', '', true, false, 1000); 35 | 36 | // current session settings 37 | print_r('
Current session settings:

'); 38 | print_r($session->get_settings()); 39 | print_r('
'); 40 | 41 | // from now on, use sessions as you would normally 42 | // the only difference is that session data is no longer saved on the server 43 | // but in your database 44 | 45 | print_r(' 46 | The first time you run the script there should be an empty array (as there\'s nothing in the $_SESSION array)
47 | After you press "refresh" on your browser, you will se the values that were written in the $_SESSION array
48 | '); 49 | 50 | print_r('
');
51 |     print_r($_SESSION);
52 |     print_r('
'); 53 | 54 | // add some values to the session 55 | $_SESSION['value1'] = 'hello'; 56 | $_SESSION['value2'] = 'world'; 57 | 58 | // now check the table and see that there is data in it! 59 | 60 | // to completely delete a session un-comment the following line 61 | // $session->stop(); 62 | 63 | ?> -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | ### GNU LESSER GENERAL PUBLIC LICENSE 2 | 3 | Version 3, 29 June 2007 4 | 5 | Copyright (C) 2007 Free Software Foundation, Inc. 6 | 7 | 8 | Everyone is permitted to copy and distribute verbatim copies of this 9 | license document, but changing it is not allowed. 10 | 11 | This version of the GNU Lesser General Public License incorporates the 12 | terms and conditions of version 3 of the GNU General Public License, 13 | supplemented by the additional permissions listed below. 14 | 15 | #### 0. Additional Definitions. 16 | 17 | As used herein, "this License" refers to version 3 of the GNU Lesser 18 | General Public License, and the "GNU GPL" refers to version 3 of the 19 | GNU General Public License. 20 | 21 | "The Library" refers to a covered work governed by this License, other 22 | than an Application or a Combined Work as defined below. 23 | 24 | An "Application" is any work that makes use of an interface provided 25 | by the Library, but which is not otherwise based on the Library. 26 | Defining a subclass of a class defined by the Library is deemed a mode 27 | of using an interface provided by the Library. 28 | 29 | A "Combined Work" is a work produced by combining or linking an 30 | Application with the Library. The particular version of the Library 31 | with which the Combined Work was made is also called the "Linked 32 | Version". 33 | 34 | The "Minimal Corresponding Source" for a Combined Work means the 35 | Corresponding Source for the Combined Work, excluding any source code 36 | for portions of the Combined Work that, considered in isolation, are 37 | based on the Application, and not on the Linked Version. 38 | 39 | The "Corresponding Application Code" for a Combined Work means the 40 | object code and/or source code for the Application, including any data 41 | and utility programs needed for reproducing the Combined Work from the 42 | Application, but excluding the System Libraries of the Combined Work. 43 | 44 | #### 1. Exception to Section 3 of the GNU GPL. 45 | 46 | You may convey a covered work under sections 3 and 4 of this License 47 | without being bound by section 3 of the GNU GPL. 48 | 49 | #### 2. Conveying Modified Versions. 50 | 51 | If you modify a copy of the Library, and, in your modifications, a 52 | facility refers to a function or data to be supplied by an Application 53 | that uses the facility (other than as an argument passed when the 54 | facility is invoked), then you may convey a copy of the modified 55 | version: 56 | 57 | - a) under this License, provided that you make a good faith effort 58 | to ensure that, in the event an Application does not supply the 59 | function or data, the facility still operates, and performs 60 | whatever part of its purpose remains meaningful, or 61 | - b) under the GNU GPL, with none of the additional permissions of 62 | this License applicable to that copy. 63 | 64 | #### 3. Object Code Incorporating Material from Library Header Files. 65 | 66 | The object code form of an Application may incorporate material from a 67 | header file that is part of the Library. You may convey such object 68 | code under terms of your choice, provided that, if the incorporated 69 | material is not limited to numerical parameters, data structure 70 | layouts and accessors, or small macros, inline functions and templates 71 | (ten or fewer lines in length), you do both of the following: 72 | 73 | - a) Give prominent notice with each copy of the object code that 74 | the Library is used in it and that the Library and its use are 75 | covered by this License. 76 | - b) Accompany the object code with a copy of the GNU GPL and this 77 | license document. 78 | 79 | #### 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, taken 82 | together, effectively do not restrict modification of the portions of 83 | the Library contained in the Combined Work and reverse engineering for 84 | debugging such modifications, if you also do each of the following: 85 | 86 | - a) Give prominent notice with each copy of the Combined Work that 87 | the Library is used in it and that the Library and its use are 88 | covered by this License. 89 | - b) Accompany the Combined Work with a copy of the GNU GPL and this 90 | license document. 91 | - c) For a Combined Work that displays copyright notices during 92 | execution, include the copyright notice for the Library among 93 | these notices, as well as a reference directing the user to the 94 | copies of the GNU GPL and this license document. 95 | - d) Do one of the following: 96 | - 0) Convey the Minimal Corresponding Source under the terms of 97 | this License, and the Corresponding Application Code in a form 98 | suitable for, and under terms that permit, the user to 99 | recombine or relink the Application with a modified version of 100 | the Linked Version to produce a modified Combined Work, in the 101 | manner specified by section 6 of the GNU GPL for conveying 102 | Corresponding Source. 103 | - 1) Use a suitable shared library mechanism for linking with 104 | the Library. A suitable mechanism is one that (a) uses at run 105 | time a copy of the Library already present on the user's 106 | computer system, and (b) will operate properly with a modified 107 | version of the Library that is interface-compatible with the 108 | Linked Version. 109 | - e) Provide Installation Information, but only if you would 110 | otherwise be required to provide such information under section 6 111 | of the GNU GPL, and only to the extent that such information is 112 | necessary to install and execute a modified version of the 113 | Combined Work produced by recombining or relinking the Application 114 | with a modified version of the Linked Version. (If you use option 115 | 4d0, the Installation Information must accompany the Minimal 116 | Corresponding Source and Corresponding Application Code. If you 117 | use option 4d1, you must provide the Installation Information in 118 | the manner specified by section 6 of the GNU GPL for conveying 119 | Corresponding Source.) 120 | 121 | #### 5. Combined Libraries. 122 | 123 | You may place library facilities that are a work based on the Library 124 | side by side in a single library together with other library 125 | facilities that are not Applications and are not covered by this 126 | License, and convey such a combined library under terms of your 127 | choice, if you do both of the following: 128 | 129 | - a) Accompany the combined library with a copy of the same work 130 | based on the Library, uncombined with any other library 131 | facilities, conveyed under the terms of this License. 132 | - b) Give prominent notice with the combined library that part of it 133 | is a work based on the Library, and explaining where to find the 134 | accompanying uncombined form of the same work. 135 | 136 | #### 6. Revised Versions of the GNU Lesser General Public License. 137 | 138 | The Free Software Foundation may publish revised and/or new versions 139 | of the GNU Lesser General Public License from time to time. Such new 140 | versions will be similar in spirit to the present version, but may 141 | differ in detail to address new problems or concerns. 142 | 143 | Each version is given a distinguishing version number. If the Library 144 | as you received it specifies that a certain numbered version of the 145 | GNU Lesser General Public License "or any later version" applies to 146 | it, you have the option of following the terms and conditions either 147 | of that published version or of any later version published by the 148 | Free Software Foundation. If the Library as you received it does not 149 | specify a version number of the GNU Lesser General Public License, you 150 | may choose any version of the GNU Lesser General Public License ever 151 | published by the Free Software Foundation. 152 | 153 | If the Library as you received it specifies that a proxy can decide 154 | whether future versions of the GNU Lesser General Public License shall 155 | apply, that proxy's public statement of acceptance of any version is 156 | permanent authorization for you to choose that version for the 157 | Library. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | zebrajs 2 | 3 | # Zebra Session  [![Tweet](https://img.shields.io/twitter/url/http/shields.io.svg?style=social)](https://twitter.com/intent/tweet?text=A+drop-in+replacement+for+PHP's+default+session+handler+which+stores+session+data+in+a+MySQL+database&url=https://github.com/stefangabos/Zebra_Session&via=stefangabos&hashtags=php) 4 | 5 | *A drop-in replacement for PHP's default session handler which stores session data in a MySQL database, providing better performance, better security and protection against session fixation and session hijacking* 6 | 7 | [![Latest Stable Version](https://poser.pugx.org/stefangabos/zebra_session/v/stable)](https://packagist.org/packages/stefangabos/zebra_session) [![Total Downloads](https://poser.pugx.org/stefangabos/zebra_session/downloads)](https://packagist.org/packages/stefangabos/zebra_session) [![Monthly Downloads](https://poser.pugx.org/stefangabos/zebra_session/d/monthly)](https://packagist.org/packages/stefangabos/zebra_session) [![Daily Downloads](https://poser.pugx.org/stefangabos/zebra_session/d/daily)](https://packagist.org/packages/stefangabos/zebra_session) [![License](https://poser.pugx.org/stefangabos/zebra_session/license)](https://packagist.org/packages/stefangabos/zebra_session) 8 | 9 | Session support in PHP consists of a way to preserve information (variables) on subsequent accesses to a website's pages. Unlike cookies, variables are not stored on the user's computer. Instead, only a *session identifier* is stored in a cookie on the visitor's computer, which is matched up with the actual session data kept on the server, and made available to us through the [$_SESSION](https://www.php.net/manual/en/reserved.variables.session.php) super-global. Session data is retrieved as soon as we open a session, usually at the beginning of each page. 10 | 11 | By default, session data is stored on the server in flat files, separate for each session. The problem with this scenario is that performance degrades proportionally with the number of session files existing in the session directory (depending on the server's operating system's ability to handle directories with numerous files). Another issue is that session files are usually stored in a location that is world readable posing a security concern on shared hosting. 12 | 13 | This is where **Zebra Session** comes in handy - a PHP library that acts as a drop-in replacement for PHP's default session handler, but instead of storing session data in flat files it stores them in a **MySQL database**, providing better security and better performance. 14 | 15 | Zebra Session is also a solution for applications that are scaled across multiple web servers (using a load balancer or a round-robin DNS) where the user's session data needs to be available. Storing sessions in a database makes them available to all of the servers! 16 | 17 | Supports *"flash data"* - session variables which will only be available for the next server request, and which will be automatically deleted afterwards. Typically used for informational or status messages (for example: "data has been successfully updated"). 18 | 19 | This class is was inspired by John Herren's code from the [Trick out your session handler](https://web.archive.org/web/20081221052326/http://devzone.zend.com/node/view/id/141) article (now only available on the [Internet Archive](https://web.archive.org/web/20081221052326/http://devzone.zend.com/node/view/id/141)) and Chris Shiflett's code from his book [Essential PHP Security](https://web.archive.org/web/20190921001622/http://phpsecurity.org/code/ch08-2), chapter 8, Shared Hosting, Pg. 78-80. 20 | 21 | Zebra Session's code is heavily commented and generates no warnings/errors/notices when PHP's error reporting level is set to [E_ALL](https://www.php.net/manual/en/function.error-reporting.php). 22 | 23 | Starting with version 2.0, Zebra Session implements [row locks](https://dev.mysql.com/doc/refman/8.0/en/locking-functions.html#function_get-lock), ensuring that data is correctly handled in a scenario with multiple concurrent AJAX requests. 24 | 25 | Citing from [Race Conditions with Ajax and PHP Sessions](http://thwartedefforts.org/2006/11/11/race-conditions-with-ajax-and-php-sessions/), a great article by Andy Bakun: 26 | 27 | > When locking is not used, multiple requests (represented in these diagrams as processes P1, P2 and P3) access the session data without any consideration for the other processes and the state of the session data. The running time of the requests are indicated by the height of each process's colored area (the actual run times are unimportant, only the relative start times and durations). 28 | 29 | ![Session access without locking](https://raw.githubusercontent.com/stefangabos/Zebra_Session/22a14834a5928337fb9cb4e47743a3c82e00486b/docs/media/session-access-without-locking.png) 30 | 31 | > In the example above, no matter how P2 and P3 change the session data, the only changes that will be reflected in the session are those that P1 made because they were written last. When locking is used, the process can start up, request a lock on the session data before it reads it, and then get a consistent read of the session once it acquires exclusive access to it. In the following diagram, all reads occur after writes: 32 | 33 | ![Session access without locking](https://raw.githubusercontent.com/stefangabos/Zebra_Session/22a14834a5928337fb9cb4e47743a3c82e00486b/docs/media/session-access-with-locking.png) 34 | 35 | > The process execution is interleaved, but access to the session data is serialized. The process is waiting for the lock to be released during the period between when the process requests the session lock and when the session is read. This means that your session data will remain consistent, but it also means that while processes P2 and P3 are waiting for their turn to acquire the lock, nothing is happening. This may not be that important if all of the requests change or write to the session data, but if P2 just needs to read the session data (perhaps to get a login identifier), it is being held up for no reason. 36 | 37 | So, in the end, this is not the best solution but still is better than nothing. The best solution is probably a *per-variable* locking. You can read a very detailed article about all this in Andy Bakun's article [Race Conditions with Ajax and PHP Sessions](http://thwartedefforts.org/2006/11/11/race-conditions-with-ajax-and-php-sessions/). 38 | 39 | Thanks to [Michael Kliewe](https://www.phpgangsta.de/) who brought this to my attention! 40 | 41 | ## Features 42 | 43 | - acts as a wrapper for PHP's default session handling functions, but instead of storing session data in flat files it stores them in a MySQL database, providing better security and better performance 44 | 45 | - it is a drop-in and seamingless replacement for PHP's default session handler: PHP sessions will be used in the same way as prior to using the library; you don't need to change any existing code! 46 | 47 | - integrates seamlesly with PDO (if you are using PDO) but works perfectly without it 48 | 49 | - implements *row locks*, ensuring that data is correctly handled in scenarios with multiple concurrent AJAX requests 50 | 51 | - because session data is stored in a database, the library represents a solution for applications that are scaled across multiple web servers (using a load balancer or a round-robin DNS) 52 | 53 | - has awesome documentation 54 | 55 | - the code is heavily commented and generates no warnings/errors/notices when PHP's error reporting level is set to E_ALL 56 | 57 | ## :notebook_with_decorative_cover: Documentation 58 | 59 | Check out the [awesome documentation](https://stefangabos.github.io/Zebra_Session/Zebra_Session/Zebra_Session.html)! 60 | 61 | ## 🎂 Support the development of this project 62 | 63 | Your support means a lot and it keeps me motivated to keep working on open source projects.
64 | If you like this project please ⭐ it by clicking on the star button at the top of the page.
65 | If you are feeling generous, you can buy me a coffee by donating through PayPal, or you can become a sponsor.
66 | Either way - **Thank you!** 🎉 67 | 68 | [Star it on GitHub](https://github.com/stefangabos/Zebra_Session) [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=8J7UKSA7G6372) [](https://github.com/sponsors/stefangabos) 69 | 70 | ## Requirements 71 | 72 | PHP 5.5.2+ with the `mysqli extension` activated, MySQL 4.1.22+ 73 | 74 | ## Installation 75 | 76 | You can install via [Composer](https://packagist.org/packages/stefangabos/zebra_session) 77 | 78 | ```bash 79 | # get the latest stable release 80 | composer require stefangabos/zebra_session 81 | 82 | # get the latest commit 83 | composer require stefangabos/zebra_session:dev-master 84 | ``` 85 | 86 | 87 | Or you can install it manually by downloading the latest version, unpacking it, and then including it in your project 88 | 89 | ```php 90 | require_once 'path/to/Zebra_Session.php'; 91 | ``` 92 | 93 | ## Install MySQL table 94 | 95 | Notice a directory called *install* containing a file named `session_data.sql`. This file contains the SQL code that will create a table that is used by the class to store session data. Import or execute the SQL code using your preferred MySQL manager (like phpMyAdmin or the fantastic Adminer) into a database of your choice. 96 | 97 | ## How to use 98 | 99 | > Note that this class assumes that there is an active connection to a MySQL database and it does not attempt to create one! 100 | 101 | ```php 102 | PDO::ERRMODE_EXCEPTION, 114 | // )); 115 | // } catch (\PDOException $e) { 116 | // throw new \PDOException($e->getMessage(), (int)$e->getCode()); 117 | // } 118 | 119 | // include the Zebra_Session class 120 | // (you don't need this if you are using Composer) 121 | require 'path/to/Zebra_Session.php'; 122 | 123 | // instantiate the class 124 | // this also calls session_start() 125 | $session = new Zebra_Session($link, 'sEcUr1tY_c0dE'); 126 | 127 | // from now on, use sessions as you would normally 128 | // this is why it is called a "drop-in replacement" :) 129 | $_SESSION['foo'] = 'bar'; 130 | 131 | // data is in the database! 132 | ``` 133 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## version 4.2.0 (TBA) 2 | 3 | - fixed [#57](https://github.com/stefangabos/Zebra_Session/issues/57); thanks to [Bob Brown](https://github.com/gurubobnz) 4 | - fixed broken `get_active_sessions` method 5 | - fixed a minor issue where `session_write_close` was registered twice 6 | - updated table structure to match latest recommended values for MySQL/MariaDB 7 | 8 | ## version 4.1.0 (September 08, 2024) 9 | 10 | - fixed [#49](https://github.com/stefangabos/Zebra_Session/issues/49) where the old `session_set_save_handler` signature with more than 2 arguments was deprecated in PHP 8 and the deprecated signature would become unsupported either in PHP 9.0 or 10.0; the library is still backwards compatible; thanks to [Joe Bordes](https://github.com/joebordes) for the heads up! 11 | - the `lock_to_ip` argument of the constructor can now also be a callable; see [#56](https://github.com/stefangabos/Zebra_Session/pull/56); this is a better and more secure fix for [#43](https://github.com/stefangabos/Zebra_Session/issues/43) and [#54](https://github.com/stefangabos/Zebra_Session/pull/54); this is also a fix for a **very old** [#7](https://github.com/stefangabos/Zebra_Session/issues/7)! thanks [Andreas Heissenberger](https://github.com/aheissenberger) for the great idea and the feedback 12 | - ~~added a fix for using the library with an AWS load balancer; see [#43](https://github.com/stefangabos/Zebra_Session/issues/43) and [#54](https://github.com/stefangabos/Zebra_Session/pull/54); thank you [Dvelopin](https://github.com/dvelopin)!~~ 13 | - fixed (hopefully) [#53](https://github.com/stefangabos/Zebra_Session/issues/53) regarding table locks not being released if script execution ended before the library being able to write session data and release the lock 14 | - fixed an issue where `get_settings()` would trigger an error if `session.gc_divisor` is set to `0`; this fixes [#48](https://github.com/stefangabos/Zebra_Session/issues/48) - thanks to [Alex](https://github.com/alexp-uk)! 15 | 16 | ## version 4.0.0 (March 24, 2023) 17 | 18 | - the library doesn't set `session.cookie_lifetime` to `0` anymore but to the number of seconds specified in the constructor; with this, finally, sessions can be kept alive even if the browser is closed - this fixes [#40](https://github.com/stefangabos/Zebra_Session/issues/40) and [#5](https://github.com/stefangabos/Zebra_Session/issues/5) 19 | - the library is not setting `gc_probability` and `gc_divisor` properties anymore - this can potentially break your code when updating, as there are now less arguments in the constructor method! 20 | - the library does not set `session.use_strict_mode` anymore - see [#37](https://github.com/stefangabos/Zebra_Session/issues/37) 21 | - updated documentation regarding what configuration options are set automatically 22 | - lots of minor bug fixes and source code formatting because we are now using [PHPStan](https://github.com/phpstan/phpstan) for static code analysis and [PHP CodeSniffer](https://github.com/squizlabs/PHP_CodeSniffer) for detecting coding standards violations, which are now [PSR12](https://www.php-fig.org/psr/psr-12/)-ish with a few of the rules excluded 23 | 24 | ## version 3.1.0 (May 31, 2020) 25 | 26 | - fixed a bug where sessions became unusable if the user agent was changed after initialization; thanks to [poisons77](https://github.com/poisons77) for the feedback - see [#32](https://github.com/stefangabos/Zebra_Session/issues/32) 27 | 28 | ## version 3.0.0 (February 22, 2020) 29 | 30 | - added integration with PDO 31 | - implemented prepared statemets as `mysqli_real_escape_string` may not be secure enough when used with PHP < `5.7.6`; see [this](https://stackoverflow.com/questions/5741187/sql-injection-that-gets-around-mysql-real-escape-string/23277864#23277864) for more information; thanks [duckboy81](https://github.com/duckboy81) for suggesting 32 | - sessions can now be started in read-only mode thus not having to do row locks; see [#26](https://github.com/stefangabos/Zebra_Session/pull/26); thanks [more7dev](https://github.com/more7dev)! 33 | - [session.use_strict_mode](https://www.php.net/manual/en/session.configuration.php#ini.session.use-strict-mode) is now always enabled by the library automatically; thanks [dnanusevski](https://github.com/dnanusevski) for suggesting 34 | - [session.cookie_secure](https://www.php.net/manual/en/session.configuration.php#ini.session.cookie-secure) is now automatically enabled by the library *if HTTPS connection is detected*; thanks [dnanusevski](https://github.com/dnanusevski) for suggesting 35 | - fixed issue when using special characters in table name; see [#27](https://github.com/stefangabos/Zebra_Session/issues/27); thanks [more7dev](https://github.com/more7dev)! 36 | - added option for disabling automatically starting the session; see [#28](https://github.com/stefangabos/Zebra_Session/issues/28); thanks [Nick Muerdter](https://github.com/GUI) for the pull request! 37 | - minimum required PHP version has changed from `5.1.0` to `5.5.2` 38 | 39 | ## version 2.1.10 (January 05, 2019) 40 | 41 | - fixed [bug](https://github.com/stefangabos/Zebra_Session/pull/24) because of incorrect logic; thanks [RolandD](https://github.com/roland-d)! 42 | 43 | ## version 2.1.9 (January 03, 2019) 44 | 45 | - fixed [#16](https://github.com/stefangabos/Zebra_Session/issues/16) where the maximum length for lock keys in MySQL 5.7.5+ is limited to 64 characters; thanks to [Andreas Heissenberger](https://github.com/aheissenberger) for providing the fix! 46 | - the library now destroys previous sessions when started 47 | - database errors now throw exceptions instead of dying; thanks [Jonathon Hill](https://github.com/compwright) 48 | 49 | ## version 2.1.8 (May 20, 2017) 50 | 51 | - documentation is now available in the repository and on GitHub 52 | - the home of the library is now exclusively on GitHub 53 | 54 | ## version 2.1.7 (May 01, 2017) 55 | 56 | - security tweaks (setting `session.cookie_httponly` and `session.use_only_cookies` to `1` by default) 57 | - the stop() method will now also remove the associated cookie 58 | 59 | ## version 2.1.6 (April 19, 2017) 60 | 61 | - hopefully [#13](https://github.com/stefangabos/Zebra_Session/issues/13) is now fixed for good 62 | 63 | ## version 2.1.5 (April 11, 2017) 64 | 65 | - closed [#11](https://github.com/stefangabos/Zebra_Session/issues/11); thanks @soren121 66 | - fixed (hopefully) [#13](https://github.com/stefangabos/Zebra_Session/issues/13); thanks to @alisonw for providing the current fix 67 | - reduced overall code complexity 68 | 69 | ## version 2.1.4 (February 19, 2016) 70 | 71 | - fixed an issue when using the library with Composer 72 | 73 | ## version 2.1.1 (September 25, 2014) 74 | 75 | - this version makes use of a feature introduced in PHP 5.1.0 for the "regenerate_id" method; thanks to **Fernando Sávio**; this also means that now the library requires PHP 5.1.0+ to work 76 | 77 | ## version 2.1.0 (August 02, 2013) 78 | 79 | - dropped support for PHP 4; minimum required version is now PHP 5 80 | - dropped support for PHP's *mysql* extension, which is [officially deprecated as of PHP v5.5.0](http://php.net/manual/en/changelog.mysql.php) and will be removed in the future; the extension was originally introduced in PHP v2.0 for MySQL v3.23, and no new features have been added since 2006; the library now relies on PHP's [mysqli](http://php.net/manual/en/book.mysqli.php) extension 81 | - because of the above, the order of the arguments passed to the [constructor](https://stefangabos.github.io/Zebra_Session/Zebra_Session/Zebra_Session.html#methodZebra_Session) have changed and now the "link" argument comes first, as with the *mysqli* extension this now needs to be explicitly given 82 | - added support for "flash data" - session variable which will only be available for the next server request, and which will be automatically deleted afterwards. Typically used for informational or status messages (for example: "data has been successfully updated"); see the newly added [set_flashdata](https://stefangabos.github.io/Zebra_Session/Zebra_Session/Zebra_Session.html#methodset_flashdata) method; thanks **Andrei Bodeanschi** for suggesting! 83 | - the project is now available on [GitHub](https://github.com/stefangabos/Zebra_Session) and also as a [package for Composer](https://packagist.org/packages/stefangabos/zebra_session) 84 | 85 | ## version 2.0.4 (January 19, 2013) 86 | 87 | - previously, session were always tied to the user agent used when the session was first opened; while this is still true, now this behavior can be disabled by setting the [constructor](https://stefangabos.github.io/Zebra_Session/Zebra_Session/Zebra_Session.html#methodZebra_Session)'s new "lock_to_user_agent" argument to FALSE; why? because in certain scenarios involving Internet Explorer, the browser will randomly change the user agent string from one page to the next by automatically switching into compatibility mode; thanks to **Andrei Bodeanschi** 88 | - also, the [constructor](https://stefangabos.github.io/Zebra_Session/Zebra_Session/Zebra_Session.html#methodZebra_Session) has another new "lock_to_ip" argument with which a session can be restricted to the same IP as when the session was first opened - use this with caution as many ISPs provide dynamic IP addresses which change over time, not to mention users who come through proxies; this is mostly useful if you know your users come from static IPs 89 | - for better protection against session hijacking, the first argument of the [constructor](https://stefangabos.github.io/Zebra_Session/Zebra_Session/Zebra_Session.html#methodZebra_Session) is now mandatory 90 | - altered the table structure 91 | - because the table's structure has changed as well as the order of the arguments in the [constructor](https://stefangabos.github.io/Zebra_Session/Zebra_Session/Zebra_Session.html#methodZebra_Session), you'll have to change a few things when switching to this version from a previous one 92 | - some documentation refinements 93 | 94 | ## version 2.0.3 (July 13, 2012) 95 | 96 | - fixed a bug where sessions' life time was twice longer than expected; thanks to **Andrei Bodeanschi** 97 | - details on how to preserve session data across sub domains was added to the documentation 98 | - the messages related database connection errors are now more meaningful 99 | 100 | ## version 2.0.2 (October 24, 2011) 101 | 102 | - fixed a bug with the get_active_sessions() method which was not working at all 103 | - fixed a bug where the script was not using the provided MySQL link identifier (if available) 104 | 105 | ## version 2.0.1 (July 03, 2011) 106 | 107 | - the constructor method now accepts an optional *link* argument which must be a MySQL link identifier. By default, the library made use of the last link opened by mysql_connect(). On some environments (particularly on a shared hosting) the "last link opened by mysql_connect" was not available at the time of the instantiation of the Zebra_Session library. For these cases, supplying the MySQL link identifier to the constructor method will fix things. Thanks to **Mark** for reporting. 108 | - some documentation refinements 109 | 110 | ## version 2.0 (April 18, 2011) 111 | 112 | - the class now implements session locking; session locking is a way to ensure that data is correctly handled in a scenario with multiple concurrent AJAX requests; thanks to **Michael Kliewe** for suggesting this and to Andy Bakun for this excellent article on the subject [Race Conditions with Ajax and PHP Sessions](http://thwartedefforts.org/2006/11/11/race-conditions-with-ajax-and-php-sessions/). 113 | 114 | ## version 1.0.8 (December 27, 2010) 115 | 116 | - fixed a small bug in the *destroy* method; thanks to **Tagir Valeev** for reporting 117 | - the script would trigger a PHP notice if the HTTP_USER_AGENT value was not available in the $_SERVER super-global 118 | - added a new method "get_settings" that returns the default session-related settings for the environment where the class is used 119 | 120 | ## version 1.0.7 (October 29, 2008) 121 | 122 | - the class will now trigger a fatal error if a database connection is not available 123 | - the class will now report if MySQL table is not available 124 | 125 | ## version 1.0.6 (October 01, 2007) 126 | 127 | - the constructor of the class now accepts a new argument: *tableName* - with this, the MySQL table used by the class can be changed 128 | 129 | ## version 1.0.5 (September 15, 2007) 130 | 131 | - 'LIMIT 1' added to the *read* method improving the performance of the script; thanks to **A. Leeming** for suggesting this 132 | 133 | ## version 1.0.4 (August 23, 2007) 134 | 135 | - rewritten the *write* method which previously had to run two queries each time; it now only runs a single one, using ON DUPLICATE KEY UPDATE; thanks to **Inchul Koo** for providing this information 136 | - the type of the *http_user_agent* column in the MySQL table has been changed from TEXT to VARCHAR(32) as now it is an MD5 hash; this should increase the performance of the script; thanks to **Inchul Koo** for suggesting this 137 | - the constructor of the class now accepts a new argument: *securityCode*, in order to try to prevent HTTP_USER_AGENT spoofing; read the documentation for more information; thanks to **Inchul Koo** for suggesting this 138 | 139 | ## version 1.0.3 (December 13, 2006) 140 | 141 | - the *get_users_online* method is now more accurate as it now runs the garbage collector before getting the number of online users; thanks to **Gilles** for the tip 142 | - the structure of the MySQL table used by the class was tweaked in so that the *http_user_agent* column has been changed from VARCHAR(255) to TEXT and the *session_data* column has been changed from TEXT to BLOB; thanks to **Gilles** for the tip 143 | 144 | ## version 1.0.2 (November 22, 2006) 145 | 146 | - the class was trying to store the session data without *mysql_real_escape_string*-ing it and therefore, whenever the data to be saved contained quotes, nothing was saved; thanks to **Ed Kelly** for reporting this 147 | 148 | ## version 1.0.1 (September 11, 2006) 149 | 150 | - the structure of the MySQL table used by the class was tweaked and now the *session_id* column is now a primary key and its type has changed from VARCHAR(255) to VARCHAR(32) as it now is an MD5 hash; previously a whole table scan was done every time a session was loaded; thanks to **Harry** for suggesting this 151 | - added a new *stop* method which deletes a session along with the stored variables from the database; this was introduced because many people were using the private *destroy* method which is only for internal purposes 152 | - the default settings for *session.gc_maxlifetime*, *session.gc_probability* and *session.gc_divisor* can now be overridden through the constructor 153 | - on popular request, an example file was added 154 | 155 | ## version 1.0 (August 01, 2006) 156 | 157 | - initial release 158 | -------------------------------------------------------------------------------- /Zebra_Session.php: -------------------------------------------------------------------------------- 1 | 12 | * @version 4.2.0 (last revision: July 14, 2025) 13 | * @copyright © 2006 - 2025 Stefan Gabos 14 | * @license https://www.gnu.org/licenses/lgpl-3.0.txt GNU LESSER GENERAL PUBLIC LICENSE 15 | * @package Zebra_Session 16 | */ 17 | class Zebra_Session implements SessionHandlerInterface { 18 | 19 | /** 20 | * @var array 21 | */ 22 | private $flash_data; 23 | 24 | /** 25 | * @var string 26 | */ 27 | private $flash_data_var; 28 | 29 | /** 30 | * @var object 31 | */ 32 | private $link; 33 | 34 | /** 35 | * @var integer 36 | */ 37 | private $lock_timeout; 38 | 39 | /** 40 | * @var boolean|callable 41 | */ 42 | private $lock_to_ip; 43 | 44 | /** 45 | * @var boolean 46 | */ 47 | private $lock_to_user_agent; 48 | 49 | /** 50 | * @var string 51 | */ 52 | private $security_code; 53 | 54 | /** 55 | * @var string|false 56 | */ 57 | private $session_lifetime; 58 | 59 | /** 60 | * @var string 61 | */ 62 | private $session_lock; 63 | 64 | /** 65 | * @var string 66 | */ 67 | private $table_name; 68 | 69 | /** 70 | * @var boolean 71 | */ 72 | private $read_only = false; 73 | 74 | /** 75 | * Constructor of class. Initializes the class and, optionally, calls 76 | * {@link https://php.net/manual/en/function.session-start.php session_start()} 77 | * 78 | * 79 | * // first, connect to a database containing the sessions table, either via PDO or using mysqli_connect 80 | * 81 | * // include the class 82 | * // (you don't need this if you are using Composer) 83 | * require 'path/to/Zebra_Session.php'; 84 | * 85 | * // start the session 86 | * // where $link is a connection link returned by mysqli_connect or a PDO instance 87 | * $session = new Zebra_Session($link, 'sEcUr1tY_c0dE'); 88 | * 89 | * 90 | * > **The following configuration options are set by the library when instantiated:** 91 | * 92 | * 93 | * // only when over HTTPS 94 | * ini_set('session.cookie_secure', 1); 95 | * 96 | * 97 | * 98 | * // don't expose the cookie to client side scripting making it harder for an attacker to hijack the session ID 99 | * ini_set('session.cookie_httponly', 1); 100 | * 101 | * 102 | * 103 | * // make sure that PHP only uses cookies for sessions and disallow session ID passing as a GET parameter 104 | * ini_set('session.use_only_cookies', 1); 105 | * 106 | * 107 | * > **The following configuration options are recommended to be set before instantiating this library:** 108 | * 109 | * 110 | * // disallows supplying session IDs via `session_id('ID HERE') 111 | * ini_set('session.use_strict_mode', 1);` 112 | * 113 | * 114 | * By default, the cookie used by PHP to propagate session data across multiple pages (`PHPSESSID`) uses the 115 | * current top-level domain and subdomain in the cookie declaration. 116 | * 117 | * Example: `www.domain.com` 118 | * 119 | * This means that the session data is not available to other subdomains. Therefore, a session started on 120 | * `www.domain.com` will not be available on `blog.domain.com`. The solution is to change the domain PHP uses when 121 | * it sets the `PHPSESSID` cookie by calling the line below *before* instantiating the Zebra_Session library: 122 | * 123 | * 124 | * // takes the domain and removes the subdomain 125 | * // blog.domain.com becoming .domain.com 126 | * ini_set( 127 | * 'session.cookie_domain', 128 | * substr($_SERVER['SERVER_NAME'], strpos($_SERVER['SERVER_NAME'], '.')) 129 | * ); 130 | * 131 | * 132 | * From now on whenever PHP sets the `PHPSESSID` cookie, the cookie will be available to all subdomains! 133 | * 134 | * @param resource &$link An object representing the connection to a MySQL Server, as returned 135 | * by calling {@link https://www.php.net/manual/en/mysqli.construct.php mysqli_connect}, 136 | * or a {@link https://www.php.net/manual/en/intro.pdo.php PDO} instance. 137 | * 138 | * If you use {@link https://github.com/stefangabos/Zebra_Database Zebra_Database} 139 | * to connect to the database, you can get the connection to the MySQL server 140 | * via Zebra_Database's {@link https://stefangabos.github.io/Zebra_Database/Zebra_Database/Zebra_Database.html#methodget_link get_link} 141 | * method. 142 | * 143 | * @param string $security_code The value of this argument is appended to the string created by 144 | * concatenating the user browser's User Agent string (or an empty string 145 | * if `lock_to_user_agent` is `FALSE`) and the user's IP address (or an 146 | * empty string if `lock_to_ip` is `FALSE`), before creating an MD5 hash out 147 | * of it and storing it in the database. 148 | * 149 | * On each call this value will be generated again and compared to the 150 | * value stored in the database ensuring that the session is correctly linked 151 | * with the user who initiated the session thus preventing session hijacking. 152 | * 153 | * > To prevent session hijacking, make sure you choose a string around 154 | * 12 characters long containing upper- and lowercase letters, as well as 155 | * digits. To simplify the process, use {@link https://www.random.org/passwords/?num=1&len=12&format=html&rnd=new this} 156 | * link to generate such a random string. 157 | * 158 | * @param integer $session_lifetime (Optional) The number of seconds after which a session will be considered 159 | * as **expired**. 160 | * 161 | * > A session is active for the number of seconds specified by this property 162 | * (or until the browser/browser tab is closed if the value is `0`) **OR** 163 | * the session has been inactive for more than the number of seconds specified 164 | * by `session.gc_maxlifetime`. 165 | * 166 | * > This property sets the value of {@link https://www.php.net/manual/en/session.configuration.php#ini.session.cookie-lifetime session.cookie_lifetime}. 167 | * 168 | * Expired sessions are cleaned up from the database whenever the garbage 169 | * collection routine runs. The probability for the garbage collection 170 | * routine to be executed is given by the values of `gc_probability` and 171 | * `gc_divisor`. 172 | * 173 | * To easily check the values of `session.gc_maxlifetime`, `gc_probability` 174 | * and `gc_divisor` for your environment use the {@link get_settings()} method. 175 | * 176 | * Default is `0` - the session is active until the browser/browser tab is 177 | * is closed **OR** the session has been inactive for more than the number 178 | * of seconds specified by `session.gc_maxlifetime`. 179 | * 180 | * @param boolean $lock_to_user_agent (Optional) Whether to restrict the session to the same User Agent (browser) 181 | * as when the session was first opened. 182 | * 183 | * > The user agent check only adds minor security, since an attacker that 184 | * hijacks the session cookie will most likely have the same user agent. 185 | * 186 | * In certain scenarios involving Internet Explorer, the browser will randomly 187 | * change the user agent string from one page to the next by automatically 188 | * switching into compatibility mode. So, on the first load you would have 189 | * something like: 190 | * 191 | * Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4... 192 | * 193 | * and reloading the page you would have 194 | * 195 | * Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Trident/4... 196 | * 197 | * So, if the situation asks for this, change this value to `false`. 198 | * 199 | * Default is `true`. 200 | * 201 | * @param boolean|callable $lock_to_ip (Optional) Whether to restrict the session to the same IP as when the 202 | * session was first opened. 203 | * 204 | * For the actual IP address that is going to be used, the library will 205 | * use the value of `$_SERVER['REMOTE_ADDR']`. 206 | * 207 | * If your application is behind a load balancer like an AWS Elastic Load Balancing 208 | * or a reverse proxy like Varnish, certain request information will be sent using 209 | * either the standard `Forwarded` header or the `X-Forwarded-*` headers. In this case, 210 | * the `REMOTE_ADDR` header will likely be the IP address of your reverse proxy while 211 | * the user's true IP will be stored in a standard `Forwarded` header or an `X-Forwarded-For` 212 | * header. 213 | * 214 | * In this case you will need to tell the library which reverse proxy IP addresses to 215 | * trust and what headers your reverse proxy uses to send information by using a `callable` 216 | * value for this argument: 217 | * 218 | * 219 | * new Zebra_Session( 220 | * $link, 221 | * 'someSecur1tyCode!', 222 | * 0, 223 | * false, 224 | * 225 | * // one way of using a callable for this argument 226 | * function() { 227 | * $ipaddress = ''; 228 | * // use the header(s) you choose to trust 229 | * foreach (['HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED'] as $key) { 230 | * // use the first one containing a value 231 | * if (($tmp = getenv($key))) { 232 | * $ipaddress = $tmp; 233 | * break; 234 | * } 235 | * } 236 | * return $ipaddress; 237 | * } 238 | * ); 239 | * 240 | * 241 | * Default is `false` 242 | * 243 | * @param int $lock_timeout (Optional) The maximum amount of time (in seconds) for which a lock on 244 | * the session data can be kept. 245 | * 246 | * > This must be lower than the maximum execution time of the script! 247 | * 248 | * Session locking is a way to ensure that data is correctly handled in a 249 | * scenario with multiple concurrent AJAX requests. 250 | * 251 | * Read more about it 252 | * {@link http://thwartedefforts.org/2006/11/11/race-conditions-with-ajax-and-php-sessions/ here}. 253 | * 254 | * Default is `60` 255 | * 256 | * @param string $table_name (Optional) Name of the MySQL table to be used by the class. 257 | * 258 | * Default is `session_data` 259 | * 260 | * @param boolean $start_session (Optional) Whether to start the session right away (by calling {@link https://php.net/manual/en/function.session-start.php session_start()}) 261 | * 262 | * Default is `true` 263 | * 264 | * @param boolean $read_only (Optional) Opens session in read-only mode and without row locks. Any changes 265 | * made to `$_SESSION` will not be saved, although the variable can be read/written. 266 | * 267 | * Default is `false` (the default session behavior). 268 | * 269 | * @return void 270 | */ 271 | public function __construct( 272 | &$link, 273 | $security_code, 274 | $session_lifetime = 0, 275 | $lock_to_user_agent = true, 276 | $lock_to_ip = false, 277 | $lock_timeout = 60, 278 | $table_name = 'session_data', 279 | $start_session = true, 280 | $read_only = false 281 | ) { 282 | 283 | // continue if the provided link is valid 284 | if (($link instanceof MySQLi && $link->connect_error === null) || $link instanceof PDO) { 285 | 286 | // store the connection link 287 | $this->link = $link; 288 | 289 | // set session's maximum lifetime 290 | ini_set('session.cookie_lifetime', (string)$session_lifetime); 291 | 292 | // tell the browser not to expose the cookie to client side scripting 293 | // this makes it harder for an attacker to hijack the session ID 294 | ini_set('session.cookie_httponly', '1'); 295 | 296 | // make sure that PHP only uses cookies for sessions and disallow session ID passing as a GET parameter 297 | ini_set('session.use_only_cookies', '1'); 298 | 299 | // if on HTTPS allows access to the session ID cookie only when the protocol is HTTPS 300 | if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') { 301 | ini_set('session.cookie_secure', '1'); 302 | } 303 | 304 | // session lifetime 305 | $this->session_lifetime = max($session_lifetime, ini_get('session.gc_maxlifetime')); 306 | 307 | // we'll use this later on in order to try to prevent HTTP_USER_AGENT spoofing 308 | $this->security_code = $security_code; 309 | 310 | // some other defaults 311 | $this->lock_to_user_agent = $lock_to_user_agent; 312 | $this->lock_to_ip = $lock_to_ip; 313 | 314 | // the table to be used by the class 315 | $this->table_name = '`' . trim($table_name, '`') . '`'; 316 | 317 | // the maximum amount of time (in seconds) for which a process can lock the session 318 | $this->lock_timeout = $lock_timeout; 319 | 320 | // set read-only flag 321 | $this->read_only = $read_only; 322 | 323 | // register the session handler 324 | session_set_save_handler($this, false); 325 | 326 | // if a session is already started, destroy it first 327 | if (session_id() !== '') { 328 | session_destroy(); 329 | } 330 | 331 | // start session if required 332 | if ($start_session) { 333 | session_start(); 334 | } 335 | 336 | // the name for the session variable that will be used for 337 | // holding information about flash data session variables 338 | $this->flash_data_var = '_zebra_session_flash_data_ec3asbuiad'; 339 | 340 | // assume no flash data 341 | $this->flash_data = array(); 342 | 343 | // if any flash data exists 344 | if (isset($_SESSION[$this->flash_data_var])) { 345 | 346 | // retrieve flash data 347 | $this->flash_data = unserialize($_SESSION[$this->flash_data_var]); 348 | 349 | // destroy the temporary session variable 350 | unset($_SESSION[$this->flash_data_var]); 351 | 352 | } 353 | 354 | // handle flash data after script execution 355 | register_shutdown_function(array($this, '_manage_flash_data')); 356 | 357 | // if no MySQL connection 358 | } else { 359 | throw new Exception('Zebra_Session: No MySQL connection'); 360 | } 361 | 362 | } 363 | 364 | /** 365 | * Custom close() function 366 | * 367 | * @return boolean 368 | * 369 | * @access private 370 | */ 371 | #[\ReturnTypeWillChange] 372 | public function close() { 373 | 374 | // release the lock associated with the current session 375 | $result = $this->query(' 376 | SELECT 377 | RELEASE_LOCK(?) 378 | ', $this->session_lock); 379 | 380 | // stop if there was an error 381 | if ($result['num_rows'] !== 1 || current($result['data']) === 0) { 382 | throw new Exception('Zebra_Session: Could not release session lock'); 383 | } 384 | 385 | return true; 386 | 387 | } 388 | 389 | /** 390 | * Custom destroy() function 391 | * 392 | * @param string $session_id The ID of the session to destroy 393 | * 394 | * @return boolean 395 | * 396 | * @access private 397 | */ 398 | #[\ReturnTypeWillChange] 399 | public function destroy($session_id) { 400 | 401 | // delete the current session from the database 402 | return $this->query(' 403 | 404 | DELETE FROM 405 | ' . $this->table_name . ' 406 | WHERE 407 | session_id = ? 408 | 409 | ', $session_id) !== false; 410 | 411 | } 412 | 413 | /** 414 | * Custom gc() function (garbage collector) 415 | * 416 | * @return boolean 417 | * 418 | * @access private 419 | */ 420 | #[\ReturnTypeWillChange] 421 | public function gc($maxlifetime) { 422 | 423 | // delete expired sessions from database 424 | $this->query(' 425 | 426 | DELETE FROM 427 | ' . $this->table_name . ' 428 | WHERE 429 | session_expire < ? 430 | 431 | ', time()); 432 | 433 | return true; 434 | 435 | } 436 | 437 | /** 438 | * Gets the number of active (not expired) sessions. 439 | * 440 | * > The returned value does not represent the exact number of active users as some sessions may be unused 441 | * although they haven't expired 442 | * 443 | * 444 | * // get the number of active sessions 445 | * $active_sessions = $session->get_active_sessions(); 446 | * 447 | * 448 | * @return integer Returns the number of active (not expired) sessions. 449 | */ 450 | public function get_active_sessions() { 451 | 452 | // call the garbage collector 453 | $this->gc(0); 454 | 455 | // count the rows from the database 456 | $result = $this->query(' 457 | 458 | SELECT 459 | COUNT(session_id) as count 460 | FROM 461 | ' . $this->table_name . ' 462 | 463 | '); 464 | 465 | // return the number of found rows 466 | return $result['data']['count']; 467 | 468 | } 469 | 470 | /** 471 | * Queries the system for the values of `session.gc_maxlifetime`, `session.gc_probability`, `session.gc_divisor` 472 | * and `session.use_strict_mode`, and returns them as an associative array. 473 | * 474 | * To view the result in a human-readable format use: 475 | * 476 | * // get default settings 477 | * print_r('
');
478 |      *  print_r($session->get_settings());
479 |      *
480 |      *  // would output something similar to (depending on your actual settings)
481 |      *  // Array
482 |      *  // (
483 |      *  //     [session.gc_maxlifetime] => 1440 seconds (24 minutes)
484 |      *  //     [session.gc_probability] => 1
485 |      *  //     [session.gc_divisor] => 1000
486 |      *  //     [probability] => 0.1%    // <- this is computed from the values above
487 |      *  //     [session.use_strict_mode] => 1
488 |      *  // )
489 |      *  
490 |      *
491 |      *  @since 1.0.8
492 |      *
493 |      *  @return array   Returns the values of `session.gc_maxlifetime`, `session.gc_probability`, `session.gc_divisor`
494 |      *                          and `session.use_strict_mode`, as an associative array.
495 |      *
496 |      */
497 |     public function get_settings() {
498 | 
499 |         // get the settings
500 |         $gc_maxlifetime     = ini_get('session.gc_maxlifetime');
501 |         $gc_probability     = ini_get('session.gc_probability');
502 |         $gc_divisor         = ini_get('session.gc_divisor');
503 |         $use_strict_mode    = ini_get('session.use_strict_mode');
504 | 
505 |         // return them as an array
506 |         return array(
507 |             'session.gc_maxlifetime'    =>  $gc_maxlifetime . ' seconds (' . round($gc_maxlifetime / 60) . ' minutes)',
508 |             'session.gc_probability'    =>  $gc_probability,
509 |             'session.gc_divisor'        =>  $gc_divisor,
510 |             'probability'               =>  ($gc_divisor > 0 ? (int)$gc_probability / (int)$gc_divisor * 100 : 0) . '%',
511 |             'session.use_strict_mode'   =>  $use_strict_mode,
512 |         );
513 | 
514 |     }
515 | 
516 |     /**
517 |      *  Custom open() function
518 |      *
519 |      *  @return boolean
520 |      *
521 |      *  @access private
522 |      */
523 |     #[\ReturnTypeWillChange]
524 |     public function open($save_path, $session_name) {
525 | 
526 |         return true;
527 | 
528 |     }
529 | 
530 |     /**
531 |      *  Custom read() function
532 |      *
533 |      *  @param  string  $session_id     The ID of the session to read from
534 |      *
535 |      *  @return string
536 |      *
537 |      *  @access private
538 |      */
539 |     #[\ReturnTypeWillChange]
540 |     public function read($session_id) {
541 | 
542 |         // get the lock name associated with the current session
543 |         // notice the use of sha1() which shortens the session ID to 40 characters so that it does not exceed the limit of
544 |         // 64 characters for locking string imposed by mySQL >= 5.7.5
545 |         // thanks to Andreas Heissenberger (see https://github.com/stefangabos/Zebra_Session/issues/16)
546 |         $this->session_lock = 'session_' . sha1($session_id);
547 | 
548 |         // if we are *not* in read-only mode
549 |         // read-only sessions do not need a lock
550 |         if (!$this->read_only) {
551 | 
552 |             // try to obtain a lock with the given name and timeout
553 |             $result = $this->query('SELECT GET_LOCK(?, ?)', $this->session_lock, $this->lock_timeout);
554 | 
555 |             // stop if there was an error
556 |             if ($result['num_rows'] !== 1 || current($result['data']) === 0) {
557 |                 throw new Exception('Zebra_Session: Could not obtain session lock');
558 |             }
559 | 
560 |         }
561 | 
562 |         $hash = '';
563 | 
564 |         // if the sessions is locked to an user agent
565 |         if ($this->lock_to_user_agent && isset($_SERVER['HTTP_USER_AGENT'])) {
566 |             $hash .= $_SERVER['HTTP_USER_AGENT'];
567 |         }
568 | 
569 |         // if session is locked to an IP address
570 | 
571 |         // if "lock_to_ip" is truthy but *not* callable
572 |         // (this is the quickest way in case "lock_to_ip" is truthy)
573 |         if ($this->lock_to_ip && !is_callable($this->lock_to_ip)) {
574 | 
575 |             // append the value of "REMOTE_ADDR" header
576 |             $hash .= $_SERVER['REMOTE_ADDR'];
577 | 
578 |         // if "lock_to_ip" is callable
579 |         } elseif (is_callable($this->lock_to_ip)) {
580 | 
581 |             // append whatever is returned by the callable
582 |             $hash .= call_user_func($this->lock_to_ip);
583 | 
584 |         }
585 | 
586 |         // append this to the end
587 |         $hash .= $this->security_code;
588 | 
589 |         // get the active (not expired) result associated with the session id and hash
590 |         $result = $this->query('
591 | 
592 |             SELECT
593 |                 session_data
594 |             FROM
595 |                 ' . $this->table_name . '
596 |             WHERE
597 |                 session_id = ?
598 |                 AND session_expire > ?
599 |                 AND hash = ?
600 |             LIMIT
601 |                 1
602 | 
603 |         ', $session_id, time(), md5($hash));
604 | 
605 |         // if there were no errors and data was found
606 |         if ($result !== false && $result['num_rows'] > 0) {
607 | 
608 |             // return session data
609 |             // don't bother with the unserialization - PHP handles this automatically
610 |             return $result['data']['session_data'];
611 | 
612 |         }
613 | 
614 |         // if hash has changed or the session expired
615 |         $this->destroy($session_id);
616 | 
617 |         // on error return an empty string - this HAS to be an empty string
618 |         return '';
619 | 
620 |     }
621 | 
622 |     /**
623 |      *  Regenerates the session id.
624 |      *
625 |      *  >   Call this method whenever you do a privilege change, in order to prevent session hijacking!
626 |      *
627 |      *  
628 |      *  // regenerate the session's ID
629 |      *  $session->regenerate_id();
630 |      *  
631 |      *
632 |      *  @return void
633 |      */
634 |     public function regenerate_id() {
635 | 
636 |         // regenerates the id (create a new session with a new id and containing the data from the old session)
637 |         // also, delete the old session
638 |         session_regenerate_id(true);
639 | 
640 |     }
641 | 
642 |     /**
643 |      *  Sets a **flash data** session variable which will only be available for the next server request and which will be
644 |      *  automatically deleted afterwards.
645 |      *
646 |      *  Typically used for informational or status messages (for example: "data has been successfully updated").
647 |      *
648 |      *  
649 |      *  // set "myvar" which will only be available
650 |      *  // for the next server request and will be
651 |      *  // automatically deleted afterwards
652 |      *  $session->set_flashdata('myvar', 'myval');
653 |      *  
654 |      *
655 |      *  "Flash data" session variables can be retrieved like any other session variable:
656 |      *
657 |      *  
658 |      *  if (isset($_SESSION['myvar'])) {
659 |      *      // do something here but remember that the
660 |      *      // flash data session variable is available
661 |      *      // for a single server request after it has
662 |      *      // been set!
663 |      *  }
664 |      *  
665 |      *
666 |      *  @param  string  $name   The name of the session variable.
667 |      *
668 |      *  @param  string  $value  The value of the session variable.
669 |      *
670 |      *  @return void
671 |      */
672 |     public function set_flashdata($name, $value) {
673 | 
674 |         // set session variable
675 |         $_SESSION[$name] = $value;
676 | 
677 |         // initialize the counter for this flash data
678 |         $this->flash_data[$name] = 0;
679 | 
680 |     }
681 | 
682 |     /**
683 |      *  Deletes all data related to the session.
684 |      *
685 |      *  >   This method runs the garbage collector respecting your environment's garbage collector-related properties.
686 |      *      Read {@link __construct() here} for more information
687 |      *
688 |      *  
689 |      *  // end current session
690 |      *  $session->stop();
691 |      *  
692 |      *
693 |      *  @since 1.0.1
694 |      *
695 |      *  @return void
696 |      */
697 |     public function stop() {
698 | 
699 |         // if a cookie is used to pass the session id
700 |         if (ini_get('session.use_cookies')) {
701 | 
702 |             // get session cookie's properties
703 |             $params = session_get_cookie_params();
704 | 
705 |             // unset the cookie
706 |             setcookie(session_name(), '', time() - 42000, $params['path'], $params['domain'], $params['secure'], $params['httponly']);
707 | 
708 |         }
709 | 
710 |         // destroy the session
711 |         session_unset();
712 |         session_destroy();
713 | 
714 |     }
715 | 
716 |     /**
717 |      *  Custom write() function
718 |      *
719 |      *  @param  string  $session_id     The ID of the session to write to
720 |      *
721 |      *  @param  mixed   $session_data   The values to be written
722 |      *
723 |      *  @return boolean
724 |      *
725 |      *  @access private
726 |      */
727 |     #[\ReturnTypeWillChange]
728 |     public function write($session_id, $session_data) {
729 | 
730 |         // we don't write session variable when in read-only mode
731 |         if ($this->read_only) {
732 |             return true;
733 |         }
734 | 
735 |         // insert OR update session's data - this is how it works:
736 |         // first it tries to insert a new row in the database BUT if session_id is already in the database then just
737 |         // update session_data and session_expire for that specific session_id
738 |         // read more here https://dev.mysql.com/doc/refman/8.0/en/insert-on-duplicate.html
739 |         return $this->query('
740 |             INSERT INTO
741 |                 ' . $this->table_name . '
742 |                 (
743 |                     session_id,
744 |                     hash,
745 |                     session_data,
746 |                     session_expire
747 |                 )
748 |             VALUES (?, ?, ?, ?)
749 |             ON DUPLICATE KEY UPDATE
750 |                 session_data = VALUES(session_data),
751 |                 session_expire = VALUES(session_expire)
752 |             ',
753 |             $session_id,
754 |             md5(
755 |                 ($this->lock_to_user_agent && isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '') .
756 |                 ($this->lock_to_ip && !is_callable($this->lock_to_ip) ? $_SERVER['REMOTE_ADDR'] : (is_callable($this->lock_to_ip) ? call_user_func($this->lock_to_ip) : '')) .
757 |                 $this->security_code
758 |             ),
759 |             $session_data,
760 |             time() + $this->session_lifetime
761 |         ) !== false;
762 | 
763 |     }
764 | 
765 |     /**
766 |      *  Manages flash data behind the scenes
767 |      *
768 |      *  @return void
769 |      *
770 |      *  @access private
771 |      */
772 |     public function _manage_flash_data() {
773 | 
774 |         // if there is flash data to be handled
775 |         if (!empty($this->flash_data)) {
776 | 
777 |             // iterate through all the entries
778 |             foreach ($this->flash_data as $variable => $counter) {
779 | 
780 |                 // increment counter representing server requests
781 |                 $this->flash_data[$variable]++;
782 | 
783 |                 // if this is not the first server request
784 |                 if ($this->flash_data[$variable] > 1) {
785 | 
786 |                     // unset the session variable
787 |                     unset($_SESSION[$variable]);
788 | 
789 |                     // stop tracking
790 |                     unset($this->flash_data[$variable]);
791 | 
792 |                 }
793 | 
794 |             }
795 | 
796 |             // if there is any flash data left to be handled
797 |             if (!empty($this->flash_data)) {
798 | 
799 |                 // store data in a temporary session variable
800 |                 $_SESSION[$this->flash_data_var] = serialize($this->flash_data);
801 | 
802 |             }
803 | 
804 |         }
805 | 
806 |         // make sure session data is written
807 |         // not matter how script execution ends
808 |         session_write_close();
809 | 
810 |     }
811 | 
812 |     /**
813 |      *  Mini-wrapper for running MySQL queries with parameter binding with or without PDO
814 |      *
815 |      *  @param  string  $query  The MySQL query to execute
816 |      *
817 |      *  @return mixed
818 |      *
819 |      *  @access private
820 |      */
821 |     private function query($query) {
822 | 
823 |         // if the provided connection link is a PDO instance
824 |         if ($this->link instanceof PDO) {
825 | 
826 |             // if executing the query was a success
827 |             if (($stmt = $this->link->prepare($query)) && $stmt->execute(array_slice(func_get_args(), 1))) {
828 | 
829 |                 // prepare a standardized return value
830 |                 $result = array(
831 |                     'num_rows'  =>  $stmt->rowCount(),
832 |                     'data'      =>  $stmt->columnCount() == 0 ? array() : $stmt->fetch(PDO::FETCH_ASSOC),
833 |                 );
834 | 
835 |                 // close the statement
836 |                 $stmt->closeCursor();
837 | 
838 |                 // return result
839 |                 return $result;
840 | 
841 |             }
842 | 
843 |         // if link connection is a regular mysqli connection object
844 |         } else {
845 | 
846 |             $stmt = mysqli_stmt_init($this->link);
847 | 
848 |             // if query is valid
849 |             if ($stmt->prepare($query)) {
850 | 
851 |                 // the arguments minus the first one (the SQL statement)
852 |                 $arguments = array_slice(func_get_args(), 1);
853 | 
854 |                 // if there are any arguments
855 |                 if (!empty($arguments)) {
856 | 
857 |                     // prepare the data for "bind_param"
858 |                     $bind_types = '';
859 |                     $bind_data = array();
860 |                     foreach ($arguments as $key => $value) {
861 |                         $bind_types .= is_numeric($value) ? 'i' : 's';
862 |                         $bind_data[] = &$arguments[$key];
863 |                     }
864 |                     array_unshift($bind_data, $bind_types);
865 | 
866 |                     // call "bind_param" with the prepared arguments
867 |                     call_user_func_array(array($stmt, 'bind_param'), $bind_data);
868 | 
869 |                 }
870 | 
871 |                 // if the query was successfully executed
872 |                 if ($stmt->execute()) {
873 | 
874 |                     // get some information about the results
875 |                     $results = $stmt->get_result();
876 | 
877 |                     // prepare a standardized return value
878 |                     $result = array(
879 |                         'num_rows'  =>  is_bool($results) ? $stmt->affected_rows : $results->num_rows,
880 |                         'data'      =>  is_bool($results) ? array() : $results->fetch_assoc(),
881 |                     );
882 | 
883 |                     // close the statement
884 |                     $stmt->close();
885 | 
886 |                     // return result
887 |                     return $result;
888 | 
889 |                 }
890 | 
891 |             }
892 | 
893 |             // if we get this far there must've been an error
894 |             throw new Exception($stmt->error);
895 | 
896 |         }
897 | 
898 |     }
899 | 
900 | }
901 | 


--------------------------------------------------------------------------------