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

codeigniter4 / CodeIgniter4 / 20640580500

01 Jan 2026 02:52PM UTC coverage: 85.498%. Remained the same
20640580500

push

github

web-flow
refactor: Session library (#9831)

10 of 12 new or added lines in 6 files covered. (83.33%)

1 existing line in 1 file now uncovered.

21808 of 25507 relevant lines covered (85.5%)

204.02 hits per line

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

77.31
/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,035✔
66
        $this->config = $config;
7,035✔
67
        $cookie       = config(CookieConfig::class);
7,035✔
68

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

79
        helper('array');
7,035✔
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
        if ((! isset($_SERVER['HTTP_X_REQUESTED_WITH']) || strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) !== 'xmlhttprequest')
92✔
124
            && ($regenerateTime = $this->config->timeToUpdate) > 0
92✔
125
        ) {
126
            if (! isset($_SESSION['__ci_last_regenerate'])) {
90✔
127
                $_SESSION['__ci_last_regenerate'] = Time::now()->getTimestamp();
89✔
128
            } elseif ($_SESSION['__ci_last_regenerate'] < (Time::now()->getTimestamp() - $regenerateTime)) {
3✔
129
                $this->regenerate($this->config->regenerateDestroy);
1✔
130
            }
131
        }
132
        // Another work-around ... PHP doesn't seem to send the session cookie
133
        // unless it is being currently created or regenerated
134
        elseif (isset($_COOKIE[$this->config->cookieName]) && $_COOKIE[$this->config->cookieName] === session_id()) {
2✔
135
            $this->setCookie();
×
136
        }
137

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

141
        return $this;
92✔
142
    }
143

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

279
        session_destroy();
×
280
    }
281

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

293
        session_write_close();
×
294
    }
295

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

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

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

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

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

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

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

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

330
        return $userdata;
1✔
331
    }
332

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

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

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

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

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

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

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

393
        return null;
×
394
    }
395

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

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

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

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

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

425
        $flashdata = [];
×
426

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

433
        return $flashdata;
×
434
    }
435

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

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

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

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

454
        return true;
11✔
455
    }
456

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

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

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

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

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

484
        $keys = [];
1✔
485

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

492
        return $keys;
1✔
493
    }
494

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

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

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

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

513
        $tempdata = [];
4✔
514

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

521
        return $tempdata;
4✔
522
    }
523

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

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

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

539
        $tempdata = [];
11✔
540

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

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

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

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

558
        return true;
10✔
559
    }
560

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

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

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

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

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

588
        $keys = [];
1✔
589

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

596
        return $keys;
1✔
597
    }
598

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

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

621
            return;
48✔
622
        }
623

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

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

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