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

nette / http / 3615717021

pending completion
3615717021

push

github

David Grudl
Session::getIterator() is deprecated

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

910 of 1105 relevant lines covered (82.35%)

0.82 hits per line

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

73.66
/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
        use Nette\SmartObject;
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 array<callable(self): void>  Occurs when the session is started */
35
        public $onStart = [];
36

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

40
        /** @var bool  has been session ID regenerated? */
41
        private $regenerated = false;
42

43
        /** @var bool  has been session started by Nette? */
44
        private $started = false;
45

46
        /** @var array default configuration */
47
        private $options = [
48
                'cookie_samesite' => IResponse::SameSiteLax,
49
                'cookie_lifetime' => 0,   // for a maximum of 3 hours or until the browser is closed
50
                'gc_maxlifetime' => self::DefaultFileLifetime, // 3 hours
51
        ];
52

53
        /** @var IRequest */
54
        private $request;
55

56
        /** @var IResponse */
57
        private $response;
58

59
        /** @var \SessionHandlerInterface */
60
        private $handler;
61

62
        /** @var bool */
63
        private $readAndClose = false;
64

65
        /** @var bool */
66
        private $fileExists = true;
67

68
        /** @var bool */
69
        private $autoStart = true;
70

71

72
        public function __construct(IRequest $request, IResponse $response)
1✔
73
        {
74
                $this->request = $request;
1✔
75
                $this->response = $response;
1✔
76
                $this->options['cookie_path'] = &$this->response->cookiePath;
1✔
77
                $this->options['cookie_domain'] = &$this->response->cookieDomain;
1✔
78
                $this->options['cookie_secure'] = &$this->response->cookieSecure;
1✔
79
        }
1✔
80

81

82
        /**
83
         * Starts and initializes session data.
84
         * @throws Nette\InvalidStateException
85
         */
86
        public function start(): void
87
        {
88
                $this->doStart();
1✔
89
        }
1✔
90

91

92
        private function doStart($mustExists = false): void
1✔
93
        {
94
                if (session_status() === PHP_SESSION_ACTIVE) { // adapt an existing session
1✔
95
                        if (!$this->started) {
×
96
                                $this->configure(self::SecurityOptions);
×
97
                                $this->initialize();
×
98
                        }
99

100
                        return;
×
101
                }
102

103
                $this->configure(self::SecurityOptions + $this->options);
1✔
104

105
                if (!session_id()) { // session is started for first time
1✔
106
                        $id = $this->request->getCookie(session_name());
1✔
107
                        $id = is_string($id) && preg_match('#^[0-9a-zA-Z,-]{22,256}$#Di', $id)
1✔
108
                                ? $id
1✔
109
                                : session_create_id();
1✔
110
                        session_id($id); // causes resend of a cookie to make sure it has the right parameters
1✔
111
                }
112

113
                try {
114
                        // session_start returns false on failure only sometimes (even in PHP >= 7.1)
115
                        Nette\Utils\Callback::invokeSafe(
1✔
116
                                'session_start',
1✔
117
                                [['read_and_close' => $this->readAndClose]],
1✔
118
                                function (string $message) use (&$e): void {
1✔
119
                                        $e = new Nette\InvalidStateException($message);
1✔
120
                                }
1✔
121
                        );
122
                } catch (\Throwable $e) {
1✔
123
                }
124

125
                if ($e) {
1✔
126
                        @session_write_close(); // this is needed
1✔
127
                        throw $e;
1✔
128
                }
129

130
                if ($mustExists && $this->request->getCookie(session_name()) !== session_id()) {
1✔
131
                        // PHP regenerated the ID which means that the session did not exist and cookie was invalid
132
                        $this->destroy();
1✔
133
                        return;
1✔
134
                }
135

136
                $this->initialize();
1✔
137
                Nette\Utils\Arrays::invoke($this->onStart, $this);
1✔
138
        }
1✔
139

140

141
        /** @internal */
142
        public function autoStart(bool $forWrite): void
