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

nette / http / 21830508055

09 Feb 2026 03:10PM UTC coverage: 83.772% (+0.03%) from 83.744%
21830508055

push

github

dg
added RequestFactory::setForceHttps()

8 of 9 new or added lines in 2 files covered. (88.89%)

75 existing lines in 9 files now uncovered.

924 of 1103 relevant lines covered (83.77%)

0.84 hits per line

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

96.26
/src/Http/RequestFactory.php
1
<?php
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
declare(strict_types=1);
9

10
namespace Nette\Http;
11

12
use Nette;
13
use Nette\Utils\Arrays;
14
use Nette\Utils\Strings;
15
use function array_filter, base64_encode, count, end, explode, file_get_contents, filter_input_array, filter_var, function_exists, get_debug_type, in_array, ini_get, is_array, is_string, key, min, preg_last_error, preg_match, preg_replace, preg_split, rtrim, sprintf, str_contains, strcasecmp, strlen, strncmp, strpos, strrpos, strtolower, strtr, substr, trim;
16
use const FILTER_UNSAFE_RAW, FILTER_VALIDATE_IP, INPUT_COOKIE, INPUT_POST, PHP_SAPI, UPLOAD_ERR_NO_FILE;
17

18

19
/**
20
 * HTTP request factory.
21
 */
