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

codeigniter4 / CodeIgniter4 / 12673986434

08 Jan 2025 03:42PM UTC coverage: 84.455% (+0.001%) from 84.454%
12673986434

Pull #9385

github

web-flow
Merge 06e47f0ee into e475fd8fa
Pull Request #9385: refactor: Fix phpstan expr.resultUnused

20699 of 24509 relevant lines covered (84.45%)

190.57 hits per line

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

98.99
/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\Cookie\Cookie;
17
use CodeIgniter\Cookie\CookieStore;
18
use CodeIgniter\Cookie\Exceptions\CookieException;
19
use CodeIgniter\Exceptions\InvalidArgumentException;
20
use CodeIgniter\HTTP\Exceptions\HTTPException;
21
use CodeIgniter\I18n\Time;
22
use CodeIgniter\Pager\PagerInterface;
23
use CodeIgniter\Security\Exceptions\SecurityException;
24
use Config\Cookie as CookieConfig;
25
use DateTime;
26
use DateTimeZone;
27

28
/**
29
 * Response Trait
30
 *
31
 * Additional methods to make a PSR-7 Response class
32
 * compliant with the framework's own ResponseInterface.
33
 *
34
 * @see https://github.com/php-fig/http-message/blob/master/src/ResponseInterface.php
35
 */
