├── 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 | [![Build Status](https://travis-ci.org/voku/session2db.svg?branch=master)](https://travis-ci.org/voku/session2db) 2 | [![Coverage Status](https://coveralls.io/repos/github/voku/session2db/badge.svg?branch=master)](https://coveralls.io/github/voku/session2db?branch=master) 3 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/836db772ff9443b18103d6a6c6ee35eb)](https://www.codacy.com/app/voku/session2db) 4 | [![Latest Stable Version](https://poser.pugx.org/voku/session2db/v/stable)](https://packagist.org/packages/voku/session2db) 5 | [![Total Downloads](https://poser.pugx.org/voku/session2db/downloads)](https://packagist.org/packages/voku/session2db) 6 | [![License](https://poser.pugx.org/voku/session2db/license)](https://packagist.org/packages/voku/session2db) 7 | [![Donate to this project using Paypal](https://img.shields.io/badge/paypal-donate-yellow.svg)](https://www.paypal.me/moelleken) 8 | [![Donate to this project using Patreon](https://img.shields.io/badge/patreon-donate-yellow.svg)](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. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /src/voku/helper/Db4Session.php: -------------------------------------------------------------------------------- 1 | empty string on error

27 | */ 28 | public function fetchColumn(string $sql, string $string); 29 | 30 | /** 31 | * @return bool 32 | */ 33 | public function ping(): bool; 34 | 35 | /** 36 | * @param string $sql 37 | * 38 | * @return false|mixed 39 | *

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 $value

The 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 | *

1283 | * 1284 | * @return $this 1285 | */ 1286 | public function use_lock_via_mysql($boolOrNull): self 1287 | { 1288 | $this->lock_via_mysql = $boolOrNull; 1289 | 1290 | return $this; 1291 | } 1292 | } 1293 | --------------------------------------------------------------------------------