Completed
Push — master ( a136b8...6cd9be )
by Ingo
07:21
created

Session::get_timeout()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 0
dl 0
loc 5
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\Control;
4
5
use SilverStripe\Core\Config\Config;
6
use SilverStripe\Core\Injector\Injectable;
7
use SilverStripe\Core\Injector\Injector;
8
use SilverStripe\Dev\Deprecation;
9
10
/**
11
 * Handles all manipulation of the session.
12
 *
13
 * The static methods are used to manipulate the currently active controller's session.
14
 * The instance methods are used to manipulate a particular session.  There can be more than one of these created.
15
 *
16
 * In order to support things like testing, the session is associated with a particular Controller.  In normal usage,
17
 * this is loaded from and saved to the regular PHP session, but for things like static-page-generation and
18
 * unit-testing, you can create multiple Controllers, each with their own session.
19
 *
20
 * The instance object is basically just a way of manipulating a set of nested maps, and isn't specific to session
21
 * data.
22
 *
23
 * <b>Saving Data</b>
24
 *
25
 * You can write a value to a users session from your PHP code using the static function {@link Session::set()}. You
26
 * can add this line in any function or file you wish to save the value.
27
 *
28
 * <code>
29
 *  Session::set('MyValue', 6);
30
 * </code>
31
 *
32
 * Saves the value of "6" to the MyValue session data. You can also save arrays or serialized objects in session (but
33
 * note there may be size restrictions as to how much you can save)
34
 *
35
 * <code>
36
 *  // save a variable
37
 *  $var = 1;
38
 *  Session::set('MyVar', $var);
39
 *
40
 *  // saves an array
41
 *  Session::set('MyArrayOfValues', array('1', '2', '3'));
42
 *
43
 *  // saves an object (you'll have to unserialize it back)
44
 *  $object = new Object();
45
 *
46
 *  Session::set('MyObject', serialize($object));
47
 * </code>
48
 *
49
 * <b>Accessing Data</b>
50
 *
51
 * Once you have saved a value to the Session you can access it by using the {@link Session::get()} function.
52
 * Like the {@link Session::set()} function you can use this anywhere in your PHP files.
53
 *
54
 * The values in the comments are the values stored from the previous example.
55
 *
56
 * <code>
57
 * public function bar() {
58
 *  $value = Session::get('MyValue'); // $value = 6
59
 *  $var   = Session::get('MyVar'); // $var = 1
60
 *  $array = Session::get('MyArrayOfValues'); // $array = array(1,2,3)
61
 *  $object = Session::get('MyObject', unserialize($object)); // $object = Object()
62
 * }
63
 * </code>
64
 *
65
 * You can also get all the values in the session at once. This is useful for debugging.
66
 *
67
 * <code>
68
 * Session::get_all(); // returns an array of all the session values.
69
 * </code>
70
 *
71
 * <b>Clearing Data</b>
72
 *
73
 * Once you have accessed a value from the Session it doesn't automatically wipe the value from the Session, you have
74
 * to specifically remove it. To clear a value you can either delete 1 session value by the name that you saved it
75
 *
76
 * <code>
77
 * Session::clear('MyValue'); // MyValue is no longer 6.
78
 * </code>
79
 *
80
 * Or you can clear every single value in the session at once. Note SilverStripe stores some of its own session data
81
 * including form and page comment information. None of this is vital but clear_all will clear everything.
82
 *
83
 * <code>
84
 *  Session::clear_all();
85
 * </code>
86
 *
87
 * @see Cookie
88
 * @todo This class is currently really basic and could do with a more well-thought-out implementation.
89
 */
