• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

nette / http / 11665283087

04 Nov 2024 01:32PM UTC coverage: 81.776% (+0.2%) from 81.537%
11665283087

push

github

dg
IRequest, IResponse: added typehints, unification (BC break)

2 of 2 new or added lines in 1 file covered. (100.0%)

24 existing lines in 2 files now uncovered.

875 of 1070 relevant lines covered (81.78%)

0.82 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

78.11
/src/Http/Session.php
1
<?php
2

3
/**
4
 * This file is part of the Nette Framework (https://nette.org)
5
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
6
 */
7

8
declare(strict_types=1);
9

10
namespace Nette\Http;
11

12
use Nette;
13

14

15
/**
16
 * Provides access to session sections as well as session settings and management methods.
17
 */
18
class Session
19
{
20
        /** Default file lifetime */
21
        private const DefaultFileLifetime = 3 * Nette\Utils\DateTime::HOUR;
22

23
        private const SecurityOptions = [
24
                'referer_check' => '',    // must be disabled because PHP implementation is invalid
25
                'use_cookies' => 1,       // must be enabled to prevent Session Hijacking and Fixation
26
                'use_only_cookies' => 1,  // must be enabled to prevent Session Fixation
27
                'use_trans_sid' => 0,     // must be disabled to prevent Session Hijacking and Fixation
28
                'use_strict_mode' => 1,   // must be enabled to prevent Session Fixation
29
                'cookie_httponly' => true, // must be enabled to prevent Session Hijacking
30
        ];
31

32
        /** @var array<callable(self): void>  Occurs when the session is started */
33
        public array $onStart = [];
34

35
        /** @var array<callable(self): void>  Occurs before the session is written to disk */
36
        public array $onBeforeWrite = [];
37

38
        private bool $regenerated = false;
39
        private bool $started = false;
40

41
        /** default configuration */
42
        private array $options = [
43
                'cookie_samesite' => IResponse::SameSiteLax,
44
                'cookie_lifetime' => 0,   // for a maximum of 3 hours or until the browser is closed
45
                'gc_maxlifetime' => self::DefaultFileLifetime, // 3 hours
46
        ];
47

48
        private readonly IRequest $request;
49
        private readonly IResponse $response;
50
        private ?\SessionHandlerInterface $handler = null;
51
        private bool $readAndClose = false;
52
        private bool $fileExists = true;
53
        private bool $autoStart = true;
54

55

56
        public function __construct(IRequest $request, IResponse $response)
1✔
57
        {
58
                $this->request = $request;
1✔
59
                $this->response = $response;
1✔
60
                $this->options['cookie_path'] = &$this->response->cookiePath;
1✔
61
                $this->options['cookie_domain'] = &$this->response->cookieDomain;
1✔
62
                $this->options['cookie_secure'] = &$this->response->cookieSecure;
1✔
63
        }
1✔
64

65

66
        /**
67
         * Starts and initializes session data.
68
         * @throws Nette\InvalidStateException
69
         */
70
        public function start(): void
71
        {
72
                $this->doStart();
1✔
73
        }
1✔
74

75

76
        private function doStart($mustExists = false): void
1✔
77
        {
78
                if (session_status() === PHP_SESSION_ACTIVE) { // adapt an existing session
1✔
79
                        if (!$this->started) {
×
80
                                $this->configure(self::SecurityOptions);
×
81
                                $this->initialize();
×
82
                        }
83

84
                        return;
×
85
                }
86

87
                $this->configure(self::SecurityOptions + $this->options);
1✔
88

89
                if (!session_id()) { // session is started for first time
1✔
90
                        $id = $this->request->getCookie(session_name());
1✔
91
                        $id = is_string($id) && preg_match('#^[0-9a-zA-Z,-]{22,256}$#Di', $id)
1✔
92
                                ? $id
1✔
93
                                : session_create_id();
1✔
94
                        session_id($id); // causes resend of a cookie to make sure it has the right parameters
1✔
95
                }
96

97
                try {
98
                        // session_start returns false on failure only sometimes (even in PHP >= 7.1)
99
                        Nette\Utils\Callback::invokeSafe(
1✔
100
                                'session_start',
1✔
101
                                [['read_and_close' => $this->readAndClose]],
1✔
102
                                function (string $message) use (&$e): void {
1✔
103
                                        $e = new Nette\InvalidStateException($message, previous: $e);
1✔
104
                                },
1✔
105
                        );
106
                } catch (\Throwable $e) {
1✔
107
                }
108

109
                if ($e) {
1✔
110
                        @session_write_close(); // this is needed
1✔
111
                        throw $e;
1✔
112
                }
113

114
                if ($mustExists && $this->request->getCookie(session_name()) !== session_id()) {
1✔
115
                        // PHP regenerated the ID which means that the session did not exist and cookie was invalid
116
                        $this->destroy();
1✔
117
                        return;
1✔
118
                }
119

120
                $this->initialize();
1✔
121
                Nette\Utils\Arrays::invoke($this->onStart, $this);
1✔
122
        }
1✔
123

124

125
        /** @internal */
126
        public function autoStart(bool $forWrite): void
1✔
127
        {
128
                if ($this->started || (!$forWrite && !$this->exists())) {
1✔
129
                        return;
1✔
130

131
                } elseif (!$this->autoStart) {
1✔
132
                        trigger_error('Cannot auto-start session because autostarting is disabled', E_USER_WARNING);
1✔
133
                        return;
1✔
134
                }
135

136
                $this->doStart(!$forWrite);
1✔
137
        }
1✔
138

139

140
        private function initialize(): void
141
        {
142
                $this->started = true;
1✔
143
                $this->fileExists = true;
1✔
144

145
                /* structure:
146
                        __NF: Data, Meta, Time
147
                                DATA: section->variable = data
148
                                META: section->variable = Timestamp
149
                */
150
                $nf = &$_SESSION['__NF'];
1✔
151

152
                if (!is_array($nf)) {
1✔
153
                        $nf = [];
1✔
154
                }
155

156
                // regenerate empty session
157
                if (empty($nf['Time']) && !$this->readAndClose) {
1✔
158
                        $nf['Time'] = time();
1✔
159
                        if ($this->request->getCookie(session_name()) === session_id()) {
1✔
160
                                // ensures that the session was created with use_strict_mode (ie by Nette)
161
                                $this->regenerateId();
1✔
162
                        }
163
                }
164

165
                // expire section variables
166
                $now = time();
1✔
167
                foreach ($nf['META'] ?? [] as $section => $metadata) {
1✔
168
                        foreach ($metadata ?? [] as $variable => $value) {
1✔
169
                                if (!empty($value['T']) && $now > $value['T']) {
1✔
170
                                        if ($variable === '') { // expire whole section
1✔
171
                                                unset($nf['META'][$section], $nf['DATA'][$section]);
1✔
172
                                                continue 2;
1✔
173
                                        }
174

175
                                        unset($nf['META'][$section][$variable], $nf['DATA'][$section][$variable]);
1✔
176
                                }
177
                        }
178
                }
179
        }
1✔
180

181

182
        public function __destruct()
183
        {
184
                $this->clean();
1✔
185
        }
1✔
186

187

188
        /**
189
         * Has been session started?
190
         */
191
        public function isStarted(): bool
192
        {
193
                return $this->started && session_status() === PHP_SESSION_ACTIVE;
1✔
194
        }
195

196

197
        /**
198
         * Ends the current session and store session data.
199
         */
200
        public function close(): void
201
        {
202
                if (session_status() === PHP_SESSION_ACTIVE) {
1✔
203
                        $this->clean();
1✔
204
                        session_write_close();
1✔
205
                        $this->started = false;
1✔
206
                }
207
        }
1✔
208

209

210
        /**
211
         * Destroys all data registered to a session.
212
         */
213
        public function destroy(): void
214
        {
215
                if (session_status() !== PHP_SESSION_ACTIVE) {
1✔
216
                        throw new Nette\InvalidStateException('Session is not started.');
×
217
                }
218

219
                session_destroy();
1✔
220
                $_SESSION = null;
1✔
221
                $this->started = false;
1✔
222
                $this->fileExists = false;
1✔
223
                if (!$this->response->isSent()) {
1✔
224
                        $params = session_get_cookie_params();
1✔
225
                        $this->response->deleteCookie(session_name(), $params['path'], $params['domain'], $params['secure']);
1✔
226
                }
227
        }
1✔
228

229

230
        /**
231
         * Does session exist for the current request?
232
         */
233
        public function exists(): bool
234
        {
235
                return session_status() === PHP_SESSION_ACTIVE
1✔
236
                        || ($this->fileExists && $this->request->getCookie($this->getName()));
1✔
237
        }
238

239

240
        /**
241
         * Regenerates the session ID.
242
         * @throws Nette\InvalidStateException
243
         */
244
        public function regenerateId(): void
245
        {
246
                if ($this->regenerated) {
1✔
247
                        return;
×
248
                }
249

250
                if (session_status() === PHP_SESSION_ACTIVE) {
1✔
251
                        if (headers_sent($file, $line)) {
1✔
252
                                throw new Nette\InvalidStateException('Cannot regenerate session ID after HTTP headers have been sent' . ($file ? " (output started at $file:$line)." : '.'));
×
253
                        }
254

255
                        session_regenerate_id(true);
1✔
256
                } else {
257
                        session_id(session_create_id());
×
258
                }
259

260
                $this->regenerated = true;
1✔
261
        }
1✔
262

263

264
        /**
265
         * Returns the current session ID. Don't make dependencies, can be changed for each request.
266
         */
267
        public function getId(): string
268
        {
269
                return session_id();
1✔
270
        }
271

272

273
        /**
274
         * Sets the session name to a specified one.
275
         */
276
        public function setName(string $name): static
277
        {
278
                if (!preg_match('#[^0-9.][^.]*$#DA', $name)) {
×
279
                        throw new Nette\InvalidArgumentException('Session name cannot contain dot.');
×
280
                }
281

282
                session_name($name);
×
283
                return $this->setOptions([
×
284
                        'name' => $name,
×
285
                ]);
286
        }
287

288

289
        /**
290
         * Gets the session name.
291
         */
292
        public function getName(): string
293
        {
294
                return $this->options['name'] ?? session_name();
1✔
295
        }
296

297

298
        /********************* sections management ****************d*g**/
299

300

301
        /**
302
         * Returns specified session section.
303
         * @template T of SessionSection
304
         * @param class-string<T> $class
305
         * @return T
306
         */
307
        public function getSection(string $section, string $class = SessionSection::class): SessionSection
1✔
308
        {
309
                return new $class($this, $section);
1✔
310
        }
311

312

313
        /**
314
         * Checks if a session section exist and is not empty.
315
         */
316
        public function hasSection(string $section): bool
1✔
317
        {
318
                if ($this->exists() && !$this->started) {
1✔
UNCOV
319
                        $this->autoStart(false);
×
320
                }
321

322
                return !empty($_SESSION['__NF']['DATA'][$section]);
1✔
323
        }
324

325

326
        /** @return string[] */
327
        public function getSectionNames(): array
328
        {
329
                if ($this->exists() && !$this->started) {
1✔
UNCOV
330
                        $this->autoStart(false);
×
331
                }
332

333
                return array_keys($_SESSION['__NF']['DATA'] ?? []);
1✔
334
        }
335

336

337
        /**
338
         * Cleans and minimizes meta structures.
339
         */
340
        private function clean(): void
341
        {
342
                if (!$this->isStarted()) {
1✔
343
                        return;
1✔
344
                }
345

346
                Nette\Utils\Arrays::invoke($this->onBeforeWrite, $this);
1✔
347

348
                $nf = &$_SESSION['__NF'];
1✔
349
                foreach ($nf['DATA'] ?? [] as $name => $data) {
1✔
350
                        foreach ($data ?? [] as $k => $v) {
1✔
351
                                if ($v === null) {
1✔
UNCOV
352
                                        unset($nf['DATA'][$name][$k], $nf['META'][$name][$k]);
×
353
                                }
354
                        }
355

356
                        if (empty($nf['DATA'][$name])) {
1✔
357
                                unset($nf['DATA'][$name], $nf['META'][$name]);
1✔
358
                        }
359
                }
360

361
                foreach ($nf['META'] ?? [] as $name => $data) {
1✔
362
                        if (empty($nf['META'][$name])) {
1✔
UNCOV
363
                                unset($nf['META'][$name]);
×
364
                        }
365
                }
366
        }
1✔
367

368

369
        /********************* configuration ****************d*g**/
370

371

372
        /**
373
         * Sets session options.
374
         * @throws Nette\NotSupportedException
375
         * @throws Nette\InvalidStateException
376
         */
377
        public function setOptions(array $options): static
1✔
378
        {
379
                $normalized = [];
1✔
380
                $allowed = ini_get_all('session', details: false) + ['session.read_and_close' => 1];
1✔
381

382
                foreach ($options as $key => $value) {
1✔
383
                        if (!strncmp($key, 'session.', 8)) { // back compatibility
1✔
384
                                $key = substr($key, 8);
1✔
385
                        }
386

387
                        $normKey = strtolower(preg_replace('#(.)(?=[A-Z])#', '$1_', $key)); // camelCase -> snake_case
1✔
388

389
                        if (!isset($allowed["session.$normKey"])) {
1✔
390
                                $hint = substr((string) Nette\Utils\Helpers::getSuggestion(array_keys($allowed), "session.$normKey"), 8);
1✔
391
                                if ($key !== $normKey) {
1✔
392
                                        $hint = preg_replace_callback('#_(.)#', fn($m) => strtoupper($m[1]), $hint); // snake_case -> camelCase
1✔
393
                                }
394

395
                                throw new Nette\InvalidStateException("Invalid session configuration option '$key'" . ($hint ? ", did you mean '$hint'?" : '.'));
1✔
396
                        }
397

398
                        $normalized[$normKey] = $value;
1✔
399
                }
400

401
                if (isset($normalized['read_and_close'])) {
1✔
402
                        if (session_status() === PHP_SESSION_ACTIVE) {
1✔
UNCOV
403
                                throw new Nette\InvalidStateException('Cannot configure "read_and_close" for already started session.');
×
404
                        }
405

406
                        $this->readAndClose = (bool) $normalized['read_and_close'];
1✔
407
                        unset($normalized['read_and_close']);
1✔
408
                }
409

410
                $this->autoStart = $normalized['auto_start'] ?? true;
1✔
411
                unset($normalized['auto_start']);
1✔
412

413
                if (session_status() === PHP_SESSION_ACTIVE) {
1✔
UNCOV
414
                        $this->configure($normalized);
×
415
                }
416

417
                $this->options = $normalized + $this->options;
1✔
418
                return $this;
1✔
419
        }
420

421

422
        /**
423
         * Returns all session options.
424
         */
425
        public function getOptions(): array
426
        {
427
                return $this->options;
1✔
428
        }
429

430

431
        /**
432
         * Configures session environment.
433
         */
434
        private function configure(array $config): void
1✔
435
        {
436
                $special = ['cache_expire' => 1, 'cache_limiter' => 1, 'save_path' => 1, 'name' => 1];
1✔
437
                $cookie = $origCookie = session_get_cookie_params();
1✔
438

439
                foreach ($config as $key => $value) {
1✔
440
                        if ($value === null || ini_get("session.$key") == $value) { // intentionally ==
1✔
441
                                continue;
1✔
442

443
                        } elseif (strncmp($key, 'cookie_', 7) === 0) {
1✔
444
                                $cookie[substr($key, 7)] = $value;
1✔
445

446
                        } else {
447
                                if (session_status() === PHP_SESSION_ACTIVE) {
1✔
UNCOV
448
                                        throw new Nette\InvalidStateException("Unable to set 'session.$key' to value '$value' when session has been started" . ($this->started ? '.' : ' by session.auto_start or session_start().'));
×
449
                                }
450

451
                                if (isset($special[$key])) {
1✔
UNCOV
452
                                        ("session_$key")($value);
×
453

454
                                } elseif (function_exists('ini_set')) {
1✔
455
                                        ini_set("session.$key", (string) $value);
1✔
456

457
                                } else {
UNCOV
458
                                        throw new Nette\NotSupportedException("Unable to set 'session.$key' to '$value' because function ini_set() is disabled.");
×
459
                                }
460
                        }
461
                }
462

463
                if ($cookie !== $origCookie) {
1✔
464
                        @session_set_cookie_params($cookie); // @ may trigger warning when session is active since PHP 7.2
1✔
465

466
                        if (session_status() === PHP_SESSION_ACTIVE) {
1✔
UNCOV
467
                                $this->sendCookie();
×
468
                        }
469
                }
470

471
                if ($this->handler) {
1✔
472
                        session_set_save_handler($this->handler);
1✔
473
                }
474
        }
1✔
475

476

477
        /**
478
         * Sets the amount of time (like '20 minutes') allowed between requests before the session will be terminated,
479
         * null means "for a maximum of 3 hours or until the browser is closed".
480
         */
481
        public function setExpiration(?string $expire): static
1✔
482
        {
483
                if ($expire === null) {
1✔
UNCOV
484
                        return $this->setOptions([
×
UNCOV
485
                                'gc_maxlifetime' => self::DefaultFileLifetime,
×
UNCOV
486
                                'cookie_lifetime' => 0,
×
487
                        ]);
488

489
                } else {
490
                        $expire = Nette\Utils\DateTime::from($expire)->format('U') - time();
1✔
491
                        return $this->setOptions([
1✔
492
                                'gc_maxlifetime' => $expire,
1✔
493
                                'cookie_lifetime' => $expire,
1✔
494
                        ]);
495
                }
496
        }
497

498

499
        /**
500
         * Sets the session cookie parameters.
501
         */
502
        public function setCookieParameters(
503
                string $path,
504
                ?string $domain = null,
505
                ?bool $secure = null,
506
                ?string $sameSite = null,
507
        ): static
508
        {
509
                return $this->setOptions([
×
510
                        'cookie_path' => $path,
×
UNCOV
511
                        'cookie_domain' => $domain,
×
UNCOV
512
                        'cookie_secure' => $secure,
×
UNCOV
513
                        'cookie_samesite' => $sameSite,
×
514
                ]);
515
        }
516

517

518
        /**
519
         * Sets path of the directory used to save session data.
520
         */
521
        public function setSavePath(string $path): static
522
        {
UNCOV
523
                return $this->setOptions([
×
UNCOV
524
                        'save_path' => $path,
×
525
                ]);
526
        }
527

528

529
        /**
530
         * Sets user session handler.
531
         */
532
        public function setHandler(\SessionHandlerInterface $handler): static
1✔
533
        {
534
                if ($this->started) {
1✔
UNCOV
535
                        throw new Nette\InvalidStateException('Unable to set handler when session has been started.');
×
536
                }
537

538
                $this->handler = $handler;
1✔
539
                return $this;
1✔
540
        }
541

542

543
        /**
544
         * Sends the session cookies.
545
         */
546
        private function sendCookie(): void
547
        {
548
                $cookie = session_get_cookie_params();
×
549
                $this->response->setCookie(
×
550
                        session_name(),
×
551
                        session_id(),
×
552
                        $cookie['lifetime'] ? $cookie['lifetime'] + time() : 0,
×
553
                        $cookie['path'],
×
554
                        $cookie['domain'],
×
UNCOV
555
                        $cookie['secure'],
×
UNCOV
556
                        $cookie['httponly'],
×
UNCOV
557
                        $cookie['samesite'] ?? null,
×
558
                );
559
        }
560
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc