• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

nette / http / 26866033208

03 Jun 2026 05:39AM UTC coverage: 84.056% (+0.2%) from 83.841%
26866033208

push

github

dg
Request::isSameSite() uses isFrom() and is deprecated; strict cookie sent only without Sec-Fetch-Site

3 of 3 new or added lines in 2 files covered. (100.0%)

3 existing lines in 1 file now uncovered.

1086 of 1292 relevant lines covered (84.06%)

0.84 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.25
/src/Http/Request.php
1
<?php declare(strict_types=1);
2

3
/**
4
 * This file is part of the Nette Framework (https://nette.org)
5
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
6
 */
7

8
namespace Nette\Http;
9

10
use Nette;
11
use function array_change_key_case, array_filter, base64_decode, count, explode, func_num_args, in_array, is_array, preg_match, strcasecmp, strlen, strtr;
12

13

14
/**
15
 * Immutable representation of an HTTP request with access to URL, headers, cookies, uploaded files, and body.
16
 *
17
 * @property-read UrlScript $url
18
 * @property-read array<string,mixed> $query
19
 * @property-read array<string,mixed> $post
20
 * @property-read array<string,mixed> $files
21
 * @property-read array<string,string> $cookies
22
 * @property-read string $method
23
 * @property-read array<string,string> $headers
24
 * @property-read ?UrlImmutable $referer
25
 * @property-read bool $secured
26
 * @property-read bool $ajax
27
 * @property-read ?string $remoteAddress
28
 * @property-deprecated ?string $remoteHost
29
 * @property-read ?string $rawBody
30
 */
