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

nette / http / 21830508055

09 Feb 2026 03:10PM UTC coverage: 83.772% (+0.03%) from 83.744%
21830508055

push

github

dg
added RequestFactory::setForceHttps()

8 of 9 new or added lines in 2 files covered. (88.89%)

75 existing lines in 9 files now uncovered.

924 of 1103 relevant lines covered (83.77%)

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
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
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;
14
use const PHP_SESSION_ACTIVE;
15

16

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

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

34
        /** @var list<callable(Session): void>  Occurs when the session is started */
35
        public array $onStart = [];
36

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

40
        private bool $regenerated = false;
41
        private bool $started = false;
42

43
        /** @var array<string, mixed> default configuration */
44
        private array $options = [
45
                'cookie_samesite' => IResponse::SameSiteLax,
46
                'cookie_lifetime' => 0,   // for a maximum of 3 hours or until the browser is closed
47
                'gc_maxlifetime' => self::DefaultFileLifetime, // 3 hours
48
        ];
49
        private ?\SessionHandlerInterface $handler = null;
50
        private bool $readAndClose = false;
51
        private bool $fileExists = true;
52
        private bool $autoStart = true;
53

54

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

64

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

74

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

83
                        return;
×
84
                }
85

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

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

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

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

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

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

123

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

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

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

138

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

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

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

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

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

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

180

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

186

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

195

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

208

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

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

228

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

238

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

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

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

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

262

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

271

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

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

287

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

296

297
        /********************* sections management ****************d*g**/
298

299

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

311

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

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

324

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

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

335

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

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

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

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

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

367

368
        /********************* configuration ****************d*g**/
369

370

371
        /**
372
         * Sets session options.
373
         * @param array<string, mixed>  $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
         * @return array<string, mixed>
425
         */
426
        public function getOptions(): array
427
        {
428
                return $this->options;
1✔
429
        }
430

431

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

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

445
                        } elseif (str_starts_with($key, 'cookie_')) {
1✔
446
                                $cookie[substr($key, 7)] = $value;
1✔
447

448
                        } else {
449
                                if (session_status() === PHP_SESSION_ACTIVE) {
1✔
UNCOV
450
                                        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().'));
×
451
                                }
452

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

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

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

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

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

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

478

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

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

500

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

519

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

530

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

540
                $this->handler = $handler;
1✔
541
                return $this;
1✔
542
        }
543

544

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