36
trait ResponseTrait
37
{
38
    /**
39
     * Content security policy handler
40
     *
41
     * @var ContentSecurityPolicy
42
     */
43
    protected $CSP;
44

45
    /**
46
     * CookieStore instance.
47
     *
48
     * @var CookieStore
49
     */
50
    protected $cookieStore;
51

52
    /**
53
     * Type of format the body is in.
54
     * Valid: html, json, xml
55
     *
56
     * @var string
57
     */
58
    protected $bodyFormat = 'html';
59

60
    /**
61
     * Return an instance with the specified status code and, optionally, reason phrase.
62
     *
63
     * If no reason phrase is specified, will default recommended reason phrase for
64
     * the response's status code.
65
     *
66
     * @see http://tools.ietf.org/html/rfc7231#section-6
67
     * @see http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
68
     *
69
     * @param int    $code   The 3-digit integer result code to set.
70
     * @param string $reason The reason phrase to use with the
71
     *                       provided status code; if none is provided, will
72
     *                       default to the IANA name.
73
     *
74
     * @return $this
75
     *
76
     * @throws HTTPException For invalid status code arguments.
77
     */
78
    public function setStatusCode(int $code, string $reason = '')
79
    {
80
        // Valid range?
81
        if ($code < 100 || $code > 599) {
243✔
82
            throw HTTPException::forInvalidStatusCode($code);
3✔
83
        }
84

85
        // Unknown and no message?
86
        if (! array_key_exists($code, static::$statusCodes) && ($reason === '')) {
240✔
87
            throw HTTPException::forUnkownStatusCode($code);
1✔
88
        }
89

90
        $this->statusCode = $code;
239✔
91

92
        $this->reason = ($reason !== '') ? $reason : static::$statusCodes[$code];
239✔
93

94
        return $this;
239✔
95
    }
96

97
    // --------------------------------------------------------------------
98
    // Convenience Methods
99
    // --------------------------------------------------------------------
100

101
    /**
102
     * Sets the date header
103
     *
104
     * @return $this
105
     */
106
    public function setDate(DateTime $date)
107
    {
108
        $date->setTimezone(new DateTimeZone('UTC'));
54✔
109

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

112
        return $this;
54✔
113
    }
114

115
    /**
116
     * Set the Link Header
117
     *
118
     * @see http://tools.ietf.org/html/rfc5988
119
     *
120
     * @return $this
121
     *
122
     * @todo Recommend moving to Pager
123
     */
124
    public function setLink(PagerInterface $pager)
125
    {
126
        $links    = '';
1✔
127
        $previous = $pager->getPreviousPageURI();
1✔
128

129
        if (is_string($previous) && $previous !== '') {
1✔
130
            $links .= '<' . $pager->getPageURI($pager->getFirstPage()) . '>; rel="first",';
1✔
131
            $links .= '<' . $previous . '>; rel="prev"';
1✔
132
        }
133

134
        $next = $pager->getNextPageURI();
1✔
135

136
        if (is_string($next) && $next !== '' && is_string($previous) && $previous !== '') {
1✔
137
            $links .= ',';
1✔
138
        }
139

140
        if (is_string($next) && $next !== '') {
1✔
141
            $links .= '<' . $next . '>; rel="next",';
1✔
142
            $links .= '<' . $pager->getPageURI($pager->getLastPage()) . '>; rel="last"';
1✔
143
        }
144

145
        $this->setHeader('Link', $links);
1✔
146

147
        return $this;
1✔
148
    }
149

150
    /**
151
     * Sets the Content Type header for this response with the mime type
152
     * and, optionally, the charset.
153
     *
154
     * @return $this
155
     */
156
    public function setContentType(string $mime, string $charset = 'UTF-8')
157
    {
158
        // add charset attribute if not already there and provided as parm
159
        if ((strpos($mime, 'charset=') < 1) && ($charset !== '')) {
743✔
160
            $mime .= '; charset=' . $charset;
743✔
161
        }
162

163
        $this->removeHeader('Content-Type'); // replace existing content type
743✔
164
        $this->setHeader('Content-Type', $mime);
743✔
165

166
        return $this;
743✔
167
    }
168

169
    /**
170
     * Converts the $body into JSON and sets the Content Type header.
171
     *
172
     * @param array|object|string $body
173
     *
174
     * @return $this
175
     */
176
    public function setJSON($body, bool $unencoded = false)
177
    {
178
        $this->body = $this->formatBody($body, 'json' . ($unencoded ? '-unencoded' : ''));
50✔
179

180
        return $this;
50✔
181
    }
182

183
    /**
184
     * Returns the current body, converted to JSON is it isn't already.
185
     *
186
     * @return string|null
187
     *
188
     * @throws InvalidArgumentException If the body property is not array.
189
     */
190
    public function getJSON()
191
    {
192
        $body = $this->body;
17✔
193

194
        if ($this->bodyFormat !== 'json') {
17✔
195
            $body = service('format')->getFormatter('application/json')->format($body);
3✔
196
        }
197

198
        return $body ?: null;
17✔
199
    }
200

201
    /**
202
     * Converts $body into XML, and sets the correct Content-Type.
203
     *
204
     * @param array|string $body
205
     *
206
     * @return $this
207
     */
208
    public function setXML($body)
209
    {
210
        $this->body = $this->formatBody($body, 'xml');
5✔
211

212
        return $this;
5✔
213
    }
214

215
    /**
216
     * Retrieves the current body into XML and returns it.
217
     *
218
     * @return bool|string|null
219
     *
220
     * @throws InvalidArgumentException If the body property is not array.
221
     */
222
    public function getXML()
223
    {
224
        $body = $this->body;
4✔
225

226
        if ($this->bodyFormat !== 'xml') {
4✔
227
            $body = service('format')->getFormatter('application/xml')->format($body);
1✔
228
        }
229

230
        return $body;
4✔
231
    }
232

233
    /**
234
     * Handles conversion of the data into the appropriate format,
235
     * and sets the correct Content-Type header for our response.
236
     *
237
     * @param array|object|string $body
238
     * @param string              $format Valid: json, xml
239
     *
240
     * @return false|string
241
     *
242
     * @throws InvalidArgumentException If the body property is not string or array.
243
     */
244
    protected function formatBody($body, string $format)
245
    {
246
        $this->bodyFormat = ($format === 'json-unencoded' ? 'json' : $format);
54✔
247
        $mime             = "application/{$this->bodyFormat}";
54✔
248
        $this->setContentType($mime);
54✔
249

250
        // Nothing much to do for a string...
251
        if (! is_string($body) || $format === 'json-unencoded') {
54✔
252
            $body = service('format')->getFormatter($mime)->format($body);
16✔
253
        }
254

255
        return $body;
54✔
256
    }
257

258
    // --------------------------------------------------------------------
259
    // Cache Control Methods
260
    //
261
    // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9
262
    // --------------------------------------------------------------------
263

264
    /**
265
     * Sets the appropriate headers to ensure this response
266
     * is not cached by the browsers.
267
     *
268
     * @return $this
269
     *
270
     * @todo Recommend researching these directives, might need: 'private', 'no-transform', 'no-store', 'must-revalidate'
271
     *
272
     * @see DownloadResponse::noCache()
273
     */
274
    public function noCache()
275
    {
276
        $this->removeHeader('Cache-Control');
715✔
277
        $this->setHeader('Cache-Control', ['no-store', 'max-age=0', 'no-cache']);
715✔
278

279
        return $this;
715✔
280
    }
281

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

314
        $this->removeHeader('Cache-Control');
2✔
315
        $this->removeHeader('ETag');
2✔
316

317
        // ETag
318
        if (isset($options['etag'])) {
2✔
319
            $this->setHeader('ETag', $options['etag']);
2✔
320
            unset($options['etag']);
2✔
321
        }
322

323
        // Last Modified
324
        if (isset($options['last-modified'])) {
2✔
325
            $this->setLastModified($options['last-modified']);
2✔
326

327
            unset($options['last-modified']);
2✔
328
        }
329

330
        $this->setHeader('Cache-Control', $options);
2✔
331

332
        return $this;
2✔
333
    }
334

335
    /**
336
     * Sets the Last-Modified date header.
337
     *
338
     * $date can be either a string representation of the date or,
339
     * preferably, an instance of DateTime.
340
     *
341
     * @param DateTime|string $date
342
     *
343
     * @return $this
344
     */
345
    public function setLastModified($date)
346
    {
347
        if ($date instanceof DateTime) {
5✔
348
            $date->setTimezone(new DateTimeZone('UTC'));
2✔
349
            $this->setHeader('Last-Modified', $date->format('D, d M Y H:i:s') . ' GMT');
2✔
350
        } elseif (is_string($date)) {
3✔
351
            $this->setHeader('Last-Modified', $date);
3✔
352
        }
353

354
        return $this;
5✔
355
    }
356

357
    // --------------------------------------------------------------------
358
    // Output Methods
359
    // --------------------------------------------------------------------
360

361
    /**
362
     * Sends the output to the browser.
363
     *
364
     * @return $this
365
     */
366
    public function send()
367
    {
368
        // If we're enforcing a Content Security Policy,
369
        // we need to give it a chance to build out it's headers.
370
        if ($this->CSP->enabled()) {
87✔
371
            $this->CSP->finalize($this);
31✔
372
        } else {
373
            $this->body = str_replace(['{csp-style-nonce}', '{csp-script-nonce}'], '', $this->body ?? '');
56✔
374
        }
375

376
        $this->sendHeaders();
87✔
377
        $this->sendCookies();
87✔
378
        $this->sendBody();
86✔
379

380
        return $this;
86✔
381
    }
382

383
    /**
384
     * Sends the headers of this HTTP response to the browser.
385
     *
386
     * @return $this
387
     */
388
    public function sendHeaders()
389
    {
390
        // Have the headers already been sent?
391
        if ($this->pretend || headers_sent()) {
90✔
392
            return $this;
38✔
393
        }
394

395
        // Per spec, MUST be sent with each request, if possible.
396
        // http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html
397
        if (! isset($this->headers['Date']) && PHP_SAPI !== 'cli-server') {
52✔
398
            $this->setDate(DateTime::createFromFormat('U', (string) Time::now()->getTimestamp()));
52✔
399
        }
400

401
        // HTTP Status
402
        header(sprintf('HTTP/%s %s %s', $this->getProtocolVersion(), $this->getStatusCode(), $this->getReasonPhrase()), true, $this->getStatusCode());
52✔
403

404
        // Send all of our headers
405
        foreach ($this->headers() as $name => $value) {
52✔
406
            if ($value instanceof Header) {
52✔
407
                header(
52✔
408
                    $name . ': ' . $value->getValueLine(),
52✔
409
                    true,
52✔
410
                    $this->getStatusCode()
52✔
411
                );
52✔
412
            } else {
413
                $replace = true;
1✔
414

415
                foreach ($value as $header) {
1✔
416
                    header(
1✔
417
                        $name . ': ' . $header->getValueLine(),
1✔
418
                        $replace,
1✔
419
                        $this->getStatusCode()
1✔
420
                    );
1✔
421
                    $replace = false;
1✔
422
                }
423
            }
424
        }
425

426
        return $this;
52✔
427
    }
