Failed Conditions
Push — develop ( 5591cb...f2c6ff )
by Florian
02:05
created

Session::consume()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 3.0123

Importance

Changes 0
Metric Value
cc 3
eloc 8
nc 3
nop 1
dl 0
loc 14
ccs 8
cts 9
cp 0.8889
crap 3.0123
rs 10
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
5
 * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
6
 *
7
 * Licensed under The MIT License
8
 * For full copyright and license information, please see the LICENSE.txt
9
 * Redistributions of files must retain the above copyright notice.
10
 *
11
 * @copyright     Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
12
 * @link          https://cakephp.org CakePHP(tm) Project
13
 * @since         0.10.0
14
 * @license       https://opensource.org/licenses/mit-license.php MIT License
15
 */
16
17
declare(strict_types=1);
18
19
namespace Phauthentic\Infrastructure\Http\Session;
20
21
use Adbar\Dot;
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 SessionHandlerInterface $handler;
45
46
    /**
47
     * The Session handler instance used as an engine for persisting the session data.
48
     *
49
     * @var \Phauthentic\Infrastructure\Http\Session\ConfigInterface
50
     */
51
    protected ConfigInterface $config;
52
53
    /**
54
     * Indicates whether the sessions has already started
55
     *
56
     * @var bool
57
     */
58
    protected bool $started = false;
59
60
    /**
61
     * The time in seconds the session will be valid for
62
     *
63
     * @var int
64
     */
65
    protected int $lifetime = 0;
66
67
    /**
68
     * Whether this session is running under a CLI environment
69
     *
70
     * @var bool
71
     */
72
    protected bool $isCli = false;
73
74
    /**
75
     * Constructor.
76
     * ### Configuration:
77
     * - timeout: The time in minutes the session should be valid for.
78
     * - cookiePath: The url path for which session cookie is set. Maps to the
79
     *   `session.cookie_path` php.ini config. Defaults to base path of app.
80
     * - ini: A list of php.ini directives to change before the session start.
81
     * - handler: An array containing at least the `class` key. To be used as the session
82
     *   engine for persisting data. The rest of the keys in the array will be passed as
83
     *   the configuration array for the engine. You can set the `class` key to an already
84
     *   instantiated session handler object.
85
     *
86
     * @param \Phauthentic\Infrastructure\Http\Session\ConfigInterface|null $config The Configuration to apply to this session object
87
     * @param \SessionHandlerInterface|null $handler
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\Infrastructure\Http\Session\ConfigInterface
112
     */
113
    public function config(): ConfigInterface
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 void
123
     */
124
    protected function setSaveHandler(SessionHandlerInterface $handler): void
125
    {
126
        if (!headers_sent()) {
127
            session_set_save_handler($handler, false);
128
        }
129
130
        $this->handler = $handler;
131
    }
132
133
    /**
134
     * Starts the Session.
135
     *
136
     * @return bool True if session was started
137
     * @throws \Phauthentic\Infrastructure\Http\Session\SessionException if the session was already started
138
     */
139 1
    public function start(): bool
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 SessionException::alreadyStarted();
154
        }
155
156
        if (ini_get('session.use_cookies') && headers_sent($file, $line)) {
157
            return false;
158
        }
159
160
        if (!session_start()) {
161
            throw SessionException::couldNotStart();
162
        }
163
164
        $this->started = true;
165
166
        if ($this->hasExpired()) {
167
            $this->destroy();
168
169
            return $this->start();
170
        }
171
172
        return $this->started;
173
    }
174
175
    /**
176
     * @return array|string|null
177
     */
178
    protected function time()
179
    {
180
        return $this->read('Config.time');
181
    }
182
183
    /**
184
     * Determine if Session has already been started.
185
     *
186
     * @return bool True if session has been started.
187
     */
188 1
    public function started(): bool
189
    {
190 1
        return $this->started || session_status() === PHP_SESSION_ACTIVE;
191
    }
192
193
    /**
194
     * Returns true if given variable name is set in session.
195
     *
196
     * @param string|null $name Variable name to check for
197
     * @return bool True if variable is there
198
     */
199 1
    public function check(?string $name = null): bool
200
    {
201 1
        if ($this->exists() && !$this->started()) {
202
            $this->start();
203
        }
204
205 1
        if (!isset($_SESSION)) {
206
            return false;
207
        }
208
209 1
        return (new Dot($_SESSION))->get($name) !== null;
210
    }
211
212
    /**
213
     * Returns given session variable, or all of them, if no parameters given.
214
     *
215
     * @param string|null $name The name of the session variable (or a path as sent to Hash.extract)
216
     * @return string|array|null The value of the session variable, null if session not available,
217
     *   session not started, or provided name not found in the session.
218
     */
219 1
    public function read(?string $name = null)
220
    {
221 1
        if ($this->exists() && !$this->started()) {
222
            $this->start();
223
        }
224
225 1
        if (!isset($_SESSION)) {
226
            return null;
227
        }
228
229 1
        if ($name === null) {
230
            return $_SESSION ?? [];
231
        }
232
233 1
        return (new Dot($_SESSION))->get($name);
234
    }
235
236
    /**
237
     * Reads and deletes a variable from session.
238
     *
239
     * @param string $name The key to read and remove (or a path as sent to Hash.extract).
240
     * @return mixed The value of the session variable, null if session not available,
241
     *   session not started, or provided name not found in the session.
242
     */
243 1
    public function consume(string $name)
244
    {
245 1
        if (empty($name)) {
246
            return null;
247
        }
248
249 1
        $value = $this->read($name);
250 1
        if ($value !== null) {
251 1
            $dot = new Dot($_SESSION);
252 1
            $dot->delete($name);
253 1
            $this->overwrite($_SESSION, (array)$dot->get());
254
        }
255
256 1
        return $value;
257
    }
258
259
    /**
260
     * Writes value to given session variable name.
261
     *
262
     * @param string|array $name Name of variable
263
     * @param mixed $value Value to write
264
     * @return void
265
     */
266 1
    public function write($name, $value = null): void
267
    {
268 1
        if (!$this->started()) {
269
            $this->start();
270
        }
271
272 1
        $write = $name;
273 1
        if (!is_array($name)) {
274 1
            $write = [$name => $value];
275
        }
276
277 1
        $data = new Dot($_SESSION ?? []);
278 1
        foreach ($write as $key => $val) {
279 1
            $data->add($key, $val);
280
        }
281
282 1
        $this->overwrite($_SESSION, $data->get());
283 1
    }
284
285
    /**
286
     * Returns the current sessions id
287
     *
288
     * @return string
289
     */
290
    public function id(): string
291
    {
292
        return (string)session_id();
293
    }
294
295
    /**
296
     * Sets the session id
297
     *
298
     * Calling this method will not auto start the session. You might have to manually
299
     * assert a started session.
300
     *
301
     * Passing an id into it, you can also replace the session id if the session
302
     * has not already been started.
303
     *
304
     * Note that depending on the session handler, not all characters are allowed
305
     * within the session id. For example, the file session handler only allows
306
     * characters in the range a-z A-Z 0-9 , (comma) and - (minus).
307
     *
308
     * @param string $id Session Id
309
     * @return $this
310
     */
311 1
    public function setId(string $id): self
312
    {
313 1
        if (headers_sent()) {
314
            throw SessionException::headersAlreadySent();
315
        }
316
317 1
        session_id($id);
318
319 1
        return $this;
320
    }
321
322
    /**
323
     * Removes a variable from session.
324
     *
325
     * @param string $name Session variable to remove
326
     * @return void
327
     */
328 1
    public function delete(string $name): void
329
    {
330 1
        if ($this->check($name)) {
331 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...
332
        }
333 1
    }
334
335
    /**
336
     * Used to write new data to _SESSION, since PHP doesn't like us setting the _SESSION var itself.
337
     *
338
     * @param array $old Set of old variables => values
339
     * @param array $new New set of variable => value
340
     * @return void
341
     */
342 1
    protected function overwrite(&$old, $new)
343
    {
344 1
        if (!empty($old)) {
345 1
            foreach ($old as $key => $var) {
346 1
                if (!isset($new[$key])) {
347 1
                    unset($old[$key]);
348
                }
349
            }
350
        }
351
352 1
        foreach ($new as $key => $var) {
353 1
            $old[$key] = $var;
354
        }
355 1
    }
356
357
    /**
358
     * Helper method to destroy invalid sessions.
359
     *
360
     * @return void
361
     */
362
    public function destroy()
363
    {
364
        if ($this->exists() && !$this->started()) {
365
            $this->start();
366
        }
367
368
        if (!$this->isCli && session_status() === PHP_SESSION_ACTIVE) {
369
            session_destroy();
370
        }
371
372
        $_SESSION = [];
373
        $this->started = false;
374
    }
375
376
    /**
377
     * Clears the session.
378
     *
379
     * Optionally it also clears the session id and renews the session.
380
     *
381
     * @param bool $renew If session should be renewed, as well. Defaults to false.
382
     * @return void
383
     */
384
    public function clear(bool $renew = false)
385
    {
386
        $_SESSION = [];
387
        if ($renew) {
388
            $this->renew();
389
        }
390
    }
391
392
    /**
393
     * Returns whether a session exists
394
     *
395
     * @return bool
396
     */
397 1
    public function exists()
398
    {
399 1
        return !ini_get('session.use_cookies')
400 1
            || isset($_COOKIE[session_name()])
401 1
            || $this->isCli
402 1
            || (ini_get('session.use_trans_sid') && isset($_GET[session_name()]));
403
    }
404
405
    /**
406
     * Restarts this session.
407
     *
408
     * @return void
409
     */
410
    public function renew(): void
411
    {
412
        if (!$this->exists() || $this->isCli) {
413
            return;
414
        }
415
416
        $this->start();
417
        $params = session_get_cookie_params();
418
        setcookie(
419
            session_name(),
420
            '',
421
            time() - 42000,
422
            $params['path'],
423
            $params['domain'],
424
            $params['secure'],
425
            $params['httponly']
426
        );
427
428
        if (session_id()) {
429
            session_regenerate_id(true);
430
        }
431
    }
432
433
    /**
434
     * Returns true if the session is no longer valid because the last time it was
435
     * accessed was after the configured timeout.
436
     *
437
     * @return bool
438
     */
439
    public function hasExpired(): bool
440
    {
441
        $time = $this->time();
442
        $result = false;
443
444
        $checkTime = $time !== null && $this->lifetime > 0;
445
        if ($checkTime && (time() - (int)$time > $this->lifetime)) {
446
            $result = true;
447
        }
448
449
        $this->write('Config.time', time());
450
451
        return $result;
452
    }
453
}
454