Completed
Branch 2.x (133470)
by Julián
06:39
created

Session::start()   B

Complexity

Conditions 5
Paths 9

Size

Total Lines 27
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 27
rs 8.439
c 0
b 0
f 0
cc 5
eloc 14
nc 9
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;
15
16
use Jgut\Sessionware\Manager\Manager;
17
use League\Event\EmitterAwareInterface;
18
use League\Event\EmitterTrait;
19
use League\Event\Event;
20
use Psr\Http\Message\ServerRequestInterface;
21
22
/**
23
 * Session helper.
24
 *
25
 * @SuppressWarnings(PMD.TooManyPublicMethods)
26
 * @SuppressWarnings(PMD.ExcessiveClassComplexity)
27
 */
28
class Session implements EmitterAwareInterface, \ArrayAccess
29
{
30
    use EmitterTrait;
31
32
    /**
33
     * Session configuration.
34
     *
35
     * @var Configuration
36
     */
37
    protected $configuration;
38
39
    /**
40
     * Session manager.
41
     *
42
     * @var Manager
43
     */
44
    protected $sessionManager;
45
46
    /**
47
     * Session initial data.
48
     *
49
     * @var Collection
50
     */
51
    protected $originalData;
52
53
    /**
54
     * Session data.
55
     *
56
     * @var Collection
57
     */
58
    protected $data;
59
60
    /**
61
     * Session shutdown method is registered.
62
     *
63
     * @var bool
64
     */
65
    protected $shutdownRegistered = false;
66
67
    /**
68
     * Session constructor.
69
     *
70
     * @param Manager $sessionManager
71
     * @param array   $initialData
72
     *
73
     * @throws \InvalidArgumentException
74
     */
75
    public function __construct(Manager $sessionManager, array $initialData = [])
76
    {
77
        $this->sessionManager = $sessionManager;
78
        $this->configuration = $sessionManager->getConfiguration();
79
80
        $this->data = new Collection($initialData);
81
        $this->originalData = clone $this->data;
82
    }
83
84
    /**
85
     * Has session been started.
86
     *
87
     * @return bool
88
     */
89
    public function isActive() : bool
90
    {
91
        return $this->sessionManager->isStarted();
92
    }
93
94
    /**
95
     * Has session been destroyed.
96
     *
97
     * @return bool
98
     */
99
    public function isDestroyed() : bool
100
    {
101
        return $this->sessionManager->isDestroyed();
102
    }
103
104
    /**
105
     * Get session identifier.
106
     *
107
     * @return string
108
     */
109
    public function getId() : string
110
    {
111
        return $this->sessionManager->getId();
112
    }
113
114
    /**
115
     * Set session identifier.
116
     *
117
     * @param string $sessionId
118
     *
119
     * @throws \RuntimeException
120
     */
121
    public function setId(string $sessionId)
122
    {
123
        if ($this->isActive() || $this->isDestroyed()) {
124
            throw new \RuntimeException('Cannot set session id on started or destroyed sessions');
125
        }
126
127
        $this->sessionManager->setId($sessionId);
128
    }
129
130
    /**
131
     * Load session identifier from request.
132
     *
133
     * @param ServerRequestInterface $request
134
     *
135
     * @throws \RuntimeException
136
     */
137
    public function loadIdFromRequest(ServerRequestInterface $request)
138
    {
139
        $requestCookies = $request->getCookieParams();
140
        $sessionName = $this->configuration->getName();
141
142
        if (array_key_exists($sessionName, $requestCookies) && !empty($requestCookies[$sessionName])) {
143
            $this->setId($requestCookies[$sessionName]);
144
        }
145
    }
146
147
    /**
148
     * Start session.
149
     *
150
     * @throws \RuntimeException
151
     */
152
    public function start()
153
    {
154
        if ($this->isActive()) {
155
            return;
156
        }
157
158
        if (!$this->shutdownRegistered) {
159
            register_shutdown_function([$this, 'close']);
160
161
            $this->shutdownRegistered = true;
162
        }
163
164
        $this->emit(Event::named('preStart'), $this);
165
166
        foreach ($this->sessionManager->start() as $key => $value) {
167
            $this->set($key, $value);
168
        }
169
        $this->originalData = clone $this->data;
170
171
        if ($this->sessionManager->shouldRegenerateId()) {
172
            $this->regenerateId();
173
        }
174
175
        $this->emit(Event::named('postStart'), $this);
176
177
        $this->manageTimeout();
178
    }
179
180
    /**
181
     * Regenerate session identifier keeping parameters.
182
     *
183
     * @throws \RuntimeException
184
     */
185
    public function regenerateId()
186
    {
187
        if (!$this->isActive()) {
188
            throw new \RuntimeException('Cannot regenerate a not started session');
189
        }
190
191
        $this->emit(Event::named('preRegenerateId'), $this);
192
193
        $this->sessionManager->regenerateId();
194
195
        $this->emit(Event::named('postRegenerateId'), $this);
196
    }
197
198
    /**
199
     * Revert session to its original data.
200
     */
201
    public function reset()
202
    {
203
        if (!$this->isActive()) {
204
            return;
205
        }
206
207
        $this->emit(Event::named('preReset'), $this);
208
209
        $this->data = clone $this->originalData;
210
211
        $this->emit(Event::named('postReset'), $this);
212
    }
213
214
    /**
215
     * Close session keeping original session data.
216
     */
217
    public function abort()
218
    {
219
        if (!$this->isActive()) {
220
            return;
221
        }
222
223
        $this->emit(Event::named('preAbort'), $this);
224
225
        $this->sessionManager->close($this->originalData->getAll());
226
227
        $this->emit(Event::named('postAbort'), $this);
228
    }
229
230
    /**
231
     * Close session.
232
     */
233
    public function close()
234
    {
235
        if (!$this->isActive()) {
236
            return;
237
        }
238
239
        $this->emit(Event::named('preClose'), $this);
240
241
        $this->sessionManager->close($this->data->getAll());
242
243
        $this->emit(Event::named('postClose'), $this);
244
    }
245
246
    /**
247
     * Destroy session.
248
     *
249
     * @throws \RuntimeException
250
     */
251
    public function destroy()
252
    {
253
        if (!$this->isActive()) {
254
            throw new \RuntimeException('Cannot destroy a not started session');
255
        }
256
257
        $this->emit(Event::named('preDestroy'), $this);
258
259
        $this->sessionManager->destroy();
260
261
        $this->emit(Event::named('postDestroy'), $this);
262
263
        $this->originalData = new Collection();
264
        $this->data = new Collection();
265
    }
266
267
    /**
268
     * Manage session timeout.
269
     *
270
     * @throws \RuntimeException
271
     */
272
    protected function manageTimeout()
273
    {
274
        $timeoutKey = $this->configuration->getTimeoutKey();
275
        $sessionTimeout = $this->data->get($timeoutKey);
276
277
        if ($sessionTimeout && $sessionTimeout < time()) {
278
            $this->emit(Event::named('preTimeout'), $this);
279
280
            $this->sessionManager->regenerateId();
281
282
            $this->emit(Event::named('postTimeout'), $this);
283
        }
284
285
        $this->data->set($timeoutKey, time() + $this->configuration->getLifetime());
286
    }
287
288
    /**
289
     * {@inheritdoc}
290
     */
291
    public function offsetExists($offset)
292
    {
293
        return $this->has($offset);
294
    }
295
296
    /**
297
     * {@inheritdoc}
298
     */
299
    public function offsetGet($offset)
300
    {
301
        return $this->get($offset);
302
    }
303
304
    /**
305
     * {@inheritdoc}
306
     */
307
    public function offsetSet($offset, $value)
308
    {
309
        $this->set($offset, $value);
310
    }
311
312
    /**
313
     * {@inheritdoc}
314
     */
315
    public function offsetUnset($offset)
316
    {
317
        $this->remove($offset);
318
    }
319
320
    /**
321
     * Session parameter existence.
322
     *
323
     * @param string $key
324
     *
325
     * @return bool
326
     */
327
    public function has(string $key) : bool
328
    {
329
        return $this->data->has($key);
330
    }
331
332
    /**
333
     * Retrieve session parameter.
334
     *
335
     * @param string     $key
336
     * @param mixed|null $default
337
     *
338
     * @return mixed
339
     */
340
    public function get(string $key, $default = null)
341
    {
342
        return $this->data->get($key, $default);
343
    }
344
345
    /**
346
     * Set session parameter.
347
     *
348
     * @param string $key
349
     * @param mixed  $value
350
     *
351
     * @throws \InvalidArgumentException
352
     *
353
     * @return self
354
     */
355
    public function set(string $key, $value) : self
356
    {
357
        $this->data->set($key, $value);
358
359
        return $this;
360
    }
361
362
    /**
363
     * Remove session parameter.
364
     *
365
     * @param string $key
366
     *
367
     * @return self
368
     */
369
    public function remove(string $key) : self
370
    {
371
        $this->data->remove($key);
372
373
        return $this;
374
    }
375
376
    /**
377
     * Remove all session parameters.
378
     *
379
     * @return self
380
     */
381
    public function clear() : self
382
    {
383
        $timeoutKey = $this->configuration->getTimeoutKey();
384
        $sessionTimeout = $this->data->get($timeoutKey, time() + $this->configuration->getLifetime());
385
386
        $this->data->clear();
387
        $this->data->set($timeoutKey, $sessionTimeout);
388
389
        return $this;
390
    }
391
392
    /**
393
     * Get session cookie content.
394
     *
395
     * @return string
396
     */
397
    public function getSessionCookieString() : string
398
    {
399
        if (empty($this->getId())) {
400
            return '';
401
        }
402
403
        if ($this->isDestroyed()) {
404
            return $this->getExpiredCookieString();
405
        }
406
407
        return $this->getInitiatedCookieString();
408
    }
409
410
    /**
411
     * Get session expired cookie.
412
     *
413
     * @return string
414
     */
415
    protected function getExpiredCookieString() : string
416
    {
417
        return sprintf(
418
            '%s=%s; %s',
419
            urlencode($this->configuration->getName()),
420
            urlencode($this->getId()),
421
            $this->getCookieParameters(0, 1)
422
        );
423
    }
424
425
    /**
426
     * Get normal session cookie.
427
     *
428
     * @return string
429
     */
430
    protected function getInitiatedCookieString() : string
431
    {
432
        $lifetime = $this->configuration->getLifetime();
433
        $timeoutKey = $this->configuration->getTimeoutKey();
434
        $expireTime = $this->data->get($timeoutKey, time() + $lifetime);
435
436
        return sprintf(
437
            '%s=%s; %s',
438
            urlencode($this->configuration->getName()),
439
            urlencode($this->getId()),
440
            $this->getCookieParameters($expireTime, $lifetime)
441
        );
442
    }
443
444
    /**
445
     * Get session cookie parameters.
446
     *
447
     * @param int $expireTime
448
     * @param int $lifetime
449
     *
450
     * @return string
451
     */
452
    protected function getCookieParameters(int $expireTime, int $lifetime) : string
453
    {
454
        $cookieParams = [
455
            sprintf('expires=%s; max-age=%s', gmdate('D, d M Y H:i:s T', $expireTime), $lifetime),
456
        ];
457
458
        if (!empty($this->configuration->getCookiePath())) {
459
            $cookieParams[] = 'path=' . $this->configuration->getCookiePath();
460
        }
461
462
        $domain = $this->getCookieDomain();
463
        if (!empty($domain)) {
464
            $cookieParams[] = 'domain=' . $domain;
465
        }
466
467
        if ($this->configuration->isCookieSecure()) {
468
            $cookieParams[] = 'secure';
469
        }
470
471
        if ($this->configuration->isCookieHttpOnly()) {
472
            $cookieParams[] = 'httponly';
473
        }
474
475
        $cookieParams[] = 'SameSite=' . $this->configuration->getCookieSameSite();
476
477
        return implode('; ', $cookieParams);
478
    }
479
480
    /**
481
     * Get normalized cookie domain.
482
     *
483
     * @return string
484
     */
485
    protected function getCookieDomain() : string
486
    {
487
        $domain = $this->configuration->getCookieDomain();
488
489
        // Current domain for local host names or IP addresses
490
        if (empty($domain)
491
            || strpos($domain, '.') === false
492
            || filter_var($domain, FILTER_VALIDATE_IP) !== false
493
        ) {
494
            return '';
495
        }
496
497
        return $domain[0] === '.' ? $domain : '.' . $domain;
498
    }
499
}
500