428

429
    /**
430
     * Sends the Body of the message to the browser.
431
     *
432
     * @return $this
433
     */
434
    public function sendBody()
435
    {
436
        echo $this->body;
86✔
437

438
        return $this;
86✔
439
    }
440

441
    /**
442
     * Perform a redirect to a new URL, in two flavors: header or location.
443
     *
444
     * @param string   $uri  The URI to redirect to
445
     * @param int|null $code The type of redirection, defaults to 302
446
     *
447
     * @return $this
448
     *
449
     * @throws HTTPException For invalid status code.
450
     */
451
    public function redirect(string $uri, string $method = 'auto', ?int $code = null)
452
    {
453
        // IIS environment likely? Use 'refresh' for better compatibility
454
        if (
455
            $method === 'auto'
61✔
456
            && isset($_SERVER['SERVER_SOFTWARE'])
61✔
457
            && str_contains($_SERVER['SERVER_SOFTWARE'], 'Microsoft-IIS')
61✔
458
        ) {
459
            $method = 'refresh';
6✔
460
        } elseif ($method !== 'refresh' && $code === null) {
55✔
461
            // override status code for HTTP/1.1 & higher
462
            if (
463
                isset($_SERVER['SERVER_PROTOCOL'], $_SERVER['REQUEST_METHOD'])
35✔
464
                && $this->getProtocolVersion() >= 1.1
35✔
465
            ) {
466
                if ($_SERVER['REQUEST_METHOD'] === Method::GET) {
8✔
467
                    $code = 302;
2✔
468
                } elseif (in_array($_SERVER['REQUEST_METHOD'], [Method::POST, Method::PUT, Method::DELETE], true)) {
6✔
469
                    // reference: https://en.wikipedia.org/wiki/Post/Redirect/Get
470
                    $code = 303;
4✔
471
                } else {
472
                    $code = 307;
2✔
473
                }
474
            }
475
        }
476

477
        if ($code === null) {
61✔
478
            $code = 302;
30✔
479
        }
480

481
        match ($method) {
482
            'refresh' => $this->setHeader('Refresh', '0;url=' . $uri),
61✔
483
            default   => $this->setHeader('Location', $uri),
54✔
484
        };
485

486
        $this->setStatusCode($code);
61✔
487

488
        return $this;
61✔
489
    }
490

491
    /**
492
     * Set a cookie
493
     *
494
     * Accepts an arbitrary number of binds (up to 7) or an associative
495
     * array in the first parameter containing all the values.
496
     *
497
     * @param array|Cookie|string $name     Cookie name / array containing binds / Cookie object
498
     * @param string              $value    Cookie value
499
     * @param int                 $expire   Cookie expiration time in seconds
500
     * @param string              $domain   Cookie domain (e.g.: '.yourdomain.com')
501
     * @param string              $path     Cookie path (default: '/')
502
     * @param string              $prefix   Cookie name prefix ('': the default prefix)
503
     * @param bool|null           $secure   Whether to only transfer cookies via SSL
504
     * @param bool|null           $httponly Whether only make the cookie accessible via HTTP (no javascript)
505
     * @param string|null         $samesite
506
     *
507
     * @return $this
508
     */
