Completed
Push — 4 ( 5fbfd8...bd8494 )
by Ingo
09:20
created

Session::start()   F

Complexity

Conditions 16
Paths 793

Size

Total Lines 60
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 16
eloc 33
nc 793
nop 1
dl 0
loc 60
rs 3.3333
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
 * The static methods are used to manipulate the currently active controller's session.
13
 * The instance methods are used to manipulate a particular session.  There can be more than one of these created.
14
 *
15
 * In order to support things like testing, the session is associated with a particular Controller.  In normal usage,
16
 * this is loaded from and saved to the regular PHP session, but for things like static-page-generation and
17
 * unit-testing, you can create multiple Controllers, each with their own session.
18
 *
19
 * The instance object is basically just a way of manipulating a set of nested maps, and isn't specific to session
20
 * data.
21
 *
22
 * <b>Saving Data</b>
23
 *
24
 * You can write a value to a users session from your PHP code using the static function {@link Session::set()}. You
25
 * can add this line in any function or file you wish to save the value.
26
 *
27
 * <code>
28
 *  Session::set('MyValue', 6);
29
 * </code>
30
 *
31
 * Saves the value of "6" to the MyValue session data. You can also save arrays or serialized objects in session (but
32
 * note there may be size restrictions as to how much you can save)
33
 *
34
 * <code>
35
 *  // save a variable
36
 *  $var = 1;
37
 *  Session::set('MyVar', $var);
38
 *
39
 *  // saves an array
40
 *  Session::set('MyArrayOfValues', array('1', '2', '3'));
41
 *
42
 *  // saves an object (you'll have to unserialize it back)
43
 *  $object = new Object();
44
 *
45
 *  Session::set('MyObject', serialize($object));
46
 * </code>
47
 *
48
 * <b>Accessing Data</b>
49
 *
50
 * Once you have saved a value to the Session you can access it by using the {@link Session::get()} function.
51
 * Like the {@link Session::set()} function you can use this anywhere in your PHP files.
52
 *
53
 * The values in the comments are the values stored from the previous example.
54
 *
55
 * <code>
56
 * public function bar() {
57
 *  $value = Session::get('MyValue'); // $value = 6
58
 *  $var   = Session::get('MyVar'); // $var = 1
59
 *  $array = Session::get('MyArrayOfValues'); // $array = array(1,2,3)
60
 *  $object = Session::get('MyObject', unserialize($object)); // $object = Object()
61
 * }
62
 * </code>
63
 *
64
 * You can also get all the values in the session at once. This is useful for debugging.
65
 *
66
 * <code>
67
 * Session::get_all(); // returns an array of all the session values.
68
 * </code>
69
 *
70
 * <b>Clearing Data</b>
71
 *
72
 * Once you have accessed a value from the Session it doesn't automatically wipe the value from the Session, you have
73
 * to specifically remove it. To clear a value you can either delete 1 session value by the name that you saved it
74
 *
75
 * <code>
76
 * Session::clear('MyValue'); // MyValue is no longer 6.
77
 * </code>
78
 *
79
 * Or you can clear every single value in the session at once. Note SilverStripe stores some of its own session data
80
 * including form and page comment information. None of this is vital but clear_all will clear everything.
81
 *
82
 * <code>
83
 *  Session::clear_all();
84
 * </code>
85
 *
86
 * @see Cookie
87
 * @todo This class is currently really basic and could do with a more well-thought-out implementation.
88
 */
