Completed
Branch BUG/pantheon-session-fatal-2 (2f2c99)
by
unknown
19:07 queued 09:56
created

EE_Session   F

Complexity

Total Complexity 153

Size/Duplication

Total Lines 1278
Duplicated Lines 3.52 %

Coupling/Cohesion

Components 1
Dependencies 12

Importance

Changes 0
Metric Value
wmc 153
lcom 1
cbo 12
dl 45
loc 1278
rs 0.8
c 0
b 0
f 0

43 Methods

Rating   Name   Duplication   Size   Complexity  
A cart() 0 6 3
A reset_cart() 0 5 1
A instance() 0 21 3
B __construct() 0 49 6
A isLoadedAndActive() 0 6 3
A isActive() 0 4 1
A open_session() 0 8 2
A expired() 0 4 1
A reset_expired() 0 4 1
A expiration() 0 4 1
A extension() 0 4 1
A extend_expiration() 0 5 2
A lifespan() 0 4 1
A _set_defaults() 0 11 3
A id() 0 4 1
A set_cart() 0 5 1
A set_checkout() 0 5 1
A reset_checkout() 0 5 1
A checkout() 0 6 3
A set_transaction() 0 7 1
A reset_transaction() 0 5 1
A transaction() 0 7 3
A get_session_data() 0 12 4
B _espresso_session() 0 45 7
F _retrieve_session_data() 10 94 17
A _generate_session_id() 0 10 2
A _get_sid_salt() 0 18 4
A _set_init_access_and_expiration() 0 9 1
F update() 0 68 18
A _create_espresso_session() 0 6 2
A set_session_data() 13 35 5
A sessionHasStuffWorthSaving() 0 12 5
B _save_session_to_db() 0 41 9
B _get_page_visit() 0 29 6
A _wp_user_id() 0 6 1
A clear_session() 0 17 1
C reset_data() 13 76 9
A wp_loaded() 0 6 2
A reset_instance() 0 5 1
A configure_garbage_collection_filters() 0 23 3
B find_serialize_error() 9 56 7
A updateSessionSettings() 0 6 1
B garbageCollection() 0 44 7

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like EE_Session often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use EE_Session, and based on these observations, apply Extract Interface, too.

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