509
    public function setCookie(
510
        $name,
511
        $value = '',
512
        $expire = 0,
513
        $domain = '',
514
        $path = '/',
515
        $prefix = '',
516
        $secure = null,
517
        $httponly = null,
518
        $samesite = null
519
    ) {
520
        if ($name instanceof Cookie) {
143✔
521
            $this->cookieStore = $this->cookieStore->put($name);
67✔
522

523
            return $this;
67✔
524
        }
525

526
        $cookieConfig = config(CookieConfig::class);
76✔
527

528
        $secure ??= $cookieConfig->secure;
76✔
529
        $httponly ??= $cookieConfig->httponly;
76✔
530
        $samesite ??= $cookieConfig->samesite;
76✔
531

532
        if (is_array($name)) {
76✔
533
            // always leave 'name' in last place, as the loop will break otherwise, due to ${$item}
534
            foreach (['samesite', 'value', 'expire', 'domain', 'path', 'prefix', 'secure', 'httponly', 'name'] as $item) {
19✔
535
                if (isset($name[$item])) {
19✔
536
                    ${$item} = $name[$item];
19✔
537
                }
538
            }
539
        }
540

541
        if (is_numeric($expire)) {
76✔
542
            $expire = $expire > 0 ? Time::now()->getTimestamp() + $expire : 0;
73✔
543
        }
544

545
        $cookie = new Cookie($name, $value, [
76✔
546
            'expires'  => $expire ?: 0,
76✔
547
            'domain'   => $domain,
76✔
548
            'path'     => $path,
76✔
549
            'prefix'   => $prefix,
76✔
550
            'secure'   => $secure,
76✔
551
            'httponly' => $httponly,
76✔
552
            'samesite' => $samesite ?? '',
76✔
553
        ]);
76✔
554

555
        $this->cookieStore = $this->cookieStore->put($cookie);
74✔
556

557
        return $this;
74✔
558
    }
559

560
    /**
561
     * Returns the `CookieStore` instance.
562
     *
563
     * @return CookieStore
564
     */
565
    public function getCookieStore()
566
    {
567
        return $this->cookieStore;
6✔
568
    }
569

570
    /**
571
     * Checks to see if the Response has a specified cookie or not.
572
     */
573
    public function hasCookie(string $name, ?string $value = null, string $prefix = ''): bool
574
    {
575
        $prefix = $prefix !== '' ? $prefix : Cookie::setDefaults()['prefix']; // to retain BC
27✔
576

577
        return $this->cookieStore->has($name, $prefix, $value);
27✔
578
    }
579

580
    /**
581
     * Returns the cookie
582
     *
583
     * @param string $prefix Cookie prefix.
584
     *                       '': the default prefix
585
     *
586
     * @return array<string, Cookie>|Cookie|null
587
     */
588
    public function getCookie(?string $name = null, string $prefix = '')
589
    {
590
        if ((string) $name === '') {
18✔
591
            return $this->cookieStore->display();
1✔
592
        }
593

594
        try {
595
            $prefix = $prefix !== '' ? $prefix : Cookie::setDefaults()['prefix']; // to retain BC
18✔
596

597
            return $this->cookieStore->get($name, $prefix);
18✔
598
        } catch (CookieException $e) {
1✔
599
            log_message('error', (string) $e);
1✔
600

601
            return null;
1✔
602
        }
603
    }
604

605
    /**
606
     * Sets a cookie to be deleted when the response is sent.
607
     *
608
     * @return $this
609
     */
610
    public function deleteCookie(string $name = '', string $domain = '', string $path = '/', string $prefix = '')
611
    {
612
        if ($name === '') {
11✔
613
            return $this;
1✔
614
        }
615

616
        $prefix = $prefix !== '' ? $prefix : Cookie::setDefaults()['prefix']; // to retain BC
11✔
617

618
        $prefixed = $prefix . $name;
11✔
619
        $store    = $this->cookieStore;
11✔
620
        $found    = false;
11✔
621

622
        /** @var Cookie $cookie */
623
        foreach ($store as $cookie) {
11✔
624
            if ($cookie->getPrefixedName() === $prefixed) {
10✔
625
                if ($domain !== $cookie->getDomain()) {
10✔
626
                    continue;
1✔
627
                }
628

629
                if ($path !== $cookie->getPath()) {
10✔
630
                    continue;
1✔
631
                }
632

633
                $cookie = $cookie->withValue('')->withExpired();
10✔
634
                $found  = true;
10✔
635

636
                $this->cookieStore = $store->put($cookie);
10✔
637
                break;
10✔
638
            }
639
        }
640

641
        if (! $found) {
11✔
642
            $this->setCookie($name, '', 0, $domain, $path, $prefix);
2✔
643
        }
644

645
        return $this;
11✔
646
    }
647

648
    /**
649
     * Returns all cookies currently set.
650
     *
651
     * @return array<string, Cookie>
652
     */
653
    public function getCookies()
654
    {
655
        return $this->cookieStore->display();
9✔
656
    }
657

658
    /**
659
     * Actually sets the cookies.
660
     *
661
     * @return void
662
     */
663
    protected function sendCookies()
664
    {
665
        if ($this->pretend) {
87✔
666
            return;
37✔
667
        }
668

669
        $this->dispatchCookies();
50✔
670
    }
671

672
    private function dispatchCookies(): void
673
    {
674
        /** @var IncomingRequest $request */
675
        $request = service('request');
50✔
676

677
        foreach ($this->cookieStore->display() as $cookie) {
50✔
678
            if ($cookie->isSecure() && ! $request->isSecure()) {
38✔
679
                throw SecurityException::forInsecureCookie();
1✔
680
            }
681

682
            $name    = $cookie->getPrefixedName();
37✔
683
            $value   = $cookie->getValue();
37✔
684
            $options = $cookie->getOptions();
37✔
685

686
            if ($cookie->isRaw()) {
37✔
687
                $this->doSetRawCookie($name, $value, $options);
×
688
            } else {
689
                $this->doSetCookie($name, $value, $options);
37✔
690
            }
691
        }
692

693
        $this->cookieStore->clear();
49✔
694
    }
695

696
    /**
697
     * Extracted call to `setrawcookie()` in order to run unit tests on it.
698
     *
699
     * @codeCoverageIgnore
700
     */
701
    private function doSetRawCookie(string $name, string $value, array $options): void
702
    {
703
        setrawcookie($name, $value, $options);
×
704
    }
705

706
    /**
707
     * Extracted call to `setcookie()` in order to run unit tests on it.
708
     *
709
     * @codeCoverageIgnore
710
     */
711
    private function doSetCookie(string $name, string $value, array $options): void
712
    {
713
        setcookie($name, $value, $options);
37✔
714
    }
715

716
    /**
717
     * Force a download.
718
     *
719
     * Generates the headers that force a download to happen. And
720
     * sends the file to the browser.
721
     *
722
     * @param string      $filename The name you want the downloaded file to be named
723
     *                              or the path to the file to send
724
     * @param string|null $data     The data to be downloaded. Set null if the $filename is the file path
725
     * @param bool        $setMime  Whether to try and send the actual MIME type
726
     *
727
     * @return DownloadResponse|null
728
     */
729
    public function download(string $filename = '', $data = '', bool $setMime = false)
730
    {
731
        if ($filename === '' || $data === '') {
4✔
732
            return null;
1✔
733
        }
734

735
        $filepath = '';
3✔
736
        if ($data === null) {
3✔
737
            $filepath = $filename;
1✔
738
            $filename = explode('/', str_replace(DIRECTORY_SEPARATOR, '/', $filename));
1✔
739
            $filename = end($filename);
1✔
740
        }
741

742
        $response = new DownloadResponse($filename, $setMime);
3✔
743

744
        if ($filepath !== '') {
3✔
745
            $response->setFilePath($filepath);
1✔
746
        } elseif ($data !== null) {
2✔
747
            $response->setBinary($data);
2✔
748
        }
749

750
        return $response;
3✔
751
    }
752

753
    public function getCSP(): ContentSecurityPolicy
754
    {
755
        return $this->CSP;
42✔
756
    }
757
}
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

© 2025 Coveralls, Inc