From 46576baf5ec8b4321e6bcfdc8252d2ce5b8ffab4 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 12 Nov 2023 09:31:30 +0900 Subject: [PATCH 1/6] docs: fix typo in comment --- system/HTTP/Header.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 */ From 310d685b4e42733231608d187d644708ed6d9fa6 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 12 Nov 2023 09:41:46 +0900 Subject: [PATCH 2/6] test: fix @param types --- tests/system/HTTP/MessageTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/system/HTTP/MessageTest.php b/tests/system/HTTP/MessageTest.php index b30984a8c7a6..c628d40c7f3f 100644 --- a/tests/system/HTTP/MessageTest.php +++ b/tests/system/HTTP/MessageTest.php @@ -207,7 +207,7 @@ public static function provideArrayHeaderValue(): iterable /** * @dataProvider provideArrayHeaderValue * - * @param mixed $arrayHeaderValue + * @param array $arrayHeaderValue */ public function testSetHeaderWithExistingArrayValuesAppendStringValue($arrayHeaderValue): void { @@ -220,7 +220,7 @@ public function testSetHeaderWithExistingArrayValuesAppendStringValue($arrayHead /** * @dataProvider provideArrayHeaderValue * - * @param mixed $arrayHeaderValue + * @param array $arrayHeaderValue */ public function testSetHeaderWithExistingArrayValuesAppendArrayValue($arrayHeaderValue): void { From 37d52bb3e476caff6d981475d353d2d70e834649 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 12 Nov 2023 10:03:21 +0900 Subject: [PATCH 3/6] docs: add , to comment --- system/HTTP/MessageTrait.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/HTTP/MessageTrait.php b/system/HTTP/MessageTrait.php index ac3e5b18938c..490fb8e18f0e 100644 --- a/system/HTTP/MessageTrait.php +++ b/system/HTTP/MessageTrait.php @@ -93,7 +93,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; } } From 6c62851c9c9858f37bbb405978e92c7c57820671 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 12 Nov 2023 10:02:26 +0900 Subject: [PATCH 4/6] feat: add Message::addHeader() --- system/HTTP/MessageInterface.php | 4 +- system/HTTP/MessageTrait.php | 66 +++++++++++++++++++++++++++++-- tests/system/HTTP/MessageTest.php | 51 ++++++++++++++++++++++++ 3 files changed, 115 insertions(+), 6 deletions(-) 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 490fb8e18f0e..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 = []; @@ -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 c628d40c7f3f..c6cfb4d874cd 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 @@ -304,4 +305,54 @@ 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'); + } } From 40d551c21afe06131f9dcd7f6469fa056c010e90 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 15 Nov 2023 06:56:38 +0900 Subject: [PATCH 5/6] feat: add error check to getHeaderLine() --- system/HTTP/Message.php | 9 +++++++++ tests/system/HTTP/MessageTest.php | 19 +++++++++++++++++++ 2 files changed, 28 insertions(+) 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/tests/system/HTTP/MessageTest.php b/tests/system/HTTP/MessageTest.php index c6cfb4d874cd..1403605a0287 100644 --- a/tests/system/HTTP/MessageTest.php +++ b/tests/system/HTTP/MessageTest.php @@ -355,4 +355,23 @@ public function testAppendHeaderWithMultipleHeaders(): void $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'); + } } From 5d55cd04b761573c86a822728f39dd0c08c47bc3 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 20 Nov 2023 06:38:01 +0900 Subject: [PATCH 6/6] docs: add docs --- user_guide_src/source/changelogs/v4.5.0.rst | 2 ++ user_guide_src/source/incoming/message.rst | 16 +++++++++++++++- user_guide_src/source/incoming/message/011.php | 4 ++++ 3 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 user_guide_src/source/incoming/message/011.php 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=/');