1<?php
2
3/*
4 * This file is part of the Symfony package.
5 *
6 * (c) Fabien Potencier <fabien@symfony.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Symfony\Component\HttpFoundation\Tests;
13
14use PHPUnit\Framework\TestCase;
15use Symfony\Component\HttpFoundation\Cookie;
16use Symfony\Component\HttpFoundation\ResponseHeaderBag;
17
18/**
19 * @group time-sensitive
20 */
21class ResponseHeaderBagTest extends TestCase
22{
23    public function testAllPreserveCase()
24    {
25        $headers = [
26            'fOo' => 'BAR',
27            'ETag' => 'xyzzy',
28            'Content-MD5' => 'Q2hlY2sgSW50ZWdyaXR5IQ==',
29            'P3P' => 'CP="CAO PSA OUR"',
30            'WWW-Authenticate' => 'Basic realm="WallyWorld"',
31            'X-UA-Compatible' => 'IE=edge,chrome=1',
32            'X-XSS-Protection' => '1; mode=block',
33        ];
34
35        $bag = new ResponseHeaderBag($headers);
36        $allPreservedCase = $bag->allPreserveCase();
37
38        foreach (array_keys($headers) as $headerName) {
39            $this->assertArrayHasKey($headerName, $allPreservedCase, '->allPreserveCase() gets all input keys in original case');
40        }
41    }
42
43    public function testCacheControlHeader()
44    {
45        $bag = new ResponseHeaderBag([]);
46        $this->assertEquals('no-cache, private', $bag->get('Cache-Control'));
47        $this->assertTrue($bag->hasCacheControlDirective('no-cache'));
48
49        $bag = new ResponseHeaderBag(['Cache-Control' => 'public']);
50        $this->assertEquals('public', $bag->get('Cache-Control'));
51        $this->assertTrue($bag->hasCacheControlDirective('public'));
52
53        $bag = new ResponseHeaderBag(['ETag' => 'abcde']);
54        $this->assertEquals('no-cache, private', $bag->get('Cache-Control'));
55        $this->assertTrue($bag->hasCacheControlDirective('private'));
56        $this->assertTrue($bag->hasCacheControlDirective('no-cache'));
57        $this->assertFalse($bag->hasCacheControlDirective('max-age'));
58
59        $bag = new ResponseHeaderBag(['Expires' => 'Wed, 16 Feb 2011 14:17:43 GMT']);
60        $this->assertEquals('private, must-revalidate', $bag->get('Cache-Control'));
61
62        $bag = new ResponseHeaderBag([
63            'Expires' => 'Wed, 16 Feb 2011 14:17:43 GMT',
64            'Cache-Control' => 'max-age=3600',
65        ]);
66        $this->assertEquals('max-age=3600, private', $bag->get('Cache-Control'));
67
68        $bag = new ResponseHeaderBag(['Last-Modified' => 'abcde']);
69        $this->assertEquals('private, must-revalidate', $bag->get('Cache-Control'));
70
71        $bag = new ResponseHeaderBag(['Etag' => 'abcde', 'Last-Modified' => 'abcde']);
72        $this->assertEquals('private, must-revalidate', $bag->get('Cache-Control'));
73
74        $bag = new ResponseHeaderBag(['cache-control' => 'max-age=100']);
75        $this->assertEquals('max-age=100, private', $bag->get('Cache-Control'));
76
77        $bag = new ResponseHeaderBag(['cache-control' => 's-maxage=100']);
78        $this->assertEquals('s-maxage=100', $bag->get('Cache-Control'));
79
80        $bag = new ResponseHeaderBag(['cache-control' => 'private, max-age=100']);
81        $this->assertEquals('max-age=100, private', $bag->get('Cache-Control'));
82
83        $bag = new ResponseHeaderBag(['cache-control' => 'public, max-age=100']);
84        $this->assertEquals('max-age=100, public', $bag->get('Cache-Control'));
85
86        $bag = new ResponseHeaderBag();
87        $bag->set('Last-Modified', 'abcde');
88        $this->assertEquals('private, must-revalidate', $bag->get('Cache-Control'));
89
90        $bag = new ResponseHeaderBag();
91        $bag->set('Cache-Control', ['public', 'must-revalidate']);
92        $this->assertCount(1, $bag->all('Cache-Control'));
93        $this->assertEquals('must-revalidate, public', $bag->get('Cache-Control'));
94
95        $bag = new ResponseHeaderBag();
96        $bag->set('Cache-Control', 'public');
97        $bag->set('Cache-Control', 'must-revalidate', false);
98        $this->assertCount(1, $bag->all('Cache-Control'));
99        $this->assertEquals('must-revalidate, public', $bag->get('Cache-Control'));
100    }
101
102    public function testCacheControlClone()
103    {
104        $headers = ['foo' => 'bar'];
105        $bag1 = new ResponseHeaderBag($headers);
106        $bag2 = new ResponseHeaderBag($bag1->allPreserveCase());
107        $this->assertEquals($bag1->allPreserveCase(), $bag2->allPreserveCase());
108    }
109
110    public function testToStringIncludesCookieHeaders()
111    {
112        $bag = new ResponseHeaderBag([]);
113        $bag->setCookie(Cookie::create('foo', 'bar'));
114
115        $this->assertSetCookieHeader('foo=bar; path=/; httponly; samesite=lax', $bag);
116
117        $bag->clearCookie('foo');
118
119        $this->assertSetCookieHeader('foo=deleted; expires='.gmdate('D, d-M-Y H:i:s T', time() - 31536001).'; Max-Age=0; path=/; httponly', $bag);
120    }
121
122    public function testClearCookieSecureNotHttpOnly()
123    {
124        $bag = new ResponseHeaderBag([]);
125
126        $bag->clearCookie('foo', '/', null, true, false);
127
128        $this->assertSetCookieHeader('foo=deleted; expires='.gmdate('D, d-M-Y H:i:s T', time() - 31536001).'; Max-Age=0; path=/; secure', $bag);
129    }
130
131    public function testClearCookieSamesite()
132    {
133        $bag = new ResponseHeaderBag([]);
134
135        $bag->clearCookie('foo', '/', null, true, false, 'none');
136        $this->assertSetCookieHeader('foo=deleted; expires='.gmdate('D, d-M-Y H:i:s T', time() - 31536001).'; Max-Age=0; path=/; secure; samesite=none', $bag);
137    }
138
139    public function testReplace()
140    {
141        $bag = new ResponseHeaderBag([]);
142        $this->assertEquals('no-cache, private', $bag->get('Cache-Control'));
143        $this->assertTrue($bag->hasCacheControlDirective('no-cache'));
144
145        $bag->replace(['Cache-Control' => 'public']);
146        $this->assertEquals('public', $bag->get('Cache-Control'));
147        $this->assertTrue($bag->hasCacheControlDirective('public'));
148    }
149
150    public function testReplaceWithRemove()
151    {
152        $bag = new ResponseHeaderBag([]);
153        $this->assertEquals('no-cache, private', $bag->get('Cache-Control'));
154        $this->assertTrue($bag->hasCacheControlDirective('no-cache'));
155
156        $bag->remove('Cache-Control');
157        $bag->replace([]);
158        $this->assertEquals('no-cache, private', $bag->get('Cache-Control'));
159        $this->assertTrue($bag->hasCacheControlDirective('no-cache'));
160    }
161
162    public function testCookiesWithSameNames()
163    {
164        $bag = new ResponseHeaderBag();
165        $bag->setCookie(Cookie::create('foo', 'bar', 0, '/path/foo', 'foo.bar'));
166        $bag->setCookie(Cookie::create('foo', 'bar', 0, '/path/bar', 'foo.bar'));
167        $bag->setCookie(Cookie::create('foo', 'bar', 0, '/path/bar', 'bar.foo'));
168        $bag->setCookie(Cookie::create('foo', 'bar'));
169
170        $this->assertCount(4, $bag->getCookies());
171        $this->assertEquals('foo=bar; path=/path/foo; domain=foo.bar; httponly; samesite=lax', $bag->get('set-cookie'));
172        $this->assertEquals([
173            'foo=bar; path=/path/foo; domain=foo.bar; httponly; samesite=lax',
174            'foo=bar; path=/path/bar; domain=foo.bar; httponly; samesite=lax',
175            'foo=bar; path=/path/bar; domain=bar.foo; httponly; samesite=lax',
176            'foo=bar; path=/; httponly; samesite=lax',
177        ], $bag->all('set-cookie'));
178
179        $this->assertSetCookieHeader('foo=bar; path=/path/foo; domain=foo.bar; httponly; samesite=lax', $bag);
180        $this->assertSetCookieHeader('foo=bar; path=/path/bar; domain=foo.bar; httponly; samesite=lax', $bag);
181        $this->assertSetCookieHeader('foo=bar; path=/path/bar; domain=bar.foo; httponly; samesite=lax', $bag);
182        $this->assertSetCookieHeader('foo=bar; path=/; httponly; samesite=lax', $bag);
183
184        $cookies = $bag->getCookies(ResponseHeaderBag::COOKIES_ARRAY);
185
186        $this->assertArrayHasKey('foo', $cookies['foo.bar']['/path/foo']);
187        $this->assertArrayHasKey('foo', $cookies['foo.bar']['/path/bar']);
188        $this->assertArrayHasKey('foo', $cookies['bar.foo']['/path/bar']);
189        $this->assertArrayHasKey('foo', $cookies['']['/']);
190    }
191
192    public function testRemoveCookie()
193    {
194        $bag = new ResponseHeaderBag();
195        $this->assertFalse($bag->has('set-cookie'));
196
197        $bag->setCookie(Cookie::create('foo', 'bar', 0, '/path/foo', 'foo.bar'));
198        $bag->setCookie(Cookie::create('bar', 'foo', 0, '/path/bar', 'foo.bar'));
199        $this->assertTrue($bag->has('set-cookie'));
200
201        $cookies = $bag->getCookies(ResponseHeaderBag::COOKIES_ARRAY);
202        $this->assertArrayHasKey('/path/foo', $cookies['foo.bar']);
203
204        $bag->removeCookie('foo', '/path/foo', 'foo.bar');
205        $this->assertTrue($bag->has('set-cookie'));
206
207        $cookies = $bag->getCookies(ResponseHeaderBag::COOKIES_ARRAY);
208        $this->assertArrayNotHasKey('/path/foo', $cookies['foo.bar']);
209
210        $bag->removeCookie('bar', '/path/bar', 'foo.bar');
211        $this->assertFalse($bag->has('set-cookie'));
212
213        $cookies = $bag->getCookies(ResponseHeaderBag::COOKIES_ARRAY);
214        $this->assertArrayNotHasKey('foo.bar', $cookies);
215    }
216
217    public function testRemoveCookieWithNullRemove()
218    {
219        $bag = new ResponseHeaderBag();
220        $bag->setCookie(Cookie::create('foo', 'bar'));
221        $bag->setCookie(Cookie::create('bar', 'foo'));
222
223        $cookies = $bag->getCookies(ResponseHeaderBag::COOKIES_ARRAY);
224        $this->assertArrayHasKey('/', $cookies['']);
225
226        $bag->removeCookie('foo', null);
227        $cookies = $bag->getCookies(ResponseHeaderBag::COOKIES_ARRAY);
228        $this->assertArrayNotHasKey('foo', $cookies['']['/']);
229
230        $bag->removeCookie('bar', null);
231        $cookies = $bag->getCookies(ResponseHeaderBag::COOKIES_ARRAY);
232        $this->assertFalse(isset($cookies['']['/']['bar']));
233    }
234
235    public function testSetCookieHeader()
236    {
237        $bag = new ResponseHeaderBag();
238        $bag->set('set-cookie', 'foo=bar');
239        $this->assertEquals([Cookie::create('foo', 'bar', 0, '/', null, false, false, true, null)], $bag->getCookies());
240
241        $bag->set('set-cookie', 'foo2=bar2', false);
242        $this->assertEquals([
243            Cookie::create('foo', 'bar', 0, '/', null, false, false, true, null),
244            Cookie::create('foo2', 'bar2', 0, '/', null, false, false, true, null),
245        ], $bag->getCookies());
246
247        $bag->remove('set-cookie');
248        $this->assertEquals([], $bag->getCookies());
249    }
250
251    public function testGetCookiesWithInvalidArgument()
252    {
253        $this->expectException('InvalidArgumentException');
254        $bag = new ResponseHeaderBag();
255
256        $bag->getCookies('invalid_argument');
257    }
258
259    public function testToStringDoesntMessUpHeaders()
260    {
261        $headers = new ResponseHeaderBag();
262
263        $headers->set('Location', 'http://www.symfony.com');
264        $headers->set('Content-type', 'text/html');
265
266        (string) $headers;
267
268        $allHeaders = $headers->allPreserveCase();
269        $this->assertEquals(['http://www.symfony.com'], $allHeaders['Location']);
270        $this->assertEquals(['text/html'], $allHeaders['Content-type']);
271    }
272
273    public function testDateHeaderAddedOnCreation()
274    {
275        $now = time();
276
277        $bag = new ResponseHeaderBag();
278        $this->assertTrue($bag->has('Date'));
279
280        $this->assertEquals($now, $bag->getDate('Date')->getTimestamp());
281    }
282
283    public function testDateHeaderCanBeSetOnCreation()
284    {
285        $someDate = 'Thu, 23 Mar 2017 09:15:12 GMT';
286        $bag = new ResponseHeaderBag(['Date' => $someDate]);
287
288        $this->assertEquals($someDate, $bag->get('Date'));
289    }
290
291    public function testDateHeaderWillBeRecreatedWhenRemoved()
292    {
293        $someDate = 'Thu, 23 Mar 2017 09:15:12 GMT';
294        $bag = new ResponseHeaderBag(['Date' => $someDate]);
295        $bag->remove('Date');
296
297        // a (new) Date header is still present
298        $this->assertTrue($bag->has('Date'));
299        $this->assertNotEquals($someDate, $bag->get('Date'));
300    }
301
302    public function testDateHeaderWillBeRecreatedWhenHeadersAreReplaced()
303    {
304        $bag = new ResponseHeaderBag();
305        $bag->replace([]);
306
307        $this->assertTrue($bag->has('Date'));
308    }
309
310    private function assertSetCookieHeader(string $expected, ResponseHeaderBag $actual)
311    {
312        $this->assertRegExp('#^Set-Cookie:\s+'.preg_quote($expected, '#').'$#m', str_replace("\r\n", "\n", (string) $actual));
313    }
314}
315