Test Failed
Push — master ( 13c8bf...f549ef )
by Florian
11:18
created

Session::write()   A

Complexity

Conditions 5
Paths 8

Size

Total Lines 17
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 9
nc 8
nop 2
dl 0
loc 17
rs 9.6111
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
/**
0 ignored issues
show
Coding Style introduced by
The file-level docblock must follow the opening PHP tag in the file header
Loading history...
6
 * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
7
 * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
8
 *
9
 * Licensed under The MIT License
10
 * For full copyright and license information, please see the LICENSE.txt
11
 * Redistributions of files must retain the above copyright notice.
12
 *
13
 * @copyright     Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
14
 * @link          https://cakephp.org CakePHP(tm) Project
15
 * @since         0.10.0
16
 * @license       https://opensource.org/licenses/mit-license.php MIT License
17
 */
18
19
namespace Phauthentic\Infrastructure\Http\Session;
20
21
use Adbar\Dot;
22
use RuntimeException;
23
use SessionHandlerInterface;
24
25
/**
26
 * This class is a wrapper for the native PHP session functions. It provides
27
 * several defaults for the most common session configuration
28
 * via external handlers and helps with using session in cli without any warnings.
29
 *
30
 * Sessions can be created from the defaults using `Session::create()` or you can get
31
 * an instance of a new session by just instantiating this class and passing the complete
32
 * options you want to use.
33
 *
34
 * When specific options are omitted, this class will take its defaults from the configuration
35
 * values from the `session.*` directives in php.ini. This class will also alter such
36
 * directives when configuration values are provided.
37
 */
38
class Session implements SessionInterface
39
{
40
    /**
41
     * The Session handler instance used as an engine for persisting the session data.
42
     *
43
     * @var \SessionHandlerInterface
44
     */
45
    protected $handler;
46
47
    /**
48
     * The Session handler instance used as an engine for persisting the session data.
49
     *
50
     * @var \Phauthentic\Session\ConfigInterface
0 ignored issues
show
Bug introduced by
The type Phauthentic\Session\ConfigInterface was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
51
     */
52
    protected $config;
53
54
    /**
55
     * Indicates whether the sessions has already started
56
     *
57
     * @var bool
58
     */
59
    protected $started;
60
61
    /**
62
     * The time in seconds the session will be valid for
63
     *
64
     * @var int
65
     */
66
    protected $lifetime;
67
68
    /**
69
     * Whether this session is running under a CLI environment
70
     *
71
     * @var bool
72
     */
73
    protected $isCLI = false;
74
75
    /**
76
     * Constructor.
77
     *
78
     * ### Configuration:
79
     *
80
     * - timeout: The time in minutes the session should be valid for.
81
     * - cookiePath: The url path for which session cookie is set. Maps to the
82
     *   `session.cookie_path` php.ini config. Defaults to base path of app.
83
     * - ini: A list of php.ini directives to change before the session start.
84
     * - handler: An array containing at least the `class` key. To be used as the session
85
     *   engine for persisting data. The rest of the keys in the array will be passed as
86
     *   the configuration array for the engine. You can set the `class` key to an already
87
     *   instantiated session handler object.
88
     *
89
     * @param array $config The Configuration to apply to this session object
90
     */
91
    public function __construct(
92
        ?ConfigInterface $config = null,
93
        ?SessionHandlerInterface $handler = null
94
    ) {
95
        if ($config !== null) {
96
            $this->config = $config;
0 ignored issues
show
Documentation Bug introduced by
It seems like $config of type Phauthentic\Infrastructu...Session\ConfigInterface is incompatible with the declared type Phauthentic\Session\ConfigInterface of property $config.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
97
        } else {
98
            $this->config = new Config();
99
            $this->config->setUseTransSid(false);
100
        }
101
102
        if ($handler !== null) {
103
            $this->setSaveHandler($handler);
104
        }
105
106
        $this->lifetime = (int)ini_get('session.gc_maxlifetime');
107
        $this->isCLI = (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg');
108
109
        session_register_shutdown();
110
    }
111
112
    /**
113
     * @return \Phauthentic\Session\ConfigInterface
114
     */
115
    public function config()
116
    {
117
        return $this->config;
118
    }
119
120
    /**
121
     * Set the engine property and update the session handler in PHP.
122
     *
123
     * @param \SessionHandlerInterface $handler The handler to set
124
     * @return \SessionHandlerInterface
125
     */
126
    protected function setSaveHandler(SessionHandlerInterface $handler)
127
    {
128
        if (!headers_sent()) {
129
            session_set_save_handler($handler, false);
130
        }
131
132
        return $this->handler = $handler;
133
    }
134
135
    /**
136
     * Starts the Session.
137
     *
138
     * @return bool True if session was started
139
     * @throws \RuntimeException if the session was already started
140
     */
141
    public function start()
142
    {
143
        if ($this->started) {
144
            return true;
145
        }
146
147
        if ($this->isCLI) {
148
            $_SESSION = [];
149
            $this->setId('cli');
150
151
            return $this->started = true;
152
        }
153
154
        if (session_status() === PHP_SESSION_ACTIVE) {
155
            throw new RuntimeException('Session was already started');
156
        }
157
158
        if (ini_get('session.use_cookies') && headers_sent($file, $line)) {
159
            return false;
160
        }
161
162
        if (!session_start()) {
163
            throw new RuntimeException('Could not start the session');
164
        }
165
166
        $this->started = true;
167
168
        if ($this->timedOut()) {
169
            $this->destroy();
170
171
            return $this->start();
172
        }
173
174
        return $this->started;
175
    }
176
177
    /**
178
     * Returns true if the session is no longer valid because the last time it was
179
     * accessed was after the configured timeout.
180
     *
181
     * @return bool
182
     */
183
    protected function timedOut(): bool
184
    {
185
        $time = $this->read('Config.time');
186
        $result = false;
187
188
        $checkTime = $time !== null && $this->lifetime > 0;
189
        if ($checkTime && (time() - (int)$time > $this->lifetime)) {
190
            $result = true;
191
        }
192
193
        $this->write('Config.time', time());
194
195
        return $result;
196
    }
197
198
    /**
199
     * Determine if Session has already been started.
200
     *
201
     * @return bool True if session has been started.
202
     */
203
    public function started(): bool
204
    {
205
        return $this->started || session_status() === PHP_SESSION_ACTIVE;
206
    }
207
208
    /**
209
     * Returns true if given variable name is set in session.
210
     *
211
     * @param string|null $name Variable name to check for
212
     * @return bool True if variable is there
213
     */
214
    public function check(?string $name = null): bool
215
    {
216
        if ($this->exists() && !$this->started()) {
217
            $this->start();
218
        }
219
220
        if (!isset($_SESSION)) {
221
            return false;
222
        }
223
224
        return (new Dot($_SESSION))->get($name) !== null;
225
    }
226
227
    /**
228
     * Returns given session variable, or all of them, if no parameters given.
229
     *
230
     * @param string|null $name The name of the session variable (or a path as sent to Hash.extract)
231
     * @return string|array|null The value of the session variable, null if session not available,
232
     *   session not started, or provided name not found in the session.
233
     */
234
    public function read(?string $name = null)
235
    {
236
        if ($this->exists() && !$this->started()) {
237
            $this->start();
238
        }
239
240
        if (!isset($_SESSION)) {
241
            return null;
242
        }
243
244
        if ($name === null) {
245
            return $_SESSION ?? [];
246
        }
247
248
        return (new Dot($_SESSION))->get($name);
249
    }
250
251
    /**
252
     * Reads and deletes a variable from session.
253
     *
254
     * @param string $name The key to read and remove (or a path as sent to Hash.extract).
255
     * @return mixed The value of the session variable, null if session not available,
256
     *   session not started, or provided name not found in the session.
257
     */
258
    public function consume(string $name)
259
    {
260
        if (empty($name)) {
261
            return null;
262
        }
263
264
        $value = $this->read($name);
265
        if ($value !== null) {
266
            $dot = new Dot($_SESSION);
267
            $dot->delete($name);
268
            $this->overwrite($_SESSION, (array)$dot->get());
269
        }
270
271
        return $value;
272
    }
273
274
    /**
275
     * Writes value to given session variable name.
276
     *
277
     * @param string|array $name Name of variable
278
     * @param mixed $value Value to write
279
     * @return void
280
     */
281
    public function write($name, $value = null): void
282
    {
283
        if (!$this->started()) {
284
            $this->start();
285
        }
286
287
        $write = $name;
288
        if (!is_array($name)) {
289
            $write = [$name => $value];
290
        }
291
292
        $data = new Dot(isset($_SESSION) ? $_SESSION : []);
293
        foreach ($write as $key => $val) {
294
            $data->add($key, $val);
295
        }
296
297
        $this->overwrite($_SESSION, $data->get());
298
    }
299
300
    /**
301
     * Returns the session id.
302
     *
303
     * Calling this method will not auto start the session. You might have to manually
304
     * assert a started session.
305
     *
306
     * Passing an id into it, you can also replace the session id if the session
307
     * has not already been started.
308
     *
309
     * Note that depending on the session handler, not all characters are allowed
310
     * within the session id. For example, the file session handler only allows
311
     * characters in the range a-z A-Z 0-9 , (comma) and - (minus).
312
     *
313
     * @param string|null $id Id to replace the current session id
314
     * @return string Session id
315
     */
316
    public function id(?string $id = null): string
317
    {
318
        if ($id !== null && !headers_sent()) {
319
            $this->setId($id);
320
        }
321
322
        return $this->getId();
323
    }
324
325
    /**
326
     * Returns the current sessions id
327
     *
328
     * @return string
329
     */
330
    public function getId(): string
331
    {
332
        return (string)session_id();
333
    }
334
335
    /**
336
     * Sets the session id
337
     *
338
     * Calling this method will not auto start the session. You might have to manually
339
     * assert a started session.
340
     *
341
     * Passing an id into it, you can also replace the session id if the session
342
     * has not already been started.
343
     *
344
     * Note that depending on the session handler, not all characters are allowed
345
     * within the session id. For example, the file session handler only allows
346
     * characters in the range a-z A-Z 0-9 , (comma) and - (minus).
347
     *
348
     * @param string $id Session Id
349
     * @return $this
350
     */
351
    public function setId(string $id): self
352
    {
353
        if (headers_sent()) {
354
            throw new RuntimeException(
355
                'Headers already sent. You can\'t set the session id anymore'
356
            );
357
        }
358
359
        session_id($id);
360
361
        return $this;
362
    }
363
364
    /**
365
     * Removes a variable from session.
366
     *
367
     * @param string $name Session variable to remove
368
     * @return void
369
     */
370
    public function delete(string $name): void
371
    {
372
        if ($this->check($name)) {
373
            $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...
374
        }
375
    }
376
377
    /**
378
     * Used to write new data to _SESSION, since PHP doesn't like us setting the _SESSION var itself.
379
     *
380
     * @param array $old Set of old variables => values
381
     * @param array $new New set of variable => value
382
     * @return void
383
     */
384
    protected function overwrite(&$old, $new)
385
    {
386
        if (!empty($old)) {
387
            foreach ($old as $key => $var) {
388
                if (!isset($new[$key])) {
389
                    unset($old[$key]);
390
                }
391
            }
392
        }
393
394
        foreach ($new as $key => $var) {
395
            $old[$key] = $var;
396
        }
397
    }
398
399
    /**
400
     * Helper method to destroy invalid sessions.
401
     *
402
     * @return void
403
     */
404
    public function destroy()
405
    {
406
        if ($this->exists() && !$this->started()) {
407
            $this->start();
408
        }
409
410
        if (!$this->isCLI && session_status() === PHP_SESSION_ACTIVE) {
411
            session_destroy();
412
        }
413
414
        $_SESSION = [];
415
        $this->started = false;
416
    }
417
418
    /**
419
     * Clears the session.
420
     *
421
     * Optionally it also clears the session id and renews the session.
422
     *
423
     * @param bool $renew If session should be renewed, as well. Defaults to false.
424
     * @return void
425
     */
426
    public function clear($renew = false)
427
    {
428
        $_SESSION = [];
429
        if ($renew) {
430
            $this->renew();
431
        }
432
    }
433
434
    /**
435
     * Returns whether a session exists
436
     *
437
     * @return bool
438
     */
439
    public function exists()
440
    {
441
        return !ini_get('session.use_cookies')
442
            || isset($_COOKIE[session_name()])
443
            || $this->isCLI
444
            || (ini_get('session.use_trans_sid') && isset($_GET[session_name()]));
445
    }
446
447
    /**
448
     * Restarts this session.
449
     *
450
     * @return void
451
     */
452
    public function renew(): void
453
    {
454
        if ($this->isCLI || !$this->exists()) {
455
            return;
456
        }
457
458
        $this->start();
459
        $params = session_get_cookie_params();
460
        setcookie(
461
            session_name(),
462
            '',
463
            time() - 42000,
464
            $params['path'],
465
            $params['domain'],
466
            $params['secure'],
467
            $params['httponly']
468
        );
469
470
        if (session_id()) {
471
            session_regenerate_id(true);
472
        }
473
    }
474
475
    /**
476
     * Returns true if the session is no longer valid because the last time it was
477
     * accessed was after the configured timeout.
478
     *
479
     * @return bool
480
     */
481
    public function hasExpired(): bool
482
    {
483
        $time = $this->read('Config.time');
484
        $result = false;
485
486
        $checkTime = $time !== null && $this->lifetime > 0;
487
        if ($checkTime && (time() - (int)$time > $this->lifetime)) {
488
            $result = true;
489
        }
490
491
        $this->write('Config.time', time());
492
493
        return $result;
494
    }
495
}
496