Allow PHP-8.2 and up Compatibility instead of just PHP-8.4

This commit is contained in:
johnnyq
2026-06-12 17:06:10 -04:00
parent 2204bd52f4
commit d3a93652f3
220 changed files with 7198 additions and 2635 deletions

View File

@@ -62,6 +62,7 @@ class Request
public const METHOD_OPTIONS = 'OPTIONS';
public const METHOD_TRACE = 'TRACE';
public const METHOD_CONNECT = 'CONNECT';
public const METHOD_QUERY = 'QUERY';
/**
* @var string[]
@@ -80,6 +81,13 @@ class Request
protected static bool $httpMethodParameterOverride = false;
/**
* The HTTP methods that can be overridden.
*
* @var uppercase-string[]|null
*/
protected static ?array $allowedHttpMethodOverride = null;
/**
* Custom parameters.
*/
@@ -94,6 +102,8 @@ class Request
/**
* Query string parameters ($_GET).
*
* @var InputBag<string>
*/
public InputBag $query;
@@ -109,6 +119,8 @@ class Request
/**
* Cookies ($_COOKIE).
*
* @var InputBag<string>
*/
public InputBag $cookies;
@@ -194,6 +206,28 @@ class Request
self::HEADER_X_FORWARDED_PREFIX => 'X_FORWARDED_PREFIX',
];
/**
* This mapping is used when no exact MIME match is found in $formats.
*
* It enables mappings like application/soap+xml -> xml.
*
* @see https://datatracker.ietf.org/doc/html/rfc6839
* @see https://datatracker.ietf.org/doc/html/rfc7303
* @see https://www.iana.org/assignments/media-types/media-types.xhtml
*/
private const STRUCTURED_SUFFIX_FORMATS = [
'json' => 'json',
'xml' => 'xml',
'xhtml' => 'html',
'cbor' => 'cbor',
'zip' => 'zip',
'ber' => 'asn1',
'der' => 'asn1',
'tlv' => 'tlv',
'wbxml' => 'xml',
'yaml' => 'yaml',
];
private bool $isIisRewrite = false;
/**
@@ -251,16 +285,30 @@ class Request
*/
public static function createFromGlobals(): static
{
$request = self::createRequestFromFactory($_GET, $_POST, [], $_COOKIE, $_FILES, $_SERVER);
if (str_starts_with($request->headers->get('CONTENT_TYPE', ''), 'application/x-www-form-urlencoded')
&& \in_array(strtoupper($request->server->get('REQUEST_METHOD', 'GET')), ['PUT', 'DELETE', 'PATCH'], true)
) {
parse_str($request->getContent(), $data);
$request->request = new InputBag($data);
if (!\in_array($_SERVER['REQUEST_METHOD'] ?? null, ['PUT', 'DELETE', 'PATCH', 'QUERY'], true)) {
return self::createRequestFromFactory($_GET, $_POST, [], $_COOKIE, $_FILES, $_SERVER);
}
return $request;
if (\PHP_VERSION_ID < 80400) {
if (!isset($_SERVER['CONTENT_TYPE']) || str_starts_with($_SERVER['CONTENT_TYPE'], 'application/x-www-form-urlencoded')) {
$content = file_get_contents('php://input');
parse_str($content, $post);
} else {
$content = null;
$post = $_POST;
}
return self::createRequestFromFactory($_GET, $post, [], $_COOKIE, $_FILES, $_SERVER, $content);
}
try {
[$post, $files] = request_parse_body();
} catch (\RequestParseBodyException) {
$post = $_POST;
$files = $_FILES;
}
return self::createRequestFromFactory($_GET, $post, [], $_COOKIE, $files, $_SERVER);
}
/**
@@ -353,14 +401,23 @@ class Request
$server['PHP_AUTH_PW'] = $components['pass'];
}
if (!isset($components['path'])) {
if ('' === $path = $components['path'] ?? '') {
$components['path'] = '/';
} elseif (!isset($components['scheme']) && !isset($components['host']) && '/' !== $path[0]) {
if (false !== $pos = strpos($path, '/')) {
$path = substr($path, 0, $pos);
}
if (str_contains($path, ':')) {
throw new BadRequestException('Invalid URI: Path is malformed.');
}
}
switch (strtoupper($method)) {
case 'POST':
case 'PUT':
case 'DELETE':
case 'QUERY':
if (!isset($server['CONTENT_TYPE'])) {
$server['CONTENT_TYPE'] = 'application/x-www-form-urlencoded';
}
@@ -451,8 +508,8 @@ class Request
$dup->method = null;
$dup->format = null;
if (!$dup->get('_format') && $this->get('_format')) {
$dup->attributes->set('_format', $this->get('_format'));
if (!$dup->attributes->has('_format') && $this->attributes->has('_format')) {
$dup->attributes->set('_format', $this->attributes->get('_format'));
}
if (!$dup->getRequestFormat(null)) {
@@ -596,7 +653,7 @@ class Request
*/
public static function setTrustedHosts(array $hostPatterns): void
{
self::$trustedHostPatterns = array_map(fn ($hostPattern) => \sprintf('{%s}i', $hostPattern), $hostPatterns);
self::$trustedHostPatterns = array_map(static fn ($hostPattern) => \sprintf('{%s}i', $hostPattern), $hostPatterns);
// we need to reset trusted hosts on trusted host patterns change
self::$trustedHosts = [];
}
@@ -653,6 +710,34 @@ class Request
return self::$httpMethodParameterOverride;
}
/**
* Sets the list of HTTP methods that can be overridden.
*
* Set to null to allow all methods to be overridden (default). Set to an
* empty array to disallow overrides entirely. Otherwise, provide the list
* of uppercased method names that are allowed.
*
* @param uppercase-string[]|null $methods
*/
public static function setAllowedHttpMethodOverride(?array $methods): void
{
if (array_intersect($methods ?? [], ['GET', 'HEAD', 'CONNECT', 'TRACE'])) {
throw new \InvalidArgumentException('The HTTP methods "GET", "HEAD", "CONNECT", and "TRACE" cannot be overridden.');
}
self::$allowedHttpMethodOverride = $methods;
}
/**
* Gets the list of HTTP methods that can be overridden.
*
* @return uppercase-string[]|null
*/
public static function getAllowedHttpMethodOverride(): ?array
{
return self::$allowedHttpMethodOverride;
}
/**
* Gets a "parameter" value from any bag.
*
@@ -662,10 +747,12 @@ class Request
*
* Order of precedence: PATH (routing placeholders or custom attributes), GET, POST
*
* @internal use explicit input sources instead
* @deprecated since Symfony 7.4, use properties `->attributes`, `query` or `request` directly instead
*/
public function get(string $key, mixed $default = null): mixed
{
trigger_deprecation('symfony/http-foundation', '7.4', 'Request::get() is deprecated, use properties ->attributes, query or request directly instead.');
if ($this !== $result = $this->attributes->get($key, $this)) {
return $result;
}
@@ -770,10 +857,6 @@ class Request
* being the original client, and each successive proxy that passed the request
* adding the IP address where it received the request from.
*
* If your reverse proxy uses a different header name than "X-Forwarded-For",
* ("Client-Ip" for instance), configure it via the $trustedHeaderSet
* argument of the Request::setTrustedProxies() method instead.
*
* @see getClientIps()
* @see https://wikipedia.org/wiki/X-Forwarded-For
*/
@@ -797,7 +880,7 @@ class Request
*
* Suppose this request is instantiated from /mysite on localhost:
*
* * http://localhost/mysite returns an empty string
* * http://localhost/mysite returns '/'
* * http://localhost/mysite/about returns '/about'
* * http://localhost/mysite/enco%20ded returns '/enco%20ded'
* * http://localhost/mysite/about?var=1 returns '/about'
@@ -1075,7 +1158,7 @@ class Request
$https = $this->server->get('HTTPS');
return $https && 'off' !== strtolower($https);
return $https && (!\is_string($https) || 'off' !== strtolower($https));
}
/**
@@ -1092,10 +1175,8 @@ class Request
{
if ($this->isFromTrustedProxy() && $host = $this->getTrustedValues(self::HEADER_X_FORWARDED_HOST)) {
$host = $host[0];
} elseif (!$host = $this->headers->get('HOST')) {
if (!$host = $this->server->get('SERVER_NAME')) {
$host = $this->server->get('SERVER_ADDR', '');
}
} else {
$host = $this->headers->get('HOST') ?: $this->server->get('SERVER_NAME') ?: $this->server->get('SERVER_ADDR', '');
}
// trim and remove port number from host
@@ -1168,7 +1249,7 @@ class Request
$this->method = strtoupper($this->server->get('REQUEST_METHOD', 'GET'));
if ('POST' !== $this->method) {
if ('POST' !== $this->method || !(self::$allowedHttpMethodOverride ?? true)) {
return $this->method;
}
@@ -1184,11 +1265,15 @@ class Request
$method = strtoupper($method);
if (\in_array($method, ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT', 'OPTIONS', 'PATCH', 'PURGE', 'TRACE'], true)) {
return $this->method = $method;
if (\in_array($method, ['GET', 'HEAD', 'CONNECT', 'TRACE'], true)) {
trigger_deprecation('symfony/http-foundation', '7.4', 'HTTP method override is deprecated for methods GET, HEAD, CONNECT and TRACE; it will be ignored in Symfony 8.0.', $method);
}
if (!preg_match('/^[A-Z]++$/D', $method)) {
if (self::$allowedHttpMethodOverride && !\in_array($method, self::$allowedHttpMethodOverride, true)) {
return $this->method;
}
if (\strlen($method) !== strspn($method, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ')) {
throw new SuspiciousOperationException('Invalid HTTP method override.');
}
@@ -1233,9 +1318,22 @@ class Request
/**
* Gets the format associated with the mime type.
*
* Resolution order:
* 1) Exact match on the full MIME type (e.g. "application/json").
* 2) Match on the canonical MIME type (i.e. before the first ";" parameter).
* 3) If the type is "application/*+suffix", use the structured syntax suffix
* mapping (e.g. "application/foo+json" → "json"), when available.
* 4) If $subtypeFallback is true and no match was found:
* - return the MIME subtype (without "x-" prefix), provided it does not
* contain a "+" (e.g. "application/x-yaml" → "yaml", "text/csv" → "csv").
*
* @param string|null $mimeType The mime type to check
* @param bool $subtypeFallback Whether to fall back to the subtype if no exact match is found
*/
public function getFormat(?string $mimeType): ?string
public function getFormat(?string $mimeType/* , bool $subtypeFallback = false */): ?string
{
$subtypeFallback = 2 <= \func_num_args() ? func_get_arg(1) : false;
$canonicalMimeType = null;
if ($mimeType && false !== $pos = strpos($mimeType, ';')) {
$canonicalMimeType = trim(substr($mimeType, 0, $pos));
@@ -1261,21 +1359,48 @@ class Request
return $format;
}
if (!$canonicalMimeType ??= $mimeType) {
return null;
}
if (str_starts_with($canonicalMimeType, 'application/') && str_contains($canonicalMimeType, '+')) {
$suffix = substr(strrchr($canonicalMimeType, '+'), 1);
if (isset(self::STRUCTURED_SUFFIX_FORMATS[$suffix])) {
return self::STRUCTURED_SUFFIX_FORMATS[$suffix];
}
}
if ($subtypeFallback && str_contains($canonicalMimeType, '/')) {
[, $subtype] = explode('/', $canonicalMimeType, 2);
if (str_starts_with($subtype, 'x-')) {
$subtype = substr($subtype, 2);
}
if (!str_contains($subtype, '+')) {
return $subtype;
}
}
return null;
}
/**
* Associates a format with mime types.
*
* @param string $format The format to set
* @param string|string[] $mimeTypes The associated mime types (the preferred one must be the first as it will be used as the content type)
*/
public function setFormat(?string $format, string|array $mimeTypes): void
{
if (null === $format) {
trigger_deprecation('symfony/http-foundation', '7.4', 'Passing "null" as the first argument of "%s()" is deprecated. The argument will be non-nullable in Symfony 8.0.', __METHOD__);
$format = '';
}
if (null === static::$formats) {
static::initializeFormats();
}
static::$formats[$format ?? ''] = (array) $mimeTypes;
static::$formats[$format] = (array) $mimeTypes;
}
/**
@@ -1367,7 +1492,7 @@ class Request
*/
public function isMethodSafe(): bool
{
return \in_array($this->getMethod(), ['GET', 'HEAD', 'OPTIONS', 'TRACE']);
return \in_array($this->getMethod(), ['GET', 'HEAD', 'OPTIONS', 'TRACE', 'QUERY'], true);
}
/**
@@ -1375,7 +1500,7 @@ class Request
*/
public function isMethodIdempotent(): bool
{
return \in_array($this->getMethod(), ['HEAD', 'GET', 'PUT', 'DELETE', 'TRACE', 'OPTIONS', 'PURGE']);
return \in_array($this->getMethod(), ['HEAD', 'GET', 'PUT', 'DELETE', 'TRACE', 'OPTIONS', 'PURGE', 'QUERY'], true);
}
/**
@@ -1385,7 +1510,7 @@ class Request
*/
public function isMethodCacheable(): bool
{
return \in_array($this->getMethod(), ['GET', 'HEAD']);
return \in_array($this->getMethod(), ['GET', 'HEAD', 'QUERY'], true);
}
/**
@@ -1421,10 +1546,8 @@ class Request
*/
public function getContent(bool $asResource = false)
{
$currentContentIsResource = \is_resource($this->content);
if (true === $asResource) {
if ($currentContentIsResource) {
if ($asResource) {
if (\is_resource($this->content)) {
rewind($this->content);
return $this->content;
@@ -1444,7 +1567,7 @@ class Request
return fopen('php://input', 'r');
}
if ($currentContentIsResource) {
if (\is_resource($this->content)) {
rewind($this->content);
return stream_get_contents($this->content);
@@ -1932,6 +2055,14 @@ class Request
'atom' => ['application/atom+xml'],
'rss' => ['application/rss+xml'],
'form' => ['application/x-www-form-urlencoded', 'multipart/form-data'],
'soap' => ['application/soap+xml'],
'problem' => ['application/problem+json'],
'hal' => ['application/hal+json', 'application/hal+xml'],
'jsonapi' => ['application/vnd.api+json'],
'yaml' => ['text/yaml', 'application/x-yaml'],
'wbxml' => ['application/vnd.wap.wbxml'],
'pdf' => ['application/pdf'],
'csv' => ['text/csv'],
];
}