Skip to content

Commit

Permalink
Merge pull request #8194 from kenjis/feat-Message-addHeader
Browse files Browse the repository at this point in the history
feat: add Message::addHeader() to add header with the same name
  • Loading branch information
kenjis authored Nov 20, 2023
2 parents 0153a66 + 5d55cd0 commit 9954ccf
Show file tree
Hide file tree
Showing 8 changed files with 168 additions and 11 deletions.
2 changes: 1 addition & 1 deletion system/HTTP/Header.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<int|string, array<string, string>|string>|string
*/
Expand Down
9 changes: 9 additions & 0 deletions system/HTTP/Message.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@

namespace CodeIgniter\HTTP;

use InvalidArgumentException;

/**
* An HTTP message
*
Expand Down Expand Up @@ -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)) {
Expand Down
4 changes: 2 additions & 2 deletions system/HTTP/MessageInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ public function populateHeaders(): void;
/**
* Returns an array containing all Headers.
*
* @return array<string, Header> An array of the Header objects
* @return array<string, Header|list<Header>> An array of the Header objects
*/
public function headers(): array;

Expand All @@ -83,7 +83,7 @@ public function hasHeader(string $name): bool;
*
* @param string $name
*
* @return array|Header|null
* @return Header|list<Header>|null
*/
public function header($name);

Expand Down
68 changes: 63 additions & 5 deletions system/HTTP/MessageTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
namespace CodeIgniter\HTTP;

use CodeIgniter\HTTP\Exceptions\HTTPException;
use InvalidArgumentException;

/**
* Message Trait
Expand All @@ -25,7 +26,11 @@ trait MessageTrait
/**
* List of all HTTP request headers.
*
* @var array<string, Header>
* [name => Header]
* or
* [name => [Header1, Header2]]
*
* @var array<string, Header|list<Header>>
*/
protected $headers = [];

Expand Down Expand Up @@ -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;
}
}
Expand All @@ -102,7 +107,7 @@ public function populateHeaders(): void
/**
* Returns an array containing all Headers.
*
* @return array<string, Header> An array of the Header objects
* @return array<string, Header|list<Header>> An array of the Header objects
*/
public function headers(): array
{
Expand All @@ -122,7 +127,7 @@ public function headers(): array
*
* @param string $name
*
* @return array|Header|null
* @return Header|list<Header>|null
*/
public function header($name)
{
Expand All @@ -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];
}
Expand All @@ -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.
*
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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);
Expand Down
74 changes: 72 additions & 2 deletions tests/system/HTTP/MessageTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

use CodeIgniter\HTTP\Exceptions\HTTPException;
use CodeIgniter\Test\CIUnitTestCase;
use InvalidArgumentException;

/**
* @internal
Expand Down Expand Up @@ -207,7 +208,7 @@ public static function provideArrayHeaderValue(): iterable
/**
* @dataProvider provideArrayHeaderValue
*
* @param mixed $arrayHeaderValue
* @param array $arrayHeaderValue
*/
public function testSetHeaderWithExistingArrayValuesAppendStringValue($arrayHeaderValue): void
{
Expand All @@ -220,7 +221,7 @@ public function testSetHeaderWithExistingArrayValuesAppendStringValue($arrayHead
/**
* @dataProvider provideArrayHeaderValue
*
* @param mixed $arrayHeaderValue
* @param array $arrayHeaderValue
*/
public function testSetHeaderWithExistingArrayValuesAppendArrayValue($arrayHeaderValue): void
{
Expand Down Expand Up @@ -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');
}
}
2 changes: 2 additions & 0 deletions user_guide_src/source/changelogs/v4.5.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
***************
Expand Down
16 changes: 15 additions & 1 deletion user_guide_src/source/incoming/message.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions user_guide_src/source/incoming/message/011.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?php

$message->addHeader('Set-Cookie', 'logged_in=no; Path=/');
$message->addHeader('Set-Cookie', 'sessid=123456; Path=/');

0 comments on commit 9954ccf

Please sign in to comment.