Passed
Push — master ( ebee01...7c71ea )
by Florian
01:21
created

Session::write()   A

Complexity

Conditions 5
Paths 8

Size

Total Lines 17
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 5.025

Importance

Changes 0
Metric Value
cc 5
eloc 9
nc 8
nop 2
dl 0
loc 17
ccs 9
cts 10
cp 0.9
crap 5.025
rs 9.6111
c 0
b 0
f 0
1
<?php
2
declare(strict_types=1);
3
4
/**
5
 * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
6
 * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
7
 *
8
 * Licensed under The MIT License
9
 * For full copyright and license information, please see the LICENSE.txt
10
 * Redistributions of files must retain the above copyright notice.
11
 *
12
 * @copyright     Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
13
 * @link          https://cakephp.org CakePHP(tm) Project
14
 * @since         0.10.0
15
 * @license       https://opensource.org/licenses/mit-license.php MIT License
16
 */
17
namespace Phauthentic\Session;
18
19
use Adbar\Dot;
20
use RuntimeException;
21
use SessionHandlerInterface;
22
23
/**
24
 * This class is a wrapper for the native PHP session functions. It provides
25
 * several defaults for the most common session configuration
26
 * via external handlers and helps with using session in cli without any warnings.
27
 *
28
 * Sessions can be created from the defaults using `Session::create()` or you can get
29
 * an instance of a new session by just instantiating this class and passing the complete
30
 * options you want to use.
31
 *
32
 * When specific options are omitted, this class will take its defaults from the configuration
33
 * values from the `session.*` directives in php.ini. This class will also alter such
34
 * directives when configuration values are provided.
35
 */
