Skip to content

Commit 4660c6d

Browse files
committed
[HttpFoundation] Create cookie from string + synchronize response cookies
1 parent 46aff51 commit 4660c6d

File tree

6 files changed

+170
-41
lines changed

6 files changed

+170
-41
lines changed

Cookie.php

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,63 @@ class Cookie
3131
const SAMESITE_LAX = 'lax';
3232
const SAMESITE_STRICT = 'strict';
3333

34+
/**
35+
* Creates cookie from raw header string.
36+
*
37+
* @param string $cookie
38+
* @param bool $decode
39+
*
40+
* @return static
41+
*/
42+
public static function fromString($cookie, $decode = false)
43+
{
44+
$data = array(
45+
'expires' => 0,
46+
'path' => '/',
47+
'domain' => null,
48+
'secure' => false,
49+
'httponly' => true,
50+
'raw' => !$decode,
51+
'samesite' => null,
52+
);
53+
foreach (explode(';', $cookie) as $part) {
54+
if (false === strpos($part, '=')) {
55+
$key = trim($part);
56+
$value = true;
57+
} else {
58+
list($key, $value) = explode('=', trim($part), 2);
59+
$key = trim($key);
60+
$value = trim($value);
61+
}
62+
if (!isset($data['name'])) {
63+
$data['name'] = $decode ? urldecode($key) : $key;
64+
$data['value'] = true === $value ? null : ($decode ? urldecode($value) : $value);
65+
continue;
66+
}
67+
switch ($key = strtolower($key)) {
68+
case 'name':
69+
case 'value':
70+
break;
71+
case 'max-age':
72+
$data['expires'] = time() + (int) $value;
73+
break;
74+
default:
75+
$data[$key] = $value;
76+
break;
77+
}
78+
}
79+
80+
return new static($data['name'], $data['value'], $data['expires'], $data['path'], $data['domain'], $data['secure'], $data['httponly'], $data['raw'], $data['samesite']);
81+
}
82+
3483
/**
3584
* Constructor.
3685
*
3786
* @param string $name The name of the cookie
38-
* @param string $value The value of the cookie
87+
* @param string|null $value The value of the cookie
3988
* @param int|string|\DateTimeInterface $expire The time the cookie expires
4089
* @param string $path The path on the server in which the cookie will be available on
41-
* @param string $domain The domain that the cookie is available to
90+
* @param string|null $domain The domain that the cookie is available to
4291
* @param bool $secure Whether the cookie should only be transmitted over a secure HTTPS connection from the client
4392
* @param bool $httpOnly Whether the cookie will be made accessible only through the HTTP protocol
4493
* @param bool $raw Whether the cookie value should be sent with no url encoding

HeaderBag.php

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,14 @@ public function __construct(array $headers = array())
4040
*/
4141
public function __toString()
4242
{
43-
if (!$this->headers) {
43+
if (!$headers = $this->all()) {
4444
return '';
4545
}
4646

47-
$max = max(array_map('strlen', array_keys($this->headers))) + 1;
47+
ksort($headers);
48+
$max = max(array_map('strlen', array_keys($headers))) + 1;
4849
$content = '';
49-
ksort($this->headers);
50-
foreach ($this->headers as $name => $values) {
50+
foreach ($headers as $name => $values) {
5151
$name = implode('-', array_map('ucfirst', explode('-', $name)));
5252
foreach ($values as $value) {
5353
$content .= sprintf("%-{$max}s %s\r\n", $name.':', $value);
@@ -74,7 +74,7 @@ public function all()
7474
*/
7575
public function keys()
7676
{
77-
return array_keys($this->headers);
77+
return array_keys($this->all());
7878
}
7979

8080
/**
@@ -112,8 +112,9 @@ public function add(array $headers)
112112
public function get($key, $default = null, $first = true)
113113
{
114114
$key = str_replace('_', '-', strtolower($key));
115+
$headers = $this->all();
115116

116-
if (!array_key_exists($key, $this->headers)) {
117+
if (!array_key_exists($key, $headers)) {
117118
if (null === $default) {
118119
return $first ? null : array();
119120
}
@@ -122,10 +123,10 @@ public function get($key, $default = null, $first = true)
122123
}
123124

124125
if ($first) {
125-
return count($this->headers[$key]) ? $this->headers[$key][0] : $default;
126+
return count($headers[$key]) ? $headers[$key][0] : $default;
126127
}
127128

128-
return $this->headers[$key];
129+
return $headers[$key];
129130
}
130131

131132
/**
@@ -161,7 +162,7 @@ public function set($key, $values, $replace = true)
161162
*/
162163
public function has($key)
163164
{
164-
return array_key_exists(str_replace('_', '-', strtolower($key)), $this->headers);
165+
return array_key_exists(str_replace('_', '-', strtolower($key)), $this->all());
165166
}
166167

167168
/**

Response.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -375,7 +375,7 @@ public function sendHeaders()
375375
}
376376

377377
// headers
378-
foreach ($this->headers->allPreserveCase() as $name => $values) {
378+
foreach ($this->headers->allPreserveCaseWithoutCookies() as $name => $values) {
379379
foreach ($values as $value) {
380380
header($name.': '.$value, false, $this->statusCode);
381381
}

ResponseHeaderBag.php

Lines changed: 56 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -54,28 +54,28 @@ public function __construct(array $headers = array())
5454
}
5555

5656
/**
57-
* {@inheritdoc}
57+
* Returns the headers, with original capitalizations.
58+
*
59+
* @return array An array of headers
5860
*/
59-
public function __toString()
61+
public function allPreserveCase()
6062
{
61-
$cookies = '';
62-
foreach ($this->getCookies() as $cookie) {
63-
$cookies .= 'Set-Cookie: '.$cookie."\r\n";
63+
$headers = array();
64+
foreach ($this->all() as $name => $value) {
65+
$headers[isset($this->headerNames[$name]) ? $this->headerNames[$name] : $name] = $value;
6466
}
6567

66-
ksort($this->headerNames);
67-
68-
return parent::__toString().$cookies;
68+
return $headers;
6969
}
7070

71-
/**
72-
* Returns the headers, with original capitalizations.
73-
*
74-
* @return array An array of headers
75-
*/
76-
public function allPreserveCase()
71+
public function allPreserveCaseWithoutCookies()
7772
{
78-
return array_combine($this->headerNames, $this->headers);
73+
$headers = $this->allPreserveCase();
74+
if (isset($this->headerNames['set-cookie'])) {
75+
unset($headers[$this->headerNames['set-cookie']]);
76+
}
77+
78+
return $headers;
7979
}
8080

8181
/**
@@ -95,13 +95,39 @@ public function replace(array $headers = array())
9595
/**
9696
* {@inheritdoc}
9797
*/
98-
public function set($key, $values, $replace = true)
98+
public function all()
9999
{
100-
parent::set($key, $values, $replace);
100+
$headers = parent::all();
101+
foreach ($this->getCookies() as $cookie) {
102+
$headers['set-cookie'][] = (string) $cookie;
103+
}
101104

105+
return $headers;
106+
}
107+
108+
/**
109+
* {@inheritdoc}
110+
*/
111+
public function set($key, $values, $replace = true)
112+
{
102113
$uniqueKey = str_replace('_', '-', strtolower($key));
114+
115+
if ('set-cookie' === $uniqueKey) {
116+
if ($replace) {
117+
$this->cookies = array();
118+
}
119+
foreach ((array) $values as $cookie) {
120+
$this->setCookie(Cookie::fromString($cookie));
121+
}
122+
$this->headerNames[$uniqueKey] = $key;
123+
124+
return;
125+
}
126+
103127
$this->headerNames[$uniqueKey] = $key;
104128

129+
parent::set($key, $values, $replace);
130+
105131
// ensure the cache-control header has sensible defaults
106132
if (in_array($uniqueKey, array('cache-control', 'etag', 'last-modified', 'expires'))) {
107133
$computed = $this->computeCacheControlValue();
@@ -116,11 +142,17 @@ public function set($key, $values, $replace = true)
116142
*/
117143
public function remove($key)
118144
{
119-
parent::remove($key);
120-
121145
$uniqueKey = str_replace('_', '-', strtolower($key));
122146
unset($this->headerNames[$uniqueKey]);
123147

148+
if ('set-cookie' === $uniqueKey) {
149+
$this->cookies = array();
150+
151+
return;
152+
}
153+
154+
parent::remove($key);
155+
124156
if ('cache-control' === $uniqueKey) {
125157
$this->computedCacheControl = array();
126158
}
@@ -150,6 +182,7 @@ public function getCacheControlDirective($key)
150182
public function setCookie(Cookie $cookie)
151183
{
152184
$this->cookies[$cookie->getDomain()][$cookie->getPath()][$cookie->getName()] = $cookie;
185+
$this->headerNames['set-cookie'] = 'Set-Cookie';
153186
}
154187

155188
/**
@@ -174,6 +207,10 @@ public function removeCookie($name, $path = '/', $domain = null)
174207
unset($this->cookies[$domain]);
175208
}
176209
}
210+
211+
if (empty($this->cookies)) {
212+
unset($this->headerNames['set-cookie']);
213+
}
177214
}
178215

179216
/**

Tests/CookieTest.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,4 +178,13 @@ public function testGetMaxAge()
178178
$cookie = new Cookie('foo', 'bar', $expire = time() - 100);
179179
$this->assertEquals($expire - time(), $cookie->getMaxAge());
180180
}
181+
182+
public function testFromString()
183+
{
184+
$cookie = Cookie::fromString('foo=bar; expires=Fri, 20-May-2011 15:25:52 GMT; path=/; domain=.myfoodomain.com; secure; httponly');
185+
$this->assertEquals(new Cookie('foo', 'bar', strtotime('Fri, 20-May-2011 15:25:52 GMT'), '/', '.myfoodomain.com', true, true, true), $cookie);
186+
187+
$cookie = Cookie::fromString('foo=bar', true);
188+
$this->assertEquals(new Cookie('foo', 'bar'), $cookie);
189+
}
181190
}

Tests/ResponseHeaderBagTest.php

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -124,11 +124,11 @@ public function testToStringIncludesCookieHeaders()
124124
$bag = new ResponseHeaderBag(array());
125125
$bag->setCookie(new Cookie('foo', 'bar'));
126126

127-
$this->assertContains('Set-Cookie: foo=bar; path=/; httponly', explode("\r\n", $bag->__toString()));
127+
$this->assertSetCookieHeader('foo=bar; path=/; httponly', $bag);
128128

129129
$bag->clearCookie('foo');
130130

131-
$this->assertRegExp('#^Set-Cookie: foo=deleted; expires='.gmdate('D, d-M-Y H:i:s T', time() - 31536001).'; max-age=-31536001; path=/; httponly#m', $bag->__toString());
131+
$this->assertSetCookieHeader('foo=deleted; expires='.gmdate('D, d-M-Y H:i:s T', time() - 31536001).'; max-age=-31536001; path=/; httponly', $bag);
132132
}
133133

134134
public function testClearCookieSecureNotHttpOnly()
@@ -137,7 +137,7 @@ public function testClearCookieSecureNotHttpOnly()
137137

138138
$bag->clearCookie('foo', '/', null, true, false);
139139

140-
$this->assertRegExp('#^Set-Cookie: foo=deleted; expires='.gmdate('D, d-M-Y H:i:s T', time() - 31536001).'; max-age=-31536001; path=/; secure#m', $bag->__toString());
140+
$this->assertSetCookieHeader('foo=deleted; expires='.gmdate('D, d-M-Y H:i:s T', time() - 31536001).'; max-age=-31536001; path=/; secure', $bag);
141141
}
142142

143143
public function testReplace()
@@ -172,14 +172,21 @@ public function testCookiesWithSameNames()
172172
$bag->setCookie(new Cookie('foo', 'bar'));
173173

174174
$this->assertCount(4, $bag->getCookies());
175-
176-
$headers = explode("\r\n", $bag->__toString());
177-
$this->assertContains('Set-Cookie: foo=bar; path=/path/foo; domain=foo.bar; httponly', $headers);
178-
$this->assertContains('Set-Cookie: foo=bar; path=/path/foo; domain=foo.bar; httponly', $headers);
179-
$this->assertContains('Set-Cookie: foo=bar; path=/path/bar; domain=bar.foo; httponly', $headers);
180-
$this->assertContains('Set-Cookie: foo=bar; path=/; httponly', $headers);
175+
$this->assertEquals('foo=bar; path=/path/foo; domain=foo.bar; httponly', $bag->get('set-cookie'));
176+
$this->assertEquals(array(
177+
'foo=bar; path=/path/foo; domain=foo.bar; httponly',
178+
'foo=bar; path=/path/bar; domain=foo.bar; httponly',
179+
'foo=bar; path=/path/bar; domain=bar.foo; httponly',
180+
'foo=bar; path=/; httponly',
181+
), $bag->get('set-cookie', null, false));
182+
183+
$this->assertSetCookieHeader('foo=bar; path=/path/foo; domain=foo.bar; httponly', $bag);
184+
$this->assertSetCookieHeader('foo=bar; path=/path/bar; domain=foo.bar; httponly', $bag);
185+
$this->assertSetCookieHeader('foo=bar; path=/path/bar; domain=bar.foo; httponly', $bag);
186+
$this->assertSetCookieHeader('foo=bar; path=/; httponly', $bag);
181187

182188
$cookies = $bag->getCookies(ResponseHeaderBag::COOKIES_ARRAY);
189+
183190
$this->assertTrue(isset($cookies['foo.bar']['/path/foo']['foo']));
184191
$this->assertTrue(isset($cookies['foo.bar']['/path/bar']['foo']));
185192
$this->assertTrue(isset($cookies['bar.foo']['/path/bar']['foo']));
@@ -189,18 +196,23 @@ public function testCookiesWithSameNames()
189196
public function testRemoveCookie()
190197
{
191198
$bag = new ResponseHeaderBag();
199+
$this->assertFalse($bag->has('set-cookie'));
200+
192201
$bag->setCookie(new Cookie('foo', 'bar', 0, '/path/foo', 'foo.bar'));
193202
$bag->setCookie(new Cookie('bar', 'foo', 0, '/path/bar', 'foo.bar'));
203+
$this->assertTrue($bag->has('set-cookie'));
194204

195205
$cookies = $bag->getCookies(ResponseHeaderBag::COOKIES_ARRAY);
196206
$this->assertTrue(isset($cookies['foo.bar']['/path/foo']));
197207

198208
$bag->removeCookie('foo', '/path/foo', 'foo.bar');
209+
$this->assertTrue($bag->has('set-cookie'));
199210

200211
$cookies = $bag->getCookies(ResponseHeaderBag::COOKIES_ARRAY);
201212
$this->assertFalse(isset($cookies['foo.bar']['/path/foo']));
202213

203214
$bag->removeCookie('bar', '/path/bar', 'foo.bar');
215+
$this->assertFalse($bag->has('set-cookie'));
204216

205217
$cookies = $bag->getCookies(ResponseHeaderBag::COOKIES_ARRAY);
206218
$this->assertFalse(isset($cookies['foo.bar']));
@@ -224,14 +236,30 @@ public function testRemoveCookieWithNullRemove()
224236
$this->assertFalse(isset($cookies['']['/']['bar']));
225237
}
226238

239+
public function testSetCookieHeader()
240+
{
241+
$bag = new ResponseHeaderBag();
242+
$bag->set('set-cookie', 'foo=bar');
243+
$this->assertEquals(array(new Cookie('foo', 'bar', 0, '/', null, false, true, true)), $bag->getCookies());
244+
245+
$bag->set('set-cookie', 'foo2=bar2', false);
246+
$this->assertEquals(array(
247+
new Cookie('foo', 'bar', 0, '/', null, false, true, true),
248+
new Cookie('foo2', 'bar2', 0, '/', null, false, true, true),
249+
), $bag->getCookies());
250+
251+
$bag->remove('set-cookie');
252+
$this->assertEquals(array(), $bag->getCookies());
253+
}
254+
227255
/**
228256
* @expectedException \InvalidArgumentException
229257
*/
230258
public function testGetCookiesWithInvalidArgument()
231259
{
232260
$bag = new ResponseHeaderBag();
233261

234-
$cookies = $bag->getCookies('invalid_argument');
262+
$bag->getCookies('invalid_argument');
235263
}
236264

237265
/**
@@ -302,4 +330,9 @@ public function provideMakeDispositionFail()
302330
array('attachment', 'föö.html'),
303331
);
304332
}
333+
334+
protected function assertSetCookieHeader($expected, ResponseHeaderBag $actual)
335+
{
336+
$this->assertRegExp('#^Set-Cookie:\s+'.preg_quote($expected, '#').'$#m', str_replace("\r\n", "\n", (string) $actual));
337+
}
305338
}

0 commit comments

Comments
 (0)