Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
77.78% covered (warning)
77.78%
42 / 54
91.67% covered (success)
91.67%
11 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
HttpSession
77.78% covered (warning)
77.78%
42 / 54
91.67% covered (success)
91.67%
11 / 12
41.55
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
7
 setCsrfProtection
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 populateFromRequest
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
30
 set
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
4
 get
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 lock
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isLocked
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 save
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 remove
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 getSID
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setSID
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 destroy
n/a
0 / 0
n/a
0 / 0
2
 __destruct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * Jingga
4 *
5 * PHP Version 8.1
6 *
7 * @package   phpOMS\DataStorage\Session
8 * @copyright Dennis Eichhorn
9 * @license   OMS License 2.0
10 * @version   1.0.0
11 * @link      https://jingga.app
12 */
13declare(strict_types=1);
14
15namespace phpOMS\DataStorage\Session;
16
17use phpOMS\Log\FileLogger;
18use phpOMS\Message\RequestAbstract;
19use phpOMS\Session\JWT;
20use phpOMS\Uri\UriFactory;
21
22/**
23 * Http session class.
24 *
25 * @package phpOMS\DataStorage\Session
26 * @license OMS License 2.0
27 * @link    https://jingga.app
28 * @since   1.0.0
29 *
30 * @SuppressWarnings(PHPMD.Superglobals)
31 */
32final class HttpSession implements SessionInterface
33{
34    /**
35     * Is session locked/already set.
36     *
37     * @var bool
38     * @since 1.0.0
39     */
40    private bool $isLocked = false;
41
42    /**
43     * Raw session data.
44     *
45     * @var array<string, mixed>
46     * @since 1.0.0
47     */
48    public array $data = [];
49
50    /**
51     * Session ID.
52     *
53     * @var string
54     * @since 1.0.0
55     */
56    public string $sid;
57
58    /**
59     * Inactivity Interval.
60     *
61     * @var int
62     * @since 1.0.0
63     */
64    public int $inactivityInterval = 0;
65
66    /**
67     * Constructor.
68     *
69     * @param int    $liftetime          Session life time
70     * @param string $sid                Session id
71     * @param int    $inactivityInterval Interval for session activity
72     *
73     * @since 1.0.0
74     */
75    public function __construct(int $liftetime = 3600, string $sid = '', int $inactivityInterval = 0)
76    {
77        if (\session_id()) {
78            \session_write_close(); // @codeCoverageIgnore
79        }
80
81        if ($sid !== '') {
82            \session_id((string) $sid); // @codeCoverageIgnore
83        }
84
85        $this->inactivityInterval = $inactivityInterval;
86
87        if (\session_status() !== \PHP_SESSION_ACTIVE && !\headers_sent()) {
88            // @codeCoverageIgnoreStart
89            \session_set_cookie_params([
90                'lifetime' => $liftetime,
91                'path'     => '/',
92                'domain'   => '',
93                'secure'   => false,
94                'httponly' => true,
95                'samesite' => 'Strict',
96            ]);
97            \session_start();
98            // @codeCoverageIgnoreEnd
99        } else {
100            $logger = FileLogger::getInstance();
101            $logger->error(
102                FileLogger::MSG_FULL, [
103                    'message' => 'Bad application flow.',
104                    'line'    => __LINE__,
105                    'file'    => self::class,
106                ]
107            );
108        }
109
110        if ($this->inactivityInterval > 0
111            && ($this->inactivityInterval + ($_SESSION['lastActivity'] ?? 0) < \time())
112        ) {
113            $this->destroy(); // @codeCoverageIgnore
114        }
115
116        $this->data                 = $_SESSION ?? [];
117        $_SESSION                   = null;
118        $this->data['lastActivity'] = \time();
119        $this->sid                  = (string) \session_id();
120
121        $this->setCsrfProtection();
122    }
123
124    /**
125     * Set Csrf protection for forms.
126     *
127     * @return void
128     *
129     * @since 1.0.0
130     */
131    private function setCsrfProtection() : void
132    {
133        $this->set('UID', 0, false);
134
135        if (($csrf = $this->get('CSRF')) === null) {
136            $csrf = \bin2hex(\random_bytes(32));
137            $this->set('CSRF', $csrf, false);
138        }
139
140        UriFactory::setQuery('$CSRF', $csrf); /* @phpstan-ignore-line */
141    }
142
143    /**
144     * Populate the session from the request.
145     *
146     * This is only used when the session data is stored in the request itself (e.g. JWT)
147     *
148     * @param string          $secret  Secret to validate the request
149     * @param RequestAbstract $request Request
150     *
151     * @return void
152     *
153     * @since 1.0.0
154     */
155    public function populateFromRequest(string $secret, RequestAbstract $request) : void
156    {
157        $authentication = $request->header->get('Authorization');
158        if (\count($authentication) !== 1) {
159            return;
160        }
161
162        $explode = \explode(' ', $authentication[0]);
163        if (\count($explode) !== 2) {
164            return;
165        }
166
167        $token  = \trim($explode[1]);
168        $header = JWT::getHeader($token);
169
170        if (($header['typ'] ?? '') !== 'jwt' || !JWT::validateJWT($secret, $token)) {
171            return;
172        }
173
174        $payload = JWT::getPayload($token);
175        $this->set('UID', (int) ($payload['uid'] ?? 0));
176    }
177
178    /**
179     * {@inheritdoc}
180     */
181    public function set(string $key, mixed $value, bool $overwrite = false) : bool
182    {
183        if (!$this->isLocked && ($overwrite || !isset($this->data[$key]))) {
184            $this->data[$key] = $value;
185
186            return true;
187        }
188
189        return false;
190    }
191
192    /**
193     * {@inheritdoc}
194     */
195    public function get(string $key) : mixed
196    {
197        return $this->data[$key] ?? null;
198    }
199
200    /**
201     * {@inheritdoc}
202     */
203    public function lock() : void
204    {
205        $this->isLocked = true;
206    }
207
208    /**
209     * Check if session is locked.
210     *
211     * @return bool Lock status
212     *
213     * @since 1.0.0
214     */
215    public function isLocked() : bool
216    {
217        return $this->isLocked;
218    }
219
220    /**
221     * {@inheritdoc}
222     */
223    public function save() : bool
224    {
225        if ($this->isLocked) {
226            return false;
227        }
228
229        $_SESSION = $this->data;
230
231        return \session_write_close();
232    }
233
234    /**
235     * {@inheritdoc}
236     */
237    public function remove(string $key) : bool
238    {
239        if (!$this->isLocked && isset($this->data[$key])) {
240            unset($this->data[$key]);
241
242            return true;
243        }
244
245        return false;
246    }
247
248    /**
249     * {@inheritdoc}
250     */
251    public function getSID() : string
252    {
253        return $this->sid;
254    }
255
256    /**
257     * {@inheritdoc}
258     */
259    public function setSID(string $sid) : void
260    {
261        $this->sid = $sid;
262    }
263
264    /**
265     * Destroy the current session.
266     *
267     * @return void
268     *
269     * @since 1.0.0
270     * @codeCoverageIgnore
271     */
272    private function destroy() : void
273    {
274        if (\session_status() !== \PHP_SESSION_NONE) {
275            \session_destroy();
276            $this->data = [];
277            \session_start();
278        }
279    }
280
281    /**
282     * Destruct session.
283     *
284     * @since 1.0.0
285     */
286    public function __destruct()
287    {
288        $this->save();
289    }
290}