From 4f879dbb9063c818b7f04fced5baf43a1c19d0fb Mon Sep 17 00:00:00 2001 From: Seth Battis Date: Tue, 5 Aug 2025 08:53:44 -0400 Subject: [PATCH 1/8] feat: support CHIPS partitioned cookies `SetCookie::withPartitioned()` --- src/Dflydev/FigCookies/SetCookie.php | 34 ++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/Dflydev/FigCookies/SetCookie.php b/src/Dflydev/FigCookies/SetCookie.php index 23283a7..747dea0 100644 --- a/src/Dflydev/FigCookies/SetCookie.php +++ b/src/Dflydev/FigCookies/SetCookie.php @@ -42,6 +42,8 @@ class SetCookie private $httpOnly = false; /** @var SameSite|null */ private $sameSite; + /** @var bool */ + private $partitioned = false; private function __construct(string $name, ?string $value = null) { @@ -94,6 +96,11 @@ public function getSameSite(): ?SameSite return $this->sameSite; } + public function getPartitioned(): bool + { + return $this->partitioned; + } + public function withValue(?string $value = null): self { $clone = clone $this; @@ -212,6 +219,15 @@ public function withoutSameSite(): self return $clone; } + public function withPartitioned(bool $partitioned = true): self + { + $clone = clone $this; + + $clone->partitioned = $partitioned; + + return $clone; + } + public function __toString(): string { $cookieStringParts = [ @@ -225,6 +241,7 @@ public function __toString(): string $cookieStringParts = $this->appendFormattedSecurePartIfSet($cookieStringParts); $cookieStringParts = $this->appendFormattedHttpOnlyPartIfSet($cookieStringParts); $cookieStringParts = $this->appendFormattedSameSitePartIfSet($cookieStringParts); + $cookieStringParts = $this->appendFormattedPartitionedPartIfSet($cookieStringParts); return implode('; ', $cookieStringParts); } @@ -301,6 +318,9 @@ public static function fromSetCookieString(string $string): self case 'samesite': $setCookie = $setCookie->withSameSite(SameSite::fromString((string) $attributeValue)); break; + case 'partitioned': + $setCookie = $setCookie->withPartitioned(); + break; } } @@ -406,4 +426,18 @@ private function appendFormattedSameSitePartIfSet(array $cookieStringParts): arr return $cookieStringParts; } + + /** + * @param string[] $cookieStringParts + * + * @return string[] + */ + private function appendFormattedPartitionedPartIfSet(array $cookieStringParts): array + { + if ($this->partitioned) { + $cookieStringParts[] = 'Partitioned'; + } + + return $cookieStringParts; + } } From 2b7fb203142252addae832f13101be72d178bd38 Mon Sep 17 00:00:00 2001 From: Seth Battis Date: Tue, 5 Aug 2025 08:54:01 -0400 Subject: [PATCH 2/8] chore: CHIPS partitioned cookies tests --- tests/Dflydev/FigCookies/SetCookieTest.php | 77 +++++++++++++++------ tests/Dflydev/FigCookies/SetCookiesTest.php | 14 ++++ 2 files changed, 68 insertions(+), 23 deletions(-) diff --git a/tests/Dflydev/FigCookies/SetCookieTest.php b/tests/Dflydev/FigCookies/SetCookieTest.php index aa0f482..ae106c2 100644 --- a/tests/Dflydev/FigCookies/SetCookieTest.php +++ b/tests/Dflydev/FigCookies/SetCookieTest.php @@ -110,38 +110,51 @@ public function provideParsesFromSetCookieStringData(): array [ 'lu=Rg3vHJZnehYLjVg7qi3bZjzg; Domain=.example.com; Path=/; Expires=Tue, 15 Jan 2013 21:47:38 GMT; Max-Age=500; Secure; HttpOnly', SetCookie::create('lu') - ->withValue('Rg3vHJZnehYLjVg7qi3bZjzg') - ->withExpires(new DateTime('Tue, 15-Jan-2013 21:47:38 GMT')) - ->withMaxAge(500) - ->withPath('/') - ->withDomain('.example.com') - ->withSecure(true) - ->withHttpOnly(true), + ->withValue('Rg3vHJZnehYLjVg7qi3bZjzg') + ->withExpires(new DateTime('Tue, 15-Jan-2013 21:47:38 GMT')) + ->withMaxAge(500) + ->withPath('/') + ->withDomain('.example.com') + ->withSecure(true) + ->withHttpOnly(true), ], [ 'lu=Rg3vHJZnehYLjVg7qi3bZjzg; Domain=.example.com; Path=/; Expires=Tue, 15 Jan 2013 21:47:38 GMT; Max-Age=500; Secure; HttpOnly; SameSite=Strict', SetCookie::create('lu') - ->withValue('Rg3vHJZnehYLjVg7qi3bZjzg') - ->withExpires(new DateTime('Tue, 15-Jan-2013 21:47:38 GMT')) - ->withMaxAge(500) - ->withPath('/') - ->withDomain('.example.com') - ->withSecure(true) - ->withHttpOnly(true) - ->withSameSite(SameSite::strict()), + ->withValue('Rg3vHJZnehYLjVg7qi3bZjzg') + ->withExpires(new DateTime('Tue, 15-Jan-2013 21:47:38 GMT')) + ->withMaxAge(500) + ->withPath('/') + ->withDomain('.example.com') + ->withSecure(true) + ->withHttpOnly(true) + ->withSameSite(SameSite::strict()), ], [ 'lu=Rg3vHJZnehYLjVg7qi3bZjzg; Domain=.example.com; Path=/; Expires=Tue, 15 Jan 2013 21:47:38 GMT; Max-Age=500; Secure; HttpOnly; SameSite=Lax', SetCookie::create('lu') - ->withValue('Rg3vHJZnehYLjVg7qi3bZjzg') - ->withExpires(new DateTime('Tue, 15-Jan-2013 21:47:38 GMT')) - ->withMaxAge(500) - ->withPath('/') - ->withDomain('.example.com') - ->withSecure(true) - ->withHttpOnly(true) - ->withSameSite(SameSite::lax()), + ->withValue('Rg3vHJZnehYLjVg7qi3bZjzg') + ->withExpires(new DateTime('Tue, 15-Jan-2013 21:47:38 GMT')) + ->withMaxAge(500) + ->withPath('/') + ->withDomain('.example.com') + ->withSecure(true) + ->withHttpOnly(true) + ->withSameSite(SameSite::lax()), ], + [ + 'lu=d2ioU4.4KDUjjnCGNut4; Domain=.example.com; Path=/; Expires=Tue, 15 Jan 2013 21:47:38 GMT; Max-Age=500; Secure; HttpOnly; SameSite=Lax; Partitioned', + SetCookie::create('lu') + ->withValue('d2ioU4.4KDUjjnCGNut4') + ->withExpires(new DateTime('Tue, 15-Jan-2013 21:47:38 GMT')) + ->withMaxAge(500) + ->withPath('/') + ->withDomain('.example.com') + ->withSecure(true) + ->withHttpOnly(true) + ->withSameSite(SameSite::lax()) + ->withPartitioned() + ] ]; } @@ -171,6 +184,24 @@ public function it_creates_long_living_cookies(): void self::assertGreaterThan($fourYearsFromNow, $setCookie->getExpires()); } + /** + * @test + */ + public function it_creates_partitioned_cookies(): void + { + $setCookie = SetCookie::create('chips_cookie')->withPartitioned(); + self::assertEquals(true, $setCookie->getPartitioned()); + } + + /** + * @test + */ + public function it_does_not_set_partitioned_cookies_by_default(): void + { + $setCookie = SetCookie::create('non_chips_cookie'); + self::assertEquals(false, $setCookie->getPartitioned()); + } + /** @test */ public function SameSite_modifier_can_be_added_and_removed(): void { diff --git a/tests/Dflydev/FigCookies/SetCookiesTest.php b/tests/Dflydev/FigCookies/SetCookiesTest.php index eb16ec7..d9febd5 100644 --- a/tests/Dflydev/FigCookies/SetCookiesTest.php +++ b/tests/Dflydev/FigCookies/SetCookiesTest.php @@ -181,6 +181,20 @@ public function provideSetCookieStringsAndExpectedSetCookiesData(): array SetCookie::create('c', 'CCC'), ], ], + [ + [ + 'd=DDD', + 'e=EEE; Partitioned', + 'f=FFF', + 'g=GGG; Partitioned', + ], + [ + SetCookie::create('d', 'DDD'), + SetCookie::create('e', 'EEE')->withPartitioned(), + SetCookie::create('f', 'FFF'), + SetCookie::create('g', 'GGG')->withPartitioned() + ] + ] ]; } From cdd564a89f80df24210b85f9feed911f8d4ef0ff Mon Sep 17 00:00:00 2001 From: Seth Battis Date: Tue, 5 Aug 2025 08:55:21 -0400 Subject: [PATCH 3/8] docs: add `SetCookie::withPartitioned()` to example --- README.md | 41 +++++++++++++---------------------------- 1 file changed, 13 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index af92fa7..70474c7 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ -FIG Cookies -=========== +# FIG Cookies Managing Cookies for PSR-7 Requests and Responses. @@ -15,20 +14,16 @@ Managing Cookies for PSR-7 Requests and Responses.
[![Join the chat at https://gitter.im/dflydev/dflydev-fig-cookies](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/dflydev/dflydev-fig-cookies?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) - -Installation ------------- +## Installation ```bash $> composer require dflydev/fig-cookies ``` +## Concepts -Concepts --------- - -FIG Cookies tackles two problems, managing **Cookie** *Request* headers and -managing **Set-Cookie** *Response* headers. It does this by way of introducing +FIG Cookies tackles two problems, managing **Cookie** _Request_ headers and +managing **Set-Cookie** _Response_ headers. It does this by way of introducing a `Cookies` class to manage collections of `Cookie` instances and a `SetCookies` class to manage collections of `SetCookie` instances. @@ -66,9 +61,7 @@ verbose very quickly. In order to get around that, FIG Cookies provides two facades in an attempt to help simplify things and make the whole process less verbose. - -Basic Usage ------------ +## Basic Usage The easiest way to start working with FIG Cookies is by using the `FigRequestCookies` and `FigResponseCookies` classes. They are facades to the @@ -81,7 +74,6 @@ process so be wary of using too many of these calls in the same section of code. In some cases it may be better to work with the primitive FIG Cookies classes directly rather than using the facades. - ### Request Cookies Requests include cookie information in the **Cookie** request header. The @@ -180,6 +172,7 @@ $setCookie = SetCookie::create('lu') ->withSecure(true) ->withHttpOnly(true) ->withSameSite(SameSite::lax()) + ->witHPartitioned() ; ``` @@ -279,9 +272,7 @@ $setCookie = SetCookie::create('ba') FigResponseCookies::set($response, $setCookie->expire()); ``` - -FAQ ---- +## FAQ ### Do you call `setcookies`? @@ -290,7 +281,6 @@ No. Delivery of the rendered `SetCookie` instances is the responsibility of the PSR-7 client implementation. - ### Do you do anything with sessions? No. @@ -298,7 +288,6 @@ No. It would be possible to build session handling using cookies on top of FIG Cookies but it is out of scope for this package. - ### Do you read from `$_COOKIES`? No. @@ -310,18 +299,14 @@ implementations should be including `$_COOKIES` values in the headers so in that case FIG Cookies may be interacting with `$_COOKIES` indirectly. - -License -------- +## License MIT, see LICENSE. - -Community ---------- +## Community Want to get involved? Here are a few ways: - * Find us in the #dflydev IRC channel on irc.freenode.org. - * Mention [@dflydev](https://twitter.com/dflydev) on Twitter. - * [![Join the chat at https://gitter.im/dflydev/dflydev-fig-cookies](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/dflydev/dflydev-fig-cookies?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +- Find us in the #dflydev IRC channel on irc.freenode.org. +- Mention [@dflydev](https://twitter.com/dflydev) on Twitter. +- [![Join the chat at https://gitter.im/dflydev/dflydev-fig-cookies](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/dflydev/dflydev-fig-cookies?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) From 4706e16f6bca01b9f4b682292f22ae11546cf35c Mon Sep 17 00:00:00 2001 From: Seth Battis Date: Tue, 5 Aug 2025 19:44:13 -0400 Subject: [PATCH 4/8] fix: partitioned cookies must also be secure Setting withPartitioned() will also set withSecure() --- src/Dflydev/FigCookies/SetCookie.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Dflydev/FigCookies/SetCookie.php b/src/Dflydev/FigCookies/SetCookie.php index 747dea0..b52bce2 100644 --- a/src/Dflydev/FigCookies/SetCookie.php +++ b/src/Dflydev/FigCookies/SetCookie.php @@ -224,6 +224,9 @@ public function withPartitioned(bool $partitioned = true): self $clone = clone $this; $clone->partitioned = $partitioned; + if ($partitioned) { + $clone->secure = true; + } return $clone; } From dcae68384ba342483ea9138c7402e3a46456b853 Mon Sep 17 00:00:00 2001 From: Seth Battis Date: Wed, 6 Aug 2025 07:37:31 -0400 Subject: [PATCH 5/8] chore: test that partitioned cookies are secure --- tests/Dflydev/FigCookies/SetCookieTest.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/Dflydev/FigCookies/SetCookieTest.php b/tests/Dflydev/FigCookies/SetCookieTest.php index ae106c2..a41457e 100644 --- a/tests/Dflydev/FigCookies/SetCookieTest.php +++ b/tests/Dflydev/FigCookies/SetCookieTest.php @@ -202,6 +202,12 @@ public function it_does_not_set_partitioned_cookies_by_default(): void self::assertEquals(false, $setCookie->getPartitioned()); } + public function partitioned_cookies_are_secure(): void + { + $setCookie = SetCookie::create('maybe_secure_partitioned_cookie')->withPartitioned(); + self::assertEquals(true, $setCookie->getSecure()); + } + /** @test */ public function SameSite_modifier_can_be_added_and_removed(): void { From 4b36278375374b5c4ede93f90d1036850bfe8592 Mon Sep 17 00:00:00 2001 From: Seth Battis Date: Tue, 12 Aug 2025 20:12:06 -0400 Subject: [PATCH 6/8] Update README.md Co-authored-by: George Steel --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 70474c7..3d16db7 100644 --- a/README.md +++ b/README.md @@ -172,7 +172,7 @@ $setCookie = SetCookie::create('lu') ->withSecure(true) ->withHttpOnly(true) ->withSameSite(SameSite::lax()) - ->witHPartitioned() + ->withPartitioned() ; ``` From ce6d98d08c0c0e780fa1f30a3eccbd27ff095d3d Mon Sep 17 00:00:00 2001 From: Seth Battis Date: Wed, 13 Aug 2025 07:58:30 -0400 Subject: [PATCH 7/8] refactor: resolve CS issues --- src/Dflydev/FigCookies/SetCookie.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Dflydev/FigCookies/SetCookie.php b/src/Dflydev/FigCookies/SetCookie.php index b52bce2..2a13816 100644 --- a/src/Dflydev/FigCookies/SetCookie.php +++ b/src/Dflydev/FigCookies/SetCookie.php @@ -431,9 +431,9 @@ private function appendFormattedSameSitePartIfSet(array $cookieStringParts): arr } /** - * @param string[] $cookieStringParts - * - * @return string[] + * @param string[] $cookieStringParts + * + * @return string[] */ private function appendFormattedPartitionedPartIfSet(array $cookieStringParts): array { From 7d39ae55c8a7907bed25772635b7bc0d569a69bd Mon Sep 17 00:00:00 2001 From: Seth Battis Date: Wed, 13 Aug 2025 08:17:06 -0400 Subject: [PATCH 8/8] refactor: _really_ resolve CS issues --- tests/Dflydev/FigCookies/SetCookieTest.php | 4 ++-- tests/Dflydev/FigCookies/SetCookiesTest.php | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/Dflydev/FigCookies/SetCookieTest.php b/tests/Dflydev/FigCookies/SetCookieTest.php index a41457e..e19e1ea 100644 --- a/tests/Dflydev/FigCookies/SetCookieTest.php +++ b/tests/Dflydev/FigCookies/SetCookieTest.php @@ -153,8 +153,8 @@ public function provideParsesFromSetCookieStringData(): array ->withSecure(true) ->withHttpOnly(true) ->withSameSite(SameSite::lax()) - ->withPartitioned() - ] + ->withPartitioned(), + ], ]; } diff --git a/tests/Dflydev/FigCookies/SetCookiesTest.php b/tests/Dflydev/FigCookies/SetCookiesTest.php index d9febd5..01d802e 100644 --- a/tests/Dflydev/FigCookies/SetCookiesTest.php +++ b/tests/Dflydev/FigCookies/SetCookiesTest.php @@ -192,9 +192,9 @@ public function provideSetCookieStringsAndExpectedSetCookiesData(): array SetCookie::create('d', 'DDD'), SetCookie::create('e', 'EEE')->withPartitioned(), SetCookie::create('f', 'FFF'), - SetCookie::create('g', 'GGG')->withPartitioned() - ] - ] + SetCookie::create('g', 'GGG')->withPartitioned(), + ], + ], ]; }