Completed
Branch 2.x (03096d)
by Julián
08:36
created

Native::getSessionId()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 2
eloc 2
nc 2
nop 0
1
<?php
2
3
/*
4
 * sessionware (https://github.com/juliangut/sessionware).
5
 * PSR7 session management middleware.
6
 *
7
 * @license BSD-3-Clause
8
 * @link https://github.com/juliangut/sessionware
9
 * @author Julián Gutiérrez <[email protected]>
10
 */
11
12
declare(strict_types=1);
13
14
namespace Jgut\Sessionware\Manager;
15
16
use Jgut\Sessionware\Configuration;
17
use Jgut\Sessionware\Handler\Handler;
18
use Jgut\Sessionware\Handler\Native as NativeHandler;
19
use Jgut\Sessionware\Traits\NativeSessionTrait;
20
21
/**
22
 * Native PHP session manager.
23
 */
24
class Native implements Manager
25
{
26
    use NativeSessionTrait;
27
28
    /**
29
     * @var Configuration
30
     */
31
    protected $configuration;
32
33
    /**
34
     * Session handler.
35
     *
36
     * @var Handler
37
     */
38
    protected $sessionHandler;
39
40
    /**
41
     * @var string
42
     */
43
    protected $sessionId;
44
45
    /**
46
     * @var bool
47
     */
48
    protected $sessionStarted = false;
49
50
    /**
51
     * Session has been destroyed.
52
     *
53
     * @var bool
54
     */
55
    protected $sessionDestroyed = false;
56
57
    /**
58
     * Session manager constructor.
59
     *
60
     * @param Configuration $configuration
61
     * @param Handler|null  $sessionHandler
62
     *
63
     * @throws \RuntimeException
64
     */
65
    public function __construct(Configuration $configuration, Handler $sessionHandler = null)
66
    {
67
        if (session_status() === PHP_SESSION_DISABLED) {
68
            // @codeCoverageIgnoreStart
69
            throw new \RuntimeException('PHP sessions are disabled');
70
            // @codeCoverageIgnoreEnd
71
        }
72
73
        $this->configuration = $configuration;
74
75
        if ($sessionHandler === null) {
76
            $sessionHandler = new NativeHandler();
77
        }
78
        $sessionHandler->setConfiguration($configuration);
79
80
        $this->sessionHandler = $sessionHandler;
81
    }
82
83
    /**
84
     * {@inheritdoc}
85
     */
86
    public function getConfiguration() : Configuration
87
    {
88
        return $this->configuration;
89
    }
90
91
    /**
92
     * {@inheritdoc}
93
     */
94
    public function getSessionId() : string
95
    {
96
        return $this->sessionStarted ? $this->sessionId : '';
97
    }
98
99
    /**
100
     * {@inheritdoc}
101
     *
102
     * @throws \RuntimeException
103
     */
104
    public function setSessionId(string $sessionId)
105
    {
106
        if ($this->sessionStarted) {
107
            throw new \RuntimeException('Session identifier cannot be manually altered once session is started');
108
        }
109
110
        $this->sessionId = $sessionId;
111
    }
112
113
    /**
114
     * {@inheritdoc}
115
     *
116
     * @throws \RuntimeException
117
     *
118
     * @return array|null
119
     */
120
    public function sessionStart() : array
121
    {
122
        $this->verifyIniSettings();
123
124
        if ($this->sessionDestroyed) {
125
            throw new \RuntimeException('Cannot start a session that has been previously destroyed');
126
        }
127
128
        if ($this->sessionStarted || session_status() === PHP_SESSION_ACTIVE) {
129
            throw new \RuntimeException('Session has already been started. Check "session.auto_start" ini setting');
130
        }
131
132
        if (headers_sent($file, $line)) {
133
            throw new \RuntimeException(
134
                sprintf(
135
                    'PHP session failed to start because headers have already been sent by "%s" at line %d.',
136
                    $file,
137
                    $line
138
                )
139
            );
140
        }
141
142
        $this->configureSession();
143
        $this->initializeSession();
144
145
        $this->sessionStarted = true;
146
147
        return $this->loadSessionData();
148
    }
149
150
    /**
151
     * Configure session settings.
152
     */
153
    protected function configureSession()
154
    {
155
        // Use better session serializer when available
156
        if ($this->getIniSetting('serialize_handler') !== 'php_serialize') {
157
            // @codeCoverageIgnoreStart
158
            $this->setIniSetting('serialize_handler', 'php_serialize');
159
            // @codeCoverageIgnoreEnd
160
        }
161
162
        $this->setIniSetting('gc_maxlifetime', $this->configuration->getLifetime());
163
164
        session_register_shutdown();
165
        session_set_save_handler($this->sessionHandler, false);
166
    }
167
168
    /**
169
     * Initialize session.
170
     *
171
     * @throws \RuntimeException
172
     */
173
    final protected function initializeSession()
174
    {
175
        if ($this->sessionId) {
176
            session_id($this->sessionId);
177
        }
178
179
        session_name($this->configuration->getName());
180
181
        session_start();
182
183
        if (session_status() !== PHP_SESSION_ACTIVE) {
184
            // @codeCoverageIgnoreStart
185
            throw new \RuntimeException('PHP session failed to start');
186
            // @codeCoverageIgnoreEnd
187
        }
188
189
        if (!$this->sessionId) {
190
            $this->sessionId = session_id();
191
        }
192
    }
193
194
    /**
195
     * Retrieve session saved data.
196
     *
197
     * @return array
198
     *
199
     * @SuppressWarnings(PHPMD.Superglobals)
200
     */
201
    final protected function loadSessionData()
202
    {
203
        $keyPattern = '/^' . $this->configuration->getName() . '\./';
204
        $data = [];
205
        foreach ($_SESSION as $key => $value) {
206
            if (preg_match($keyPattern, $key)) {
207
                $data[preg_replace($keyPattern, '', $key)] = $value;
208
            }
209
        }
210
211
        $_SESSION = null;
212
213
        return $data;
214
    }
215
216
    /**
217
     * {@inheritdoc}
218
     *
219
     * @throws \RuntimeException
220
     *
221
     * @SuppressWarnings(PMD.Superglobals)
222
     */
223
    public function sessionRegenerateId()
224
    {
225
        if (!$this->sessionStarted) {
226
            throw new \RuntimeException('Cannot regenerate id a not started session');
227
        }
228
229
        $_SESSION = null;
230
        session_unset();
231
        session_destroy();
232
233
        $this->sessionStarted = false;
234
235
        if (session_status() === PHP_SESSION_ACTIVE) {
236
            // @codeCoverageIgnoreStart
237
            throw new \RuntimeException('PHP session failed to regenerate id');
238
            // @codeCoverageIgnoreEnd
239
        }
240
241
        $this->sessionId = $this->getNewSessionId();
242
243
        $this->sessionStart();
244
    }
245
246
    /**
247
     * {@inheritdoc}
248
     *
249
     * @throws \RuntimeException
250
     *
251
     * @SuppressWarnings(PMD.Superglobals)
252
     */
253
    public function sessionEnd(array $data = [])
254
    {
255
        if (!$this->sessionStarted) {
256
            throw new \RuntimeException('Cannot end a not started session');
257
        }
258
259
        $keyPrefix = $this->configuration->getName();
260
        $sessionData = [];
261
        foreach ($data as $key => $value) {
262
            $sessionData[$keyPrefix . '.' . $key] = $value;
263
        }
264
        $_SESSION = $sessionData;
265
266
        session_write_close();
267
268
        $_SESSION = null;
269
270
        $this->sessionStarted = false;
271
        $this->sessionId = null;
272
273
        if (session_status() === PHP_SESSION_ACTIVE) {
274
            // @codeCoverageIgnoreStart
275
            throw new \RuntimeException('PHP session failed to finish');
276
            // @codeCoverageIgnoreEnd
277
        }
278
    }
279
280
    /**
281
     * {@inheritdoc}
282
     *
283
     * @throws \RuntimeException
284
     *
285
     * @SuppressWarnings(PMD.Superglobals)
286
     */
287
    public function sessionDestroy()
288
    {
289
        if (!$this->sessionStarted) {
290
            throw new \RuntimeException('Cannot destroy a not started session');
291
        }
292
293
        unset($_SESSION);
294
        session_unset();
295
        session_destroy();
296
297
        $this->sessionStarted = false;
298
        $this->sessionDestroyed = true;
299
300
        if (session_status() === PHP_SESSION_ACTIVE) {
301
            // @codeCoverageIgnoreStart
302
            throw new \RuntimeException('PHP session failed to finish');
303
            // @codeCoverageIgnoreEnd
304
        }
305
    }
306
307
    /**
308
     * {@inheritdoc}
309
     */
310
    public function isSessionStarted() : bool
311
    {
312
        return $this->sessionStarted;
313
    }
314
315
    /**
316
     * {@inheritdoc}
317
     */
318
    public function isSessionDestroyed() : bool
319
    {
320
        return $this->sessionDestroyed;
321
    }
322
323
    /**
324
     * {@inheritdoc}
325
     */
326
    public function shouldRegenerateId() : bool
327
    {
328
        return strlen($this->sessionId) !== Configuration::SESSION_ID_LENGTH;
329
    }
330
331
    /**
332
     * Verify session ini settings.
333
     *
334
     * @throws \RuntimeException
335
     */
336
    final protected function verifyIniSettings()
337
    {
338
        if ($this->hasBoolIniSetting('use_trans_sid') !== false) {
339
            throw new \RuntimeException('"session.use_trans_sid" ini setting must be set to false');
340
        }
341
342
        if ($this->hasBoolIniSetting('use_cookies') !== true) {
343
            throw new \RuntimeException('"session.use_cookies" ini setting must be set to true');
344
        }
345
346
        if ($this->hasBoolIniSetting('use_only_cookies') !== true) {
347
            throw new \RuntimeException('"session.use_only_cookies" ini setting must be set to true');
348
        }
349
350
        if ($this->hasBoolIniSetting('use_strict_mode') !== false) {
351
            throw new \RuntimeException('"session.use_strict_mode" ini setting must be set to false');
352
        }
353
354
        if ($this->getStringIniSetting('cache_limiter') !== '') {
355
            throw new \RuntimeException('"session.cache_limiter" ini setting must be set to empty string');
356
        }
357
    }
358
359
    /**
360
     * Generates cryptographically secure session identifier.
361
     *
362
     * @param int $length
363
     *
364
     * @return string
365
     */
366
    private function getNewSessionId($length = Configuration::SESSION_ID_LENGTH)
367
    {
368
        return substr(
369
            preg_replace('/[^a-zA-Z0-9-]+/', '', base64_encode(random_bytes((int) $length))),
370
            0,
371
            (int) $length
372
        );
373
    }
374
}
375