Completed
Branch 2.x (8ca90a)
by Julián
08:08
created

Native::configureIniSettings()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 8
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 6
nc 1
nop 0
1
<?php
2
3
/*
4
 * sessionware (https://github.com/juliangut/sessionware).
5
 * PSR7 compatible session management.
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->getStringIniSetting('serialize_handler') !== 'php_serialize') {
157
            // @codeCoverageIgnoreStart
158
            $this->setIniSetting('serialize_handler', 'php_serialize');
159
            // @codeCoverageIgnoreEnd
160
        }
161
162
        $this->setIniSetting('gc_maxlifetime', (string) $this->configuration->getLifetime());
163
164
        session_set_save_handler($this->sessionHandler, false);
165
        session_module_name('user');
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
272
        if (session_status() === PHP_SESSION_ACTIVE) {
273
            // @codeCoverageIgnoreStart
274
            throw new \RuntimeException('PHP session failed to finish');
275
            // @codeCoverageIgnoreEnd
276
        }
277
    }
278
279
    /**
280
     * {@inheritdoc}
281
     *
282
     * @throws \RuntimeException
283
     *
284
     * @SuppressWarnings(PMD.Superglobals)
285
     */
286
    public function sessionDestroy()
287
    {
288
        if (!$this->sessionStarted) {
289
            throw new \RuntimeException('Cannot destroy a not started session');
290
        }
291
292
        unset($_SESSION);
293
        session_unset();
294
        session_destroy();
295
296
        $this->sessionStarted = false;
297
        $this->sessionDestroyed = true;
298
299
        if (session_status() === PHP_SESSION_ACTIVE) {
300
            // @codeCoverageIgnoreStart
301
            throw new \RuntimeException('PHP session failed to finish');
302
            // @codeCoverageIgnoreEnd
303
        }
304
    }
305
306
    /**
307
     * {@inheritdoc}
308
     */
309
    public function isSessionStarted() : bool
310
    {
311
        return $this->sessionStarted;
312
    }
313
314
    /**
315
     * {@inheritdoc}
316
     */
317
    public function isSessionDestroyed() : bool
318
    {
319
        return $this->sessionDestroyed;
320
    }
321
322
    /**
323
     * {@inheritdoc}
324
     */
325
    public function shouldRegenerateId() : bool
326
    {
327
        return strlen($this->sessionId) !== Configuration::SESSION_ID_LENGTH;
328
    }
329
330
    /**
331
     * Verify session ini settings.
332
     *
333
     * @throws \RuntimeException
334
     */
335
    final protected function verifyIniSettings()
336
    {
337
        if ($this->hasBoolIniSetting('use_trans_sid') !== false) {
338
            throw new \RuntimeException('"session.use_trans_sid" ini setting must be set to false');
339
        }
340
341
        if ($this->hasBoolIniSetting('use_cookies') !== true) {
342
            throw new \RuntimeException('"session.use_cookies" ini setting must be set to true');
343
        }
344
345
        if ($this->hasBoolIniSetting('use_only_cookies') !== true) {
346
            throw new \RuntimeException('"session.use_only_cookies" ini setting must be set to true');
347
        }
348
349
        if ($this->hasBoolIniSetting('use_strict_mode') !== false) {
350
            throw new \RuntimeException('"session.use_strict_mode" ini setting must be set to false');
351
        }
352
353
        if ($this->getStringIniSetting('cache_limiter') !== '') {
354
            throw new \RuntimeException('"session.cache_limiter" ini setting must be set to empty string');
355
        }
356
    }
357
358
    /**
359
     * Ini settings configuration helper.
360
     */
361
    final public function configureIniSettings()
362
    {
363
        $this->setIniSetting('use_trans_sid', '0');
364
        $this->setIniSetting('use_cookies', '1');
365
        $this->setIniSetting('use_only_cookies', '1');
366
        $this->setIniSetting('use_strict_mode', '0');
367
        $this->setIniSetting('cache_limiter', '');
368
    }
369
370
    /**
371
     * Generates cryptographically secure session identifier.
372
     *
373
     * @param int $length
374
     *
375
     * @return string
376
     */
377
    private function getNewSessionId($length = Configuration::SESSION_ID_LENGTH)
378
    {
379
        return substr(
380
            preg_replace('/[^a-zA-Z0-9-]+/', '', base64_encode(random_bytes((int) $length))),
381
            0,
382
            (int) $length
383
        );
384
    }
385
}
386