1313
1414namespace CodeIgniter \HTTP ;
1515
16+ use CodeIgniter \Config \Services ;
1617use CodeIgniter \Cookie \Cookie ;
1718use CodeIgniter \Cookie \CookieStore ;
1819use CodeIgniter \Cookie \Exceptions \CookieException ;
2122use CodeIgniter \I18n \Time ;
2223use CodeIgniter \Pager \PagerInterface ;
2324use CodeIgniter \Security \Exceptions \SecurityException ;
25+ use Config \App ;
26+ use Config \ContentSecurityPolicy as ContentSecurityPolicyConfig ;
2427use Config \Cookie as CookieConfig ;
2528use DateTime ;
2629use DateTimeZone ;
3134 * Additional methods to make a PSR-7 Response class
3235 * compliant with the framework's own ResponseInterface.
3336 *
37+ * @property array<int, string> $statusCodes
38+ * @property string|null $body
39+ *
3440 * @see https://github.com/php-fig/http-message/blob/master/src/ResponseInterface.php
3541 */
3642trait ResponseTrait
3743{
3844 /**
39- * Content security policy handler
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.
4049 *
41- * @var ContentSecurityPolicy
50+ * @var ContentSecurityPolicy|null
4251 */
4352 protected $ CSP ;
4453
4554 /**
4655 * CookieStore instance.
4756 *
48- * @var CookieStore
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
4961 */
5062 protected $ cookieStore ;
5163
@@ -77,19 +89,17 @@ trait ResponseTrait
7789 */
7890 public function setStatusCode (int $ code , string $ reason = '' )
7991 {
80- // Valid range?
8192 if ($ code < 100 || $ code > 599 ) {
8293 throw HTTPException::forInvalidStatusCode ($ code );
8394 }
8495
85- // Unknown and no message?
86- if (! array_key_exists ($ code , static ::$ statusCodes ) && ($ reason === '' )) {
96+ if (! array_key_exists ($ code , static ::$ statusCodes ) && $ reason === '' ) {
8797 throw HTTPException::forUnkownStatusCode ($ code );
8898 }
8999
90100 $ this ->statusCode = $ code ;
91101
92- $ this ->reason = ( $ reason !== '' ) ? $ reason : static ::$ statusCodes [$ code ];
102+ $ this ->reason = $ reason !== '' ? $ reason : static ::$ statusCodes [$ code ];
93103
94104 return $ this ;
95105 }
@@ -366,8 +376,10 @@ public function setLastModified($date)
366376 public function send ()
367377 {
368378 // If we're enforcing a Content Security Policy,
369- // we need to give it a chance to build out it's headers.
370- $ this ->CSP ->finalize ($ this );
379+ // we need to give it a chance to build out its headers.
380+ if ($ this ->shouldFinalizeCsp ()) {
381+ $ this ->getCSP ()->finalize ($ this );
382+ }
371383
372384 $ this ->sendHeaders ();
373385 $ this ->sendCookies ();
@@ -376,6 +388,44 @@ public function send()
376388 return $ this ;
377389 }
378390
391+ /**
392+ * Decides whether {@see ContentSecurityPolicy::finalize()} should run for
393+ * this response. Keeping the CSP class unloaded on requests that do not
394+ * need it avoids the cost of constructing a 1000+ line service on every
395+ * request.
396+ */
397+ private function shouldFinalizeCsp (): bool
398+ {
399+ // Developer already touched CSP through getCSP(); respect it.
400+ if ($ this ->CSP !== null ) {
401+ return true ;
402+ }
403+
404+ // A CSP instance has been registered (e.g., via Services::injectMock()
405+ // or any earlier service('csp') call) — reuse it instead of skipping.
406+ if (Services::has ('csp ' )) {
407+ return true ;
408+ }
409+
410+ if (config (App::class)->CSPEnabled ) {
411+ return true ;
412+ }
413+
414+ // Placeholders in the body still need to be stripped even when CSP
415+ // is disabled, so the body is scanned for the configured nonce tags
416+ // before committing to loading the full CSP class.
417+ $ body = (string ) $ this ->body ;
418+
419+ if ($ body === '' ) {
420+ return false ;
421+ }
422+
423+ $ cspConfig = config (ContentSecurityPolicyConfig::class);
424+
425+ return str_contains ($ body , $ cspConfig ->scriptNonceTag )
426+ || str_contains ($ body , $ cspConfig ->styleNonceTag );
427+ }
428+
379429 /**
380430 * Sends the headers of this HTTP response to the browser.
381431 *
@@ -518,8 +568,10 @@ public function setCookie(
518568 $ httponly = null ,
519569 $ samesite = null ,
520570 ) {
571+ $ store = $ this ->initializeCookieStore ();
572+
521573 if ($ name instanceof Cookie) {
522- $ this ->cookieStore = $ this -> cookieStore ->put ($ name );
574+ $ this ->cookieStore = $ store ->put ($ name );
523575
524576 return $ this ;
525577 }
@@ -553,7 +605,7 @@ public function setCookie(
553605 'samesite ' => $ samesite ?? '' ,
554606 ]);
555607
556- $ this ->cookieStore = $ this -> cookieStore ->put ($ cookie );
608+ $ this ->cookieStore = $ store ->put ($ cookie );
557609
558610 return $ this ;
559611 }
@@ -565,17 +617,18 @@ public function setCookie(
565617 */
566618 public function getCookieStore ()
567619 {
568- return $ this ->cookieStore ;
620+ return $ this ->initializeCookieStore () ;
569621 }
570622
571623 /**
572624 * Checks to see if the Response has a specified cookie or not.
573625 */
574626 public function hasCookie (string $ name , ?string $ value = null , string $ prefix = '' ): bool
575627 {
628+ $ store = $ this ->initializeCookieStore ();
576629 $ prefix = $ prefix !== '' ? $ prefix : Cookie::setDefaults ()['prefix ' ]; // to retain BC
577630
578- return $ this -> cookieStore ->has ($ name , $ prefix , $ value );
631+ return $ store ->has ($ name , $ prefix , $ value );
579632 }
580633
581634 /**
@@ -588,14 +641,16 @@ public function hasCookie(string $name, ?string $value = null, string $prefix =
588641 */
589642 public function getCookie (?string $ name = null , string $ prefix = '' )
590643 {
644+ $ store = $ this ->initializeCookieStore ();
645+
591646 if ((string ) $ name === '' ) {
592- return $ this -> cookieStore ->display ();
647+ return $ store ->display ();
593648 }
594649
595650 try {
596651 $ prefix = $ prefix !== '' ? $ prefix : Cookie::setDefaults ()['prefix ' ]; // to retain BC
597652
598- return $ this -> cookieStore ->get ($ name , $ prefix );
653+ return $ store ->get ($ name , $ prefix );
599654 } catch (CookieException $ e ) {
600655 log_message ('error ' , (string ) $ e );
601656
@@ -614,10 +669,10 @@ public function deleteCookie(string $name = '', string $domain = '', string $pat
614669 return $ this ;
615670 }
616671
672+ $ store = $ this ->initializeCookieStore ();
617673 $ prefix = $ prefix !== '' ? $ prefix : Cookie::setDefaults ()['prefix ' ]; // to retain BC
618674
619675 $ prefixed = $ prefix . $ name ;
620- $ store = $ this ->cookieStore ;
621676 $ found = false ;
622677
623678 /** @var Cookie $cookie */
@@ -653,6 +708,10 @@ public function deleteCookie(string $name = '', string $domain = '', string $pat
653708 */
654709 public function getCookies ()
655710 {
711+ if ($ this ->cookieStore === null ) {
712+ return [];
713+ }
714+
656715 return $ this ->cookieStore ->display ();
657716 }
658717
@@ -663,7 +722,7 @@ public function getCookies()
663722 */
664723 protected function sendCookies ()
665724 {
666- if ($ this ->pretend ) {
725+ if ($ this ->pretend || $ this -> cookieStore === null ) {
667726 return ;
668727 }
669728
@@ -753,6 +812,21 @@ public function download(string $filename = '', $data = '', bool $setMime = fals
753812
754813 public function getCSP (): ContentSecurityPolicy
755814 {
756- return $ this ->CSP ;
815+ return $ this ->CSP ??= service ('csp ' );
816+ }
817+
818+ /**
819+ * Lazily initializes the cookie store and the Cookie class defaults.
820+ * Called by every cookie-related method so cookie machinery is only
821+ * loaded when the developer actually interacts with cookies.
822+ */
823+ private function initializeCookieStore (): CookieStore
824+ {
825+ if ($ this ->cookieStore === null ) {
826+ Cookie::setDefaults (config (CookieConfig::class));
827+ $ this ->cookieStore = new CookieStore ([]);
828+ }
829+
830+ return $ this ->cookieStore ;
757831 }
758832}
0 commit comments