Completed
Push — director-middleware ( 408832...334a87 )
by Sam
08:23
created

Session::start()   D

Complexity

Conditions 15
Paths 409

Size

Total Lines 57
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 15
eloc 31
nc 409
nop 1
dl 0
loc 57
rs 4.7713
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
8
/**
9
 * Handles all manipulation of the session.
10
 *
11
 * The static methods are used to manipulate the currently active controller's session.
12
 * The instance methods are used to manipulate a particular session.  There can be more than one of these created.
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
 * The instance object is basically just a way of manipulating a set of nested maps, and isn't specific to session
19
 * data.
20
 *
21
 * <b>Saving Data</b>
22
 *
23
 * You can write a value to a users session from your PHP code using the static function {@link Session::set()}. You
24
 * can add this line in any function or file you wish to save the value.
25
 *
26
 * <code>
27
 *  Session::set('MyValue', 6);
28
 * </code>
29
 *
30
 * Saves the value of "6" to the MyValue session data. You can also save arrays or serialized objects in session (but
31
 * note there may be size restrictions as to how much you can save)
32
 *
33
 * <code>
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
 * Like the {@link Session::set()} function you can use this anywhere in your PHP files.
51
 *
52
 * The values in the comments are the values stored from the previous example.
53
 *
54
 * <code>
55
 * public function bar() {
56
 *  $value = Session::get('MyValue'); // $value = 6
57
 *  $var   = Session::get('MyVar'); // $var = 1
58
 *  $array = Session::get('MyArrayOfValues'); // $array = array(1,2,3)
59
 *  $object = Session::get('MyObject', unserialize($object)); // $object = Object()
60
 * }
61
 * </code>
62
 *
63
 * You can also get all the values in the session at once. This is useful for debugging.
64
 *
65
 * <code>
66
 * Session::get_all(); // returns an array of all the session values.
67
 * </code>
68
 *
69
 * <b>Clearing Data</b>
70
 *
71
 * Once you have accessed a value from the Session it doesn't automatically wipe the value from the Session, you have
72
 * to specifically remove it. To clear a value you can either delete 1 session value by the name that you saved it
73
 *
74
 * <code>
75
 * Session::clear('MyValue'); // MyValue is no longer 6.
76
 * </code>
77
 *
78
 * Or you can clear every single value in the session at once. Note SilverStripe stores some of its own session data
79
 * including form and page comment information. None of this is vital but clear_all will clear everything.
80
 *
81
 * <code>
82
 *  Session::clear_all();
83
 * </code>
84
 *
85
 * @see Cookie
86
 * @todo This class is currently really basic and could do with a more well-thought-out implementation.
87
 */