22
class RequestFactory
23
{
24
        /** @internal */
25
        private const ValidChars = '\x09\x0A\x0D\x20-\x7E\xA0-\x{10FFFF}';
26

27
        /** @var array<string, array<string, string>> */
28
        public array $urlFilters = [
29
                'path' => ['#//#' => '/'], // '%20' => ''
30
                'url' => [], // '#[.,)]$#D' => ''
31
        ];
32

33
        private bool $binary = false;
34

35
        /** @var list<string> */
36
        private array $proxies = [];
37

38
        private bool $forceHttps = false;
39

40

41
        public function setBinary(bool $binary = true): static
1✔
42
        {
43
                $this->binary = $binary;
1✔
44
                return $this;
1✔
45
        }
46

47

48
        /**
49
         * @param string|list<string>  $proxy
50
         */
51
        public function setProxy($proxy): static
52
        {
53
                $this->proxies = (array) $proxy;
1✔
54
                return $this;
1✔
55
        }
56

57

58
        public function setForceHttps(bool $forceHttps = true): static
1✔
59
        {
60
                $this->forceHttps = $forceHttps;
1✔
61
                return $this;
1✔
62
        }
63

64

65
        /**
66
         * Returns new Request instance, using values from superglobals.
67
         */
68
        public function fromGlobals(): Request
69
        {
70
                $url = new Url;
1✔
71
                $this->getServer($url);
1✔
72
                $this->getPathAndQuery($url);
1✔
73
                [$post, $cookies] = $this->getGetPostCookie($url);
1✔
74
                [$remoteAddr, $remoteHost] = $this->getClient($url);
1✔
75

76
                if ($this->forceHttps) {
1✔
77
                        $url->setScheme('https');
1✔
78
                }
79

80
                return new Request(
1✔
81
                        new UrlScript($url, $this->getScriptPath($url)),
1✔
82
                        $post,
83
                        $this->getFiles(),
1✔
84
                        $cookies,
85
                        $this->getHeaders(),
1✔
86
                        $this->getMethod(),
1✔
87
                        $remoteAddr,
88
                        $remoteHost,
89
                        fn(): string => file_get_contents('php://input'),
1✔
90
                );
91
        }
92

93

94
        private function getServer(Url $url): void
1✔
95
        {
96
                $url->setScheme(!empty($_SERVER['HTTPS']) && strcasecmp($_SERVER['HTTPS'], 'off') ? 'https' : 'http');
1✔
97

98
                if (
99
                        (isset($_SERVER[$tmp = 'HTTP_HOST']) || isset($_SERVER[$tmp = 'SERVER_NAME']))
1✔
100
                        && ($pair = $this->parseHostAndPort($_SERVER[$tmp]))
1✔
101
                ) {
102
                        $url->setHost($pair[0]);
1✔
103
                        if (isset($pair[1])) {
1✔
104
                                $url->setPort($pair[1]);
1✔
105
                        } elseif ($tmp === 'SERVER_NAME' && isset($_SERVER['SERVER_PORT'])) {
1✔
106
                                $url->setPort((int) $_SERVER['SERVER_PORT']);
1✔
107
                        }
108
                }
109
        }
1✔
110

111

112
        private function getPathAndQuery(Url $url): void
1✔
113
        {
114
                $requestUrl = $_SERVER['REQUEST_URI'] ?? '/';
1✔
115
                $requestUrl = preg_replace('#^\w++://[^/]++#', '', $requestUrl);
1✔
116
                $requestUrl = Strings::replace($requestUrl, $this->urlFilters['url']);
1✔
117

118
                $tmp = explode('?', $requestUrl, 2);
1✔
119
                $path = Url::unescape($tmp[0], '%/?#');
1✔
120
                $path = Strings::fixEncoding(Strings::replace($path, $this->urlFilters['path']));
1✔
121
                $url->setPath($path);
1✔
122
                $url->setQuery($tmp[1] ?? '');
1✔
123
        }
1✔
124

125

126
        private function getScriptPath(Url $url): string
1✔
127
        {
128
                if (PHP_SAPI === 'cli-server') {
1✔
129
                        return '/';
×
130
                }
131

132
                $path = $url->getPath();
1✔
133
                $lpath = strtolower($path);
1✔
134
                $script = strtolower($_SERVER['SCRIPT_NAME'] ?? '');
1✔
135
                if ($lpath !== $script) {
1✔
136
                        $max = min(strlen($lpath), strlen($script));
1✔
137
                        for ($i = 0; $i < $max && $lpath[$i] === $script[$i]; $i++);
1✔
138
                        $path = $i
1✔
139
                                ? substr($path, 0, strrpos($path, '/', $i - strlen($path) - 1) + 1)
1✔
140
                                : '/';
1✔
141
                }
142

143
                return $path;
1✔
144
        }
145

146

147
        /** @return array{array<string, mixed>, array<string, mixed>} */
148
        private function getGetPostCookie(Url $url): array
1✔
149
        {
150
                $useFilter = (!in_array((string) ini_get('filter.default'), ['', 'unsafe_raw'], strict: true) || ini_get('filter.default_flags'));
1✔
151

152
                $query = $url->getQueryParameters();
1✔
153
                $post = $useFilter
1✔
UNCOV
154
                        ? filter_input_array(INPUT_POST, FILTER_UNSAFE_RAW)
×
155
                        : (empty($_POST) ? [] : $_POST);
1✔
156
                $cookies = $useFilter
1✔
UNCOV
157
                        ? filter_input_array(INPUT_COOKIE, FILTER_UNSAFE_RAW)
×
158
                        : (empty($_COOKIE) ? [] : $_COOKIE);
1✔
159

160
                // remove invalid characters
161
                $reChars = '#^[' . self::ValidChars . ']*+$#Du';
1✔
162
                if (!$this->binary) {
1✔
163
                        $list = [&$query, &$post, &$cookies];
1✔
164
                        foreach ($list as $key => &$val) {
1✔
165
                                foreach ($val as $k => $v) {
1✔
166
                                        if (is_string($k) && (!preg_match($reChars, $k) || preg_last_error())) {
1✔
167
                                                unset($list[$key][$k]);
1✔
168

169
                                        } elseif (is_array($v)) {
1✔
170
                                                $list[$key][$k] = $v;
1✔
171
                                                $list[] = &$list[$key][$k];
1✔
172

173
                                        } elseif (is_string($v)) {
1✔
174
                                                $list[$key][$k] = (string) preg_replace('#[^' . self::ValidChars . ']+#u', '', $v);
1✔
175

176
                                        } else {
177
                                                throw new Nette\InvalidStateException(sprintf('Invalid value in $_POST/$_COOKIE in key %s, expected string, %s given.', "'$k'", get_debug_type($v)));
1✔
178
                                        }
179
                                }
180
                        }
181

182
                        unset($list, $key, $val, $k, $v);
1✔
183
                }
184

185
                $url->setQuery($query);
1✔
186
                return [$post, $cookies];
1✔
187
        }
188

189

190
        /** @return array<string, FileUpload|array<string, FileUpload|array>> */
191
        private function getFiles(): array
192
        {
193
                $reChars = '#^[' . self::ValidChars . ']*+$#Du';
1✔
194
                $files = [];
1✔
195
                $list = [];
1✔
196
                foreach ($_FILES ?? [] as $k => $v) {
1✔
197
                        if (
198
                                !is_array($v)
1✔
199
                                || !isset($v['name'], $v['type'], $v['size'], $v['tmp_name'], $v['error'])
1✔
200
                                || (!$this->binary && is_string($k) && (!preg_match($reChars, $k) || preg_last_error()))
1✔
201
                        ) {
202
                                continue;
1✔
203
                        }
204

205
                        $v['@'] = &$files[$k];
1✔
206
                        $list[] = $v;
1✔
207
                }
208

209
                // create FileUpload objects
210
                foreach ($list as &$v) {
1✔
211
                        if (!isset($v['name'])) {
1✔
UNCOV
212
                                continue;
×
213

214
                        } elseif (!is_array($v['name'])) {
1✔
215
                                if (!$this->binary && (!preg_match($reChars, $v['name']) || preg_last_error())) {
1✔
216
                                        $v['name'] = '';
1✔
217
                                }
218

219
                                if ($v['error'] !== UPLOAD_ERR_NO_FILE) {
1✔
220
                                        $v['@'] = new FileUpload($v);
1✔
221
                                }
222

223
                                continue;
1✔
224
                        }
225

226
                        foreach ($v['name'] as $k => $foo) {
1✔
227
                                if (!$this->binary && is_string($k) && (!preg_match($reChars, $k) || preg_last_error())) {
1✔
UNCOV
228
                                        continue;
×
229
                                }
230

231
                                $list[] = [
1✔
232
                                        'name' => $v['name'][$k],
1✔
233
                                        'type' => $v['type'][$k],
1✔
234
                                        'size' => $v['size'][$k],
1✔
235
                                        'full_path' => $v['full_path'][$k] ?? null,
1✔
236
                                        'tmp_name' => $v['tmp_name'][$k],
1✔
237
                                        'error' => $v['error'][$k],
1✔
238
                                        '@' => &$v['@'][$k],
1✔
239
                                ];
240
                        }
241
                }
242

243
                return $files;
1✔
244
        }
245

246

247
        /** @return array<string, string> */
248
        private function getHeaders(): array
249
        {
250
                if (function_exists('apache_request_headers')) {
1✔
UNCOV
251
                        $headers = apache_request_headers();
×
252
                } else {
253
                        $headers = [];
1✔
254
                        foreach ($_SERVER as $k => $v) {
1✔
255
                                if (str_starts_with($k, 'HTTP_')) {
1✔
256
                                        $k = substr($k, 5);
1✔
257
                                } elseif (strncmp($k, 'CONTENT_', 8)) {
1✔
258
                                        continue;
1✔
259
                                }
260

261
                                $headers[strtr($k, '_', '-')] = $v;
1✔
262
                        }
263
                }
264

265
                if (!isset($headers['Authorization'])) {
1✔
266
                        if (isset($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW'])) {
1✔
267
                                $headers['Authorization'] = 'Basic ' . base64_encode($_SERVER['PHP_AUTH_USER'] . ':' . $_SERVER['PHP_AUTH_PW']);
1✔
268
                        } elseif (isset($_SERVER['PHP_AUTH_DIGEST'])) {
1✔
269
                                $headers['Authorization'] = 'Digest ' . $_SERVER['PHP_AUTH_DIGEST'];
1✔
270
                        }
271
                }
272

273
                return $headers;
1✔
274
        }
275

276

277
        private function getMethod(): string
278
        {
279
                $method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
1✔
280
                if (
281
                        $method === 'POST'
1✔
282
                        && preg_match('#^[A-Z]+$#D', $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] ?? '')
1✔
283
                ) {
284
                        $method = $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'];
1✔
285
                }
286

287
                return $method;
1✔
288
        }
289

290

291
        /** @return array{?string, ?string}  [remoteAddr, remoteHost] */
292
        private function getClient(Url $url): array
1✔
293
        {
294
                $remoteAddr = !empty($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : null;
1✔
295

296
                // use real client address and host if trusted proxy is used
297
                $usingTrustedProxy = $remoteAddr && Arrays::some($this->proxies, fn(string $proxy): bool => Helpers::ipMatch($remoteAddr, $proxy));
1✔
298
                if ($usingTrustedProxy) {
1✔
299
                        $remoteHost = null;
1✔
300
                        $remoteAddr = empty($_SERVER['HTTP_FORWARDED'])
1✔
301
                                ? $this->useNonstandardProxy($url)
1✔
302
                                : $this->useForwardedProxy($url);
1✔
303

304
                } else {
305
                        $remoteHost = !empty($_SERVER['REMOTE_HOST']) ? $_SERVER['REMOTE_HOST'] : null;
1✔
306
                }
307

308
                return [$remoteAddr, $remoteHost];
1✔
309
        }
310

311

312
        private function useForwardedProxy(Url $url): ?string
1✔
313
        {
314
                $forwardParams = preg_split('/[,;]/', $_SERVER['HTTP_FORWARDED']);
1✔
315
                foreach ($forwardParams as $forwardParam) {
1✔
316
                        [$key, $value] = explode('=', $forwardParam, 2) + [1 => ''];
1✔
317
                        $proxyParams[strtolower(trim($key))][] = trim($value, " \t\"");
1✔
318
                }
319

320
                if (isset($proxyParams['for'])) {
1✔
321
                        $address = $proxyParams['for'][0];
1✔
322
                        $remoteAddr = str_contains($address, '[')
1✔
323
                                ? substr($address, 1, strpos($address, ']') - 1) // IPv6
1✔
324
                                : explode(':', $address)[0];  // IPv4
1✔
325
                }
326

327
                if (isset($proxyParams['proto']) && count($proxyParams['proto']) === 1) {
1✔
328
                        $url->setScheme(strcasecmp($proxyParams['proto'][0], 'https') === 0 ? 'https' : 'http');
1✔
329
                        $url->setPort($url->getScheme() === 'https' ? 443 : 80);
1✔
330
                }
331

332
                if (
333
                        isset($proxyParams['host']) && count($proxyParams['host']) === 1
1✔
334
                        && ($pair = $this->parseHostAndPort($proxyParams['host'][0]))
1✔
335
                ) {
336
                        $url->setHost($pair[0]);
1✔
337
                        if (isset($pair[1])) {
1✔
338
                                $url->setPort($pair[1]);
1✔
339
                        }
340
                }
341
                return $remoteAddr ?? null;
1✔
342
        }
343

344

345
        private function useNonstandardProxy(Url $url): ?string
1✔
346
        {
347
                if (!empty($_SERVER['HTTP_X_FORWARDED_PROTO'])) {
1✔
348
                        $url->setScheme(strcasecmp($_SERVER['HTTP_X_FORWARDED_PROTO'], 'https') === 0 ? 'https' : 'http');
1✔
349
                        $url->setPort($url->getScheme() === 'https' ? 443 : 80);
1✔
350
                }
351

352
                if (!empty($_SERVER['HTTP_X_FORWARDED_PORT'])) {
1✔
353
                        $url->setPort((int) $_SERVER['HTTP_X_FORWARDED_PORT']);
1✔
354
                }
355

356
                if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
1✔
357
                        $xForwardedForWithoutProxies = array_filter(
1✔
358
                                explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']),
1✔
359
                                fn(string $ip): bool => filter_var($ip = trim($ip), FILTER_VALIDATE_IP) === false
1✔
360
                                        || !Arrays::some($this->proxies, fn(string $proxy): bool => Helpers::ipMatch($ip, $proxy)),
1✔
361
                        );
362
                        if ($xForwardedForWithoutProxies) {
1✔
363
                                $remoteAddr = trim(end($xForwardedForWithoutProxies));
1✔
364
                                $xForwardedForRealIpKey = key($xForwardedForWithoutProxies);
1✔
365
                        }
366
                }
367

368
                if (isset($xForwardedForRealIpKey) && !empty($_SERVER['HTTP_X_FORWARDED_HOST'])) {
1✔
369
                        $xForwardedHost = explode(',', $_SERVER['HTTP_X_FORWARDED_HOST']);
1✔
370
                        if (
371
                                isset($xForwardedHost[$xForwardedForRealIpKey])
1✔
372
                                && ($pair = $this->parseHostAndPort(trim($xForwardedHost[$xForwardedForRealIpKey])))
1✔
373
                        ) {
374
                                $url->setHost($pair[0]);
1✔
375
                                if (isset($pair[1])) {
1✔
376
                                        $url->setPort($pair[1]);
1✔
377
                                }
378
                        }
379
                }
380

381
                return $remoteAddr ?? null;
1✔
382
        }
383

384

385
        /** @return array{string, ?int}|null */
386
        private function parseHostAndPort(string $s): ?array
1✔
387
        {
388
                return preg_match('#^([a-z0-9_.-]+|\[[a-f0-9:]+])(:\d+)?$#Di', $s, $matches)
1✔
389
                        ? [
390
                                rtrim(strtolower($matches[1]), '.'),
1✔
391
                                isset($matches[2]) ? (int) substr($matches[2], 1) : null,
1✔
392
                        ]
393
                        : null;
1✔
394
        }
395

396

397
        /** @deprecated */
398
        public function createHttpRequest(): Request
399
        {
UNCOV
400
                return $this->fromGlobals();
×
401
        }
402
}
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