├── run-tests.sh
├── install
└── session_data.sql
├── examples
├── example.php
└── example-pdo.php
├── LICENSE.md
├── README.md
├── CHANGELOG.md
└── Zebra_Session.php
/run-tests.sh:
--------------------------------------------------------------------------------
1 | vendor/bin/phpstan analyse; vendor/bin/phpcs --standard=coding-standards.xml
--------------------------------------------------------------------------------
/install/session_data.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE `session_data` (
2 | `session_id` varchar(32) NOT NULL default '',
3 | `hash` varchar(32) NOT NULL default '',
4 | `session_data` blob NOT NULL,
5 | `session_expire` int(11) NOT NULL default '0',
6 | PRIMARY KEY (`session_id`)
7 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
--------------------------------------------------------------------------------
/examples/example.php:
--------------------------------------------------------------------------------
1 | Current session settings:
');
29 | print_r($session->get_settings());
30 | print_r('');
31 |
32 | // from now on, use sessions as you would normally
33 | // the only difference is that session data is no longer saved on the server
34 | // but in your database
35 |
36 | print_r('
37 | The first time you run the script there should be an empty array (as there\'s nothing in the $_SESSION array)
38 | After you press "refresh" on your browser, you will se the values that were written in the $_SESSION array
39 | ');
40 |
41 | print_r('
');
42 | print_r($_SESSION);
43 | print_r('');
44 |
45 | // add some values to the session
46 | $_SESSION['value1'] = 'hello';
47 | $_SESSION['value2'] = 'world';
48 |
49 | // now check the table and see that there is data in it!
50 |
51 | // to completely delete a session un-comment the following line
52 | //$session->stop();
53 |
54 | ?>
--------------------------------------------------------------------------------
/examples/example-pdo.php:
--------------------------------------------------------------------------------
1 | PDO::ERRMODE_EXCEPTION,
22 | ));
23 | } catch (\PDOException $e) {
24 | throw new \PDOException($e->getMessage(), (int)$e->getCode());
25 | }
26 |
27 | // include the Zebra_Session class
28 | require '../Zebra_Session.php';
29 |
30 | // instantiate the class
31 | // note that you don't need to call the session_start() function
32 | // as it is called automatically when the object is instantiated
33 | // also note that we are passing the PDO instance as the first argument
34 | $session = new Zebra_Session($pdo, 'sEcUr1tY_c0dE', '', true, false, 1000);
35 |
36 | // current session settings
37 | print_r('Current session settings:'); 40 | 41 | // from now on, use sessions as you would normally 42 | // the only difference is that session data is no longer saved on the server 43 | // but in your database 44 | 45 | print_r(' 46 | The first time you run the script there should be an empty array (as there\'s nothing in the $_SESSION array)
'); 38 | print_r($session->get_settings()); 39 | print_r('
');
51 | print_r($_SESSION);
52 | print_r('');
53 |
54 | // add some values to the session
55 | $_SESSION['value1'] = 'hello';
56 | $_SESSION['value2'] = 'world';
57 |
58 | // now check the table and see that there is data in it!
59 |
60 | // to completely delete a session un-comment the following line
61 | // $session->stop();
62 |
63 | ?>
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | ### GNU LESSER GENERAL PUBLIC LICENSE
2 |
3 | Version 3, 29 June 2007
4 |
5 | Copyright (C) 2007 Free Software Foundation, Inc.
6 |
2 |
3 | # Zebra Session [](https://twitter.com/intent/tweet?text=A+drop-in+replacement+for+PHP's+default+session+handler+which+stores+session+data+in+a+MySQL+database&url=https://github.com/stefangabos/Zebra_Session&via=stefangabos&hashtags=php)
4 |
5 | *A drop-in replacement for PHP's default session handler which stores session data in a MySQL database, providing better performance, better security and protection against session fixation and session hijacking*
6 |
7 | [](https://packagist.org/packages/stefangabos/zebra_session) [](https://packagist.org/packages/stefangabos/zebra_session) [](https://packagist.org/packages/stefangabos/zebra_session) [](https://packagist.org/packages/stefangabos/zebra_session) [](https://packagist.org/packages/stefangabos/zebra_session)
8 |
9 | Session support in PHP consists of a way to preserve information (variables) on subsequent accesses to a website's pages. Unlike cookies, variables are not stored on the user's computer. Instead, only a *session identifier* is stored in a cookie on the visitor's computer, which is matched up with the actual session data kept on the server, and made available to us through the [$_SESSION](https://www.php.net/manual/en/reserved.variables.session.php) super-global. Session data is retrieved as soon as we open a session, usually at the beginning of each page.
10 |
11 | By default, session data is stored on the server in flat files, separate for each session. The problem with this scenario is that performance degrades proportionally with the number of session files existing in the session directory (depending on the server's operating system's ability to handle directories with numerous files). Another issue is that session files are usually stored in a location that is world readable posing a security concern on shared hosting.
12 |
13 | This is where **Zebra Session** comes in handy - a PHP library that acts as a drop-in replacement for PHP's default session handler, but instead of storing session data in flat files it stores them in a **MySQL database**, providing better security and better performance.
14 |
15 | Zebra Session is also a solution for applications that are scaled across multiple web servers (using a load balancer or a round-robin DNS) where the user's session data needs to be available. Storing sessions in a database makes them available to all of the servers!
16 |
17 | Supports *"flash data"* - session variables which will only be available for the next server request, and which will be automatically deleted afterwards. Typically used for informational or status messages (for example: "data has been successfully updated").
18 |
19 | This class is was inspired by John Herren's code from the [Trick out your session handler](https://web.archive.org/web/20081221052326/http://devzone.zend.com/node/view/id/141) article (now only available on the [Internet Archive](https://web.archive.org/web/20081221052326/http://devzone.zend.com/node/view/id/141)) and Chris Shiflett's code from his book [Essential PHP Security](https://web.archive.org/web/20190921001622/http://phpsecurity.org/code/ch08-2), chapter 8, Shared Hosting, Pg. 78-80.
20 |
21 | Zebra Session's code is heavily commented and generates no warnings/errors/notices when PHP's error reporting level is set to [E_ALL](https://www.php.net/manual/en/function.error-reporting.php).
22 |
23 | Starting with version 2.0, Zebra Session implements [row locks](https://dev.mysql.com/doc/refman/8.0/en/locking-functions.html#function_get-lock), ensuring that data is correctly handled in a scenario with multiple concurrent AJAX requests.
24 |
25 | Citing from [Race Conditions with Ajax and PHP Sessions](http://thwartedefforts.org/2006/11/11/race-conditions-with-ajax-and-php-sessions/), a great article by Andy Bakun:
26 |
27 | > When locking is not used, multiple requests (represented in these diagrams as processes P1, P2 and P3) access the session data without any consideration for the other processes and the state of the session data. The running time of the requests are indicated by the height of each process's colored area (the actual run times are unimportant, only the relative start times and durations).
28 |
29 | 
30 |
31 | > In the example above, no matter how P2 and P3 change the session data, the only changes that will be reflected in the session are those that P1 made because they were written last. When locking is used, the process can start up, request a lock on the session data before it reads it, and then get a consistent read of the session once it acquires exclusive access to it. In the following diagram, all reads occur after writes:
32 |
33 | 
34 |
35 | > The process execution is interleaved, but access to the session data is serialized. The process is waiting for the lock to be released during the period between when the process requests the session lock and when the session is read. This means that your session data will remain consistent, but it also means that while processes P2 and P3 are waiting for their turn to acquire the lock, nothing is happening. This may not be that important if all of the requests change or write to the session data, but if P2 just needs to read the session data (perhaps to get a login identifier), it is being held up for no reason.
36 |
37 | So, in the end, this is not the best solution but still is better than nothing. The best solution is probably a *per-variable* locking. You can read a very detailed article about all this in Andy Bakun's article [Race Conditions with Ajax and PHP Sessions](http://thwartedefforts.org/2006/11/11/race-conditions-with-ajax-and-php-sessions/).
38 |
39 | Thanks to [Michael Kliewe](https://www.phpgangsta.de/) who brought this to my attention!
40 |
41 | ## Features
42 |
43 | - acts as a wrapper for PHP's default session handling functions, but instead of storing session data in flat files it stores them in a MySQL database, providing better security and better performance
44 |
45 | - it is a drop-in and seamingless replacement for PHP's default session handler: PHP sessions will be used in the same way as prior to using the library; you don't need to change any existing code!
46 |
47 | - integrates seamlesly with PDO (if you are using PDO) but works perfectly without it
48 |
49 | - implements *row locks*, ensuring that data is correctly handled in scenarios with multiple concurrent AJAX requests
50 |
51 | - because session data is stored in a database, the library represents a solution for applications that are scaled across multiple web servers (using a load balancer or a round-robin DNS)
52 |
53 | - has awesome documentation
54 |
55 | - the code is heavily commented and generates no warnings/errors/notices when PHP's error reporting level is set to E_ALL
56 |
57 | ## :notebook_with_decorative_cover: Documentation
58 |
59 | Check out the [awesome documentation](https://stefangabos.github.io/Zebra_Session/Zebra_Session/Zebra_Session.html)!
60 |
61 | ## 🎂 Support the development of this project
62 |
63 | Your support means a lot and it keeps me motivated to keep working on open source projects.
79 | * // first, connect to a database containing the sessions table, either via PDO or using mysqli_connect
80 | *
81 | * // include the class
82 | * // (you don't need this if you are using Composer)
83 | * require 'path/to/Zebra_Session.php';
84 | *
85 | * // start the session
86 | * // where $link is a connection link returned by mysqli_connect or a PDO instance
87 | * $session = new Zebra_Session($link, 'sEcUr1tY_c0dE');
88 | *
89 | *
90 | * > **The following configuration options are set by the library when instantiated:**
91 | *
92 | *
93 | * // only when over HTTPS
94 | * ini_set('session.cookie_secure', 1);
95 | *
96 | *
97 | *
98 | * // don't expose the cookie to client side scripting making it harder for an attacker to hijack the session ID
99 | * ini_set('session.cookie_httponly', 1);
100 | *
101 | *
102 | *
103 | * // make sure that PHP only uses cookies for sessions and disallow session ID passing as a GET parameter
104 | * ini_set('session.use_only_cookies', 1);
105 | *
106 | *
107 | * > **The following configuration options are recommended to be set before instantiating this library:**
108 | *
109 | *
110 | * // disallows supplying session IDs via `session_id('ID HERE')
111 | * ini_set('session.use_strict_mode', 1);`
112 | *
113 | *
114 | * By default, the cookie used by PHP to propagate session data across multiple pages (`PHPSESSID`) uses the
115 | * current top-level domain and subdomain in the cookie declaration.
116 | *
117 | * Example: `www.domain.com`
118 | *
119 | * This means that the session data is not available to other subdomains. Therefore, a session started on
120 | * `www.domain.com` will not be available on `blog.domain.com`. The solution is to change the domain PHP uses when
121 | * it sets the `PHPSESSID` cookie by calling the line below *before* instantiating the Zebra_Session library:
122 | *
123 | *
124 | * // takes the domain and removes the subdomain
125 | * // blog.domain.com becoming .domain.com
126 | * ini_set(
127 | * 'session.cookie_domain',
128 | * substr($_SERVER['SERVER_NAME'], strpos($_SERVER['SERVER_NAME'], '.'))
129 | * );
130 | *
131 | *
132 | * From now on whenever PHP sets the `PHPSESSID` cookie, the cookie will be available to all subdomains!
133 | *
134 | * @param resource &$link An object representing the connection to a MySQL Server, as returned
135 | * by calling {@link https://www.php.net/manual/en/mysqli.construct.php mysqli_connect},
136 | * or a {@link https://www.php.net/manual/en/intro.pdo.php PDO} instance.
137 | *
138 | * If you use {@link https://github.com/stefangabos/Zebra_Database Zebra_Database}
139 | * to connect to the database, you can get the connection to the MySQL server
140 | * via Zebra_Database's {@link https://stefangabos.github.io/Zebra_Database/Zebra_Database/Zebra_Database.html#methodget_link get_link}
141 | * method.
142 | *
143 | * @param string $security_code The value of this argument is appended to the string created by
144 | * concatenating the user browser's User Agent string (or an empty string
145 | * if `lock_to_user_agent` is `FALSE`) and the user's IP address (or an
146 | * empty string if `lock_to_ip` is `FALSE`), before creating an MD5 hash out
147 | * of it and storing it in the database.
148 | *
149 | * On each call this value will be generated again and compared to the
150 | * value stored in the database ensuring that the session is correctly linked
151 | * with the user who initiated the session thus preventing session hijacking.
152 | *
153 | * > To prevent session hijacking, make sure you choose a string around
154 | * 12 characters long containing upper- and lowercase letters, as well as
155 | * digits. To simplify the process, use {@link https://www.random.org/passwords/?num=1&len=12&format=html&rnd=new this}
156 | * link to generate such a random string.
157 | *
158 | * @param integer $session_lifetime (Optional) The number of seconds after which a session will be considered
159 | * as **expired**.
160 | *
161 | * > A session is active for the number of seconds specified by this property
162 | * (or until the browser/browser tab is closed if the value is `0`) **OR**
163 | * the session has been inactive for more than the number of seconds specified
164 | * by `session.gc_maxlifetime`.
165 | *
166 | * > This property sets the value of {@link https://www.php.net/manual/en/session.configuration.php#ini.session.cookie-lifetime session.cookie_lifetime}.
167 | *
168 | * Expired sessions are cleaned up from the database whenever the garbage
169 | * collection routine runs. The probability for the garbage collection
170 | * routine to be executed is given by the values of `gc_probability` and
171 | * `gc_divisor`.
172 | *
173 | * To easily check the values of `session.gc_maxlifetime`, `gc_probability`
174 | * and `gc_divisor` for your environment use the {@link get_settings()} method.
175 | *
176 | * Default is `0` - the session is active until the browser/browser tab is
177 | * is closed **OR** the session has been inactive for more than the number
178 | * of seconds specified by `session.gc_maxlifetime`.
179 | *
180 | * @param boolean $lock_to_user_agent (Optional) Whether to restrict the session to the same User Agent (browser)
181 | * as when the session was first opened.
182 | *
183 | * > The user agent check only adds minor security, since an attacker that
184 | * hijacks the session cookie will most likely have the same user agent.
185 | *
186 | * In certain scenarios involving Internet Explorer, the browser will randomly
187 | * change the user agent string from one page to the next by automatically
188 | * switching into compatibility mode. So, on the first load you would have
189 | * something like:
190 | *
191 | * Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4...
192 | *
193 | * and reloading the page you would have
194 | *
195 | * Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Trident/4...
196 | *
197 | * So, if the situation asks for this, change this value to `false`.
198 | *
199 | * Default is `true`.
200 | *
201 | * @param boolean|callable $lock_to_ip (Optional) Whether to restrict the session to the same IP as when the
202 | * session was first opened.
203 | *
204 | * For the actual IP address that is going to be used, the library will
205 | * use the value of `$_SERVER['REMOTE_ADDR']`.
206 | *
207 | * If your application is behind a load balancer like an AWS Elastic Load Balancing
208 | * or a reverse proxy like Varnish, certain request information will be sent using
209 | * either the standard `Forwarded` header or the `X-Forwarded-*` headers. In this case,
210 | * the `REMOTE_ADDR` header will likely be the IP address of your reverse proxy while
211 | * the user's true IP will be stored in a standard `Forwarded` header or an `X-Forwarded-For`
212 | * header.
213 | *
214 | * In this case you will need to tell the library which reverse proxy IP addresses to
215 | * trust and what headers your reverse proxy uses to send information by using a `callable`
216 | * value for this argument:
217 | *
218 | *
219 | * new Zebra_Session(
220 | * $link,
221 | * 'someSecur1tyCode!',
222 | * 0,
223 | * false,
224 | *
225 | * // one way of using a callable for this argument
226 | * function() {
227 | * $ipaddress = '';
228 | * // use the header(s) you choose to trust
229 | * foreach (['HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED'] as $key) {
230 | * // use the first one containing a value
231 | * if (($tmp = getenv($key))) {
232 | * $ipaddress = $tmp;
233 | * break;
234 | * }
235 | * }
236 | * return $ipaddress;
237 | * }
238 | * );
239 | *
240 | *
241 | * Default is `false`
242 | *
243 | * @param int $lock_timeout (Optional) The maximum amount of time (in seconds) for which a lock on
244 | * the session data can be kept.
245 | *
246 | * > This must be lower than the maximum execution time of the script!
247 | *
248 | * Session locking is a way to ensure that data is correctly handled in a
249 | * scenario with multiple concurrent AJAX requests.
250 | *
251 | * Read more about it
252 | * {@link http://thwartedefforts.org/2006/11/11/race-conditions-with-ajax-and-php-sessions/ here}.
253 | *
254 | * Default is `60`
255 | *
256 | * @param string $table_name (Optional) Name of the MySQL table to be used by the class.
257 | *
258 | * Default is `session_data`
259 | *
260 | * @param boolean $start_session (Optional) Whether to start the session right away (by calling {@link https://php.net/manual/en/function.session-start.php session_start()})
261 | *
262 | * Default is `true`
263 | *
264 | * @param boolean $read_only (Optional) Opens session in read-only mode and without row locks. Any changes
265 | * made to `$_SESSION` will not be saved, although the variable can be read/written.
266 | *
267 | * Default is `false` (the default session behavior).
268 | *
269 | * @return void
270 | */
271 | public function __construct(
272 | &$link,
273 | $security_code,
274 | $session_lifetime = 0,
275 | $lock_to_user_agent = true,
276 | $lock_to_ip = false,
277 | $lock_timeout = 60,
278 | $table_name = 'session_data',
279 | $start_session = true,
280 | $read_only = false
281 | ) {
282 |
283 | // continue if the provided link is valid
284 | if (($link instanceof MySQLi && $link->connect_error === null) || $link instanceof PDO) {
285 |
286 | // store the connection link
287 | $this->link = $link;
288 |
289 | // set session's maximum lifetime
290 | ini_set('session.cookie_lifetime', (string)$session_lifetime);
291 |
292 | // tell the browser not to expose the cookie to client side scripting
293 | // this makes it harder for an attacker to hijack the session ID
294 | ini_set('session.cookie_httponly', '1');
295 |
296 | // make sure that PHP only uses cookies for sessions and disallow session ID passing as a GET parameter
297 | ini_set('session.use_only_cookies', '1');
298 |
299 | // if on HTTPS allows access to the session ID cookie only when the protocol is HTTPS
300 | if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') {
301 | ini_set('session.cookie_secure', '1');
302 | }
303 |
304 | // session lifetime
305 | $this->session_lifetime = max($session_lifetime, ini_get('session.gc_maxlifetime'));
306 |
307 | // we'll use this later on in order to try to prevent HTTP_USER_AGENT spoofing
308 | $this->security_code = $security_code;
309 |
310 | // some other defaults
311 | $this->lock_to_user_agent = $lock_to_user_agent;
312 | $this->lock_to_ip = $lock_to_ip;
313 |
314 | // the table to be used by the class
315 | $this->table_name = '`' . trim($table_name, '`') . '`';
316 |
317 | // the maximum amount of time (in seconds) for which a process can lock the session
318 | $this->lock_timeout = $lock_timeout;
319 |
320 | // set read-only flag
321 | $this->read_only = $read_only;
322 |
323 | // register the session handler
324 | session_set_save_handler($this, false);
325 |
326 | // if a session is already started, destroy it first
327 | if (session_id() !== '') {
328 | session_destroy();
329 | }
330 |
331 | // start session if required
332 | if ($start_session) {
333 | session_start();
334 | }
335 |
336 | // the name for the session variable that will be used for
337 | // holding information about flash data session variables
338 | $this->flash_data_var = '_zebra_session_flash_data_ec3asbuiad';
339 |
340 | // assume no flash data
341 | $this->flash_data = array();
342 |
343 | // if any flash data exists
344 | if (isset($_SESSION[$this->flash_data_var])) {
345 |
346 | // retrieve flash data
347 | $this->flash_data = unserialize($_SESSION[$this->flash_data_var]);
348 |
349 | // destroy the temporary session variable
350 | unset($_SESSION[$this->flash_data_var]);
351 |
352 | }
353 |
354 | // handle flash data after script execution
355 | register_shutdown_function(array($this, '_manage_flash_data'));
356 |
357 | // if no MySQL connection
358 | } else {
359 | throw new Exception('Zebra_Session: No MySQL connection');
360 | }
361 |
362 | }
363 |
364 | /**
365 | * Custom close() function
366 | *
367 | * @return boolean
368 | *
369 | * @access private
370 | */
371 | #[\ReturnTypeWillChange]
372 | public function close() {
373 |
374 | // release the lock associated with the current session
375 | $result = $this->query('
376 | SELECT
377 | RELEASE_LOCK(?)
378 | ', $this->session_lock);
379 |
380 | // stop if there was an error
381 | if ($result['num_rows'] !== 1 || current($result['data']) === 0) {
382 | throw new Exception('Zebra_Session: Could not release session lock');
383 | }
384 |
385 | return true;
386 |
387 | }
388 |
389 | /**
390 | * Custom destroy() function
391 | *
392 | * @param string $session_id The ID of the session to destroy
393 | *
394 | * @return boolean
395 | *
396 | * @access private
397 | */
398 | #[\ReturnTypeWillChange]
399 | public function destroy($session_id) {
400 |
401 | // delete the current session from the database
402 | return $this->query('
403 |
404 | DELETE FROM
405 | ' . $this->table_name . '
406 | WHERE
407 | session_id = ?
408 |
409 | ', $session_id) !== false;
410 |
411 | }
412 |
413 | /**
414 | * Custom gc() function (garbage collector)
415 | *
416 | * @return boolean
417 | *
418 | * @access private
419 | */
420 | #[\ReturnTypeWillChange]
421 | public function gc($maxlifetime) {
422 |
423 | // delete expired sessions from database
424 | $this->query('
425 |
426 | DELETE FROM
427 | ' . $this->table_name . '
428 | WHERE
429 | session_expire < ?
430 |
431 | ', time());
432 |
433 | return true;
434 |
435 | }
436 |
437 | /**
438 | * Gets the number of active (not expired) sessions.
439 | *
440 | * > The returned value does not represent the exact number of active users as some sessions may be unused
441 | * although they haven't expired
442 | *
443 | *
444 | * // get the number of active sessions
445 | * $active_sessions = $session->get_active_sessions();
446 | *
447 | *
448 | * @return integer Returns the number of active (not expired) sessions.
449 | */
450 | public function get_active_sessions() {
451 |
452 | // call the garbage collector
453 | $this->gc(0);
454 |
455 | // count the rows from the database
456 | $result = $this->query('
457 |
458 | SELECT
459 | COUNT(session_id) as count
460 | FROM
461 | ' . $this->table_name . '
462 |
463 | ');
464 |
465 | // return the number of found rows
466 | return $result['data']['count'];
467 |
468 | }
469 |
470 | /**
471 | * Queries the system for the values of `session.gc_maxlifetime`, `session.gc_probability`, `session.gc_divisor`
472 | * and `session.use_strict_mode`, and returns them as an associative array.
473 | *
474 | * To view the result in a human-readable format use:
475 | *
476 | * // get default settings
477 | * print_r('');
478 | * print_r($session->get_settings());
479 | *
480 | * // would output something similar to (depending on your actual settings)
481 | * // Array
482 | * // (
483 | * // [session.gc_maxlifetime] => 1440 seconds (24 minutes)
484 | * // [session.gc_probability] => 1
485 | * // [session.gc_divisor] => 1000
486 | * // [probability] => 0.1% // <- this is computed from the values above
487 | * // [session.use_strict_mode] => 1
488 | * // )
489 | *
490 | *
491 | * @since 1.0.8
492 | *
493 | * @return array
628 | * // regenerate the session's ID
629 | * $session->regenerate_id();
630 | *
631 | *
632 | * @return void
633 | */
634 | public function regenerate_id() {
635 |
636 | // regenerates the id (create a new session with a new id and containing the data from the old session)
637 | // also, delete the old session
638 | session_regenerate_id(true);
639 |
640 | }
641 |
642 | /**
643 | * Sets a **flash data** session variable which will only be available for the next server request and which will be
644 | * automatically deleted afterwards.
645 | *
646 | * Typically used for informational or status messages (for example: "data has been successfully updated").
647 | *
648 | *
649 | * // set "myvar" which will only be available
650 | * // for the next server request and will be
651 | * // automatically deleted afterwards
652 | * $session->set_flashdata('myvar', 'myval');
653 | *
654 | *
655 | * "Flash data" session variables can be retrieved like any other session variable:
656 | *
657 | *
658 | * if (isset($_SESSION['myvar'])) {
659 | * // do something here but remember that the
660 | * // flash data session variable is available
661 | * // for a single server request after it has
662 | * // been set!
663 | * }
664 | *
665 | *
666 | * @param string $name The name of the session variable.
667 | *
668 | * @param string $value The value of the session variable.
669 | *
670 | * @return void
671 | */
672 | public function set_flashdata($name, $value) {
673 |
674 | // set session variable
675 | $_SESSION[$name] = $value;
676 |
677 | // initialize the counter for this flash data
678 | $this->flash_data[$name] = 0;
679 |
680 | }
681 |
682 | /**
683 | * Deletes all data related to the session.
684 | *
685 | * > This method runs the garbage collector respecting your environment's garbage collector-related properties.
686 | * Read {@link __construct() here} for more information
687 | *
688 | *
689 | * // end current session
690 | * $session->stop();
691 | *
692 | *
693 | * @since 1.0.1
694 | *
695 | * @return void
696 | */
697 | public function stop() {
698 |
699 | // if a cookie is used to pass the session id
700 | if (ini_get('session.use_cookies')) {
701 |
702 | // get session cookie's properties
703 | $params = session_get_cookie_params();
704 |
705 | // unset the cookie
706 | setcookie(session_name(), '', time() - 42000, $params['path'], $params['domain'], $params['secure'], $params['httponly']);
707 |
708 | }
709 |
710 | // destroy the session
711 | session_unset();
712 | session_destroy();
713 |
714 | }
715 |
716 | /**
717 | * Custom write() function
718 | *
719 | * @param string $session_id The ID of the session to write to
720 | *
721 | * @param mixed $session_data The values to be written
722 | *
723 | * @return boolean
724 | *
725 | * @access private
726 | */
727 | #[\ReturnTypeWillChange]
728 | public function write($session_id, $session_data) {
729 |
730 | // we don't write session variable when in read-only mode
731 | if ($this->read_only) {
732 | return true;
733 | }
734 |
735 | // insert OR update session's data - this is how it works:
736 | // first it tries to insert a new row in the database BUT if session_id is already in the database then just
737 | // update session_data and session_expire for that specific session_id
738 | // read more here https://dev.mysql.com/doc/refman/8.0/en/insert-on-duplicate.html
739 | return $this->query('
740 | INSERT INTO
741 | ' . $this->table_name . '
742 | (
743 | session_id,
744 | hash,
745 | session_data,
746 | session_expire
747 | )
748 | VALUES (?, ?, ?, ?)
749 | ON DUPLICATE KEY UPDATE
750 | session_data = VALUES(session_data),
751 | session_expire = VALUES(session_expire)
752 | ',
753 | $session_id,
754 | md5(
755 | ($this->lock_to_user_agent && isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '') .
756 | ($this->lock_to_ip && !is_callable($this->lock_to_ip) ? $_SERVER['REMOTE_ADDR'] : (is_callable($this->lock_to_ip) ? call_user_func($this->lock_to_ip) : '')) .
757 | $this->security_code
758 | ),
759 | $session_data,
760 | time() + $this->session_lifetime
761 | ) !== false;
762 |
763 | }
764 |
765 | /**
766 | * Manages flash data behind the scenes
767 | *
768 | * @return void
769 | *
770 | * @access private
771 | */
772 | public function _manage_flash_data() {
773 |
774 | // if there is flash data to be handled
775 | if (!empty($this->flash_data)) {
776 |
777 | // iterate through all the entries
778 | foreach ($this->flash_data as $variable => $counter) {
779 |
780 | // increment counter representing server requests
781 | $this->flash_data[$variable]++;
782 |
783 | // if this is not the first server request
784 | if ($this->flash_data[$variable] > 1) {
785 |
786 | // unset the session variable
787 | unset($_SESSION[$variable]);
788 |
789 | // stop tracking
790 | unset($this->flash_data[$variable]);
791 |
792 | }
793 |
794 | }
795 |
796 | // if there is any flash data left to be handled
797 | if (!empty($this->flash_data)) {
798 |
799 | // store data in a temporary session variable
800 | $_SESSION[$this->flash_data_var] = serialize($this->flash_data);
801 |
802 | }
803 |
804 | }
805 |
806 | // make sure session data is written
807 | // not matter how script execution ends
808 | session_write_close();
809 |
810 | }
811 |
812 | /**
813 | * Mini-wrapper for running MySQL queries with parameter binding with or without PDO
814 | *
815 | * @param string $query The MySQL query to execute
816 | *
817 | * @return mixed
818 | *
819 | * @access private
820 | */
821 | private function query($query) {
822 |
823 | // if the provided connection link is a PDO instance
824 | if ($this->link instanceof PDO) {
825 |
826 | // if executing the query was a success
827 | if (($stmt = $this->link->prepare($query)) && $stmt->execute(array_slice(func_get_args(), 1))) {
828 |
829 | // prepare a standardized return value
830 | $result = array(
831 | 'num_rows' => $stmt->rowCount(),
832 | 'data' => $stmt->columnCount() == 0 ? array() : $stmt->fetch(PDO::FETCH_ASSOC),
833 | );
834 |
835 | // close the statement
836 | $stmt->closeCursor();
837 |
838 | // return result
839 | return $result;
840 |
841 | }
842 |
843 | // if link connection is a regular mysqli connection object
844 | } else {
845 |
846 | $stmt = mysqli_stmt_init($this->link);
847 |
848 | // if query is valid
849 | if ($stmt->prepare($query)) {
850 |
851 | // the arguments minus the first one (the SQL statement)
852 | $arguments = array_slice(func_get_args(), 1);
853 |
854 | // if there are any arguments
855 | if (!empty($arguments)) {
856 |
857 | // prepare the data for "bind_param"
858 | $bind_types = '';
859 | $bind_data = array();
860 | foreach ($arguments as $key => $value) {
861 | $bind_types .= is_numeric($value) ? 'i' : 's';
862 | $bind_data[] = &$arguments[$key];
863 | }
864 | array_unshift($bind_data, $bind_types);
865 |
866 | // call "bind_param" with the prepared arguments
867 | call_user_func_array(array($stmt, 'bind_param'), $bind_data);
868 |
869 | }
870 |
871 | // if the query was successfully executed
872 | if ($stmt->execute()) {
873 |
874 | // get some information about the results
875 | $results = $stmt->get_result();
876 |
877 | // prepare a standardized return value
878 | $result = array(
879 | 'num_rows' => is_bool($results) ? $stmt->affected_rows : $results->num_rows,
880 | 'data' => is_bool($results) ? array() : $results->fetch_assoc(),
881 | );
882 |
883 | // close the statement
884 | $stmt->close();
885 |
886 | // return result
887 | return $result;
888 |
889 | }
890 |
891 | }
892 |
893 | // if we get this far there must've been an error
894 | throw new Exception($stmt->error);
895 |
896 | }
897 |
898 | }
899 |
900 | }
901 |
--------------------------------------------------------------------------------