Completed
Branch sideload-all-language-files (d12b05)
by
unknown
719:32 queued 710:22
created

EE_Session::wp_loaded()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 0
dl 0
loc 6
rs 10
c 0
b 0
f 0
1
<?php
2
3
use EventEspresso\core\domain\services\session\SessionIdentifierInterface;
4
use EventEspresso\core\domain\values\session\SessionLifespan;
5
use EventEspresso\core\exceptions\InvalidDataTypeException;
6
use EventEspresso\core\exceptions\InvalidInterfaceException;
7
use EventEspresso\core\exceptions\InvalidSessionDataException;
8
use EventEspresso\core\services\cache\CacheStorageInterface;
9
use EventEspresso\core\services\loaders\LoaderFactory;
10
use EventEspresso\core\services\request\RequestInterface;
11
use EventEspresso\core\services\session\SessionStartHandler;
12
13
/**
14
 * EE_Session class
15
 *
16
 * Please note that the session doesn't save by default, except when it has a cart set on it.
17
 * In order for it to save on other pages, you must execute
18
 * `add_action('FHEE__EE_Session___save_session_to_db__abort_session_save', '__return_false');`
19
 * somewhere during the request
20
 *
21
 * @package    Event Espresso
22
 * @subpackage includes/classes
23
 * @author     Brent Christensen
24
 */
