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

codeigniter4 / CodeIgniter4 / 29841004404

21 Jul 2026 02:49PM UTC coverage: 89.601% (-0.03%) from 89.627%
29841004404

Pull #10423

github

web-flow
Merge 92f88bcf3 into f4b5bebd7
Pull Request #10423: feat: Add `post_response` event that gets triggered after response is sent to the client

5 of 14 new or added lines in 2 files covered. (35.71%)

25434 of 28386 relevant lines covered (89.6%)

232.39 hits per line

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

95.63
/system/HTTP/ResponseTrait.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\HTTP;
15

16
use CodeIgniter\Config\Services;
17
use CodeIgniter\Cookie\Cookie;
18
use CodeIgniter\Cookie\CookieStore;
19
use CodeIgniter\Cookie\Exceptions\CookieException;
20
use CodeIgniter\Exceptions\InvalidArgumentException;
21
use CodeIgniter\HTTP\Exceptions\HTTPException;
22
use CodeIgniter\I18n\Time;
23
use CodeIgniter\Pager\PagerInterface;
24
use CodeIgniter\Security\Exceptions\SecurityException;
25
use Config\App;
26
use Config\ContentSecurityPolicy as ContentSecurityPolicyConfig;
27
use Config\Cookie as CookieConfig;
28
use DateTime;
29
use DateTimeZone;
30

31
/**
32
 * Response Trait
33
 *
34
 * Additional methods to make a PSR-7 Response class
35
 * compliant with the framework's own ResponseInterface.
36
 *
37
 * @property array<int, string> $statusCodes
38
 * @property string|null        $body
39
 *
40
 * @see https://github.com/php-fig/http-message/blob/master/src/ResponseInterface.php
41
 */