1✔
143
        {
144
                if ($this->started || (!$forWrite && !$this->exists())) {
1✔
145
                        return;
1✔
146

147
                } elseif (!$this->autoStart) {
1✔
148
                        trigger_error('Cannot auto-start session because autostarting is disabled', E_USER_WARNING);
1✔
149
                        return;
1✔
150
                }
151

152
                $this->doStart(!$forWrite);
1✔
153
        }
1✔
154

155

156
        private function initialize(): void
157
        {
158
                $this->started = true;
1✔
159
                $this->fileExists = true;
1✔
160

161
                /* structure:
162
                        __NF: Data, Meta, Time
163
                                DATA: section->variable = data
164
                                META: section->variable = Timestamp
165
                */
166
                $nf = &$_SESSION['__NF'];
1✔
167

168
                if (!is_array($nf)) {
1✔
169
                        $nf = [];
1✔
170
                }
171

172
                // regenerate empty session
173
                if (empty($nf['Time']) && !$this->readAndClose) {
1✔
174
                        $nf['Time'] = time();
1✔
175
                        if ($this->request->getCookie(session_name()) === session_id()) {
1✔
176
                                // ensures that the session was created with use_strict_mode (ie by Nette)
177
                                $this->regenerateId();
1✔
178
                        }
179
                }
180

181
                // expire section variables
182
                $now = time();
1✔
183
                foreach ($nf['META'] ?? [] as $section => $metadata) {
1✔
184
                        foreach ($metadata ?? [] as $variable => $value) {
1✔
185
                                if (!empty($value['T']) && $now > $value['T']) {
1✔
186
                                        if ($variable === '') { // expire whole section
1✔
187
                                                unset($nf['META'][$section], $nf['DATA'][$section]);
1✔
188
                                                continue 2;
1✔
189
                                        }
190

191
                                        unset($nf['META'][$section][$variable], $nf['DATA'][$section][$variable]);
1✔
192
                                }
193
                        }
194
                }
195
        }
1✔
196

197

198
        public function __destruct()
199
        {
200
                $this->clean();
1✔
201
        }
1✔
202

203

204
        /**
205
         * Has been session started?
206
         */
207
        public function isStarted(): bool
208
        {
209
                return $this->started && session_status() === PHP_SESSION_ACTIVE;
1✔
210
        }
211

212

213
        /**
214
         * Ends the current session and store session data.
215
         */
216
        public function close(): void
217
        {
218
                if (session_status() === PHP_SESSION_ACTIVE) {
1✔
219
                        $this->clean();
1✔
220
                        session_write_close();
1✔
221
                        $this->started = false;
1✔
222
                }
223
        }
1✔
224

225

226
        /**
227
         * Destroys all data registered to a session.
228
         */
229
        public function destroy(): void
230
        {
231
                if (session_status() !== PHP_SESSION_ACTIVE) {
1✔
232
                        throw new Nette\InvalidStateException('Session is not started.');
×
233
                }
234

235
                session_destroy();
1✔
236
                $_SESSION = null;
1✔
237
                $this->started = false;
1✔
238
                $this->fileExists = false;
1✔
239
                if (!$this->response->isSent()) {
1✔
240
                        $params = session_get_cookie_params();
1✔
241
                        $this->response->deleteCookie(session_name(), $params['path'], $params['domain'], $params['secure']);
1✔
242
                }
243
        }
1✔
244

245

246
        /**
247
         * Does session exist for the current request?
248
         */
249
        public function exists(): bool
250
        {
251
                return session_status() === PHP_SESSION_ACTIVE
1✔
252
                        || ($this->fileExists && $this->request->getCookie($this->getName()));
1✔
253
        }
254

255

256
        /**
257
         * Regenerates the session ID.
258
         * @throws Nette\InvalidStateException
259
         */
260
        public function regenerateId(): void
261
        {
262
                if ($this->regenerated) {
1✔
263
                        return;
×
264
                }
265

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

271
                        session_regenerate_id(true);
1✔
272
                } else {
273
                        session_id(session_create_id());
×
274
                }
