Skip to content

Commit 565afcb

Browse files
committed
added Request::isFrom()
1 parent 6fab16b commit 565afcb

File tree

3 files changed

+135
-1
lines changed

3 files changed

+135
-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 $dest = null)
1718
*/
1819
interface IRequest
1920
{

src/Http/Request.php

Lines changed: 36 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, is_array, preg_match, preg_match_all, rsort, strcasecmp, strtr;
1414
use const CASE_LOWER;
1515

1616

@@ -230,6 +230,25 @@ 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 $dest = null): bool
237+
{
238+
$actualDest = $this->headers['sec-fetch-dest'] ?? null;
239+
$actualSite = $this->headers['sec-fetch-site'] ?? null;
240+
if ($actualSite === null && ($origin = $this->getOrigin())) {
241+
$actualSite = strcasecmp($origin->getScheme(), $this->url->getScheme()) === 0
242+
&& strcasecmp(rtrim($origin->getHost(), '.'), rtrim($this->url->getHost(), '.')) === 0
243+
&& $origin->getPort() === $this->url->getPort()
244+
? 'same-origin'
245+
: 'cross-site';
246+
}
247+
return ($dest === null || ($actualDest !== null && in_array($actualDest, (array) $dest, strict: true)))
248+
&& ($site === null || ($actualSite !== null && in_array($actualSite, (array) $site, strict: true)));
249+
}
250+
251+
233252
/**
234253
* Is it an AJAX request?
235254
*/
@@ -321,4 +340,20 @@ public function detectLanguage(array $langs): ?string
321340

322341
return $lang;
323342
}
343+
344+
345+
private function getSecFetchSiteFallback(): ?string
346+
{
347+
$origin = $this->getOrigin();
348+
if (!$origin) {
349+
return null;
350+
}
351+
352+
$url = $this->url;
353+
return strcasecmp($origin->getScheme(), $url->getScheme()) === 0
354+
&& strcasecmp($origin->getHost(), $url->getHost()) === 0
355+
&& $origin->getPort() === $url->getPort()
356+
? 'same-origin'
357+
: 'cross-site';
358+
}
324359
}

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)