90
class Session
91
{
92
    use Injectable;
93
94
    /**
95
     * Set session timeout in seconds.
96
     *
97
     * @var int
98
     * @config
99
     */
100
    private static $timeout = 0;
101
102
    /**
103
     * @config
104
     * @var array
105
     */
106
    private static $session_ips = array();
107
108
    /**
109
     * @config
110
     * @var string
111
     */
112
    private static $cookie_domain;
113
114
    /**
115
     * @config
116
     * @var string
117
     */
118
    private static $cookie_path;
119
120
    /**
121
     * @config
122
     * @var string
123
     */
124
    private static $session_store_path;
125
126
    /**
127
     * @config
128
     * @var boolean
129
     */
130
    private static $cookie_secure = false;
131
132
    /**
133
     * Session data
134
     */
135
    protected $data = array();
136
137
    protected $changedData = array();
138
139
    protected function userAgent()
0 ignored issues
show
Coding Style introduced by
userAgent uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
140
    {
141
        if (isset($_SERVER['HTTP_USER_AGENT'])) {
142
            return $_SERVER['HTTP_USER_AGENT'];
143
        } else {
144
            return '';
145
        }
146
    }
147
148
    /**
149
     * Start PHP session, then create a new Session object with the given start data.
150
     *
151
     * @param $data array|Session Can be an array of data (such as $_SESSION) or another Session object to clone.
152
     */
153
    public function __construct($data)
154
    {
155
        if ($data instanceof Session) {
156
            $data = $data->inst_getAll();
157
        }
158
159
        $this->data = $data;
160
161
        if (isset($this->data['HTTP_USER_AGENT'])) {
162
            if ($this->data['HTTP_USER_AGENT'] != $this->userAgent()) {
163
                // Funny business detected!
164
                $this->inst_clearAll();
165
                $this->inst_destroy();
166
                $this->inst_start();
167
            }
168
        }
169
    }
170
171
    /**
172
     * Add a value to a specific key in the session array
173
     *
174
     * @param string $name
175
     * @param mixed $val
176
     */
177
    public static function add_to_array($name, $val)
178
    {
179
        return self::current_session()->inst_addToArray($name, $val);
180
    }
181
182
    /**
183
     * Set a key/value pair in the session
184
     *
185
     * @param string $name Key
186
     * @param string $val Value
187
     */
188
    public static function set($name, $val)
189
    {
190
        return self::current_session()->inst_set($name, $val);
191
    }
192
193
    /**
194
     * Return a specific value by session key
195
     *
196
     * @param string $name Key to lookup
197
     * @return mixed
198
     */
199
    public static function get($name)
200
    {
201
        return self::current_session()->inst_get($name);
202
    }
203
204
    /**
205
     * Return all the values in session
206
     *
207
     * @return array
208
     */
209
    public static function get_all()
210
    {
211
        return self::current_session()->inst_getAll();
212
    }
213
214
    /**
215
     * Clear a given session key, value pair.
216
     *
217
     * @param string $name Key to lookup
218
     */
219
    public static function clear($name)
220
    {
221
        return self::current_session()->inst_clear($name);
222
    }
223
224
    /**
225
     * Clear all the values
226
     *
227
     * @return void
228
     */
229
    public static function clear_all()
230
    {
231
        self::current_session()->inst_clearAll();
232
        self::$default_session = null;
233
    }
234
235
    /**
236
     * Save all the values in our session to $_SESSION
237
     */
238
    public static function save()
239
    {
240
        return self::current_session()->inst_save();
241
    }
242
243
    protected static $default_session = null;
244
245
    protected static function current_session()
0 ignored issues
show
Coding Style introduced by
current_session uses the super-global variable $_SESSION which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
246
    {
247
        if (Controller::has_curr()) {
248
            return Controller::curr()->getSession();
249
        } else {
250
            if (!self::$default_session) {
251
                self::$default_session = Injector::inst()->create('SilverStripe\\Control\\Session', isset($_SESSION) ? $_SESSION : array());
252
            }
253
254
            return self::$default_session;
255
        }
256
    }
257
258
    public function inst_start($sid = null)
0 ignored issues
show
Coding Style introduced by
inst_start uses the super-global variable $_SESSION which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
259
    {
260
        $path = Config::inst()->get('SilverStripe\\Control\\Session', 'cookie_path');
261
        if (!$path) {
262
            $path = Director::baseURL();
263
        }
264
        $domain = Config::inst()->get('SilverStripe\\Control\\Session', 'cookie_domain');
265
        $secure = Director::is_https() && Config::inst()->get('SilverStripe\\Control\\Session', 'cookie_secure');
266
        $session_path = Config::inst()->get('SilverStripe\\Control\\Session', 'session_store_path');
267
        $timeout = Config::inst()->get('SilverStripe\\Control\\Session', 'timeout');
268
269
        // Director::baseURL can return absolute domain names - this extracts the relevant parts
270
        // for the session otherwise we can get broken session cookies
271
        if (Director::is_absolute_url($path)) {
272
            $urlParts = parse_url($path);
273
            $path = $urlParts['path'];
274
            if (!$domain) {
275
                $domain = $urlParts['host'];
276
            }
277
        }
278
279
        if (!session_id() && !headers_sent()) {
280
            if ($domain) {
281
                session_set_cookie_params($timeout, $path, $domain, $secure, true);
282
            } else {
283
                session_set_cookie_params($timeout, $path, null, $secure, true);
284
            }
285
286
            // Allow storing the session in a non standard location
287
            if ($session_path) {
288
                session_save_path($session_path);
289
            }
290
291
            // If we want a secure cookie for HTTPS, use a seperate session name. This lets us have a
292
            // seperate (less secure) session for non-HTTPS requests
293
            if ($secure) {
294
                session_name('SECSESSID');
295
            }
296
297
            if ($sid) {
298
                session_id($sid);
299
            }
300
            session_start();
301
302
            $this->data = isset($_SESSION) ? $_SESSION : array();
303
        }
304
305
        // Modify the timeout behaviour so it's the *inactive* time before the session expires.
306
        // By default it's the total session lifetime
307
        if ($timeout && !headers_sent()) {
308
            Cookie::set(session_name(), session_id(), $timeout/86400, $path, $domain ? $domain
309
                : null, $secure, true);
310
        }
311
    }
312
313
    public function inst_destroy($removeCookie = true)
0 ignored issues
show
Coding Style introduced by
inst_destroy uses the super-global variable $_SESSION which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
314
    {
315
        if (session_id()) {
316
            if ($removeCookie) {
317
                $path = Config::inst()->get('SilverStripe\\Control\\Session', 'cookie_path') ?: Director::baseURL();
318
                $domain = Config::inst()->get('SilverStripe\\Control\\Session', 'cookie_domain');
319
                $secure = Config::inst()->get('SilverStripe\\Control\\Session', 'cookie_secure');
320
321
                Cookie::force_expiry(session_name(), $path, $domain, $secure, true);
322
            }
323
324
            session_destroy();
325
326
            // Clean up the superglobal - session_destroy does not do it.
327
            // http://nz1.php.net/manual/en/function.session-destroy.php
328
            unset($_SESSION);
329
            $this->data = array();
330
        }
331
    }
332
333
    public function inst_set($name, $val)
334
    {
335
        // Quicker execution path for "."-free names
336
        if (strpos($name, '.') === false) {
337
            $this->data[$name] = $val;
338
            $this->changedData[$name] = $val;
339
        } else {
340
            $names = explode('.', $name);
341
342
            // We still want to do this even if we have strict path checking for legacy code
343
            $var = &$this->data;
344
            $diffVar = &$this->changedData;
345
346
            // Iterate twice over the names - once to see if the value needs to be changed,
347
            // and secondly to get the changed data value. This is done to solve a problem
348
            // where iterating over the diff var would create empty arrays, and the value
349
            // would then not be set, inadvertently clearing session values.
350
            foreach ($names as $n) {
351
                $var = &$var[$n];
352
            }
353
354
            if ($var !== $val) {
355
                foreach ($names as $n) {
356
                    $diffVar = &$diffVar[$n];
357
                }
358
359
                $var = $val;
360
                $diffVar = $val;
361
            }
362
        }
363
    }
364
365
    public function inst_addToArray($name, $val)
366
    {
367
        $names = explode('.', $name);
368
369
        // We still want to do this even if we have strict path checking for legacy code
370
        $var = &$this->data;
371
        $diffVar = &$this->changedData;
372
373
        foreach ($names as $n) {
374
            $var = &$var[$n];
375
            $diffVar = &$diffVar[$n];
376
        }
377
378
        $var[] = $val;
379
        $diffVar[sizeof($var)-1] = $val;
380
    }
381
382
    public function inst_get($name)
383
    {
384
        // Quicker execution path for "."-free names
385
        if (strpos($name, '.') === false) {
386
            if (isset($this->data[$name])) {
387
                return $this->data[$name];
388
            }
389
            return null;
390
        } else {
391
            $names = explode('.', $name);
392
393
            if (!isset($this->data)) {
394
                return null;
395
            }
396
397
            $var = $this->data;
398
399
            foreach ($names as $n) {
400
                if (!isset($var[$n])) {
401
                    return null;
402
                }
403
                $var = $var[$n];
404
            }
405
406
            return $var;
407
        }
408
    }
409
410
    public function inst_clear($name)
411
    {
412
        $names = explode('.', $name);
413
414
        // We still want to do this even if we have strict path checking for legacy code
415
        $var = &$this->data;
416
        $diffVar = &$this->changedData;
417
418
        foreach ($names as $n) {
419
            // don't clear a record that doesn't exist
420
            if (!isset($var[$n])) {
421
                return;
422
            }
423
            $var = &$var[$n];
424
        }
425
426
        // only loop to find data within diffVar if var is proven to exist in the above loop
427
        foreach ($names as $n) {
428
            $diffVar = &$diffVar[$n];
429
        }
430
431
        if ($var !== null) {
432
            $var = null;
433
            $diffVar = null;
434
        }
435
    }
436
437
    public function inst_clearAll()
438
    {
439
        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...
440
            foreach (array_keys($this->data) as $key) {
441
                $this->inst_clear($key);
442
            }
443
        }
444
    }
