├── CHANGELOG.md
├── README.md
├── composer.json
├── license.txt
└── src
└── voku
└── helper
├── Db4Session.php
├── DbWrapper4Session.php
└── Session2DB.php
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog 4.0.4 (2020-03-08)
2 |
3 | * fix typo in the "security-key-fallback" code | thanks @svgaman
4 | * fix typo in php settings
5 | * fix reported problems from phpstan
6 |
7 |
8 | # Changelog 4.0.3 (2019-10-18)
9 |
10 | * use more secure session settings
11 |
12 |
13 | # Changelog 4.0.2 (2018-12-29)
14 |
15 | * update "Simple-MySQL"-dependency use v7 or v8
16 | * use phpcs fixer for the code-style
17 |
18 |
19 | # Changelog 4.0.1 (2018-04-29)
20 |
21 | * fix "integrity constraint violation"
22 |
23 | -> via "ON DUPLICATE KEY UPDATE" in the sql-query
24 |
25 |
26 | # Chanelog 4.0.0 (2017-12-23)
27 |
28 | * update "Portable UTF8" from v4 -> v5
29 |
30 | -> this is a breaking change without API-changes - but the requirement from
31 | "Portable UTF8" has been changed (it no longer requires all polyfills from Symfony)
32 |
33 |
34 | # Chanelog 3.3.0 (2017-12-20)
35 |
36 | * use more php7 type-hints
37 | * add "$start_session" to the "Session2DB"-constructor
38 |
39 | -> If you want to modify the settings via setters before starting the session, you can skip the session-start and do it manually via "Session2DB->start()"
40 |
41 |
42 | # Changelog 3.2.1 (2017-12-14)
43 |
44 | * use php7 type-hints
45 |
46 |
47 | # Changelog 3.2.0 (2017-12-14)
48 |
49 | * edit "Session2DB->use_lock_via_mysql(bool|null)"
50 |
51 | - true => use mysql GET_LOCK() / RELEASE_LOCK()
52 | - false => use php flock() + LOCK_EX
53 | - null => use mysql + extra lock-table
54 |
55 |
56 | # Changelog 3.1.0 (2017-12-14)
57 |
58 | * add "Session2DB->use_lock_via_mysql(bool)"
59 | * use new version of "Simple MySQLi" (voku/simple-mysqli)
60 |
61 |
62 | # Changelog 3.0.0 (2017-11-25)
63 |
64 | * drop support for PHP < 7.0
65 | * use "strict_types"
66 |
67 |
68 | # Changelog 2.1.0 (2017-12-20)
69 |
70 | * backport changes from the "master"-branch into "php_old"-branch
71 |
72 |
73 | # Changelog 2.0.0 (2017-10-15)
74 |
75 | * add a interface && a wrapper class for the database-connection
76 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://travis-ci.org/voku/session2db)
2 | [](https://coveralls.io/github/voku/session2db?branch=master)
3 | [](https://www.codacy.com/app/voku/session2db)
4 | [](https://packagist.org/packages/voku/session2db)
5 | [](https://packagist.org/packages/voku/session2db)
6 | [](https://packagist.org/packages/voku/session2db)
7 | [](https://www.paypal.me/moelleken)
8 | [](https://www.patreon.com/voku)
9 |
10 | # :crown: Session2DB
11 |
12 | #### A drop-in replacement for PHP's default session handler which stores session data in a database, providing both better performance and better security and protection against session fixation and session hijacking.
13 |
14 | Session2DB implements *session locking* - a way to ensure that data is correctly handled in a scenario with multiple concurrent AJAX requests.
15 |
16 | It is also a solution for applications that are scaled across multiple web servers (using a load balancer or a round-robin DNS) and where the user's session data needs to be available. Storing sessions in a database makes them available to all of the servers!
17 |
18 | The library supports "flashdata" - 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").
19 |
20 | Session2DB is was inspired by John Herren's code from the [Trick out your session handler](http://devzone.zend.com/413/trick-out-your-session-handler/) article and [Chris Shiflett](http://shiflett.org/articles/the-truth-about-sessions)'s articles about PHP sessions and based on [Zebra_Session](https://github.com/stefangabos/Zebra_Session).
21 |
22 | The code is heavily commented and generates no warnings/errors/notices when PHP's error reporting level is set to E_ALL.
23 |
24 | ### Requirements
25 |
26 | PHP 7.x with the **mysqli extension** activated, MySQL 5.x+ (recommanded: **mysqlnd extension**)
27 |
28 | ### How to install
29 |
30 | ```shell
31 | composer require voku/session2db
32 | ```
33 |
34 | ### How to use
35 |
36 | After installing, you will need to initialise the database table from the *install* directory from this repo, it will 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.
37 |
38 | *Note that this class assumes that there is an active connection to a MySQL database and it does not attempt to create one!
39 |
40 | ```php
41 | //
42 | // simple (dirty) example
43 | //
44 |
45 | start() afterwards)
100 | );
101 |
102 | // from now on, use sessions as you would normally
103 | // this is why it is called a "drop-in replacement" :)
104 | $_SESSION['foo'] = 'bar';
105 |
106 | // data is in the database!
107 | ```
108 |
109 | ### Support
110 |
111 | For support and donations please visit [Github](https://github.com/voku/session2db/) | [Issues](https://github.com/voku/session2db/issues) | [PayPal](https://paypal.me/moelleken) | [Patreon](https://www.patreon.com/voku).
112 |
113 | For status updates and release announcements please visit [Releases](https://github.com/voku/session2db/releases) | [Twitter](https://twitter.com/suckup_de) | [Patreon](https://www.patreon.com/voku/posts).
114 |
115 | For professional support please contact [me](https://about.me/voku).
116 |
117 | ### Thanks
118 |
119 | - Thanks to [GitHub](https://github.com) (Microsoft) for hosting the code and a good infrastructure including Issues-Managment, etc.
120 | - Thanks to [IntelliJ](https://www.jetbrains.com) as they make the best IDEs for PHP and they gave me an open source license for PhpStorm!
121 | - Thanks to [Travis CI](https://travis-ci.com/) for being the most awesome, easiest continous integration tool out there!
122 | - Thanks to [StyleCI](https://styleci.io/) for the simple but powerfull code style check.
123 | - Thanks to [PHPStan](https://github.com/phpstan/phpstan) && [Psalm](https://github.com/vimeo/psalm) for relly great Static analysis tools and for discover bugs in the code!
124 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "voku/session2db",
3 | "type": "library",
4 | "description": "A PHP library acting as a wrapper for PHP's default session handling functions which stores data in a MySQL database, providing both better performance and better security and protection against session fixation and session hijacking.",
5 | "keywords": [
6 | "session",
7 | "locking",
8 | "flash",
9 | "flashdata",
10 | "fixation",
11 | "hijack",
12 | "mysqli",
13 | "mysql",
14 | "database"
15 | ],
16 | "homepage": "http://stefangabos.ro/php-libraries/zebra-session/",
17 | "license": "LGPL-3.0",
18 | "authors": [
19 | {
20 | "name": "Stefan Gabos",
21 | "email": "contact@stefangabos.ro",
22 | "homepage": "http://stefangabos.ro/",
23 | "role": "Developer"
24 | },
25 | {
26 | "name": "Lars Moelleken",
27 | "email": "lars@moelleken.org",
28 | "homepage": "http://moelleken.org/",
29 | "role": "Developer"
30 | }
31 | ],
32 | "require": {
33 | "php": ">=7.0.0",
34 | "voku/simple-mysqli": "~7.0 || ~8.0"
35 | },
36 | "require-dev": {
37 | "phpunit/phpunit": "~6.0 || ~7.0"
38 | },
39 | "autoload": {
40 | "psr-4": {
41 | "voku\\helper\\": "src/voku/helper/"
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/license.txt:
--------------------------------------------------------------------------------
1 | GNU LESSER GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
false on error
40 | */ 41 | public function query(string $sql); 42 | 43 | /** 44 | * @param string $string 45 | * 46 | * @return string 47 | */ 48 | public function quote_string(string $string): string; 49 | 50 | /** 51 | * @return bool 52 | */ 53 | public function reconnect(): bool; 54 | } 55 | -------------------------------------------------------------------------------- /src/voku/helper/DbWrapper4Session.php: -------------------------------------------------------------------------------- 1 | db = $db; 25 | } else { 26 | $this->db = DB::getInstance(); 27 | } 28 | 29 | $this->db->setConfigExtra(['session_to_db' => true]); 30 | } 31 | 32 | /** 33 | * {@inheritdoc} 34 | */ 35 | public function close(): bool 36 | { 37 | return $this->db->close(); 38 | } 39 | 40 | /** 41 | * {@inheritdoc} 42 | */ 43 | public function escape($var) 44 | { 45 | return $this->db->escape($var); 46 | } 47 | 48 | /** 49 | * {@inheritdoc} 50 | */ 51 | public function fetchColumn(string $sql, string $string) 52 | { 53 | $result = $this->db->query($sql); 54 | \assert($result instanceof \voku\db\Result); 55 | 56 | return $result->fetchColumn($string); 57 | } 58 | 59 | /** 60 | * {@inheritdoc} 61 | */ 62 | public function ping(): bool 63 | { 64 | return $this->db->ping(); 65 | } 66 | 67 | /** 68 | * {@inheritdoc} 69 | */ 70 | public function query(string $sql) 71 | { 72 | return $this->db->query($sql); 73 | } 74 | 75 | /** 76 | * {@inheritdoc} 77 | */ 78 | public function quote_string(string $string): string 79 | { 80 | return $this->db->quote_string($string); 81 | } 82 | 83 | /** 84 | * {@inheritdoc} 85 | */ 86 | public function reconnect(): bool 87 | { 88 | return $this->db->reconnect(); 89 | } 90 | 91 | /** 92 | * @return DB 93 | */ 94 | public function getDb(): DB 95 | { 96 | return $this->db; 97 | } 98 | 99 | /** 100 | * @return DbWrapper4Session 101 | */ 102 | public static function getInstance(): self 103 | { 104 | return new self(); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/voku/helper/Session2DB.php: -------------------------------------------------------------------------------- 1 | session locking. Session locking is a way to ensure that data is 13 | * correctly handled in a scenario with multiple concurrent AJAX requests. Read more about it in this excellent 14 | * article by Andy Bakun called {@link * 15 | * http://thwartedefforts.org/2006/11/11/race-conditions-with-ajax-and-php-sessions/ Race Conditions with Ajax and PHP 16 | * Sessions}. 17 | * 18 | * This library is also a solution for applications that are scaled across multiple web servers (using a 19 | * load balancer or a round-robin DNS) and where the user's session data needs to be available. Storing sessions in a 20 | * database makes them available to all of the servers! 21 | * 22 | * Session (Zebra_Session ) supports "flashdata" - session variable which will only be available for the next server 23 | * request, and which will be automatically deleted afterwards. Typically used for informational or status messages 24 | * (for example: "data has been successfully updated"). 25 | * 26 | * This is a fork of "Zebra_Session " and that was inspired by John Herren's code from 27 | * the {@link http://devzone.zend.com/413/trick-out-your-session-handler/ Trick out your session handler} 28 | * article and {@link http://shiflett.org/articles/the-truth-about-sessions Chris Shiflett}'s articles about PHP 29 | * sessions. 30 | * 31 | * Visit {@link http://stefangabos.ro/php-libraries/zebra-session/} for more information. 32 | * 33 | * @license http://www.gnu.org/licenses/lgpl-3.0.txt GNU LESSER GENERAL PUBLIC LICENSE 34 | */ 35 | class Session2DB implements \SessionHandlerInterface 36 | { 37 | /** 38 | * the name for the session variable that will be created upon script execution 39 | * and destroyed when instantiating this library, and which will hold information 40 | * about flashdata session variables 41 | * 42 | * @var string 43 | */ 44 | const flashDataVarName = '_menadwork_session_flashdata_ec3asbuiad'; 45 | 46 | /** 47 | * @var Db4Session 48 | */ 49 | private $db; 50 | 51 | /** 52 | * @var array 53 | */ 54 | private $flashdata = []; 55 | 56 | /** 57 | * @var int 58 | */ 59 | private $session_lifetime; 60 | 61 | /** 62 | * @var string 63 | */ 64 | private $lock_file_tmp; 65 | 66 | /** 67 | * @var bool|null 68 | */ 69 | private $lock_via_mysql = true; 70 | 71 | /** 72 | * @var int 73 | */ 74 | private $lock_timeout; 75 | 76 | /** 77 | * @var bool 78 | */ 79 | private $lock_to_ip; 80 | 81 | /** 82 | * @var bool 83 | */ 84 | private $lock_to_user_agent; 85 | 86 | /** 87 | * @var string 88 | */ 89 | private $table_name = 'session_data'; 90 | 91 | /** 92 | * @var string 93 | */ 94 | private $table_name_lock = 'lock_data'; 95 | 96 | /** 97 | * @var string 98 | */ 99 | private $security_code; 100 | 101 | /** 102 | * @var string 103 | */ 104 | private $_fingerprint; 105 | 106 | /** 107 | * @var string 108 | */ 109 | private $_session_id; 110 | 111 | /** 112 | * Constructor of class. Initializes the class and automatically calls 113 | * {@link http://php.net/manual/en/function.session-start.php start_session()}. 114 | * 115 | *
116 | * // first, connect to a database containing the sessions table
117 | *
118 | * // include the class (use the composer-"autoloader")
119 | * require 'vendor/autoload.php';
120 | *
121 | * // start the session
122 | * $session = new Session2DB();
123 | *
124 | *
125 | * By default, the cookie used by PHP to propagate session data across multiple pages ('PHPSESSID') uses the
126 | * current top-level domain and subdomain in the cookie declaration.
127 | *
128 | * Example: www.domain.com
129 | *
130 | * This means that the session data is not available to other subdomains. Therefore, a session started on
131 | * www.domain.com will not be available on blog.domain.com. The solution is to change the domain PHP uses when it
132 | * sets the 'PHPSESSID' cookie by calling the line below *before* instantiating the Session library.
133 | *
134 | *
135 | * // takes the domain and removes the subdomain
136 | * // blog.domain.com becoming .domain.com
137 | * ini_set(
138 | * 'session.cookie_domain',
139 | * substr($_SERVER['SERVER_NAME'], strpos($_SERVER['SERVER_NAME'], '.'))
140 | * );
141 | *
142 | *
143 | * From now on whenever PHP sets the 'PHPSESSID' cookie, the cookie will be available to all subdomains!
144 | *
145 | * @param string $security_code [Optional] The value of this argument is appended to the string
146 | * created by concatenating the user's User Agent (browser) string (or
147 | * an empty string if "lock_to_user_agent" is FALSE) and to the user's
148 | * IP address (or an empty string if "lock_to_ip" is FALSE), before
149 | * creating an SHA1 hash out of it and storing it in the database.
150 | *
151 | * On each call this value will be generated again and compared to the
152 | * value stored in the database ensuring that the session is correctly
153 | * linked
154 | * with the user who initiated the session thus preventing session
155 | * hijacking.
156 | *
157 | * To prevent session hijacking, make sure you choose a string
158 | * around
159 | * 12 characters long containing upper- and lowercase letters, as well as
160 | * digits. To simplify the process, use
161 | * {@link * https://www.random.org/passwords/?num=1&len=12&format=html&rnd=new this} link to generate such a random string.
162 | * @param int $session_lifetime [Optional] The number of seconds after which a session will be
163 | * considered as expired.
164 | *
165 | * Expired sessions are cleaned up from the database whenever the
166 | * garbage collection routine is run. The probability of the
167 | * garbage collection routine to be executed is given by the values
168 | * of
169 | * $gc_probability and $gc_divisor. See below.
170 | *
171 | * Default is the value of session.gc_maxlifetime as set in in
172 | * php.ini. Read more at
173 | * {@link * http://www.php.net/manual/en/session.configuration.php}
174 | *
175 | * To clear any confusions that may arise: in reality,
176 | * session.gc_maxlifetime does not represent a session's lifetime
177 | * but the number of seconds after which a session is seen as
178 | * garbage and is deleted by the garbage collection routine.
179 | * The PHP setting that sets a session's lifetime is
180 | * session.cookie_lifetime and is usually set to "0" - indicating
181 | * that
182 | * a session is active until the browser/browser tab is closed. When this
183 | * class is used, a session is active until the browser/browser tab is
184 | * closed and/or a session has been inactive for more than the number of
185 | * seconds specified by session.gc_maxlifetime.
186 | *
187 | * To see the actual value of session.gc_maxlifetime for your
188 | * environment, use the {@link get_settings()} method.
189 | *
190 | * Pass an empty string to keep default value.
191 | * @param bool $lock_to_user_agent [Optional] Whether to restrict the session to the same User Agent (or
192 | * browser) as when the session was first opened.
193 | *
194 | * The user agent check only adds minor security, since an attacker
195 | * that
196 | * hijacks the session cookie will most likely have the same user
197 | * agent.
198 | *
199 | * In certain scenarios involving Internet Explorer, the browser will
200 | * randomly change the user agent string from one page to the next by
201 | * automatically switching into compatibility mode. So, on the first load
202 | * you would have something like:
203 | *
204 | * Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0;
205 | * etc...
206 | *
207 | * and reloading the page you would have
208 | *
209 | * Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Trident/4.0;
210 | * etc...
211 | *
212 | * So, if the situation asks for this, change this value to FALSE.
213 | *
214 | * Default is FALSE.
215 | * @param bool $lock_to_ip [Optional] Whether to restrict the session to the same IP as when
216 | * the
217 | * session was first opened.
218 | *
219 | * Use this with caution as many users have dynamic IP addresses which may
220 | * change over time, or may come through proxies.
221 | *
222 | * This is mostly useful if your know that all your users come from static
223 | * IPs.
224 | *
225 | * Default is FALSE
226 | * @param int $gc_probability [Optional] Used in conjunction with $gc_divisor. It defines
227 | * the probability that the garbage collection routine is
228 | * started.
229 | *
230 | * The probability is expressed by the formula:
231 | *
232 | *
233 | * $probability = $gc_probability / $gc_divisor;
234 | *
235 | *
236 | * So, if $gc_probability is 1 and $gc_divisor is 100, it
237 | * means
238 | * that there is a 1% chance the the garbage collection routine
239 | * will
240 | * be called on each request.
241 | *
242 | * Default is the value of session.gc_probability as set in
243 | * php.ini.
244 | * Read more at
245 | * {@link * http://www.php.net/manual/en/session.configuration.php}
246 | *
247 | * To see the actual value of session.gc_probability for your
248 | * environment, and the computed probability, use the
249 | * {@link get_settings()} method.
250 | *
251 | * Pass an empty string to keep default value.
252 | * @param int $gc_divisor [Optional] Used in conjunction with $gc_probability. It
253 | * defines the probability that the garbage collection routine is
254 | * started.
255 | *
256 | * The probability is expressed by the formula:
257 | *
258 | *
259 | * $probability = $gc_probability / $gc_divisor;
260 | *
261 | *
262 | * So, if $gc_probability is 1 and $gc_divisor is 100, it
263 | * means
264 | * that there is a 1% chance the the garbage collection routine
265 | * will
266 | * be called on each request.
267 | *
268 | * Default is the value of session.gc_divisor as set in php.ini.
269 | * Read more at
270 | * {@link * http://www.php.net/manual/en/session.configuration.php}
271 | *
272 | * To see the actual value of session.gc_divisor for your
273 | * environment, and the computed probability, use the
274 | * {@link get_settings()} method.
275 | *
276 | * Pass an empty string to keep default value.
277 | * @param string $table_name [Optional] Name of the DB table used by the class.
278 | *
279 | * Default is session_data
280 | * @param int $lock_timeout [Optional] The maximum amount of time (in seconds) for which a
281 | * lock on the session data can be kept.
282 | *
283 | * This must be lower than the maximum execution time of the
284 | * script!
285 | *
286 | * Session locking is a way to ensure that data is correctly handled in a
287 | * scenario with multiple concurrent AJAX requests.
288 | *
289 | * Read more about it at
290 | * {@link * http://thwartedefforts.org/2006/11/11/race-conditions-with-ajax-and-php-sessions/}
291 | *
292 | * Default is 60
293 | * @param Db4Session|null $db [Optional] A database instance from voku\db\DB ("voku/simple-mysqli")
294 | * @param bool $start_session [Optional] If you want to modify the settings via setters before
295 | * starting the session, you can skip the session-start and do it
296 | * manually via "Session2DB->start()"
297 | */
298 | public function __construct(string $security_code = '', int $session_lifetime = 3600, bool $lock_to_user_agent = false, bool $lock_to_ip = false, int $gc_probability = 1, int $gc_divisor = 1000, string $table_name = '', int $lock_timeout = 60, Db4Session $db = null, bool $start_session = true)
299 | {
300 | if ($db !== null) {
301 | $this->db = $db;
302 | } else {
303 | $this->db = DbWrapper4Session::getInstance();
304 | }
305 |
306 | // If no DB connections could be found and
307 | // we could not connect to the DB, then
308 | // trigger a fatal error message and stop execution.
309 | if (
310 | !$this->db->ping()
311 | &&
312 | !$this->db->reconnect()
313 | ) {
314 | \trigger_error('Session: No DB-Connection!', \E_USER_ERROR);
315 | }
316 |
317 | $this->set_ini_settings($session_lifetime, $gc_probability, $gc_divisor);
318 |
319 | // we'll use this later on in order to try to prevent HTTP_USER_AGENT spoofing
320 | $this->set_security_code($security_code);
321 |
322 | // some other defaults
323 | $this->set_lock_to_user_agent($lock_to_user_agent);
324 | $this->set_lock_to_ip($lock_to_ip);
325 | $this->set_lock_file_tmp(\sys_get_temp_dir() . \DIRECTORY_SEPARATOR . 'session2db.lock.');
326 |
327 | // the table to be used by the class
328 | $this->set_table_name($table_name);
329 |
330 | // the maximum amount of time (in seconds) for which a process can lock the session
331 | $this->set_lock_timeout($lock_timeout);
332 |
333 | // initialize session2db
334 | if ($start_session === true) {
335 | $this->start();
336 | }
337 | }
338 |
339 | /**
340 | * @param string $session_id
341 | *
342 | * @return bool
343 | */
344 | private function _get_lock(string $session_id): bool
345 | {
346 | // skip if we don't use the lock
347 | if (!$this->lock_timeout) {
348 | return true;
349 | }
350 |
351 | // get the lock name, associated with the current session
352 | $look_name = $this->_lock_name($session_id);
353 |
354 | // try to obtain a lock with the given name and timeout
355 |
356 | $time = \time();
357 | $lock_time = (string) ($time + $this->lock_timeout);
358 | $time = (string) $time;
359 | $old_lock_timeout = null;
360 |
361 | if ($this->lock_via_mysql === true) {
362 | $result_lock = $this->_get_lock_mysql_native($look_name);
363 | } elseif ($this->lock_via_mysql === null) {
364 | list($old_lock_timeout, $result_lock) = $this->_get_lock_mysql_fake($look_name, $lock_time);
365 | } else {
366 | list($old_lock_timeout, $result_lock) = $this->_get_lock_php_native($look_name, $lock_time);
367 | }
368 |
369 | if ($old_lock_timeout) {
370 | return $old_lock_timeout >= $time;
371 | }
372 |
373 | // if there was an error, then stop the execution
374 | if (!$result_lock) {
375 | return false;
376 | }
377 |
378 | return true;
379 | }
380 |
381 | /**
382 | * @param string $look_name
383 | * @param string $lock_time
384 | *
385 | * @return array
386 | */
387 | private function _get_lock_mysql_fake(string $look_name, string $lock_time): array
388 | {
389 | // init
390 | $result_lock = false;
391 |
392 | $query_lock = '
393 | SELECT lock_time
394 | FROM ' . $this->table_name_lock . "
395 | WHERE lock_hash = '" . $this->db->escape($look_name) . "'
396 | LIMIT 0, 1
397 | ";
398 | $old_lock_timeout = $this->db->fetchColumn($query_lock, 'lock_time');
399 |
400 | if (!$old_lock_timeout) {
401 | $query_lock = '
402 | INSERT INTO
403 | ' . $this->table_name_lock . "
404 | (
405 | lock_hash,
406 | lock_time
407 | )
408 | VALUES
409 | (
410 | '" . $this->db->escape($look_name) . "',
411 | '" . $this->db->escape($lock_time) . "'
412 | )
413 | ON DUPLICATE KEY UPDATE
414 | lock_time = '" . $this->db->escape($lock_time) . "'
415 | ";
416 |
417 | if ($this->db->query($query_lock) !== false) {
418 | $result_lock = true;
419 | }
420 | }
421 |
422 | return [$old_lock_timeout, $result_lock];
423 | }
424 |
425 | /**
426 | * @param string $look_name
427 | *
428 | * @return bool
429 | */
430 | private function _get_lock_mysql_native($look_name): bool
431 | {
432 | $query_lock = "SELECT GET_LOCK('" . $this->db->escape($look_name) . "', " . $this->db->escape($this->lock_timeout) . ') as result';
433 | $db_result = $this->db->query($query_lock);
434 |
435 | return (bool) $db_result->fetchColumn('result');
436 | }
437 |
438 | /**
439 | * @param string $look_name
440 | * @param string $lock_time
441 | *
442 | * @return array
443 | */
444 | private function _get_lock_php_native(string $look_name, string $lock_time): array
445 | {
446 | // init
447 | $result_lock = false;
448 | $lock_file = $this->lock_file_tmp . $look_name;
449 |
450 | /** @noinspection PhpUsageOfSilenceOperatorInspection */
451 | $fp = @\fopen($lock_file, 'rb');
452 | $old_lock_timeout = '';
453 | if ($fp && \flock($fp, \LOCK_SH | \LOCK_NB)) {
454 | while (!\feof($fp)) {
455 | $line = \fgets($fp);
456 | $old_lock_timeout .= $line;
457 | }
458 | \flock($fp, \LOCK_UN);
459 | }
460 | if ($fp) {
461 | \fclose($fp);
462 | }
463 |
464 | if (!$old_lock_timeout) {
465 | /** @noinspection PhpUsageOfSilenceOperatorInspection */
466 | $fp = @\fopen($lock_file, 'ab');
467 | if ($fp && \flock($fp, \LOCK_EX | \LOCK_NB)) {
468 | \ftruncate($fp, 0);
469 | $result_lock = \fwrite($fp, $lock_time);
470 | \fflush($fp);
471 | \flock($fp, \LOCK_UN);
472 | \fclose($fp);
473 | }
474 | }
475 |
476 | return [$old_lock_timeout, $result_lock];
477 | }
478 |
479 | /**
480 | * @param string $session_id
481 | *
482 | * @return string
483 | */
484 | private function _lock_name(string $session_id): string
485 | {
486 | // MySQL >=5.7.5 | the new GET_LOCK implementation has a limit on the identifier name
487 | // -> https://bugs.mysql.com/bug.php?id=80721
488 | return 'session_' . \sha1($session_id);
489 | }
490 |
491 | /**
492 | * Manages flashdata behind the scenes.
493 | *
494 | * @return void
495 | */
496 | public function _manage_flashdata()
497 | {
498 | // if there is flashdata to be handled
499 | if (!empty($this->flashdata)) {
500 |
501 | // iterate through all the entries
502 | foreach ($this->flashdata as $variable => $counter) {
503 |
504 | // increment counter representing server requests
505 | $this->flashdata[$variable]++;
506 |
507 | // if we're past the first server request
508 | if ($this->flashdata[$variable] > 1) {
509 |
510 | // unset the session variable & stop tracking
511 | unset($_SESSION[$variable], $this->flashdata[$variable]);
512 | }
513 | }
514 |
515 | // if there is any flashdata left to be handled
516 | // ... then store data in a temporary session variable
517 | if (!empty($this->flashdata)) {
518 | $_SESSION[self::flashDataVarName] = \serialize($this->flashdata);
519 | }
520 | }
521 | }
522 |
523 | /**
524 | * @param string $session_id
525 | *
526 | * @return bool
527 | */
528 | private function _release_lock(string $session_id): bool
529 | {
530 | // skip if we don't use the lock
531 | if (!$this->lock_timeout) {
532 | return true;
533 | }
534 |
535 | // get the lock name, associated with the current session
536 | $look_name = $this->_lock_name($session_id);
537 |
538 | // release the lock associated with the current session
539 |
540 | if ($this->lock_via_mysql === true) {
541 | $result_unlock = $this->_release_lock_sql_native($look_name);
542 | } elseif ($this->lock_via_mysql === null) {
543 | $result_unlock = $this->_release_lock_sql_fake($look_name);
544 | } else {
545 | $result_unlock = $this->_release_lock_php_native($look_name);
546 | }
547 |
548 | // if there was an error, then stop the execution
549 | if (!$result_unlock) {
550 | return false;
551 | }
552 |
553 | return true;
554 | }
555 |
556 | /**
557 | * @param string $look_name
558 | *
559 | * @return bool
560 | */
561 | private function _release_lock_php_native(string $look_name): bool
562 | {
563 | $lock_file = $this->lock_file_tmp . $look_name;
564 | if (\file_exists($lock_file) === true) {
565 | /** @noinspection PhpUsageOfSilenceOperatorInspection */
566 | $result_unlock = @\unlink($lock_file);
567 | } else {
568 | $result_unlock = true;
569 | }
570 |
571 | return $result_unlock;
572 | }
573 |
574 | /**
575 | * @param string $look_name
576 | *
577 | * @return bool
578 | */
579 | private function _release_lock_sql_fake(string $look_name): bool
580 | {
581 | $query = 'DELETE FROM ' . $this->table_name_lock . "
582 | WHERE lock_hash = '" . $this->db->escape($look_name) . "'
583 | ";
584 |
585 | return (bool) $this->db->query($query);
586 | }
587 |
588 | /**
589 | * @param string $look_name
590 | *
591 | * @return bool
592 | */
593 | private function _release_lock_sql_native(string $look_name): bool
594 | {
595 | $query = "SELECT RELEASE_LOCK('" . $this->db->escape($look_name) . "') as result";
596 | $db_result = $this->db->query($query);
597 |
598 | return (bool) $db_result->fetchColumn('result');
599 | }
600 |
601 | /**
602 | * Custom close() function.
603 | *
604 | * @return bool
605 | */
606 | public function close(): bool
607 | {
608 | // 1. write all data into the db
609 | \session_register_shutdown();
610 |
611 | // 2. release the lock, if there is a lock
612 | if ($this->_session_id) {
613 | $this->_release_lock($this->_session_id);
614 | }
615 |
616 | // 3. close the db-connection
617 | $this->db->close();
618 |
619 | return true;
620 | }
621 |
622 | /**
623 | * Custom destroy() function.
624 | *
625 | * @param string $session_id
626 | *
627 | * @return bool
628 | */
629 | public function destroy($session_id): bool
630 | {
631 | // deletes the current locks from the database
632 | if ($this->lock_via_mysql === null) {
633 | $queryLock = 'DELETE FROM ' . $this->table_name_lock . "
634 | WHERE lock_time < '" . $this->db->escape(\time()) . "'
635 | ";
636 | $this->db->query($queryLock);
637 | }
638 |
639 | // deletes the current session id from the database
640 |
641 | $query = 'DELETE FROM ' . $this->table_name . "
642 | WHERE session_id = '" . $this->db->escape($session_id) . "'
643 | ";
644 | $result = $this->db->query($query);
645 |
646 | return $result > 0;
647 | }
648 |
649 | /**
650 | * Custom gc() function (garbage collector).
651 | *
652 | * @param int $maxlifetime INFO: must be set for the interface.
653 | * 654 | * @return bool 655 | */ 656 | public function gc($maxlifetime): bool 657 | { 658 | // deletes expired locks from database 659 | if ($this->lock_via_mysql === null) { 660 | $queryLock = 'DELETE FROM ' . $this->table_name_lock . " 661 | WHERE lock_time < '" . $this->db->escape(\time()) . "' 662 | "; 663 | $this->db->query($queryLock); 664 | } 665 | 666 | // deletes expired sessions from database 667 | 668 | $query = 'DELETE FROM ' . $this->table_name . " 669 | WHERE session_expire < '" . $this->db->escape(\time()) . "' 670 | "; 671 | 672 | $this->db->query($query); 673 | 674 | return true; 675 | } 676 | 677 | /** 678 | * Custom open() function. 679 | * 680 | * @param string $save_path 681 | * @param string $session_name 682 | * 683 | * @return bool 684 | */ 685 | public function open($save_path, $session_name): bool 686 | { 687 | // session_regenerate_id() ---> 688 | // 689 | // PHP5: call -> "destroy" 690 | // 691 | // PHP7: call -> "destroy", "read", "close", "open", "read" 692 | // 693 | // WARNING: PHP >= 7.0 will reuse $this session-handler-object, so we need to reconnect to the database 694 | // 695 | if (!$this->db->ping()) { 696 | $this->db->reconnect(); 697 | } 698 | 699 | return $this->db->ping(); 700 | } 701 | 702 | /** 703 | * Custom read() function. 704 | * 705 | * @param string $session_id 706 | * 707 | * @return string 708 | */ 709 | public function read($session_id): string 710 | { 711 | // Needed by write() to detect session_regenerate_id() calls 712 | $this->_session_id = $session_id; 713 | 714 | // try to obtain a lock with the given name and timeout 715 | $locked = $this->_get_lock($session_id); 716 | 717 | // if there was an error, then stop the execution 718 | if ($locked === false) { 719 | \trigger_error('Session: Could not obtain session lock!', \E_USER_ERROR); 720 | } 721 | 722 | $hash = $this->get_fingerprint(); 723 | 724 | $query = 'SELECT 725 | session_data 726 | FROM 727 | ' . $this->table_name . " 728 | WHERE session_id = '" . $this->db->escape($session_id) . "' 729 | AND hash = '" . $this->db->escape($hash) . "' 730 | AND session_expire > '" . $this->db->escape(\time()) . "' 731 | LIMIT 1 732 | "; 733 | 734 | /** @var string $data */ 735 | $data = $this->db->fetchColumn($query, 'session_data'); 736 | 737 | // if anything was found 738 | if ($data) { 739 | // don't bother with the unserialization - PHP handles this automatically 740 | return $data; 741 | } 742 | 743 | // on error return an empty string - this HAS to be an empty string 744 | return ''; 745 | } 746 | 747 | /** 748 | * Custom write() function. 749 | * 750 | * @param string $session_id 751 | * @param string $session_data 752 | * 753 | * @return bool|string 754 | */ 755 | public function write($session_id, $session_data) 756 | { 757 | // check if the "$session_id" was regenerated 758 | if ( 759 | $this->_session_id 760 | && 761 | $session_id !== $this->_session_id 762 | ) { 763 | if ( 764 | $this->_release_lock($this->_session_id) === false 765 | || 766 | $this->_get_lock($session_id) === false 767 | ) { 768 | return false; 769 | } 770 | 771 | $this->_session_id = $session_id; 772 | } 773 | 774 | $hash = $this->get_fingerprint(); 775 | $expire_time = \time() + (int) $this->session_lifetime; 776 | 777 | $query = 'INSERT INTO 778 | ' . $this->table_name . " 779 | ( 780 | session_id, 781 | hash, 782 | session_data, 783 | session_expire 784 | ) 785 | VALUES 786 | ( 787 | '" . $this->db->escape($session_id) . "', 788 | '" . $this->db->escape($hash) . "', 789 | '" . $this->db->escape($session_data) . "', 790 | '" . $this->db->escape($expire_time) . "' 791 | ) 792 | ON DUPLICATE KEY UPDATE 793 | session_data = '" . $this->db->escape($session_data) . "', 794 | session_expire = '" . $this->db->escape($expire_time) . "' 795 | "; 796 | 797 | // insert OR update session's data 798 | $result = $this->db->query($query); 799 | 800 | return $result !== false; 801 | } 802 | 803 | /** 804 | * @return void 805 | */ 806 | private function generate_fingerprint() 807 | { 808 | // reads session data associated with a session id, but only if 809 | // - the session ID exists; 810 | // - the session has not expired; 811 | // - 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; 812 | // - if lock_to_ip is TRUE and the host is the same as the one who had previously been associated with this particular session; 813 | $hash = ''; 814 | 815 | // if we need to identify sessions by also checking the user agent 816 | if ($this->lock_to_user_agent && isset($_SERVER['HTTP_USER_AGENT'])) { 817 | $hash .= $_SERVER['HTTP_USER_AGENT']; 818 | } 819 | 820 | // if we need to identify sessions by also checking the host 821 | if ($this->lock_to_ip && isset($_SERVER['REMOTE_ADDR'])) { 822 | $hash .= $_SERVER['REMOTE_ADDR']; 823 | } 824 | 825 | // append this to the end 826 | $hash .= $this->security_code; 827 | 828 | // save the fingerprint-hash into the current object 829 | $this->_fingerprint = \sha1($hash); 830 | } 831 | 832 | /** 833 | * Get the number of active sessions - sessions that have not expired. 834 | * 835 | * The returned value does not represent the exact number of active users as some sessions may be unused 836 | * although they haven't expired. 837 | * 838 | *
839 | * // first, connect to a database containing the sessions table
840 | *
841 | * // include the class (use the composer-"autoloader")
842 | * require 'vendor/autoload.php';
843 | *
844 | * // start the session
845 | * $session = new Session2DB();
846 | *
847 | * // get the (approximate) number of active sessions
848 | * $active_sessions = $session->get_active_sessions();
849 | *
850 | *
851 | * @return int
852 | * Returns the number of active (not expired) sessions.
853 | */ 854 | public function get_active_sessions(): int 855 | { 856 | // call the garbage collector 857 | $this->gc($this->session_lifetime); 858 | 859 | $query = 'SELECT COUNT(session_id) AS count 860 | FROM ' . $this->table_name . ' 861 | '; 862 | 863 | // counts the rows from the database 864 | /** @var int|string $count */ 865 | $count = $this->db->fetchColumn($query, 'count'); 866 | 867 | return (int) $count; 868 | } 869 | 870 | /** 871 | * @return string 872 | */ 873 | public function get_fingerprint(): string 874 | { 875 | return $this->_fingerprint; 876 | } 877 | 878 | /** 879 | * Queries the system for the values of session.gc_maxlifetime, session.gc_probability and 880 | * session.gc_divisor and returns them as an associative array. 881 | * 882 | * To view the result in a human-readable format use: 883 | *
884 | * // include the class (use the composer-"autoloader")
885 | * require 'vendor/autoload.php';
886 | *
887 | * // start the session
888 | * $session = new Session2DB();
889 | *
890 | * // get default settings
891 | * print_r('');
892 | * print_r($session->get_settings());
893 | *
894 | * // would output something similar to (depending on your actual settings)
895 | * // array
896 | * // (
897 | * // [session.gc_maxlifetime] => 1440 seconds (24 minutes)
898 | * // [session.gc_probability] => 1
899 | * // [session.gc_divisor] => 1000
900 | * // [probability] => 0.1%
901 | * // )
902 | *
903 | *
904 | * @return string[]
905 | * 906 | * Returns the values of session.gc_maxlifetime, session.gc_probability and 907 | * session.gc_divisor as an associative array. 908 | *
909 | */ 910 | public function get_settings(): array 911 | { 912 | // get the settings 913 | $gc_maxlifetime = \ini_get('session.gc_maxlifetime'); 914 | $gc_probability = \ini_get('session.gc_probability'); 915 | $gc_divisor = \ini_get('session.gc_divisor'); 916 | 917 | // return them as an array 918 | return [ 919 | 'session.gc_maxlifetime' => (string)$gc_maxlifetime . ' seconds (' . \round($gc_maxlifetime / 60) . ' minutes)', 920 | 'session.gc_probability' => (string)$gc_probability, 921 | 'session.gc_divisor' => (string)$gc_divisor, 922 | 'probability' => (string)($gc_probability / $gc_divisor * 100) . '%', 923 | ]; 924 | } 925 | 926 | /** 927 | * Regenerates the session id. 928 | * 929 | * Call this method whenever you do a privilege change in order to prevent session hijacking! 930 | * 931 | *
932 | * // first, connect to a database containing the sessions table
933 | *
934 | * // include the class (use the composer-"autoloader")
935 | * require 'vendor/autoload.php';
936 | *
937 | * // start the session
938 | * $session = new Session2DB();
939 | *
940 | * // regenerate the session's ID
941 | * $session->regenerate_id();
942 | *
943 | *
944 | * @return void
945 | */
946 | public function regenerate_id()
947 | {
948 | // regenerates the id (create a new session with a new id and containing the data from the old session)
949 | // also, delete the old session
950 | \session_regenerate_id(true);
951 | }
952 |
953 | /**
954 | * @return bool
955 | */
956 | private function register_session_handler(): bool
957 | {
958 | // WARNING: PHP 7.2 throws a warning for "session"-ini, so we catch it here ...
959 | if (
960 | \PHP_SAPI !== 'cli'
961 | &&
962 | \headers_sent() === true
963 | ) {
964 | \trigger_error('Cannot change save handler when headers already sent', \E_USER_WARNING);
965 | }
966 |
967 | /** @noinspection PhpUsageOfSilenceOperatorInspection */
968 | return @\session_set_save_handler($this, true);
969 | }
970 |
971 | /**
972 | * Sets a "flashdata" session variable which will only be available for the next server request, and which will be
973 | * automatically deleted afterwards.
974 | *
975 | * Typically used for informational or status messages (for example: "data has been successfully updated").
976 | *
977 | *
978 | * // first, connect to a database containing the sessions table
979 | *
980 | * // include the class (use the composer-"autoloader")
981 | * require 'vendor/autoload.php';
982 | *
983 | * // start the session
984 | * $session = new Session2DB();
985 | *
986 | * // set "myvar" which will only be available
987 | * // for the next server request and will be
988 | * // automatically deleted afterwards
989 | * $session->set_flashdata('myvar', 'myval');
990 | *
991 | *
992 | * Flashdata session variables can be retrieved as any other session variable:
993 | *
994 | *
995 | * if (isset($_SESSION['myvar'])) {
996 | * // do something here but remember that the
997 | * // flashdata session variable is available
998 | * // for a single server request after it has
999 | * // been set!
1000 | * }
1001 | *
1002 | *
1003 | * @param string $name The name of the session variable.
1004 | * @param mixed $valueThe value of the session variable.
1005 | * 1006 | * @return $this 1007 | */ 1008 | public function set_flashdata(string $name, $value): self 1009 | { 1010 | // set session variable 1011 | $_SESSION[$name] = $value; 1012 | 1013 | // initialize the counter for this flashdata 1014 | $this->flashdata[$name] = 0; 1015 | 1016 | return $this; 1017 | } 1018 | 1019 | /** 1020 | * @param int $session_lifetime 1021 | * @param int $gc_probability 1022 | * @param int $gc_divisor 1023 | * 1024 | * @return $this 1025 | */ 1026 | public function set_ini_settings(int $session_lifetime, int $gc_probability, int $gc_divisor): self 1027 | { 1028 | // WARNING: PHP 7.2 throws a warning for "session"-ini, so we catch it here ... 1029 | if ( 1030 | \PHP_SAPI !== 'cli' 1031 | && 1032 | \headers_sent() === true 1033 | ) { 1034 | \trigger_error('You cannot change the session module\'s ini settings at this time', \E_USER_WARNING); 1035 | } 1036 | 1037 | // Prevent session-fixation 1038 | // See: http://en.wikipedia.org/wiki/Session_fixation 1039 | // 1040 | // Tell the browser not to expose the cookie to client side scripting, 1041 | // this makes it harder for an attacker to hijack the session ID. 1042 | /** @noinspection PhpUsageOfSilenceOperatorInspection */ 1043 | @\ini_set('session.cookie_httponly', '1'); 1044 | 1045 | // Make sure that PHP only uses cookies for sessions and disallow session ID passing as a GET parameter, 1046 | /** @noinspection PhpUsageOfSilenceOperatorInspection */ 1047 | @\ini_set('session.use_only_cookies', '1'); 1048 | 1049 | // PHP 7.1 Incompatible Changes 1050 | // -> http://php.net/manual/en/migration71.incompatible.php 1051 | if (Bootup::is_php('7.1') === false) { 1052 | // Use the SHA-1 hashing algorithm 1053 | /** @noinspection PhpUsageOfSilenceOperatorInspection */ 1054 | @\ini_set('session.hash_function', '1'); 1055 | 1056 | // Increase character-range of the session ID to help prevent brute-force attacks. 1057 | // 1058 | // INFO: The possible values are '4' (0-9, a-f), '5' (0-9, a-v), and '6' (0-9, a-z, A-Z, "-", ","). 1059 | /** @noinspection PhpUsageOfSilenceOperatorInspection */ 1060 | @\ini_set('session.hash_bits_per_character', '6'); 1061 | } else { 1062 | // Use longer session id length. 1063 | /** @noinspection PhpUsageOfSilenceOperatorInspection */ 1064 | @\ini_set('session.sid_length', '42'); 1065 | 1066 | // Increase character-range of the session ID to help prevent brute-force attacks. 1067 | // 1068 | // INFO: The possible values are '4' (0-9, a-f), '5' (0-9, a-v), and '6' (0-9, a-z, A-Z, "-", ","). 1069 | /** @noinspection PhpUsageOfSilenceOperatorInspection */ 1070 | @\ini_set('session.sid_bits_per_character', '6'); 1071 | } 1072 | 1073 | // make sure session cookies never expire so that session lifetime 1074 | // will depend only on the value of $session_lifetime 1075 | /** @noinspection PhpUsageOfSilenceOperatorInspection */ 1076 | @\ini_set('session.cookie_lifetime', '0'); 1077 | 1078 | /** @noinspection PhpUsageOfSilenceOperatorInspection */ 1079 | @\ini_set('session.gc_maxlifetime', (string) $session_lifetime); 1080 | $this->session_lifetime = (int)\ini_get('session.gc_maxlifetime'); 1081 | 1082 | /** @noinspection PhpUsageOfSilenceOperatorInspection */ 1083 | @\ini_set('session.gc_probability', (string) $gc_probability); 1084 | 1085 | /** @noinspection PhpUsageOfSilenceOperatorInspection */ 1086 | @\ini_set('session.gc_divisor', (string) $gc_divisor); 1087 | 1088 | return $this; 1089 | } 1090 | 1091 | /** 1092 | * @param string $lock_file_tmp 1093 | * 1094 | * @return $this 1095 | */ 1096 | public function set_lock_file_tmp(string $lock_file_tmp): self 1097 | { 1098 | if ($lock_file_tmp) { 1099 | $this->lock_file_tmp = $lock_file_tmp; 1100 | } 1101 | 1102 | return $this; 1103 | } 1104 | 1105 | /** 1106 | * @param int $lock_timeout 1107 | * 1108 | * @return $this 1109 | */ 1110 | public function set_lock_timeout(int $lock_timeout): self 1111 | { 1112 | $this->lock_timeout = $lock_timeout; 1113 | 1114 | return $this; 1115 | } 1116 | 1117 | /** 1118 | * @param bool $lock_to_ip 1119 | * 1120 | * @return $this 1121 | */ 1122 | public function set_lock_to_ip(bool $lock_to_ip): self 1123 | { 1124 | $this->lock_to_ip = $lock_to_ip; 1125 | 1126 | $this->generate_fingerprint(); 1127 | 1128 | return $this; 1129 | } 1130 | 1131 | /** 1132 | * @param bool $lock_to_user_agent 1133 | * 1134 | * @return $this 1135 | */ 1136 | public function set_lock_to_user_agent(bool $lock_to_user_agent): self 1137 | { 1138 | $this->lock_to_user_agent = $lock_to_user_agent; 1139 | 1140 | $this->generate_fingerprint(); 1141 | 1142 | return $this; 1143 | } 1144 | 1145 | /** 1146 | * @param string $security_code 1147 | * 1148 | * @return $this 1149 | */ 1150 | public function set_security_code(string $security_code): self 1151 | { 1152 | // fallback for the security-code 1153 | if ( 1154 | $security_code === '' 1155 | || 1156 | $security_code === '###set_the_security_key###' 1157 | ) { 1158 | $security_code = 'sEcUrmenadwork_))'; 1159 | } 1160 | 1161 | $this->security_code = $security_code; 1162 | 1163 | $this->generate_fingerprint(); 1164 | 1165 | return $this; 1166 | } 1167 | 1168 | /** 1169 | * @param string $table_name 1170 | * 1171 | * @return $this 1172 | */ 1173 | public function set_table_name(string $table_name): self 1174 | { 1175 | if ($table_name) { 1176 | $this->table_name = $this->db->quote_string($table_name); 1177 | } 1178 | 1179 | return $this; 1180 | } 1181 | 1182 | /** 1183 | * @param string $table_name_lock 1184 | * 1185 | * @return $this 1186 | */ 1187 | public function set_table_name_lock(string $table_name_lock): self 1188 | { 1189 | if ($table_name_lock) { 1190 | $this->table_name_lock = $this->db->quote_string($table_name_lock); 1191 | } 1192 | 1193 | return $this; 1194 | } 1195 | 1196 | /** 1197 | * @return bool 1198 | */ 1199 | public function start(): bool 1200 | { 1201 | // register the new session-handler 1202 | $result = $this->register_session_handler(); 1203 | if ($result === false) { 1204 | return false; 1205 | } 1206 | 1207 | // start the session 1208 | if (\PHP_SAPI === 'cli') { 1209 | $_SESSION = []; 1210 | $result = true; 1211 | } else { 1212 | $result = \session_start(); 1213 | } 1214 | if ($result === false) { 1215 | return false; 1216 | } 1217 | 1218 | // if there are any flashdata variables that need to be handled 1219 | if (isset($_SESSION[self::flashDataVarName])) { 1220 | 1221 | // store them 1222 | /** @noinspection UnserializeExploitsInspection */ 1223 | $this->flashdata = \unserialize($_SESSION[self::flashDataVarName]); 1224 | 1225 | // and destroy the temporary session variable 1226 | unset($_SESSION[self::flashDataVarName]); 1227 | } 1228 | 1229 | // handle flashdata after script execution 1230 | \register_shutdown_function( 1231 | [ 1232 | $this, 1233 | '_manage_flashdata', 1234 | ] 1235 | ); 1236 | 1237 | return $result; 1238 | } 1239 | 1240 | /** 1241 | * Deletes all data related to the session 1242 | * 1243 | *
1244 | * // first, connect to a database containing the sessions table
1245 | *
1246 | * // include the class (use the composer-"autoloader")
1247 | * require 'vendor/autoload.php';
1248 | *
1249 | * // start the session
1250 | * $session = new Session2DB();
1251 | *
1252 | * // end current session
1253 | * $session->stop();
1254 | *
1255 | *
1256 | * @return void
1257 | */
1258 | public function stop()
1259 | {
1260 | if (\PHP_SAPI === 'cli') {
1261 | return;
1262 | }
1263 |
1264 | // if a cookie is used to pass the session id
1265 | if (\ini_get('session.use_cookies')) {
1266 | // get session cookie's properties
1267 | $params = \session_get_cookie_params();
1268 |
1269 | // unset the cookie
1270 | \setcookie(\session_name(), '', \time() - 42000, $params['path'], $params['domain'], $params['secure'], $params['httponly']);
1271 | }
1272 |
1273 | \session_unset();
1274 | \session_destroy();
1275 | }
1276 |
1277 | /**
1278 | * @param bool|null $boolOrNull
1279 | * true => use mysql GET_LOCK() / RELEASE_LOCK()
1280 | * false => use php flock() + LOCK_EX
1281 | * null => use mysql + extra lock-table
1282 | *