Completed
Branch BUG/plugin-editor-session-fail (9bf3d9)
by
unknown
09:52 queued 43s
created

EE_Session::sessionHasStuffWorthSaving()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
nc 5
nop 0
dl 0
loc 12
rs 9.5555
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
11
/**
12
 * EE_Session class
13
 *
14
 * Please note that the session doesn't save by default, except when it has a cart set on it.
15
 * In order for it to save on other pages, you must execute
16
 * `add_action('FHEE__EE_Session___save_session_to_db__abort_session_save', '__return_false');`
17
 * somewhere during the request
18
 *
19
 * @package    Event Espresso
20
 * @subpackage includes/classes
21
 * @author     Brent Christensen
22
 */
23
class EE_Session implements SessionIdentifierInterface
24
{
25
26
    const session_id_prefix = 'ee_ssn_';
27
28
    const hash_check_prefix = 'ee_shc_';
29
30
    const OPTION_NAME_SETTINGS = 'ee_session_settings';
31
32
    const STATUS_CLOSED = 0;
33
34
    const STATUS_OPEN = 1;
35
36
    /**
37
     * instance of the EE_Session object
38
     *
39
     * @var EE_Session
40
     */
41
    private static $_instance;
42
43
    /**
44
     * @var CacheStorageInterface $cache_storage
45
     */
46
    protected $cache_storage;
47
48
    /**
49
     * EE_Encryption object
50
     *
51
     * @var EE_Encryption
52
     */
53
    protected $encryption;
54
55
    /**
56
     * the session id
57
     *
58
     * @var string
59
     */
60
    private $_sid;
61
62
    /**
63
     * session id salt
64
     *
65
     * @var string
66
     */
67
    private $_sid_salt;
68
69
    /**
70
     * session data
71
     *
72
     * @var array
73
     */
74
    private $_session_data = array();
75
76
    /**
77
     * how long an EE session lasts
78
     * default session lifespan of 1 hour (for not so instant IPNs)
79
     *
80
     * @var SessionLifespan $session_lifespan
81
     */
82
    private $session_lifespan;
83
84
    /**
85
     * session expiration time as Unix timestamp in GMT
86
     *
87
     * @var int
88
     */
89
    private $_expiration;
90
91
    /**
92
     * whether or not session has expired at some point
93
     *
94
     * @var boolean
95
     */
96
    private $_expired = false;
97
98
    /**
99
     * current time as Unix timestamp in GMT
100
     *
101
     * @var int
102
     */
103
    private $_time;
104
105
    /**
106
     * whether to encrypt session data
107
     *
108
     * @var bool
109
     */
110
    private $_use_encryption;
111
112
    /**
113
     * well... according to the server...
114
     *
115
     * @var null
116
     */
117
    private $_user_agent;
118
119
    /**
120
     * do you really trust the server ?
121
     *
122
     * @var null
123
     */
124
    private $_ip_address;
125
126
    /**
127
     * current WP user_id
128
     *
129
     * @var null
130
     */
131
    private $_wp_user_id;
132
133
    /**
134
     * array for defining default session vars
135
     *
136
     * @var array
137
     */
138
    private $_default_session_vars = array(
139
        'id'            => null,
140
        'user_id'       => null,
141
        'ip_address'    => null,
142
        'user_agent'    => null,
143
        'init_access'   => null,
144
        'last_access'   => null,
145
        'expiration'    => null,
146
        'pages_visited' => array(),
147
    );
148
149
    /**
150
     * timestamp for when last garbage collection cycle was performed
151
     *
152
     * @var int $_last_gc
153
     */
154
    private $_last_gc;
155
156
    /**
157
     * @var RequestInterface $request
158
     */
159
    protected $request;
160
161
    /**
162
     * whether session is active or not
163
     *
164
     * @var int $status
165
     */
166
    private $status = EE_Session::STATUS_CLOSED;
167
168
169
    /**
170
     * @singleton method used to instantiate class object
171
     * @param CacheStorageInterface $cache_storage
172
     * @param SessionLifespan|null  $lifespan
173
     * @param RequestInterface      $request
174
     * @param EE_Encryption         $encryption
175
     * @return EE_Session
176
     * @throws InvalidArgumentException
177
     * @throws InvalidDataTypeException
178
     * @throws InvalidInterfaceException
179
     */
180
    public static function instance(
181
        CacheStorageInterface $cache_storage = null,
182
        SessionLifespan $lifespan = null,
183
        RequestInterface $request = null,
184
        EE_Encryption $encryption = null
185
    ) {
186
        // check if class object is instantiated
187
        // session loading is turned ON by default, but prior to the init hook, can be turned back OFF via:
188
        // add_filter( 'FHEE_load_EE_Session', '__return_false' );
189
        if (! self::$_instance instanceof EE_Session && apply_filters('FHEE_load_EE_Session', true)) {
190
            self::$_instance = new self(
191
                $cache_storage,
0 ignored issues
show
Bug introduced by
It seems like $cache_storage defined by parameter $cache_storage on line 181 can be null; however, EE_Session::__construct() does not accept null, maybe add an additional type check?

It seems like you allow that null is being passed for a parameter, however the function which is called does not seem to accept null.

We recommend to add an additional type check (or disallow null for the parameter):

function notNullable(stdClass $x) { }

// Unsafe
function withoutCheck(stdClass $x = null) {
    notNullable($x);
}

// Safe - Alternative 1: Adding Additional Type-Check
function withCheck(stdClass $x = null) {
    if ($x instanceof stdClass) {
        notNullable($x);
    }
}

// Safe - Alternative 2: Changing Parameter
function withNonNullableParam(stdClass $x) {
    notNullable($x);
}
Loading history...
192
                $lifespan,
0 ignored issues
show
Bug introduced by
It seems like $lifespan defined by parameter $lifespan on line 182 can be null; however, EE_Session::__construct() does not accept null, maybe add an additional type check?

It seems like you allow that null is being passed for a parameter, however the function which is called does not seem to accept null.

We recommend to add an additional type check (or disallow null for the parameter):

function notNullable(stdClass $x) { }

// Unsafe
function withoutCheck(stdClass $x = null) {
    notNullable($x);
}

// Safe - Alternative 1: Adding Additional Type-Check
function withCheck(stdClass $x = null) {
    if ($x instanceof stdClass) {
        notNullable($x);
    }
}

// Safe - Alternative 2: Changing Parameter
function withNonNullableParam(stdClass $x) {
    notNullable($x);
}
Loading history...
193
                $request,
0 ignored issues
show
Bug introduced by
It seems like $request defined by parameter $request on line 183 can be null; however, EE_Session::__construct() does not accept null, maybe add an additional type check?

It seems like you allow that null is being passed for a parameter, however the function which is called does not seem to accept null.

We recommend to add an additional type check (or disallow null for the parameter):

function notNullable(stdClass $x) { }

// Unsafe
function withoutCheck(stdClass $x = null) {
    notNullable($x);
}

// Safe - Alternative 1: Adding Additional Type-Check
function withCheck(stdClass $x = null) {
    if ($x instanceof stdClass) {
        notNullable($x);
    }
}

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