42
trait ResponseTrait
43
{
44
    /**
45
     * Content security policy handler.
46
     *
47
     * Lazily instantiated on first use via `self::getCSP()` so that the
48
     * ContentSecurityPolicy class is not loaded on requests that do not use CSP.
49
     *
50
     * @var ContentSecurityPolicy|null
51
     */
52
    protected $CSP;
53

54
    /**
55
     * CookieStore instance.
56
     *
57
     * Lazily instantiated on first cookie-related call so that the Cookie and
58
     * CookieStore classes are not loaded on requests that do not use cookies.
59
     *
60
     * @var CookieStore|null
61
     */
62
    protected $cookieStore;
63

64
    /**
65
     * Type of format the body is in.
66
     * Valid: html, json, xml
67
     *
68
     * @var string
69
     */
70
    protected $bodyFormat = 'html';
71

72
    /**
73
     * Return an instance with the specified status code and, optionally, reason phrase.
74
     *
75
     * If no reason phrase is specified, will default recommended reason phrase for
76
     * the response's status code.
77
     *
78
     * @see http://tools.ietf.org/html/rfc7231#section-6
79
     * @see http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
80
     *
81
     * @param int    $code   The 3-digit integer result code to set.
82
     * @param string $reason The reason phrase to use with the
83
     *                       provided status code; if none is provided, will
84
     *                       default to the IANA name.
85
     *
86
     * @return $this
87
     *
88
     * @throws HTTPException For invalid status code arguments.
89
     */
90
    public function setStatusCode(int $code, string $reason = '')
91
    {
92
        if ($code < 100 || $code > 599) {
342✔
93
            throw HTTPException::forInvalidStatusCode($code);
3✔
94
        }
95

96
        if (! array_key_exists($code, static::$statusCodes) && $reason === '') {
339✔
97
            throw HTTPException::forUnkownStatusCode($code);
1✔
98
        }
99

100
        $this->statusCode = $code;
338✔
101

102
        $this->reason = $reason !== '' ? $reason : static::$statusCodes[$code];
338✔
103

104
        return $this;
338✔
105
    }
106

107
    // --------------------------------------------------------------------
108
    // Convenience Methods
109
    // --------------------------------------------------------------------
110

111
    /**
112
     * Sets the date header
113
     *
114
     * @return $this
115
     */
116
    public function setDate(DateTime $date)
117
    {
118
        $date->setTimezone(new DateTimeZone('UTC'));
76✔
119

120
        $this->setHeader('Date', $date->format('D, d M Y H:i:s') . ' GMT');
76✔
121

122
        return $this;
76✔
123
    }
124

125
    /**
126
     * Set the Link Header
127
     *
128
     * @see http://tools.ietf.org/html/rfc5988
129
     *
130
     * @return $this
131
     *
132
     * @todo Recommend moving to Pager
133
     */
134
    public function setLink(PagerInterface $pager)
135
    {
136
        $links    = '';
1✔
137
        $previous = $pager->getPreviousPageURI();
1✔
138

139
        if (is_string($previous) && $previous !== '') {
1✔
140
            $links .= '<' . $pager->getPageURI($pager->getFirstPage()) . '>; rel="first",';
1✔
141
            $links .= '<' . $previous . '>; rel="prev"';
1✔
142
        }
143

144
        $next = $pager->getNextPageURI();
1✔
145

146
        if (is_string($next) && $next !== '' && is_string($previous) && $previous !== '') {
1✔
147
            $links .= ',';
1✔
148
        }
149

150
        if (is_string($next) && $next !== '') {
1✔
151
            $links .= '<' . $next . '>; rel="next",';
1✔
152
            $links .= '<' . $pager->getPageURI($pager->getLastPage()) . '>; rel="last"';
1✔
153
        }
154

155
        $this->setHeader('Link', $links);
1✔
156

157
        return $this;
1✔
158
    }
159

160
    /**
161
     * Sets the Content Type header for this response with the mime type
162
     * and, optionally, the charset.
163
     *
164
     * @return $this
165
     */
166
    public function setContentType(string $mime, string $charset = 'UTF-8')
167
    {
168
        // add charset attribute if not already there and provided as parm
169
        if ((strpos($mime, 'charset=') < 1) && ($charset !== '')) {
1,002✔
170
            $mime .= '; charset=' . $charset;
1,002✔
171
        }
172

173
        $this->removeHeader('Content-Type'); // replace existing content type
1,002✔
174
        $this->setHeader('Content-Type', $mime);
1,002✔
175

176
        return $this;
1,002✔
177
    }
178

179
    /**
180
     * Converts the $body into JSON and sets the Content Type header.
181
     *
182
     * @param array|object|string $body
183
     *
184
     * @return $this
185
     */
186
    public function setJSON($body, bool $unencoded = false)
187
    {
188
        $this->body = $this->formatBody($body, 'json' . ($unencoded ? '-unencoded' : ''));
77✔
189

190
        return $this;
77✔
191
    }
192

193
    /**
194
     * Returns the current body, converted to JSON is it isn't already.
195
     *
196
     * @return string|null
197
     *
198
     * @throws InvalidArgumentException If the body property is not array.
199
     */
200
    public function getJSON()
201
    {
202
        $body = $this->body;
18✔
203

204
        if ($this->bodyFormat !== 'json') {
18✔
205
            $body = service('format')->getFormatter('application/json')->format($body);
3✔
206
        }
207

208
        return $body ?: null;
18✔
209
    }
210

211
    /**
212
     * Converts $body into XML, and sets the correct Content-Type.
213
     *
214
     * @param array|string $body
215
     *
216
     * @return $this
217
     */
218
    public function setXML($body)
219
    {
220
        $this->body = $this->formatBody($body, 'xml');
5✔
221

222
        return $this;
5✔
223
    }
224

225
    /**
226
     * Retrieves the current body into XML and returns it.
227
     *
228
     * @return bool|string|null
229
     *
230
     * @throws InvalidArgumentException If the body property is not array.
231
     */
232
    public function getXML()
233
    {
234
        $body = $this->body;
4✔
235

236
        if ($this->bodyFormat !== 'xml') {
4✔
237
            $body = service('format')->getFormatter('application/xml')->format($body);
1✔
238
        }
239

240
        return $body;
4✔
241
    }
242

243
    /**
244
     * Handles conversion of the data into the appropriate format,
245
     * and sets the correct Content-Type header for our response.
246
     *
247
     * @param array|object|string $body
248
     * @param string              $format Valid: json, xml
249
     *
250
     * @return false|string
251
     *
252
     * @throws InvalidArgumentException If the body property is not string or array.
253
     */
254
    protected function formatBody($body, string $format)
255
    {
256
        $this->bodyFormat = ($format === 'json-unencoded' ? 'json' : $format);
81✔
257
        $mime             = "application/{$this->bodyFormat}";
81✔
258
        $this->setContentType($mime);
81✔
259

260
        // Nothing much to do for a string...
261
        if (! is_string($body) || $format === 'json-unencoded') {
81✔
262
            $body = service('format')->getFormatter($mime)->format($body);
25✔
263
        }
264

265
        return $body;
81✔
266
    }
267

268
    // --------------------------------------------------------------------
269
    // Cache Control Methods
270
    //
271
    // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9
272
    // --------------------------------------------------------------------
273

274
    /**
275
     * Sets the appropriate headers to ensure this response
276
     * is not cached by the browsers.
277
     *
278
     * @return $this
279
     *
280
     * @todo Recommend researching these directives, might need: 'private', 'no-transform', 'no-store', 'must-revalidate'
281
     *
282
     * @see DownloadResponse::noCache()
283
     */
284
    public function noCache()
285
    {
286
        $this->removeHeader('Cache-Control');
972✔
287
        $this->setHeader('Cache-Control', ['no-store', 'max-age=0', 'no-cache']);
972✔
288

289
        return $this;
972✔
290
    }
291

292
    /**
293
     * A shortcut method that allows the developer to set all of the
294
     * cache-control headers in one method call.
295
     *
296
     * The options array is used to provide the cache-control directives
297
     * for the header. It might look something like:
298
     *
299
     *      $options = [
300
     *          'max-age'  => 300,
301
     *          's-maxage' => 900
302
     *          'etag'     => 'abcde',
303
     *      ];
304
     *
305
     * Typical options are:
306
     *  - etag
307
     *  - last-modified
308
     *  - max-age
309
     *  - s-maxage
310
     *  - private
311
     *  - public
312
     *  - must-revalidate
313
     *  - proxy-revalidate
314
     *  - no-transform
315
     *
316
     * @return $this
317
     */
318
    public function setCache(array $options = [])
319
    {
320
        if ($options === []) {
3✔
321
            return $this;
1✔
322
        }
323

324
        $this->removeHeader('Cache-Control');
2✔
325
        $this->removeHeader('ETag');
2✔
326

327
        // ETag
328
        if (isset($options['etag'])) {
2✔
329
            $this->setHeader('ETag', $options['etag']);
2✔
330
            unset($options['etag']);
2✔
331
        }
332

333
        // Last Modified
334
        if (isset($options['last-modified'])) {
2✔
335
            $this->setLastModified($options['last-modified']);
2✔
336

337
            unset($options['last-modified']);
2✔
338
        }
339

340
        $this->setHeader('Cache-Control', $options);
2✔
341

342
        return $this;
2✔
343
    }
344

345
    /**
346
     * Sets the Last-Modified date header.
347
     *
348
     * $date can be either a string representation of the date or,
349
     * preferably, an instance of DateTime.
350
     *
351
     * @param DateTime|string $date
352
     *
353
     * @return $this
354
     */
355
    public function setLastModified($date)
356
    {
357
        if ($date instanceof DateTime) {
5✔
358
            $date->setTimezone(new DateTimeZone('UTC'));
2✔
359
            $this->setHeader('Last-Modified', $date->format('D, d M Y H:i:s') . ' GMT');
2✔
360
        } elseif (is_string($date)) {
3✔
361
            $this->setHeader('Last-Modified', $date);
3✔
362
        }
363

364
        return $this;
5✔
365
    }
366

367
    // --------------------------------------------------------------------
368
    // Output Methods
369
    // --------------------------------------------------------------------
370

371
    /**
372
     * Sends the output to the browser.
373
     *
374
     * @return $this
375
     */
376
    public function send()
377
    {
378
        // If we're enforcing a Content Security Policy,
379
        // we need to give it a chance to build out its headers.
380
        if ($this->shouldFinalizeCsp()) {
122✔
381
            $this->getCSP()->finalize($this);
72✔
382
        }
383

384
        $this->sendHeaders();
122✔
385
        $this->sendCookies();
122✔
386
        $this->sendBody();
121✔
387

388
        $this->finishResponse();
121✔
389

390
        return $this;
121✔
391
    }
392

393
    private function finishResponse(): void
394
    {
395
        if (function_exists('fastcgi_finish_request')) {
121✔
NEW
396
            fastcgi_finish_request();
×
397
        } elseif (function_exists('litespeed_finish_request')) {
121✔
NEW
398
            litespeed_finish_request();
×
399
        } elseif (! in_array(PHP_SAPI, ['cli', 'phpdbg', 'embed'], true)) {
121✔
NEW
400
            $this->closeOutputBuffers();
×
NEW
401
            flush();
×
402
        }
403
    }
404

405
    private function closeOutputBuffers(): void
406
    {
NEW
407
        $status = ob_get_status(true);
×
NEW
408
        $level  = count($status);
×
NEW
409
        $flags  = PHP_OUTPUT_HANDLER_REMOVABLE | PHP_OUTPUT_HANDLER_FLUSHABLE;
×
410

NEW
411
        while ($level-- > 0 && ($s = $status[$level]) && ($s['del'] ?? (! isset($s['flags']) || ($s['flags'] & $flags) === $flags))) {
×
NEW
412
            ob_end_flush();
×
413
        }
414
    }
415

416
    /**
417
     * Decides whether {@see ContentSecurityPolicy::finalize()} should run for
418
     * this response. Keeping the CSP class unloaded on requests that do not
419
     * need it avoids the cost of constructing a 1000+ line service on every
420
     * request.
421
     */
422
    private function shouldFinalizeCsp(): bool
423
    {
424
        // Developer already touched CSP through getCSP(); respect it.
425
        if ($this->CSP !== null) {
122✔
426
            return true;
50✔
427
        }
428

429
        // A CSP instance has been registered (e.g., via Services::injectMock()
430
        // or any earlier service('csp') call) — reuse it instead of skipping.
431
        if (Services::has('csp')) {
72✔
432
            return true;
8✔
433
        }
434

435
        if (config(App::class)->CSPEnabled) {
64✔
436
            return true;
1✔
437
        }
438

439
        // Placeholders in the body still need to be stripped even when CSP
440
        // is disabled, so the body is scanned for the configured nonce tags
441
        // before committing to loading the full CSP class.
442
        $body = (string) $this->body;
63✔
443

444
        if ($body === '') {
63✔
445
            return false;
9✔
446
        }
447

448
        $cspConfig = config(ContentSecurityPolicyConfig::class);
54✔
449

450
        return str_contains($body, $cspConfig->scriptNonceTag)
54✔
451
            || str_contains($body, $cspConfig->styleNonceTag);
54✔
452
    }
453

454
    /**
455
     * Sends the headers of this HTTP response to the browser.
456
     *
457
     * @return $this
458
     */
459
    public function sendHeaders()
460
    {
461
        // Have the headers already been sent?
462
        if ($this->pretend || headers_sent()) {
126✔
463
            return $this;
53✔
464
        }
465

466
        // Per spec, MUST be sent with each request, if possible.
467
        // http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html
468
        if (! isset($this->headers['Date']) && PHP_SAPI !== 'cli-server') {
74✔
469
            $this->setDate(DateTime::createFromFormat('U', (string) Time::now()->getTimestamp()));
74✔
470
        }
471

472
        // HTTP Status
473
        header(sprintf('HTTP/%s %s %s', $this->getProtocolVersion(), $this->getStatusCode(), $this->getReasonPhrase()), true, $this->getStatusCode());
74✔
474

475
        // Send all of our headers
476
        foreach ($this->headers() as $name => $value) {
74✔
477
            if ($value instanceof Header) {
74✔
478
                header(
74✔
479
                    $name . ': ' . $value->getValueLine(),
74✔
480
                    true,
74✔
481
                    $this->getStatusCode(),
74✔
482
                );
74✔
483
            } else {
484
                $replace = true;
1✔
485

486
                foreach ($value as $header) {
1✔
487
                    header(
1✔
488
                        $name . ': ' . $header->getValueLine(),
1✔
489
                        $replace,
1✔
490
                        $this->getStatusCode(),
1✔
491
                    );
1✔
492
                    $replace = false;
1✔
493
                }
494
            }
495
        }
496

497
        return $this;
74✔
498
    }
499

500
    /**
501
     * Sends the Body of the message to the browser.
502
     *
503
     * @return $this
504
     */
505
    public function sendBody()
506
    {
507
        echo $this->body;
121✔
508

509
        return $this;
121✔
510
    }
511

512
    /**
513
     * Perform a redirect to a new URL, in two flavors: header or location.
514
     *
515
     * @param string   $uri  The URI to redirect to
516
     * @param int|null $code The type of redirection, defaults to 302
517
     *
518
     * @return $this
519
     *
520
     * @throws HTTPException For invalid status code.
521
     */
522
    public function redirect(string $uri, string $method = 'auto', ?int $code = null)
523
    {
524
        // IIS environment likely? Use 'refresh' for better compatibility
525
        $superglobals   = service('superglobals');
71✔
526
        $serverSoftware = $superglobals->server('SERVER_SOFTWARE');
71✔
527
        if (
528
            $method === 'auto'
71✔
529
            && $serverSoftware !== null
71✔
530
            && str_contains($serverSoftware, 'Microsoft-IIS')
71✔
531
        ) {
532
            $method = 'refresh';
6✔
533
        } elseif ($method !== 'refresh' && $code === null) {
65✔
534
            // override status code for HTTP/1.1 & higher
535
            $serverProtocol = $superglobals->server('SERVER_PROTOCOL');
46✔
536
            $requestMethod  = $superglobals->server('REQUEST_METHOD');
46✔
537
            if (
538
                $serverProtocol !== null
46✔
539
                && $requestMethod !== null
46✔
540
                && $this->getProtocolVersion() >= 1.1
46✔
541
            ) {
542
                if ($requestMethod === Method::GET) {
17✔
543
                    $code = 302;
3✔
544
                } elseif (in_array($requestMethod, [Method::POST, Method::PUT, Method::DELETE], true)) {
14✔
545
                    // reference: https://en.wikipedia.org/wiki/Post/Redirect/Get
546
                    $code = 303;
12✔
547
                } else {
548
                    $code = 307;
2✔
549
                }
550
            }
551
        }
552

553
        if ($code === null) {
71✔
554
            $code = 302;
32✔
555
        }
556

557
        match ($method) {
558
            'refresh' => $this->setHeader('Refresh', '0;url=' . $uri),
71✔
559
            default   => $this->setHeader('Location', $uri),
64✔
560
        };
561

562
        $this->setStatusCode($code);
71✔
563

564
        return $this;
71✔
565
    }
566

567
    /**
568
     * Set a cookie
569
     *
570
     * Accepts an arbitrary number of binds (up to 7) or an associative
571
     * array in the first parameter containing all the values.
572
     *
573
     * @param array|Cookie|string $name     Cookie name / array containing binds / Cookie object
574
     * @param string              $value    Cookie value
575
     * @param int                 $expire   Cookie expiration time in seconds
576
     * @param string              $domain   Cookie domain (e.g.: '.yourdomain.com')
577
     * @param string              $path     Cookie path (default: '/')
578
     * @param string              $prefix   Cookie name prefix ('': the default prefix)
579
     * @param bool|null           $secure   Whether to only transfer cookies via SSL
580
     * @param bool|null           $httponly Whether only make the cookie accessible via HTTP (no javascript)
581
     * @param string|null         $samesite
582
     *
583
     * @return $this
584
     */
585
    public function setCookie(
586
        $name,
587
        $value = '',
588
        $expire = 0,
589
        $domain = '',
590
        $path = '/',
591
        $prefix = '',
592
        $secure = null,
593
        $httponly = null,
594
        $samesite = null,
595
    ) {
596
        $store = $this->getCookieStore();
197✔
597

598
        if ($name instanceof Cookie) {
197✔
599
            $this->cookieStore = $store->put($name);
99✔
600

601
            return $this;
99✔
602
        }
603

604
        $cookieConfig = config(CookieConfig::class);
98✔
605

606
        $secure ??= $cookieConfig->secure;
98✔
607
        $httponly ??= $cookieConfig->httponly;
98✔
608
        $samesite ??= $cookieConfig->samesite;
98✔
609

610
        if (is_array($name)) {
98✔
611
            // always leave 'name' in last place, as the loop will break otherwise, due to ${$item}
612
            foreach (['samesite', 'value', 'expire', 'domain', 'path', 'prefix', 'secure', 'httponly', 'name'] as $item) {
19✔
613
                if (isset($name[$item])) {
19✔
614
                    ${$item} = $name[$item];
19✔
615
                }
616
            }
617
        }
618

619
        if (is_numeric($expire)) {
98✔
620
            $expire = $expire > 0 ? Time::now()->getTimestamp() + $expire : 0;
95✔
621
        }
622

623
        $cookie = new Cookie($name, $value, [
98✔
624
            'expires'  => $expire ?: 0,
98✔
625
            'domain'   => $domain,
98✔
626
            'path'     => $path,
98✔
627
            'prefix'   => $prefix,
98✔
628
            'secure'   => $secure,
98✔
629
            'httponly' => $httponly,
98✔
630
            'samesite' => $samesite ?? '',
98✔
631
        ]);
98✔
632

633
        $this->cookieStore = $store->put($cookie);
96✔
634

635
        return $this;
96✔
636
    }
637

638
    /**
639
     * Returns the `CookieStore` instance.
640
     *
641
     * Lazily instantiates the `CookieStore` on first call, so that the Cookie and
642
     * CookieStore classes are not loaded on requests that do not use cookies.
643
     *
644
     * @return CookieStore
645
     */
646
    public function getCookieStore()
647
    {
648
        $this->cookieStore ??= new CookieStore([]);
203✔
649

650
        return $this->cookieStore;
203✔
651
    }
652

653
    /**
654
     * Checks to see if the Response has a specified cookie or not.
655
     */
656
    public function hasCookie(string $name, ?string $value = null, string $prefix = ''): bool
657
    {
658
        $store  = $this->getCookieStore();
29✔
659
        $prefix = $prefix !== '' ? $prefix : Cookie::setDefaults()['prefix']; // to retain BC
29✔
660

661
        return $store->has($name, $prefix, $value);
29✔
662
    }
663

664
    /**
665
     * Returns the cookie
666
     *
667
     * @param string $prefix Cookie prefix.
668
     *                       '': the default prefix
669
     *
670
     * @return array<string, Cookie>|Cookie|null
671
     */
672
    public function getCookie(?string $name = null, string $prefix = '')
673
    {
674
        $store = $this->getCookieStore();
20✔
675

676
        if ((string) $name === '') {
20✔
677
            return $store->display();
1✔
678
        }
679

680
        try {
681
            $prefix = $prefix !== '' ? $prefix : Cookie::setDefaults()['prefix']; // to retain BC
20✔
682

683
            return $store->get($name, $prefix);
20✔
684
        } catch (CookieException $e) {
1✔
685
            log_message('error', (string) $e);
1✔
686

687
            return null;
1✔
688
        }
689
    }
690

691
    /**
692
     * Sets a cookie to be deleted when the response is sent.
693
     *
694
     * @return $this
695
     */
696
    public function deleteCookie(string $name = '', string $domain = '', string $path = '/', string $prefix = '')
697
    {
698
        if ($name === '') {
11✔
699
            return $this;
1✔
700
        }
701

702
        $store  = $this->getCookieStore();
11✔
703
        $prefix = $prefix !== '' ? $prefix : Cookie::setDefaults()['prefix']; // to retain BC
11✔
704

705
        $prefixed = $prefix . $name;
11✔
706
        $found    = false;
11✔
707

708
        /** @var Cookie $cookie */
709
        foreach ($store as $cookie) {
11✔
710
            if ($cookie->getPrefixedName() === $prefixed) {
10✔
711
                if ($domain !== $cookie->getDomain()) {
10✔
712
                    continue;
1✔
713
                }
714

715
                if ($path !== $cookie->getPath()) {
10✔
716
                    continue;
1✔
717
                }
718

719
                $cookie = $cookie->withValue('')->withExpired();
10✔
720
                $found  = true;
10✔
721

722
                $this->cookieStore = $store->put($cookie);
10✔
723
                break;
10✔
724
            }
725
        }
726

727
        if (! $found) {
11✔
728
            $this->setCookie($name, '', 0, $domain, $path, $prefix);
2✔
729
        }
730

731
        return $this;
11✔
732
    }
733

734
    /**
735
     * Returns all cookies currently set.
736
     *
737
     * @return array<string, Cookie>
738
     */
739
    public function getCookies()
740
    {
741
        if ($this->cookieStore === null) {
10✔
742
            return [];
2✔
743
        }
744

745
        return $this->cookieStore->display();
9✔
746
    }
747

748
    /**
749
     * Actually sets the cookies.
750
     *
751
     * @return void
752
     */
753
    protected function sendCookies()
754
    {
755
        if ($this->pretend || $this->cookieStore === null) {
123✔
756
            return;
65✔
757
        }
758

759
        $this->dispatchCookies();
58✔
760
    }
761

762
    private function dispatchCookies(): void
763
    {
764
        /** @var IncomingRequest $request */
765
        $request = service('request');
58✔
766

767
        foreach ($this->cookieStore->display() as $cookie) {
58✔
768
            if ($cookie->isSecure() && ! $request->isSecure()) {
58✔
769
                throw SecurityException::forInsecureCookie();
1✔
770
            }
771

772
            $name    = $cookie->getPrefixedName();
57✔
773
            $value   = $cookie->getValue();
57✔
774
            $options = $cookie->getOptions();
57✔
775

776
            if ($cookie->isRaw()) {
57✔
777
                $this->doSetRawCookie($name, $value, $options);
×
778
            } else {
779
                $this->doSetCookie($name, $value, $options);
57✔
780
            }
781
        }
782

783
        $this->cookieStore->clear();
57✔
784
    }
785

786
    /**
787
     * Extracted call to `setrawcookie()` in order to run unit tests on it.
788
     *
789
     * @codeCoverageIgnore
790
     */
791
    private function doSetRawCookie(string $name, string $value, array $options): void
792
    {
793
        setrawcookie($name, $value, $options);
794
    }
795

796
    /**
797
     * Extracted call to `setcookie()` in order to run unit tests on it.
798
     *
799
     * @codeCoverageIgnore
800
     */
801
    private function doSetCookie(string $name, string $value, array $options): void
802
    {
803
        setcookie($name, $value, $options);
804
    }
805

806
    /**
807
     * Force a download.
808
     *
809
     * Generates the headers that force a download to happen. And
810
     * sends the file to the browser.
811
     *
812
     * @param string      $filename The name you want the downloaded file to be named
813
     *                              or the path to the file to send
814
     * @param string|null $data     The data to be downloaded. Set null if the $filename is the file path
815
     * @param bool        $setMime  Whether to try and send the actual MIME type
816
     *
817
     * @return DownloadResponse|null
818
     */
819
    public function download(string $filename = '', $data = '', bool $setMime = false)
820
    {
821
        if ($filename === '' || $data === '') {
5✔
822
            return null;
1✔
823
        }
824

825
        if ($data === null) {
4✔
826
            $response = new DownloadResponse(basename($filename), $setMime);
2✔
827
            $response->setFilePath($filename);
2✔
828

829
            return $response;
2✔
830
        }
831

832
        $response = new DownloadResponse($filename, $setMime);
2✔
833
        $response->setBinary($data);
2✔
834

835
        return $response;
2✔
836
    }
837

838
    public function getCSP(): ContentSecurityPolicy
839
    {
840
        $this->CSP ??= service('csp');
99✔
841

842
        return $this->CSP;
99✔
843
    }
844
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc