bug #25348 [HttpFoundation] Send cookies using header() to fix "SameSite" ones (nicolas-grekas, cvilleger)
This PR was merged into the 3.4 branch. Discussion ---------- [HttpFoundation] Send cookies using header() to fix "SameSite" ones | Q | A | ------------- | --- | Branch? | 3.4 | Bug fix? | yes | New feature? | no | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | #25344 | License | MIT | Doc PR | - Commits -------73fec237da
[HttpFoundation] Add functional tests for Response::sendHeaders()e350ea000f
[HttpFoundation] Send cookies using header() to fix "SameSite" ones
This commit is contained in:
commit
82a95dfb22
@ -145,12 +145,12 @@ class Cookie
|
|||||||
$str = ($this->isRaw() ? $this->getName() : urlencode($this->getName())).'=';
|
$str = ($this->isRaw() ? $this->getName() : urlencode($this->getName())).'=';
|
||||||
|
|
||||||
if ('' === (string) $this->getValue()) {
|
if ('' === (string) $this->getValue()) {
|
||||||
$str .= 'deleted; expires='.gmdate('D, d-M-Y H:i:s T', time() - 31536001).'; max-age=-31536001';
|
$str .= 'deleted; expires='.gmdate('D, d-M-Y H:i:s T', time() - 31536001).'; Max-Age=0';
|
||||||
} else {
|
} else {
|
||||||
$str .= $this->isRaw() ? $this->getValue() : rawurlencode($this->getValue());
|
$str .= $this->isRaw() ? $this->getValue() : rawurlencode($this->getValue());
|
||||||
|
|
||||||
if (0 !== $this->getExpiresTime()) {
|
if (0 !== $this->getExpiresTime()) {
|
||||||
$str .= '; expires='.gmdate('D, d-M-Y H:i:s T', $this->getExpiresTime()).'; max-age='.$this->getMaxAge();
|
$str .= '; expires='.gmdate('D, d-M-Y H:i:s T', $this->getExpiresTime()).'; Max-Age='.$this->getMaxAge();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -224,7 +224,9 @@ class Cookie
|
|||||||
*/
|
*/
|
||||||
public function getMaxAge()
|
public function getMaxAge()
|
||||||
{
|
{
|
||||||
return 0 !== $this->expire ? $this->expire - time() : 0;
|
$maxAge = $this->expire - time();
|
||||||
|
|
||||||
|
return 0 >= $maxAge ? 0 : $maxAge;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -327,7 +327,7 @@ class Response
|
|||||||
}
|
}
|
||||||
|
|
||||||
// headers
|
// headers
|
||||||
foreach ($this->headers->allPreserveCaseWithoutCookies() as $name => $values) {
|
foreach ($this->headers->allPreserveCase() as $name => $values) {
|
||||||
foreach ($values as $value) {
|
foreach ($values as $value) {
|
||||||
header($name.': '.$value, false, $this->statusCode);
|
header($name.': '.$value, false, $this->statusCode);
|
||||||
}
|
}
|
||||||
@ -336,15 +336,6 @@ class Response
|
|||||||
// status
|
// status
|
||||||
header(sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText), true, $this->statusCode);
|
header(sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText), true, $this->statusCode);
|
||||||
|
|
||||||
// cookies
|
|
||||||
foreach ($this->headers->getCookies() as $cookie) {
|
|
||||||
if ($cookie->isRaw()) {
|
|
||||||
setrawcookie($cookie->getName(), $cookie->getValue(), $cookie->getExpiresTime(), $cookie->getPath(), $cookie->getDomain(), $cookie->isSecure(), $cookie->isHttpOnly());
|
|
||||||
} else {
|
|
||||||
setcookie($cookie->getName(), $cookie->getValue(), $cookie->getExpiresTime(), $cookie->getPath(), $cookie->getDomain(), $cookie->isSecure(), $cookie->isHttpOnly());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -162,13 +162,13 @@ class CookieTest extends TestCase
|
|||||||
public function testToString()
|
public function testToString()
|
||||||
{
|
{
|
||||||
$cookie = new Cookie('foo', 'bar', $expire = strtotime('Fri, 20-May-2011 15:25:52 GMT'), '/', '.myfoodomain.com', true);
|
$cookie = new Cookie('foo', 'bar', $expire = strtotime('Fri, 20-May-2011 15:25:52 GMT'), '/', '.myfoodomain.com', true);
|
||||||
$this->assertEquals('foo=bar; expires=Fri, 20-May-2011 15:25:52 GMT; max-age='.($expire - time()).'; path=/; domain=.myfoodomain.com; secure; httponly', (string) $cookie, '->__toString() returns string representation of the cookie');
|
$this->assertEquals('foo=bar; expires=Fri, 20-May-2011 15:25:52 GMT; Max-Age=0; path=/; domain=.myfoodomain.com; secure; httponly', (string) $cookie, '->__toString() returns string representation of the cookie');
|
||||||
|
|
||||||
$cookie = new Cookie('foo', 'bar with white spaces', strtotime('Fri, 20-May-2011 15:25:52 GMT'), '/', '.myfoodomain.com', true);
|
$cookie = new Cookie('foo', 'bar with white spaces', strtotime('Fri, 20-May-2011 15:25:52 GMT'), '/', '.myfoodomain.com', true);
|
||||||
$this->assertEquals('foo=bar%20with%20white%20spaces; expires=Fri, 20-May-2011 15:25:52 GMT; max-age='.($expire - time()).'; path=/; domain=.myfoodomain.com; secure; httponly', (string) $cookie, '->__toString() encodes the value of the cookie according to RFC 3986 (white space = %20)');
|
$this->assertEquals('foo=bar%20with%20white%20spaces; expires=Fri, 20-May-2011 15:25:52 GMT; Max-Age=0; path=/; domain=.myfoodomain.com; secure; httponly', (string) $cookie, '->__toString() encodes the value of the cookie according to RFC 3986 (white space = %20)');
|
||||||
|
|
||||||
$cookie = new Cookie('foo', null, 1, '/admin/', '.myfoodomain.com');
|
$cookie = new Cookie('foo', null, 1, '/admin/', '.myfoodomain.com');
|
||||||
$this->assertEquals('foo=deleted; expires='.gmdate('D, d-M-Y H:i:s T', $expire = time() - 31536001).'; max-age='.($expire - time()).'; path=/admin/; domain=.myfoodomain.com; httponly', (string) $cookie, '->__toString() returns string representation of a cleared cookie if value is NULL');
|
$this->assertEquals('foo=deleted; expires='.gmdate('D, d-M-Y H:i:s T', $expire = time() - 31536001).'; Max-Age=0; path=/admin/; domain=.myfoodomain.com; httponly', (string) $cookie, '->__toString() returns string representation of a cleared cookie if value is NULL');
|
||||||
|
|
||||||
$cookie = new Cookie('foo', 'bar', 0, '/', '');
|
$cookie = new Cookie('foo', 'bar', 0, '/', '');
|
||||||
$this->assertEquals('foo=bar; path=/; httponly', (string) $cookie);
|
$this->assertEquals('foo=bar; path=/; httponly', (string) $cookie);
|
||||||
@ -194,7 +194,7 @@ class CookieTest extends TestCase
|
|||||||
$this->assertEquals($expire - time(), $cookie->getMaxAge());
|
$this->assertEquals($expire - time(), $cookie->getMaxAge());
|
||||||
|
|
||||||
$cookie = new Cookie('foo', 'bar', $expire = time() - 100);
|
$cookie = new Cookie('foo', 'bar', $expire = time() - 100);
|
||||||
$this->assertEquals($expire - time(), $cookie->getMaxAge());
|
$this->assertEquals(0, $cookie->getMaxAge());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testFromString()
|
public function testFromString()
|
||||||
|
@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
$parent = __DIR__;
|
||||||
|
while (!@file_exists($parent.'/vendor/autoload.php')) {
|
||||||
|
if (!@file_exists($parent)) {
|
||||||
|
// open_basedir restriction in effect
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if ($parent === dirname($parent)) {
|
||||||
|
echo "vendor/autoload.php not found\n";
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$parent = dirname($parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
require $parent.'/vendor/autoload.php';
|
||||||
|
|
||||||
|
error_reporting(-1);
|
||||||
|
ini_set('html_errors', 0);
|
||||||
|
ini_set('display_errors', 1);
|
||||||
|
|
||||||
|
header_remove('X-Powered-By');
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
|
||||||
|
register_shutdown_function(function () {
|
||||||
|
echo "\n";
|
||||||
|
session_write_close();
|
||||||
|
print_r(headers_list());
|
||||||
|
echo "shutdown\n";
|
||||||
|
});
|
||||||
|
ob_start();
|
||||||
|
|
||||||
|
$r = new Response();
|
||||||
|
$r->headers->set('Date', 'Sat, 12 Nov 1955 20:04:00 GMT');
|
||||||
|
|
||||||
|
return $r;
|
@ -0,0 +1,11 @@
|
|||||||
|
|
||||||
|
Warning: Expiry date cannot have a year greater than 9999 in %scookie_max_age.php on line 10
|
||||||
|
|
||||||
|
Array
|
||||||
|
(
|
||||||
|
[0] => Content-Type: text/plain; charset=utf-8
|
||||||
|
[1] => Cache-Control: no-cache, private
|
||||||
|
[2] => Date: Sat, 12 Nov 1955 20:04:00 GMT
|
||||||
|
[3] => Set-Cookie: foo=bar; expires=Sat, 01-Jan-10000 02:46:40 GMT; Max-Age=%d; path=/
|
||||||
|
)
|
||||||
|
shutdown
|
@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Symfony\Component\HttpFoundation\Cookie;
|
||||||
|
|
||||||
|
$r = require __DIR__.'/common.inc';
|
||||||
|
|
||||||
|
$r->headers->setCookie(new Cookie('foo', 'bar', 253402310800, '', null, false, false));
|
||||||
|
$r->sendHeaders();
|
||||||
|
|
||||||
|
setcookie('foo2', 'bar', 253402310800, '/');
|
@ -0,0 +1,10 @@
|
|||||||
|
|
||||||
|
Array
|
||||||
|
(
|
||||||
|
[0] => Content-Type: text/plain; charset=utf-8
|
||||||
|
[1] => Cache-Control: no-cache, private
|
||||||
|
[2] => Date: Sat, 12 Nov 1955 20:04:00 GMT
|
||||||
|
[3] => Set-Cookie: ?*():@&+$/%#[]=?*():@&+$/%#[]; path=/
|
||||||
|
[4] => Set-Cookie: ?*():@&+$/%#[]=?*():@&+$/%#[]; path=/
|
||||||
|
)
|
||||||
|
shutdown
|
@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Symfony\Component\HttpFoundation\Cookie;
|
||||||
|
|
||||||
|
$r = require __DIR__.'/common.inc';
|
||||||
|
|
||||||
|
$str = '?*():@&+$/%#[]';
|
||||||
|
|
||||||
|
$r->headers->setCookie(new Cookie($str, $str, 0, '/', null, false, false, true));
|
||||||
|
$r->sendHeaders();
|
||||||
|
|
||||||
|
setrawcookie($str, $str, 0, '/', null, false, false);
|
@ -0,0 +1,9 @@
|
|||||||
|
|
||||||
|
Array
|
||||||
|
(
|
||||||
|
[0] => Content-Type: text/plain; charset=utf-8
|
||||||
|
[1] => Cache-Control: no-cache, private
|
||||||
|
[2] => Date: Sat, 12 Nov 1955 20:04:00 GMT
|
||||||
|
[3] => Set-Cookie: CookieSamesiteLaxTest=LaxValue; path=/; httponly; samesite=lax
|
||||||
|
)
|
||||||
|
shutdown
|
@ -0,0 +1,8 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Symfony\Component\HttpFoundation\Cookie;
|
||||||
|
|
||||||
|
$r = require __DIR__.'/common.inc';
|
||||||
|
|
||||||
|
$r->headers->setCookie(new Cookie('CookieSamesiteLaxTest', 'LaxValue', 0, '/', null, false, true, false, Cookie::SAMESITE_LAX));
|
||||||
|
$r->sendHeaders();
|
@ -0,0 +1,9 @@
|
|||||||
|
|
||||||
|
Array
|
||||||
|
(
|
||||||
|
[0] => Content-Type: text/plain; charset=utf-8
|
||||||
|
[1] => Cache-Control: no-cache, private
|
||||||
|
[2] => Date: Sat, 12 Nov 1955 20:04:00 GMT
|
||||||
|
[3] => Set-Cookie: CookieSamesiteStrictTest=StrictValue; path=/; httponly; samesite=strict
|
||||||
|
)
|
||||||
|
shutdown
|
@ -0,0 +1,8 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Symfony\Component\HttpFoundation\Cookie;
|
||||||
|
|
||||||
|
$r = require __DIR__.'/common.inc';
|
||||||
|
|
||||||
|
$r->headers->setCookie(new Cookie('CookieSamesiteStrictTest', 'StrictValue', 0, '/', null, false, true, false, Cookie::SAMESITE_STRICT));
|
||||||
|
$r->sendHeaders();
|
@ -0,0 +1,10 @@
|
|||||||
|
|
||||||
|
Array
|
||||||
|
(
|
||||||
|
[0] => Content-Type: text/plain; charset=utf-8
|
||||||
|
[1] => Cache-Control: no-cache, private
|
||||||
|
[2] => Date: Sat, 12 Nov 1955 20:04:00 GMT
|
||||||
|
[3] => Set-Cookie: %3F%2A%28%29%3A%40%26%2B%24%2F%25%23%5B%5D=%3F%2A%28%29%3A%40%26%2B%24%2F%25%23%5B%5D; path=/
|
||||||
|
[4] => Set-Cookie: ?*():@&+$/%#[]=%3F%2A%28%29%3A%40%26%2B%24%2F%25%23%5B%5D; path=/
|
||||||
|
)
|
||||||
|
shutdown
|
@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Symfony\Component\HttpFoundation\Cookie;
|
||||||
|
|
||||||
|
$r = require __DIR__.'/common.inc';
|
||||||
|
|
||||||
|
$str = '?*():@&+$/%#[]';
|
||||||
|
|
||||||
|
$r->headers->setCookie(new Cookie($str, $str, 0, '', null, false, false));
|
||||||
|
$r->sendHeaders();
|
||||||
|
|
||||||
|
setcookie($str, $str, 0, '/');
|
@ -0,0 +1,6 @@
|
|||||||
|
The cookie name "Hello + world" contains invalid characters.
|
||||||
|
Array
|
||||||
|
(
|
||||||
|
[0] => Content-Type: text/plain; charset=utf-8
|
||||||
|
)
|
||||||
|
shutdown
|
@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Symfony\Component\HttpFoundation\Cookie;
|
||||||
|
|
||||||
|
$r = require __DIR__.'/common.inc';
|
||||||
|
|
||||||
|
try {
|
||||||
|
$r->headers->setCookie(new Cookie('Hello + world', 'hodor'));
|
||||||
|
} catch (\InvalidArgumentException $e) {
|
||||||
|
echo $e->getMessage();
|
||||||
|
}
|
@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Component\HttpFoundation\Tests;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @requires PHP 7.0
|
||||||
|
*/
|
||||||
|
class ResponseFunctionalTest extends TestCase
|
||||||
|
{
|
||||||
|
private static $server;
|
||||||
|
|
||||||
|
public static function setUpBeforeClass()
|
||||||
|
{
|
||||||
|
$spec = array(
|
||||||
|
1 => array('file', '/dev/null', 'w'),
|
||||||
|
2 => array('file', '/dev/null', 'w'),
|
||||||
|
);
|
||||||
|
if (!self::$server = @proc_open('exec php -S localhost:8054', $spec, $pipes, __DIR__.'/Fixtures/response-functional')) {
|
||||||
|
self::markTestSkipped('PHP server unable to start.');
|
||||||
|
}
|
||||||
|
sleep(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function tearDownAfterClass()
|
||||||
|
{
|
||||||
|
if (self::$server) {
|
||||||
|
proc_terminate(self::$server);
|
||||||
|
proc_close(self::$server);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider provideCookie
|
||||||
|
*/
|
||||||
|
public function testCookie($fixture)
|
||||||
|
{
|
||||||
|
$result = file_get_contents(sprintf('http://localhost:8054/%s.php', $fixture));
|
||||||
|
$this->assertStringMatchesFormatFile(__DIR__.sprintf('/Fixtures/response-functional/%s.expected', $fixture), $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function provideCookie()
|
||||||
|
{
|
||||||
|
foreach (glob(__DIR__.'/Fixtures/response-functional/*.php') as $file) {
|
||||||
|
yield array(pathinfo($file, PATHINFO_FILENAME));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -116,7 +116,7 @@ class ResponseHeaderBagTest extends TestCase
|
|||||||
|
|
||||||
$bag->clearCookie('foo');
|
$bag->clearCookie('foo');
|
||||||
|
|
||||||
$this->assertSetCookieHeader('foo=deleted; expires='.gmdate('D, d-M-Y H:i:s T', time() - 31536001).'; max-age=-31536001; path=/; httponly', $bag);
|
$this->assertSetCookieHeader('foo=deleted; expires='.gmdate('D, d-M-Y H:i:s T', time() - 31536001).'; Max-Age=0; path=/; httponly', $bag);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testClearCookieSecureNotHttpOnly()
|
public function testClearCookieSecureNotHttpOnly()
|
||||||
@ -125,7 +125,7 @@ class ResponseHeaderBagTest extends TestCase
|
|||||||
|
|
||||||
$bag->clearCookie('foo', '/', null, true, false);
|
$bag->clearCookie('foo', '/', null, true, false);
|
||||||
|
|
||||||
$this->assertSetCookieHeader('foo=deleted; expires='.gmdate('D, d-M-Y H:i:s T', time() - 31536001).'; max-age=-31536001; path=/; secure', $bag);
|
$this->assertSetCookieHeader('foo=deleted; expires='.gmdate('D, d-M-Y H:i:s T', time() - 31536001).'; Max-Age=0; path=/; secure', $bag);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testReplace()
|
public function testReplace()
|
||||||
|
@ -60,22 +60,17 @@ class ClientTest extends TestCase
|
|||||||
$m = $r->getMethod('filterResponse');
|
$m = $r->getMethod('filterResponse');
|
||||||
$m->setAccessible(true);
|
$m->setAccessible(true);
|
||||||
|
|
||||||
$expected = array(
|
$response = new Response();
|
||||||
'foo=bar; expires=Sun, 15-Feb-2009 20:00:00 GMT; max-age='.(strtotime('Sun, 15-Feb-2009 20:00:00 GMT') - time()).'; path=/foo; domain=http://example.com; secure; httponly',
|
$response->headers->setCookie($cookie1 = new Cookie('foo', 'bar', \DateTime::createFromFormat('j-M-Y H:i:s T', '15-Feb-2009 20:00:00 GMT')->format('U'), '/foo', 'http://example.com', true, true));
|
||||||
'foo1=bar1; expires=Sun, 15-Feb-2009 20:00:00 GMT; max-age='.(strtotime('Sun, 15-Feb-2009 20:00:00 GMT') - time()).'; path=/foo; domain=http://example.com; secure; httponly',
|
$domResponse = $m->invoke($client, $response);
|
||||||
);
|
$this->assertSame((string) $cookie1, $domResponse->getHeader('Set-Cookie'));
|
||||||
|
|
||||||
$response = new Response();
|
$response = new Response();
|
||||||
$response->headers->setCookie(new Cookie('foo', 'bar', \DateTime::createFromFormat('j-M-Y H:i:s T', '15-Feb-2009 20:00:00 GMT')->format('U'), '/foo', 'http://example.com', true, true));
|
$response->headers->setCookie($cookie1 = new Cookie('foo', 'bar', \DateTime::createFromFormat('j-M-Y H:i:s T', '15-Feb-2009 20:00:00 GMT')->format('U'), '/foo', 'http://example.com', true, true));
|
||||||
|
$response->headers->setCookie($cookie2 = new Cookie('foo1', 'bar1', \DateTime::createFromFormat('j-M-Y H:i:s T', '15-Feb-2009 20:00:00 GMT')->format('U'), '/foo', 'http://example.com', true, true));
|
||||||
$domResponse = $m->invoke($client, $response);
|
$domResponse = $m->invoke($client, $response);
|
||||||
$this->assertEquals($expected[0], $domResponse->getHeader('Set-Cookie'));
|
$this->assertSame((string) $cookie1, $domResponse->getHeader('Set-Cookie'));
|
||||||
|
$this->assertSame(array((string) $cookie1, (string) $cookie2), $domResponse->getHeader('Set-Cookie', false));
|
||||||
$response = new Response();
|
|
||||||
$response->headers->setCookie(new Cookie('foo', 'bar', \DateTime::createFromFormat('j-M-Y H:i:s T', '15-Feb-2009 20:00:00 GMT')->format('U'), '/foo', 'http://example.com', true, true));
|
|
||||||
$response->headers->setCookie(new Cookie('foo1', 'bar1', \DateTime::createFromFormat('j-M-Y H:i:s T', '15-Feb-2009 20:00:00 GMT')->format('U'), '/foo', 'http://example.com', true, true));
|
|
||||||
$domResponse = $m->invoke($client, $response);
|
|
||||||
$this->assertEquals($expected[0], $domResponse->getHeader('Set-Cookie'));
|
|
||||||
$this->assertEquals($expected, $domResponse->getHeader('Set-Cookie', false));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testFilterResponseSupportsStreamedResponses()
|
public function testFilterResponseSupportsStreamedResponses()
|
||||||
|
Reference in New Issue
Block a user