88
class Session
89
{
90
    use Configurable;
91
92
    /**
93
     * Set session timeout in seconds.
94
     *
95
     * @var int
96
     * @config
97
     */
98
    private static $timeout = 0;
99
100
    /**
101
     * @config
102
     * @var array
103
     */
104
    private static $session_ips = array();
105
106
    /**
107
     * @config
108
     * @var string
109
     */
110
    private static $cookie_domain;
111
112
    /**
113
     * @config
114
     * @var string
115
     */
116
    private static $cookie_path;
117
118
    /**
119
     * @config
120
     * @var string
121
     */
122
    private static $session_store_path;
123
124
    /**
125
     * @config
126
     * @var boolean
127
     */
128
    private static $cookie_secure = false;
129
130
    /**
131
     * Session data.
132
     * Will be null if session has not been started
133
     *
134
     * @var array|null
135
     */
136
    protected $data = null;
137
138
    /**
139
     * @var array
140
     */
141
    protected $changedData = array();
142
143
    /**
144
     * Get user agent for this request
145
     *
146
     * @return string
147
     */
148
    protected function userAgent()
149
    {
150
        if (isset($_SERVER['HTTP_USER_AGENT'])) {
151
            return $_SERVER['HTTP_USER_AGENT'];
152
        } else {
153
            return '';
154
        }
155
    }
156
157
    /**
158
     * Start PHP session, then create a new Session object with the given start data.
159
     *
160
     * @param array|null|Session $data Can be an array of data (such as $_SESSION) or another Session object to clone.
161
     * If null, this session is treated as unstarted.
162
     */
163
    public function __construct($data)
164
    {
165
        if ($data instanceof Session) {
166
            $data = $data->getAll();
167
        }
168
169
        $this->data = $data;
170
    }
171
172
    /**
173
     * Init this session instance before usage
174
     */
175
    public function init(HTTPRequest $request)
176
    {
177
        if (!$this->isStarted()) {
178
            $this->start($request);
179
        }
180
181
        // Funny business detected!
182
        if (isset($this->data['HTTP_USER_AGENT'])) {
183
            if ($this->data['HTTP_USER_AGENT'] !== $this->userAgent()) {
184
                $this->clearAll();
185
                $this->destroy();
186
                $this->start($request);
187
            }
188
        }
189
    }
190
191
    /**
192
     * Destroy existing session and restart
193
     */
194
    public function restart(HTTPRequest $request)
195
    {
196
        $this->destroy();
197
        $this->init($request);
198
    }
199
200
    /**
201
     * Determine if this session has started
202
     *
203
     * @return bool
204
     */
205
    public function isStarted()
206
    {
207
        return isset($this->data);
208
    }
209
210
    /**
211
     * Begin session
212
     *
213
     * @param $request The request for which to start a session
214
     */
215
    public function start(HTTPRequest $request)
216
    {
217
        if ($this->isStarted()) {
218
            throw new BadMethodCallException("Session has already started");
219
        }
220
221
        $path = $this->config()->get('cookie_path');
222
        if (!$path) {
223
            $path = Director::baseURL();
224
        }
225
        $domain = $this->config()->get('cookie_domain');
226
        $secure = Director::is_https($request) && $this->config()->get('cookie_secure');
227
        $session_path = $this->config()->get('session_store_path');
228
        $timeout = $this->config()->get('timeout');
229
230
        // Director::baseURL can return absolute domain names - this extracts the relevant parts
231
        // for the session otherwise we can get broken session cookies
232
        if (Director::is_absolute_url($path)) {
233
            $urlParts = parse_url($path);
234
            $path = $urlParts['path'];
235
            if (!$domain) {
236
                $domain = $urlParts['host'];
237
            }
238
        }
239
240
        if (!session_id() && !headers_sent()) {
241
            if ($domain) {
242
                session_set_cookie_params($timeout, $path, $domain, $secure, true);
243
            } else {
244
                session_set_cookie_params($timeout, $path, null, $secure, true);
245
            }
246
247
            // Allow storing the session in a non standard location
248
            if ($session_path) {
249
                session_save_path($session_path);
250
            }
251
252
            // If we want a secure cookie for HTTPS, use a seperate session name. This lets us have a
253
            // seperate (less secure) session for non-HTTPS requests
254
            if ($secure) {
255
                session_name('SECSESSID');
256
            }
257
258
            session_start();
259
260
            $this->data = isset($_SESSION) ? $_SESSION : array();
261
        } else {
262
            $this->data = [];
263
        }
264
265
        // Modify the timeout behaviour so it's the *inactive* time before the session expires.
266
        // By default it's the total session lifetime
267
        if ($timeout && !headers_sent()) {
268
            Cookie::set(session_name(), session_id(), $timeout/86400, $path, $domain ? $domain
269
                : null, $secure, true);
270
        }
271
    }
272
273
    /**
274
     * Destroy this session
275
     *
276
     * @param bool $removeCookie
277
     */
278
    public function destroy($removeCookie = true)
279
    {
280
        if (session_id()) {
281
            if ($removeCookie) {
282
                $path = $this->config()->get('cookie_path') ?: Director::baseURL();
283
                $domain = $this->config()->get('cookie_domain');
284
                $secure = $this->config()->get('cookie_secure');
285
                Cookie::force_expiry(session_name(), $path, $domain, $secure, true);
286
            }
287
            session_destroy();
288
        }
289
        // Clean up the superglobal - session_destroy does not do it.
290
        // http://nz1.php.net/manual/en/function.session-destroy.php
291
        unset($_SESSION);
292
        $this->data = null;
293
    }
294
295
    /**
296
     * Set session value
297
     *
298
     * @param string $name
299
     * @param mixed $val
300
     * @return $this
301
     */
302
    public function set($name, $val)
303
    {
304
        if (!$this->isStarted()) {
305
            throw new BadMethodCallException("Session cannot be modified until it's started");
306
        }
307
308
        // Quicker execution path for "."-free names
309
        if (strpos($name, '.') === false) {
310
            $this->data[$name] = $val;
311
            $this->changedData[$name] = $val;
312
        } else {
313
            $names = explode('.', $name);
314
315
            // We still want to do this even if we have strict path checking for legacy code
316
            $var = &$this->data;
317
            $diffVar = &$this->changedData;
318
319
            // Iterate twice over the names - once to see if the value needs to be changed,
320
            // and secondly to get the changed data value. This is done to solve a problem
321
            // where iterating over the diff var would create empty arrays, and the value
322
            // would then not be set, inadvertently clearing session values.
323
            foreach ($names as $n) {
324
                $var = &$var[$n];
325
            }
326
327
            if ($var !== $val) {
328
                foreach ($names as $n) {
329
                    $diffVar = &$diffVar[$n];
330
                }
331
332
                $var = $val;
333
                $diffVar = $val;
334
            }
335
        }
336
        return $this;
337
    }
338
339
    /**
340
     * Merge value with array
341
     *
342
     * @param string $name
343
     * @param mixed $val
344
     */
345
    public function addToArray($name, $val)
346
    {
347
        if (!$this->isStarted()) {
348
            throw new BadMethodCallException("Session cannot be modified until it's started");
349
        }
350
351
        $names = explode('.', $name);
352
353
        // We still want to do this even if we have strict path checking for legacy code
354
        $var = &$this->data;
355
        $diffVar = &$this->changedData;
356
357
        foreach ($names as $n) {
358
            $var = &$var[$n];
359
            $diffVar = &$diffVar[$n];
360
        }
361
362
        $var[] = $val;
363
        $diffVar[sizeof($var)-1] = $val;
364
    }
365
366
    /**
367
     * Get session value
368
     *
369
     * @param string $name
370
     * @return mixed
371
     */
372
    public function get($name)
373
    {
374
        if (!$this->isStarted()) {
375
            throw new BadMethodCallException("Session cannot be accessed until it's started");
376
        }
377
378
        // Quicker execution path for "."-free names
379
        if (strpos($name, '.') === false) {
380
            if (isset($this->data[$name])) {
381
                return $this->data[$name];
382
            }
383
            return null;
384
        } else {
385
            $names = explode('.', $name);
386
387
            if (!isset($this->data)) {
388
                return null;
389
            }
390
391
            $var = $this->data;
392
393 View Code Duplication
            foreach ($names as $n) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
394
                if (!isset($var[$n])) {
395
                    return null;
396
                }
397
                $var = $var[$n];
398
            }
399
400
            return $var;
401
        }
402
    }
