Completed
Branch Gutenberg/master (5ab96a)
by
unknown
25:09 queued 17:00
created

EE_Session::isLoadedAndActive()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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