PluginProbe
WPIDE – File Manager & Code Editor / 3.5.9
WPIDE – File Manager & Code Editor v3.5.9
3.5.9 3.5.8 3.5.7 2.0.14 2.0.15 2.0.16 2.0.2 2.0.4 2.0.5 2.0.6 2.0.7 2.0.8 2.0.9 2.1 2.2 2.3 2.3.1 2.3.2 2.4.0 2.5 2.6 3.0 3.1 3.2 3.3 All 55 releases
wpide / vendor / symfony / http-foundation / Request.php

Request.php in WPIDE – File Manager & Code Editor 3.5.9, at vendor/symfony/http-foundation/Request.php

2,188 lines 67.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /*
4 * This file is part of the Symfony package.
5 *
6 * (c) Fabien Potencier <fabien@symfony.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12 namespace Symfony\Component\HttpFoundation;
13
14 use Symfony\Component\HttpFoundation\Exception\BadRequestException;
15 use Symfony\Component\HttpFoundation\Exception\ConflictingHeadersException;
16 use Symfony\Component\HttpFoundation\Exception\JsonException;
17 use Symfony\Component\HttpFoundation\Exception\SessionNotFoundException;
18 use Symfony\Component\HttpFoundation\Exception\SuspiciousOperationException;
19 use Symfony\Component\HttpFoundation\Session\SessionInterface;
20
21 // Help opcache.preload discover always-needed symbols
22 class_exists(AcceptHeader::class);
23 class_exists(FileBag::class);
24 class_exists(HeaderBag::class);
25 class_exists(HeaderUtils::class);
26 class_exists(InputBag::class);
27 class_exists(ParameterBag::class);
28 class_exists(ServerBag::class);
29
30 /**
31 * Request represents an HTTP request.
32 *
33 * The methods dealing with URL accept / return a raw path (% encoded):
34 * * getBasePath
35 * * getBaseUrl
36 * * getPathInfo
37 * * getRequestUri
38 * * getUri
39 * * getUriForPath
40 *
41 * @author Fabien Potencier <fabien@symfony.com>
42 */
43 class Request
44 {
45 public const HEADER_FORWARDED = 0b000001; // When using RFC 7239
46 public const HEADER_X_FORWARDED_FOR = 0b000010;
47 public const HEADER_X_FORWARDED_HOST = 0b000100;
48 public const HEADER_X_FORWARDED_PROTO = 0b001000;
49 public const HEADER_X_FORWARDED_PORT = 0b010000;
50 public const HEADER_X_FORWARDED_PREFIX = 0b100000;
51
52 /** @deprecated since Symfony 5.2, use either "HEADER_X_FORWARDED_FOR | HEADER_X_FORWARDED_HOST | HEADER_X_FORWARDED_PORT | HEADER_X_FORWARDED_PROTO" or "HEADER_X_FORWARDED_AWS_ELB" or "HEADER_X_FORWARDED_TRAEFIK" constants instead. */
53 public const HEADER_X_FORWARDED_ALL = 0b1011110; // All "X-Forwarded-*" headers sent by "usual" reverse proxy
54 public const HEADER_X_FORWARDED_AWS_ELB = 0b0011010; // AWS ELB doesn't send X-Forwarded-Host
55 public const HEADER_X_FORWARDED_TRAEFIK = 0b0111110; // All "X-Forwarded-*" headers sent by Traefik reverse proxy
56
57 public const METHOD_HEAD = 'HEAD';
58 public const METHOD_GET = 'GET';
59 public const METHOD_POST = 'POST';
60 public const METHOD_PUT = 'PUT';
61 public const METHOD_PATCH = 'PATCH';
62 public const METHOD_DELETE = 'DELETE';
63 public const METHOD_PURGE = 'PURGE';
64 public const METHOD_OPTIONS = 'OPTIONS';
65 public const METHOD_TRACE = 'TRACE';
66 public const METHOD_CONNECT = 'CONNECT';
67
68 /**
69 * @var string[]
70 */
71 protected static $trustedProxies = [];
72
73 /**
74 * @var string[]
75 */
76 protected static $trustedHostPatterns = [];
77
78 /**
79 * @var string[]
80 */
81 protected static $trustedHosts = [];
82
83 protected static $httpMethodParameterOverride = false;
84
85 /**
86 * Custom parameters.
87 *
88 * @var ParameterBag
89 */
90 public $attributes;
91
92 /**
93 * Request body parameters ($_POST).
94 *
95 * @var InputBag
96 */
97 public $request;
98
99 /**
100 * Query string parameters ($_GET).
101 *
102 * @var InputBag
103 */
104 public $query;
105
106 /**
107 * Server and execution environment parameters ($_SERVER).
108 *
109 * @var ServerBag
110 */
111 public $server;
112
113 /**
114 * Uploaded files ($_FILES).
115 *
116 * @var FileBag
117 */
118 public $files;
119
120 /**
121 * Cookies ($_COOKIE).
122 *
123 * @var InputBag
124 */
125 public $cookies;
126
127 /**
128 * Headers (taken from the $_SERVER).
129 *
130 * @var HeaderBag
131 */
132 public $headers;
133
134 /**
135 * @var string|resource|false|null
136 */
137 protected $content;
138
139 /**
140 * @var array
141 */
142 protected $languages;
143
144 /**
145 * @var array
146 */
147 protected $charsets;
148
149 /**
150 * @var array
151 */
152 protected $encodings;
153
154 /**
155 * @var array
156 */
157 protected $acceptableContentTypes;
158
159 /**
160 * @var string
161 */
162 protected $pathInfo;
163
164 /**
165 * @var string
166 */
167 protected $requestUri;
168
169 /**
170 * @var string
171 */
172 protected $baseUrl;
173
174 /**
175 * @var string
176 */
177 protected $basePath;
178
179 /**
180 * @var string
181 */
182 protected $method;
183
184 /**
185 * @var string
186 */
187 protected $format;
188
189 /**
190 * @var SessionInterface|callable(): SessionInterface
191 */
192 protected $session;
193
194 /**
195 * @var string|null
196 */
197 protected $locale;
198
199 /**
200 * @var string
201 */
202 protected $defaultLocale = 'en';
203
204 /**
205 * @var array
206 */
207 protected static $formats;
208
209 protected static $requestFactory;
210
211 /**
212 * @var string|null
213 */
214 private $preferredFormat;
215 private $isHostValid = true;
216 private $isForwardedValid = true;
217
218 /**
219 * @var bool|null
220 */
221 private $isSafeContentPreferred;
222
223 private static $trustedHeaderSet = -1;
224
225 private const FORWARDED_PARAMS = [
226 self::HEADER_X_FORWARDED_FOR => 'for',
227 self::HEADER_X_FORWARDED_HOST => 'host',
228 self::HEADER_X_FORWARDED_PROTO => 'proto',
229 self::HEADER_X_FORWARDED_PORT => 'host',
230 ];
231
232 /**
233 * Names for headers that can be trusted when
234 * using trusted proxies.
235 *
236 * The FORWARDED header is the standard as of rfc7239.
237 *
238 * The other headers are non-standard, but widely used
239 * by popular reverse proxies (like Apache mod_proxy or Amazon EC2).
240 */
241 private const TRUSTED_HEADERS = [
242 self::HEADER_FORWARDED => 'FORWARDED',
243 self::HEADER_X_FORWARDED_FOR => 'X_FORWARDED_FOR',
244 self::HEADER_X_FORWARDED_HOST => 'X_FORWARDED_HOST',
245 self::HEADER_X_FORWARDED_PROTO => 'X_FORWARDED_PROTO',
246 self::HEADER_X_FORWARDED_PORT => 'X_FORWARDED_PORT',
247 self::HEADER_X_FORWARDED_PREFIX => 'X_FORWARDED_PREFIX',
248 ];
249
250 /** @var bool */
251 private $isIisRewrite = false;
252
253 /**
254 * @param array $query The GET parameters
255 * @param array $request The POST parameters
256 * @param array $attributes The request attributes (parameters parsed from the PATH_INFO, ...)
257 * @param array $cookies The COOKIE parameters
258 * @param array $files The FILES parameters
259 * @param array $server The SERVER parameters
260 * @param string|resource|null $content The raw body data
261 */
262 public function __construct(array $query = [], array $request = [], array $attributes = [], array $cookies = [], array $files = [], array $server = [], $content = null)
263 {
264 $this->initialize($query, $request, $attributes, $cookies, $files, $server, $content);
265 }
266
267 /**
268 * Sets the parameters for this request.
269 *
270 * This method also re-initializes all properties.
271 *
272 * @param array $query The GET parameters
273 * @param array $request The POST parameters
274 * @param array $attributes The request attributes (parameters parsed from the PATH_INFO, ...)
275 * @param array $cookies The COOKIE parameters
276 * @param array $files The FILES parameters
277 * @param array $server The SERVER parameters
278 * @param string|resource|null $content The raw body data
279 */
280 public function initialize(array $query = [], array $request = [], array $attributes = [], array $cookies = [], array $files = [], array $server = [], $content = null)
281 {
282 $this->request = new InputBag($request);
283 $this->query = new InputBag($query);
284 $this->attributes = new ParameterBag($attributes);
285 $this->cookies = new InputBag($cookies);
286 $this->files = new FileBag($files);
287 $this->server = new ServerBag($server);
288 $this->headers = new HeaderBag($this->server->getHeaders());
289
290 $this->content = $content;
291 $this->languages = null;
292 $this->charsets = null;
293 $this->encodings = null;
294 $this->acceptableContentTypes = null;
295 $this->pathInfo = null;
296 $this->requestUri = null;
297 $this->baseUrl = null;
298 $this->basePath = null;
299 $this->method = null;
300 $this->format = null;
301 }
302
303 /**
304 * Creates a new request with values from PHP's super globals.
305 *
306 * @return static
307 */
308 public static function createFromGlobals()
309 {
310 $request = self::createRequestFromFactory($_GET, $_POST, [], $_COOKIE, $_FILES, $_SERVER);
311
312 if (str_starts_with($request->headers->get('CONTENT_TYPE', ''), 'application/x-www-form-urlencoded')
313 && \in_array(strtoupper($request->server->get('REQUEST_METHOD', 'GET')), ['PUT', 'DELETE', 'PATCH'])
314 ) {
315 parse_str($request->getContent(), $data);
316 $request->request = new InputBag($data);
317 }
318
319 return $request;
320 }
321
322 /**
323 * Creates a Request based on a given URI and configuration.
324 *
325 * The information contained in the URI always take precedence
326 * over the other information (server and parameters).
327 *
328 * @param string $uri The URI
329 * @param string $method The HTTP method
330 * @param array $parameters The query (GET) or request (POST) parameters
331 * @param array $cookies The request cookies ($_COOKIE)
332 * @param array $files The request files ($_FILES)
333 * @param array $server The server parameters ($_SERVER)
334 * @param string|resource|null $content The raw body data
335 *
336 * @return static
337 *
338 * @throws BadRequestException When the URI is invalid
339 */
340 public static function create(string $uri, string $method = 'GET', array $parameters = [], array $cookies = [], array $files = [], array $server = [], $content = null)
341 {
342 $server = array_replace([
343 'SERVER_NAME' => 'localhost',
344 'SERVER_PORT' => 80,
345 'HTTP_HOST' => 'localhost',
346 'HTTP_USER_AGENT' => 'Symfony',
347 'HTTP_ACCEPT' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
348 'HTTP_ACCEPT_LANGUAGE' => 'en-us,en;q=0.5',
349 'HTTP_ACCEPT_CHARSET' => 'ISO-8859-1,utf-8;q=0.7,*;q=0.7',
350 'REMOTE_ADDR' => '127.0.0.1',
351 'SCRIPT_NAME' => '',
352 'SCRIPT_FILENAME' => '',
353 'SERVER_PROTOCOL' => 'HTTP/1.1',
354 'REQUEST_TIME' => time(),
355 'REQUEST_TIME_FLOAT' => microtime(true),
356 ], $server);
357
358 $server['PATH_INFO'] = '';
359 $server['REQUEST_METHOD'] = strtoupper($method);
360
361 if (false === $components = parse_url(\strlen($uri) !== strcspn($uri, '?#') ? $uri : $uri.'#')) {
362 throw new BadRequestException('Invalid URI.');
363 }
364
365 if (false !== ($i = strpos($uri, '\\')) && $i < strcspn($uri, '?#')) {
366 throw new BadRequestException('Invalid URI: A URI cannot contain a backslash.');
367 }
368 if (\strlen($uri) !== strcspn($uri, "\r\n\t")) {
369 throw new BadRequestException('Invalid URI: A URI cannot contain CR/LF/TAB characters.');
370 }
371 if ('' !== $uri && (\ord($uri[0]) <= 32 || \ord($uri[-1]) <= 32)) {
372 throw new BadRequestException('Invalid URI: A URI must not start nor end with ASCII control characters or spaces.');
373 }
374
375 if (isset($components['host'])) {
376 $server['SERVER_NAME'] = $components['host'];
377 $server['HTTP_HOST'] = $components['host'];
378 }
379
380 if (isset($components['scheme'])) {
381 if ('https' === $components['scheme']) {
382 $server['HTTPS'] = 'on';
383 $server['SERVER_PORT'] = 443;
384 } else {
385 unset($server['HTTPS']);
386 $server['SERVER_PORT'] = 80;
387 }
388 }
389
390 if (isset($components['port'])) {
391 $server['SERVER_PORT'] = $components['port'];
392 $server['HTTP_HOST'] .= ':'.$components['port'];
393 }
394
395 if (isset($components['user'])) {
396 $server['PHP_AUTH_USER'] = $components['user'];
397 }
398
399 if (isset($components['pass'])) {
400 $server['PHP_AUTH_PW'] = $components['pass'];
401 }
402
403 if (!isset($components['path'])) {
404 $components['path'] = '/';
405 }
406
407 switch (strtoupper($method)) {
408 case 'POST':
409 case 'PUT':
410 case 'DELETE':
411 if (!isset($server['CONTENT_TYPE'])) {
412 $server['CONTENT_TYPE'] = 'application/x-www-form-urlencoded';
413 }
414 // no break
415 case 'PATCH':
416 $request = $parameters;
417 $query = [];
418 break;
419 default:
420 $request = [];
421 $query = $parameters;
422 break;
423 }
424
425 $queryString = '';
426 if (isset($components['query'])) {
427 parse_str(html_entity_decode($components['query']), $qs);
428
429 if ($query) {
430 $query = array_replace($qs, $query);
431 $queryString = http_build_query($query, '', '&');
432 } else {
433 $query = $qs;
434 $queryString = $components['query'];
435 }
436 } elseif ($query) {
437 $queryString = http_build_query($query, '', '&');
438 }
439
440 $server['REQUEST_URI'] = $components['path'].('' !== $queryString ? '?'.$queryString : '');
441 $server['QUERY_STRING'] = $queryString;
442
443 return self::createRequestFromFactory($query, $request, [], $cookies, $files, $server, $content);
444 }
445
446 /**
447 * Sets a callable able to create a Request instance.
448 *
449 * This is mainly useful when you need to override the Request class
450 * to keep BC with an existing system. It should not be used for any
451 * other purpose.
452 */
453 public static function setFactory(?callable $callable)
454 {
455 self::$requestFactory = $callable;
456 }
457
458 /**
459 * Clones a request and overrides some of its parameters.
460 *
461 * @param array|null $query The GET parameters
462 * @param array|null $request The POST parameters
463 * @param array|null $attributes The request attributes (parameters parsed from the PATH_INFO, ...)
464 * @param array|null $cookies The COOKIE parameters
465 * @param array|null $files The FILES parameters
466 * @param array|null $server The SERVER parameters
467 *
468 * @return static
469 */
470 public function duplicate(?array $query = null, ?array $request = null, ?array $attributes = null, ?array $cookies = null, ?array $files = null, ?array $server = null)
471 {
472 $dup = clone $this;
473 if (null !== $query) {
474 $dup->query = new InputBag($query);
475 }
476 if (null !== $request) {
477 $dup->request = new InputBag($request);
478 }
479 if (null !== $attributes) {
480 $dup->attributes = new ParameterBag($attributes);
481 }
482 if (null !== $cookies) {
483 $dup->cookies = new InputBag($cookies);
484 }
485 if (null !== $files) {
486 $dup->files = new FileBag($files);
487 }
488 if (null !== $server) {
489 $dup->server = new ServerBag($server);
490 $dup->headers = new HeaderBag($dup->server->getHeaders());
491 }
492 $dup->languages = null;
493 $dup->charsets = null;
494 $dup->encodings = null;
495 $dup->acceptableContentTypes = null;
496 $dup->pathInfo = null;
497 $dup->requestUri = null;
498 $dup->baseUrl = null;
499 $dup->basePath = null;
500 $dup->method = null;
501 $dup->format = null;
502
503 if (!$dup->get('_format') && $this->get('_format')) {
504 $dup->attributes->set('_format', $this->get('_format'));
505 }
506
507 if (!$dup->getRequestFormat(null)) {
508 $dup->setRequestFormat($this->getRequestFormat(null));
509 }
510
511 return $dup;
512 }
513
514 /**
515 * Clones the current request.
516 *
517 * Note that the session is not cloned as duplicated requests
518 * are most of the time sub-requests of the main one.
519 */
520 public function __clone()
521 {
522 $this->query = clone $this->query;
523 $this->request = clone $this->request;
524 $this->attributes = clone $this->attributes;
525 $this->cookies = clone $this->cookies;
526 $this->files = clone $this->files;
527 $this->server = clone $this->server;
528 $this->headers = clone $this->headers;
529 }
530
531 /**
532 * Returns the request as a string.
533 *
534 * @return string
535 */
536 public function __toString()
537 {
538 $content = $this->getContent();
539
540 $cookieHeader = '';
541 $cookies = [];
542
543 foreach ($this->cookies as $k => $v) {
544 $cookies[] = \is_array($v) ? http_build_query([$k => $v], '', '; ', \PHP_QUERY_RFC3986) : "$k=$v";
545 }
546
547 if ($cookies) {
548 $cookieHeader = 'Cookie: '.implode('; ', $cookies)."\r\n";
549 }
550
551 return
552 sprintf('%s %s %s', $this->getMethod(), $this->getRequestUri(), $this->server->get('SERVER_PROTOCOL'))."\r\n".
553 $this->headers.
554 $cookieHeader."\r\n".
555 $content;
556 }
557
558 /**
559 * Overrides the PHP global variables according to this request instance.
560 *
561 * It overrides $_GET, $_POST, $_REQUEST, $_SERVER, $_COOKIE.
562 * $_FILES is never overridden, see rfc1867
563 */
564 public function overrideGlobals()
565 {
566 $this->server->set('QUERY_STRING', static::normalizeQueryString(http_build_query($this->query->all(), '', '&')));
567
568 $_GET = $this->query->all();
569 $_POST = $this->request->all();
570 $_SERVER = $this->server->all();
571 $_COOKIE = $this->cookies->all();
572
573 foreach ($this->headers->all() as $key => $value) {
574 $key = strtoupper(str_replace('-', '_', $key));
575 if (\in_array($key, ['CONTENT_TYPE', 'CONTENT_LENGTH', 'CONTENT_MD5'], true)) {
576 $_SERVER[$key] = implode(', ', $value);
577 } else {
578 $_SERVER['HTTP_'.$key] = implode(', ', $value);
579 }
580 }
581
582 $request = ['g' => $_GET, 'p' => $_POST, 'c' => $_COOKIE];
583
584 $requestOrder = \ini_get('request_order') ?: \ini_get('variables_order');
585 $requestOrder = preg_replace('#[^cgp]#', '', strtolower($requestOrder)) ?: 'gp';
586
587 $_REQUEST = [[]];
588
589 foreach (str_split($requestOrder) as $order) {
590 $_REQUEST[] = $request[$order];
591 }
592
593 $_REQUEST = array_merge(...$_REQUEST);
594 }
595
596 /**
597 * Sets a list of trusted proxies.
598 *
599 * You should only list the reverse proxies that you manage directly.
600 *
601 * @param array $proxies A list of trusted proxies, the string 'REMOTE_ADDR' will be replaced with $_SERVER['REMOTE_ADDR']
602 * @param int $trustedHeaderSet A bit field of Request::HEADER_*, to set which headers to trust from your proxies
603 */
604 public static function setTrustedProxies(array $proxies, int $trustedHeaderSet)
605 {
606 if (self::HEADER_X_FORWARDED_ALL === $trustedHeaderSet) {
607 trigger_deprecation('symfony/http-foundation', '5.2', 'The "HEADER_X_FORWARDED_ALL" constant is deprecated, use either "HEADER_X_FORWARDED_FOR | HEADER_X_FORWARDED_HOST | HEADER_X_FORWARDED_PORT | HEADER_X_FORWARDED_PROTO" or "HEADER_X_FORWARDED_AWS_ELB" or "HEADER_X_FORWARDED_TRAEFIK" constants instead.');
608 }
609 self::$trustedProxies = array_reduce($proxies, function ($proxies, $proxy) {
610 if ('REMOTE_ADDR' !== $proxy) {
611 $proxies[] = $proxy;
612 } elseif (isset($_SERVER['REMOTE_ADDR'])) {
613 $proxies[] = $_SERVER['REMOTE_ADDR'];
614 }
615
616 return $proxies;
617 }, []);
618 self::$trustedHeaderSet = $trustedHeaderSet;
619 }
620
621 /**
622 * Gets the list of trusted proxies.
623 *
624 * @return array
625 */
626 public static function getTrustedProxies()
627 {
628 return self::$trustedProxies;
629 }
630
631 /**
632 * Gets the set of trusted headers from trusted proxies.
633 *
634 * @return int A bit field of Request::HEADER_* that defines which headers are trusted from your proxies
635 */
636 public static function getTrustedHeaderSet()
637 {
638 return self::$trustedHeaderSet;
639 }
640
641 /**
642 * Sets a list of trusted host patterns.
643 *
644 * You should only list the hosts you manage using regexs.
645 *
646 * @param array $hostPatterns A list of trusted host patterns
647 */
648 public static function setTrustedHosts(array $hostPatterns)
649 {
650 self::$trustedHostPatterns = array_map(function ($hostPattern) {
651 return sprintf('{%s}i', $hostPattern);
652 }, $hostPatterns);
653 // we need to reset trusted hosts on trusted host patterns change
654 self::$trustedHosts = [];
655 }
656
657 /**
658 * Gets the list of trusted host patterns.
659 *
660 * @return array
661 */
662 public static function getTrustedHosts()
663 {
664 return self::$trustedHostPatterns;
665 }
666
667 /**
668 * Normalizes a query string.
669 *
670 * It builds a normalized query string, where keys/value pairs are alphabetized,
671 * have consistent escaping and unneeded delimiters are removed.
672 *
673 * @return string
674 */
675 public static function normalizeQueryString(?string $qs)
676 {
677 if ('' === ($qs ?? '')) {
678 return '';
679 }
680
681 $qs = HeaderUtils::parseQuery($qs);
682 ksort($qs);
683
684 return http_build_query($qs, '', '&', \PHP_QUERY_RFC3986);
685 }
686
687 /**
688 * Enables support for the _method request parameter to determine the intended HTTP method.
689 *
690 * Be warned that enabling this feature might lead to CSRF issues in your code.
691 * Check that you are using CSRF tokens when required.
692 * If the HTTP method parameter override is enabled, an html-form with method "POST" can be altered
693 * and used to send a "PUT" or "DELETE" request via the _method request parameter.
694 * If these methods are not protected against CSRF, this presents a possible vulnerability.
695 *
696 * The HTTP method can only be overridden when the real HTTP method is POST.
697 */
698 public static function enableHttpMethodParameterOverride()
699 {
700 self::$httpMethodParameterOverride = true;
701 }
702
703 /**
704 * Checks whether support for the _method request parameter is enabled.
705 *
706 * @return bool
707 */
708 public static function getHttpMethodParameterOverride()
709 {
710 return self::$httpMethodParameterOverride;
711 }
712
713 /**
714 * Gets a "parameter" value from any bag.
715 *
716 * This method is mainly useful for libraries that want to provide some flexibility. If you don't need the
717 * flexibility in controllers, it is better to explicitly get request parameters from the appropriate
718 * public property instead (attributes, query, request).
719 *
720 * Order of precedence: PATH (routing placeholders or custom attributes), GET, POST
721 *
722 * @param mixed $default The default value if the parameter key does not exist
723 *
724 * @return mixed
725 *
726 * @internal since Symfony 5.4, use explicit input sources instead
727 */
728 public function get(string $key, $default = null)
729 {
730 if ($this !== $result = $this->attributes->get($key, $this)) {
731 return $result;
732 }
733
734 if ($this->query->has($key)) {
735 return $this->query->all()[$key];
736 }
737
738 if ($this->request->has($key)) {
739 return $this->request->all()[$key];
740 }
741
742 return $default;
743 }
744
745 /**
746 * Gets the Session.
747 *
748 * @return SessionInterface
749 */
750 public function getSession()
751 {
752 $session = $this->session;
753 if (!$session instanceof SessionInterface && null !== $session) {
754 $this->setSession($session = $session());
755 }
756
757 if (null === $session) {
758 throw new SessionNotFoundException('Session has not been set.');
759 }
760
761 return $session;
762 }
763
764 /**
765 * Whether the request contains a Session which was started in one of the
766 * previous requests.
767 *
768 * @return bool
769 */
770 public function hasPreviousSession()
771 {
772 // the check for $this->session avoids malicious users trying to fake a session cookie with proper name
773 return $this->hasSession() && $this->cookies->has($this->getSession()->getName());
774 }
775
776 /**
777 * Whether the request contains a Session object.
778 *
779 * This method does not give any information about the state of the session object,
780 * like whether the session is started or not. It is just a way to check if this Request
781 * is associated with a Session instance.
782 *
783 * @param bool $skipIfUninitialized When true, ignores factories injected by `setSessionFactory`
784 *
785 * @return bool
786 */
787 public function hasSession(/* bool $skipIfUninitialized = false */)
788 {
789 $skipIfUninitialized = \func_num_args() > 0 ? func_get_arg(0) : false;
790
791 return null !== $this->session && (!$skipIfUninitialized || $this->session instanceof SessionInterface);
792 }
793
794 public function setSession(SessionInterface $session)
795 {
796 $this->session = $session;
797 }
798
799 /**
800 * @internal
801 *
802 * @param callable(): SessionInterface $factory
803 */
804 public function setSessionFactory(callable $factory)
805 {
806 $this->session = $factory;
807 }
808
809 /**
810 * Returns the client IP addresses.
811 *
812 * In the returned array the most trusted IP address is first, and the
813 * least trusted one last. The "real" client IP address is the last one,
814 * but this is also the least trusted one. Trusted proxies are stripped.
815 *
816 * Use this method carefully; you should use getClientIp() instead.
817 *
818 * @return array
819 *
820 * @see getClientIp()
821 */
822 public function getClientIps()
823 {
824 $ip = $this->server->get('REMOTE_ADDR');
825
826 if (!$this->isFromTrustedProxy()) {
827 return [$ip];
828 }
829
830 return $this->getTrustedValues(self::HEADER_X_FORWARDED_FOR, $ip) ?: [$ip];
831 }
832
833 /**
834 * Returns the client IP address.
835 *
836 * This method can read the client IP address from the "X-Forwarded-For" header
837 * when trusted proxies were set via "setTrustedProxies()". The "X-Forwarded-For"
838 * header value is a comma+space separated list of IP addresses, the left-most
839 * being the original client, and each successive proxy that passed the request
840 * adding the IP address where it received the request from.
841 *
842 * If your reverse proxy uses a different header name than "X-Forwarded-For",
843 * ("Client-Ip" for instance), configure it via the $trustedHeaderSet
844 * argument of the Request::setTrustedProxies() method instead.
845 *
846 * @return string|null
847 *
848 * @see getClientIps()
849 * @see https://wikipedia.org/wiki/X-Forwarded-For
850 */
851 public function getClientIp()
852 {
853 $ipAddresses = $this->getClientIps();
854
855 return $ipAddresses[0];
856 }
857
858 /**
859 * Returns current script name.
860 *
861 * @return string
862 */
863 public function getScriptName()
864 {
865 return $this->server->get('SCRIPT_NAME', $this->server->get('ORIG_SCRIPT_NAME', ''));
866 }
867
868 /**
869 * Returns the path being requested relative to the executed script.
870 *
871 * The path info always starts with a /.
872 *
873 * Suppose this request is instantiated from /mysite on localhost:
874 *
875 * * http://localhost/mysite returns an empty string
876 * * http://localhost/mysite/about returns '/about'
877 * * http://localhost/mysite/enco%20ded returns '/enco%20ded'
878 * * http://localhost/mysite/about?var=1 returns '/about'
879 *
880 * @return string The raw path (i.e. not urldecoded)
881 */
882 public function getPathInfo()
883 {
884 if (null === $this->pathInfo) {
885 $this->pathInfo = $this->preparePathInfo();
886 }
887
888 return $this->pathInfo;
889 }
890
891 /**
892 * Returns the root path from which this request is executed.
893 *
894 * Suppose that an index.php file instantiates this request object:
895 *
896 * * http://localhost/index.php returns an empty string
897 * * http://localhost/index.php/page returns an empty string
898 * * http://localhost/web/index.php returns '/web'
899 * * http://localhost/we%20b/index.php returns '/we%20b'
900 *
901 * @return string The raw path (i.e. not urldecoded)
902 */
903 public function getBasePath()
904 {
905 if (null === $this->basePath) {
906 $this->basePath = $this->prepareBasePath();
907 }
908
909 return $this->basePath;
910 }
911
912 /**
913 * Returns the root URL from which this request is executed.
914 *
915 * The base URL never ends with a /.
916 *
917 * This is similar to getBasePath(), except that it also includes the
918 * script filename (e.g. index.php) if one exists.
919 *
920 * @return string The raw URL (i.e. not urldecoded)
921 */
922 public function getBaseUrl()
923 {
924 $trustedPrefix = '';
925
926 // the proxy prefix must be prepended to any prefix being needed at the webserver level
927 if ($this->isFromTrustedProxy() && $trustedPrefixValues = $this->getTrustedValues(self::HEADER_X_FORWARDED_PREFIX)) {
928 $trustedPrefix = rtrim($trustedPrefixValues[0], '/');
929 }
930
931 return $trustedPrefix.$this->getBaseUrlReal();
932 }
933
934 /**
935 * Returns the real base URL received by the webserver from which this request is executed.
936 * The URL does not include trusted reverse proxy prefix.
937 *
938 * @return string The raw URL (i.e. not urldecoded)
939 */
940 private function getBaseUrlReal(): string
941 {
942 if (null === $this->baseUrl) {
943 $this->baseUrl = $this->prepareBaseUrl();
944 }
945
946 return $this->baseUrl;
947 }
948
949 /**
950 * Gets the request's scheme.
951 *
952 * @return string
953 */
954 public function getScheme()
955 {
956 return $this->isSecure() ? 'https' : 'http';
957 }
958
959 /**
960 * Returns the port on which the request is made.
961 *
962 * This method can read the client port from the "X-Forwarded-Port" header
963 * when trusted proxies were set via "setTrustedProxies()".
964 *
965 * The "X-Forwarded-Port" header must contain the client port.
966 *
967 * @return int|string|null Can be a string if fetched from the server bag
968 */
969 public function getPort()
970 {
971 if ($this->isFromTrustedProxy() && $host = $this->getTrustedValues(self::HEADER_X_FORWARDED_PORT)) {
972 $host = $host[0];
973 } elseif ($this->isFromTrustedProxy() && $host = $this->getTrustedValues(self::HEADER_X_FORWARDED_HOST)) {
974 $host = $host[0];
975 } elseif (!$host = $this->headers->get('HOST')) {
976 return $this->server->get('SERVER_PORT');
977 }
978
979 if ('[' === $host[0]) {
980 $pos = strpos($host, ':', strrpos($host, ']'));
981 } else {
982 $pos = strrpos($host, ':');
983 }
984
985 if (false !== $pos && $port = substr($host, $pos + 1)) {
986 return (int) $port;
987 }
988
989 return 'https' === $this->getScheme() ? 443 : 80;
990 }
991
992 /**
993 * Returns the user.
994 *
995 * @return string|null
996 */
997 public function getUser()
998 {
999 return $this->headers->get('PHP_AUTH_USER');
1000 }
1001
1002 /**
1003 * Returns the password.
1004 *
1005 * @return string|null
1006 */
1007 public function getPassword()
1008 {
1009 return $this->headers->get('PHP_AUTH_PW');
1010 }
1011
1012 /**
1013 * Gets the user info.
1014 *
1015 * @return string|null A user name if any and, optionally, scheme-specific information about how to gain authorization to access the server
1016 */
1017 public function getUserInfo()
1018 {
1019 $userinfo = $this->getUser();
1020
1021 $pass = $this->getPassword();
1022 if ('' != $pass) {
1023 $userinfo .= ":$pass";
1024 }
1025
1026 return $userinfo;
1027 }
1028
1029 /**
1030 * Returns the HTTP host being requested.
1031 *
1032 * The port name will be appended to the host if it's non-standard.
1033 *
1034 * @return string
1035 */
1036 public function getHttpHost()
1037 {
1038 $scheme = $this->getScheme();
1039 $port = $this->getPort();
1040
1041 if (('http' == $scheme && 80 == $port) || ('https' == $scheme && 443 == $port)) {
1042 return $this->getHost();
1043 }
1044
1045 return $this->getHost().':'.$port;
1046 }
1047
1048 /**
1049 * Returns the requested URI (path and query string).
1050 *
1051 * @return string The raw URI (i.e. not URI decoded)
1052 */
1053 public function getRequestUri()
1054 {
1055 if (null === $this->requestUri) {
1056 $this->requestUri = $this->prepareRequestUri();
1057 }
1058
1059 return $this->requestUri;
1060 }
1061
1062 /**
1063 * Gets the scheme and HTTP host.
1064 *
1065 * If the URL was called with basic authentication, the user
1066 * and the password are not added to the generated string.
1067 *
1068 * @return string
1069 */
1070 public function getSchemeAndHttpHost()
1071 {
1072 return $this->getScheme().'://'.$this->getHttpHost();
1073 }
1074
1075 /**
1076 * Generates a normalized URI (URL) for the Request.
1077 *
1078 * @return string
1079 *
1080 * @see getQueryString()
1081 */
1082 public function getUri()
1083 {
1084 if (null !== $qs = $this->getQueryString()) {
1085 $qs = '?'.$qs;
1086 }
1087
1088 return $this->getSchemeAndHttpHost().$this->getBaseUrl().$this->getPathInfo().$qs;
1089 }
1090
1091 /**
1092 * Generates a normalized URI for the given path.
1093 *
1094 * @param string $path A path to use instead of the current one
1095 *
1096 * @return string
1097 */
1098 public function getUriForPath(string $path)
1099 {
1100 return $this->getSchemeAndHttpHost().$this->getBaseUrl().$path;
1101 }
1102
1103 /**
1104 * Returns the path as relative reference from the current Request path.
1105 *
1106 * Only the URIs path component (no schema, host etc.) is relevant and must be given.
1107 * Both paths must be absolute and not contain relative parts.
1108 * Relative URLs from one resource to another are useful when generating self-contained downloadable document archives.
1109 * Furthermore, they can be used to reduce the link size in documents.
1110 *
1111 * Example target paths, given a base path of "/a/b/c/d":
1112 * - "/a/b/c/d" -> ""
1113 * - "/a/b/c/" -> "./"
1114 * - "/a/b/" -> "../"
1115 * - "/a/b/c/other" -> "other"
1116 * - "/a/x/y" -> "../../x/y"
1117 *
1118 * @return string
1119 */
1120 public function getRelativeUriForPath(string $path)
1121 {
1122 // be sure that we are dealing with an absolute path
1123 if (!isset($path[0]) || '/' !== $path[0]) {
1124 return $path;
1125 }
1126
1127 if ($path === $basePath = $this->getPathInfo()) {
1128 return '';
1129 }
1130
1131 $sourceDirs = explode('/', isset($basePath[0]) && '/' === $basePath[0] ? substr($basePath, 1) : $basePath);
1132 $targetDirs = explode('/', substr($path, 1));
1133 array_pop($sourceDirs);
1134 $targetFile = array_pop($targetDirs);
1135
1136 foreach ($sourceDirs as $i => $dir) {
1137 if (isset($targetDirs[$i]) && $dir === $targetDirs[$i]) {
1138 unset($sourceDirs[$i], $targetDirs[$i]);
1139 } else {
1140 break;
1141 }
1142 }
1143
1144 $targetDirs[] = $targetFile;
1145 $path = str_repeat('../', \count($sourceDirs)).implode('/', $targetDirs);
1146
1147 // A reference to the same base directory or an empty subdirectory must be prefixed with "./".
1148 // This also applies to a segment with a colon character (e.g., "file:colon") that cannot be used
1149 // as the first segment of a relative-path reference, as it would be mistaken for a scheme name
1150 // (see https://tools.ietf.org/html/rfc3986#section-4.2).
1151 return !isset($path[0]) || '/' === $path[0]
1152 || false !== ($colonPos = strpos($path, ':')) && ($colonPos < ($slashPos = strpos($path, '/')) || false === $slashPos)
1153 ? "./$path" : $path;
1154 }
1155
1156 /**
1157 * Generates the normalized query string for the Request.
1158 *
1159 * It builds a normalized query string, where keys/value pairs are alphabetized
1160 * and have consistent escaping.
1161 *
1162 * @return string|null
1163 */
1164 public function getQueryString()
1165 {
1166 $qs = static::normalizeQueryString($this->server->get('QUERY_STRING'));
1167
1168 return '' === $qs ? null : $qs;
1169 }
1170
1171 /**
1172 * Checks whether the request is secure or not.
1173 *
1174 * This method can read the client protocol from the "X-Forwarded-Proto" header
1175 * when trusted proxies were set via "setTrustedProxies()".
1176 *
1177 * The "X-Forwarded-Proto" header must contain the protocol: "https" or "http".
1178 *
1179 * @return bool
1180 */
1181 public function isSecure()
1182 {
1183 if ($this->isFromTrustedProxy() && $proto = $this->getTrustedValues(self::HEADER_X_FORWARDED_PROTO)) {
1184 return \in_array(strtolower($proto[0]), ['https', 'on', 'ssl', '1'], true);
1185 }
1186
1187 $https = $this->server->get('HTTPS');
1188
1189 return !empty($https) && 'off' !== strtolower($https);
1190 }
1191
1192 /**
1193 * Returns the host name.
1194 *
1195 * This method can read the client host name from the "X-Forwarded-Host" header
1196 * when trusted proxies were set via "setTrustedProxies()".
1197 *
1198 * The "X-Forwarded-Host" header must contain the client host name.
1199 *
1200 * @return string
1201 *
1202 * @throws SuspiciousOperationException when the host name is invalid or not trusted
1203 */
1204 public function getHost()
1205 {
1206 if ($this->isFromTrustedProxy() && $host = $this->getTrustedValues(self::HEADER_X_FORWARDED_HOST)) {
1207 $host = $host[0];
1208 } elseif (!$host = $this->headers->get('HOST')) {
1209 if (!$host = $this->server->get('SERVER_NAME')) {
1210 $host = $this->server->get('SERVER_ADDR', '');
1211 }
1212 }
1213
1214 // trim and remove port number from host
1215 // host is lowercase as per RFC 952/2181
1216 $host = strtolower(preg_replace('/:\d+$/', '', trim($host)));
1217
1218 // as the host can come from the user (HTTP_HOST and depending on the configuration, SERVER_NAME too can come from the user)
1219 // check that it does not contain forbidden characters (see RFC 952 and RFC 2181)
1220 // use preg_replace() instead of preg_match() to prevent DoS attacks with long host names
1221 if ($host && '' !== preg_replace('/(?:^\[)?[a-zA-Z0-9-:\]_]+\.?/', '', $host)) {
1222 if (!$this->isHostValid) {
1223 return '';
1224 }
1225 $this->isHostValid = false;
1226
1227 throw new SuspiciousOperationException(sprintf('Invalid Host "%s".', $host));
1228 }
1229
1230 if (\count(self::$trustedHostPatterns) > 0) {
1231 // to avoid host header injection attacks, you should provide a list of trusted host patterns
1232
1233 if (\in_array($host, self::$trustedHosts)) {
1234 return $host;
1235 }
1236
1237 foreach (self::$trustedHostPatterns as $pattern) {
1238 if (preg_match($pattern, $host)) {
1239 self::$trustedHosts[] = $host;
1240
1241 return $host;
1242 }
1243 }
1244
1245 if (!$this->isHostValid) {
1246 return '';
1247 }
1248 $this->isHostValid = false;
1249
1250 throw new SuspiciousOperationException(sprintf('Untrusted Host "%s".', $host));
1251 }
1252
1253 return $host;
1254 }
1255
1256 /**
1257 * Sets the request method.
1258 */
1259 public function setMethod(string $method)
1260 {
1261 $this->method = null;
1262 $this->server->set('REQUEST_METHOD', $method);
1263 }
1264
1265 /**
1266 * Gets the request "intended" method.
1267 *
1268 * If the X-HTTP-Method-Override header is set, and if the method is a POST,
1269 * then it is used to determine the "real" intended HTTP method.
1270 *
1271 * The _method request parameter can also be used to determine the HTTP method,
1272 * but only if enableHttpMethodParameterOverride() has been called.
1273 *
1274 * The method is always an uppercased string.
1275 *
1276 * @return string
1277 *
1278 * @see getRealMethod()
1279 */
1280 public function getMethod()
1281 {
1282 if (null !== $this->method) {
1283 return $this->method;
1284 }
1285
1286 $this->method = strtoupper($this->server->get('REQUEST_METHOD', 'GET'));
1287
1288 if ('POST' !== $this->method) {
1289 return $this->method;
1290 }
1291
1292 $method = $this->headers->get('X-HTTP-METHOD-OVERRIDE');
1293
1294 if (!$method && self::$httpMethodParameterOverride) {
1295 $method = $this->request->get('_method', $this->query->get('_method', 'POST'));
1296 }
1297
1298 if (!\is_string($method)) {
1299 return $this->method;
1300 }
1301
1302 $method = strtoupper($method);
1303
1304 if (\in_array($method, ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT', 'OPTIONS', 'PATCH', 'PURGE', 'TRACE'], true)) {
1305 return $this->method = $method;
1306 }
1307
1308 if (!preg_match('/^[A-Z]++$/D', $method)) {
1309 throw new SuspiciousOperationException('Invalid HTTP method override.');
1310 }
1311
1312 return $this->method = $method;
1313 }
1314
1315 /**
1316 * Gets the "real" request method.
1317 *
1318 * @return string
1319 *
1320 * @see getMethod()
1321 */
1322 public function getRealMethod()
1323 {
1324 return strtoupper($this->server->get('REQUEST_METHOD', 'GET'));
1325 }
1326
1327 /**
1328 * Gets the mime type associated with the format.
1329 *
1330 * @return string|null
1331 */
1332 public function getMimeType(string $format)
1333 {
1334 if (null === static::$formats) {
1335 static::initializeFormats();
1336 }
1337
1338 return isset(static::$formats[$format]) ? static::$formats[$format][0] : null;
1339 }
1340
1341 /**
1342 * Gets the mime types associated with the format.
1343 *
1344 * @return array
1345 */
1346 public static function getMimeTypes(string $format)
1347 {
1348 if (null === static::$formats) {
1349 static::initializeFormats();
1350 }
1351
1352 return static::$formats[$format] ?? [];
1353 }
1354
1355 /**
1356 * Gets the format associated with the mime type.
1357 *
1358 * @return string|null
1359 */
1360 public function getFormat(?string $mimeType)
1361 {
1362 $canonicalMimeType = null;
1363 if ($mimeType && false !== $pos = strpos($mimeType, ';')) {
1364 $canonicalMimeType = trim(substr($mimeType, 0, $pos));
1365 }
1366
1367 if (null === static::$formats) {
1368 static::initializeFormats();
1369 }
1370
1371 foreach (static::$formats as $format => $mimeTypes) {
1372 if (\in_array($mimeType, (array) $mimeTypes)) {
1373 return $format;
1374 }
1375 if (null !== $canonicalMimeType && \in_array($canonicalMimeType, (array) $mimeTypes)) {
1376 return $format;
1377 }
1378 }
1379
1380 return null;
1381 }
1382
1383 /**
1384 * Associates a format with mime types.
1385 *
1386 * @param string|array $mimeTypes The associated mime types (the preferred one must be the first as it will be used as the content type)
1387 */
1388 public function setFormat(?string $format, $mimeTypes)
1389 {
1390 if (null === static::$formats) {
1391 static::initializeFormats();
1392 }
1393
1394 static::$formats[$format] = \is_array($mimeTypes) ? $mimeTypes : [$mimeTypes];
1395 }
1396
1397 /**
1398 * Gets the request format.
1399 *
1400 * Here is the process to determine the format:
1401 *
1402 * * format defined by the user (with setRequestFormat())
1403 * * _format request attribute
1404 * * $default
1405 *
1406 * @see getPreferredFormat
1407 *
1408 * @return string|null
1409 */
1410 public function getRequestFormat(?string $default = 'html')
1411 {
1412 if (null === $this->format) {
1413 $this->format = $this->attributes->get('_format');
1414 }
1415
1416 return $this->format ?? $default;
1417 }
1418
1419 /**
1420 * Sets the request format.
1421 */
1422 public function setRequestFormat(?string $format)
1423 {
1424 $this->format = $format;
1425 }
1426
1427 /**
1428 * Gets the format associated with the request.
1429 *
1430 * @return string|null
1431 */
1432 public function getContentType()
1433 {
1434 return $this->getFormat($this->headers->get('CONTENT_TYPE', ''));
1435 }
1436
1437 /**
1438 * Sets the default locale.
1439 */
1440 public function setDefaultLocale(string $locale)
1441 {
1442 $this->defaultLocale = $locale;
1443
1444 if (null === $this->locale) {
1445 $this->setPhpDefaultLocale($locale);
1446 }
1447 }
1448
1449 /**
1450 * Get the default locale.
1451 *
1452 * @return string
1453 */
1454 public function getDefaultLocale()
1455 {
1456 return $this->defaultLocale;
1457 }
1458
1459 /**
1460 * Sets the locale.
1461 */
1462 public function setLocale(string $locale)
1463 {
1464 $this->setPhpDefaultLocale($this->locale = $locale);
1465 }
1466
1467 /**
1468 * Get the locale.
1469 *
1470 * @return string
1471 */
1472 public function getLocale()
1473 {
1474 return $this->locale ?? $this->defaultLocale;
1475 }
1476
1477 /**
1478 * Checks if the request method is of specified type.
1479 *
1480 * @param string $method Uppercase request method (GET, POST etc)
1481 *
1482 * @return bool
1483 */
1484 public function isMethod(string $method)
1485 {
1486 return $this->getMethod() === strtoupper($method);
1487 }
1488
1489 /**
1490 * Checks whether or not the method is safe.
1491 *
1492 * @see https://tools.ietf.org/html/rfc7231#section-4.2.1
1493 *
1494 * @return bool
1495 */
1496 public function isMethodSafe()
1497 {
1498 return \in_array($this->getMethod(), ['GET', 'HEAD', 'OPTIONS', 'TRACE']);
1499 }
1500
1501 /**
1502 * Checks whether or not the method is idempotent.
1503 *
1504 * @return bool
1505 */
1506 public function isMethodIdempotent()
1507 {
1508 return \in_array($this->getMethod(), ['HEAD', 'GET', 'PUT', 'DELETE', 'TRACE', 'OPTIONS', 'PURGE']);
1509 }
1510
1511 /**
1512 * Checks whether the method is cacheable or not.
1513 *
1514 * @see https://tools.ietf.org/html/rfc7231#section-4.2.3
1515 *
1516 * @return bool
1517 */
1518 public function isMethodCacheable()
1519 {
1520 return \in_array($this->getMethod(), ['GET', 'HEAD']);
1521 }
1522
1523 /**
1524 * Returns the protocol version.
1525 *
1526 * If the application is behind a proxy, the protocol version used in the
1527 * requests between the client and the proxy and between the proxy and the
1528 * server might be different. This returns the former (from the "Via" header)
1529 * if the proxy is trusted (see "setTrustedProxies()"), otherwise it returns
1530 * the latter (from the "SERVER_PROTOCOL" server parameter).
1531 *
1532 * @return string|null
1533 */
1534 public function getProtocolVersion()
1535 {
1536 if ($this->isFromTrustedProxy()) {
1537 preg_match('~^(HTTP/)?([1-9]\.[0-9]) ~', $this->headers->get('Via') ?? '', $matches);
1538
1539 if ($matches) {
1540 return 'HTTP/'.$matches[2];
1541 }
1542 }
1543
1544 return $this->server->get('SERVER_PROTOCOL');
1545 }
1546
1547 /**
1548 * Returns the request body content.
1549 *
1550 * @param bool $asResource If true, a resource will be returned
1551 *
1552 * @return string|resource
1553 */
1554 public function getContent(bool $asResource = false)
1555 {
1556 $currentContentIsResource = \is_resource($this->content);
1557
1558 if (true === $asResource) {
1559 if ($currentContentIsResource) {
1560 rewind($this->content);
1561
1562 return $this->content;
1563 }
1564
1565 // Content passed in parameter (test)
1566 if (\is_string($this->content)) {
1567 $resource = fopen('php://temp', 'r+');
1568 fwrite($resource, $this->content);
1569 rewind($resource);
1570
1571 return $resource;
1572 }
1573
1574 $this->content = false;
1575
1576 return fopen('php://input', 'r');
1577 }
1578
1579 if ($currentContentIsResource) {
1580 rewind($this->content);
1581
1582 return stream_get_contents($this->content);
1583 }
1584
1585 if (null === $this->content || false === $this->content) {
1586 $this->content = file_get_contents('php://input');
1587 }
1588
1589 return $this->content;
1590 }
1591
1592 /**
1593 * Gets the request body decoded as array, typically from a JSON payload.
1594 *
1595 * @return array
1596 *
1597 * @throws JsonException When the body cannot be decoded to an array
1598 */
1599 public function toArray()
1600 {
1601 if ('' === $content = $this->getContent()) {
1602 throw new JsonException('Request body is empty.');
1603 }
1604
1605 try {
1606 $content = json_decode($content, true, 512, \JSON_BIGINT_AS_STRING | (\PHP_VERSION_ID >= 70300 ? \JSON_THROW_ON_ERROR : 0));
1607 } catch (\JsonException $e) {
1608 throw new JsonException('Could not decode request body.', $e->getCode(), $e);
1609 }
1610
1611 if (\PHP_VERSION_ID < 70300 && \JSON_ERROR_NONE !== json_last_error()) {
1612 throw new JsonException('Could not decode request body: '.json_last_error_msg(), json_last_error());
1613 }
1614
1615 if (!\is_array($content)) {
1616 throw new JsonException(sprintf('JSON content was expected to decode to an array, "%s" returned.', get_debug_type($content)));
1617 }
1618
1619 return $content;
1620 }
1621
1622 /**
1623 * Gets the Etags.
1624 *
1625 * @return array
1626 */
1627 public function getETags()
1628 {
1629 return preg_split('/\s*,\s*/', $this->headers->get('If-None-Match', ''), -1, \PREG_SPLIT_NO_EMPTY);
1630 }
1631
1632 /**
1633 * @return bool
1634 */
1635 public function isNoCache()
1636 {
1637 return $this->headers->hasCacheControlDirective('no-cache') || 'no-cache' == $this->headers->get('Pragma');
1638 }
1639
1640 /**
1641 * Gets the preferred format for the response by inspecting, in the following order:
1642 * * the request format set using setRequestFormat;
1643 * * the values of the Accept HTTP header.
1644 *
1645 * Note that if you use this method, you should send the "Vary: Accept" header
1646 * in the response to prevent any issues with intermediary HTTP caches.
1647 */
1648 public function getPreferredFormat(?string $default = 'html'): ?string
1649 {
1650 if (null !== $this->preferredFormat || null !== $this->preferredFormat = $this->getRequestFormat(null)) {
1651 return $this->preferredFormat;
1652 }
1653
1654 foreach ($this->getAcceptableContentTypes() as $mimeType) {
1655 if ($this->preferredFormat = $this->getFormat($mimeType)) {
1656 return $this->preferredFormat;
1657 }
1658 }
1659
1660 return $default;
1661 }
1662
1663 /**
1664 * Returns the preferred language.
1665 *
1666 * @param string[] $locales An array of ordered available locales
1667 *
1668 * @return string|null
1669 */
1670 public function getPreferredLanguage(?array $locales = null)
1671 {
1672 $preferredLanguages = $this->getLanguages();
1673
1674 if (empty($locales)) {
1675 return $preferredLanguages[0] ?? null;
1676 }
1677
1678 if (!$preferredLanguages) {
1679 return $locales[0];
1680 }
1681
1682 $extendedPreferredLanguages = [];
1683 foreach ($preferredLanguages as $language) {
1684 $extendedPreferredLanguages[] = $language;
1685 if (false !== $position = strpos($language, '_')) {
1686 $superLanguage = substr($language, 0, $position);
1687 if (!\in_array($superLanguage, $preferredLanguages)) {
1688 $extendedPreferredLanguages[] = $superLanguage;
1689 }
1690 }
1691 }
1692
1693 $preferredLanguages = array_values(array_intersect($extendedPreferredLanguages, $locales));
1694
1695 return $preferredLanguages[0] ?? $locales[0];
1696 }
1697
1698 /**
1699 * Gets a list of languages acceptable by the client browser ordered in the user browser preferences.
1700 *
1701 * @return array
1702 */
1703 public function getLanguages()
1704 {
1705 if (null !== $this->languages) {
1706 return $this->languages;
1707 }
1708
1709 $languages = AcceptHeader::fromString($this->headers->get('Accept-Language'))->all();
1710 $this->languages = [];
1711 foreach ($languages as $acceptHeaderItem) {
1712 $lang = $acceptHeaderItem->getValue();
1713 if (str_contains($lang, '-')) {
1714 $codes = explode('-', $lang);
1715 if ('i' === $codes[0]) {
1716 // Language not listed in ISO 639 that are not variants
1717 // of any listed language, which can be registered with the
1718 // i-prefix, such as i-cherokee
1719 if (\count($codes) > 1) {
1720 $lang = $codes[1];
1721 }
1722 } else {
1723 for ($i = 0, $max = \count($codes); $i < $max; ++$i) {
1724 if (0 === $i) {
1725 $lang = strtolower($codes[0]);
1726 } else {
1727 $lang .= '_'.strtoupper($codes[$i]);
1728 }
1729 }
1730 }
1731 }
1732
1733 $this->languages[] = $lang;
1734 }
1735
1736 return $this->languages;
1737 }
1738
1739 /**
1740 * Gets a list of charsets acceptable by the client browser in preferable order.
1741 *
1742 * @return array
1743 */
1744 public function getCharsets()
1745 {
1746 if (null !== $this->charsets) {
1747 return $this->charsets;
1748 }
1749
1750 return $this->charsets = array_map('strval', array_keys(AcceptHeader::fromString($this->headers->get('Accept-Charset'))->all()));
1751 }
1752
1753 /**
1754 * Gets a list of encodings acceptable by the client browser in preferable order.
1755 *
1756 * @return array
1757 */
1758 public function getEncodings()
1759 {
1760 if (null !== $this->encodings) {
1761 return $this->encodings;
1762 }
1763
1764 return $this->encodings = array_map('strval', array_keys(AcceptHeader::fromString($this->headers->get('Accept-Encoding'))->all()));
1765 }
1766
1767 /**
1768 * Gets a list of content types acceptable by the client browser in preferable order.
1769 *
1770 * @return array
1771 */
1772 public function getAcceptableContentTypes()
1773 {
1774 if (null !== $this->acceptableContentTypes) {
1775 return $this->acceptableContentTypes;
1776 }
1777
1778 return $this->acceptableContentTypes = array_map('strval', array_keys(AcceptHeader::fromString($this->headers->get('Accept'))->all()));
1779 }
1780
1781 /**
1782 * Returns true if the request is an XMLHttpRequest.
1783 *
1784 * It works if your JavaScript library sets an X-Requested-With HTTP header.
1785 * It is known to work with common JavaScript frameworks:
1786 *
1787 * @see https://wikipedia.org/wiki/List_of_Ajax_frameworks#JavaScript
1788 *
1789 * @return bool
1790 */
1791 public function isXmlHttpRequest()
1792 {
1793 return 'XMLHttpRequest' == $this->headers->get('X-Requested-With');
1794 }
1795
1796 /**
1797 * Checks whether the client browser prefers safe content or not according to RFC8674.
1798 *
1799 * @see https://tools.ietf.org/html/rfc8674
1800 */
1801 public function preferSafeContent(): bool
1802 {
1803 if (null !== $this->isSafeContentPreferred) {
1804 return $this->isSafeContentPreferred;
1805 }
1806
1807 if (!$this->isSecure()) {
1808 // see https://tools.ietf.org/html/rfc8674#section-3
1809 return $this->isSafeContentPreferred = false;
1810 }
1811
1812 return $this->isSafeContentPreferred = AcceptHeader::fromString($this->headers->get('Prefer'))->has('safe');
1813 }
1814
1815 /*
1816 * The following methods are derived from code of the Zend Framework (1.10dev - 2010-01-24)
1817 *
1818 * Code subject to the new BSD license (https://framework.zend.com/license).
1819 *
1820 * Copyright (c) 2005-2010 Zend Technologies USA Inc. (https://www.zend.com/)
1821 */
1822
1823 protected function prepareRequestUri()
1824 {
1825 $requestUri = '';
1826
1827 if ($this->isIisRewrite() && '' != $this->server->get('UNENCODED_URL')) {
1828 // IIS7 with URL Rewrite: make sure we get the unencoded URL (double slash problem)
1829 $requestUri = $this->server->get('UNENCODED_URL');
1830 $this->server->remove('UNENCODED_URL');
1831 } elseif ($this->server->has('REQUEST_URI')) {
1832 $requestUri = $this->server->get('REQUEST_URI');
1833
1834 if ('' !== $requestUri && '/' === $requestUri[0]) {
1835 // To only use path and query remove the fragment.
1836 if (false !== $pos = strpos($requestUri, '#')) {
1837 $requestUri = substr($requestUri, 0, $pos);
1838 }
1839 } else {
1840 // HTTP proxy reqs setup request URI with scheme and host [and port] + the URL path,
1841 // only use URL path.
1842 $uriComponents = parse_url($requestUri);
1843
1844 if (isset($uriComponents['path'])) {
1845 $requestUri = $uriComponents['path'];
1846 }
1847
1848 if (isset($uriComponents['query'])) {
1849 $requestUri .= '?'.$uriComponents['query'];
1850 }
1851 }
1852 } elseif ($this->server->has('ORIG_PATH_INFO')) {
1853 // IIS 5.0, PHP as CGI
1854 $requestUri = $this->server->get('ORIG_PATH_INFO');
1855 if ('' != $this->server->get('QUERY_STRING')) {
1856 $requestUri .= '?'.$this->server->get('QUERY_STRING');
1857 }
1858 $this->server->remove('ORIG_PATH_INFO');
1859 }
1860
1861 // normalize the request URI to ease creating sub-requests from this request
1862 $this->server->set('REQUEST_URI', $requestUri);
1863
1864 return $requestUri;
1865 }
1866
1867 /**
1868 * Prepares the base URL.
1869 *
1870 * @return string
1871 */
1872 protected function prepareBaseUrl()
1873 {
1874 $filename = basename($this->server->get('SCRIPT_FILENAME', ''));
1875
1876 if (basename($this->server->get('SCRIPT_NAME', '')) === $filename) {
1877 $baseUrl = $this->server->get('SCRIPT_NAME');
1878 } elseif (basename($this->server->get('PHP_SELF', '')) === $filename) {
1879 $baseUrl = $this->server->get('PHP_SELF');
1880 } elseif (basename($this->server->get('ORIG_SCRIPT_NAME', '')) === $filename) {
1881 $baseUrl = $this->server->get('ORIG_SCRIPT_NAME'); // 1and1 shared hosting compatibility
1882 } else {
1883 // Backtrack up the script_filename to find the portion matching
1884 // php_self
1885 $path = $this->server->get('PHP_SELF', '');
1886 $file = $this->server->get('SCRIPT_FILENAME', '');
1887 $segs = explode('/', trim($file, '/'));
1888 $segs = array_reverse($segs);
1889 $index = 0;
1890 $last = \count($segs);
1891 $baseUrl = '';
1892 do {
1893 $seg = $segs[$index];
1894 $baseUrl = '/'.$seg.$baseUrl;
1895 ++$index;
1896 } while ($last > $index && (false !== $pos = strpos($path, $baseUrl)) && 0 != $pos);
1897 }
1898
1899 // Does the baseUrl have anything in common with the request_uri?
1900 $requestUri = $this->getRequestUri();
1901 if ('' !== $requestUri && '/' !== $requestUri[0]) {
1902 $requestUri = '/'.$requestUri;
1903 }
1904
1905 if ($baseUrl && null !== $prefix = $this->getUrlencodedPrefix($requestUri, $baseUrl)) {
1906 // full $baseUrl matches
1907 return $prefix;
1908 }
1909
1910 if ($baseUrl && null !== $prefix = $this->getUrlencodedPrefix($requestUri, rtrim(\dirname($baseUrl), '/'.\DIRECTORY_SEPARATOR).'/')) {
1911 // directory portion of $baseUrl matches
1912 return rtrim($prefix, '/'.\DIRECTORY_SEPARATOR);
1913 }
1914
1915 $truncatedRequestUri = $requestUri;
1916 if (false !== $pos = strpos($requestUri, '?')) {
1917 $truncatedRequestUri = substr($requestUri, 0, $pos);
1918 }
1919
1920 $basename = basename($baseUrl ?? '');
1921 if (empty($basename) || !strpos(rawurldecode($truncatedRequestUri), $basename)) {
1922 // no match whatsoever; set it blank
1923 return '';
1924 }
1925
1926 // If using mod_rewrite or ISAPI_Rewrite strip the script filename
1927 // out of baseUrl. $pos !== 0 makes sure it is not matching a value
1928 // from PATH_INFO or QUERY_STRING
1929 if (\strlen($requestUri) >= \strlen($baseUrl) && (false !== $pos = strpos($requestUri, $baseUrl)) && 0 !== $pos) {
1930 $baseUrl = substr($requestUri, 0, $pos + \strlen($baseUrl));
1931 }
1932
1933 return rtrim($baseUrl, '/'.\DIRECTORY_SEPARATOR);
1934 }
1935
1936 /**
1937 * Prepares the base path.
1938 *
1939 * @return string
1940 */
1941 protected function prepareBasePath()
1942 {
1943 $baseUrl = $this->getBaseUrl();
1944 if (empty($baseUrl)) {
1945 return '';
1946 }
1947
1948 $filename = basename($this->server->get('SCRIPT_FILENAME'));
1949 if (basename($baseUrl) === $filename) {
1950 $basePath = \dirname($baseUrl);
1951 } else {
1952 $basePath = $baseUrl;
1953 }
1954
1955 if ('\\' === \DIRECTORY_SEPARATOR) {
1956 $basePath = str_replace('\\', '/', $basePath);
1957 }
1958
1959 return rtrim($basePath, '/');
1960 }
1961
1962 /**
1963 * Prepares the path info.
1964 *
1965 * @return string
1966 */
1967 protected function preparePathInfo()
1968 {
1969 if (null === ($requestUri = $this->getRequestUri())) {
1970 return '/';
1971 }
1972
1973 // Remove the query string from REQUEST_URI
1974 if (false !== $pos = strpos($requestUri, '?')) {
1975 $requestUri = substr($requestUri, 0, $pos);
1976 }
1977 if ('' !== $requestUri && '/' !== $requestUri[0]) {
1978 $requestUri = '/'.$requestUri;
1979 }
1980
1981 if (null === ($baseUrl = $this->getBaseUrlReal())) {
1982 return $requestUri;
1983 }
1984
1985 $pathInfo = substr($requestUri, \strlen($baseUrl));
1986 if (false === $pathInfo || '' === $pathInfo || '/' !== $pathInfo[0]) {
1987 return '/'.$pathInfo;
1988 }
1989
1990 return $pathInfo;
1991 }
1992
1993 /**
1994 * Initializes HTTP request formats.
1995 */
1996 protected static function initializeFormats()
1997 {
1998 static::$formats = [
1999 'html' => ['text/html', 'application/xhtml+xml'],
2000 'txt' => ['text/plain'],
2001 'js' => ['application/javascript', 'application/x-javascript', 'text/javascript'],
2002 'css' => ['text/css'],
2003 'json' => ['application/json', 'application/x-json'],
2004 'jsonld' => ['application/ld+json'],
2005 'xml' => ['text/xml', 'application/xml', 'application/x-xml'],
2006 'rdf' => ['application/rdf+xml'],
2007 'atom' => ['application/atom+xml'],
2008 'rss' => ['application/rss+xml'],
2009 'form' => ['application/x-www-form-urlencoded', 'multipart/form-data'],
2010 ];
2011 }
2012
2013 private function setPhpDefaultLocale(string $locale): void
2014 {
2015 // if either the class Locale doesn't exist, or an exception is thrown when
2016 // setting the default locale, the intl module is not installed, and
2017 // the call can be ignored:
2018 try {
2019 if (class_exists(\Locale::class, false)) {
2020 \Locale::setDefault($locale);
2021 }
2022 } catch (\Exception $e) {
2023 }
2024 }
2025
2026 /**
2027 * Returns the prefix as encoded in the string when the string starts with
2028 * the given prefix, null otherwise.
2029 */
2030 private function getUrlencodedPrefix(string $string, string $prefix): ?string
2031 {
2032 if ($this->isIisRewrite()) {
2033 // ISS with UrlRewriteModule might report SCRIPT_NAME/PHP_SELF with wrong case
2034 // see https://github.com/php/php-src/issues/11981
2035 if (0 !== stripos(rawurldecode($string), $prefix)) {
2036 return null;
2037 }
2038 } elseif (!str_starts_with(rawurldecode($string), $prefix)) {
2039 return null;
2040 }
2041
2042 $len = \strlen($prefix);
2043
2044 if (preg_match(sprintf('#^(%%[[:xdigit:]]{2}|.){%d}#', $len), $string, $match)) {
2045 return $match[0];
2046 }
2047
2048 return null;
2049 }
2050
2051 private static function createRequestFromFactory(array $query = [], array $request = [], array $attributes = [], array $cookies = [], array $files = [], array $server = [], $content = null): self
2052 {
2053 if (self::$requestFactory) {
2054 $request = (self::$requestFactory)($query, $request, $attributes, $cookies, $files, $server, $content);
2055
2056 if (!$request instanceof self) {
2057 throw new \LogicException('The Request factory must return an instance of Symfony\Component\HttpFoundation\Request.');
2058 }
2059
2060 return $request;
2061 }
2062
2063 return new static($query, $request, $attributes, $cookies, $files, $server, $content);
2064 }
2065
2066 /**
2067 * Indicates whether this request originated from a trusted proxy.
2068 *
2069 * This can be useful to determine whether or not to trust the
2070 * contents of a proxy-specific header.
2071 *
2072 * @return bool
2073 */
2074 public function isFromTrustedProxy()
2075 {
2076 return self::$trustedProxies && IpUtils::checkIp($this->server->get('REMOTE_ADDR', ''), self::$trustedProxies);
2077 }
2078
2079 private function getTrustedValues(int $type, ?string $ip = null): array
2080 {
2081 $clientValues = [];
2082 $forwardedValues = [];
2083
2084 if ((self::$trustedHeaderSet & $type) && $this->headers->has(self::TRUSTED_HEADERS[$type])) {
2085 foreach (explode(',', $this->headers->get(self::TRUSTED_HEADERS[$type])) as $v) {
2086 $clientValues[] = (self::HEADER_X_FORWARDED_PORT === $type ? '0.0.0.0:' : '').trim($v);
2087 }
2088 }
2089
2090 if ((self::$trustedHeaderSet & self::HEADER_FORWARDED) && (isset(self::FORWARDED_PARAMS[$type])) && $this->headers->has(self::TRUSTED_HEADERS[self::HEADER_FORWARDED])) {
2091 $forwarded = $this->headers->get(self::TRUSTED_HEADERS[self::HEADER_FORWARDED]);
2092 $parts = HeaderUtils::split($forwarded, ',;=');
2093 $forwardedValues = [];
2094 $param = self::FORWARDED_PARAMS[$type];
2095 foreach ($parts as $subParts) {
2096 if (null === $v = HeaderUtils::combine($subParts)[$param] ?? null) {
2097 continue;
2098 }
2099 if (self::HEADER_X_FORWARDED_PORT === $type) {
2100 if (str_ends_with($v, ']') || false === $v = strrchr($v, ':')) {
2101 $v = $this->isSecure() ? ':443' : ':80';
2102 }
2103 $v = '0.0.0.0'.$v;
2104 }
2105 $forwardedValues[] = $v;
2106 }
2107 }
2108
2109 if (null !== $ip) {
2110 $clientValues = $this->normalizeAndFilterClientIps($clientValues, $ip);
2111 $forwardedValues = $this->normalizeAndFilterClientIps($forwardedValues, $ip);
2112 }
2113
2114 if ($forwardedValues === $clientValues || !$clientValues) {
2115 return $forwardedValues;
2116 }
2117
2118 if (!$forwardedValues) {
2119 return $clientValues;
2120 }
2121
2122 if (!$this->isForwardedValid) {
2123 return null !== $ip ? ['0.0.0.0', $ip] : [];
2124 }
2125 $this->isForwardedValid = false;
2126
2127 throw new ConflictingHeadersException(sprintf('The request has both a trusted "%s" header and a trusted "%s" header, conflicting with each other. You should either configure your proxy to remove one of them, or configure your project to distrust the offending one.', self::TRUSTED_HEADERS[self::HEADER_FORWARDED], self::TRUSTED_HEADERS[$type]));
2128 }
2129
2130 private function normalizeAndFilterClientIps(array $clientIps, string $ip): array
2131 {
2132 if (!$clientIps) {
2133 return [];
2134 }
2135 $clientIps[] = $ip; // Complete the IP chain with the IP the request actually came from
2136 $firstTrustedIp = null;
2137
2138 foreach ($clientIps as $key => $clientIp) {
2139 if (strpos($clientIp, '.')) {
2140 // Strip :port from IPv4 addresses. This is allowed in Forwarded
2141 // and may occur in X-Forwarded-For.
2142 $i = strpos($clientIp, ':');
2143 if ($i) {
2144 $clientIps[$key] = $clientIp = substr($clientIp, 0, $i);
2145 }
2146 } elseif (str_starts_with($clientIp, '[')) {
2147 // Strip brackets and :port from IPv6 addresses.
2148 $i = strpos($clientIp, ']', 1);
2149 $clientIps[$key] = $clientIp = substr($clientIp, 1, $i - 1);
2150 }
2151
2152 if (!filter_var($clientIp, \FILTER_VALIDATE_IP)) {
2153 unset($clientIps[$key]);
2154
2155 continue;
2156 }
2157
2158 if (IpUtils::checkIp($clientIp, self::$trustedProxies)) {
2159 unset($clientIps[$key]);
2160
2161 // Fallback to this when the client IP falls into the range of trusted proxies
2162 if (null === $firstTrustedIp) {
2163 $firstTrustedIp = $clientIp;
2164 }
2165 }
2166 }
2167
2168 // Now the IP chain contains only untrusted proxies and the client IP
2169 return $clientIps ? array_reverse($clientIps) : [$firstTrustedIp];
2170 }
2171
2172 /**
2173 * Is this IIS with UrlRewriteModule?
2174 *
2175 * This method consumes, caches and removed the IIS_WasUrlRewritten env var,
2176 * so we don't inherit it to sub-requests.
2177 */
2178 private function isIisRewrite(): bool
2179 {
2180 if (1 === $this->server->getInt('IIS_WasUrlRewritten')) {
2181 $this->isIisRewrite = true;
2182 $this->server->remove('IIS_WasUrlRewritten');
2183 }
2184
2185 return $this->isIisRewrite;
2186 }
2187 }
2188