403
404
    /**
405
     * Clear session value
406
     *
407
     * @param string $name
408
     * @return $this
409
     */
410
    public function clear($name)
411
    {
412
        if (!$this->isStarted()) {
413
            throw new BadMethodCallException("Session cannot be modified until it's started");
414
        }
415
416
        $names = explode('.', $name);
417
418
        // We still want to do this even if we have strict path checking for legacy code
419
        $var = &$this->data;
420
        $diffVar = &$this->changedData;
421
422 View Code Duplication
        foreach ($names as $n) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
423
            // don't clear a record that doesn't exist
424
            if (!isset($var[$n])) {
425
                return $this;
426
            }
427
            $var = &$var[$n];
428
        }
429
430
        // only loop to find data within diffVar if var is proven to exist in the above loop
431
        foreach ($names as $n) {
432
            $diffVar = &$diffVar[$n];
433
        }
434
435
        if ($var !== null) {
436
            $var = null;
437
            $diffVar = null;
438
        }
439
        return $this;
440
    }
441
442
    /**
443
     * Clear all values
444
     */
445
    public function clearAll()
446
    {
447
        if (!$this->isStarted()) {
448
            throw new BadMethodCallException("Session cannot be modified until it's started");
449
        }
450
451
        if ($this->data && is_array($this->data)) {
452
            foreach (array_keys($this->data) as $key) {
453
                $this->clear($key);
454
            }
455
        }
456
    }
457
458
    /**
459
     * Get all values
460
     *
461
     * @return array|null
462
     */
463
    public function getAll()
464
    {
465
        return $this->data;
466
    }
467
468
    /**
469
     * Set user agent key
470
     */
471
    public function finalize()
472
    {
473
        $this->set('HTTP_USER_AGENT', $this->userAgent());
474
    }
475
476
    /**
477
     * Save data to session
478
     * Only save the changes, so that anyone manipulating $_SESSION directly doesn't get burned.
479
     */
480
    public function save(HTTPRequest $request)
481
    {
482
        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...
483
            $this->finalize();
484
485
            if (!$this->isStarted()) {
486
                $this->start($request);
487
            }
488
489
            $this->recursivelyApply($this->changedData, $_SESSION);
490
        }
491
    }
492
493
    /**
494
     * Recursively apply the changes represented in $data to $dest.
495
     * Used to update $_SESSION
496
     *
497
     * @param array $data
498
     * @param array $dest
499
     */
500
    protected function recursivelyApply($data, &$dest)
501
    {
502
        foreach ($data as $k => $v) {
503
            if (is_array($v)) {
504
                if (!isset($dest[$k]) || !is_array($dest[$k])) {
505
                    $dest[$k] = array();
506
                }
507
                $this->recursivelyApply($v, $dest[$k]);
508
            } else {
509
                $dest[$k] = $v;
510
            }
511
        }
512
    }
513
514
    /**
515
     * Return the changed data, for debugging purposes.
516
     *
517
     * @return array
518
     */
519
    public function changedData()
520
    {
521
        return $this->changedData;
522
    }
523
}
524