Skip to content
Open
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
/test/phpunit/_coverage
/*.phar
.phpunit.*.cache
/test/phpunit/.phpunit.cache/
2 changes: 1 addition & 1 deletion src/Header/HeaderLine.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public function __construct(string $name, string...$values) {
}

public function __toString():string {
if(in_array($this->name, Headers::COMMA_HEADERS)) {
if(in_array($this->name, Headers::NON_COMBINABLE_HEADERS)) {
return $this->getValuesNewlineSeparated();
}
else {
Expand Down
69 changes: 48 additions & 21 deletions src/Header/Headers.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,8 @@
class Headers implements Iterator, Countable, TypeSafeGetter {
use NullableTypeSafeGetter;

const COMMA_HEADERS = [
// These cookies use commas within the value, so can't be comma separated.
"cookie-set",
"www-authenticate",
"proxy-authenticate"
const NON_COMBINABLE_HEADERS = [
"set-cookie",
];

/** @var HeaderLine[] */
Expand All @@ -43,17 +40,29 @@ public function asArray(bool $nested = false):array {

foreach($this->headerLines as $header) {
$name = $header->getName();
$nameLower = strtolower($name);

if($nested) {
$array[$name] = $header->getValues();
$array[$name] ??= [];
$array[$name] = array_merge($array[$name], $header->getValues());
continue;
}

if(in_array(strtolower($name), self::COMMA_HEADERS)) {
$array[$name] = $header->getValuesNewlineSeparated();
if(!array_key_exists($name, $array)) {
$array[$name] = "";
}

if(in_array($nameLower, self::NON_COMBINABLE_HEADERS)) {
if($array[$name] !== "") {
$array[$name] .= "\n";
}
$array[$name] .= $header->getValuesNewlineSeparated();
}
else {
$array[$name] = $header->getValuesCommaSeparated();
if($array[$name] !== "") {
$array[$name] .= ",";
}
$array[$name] .= $header->getValuesCommaSeparated();
}
}

Expand All @@ -67,7 +76,14 @@ public function fromArray(array $headerArray):void {
$value = [$value];
}

$this->headerLines []= new HeaderLine($key, ...$value);
if(in_array(strtolower($key), self::NON_COMBINABLE_HEADERS)) {
foreach($value as $singleValue) {
array_push($this->headerLines, new HeaderLine($key, $singleValue));
}
}
else {
array_push($this->headerLines, new HeaderLine($key, ...$value));
}
}
}

Expand All @@ -82,10 +98,11 @@ public function contains(string $name):bool {
}

