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