275

276
                $this->regenerated = true;
1✔
277
        }
1✔
278

279

280
        /**
281
         * Returns the current session ID. Don't make dependencies, can be changed for each request.
282
         */
283
        public function getId(): string
284
        {
285
                return session_id();
1✔
286
        }
287

288

289
        /**
290
         * Sets the session name to a specified one.
291
         * @return static
292
         */
293
        public function setName(string $name)
294
        {
295
                if (!preg_match('#[^0-9.][^.]*$#DA', $name)) {
×
296
                        throw new Nette\InvalidArgumentException('Session name cannot contain dot.');
×
297
                }
298

299
                session_name($name);
×
300
                return $this->setOptions([
×
301
                        'name' => $name,
×
302
                ]);
303
        }
304

305

306
        /**
307
         * Gets the session name.
308
         */
309
        public function getName(): string
310
        {
311
                return $this->options['name'] ?? session_name();
1✔
312
        }
313

314

315
        /********************* sections management ****************d*g**/
316

317

318
        /**
319
         * Returns specified session section.
320
         * @throws Nette\InvalidArgumentException
321
         */
322
        public function getSection(string $section, string $class = SessionSection::class): SessionSection
1✔
323
        {
324
                return new $class($this, $section);
1✔
325
        }
326

327

328
        /**
329
         * Checks if a session section exist and is not empty.
330
         */
331
        public function hasSection(string $section): bool
1✔
332
        {
333
                if ($this->exists() && !$this->started) {
1✔
334
                        $this->autoStart(false);
×
335
                }
336

337
                return !empty($_SESSION['__NF']['DATA'][$section]);
1✔
338
        }
339

340

341
        /** @deprecated */
342
        public function getIterator(): \Iterator
343
        {
344
                trigger_error(__METHOD__ . '() is deprecated', E_USER_DEPRECATED);
×
345
                if ($this->exists() && !$this->started) {
×
346
                        $this->autoStart(false);
×
347
                }
348

349
                return new \ArrayIterator(array_keys($_SESSION['__NF']['DATA'] ?? []));
×
350
        }
351

352

353
        /**
354
         * Cleans and minimizes meta structures.
355
         */
356
        private function clean(): void
357
        {
358
                if (!$this->isStarted()) {
1✔
359
                        return;
1✔
360
                }
361

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

364
                $nf = &$_SESSION['__NF'];
1✔
365
                foreach ($nf['META'] ?? [] as $name => $foo) {
1✔
366
                        if (empty($nf['META'][$name])) {
1✔
367
                                unset($nf['META'][$name]);
×
368
                        }
369
                }
370
        }
1✔
371

372

373
        /********************* configuration ****************d*g**/
374

375

376
        /**
377
         * Sets session options.
378
         * @return static
379
         * @throws Nette\NotSupportedException
380
         * @throws Nette\InvalidStateException
381
         */
382
        public function setOptions(array $options)
1✔
383
        {
384
                $normalized = [];
1✔
385
                $allowed = ini_get_all('session', false) + ['session.read_and_close' => 1, 'session.cookie_samesite' => 1]; // for PHP < 7.3
1✔
386

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

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

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

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

403
                        $normalized[$normKey] = $value;
1✔
404
                }
405

406
                if (array_key_exists('read_and_close', $normalized)) {
1✔
407
                        if (session_status() === PHP_SESSION_ACTIVE) {
1✔
408
                                throw new Nette\InvalidStateException('Cannot configure "read_and_close" for already started session.');
×
409
                        }
410

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

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

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

422
                $this->options = $normalized + $this->options;
1✔
423
                return $this;
1✔
424
        }
425

426

427
        /**
428
         * Returns all session options.
429
         */
430
        public function getOptions(): array
431
        {
432
                return $this->options;
1✔
433
        }
434

435

