From 26df07bce02e50fbd261212814dd3e4dd74bab5c Mon Sep 17 00:00:00 2001 From: Fabrice Locher Date: Thu, 12 Oct 2023 00:58:45 +0200 Subject: [PATCH] [HttpFoundation] Cookies Having Independent Partitioned State (CHIPS) --- .../Component/HttpFoundation/CHANGELOG.md | 1 + .../Component/HttpFoundation/Cookie.php | 41 ++++++++++++++++--- .../HttpFoundation/Tests/CookieTest.php | 41 +++++++++++++++++++ 3 files changed, 77 insertions(+), 6 deletions(-) diff --git a/src/Symfony/Component/HttpFoundation/CHANGELOG.md b/src/Symfony/Component/HttpFoundation/CHANGELOG.md index 603314b009d94..d504dac2c3ee2 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 9f43cc2aedd19..706f5ca25614a 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 874758e9de38d..eca5ee3e30bb2 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() pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

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:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy