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

nette / http / 22293514258

23 Feb 2026 04:58AM UTC coverage: 83.62%. Remained the same
22293514258

push

github

dg
added CLAUDE.md

924 of 1105 relevant lines covered (83.62%)

0.84 hits per line

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

77.89
/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();
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
         * Has been session 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());
×
255
                }
256

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

260

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

269

270
        /**
271
         * Sets the session name to a specified one.
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
        /** @return list<string> */
324
        public function getSectionNames(): array
325
        {
326
                if ($this->exists() && !$this->started) {
1✔
327
                        $this->autoStart(forWrite: false);
×
328
                }
329

330
                return array_keys($_SESSION['__NF']['DATA'] ?? []);
1✔
331
        }
332

333

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

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

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

353
                        if (empty($nf['DATA'][$name])) {
1✔
354
                                unset($nf['DATA'][$name], $nf['META'][$name]);
1✔
355
                        }
356
                }
357

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

365

366
        /********************* configuration ****************d*g**/
367

368

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

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

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

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

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

396
                        $normalized[$normKey] = $value;
1✔
397
                }
398

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

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

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

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

415
                $this->options = $normalized + $this->options;
1✔
416
                return $this;
1✔
417
        }
418

419

420
        /**
421
         * Returns all session options.
422
         * @return array<string, mixed>
423
         */
424
        public function getOptions(): array
425
        {
426
                return $this->options;
1✔
427
        }
428

429

430
        /**
431
         * Configures session environment.
432
         * @param array<string, mixed>  $config
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 (str_starts_with($key, 'cookie_')) {
1✔
444
                                $cookie[substr($key, 7)] = $value;
1✔
445

446
                        } else {
447
                                if (session_status() === PHP_SESSION_ACTIVE) {
1✔
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✔
452
                                        ("session_$key")($value);
×
453

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

457
                                } else {
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✔
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✔
484
                        return $this->setOptions([
×
485
                                'gc_maxlifetime' => self::DefaultFileLifetime,
×
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,
×
511
                        'cookie_domain' => $domain,
×
512
                        'cookie_secure' => $secure,
×
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
        {
523
                return $this->setOptions([
×
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✔
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'],
×
555
                        $cookie['secure'],
×
556
                        $cookie['httponly'],
×
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