89
class Session
90
{
91
    use Configurable;
92
93
    /**
94
     * Set session timeout in seconds.
95
     *
96
     * @var int
97
     * @config
98
     */
99
    private static $timeout = 0;
0 ignored issues
show
introduced by
The private property $timeout is not used, and could be removed.
Loading history...
100
101
    /**
102
     * @config
103
     * @var array
104
     */
105
    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...
106
107
    /**
108
     * @config
109
     * @var string
110
     */
111
    private static $cookie_domain;
0 ignored issues
show
introduced by
The private property $cookie_domain is not used, and could be removed.
Loading history...
112
113
    /**
114
     * @config
115
     * @var string
116
     */
117
    private static $cookie_path;
0 ignored issues
show
introduced by
The private property $cookie_path is not used, and could be removed.
Loading history...
118
119
    /**
120
     * @config
121
     * @var string
122
     */
123
    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...
124
125
    /**
126
     * @config
127
     * @var boolean
128
     */
129
    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...
130
131
    /**
132
     * Name of session cache limiter to use.
133
     * Defaults to '' to disable cache limiter entirely.
134
     *
135
     * @see https://secure.php.net/manual/en/function.session-cache-limiter.php
136
     * @var string|null
137
     */
138
    private static $sessionCacheLimiter = '';
0 ignored issues
show
introduced by
The private property $sessionCacheLimiter is not used, and could be removed.
Loading history...
139
140
    /**
141
     * Session data.
142
     * Will be null if session has not been started
143
     *
144
     * @var array|null
145
     */
146
    protected $data = null;
147
148
    /**
149
     * List of keys changed. This is a nested array which represents the
150
     * keys modified in $this->data. The value of each item is either "true"
151
     * or a nested array.
152
     *
153
     * If a value is in changedData but not in data, it must be removed
154
     * from the destination during save().
155
     *
156
     * Only highest level changes are stored. E.g. changes to `Base.Sub`
157
     * and then `Base` only records `Base` as the change.
158
     *
159
     * E.g.
160
     * [
161
     *   'Base' => true,
162
     *   'Key' => [
163
     *      'Nested' => true,
164
     *   ],
165
     * ]
166
     *
167
     * @var array
168
     */
169
    protected $changedData = array();
170
171
    /**
172
     * Get user agent for this request
173
     *
174
     * @param HTTPRequest $request
175
     * @return string
176
     */
177
    protected function userAgent(HTTPRequest $request)
178
    {
179
        return $request->getHeader('User-Agent');
180
    }
181
182
    /**
183
     * Start PHP session, then create a new Session object with the given start data.
184
     *
185
     * @param array|null|Session $data Can be an array of data (such as $_SESSION) or another Session object to clone.
186
     * If null, this session is treated as unstarted.
187
     */
188
    public function __construct($data)
189
    {
190
        if ($data instanceof Session) {
191
            $data = $data->getAll();
192
        }
193
194
        $this->data = $data;
195
    }
196
197
    /**
198
     * Init this session instance before usage
199
     *
200
     * @param HTTPRequest $request
201
     */
202
    public function init(HTTPRequest $request)
203
    {
204
        if (!$this->isStarted()) {
205
            $this->start($request);
206
        }
207
208
        // Funny business detected!
209
        if (isset($this->data['HTTP_USER_AGENT'])) {
210
            if ($this->data['HTTP_USER_AGENT'] !== $this->userAgent($request)) {
211
                $this->clearAll();
212
                $this->destroy();
213
                $this->start($request);
214
            }
215
        }
216
    }
217
218
    /**
219
     * Destroy existing session and restart
220
     *
221
     * @param HTTPRequest $request
222
     */
223
    public function restart(HTTPRequest $request)
224
    {
225
        $this->destroy();
226
        $this->init($request);
227
    }
228
229
    /**
230
     * Determine if this session has started
231
     *
232
     * @return bool
233
     */
234
    public function isStarted()
235
    {
236
        return isset($this->data);
237
    }
238
239
    /**
240
     * Begin session
241
     *
242
     * @param HTTPRequest $request The request for which to start a session
243
     */
244
    public function start(HTTPRequest $request)
245
    {
246
        if ($this->isStarted()) {
247
            throw new BadMethodCallException("Session has already started");
248
        }
249
250
        $path = $this->config()->get('cookie_path');
251
        if (!$path) {
252
            $path = Director::baseURL();
253
        }
254
        $domain = $this->config()->get('cookie_domain');
255
        $secure = Director::is_https($request) && $this->config()->get('cookie_secure');
256
        $session_path = $this->config()->get('session_store_path');
257
        $timeout = $this->config()->get('timeout');
258
259
        // Director::baseURL can return absolute domain names - this extracts the relevant parts
260
        // for the session otherwise we can get broken session cookies
261
        if (Director::is_absolute_url($path)) {
262
            $urlParts = parse_url($path);
263
            $path = $urlParts['path'];
264
            if (!$domain) {
265
                $domain = $urlParts['host'];
266
            }
267
        }
268
269
        if (!session_id() && !headers_sent()) {
270
            if ($domain) {
271
                session_set_cookie_params($timeout, $path, $domain, $secure, true);
272
            } else {
273
                session_set_cookie_params($timeout, $path, null, $secure, true);
274
            }
275
276
            // Allow storing the session in a non standard location
277
            if ($session_path) {
278
                session_save_path($session_path);
279
            }
280
281
            // If we want a secure cookie for HTTPS, use a seperate session name. This lets us have a
282
            // seperate (less secure) session for non-HTTPS requests
283
            if ($secure) {
284
                session_name('SECSESSID');
285
            }
286
287
            $limiter = $this->config()->get('sessionCacheLimiter');
288
            if (isset($limiter)) {
289
                session_cache_limiter($limiter);
290
            }
291
292
            session_start();
293
294
            $this->data = isset($_SESSION) ? $_SESSION : array();
295
        } else {
296
            $this->data = [];
297
        }
298
299
        // Modify the timeout behaviour so it's the *inactive* time before the session expires.
300
        // By default it's the total session lifetime
301
        if ($timeout && !headers_sent()) {
302
            Cookie::set(session_name(), session_id(), $timeout/86400, $path, $domain ? $domain
303
                : null, $secure, true);
304
        }
305
    }
306
307
    /**
308
     * Destroy this session
309
     *
310
     * @param bool $removeCookie
311
     */
312
    public function destroy($removeCookie = true)
313
    {
314
        if (session_id()) {
315
            if ($removeCookie) {
316
                $path = $this->config()->get('cookie_path') ?: Director::baseURL();
317
                $domain = $this->config()->get('cookie_domain');
318
                $secure = $this->config()->get('cookie_secure');
319
                Cookie::force_expiry(session_name(), $path, $domain, $secure, true);
320
            }
321
            session_destroy();
322
        }
323
        // Clean up the superglobal - session_destroy does not do it.
324
        // http://nz1.php.net/manual/en/function.session-destroy.php
325
        unset($_SESSION);
326
        $this->data = null;
327
    }
328
329
    /**
330
     * Set session value
331
     *
332
     * @param string $name
333
     * @param mixed $val
334
     * @return $this
335
     */
336
    public function set($name, $val)
337
    {
338
        if (!$this->isStarted()) {
339
            throw new BadMethodCallException("Session cannot be modified until it's started");
340
        }
341
        $var = &$this->nestedValueRef($name, $this->data);
342
343
        // Mark changed
344
        if ($var !== $val) {
345
            $var = $val;
346
            $this->markChanged($name);
347
        }
348
        return $this;
349
    }
350
351
    /**
352
     * Mark key as changed
353
     *
354
     * @internal
355
     * @param string $name
356
     */
357
    protected function markChanged($name)
358
    {
359
        $diffVar = &$this->changedData;
360
        foreach (explode('.', $name) as $namePart) {
361
            if (!isset($diffVar[$namePart])) {
362
                $diffVar[$namePart] = [];
363
            }
364
            $diffVar = &$diffVar[$namePart];
365
366
            // Already diffed
367
            if ($diffVar === true) {
368
                return;
369
            }
370
        }
371
        // Mark changed
372
        $diffVar = true;
373
    }
374
375
    /**
376
     * Merge value with array
377
     *
378
     * @param string $name
379
     * @param mixed $val
380
     */
381
    public function addToArray($name, $val)
382
    {
383
        if (!$this->isStarted()) {
384
            throw new BadMethodCallException("Session cannot be modified until it's started");
385
        }
386
387
        $names = explode('.', $name);
388
389
        // We still want to do this even if we have strict path checking for legacy code
390
        $var = &$this->data;
391
        $diffVar = &$this->changedData;
392
393
        foreach ($names as $n) {
394
            $var = &$var[$n];
395
            $diffVar = &$diffVar[$n];
396
        }
397
398
        $var[] = $val;
399
        $diffVar[sizeof($var)-1] = $val;
400
    }
401
402
    /**
403
     * Get session value
404
     *
405
     * @param string $name
406
     * @return mixed
407
     */
408
    public function get($name)
409
    {
410
        if (!$this->isStarted()) {
411
            throw new BadMethodCallException("Session cannot be accessed until it's started");
412
        }
413
        return $this->nestedValue($name, $this->data);
414
    }
415
416
    /**
417
     * Clear session value
418
     *
419
     * @param string $name
420
     * @return $this
421
     */
422
    public function clear($name)
423
    {
424
        if (!$this->isStarted()) {
425
            throw new BadMethodCallException("Session cannot be modified until it's started");
426
        }
427
428
        // Get var by path
429
        $var = $this->nestedValue($name, $this->data);
430
431
        // Unset var
432
        if ($var !== null) {
433
            // Unset parent key
434
            $parentParts = explode('.', $name);
435
            $basePart = array_pop($parentParts);
436
            if ($parentParts) {
437
                $parent = &$this->nestedValueRef(implode('.', $parentParts), $this->data);
438
                unset($parent[$basePart]);
439
            } else {
440
                unset($this->data[$name]);
441
            }
442
            $this->markChanged($name);
443
        }
444
        return $this;
445
    }
446
447
    /**
448
     * Clear all values
449
     */
450
    public function clearAll()
451
    {
452
        if (!$this->isStarted()) {
453
            throw new BadMethodCallException("Session cannot be modified until it's started");
454
        }
455
456
        if ($this->data && is_array($this->data)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->data 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...
457
            foreach (array_keys($this->data) as $key) {
458
                $this->clear($key);
459
            }
460
        }
461
    }
462
463
    /**
464
     * Get all values
465
     *
466
     * @return array|null
467
     */
468
    public function getAll()
469
    {
470
        return $this->data;
471
    }
472
473
    /**
474
     * Set user agent key
475
     *
476
     * @param HTTPRequest $request
477
     */
478
    public function finalize(HTTPRequest $request)
479
    {
480
        $this->set('HTTP_USER_AGENT', $this->userAgent($request));
481
    }
482
483
    /**
484
     * Save data to session
485
     * Only save the changes, so that anyone manipulating $_SESSION directly doesn't get burned.
486
     *
487
     * @param HTTPRequest $request
488
     */
489
    public function save(HTTPRequest $request)
490
    {
491
        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...
492
            $this->finalize($request);
493
494
            if (!$this->isStarted()) {
495
                $this->start($request);
496
            }
497
498
            // Apply all changes recursively
499
            $this->recursivelyApplyChanges($this->changedData, $this->data, $_SESSION);
500
        }
501
    }
