Completed
Push — resource-url-generator ( 23f763...cfa198 )
by Sam
18:27 queued 08:34
created

Session::userAgent()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 4
rs 10
c 0
b 0
f 0
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
     * @param HTTPRequest $request
147
     * @return string
148
     */
149
    protected function userAgent(HTTPRequest $request)
150
    {
151
        return $request->getHeader('User-Agent');
152
    }
153
154
    /**
155
     * Start PHP session, then create a new Session object with the given start data.
156
     *
157
     * @param array|null|Session $data Can be an array of data (such as $_SESSION) or another Session object to clone.
158
     * If null, this session is treated as unstarted.
159
     */
160
    public function __construct($data)
161
    {
162
        if ($data instanceof Session) {
163
            $data = $data->getAll();
164
        }
165
166
        $this->data = $data;
167
    }
168
169
    /**
170
     * Init this session instance before usage
171
     *
172
     * @param HTTPRequest $request
173
     */
174
    public function init(HTTPRequest $request)
175
    {
176
        if (!$this->isStarted()) {
177
            $this->start($request);
178
        }
179
180
        // Funny business detected!
181
        if (isset($this->data['HTTP_USER_AGENT'])) {
182
            if ($this->data['HTTP_USER_AGENT'] !== $this->userAgent($request)) {
183
                $this->clearAll();
184
                $this->destroy();
185
                $this->start($request);
186
            }
187
        }
188
    }
189
190
    /**
191
     * Destroy existing session and restart
192
     *
193
     * @param HTTPRequest $request
194
     */
195
    public function restart(HTTPRequest $request)
196
    {
197
        $this->destroy();
198
        $this->init($request);
199
    }
200
201
    /**
202
     * Determine if this session has started
203
     *
204
     * @return bool
205
     */
206
    public function isStarted()
207
    {
208
        return isset($this->data);
209
    }
210
211
    /**
212
     * Begin session
213
     *
214
     * @param HTTPRequest $request The request for which to start a session
215
     */
216
    public function start(HTTPRequest $request)
217
    {
218
        if ($this->isStarted()) {
219
            throw new BadMethodCallException("Session has already started");
220
        }
221
222
        $path = $this->config()->get('cookie_path');
223
        if (!$path) {
224
            $path = Director::baseURL();
225
        }
226
        $domain = $this->config()->get('cookie_domain');
227
        $secure = Director::is_https($request) && $this->config()->get('cookie_secure');
228
        $session_path = $this->config()->get('session_store_path');
229
        $timeout = $this->config()->get('timeout');
230
231
        // Director::baseURL can return absolute domain names - this extracts the relevant parts
232
        // for the session otherwise we can get broken session cookies
233
        if (Director::is_absolute_url($path)) {
234
            $urlParts = parse_url($path);
235
            $path = $urlParts['path'];
236
            if (!$domain) {
237
                $domain = $urlParts['host'];
238
            }
239
        }
240
241
        if (!session_id() && !headers_sent()) {
242
            if ($domain) {
243
                session_set_cookie_params($timeout, $path, $domain, $secure, true);
244
            } else {
245
                session_set_cookie_params($timeout, $path, null, $secure, true);
246
            }
247
248
            // Allow storing the session in a non standard location
249
            if ($session_path) {
250
                session_save_path($session_path);
251
            }
252
253
            // If we want a secure cookie for HTTPS, use a seperate session name. This lets us have a
254
            // seperate (less secure) session for non-HTTPS requests
255
            if ($secure) {
256
                session_name('SECSESSID');
257
            }
258
259
            session_start();
260
261
            $this->data = isset($_SESSION) ? $_SESSION : array();
262
        } else {
263
            $this->data = [];
264
        }
265
266
        // Modify the timeout behaviour so it's the *inactive* time before the session expires.
267
        // By default it's the total session lifetime
268
        if ($timeout && !headers_sent()) {
269
            Cookie::set(session_name(), session_id(), $timeout/86400, $path, $domain ? $domain
270
                : null, $secure, true);
271
        }
272
    }
273
274
    /**
275
     * Destroy this session
276
     *
277
     * @param bool $removeCookie
278
     */
279
    public function destroy($removeCookie = true)
280
    {
281
        if (session_id()) {
282
            if ($removeCookie) {
283
                $path = $this->config()->get('cookie_path') ?: Director::baseURL();
284
                $domain = $this->config()->get('cookie_domain');
285
                $secure = $this->config()->get('cookie_secure');
286
                Cookie::force_expiry(session_name(), $path, $domain, $secure, true);
287
            }
288
            session_destroy();
289
        }
290
        // Clean up the superglobal - session_destroy does not do it.
291
        // http://nz1.php.net/manual/en/function.session-destroy.php
292
        unset($_SESSION);
293
        $this->data = null;
294
    }
295
296
    /**
297
     * Set session value
298
     *
299
     * @param string $name
300
     * @param mixed $val
301
     * @return $this
302
     */
303
    public function set($name, $val)
