diff --git a/system/HTTP/Header.php b/system/HTTP/Header.php index 67c6cb0636d3..b152509a1cee 100644 --- a/system/HTTP/Header.php +++ b/system/HTTP/Header.php @@ -64,7 +64,7 @@ public function getName(): string /** * Gets the raw value of the header. This may return either a string - * of an array, depending on whether the header has multiple values or not. + * or an array, depending on whether the header has multiple values or not. * * @return array|string>|string */ diff --git a/system/HTTP/Message.php b/system/HTTP/Message.php index 9d0b517778a4..6de3835ddcdd 100644 --- a/system/HTTP/Message.php +++ b/system/HTTP/Message.php @@ -11,6 +11,8 @@ namespace CodeIgniter\HTTP; +use InvalidArgumentException; + /** * An HTTP message * @@ -112,6 +114,13 @@ public function hasHeader(string $name): bool */ public function getHeaderLine(string $name): string { + if ($this->hasMultipleHeaders($name)) { + throw new InvalidArgumentException( + 'The header "' . $name . '" already has multiple headers.' + . ' You cannot use getHeaderLine().' + ); + } + $origName = $this->getHeaderName($name); if (! array_key_exists($origName, $this->headers)) { diff --git a/system/HTTP/MessageInterface.php b/system/HTTP/MessageInterface.php index 99867bde5800..04a286fad92c 100644 --- a/system/HTTP/MessageInterface.php +++ b/system/HTTP/MessageInterface.php @@ -62,7 +62,7 @@ public function populateHeaders(): void; /** * Returns an array containing all Headers. * - * @return array An array of the Header objects + * @return array> An array of the Header objects */ public function headers(): array; @@ -83,7 +83,7 @@ public function hasHeader(string $name): bool; * * @param string $name * - * @return array|Header|null + * @return Header|list
|null */ public function header($name); diff --git a/system/HTTP/MessageTrait.php b/system/HTTP/MessageTrait.php index ac3e5b18938c..85280da092e8 100644 --- a/system/HTTP/MessageTrait.php +++ b/system/HTTP/MessageTrait.php @@ -12,6 +12,7 @@ namespace CodeIgniter\HTTP; use CodeIgniter\HTTP\Exceptions\HTTPException; +use InvalidArgumentException; /** * Message Trait @@ -25,7 +26,11 @@ trait MessageTrait /** * List of all HTTP request headers. * - * @var array + * [name => Header] + * or + * [name => [Header1, Header2]] + * + * @var array> */ protected $headers = []; @@ -93,7 +98,7 @@ public function populateHeaders(): void $this->setHeader($header, $_SERVER[$key]); - // Add us to the header map so we can find them case-insensitively + // Add us to the header map, so we can find them case-insensitively $this->headerMap[strtolower($header)] = $header; } } @@ -102,7 +107,7 @@ public function populateHeaders(): void /** * Returns an array containing all Headers. * - * @return array An array of the Header objects + * @return array> An array of the Header objects */ public function headers(): array { @@ -122,7 +127,7 @@ public function headers(): array * * @param string $name * - * @return array|Header|null + * @return Header|list
|null */ public function header($name) { @@ -140,9 +145,14 @@ public function header($name) */ public function setHeader(string $name, $value): self { + $this->checkMultipleHeaders($name); + $origName = $this->getHeaderName($name); - if (isset($this->headers[$origName]) && is_array($this->headers[$origName]->getValue())) { + if ( + isset($this->headers[$origName]) + && is_array($this->headers[$origName]->getValue()) + ) { if (! is_array($value)) { $value = [$value]; } @@ -158,6 +168,23 @@ public function setHeader(string $name, $value): self return $this; } + private function hasMultipleHeaders(string $name): bool + { + $origName = $this->getHeaderName($name); + + return isset($this->headers[$origName]) && is_array($this->headers[$origName]); + } + + private function checkMultipleHeaders(string $name): void + { + if ($this->hasMultipleHeaders($name)) { + throw new InvalidArgumentException( + 'The header "' . $name . '" already has multiple headers.' + . ' You cannot change them. If you really need to change, remove the header first.' + ); + } + } + /** * Removes a header from the list of headers we track. * @@ -179,6 +206,8 @@ public function removeHeader(string $name): self */ public function appendHeader(string $name, ?string $value): self { + $this->checkMultipleHeaders($name); + $origName = $this->getHeaderName($name); array_key_exists($origName, $this->headers) @@ -188,6 +217,33 @@ public function appendHeader(string $name, ?string $value): self return $this; } + /** + * Adds a header (not a header value) with the same name. + * Use this only when you set multiple headers with the same name, + * typically, for `Set-Cookie`. + * + * @return $this + */ + public function addHeader(string $name, string $value): static + { + $origName = $this->getHeaderName($name); + + if (! isset($this->headers[$origName])) { + $this->setHeader($name, $value); + + return $this; + } + + if (! $this->hasMultipleHeaders($name) && isset($this->headers[$origName])) { + $this->headers[$origName] = [$this->headers[$origName]]; + } + + // Add the header. + $this->headers[$origName][] = new Header($origName, $value); + + return $this; + } + /** * Adds an additional header value to any headers that accept * multiple values (i.e. are an array or implement ArrayAccess) @@ -196,6 +252,8 @@ public function appendHeader(string $name, ?string $value): self */ public function prependHeader(string $name, string $value): self { + $this->checkMultipleHeaders($name); + $origName = $this->getHeaderName($name); $this->headers[$origName]->prependValue($value); diff --git a/tests/system/HTTP/MessageTest.php b/tests/system/HTTP/MessageTest.php index b30984a8c7a6..1403605a0287 100644 --- a/tests/system/HTTP/MessageTest.php +++ b/tests/system/HTTP/MessageTest.php @@ -13,6 +13,7 @@ use CodeIgniter\HTTP\Exceptions\HTTPException; use CodeIgniter\Test\CIUnitTestCase; +use InvalidArgumentException; /** * @internal @@ -207,7 +208,7 @@ public static function provideArrayHeaderValue(): iterable /** * @dataProvider provideArrayHeaderValue * - * @param mixed $arrayHeaderValue + * @param array $arrayHeaderValue */ public function testSetHeaderWithExistingArrayValuesAppendStringValue($arrayHeaderValue): void { @@ -220,7 +221,7 @@ public function testSetHeaderWithExistingArrayValuesAppendStringValue($arrayHead /** * @dataProvider provideArrayHeaderValue * - * @param mixed $arrayHeaderValue + * @param array $arrayHeaderValue */ public function testSetHeaderWithExistingArrayValuesAppendArrayValue($arrayHeaderValue): void { @@ -304,4 +305,73 @@ public function testPopulateHeaders(): void $_SERVER = $original; // restore so code coverage doesn't break } + + public function testAddHeaderAddsFirstHeader(): void + { + $this->message->addHeader( + 'Set-Cookie', + 'logged_in=no; Path=/' + ); + + $header = $this->message->header('Set-Cookie'); + + $this->assertInstanceOf(Header::class, $header); + $this->assertSame('logged_in=no; Path=/', $header->getValue()); + } + + public function testAddHeaderAddsTwoHeaders(): void + { + $this->message->addHeader( + 'Set-Cookie', + 'logged_in=no; Path=/' + ); + $this->message->addHeader( + 'Set-Cookie', + 'sessid=123456; Path=/' + ); + + $headers = $this->message->header('Set-Cookie'); + + $this->assertCount(2, $headers); + $this->assertSame('logged_in=no; Path=/', $headers[0]->getValue()); + $this->assertSame('sessid=123456; Path=/', $headers[1]->getValue()); + } + + public function testAppendHeaderWithMultipleHeaders(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage( + 'The header "Set-Cookie" already has multiple headers. You cannot change them. If you really need to change, remove the header first.' + ); + + $this->message->addHeader( + 'Set-Cookie', + 'logged_in=no; Path=/' + ); + $this->message->addHeader( + 'Set-Cookie', + 'sessid=123456; Path=/' + ); + + $this->message->appendHeader('Set-Cookie', 'HttpOnly'); + } + + public function testGetHeaderLineWithMultipleHeaders(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage( + 'The header "Set-Cookie" already has multiple headers. You cannot use getHeaderLine().' + ); + + $this->message->addHeader( + 'Set-Cookie', + 'logged_in=no; Path=/' + ); + $this->message->addHeader( + 'Set-Cookie', + 'sessid=123456; Path=/' + ); + + $this->message->getHeaderLine('Set-Cookie'); + } } diff --git a/user_guide_src/source/changelogs/v4.5.0.rst b/user_guide_src/source/changelogs/v4.5.0.rst index 09e64936179f..076925e0cf6c 100644 --- a/user_guide_src/source/changelogs/v4.5.0.rst +++ b/user_guide_src/source/changelogs/v4.5.0.rst @@ -299,6 +299,8 @@ Others usage in your view files, which was supported by CodeIgniter 3. - **CSP:** Added ``ContentSecurityPolicy::clearDirective()`` method to clear existing CSP directives. See :ref:`csp-clear-directives`. +- **HTTP:** Added ``Message::addHeader()`` method to add another header with + the same name. See :php:meth:`CodeIgniter\\HTTP\\Message::addHeader()`. Message Changes *************** diff --git a/user_guide_src/source/incoming/message.rst b/user_guide_src/source/incoming/message.rst index 2609c0b5fb07..5ea83f1d2bd2 100644 --- a/user_guide_src/source/incoming/message.rst +++ b/user_guide_src/source/incoming/message.rst @@ -7,7 +7,7 @@ requests and responses, including the message body, protocol version, utilities the headers, and methods for handling content negotiation. This class is the parent class that both the :doc:`Request Class <../incoming/request>` and the -:doc:`Response Class <../outgoing/response>` extend from. +:doc:`Response Class <../outgoing/response>` extend from, and it is not used directly. *************** Class Reference @@ -146,6 +146,20 @@ Class Reference .. literalinclude:: message/009.php + .. php:method:: addHeader($name, $value) + + .. versionadded:: 4.5.0 + + :param string $name: The name of the header to add. + :param string $value: The value of the header. + :returns: The current message instance + :rtype: CodeIgniter\\HTTP\\Message + + Adds a header (not a header value) with the same name. + Use this only when you set multiple headers with the same name, + + .. literalinclude:: message/011.php + .. php:method:: getProtocolVersion() :returns: The current HTTP protocol version diff --git a/user_guide_src/source/incoming/message/011.php b/user_guide_src/source/incoming/message/011.php new file mode 100644 index 000000000000..b5b2c323c513 --- /dev/null +++ b/user_guide_src/source/incoming/message/011.php @@ -0,0 +1,4 @@ +addHeader('Set-Cookie', 'logged_in=no; Path=/'); +$message->addHeader('Set-Cookie', 'sessid=123456; Path=/');