Skip to content
This repository was archived by the owner on Jan 29, 2020. It is now read-only.

Commit c8664b9

Browse files
committed
Merge branch 'hotfix/273'
Close #273
2 parents fb7f06e + 8b2bbca commit c8664b9

File tree

9 files changed

+129
-38
lines changed

9 files changed

+129
-38
lines changed

CHANGELOG.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,18 @@
22

33
All notable changes to this project will be documented in this file, in reverse chronological order by release.
44

5-
## 1.6.1 - TBD
5+
## 1.6.1 - 2017-10-12
66

77
### Added
88

99
- Nothing.
1010

1111
### Changed
1212

13-
- Nothing.
13+
- [#273](https://github.com/zendframework/zend-diactoros/pull/273) updates each
14+
of the SAPI emitter implementations to emit the status line after emitting
15+
other headers; this is done to ensure that the status line is not overridden
16+
by PHP.
1417

1518
### Deprecated
1619

@@ -22,7 +25,10 @@ All notable changes to this project will be documented in this file, in reverse
2225

2326
### Fixed
2427

25-
- Nothing.
28+
- [#273](https://github.com/zendframework/zend-diactoros/pull/273) modifies how
29+
the `SapiEmitterTrait` calls `header()` to ensure that a response code is
30+
_always_ passed as the third argument; this is done to prevent PHP from
31+
silently overriding it.
2632

2733
## 1.6.0 - 2017-09-13
2834

src/Response/SapiEmitter.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ public function emit(ResponseInterface $response)
2626
{
2727
$this->assertNoPreviousOutput();
2828

29-
$this->emitStatusLine($response);
3029
$this->emitHeaders($response);
30+
$this->emitStatusLine($response);
3131
$this->emitBody($response);
3232
}
3333

src/Response/SapiEmitterTrait.php

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,17 +38,25 @@ private function assertNoPreviousOutput()
3838
* Emits the status line using the protocol version and status code from
3939
* the response; if a reason phrase is available, it, too, is emitted.
4040
*
41+
* It is important to mention that this method should be called after
42+
* `emitHeaders()` in order to prevent PHP from changing the status code of
43+
* the emitted response.
44+
*
4145
* @param ResponseInterface $response
46+
*
47+
* @see \Zend\Diactoros\Response\SapiEmitterTrait::emitHeaders()
4248
*/
4349
private function emitStatusLine(ResponseInterface $response)
4450
{
4551
$reasonPhrase = $response->getReasonPhrase();
52+
$statusCode = $response->getStatusCode();
53+
4654
header(sprintf(
4755
'HTTP/%s %d%s',
4856
$response->getProtocolVersion(),
49-
$response->getStatusCode(),
57+
$statusCode,
5058
($reasonPhrase ? ' ' . $reasonPhrase : '')
51-
));
59+
), true, $statusCode);
5260
}
5361

5462
/**
@@ -63,6 +71,8 @@ private function emitStatusLine(ResponseInterface $response)
6371
*/
6472
private function emitHeaders(ResponseInterface $response)
6573
{
74+
$statusCode = $response->getStatusCode();
75+
6676
foreach ($response->getHeaders() as $header => $values) {
6777
$name = $this->filterHeader($header);
6878
$first = $name === 'Set-Cookie' ? false : true;
@@ -71,7 +81,7 @@ private function emitHeaders(ResponseInterface $response)
7181
'%s: %s',
7282
$name,
7383
$value
74-
), $first);
84+
), $first, $statusCode);
7585
$first = false;
7686
}
7787
}

src/Response/SapiStreamEmitter.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ class SapiStreamEmitter implements EmitterInterface
2727
public function emit(ResponseInterface $response, $maxBufferLength = 8192)
2828
{
2929
$this->assertNoPreviousOutput();
30-
$this->emitStatusLine($response);
3130
$this->emitHeaders($response);
31+
$this->emitStatusLine($response);
3232

3333
$range = $this->parseContentRange($response->getHeaderLine('Content-Range'));
3434

test/Response/AbstractEmitterTest.php

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,9 @@ public function testEmitsResponseHeaders()
4040
ob_start();
4141
$this->emitter->emit($response);
4242
ob_end_clean();
43-
$this->assertContains('HTTP/1.1 200 OK', HeaderStack::stack());
44-
$this->assertContains('Content-Type: text/plain', HeaderStack::stack());
43+
44+
$this->assertTrue(HeaderStack::has('HTTP/1.1 200 OK'));
45+
$this->assertTrue(HeaderStack::has('Content-Type: text/plain'));
4546
}
4647

4748
public function testEmitsMessageBody()
@@ -55,6 +56,42 @@ public function testEmitsMessageBody()
5556
$this->emitter->emit($response);
5657
}
5758

