Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

[HttpFoundation] Create cookie from string + synchronize response cookies #20569

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Dec 17, 2016
Merged
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
53 changes: 51 additions & 2 deletions 53 src/Symfony/Component/HttpFoundation/Cookie.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,63 @@ class Cookie
const SAMESITE_LAX = 'lax';
const SAMESITE_STRICT = 'strict';

/**
* Creates cookie from raw header string.
*
* @param string $cookie
* @param bool $decode
*
* @return static
*/
public static function fromString($cookie, $decode = false)
{
$data = array(
'expires' => 0,
'path' => '/',
'domain' => null,
'secure' => false,
'httponly' => true,
'raw' => !$decode,
'samesite' => null,
);
foreach (explode(';', $cookie) as $part) {
if (false === strpos($part, '=')) {
$key = trim($part);
$value = true;
} else {
list($key, $value) = explode('=', trim($part), 2);
$key = trim($key);
$value = trim($value);
}
if (!isset($data['name'])) {
$data['name'] = $decode ? urldecode($key) : $key;
$data['value'] = true === $value ? null : ($decode ? urldecode($value) : $value);
continue;
}
switch ($key = strtolower($key)) {
case 'name':
case 'value':
break;
case 'max-age':
$data['expires'] = time() + (int) $value;
break;
default:
$data[$key] = $value;
break;
}
}

return new static($data['name'], $data['value'], $data['expires'], $data['path'], $data['domain'], $data['secure'], $data['httponly'], $data['raw'], $data['samesite']);
}

