forked from stefangabos/Zebra_Session
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathZebra_Session.php
895 lines (745 loc) · 33.3 KB
/
Zebra_Session.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
<?php
/**
* A drop-in replacement for PHP's default session handler, using MySQL for storage and providing better performance,
* better security and protection against session fixation and session hijacking.
*
* Works with or without PDO.
*
* Read more {@link https://github.com/stefangabos/Zebra_Session/#zebra-session- here}.
*
* @author Stefan Gabos <[email protected]>
* @version 4.1.1 (last revision: September 09, 2024)
* @copyright © 2006 - 2024 Stefan Gabos
* @license https://www.gnu.org/licenses/lgpl-3.0.txt GNU LESSER GENERAL PUBLIC LICENSE
* @package Zebra_Session
*/
class Zebra_Session implements SessionHandlerInterface {
/**
* @var array<string>
*/
private $flash_data;
/**
* @var string
*/
private $flash_data_var;
/**
* @var object
*/
private $link;
/**
* @var integer
*/
private $lock_timeout;
/**
* @var boolean|callable
*/
private $lock_to_ip;
/**
* @var boolean
*/
private $lock_to_user_agent;
/**
* @var string
*/
private $security_code;
/**
* @var string|false
*/
private $session_lifetime;
/**
* @var string
*/
private $session_lock;
/**
* @var string
*/
private $table_name;
/**
* @var boolean
*/
private $read_only = false;
/**
* Constructor of class. Initializes the class and, optionally, calls
* {@link https://php.net/manual/en/function.session-start.php session_start()}
*
* <code>
* // first, connect to a database containing the sessions table, either via PDO or using mysqli_connect
*
* // include the class
* // (you don't need this if you are using Composer)
* require 'path/to/Zebra_Session.php';
*
* // start the session
* // where $link is a connection link returned by mysqli_connect or a PDO instance
* $session = new Zebra_Session($link, 'sEcUr1tY_c0dE');
* </code>
*
* > **The following configuration options are set by the library when instantiated:**
*
* <code>
* // only when over HTTPS
* ini_set('session.cookie_secure', 1);
* </code>
*
* <code>
* // don't expose the cookie to client side scripting making it harder for an attacker to hijack the session ID
* ini_set('session.cookie_httponly', 1);
* </code>
*
* <code>
* // make sure that PHP only uses cookies for sessions and disallow session ID passing as a GET parameter
* ini_set('session.use_only_cookies', 1);
* </code>
*
* > **The following configuration options are recommended to be set before instantiating this library:**
*
* <code>
* // disallows supplying session IDs via `session_id('ID HERE')
* ini_set('session.use_strict_mode', 1);`
* </code>
*
* By default, the cookie used by PHP to propagate session data across multiple pages (`PHPSESSID`) uses the
* current top-level domain and subdomain in the cookie declaration.
*
* Example: `www.domain.com`
*
* This means that the session data is not available to other subdomains. Therefore, a session started on
* `www.domain.com` will not be available on `blog.domain.com`. The solution is to change the domain PHP uses when
* it sets the `PHPSESSID` cookie by calling the line below *before* instantiating the Zebra_Session library:
*
* <code>
* // takes the domain and removes the subdomain
* // blog.domain.com becoming .domain.com
* ini_set(
* 'session.cookie_domain',
* substr($_SERVER['SERVER_NAME'], strpos($_SERVER['SERVER_NAME'], '.'))
* );
* </code>
*
* From now on whenever PHP sets the `PHPSESSID` cookie, the cookie will be available to all subdomains!
*
* @param resource &$link An object representing the connection to a MySQL Server, as returned
* by calling {@link https://www.php.net/manual/en/mysqli.construct.php mysqli_connect},
* or a {@link https://www.php.net/manual/en/intro.pdo.php PDO} instance.
*
* If you use {@link https://github.com/stefangabos/Zebra_Database Zebra_Database}
* to connect to the database, you can get the connection to the MySQL server
* via Zebra_Database's {@link https://stefangabos.github.io/Zebra_Database/Zebra_Database/Zebra_Database.html#methodget_link get_link}
* method.
*
* @param string $security_code The value of this argument is appended to the string created by
* concatenating the user browser's User Agent string (or an empty string
* if `lock_to_user_agent` is `FALSE`) and the user's IP address (or an
* empty string if `lock_to_ip` is `FALSE`), before creating an MD5 hash out
* of it and storing it in the database.
*
* On each call this value will be generated again and compared to the
* value stored in the database ensuring that the session is correctly linked
* with the user who initiated the session thus preventing session hijacking.
*
* > To prevent session hijacking, make sure you choose a string around
* 12 characters long containing upper- and lowercase letters, as well as
* digits. To simplify the process, use {@link https://www.random.org/passwords/?num=1&len=12&format=html&rnd=new this}
* link to generate such a random string.
*
* @param integer $session_lifetime (Optional) The number of seconds after which a session will be considered
* as **expired**.
*
* > A session is active for the number of seconds specified by this property
* (or until the browser/browser tab is closed if the value is `0`) **OR**
* the session has been inactive for more than the number of seconds specified
* by `session.gc_maxlifetime`.
*
* > This property sets the value of {@link https://www.php.net/manual/en/session.configuration.php#ini.session.cookie-lifetime session.cookie_lifetime}.
*
* Expired sessions are cleaned up from the database whenever the garbage
* collection routine runs. The probability for the garbage collection
* routine to be executed is given by the values of `gc_probability` and
* `gc_divisor`.
*
* To easily check the values of `session.gc_maxlifetime`, `gc_probability`
* and `gc_divisor` for your environment use the {@link get_settings()} method.
*
* Default is `0` - the session is active until the browser/browser tab is
* is closed **OR** the session has been inactive for more than the number
* of seconds specified by `session.gc_maxlifetime`.
*
* @param boolean $lock_to_user_agent (Optional) Whether to restrict the session to the same User Agent (browser)
* as when the session was first opened.
*
* > The user agent check only adds minor security, since an attacker that
* hijacks the session cookie will most likely have the same user agent.
*
* In certain scenarios involving Internet Explorer, the browser will randomly
* change the user agent string from one page to the next by automatically
* switching into compatibility mode. So, on the first load you would have
* something like:
*
* <code>Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4...</code>
*
* and reloading the page you would have
*
* <code> Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Trident/4...</code>
*
* So, if the situation asks for this, change this value to `false`.
*
* Default is `true`.
*
* @param boolean|callable $lock_to_ip (Optional) Whether to restrict the session to the same IP as when the
* session was first opened.
*
* For the actual IP address that is going to be used, the library will
* use the value of `$_SERVER['REMOTE_ADDR']`.
*
* If your application is behind a load balancer like an AWS Elastic Load Balancing
* or a reverse proxy like Varnish, certain request information will be sent using
* either the standard `Forwarded` header or the `X-Forwarded-*` headers. In this case,
* the `REMOTE_ADDR` header will likely be the IP address of your reverse proxy while
* the user's true IP will be stored in a standard `Forwarded` header or an `X-Forwarded-For`
* header.
*
* In this case you will need to tell the library which reverse proxy IP addresses to
* trust and what headers your reverse proxy uses to send information by using a `callable`
* value for this argument:
*
* <code>
* new Zebra_Session(
* $link,
* 'someSecur1tyCode!',
* 0,
* false,
*
* // one way of using a callable for this argument
* function() {
* $ipaddress = '';
* // use the header(s) you choose to trust
* foreach (['HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED'] as $key) {
* // use the first one containing a value
* if (($tmp = getenv($key))) {
* $ipaddress = $tmp;
* break;
* }
* }
* return $ipaddress;
* }
* );
* </code>
*
* Default is `false`
*
* @param int $lock_timeout (Optional) The maximum amount of time (in seconds) for which a lock on
* the session data can be kept.
*
* > This must be lower than the maximum execution time of the script!
*
* Session locking is a way to ensure that data is correctly handled in a
* scenario with multiple concurrent AJAX requests.
*
* Read more about it
* {@link http://thwartedefforts.org/2006/11/11/race-conditions-with-ajax-and-php-sessions/ here}.
*
* Default is `60`
*
* @param string $table_name (Optional) Name of the MySQL table to be used by the class.
*
* Default is `session_data`
*
* @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()})
*
* Default is `true`
*
* @param boolean $read_only (Optional) Opens session in read-only mode and without row locks. Any changes
* made to `$_SESSION` will not be saved, although the variable can be read/written.
*
* Default is `false` (the default session behavior).
*
* @return void
*/
public function __construct(
&$link,
$security_code,
$session_lifetime = 0,
$lock_to_user_agent = true,
$lock_to_ip = false,
$lock_timeout = 60,
$table_name = 'session_data',
$start_session = true,
$read_only = false
) {
// continue if the provided link is valid
if (($link instanceof MySQLi && $link->connect_error === null) || $link instanceof PDO) {
// store the connection link
$this->link = $link;
// set session's maximum lifetime
ini_set('session.cookie_lifetime', (string)$session_lifetime);
// tell the browser not to expose the cookie to client side scripting
// this makes it harder for an attacker to hijack the session ID
ini_set('session.cookie_httponly', '1');
// make sure that PHP only uses cookies for sessions and disallow session ID passing as a GET parameter
ini_set('session.use_only_cookies', '1');
// if on HTTPS allows access to the session ID cookie only when the protocol is HTTPS
if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') {
ini_set('session.cookie_secure', '1');
}
// session lifetime
$this->session_lifetime = max($session_lifetime, ini_get('session.gc_maxlifetime'));
// we'll use this later on in order to try to prevent HTTP_USER_AGENT spoofing
$this->security_code = $security_code;
// some other defaults
$this->lock_to_user_agent = $lock_to_user_agent;
$this->lock_to_ip = $lock_to_ip;
// the table to be used by the class
$this->table_name = '`' . trim($table_name, '`') . '`';
// the maximum amount of time (in seconds) for which a process can lock the session
$this->lock_timeout = $lock_timeout;
// set read-only flag
$this->read_only = $read_only;
// register the session handler
session_set_save_handler($this, false);
// if a session is already started, destroy it first
if (session_id() !== '') {
session_destroy();
}
// start session if required
if ($start_session) {
session_start();
}
// the name for the session variable that will be used for
// holding information about flash data session variables
$this->flash_data_var = '_zebra_session_flash_data_ec3asbuiad';
// assume no flash data
$this->flash_data = array();
// if any flash data exists
if (isset($_SESSION[$this->flash_data_var])) {
// retrieve flash data
$this->flash_data = unserialize($_SESSION[$this->flash_data_var]);
// destroy the temporary session variable
unset($_SESSION[$this->flash_data_var]);
}
// handle flash data after script execution
register_shutdown_function(array($this, '_manage_flash_data'));
// if no MySQL connection
} else {
throw new Exception('Zebra_Session: No MySQL connection');
}
}
/**
* Custom close() function
*
* @return boolean
*
* @access private
*/
#[\ReturnTypeWillChange]
public function close() {
// release the lock associated with the current session
return $this->query('
SELECT
RELEASE_LOCK(?)
', $this->session_lock) !== false;
}
/**
* Custom destroy() function
*
* @param string $session_id The ID of the session to destroy
*
* @return boolean
*
* @access private
*/
#[\ReturnTypeWillChange]
public function destroy($session_id) {
// delete the current session from the database
return $this->query('
DELETE FROM
' . $this->table_name . '
WHERE
session_id = ?
', $session_id) !== false;
}
/**
* Custom gc() function (garbage collector)
*
* @return boolean
*
* @access private
*/
#[\ReturnTypeWillChange]
public function gc($maxlifetime) {
// delete expired sessions from database
$this->query('
DELETE FROM
' . $this->table_name . '
WHERE
session_expire < ?
', time());
return true;
}
/**
* Gets the number of active (not expired) sessions.
*
* > The returned value does not represent the exact number of active users as some sessions may be unused
* although they haven't expired
*
* <code>
* // get the number of active sessions
* $active_sessions = $session->get_active_sessions();
* </code>
*
* @return integer Returns the number of active (not expired) sessions.
*/
public function get_active_sessions() {
// call the garbage collector
$this->gc();
// count the rows from the database
$result = $this->query('
SELECT
COUNT(session_id) as count
FROM
' . $this->table_name . '
');
// return the number of found rows
return $result['data']['count'];
}
/**
* Queries the system for the values of `session.gc_maxlifetime`, `session.gc_probability`, `session.gc_divisor`
* and `session.use_strict_mode`, and returns them as an associative array.
*
* To view the result in a human-readable format use:
* <code>
* // get default settings
* print_r('<pre>');
* print_r($session->get_settings());
*
* // would output something similar to (depending on your actual settings)
* // Array
* // (
* // [session.gc_maxlifetime] => 1440 seconds (24 minutes)
* // [session.gc_probability] => 1
* // [session.gc_divisor] => 1000
* // [probability] => 0.1% // <- this is computed from the values above
* // [session.use_strict_mode] => 1
* // )
* </code>
*
* @since 1.0.8
*
* @return array<string> Returns the values of `session.gc_maxlifetime`, `session.gc_probability`, `session.gc_divisor`
* and `session.use_strict_mode`, as an associative array.
*
*/
public function get_settings() {
// get the settings
$gc_maxlifetime = ini_get('session.gc_maxlifetime');
$gc_probability = ini_get('session.gc_probability');
$gc_divisor = ini_get('session.gc_divisor');
$use_strict_mode = ini_get('session.use_strict_mode');
// return them as an array
return array(
'session.gc_maxlifetime' => $gc_maxlifetime . ' seconds (' . round($gc_maxlifetime / 60) . ' minutes)',
'session.gc_probability' => $gc_probability,
'session.gc_divisor' => $gc_divisor,
'probability' => ($gc_divisor > 0 ? (int)$gc_probability / (int)$gc_divisor * 100 : 0) . '%',
'session.use_strict_mode' => $use_strict_mode,
);
}
/**
* Custom open() function
*
* @return boolean
*
* @access private
*/
#[\ReturnTypeWillChange]
public function open($save_path, $session_name) {
return true;
}
/**
* Custom read() function
*
* @param string $session_id The ID of the session to read from
*
* @return string
*
* @access private
*/
#[\ReturnTypeWillChange]
public function read($session_id) {
// get the lock name associated with the current session
// notice the use of sha1() which shortens the session ID to 40 characters so that it does not exceed the limit of
// 64 characters for locking string imposed by mySQL >= 5.7.5
// thanks to Andreas Heissenberger (see https://github.com/stefangabos/Zebra_Session/issues/16)
$this->session_lock = 'session_' . sha1($session_id);
// if we are *not* in read-only mode
// read-only sessions do not need a lock
if (!$this->read_only) {
// try to obtain a lock with the given name and timeout
$result = $this->query('SELECT GET_LOCK(?, ?)', $this->session_lock, $this->lock_timeout);
// stop if there was an error
if ($result['num_rows'] != 1) {
throw new Exception('Zebra_Session: Could not obtain session lock');
}
}
$hash = '';
// if the sessions is locked to an user agent
if ($this->lock_to_user_agent && isset($_SERVER['HTTP_USER_AGENT'])) {
$hash .= $_SERVER['HTTP_USER_AGENT'];
}
// if session is locked to an IP address
// if "lock_to_ip" is truthy but *not* callable
// (this is the quickest way in case "lock_to_ip" is truthy)
if ($this->lock_to_ip && !is_callable($this->lock_to_ip)) {
// append the value of "REMOTE_ADDR" header
$hash .= $_SERVER['REMOTE_ADDR'];
// if "lock_to_ip" is callable
} elseif (is_callable($this->lock_to_ip)) {
// append whatever is returned by the callable
$hash .= call_user_func($this->lock_to_ip);
}
// append this to the end
$hash .= $this->security_code;
// get the active (not expired) result associated with the session id and hash
$result = $this->query('
SELECT
session_data
FROM
' . $this->table_name . '
WHERE
session_id = ?
AND session_expire > ?
AND hash = ?
LIMIT
1
', $session_id, time(), md5($hash));
// if there were no errors and data was found
if ($result !== false && $result['num_rows'] > 0) {
// return session data
// don't bother with the unserialization - PHP handles this automatically
return $result['data']['session_data'];
}
// if hash has changed or the session expired
$this->destroy($session_id);
// on error return an empty string - this HAS to be an empty string
return '';
}
/**
* Regenerates the session id.
*
* > Call this method whenever you do a privilege change, in order to prevent session hijacking!
*
* <code>
* // regenerate the session's ID
* $session->regenerate_id();
* </code>
*
* @return void
*/
public function regenerate_id() {
// regenerates the id (create a new session with a new id and containing the data from the old session)
// also, delete the old session
session_regenerate_id(true);
}
/**
* Sets a **flash data** session variable which will only be available for the next server request and which will be
* automatically deleted afterwards.
*
* Typically used for informational or status messages (for example: "data has been successfully updated").
*
* <code>
* // set "myvar" which will only be available
* // for the next server request and will be
* // automatically deleted afterwards
* $session->set_flashdata('myvar', 'myval');
* </code>
*
* "Flash data" session variables can be retrieved like any other session variable:
*
* <code>
* if (isset($_SESSION['myvar'])) {
* // do something here but remember that the
* // flash data session variable is available
* // for a single server request after it has
* // been set!
* }
* </code>
*
* @param string $name The name of the session variable.
*
* @param string $value The value of the session variable.
*
* @return void
*/
public function set_flashdata($name, $value) {
// set session variable
$_SESSION[$name] = $value;
// initialize the counter for this flash data
$this->flash_data[$name] = 0;
}
/**
* Deletes all data related to the session.
*
* > This method runs the garbage collector respecting your environment's garbage collector-related properties.
* Read {@link __construct() here} for more information
*
* <code>
* // end current session
* $session->stop();
* </code>
*
* @since 1.0.1
*
* @return void
*/
public function stop() {
// if a cookie is used to pass the session id
if (ini_get('session.use_cookies')) {
// get session cookie's properties
$params = session_get_cookie_params();
// unset the cookie
setcookie(session_name(), '', time() - 42000, $params['path'], $params['domain'], $params['secure'], $params['httponly']);
}
// destroy the session
session_unset();
session_destroy();
}
/**
* Custom write() function
*
* @param string $session_id The ID of the session to write to
*
* @param mixed $session_data The values to be written
*
* @return boolean
*
* @access private
*/
#[\ReturnTypeWillChange]
public function write($session_id, $session_data) {
// we don't write session variable when in read-only mode
if ($this->read_only) {
return true;
}
// insert OR update session's data - this is how it works:
// first it tries to insert a new row in the database BUT if session_id is already in the database then just
// update session_data and session_expire for that specific session_id
// read more here https://dev.mysql.com/doc/refman/8.0/en/insert-on-duplicate.html
return $this->query('
INSERT INTO
' . $this->table_name . '
(
session_id,
hash,
session_data,
session_expire
)
VALUES (?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
session_data = VALUES(session_data),
session_expire = VALUES(session_expire)
',
$session_id,
md5(
($this->lock_to_user_agent && isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '') .
($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) : '')) .
$this->security_code
),
$session_data,
time() + $this->session_lifetime
) !== false;
}
/**
* Manages flash data behind the scenes
*
* @return void
*
* @access private
*/
public function _manage_flash_data() {
// if there is flash data to be handled
if (!empty($this->flash_data)) {
// iterate through all the entries
foreach ($this->flash_data as $variable => $counter) {
// increment counter representing server requests
$this->flash_data[$variable]++;
// if this is not the first server request
if ($this->flash_data[$variable] > 1) {
// unset the session variable
unset($_SESSION[$variable]);
// stop tracking
unset($this->flash_data[$variable]);
}
}
// if there is any flash data left to be handled
if (!empty($this->flash_data)) {
// store data in a temporary session variable
$_SESSION[$this->flash_data_var] = serialize($this->flash_data);
}
}
// make sure session data is written
// not matter how script execution ends
session_write_close();
}
/**
* Mini-wrapper for running MySQL queries with parameter binding with or without PDO
*
* @param string $query The MySQL query to execute
*
* @return mixed
*
* @access private
*/
private function query($query) {
// if the provided connection link is a PDO instance
if ($this->link instanceof PDO) {
// if executing the query was a success
if (($stmt = $this->link->prepare($query)) && $stmt->execute(array_slice(func_get_args(), 1))) {
// prepare a standardized return value
$result = array(
'num_rows' => $stmt->rowCount(),
'data' => $stmt->columnCount() == 0 ? array() : $stmt->fetch(PDO::FETCH_ASSOC),
);
// close the statement
$stmt->closeCursor();
// return result
return $result;
}
// if link connection is a regular mysqli connection object
} else {
$stmt = mysqli_stmt_init($this->link);
// if query is valid
if ($stmt->prepare($query)) {
// the arguments minus the first one (the SQL statement)
$arguments = array_slice(func_get_args(), 1);
// if there are any arguments
if (!empty($arguments)) {
// prepare the data for "bind_param"
$bind_types = '';
$bind_data = array();
foreach ($arguments as $key => $value) {
$bind_types .= is_numeric($value) ? 'i' : 's';
$bind_data[] = &$arguments[$key];
}
array_unshift($bind_data, $bind_types);
// call "bind_param" with the prepared arguments
call_user_func_array(array($stmt, 'bind_param'), $bind_data);
}
// if the query was successfully executed
if ($stmt->execute()) {
// get some information about the results
$results = $stmt->get_result();
// prepare a standardized return value
$result = array(
'num_rows' => is_bool($results) ? $stmt->affected_rows : $results->num_rows,
'data' => is_bool($results) ? array() : $results->fetch_assoc(),
);
// close the statement
$stmt->close();
// return result
return $result;
}
}
// if we get this far there must've been an error
throw new Exception($stmt->error);
}
}
}