diff --git a/src/Symfony/Component/HttpFoundation/CHANGELOG.md b/src/Symfony/Component/HttpFoundation/CHANGELOG.md index 603314b009d9..d504dac2c3ee 100644 --- a/src/Symfony/Component/HttpFoundation/CHANGELOG.md +++ b/src/Symfony/Component/HttpFoundation/CHANGELOG.md @@ -7,6 +7,7 @@ CHANGELOG * Make `HeaderBag::getDate()`, `Response::getDate()`, `getExpires()` and `getLastModified()` return a `DateTimeImmutable` * Support root-level `Generator` in `StreamedJsonResponse` * Add `UriSigner` from the HttpKernel component + * Add `partitioned` flag to `Cookie` (CHIPS Cookie) 6.3 --- diff --git a/src/Symfony/Component/HttpFoundation/Cookie.php b/src/Symfony/Component/HttpFoundation/Cookie.php index 9f43cc2aedd1..706f5ca25614 100644 --- a/src/Symfony/Component/HttpFoundation/Cookie.php +++ b/src/Symfony/Component/HttpFoundation/Cookie.php @@ -32,6 +32,7 @@ class Cookie private bool $raw; private ?string $sameSite = null; + private bool $partitioned = false; private bool $secureDefault = false; private const RESERVED_CHARS_LIST = "=,; \t\r\n\v\f"; @@ -51,6 +52,7 @@ public static function fromString(string $cookie, bool $decode = false): static 'httponly' => false, 'raw' => !$decode, 'samesite' => null, + 'partitioned' => false, ]; $parts = HeaderUtils::split($cookie, ';='); @@ -66,17 +68,20 @@ public static function fromString(string $cookie, bool $decode = false): static $data['expires'] = time() + (int) $data['max-age']; } - return new static($name, $value, $data['expires'], $data['path'], $data['domain'], $data['secure'], $data['httponly'], $data['raw'], $data['samesite']); + return new static($name, $value, $data['expires'], $data['path'], $data['domain'], $data['secure'], $data['httponly'], $data['raw'], $data['samesite'], $data['partitioned']); } /** * @see self::__construct * * @param self::SAMESITE_*|''|null $sameSite + * @param bool $partitioned */ - public static function create(string $name, string $value = null, int|string|\DateTimeInterface $expire = 0, ?string $path = '/', string $domain = null, bool $secure = null, bool $httpOnly = true, bool $raw = false, ?string $sameSite = self::SAMESITE_LAX): self + public static function create(string $name, string $value = null, int|string|\DateTimeInterface $expire = 0, ?string $path = '/', string $domain = null, bool $secure = null, bool $httpOnly = true, bool $raw = false, ?string $sameSite = self::SAMESITE_LAX /* , bool $partitioned = false */): self { - return new self($name, $value, $expire, $path, $domain, $secure, $httpOnly, $raw, $sameSite); + $partitioned = 9 < \func_num_args() ? func_get_arg(9) : false; + + return new self($name, $value, $expire, $path, $domain, $secure, $httpOnly, $raw, $sameSite, $partitioned); } /** @@ -92,7 +97,7 @@ public static function create(string $name, string $value = null, int|string|\Da * * @throws \InvalidArgumentException */ - public function __construct(string $name, string $value = null, int|string|\DateTimeInterface $expire = 0, ?string $path = '/', string $domain = null, bool $secure = null, bool $httpOnly = true, bool $raw = false, ?string $sameSite = self::SAMESITE_LAX) + public function __construct(string $name, string $value = null, int|string|\DateTimeInterface $expire = 0, ?string $path = '/', string $domain = null, bool $secure = null, bool $httpOnly = true, bool $raw = false, ?string $sameSite = self::SAMESITE_LAX, bool $partitioned = false) { // from PHP source code if ($raw && false !== strpbrk($name, self::RESERVED_CHARS_LIST)) { @@ -112,6 +117,7 @@ public function __construct(string $name, string $value = null, int|string|\Date $this->httpOnly = $httpOnly; $this->raw = $raw; $this->sameSite = $this->withSameSite($sameSite)->sameSite; + $this->partitioned = $partitioned; } /** @@ -237,6 +243,17 @@ public function withSameSite(?string $sameSite): static return $cookie; } + /** + * Creates a cookie copy that is tied to the top-level site in cross-site context. + */ + public function withPartitioned(bool $partitioned = true): static + { + $cookie = clone $this; + $cookie->partitioned = $partitioned; + + return $cookie; + } + /** * Returns the cookie as a string. */ @@ -268,11 +285,11 @@ public function __toString(): string $str .= '; domain='.$this->getDomain(); } - if (true === $this->isSecure()) { + if ($this->isSecure()) { $str .= '; secure'; } - if (true === $this->isHttpOnly()) { + if ($this->isHttpOnly()) { $str .= '; httponly'; } @@ -280,6 +297,10 @@ public function __toString(): string $str .= '; samesite='.$this->getSameSite(); } + if ($this->isPartitioned()) { + $str .= '; partitioned'; + } + return $str; } @@ -365,6 +386,14 @@ public function isRaw(): bool return $this->raw; } + /** + * Checks whether the cookie should be tied to the top-level site in cross-site context. + */ + public function isPartitioned(): bool + { + return $this->partitioned; + } + /** * @return self::SAMESITE_*|null */ diff --git a/src/Symfony/Component/HttpFoundation/Tests/CookieTest.php b/src/Symfony/Component/HttpFoundation/Tests/CookieTest.php index 874758e9de38..eca5ee3e30bb 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/CookieTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/CookieTest.php @@ -87,6 +87,19 @@ public function testNegativeExpirationIsNotPossible() $this->assertSame(0, $cookie->getExpiresTime()); } + public function testMinimalParameters() + { + $constructedCookie = new Cookie('foo'); + + $createdCookie = Cookie::create('foo'); + + $cookie = new Cookie('foo', null, 0, '/', null, null, true, false, 'lax'); + + $this->assertEquals($constructedCookie, $cookie); + + $this->assertEquals($createdCookie, $cookie); + } + public function testGetValue() { $value = 'MyValue'; @@ -187,6 +200,17 @@ public function testIsHttpOnly() $this->assertTrue($cookie->isHttpOnly(), '->isHttpOnly() returns whether the cookie is only transmitted over HTTP'); } + public function testIsPartitioned() + { + $cookie = new Cookie('foo', 'bar', 0, '/', '.myfoodomain.com', true, true, false, 'Lax', true); + + $this->assertTrue($cookie->isPartitioned()); + + $cookie = Cookie::create('foo')->withPartitioned(true); + + $this->assertTrue($cookie->isPartitioned()); + } + public function testCookieIsNotCleared() { $cookie = Cookie::create('foo', 'bar', time() + 3600 * 24); @@ -262,6 +286,20 @@ public function testToString() ->withSameSite(null); $this->assertEquals($expected, (string) $cookie, '->__toString() returns string representation of a cleared cookie if value is NULL'); + $expected = 'foo=deleted; expires='.gmdate('D, d M Y H:i:s T', $expire = time() - 31536001).'; Max-Age=0; path=/admin/; domain=.myfoodomain.com; secure; httponly; samesite=none; partitioned'; + $cookie = new Cookie('foo', null, 1, '/admin/', '.myfoodomain.com', true, true, false, 'none', true); + $this->assertEquals($expected, (string) $cookie, '->__toString() returns string representation of a cleared cookie if value is NULL'); + + $cookie = Cookie::create('foo') + ->withExpires(1) + ->withPath('/admin/') + ->withDomain('.myfoodomain.com') + ->withSecure(true) + ->withHttpOnly(true) + ->withSameSite('none') + ->withPartitioned(true); + $this->assertEquals($expected, (string) $cookie, '->__toString() returns string representation of a cleared cookie if value is NULL'); + $expected = 'foo=bar; path=/; httponly; samesite=lax'; $cookie = Cookie::create('foo', 'bar'); $this->assertEquals($expected, (string) $cookie); @@ -321,6 +359,9 @@ public function testFromString() $cookie = Cookie::fromString('foo_cookie=foo==; expires=Tue, 22 Sep 2020 06:27:09 GMT; path=/'); $this->assertEquals(Cookie::create('foo_cookie', 'foo==', strtotime('Tue, 22 Sep 2020 06:27:09 GMT'), '/', null, false, false, true, null), $cookie); + + $cookie = Cookie::fromString('foo_cookie=foo==; expires=Tue, 22 Sep 2020 06:27:09 GMT; path=/; secure; httponly; samesite=none; partitioned'); + $this->assertEquals(new Cookie('foo_cookie', 'foo==', strtotime('Tue, 22 Sep 2020 06:27:09 GMT'), '/', null, true, true, true, 'none', true), $cookie); } public function testFromStringWithHttpOnly()
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: