Completed
Branch 2.x (525e63)
by Julián
09:21
created

Native::configureSession()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 14
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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