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