/**
* Constructor.
*
* @param string $name The name of the cookie
* @param string $value The value of the cookie
* @param string|null $value The value of the cookie
* @param int|string|\DateTimeInterface $expire The time the cookie expires
* @param string $path The path on the server in which the cookie will be available on
* @param string $domain The domain that the cookie is available to
* @param string|null $domain The domain that the cookie is available to
* @param bool $secure Whether the cookie should only be transmitted over a secure HTTPS connection from the client
* @param bool $httpOnly Whether the cookie will be made accessible only through the HTTP protocol
* @param bool $raw Whether the cookie value should be sent with no url encoding
Expand Down
19 changes: 10 additions & 9 deletions 19 src/Symfony/Component/HttpFoundation/HeaderBag.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,14 @@ public function __construct(array $headers = array())
*/
public function __toString()
{
if (!$this->headers) {
if (!$headers = $this->all()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you remove headers usage, the property should be removed as well... but it is protected, so that's a BC break.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ResponsHeaderBag overrides all() by including the additional cookie headers. HeaderBag::all still returns $this->headers... so it's not removed.

However internally it now uses all() (the API method) instead of $headers (the property) so those cookie headers are taken into account when using for example HeaderBag::has.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the property is still used.

In the normal HeaderBag, ->all() is a simple getter for it. But it is now overwritten in ResponseHeaderBag to add the set-cookie header

return '';
}

$max = max(array_map('strlen', array_keys($this->headers))) + 1;
ksort($headers);
$max = max(array_map('strlen', array_keys($headers))) + 1;
$content = '';
ksort($this->headers);
foreach ($this->headers as $name => $values) {
foreach ($headers as $name => $values) {
$name = implode('-', array_map('ucfirst', explode('-', $name)));
foreach ($values as $value) {
$content .= sprintf("%-{$max}s %s\r\n", $name.':', $value);
Expand All @@ -74,7 +74,7 @@ public function all()
*/
public function keys()
{
return array_keys($this->headers);
return array_keys($this->all());
}

/**
Expand Down Expand Up @@ -112,8 +112,9 @@ public function add(array $headers)
public function get($key, $default = null, $first = true)
{
$key = str_replace('_', '-', strtolower($key));
$headers = $this->all();

if (!array_key_exists($key, $this->headers)) {
if (!array_key_exists($key, $headers)) {
if (null === $default) {
return $first ? null : array();
}
Expand All @@ -122,10 +123,10 @@ public function get($key, $default = null, $first = true)
}

if ($first) {
return count($this->headers[$key]) ? $this->headers[$key][0] : $default;
return count($headers[$key]) ? $headers[$key][0] : $default;
}

return $this->headers[$key];
return $headers[$key];
}

/**
Expand Down Expand Up @@ -161,7 +162,7 @@ public function set($key, $values, $replace = true)
*/
public function has($key)
{
return array_key_exists(str_replace('_', '-', strtolower($key)), $this->headers);
return array_key_exists(str_replace('_', '-', strtolower($key)), $this->all());
}

/**
Expand Down
2 changes: 1 addition & 1 deletion 2 src/Symfony/Component/HttpFoundation/Response.php
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,7 @@ public function sendHeaders()
}

// headers
foreach ($this->headers->allPreserveCase() as $name => $values) {
foreach ($this->headers->allPreserveCaseWithoutCookies() as $name => $values) {
foreach ($values as $value) {
header($name.': '.$value, false, $this->statusCode);
}
Expand Down
75 changes: 56 additions & 19 deletions 75 src/Symfony/Component/HttpFoundation/ResponseHeaderBag.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,28 +54,28 @@ public function __construct(array $headers = array())
}

/**
* {@inheritdoc}
* Returns the headers, with original capitalizations.
*
* @return array An array of headers
*/
public function __toString()
public function allPreserveCase()
{
$cookies = '';
foreach ($this->getCookies() as $cookie) {
$cookies .= 'Set-Cookie: '.$cookie."\r\n";
$headers = array();
foreach ($this->all() as $name => $value) {
$headers[isset($this->headerNames[$name]) ? $this->headerNames[$name] : $name] = $value;
}

ksort($this->headerNames);

return parent::__toString().$cookies;
return $headers;
}

/**
* Returns the headers, with original capitalizations.
*
* @return array An array of headers
*/
public function allPreserveCase()
public function allPreserveCaseWithoutCookies()
{
return array_combine($this->headerNames, $this->headers);
$headers = $this->allPreserveCase();
if (isset($this->headerNames['set-cookie'])) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what if a case is not provided for the set-cookie header ? you will not remove it at all

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's always provided.. ie. here and here

unset($headers[$this->headerNames['set-cookie']]);
}

return $headers;
}

/**
Expand All @@ -95,13 +95,39 @@ public function replace(array $headers = array())
/**
* {@inheritdoc}
*/
public function set($key, $values, $replace = true)
public function all()
{
parent::set($key, $values, $replace);
$headers = parent::all();
foreach ($this->getCookies() as $cookie) {
$headers['set-cookie'][] = (string) $cookie;
}

return $headers;
}

/**
* {@inheritdoc}
*/
public function set($key, $values, $replace = true)
{
$uniqueKey = str_replace('_', '-', strtolower($key));

if ('set-cookie' === $uniqueKey) {
if ($replace) {
$this->cookies = array();
}
foreach ((array) $values as $cookie) {
$this->setCookie(Cookie::fromString($cookie));
}
$this->headerNames[$uniqueKey] = $key;

return;
}

$this->headerNames[$uniqueKey] = $key;

parent::set($key, $values, $replace);

// ensure the cache-control header has sensible defaults
if (in_array($uniqueKey, array('cache-control', 'etag', 'last-modified', 'expires'))) {
$computed = $this->computeCacheControlValue();
Expand All @@ -116,11 +142,17 @@ public function set($key, $values, $replace = true)
*/
public function remove($key)
{
parent::remove($key);

$uniqueKey = str_replace('_', '-', strtolower($key));
unset($this->headerNames[$uniqueKey]);

if ('set-cookie' === $uniqueKey) {
$this->cookies = array();

return;
}

parent::remove($key);

if ('cache-control' === $uniqueKey) {
$this->computedCacheControl = array();
}
Expand Down Expand Up @@ -150,6 +182,7 @@ public function getCacheControlDirective($key)
public function setCookie(Cookie $cookie)
{
$this->cookies[$cookie->getDomain()][$cookie->getPath()][$cookie->getName()] = $cookie;
$this->headerNames['set-cookie'] = 'Set-Cookie';
}

/**
Expand All @@ -174,6 +207,10 @@ public function removeCookie($name, $path = '/', $domain = null)
unset($this->cookies[$domain]);
}
}

if (empty($this->cookies)) {
unset($this->headerNames['set-cookie']);
}
}

/**
Expand Down
9 changes: 9 additions & 0 deletions 9 src/Symfony/Component/HttpFoundation/Tests/CookieTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -178,4 +178,13 @@ public function testGetMaxAge()
$cookie = new Cookie('foo', 'bar', $expire = time() - 100);
$this->assertEquals($expire - time(), $cookie->getMaxAge());
}

public function testFromString()
{
$cookie = Cookie::fromString('foo=bar; expires=Fri, 20-May-2011 15:25:52 GMT; path=/; domain=.myfoodomain.com; secure; httponly');
$this->assertEquals(new Cookie('foo', 'bar', strtotime('Fri, 20-May-2011 15:25:52 GMT'), '/', '.myfoodomain.com', true, true, true), $cookie);

$cookie = Cookie::fromString('foo=bar', true);
$this->assertEquals(new Cookie('foo', 'bar'), $cookie);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -124,11 +124,11 @@ public function testToStringIncludesCookieHeaders()
$bag = new ResponseHeaderBag(array());
$bag->setCookie(new Cookie('foo', 'bar'));

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

$bag->clearCookie('foo');

$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());
$this->assertSetCookieHeader('foo=deleted; expires='.gmdate('D, d-M-Y H:i:s T', time() - 31536001).'; max-age=-31536001; path=/; httponly', $bag);
}

public function testClearCookieSecureNotHttpOnly()
Expand All @@ -137,7 +137,7 @@ public function testClearCookieSecureNotHttpOnly()

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

$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());
$this->assertSetCookieHeader('foo=deleted; expires='.gmdate('D, d-M-Y H:i:s T', time() - 31536001).'; max-age=-31536001; path=/; secure', $bag);
}

public function testReplace()
Expand Down Expand Up @@ -172,14 +172,21 @@ public function testCookiesWithSameNames()
$bag->setCookie(new Cookie('foo', 'bar'));

$this->assertCount(4, $bag->getCookies());

$headers = explode("\r\n", $bag->__toString());
$this->assertContains('Set-Cookie: foo=bar; path=/path/foo; domain=foo.bar; httponly', $headers);
$this->assertContains('Set-Cookie: foo=bar; path=/path/foo; domain=foo.bar; httponly', $headers);
$this->assertContains('Set-Cookie: foo=bar; path=/path/bar; domain=bar.foo; httponly', $headers);
$this->assertContains('Set-Cookie: foo=bar; path=/; httponly', $headers);
$this->assertEquals('foo=bar; path=/path/foo; domain=foo.bar; httponly', $bag->get('set-cookie'));
$this->assertEquals(array(
'foo=bar; path=/path/foo; domain=foo.bar; httponly',
'foo=bar; path=/path/bar; domain=foo.bar; httponly',
'foo=bar; path=/path/bar; domain=bar.foo; httponly',
'foo=bar; path=/; httponly',
), $bag->get('set-cookie', null, false));

$this->assertSetCookieHeader('foo=bar; path=/path/foo; domain=foo.bar; httponly', $bag);
$this->assertSetCookieHeader('foo=bar; path=/path/bar; domain=foo.bar; httponly', $bag);
$this->assertSetCookieHeader('foo=bar; path=/path/bar; domain=bar.foo; httponly', $bag);
$this->assertSetCookieHeader('foo=bar; path=/; httponly', $bag);

$cookies = $bag->getCookies(ResponseHeaderBag::COOKIES_ARRAY);

$this->assertTrue(isset($cookies['foo.bar']['/path/foo']['foo']));
$this->assertTrue(isset($cookies['foo.bar']['/path/bar']['foo']));
$this->assertTrue(isset($cookies['bar.foo']['/path/bar']['foo']));
Expand All @@ -189,18 +196,23 @@ public function testCookiesWithSameNames()
public function testRemoveCookie()
{
$bag = new ResponseHeaderBag();
$this->assertFalse($bag->has('set-cookie'));

$bag->setCookie(new Cookie('foo', 'bar', 0, '/path/foo', 'foo.bar'));
$bag->setCookie(new Cookie('bar', 'foo', 0, '/path/bar', 'foo.bar'));
$this->assertTrue($bag->has('set-cookie'));

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

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

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

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

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

public function testSetCookieHeader()
{
$bag = new ResponseHeaderBag();
$bag->set('set-cookie', 'foo=bar');
$this->assertEquals(array(new Cookie('foo', 'bar', 0, '/', null, false, true, true)), $bag->getCookies());

$bag->set('set-cookie', 'foo2=bar2', false);
$this->assertEquals(array(
new Cookie('foo', 'bar', 0, '/', null, false, true, true),
new Cookie('foo2', 'bar2', 0, '/', null, false, true, true),
), $bag->getCookies());

$bag->remove('set-cookie');
$this->assertEquals(array(), $bag->getCookies());
}

/**
* @expectedException \InvalidArgumentException
*/
public function testGetCookiesWithInvalidArgument()
{
$bag = new ResponseHeaderBag();

$cookies = $bag->getCookies('invalid_argument');
$bag->getCookies('invalid_argument');
}

/**
Expand Down Expand Up @@ -302,4 +330,9 @@ public function provideMakeDispositionFail()
array('attachment', 'föö.html'),
);
}

protected function assertSetCookieHeader($expected, ResponseHeaderBag $actual)
{
$this->assertRegExp('#^Set-Cookie:\s+'.preg_quote($expected, '#').'$#m', str_replace("\r\n", "\n", (string) $actual));
}
}
Loading
Morty Proxy This is a proxified and sanitized view of the page, visit original site.