502
503
    /**
504
     * Recursively apply the changes represented in $data to $dest.
505
     * Used to update $_SESSION
506
     *
507
     * @deprecated 4.1...5.0 Use recursivelyApplyChanges() instead
508
     * @param array $data
509
     * @param array $dest
510
     */
511
    protected function recursivelyApply($data, &$dest)
512
    {
513
        Deprecation::notice('5.0', 'Use recursivelyApplyChanges() instead');
514
        foreach ($data as $k => $v) {
515
            if (is_array($v)) {
516
                if (!isset($dest[$k]) || !is_array($dest[$k])) {
517
                    $dest[$k] = array();
518
                }
519
                $this->recursivelyApply($v, $dest[$k]);
0 ignored issues
show
Deprecated Code introduced by
The function SilverStripe\Control\Session::recursivelyApply() has been deprecated: 4.1...5.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

519
                /** @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...
520
            } else {
521
                $dest[$k] = $v;
522
            }
523
        }
524
    }
525
526
    /**
527
     * Returns the list of changed keys
528
     *
529
     * @return array
530
     */
531
    public function changedData()
532
    {
533
        return $this->changedData;
534
    }
535
536
    /**
537
     * Navigate to nested value in source array by name,
538
     * creating a null placeholder if it doesn't exist.
539
     *
540
     * @internal
541
     * @param string $name
542
     * @param array $source
543
     * @return mixed Reference to value in $source
544
     */
545
    protected function &nestedValueRef($name, &$source)
546
    {
547
        // Find var to change
548
        $var = &$source;
549
        foreach (explode('.', $name) as $namePart) {
550
            if (!isset($var)) {
551
                $var = [];
552
            }
553
            if (!isset($var[$namePart])) {
554
                $var[$namePart] = null;
555
            }
556
            $var = &$var[$namePart];
557
        }
558
        return $var;
559
    }
560
561
    /**
562
     * Navigate to nested value in source array by name,
563
     * returning null if it doesn't exist.
564
     *
565
     * @internal
566
     * @param string $name
567
     * @param array $source
568
     * @return mixed Value in array in $source
569
     */
570
    protected function nestedValue($name, $source)
571
    {
572
        // Find var to change
573
        $var = $source;
574
        foreach (explode('.', $name) as $namePart) {
575
            if (!isset($var[$namePart])) {
576
                return null;
577
            }
578
            $var = $var[$namePart];
579
        }
580
        return $var;
581
    }
582
583
    /**
584
     * Apply all changes using separate keys and data sources and a destination
585
     *
586
     * @internal
587
     * @param array $changes
588
     * @param array $source
589
     * @param array $destination
590
     */
591
    protected function recursivelyApplyChanges($changes, $source, &$destination)
592
    {
593
        foreach ($changes as $key => $changed) {
594
            if ($changed === true) {
595
                // Determine if replacement or removal
596
                if (array_key_exists($key, $source)) {
597
                    $destination[$key] = $source[$key];
598
                } else {
599
                    unset($destination[$key]);
600
                }
601
            } else {
602
                // Recursively apply
603
                $destVal = &$this->nestedValueRef($key, $destination);
604
                $sourceVal = $this->nestedValue($key, $source);
605
                $this->recursivelyApplyChanges($changed, $sourceVal, $destVal);
606
            }
607
        }
608
    }
609
}
610