Skip to content

Commit 917c6c4

Browse files
committed
added Request::isFrom()
1 parent 0ac1704 commit 917c6c4

File tree

3 files changed

+121
-1
lines changed

3 files changed

+121
-1
lines changed

src/Http/IRequest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
* HTTP request provides access scheme for request sent via HTTP.
1515
* @method UrlImmutable|null getReferer() Returns referrer.
1616
* @method bool isSameSite() Is the request sent from the same origin?
17+
* @method bool isFrom(string|array|null $site = null, string|array|null $initiator = null)
1718
*/
1819
interface IRequest
1920
{

src/Http/Request.php

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
namespace Nette\Http;
1111

1212
use Nette;
13-
use function array_change_key_case, base64_decode, count, explode, func_num_args, gethostbyaddr, implode, preg_match, preg_match_all, rsort, strcasecmp, strtolower, strtr;
13+
use function array_change_key_case, base64_decode, count, explode, func_num_args, gethostbyaddr, implode, in_array, preg_match, preg_match_all, rsort, strcasecmp, strtr;
1414
use const CASE_LOWER;
1515

1616

@@ -230,6 +230,27 @@ public function isSameSite(): bool
230230
}
231231

232232

233+
/**
234+
* Checks whether Sec-Fetch headers match the expected values.
235+
*/
236+
public function isFrom(string|array|null $site = null, string|array|null $initiator = null): bool
237+
{
238+
$actualSite = $this->headers['sec-fetch-site'] ?? null;
239+
$actualDest = $this->headers['sec-fetch-dest'] ?? null;
240+
241+
if ($actualSite === null && ($origin = $this->getOrigin())) { // fallback for Safari < 16.4
242+
$actualSite = strcasecmp($origin->getScheme(), $this->url->getScheme()) === 0
243+
&& strcasecmp(rtrim($origin->getHost(), '.'), rtrim($this->url->getHost(), '.')) === 0
244+
&& $origin->getPort() === $this->url->getPort()
245+
? 'same-origin'
246+
: 'cross-site';
247+
}
248+
249+
return ($site === null || ($actualSite !== null && in_array($actualSite, (array) $site, strict: true)))
250+
&& ($initiator === null || ($actualDest !== null && in_array($actualDest, (array) $initiator, strict: true)));
251+
}
252+
253+
233254
/**
234255
* Is it an AJAX request?
235256
*/

tests/Http/Request.isFrom.phpt

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Nette\Http;
6+
use Tester\Assert;
7+
8+
require __DIR__ . '/../bootstrap.php';
9+
10+
11+
test('matches both headers', function () {
12+
$request = new Http\Request(new Http\UrlScript, headers: [
13+
'Sec-Fetch-Site' => 'same-origin',
14+
'Sec-Fetch-Dest' => 'document',
15+
]);
16+
17+
Assert::true($request->isFrom('same-origin', 'document'));
18+
});
19+
20+
21+
test('fails when expected header missing', function () {
22+
$request = new Http\Request(new Http\UrlScript, headers: [
23+
'Sec-Fetch-Site' => 'same-origin',
24+
]);
25+
26+
Assert::false($request->isFrom('same-origin', 'document'));
27+
});
28+
29+
30+
test('accepts multiple expected values', function () {
31+
$request = new Http\Request(new Http\UrlScript, headers: [
32+
'Sec-Fetch-Site' => 'cross-site',
33+
'Sec-Fetch-Dest' => 'image',
34+
]);
35+
36+
Assert::true($request->isFrom(['same-origin', 'cross-site'], ['document', 'image']));
37+
Assert::false($request->isFrom(['cross-site'], ['Document']));
38+
Assert::false($request->isFrom(['Cross-Site'], ['image']));
39+
});
40+
41+
42+
test('fallback same-origin from Origin header', function () {
43+
$url = new Http\UrlScript('https://nette.org/app/');
44+
$request = new Http\Request($url, headers: [
45+
'Origin' => 'https://nette.org',
46+
]);
47+
48+
Assert::true($request->isFrom('same-origin'));
49+
});
50+
51+
52+
test('fallback cross-site from Origin header', function () {
53+
$url = new Http\UrlScript('https://nette.org/');
54+
$request = new Http\Request($url, headers: [
55+
'Origin' => 'https://example.com',
56+
]);
57+
58+
Assert::true($request->isFrom('cross-site'));
59+
});
60+
61+
62+
test('fallback missing without Origin header', function () {
63+
$url = new Http\UrlScript('https://nette.org/');
64+
$request = new Http\Request($url);
65+
66+
Assert::false($request->isFrom('same-origin'));
67+
});
68+
69+
70+
test('fallback not used when header present', function () {
71+
$url = new Http\UrlScript('https://nette.org/');
72+
$request = new Http\Request($url, headers: [
73+
'Sec-Fetch-Site' => 'none',
74+
'Origin' => 'https://nette.org',
75+
]);
76+
77+
Assert::false($request->isFrom('same-origin'));
78+
});
79+
80+
81+
test('fallback cross-site when port differs', function () {
82+
$url = new Http\UrlScript('https://nette.org:443');
83+
$request = new Http\Request($url, headers: [
84+
'Origin' => 'https://nette.org:444',
85+
]);
86+
87+
Assert::true($request->isFrom('cross-site'));
88+
});
89+
90+
91+
test('fallback ignored for invalid Origin', function () {
92+
$url = new Http\UrlScript('https://nette.org/');
93+
$request = new Http\Request($url, headers: [
94+
'Origin' => 'null',
95+
]);
96+
97+
Assert::false($request->isFrom('same-origin'));
98+
});

0 commit comments

Comments
 (0)