Session::start()   F
last analyzed

Complexity

Conditions 19
Paths 421

Size

Total Lines 76
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 19
eloc 35
nc 421
nop 1
dl 0
loc 76
rs 1.154
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace SilverStripe\Control;
4
5
use BadMethodCallException;
6
use SilverStripe\Core\Config\Configurable;
7
use SilverStripe\Dev\Deprecation;
8
9
/**
10
 * Handles all manipulation of the session.
11
 *
12
 * An instance of a `Session` object can be retrieved via an `HTTPRequest` by calling the `getSession()` method.
13
 *
14
 * In order to support things like testing, the session is associated with a particular Controller.  In normal usage,
15
 * this is loaded from and saved to the regular PHP session, but for things like static-page-generation and
16
 * unit-testing, you can create multiple Controllers, each with their own session.
17
 *
18
 * <b>Saving Data</b>
19
 *
20
 * Once you've retrieved a session instance, you can write a value to a users session using the function
21
 * {@link Session::set()}.
22
 *
23
 * <code>
24
 *  $request->getSession()->set('MyValue', 6);
25
 * </code>
26
 *
27
 * Saves the value of "6" to the MyValue session data. You can also save arrays or serialized objects in session (but
28
 * note there may be size restrictions as to how much you can save)
29
 *
30
 * <code>
31
 *
32
 *  $session = $request->getSession();
33
 *
34
 *  // save a variable
35
 *  $var = 1;
36
 *  $session->set('MyVar', $var);
37
 *
38
 *  // saves an array
39
 *  $session->set('MyArrayOfValues', array('1', '2', '3'));
40
 *
41
 *  // saves an object (you'll have to unserialize it back)
42
 *  $object = new Object();
43
 *
44
 *  $session->set('MyObject', serialize($object));
45
 * </code>
46
 *
47
 * <b>Accessing Data</b>
48
 *
49
 * Once you have saved a value to the Session you can access it by using the {@link Session::get()} function.
50
 * Note that session data isn't persisted in PHP's own session store (via $_SESSION)
51
 * until {@link Session::save()} is called, which happens automatically at the end of a standard request
52
 * through {@link SilverStripe\Control\Middleware\SessionMiddleware}.
53
 *
54
 * The values in the comments are the values stored from the previous example.
55
 *
56
 * <code>
57
 * public function bar() {
58
 *  $session = $this->getRequest()->getSession();
59
 *  $value = $session->get('MyValue'); // $value = 6
60
 *  $var   = $session->get('MyVar'); // $var = 1
61
 *  $array = $session->get('MyArrayOfValues'); // $array = array(1,2,3)
62
 *  $object = $session->get('MyObject', unserialize($object)); // $object = Object()
63
 * }
64
 * </code>
65
 *
66
 * You can also get all the values in the session at once. This is useful for debugging.
67
 *
68
 * <code>
69
 * $session->getAll(); // returns an array of all the session values.
70
 * </code>
71
 *
72
 * <b>Clearing Data</b>
73
 *
74
 * Once you have accessed a value from the Session it doesn't automatically wipe the value from the Session, you have
75
 * to specifically remove it. To clear a value you can either delete 1 session value by the name that you saved it
76
 *
77
 * <code>
78
 * $session->clear('MyValue'); // MyValue is no longer 6.
79
 * </code>
80
 *
81
 * Or you can clear every single value in the session at once. Note SilverStripe stores some of its own session data
82
 * including form and page comment information. None of this is vital but `clearAll()` will clear everything.
83
 *
84
 * <code>
85
 *  $session->clearAll();
86
 * </code>
87
 *
88
 * @see Cookie
89
 * @see HTTPRequest
90
 */
91
class Session
92
{
93
    use Configurable;
94
95
    /**
96
     * Set session timeout in seconds.
97
     *
98
     * @var int
99
     * @config
100
     */
101
    private static $timeout = 0;
0 ignored issues
show
introduced by
The private property $timeout is not used, and could be removed.
Loading history...
102
103
    /**
104
     * @config
105
     * @var array
106
     */
107
    private static $session_ips = array();
0 ignored issues
show
introduced by
The private property $session_ips is not used, and could be removed.
Loading history...
108
109
    /**
110
     * @config
111
     * @var string
112
     */
113
    private static $cookie_domain;
0 ignored issues
show
introduced by
The private property $cookie_domain is not used, and could be removed.
Loading history...
114
115
    /**
116
     * @config
117
     * @var string
118
     */
119
    private static $cookie_path;
0 ignored issues
show
introduced by
The private property $cookie_path is not used, and could be removed.
Loading history...
120
121
    /**
122
     * @config
123
     * @var string
124
     */
125
    private static $session_store_path;
0 ignored issues
show
introduced by
The private property $session_store_path is not used, and could be removed.
Loading history...
126
127
    /**
128
     * @config
129
     * @var boolean
130
     */
131
    private static $cookie_secure = false;
0 ignored issues
show
introduced by
The private property $cookie_secure is not used, and could be removed.
Loading history...
132
133
    /**
134
     * @config
135
     * @var string
136
     */
137
    private static $cookie_name_secure = 'SECSESSID';
0 ignored issues
show
introduced by
The private property $cookie_name_secure is not used, and could be removed.
Loading history...
138
139
    /**
140
     * Name of session cache limiter to use.
141
     * Defaults to '' to disable cache limiter entirely.
142
     *
143
     * @see https://secure.php.net/manual/en/function.session-cache-limiter.php
144
     * @var string|null
145
     */
146
    private static $sessionCacheLimiter = '';
0 ignored issues
show
introduced by
The private property $sessionCacheLimiter is not used, and could be removed.
Loading history...
147
148
    /**
149
     * Invalidate the session if user agent header changes between request. Defaults to true. Disabling this checks is
150
     * not recommended.
151
     * @var bool
152
     * @config
153
     */
154
    private static $strict_user_agent_check = true;
0 ignored issues
show
introduced by
The private property $strict_user_agent_check is not used, and could be removed.
Loading history...
155
156
    /**
157
     * Session data.
158
     * Will be null if session has not been started
159
     *
160
     * @var array|null
161
     */
162
    protected $data = null;
163
164
    /**
165
     * @var bool
166
     */
167
    protected $started = false;
168
169
    /**
170
     * List of keys changed. This is a nested array which represents the
171
     * keys modified in $this->data. The value of each item is either "true"
172
     * or a nested array.
173
     *
174
     * If a value is in changedData but not in data, it must be removed
175
     * from the destination during save().
176
     *
177
     * Only highest level changes are stored. E.g. changes to `Base.Sub`
178
     * and then `Base` only records `Base` as the change.
179
     *
180
     * E.g.
181
     * [
182
     *   'Base' => true,
183
     *   'Key' => [
184
     *      'Nested' => true,
185
     *   ],
186
     * ]
187
     *
188
     * @var array
189
     */
190
    protected $changedData = array();
191
192
    /**
193
     * Get user agent for this request
194
     *
195
     * @param HTTPRequest $request
196
     * @return string
197
     */
198
    protected function userAgent(HTTPRequest $request)
199
    {
200
        return $request->getHeader('User-Agent');
201
    }
202
203
    /**
204
     * Start PHP session, then create a new Session object with the given start data.
205
     *
206
     * @param array|null|Session $data Can be an array of data (such as $_SESSION) or another Session object to clone.
207
     * If null, this session is treated as unstarted.
208
     */
209
    public function __construct($data)
210
    {
211
        if ($data instanceof Session) {
212
            $data = $data->getAll();
213
        }
214
215
        $this->data = $data;
216
        $this->started = isset($data);
217
    }
218
219
    /**
220
     * Init this session instance before usage,
221
     * if a session identifier is part of the passed in request.
222
     * Otherwise, a session might be started in {@link save()}
223
     * if session data needs to be written with a new session identifier.
224
     *
225
     * @param HTTPRequest $request
226
     */
227
    public function init(HTTPRequest $request)
228
    {
229
        if (!$this->isStarted() && $this->requestContainsSessionId($request)) {
230
            $this->start($request);
231
        }
232
233
        // Funny business detected!
234
        if (self::config()->get('strict_user_agent_check') && isset($this->data['HTTP_USER_AGENT'])) {
235
            if ($this->data['HTTP_USER_AGENT'] !== $this->userAgent($request)) {
236
                $this->clearAll();
237
                $this->restart($request);
238
            }
239
        }
240
    }
241
242
    /**
243
     * Destroy existing session and restart
244
     *
245
     * @param HTTPRequest $request
246
     */
247
    public function restart(HTTPRequest $request)
248
    {
249
        $this->destroy();
250
        $this->start($request);
251
    }
252
253
    /**
254
     * Determine if this session has started
255
     *
256
     * @return bool
257
     */
258
    public function isStarted()
259
    {
260
        return $this->started;
261
    }
262
263
    /**
264
     * @param HTTPRequest $request
265
     * @return bool
266
     */
267
    public function requestContainsSessionId(HTTPRequest $request)
268
    {
269
        $secure = Director::is_https($request) && $this->config()->get('cookie_secure');
270
        $name = $secure ? $this->config()->get('cookie_name_secure') : session_name();
271
        return (bool)Cookie::get($name);
272
    }
273
274
    /**
275
     * Begin session, regardless if a session identifier is present in the request,
276
     * or whether any session data needs to be written.
277
     * See {@link init()} if you want to "lazy start" a session.
278
     *
279
     * @param HTTPRequest $request The request for which to start a session
280
     */
281
    public function start(HTTPRequest $request)
282
    {
283
        if ($this->isStarted()) {
284
            throw new BadMethodCallException("Session has already started");
285
        }
286
287
        $path = $this->config()->get('cookie_path');
288
        if (!$path) {
289
            $path = Director::baseURL();
290
        }
291
        $domain = $this->config()->get('cookie_domain');
292
        $secure = Director::is_https($request) && $this->config()->get('cookie_secure');
293
        $session_path = $this->config()->get('session_store_path');
294
        $timeout = $this->config()->get('timeout');
295
296
        // Director::baseURL can return absolute domain names - this extracts the relevant parts
297
        // for the session otherwise we can get broken session cookies
298
        if (Director::is_absolute_url($path)) {
299
            $urlParts = parse_url($path);
300
            $path = $urlParts['path'];
301
            if (!$domain) {
302
                $domain = $urlParts['host'];
303
            }
304
        }
305
306
        // If the session cookie is already set, then the session can be read even if headers_sent() = true
307
        // This helps with edge-case such as debugging.
308
        $data = [];
309
        if (!session_id() && (!headers_sent() || $this->requestContainsSessionId($request))) {
310
            if (!headers_sent()) {
311
                session_set_cookie_params($timeout ?: 0, $path, $domain ?: null, $secure, true);
312
313
                $limiter = $this->config()->get('sessionCacheLimiter');
314
                if (isset($limiter)) {
315
                    session_cache_limiter($limiter);
316
                }
317
318
                // Allow storing the session in a non standard location
319
                if ($session_path) {
320
                    session_save_path($session_path);
321
                }
322
323
                // If we want a secure cookie for HTTPS, use a separate session name. This lets us have a
324
                // separate (less secure) session for non-HTTPS requests
325
                // if headers_sent() is true then it's best to throw the resulting error rather than risk
326
                // a security hole.
327
                if ($secure) {
328
                    session_name($this->config()->get('cookie_name_secure'));
329
                }
330
331
                session_start();
332
333
                // Session start emits a cookie, but only if there's no existing session. If there is a session timeout
334
                // tied to this request, make sure the session is held for the entire timeout by refreshing the cookie
335
                // age.
336
                if ($timeout && $this->requestContainsSessionId($request)) {
337
                    Cookie::set(session_name(), session_id(), $timeout / 86400, $path, $domain ?: null, $secure, true);
338
                }
339
            } else {
340
                // If headers are sent then we can't have a session_cache_limiter otherwise we'll get a warning
341
                session_cache_limiter(null);
342
            }
343
344
            if (isset($_SESSION)) {
345
                // Initialise data from session store if present
346
                $data = $_SESSION;
347
348
                // Merge in existing in-memory data, taking priority over session store data
349
                $this->recursivelyApply((array)$this->data, $data);
0 ignored issues
show
Deprecated Code introduced by
The function SilverStripe\Control\Session::recursivelyApply() has been deprecated: 4.1.0:5.0.0 Use recursivelyApplyChanges() instead ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

349
                /** @scrutinizer ignore-deprecated */ $this->recursivelyApply((array)$this->data, $data);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
350
            }
351
        }
352
353
        // Save any modified session data back to the session store if present, otherwise initialise it to an array.
354
        $this->data = $data;
355
356
        $this->started = true;
357
    }
358
359
    /**
360
     * Destroy this session
361
     *
362
     * @param bool $removeCookie
363
     */
364
    public function destroy($removeCookie = true)
365
    {
366
        if (session_id()) {
367
            if ($removeCookie) {
368
                $path = $this->config()->get('cookie_path') ?: Director::baseURL();
369
                $domain = $this->config()->get('cookie_domain');
370
                $secure = $this->config()->get('cookie_secure');
371
                Cookie::force_expiry(session_name(), $path, $domain, $secure, true);
372
            }
373
            session_destroy();
374
        }
375
        // Clean up the superglobal - session_destroy does not do it.
376
        // http://nz1.php.net/manual/en/function.session-destroy.php
377
        unset($_SESSION);
378
        $this->data = null;
379
        $this->started = false;
380
    }
381
382
    /**
383
     * Set session value
384
     *
385
     * @param string $name
386
     * @param mixed $val
387
     * @return $this
388
     */
389
    public function set($name, $val)
390
    {
391
        $var = &$this->nestedValueRef($name, $this->data);
392
393
        // Mark changed
394
        if ($var !== $val) {
395
            $var = $val;
396
            $this->markChanged($name);
397
        }
398
        return $this;
399
    }
400
401
    /**
402
     * Mark key as changed
403
     *
404
     * @internal
405
     * @param string $name
406
     */
407
    protected function markChanged($name)
408
    {
409
        $diffVar = &$this->changedData;
410
        foreach (explode('.', $name) as $namePart) {
411
            if (!isset($diffVar[$namePart])) {
412
                $diffVar[$namePart] = [];
413
            }
414
            $diffVar = &$diffVar[$namePart];
415
416
            // Already diffed
417
            if ($diffVar === true) {
418
                return;
419
            }
420
        }
421
        // Mark changed
422
        $diffVar = true;
423
    }
424
425
    /**
426
     * Merge value with array
427
     *
428
     * @param string $name
429
     * @param mixed $val
430
     */
431
    public function addToArray($name, $val)
432
    {
433
        $names = explode('.', $name);
434
435
        // We still want to do this even if we have strict path checking for legacy code
436
        $var = &$this->data;
437
        $diffVar = &$this->changedData;
438
439
        foreach ($names as $n) {
440
            $var = &$var[$n];
441
            $diffVar = &$diffVar[$n];
442
        }
443
444
        $var[] = $val;
445
        $diffVar[sizeof($var) - 1] = $val;
446
    }
447
448
    /**
449
     * Get session value
450
     *
451
     * @param string $name
452
     * @return mixed
453
     */
454
    public function get($name)
455
    {
456
        return $this->nestedValue($name, $this->data);
457
    }
458
459
    /**
460
     * Clear session value
461
     *
462
     * @param string $name
463
     * @return $this
464
     */
465
    public function clear($name)
466
    {
467
        // Get var by path
468
        $var = $this->nestedValue($name, $this->data);
469
470
        // Unset var
471
        if ($var !== null) {
472
            // Unset parent key
473
            $parentParts = explode('.', $name);
474
            $basePart = array_pop($parentParts);
475
            if ($parentParts) {
476
                $parent = &$this->nestedValueRef(implode('.', $parentParts), $this->data);
477
                unset($parent[$basePart]);
478
            } else {
479
                unset($this->data[$name]);
480
            }
481
            $this->markChanged($name);
482
        }
483
        return $this;
484
    }
485
486
    /**
487
     * Clear all values
488
     */
489
    public function clearAll()
490
    {
491
        if ($this->data && is_array($this->data)) {
492
            foreach (array_keys($this->data) as $key) {
493
                $this->clear($key);
494
            }
495
        }
496
    }
497
498
    /**
499
     * Get all values
500
     *
501
     * @return array|null
502
     */
503
    public function getAll()
504
    {
505
        return $this->data;
506
    }
507
508
    /**
509
     * Set user agent key
510
     *
511
     * @param HTTPRequest $request
512
     */
513
    public function finalize(HTTPRequest $request)
514
    {
515
        $this->set('HTTP_USER_AGENT', $this->userAgent($request));
516
    }
517
518
    /**
519
     * Save data to session
520
     * Only save the changes, so that anyone manipulating $_SESSION directly doesn't get burned.
521
     *
522
     * @param HTTPRequest $request
523
     */
524
    public function save(HTTPRequest $request)
525
    {
526
        if ($this->changedData) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->changedData of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
527
            $this->finalize($request);
528
529
            if (!$this->isStarted()) {
530
                $this->start($request);
531
            }
532
533
            // Apply all changes recursively, implicitly writing them to the actual PHP session store.
534
            $this->recursivelyApplyChanges($this->changedData, $this->data, $_SESSION);
535
        }
536
    }
