Skip to content

Commit a0660f5

Browse files
committed
added SameSite enum; setCookie() and Session accept it, IResponse::SameSite* constants deprecated
1 parent 13f6149 commit a0660f5

7 files changed

Lines changed: 43 additions & 13 deletions

File tree

src/Bridges/HttpDI/SessionExtension.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
namespace Nette\Bridges\HttpDI;
99

1010
use Nette;
11-
use Nette\Http\IResponse;
11+
use Nette\Http\SameSite;
1212
use Nette\Schema\Expect;
1313

1414

@@ -42,7 +42,7 @@ public function getConfigSchema(): Nette\Schema\Schema
4242
'expiration' => Expect::string()->dynamic(),
4343
'handler' => Expect::string()->dynamic(),
4444
'readAndClose' => Expect::bool(),
45-
'cookieSamesite' => Expect::anyOf(IResponse::SameSiteLax, IResponse::SameSiteStrict, IResponse::SameSiteNone)
45+
'cookieSamesite' => Expect::anyOf(SameSite::Lax->value, SameSite::Strict->value, SameSite::None->value)
4646
->firstIsDefault(),
4747
])->otherItems();
4848
}

src/Http/Helpers.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ public static function ipMatch(string $ip, string $mask): bool
119119
public static function initCookie(IRequest $request, IResponse $response): void
120120
{
121121
if ($request->getHeader('Sec-Fetch-Site') === null) {
122-
$response->setCookie(self::StrictCookieName, '1', null, '/', sameSite: IResponse::SameSiteStrict);
122+
$response->setCookie(self::StrictCookieName, '1', null, '/', sameSite: SameSite::Strict);
123123
}
124124
}
125125
}

src/Http/IResponse.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ interface IResponse
138138
511 => 'Network Authentication Required',
139139
];
140140

141-
/** SameSite cookie */
141+
/** @deprecated use Nette\Http\SameSite enum */
142142
public const
143143
SameSiteLax = 'Lax',
144144
SameSiteStrict = 'Strict',

src/Http/Response.php

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ public function getHeaders(): array
225225

226226
/**
227227
* Sends a cookie.
228-
* @param self::SameSite*|null $sameSite
228+
* @param SameSite|self::SameSite*|null $sameSite
229229
* @throws Nette\InvalidStateException if HTTP headers have been sent
230230
*/
231231
public function setCookie(
@@ -236,11 +236,12 @@ public function setCookie(
236236
?string $domain = null,
237237
?bool $secure = null,
238238
?bool $httpOnly = null,
239-
?string $sameSite = null,
239+
SameSite|string|null $sameSite = null,
240240
bool $partitioned = false,
241241
): static
242242
{
243243
self::checkHeaders();
244+
$sameSite = $sameSite instanceof SameSite ? $sameSite->value : $sameSite;
244245
[$path, $domain] = [ // resolve the defaults first, so the final values (incl. those from cookiePath/cookieDomain) are validated
245246
$path ?? ($domain ? '/' : $this->cookiePath),
246247
$domain ?? ($path ? '' : $this->cookieDomain),
@@ -258,9 +259,9 @@ public function setCookie(
258259
}
259260

260261
$seconds = Helpers::expirationToSeconds($expire);
261-
$sameSite ??= self::SameSiteLax;
262+
$sameSite ??= SameSite::Lax->value;
262263
// both SameSite=None and Partitioned are rejected by the browser without the Secure attribute
263-
$secure = $sameSite === self::SameSiteNone || $partitioned
264+
$secure = $sameSite === SameSite::None->value || $partitioned
264265
? true
265266
: $secure ?? $this->cookieSecure;
266267
// the value is raw-url-encoded the same way PHP reads it back from $_COOKIE;

src/Http/Session.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ class Session
4040

4141
/** @var array<string, mixed> default configuration */
4242
private array $options = [
43-
'cookie_samesite' => IResponse::SameSiteLax,
43+
'cookie_samesite' => SameSite::Lax->value,
4444
'cookie_lifetime' => 0, // session cookie - kept by the browser per its own policy
4545
'gc_maxlifetime' => self::DefaultFileLifetime, // server-side idle timeout, independent of the cookie lifetime above
4646
];
@@ -509,14 +509,14 @@ public function setCookieParameters(
509509
string $path,
510510
?string $domain = null,
511511
?bool $secure = null,
512-
?string $sameSite = null,
512+
SameSite|string|null $sameSite = null,
513513
): static
514514
{
515515
return $this->setOptions([
516516
'cookie_path' => $path,
517517
'cookie_domain' => $domain,
518518
'cookie_secure' => $secure,
519-
'cookie_samesite' => $sameSite,
519+
'cookie_samesite' => $sameSite instanceof SameSite ? $sameSite->value : $sameSite,
520520
]);
521521
}
522522

src/Http/enums.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,20 @@ enum FetchDest: string
6060
case Worker = 'worker';
6161
case Xslt = 'xslt';
6262
}
63+
64+
65+
/**
66+
* Values of the SameSite cookie attribute, controlling whether the cookie is sent with
67+
* cross-site requests (a protection against CSRF).
68+
*/
69+
enum SameSite: string
70+
{
71+
/** sent with same-site requests and top-level cross-site navigation (the safe default) */
72+
case Lax = 'Lax';
73+
74+
/** sent only with same-site requests, never when coming from a foreign site */
75+
case Strict = 'Strict';
76+
77+
/** sent with all requests, including cross-site; requires the Secure attribute */
78+
case None = 'None';
79+
}

tests/Http/Response.setCookie.phpt

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,19 +138,31 @@ Assert::exception(
138138
// SameSite=None forces the Secure attribute (otherwise the browser rejects the cookie)
139139
$response = new Http\Response;
140140
$old = headers_list();
141-
$response->setCookie('test', 'value', null, sameSite: Http\IResponse::SameSiteNone);
141+
$response->setCookie('test', 'value', null, sameSite: Http\SameSite::None);
142142
$headers = array_values(array_diff(headers_list(), $old, ['Set-Cookie:']));
143143
Assert::same(['Set-Cookie: test=value; path=/; secure; HttpOnly; SameSite=None'], $headers);
144144

145145

146146
// Partitioned (CHIPS) adds the attribute and forces Secure
147147
$response = new Http\Response;
148148
$old = headers_list();
149-
$response->setCookie('test', 'value', null, sameSite: Http\IResponse::SameSiteNone, partitioned: true);
149+
$response->setCookie('test', 'value', null, sameSite: Http\SameSite::None, partitioned: true);
150150
$headers = array_values(array_diff(headers_list(), $old, ['Set-Cookie:']));
151151
Assert::same(['Set-Cookie: test=value; path=/; secure; HttpOnly; SameSite=None; Partitioned'], $headers);
152152

153153

154+
// the SameSite enum and the equivalent string produce the same header
155+
$response = new Http\Response;
156+
$old = headers_list();
157+
$response->setCookie('test', 'a', null, sameSite: Http\SameSite::Strict);
158+
$response->setCookie('test', 'b', null, sameSite: 'Strict');
159+
$headers = array_values(array_diff(headers_list(), $old, ['Set-Cookie:']));
160+
Assert::same([
161+
'Set-Cookie: test=a; path=/; HttpOnly; SameSite=Strict',
162+
'Set-Cookie: test=b; path=/; HttpOnly; SameSite=Strict',
163+
], $headers);
164+
165+
154166
// integer 0 is deprecated, but kept as a session cookie for BC
155167
$response = new Http\Response;
156168
$old = headers_list();

0 commit comments

Comments
 (0)