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

nette / http / 22837209177

09 Mar 2026 03:32AM UTC coverage: 83.513% (-0.1%) from 83.62%
22837209177

push

github

dg
added CLAUDE.md

932 of 1116 relevant lines covered (83.51%)

0.84 hits per line

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

78.0
/src/Http/Session.php
1
<?php declare(strict_types=1);
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
namespace Nette\Http;
9

10
use Nette;
11
use function array_keys, function_exists, headers_sent, ini_get, ini_get_all, ini_set, is_array, is_string, preg_match, preg_replace, preg_replace_callback, session_create_id, session_destroy, session_get_cookie_params, session_id, session_name, session_regenerate_id, session_set_cookie_params, session_set_save_handler, session_status, session_write_close, strncmp, strtolower, strtoupper, substr, time;
12
use const PHP_SESSION_ACTIVE;
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 list<callable(Session): void>  Occurs when the session is started */
33
        public array $onStart = [];
34

35
        /** @var list<callable(Session): 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
        /** @var array<string, mixed> 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
        private ?\SessionHandlerInterface $handler = null;
48
        private bool $readAndClose = false;
49
        private bool $fileExists = true;
50
        private bool $autoStart = true;
51

52

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

62

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

72

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

81
                        return;
×
82
                }
83

84
                $this->configure(self::SecurityOptions + $this->options);
1✔
85

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

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

106
                if ($e) {
1✔
107
                        @session_write_close(); // this is needed
1✔
108
                        throw $e;
1✔
109
                }
110

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

117
                $this->initialize();
1✔
118
                Nette\Utils\Arrays::invoke($this->onStart, $this);
1✔
119
        }
1✔
120

121

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

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

133
                $this->doStart(!$forWrite);
1✔
134
        }
1✔
135

136

137
        private function initialize(): void
138
        {
139
                $this->started = true;
1✔
140
                $this->fileExists = true;
1✔
141

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

149
                if (!is_array($nf)) {
1✔
150
                        $nf = [];
1✔
151
                }
152

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

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

172
                                        unset($nf['META'][$section][$variable], $nf['DATA'][$section][$variable]);
1✔
173
                                }
174
                        }
175
                }
176
        }
1✔
177

178

179
        public function __destruct()
180
        {
181
                $this->clean();
1✔
182
        }
1✔
183

184

185
        /**
186
         * Checks whether the session has been started.
187
         */
188
        public function isStarted(): bool
189
        {
190
                return $this->started && session_status() === PHP_SESSION_ACTIVE;
1✔
191
        }
192

193

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

206

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

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

226

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

236

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

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

252
                        session_regenerate_id(delete_old_session: true);
1✔
253
                } else {
254
                        session_id(session_create_id() ?: throw new Nette\InvalidStateException('Failed to create session ID.'));
×
255
                }
256

257
                $this->regenerated = true;
1✔
258
        }
1✔
259

260

261
        /**
262
         * Returns the current session ID. Avoid relying on the value - it may change between requests.
263
         */
264
        public function getId(): string
265
        {
266
                return session_id();
1✔
267
        }
268

269

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

279
                session_name($name);
×
280
                return $this->setOptions([
×
281
                        'name' => $name,
×
282
                ]);
283
        }
284

285

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

294

295
        /********************* sections management ****************d*g**/
296

297

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

309

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

319
                return !empty($_SESSION['__NF']['DATA'][$section]);
1✔
320
        }
321

322

323
        /**
324
         * Returns the names of all existing session sections.
325
         * @return list<string>
326
         */
327
        public function getSectionNames(): array
328
        {
329
                if ($this->exists() && !$this->started) {
1✔
330
                        $this->autoStart(forWrite: false);
×
331
                }
332

333
                /** @var array<string, mixed> $data */
334
                $data = $_SESSION['__NF']['DATA'] ?? [];
1✔
335
                return array_keys($data);
1✔
336
        }
337

338

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

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

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

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

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

370

371
        /********************* configuration ****************d*g**/
372

373

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

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

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

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

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

401
                        $normalized[$normKey] = $value;
1✔
402
                }
403

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

409
                        $this->readAndClose = (bool) $normalized['read_and_close'];
1✔
410
                        unset($normalized['read_and_close']);
1✔
411
                }
412

413
                $this->autoStart = $normalized['auto_start'] ?? true;
1✔
414
                unset($normalized['auto_start']);
1✔
415

416
                if (session_status() === PHP_SESSION_ACTIVE) {
1✔
417
                        $this->configure($normalized);
×
418
                }
419

420
                $this->options = $normalized + $this->options;
1✔
421
                return $this;
1✔
422
        }
423

424

425
        /**
426
         * Returns all session options.
427
         * @return array<string, mixed>
428
         */
429
        public function getOptions(): array
430
        {
431
                return $this->options;
1✔
432
        }
433

434

435
        /**
436
         * Configures session environment.
437
         * @param array<string, mixed>  $config
438
         */
439
        private function configure(array $config): void
1✔
440
        {
441
                $special = ['cache_expire' => 1, 'cache_limiter' => 1, 'save_path' => 1, 'name' => 1];
1✔
442
                $cookie = $origCookie = session_get_cookie_params();
1✔
443

444
                foreach ($config as $key => $value) {
1✔
445
                        if ($value === null || ini_get("session.$key") == $value) { // intentionally ==
1✔
446
                                continue;
1✔
447

448
                        } elseif (str_starts_with($key, 'cookie_')) {
1✔
449
                                $cookie[substr($key, 7)] = $value;
1✔
450

451
                        } else {
452
                                if (session_status() === PHP_SESSION_ACTIVE) {
1✔
453
                                        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().'));
×
454
                                }
455

456
                                if (isset($special[$key])) {
1✔
457
                                        ("session_$key")($value);
×
458

459
                                } elseif (function_exists('ini_set')) {
1✔
460
                                        ini_set("session.$key", (string) $value);
1✔
461

462
                                } else {
463
                                        throw new Nette\NotSupportedException("Unable to set 'session.$key' to '$value' because function ini_set() is disabled.");
×
464
                                }
465
                        }
466
                }
467

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

471
                        if (session_status() === PHP_SESSION_ACTIVE) {
1✔
472
                                $this->sendCookie();
×
473
                        }
474
                }
475

476
                if ($this->handler) {
1✔
477
                        session_set_save_handler($this->handler);
1✔
478
                }
479
        }
1✔
480

481

482
        /**
483
         * Sets the session lifetime as a time string (e.g. '20 minutes'), or null to revert to the default
484
         * (up to 3 hours or until the browser is closed).
485
         */
486
        public function setExpiration(?string $expire): static
1✔
487
        {
488
                if ($expire === null) {
1✔
489
                        return $this->setOptions([
×
490
                                'gc_maxlifetime' => self::DefaultFileLifetime,
×
491
                                'cookie_lifetime' => 0,
×
492
                        ]);
493

494
                } else {
495
                        $expire = Nette\Utils\DateTime::from($expire)->format('U') - time();
1✔
496
                        return $this->setOptions([
1✔
497
                                'gc_maxlifetime' => $expire,
1✔
498
                                'cookie_lifetime' => $expire,
1✔
499
                        ]);
500
                }
501
        }
502

503

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

522

523
        /**
524
         * Sets path of the directory used to save session data.
525
         */
526
        public function setSavePath(string $path): static
527
        {
528
                return $this->setOptions([
×
529
                        'save_path' => $path,
×
530
                ]);
531
        }
532

533

534
        /**
535
         * Sets user session handler.
536
         */
537
        public function setHandler(\SessionHandlerInterface $handler): static
1✔
538
        {
539
                if ($this->started) {
1✔
540
                        throw new Nette\InvalidStateException('Unable to set handler when session has been started.');
×
541
                }
542

543
                $this->handler = $handler;
1✔
544
                return $this;
1✔
545
        }
546

547

548
        /**
549
         * Sends the session cookies.
550
         */
551
        private function sendCookie(): void
552
        {
553
                $cookie = session_get_cookie_params();
×
554
                $this->response->setCookie(
×
555
                        session_name(),
×
556
                        session_id(),
×
557
                        $cookie['lifetime'] ? $cookie['lifetime'] + time() : 0,
×
558
                        $cookie['path'],
×
559
                        $cookie['domain'],
×
560
                        $cookie['secure'],
×
561
                        $cookie['httponly'],
×
562
                        $cookie['samesite'] ?? null,
×
563
                );
564
        }
565
}
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