public function add(string $name, string...$values):void {
$isCommaHeader = false;
if(strstr($values[0], ",")
&& in_array(strtolower($name), self::COMMA_HEADERS)) {
$isCommaHeader = true;
if(in_array(strtolower($name), self::NON_COMBINABLE_HEADERS)) {
foreach($values as $value) {
array_push($this->headerLines, new HeaderLine($name, $value));
}
return;
}

$headerLineToAdd = null;
Expand All @@ -97,7 +114,7 @@ public function add(string $name, string...$values):void {
$headerLineToAdd = $headerLine;
}

if(is_null($headerLineToAdd) || $isCommaHeader) {
if(is_null($headerLineToAdd)) {
array_push(
$this->headerLines,
new HeaderLine($name, ...$values)
Expand All @@ -123,24 +140,34 @@ public function remove(string $name):void {
}

public function get(string $name):?HeaderLine {
$matchingValues = [];
$headerName = null;

foreach($this->headerLines as $line) {
if($line->isNamed($name)) {
return $line;
$headerName ??= $line->getName();
$matchingValues = array_merge($matchingValues, $line->getValues());
}
}

return null;
if(!$headerName) {
return null;
}

return new HeaderLine($headerName, ...$matchingValues);
}

/** @return null|array<int, string> */
public function getAll(string $name):?array {
/** @return array<int, string> */
public function getAll(string $name):array {
$allValues = [];

foreach($this->headerLines as $line) {
if($line->isNamed($name)) {
return $line->getValues();
$allValues = array_merge($allValues, $line->getValues());
}
}

return null;
return $allValues;
}

public function getFirst():string {
Expand Down
16 changes: 14 additions & 2 deletions src/Header/Parser.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,20 @@ public function getKeyValues():array {
$kvp = explode(":", $h, 2);
$key = $kvp[0];
$value = $kvp[1] ?? "";
$value = trim($value);

$keyValues[$key] = trim($value);
if(array_key_exists($key, $keyValues)) {
if(strtolower($key) === "set-cookie") {
$keyValues[$key] .= "\n" . $value;
}
else {
$keyValues[$key] .= ", " . $value;
}

continue;
}

$keyValues[$key] = $value;
}

return $keyValues;
Expand All @@ -50,6 +62,6 @@ protected function pregMatchProtocol(string $matchName):string {
return "";
}

return (string)($matches[$matchName] ?? "");
return $matches[$matchName] ?? "";
}
}
59 changes: 55 additions & 4 deletions test/phpunit/Header/HeadersTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,12 @@ public function testAddMultiple() {
public function testAddMultipleCommaHeader() {
$headers = new Headers(self::HEADER_ARRAY);
$headers->add(
"Cookie-set",
"Set-Cookie",
"language=en; expires=Thu, 1-Jan-1970 00:00:00 UTC; path=/; domain=example.com",
"id=123; expires=Thu, 1-Jan-1970 00:00:00 UTC; path=/; domain=example.com httponly"
);
$headerArray = $headers->asArray();
$cookie = explode("\n", $headerArray["Cookie-set"]);
$cookie = explode("\n", $headerArray["Set-Cookie"]);
self::assertContains("language=en; expires=Thu, 1-Jan-1970 00:00:00 UTC; path=/; domain=example.com", $cookie);
self::assertContains("id=123; expires=Thu, 1-Jan-1970 00:00:00 UTC; path=/; domain=example.com httponly", $cookie);
}
Expand Down Expand Up @@ -115,15 +115,66 @@ public function testGetMultiple() {
public function testGetMultipleCommas() {
$headers = new Headers(self::HEADER_ARRAY);
$headers->add(
"Cookie-set",
"Set-Cookie",
"language=en; expires=Thu, 1-Jan-1970 00:00:00 UTC; path=/; domain=example.com",
"id=123; expires=Thu, 1-Jan-1970 00:00:00 UTC; path=/; domain=example.com httponly"
);
self::assertEquals(
"language=en; expires=Thu, 1-Jan-1970 00:00:00 UTC; path=/; domain=example.com"
. "\n"
. "id=123; expires=Thu, 1-Jan-1970 00:00:00 UTC; path=/; domain=example.com httponly",
$headers->get("Cookie-set")
$headers->get("Set-Cookie")
);
}

public function testAddMultipleSetCookieWithoutCommaPreservesSeparateLines():void {
$headers = new Headers(self::HEADER_ARRAY);
$headers->add("Set-Cookie", "language=en; Path=/");
$headers->add("Set-Cookie", "id=123; HttpOnly");

self::assertCount(5, $headers);
self::assertSame(
"language=en; Path=/\nid=123; HttpOnly",
$headers->asArray()["Set-Cookie"]
);
}

public function testGetAllAggregatesValuesAcrossSeparateSetCookieHeaderLines():void {
$firstValue = "language=en; Path=/";
$secondValue = "id=123; HttpOnly";
$headers = new Headers(self::HEADER_ARRAY);
$headers->add("Set-Cookie", $firstValue);
$headers->add("Set-Cookie", $secondValue);

self::assertSame(
[$firstValue, $secondValue],
$headers->getAll("Set-Cookie")
);
}

public function testAsArrayPreservesAllDuplicateSetCookieHeaderLines():void {
$firstValue = "language=en; Path=/";
$secondValue = "id=123; HttpOnly";
$headers = new Headers(self::HEADER_ARRAY);
$headers->add("Set-Cookie", $firstValue);
$headers->add("Set-Cookie", $secondValue);

self::assertSame(
$firstValue . "\n" . $secondValue,
$headers->asArray()["Set-Cookie"]
);
}

public function testAsArrayNestedPreservesAllDuplicateSetCookieHeaderLineValues():void {
$firstValue = "language=en; Path=/";
$secondValue = "id=123; HttpOnly";
$headers = new Headers(self::HEADER_ARRAY);
$headers->add("Set-Cookie", $firstValue);
$headers->add("Set-Cookie", $secondValue);

self::assertSame(
[$firstValue, $secondValue],
$headers->asArray(true)["Set-Cookie"]
);
}

Expand Down
14 changes: 14 additions & 0 deletions test/phpunit/Header/ParserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,18 @@ public function testGetKeyValuesResponse() {
self::assertArrayHasKey("X-Test-For", $keyValues);
self::assertEquals("PHP.Gt", $keyValues["X-Test-For"]);
}

public function testGetKeyValuesCombinesRepeatedListValuedHeaders():void {
$headers = <<<HEADERS
HTTP/1.1 200 OK
Accept: text/html
Accept: application/json
HEADERS;
$parser = new Parser($headers);

self::assertSame(
"text/html, application/json",
$parser->getKeyValues()["Accept"]
);
}
}
20 changes: 20 additions & 0 deletions test/phpunit/RequestTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,26 @@ public function testWithFormDataBodyAutomaticallySetsContentTypeHeader():void {
);
}

public function testGetHeaderReturnsAllValuesAcrossSeparateHeaderLines():void {
$headers = new RequestHeaders();
$headers->add("Set-Cookie", "language=en; Path=/");
$headers->add("Set-Cookie", "id=123; HttpOnly");

$request = new Request(
"GET",
self::getUriMock("/"),
$headers
);

self::assertSame(
[
"language=en; Path=/",
"id=123; HttpOnly",
],
$request->getHeader("Set-Cookie")
);
}

/** @return MockObject|Uri */
protected function getUriMock(string $uriPath = ""):MockObject {
$partPath = parse_url($uriPath, PHP_URL_PATH);
Expand Down
Loading