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

codeigniter4 / CodeIgniter4 / 21568681844

01 Feb 2026 07:16PM UTC coverage: 85.41% (+1.0%) from 84.387%
21568681844

push

github

web-flow
Merge pull request #9916 from codeigniter4/4.7

4.7.0 Merge code

1603 of 1888 new or added lines in 101 files covered. (84.9%)

31 existing lines in 11 files now uncovered.

22163 of 25949 relevant lines covered (85.41%)

205.52 hits per line

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

77.42
/system/Session/Session.php
1
<?php
2

3
declare(strict_types=1);
4

5
/**
6
 * This file is part of CodeIgniter 4 framework.
7
 *
8
 * (c) CodeIgniter Foundation <admin@codeigniter.com>
9
 *
10
 * For the full copyright and license information, please view
11
 * the LICENSE file that was distributed with this source code.
12
 */
13

14
namespace CodeIgniter\Session;
15

16
use CodeIgniter\Cookie\Cookie;
17
use CodeIgniter\I18n\Time;
18
use Config\Cookie as CookieConfig;
19
use Config\Session as SessionConfig;
20
use Psr\Log\LoggerAwareTrait;
21
use SessionHandlerInterface;
22

23
/**
24
 * Implementation of CodeIgniter session container.
25
 *
26
 * Session configuration is done through session variables and cookie related
27
 * variables in `Сonfig\Session`.
28
 *
29
 * @property string $session_id
30
 *
31
 * @see \CodeIgniter\Session\SessionTest
32
 */
