Failed Conditions
Push — master ( 435265...051ce3 )
by Florian
02:47
created

Session::exists()   A

Complexity

Conditions 5
Paths 7

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 5

Importance

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