537
538
    /**
539
     * Recursively apply the changes represented in $data to $dest.
540
     * Used to update $_SESSION
541
     *
542
     * @deprecated 4.1.0:5.0.0 Use recursivelyApplyChanges() instead
543
     * @param array $data
544
     * @param array $dest
545
     */
546
    protected function recursivelyApply($data, &$dest)
547
    {
548
        Deprecation::notice('5.0', 'Use recursivelyApplyChanges() instead');
549
        foreach ($data as $k => $v) {
550
            if (is_array($v)) {
551
                if (!isset($dest[$k]) || !is_array($dest[$k])) {
552
                    $dest[$k] = array();
553
                }
554
                $this->recursivelyApply($v, $dest[$k]);
0 ignored issues
show
Deprecated Code introduced by
The function SilverStripe\Control\Session::recursivelyApply() has been deprecated: 4.1.0:5.0.0 Use recursivelyApplyChanges() instead ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

554
                /** @scrutinizer ignore-deprecated */ $this->recursivelyApply($v, $dest[$k]);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
555
            } else {
556
                $dest[$k] = $v;
557
            }
558
        }
559
    }
560
561
    /**
562
     * Returns the list of changed keys
563
     *
564
     * @return array
565
     */
566
    public function changedData()
567
    {
568
        return $this->changedData;
569
    }