36
class Session implements SessionInterface
37
{
38
    /**
39
     * The Session handler instance used as an engine for persisting the session data.
40
     *
41
     * @var \SessionHandlerInterface
42
     */
43
    protected $handler;
44
45
    /**
46
     * The Session handler instance used as an engine for persisting the session data.
47
     *
48
     * @var \Phauthentic\Session\ConfigInterface
49
     */
50
    protected $config;
51
52
    /**
53
     * Indicates whether the sessions has already started
54
     *
55
     * @var bool
56
     */
57
    protected $started;
58
59
    /**
60
     * The time in seconds the session will be valid for
61
     *
62
     * @var int
63
     */
64
    protected $lifetime;
65
66
    /**
67
     * Whether this session is running under a CLI environment
68
     *
69
     * @var bool
70
     */
71
    protected $isCLI = false;
72
73
    /**
74
     * Constructor.
75
     *
76
     * ### Configuration:
77
     *
78
     * - timeout: The time in minutes the session should be valid for.
79
     * - cookiePath: The url path for which session cookie is set. Maps to the
80
     *   `session.cookie_path` php.ini config. Defaults to base path of app.
81
     * - ini: A list of php.ini directives to change before the session start.
82
     * - handler: An array containing at least the `class` key. To be used as the session
83
     *   engine for persisting data. The rest of the keys in the array will be passed as
84
     *   the configuration array for the engine. You can set the `class` key to an already
85
     *   instantiated session handler object.
86
     *
87
     * @param array $config The Configuration to apply to this session object
88
     */
89 1
    public function __construct(
90
        ?ConfigInterface $config = null,
91
        ?SessionHandlerInterface $handler = null
92
    ) {
93 1
        if ($config !== null) {
94
            $this->config = $config;
95
        } else {
96 1
            $this->config = new Config();
97 1
            $this->config->setUseTransSid(false);
98
        }
99
100 1
        if ($handler !== null) {
101
            $this->setSaveHandler($handler);
102
        }
103
104 1
        $this->lifetime = (int)ini_get('session.gc_maxlifetime');
105 1
        $this->isCLI = (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg');
106
107 1
        session_register_shutdown();
108 1
    }
109
110
    /**
111
     * @return \Phauthentic\Session\ConfigInterface
112
     */
113
    public function config()
114
    {
115
        return $this->config;
116
    }
117
118
    /**
119
     * Set the engine property and update the session handler in PHP.
120
     *
121
     * @param \SessionHandlerInterface $handler The handler to set
122
     * @return \SessionHandlerInterface
123
     */
124
    protected function setSaveHandler(SessionHandlerInterface $handler)
125
    {
126
        if (!headers_sent()) {
127
            session_set_save_handler($handler, false);
128
        }
129
130
        return $this->handler = $handler;
131
    }
132
133
    /**
134
     * Starts the Session.
135
     *
136
     * @return bool True if session was started
137
     * @throws \RuntimeException if the session was already started
138
     */
139 1
    public function start()
140
    {
141 1
        if ($this->started) {
142
            return true;
143
        }
144
145 1
        if ($this->isCLI) {
146 1
            $_SESSION = [];
147 1
            $this->setId('cli');
148
149 1
            return $this->started = true;
150
        }
151
152
        if (session_status() === PHP_SESSION_ACTIVE) {
153
            throw new RuntimeException('Session was already started');
154
        }
155
156
        if (ini_get('session.use_cookies') && headers_sent($file, $line)) {
157
            return false;
158
        }
159
160
        if (!session_start()) {
161
            throw new RuntimeException('Could not start the session');
162
        }
163
164
        $this->started = true;
165
166
        if ($this->timedOut()) {
167
            $this->destroy();
168
169
            return $this->start();
170
        }
171
172
        return $this->started;
173
    }
174
175
    /**
176
     * Returns true if the session is no longer valid because the last time it was
177
     * accessed was after the configured timeout.
178
     *
179
     * @return bool
180
     */
181
    protected function timedOut(): bool
182
    {
183
        $time = $this->read('Config.time');
184
        $result = false;
185
186
        $checkTime = $time !== null && $this->lifetime > 0;
187
        if ($checkTime && (time() - (int)$time > $this->lifetime)) {
188
            $result = true;
189
        }
190
191
        $this->write('Config.time', time());
192
193
        return $result;
194
    }
195
196
    /**
197
     * Determine if Session has already been started.
198
     *
199
     * @return bool True if session has been started.
200
     */
201 1
    public function started(): bool
202
    {
203 1
        return $this->started || session_status() === PHP_SESSION_ACTIVE;
204
    }
205
206
    /**
207
     * Returns true if given variable name is set in session.
208
     *
209
     * @param string|null $name Variable name to check for
210
     * @return bool True if variable is there
211
     */
212 1
    public function check(?string $name = null): bool
213
    {
214 1
        if ($this->exists() && !$this->started()) {
215
            $this->start();
216
        }
217
218 1
        if (!isset($_SESSION)) {
219
            return false;
220
        }
221
222 1
        return (new Dot($_SESSION))->get($name) !== null;
223
    }
224
225
    /**
226
     * Returns given session variable, or all of them, if no parameters given.
227
     *
228
     * @param string|null $name The name of the session variable (or a path as sent to Hash.extract)
229
     * @return string|array|null The value of the session variable, null if session not available,
230
     *   session not started, or provided name not found in the session.
231
     */
232 1
    public function read(?string $name = null)
233
    {
234 1
        if ($this->exists() && !$this->started()) {
235
            $this->start();
236
        }
237
238 1
        if (!isset($_SESSION)) {
239
            return null;
240
        }
241
242 1
        if ($name === null) {
243
            return $_SESSION ?? [];
244
        }
245
246 1
        return (new Dot($_SESSION))->get($name);
247
    }
248
249
    /**
250
     * Reads and deletes a variable from session.
251
     *
252
     * @param string $name The key to read and remove (or a path as sent to Hash.extract).
253
     * @return mixed The value of the session variable, null if session not available,
254
     *   session not started, or provided name not found in the session.
255
     */
256 1
    public function consume(string $name)
257
    {
258 1
        if (empty($name)) {
259
            return null;
260
        }
261
262 1
        $value = $this->read($name);
263 1
        if ($value !== null) {
264 1
            $dot = new Dot($_SESSION);
265 1
            $dot->delete($name);
266 1
            $this->overwrite($_SESSION, (array)$dot->get());
267
        }
268
269 1
        return $value;
270
    }
271
272
    /**
273
     * Writes value to given session variable name.
274
     *
275
     * @param string|array $name Name of variable
276
     * @param mixed $value Value to write
277
     * @return void
278
     */
279 1
    public function write($name, $value = null): void
280
    {
281 1
        if (!$this->started()) {
282
            $this->start();
283
        }
284
285 1
        $write = $name;
286 1
        if (!is_array($name)) {
287 1
            $write = [$name => $value];
288
        }
289
290 1
        $data = new Dot(isset($_SESSION) ? $_SESSION : []);
291 1
        foreach ($write as $key => $val) {
292 1
            $data->add($key, $val);
293
        }
294
295 1
        $this->overwrite($_SESSION, $data->get());
296 1
    }
297
298
    /**
299
     * Returns the session id.
300
     *
301
     * Calling this method will not auto start the session. You might have to manually
302
     * assert a started session.
303
     *
304
     * Passing an id into it, you can also replace the session id if the session
305
     * has not already been started.
306
     *
307
     * Note that depending on the session handler, not all characters are allowed
308
     * within the session id. For example, the file session handler only allows
309
     * characters in the range a-z A-Z 0-9 , (comma) and - (minus).
310
     *
311
     * @param string|null $id Id to replace the current session id
312
     * @return string Session id
313
     */
314
    public function id(?string $id = null): string
315
    {
316
        if ($id !== null && !headers_sent()) {
317
            $this->setId($id);
318
        }
319
320
        return $this->getId();
321
    }
322
323
    /**
324
     * Returns the current sessions id
325
     *
326
     * @return string
327
     */
328
    public function getId(): string
329
    {
330
        return (string)session_id();
331
    }
332
333
    /**
334
     * Sets the session id
335
     *
336
     * Calling this method will not auto start the session. You might have to manually
337
     * assert a started session.
338
     *
339
     * Passing an id into it, you can also replace the session id if the session
340
     * has not already been started.
341
     *
342
     * Note that depending on the session handler, not all characters are allowed
343
     * within the session id. For example, the file session handler only allows
344
     * characters in the range a-z A-Z 0-9 , (comma) and - (minus).
345
     *
346
     * @param string $id Session Id
347
     * @return $this
348
     */
349 1
    public function setId(string $id): self
350
    {
351 1
        if (headers_sent()) {
352
            throw new RuntimeException(
353
                'Headers already sent. You can\'t set the session id anymore'
354
            );
355
        }
356
357 1
        session_id($id);
358
359 1
        return $this;
360
    }
361
362
    /**
363
     * Removes a variable from session.
364
     *
365
     * @param string $name Session variable to remove
366
     * @return void
367
     */
368 1
    public function delete(string $name): void
369
    {
370 1
        if ($this->check($name)) {
371 1
            $this->overwrite($_SESSION, (array)(new Dot($_SESSION))->delete($name));
0 ignored issues
show
Bug introduced by
Are you sure the usage of new Adbar\Dot($_SESSION)->delete($name) targeting Adbar\Dot::delete() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
372
        }
373 1
    }
374
375
    /**
376
     * Used to write new data to _SESSION, since PHP doesn't like us setting the _SESSION var itself.
377
     *
378
     * @param array $old Set of old variables => values
379
     * @param array $new New set of variable => value
380
     * @return void
381
     */
382 1
    protected function overwrite(&$old, $new)
383
    {
384 1
        if (!empty($old)) {
385 1
            foreach ($old as $key => $var) {
386 1
                if (!isset($new[$key])) {
387 1
                    unset($old[$key]);
388
                }
389
            }
390
        }
391
392 1
        foreach ($new as $key => $var) {
393 1
            $old[$key] = $var;
394
        }
395 1
    }
396
397
    /**
398
     * Helper method to destroy invalid sessions.
399
     *
400
     * @return void
401
     */
402
    public function destroy()
403
    {
404
        if ($this->exists() && !$this->started()) {
405
            $this->start();
406
        }
407
408
        if (!$this->isCLI && session_status() === PHP_SESSION_ACTIVE) {
409
            session_destroy();
410
        }
411
412
        $_SESSION = [];
413
        $this->started = false;
414
    }
415
416
    /**
417
     * Clears the session.
418
     *
419
     * Optionally it also clears the session id and renews the session.
420
     *
421
     * @param bool $renew If session should be renewed, as well. Defaults to false.
422
     * @return void
423
     */
424
    public function clear($renew = false)
425
    {
426
        $_SESSION = [];
427
        if ($renew) {
428
            $this->renew();
429
        }
430
    }
431
432
    /**
433
     * Returns whether a session exists
434
     *
435
     * @return bool
436
     */
437 1
    public function exists()
438
    {
439 1
        return !ini_get('session.use_cookies')
440 1
            || isset($_COOKIE[session_name()])
441 1
            || $this->isCLI
442 1
            || (ini_get('session.use_trans_sid') && isset($_GET[session_name()]));
443
    }
444
445
    /**
446
     * Restarts this session.
447
     *
448
     * @return void
449
     */
450
    public function renew(): void
451
    {
452
        if (!$this->exists() || $this->isCLI) {
453
            return;
454
        }
455
456
        $this->start();
457
        $params = session_get_cookie_params();
458
        setcookie(
459
            session_name(),
460
            '',
461
            time() - 42000,
462
            $params['path'],
463
            $params['domain'],
464
            $params['secure'],
465
            $params['httponly']
466
        );
467
468
        if (session_id()) {
469
            session_regenerate_id(true);
470
        }
471
    }
472
473
    /**
474
     * Returns true if the session is no longer valid because the last time it was
475
     * accessed was after the configured timeout.
476
     *
477
     * @return bool
478
     */
479
    public function hasExpired(): bool
480
    {
481
        $time = $this->read('Config.time');
482
        $result = false;
483
484
        $checkTime = $time !== null && $this->lifetime > 0;
485
        if ($checkTime && (time() - (int)$time > $this->lifetime)) {
486
            $result = true;
487
        }
488
489
        $this->write('Config.time', time());
490
491
        return $result;
492
    }
493
}
494