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