304
    {
305
        if (!$this->isStarted()) {
306
            throw new BadMethodCallException("Session cannot be modified until it's started");
307
        }
308
309
        // Quicker execution path for "."-free names
310
        if (strpos($name, '.') === false) {
311
            $this->data[$name] = $val;
312
            $this->changedData[$name] = $val;
313
        } else {
314
            $names = explode('.', $name);
315
316
            // We still want to do this even if we have strict path checking for legacy code
317
            $var = &$this->data;
318
            $diffVar = &$this->changedData;
319
320
            // Iterate twice over the names - once to see if the value needs to be changed,
321
            // and secondly to get the changed data value. This is done to solve a problem
322
            // where iterating over the diff var would create empty arrays, and the value
323
            // would then not be set, inadvertently clearing session values.
324
            foreach ($names as $n) {
325
                $var = &$var[$n];
326
            }
327
328
            if ($var !== $val) {
329
                foreach ($names as $n) {
330
                    $diffVar = &$diffVar[$n];
331
                }
332
333
                $var = $val;
334
                $diffVar = $val;
335
            }
336
        }
337
        return $this;
338
    }
339
340
    /**
341
     * Merge value with array
342
     *
343
     * @param string $name
344
     * @param mixed $val
345
     */
346
    public function addToArray($name, $val)
347
    {
348
        if (!$this->isStarted()) {
349
            throw new BadMethodCallException("Session cannot be modified until it's started");
350
        }
351
352
        $names = explode('.', $name);
353
354
        // We still want to do this even if we have strict path checking for legacy code
355
        $var = &$this->data;
356
        $diffVar = &$this->changedData;
357
358
        foreach ($names as $n) {
359
            $var = &$var[$n];
360
            $diffVar = &$diffVar[$n];
361
        }
362
363
        $var[] = $val;
364
        $diffVar[sizeof($var)-1] = $val;
365
    }
366
367
    /**
368
     * Get session value
369
     *
370
     * @param string $name
371
     * @return mixed
372
     */
373
    public function get($name)
374
    {
375
        if (!$this->isStarted()) {
376
            throw new BadMethodCallException("Session cannot be accessed until it's started");
377
        }
378
379
        // Quicker execution path for "."-free names
380
        if (strpos($name, '.') === false) {
381
            if (isset($this->data[$name])) {
382
                return $this->data[$name];
383
            }
384
            return null;
385
        } else {
386
            $names = explode('.', $name);
387
388
            if (!isset($this->data)) {
389
                return null;
390
            }
391
392
            $var = $this->data;
393
394 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...
395
                if (!isset($var[$n])) {
396
                    return null;
397
                }
398
                $var = $var[$n];
399
            }
400
401
            return $var;
402
        }
403
    }
404
405
    /**
406
     * Clear session value
407
     *
408
     * @param string $name
409
     * @return $this
410
     */
411
    public function clear($name)
412
    {
413
        if (!$this->isStarted()) {
414
            throw new BadMethodCallException("Session cannot be modified until it's started");
415
        }
416
417
        $names = explode('.', $name);
418
419
        // We still want to do this even if we have strict path checking for legacy code
420
        $var = &$this->data;
421
        $diffVar = &$this->changedData;
422
423 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...
424
            // don't clear a record that doesn't exist
425
            if (!isset($var[$n])) {
426
                return $this;
427
            }
428
            $var = &$var[$n];
429
        }
430
431
        // only loop to find data within diffVar if var is proven to exist in the above loop
432
        foreach ($names as $n) {
433
            $diffVar = &$diffVar[$n];
434
        }
435
436
        if ($var !== null) {
437
            $var = null;
438
            $diffVar = null;
439
        }
440
        return $this;
441
    }
442
443
    /**
444
     * Clear all values
445
     */
446
    public function clearAll()
447
    {
448
        if (!$this->isStarted()) {
449
            throw new BadMethodCallException("Session cannot be modified until it's started");
450
        }
451
452
        if ($this->data && is_array($this->data)) {
453
            foreach (array_keys($this->data) as $key) {
454
                $this->clear($key);
455
            }
456
        }
457
    }
458
459
    /**
460
     * Get all values
461
     *
462
     * @return array|null
463
     */
464
    public function getAll()
465
    {
466
        return $this->data;
467
    }
468
469
    /**
470
     * Set user agent key
471
     *
472
     * @param HTTPRequest $request
473
     */
474
    public function finalize(HTTPRequest $request)
475
    {
476
        $this->set('HTTP_USER_AGENT', $this->userAgent($request));
477
    }
478
479
    /**
480
     * Save data to session
481
     * Only save the changes, so that anyone manipulating $_SESSION directly doesn't get burned.
482
     *
483
     * @param HTTPRequest $request
484
     */
485
    public function save(HTTPRequest $request)
486
    {
487
        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...
488
            $this->finalize($request);
489
490
            if (!$this->isStarted()) {
491
                $this->start($request);
492
            }
493
494
            $this->recursivelyApply($this->changedData, $_SESSION);
495
        }
496
    }
497
498
    /**
499
     * Recursively apply the changes represented in $data to $dest.
500
     * Used to update $_SESSION
501
     *
502
     * @param array $data
503
     * @param array $dest
504
     */
505
    protected function recursivelyApply($data, &$dest)
506
    {
507
        foreach ($data as $k => $v) {
508
            if (is_array($v)) {
509
                if (!isset($dest[$k]) || !is_array($dest[$k])) {
510
                    $dest[$k] = array();
511
                }
512
                $this->recursivelyApply($v, $dest[$k]);
513
            } else {
514
                $dest[$k] = $v;
515
            }
516
        }
517
    }
518
519
    /**
520
     * Return the changed data, for debugging purposes.
521
     *
522
     * @return array
523
     */
524
    public function changedData()
525
    {
526
        return $this->changedData;
527
    }
528
}
529