Completed
Branch 2.x (b1c655)
by Julián
07:53
created

Native::isCli()   A

Complexity

Conditions 1
Paths 1

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 1
eloc 2
nc 1
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
namespace Jgut\Middleware\Sessionware\Manager;
13
14
use Jgut\Middleware\Sessionware\Configuration;
15
use Jgut\Middleware\Sessionware\Handler\Handler;
16
use Jgut\Middleware\Sessionware\Handler\Native as NativeHandler;
17
use Jgut\Middleware\Sessionware\SessionIniSettingsTrait;
18
19
/**
20
 * Native PHP session manager.
21
 */
22
class Native implements Manager
23
{
24
    use SessionIniSettingsTrait;
25
26
    /**
27
     * @var Configuration
28
     */
29
    protected $configuration;
30
31
    /**
32
     * @var string
33
     */
34
    protected $sessionId;
35
36
    /**
37
     * @var bool
38
     */
39
    protected $sessionStarted = false;
40
41
    /**
42
     * Session manager constructor.
43
     *
44
     * @param Configuration $configuration
45
     * @param Handler       $sessionHandler
0 ignored issues
show
Documentation introduced by
Should the type for parameter $sessionHandler not be null|Handler?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
46
     *
47
     * @throws \RuntimeException
48
     */
49
    public function __construct(Configuration $configuration, Handler $sessionHandler = null)
50
    {
51
        if (session_status() === PHP_SESSION_DISABLED) {
52
            // @codeCoverageIgnoreStart
53
            throw new \RuntimeException('PHP sessions are disabled');
54
            // @codeCoverageIgnoreEnd
55
        }
56
57
        if (session_status() === PHP_SESSION_ACTIVE) {
58
            throw new \RuntimeException('Session has already been started. Check "session.auto_start" ini setting');
59
        }
60
61
        if (!$this->isCli()) {
62
            // @codeCoverageIgnoreStart
63
            $this->verifyIniSettings();
64
            // @codeCoverageIgnoreEnd
65
        }
66
67
        $this->configuration = $configuration;
68
69
        $this->configureSessionSerializer();
70
        $this->configureSessionGarbageCollector();
71
        $this->configureSessionSaveHandler($sessionHandler);
72
    }
73
74
    /**
75
     * {@inheritdoc}
76
     */
77
    public function getConfiguration()
78
    {
79
        return $this->configuration;
80
    }
81
82
    /**
83
     * {@inheritdoc}
84
     */
85
    public function getSessionId()
86
    {
87
        return $this->sessionStarted ? $this->sessionId : null;
88
    }
89
90
    /**
91
     * {@inheritdoc}
92
     *
93
     * @throws \RuntimeException
94
     */
95
    public function setSessionId($sessionId)
96
    {
97
        if ($this->sessionStarted) {
98
            throw new \RuntimeException('Session identifier cannot be manually altered once session is started');
99
        }
100
101
        $this->sessionId = $sessionId;
102
103
        return $this;
104
    }
105
106
    /**
107
     * {@inheritdoc}
108
     *
109
     * @throws \RuntimeException
110
     */
111
    public function sessionStart()
112
    {
113
        if ($this->sessionStarted) {
114
            return;
115
        }
116
117
        if (session_status() === PHP_SESSION_ACTIVE) {
118
            throw new \RuntimeException('Session has already been started. Check "session.auto_start" ini setting');
119
        }
120
121
        if (headers_sent($file, $line)) {
122
            throw new \RuntimeException(
123
                sprintf(
124
                    'PHP session failed to start because headers have already been sent by "%s" at line %d.',
125
                    $file,
126
                    $line
127
                )
128
            );
129
        }
130
131
        $this->sessionInitialize();
132
133
        $this->sessionStarted = true;
134
    }
135
136
    /**
137
     * Initialize session.
138
     *
139
     * @throws \RuntimeException
140
     */
141
    final protected function sessionInitialize()
142
    {
143
        if ($this->sessionId) {
144
            session_id($this->sessionId);
145
        }
146
147
        session_start();
148
149
        if (session_status() !== PHP_SESSION_ACTIVE) {
150
            // @codeCoverageIgnoreStart
151
            throw new \RuntimeException('PHP session failed to start');
152
            // @codeCoverageIgnoreEnd
153
        }
154
155
        if (!$this->sessionId) {
156
            $this->sessionId = session_id();
157
        }
158
    }
159
160
    /**
161
     * {@inheritdoc}
162
     *
163
     * @throws \RuntimeException
164
     *
165
     * @SuppressWarnings(PHPMD.Superglobals)
166
     */
167
    public function loadSessionData()
168
    {
169
        if (!$this->sessionStarted) {
170
            throw new \RuntimeException('Cannot load data from a not started session');
171
        }
172
173
        if (!isset($_SESSION)) {
174
            return [];
175
        }
176
177
        $keyPattern = '/^' . $this->configuration->getName() . '\./';
178
        $data = [];
179
        foreach ($_SESSION as $key => $value) {
180
            if (preg_match($keyPattern, $key)) {
181
                $data[preg_replace($keyPattern, '', $key)] = $value;
182
            }
183
        }
184
185
        unset($_SESSION);
186
187
        return $data;
188
    }
189
190
    /**
191
     * {@inheritdoc}
192
     *
193
     * @throws \RuntimeException
194
     *
195
     * @SuppressWarnings(PMD.Superglobals)
196
     */
197
    public function sessionEnd(array $data = [])
198
    {
199
        if (!$this->sessionStarted) {
200
            throw new \RuntimeException('Cannot end a not started session');
201
        }
202
203
        $keyPrefix = $this->configuration->getName();
204
        $sessionData = [];
205
        foreach ($data as $key => $value) {
206
            $sessionData[$keyPrefix . '.' . $key] = $value;
207
        }
208
        $_SESSION = $sessionData;
209
210
        session_write_close();
211
212
        unset($_SESSION);
213
214
        $this->sessionStarted = false;
215
        $this->sessionId = null;
216
217
        if (session_status() === PHP_SESSION_ACTIVE) {
218
            // @codeCoverageIgnoreStart
219
            throw new \RuntimeException('PHP session failed to finish');
220
            // @codeCoverageIgnoreEnd
221
        }
222
    }
223
224
    /**
225
     * {@inheritdoc}
226
     *
227
     * @throws \RuntimeException
228
     *
229
     * @SuppressWarnings(PMD.Superglobals)
230
     */
231
    public function sessionReset()
232
    {
233
        if (!$this->sessionStarted) {
234
            throw new \RuntimeException('Cannot reset a not started session');
235
        }
236
237
        unset($_SESSION);
238
        session_unset();
239
        session_destroy();
240
241
        if (session_status() === PHP_SESSION_ACTIVE) {
242
            // @codeCoverageIgnoreStart
243
            throw new \RuntimeException('PHP session failed to finish');
244
            // @codeCoverageIgnoreEnd
245
        }
246
247
        $this->sessionStarted = false;
248
249
        $this->sessionId = $this->getNewSessionId();
250
251
        $this->sessionStart();
252
    }
253
254
    /**
255
     * {@inheritdoc}
256
     */
257
    public function isSessionStarted()
258
    {
259
        return $this->sessionStarted;
260
    }
261
262
    /**
263
     * {@inheritdoc}
264
     */
265
    public function shouldRegenerate()
266
    {
267
        return strlen($this->sessionId) !== Configuration::SESSION_ID_LENGTH;
268
    }
269
270
    /**
271
     * Check if running on CLI.
272
     *
273
     * @return bool
274
     */
275
    protected function isCli()
276
    {
277
        return \PHP_SAPI === 'cli';
278
    }
279
280
    /**
281
     * Verify session ini settings.
282
     *
283
     * @throws \RuntimeException
284
     *
285
     * @codeCoverageIgnore
286
     */
287
    final protected function verifyIniSettings()
288
    {
289
        if ($this->hasBoolIniSetting('use_trans_sid') !== false) {
290
            throw new \RuntimeException('"session.use_trans_sid" ini setting must be set to false');
291
        }
292
293
        if ($this->hasBoolIniSetting('use_cookies') !== true) {
294
            throw new \RuntimeException('"session.use_cookies" ini setting must be set to true');
295
        }
296
297
        if ($this->hasBoolIniSetting('use_only_cookies') !== true) {
298
            throw new \RuntimeException('"session.use_only_cookies" ini setting must be set to true');
299
        }
300
301
        if ($this->hasBoolIniSetting('use_strict_mode') !== false) {
302
            throw new \RuntimeException('"session.use_strict_mode" ini setting must be set to false');
303
        }
304
305
        if ($this->hasBoolIniSetting('cache_limiter') !== null) {
306
            throw new \RuntimeException('"session.cache_limiter" ini setting must be set to empty string');
307
        }
308
    }
309
310
    /**
311
     * Configure session data serializer.
312
     */
313
    protected function configureSessionSerializer()
314
    {
315
        // Use better session serializer when available
316
        if ($this->getIniSetting('serialize_handler') !== 'php_serialize'
317
            && version_compare(PHP_VERSION, '5.5.4', '>=')
318
        ) {
319
            // @codeCoverageIgnoreStart
320
            $this->setIniSetting('serialize_handler', 'php_serialize');
321
            // @codeCoverageIgnoreEnd
322
        }
323
    }
324
325
    /**
326
     * Configure session timeout.
327
     */
328
    protected function configureSessionGarbageCollector()
329
    {
330
        $this->setIniSetting('gc_maxlifetime', $this->configuration->getLifetime());
331
    }
332
333
    /**
334
     * Configure session save handler.
335
     *
336
     * @param Handler $sessionHandler
0 ignored issues
show
Documentation introduced by
Should the type for parameter $sessionHandler not be null|Handler?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
337
     */
338
    protected function configureSessionSaveHandler(Handler $sessionHandler = null)
339
    {
340
        if ($sessionHandler === null) {
341
            $sessionHandler = new NativeHandler();
342
        }
343
344
        $sessionHandler->setConfiguration($this->configuration);
345
346
        session_register_shutdown();
347
        session_set_save_handler($sessionHandler, false);
348
    }
349
350
    /**
351
     * Generates cryptographically secure session identifier.
352
     *
353
     * @param int $length
354
     *
355
     * @return string
356
     */
357
    final protected function getNewSessionId($length = Configuration::SESSION_ID_LENGTH)
358
    {
359
        return substr(
360
            preg_replace('/[^a-zA-Z0-9-]+/', '', base64_encode(random_bytes((int) $length))),
361
            0,
362
            (int) $length
363
        );
364
    }
365
}
366