Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
99.36% covered (success)
99.36%
155 / 156
97.37% covered (success)
97.37%
37 / 38
CRAP
0.00% covered (danger)
0.00%
0 / 1
Session
99.36% covered (success)
99.36%
155 / 156
97.37% covered (success)
97.37%
37 / 38
72
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 __destruct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 __get
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 __set
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 __isset
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 __unset
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setOptions
100.00% covered (success)
100.00%
26 / 26
100.00% covered (success)
100.00%
1 / 1
6
 getOptions
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 start
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
3
 activate
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 autoRegenerate
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
5
 clearFlash
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 clearTemp
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
5
 isActive
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 destroy
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 destroyCookie
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
2.00
 stop
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 abort
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 has
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getAll
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getMulti
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 set
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setMulti
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 remove
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 removeMulti
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 removeAll
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 regenerateId
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 reset
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFlash
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 setFlash
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 removeFlash
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 getTemp
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 setTemp
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 removeTemp
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 id
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 gc
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setDebugCollector
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
1<?php declare(strict_types=1);
2/*
3 * This file is part of Aplus Framework Session Library.
4 *
5 * (c) Natan Felles <natanfelles@gmail.com>
6 *
7 * For the full copyright and license information, please view the LICENSE
8 * file that was distributed with this source code.
9 */
10namespace Framework\Session;
11
12use Framework\Session\Debug\SessionCollector;
13use JetBrains\PhpStorm\Pure;
14use LogicException;
15use RuntimeException;
16
17/**
18 * Class Session.
19 *
20 * @package session
21 */
22class Session
23{
24    /**
25     * @var array<string,mixed>
26     */
27    protected array $options = [];
28    protected SaveHandler $saveHandler;
29    protected SessionCollector $debugCollector;
30
31    /**
32     * Session constructor.
33     *
34     * @param array<string,int|string> $options
35     * @param SaveHandler|null $handler
36     */
37    public function __construct(array $options = [], SaveHandler $handler = null)
38    {
39        $this->setOptions($options);
40        if ($handler) {
41            $this->saveHandler = $handler;
42            \session_set_save_handler($handler);
43        }
44    }
45
46    public function __destruct()
47    {
48        $this->stop();
49    }
50
51    public function __get(string $key) : mixed
52    {
53        return $this->get($key);
54    }
55
56    public function __set(string $key, mixed $value) : void
57    {
58        $this->set($key, $value);
59    }
60
61    public function __isset(string $key) : bool
62    {
63        return $this->has($key);
64    }
65
66    public function __unset(string $key) : void
67    {
68        $this->remove($key);
69    }
70
71    /**
72     * @see http://php.net/manual/en/session.security.ini.php
73     *
74     * @param array<string,int|string> $custom
75     */
76    protected function setOptions(array $custom) : void
77    {
78        $serializer = \ini_get('session.serialize_handler');
79        $serializer = $serializer === 'php' ? 'php_serialize' : $serializer;
80        $secure = (isset($_SERVER['REQUEST_SCHEME']) && $_SERVER['REQUEST_SCHEME'] === 'https')
81            || (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on');
82        $default = [
83            'name' => 'session_id',
84            'serialize_handler' => $serializer,
85            'sid_bits_per_character' => 6,
86            'sid_length' => 48,
87            'cookie_domain' => '',
88            'cookie_httponly' => 1,
89            'cookie_lifetime' => 7200,
90            'cookie_path' => '/',
91            'cookie_samesite' => 'Strict',
92            'cookie_secure' => $secure,
93            'referer_check' => '',
94            'use_cookies' => 1,
95            'use_only_cookies' => 1,
96            'use_strict_mode' => 1,
97            'use_trans_sid' => 0,
98            // used to auto-regenerate the session id:
99            'auto_regenerate_maxlifetime' => 0,
100            'auto_regenerate_destroy' => true,
101        ];
102        $this->options = $custom
103            ? \array_replace($default, $custom)
104            : $default;
105    }
106
107    /**
108     * @param array<string,mixed> $custom
109     *
110     * @return array<string,mixed>
111     */
112    protected function getOptions(array $custom = []) : array
113    {
114        $options = $custom
115            ? \array_replace($this->options, $custom)
116            : $this->options;
117        unset(
118            $options['auto_regenerate_maxlifetime'],
119            $options['auto_regenerate_destroy']
120        );
121        return $options;
122    }
123
124    /**
125     * @param array<string,int|string> $customOptions
126     *
127     * @throws LogicException if session was already active
128     * @throws RuntimeException if session could not be started
129     *
130     * @return bool
131     */
132    public function start(array $customOptions = []) : bool
133    {
134        if ($this->isActive()) {
135            throw new LogicException('Session was already active');
136        }
137        if ( ! @\session_start($this->getOptions($customOptions))) {
138            throw new RuntimeException(
139                'Session could not be started: ' . \error_get_last()['message']
140            );
141        }
142        $time = \time();
143        $this->autoRegenerate($time);
144        $this->clearTemp($time);
145        $this->clearFlash();
146        return true;
147    }
148
149    /**
150     * Make sure the session is active.
151     *
152     * If it is not active, it will start it.
153     *
154     * @throws RuntimeException if session could not be started
155     *
156     * @return bool
157     */
158    public function activate() : bool
159    {
160        if ($this->isActive()) {
161            return true;
162        }
163        return $this->start();
164    }
165
166    /**
167     * Auto regenerate the session id.
168     *
169     * @param int $time
170     *
171     * @see https://owasp.org/www-community/attacks/Session_fixation
172     */
173    protected function autoRegenerate(int $time) : void
174    {
175        $maxlifetime = (int) $this->options['auto_regenerate_maxlifetime'];
176        $isActive = $maxlifetime > 0;
177        if (($isActive && empty($_SESSION['$']['regenerated_at']))
178            || ($isActive && $_SESSION['$']['regenerated_at'] < ($time - $maxlifetime))
179        ) {
180            $this->regenerateId((bool) $this->options['auto_regenerate_destroy']);
181        }
182    }
183
184    /**
185     * Clears the Flash Data.
186     */
187    protected function clearFlash() : void
188    {
189        unset($_SESSION['$']['flash']['old']);
190        if (isset($_SESSION['$']['flash']['new'])) {
191            foreach ($_SESSION['$']['flash']['new'] as $key => $value) {
192                $_SESSION['$']['flash']['old'][$key] = $value;
193            }
194        }
195        unset($_SESSION['$']['flash']['new']);
196        if (empty($_SESSION['$']['flash'])) {
197            unset($_SESSION['$']['flash']);
198        }
199    }
200
201    /**
202     * Clears the Temp Data.
203     *
204     * @param int $time The max time to temp data survive
205     */
206    protected function clearTemp(int $time) : void
207    {
208        if (isset($_SESSION['$']['temp'])) {
209            foreach ($_SESSION['$']['temp'] as $key => $value) {
210                if ($value['ttl'] < $time) {
211                    unset($_SESSION['$']['temp'][$key]);
212                }
213            }
214        }
215        if (empty($_SESSION['$']['temp'])) {
216            unset($_SESSION['$']['temp']);
217        }
218    }
219
220    /**
221     * Tells if sessions are enabled, and one exists.
222     *
223     * @return bool
224     */
225    public function isActive() : bool
226    {
227        return \session_status() === \PHP_SESSION_ACTIVE;
228    }
229
230    /**
231     * Destroys all data registered to a session.
232     *
233     * @return bool true on success or false on failure
234     */
235    public function destroy() : bool
236    {
237        if ($this->isActive()) {
238            $destroyed = \session_destroy();
239        }
240        unset($_SESSION);
241        return $destroyed ?? true;
242    }
243
244    /**
245     * Sets a Cookie with the session name to be destroyed in the user-agent.
246     *
247     * @throws RuntimeException If it could not get the session name
248     *
249     * @return bool True if the Set-Cookie header was set to invalidate the
250     * session cookie, false if output exists
251     */
252    public function destroyCookie() : bool
253    {
254        $name = \session_name();
255        if ($name === false) {
256            throw new RuntimeException('Could not get the session name');
257        }
258        $params = \session_get_cookie_params();
259        return \setcookie($name, '', [
260            'expires' => 0,
261            'path' => $params['path'],
262            'domain' => $params['domain'],
263            'secure' => $params['secure'],
264            'httponly' => $params['httponly'],
265            'samesite' => $params['samesite'],
266        ]);
267    }
268
269    /**
270     * Write session data and end session.
271     *
272     * @return bool returns true on success or false on failure
273     */
274    public function stop() : bool
275    {
276        if ($this->isActive()) {
277            $closed = \session_write_close();
278        }
279        return $closed ?? true;
280    }
281
282    /**
283     * Discard session data changes and end session.
284     *
285     * @return bool returns true on success or false on failure
286     */
287    public function abort() : bool
288    {
289        if ($this->isActive()) {
290            $aborted = \session_abort();
291        }
292        return $aborted ?? true;
293    }
294
295    /**
296     * Tells if the session has an item.
297     *
298     * @param string $key The item key name
299     *
300     * @return bool True if it has, otherwise false
301     */
302    #[Pure]
303    public function has(string $key) : bool
304    {
305        return isset($_SESSION[$key]);
306    }
307
308    /**
309     * Gets one session item.
310     *
311     * @param string $key The item key name
312     *
313     * @return mixed The item value or null if no set
314     */
315    #[Pure]
316    public function get(string $key) : mixed
317    {
318        return $_SESSION[$key] ?? null;
319    }
320
321    /**
322     * Get all session items.
323     *
324     * @return array<mixed> The value of the $_SESSION global
325     */
326    #[Pure]
327    public function getAll() : array
328    {
329        return $_SESSION;
330    }
331
332    /**
333     * Get multiple session items.
334     *
335     * @param array<string> $keys An array of key item names
336     *
337     * @return array<string,mixed> An associative array with items keys and
338     * values. Item not set will return as null.
339     */
340    #[Pure]
341    public function getMulti(array $keys) : array
342    {
343        $items = [];
344        foreach ($keys as $key) {
345            $items[$key] = $this->get($key);
346        }
347        return $items;
348    }
349
350    /**
351     * Set a session item.
352     *
353     * @param string $key The item key name
354     * @param mixed $value The item value
355     *
356     * @rerun static
357     */
358    public function set(string $key, mixed $value) : static
359    {
360        $_SESSION[$key] = $value;
361        return $this;
362    }
363
364    /**
365     * Set multiple session items.
366     *
367     * @param array<string,mixed> $items An associative array of items keys and
368     * values
369     *
370     * @rerun static
371     */
372    public function setMulti(array $items) : static
373    {
374        foreach ($items as $key => $value) {
375            $this->set($key, $value);
376        }
377        return $this;
378    }
379
380    /**
381     * Remove (unset) a session item.
382     *
383     * @param string $key The item key name
384     *
385     * @rerun static
386     */
387    public function remove(string $key) : static
388    {
389        unset($_SESSION[$key]);
390        return $this;
391    }
392
393    /**
394     * Remove (unset) multiple session items.
395     *
396     * @param array<string> $keys A list of items keys names
397     *
398     * @rerun static
399     */
400    public function removeMulti(array $keys) : static
401    {
402        foreach ($keys as $key) {
403            $this->remove($key);
404        }
405        return $this;
406    }
407
408    /**
409     * Remove (unset) all session items.
410     *
411     * @rerun static
412     */
413    public function removeAll() : static
414    {
415        @\session_unset();
416        $_SESSION = [];
417        return $this;
418    }
419
420    /**
421     * Update the current session id with a newly generated one.
422     *
423     * @param bool $deleteOldSession Whether to delete the old associated session item or not
424     *
425     * @return bool
426     */
427    public function regenerateId(bool $deleteOldSession = false) : bool
428    {
429        $regenerated = \session_regenerate_id($deleteOldSession);
430        if ($regenerated) {
431            $_SESSION['$']['regenerated_at'] = \time();
432        }
433        return $regenerated;
434    }
435
436    /**
437     * Re-initialize session array with original values.
438     *
439     * @return bool true if the session was successfully reinitialized or false on failure
440     */
441    public function reset() : bool
442    {
443        return \session_reset();
444    }
445
446    /**
447     * Get a Flash Data item.
448     *
449     * @param string $key The Flash item key name
450     *
451     * @return mixed The item value or null if not exists
452     */
453    #[Pure]
454    public function getFlash(string $key) : mixed
455    {
456        return $_SESSION['$']['flash']['new'][$key]
457            ?? $_SESSION['$']['flash']['old'][$key]
458            ?? null;
459    }
460
461    /**
462     * Set a Flash Data item, available only in the next time the session is started.
463     *
464     * @param string $key The Flash Data item key name
465     * @param mixed $value The item value
466     *
467     * @rerun static
468     */
469    public function setFlash(string $key, mixed $value) : static
470    {
471        $_SESSION['$']['flash']['new'][$key] = $value;
472        return $this;
473    }
474
475    /**
476     * Remove a Flash Data item.
477     *
478     * @param string $key The item key name
479     *
480     * @rerun static
481     */
482    public function removeFlash(string $key) : static
483    {
484        unset(
485            $_SESSION['$']['flash']['old'][$key],
486            $_SESSION['$']['flash']['new'][$key]
487        );
488        return $this;
489    }
490
491    /**
492     * Get a Temp Data item.
493     *
494     * @param string $key The item key name
495     *
496     * @return mixed The item value or null if it is expired or not set
497     */
498    public function getTemp(string $key) : mixed
499    {
500        if (isset($_SESSION['$']['temp'][$key])) {
501            if ($_SESSION['$']['temp'][$key]['ttl'] > \time()) {
502                return $_SESSION['$']['temp'][$key]['data'];
503            }
504            unset($_SESSION['$']['temp'][$key]);
505        }
506        return null;
507    }
508
509    /**
510     * Set a Temp Data item.
511     *
512     * @param string $key The item key name
513     * @param mixed $value The item value
514     * @param int $ttl The Time-To-Live of the item, in seconds
515     *
516     * @rerun static
517     */
518    public function setTemp(string $key, mixed $value, int $ttl = 60) : static
519    {
520        $_SESSION['$']['temp'][$key] = [
521            'ttl' => \time() + $ttl,
522            'data' => $value,
523        ];
524        return $this;
525    }
526
527    /**
528     * Remove (unset) a Temp Data item.
529     *
530     * @param string $key The item key name
531     *
532     * @rerun static
533     */
534    public function removeTemp(string $key) : static
535    {
536        unset($_SESSION['$']['temp'][$key]);
537        return $this;
538    }
539
540    /**
541     * Get/Set the session id.
542     *
543     * @param string|null $newId [optional] The new session id
544     *
545     * @throws LogicException when trying to set a new id and the session is active
546     *
547     * @return false|string The old session id or false on failure. Note: If a
548     * $newId is set, it is accepted but not validated. When session_start is
549     * called, the id is only used if it is valid
550     */
551    public function id(string $newId = null) : string | false
552    {
553        if ($newId !== null && $this->isActive()) {
554            throw new LogicException(
555                'Session ID cannot be changed when a session is active'
556            );
557        }
558        return \session_id($newId);
559    }
560
561    /**
562     * Perform session data garbage collection.
563     *
564     * If return false, use {@see error_get_last()} to get error details.
565     *
566     * @return false|int Returns the number of deleted session data for success,
567     * false for failure
568     */
569    public function gc() : int | false
570    {
571        return @\session_gc();
572    }
573
574    public function setDebugCollector(SessionCollector $collector) : static
575    {
576        $this->debugCollector = $collector;
577        $this->debugCollector->setSession($this)->setOptions($this->options);
578        if (isset($this->saveHandler)) {
579            $this->debugCollector->setSaveHandler($this->saveHandler);
580        }
581        return $this;
582    }
583}