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
← All changes | vendor/symfony/http-foundation/Request.php +243 -141 3.0 → 3.5.9 View file →
@@ -10,9 +10,12 @@
10 10 */
11 11
12 12 namespace Symfony\Component\HttpFoundation;
13 13
14 +use Symfony\Component\HttpFoundation\Exception\BadRequestException;
14 15 use Symfony\Component\HttpFoundation\Exception\ConflictingHeadersException;
16 +use Symfony\Component\HttpFoundation\Exception\JsonException;
17 +use Symfony\Component\HttpFoundation\Exception\SessionNotFoundException;
15 18 use Symfony\Component\HttpFoundation\Exception\SuspiciousOperationException;
16 19 use Symfony\Component\HttpFoundation\Session\SessionInterface;
17 20
18 21 // Help opcache.preload discover always-needed symbols
@@ -19,8 +22,9 @@
19 22 class_exists(AcceptHeader::class);
20 23 class_exists(FileBag::class);
21 24 class_exists(HeaderBag::class);
22 25 class_exists(HeaderUtils::class);
26 +class_exists(InputBag::class);
23 27 class_exists(ParameterBag::class);
24 28 class_exists(ServerBag::class);
25 29
26 30 /**
@@ -37,16 +41,20 @@
37 41 * @author Fabien Potencier <[email protected]>
38 42 */
39 43 class Request
40 44 {
41 - public const HEADER_FORWARDED = 0b00001; // When using RFC 7239
42 - public const HEADER_X_FORWARDED_FOR = 0b00010;
43 - public const HEADER_X_FORWARDED_HOST = 0b00100;
44 - public const HEADER_X_FORWARDED_PROTO = 0b01000;
45 - public const HEADER_X_FORWARDED_PORT = 0b10000;
46 - public const HEADER_X_FORWARDED_ALL = 0b11110; // All "X-Forwarded-*" headers
47 - public const HEADER_X_FORWARDED_AWS_ELB = 0b11010; // AWS ELB doesn't send X-Forwarded-Host
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;
48 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 +
49 57 public const METHOD_HEAD = 'HEAD';
50 58 public const METHOD_GET = 'GET';
51 59 public const METHOD_POST = 'POST';
52 60 public const METHOD_PUT = 'PUT';
@@ -83,9 +91,9 @@
83 91
84 92 /**
85 93 * Request body parameters ($_POST).
86 94 *
87 - * @var ParameterBag
95 + * @var InputBag
88 96 */
89 97 public $request;
90 98
91 99 /**
@@ -90,9 +98,9 @@
90 98
91 99 /**
92 100 * Query string parameters ($_GET).
93 101 *
94 - * @var ParameterBag
102 + * @var InputBag
95 103 */
96 104 public $query;
97 105
98 106 /**
@@ -111,9 +119,9 @@
111 119
112 120 /**
113 121 * Cookies ($_COOKIE).
114 122 *
115 - * @var ParameterBag
123 + * @var InputBag
116 124 */
117 125 public $cookies;
118 126
119 127 /**
@@ -178,14 +186,14 @@
178 186 */
179 187 protected $format;
180 188
181 189 /**
182 - * @var SessionInterface|callable
190 + * @var SessionInterface|callable(): SessionInterface
183 191 */
184 192 protected $session;
185 193
186 194 /**
187 - * @var string
195 + * @var string|null
188 196 */
189 197 protected $locale;
190 198
191 199 /**
@@ -206,8 +214,13 @@
206 214 private $preferredFormat;
207 215 private $isHostValid = true;
208 216 private $isForwardedValid = true;
209 217
218 + /**
219 + * @var bool|null
220 + */
221 + private $isSafeContentPreferred;
222 +
210 223 private static $trustedHeaderSet = -1;
211 224
212 225 private const FORWARDED_PARAMS = [
213 226 self::HEADER_X_FORWARDED_FOR => 'for',
@@ -230,10 +243,14 @@
230 243 self::HEADER_X_FORWARDED_FOR => 'X_FORWARDED_FOR',
231 244 self::HEADER_X_FORWARDED_HOST => 'X_FORWARDED_HOST',
232 245 self::HEADER_X_FORWARDED_PROTO => 'X_FORWARDED_PROTO',
233 246 self::HEADER_X_FORWARDED_PORT => 'X_FORWARDED_PORT',
247 + self::HEADER_X_FORWARDED_PREFIX => 'X_FORWARDED_PREFIX',
234 248 ];
235 249
250 + /** @var bool */
251 + private $isIisRewrite = false;
252 +
236 253 /**
237 254 * @param array $query The GET parameters
238 255 * @param array $request The POST parameters
239 256 * @param array $attributes The request attributes (parameters parsed from the PATH_INFO, ...)
@@ -261,12 +278,12 @@
261 278 * @param string|resource|null $content The raw body data
262 279 */
263 280 public function initialize(array $query = [], array $request = [], array $attributes = [], array $cookies = [], array $files = [], array $server = [], $content = null)
264 281 {
265 - $this->request = new ParameterBag($request);
266 - $this->query = new ParameterBag($query);
282 + $this->request = new InputBag($request);
283 + $this->query = new InputBag($query);
267 284 $this->attributes = new ParameterBag($attributes);
268 - $this->cookies = new ParameterBag($cookies);
285 + $this->cookies = new InputBag($cookies);
269 286 $this->files = new FileBag($files);
270 287 $this->server = new ServerBag($server);
271 288 $this->headers = new HeaderBag($this->server->getHeaders());
272 289
@@ -295,9 +312,9 @@
295 312 if (str_starts_with($request->headers->get('CONTENT_TYPE', ''), 'application/x-www-form-urlencoded')
296 313 && \in_array(strtoupper($request->server->get('REQUEST_METHOD', 'GET')), ['PUT', 'DELETE', 'PATCH'])
297 314 ) {
298 315 parse_str($request->getContent(), $data);
299 - $request->request = new ParameterBag($data);
316 + $request->request = new InputBag($data);
300 317 }
301 318
302 319 return $request;
303 320 }
@@ -316,10 +333,12 @@
316 333 * @param array $server The server parameters ($_SERVER)
317 334 * @param string|resource|null $content The raw body data
318 335 *
319 336 * @return static
337 + *
338 + * @throws BadRequestException When the URI is invalid
320 339 */
321 - public static function create($uri, $method = 'GET', $parameters = [], $cookies = [], $files = [], $server = [], $content = null)
340 + public static function create(string $uri, string $method = 'GET', array $parameters = [], array $cookies = [], array $files = [], array $server = [], $content = null)
322 341 {
323 342 $server = array_replace([
324 343 'SERVER_NAME' => 'localhost',
325 344 'SERVER_PORT' => 80,
@@ -338,9 +357,22 @@
338 357
339 358 $server['PATH_INFO'] = '';
340 359 $server['REQUEST_METHOD'] = strtoupper($method);
341 360
342 - $components = parse_url($uri);
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 +
343 375 if (isset($components['host'])) {
344 376 $server['SERVER_NAME'] = $components['host'];
345 377 $server['HTTP_HOST'] = $components['host'];
346 378 }
@@ -416,12 +448,10 @@
416 448 *
417 449 * This is mainly useful when you need to override the Request class
418 450 * to keep BC with an existing system. It should not be used for any
419 451 * other purpose.
420 - *
421 - * @param callable|null $callable A PHP callable
422 452 */
423 - public static function setFactory($callable)
453 + public static function setFactory(?callable $callable)
424 454 {
425 455 self::$requestFactory = $callable;
426 456 }
427 457
@@ -427,31 +457,31 @@
427 457
428 458 /**
429 459 * Clones a request and overrides some of its parameters.
430 460 *
431 - * @param array $query The GET parameters
432 - * @param array $request The POST parameters
433 - * @param array $attributes The request attributes (parameters parsed from the PATH_INFO, ...)
434 - * @param array $cookies The COOKIE parameters
435 - * @param array $files The FILES parameters
436 - * @param array $server The SERVER parameters
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
437 467 *
438 468 * @return static
439 469 */
440 - public function duplicate(array $query = null, array $request = null, array $attributes = null, array $cookies = null, array $files = null, array $server = null)
470 + public function duplicate(?array $query = null, ?array $request = null, ?array $attributes = null, ?array $cookies = null, ?array $files = null, ?array $server = null)
441 471 {
442 472 $dup = clone $this;
443 473 if (null !== $query) {
444 - $dup->query = new ParameterBag($query);
474 + $dup->query = new InputBag($query);
445 475 }
446 476 if (null !== $request) {
447 - $dup->request = new ParameterBag($request);
477 + $dup->request = new InputBag($request);
448 478 }
449 479 if (null !== $attributes) {
450 480 $dup->attributes = new ParameterBag($attributes);
451 481 }
452 482 if (null !== $cookies) {
453 - $dup->cookies = new ParameterBag($cookies);
483 + $dup->cookies = new InputBag($cookies);
454 484 }
455 485 if (null !== $files) {
456 486 $dup->files = new FileBag($files);
457 487 }
@@ -500,9 +530,9 @@
500 530
501 531 /**
502 532 * Returns the request as a string.
503 533 *
504 - * @return string The request
534 + * @return string
505 535 */
506 536 public function __toString()
507 537 {
508 538 $content = $this->getContent();
@@ -510,12 +540,12 @@
510 540 $cookieHeader = '';
511 541 $cookies = [];
512 542
513 543 foreach ($this->cookies as $k => $v) {
514 - $cookies[] = $k.'='.$v;
544 + $cookies[] = \is_array($v) ? http_build_query([$k => $v], '', '; ', \PHP_QUERY_RFC3986) : "$k=$v";
515 545 }
516 546
517 - if (!empty($cookies)) {
547 + if ($cookies) {
518 548 $cookieHeader = 'Cookie: '.implode('; ', $cookies)."\r\n";
519 549 }
520 550
521 551 return
@@ -572,8 +602,11 @@
572 602 * @param int $trustedHeaderSet A bit field of Request::HEADER_*, to set which headers to trust from your proxies
573 603 */
574 604 public static function setTrustedProxies(array $proxies, int $trustedHeaderSet)
575 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 + }
576 609 self::$trustedProxies = array_reduce($proxies, function ($proxies, $proxy) {
577 610 if ('REMOTE_ADDR' !== $proxy) {
578 611 $proxies[] = $proxy;
579 612 } elseif (isset($_SERVER['REMOTE_ADDR'])) {
@@ -587,9 +620,9 @@
587 620
588 621 /**
589 622 * Gets the list of trusted proxies.
590 623 *
591 - * @return array An array of trusted proxies
624 + * @return array
592 625 */
593 626 public static function getTrustedProxies()
594 627 {
595 628 return self::$trustedProxies;
@@ -623,9 +656,9 @@
623 656
624 657 /**
625 658 * Gets the list of trusted host patterns.
626 659 *
627 - * @return array An array of trusted host patterns
660 + * @return array
628 661 */
629 662 public static function getTrustedHosts()
630 663 {
631 664 return self::$trustedHostPatterns;
@@ -636,19 +669,17 @@
636 669 *
637 670 * It builds a normalized query string, where keys/value pairs are alphabetized,
638 671 * have consistent escaping and unneeded delimiters are removed.
639 672 *
640 - * @param string $qs Query string
641 - *
642 - * @return string A normalized query string for the Request
673 + * @return string
643 674 */
644 - public static function normalizeQueryString($qs)
675 + public static function normalizeQueryString(?string $qs)
645 676 {
646 677 if ('' === ($qs ?? '')) {
647 678 return '';
648 679 }
649 680
650 - parse_str($qs, $qs);
681 + $qs = HeaderUtils::parseQuery($qs);
651 682 ksort($qs);
652 683
653 684 return http_build_query($qs, '', '&', \PHP_QUERY_RFC3986);
654 685 }
@@ -671,9 +702,9 @@
671 702
672 703 /**
673 704 * Checks whether support for the _method request parameter is enabled.
674 705 *
675 - * @return bool True when the _method request parameter is enabled, false otherwise
706 + * @return bool
676 707 */
677 708 public static function getHttpMethodParameterOverride()
678 709 {
679 710 return self::$httpMethodParameterOverride;
@@ -687,25 +718,26 @@
687 718 * public property instead (attributes, query, request).
688 719 *
689 720 * Order of precedence: PATH (routing placeholders or custom attributes), GET, POST
690 721 *
691 - * @param string $key The key
692 - * @param mixed $default The default value if the parameter key does not exist
722 + * @param mixed $default The default value if the parameter key does not exist
693 723 *
694 724 * @return mixed
725 + *
726 + * @internal since Symfony 5.4, use explicit input sources instead
695 727 */
696 - public function get($key, $default = null)
728 + public function get(string $key, $default = null)
697 729 {
698 730 if ($this !== $result = $this->attributes->get($key, $this)) {
699 731 return $result;
700 732 }
701 733
702 - if ($this !== $result = $this->query->get($key, $this)) {
703 - return $result;
734 + if ($this->query->has($key)) {
735 + return $this->query->all()[$key];
704 736 }
705 737
706 - if ($this !== $result = $this->request->get($key, $this)) {
707 - return $result;
738 + if ($this->request->has($key)) {
739 + return $this->request->all()[$key];
708 740 }
709 741
710 742 return $default;
711 743 }
@@ -712,9 +744,9 @@
712 744
713 745 /**
714 746 * Gets the Session.
715 747 *
716 - * @return SessionInterface The session
748 + * @return SessionInterface
717 749 */
718 750 public function getSession()
719 751 {
720 752 $session = $this->session;
@@ -722,10 +754,9 @@
722 754 $this->setSession($session = $session());
723 755 }
724 756
725 757 if (null === $session) {
726 - @trigger_error(sprintf('Calling "%s()" when no session has been set is deprecated since Symfony 4.1 and will throw an exception in 5.0. Use "hasSession()" instead.', __METHOD__), \E_USER_DEPRECATED);
727 - // throw new \BadMethodCallException('Session has not been set.');
758 + throw new SessionNotFoundException('Session has not been set.');
728 759 }
729 760
730 761 return $session;
731 762 }
@@ -748,13 +779,17 @@
748 779 * This method does not give any information about the state of the session object,
749 780 * like whether the session is started or not. It is just a way to check if this Request
750 781 * is associated with a Session instance.
751 782 *
752 - * @return bool true when the Request contains a Session object, false otherwise
783 + * @param bool $skipIfUninitialized When true, ignores factories injected by `setSessionFactory`
784 + *
785 + * @return bool
753 786 */
754 - public function hasSession()
787 + public function hasSession(/* bool $skipIfUninitialized = false */)
755 788 {
756 - return null !== $this->session;
789 + $skipIfUninitialized = \func_num_args() > 0 ? func_get_arg(0) : false;
790 +
791 + return null !== $this->session && (!$skipIfUninitialized || $this->session instanceof SessionInterface);
757 792 }
758 793
759 794 public function setSession(SessionInterface $session)
760 795 {
@@ -762,8 +797,10 @@
762 797 }
763 798
764 799 /**
765 800 * @internal
801 + *
802 + * @param callable(): SessionInterface $factory
766 803 */
767 804 public function setSessionFactory(callable $factory)
768 805 {
769 806 $this->session = $factory;
@@ -777,9 +814,9 @@
777 814 * but this is also the least trusted one. Trusted proxies are stripped.
778 815 *
779 816 * Use this method carefully; you should use getClientIp() instead.
780 817 *
781 - * @return array The client IP addresses
818 + * @return array
782 819 *
783 820 * @see getClientIp()
784 821 */
785 822 public function getClientIps()
@@ -805,9 +842,9 @@
805 842 * If your reverse proxy uses a different header name than "X-Forwarded-For",
806 843 * ("Client-Ip" for instance), configure it via the $trustedHeaderSet
807 844 * argument of the Request::setTrustedProxies() method instead.
808 845 *
809 - * @return string|null The client IP address
846 + * @return string|null
810 847 *
811 848 * @see getClientIps()
812 849 * @see https://wikipedia.org/wiki/X-Forwarded-For
813 850 */
@@ -883,8 +920,26 @@
883 920 * @return string The raw URL (i.e. not urldecoded)
884 921 */
885 922 public function getBaseUrl()
886 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 + {
887 942 if (null === $this->baseUrl) {
888 943 $this->baseUrl = $this->prepareBaseUrl();
889 944 }
890 945
@@ -908,9 +963,9 @@
908 963 * when trusted proxies were set via "setTrustedProxies()".
909 964 *
910 965 * The "X-Forwarded-Port" header must contain the client port.
911 966 *
912 - * @return int|string can be a string if fetched from the server bag
967 + * @return int|string|null Can be a string if fetched from the server bag
913 968 */
914 969 public function getPort()
915 970 {
916 971 if ($this->isFromTrustedProxy() && $host = $this->getTrustedValues(self::HEADER_X_FORWARDED_PORT)) {
@@ -1009,9 +1064,9 @@
1009 1064 *
1010 1065 * If the URL was called with basic authentication, the user
1011 1066 * and the password are not added to the generated string.
1012 1067 *
1013 - * @return string The scheme and HTTP host
1068 + * @return string
1014 1069 */
1015 1070 public function getSchemeAndHttpHost()
1016 1071 {
1017 1072 return $this->getScheme().'://'.$this->getHttpHost();
@@ -1019,9 +1074,9 @@
1019 1074
1020 1075 /**
1021 1076 * Generates a normalized URI (URL) for the Request.
1022 1077 *
1023 - * @return string A normalized URI (URL) for the Request
1078 + * @return string
1024 1079 *
1025 1080 * @see getQueryString()
1026 1081 */
1027 1082 public function getUri()
@@ -1037,11 +1092,11 @@
1037 1092 * Generates a normalized URI for the given path.
1038 1093 *
1039 1094 * @param string $path A path to use instead of the current one
1040 1095 *
1041 - * @return string The normalized URI for the path
1096 + * @return string
1042 1097 */
1043 - public function getUriForPath($path)
1098 + public function getUriForPath(string $path)
1044 1099 {
1045 1100 return $this->getSchemeAndHttpHost().$this->getBaseUrl().$path;
1046 1101 }
1047 1102
@@ -1059,13 +1114,11 @@
1059 1114 * - "/a/b/" -> "../"
1060 1115 * - "/a/b/c/other" -> "other"
1061 1116 * - "/a/x/y" -> "../../x/y"
1062 1117 *
1063 - * @param string $path The target path
1064 - *
1065 - * @return string The relative target path
1118 + * @return string
1066 1119 */
1067 - public function getRelativeUriForPath($path)
1120 + public function getRelativeUriForPath(string $path)
1068 1121 {
1069 1122 // be sure that we are dealing with an absolute path
1070 1123 if (!isset($path[0]) || '/' !== $path[0]) {
1071 1124 return $path;
@@ -1105,9 +1158,9 @@
1105 1158 *
1106 1159 * It builds a normalized query string, where keys/value pairs are alphabetized
1107 1160 * and have consistent escaping.
1108 1161 *
1109 - * @return string|null A normalized query string for the Request
1162 + * @return string|null
1110 1163 */
1111 1164 public function getQueryString()
1112 1165 {
1113 1166 $qs = static::normalizeQueryString($this->server->get('QUERY_STRING'));
@@ -1201,12 +1254,10 @@
1201 1254 }
1202 1255
1203 1256 /**
1204 1257 * Sets the request method.
1205 - *
1206 - * @param string $method
1207 1258 */
1208 - public function setMethod($method)
1259 + public function setMethod(string $method)
1209 1260 {
1210 1261 $this->method = null;
1211 1262 $this->server->set('REQUEST_METHOD', $method);
1212 1263 }
@@ -1221,9 +1272,9 @@
1221 1272 * but only if enableHttpMethodParameterOverride() has been called.
1222 1273 *
1223 1274 * The method is always an uppercased string.
1224 1275 *
1225 - * @return string The request method
1276 + * @return string
1226 1277 *
1227 1278 * @see getRealMethod()
1228 1279 */
1229 1280 public function getMethod()
@@ -1254,9 +1305,9 @@
1254 1305 return $this->method = $method;
1255 1306 }
1256 1307
1257 1308 if (!preg_match('/^[A-Z]++$/D', $method)) {
1258 - throw new SuspiciousOperationException(sprintf('Invalid method override "%s".', $method));
1309 + throw new SuspiciousOperationException('Invalid HTTP method override.');
1259 1310 }
1260 1311
1261 1312 return $this->method = $method;
1262 1313 }
@@ -1263,9 +1314,9 @@
1263 1314
1264 1315 /**
1265 1316 * Gets the "real" request method.
1266 1317 *
1267 - * @return string The request method
1318 + * @return string
1268 1319 *
1269 1320 * @see getMethod()
1270 1321 */
1271 1322 public function getRealMethod()
@@ -1275,13 +1326,11 @@
1275 1326
1276 1327 /**
1277 1328 * Gets the mime type associated with the format.
1278 1329 *
1279 - * @param string $format The format
1280 - *
1281 - * @return string|null The associated mime type (null if not found)
1330 + * @return string|null
1282 1331 */
1283 - public function getMimeType($format)
1332 + public function getMimeType(string $format)
1284 1333 {
1285 1334 if (null === static::$formats) {
1286 1335 static::initializeFormats();
1287 1336 }
@@ -1291,13 +1340,11 @@
1291 1340
1292 1341 /**
1293 1342 * Gets the mime types associated with the format.
1294 1343 *
1295 - * @param string $format The format
1296 - *
1297 - * @return array The associated mime types
1344 + * @return array
1298 1345 */
1299 - public static function getMimeTypes($format)
1346 + public static function getMimeTypes(string $format)
1300 1347 {
1301 1348 if (null === static::$formats) {
1302 1349 static::initializeFormats();
1303 1350 }
@@ -1307,16 +1354,14 @@
1307 1354
1308 1355 /**
1309 1356 * Gets the format associated with the mime type.
1310 1357 *
1311 - * @param string $mimeType The associated mime type
1312 - *
1313 - * @return string|null The format (null if not found)
1358 + * @return string|null
1314 1359 */
1315 - public function getFormat($mimeType)
1360 + public function getFormat(?string $mimeType)
1316 1361 {
1317 1362 $canonicalMimeType = null;
1318 - if (false !== $pos = strpos($mimeType, ';')) {
1363 + if ($mimeType && false !== $pos = strpos($mimeType, ';')) {
1319 1364 $canonicalMimeType = trim(substr($mimeType, 0, $pos));
1320 1365 }
1321 1366
1322 1367 if (null === static::$formats) {
@@ -1337,12 +1382,11 @@
1337 1382
1338 1383 /**
1339 1384 * Associates a format with mime types.
1340 1385 *
1341 - * @param string $format The format
1342 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)
1343 1387 */
1344 - public function setFormat($format, $mimeTypes)
1388 + public function setFormat(?string $format, $mimeTypes)
1345 1389 {
1346 1390 if (null === static::$formats) {
1347 1391 static::initializeFormats();
1348 1392 }
@@ -1360,13 +1404,11 @@
1360 1404 * * $default
1361 1405 *
1362 1406 * @see getPreferredFormat
1363 1407 *
1364 - * @param string|null $default The default format
1365 - *
1366 - * @return string|null The request format
1408 + * @return string|null
1367 1409 */
1368 - public function getRequestFormat($default = 'html')
1410 + public function getRequestFormat(?string $default = 'html')
1369 1411 {
1370 1412 if (null === $this->format) {
1371 1413 $this->format = $this->attributes->get('_format');
1372 1414 }
@@ -1375,12 +1417,10 @@
1375 1417 }
1376 1418
1377 1419 /**
1378 1420 * Sets the request format.
1379 - *
1380 - * @param string $format The request format
1381 1421 */
1382 - public function setRequestFormat($format)
1422 + public function setRequestFormat(?string $format)
1383 1423 {
1384 1424 $this->format = $format;
1385 1425 }
1386 1426
@@ -1386,9 +1426,9 @@
1386 1426
1387 1427 /**
1388 1428 * Gets the format associated with the request.
1389 1429 *
1390 - * @return string|null The format (null if no content type is present)
1430 + * @return string|null
1391 1431 */
1392 1432 public function getContentType()
1393 1433 {
1394 1434 return $this->getFormat($this->headers->get('CONTENT_TYPE', ''));
@@ -1395,12 +1435,10 @@
1395 1435 }
1396 1436
1397 1437 /**
1398 1438 * Sets the default locale.
1399 - *
1400 - * @param string $locale
1401 1439 */
1402 - public function setDefaultLocale($locale)
1440 + public function setDefaultLocale(string $locale)
1403 1441 {
1404 1442 $this->defaultLocale = $locale;
1405 1443
1406 1444 if (null === $this->locale) {
@@ -1419,12 +1457,10 @@
1419 1457 }
1420 1458
1421 1459 /**
1422 1460 * Sets the locale.
1423 - *
1424 - * @param string $locale
1425 1461 */
1426 - public function setLocale($locale)
1462 + public function setLocale(string $locale)
1427 1463 {
1428 1464 $this->setPhpDefaultLocale($this->locale = $locale);
1429 1465 }
1430 1466
@@ -1434,9 +1470,9 @@
1434 1470 * @return string
1435 1471 */
1436 1472 public function getLocale()
1437 1473 {
1438 - return null === $this->locale ? $this->defaultLocale : $this->locale;
1474 + return $this->locale ?? $this->defaultLocale;
1439 1475 }
1440 1476
1441 1477 /**
1442 1478 * Checks if the request method is of specified type.
@@ -1444,9 +1480,9 @@
1444 1480 * @param string $method Uppercase request method (GET, POST etc)
1445 1481 *
1446 1482 * @return bool
1447 1483 */
1448 - public function isMethod($method)
1484 + public function isMethod(string $method)
1449 1485 {
1450 1486 return $this->getMethod() === strtoupper($method);
1451 1487 }
1452 1488
@@ -1458,12 +1494,8 @@
1458 1494 * @return bool
1459 1495 */
1460 1496 public function isMethodSafe()
1461 1497 {
1462 - if (\func_num_args() > 0) {
1463 - @trigger_error(sprintf('Passing arguments to "%s()" has been deprecated since Symfony 4.4; use "%s::isMethodCacheable()" to check if the method is cacheable instead.', __METHOD__, __CLASS__), \E_USER_DEPRECATED);
1464 - }
1465 -
1466 1498 return \in_array($this->getMethod(), ['GET', 'HEAD', 'OPTIONS', 'TRACE']);
1467 1499 }
1468 1500
1469 1501 /**
@@ -1480,9 +1512,9 @@
1480 1512 * Checks whether the method is cacheable or not.
1481 1513 *
1482 1514 * @see https://tools.ietf.org/html/rfc7231#section-4.2.3
1483 1515 *
1484 - * @return bool True for GET and HEAD, false otherwise
1516 + * @return bool
1485 1517 */
1486 1518 public function isMethodCacheable()
1487 1519 {
1488 1520 return \in_array($this->getMethod(), ['GET', 'HEAD']);
@@ -1516,11 +1548,11 @@
1516 1548 * Returns the request body content.
1517 1549 *
1518 1550 * @param bool $asResource If true, a resource will be returned
1519 1551 *
1520 - * @return string|resource The request body content or a resource to read the body stream
1552 + * @return string|resource
1521 1553 */
1522 - public function getContent($asResource = false)
1554 + public function getContent(bool $asResource = false)
1523 1555 {
1524 1556 $currentContentIsResource = \is_resource($this->content);
1525 1557
1526 1558 if (true === $asResource) {
@@ -1557,11 +1589,41 @@
1557 1589 return $this->content;
1558 1590 }
1559 1591
1560 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 + /**
1561 1623 * Gets the Etags.
1562 1624 *
1563 - * @return array The entity tags
1625 + * @return array
1564 1626 */
1565 1627 public function getETags()
1566 1628 {
1567 1629 return preg_split('/\s*,\s*/', $this->headers->get('If-None-Match', ''), -1, \PREG_SPLIT_NO_EMPTY);
@@ -1576,9 +1638,9 @@
1576 1638 }
1577 1639
1578 1640 /**
1579 1641 * Gets the preferred format for the response by inspecting, in the following order:
1580 - * * the request format set using setRequestFormat
1642 + * * the request format set using setRequestFormat;
1581 1643 * * the values of the Accept HTTP header.
1582 1644 *
1583 1645 * Note that if you use this method, you should send the "Vary: Accept" header
1584 1646 * in the response to prevent any issues with intermediary HTTP caches.
@@ -1602,11 +1664,11 @@
1602 1664 * Returns the preferred language.
1603 1665 *
1604 1666 * @param string[] $locales An array of ordered available locales
1605 1667 *
1606 - * @return string|null The preferred locale
1668 + * @return string|null
1607 1669 */
1608 - public function getPreferredLanguage(array $locales = null)
1670 + public function getPreferredLanguage(?array $locales = null)
1609 1671 {
1610 1672 $preferredLanguages = $this->getLanguages();
1611 1673
1612 1674 if (empty($locales)) {
@@ -1633,11 +1695,11 @@
1633 1695 return $preferredLanguages[0] ?? $locales[0];
1634 1696 }
1635 1697
1636 1698 /**
1637 - * Gets a list of languages acceptable by the client browser.
1699 + * Gets a list of languages acceptable by the client browser ordered in the user browser preferences.
1638 1700 *
1639 - * @return array Languages ordered in the user browser preferences
1701 + * @return array
1640 1702 */
1641 1703 public function getLanguages()
1642 1704 {
1643 1705 if (null !== $this->languages) {
@@ -1645,9 +1707,10 @@
1645 1707 }
1646 1708
1647 1709 $languages = AcceptHeader::fromString($this->headers->get('Accept-Language'))->all();
1648 1710 $this->languages = [];
1649 - foreach ($languages as $lang => $acceptHeaderItem) {
1711 + foreach ($languages as $acceptHeaderItem) {
1712 + $lang = $acceptHeaderItem->getValue();
1650 1713 if (str_contains($lang, '-')) {
1651 1714 $codes = explode('-', $lang);
1652 1715 if ('i' === $codes[0]) {
1653 1716 // Language not listed in ISO 639 that are not variants
@@ -1673,11 +1736,11 @@
1673 1736 return $this->languages;
1674 1737 }
1675 1738
1676 1739 /**
1677 - * Gets a list of charsets acceptable by the client browser.
1740 + * Gets a list of charsets acceptable by the client browser in preferable order.
1678 1741 *
1679 - * @return array List of charsets in preferable order
1742 + * @return array
1680 1743 */
1681 1744 public function getCharsets()
1682 1745 {
1683 1746 if (null !== $this->charsets) {
@@ -1683,15 +1746,15 @@
1683 1746 if (null !== $this->charsets) {
1684 1747 return $this->charsets;
1685 1748 }
1686 1749
1687 - return $this->charsets = array_keys(AcceptHeader::fromString($this->headers->get('Accept-Charset'))->all());
1750 + return $this->charsets = array_map('strval', array_keys(AcceptHeader::fromString($this->headers->get('Accept-Charset'))->all()));
1688 1751 }
1689 1752
1690 1753 /**
1691 - * Gets a list of encodings acceptable by the client browser.
1754 + * Gets a list of encodings acceptable by the client browser in preferable order.
1692 1755 *
1693 - * @return array List of encodings in preferable order
1756 + * @return array
1694 1757 */
1695 1758 public function getEncodings()
1696 1759 {
1697 1760 if (null !== $this->encodings) {
@@ -1697,15 +1760,15 @@
1697 1760 if (null !== $this->encodings) {
1698 1761 return $this->encodings;
1699 1762 }
1700 1763
1701 - return $this->encodings = array_keys(AcceptHeader::fromString($this->headers->get('Accept-Encoding'))->all());
1764 + return $this->encodings = array_map('strval', array_keys(AcceptHeader::fromString($this->headers->get('Accept-Encoding'))->all()));
1702 1765 }
1703 1766
1704 1767 /**
1705 - * Gets a list of content types acceptable by the client browser.
1768 + * Gets a list of content types acceptable by the client browser in preferable order.
1706 1769 *
1707 - * @return array List of content types in preferable order
1770 + * @return array
1708 1771 */
1709 1772 public function getAcceptableContentTypes()
1710 1773 {
1711 1774 if (null !== $this->acceptableContentTypes) {
@@ -1711,9 +1774,9 @@
1711 1774 if (null !== $this->acceptableContentTypes) {
1712 1775 return $this->acceptableContentTypes;
1713 1776 }
1714 1777
1715 - return $this->acceptableContentTypes = array_keys(AcceptHeader::fromString($this->headers->get('Accept'))->all());
1778 + return $this->acceptableContentTypes = array_map('strval', array_keys(AcceptHeader::fromString($this->headers->get('Accept'))->all()));
1716 1779 }
1717 1780
1718 1781 /**
1719 1782 * Returns true if the request is an XMLHttpRequest.
@@ -1722,9 +1785,9 @@
1722 1785 * It is known to work with common JavaScript frameworks:
1723 1786 *
1724 1787 * @see https://wikipedia.org/wiki/List_of_Ajax_frameworks#JavaScript
1725 1788 *
1726 - * @return bool true if the request is an XMLHttpRequest, false otherwise
1789 + * @return bool
1727 1790 */
1728 1791 public function isXmlHttpRequest()
1729 1792 {
1730 1793 return 'XMLHttpRequest' == $this->headers->get('X-Requested-With');
@@ -1729,8 +1792,27 @@
1729 1792 {
1730 1793 return 'XMLHttpRequest' == $this->headers->get('X-Requested-With');
1731 1794 }
1732 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 +
1733 1815 /*
1734 1816 * The following methods are derived from code of the Zend Framework (1.10dev - 2010-01-24)
1735 1817 *
1736 1818 * Code subject to the new BSD license (https://framework.zend.com/license).
@@ -1741,13 +1823,12 @@
1741 1823 protected function prepareRequestUri()
1742 1824 {
1743 1825 $requestUri = '';
1744 1826
1745 - if ('1' == $this->server->get('IIS_WasUrlRewritten') && '' != $this->server->get('UNENCODED_URL')) {
1827 + if ($this->isIisRewrite() && '' != $this->server->get('UNENCODED_URL')) {
1746 1828 // IIS7 with URL Rewrite: make sure we get the unencoded URL (double slash problem)
1747 1829 $requestUri = $this->server->get('UNENCODED_URL');
1748 1830 $this->server->remove('UNENCODED_URL');
1749 - $this->server->remove('IIS_WasUrlRewritten');
1750 1831 } elseif ($this->server->has('REQUEST_URI')) {
1751 1832 $requestUri = $this->server->get('REQUEST_URI');
1752 1833
1753 1834 if ('' !== $requestUri && '/' === $requestUri[0]) {
@@ -1854,9 +1935,9 @@
1854 1935
1855 1936 /**
1856 1937 * Prepares the base path.
1857 1938 *
1858 - * @return string base path
1939 + * @return string
1859 1940 */
1860 1941 protected function prepareBasePath()
1861 1942 {
1862 1943 $baseUrl = $this->getBaseUrl();
@@ -1880,9 +1961,9 @@
1880 1961
1881 1962 /**
1882 1963 * Prepares the path info.
1883 1964 *
1884 - * @return string path info
1965 + * @return string
1885 1966 */
1886 1967 protected function preparePathInfo()
1887 1968 {
1888 1969 if (null === ($requestUri = $this->getRequestUri())) {
@@ -1896,16 +1977,15 @@
1896 1977 if ('' !== $requestUri && '/' !== $requestUri[0]) {
1897 1978 $requestUri = '/'.$requestUri;
1898 1979 }
1899 1980
1900 - if (null === ($baseUrl = $this->getBaseUrl())) {
1981 + if (null === ($baseUrl = $this->getBaseUrlReal())) {
1901 1982 return $requestUri;
1902 1983 }
1903 1984
1904 1985 $pathInfo = substr($requestUri, \strlen($baseUrl));
1905 - if (false === $pathInfo || '' === $pathInfo) {
1906 - // If substr() returns false then PATH_INFO is set to an empty string
1907 - return '/';
1986 + if (false === $pathInfo || '' === $pathInfo || '/' !== $pathInfo[0]) {
1987 + return '/'.$pathInfo;
1908 1988 }
1909 1989
1910 1990 return $pathInfo;
1911 1991 }
@@ -1925,9 +2005,9 @@
1925 2005 'xml' => ['text/xml', 'application/xml', 'application/x-xml'],
1926 2006 'rdf' => ['application/rdf+xml'],
1927 2007 'atom' => ['application/atom+xml'],
1928 2008 'rss' => ['application/rss+xml'],
1929 - 'form' => ['application/x-www-form-urlencoded'],
2009 + 'form' => ['application/x-www-form-urlencoded', 'multipart/form-data'],
1930 2010 ];
1931 2011 }
1932 2012
1933 2013 private function setPhpDefaultLocale(string $locale): void
@@ -1948,9 +2028,15 @@
1948 2028 * the given prefix, null otherwise.
1949 2029 */
1950 2030 private function getUrlencodedPrefix(string $string, string $prefix): ?string
1951 2031 {
1952 - if (!str_starts_with(rawurldecode($string), $prefix)) {
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)) {
1953 2039 return null;
1954 2040 }
1955 2041
1956 2042 $len = \strlen($prefix);
@@ -1982,9 +2068,9 @@
1982 2068 *
1983 2069 * This can be useful to determine whether or not to trust the
1984 2070 * contents of a proxy-specific header.
1985 2071 *
1986 - * @return bool true if the request came from a trusted proxy, false otherwise
2072 + * @return bool
1987 2073 */
1988 2074 public function isFromTrustedProxy()
1989 2075 {
1990 2076 return self::$trustedProxies && IpUtils::checkIp($this->server->get('REMOTE_ADDR', ''), self::$trustedProxies);
@@ -1989,9 +2075,9 @@
1989 2075 {
1990 2076 return self::$trustedProxies && IpUtils::checkIp($this->server->get('REMOTE_ADDR', ''), self::$trustedProxies);
1991 2077 }
1992 2078
1993 - private function getTrustedValues(int $type, string $ip = null): array
2079 + private function getTrustedValues(int $type, ?string $ip = null): array
1994 2080 {
1995 2081 $clientValues = [];
1996 2082 $forwardedValues = [];
1997 2083
@@ -2000,9 +2086,9 @@
2000 2086 $clientValues[] = (self::HEADER_X_FORWARDED_PORT === $type ? '0.0.0.0:' : '').trim($v);
2001 2087 }
2002 2088 }
2003 2089
2004 - if ((self::$trustedHeaderSet & self::HEADER_FORWARDED) && $this->headers->has(self::TRUSTED_HEADERS[self::HEADER_FORWARDED])) {
2090 + if ((self::$trustedHeaderSet & self::HEADER_FORWARDED) && (isset(self::FORWARDED_PARAMS[$type])) && $this->headers->has(self::TRUSTED_HEADERS[self::HEADER_FORWARDED])) {
2005 2091 $forwarded = $this->headers->get(self::TRUSTED_HEADERS[self::HEADER_FORWARDED]);
2006 2092 $parts = HeaderUtils::split($forwarded, ',;=');
2007 2093 $forwardedValues = [];
2008 2094 $param = self::FORWARDED_PARAMS[$type];
@@ -2080,6 +2166,22 @@
2080 2166 }
2081 2167
2082 2168 // Now the IP chain contains only untrusted proxies and the client IP
2083 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;
2084 2186 }
2085 2187 }