25
class EE_Session implements SessionIdentifierInterface
26
{
27
28
    const session_id_prefix = 'ee_ssn_';
29
30
    const hash_check_prefix = 'ee_shc_';
31
32
    const OPTION_NAME_SETTINGS = 'ee_session_settings';
33
34
    const STATUS_CLOSED = 0;
35
36
    const STATUS_OPEN = 1;
37
38
    /**
39
     * instance of the EE_Session object
40
     *
41
     * @var EE_Session
42
     */
43
    private static $_instance;
44
45
    /**
46
     * @var CacheStorageInterface $cache_storage
47
     */
48
    protected $cache_storage;
49
50
    /**
51
     * @var EE_Encryption $encryption
52
     */
53
    protected $encryption;
54
55
    /**
56
     * @var SessionStartHandler $session_start_handler
57
     */
58
    protected $session_start_handler;
59
60
    /**
61
     * the session id
62
     *
63
     * @var string
64
     */
65
    private $_sid;
66
67
    /**
68
     * session id salt
69
     *
70
     * @var string
71
     */
72
    private $_sid_salt;
73
74
    /**
75
     * session data
76
     *
77
     * @var array
78
     */
79
    private $_session_data = array();
80
81
    /**
82
     * how long an EE session lasts
83
     * default session lifespan of 1 hour (for not so instant IPNs)
84
     *
85
     * @var SessionLifespan $session_lifespan
86
     */
87
    private $session_lifespan;
88
89
    /**
90
     * session expiration time as Unix timestamp in GMT
91
     *
92
     * @var int
93
     */
94
    private $_expiration;
95
96
    /**
97
     * whether or not session has expired at some point
98
     *
99
     * @var boolean
100
     */
101
    private $_expired = false;
102
103
    /**
104
     * current time as Unix timestamp in GMT
105
     *
106
     * @var int
107
     */
108
    private $_time;
109
110
    /**
111
     * whether to encrypt session data
112
     *
113
     * @var bool
114
     */
115
    private $_use_encryption;
116
117
    /**
118
     * well... according to the server...
119
     *
120
     * @var null
121
     */
122
    private $_user_agent;
123
124
    /**
125
     * do you really trust the server ?
126
     *
127
     * @var null
128
     */
129
    private $_ip_address;
130
131
    /**
132
     * current WP user_id
133
     *
134
     * @var null
135
     */
136
    private $_wp_user_id;
137
138
    /**
139
     * array for defining default session vars
140
     *
141
     * @var array
142
     */
143
    private $_default_session_vars = array(
144
        'id'            => null,
145
        'user_id'       => null,
146
        'ip_address'    => null,
147
        'user_agent'    => null,
148
        'init_access'   => null,
149
        'last_access'   => null,
150
        'expiration'    => null,
151
        'pages_visited' => array(),
152
    );
153
154
    /**
155
     * timestamp for when last garbage collection cycle was performed
156
     *
157
     * @var int $_last_gc
158
     */
159
    private $_last_gc;
160
161
    /**
162
     * @var RequestInterface $request
163
     */
164
    protected $request;
165
166
    /**
167
     * whether session is active or not
168
     *
169
     * @var int $status
170
     */
171
    private $status = EE_Session::STATUS_CLOSED;
172
173
174
    /**
175
     * @singleton method used to instantiate class object
176
     * @param CacheStorageInterface $cache_storage
177
     * @param SessionLifespan|null  $lifespan
178
     * @param RequestInterface      $request
179
     * @param SessionStartHandler   $session_start_handler
180
     * @param EE_Encryption         $encryption
181
     * @return EE_Session
182
     * @throws InvalidArgumentException
183
     * @throws InvalidDataTypeException
184
     * @throws InvalidInterfaceException
185
     */
186
    public static function instance(
187
        CacheStorageInterface $cache_storage = null,
188
        SessionLifespan $lifespan = null,
189
        RequestInterface $request = null,
190
        SessionStartHandler $session_start_handler = null,
191
        EE_Encryption $encryption = null
192
    ) {
193
        // check if class object is instantiated
194
        // session loading is turned ON by default, but prior to the init hook, can be turned back OFF via:
195
        // add_filter( 'FHEE_load_EE_Session', '__return_false' );
196
        if (! self::$_instance instanceof EE_Session
197
            && apply_filters('FHEE_load_EE_Session', true)
198
            && $cache_storage instanceof CacheStorageInterface
199
            && $lifespan instanceof SessionLifespan
200
            && $request instanceof RequestInterface
201
            && $session_start_handler instanceof SessionStartHandler
202
        ) {
203
            self::$_instance = new self(
204
                $cache_storage,
205
                $lifespan,
206
                $request,
207
                $session_start_handler,
208
                $encryption
209
            );
210
        }
211
        return self::$_instance;
212
    }
213
214
215
    /**
216
     * protected constructor to prevent direct creation
217
     *
218
     * @param CacheStorageInterface $cache_storage
219
     * @param SessionLifespan       $lifespan
220
     * @param RequestInterface      $request
221
     * @param SessionStartHandler   $session_start_handler
222
     * @param EE_Encryption         $encryption
223
     * @throws InvalidArgumentException
224
     * @throws InvalidDataTypeException
225
     * @throws InvalidInterfaceException
226
     */
227
    protected function __construct(
228
        CacheStorageInterface $cache_storage,
229
        SessionLifespan $lifespan,
230
        RequestInterface $request,
231
        SessionStartHandler $session_start_handler,
232
        EE_Encryption $encryption = null
233
    ) {
234
        // session loading is turned ON by default,
235
        // but prior to the 'AHEE__EE_System__core_loaded_and_ready' hook
236
        // (which currently fires on the init hook at priority 9),
237
        // can be turned back OFF via: add_filter( 'FHEE_load_EE_Session', '__return_false' );
238
        if (! apply_filters('FHEE_load_EE_Session', true)) {
239
            return;
240
        }
241
        $this->session_start_handler = $session_start_handler;
242
        $this->session_lifespan = $lifespan;
243
        $this->request = $request;
244
        if (! defined('ESPRESSO_SESSION')) {
245
            define('ESPRESSO_SESSION', true);
246
        }
247
        // retrieve session options from db
248
        $session_settings = (array) get_option(EE_Session::OPTION_NAME_SETTINGS, array());
249
        if (! empty($session_settings)) {
250
            // cycle though existing session options
251
            foreach ($session_settings as $var_name => $session_setting) {
252
                // set values for class properties
253
                $var_name = '_' . $var_name;
254
                $this->{$var_name} = $session_setting;
255
            }
256
        }
257
        $this->cache_storage = $cache_storage;
258
        // are we using encryption?
259
        $this->_use_encryption = $encryption instanceof EE_Encryption
260
                                 && EE_Registry::instance()->CFG->admin->encode_session_data();
261
        // encrypt data via: $this->encryption->encrypt();
262
        $this->encryption = $encryption;
263
        // filter hook allows outside functions/classes/plugins to change default empty cart
264
        $extra_default_session_vars = apply_filters('FHEE__EE_Session__construct__extra_default_session_vars', array());
265
        array_merge($this->_default_session_vars, $extra_default_session_vars);
266
        // apply default session vars
267
        $this->_set_defaults();
268
        add_action('AHEE__EE_System__initialize', array($this, 'open_session'));
269
        // check request for 'clear_session' param
270
        add_action('AHEE__EE_Request_Handler__construct__complete', array($this, 'wp_loaded'));
271
        // once everything is all said and done,
272
        add_action('shutdown', array($this, 'update'), 100);
273
        add_action('shutdown', array($this, 'garbageCollection'), 1000);
274
        $this->configure_garbage_collection_filters();
275
    }
276
277
278
    /**
279
     * @return bool
280
     * @throws InvalidArgumentException
281
     * @throws InvalidDataTypeException
282
     * @throws InvalidInterfaceException
283
     */
284
    public static function isLoadedAndActive()
285
    {
286
        return did_action('AHEE__EE_System__core_loaded_and_ready')
287
               && EE_Session::instance() instanceof EE_Session
288
               && EE_Session::instance()->isActive();
289
    }
290
291
292
    /**
293
     * @return bool
294
     */
295
    public function isActive()
296
    {
297
        return $this->status === EE_Session::STATUS_OPEN;
298
    }
299
300
301
    /**
302
     * @return void
303
     * @throws EE_Error
304
     * @throws InvalidArgumentException
305
     * @throws InvalidDataTypeException
306
     * @throws InvalidInterfaceException
307
     * @throws InvalidSessionDataException
308
     */
309
    public function open_session()
310
    {
311
        // check for existing session and retrieve it from db
312
        if (! $this->_espresso_session()) {
313
            // or just start a new one
314
            $this->_create_espresso_session();
315
        }
316
    }
317
318
319
    /**
320
     * @return bool
321
     */
322
    public function expired()
323
    {
324
        return $this->_expired;
325
    }
326
327
328
    /**
329
     * @return void
330
     */
331
    public function reset_expired()
332
    {
333
        $this->_expired = false;
334
    }
335
336
337
    /**
338
     * @return int
339
     */
340
    public function expiration()
341
    {
342
        return $this->_expiration;
343
    }
344
345
346
    /**
347
     * @return int
348
     */
349
    public function extension()
350
    {
351
        return apply_filters('FHEE__EE_Session__extend_expiration__seconds_added', 10 * MINUTE_IN_SECONDS);
352
    }
353
354
355
    /**
356
     * @param int $time number of seconds to add to session expiration
357
     */
358
    public function extend_expiration($time = 0)
359
    {
360
        $time = $time ? $time : $this->extension();
361
        $this->_expiration += absint($time);
362
    }
363
364
365
    /**
366
     * @return int
367
     */
368
    public function lifespan()
369
    {
370
        return $this->session_lifespan->inSeconds();
371
    }
372
373
374
    /**
375
     * This just sets some defaults for the _session data property
376
     *
377
     * @access private
378
     * @return void
379
     */
380
    private function _set_defaults()
381
    {
382
        // set some defaults
383
        foreach ($this->_default_session_vars as $key => $default_var) {
384
            if (is_array($default_var)) {
385
                $this->_session_data[ $key ] = array();
386
            } else {
387
                $this->_session_data[ $key ] = '';
388
            }
389
        }
390
    }
391
392
393
    /**
394
     * @retrieve  session data
395
     * @access    public
396
     * @return    string
397
     */
398
    public function id()
399
    {
400
        return $this->_sid;
401
    }
402
403
404
    /**
405
     * @param \EE_Cart $cart
406
     * @return bool
407
     */
408
    public function set_cart(EE_Cart $cart)
409
    {
410
        $this->_session_data['cart'] = $cart;
411
        return true;
412
    }
413
414
415
    /**
416
     * reset_cart
417
     */
418
    public function reset_cart()
419
    {
420
        do_action('AHEE__EE_Session__reset_cart__before_reset', $this);
421
        $this->_session_data['cart'] = null;
422
    }
423
424
425
    /**
426
     * @return \EE_Cart
427
     */
428
    public function cart()
429
    {
430
        return isset($this->_session_data['cart']) && $this->_session_data['cart'] instanceof EE_Cart
431
            ? $this->_session_data['cart']
432
            : null;
433
    }
434
435
436
    /**
437
     * @param \EE_Checkout $checkout
438
     * @return bool
439
     */
440
    public function set_checkout(EE_Checkout $checkout)
441
    {
442
        $this->_session_data['checkout'] = $checkout;
443
        return true;
444
    }
445
446
447
    /**
448
     * reset_checkout
449
     */
450
    public function reset_checkout()
451
    {
452
        do_action('AHEE__EE_Session__reset_checkout__before_reset', $this);
453
        $this->_session_data['checkout'] = null;
454
    }
455
456
457
    /**
458
     * @return \EE_Checkout
459
     */
460
    public function checkout()
461
    {
462
        return isset($this->_session_data['checkout']) && $this->_session_data['checkout'] instanceof EE_Checkout
463
            ? $this->_session_data['checkout']
464
            : null;
465
    }
466
467
468
    /**
469
     * @param \EE_Transaction $transaction
470
     * @return bool
471
     * @throws EE_Error
472
     */
473
    public function set_transaction(EE_Transaction $transaction)
474
    {
475
        // first remove the session from the transaction before we save the transaction in the session
476
        $transaction->set_txn_session_data(null);
477
        $this->_session_data['transaction'] = $transaction;
478
        return true;
479
    }
480
481
482
    /**
483
     * reset_transaction
484
     */
485
    public function reset_transaction()
486
    {
487
        do_action('AHEE__EE_Session__reset_transaction__before_reset', $this);
488
        $this->_session_data['transaction'] = null;
489
    }
490
491
492
    /**
493
     * @return \EE_Transaction
494
     */
495
    public function transaction()
496
    {
497
        return isset($this->_session_data['transaction'])
498
               && $this->_session_data['transaction'] instanceof EE_Transaction
499
            ? $this->_session_data['transaction']
500
            : null;
501
    }
502
503
504
    /**
505
     * retrieve session data
506
     *
507
     * @param null $key
508
     * @param bool $reset_cache
509
     * @return array
510
     */
511
    public function get_session_data($key = null, $reset_cache = false)
512
    {
513
        if ($reset_cache) {
514
            $this->reset_cart();
515
            $this->reset_checkout();
516
            $this->reset_transaction();
517
        }
518
        if (! empty($key)) {
519
            return isset($this->_session_data[ $key ]) ? $this->_session_data[ $key ] : null;
520
        }
521
        return $this->_session_data;
522
    }
523
524
525
    /**
526
     * Returns TRUE on success, FALSE on fail
527
     *
528
     * @param array $data
529
     * @return bool
530
     */
531
    public function set_session_data($data)
532
    {
533
        // nothing ??? bad data ??? go home!
534
        if (empty($data) || ! is_array($data)) {
535
            EE_Error::add_error(
536
                esc_html__(
537
                    'No session data or invalid session data was provided.',
538
                    'event_espresso'
539
                ),
540
                __FILE__,
541
                __FUNCTION__,
542
                __LINE__
543
            );
544
            return false;
545
        }
546
        foreach ($data as $key => $value) {
547 View Code Duplication
            if (isset($this->_default_session_vars[ $key ])) {
548
                EE_Error::add_error(
549
                    sprintf(
550
                        esc_html__(
551
                            'Sorry! %s is a default session datum and can not be reset.',
552
                            'event_espresso'
553
                        ),
554
                        $key
555
                    ),
556
                    __FILE__,
557
                    __FUNCTION__,
558
                    __LINE__
559
                );
560
                return false;
561
            }
562
            $this->_session_data[ $key ] = $value;
563
        }
564
        return true;
565
    }
566
567
568
    /**
569
     * @initiate session
570
     * @access   private
571
     * @return TRUE on success, FALSE on fail
572
     * @throws EE_Error
573
     * @throws InvalidArgumentException
574
     * @throws InvalidDataTypeException
575
     * @throws InvalidInterfaceException
576
     * @throws InvalidSessionDataException
577
     */
578
    private function _espresso_session()
579
    {
580
        do_action('AHEE_log', __FILE__, __FUNCTION__, '');
581
        $this->session_start_handler->startSession();
582
        $this->status = EE_Session::STATUS_OPEN;
583
        // get our modified session ID
584
        $this->_sid = $this->_generate_session_id();
585
        // and the visitors IP
586
        $this->_ip_address = $this->request->ipAddress();
587
        // set the "user agent"
588
        $this->_user_agent = $this->request->userAgent();
589
        // now let's retrieve what's in the db
590
        $session_data = $this->_retrieve_session_data();
591
        if (! empty($session_data)) {
592
            // get the current time in UTC
593
            $this->_time = $this->_time !== null ? $this->_time : time();
594
            // and reset the session expiration
595
            $this->_expiration = isset($session_data['expiration'])
596
                ? $session_data['expiration']
597
                : $this->_time + $this->session_lifespan->inSeconds();
598
        } else {
599
            // set initial site access time and the session expiration
600
            $this->_set_init_access_and_expiration();
601
            // set referer
602
            $this->_session_data['pages_visited'][ $this->_session_data['init_access'] ] = isset($_SERVER['HTTP_REFERER'])
603
                ? esc_attr($_SERVER['HTTP_REFERER'])
604
                : '';
605
            // no previous session = go back and create one (on top of the data above)
606
            return false;
607
        }
608
        // now the user agent
609
        if ($session_data['user_agent'] !== $this->_user_agent) {
610
            return false;
611
        }
612
        // wait a minute... how old are you?
613
        if ($this->_time > $this->_expiration) {
614
            // yer too old fer me!
615
            $this->_expired = true;
616
            // wipe out everything that isn't a default session datum
617
            $this->clear_session(__CLASS__, __FUNCTION__);
618
        }
619
        // make event espresso session data available to plugin
620
        $this->_session_data = array_merge($this->_session_data, $session_data);
621
        return true;
622
    }
623
624
625
    /**
626
     * _get_session_data
627
     * Retrieves the session data, and attempts to correct any encoding issues that can occur due to improperly setup
628
     * databases
629
     *
630
     * @return array
631
     * @throws EE_Error
632
     * @throws InvalidArgumentException
633
     * @throws InvalidSessionDataException
634
     * @throws InvalidDataTypeException
635
     * @throws InvalidInterfaceException
636
     */
637
    protected function _retrieve_session_data()
638
    {
639
        $ssn_key = EE_Session::session_id_prefix . $this->_sid;
640
        try {
641
            // we're using WP's Transient API to store session data using the PHP session ID as the option name
642
            $session_data = $this->cache_storage->get($ssn_key, false);
643
            if (empty($session_data)) {
644
                return array();
645
            }
646
            if (apply_filters('FHEE__EE_Session___perform_session_id_hash_check', WP_DEBUG)) {
647
                $hash_check = $this->cache_storage->get(
648
                    EE_Session::hash_check_prefix . $this->_sid,
649
                    false
650
                );
651
                if ($hash_check && $hash_check !== md5($session_data)) {
652
                    EE_Error::add_error(
653
                        sprintf(
654
                            __(
655
                                'The stored data for session %1$s failed to pass a hash check and therefore appears to be invalid.',
656
                                'event_espresso'
657
                            ),
658
                            EE_Session::session_id_prefix . $this->_sid
659
                        ),
660
                        __FILE__,
661
                        __FUNCTION__,
662
                        __LINE__
663
                    );
664
                }
665
            }
666
        } catch (Exception $e) {
667
            // let's just eat that error for now and attempt to correct any corrupted data
668
            global $wpdb;
669
            $row = $wpdb->get_row(
670
                $wpdb->prepare(
671
                    "SELECT option_value FROM {$wpdb->options} WHERE option_name = %s LIMIT 1",
672
                    '_transient_' . $ssn_key
673
                )
674
            );
675
            $session_data = is_object($row) ? $row->option_value : null;
676
            if ($session_data) {
677
                $session_data = preg_replace_callback(
678
                    '!s:(d+):"(.*?)";!',
679 View Code Duplication
                    function ($match) {
680
                        return $match[1] === strlen($match[2])
681
                            ? $match[0]
682
                            : 's:' . strlen($match[2]) . ':"' . $match[2] . '";';
683
                    },
684
                    $session_data
685
                );
686
            }
687
            $session_data = maybe_unserialize($session_data);
688
        }
689
        // in case the data is encoded... try to decode it
690
        $session_data = $this->encryption instanceof EE_Encryption
691
            ? $this->encryption->base64_string_decode($session_data)
692
            : $session_data;
693
        if (! is_array($session_data)) {
694
            try {
695
                $session_data = maybe_unserialize($session_data);
696
            } catch (Exception $e) {
697
                $msg = esc_html__(
698
                    'An error occurred while attempting to unserialize the session data.',
699
                    'event_espresso'
700
                );
701
                $msg .= WP_DEBUG
702
                    ? '<br><pre>'
703
                      . print_r($session_data, true)
704
                      . '</pre><br>'
705
                      . $this->find_serialize_error($session_data)
706
                    : '';
707
                $this->cache_storage->delete(EE_Session::session_id_prefix . $this->_sid);
708
                throw new InvalidSessionDataException($msg, 0, $e);
709
            }
710
        }
711
        // just a check to make sure the session array is indeed an array
712
        if (! is_array($session_data)) {
713
            // no?!?! then something is wrong
714
            $msg = esc_html__(
715
                'The session data is missing, invalid, or corrupted.',
716
                'event_espresso'
717
            );
718
            $msg .= WP_DEBUG
719
                ? '<br><pre>' . print_r($session_data, true) . '</pre><br>' . $this->find_serialize_error($session_data)
720
                : '';
721
            $this->cache_storage->delete(EE_Session::session_id_prefix . $this->_sid);
722
            throw new InvalidSessionDataException($msg);
723
        }
724 View Code Duplication
        if (isset($session_data['transaction']) && absint($session_data['transaction']) !== 0) {
725
            $session_data['transaction'] = EEM_Transaction::instance()->get_one_by_ID(
726
                $session_data['transaction']
727
            );
728
        }
729
        return $session_data;
730
    }
731
732
733
    /**
734
     * _generate_session_id
735
     * Retrieves the PHP session id either directly from the PHP session,
736
     * or from the $_REQUEST array if it was passed in from an AJAX request.
737
     * The session id is then salted and hashed (mmm sounds tasty)
738
     * so that it can be safely used as a $_REQUEST param
739
     *
740
     * @return string
741
     */
742
    protected function _generate_session_id()
743
    {
744
        // check if the SID was passed explicitly, otherwise get from session, then add salt and hash it to reduce length
745
        if (isset($_REQUEST['EESID'])) {
746
            $session_id = sanitize_text_field($_REQUEST['EESID']);
747
        } else {
748
            $session_id = md5(session_id() . get_current_blog_id() . $this->_get_sid_salt());
749
        }
750
        return apply_filters('FHEE__EE_Session___generate_session_id__session_id', $session_id);
751
    }
752
753
754
    /**
755
     * _get_sid_salt
756
     *
757
     * @return string
758
     */
759
    protected function _get_sid_salt()
760
    {
761
        // was session id salt already saved to db ?
762
        if (empty($this->_sid_salt)) {
763
            // no?  then maybe use WP defined constant
764
            if (defined('AUTH_SALT')) {
765
                $this->_sid_salt = AUTH_SALT;
766
            }
767
            // if salt doesn't exist or is too short
768
            if (strlen($this->_sid_salt) < 32) {
769
                // create a new one
770
                $this->_sid_salt = wp_generate_password(64);
771
            }
772
            // and save it as a permanent session setting
773
            $this->updateSessionSettings(array('sid_salt' => $this->_sid_salt));
774
        }
775
        return $this->_sid_salt;
776
    }
777
778
779
    /**
780
     * _set_init_access_and_expiration
781
     *
782
     * @return void
783
     */
784
    protected function _set_init_access_and_expiration()
785
    {
786
        $this->_time = time();
787
        $this->_expiration = $this->_time + $this->session_lifespan->inSeconds();
788
        // set initial site access time
789
        $this->_session_data['init_access'] = $this->_time;
790
        // and the session expiration
791
        $this->_session_data['expiration'] = $this->_expiration;
792
    }
793
794
795
    /**
796
     * @update session data  prior to saving to the db
797
     * @access public
798
     * @param bool $new_session
799
     * @return TRUE on success, FALSE on fail
800
     * @throws EE_Error
801
     * @throws InvalidArgumentException
802
     * @throws InvalidDataTypeException
803
     * @throws InvalidInterfaceException
804
     */
805
    public function update($new_session = false)
806
    {
807
        $this->_session_data = $this->_session_data !== null
808
                               && is_array($this->_session_data)
809
                               && isset($this->_session_data['id'])
810
            ? $this->_session_data
811
            : array();
812
        if (empty($this->_session_data)) {
813
            $this->_set_defaults();
814
        }
815
        $session_data = array();
816
        foreach ($this->_session_data as $key => $value) {
817
            switch ($key) {
818
                case 'id':
819
                    // session ID
820
                    $session_data['id'] = $this->_sid;
821
                    break;
822
                case 'ip_address':
823
                    // visitor ip address
824
                    $session_data['ip_address'] = $this->request->ipAddress();
825
                    break;
826
                case 'user_agent':
827
                    // visitor user_agent
828
                    $session_data['user_agent'] = $this->_user_agent;
829
                    break;
830
                case 'init_access':
831
                    $session_data['init_access'] = absint($value);
832
                    break;
833
                case 'last_access':
834
                    // current access time
835
                    $session_data['last_access'] = $this->_time;
836
                    break;
837
                case 'expiration':
838
                    // when the session expires
839
                    $session_data['expiration'] = ! empty($this->_expiration)
840
                        ? $this->_expiration
841
                        : $session_data['init_access'] + $this->session_lifespan->inSeconds();
842
                    break;
843
                case 'user_id':
844
                    // current user if logged in
845
                    $session_data['user_id'] = $this->_wp_user_id();
846
                    break;
847
                case 'pages_visited':
848
                    $page_visit = $this->_get_page_visit();
849
                    if ($page_visit) {
850
                        // set pages visited where the first will be the http referrer
851
                        $this->_session_data['pages_visited'][ $this->_time ] = $page_visit;
852
                        // we'll only save the last 10 page visits.
853
                        $session_data['pages_visited'] = array_slice($this->_session_data['pages_visited'], -10);
854
                    }
855
                    break;
856
                default:
857
                    // carry any other data over
858
                    $session_data[ $key ] = $this->_session_data[ $key ];
859
            }
860
        }
861
        $this->_session_data = $session_data;
862
        // creating a new session does not require saving to the db just yet
863
        if (! $new_session) {
864
            // ready? let's save
865
            if ($this->_save_session_to_db()) {
866
                return true;
867
            }
868
            return false;
869
        }
870
        // meh, why not?
871
        return true;
872
    }
873
874
875
    /**
876
     * @create session data array
877
     * @access public
878
     * @return bool
879
     * @throws EE_Error
880
     * @throws InvalidArgumentException
881
     * @throws InvalidDataTypeException
882
     * @throws InvalidInterfaceException
883
     */
884
    private function _create_espresso_session()
885
    {
886
        do_action('AHEE_log', __CLASS__, __FUNCTION__, '');
887
        // use the update function for now with $new_session arg set to TRUE
888
        return $this->update(true) ? true : false;
889
    }
890
891
    /**
892
     * Detects if there is anything worth saving in the session (eg the cart is a good one, notices are pretty good
893
     * too). This is used when determining if we want to save the session or not.
894
     * @since 4.9.67.p
895
     * @return bool
896
     */
897
    private function sessionHasStuffWorthSaving()
898
    {
899
        return $this->cart() instanceof EE_Cart
900
            || (
901
                isset($this->_session_data['ee_notices'])
902
                && (
903
                    ! empty($this->_session_data['ee_notices']['attention'])
904
                    || !empty($this->_session_data['ee_notices']['errors'])
905
                    || !empty($this->_session_data['ee_notices']['success'])
906
                )
907
            );
908
    }
909
    /**
910
     * _save_session_to_db
911
     *
912
     * @param bool $clear_session
913
     * @return string
914
     * @throws EE_Error
915
     * @throws InvalidArgumentException
916
     * @throws InvalidDataTypeException
917
     * @throws InvalidInterfaceException
918
     */
919
    private function _save_session_to_db($clear_session = false)
920
    {
921
        // don't save sessions for crawlers
922
        // and unless we're deleting the session data, don't save anything if there isn't a cart
923
        if ($this->request->isBot()
924
            || (
925
                ! $clear_session
926
                && ! $this->sessionHasStuffWorthSaving()
927
                && apply_filters('FHEE__EE_Session___save_session_to_db__abort_session_save', true)
928
            )
929
        ) {
930
            return false;
931
        }
932
        $transaction = $this->transaction();
933
        if ($transaction instanceof EE_Transaction) {
934
            if (! $transaction->ID()) {
935
                $transaction->save();
936
            }
937
            $this->_session_data['transaction'] = $transaction->ID();
938
        }
939
        // then serialize all of our session data
940
        $session_data = serialize($this->_session_data);
941
        // do we need to also encode it to avoid corrupted data when saved to the db?
942
        $session_data = $this->_use_encryption
943
            ? $this->encryption->base64_string_encode($session_data)
944
            : $session_data;
945
        // maybe save hash check
946
        if (apply_filters('FHEE__EE_Session___perform_session_id_hash_check', WP_DEBUG)) {
947
            $this->cache_storage->add(
948
                EE_Session::hash_check_prefix . $this->_sid,
949
                md5($session_data),
950
                $this->session_lifespan->inSeconds()
951
            );
952
        }
953
        // we're using the Transient API for storing session data,
954
        return $this->cache_storage->add(
955
            EE_Session::session_id_prefix . $this->_sid,
956
            $session_data,
957
            $this->session_lifespan->inSeconds()
958
        );
959
    }
960
961
962
    /**
963
     * @get    the full page request the visitor is accessing
964
     * @access public
965
     * @return string
966
     */
967
    public function _get_page_visit()
968
    {
969
        $page_visit = home_url('/') . 'wp-admin/admin-ajax.php';
970
        // check for request url
971
        if (isset($_SERVER['REQUEST_URI'])) {
972
            $http_host = '';
973
            $page_id = '?';
974
            $e_reg = '';
975
            $request_uri = esc_url($_SERVER['REQUEST_URI']);
976
            $ru_bits = explode('?', $request_uri);
977
            $request_uri = $ru_bits[0];
978
            // check for and grab host as well
979
            if (isset($_SERVER['HTTP_HOST'])) {
980
                $http_host = esc_url($_SERVER['HTTP_HOST']);
981
            }
982
            // check for page_id in SERVER REQUEST
983
            if (isset($_REQUEST['page_id'])) {
984
                // rebuild $e_reg without any of the extra parameters
985
                $page_id = '?page_id=' . esc_attr($_REQUEST['page_id']) . '&amp;';
986
            }
987
            // check for $e_reg in SERVER REQUEST
988
            if (isset($_REQUEST['ee'])) {
989
                // rebuild $e_reg without any of the extra parameters
990
                $e_reg = 'ee=' . esc_attr($_REQUEST['ee']);
991
            }
992
            $page_visit = rtrim($http_host . $request_uri . $page_id . $e_reg, '?');
993
        }
994
        return $page_visit !== home_url('/wp-admin/admin-ajax.php') ? $page_visit : '';
995
    }
996
997
998
    /**
999
     * @the    current wp user id
1000
     * @access public
1001
     * @return int
1002
     */
1003
    public function _wp_user_id()
1004
    {
1005
        // if I need to explain the following lines of code, then you shouldn't be looking at this!
1006
        $this->_wp_user_id = get_current_user_id();
1007
        return $this->_wp_user_id;
1008
    }
1009
1010
1011
    /**
1012
     * Clear EE_Session data
1013
     *
1014
     * @access public
1015
     * @param string $class
1016
     * @param string $function
1017
     * @return void
1018
     * @throws EE_Error
1019
     * @throws InvalidArgumentException
1020
     * @throws InvalidDataTypeException
1021
     * @throws InvalidInterfaceException
1022
     */
1023
    public function clear_session($class = '', $function = '')
1024
    {
1025
//         echo '
1026
// <h3 style="color:#999;line-height:.9em;">
1027
// <span style="color:#2EA2CC">' . __CLASS__ . '</span>::<span style="color:#E76700">' . __FUNCTION__ . '( ' . $class . '::' . $function . '() )</span><br/>
1028
// <span style="font-size:9px;font-weight:normal;">' . __FILE__ . '</span>    <b style="font-size:10px;">  ' . __LINE__ . ' </b>
1029
// </h3>';
1030
        do_action('AHEE_log', __FILE__, __FUNCTION__, 'session cleared by : ' . $class . '::' . $function . '()');
1031
        $this->reset_cart();
1032
        $this->reset_checkout();
1033
        $this->reset_transaction();
1034
        // wipe out everything that isn't a default session datum
1035
        $this->reset_data(array_keys($this->_session_data));
1036
        // reset initial site access time and the session expiration
1037
        $this->_set_init_access_and_expiration();
1038
        $this->_save_session_to_db(true);
1039
    }
1040
1041
1042
    /**
1043
     * resets all non-default session vars. Returns TRUE on success, FALSE on fail
1044
     *
1045
     * @param array|mixed $data_to_reset
1046
     * @param bool        $show_all_notices
1047
     * @return bool
1048
     */
1049
    public function reset_data($data_to_reset = array(), $show_all_notices = false)
1050
    {
1051
        // if $data_to_reset is not in an array, then put it in one
1052
        if (! is_array($data_to_reset)) {
1053
            $data_to_reset = array($data_to_reset);
1054
        }
1055
        // nothing ??? go home!
1056
        if (empty($data_to_reset)) {
1057
            EE_Error::add_error(
1058
                __(
1059
                    'No session data could be reset, because no session var name was provided.',
1060
                    'event_espresso'
1061
                ),
1062
                __FILE__,
1063
                __FUNCTION__,
1064
                __LINE__
1065
            );
1066
            return false;
1067
        }
1068
        $return_value = true;
1069
        // since $data_to_reset is an array, cycle through the values
1070
        foreach ($data_to_reset as $reset) {
1071
            // first check to make sure it is a valid session var
1072
            if (isset($this->_session_data[ $reset ])) {
1073
                // then check to make sure it is not a default var
1074
                if (! array_key_exists($reset, $this->_default_session_vars)) {
1075
                    // remove session var
1076
                    unset($this->_session_data[ $reset ]);
1077
                    if ($show_all_notices) {
1078
                        EE_Error::add_success(
1079
                            sprintf(
1080
                                __('The session variable %s was removed.', 'event_espresso'),
1081
                                $reset
1082
                            ),
1083
                            __FILE__,
1084
                            __FUNCTION__,
1085
                            __LINE__
1086
                        );
1087
                    }
1088 View Code Duplication
                } else {
1089
                    // yeeeeeeeeerrrrrrrrrrr OUT !!!!
1090
                    if ($show_all_notices) {
1091
                        EE_Error::add_error(
1092
                            sprintf(
1093
                                __(
1094
                                    'Sorry! %s is a default session datum and can not be reset.',
1095
                                    'event_espresso'
1096
                                ),
1097
                                $reset
1098
                            ),
1099
                            __FILE__,
1100
                            __FUNCTION__,
1101
                            __LINE__
1102
                        );
1103
                    }
1104
                    $return_value = false;
1105
                }
1106 View Code Duplication
            } elseif ($show_all_notices) {
1107
                // oops! that session var does not exist!
1108
                EE_Error::add_error(
1109
                    sprintf(
1110
                        __(
1111
                            'The session item provided, %s, is invalid or does not exist.',
1112
                            'event_espresso'
1113
                        ),
1114
                        $reset
1115
                    ),
1116
                    __FILE__,
1117
                    __FUNCTION__,
1118
                    __LINE__
1119
                );
1120
                $return_value = false;
1121
            }
1122
        } // end of foreach
1123
        return $return_value;
1124
    }
1125
1126
1127
    /**
1128
     *   wp_loaded
1129
     *
1130
     * @access public
1131
     * @throws EE_Error
1132
     * @throws InvalidDataTypeException
1133
     * @throws InvalidInterfaceException
1134
     * @throws InvalidArgumentException
1135
     */
1136
    public function wp_loaded()
1137
    {
1138
        if ($this->request->requestParamIsSet('clear_session')) {
1139
            $this->clear_session(__CLASS__, __FUNCTION__);
1140
        }
1141
    }
1142
1143
1144
    /**
1145
     * Used to reset the entire object (for tests).
1146
     *
1147
     * @since 4.3.0
1148
     * @throws EE_Error
1149
     * @throws InvalidDataTypeException
1150
     * @throws InvalidInterfaceException
1151
     * @throws InvalidArgumentException
1152
     */
1153
    public function reset_instance()
1154
    {
1155
        $this->clear_session();
1156
        self::$_instance = null;
1157
    }
1158
1159
1160
    public function configure_garbage_collection_filters()
1161
    {
1162
        // run old filter we had for controlling session cleanup
1163
        $expired_session_transient_delete_query_limit = absint(
1164
            apply_filters(
1165
                'FHEE__EE_Session__garbage_collection___expired_session_transient_delete_query_limit',
1166
                50
1167
            )
1168
        );
1169
        // is there a value? or one that is different than the default 50 records?
1170
        if ($expired_session_transient_delete_query_limit === 0) {
1171
            // hook into TransientCacheStorage in case Session cleanup was turned off
1172
            add_filter('FHEE__TransientCacheStorage__transient_cleanup_schedule', '__return_zero');
1173
        } elseif ($expired_session_transient_delete_query_limit !== 50) {
1174
            // or use that for the new transient cleanup query limit
1175
            add_filter(
1176
                'FHEE__TransientCacheStorage__clearExpiredTransients__limit',
1177
                function () use ($expired_session_transient_delete_query_limit) {
1178
                    return $expired_session_transient_delete_query_limit;
1179
                }
1180
            );
1181
        }
1182
    }
1183
1184
1185
    /**
1186
     * @see http://stackoverflow.com/questions/10152904/unserialize-function-unserialize-error-at-offset/21389439#10152996
1187
     * @param $data1
1188
     * @return string
1189
     */
1190
    private function find_serialize_error($data1)
1191
    {
1192
        $error = '<pre>';
1193
        $data2 = preg_replace_callback(
1194
            '!s:(\d+):"(.*?)";!',
1195 View Code Duplication
            function ($match) {
1196
                return ($match[1] === strlen($match[2]))
1197
                    ? $match[0]
1198
                    : 's:'
1199
                      . strlen($match[2])
1200
                      . ':"'
1201
                      . $match[2]
1202
                      . '";';
1203
            },
1204
            $data1
1205
        );
1206
        $max = (strlen($data1) > strlen($data2)) ? strlen($data1) : strlen($data2);
1207
        $error .= $data1 . PHP_EOL;
1208
        $error .= $data2 . PHP_EOL;
1209
        for ($i = 0; $i < $max; $i++) {
1210
            if (@$data1[ $i ] !== @$data2[ $i ]) {
1211
                $error .= 'Difference ' . @$data1[ $i ] . ' != ' . @$data2[ $i ] . PHP_EOL;
1212
                $error .= "\t-> ORD number " . ord(@$data1[ $i ]) . ' != ' . ord(@$data2[ $i ]) . PHP_EOL;
1213
                $error .= "\t-> Line Number = $i" . PHP_EOL;
1214
                $start = ($i - 20);
1215
                $start = ($start < 0) ? 0 : $start;
1216
                $length = 40;
1217
                $point = $max - $i;
1218
                if ($point < 20) {
1219
                    $rlength = 1;
1220
                    $rpoint = -$point;
1221
                } else {
1222
                    $rpoint = $length - 20;
1223
                    $rlength = 1;
1224
                }
1225
                $error .= "\t-> Section Data1  = ";
1226
                $error .= substr_replace(
1227
                    substr($data1, $start, $length),
1228
                    "<b style=\"color:green\">{$data1[ $i ]}</b>",
1229
                    $rpoint,
1230
                    $rlength
1231
                );
1232
                $error .= PHP_EOL;
1233
                $error .= "\t-> Section Data2  = ";
1234
                $error .= substr_replace(
1235
                    substr($data2, $start, $length),
1236
                    "<b style=\"color:red\">{$data2[ $i ]}</b>",
1237
                    $rpoint,
1238
                    $rlength
1239
                );
1240
                $error .= PHP_EOL;
1241
            }
1242
        }
1243
        $error .= '</pre>';
1244
        return $error;
1245
    }
1246
1247
1248
    /**
1249
     * Saves an  array of settings used for configuring aspects of session behaviour
1250
     *
1251
     * @param array $updated_settings
1252
     */
1253
    private function updateSessionSettings(array $updated_settings = array())
1254
    {
1255
        // add existing settings, but only if not included in incoming $updated_settings array
1256
        $updated_settings += get_option(EE_Session::OPTION_NAME_SETTINGS, array());
1257
        update_option(EE_Session::OPTION_NAME_SETTINGS, $updated_settings);
1258
    }
1259
1260
1261
    /**
1262
     * garbage_collection
1263
     */
1264
    public function garbageCollection()
1265
    {
1266
        // only perform during regular requests if last garbage collection was over an hour ago
1267
        if (! (defined('DOING_AJAX') && DOING_AJAX) && (time() - HOUR_IN_SECONDS) >= $this->_last_gc) {
1268
            $this->_last_gc = time();
1269
            $this->updateSessionSettings(array('last_gc' => $this->_last_gc));
1270
            /** @type WPDB $wpdb */
1271
            global $wpdb;
1272
            // filter the query limit. Set to 0 to turn off garbage collection
1273
            $expired_session_transient_delete_query_limit = absint(
1274
                apply_filters(
1275
                    'FHEE__EE_Session__garbage_collection___expired_session_transient_delete_query_limit',
1276
                    50
1277
                )
1278
            );
1279
            // non-zero LIMIT means take out the trash
1280
            if ($expired_session_transient_delete_query_limit) {
1281
                $session_key = str_replace('_', '\_', EE_Session::session_id_prefix);
1282
                $hash_check_key = str_replace('_', '\_', EE_Session::hash_check_prefix);
1283
                // since transient expiration timestamps are set in the future, we can compare against NOW
1284
                // but we only want to pick up any trash that's been around for more than a day
1285
                $expiration = time() - DAY_IN_SECONDS;
1286
                $SQL = "
1287
                    SELECT option_name
1288
                    FROM {$wpdb->options}
1289
                    WHERE
1290
                      ( option_name LIKE '\_transient\_timeout\_{$session_key}%'
1291
                      OR option_name LIKE '\_transient\_timeout\_{$hash_check_key}%' )
1292
                    AND option_value < {$expiration}
1293
                    LIMIT {$expired_session_transient_delete_query_limit}
1294
                ";
1295
                // produces something like:
1296
                // SELECT option_name FROM wp_options
1297
                // WHERE ( option_name LIKE '\_transient\_timeout\_ee\_ssn\_%'
1298
                // OR option_name LIKE '\_transient\_timeout\_ee\_shc\_%' )
1299
                // AND option_value < 1508368198 LIMIT 50
1300
                $expired_sessions = $wpdb->get_col($SQL);
1301
                // valid results?
1302
                if (! $expired_sessions instanceof WP_Error && ! empty($expired_sessions)) {
1303
                    $this->cache_storage->deleteMany($expired_sessions, true);
1304
                }
1305
            }
1306
        }
1307
    }
1308
}
1309