31
class Request implements IRequest
32
{
33
        use Nette\SmartObject;
34

35
        /** @var array<string, string> */
36
        private readonly array $headers;
37

38
        /** @var (\Closure(): string)|null */
39
        private readonly ?\Closure $rawBodyCallback;
40

41

42
        /**
43
         * @param array<string, string> $headers
44
         * @param ?(callable(): string) $rawBodyCallback
45
         */
46
        public function __construct(
1✔
47
                private UrlScript $url,
48
                /** @var mixed[] */
49
                private readonly array $post = [],
50
                /** @var mixed[] */
51
                private readonly array $files = [],
52
                /** @var array<string, string> */
53
                private readonly array $cookies = [],
54
                array $headers = [],
55
                private readonly string $method = 'GET',
56
                private readonly ?string $remoteAddress = null,
57
                ?string $remoteHost = null,
58
                ?callable $rawBodyCallback = null,
59
        ) {
60
                $this->headers = array_change_key_case($headers);
1✔
61
                $this->rawBodyCallback = $rawBodyCallback ? $rawBodyCallback(...) : null;
1✔
62
        }
1✔
63

64

65
        /**
66
         * Returns a clone with a different URL.
67
         */
68
        public function withUrl(UrlScript $url): static
1✔
69
        {
70
                $dolly = clone $this;
1✔
71
                $dolly->url = $url;
1✔
72
                return $dolly;
1✔
73
        }
74

75

76
        /**
77
         * Returns the URL of the request.
78
         */
79
        public function getUrl(): UrlScript
80
        {
81
                return $this->url;
1✔
82
        }
83

84

85
        /********************* query, post, files & cookies ****************d*g**/
86

87

88
        /**
89
         * Returns a URL query parameter, or all parameters as an array if no key is given.
90
         */
91
        public function getQuery(?string $key = null): mixed
1✔
92
        {
93
                if (func_num_args() === 0) {
1✔
94
                        return $this->url->getQueryParameters();
1✔
95
                }
96

97
                assert($key !== null);
98
                return $this->url->getQueryParameter($key);
1✔
99
        }
100

101

102
        /**
103
         * Returns a POST parameter, or all POST parameters as an array if no key is given.
104
         */
105
        public function getPost(?string $key = null): mixed
1✔
106
        {
107
                if (func_num_args() === 0) {
1✔
108
                        return $this->post;
1✔
109
                }
110

111
                assert($key !== null);
112
                return $this->post[$key] ?? null;
1✔
113
        }
114

115

116
        /**
117
         * Returns the uploaded file for the given key, or null if not present.
118
         * Accepts a string key or an array of keys for nested file structures (e.g. ['form', 'avatar']).
119
         * @param  string|string[]  $key
120
         */
121
        public function getFile($key): ?FileUpload
122
        {
123
                $res = Nette\Utils\Arrays::get($this->files, $key, null);
1✔
124
                return $res instanceof FileUpload ? $res : null;
1✔
125
        }
126

127

128
        /**
129
         * Returns the tree of uploaded files, with each leaf being a FileUpload instance.
130
         * @return mixed[]
131
         */
132
        public function getFiles(): array
133
        {
134
                return $this->files;
1✔
135
        }
136

137

138
        /**
139
         * Returns a cookie or `null` if it does not exist.
140
         */
141
        public function getCookie(string $key): ?string
1✔
142
        {
143
                return $this->cookies[$key] ?? null;
1✔
144
        }
145

146

147
        /**
148
         * Returns all cookies.
149
         * @return array<string, string>
150
         */
151
        public function getCookies(): array
152
        {
153
                return $this->cookies;
1✔
154
        }
155

156

157
        /********************* method & headers ****************d*g**/
158

159

160
        /**
161
         * Returns the HTTP method with which the request was made (GET, POST, HEAD, PUT, ...).
162
         */
163
        public function getMethod(): string
164
        {
165
                return $this->method;
1✔
166
        }
167

168

169
        /**
170
         * Checks the HTTP method with which the request was made. The parameter is case-insensitive.
171
         */
172
        public function isMethod(string $method): bool
173
        {
174
                return strcasecmp($this->method, $method) === 0;
×
175
        }
176

177

178
        /**
179
         * Returns an HTTP header or `null` if it does not exist. The parameter is case-insensitive.
180
         */
181
        public function getHeader(string $header): ?string
1✔
182
        {
183
                $header = strtolower($header);
1✔
184
                return $this->headers[$header] ?? null;
1✔
185
        }
186

187

188
        /**
189
         * Returns all HTTP headers as associative array.
190
         * @return array<string, string>
191
         */
192
        public function getHeaders(): array
193
        {
194
                return $this->headers;
1✔
195
        }
196

197

198
        /**
199
         * Returns the referrer URL from the Referer header. Unreliable - clients may omit or spoof it.
200
         * @deprecated  use getOrigin()
201
         */
202
        public function getReferer(): ?UrlImmutable
203
        {
204
                return isset($this->headers['referer'])
×
205
                        ? new UrlImmutable($this->headers['referer'])
×
206
                        : null;
×
207
        }
208

209

210
        /**
211
         * Returns the request origin (scheme + host + port) from the Origin header, or null if absent or invalid.
212
         */
213
        public function getOrigin(): ?UrlImmutable
214
        {
215
                $header = $this->headers['origin'] ?? '';
1✔
216
                if (!preg_match('~^[a-z][a-z0-9+.-]*://[^/]+$~i', $header)) {
1✔
217
                        return null;
1✔
218
                }
219
                return new UrlImmutable($header);
1✔
220
        }
221

222

223
        /**
224
         * Checks whether the request was sent via a secure channel (HTTPS).
225
         */
226
        public function isSecured(): bool
227
        {
228
                return $this->url->getScheme() === 'https';
1✔
229
        }
230

231

232
        /**
233
         * Checks whether the request originated from your own site (same-site), i.e. it was not
234
         * triggered from a foreign website. Serves as a CSRF-like protection for forms and signals.
235
         * @deprecated use isFrom()
236
         */
237
        public function isSameSite(): bool
238
        {
239
                return $this->isFrom([FetchSite::SameSite, FetchSite::SameOrigin]);
1✔
240
        }
241

242

243
        /**
244
         * Checks whether the request matches the given Sec-Fetch-Site, Sec-Fetch-Dest and Sec-Fetch-User values.
245
         * Falls back to the SameSite=Strict cookie in browsers without Sec-Fetch (Safari < 16.4)
246
         * @param  FetchSite|list<FetchSite>  $site
247
         * @param  FetchDest|list<FetchDest>|null  $dest
248
         */
249
        public function isFrom(
1✔
250
                FetchSite|array $site,
251
                FetchDest|array|null $dest = null,
252
                ?bool $user = null,
253
        ): bool
254
        {
255
                $siteHeader = $this->headers['sec-fetch-site'] ?? null;
1✔
256
                $actualDest = FetchDest::tryFrom($this->headers['sec-fetch-dest'] ?? '');
1✔
257
                $actualUser = ($this->headers['sec-fetch-user'] ?? null) === '?1';
1✔
258
                $site = is_array($site) ? $site : [$site];
1✔
259
                $dest = $dest === null || is_array($dest) ? $dest : [$dest];
1✔
260

261
                if ($siteHeader === null) { // fallback for browsers without Sec-Fetch (Safari < 16.4)
1✔
262
                        return $dest === null
1✔
263
                                && $user === null
1✔
264
                                && isset($this->cookies[Helpers::StrictCookieName])
1✔
265
                                && array_filter($site, fn(FetchSite $s) => $s !== FetchSite::CrossSite) !== [];
1✔
266
                }
267

268
                $actualSite = FetchSite::tryFrom($siteHeader);
1✔
269
                return $actualSite !== null
1✔
270
                        && in_array($actualSite, $site, strict: true)
1✔
271
                        && ($dest === null || ($actualDest !== null && in_array($actualDest, $dest, strict: true)))
1✔
272
                        && ($user === null || $user === $actualUser);
1✔
273
        }
274

275

276
        /**
277
         * Checks whether the request was made via AJAX (X-Requested-With: XMLHttpRequest).
278
         */
279
        public function isAjax(): bool
280
        {
UNCOV
281
                return $this->getHeader('X-Requested-With') === 'XMLHttpRequest';
×
282
        }
283

284

285
        /**
286
         * Returns the IP address of the remote client.
287
         */
288
        public function getRemoteAddress(): ?string
289
        {
290
                return $this->remoteAddress;
1✔
291
        }
292

293

294
        #[\Deprecated]
295
        public function getRemoteHost(): ?string
296
        {
UNCOV
297
                trigger_error(__METHOD__ . '() is deprecated.', E_USER_DEPRECATED);
×
UNCOV
298
                return null;
×
299
        }
300

301

302
        /**
303
         * Returns raw content of HTTP request body.
304
         */
305
        public function getRawBody(): ?string
306
        {
307
                return $this->rawBodyCallback ? ($this->rawBodyCallback)() : null;
1✔
308
        }
309

310

311
        /**
312
         * Returns basic HTTP authentication credentials.
313
         * @return array{string, string}|null
314
         */
315
        public function getBasicCredentials(): ?array
316
        {
317
                return preg_match(
1✔
318
                        '~^Basic (\S+)$~',
1✔
319
                        $this->headers['authorization'] ?? '',
1✔
320
                        $t,
1✔
321
                )
322
                        && ($t = base64_decode($t[1], strict: true))
1✔
323
                        && ($t = explode(':', $t, 2))
1✔
324
                        && (count($t) === 2)
1✔
325
                        ? $t
1✔
326
                        : null;
1✔
327
        }
328

329

330
        /**
331
         * Returns the most preferred language from the Accept-Language header that matches one of the supported languages,
332
         * or null if no match is found.
333
         * @param  array<string>  $langs  supported language codes (e.g. ['en', 'cs', 'de'])
334
         */
335
        public function detectLanguage(array $langs): ?string
1✔
336
        {
337
                $header = $this->getHeader('Accept-Language');
1✔
338
                if (!$header) {
1✔
339
                        return null;
1✔
340
                }
341

342
                usort($langs, fn($a, $b) => strlen($b) <=> strlen($a)); // more specific first
1✔
343
                $accepted = Helpers::parseQualityList(strtr($header, '_', '-')); // cs_CZ means cs-CZ
1✔
344

345
                foreach (array_keys($accepted) as $token) {
1✔
346
                        foreach ($langs as $lang) {
1✔
347
                                $l = strtolower($lang);
1✔
348
                                if ($token === '*' || $token === $l || str_starts_with($token, $l . '-')) {
1✔
349
                                        return $lang;
1✔
350
                                }
351
                        }
352
                }
353

354
                return null;
1✔
355
        }
356
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc