Test Failed
Branch master (bc2285)
by Florian
04:28
created

Session::start()   B

Complexity

Conditions 8
Paths 7

Size

Total Lines 34
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 8
eloc 17
nc 7
nop 0
dl 0
loc 34
rs 8.4444
c 1
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;
0 ignored issues
show
Bug introduced by
The type Adbar\Dot 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...
22
use SessionHandlerInterface;
23
24
/**
25
 * This class is a wrapper for the native PHP session functions. It provides
26
 * several defaults for the most common session configuration
27
 * via external handlers and helps with using session in cli without any warnings.
28
 *
29
 * Sessions can be created from the defaults using `Session::create()` or you can get
30
 * an instance of a new session by just instantiating this class and passing the complete
31
 * options you want to use.
32
 *
33
 * When specific options are omitted, this class will take its defaults from the configuration
34
 * values from the `session.*` directives in php.ini. This class will also alter such
35
 * directives when configuration values are provided.
36
 */
37
class Session implements SessionInterface
38
{
39
    /**
40
     * The Session handler instance used as an engine for persisting the session data.
41
     *
42
     * @var \SessionHandlerInterface
43
     */
44
    protected $handler;
45
46
    /**
47
     * The Session handler instance used as an engine for persisting the session data.
48
     *
49
     * @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...
50
     */
51
    protected $config;
52
53
    /**
54
     * Indicates whether the sessions has already started
55
     *
56
     * @var bool
57
     */
58
    protected $started;
59
60
    /**
61
     * The time in seconds the session will be valid for
62
     *
63
     * @var int
64
     */
65
    protected $lifetime;
66
67
    /**
68
     * Whether this session is running under a CLI environment
69
     *
70
     * @var bool
71
     */
72
    protected $isCLI = false;
73
74
    /**
75
     * Constructor.
76
     *
77
     * ### Configuration:
78
     *
79
     * - timeout: The time in minutes the session should be valid for.
80
     * - cookiePath: The url path for which session cookie is set. Maps to the
81
     *   `session.cookie_path` php.ini config. Defaults to base path of app.
82
     * - ini: A list of php.ini directives to change before the session start.
83
     * - handler: An array containing at least the `class` key. To be used as the session
84
     *   engine for persisting data. The rest of the keys in the array will be passed as
85
     *   the configuration array for the engine. You can set the `class` key to an already
86
     *   instantiated session handler object.
87
     *
88
     * @param \Phauthentic\Infrastructure\Http\Session\ConfigInterface $config The Configuration to apply to this session object
89
     * @param \SessionHandlerInterface $handler Session Handler
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\Infrastructure\Http\Session\ConfigInterface
114
     */
115
    public function config(): ConfigInterface
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): SessionHandlerInterface
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 \Phauthentic\Infrastructure\Http\Session\SessionException 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 SessionException('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 SessionException('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 hasStarted(): 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->hasStarted()) {
217
            $this->start();
218
        }
219
220
        if (!isset($_SESSION)) {
221
            return false;
222
        }
223
224
        return $this->getFromArray($_SESSION, $name) !== null;
0 ignored issues
show
Bug introduced by
It seems like $name can also be of type null; however, parameter $key of Phauthentic\Infrastructu...Session::getFromArray() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

224
        return $this->getFromArray($_SESSION, /** @scrutinizer ignore-type */ $name) !== null;
Loading history...
225
     }
0 ignored issues
show
Coding Style introduced by
Closing brace indented incorrectly; expected 4 spaces, found 5
Loading history...
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->hasStarted()) {
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 $this->getFromArray($_SESSION, $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
            $data = $_SESSION;
267
            $this->setToArray($data, $name, null);
268
            $this->overwrite($_SESSION, $data);
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->hasStarted()) {
284
            $this->start();
285
        }
286
287
        $write = $name;
288
        if (!is_array($name)) {
289
            $write = [$name => $value];
290
        }
291
292
        $data = $_SESSION;
293
        foreach ($write as $key => $val) {
294
            $this->setToArray($data, $key, $value);
295
        }
296
297
        $this->overwrite($_SESSION, $data);
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 SessionException(
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
            $data = $_SESSION;
374
            $this->setToArray($data, $name, null);
375
            $this->overwrite($_SESSION, $data);
376
        }
377
    }
378
379
    /**
380
     * Used to write new data to _SESSION, since PHP doesn't like us setting the _SESSION var itself.
381
     *
382
     * @param array $old Set of old variables => values
383
     * @param array $new New set of variable => value
384
     * @return void
385
     */
386
    protected function overwrite(&$old, $new)
387
    {
388
        if (!empty($old)) {
389
            foreach ($old as $key => $var) {
390
                if (!isset($new[$key])) {
391
                    unset($old[$key]);
392
                }
393
            }
394
        }
395
396
        foreach ($new as $key => $var) {
397
            $old[$key] = $var;
398
        }
399
    }
400
401
    /**
402
     * Helper method to destroy invalid sessions.
403
     *
404
     * @return void
405
     */
406
    public function destroy()
407
    {
408
        if ($this->exists() && !$this->hasStarted()) {
409
            $this->start();
410
        }
411
412
        if (!$this->isCLI && session_status() === PHP_SESSION_ACTIVE) {
413
            session_destroy();
414
        }
415
416
        $_SESSION = [];
417
        $this->started = false;
418
    }
419
420
    /**
421
     * Clears the session.
422
     *
423
     * Optionally it also clears the session id and renews the session.
424
     *
425
     * @param bool $renew If session should be renewed, as well. Defaults to false.
426
     * @return void
427
     */
428
    public function clear($renew = false)
429
    {
430
        $_SESSION = [];
431
        if ($renew) {
432
            $this->renew();
433
        }
434
    }
435
436
    /**
437
     * Returns whether a session exists
438
     *
439
     * @return bool
440
     */
441
    public function exists()
442
    {
443
        return !ini_get('session.use_cookies')
444
            || isset($_COOKIE[session_name()])
445
            || $this->isCLI
446
            || (ini_get('session.use_trans_sid') && isset($_GET[session_name()]));
447
    }
448
449
    /**
450
     * Restarts this session.
451
     *
452
     * @return void
453
     */
454
    public function renew(): void
455
    {
456
        if ($this->isCLI || !$this->exists()) {
457
            return;
458
        }
459
460
        $this->start();
461
        $params = session_get_cookie_params();
462
        setcookie(
463
            session_name(),
464
            '',
465
            time() - 42000,
466
            $params['path'],
467
            $params['domain'],
468
            $params['secure'],
469
            $params['httponly']
470
        );
471
472
        if (session_id()) {
473
            session_regenerate_id(true);
474
        }
475
    }
476
477
    /**
478
     * Returns true if the session is no longer valid because the last time it was
479
     * accessed was after the configured timeout.
480
     *
481
     * @return bool
482
     */
483
    public function hasExpired(): bool
484
    {
485
        $time = $this->read('Config.time');
486
        $result = false;
487
488
        $checkTime = $time !== null && $this->lifetime > 0;
489
        if ($checkTime && (time() - (int)$time > $this->lifetime)) {
490
            $result = true;
491
        }
492
493
        $this->write('Config.time', time());
494
495
        return $result;
496
    }
497
498
    /**
499
     * @link https://medium.com/@assertchris/dot-notation-3fd3e42edc61
500
     */
501
    protected function getFromArray($array, string $key, $default = null)
502
    {
503
        if (is_null($key)) {
0 ignored issues
show
introduced by
The condition is_null($key) is always false.
Loading history...
504
            return $array;
505
        }
506
507
        if (isset($array[$key])) {
508
            return $array[$key];
509
        }
510
511
        foreach (explode('.', $key) as $segment) {
512
            if (!is_array($array) ||
0 ignored issues
show
Coding Style introduced by
The first expression of a multi-line control structure must be on the line after the opening parenthesis
Loading history...
513
                !array_key_exists($segment, $array)) {
0 ignored issues
show
Coding Style introduced by
The closing parenthesis of a multi-line control structure must be on the line after the last expression
Loading history...
514
                return $default;
515
            }
516
517
            $array = $array[$segment];
518
        }
519
520
        return $array;
521
    }
522
523
    /**
524
     * @link https://medium.com/@assertchris/dot-notation-3fd3e42edc61
525
     */
526
    protected function setToArray(&$array, string $key, $value)
527
    {
528
        if (is_null($key)) {
0 ignored issues
show
introduced by
The condition is_null($key) is always false.
Loading history...
529
            return $array = $value;
530
        }
531
532
        $keys = explode('.', $key);
533
534
        while (count($keys) > 1) {
535
            $key = array_shift($keys);
536
537
            if (!isset($array[$key]) || !is_array($array[$key])) {
538
                $array[$key] = [];
539
            }
540
541
            $array =& $array[$key];
542
        }
543
544
        $array[array_shift($keys)] = $value;
545
546
        return $array;
547
    }
548
}
549