33
class Session implements SessionInterface
34
{
35
    use LoggerAwareTrait;
36

37
    /**
38
     * Instance of the driver to use.
39
     *
40
     * @var SessionHandlerInterface
41
     */
42
    protected $driver;
43

44
    /**
45
     * The session cookie instance.
46
     *
47
     * @var Cookie
48
     */
49
    protected $cookie;
50

51
    /**
52
     * Session ID regex expression.
53
     *
54
     * @var string
55
     */
56
    protected $sidRegexp;
57

58
    protected SessionConfig $config;
59

60
    /**
61
     * Extract configuration settings and save them here.
62
     */
63
    public function __construct(SessionHandlerInterface $driver, SessionConfig $config)
64
    {
65
        $this->driver = $driver;
7,165✔
66
        $this->config = $config;
7,165✔
67
        $cookie       = config(CookieConfig::class);
7,165✔
68

69
        $this->cookie = (new Cookie($this->config->cookieName, '', [
7,165✔
70
            'expires'  => $this->config->expiration === 0 ? 0 : Time::now()->getTimestamp() + $this->config->expiration,
7,165✔
71
            'path'     => $cookie->path,
7,165✔
72
            'domain'   => $cookie->domain,
7,165✔
73
            'secure'   => $cookie->secure,
7,165✔
74
            'httponly' => true, // for security
7,165✔
75
            'samesite' => $cookie->samesite ?? Cookie::SAMESITE_LAX,
7,165✔
76
            'raw'      => $cookie->raw ?? false,
7,165✔
77
        ]))->withPrefix(''); // Cookie prefix should be ignored.
7,165✔
78

79
        helper('array');
7,165✔
80
    }
81

82
    /**
83
     * Initialize the session container and starts up the session.
84
     *
85
     * @return $this|null
86
     */
87
    public function start()
88
    {
89
        if (is_cli() && ENVIRONMENT !== 'testing') {
92✔
90
            // @codeCoverageIgnoreStart
91
            $this->logger->debug('Session: Initialization under CLI aborted.');
×
92

93
            return null;
×
94
            // @codeCoverageIgnoreEnd
95
        }
96

97
        if ((bool) ini_get('session.auto_start')) {
92✔
98
            $this->logger->error('Session: session.auto_start is enabled in php.ini. Aborting.');
×
99

100
            return null;
×
101
        }
102

103
        if (session_status() === PHP_SESSION_ACTIVE) {
92✔
104
            $this->logger->warning('Session: Sessions is enabled, and one exists. Please don\'t $session->start();');
×
105

106
            return null;
×
107
        }
108

109
        $this->configure();
92✔
110
        $this->setSaveHandler();
92✔
111

112
        // Sanitize the cookie, because apparently PHP doesn't do that for userspace handlers
113
        if (
114
            isset($_COOKIE[$this->config->cookieName])
92✔
115
            && (! is_string($_COOKIE[$this->config->cookieName]) || preg_match('#\A' . $this->sidRegexp . '\z#', $_COOKIE[$this->config->cookieName]) !== 1)
92✔
116
        ) {
117
            unset($_COOKIE[$this->config->cookieName]);
×
118
        }
119

120
        $this->startSession();
92✔
121

122
        // Is session ID auto-regeneration configured? (ignoring ajax requests)
123
        $requestedWith = service('superglobals')->server('HTTP_X_REQUESTED_WITH');
92✔
124
        if (($requestedWith === null || strtolower($requestedWith) !== 'xmlhttprequest')
92✔
125
            && ($regenerateTime = $this->config->timeToUpdate) > 0
92✔
126
        ) {
127
            if (! isset($_SESSION['__ci_last_regenerate'])) {
90✔
128
                $_SESSION['__ci_last_regenerate'] = Time::now()->getTimestamp();
89✔
129
            } elseif ($_SESSION['__ci_last_regenerate'] < (Time::now()->getTimestamp() - $regenerateTime)) {
3✔
130
                $this->regenerate($this->config->regenerateDestroy);
1✔
131
            }
132
        }
133
        // Another work-around ... PHP doesn't seem to send the session cookie
134
        // unless it is being currently created or regenerated
135
        elseif (isset($_COOKIE[$this->config->cookieName]) && $_COOKIE[$this->config->cookieName] === session_id()) {
2✔
136
            $this->setCookie();
×
137
        }
138

139
        $this->initVars();
92✔
140
        $this->logger->debug("Session: Class initialized using '" . $this->config->driver . "' driver.");
92✔
141

142
        return $this;
92✔
143
    }
144

145
    /**
146
     * Configuration.
147
     *
148
     * Handle input binds and configuration defaults.
149
     *
150
     * @return void
151
     */
152
    protected function configure()
153
    {
154
        ini_set('session.name', $this->config->cookieName);
92✔
155

156
        $sameSite = $this->cookie->getSameSite() === ''
92✔
NEW
157
            ? ucfirst(Cookie::SAMESITE_LAX)
×
158
            : $this->cookie->getSameSite();
92✔
159

160
        $params = [
92✔
161
            'lifetime' => $this->config->expiration,
92✔
162
            'path'     => $this->cookie->getPath(),
92✔
163
            'domain'   => $this->cookie->getDomain(),
92✔
164
            'secure'   => $this->cookie->isSecure(),
92✔
165
            'httponly' => true, // HTTP only; Yes, this is intentional and not configurable for security reasons.
92✔
166
            'samesite' => $sameSite,
92✔
167
        ];
92✔
168

169
        ini_set('session.cookie_samesite', $sameSite);
92✔
170
        session_set_cookie_params($params);
92✔
171

172
        if ($this->config->expiration > 0) {
92✔
173
            ini_set('session.gc_maxlifetime', (string) $this->config->expiration);
91✔
174
        }
175

176
        if ($this->config->savePath !== '') {
92✔
177
            ini_set('session.save_path', $this->config->savePath);
48✔
178
        }
179

180
        // Security is king
181
        ini_set('session.use_trans_sid', '0');
92✔
182
        ini_set('session.use_strict_mode', '1');
92✔
183
        ini_set('session.use_cookies', '1');
92✔
184
        ini_set('session.use_only_cookies', '1');
92✔
185

186
        $this->configureSidLength();
92✔
187
    }
188

189
    /**
190
     * Configure session ID length.
191
     *
192
     * To make life easier, we force the PHP defaults. Because PHP9 forces them.
193
     * See https://wiki.php.net/rfc/deprecations_php_8_4#sessionsid_length_and_sessionsid_bits_per_character
194
     *
195
     * @return void
196
     */
197
    protected function configureSidLength()
198
    {
199
        $bitsPerCharacter = (int) ini_get('session.sid_bits_per_character');
92✔
200
        $sidLength        = (int) ini_get('session.sid_length');
92✔
201

202
        // We force the PHP defaults.
203
        if (PHP_VERSION_ID < 90000) {
92✔
204
            if ($bitsPerCharacter !== 4) {
92✔
205
                ini_set('session.sid_bits_per_character', '4');
×
206
            }
207
            if ($sidLength !== 32) {
92✔
208
                ini_set('session.sid_length', '32');
×
209
            }
210
        }
211

212
        $this->sidRegexp = '[0-9a-f]{32}';
92✔
213
    }
214

215
    /**
216
     * Handle temporary variables.
217
     *
218
     * Clears old "flash" data, marks the new one for deletion and handles
219
     * "temp" data deletion.
220
     *
221
     * @return void
222
     */
223
    protected function initVars()
224
    {
225
        if (! isset($_SESSION['__ci_vars'])) {
92✔
226
            return;
92✔
227
        }
228

229
        $currentTime = Time::now()->getTimestamp();
2✔
230

231
        foreach ($_SESSION['__ci_vars'] as $key => &$value) {
2✔
232
            if ($value === 'new') {
2✔
233
                $_SESSION['__ci_vars'][$key] = 'old';
2✔
234
            }
235
            // DO NOT move this above the 'new' check!
236
            elseif ($value === 'old' || $value < $currentTime) {
1✔
237
                unset($_SESSION[$key], $_SESSION['__ci_vars'][$key]);
1✔
238
            }
239
        }
240

241
        if ($_SESSION['__ci_vars'] === []) {
2✔
242
            unset($_SESSION['__ci_vars']);
1✔
243
        }
244
    }
245

246
    public function regenerate(bool $destroy = false)
247
    {
248
        $_SESSION['__ci_last_regenerate'] = Time::now()->getTimestamp();
×
249
        session_regenerate_id($destroy);
×
250

251
        $this->removeOldSessionCookie();
×
252
    }
253

254
    private function removeOldSessionCookie(): void
255
    {
256
        $response              = service('response');
×
257
        $cookieStoreInResponse = $response->getCookieStore();
×
258

259
        if (! $cookieStoreInResponse->has($this->config->cookieName)) {
×
260
            return;
×
261
        }
262

263
        // CookieStore is immutable.
264
        $newCookieStore = $cookieStoreInResponse->remove($this->config->cookieName);
×
265

266
        // But clear() method clears cookies in the object (not immutable).
267
        $cookieStoreInResponse->clear();
×
268

269
        foreach ($newCookieStore as $cookie) {
×
270
            $response->setCookie($cookie);
×
271
        }
272
    }
273

274
    public function destroy()
275
    {
276
        if (ENVIRONMENT === 'testing') {
×
277
            return;
×
278
        }
279

280
        session_destroy();
×
281
    }
282

283
    /**
284
     * Writes session data and close the current session.
285
     *
286
     * @return void
287
     */
288
    public function close()
289
    {
290
        if (ENVIRONMENT === 'testing') {
×
291
            return;
×
292
        }
293

294
        session_write_close();
×
295
    }
296

297
    public function set($data, $value = null)
298
    {
299
        $data = is_array($data) ? $data : [$data => $value];
125✔
300

301
        if (array_is_list($data)) {
125✔
302
            $data = array_fill_keys($data, null);
1✔
303
        }
304

305
        foreach ($data as $sessionKey => $sessionValue) {
125✔
306
            $_SESSION[$sessionKey] = $sessionValue;
125✔
307
        }
308
    }
309

310
    public function get(?string $key = null)
311
    {
312
        if (! isset($_SESSION) || $_SESSION === []) {
67✔
313
            return $key === null ? [] : null;
12✔
314
        }
315

316
        $key ??= '';
57✔
317

318
        if ($key !== '') {
57✔
319
            return $_SESSION[$key] ?? dot_array_search($key, $_SESSION);
56✔
320
        }
321

322
        $userdata = [];
1✔
323
        $exclude  = array_merge(['__ci_vars'], $this->getFlashKeys(), $this->getTempKeys());
1✔
324

325
        foreach (array_keys($_SESSION) as $key) {
1✔
326
            if (! in_array($key, $exclude, true)) {
1✔
327
                $userdata[$key] = $_SESSION[$key];
1✔
328
            }
329
        }
330

331
        return $userdata;
1✔
332
    }
333

334
    public function has(string $key): bool
335
    {
336
        return isset($_SESSION[$key]);
38✔
337
    }
338

339
    /**
340
     * Push new value onto session value that is array.
341
     *
342
     * @param string               $key  Identifier of the session property we are interested in.
343
     * @param array<string, mixed> $data value to be pushed to existing session key.
344
     *
345
     * @return void
346
     */
347
    public function push(string $key, array $data)
348
    {
349
        if ($this->has($key) && is_array($value = $this->get($key))) {
1✔
350
            $this->set($key, array_merge($value, $data));
1✔
351
        }
352
    }
353

354
    public function remove($key)
355
    {
356
        $key = is_array($key) ? $key : [$key];
2✔
357

358
        foreach ($key as $k) {
2✔
359
            unset($_SESSION[$k]);
2✔
360
        }
361
    }
362

363
    /**
364
     * Magic method to set variables in the session by simply calling
365
     *  $session->foo = 'bar';
366
     *
367
     * @param mixed $value
368
     *
369
     * @return void
370
     */
371
    public function __set(string $key, $value)
372
    {
373
        $_SESSION[$key] = $value;
1✔
374
    }
375

376
    /**
377
     * Magic method to get session variables by simply calling
378
     *  $foo = $session->foo;
379
     *
380
     * @return mixed
381
     */
382
    public function __get(string $key)
383
    {
384
        // Note: Keep this order the same, just in case somebody wants to
385
        // use 'session_id' as a session data key, for whatever reason
386
        if (isset($_SESSION[$key])) {
1✔
387
            return $_SESSION[$key];
1✔
388
        }
389

390
        if ($key === 'session_id') {
×
391
            return session_id();
×
392
        }
393

394
        return null;
×
395
    }
396

397
    /**
398
     * Magic method to check for session variables.
399
     *
400
     * Different from `has()` in that it will validate 'session_id' as well.
401
     * Mostly used by internal PHP functions, users should stick to `has()`.
402
     */
403
    public function __isset(string $key): bool
404
    {
405
        return isset($_SESSION[$key]) || $key === 'session_id';
2✔
406
    }
407

408
    public function setFlashdata($data, $value = null)
409
    {
410
        $this->set($data, $value);
11✔
411
        $this->markAsFlashdata(is_array($data) ? array_keys($data) : $data);
11✔
412
    }
413

414
    public function getFlashdata(?string $key = null)
415
    {
416
        $_SESSION['__ci_vars'] ??= [];
×
417

418
        if (isset($key)) {
×
419
            if (! isset($_SESSION['__ci_vars'][$key]) || is_int($_SESSION['__ci_vars'][$key])) {
×
420
                return null;
×
421
            }
422

423
            return $_SESSION[$key] ?? null;
×
424
        }
425

426
        $flashdata = [];
×
427

428
        foreach ($_SESSION['__ci_vars'] as $key => $value) {
×
429
            if (! is_int($value)) {
×
430
                $flashdata[$key] = $_SESSION[$key];
×
431
            }
432
        }
433

434
        return $flashdata;
×
435
    }
436

437
    public function keepFlashdata($key)
438
    {
439
        $this->markAsFlashdata($key);
1✔
440
    }
441

442
    public function markAsFlashdata($key): bool
443
    {
444
        $keys = is_array($key) ? $key : [$key];
12✔
445

446
        foreach ($keys as $sessionKey) {
12✔
447
            if (! isset($_SESSION[$sessionKey])) {
12✔
448
                return false;
1✔
449
            }
450
        }
451

452
        $_SESSION['__ci_vars'] ??= [];
11✔
453
        $_SESSION['__ci_vars'] = [...$_SESSION['__ci_vars'], ...array_fill_keys($keys, 'new')];
11✔
454

455
        return true;
11✔
456
    }
457

458
    public function unmarkFlashdata($key)
459
    {
460
        if (! isset($_SESSION['__ci_vars'])) {
1✔
461
            return;
×
462
        }
463

464
        if (! is_array($key)) {
1✔
465
            $key = [$key];
1✔
466
        }
467

468
        foreach ($key as $k) {
1✔
469
            if (isset($_SESSION['__ci_vars'][$k]) && ! is_int($_SESSION['__ci_vars'][$k])) {
1✔
470
                unset($_SESSION['__ci_vars'][$k]);
1✔
471
            }
472
        }
473

474
        if ($_SESSION['__ci_vars'] === []) {
1✔
475
            unset($_SESSION['__ci_vars']);
1✔
476
        }
477
    }
478

479
    public function getFlashKeys(): array
480
    {
481
        if (! isset($_SESSION['__ci_vars'])) {
2✔
482
            return [];
1✔
483
        }
484

485
        $keys = [];
1✔
486

487
        foreach (array_keys($_SESSION['__ci_vars']) as $key) {
1✔
488
            if (! is_int($_SESSION['__ci_vars'][$key])) {
1✔
489
                $keys[] = $key;
1✔
490
            }
491
        }
492

493
        return $keys;
1✔
494
    }
495

496
    public function setTempdata($data, $value = null, int $ttl = 300)
497
    {
498
        $this->set($data, $value);
10✔
499
        $this->markAsTempdata($data, $ttl);
10✔
500
    }
501

502
    public function getTempdata(?string $key = null)
503
    {
504
        $_SESSION['__ci_vars'] ??= [];
5✔
505

506
        if (isset($key)) {
5✔
507
            if (! isset($_SESSION['__ci_vars'][$key]) || ! is_int($_SESSION['__ci_vars'][$key])) {
1✔
508
                return null;
×
509
            }
510

511
            return $_SESSION[$key] ?? null;
1✔
512
        }
513

514
        $tempdata = [];
4✔
515

516
        foreach ($_SESSION['__ci_vars'] as $key => $value) {
4✔
517
            if (is_int($value)) {
3✔
518
                $tempdata[$key] = $_SESSION[$key];
3✔
519
            }
520
        }
521

522
        return $tempdata;
4✔
523
    }
524

525
    public function removeTempdata(string $key)
526
    {
527
        $this->unmarkTempdata($key);
1✔
528
        unset($_SESSION[$key]);
1✔
529
    }
530

531
    public function markAsTempdata($key, int $ttl = 300): bool
532
    {
533
        $time = Time::now()->getTimestamp();
11✔
534
        $keys = is_array($key) ? $key : [$key];
11✔
535

536
        if (array_is_list($keys)) {
11✔
537
            $keys = array_fill_keys($keys, $ttl);
4✔
538
        }
539

540
        $tempdata = [];
11✔
541

542
        foreach ($keys as $sessionKey => $timeToLive) {
11✔
543
            if (! array_key_exists($sessionKey, $_SESSION)) {
11✔
544
                return false;
1✔
545
            }
546

547
            if (is_int($timeToLive)) {
11✔
548
                $timeToLive += $time;
5✔
549
            } else {
550
                $timeToLive = $time + $ttl;
7✔
551
            }
552

553
            $tempdata[$sessionKey] = $timeToLive;
11✔
554
        }
555

556
        $_SESSION['__ci_vars'] ??= [];
10✔
557
        $_SESSION['__ci_vars'] = [...$_SESSION['__ci_vars'], ...$tempdata];
10✔
558

559
        return true;
10✔
560
    }
561

562
    public function unmarkTempdata($key)
563
    {
564
        if (! isset($_SESSION['__ci_vars'])) {
3✔
565
            return;
×
566
        }
567

568
        if (! is_array($key)) {
3✔
569
            $key = [$key];
2✔
570
        }
571

572
        foreach ($key as $k) {
3✔
573
            if (isset($_SESSION['__ci_vars'][$k]) && is_int($_SESSION['__ci_vars'][$k])) {
3✔
574
                unset($_SESSION['__ci_vars'][$k]);
3✔
575
            }
576
        }
577

578
        if ($_SESSION['__ci_vars'] === []) {
3✔
579
            unset($_SESSION['__ci_vars']);
1✔
580
        }
581
    }
582

583
    public function getTempKeys(): array
584
    {
585
        if (! isset($_SESSION['__ci_vars'])) {
2✔
586
            return [];
1✔
587
        }
588

589
        $keys = [];
1✔
590

591
        foreach (array_keys($_SESSION['__ci_vars']) as $key) {
1✔
592
            if (is_int($_SESSION['__ci_vars'][$key])) {
1✔
593
                $keys[] = $key;
1✔
594
            }
595
        }
596

597
        return $keys;
1✔
598
    }
599

600
    /**
601
     * Sets the driver as the session handler in PHP.
602
     * Extracted for easier testing.
603
     *
604
     * @return void
605
     */
606
    protected function setSaveHandler()
607
    {
608
        session_set_save_handler($this->driver, true);
48✔
609
    }
610

611
    /**
612
     * Starts the session.
613
     * Extracted for testing reasons.
614
     *
615
     * @return void
616
     */
617
    protected function startSession()
618
    {
619
        if (ENVIRONMENT === 'testing') {
48✔
620
            $_SESSION = [];
48✔
621

622
            return;
48✔
623
        }
624

625
        session_start(); // @codeCoverageIgnore
×
626
    }
627

628
    /**
629
     * Takes care of setting the cookie on the client side.
630
     *
631
     * @codeCoverageIgnore
632
     *
633
     * @return void
634
     */
635
    protected function setCookie()
636
    {
637
        $expiration   = $this->config->expiration === 0 ? 0 : Time::now()->getTimestamp() + $this->config->expiration;
×
638
        $this->cookie = $this->cookie->withValue(session_id())->withExpires($expiration);
×
639

640
        $response = service('response');
×
641
        $response->setCookie($this->cookie);
×
642
    }
643
}
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