Skip to content

Commit c9d5c42

Browse files
committed
- improved header methods (PSR-7)
- unit tests updated
1 parent 9258440 commit c9d5c42

File tree

3 files changed

+90
-20
lines changed

3 files changed

+90
-20
lines changed

HeaderTrait.php

Lines changed: 72 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@
1212

1313
namespace Koded\Http;
1414

15+
use InvalidArgumentException;
16+
use Throwable;
17+
1518

1619
trait HeaderTrait
1720
{
18-
1921
/**
2022
* @var array Message headers.
2123
*/
@@ -52,9 +54,11 @@ public function getHeaderLine($name): string
5254
public function withHeader($name, $value): self
5355
{
5456
$instance = clone $this;
57+
$name = $instance->normalizeHeaderName($name);
5558

5659
$instance->headersMap[strtolower($name)] = $name;
57-
$instance->headers[$name] = (array)$value;
60+
61+
$instance->headers[$name] = $this->normalizeHeaderValue($name, $value);
5862

5963
return $instance;
6064
}
@@ -73,22 +77,24 @@ public function withHeaders(array $headers): self
7377
public function withoutHeader($name): self
7478
{
7579
$instance = clone $this;
76-
$key = strtolower($name);
77-
unset($instance->headersMap[$key], $instance->headers[$this->headersMap[$key]]);
80+
$name = strtolower($name);
81+
unset($instance->headers[$this->headersMap[$name]], $instance->headersMap[$name]);
7882

7983
return $instance;
8084
}
8185

8286
public function withAddedHeader($name, $value): self
8387
{
84-
$value = (array)$value;
8588
$instance = clone $this;
89+
$name = $instance->normalizeHeaderName($name);
90+
$value = $instance->normalizeHeaderValue($name, $value);
8691

8792
if (isset($instance->headersMap[$header = strtolower($name)])) {
88-
$instance->headers[$name] = array_unique(array_merge((array)$this->headers[$name], $value));
93+
$header = $instance->headersMap[$header];
94+
$instance->headers[$header] = array_unique(array_merge((array)$instance->headers[$header], $value));
8995
} else {
90-
$instance->headersMap[strtolower($name)] = $name;
91-
$instance->headers[$name] = $value;
96+
$instance->headersMap[$header] = $name;
97+
$instance->headers[$name] = $value;
9298
}
9399

94100
return $instance;
@@ -149,22 +155,22 @@ public function getCanonicalizedHeaders(array $names = []): string
149155

150156
/**
151157
* @param string $name
152-
* @param string $value
158+
* @param mixed $value
153159
* @param bool $skipKey
154160
*
155161
* @return void
156162
*/
157163
protected function normalizeHeader(string $name, $value, bool $skipKey): void
158164
{
159-
$name = trim($name);
165+
$name = str_replace(["\r", "\n", "\t"], '', trim($name));
160166

161167
if (false === $skipKey) {
162168
$name = ucwords(str_replace('_', '-', strtolower($name)), '-');
163169
}
164170

165171
$this->headersMap[strtolower($name)] = $name;
166172

167-
$this->headers[$name] = array_map('trim', (array)$value);
173+
$this->headers[$name] = $this->normalizeHeaderValue($name, $value);
168174
}
169175

170176
/**
@@ -180,4 +186,59 @@ protected function setHeaders(array $headers)
180186

181187
return $this;
182188
}
189+
190+
/**
191+
* @param string $name
192+
*
193+
* @return string Normalized header name
194+
*/
195+
protected function normalizeHeaderName($name): string
196+
{
197+
try {
198+
$name = str_replace(["\r", "\n", "\t"], '', trim($name));
199+
} catch (Throwable $e) {
200+
throw new InvalidArgumentException(
201+
sprintf('Header name must be a string, %s given', gettype($name)), StatusCode::BAD_REQUEST
202+
);
203+
}
204+
205+
if ('' === $name) {
206+
throw new InvalidArgumentException('Empty header name', StatusCode::BAD_REQUEST);
207+
}
208+
209+
return $name;
210+
}
211+
212+
/**
213+
* @param string $name
214+
* @param string|string[] $value
215+
*
216+
* @return array
217+
*/
218+
protected function normalizeHeaderValue(string $name, $value): array
219+
{
220+
$type = gettype($value);
221+
switch ($type) {
222+
case 'array':
223+
case 'integer':
224+
case 'double':
225+
case 'string':
226+
$value = (array)$value;
227+
break;
228+
default:
229+
throw new InvalidArgumentException(
230+
sprintf('Invalid header value, expects string or array, "%s" given', $type), StatusCode::BAD_REQUEST
231+
);
232+
}
233+
234+
if (empty($value = array_map(function($v) {
235+
return trim(preg_replace('/\s+/', ' ', $v));
236+
}, $value))) {
237+
throw new InvalidArgumentException(
238+
sprintf('The value for header "%s" cannot be empty', $name), StatusCode::BAD_REQUEST
239+
);
240+
}
241+
242+
return $value;
243+
}
183244
}

Tests/HeaderTraitTest.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -174,14 +174,16 @@ public function test_normalizing_headers_key_and_value()
174174
{
175175
$this->SUT = $this->SUT->withHeaders([
176176
"HTTP/1.1 401 Authorization Required\r\n" => "\r\n",
177-
"cache-control\r\n" => " no-cache, no-store, must-revalidate, pre-check=0, post-check=0\r\n",
178-
"x-xss-protection\r\n" => "0 \r\n"
177+
"cache-control\n" => " no-cache, no-store, must-revalidate, pre-check=0, post-check=0\r\n",
178+
"x-xss-protection\r\n" => "0 \r\n",
179+
" Nasty-\tHeader-\r\nName" => "weird\nvalue\r",
179180
]);
180181

181182
$this->assertSame([
182183
'Http/1.1 401 authorization required' => [''],
183184
'Cache-Control' => ['no-cache, no-store, must-revalidate, pre-check=0, post-check=0'],
184-
'X-Xss-Protection' => ['0']
185+
'X-Xss-Protection' => ['0'],
186+
"Nasty-Header-Name" => ["weird value"],
185187
], $this->SUT->getHeaders());
186188
}
187189

Tests/Integration/RequestIntegrationTest.php

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,26 @@
66

77
class RequestIntegrationTest extends \Http\Psr7Test\RequestIntegrationTest
88
{
9-
109
protected $skippedTests = [
1110
'testUri' => 'Skipped because of the host requirement',
1211
'testMethod' => 'Implementation uses constants where capitalization matters',
1312
'testMethodWithInvalidArguments' => 'Does not make sense for strict type implementation',
14-
'testWithHeaderInvalidArguments' => 'Does not make sense for strict type implementation',
15-
'testWithAddedHeaderInvalidArguments' => 'Does not make sense for strict type implementation',
16-
'testWithAddedHeaderArrayValueAndKeys' => 'Skipped, the test is weird',
17-
'testWithAddedHeader' => 'Skipped, the test is weird',
1813
'testUriPreserveHost_NoHost_Host' => 'Skipped because of the host requirement',
1914
];
2015

16+
/**
17+
* @overridden The header is not merged as the test authors think it should
18+
*/
19+
public function testWithAddedHeaderArrayValueAndKeys()
20+
{
21+
$message = $this->getMessage()->withAddedHeader('content-type', ['foo' => 'text/html']);
22+
$message = $message->withAddedHeader('content-type', ['foo' => 'text/plain', 'bar' => 'application/json']);
23+
$headerLine = $message->getHeaderLine('content-type');
24+
25+
$this->assertRegExp('|text/plain|', $headerLine);
26+
$this->assertRegExp('|application/json|', $headerLine);
27+
}
28+
2129
/**
2230
* @return RequestInterface that is used in the tests
2331
*/
@@ -26,5 +34,4 @@ public function createSubject()
2634
unset($_SERVER['HTTP_HOST']);
2735
return new ClientRequest('GET', '');
2836
}
29-
3037
}

0 commit comments

Comments
 (0)