570
571
    /**
572
     * Navigate to nested value in source array by name,
573
     * creating a null placeholder if it doesn't exist.
574
     *
575
     * @internal
576
     * @param string $name
577
     * @param array $source
578
     * @return mixed Reference to value in $source
579
     */
580
    protected function &nestedValueRef($name, &$source)
581
    {
582
        // Find var to change
583
        $var = &$source;
584
        foreach (explode('.', $name) as $namePart) {
585
            if (!isset($var)) {
586
                $var = [];
587
            }
588
            if (!isset($var[$namePart])) {
589
                $var[$namePart] = null;
590
            }
591
            $var = &$var[$namePart];
592
        }
593
        return $var;
594
    }
595
596
    /**
597
     * Navigate to nested value in source array by name,
598
     * returning null if it doesn't exist.
599
     *
600
     * @internal
601
     * @param string $name
602
     * @param array $source
603
     * @return mixed Value in array in $source
604
     */
605
    protected function nestedValue($name, $source)
606
    {
607
        // Find var to change
608
        $var = $source;
609
        foreach (explode('.', $name) as $namePart) {
610
            if (!isset($var[$namePart])) {
611
                return null;
612
            }
613
            $var = $var[$namePart];
614
        }
615
        return $var;
616
    }
617
618
    /**
619
     * Apply all changes using separate keys and data sources and a destination
620
     *
621
     * @internal
622
     * @param array $changes
623
     * @param array $source
624
     * @param array $destination
625
     */
626
    protected function recursivelyApplyChanges($changes, $source, &$destination)
627
    {
628
        $source = $source ?: [];
629
        foreach ($changes as $key => $changed) {
630
            if ($changed === true) {
631
                // Determine if replacement or removal
632
                if (array_key_exists($key, $source)) {
633
                    $destination[$key] = $source[$key];
634
                } else {
635
                    unset($destination[$key]);
636
                }
637
            } else {
638
                // Recursively apply
639
                $destVal = &$this->nestedValueRef($key, $destination);
640
                $sourceVal = $this->nestedValue($key, $source);
641
                $this->recursivelyApplyChanges($changed, $sourceVal, $destVal);
642
            }
643
        }
644
    }
645
646
    /**
647
     * Regenerate session id
648
     *
649
     * @internal This is for internal use only. Isn't a part of public API.
650
     */
651
    public function regenerateSessionId()
652
    {
653
        if (!headers_sent()) {
654
            session_regenerate_id(true);
655
        }
656
    }
657
}
658