445
446
    public function inst_getAll()
447
    {
448
        return $this->data;
449
    }
450
451
    public function inst_finalize()
452
    {
453
        $this->inst_set('HTTP_USER_AGENT', $this->userAgent());
454
    }
455
456
    /**
457
     * Save data to session
458
     * Only save the changes, so that anyone manipulating $_SESSION directly doesn't get burned.
459
     */
460
    public function inst_save()
0 ignored issues
show
Coding Style introduced by
inst_save uses the super-global variable $_SESSION which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
461
    {
462
        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...
463
            $this->inst_finalize();
464
465
            if (!isset($_SESSION)) {
466
                $this->inst_start();
467
            }
468
469
            $this->recursivelyApply($this->changedData, $_SESSION);
470
        }
471
    }
472
473
    /**
474
     * Recursively apply the changes represented in $data to $dest.
475
     * Used to update $_SESSION
476
     *
477
     * @param array $data
478
     * @param array $dest
479
     */
480
    protected function recursivelyApply($data, &$dest)
481
    {
482
        foreach ($data as $k => $v) {
483
            if (is_array($v)) {
484
                if (!isset($dest[$k]) || !is_array($dest[$k])) {
485
                    $dest[$k] = array();
486
                }
487
                $this->recursivelyApply($v, $dest[$k]);
488
            } else {
489
                $dest[$k] = $v;
490
            }
491
        }
492
    }