59+
public function testMultipleSetCookieHeadersAreNotReplaced()
60+
{
61+
$response = (new Response())
62+
->withStatus(200)
63+
->withAddedHeader('Set-Cookie', 'foo=bar')
64+
->withAddedHeader('Set-Cookie', 'bar=baz');
65+
66+
$this->emitter->emit($response);
67+
68+
$expectedStack = [
69+
['header' => 'Set-Cookie: foo=bar', 'replace' => false, 'status_code' => 200],
70+
['header' => 'Set-Cookie: bar=baz', 'replace' => false, 'status_code' => 200],
71+
['header' => 'HTTP/1.1 200 OK', 'replace' => true, 'status_code' => 200],
72+
];
73+
74+
$this->assertSame($expectedStack, HeaderStack::stack());
75+
}
76+
77+
public function testDoesNotLetResponseCodeBeOverriddenByPHP()
78+
{
79+
$response = (new Response())
80+
->withStatus(202)
81+
->withAddedHeader('Location', 'http://api.my-service.com/12345678')
82+
->withAddedHeader('Content-Type', 'text/plain');
83+
84+
$this->emitter->emit($response);
85+
86+
$expectedStack = [
87+
['header' => 'Location: http://api.my-service.com/12345678', 'replace' => true, 'status_code' => 202],
88+
['header' => 'Content-Type: text/plain', 'replace' => true, 'status_code' => 202],
89+
['header' => 'HTTP/1.1 202 Accepted', 'replace' => true, 'status_code' => 202],
90+
];
91+
92+
$this->assertSame($expectedStack, HeaderStack::stack());
93+
}
94+
5895
public function testDoesNotInjectContentLengthHeaderIfStreamSizeIsUnknown()
5996
{
6097
$stream = $this->prophesize('Psr\Http\Message\StreamInterface');
@@ -68,7 +105,7 @@ public function testDoesNotInjectContentLengthHeaderIfStreamSizeIsUnknown()
68105
$this->emitter->emit($response);
69106
ob_end_clean();
70107
foreach (HeaderStack::stack() as $header) {
71-
$this->assertNotContains('Content-Length:', $header);
108+
$this->assertNotContains('Content-Length:', $header['header']);
72109
}
73110
}
74111
}

test/Response/SapiStreamEmitterTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ public function testDoesNotInjectContentLengthHeaderIfStreamSizeIsUnknown()
5656
$this->emitter->emit($response);
5757
ob_end_clean();
5858
foreach (HeaderStack::stack() as $header) {
59-
$this->assertNotContains('Content-Length:', $header);
59+
$this->assertNotContains('Content-Length:', $header['header']);
6060
}
6161
}
6262

test/ServerTest.php

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -151,8 +151,8 @@ public function testListenInvokesCallbackAndSendsResponse()
151151
$this->expectOutputString('FOOBAR');
152152
$server->listen();
153153

154-
$this->assertContains('HTTP/1.1 200 OK', HeaderStack::stack());
155-
$this->assertContains('Content-Type: text/plain', HeaderStack::stack());
154+
$this->assertTrue(HeaderStack::has('HTTP/1.1 200 OK'));
155+
$this->assertTrue(HeaderStack::has('Content-Type: text/plain'));
156156
}
157157

158158
public function testListenEmitsStatusHeaderWithoutReasonPhraseIfNoReasonPhrase()
@@ -176,8 +176,8 @@ public function testListenEmitsStatusHeaderWithoutReasonPhraseIfNoReasonPhrase()
176176
$this->expectOutputString('FOOBAR');
177177
$server->listen();
178178

179-
$this->assertContains('HTTP/1.1 299', HeaderStack::stack());
180-
$this->assertContains('Content-Type: text/plain', HeaderStack::stack());
179+
$this->assertTrue(HeaderStack::has('HTTP/1.1 299'));
180+
$this->assertTrue(HeaderStack::has('Content-Type: text/plain'));
181181
}
182182

183183
public function testEnsurePercentCharactersDoNotResultInOutputError()
@@ -200,8 +200,8 @@ public function testEnsurePercentCharactersDoNotResultInOutputError()
200200
$this->expectOutputString('100%');
201201
$server->listen();
202202

203-
$this->assertContains('HTTP/1.1 200 OK', HeaderStack::stack());
204-
$this->assertContains('Content-Type: text/plain', HeaderStack::stack());
203+
$this->assertTrue(HeaderStack::has('HTTP/1.1 200 OK'));
204+
$this->assertTrue(HeaderStack::has('Content-Type: text/plain'));
205205
}
206206

207207
public function testEmitsHeadersWithMultipleValuesMultipleTimes()
@@ -228,19 +228,16 @@ public function testEmitsHeadersWithMultipleValuesMultipleTimes()
228228

229229
$server->listen();
230230

231-
$this->assertContains('HTTP/1.1 200 OK', HeaderStack::stack());
232-
$this->assertContains('Content-Type: text/plain', HeaderStack::stack());
233-
$this->assertContains(
234-
'Set-Cookie: foo=bar; expires=Wed, 1 Oct 2014 10:30; path=/foo; domain=example.com',
235-
HeaderStack::stack()
231+
$this->assertTrue(HeaderStack::has('HTTP/1.1 200 OK'));
232+
$this->assertTrue(HeaderStack::has('Content-Type: text/plain'));
233+
$this->assertTrue(
234+
HeaderStack::has('Set-Cookie: foo=bar; expires=Wed, 1 Oct 2014 10:30; path=/foo; domain=example.com')
236235
);
237-
$this->assertContains(
238-
'Set-Cookie: bar=baz; expires=Wed, 8 Oct 2014 10:30; path=/foo/bar; domain=example.com',
239-
HeaderStack::stack()
236+
$this->assertTrue(
237+
HeaderStack::has('Set-Cookie: bar=baz; expires=Wed, 8 Oct 2014 10:30; path=/foo/bar; domain=example.com')
240238
);
241239

242-
$stack = HeaderStack::stack();
243-
return $stack;
240+
return HeaderStack::stack();
244241
}
245242

246243
/**
@@ -249,16 +246,23 @@ public function testEmitsHeadersWithMultipleValuesMultipleTimes()
249246
*/
250247
public function testHeaderOrderIsHonoredWhenEmitted($stack)
251248
{
249+
$header = array_pop($stack);
250+
$this->assertContains('Content-Type: text/plain', $header);
251+
252252
$header = array_pop($stack);
253253
$this->assertContains(
254254
'Set-Cookie: bar=baz; expires=Wed, 8 Oct 2014 10:30; path=/foo/bar; domain=example.com',
255255
$header
256256
);
257+
257258
$header = array_pop($stack);
258259
$this->assertContains(
259260
'Set-Cookie: foo=bar; expires=Wed, 1 Oct 2014 10:30; path=/foo; domain=example.com',
260261
$header
261262
);
263+
264+
$header = array_pop($stack);
265+
$this->assertContains('HTTP/1.1 200 OK', $header);
262266
}
263267

264268
public function testListenPassesCallableArgumentToCallback()

test/TestAsset/Functions.php

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
class HeaderStack
2828
{
2929
/**
30-
* @var array
30+
* @var string[][]
3131
*/
3232
private static $data = [];
3333

@@ -42,22 +42,40 @@ public static function reset()
4242
/**
4343
* Push a header on the stack
4444
*
45-
* @param string $header
45+
* @param string[] $header
4646
*/
47-
public static function push($header)
47+
public static function push(array $header)
4848
{
4949
self::$data[] = $header;
5050
}
5151

5252
/**
5353
* Return the current header stack
5454
*
55-
* @return array
55+
* @return string[][]
5656
*/
5757
public static function stack()
5858
{
5959
return self::$data;
6060
}
61+
62+
/**
63+
* Verify if there's a header line on the stack
64+
*
65+
* @param string $header
66+
*
67+
* @return bool
68+
*/
69+
public static function has($header)
70+
{
71+
foreach (self::$data as $item) {
72+
if ($item['header'] === $header) {
73+
return true;
74+
}
75+
}
76+
77+
return false;
78+
}
6179
}
6280

6381
/**
@@ -73,9 +91,17 @@ function headers_sent()
7391
/**
7492
* Emit a header, without creating actual output artifacts
7593
*
76-
* @param string $value
94+
* @param string $string
95+
* @param bool $replace
96+
* @param int|null $statusCode
7797
*/
78-
function header($value)
98+
function header($string, $replace = true, $statusCode = null)
7999
{
80-
HeaderStack::push($value);
100+
HeaderStack::push(
101+
[
102+
'header' => $string,
103+
'replace' => $replace,
104+
'status_code' => $statusCode,
105+
]
106+
);
81107
}

test/TestAsset/SapiResponse.php

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,17 @@ function headers_sent()
3535
/**
3636
* Emit a header, without creating actual output artifacts
3737
*
38-
* @param string $value
38+
* @param string $string
39+
* @param bool $replace
40+
* @param int|null $http_response_code
3941
*/
40-
function header($value)
42+
function header($string, $replace = true, $http_response_code = null)
4143
{
42-
HeaderStack::push($value);
44+
HeaderStack::push(
45+
[
46+
'header' => $string,
47+
'replace' => $replace,
48+
'status_code' => $http_response_code,
49+
]
50+
);
4351
}

0 commit comments

Comments
 (0)