436
        /**
437
         * Configures session environment.
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 (strncmp($key, 'cookie_', 7) === 0) {
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
                        if (PHP_VERSION_ID >= 70300) {
1✔
470
                                @session_set_cookie_params($cookie); // @ may trigger warning when session is active since PHP 7.2
1✔
471
                        } else {
472
                                @session_set_cookie_params( // @ may trigger warning when session is active since PHP 7.2
×
473
                                        $cookie['lifetime'],
×
474
                                        $cookie['path'] . (isset($cookie['samesite']) ? '; SameSite=' . $cookie['samesite'] : ''),
×
475
                                        $cookie['domain'],
×
476
                                        $cookie['secure'],
×
477
                                        $cookie['httponly']
×
478
                                );
479
                        }
480

481
                        if (session_status() === PHP_SESSION_ACTIVE) {
1✔
482
                                $this->sendCookie();
×
483
                        }
484
                }
485

486
                if ($this->handler) {
1✔
487
                        session_set_save_handler($this->handler);
1✔
488
                }
489
        }
1✔
490

491

492
        /**
493
         * Sets the amount of time (like '20 minutes') allowed between requests before the session will be terminated,
494
         * null means "for a maximum of 3 hours or until the browser is closed".
495
         * @return static
496
         */
497
        public function setExpiration(?string $expire)
1✔
498
        {
499
                if ($expire === null) {
1✔
500
                        return $this->setOptions([
×
501
                                'gc_maxlifetime' => self::DefaultFileLifetime,
×
502
                                'cookie_lifetime' => 0,
×
503
                        ]);
504

505
                } else {
506
                        $expire = Nette\Utils\DateTime::from($expire)->format('U') - time();
1✔
507
                        return $this->setOptions([
1✔
508
                                'gc_maxlifetime' => $expire,
1✔
509
                                'cookie_lifetime' => $expire,
1✔
510
                        ]);
511
                }
512
        }
513

514

515
        /**
516
         * Sets the session cookie parameters.
517
         * @return static
518
         */
519
        public function setCookieParameters(
520
                string $path,
521
                ?string $domain = null,
522
                ?bool $secure = null,
523
                ?string $sameSite = null
524
        ) {
525
                return $this->setOptions([
×
526
                        'cookie_path' => $path,
×
527
                        'cookie_domain' => $domain,
×
528
                        'cookie_secure' => $secure,
×
529
                        'cookie_samesite' => $sameSite,
×
530
                ]);
531
        }
532

533

534
        /** @deprecated */
535
        public function getCookieParameters(): array
536
        {
537
                trigger_error(__METHOD__ . '() is deprecated.', E_USER_DEPRECATED);
×
538
                return session_get_cookie_params();
×
539
        }
540

541

542
        /**
543
         * Sets path of the directory used to save session data.
544
         * @return static
545
         */
546
        public function setSavePath(string $path)
547
        {
548
                return $this->setOptions([
×
549
                        'save_path' => $path,
×
550
                ]);
551
        }
552

553

554
        /**
555
         * Sets user session handler.
556
         * @return static
557
         */
558
        public function setHandler(\SessionHandlerInterface $handler)
1✔
559
        {
560
                if ($this->started) {
1✔
561
                        throw new Nette\InvalidStateException('Unable to set handler when session has been started.');
×
562
                }
563

564
                $this->handler = $handler;
1✔
565
                return $this;
1✔
566
        }
567

568

569
        /**
570
         * Sends the session cookies.
571
         */
572
        private function sendCookie(): void
573
        {
574
                $cookie = session_get_cookie_params();
×
575
                $this->response->setCookie(
×
576
                        session_name(),
×
577
                        session_id(),
×
578
                        $cookie['lifetime'] ? $cookie['lifetime'] + time() : 0,
×
579
                        $cookie['path'],
×
580
                        $cookie['domain'],
×
581
                        $cookie['secure'],
×
582
                        $cookie['httponly'],
×
583
                        $cookie['samesite'] ?? null
×
584
                );
585
        }
586
}
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