493
494
    /**
495
     * Return the changed data, for debugging purposes.
496
     * @return array
497
     */
498
    public function inst_changedData()
499
    {
500
        return $this->changedData;
501
    }
502
503
    /**
504
    * Sets the appropriate form message in session, with type. This will be shown once,
505
    * for the form specified.
506
    *
507
    * @param string $formname the form name you wish to use ( usually $form->FormName() )
508
    * @param string $message the message you wish to add to it
509
    * @param string $type the type of message
510
    */
511
    public static function setFormMessage($formname, $message, $type)
512
    {
513
        Session::set("FormInfo.$formname.formError.message", $message);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
514
        Session::set("FormInfo.$formname.formError.type", $type);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
515
    }
516
517
    /**
518
     * Is there a session ID in the request?
519
     * @return bool
520
     */
521
    public static function request_contains_session_id()
0 ignored issues
show
Coding Style introduced by
request_contains_session_id uses the super-global variable $_COOKIE which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
Coding Style introduced by
request_contains_session_id uses the super-global variable $_REQUEST which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
522
    {
523
        $secure = Director::is_https() && Config::inst()->get('SilverStripe\\Control\\Session', 'cookie_secure');
524
        $name = $secure ? 'SECSESSID' : session_name();
525
        return isset($_COOKIE[$name]) || isset($_REQUEST[$name]);
526
    }
527
528
    /**
529
     * Initialize session.
530
     *
531
     * @param string $sid Start the session with a specific ID
532
     */
533
    public static function start($sid = null)
534
    {
535
        self::current_session()->inst_start($sid);
536
    }
537
538
    /**
539
     * Destroy the active session.
540
     *
541
     * @param bool $removeCookie If set to TRUE, removes the user's cookie, FALSE does not remove
542
     */
543
    public static function destroy($removeCookie = true)
544
    {
545
        self::current_session()->inst_destroy($removeCookie);
546
    }
547
}
548