Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add Message::addHeader() to add header with the same name #8194

Merged
merged 6 commits into from
Nov 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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=/');
Loading