Skip to content

Commit 8e7abc6

Browse files
committed
Merge branch '7.4' into 8.0
* 7.4: use PHPUnit attributes instead of annotations [WebProfilerBundle] Add link to `xdebug_info()` to config panel issue fix #59412 catalan translation [DependencyInjection][Routing] Define array-shapes to help writing PHP configs using yaml-like arrays [HttpFoundation] Make `Request::createFromGlobals()` use `request_parse_body` when possible
2 parents e7e0520 + f9b8417 commit 8e7abc6

File tree

5 files changed

+170
-51
lines changed

5 files changed

+170
-51
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ CHANGELOG
2020
* Add support for structured MIME suffix
2121
* Deprecate using `Request::sendHeaders()` after headers have already been sent; use a `StreamedResponse` instead
2222
* Deprecate method `Request::get()`, use properties `->attributes`, `query` or `request` directly instead
23+
* Make `Request::createFromGlobals()` parse the body of PUT, DELETE, PATCH and QUERY requests
2324

2425
7.3
2526
---

Request.php

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -280,9 +280,19 @@ public static function createFromGlobals(): static
280280
{
281281
$request = self::createRequestFromFactory($_GET, $_POST, [], $_COOKIE, $_FILES, $_SERVER);
282282

283-
if (str_starts_with($request->headers->get('CONTENT_TYPE', ''), 'application/x-www-form-urlencoded')
284-
&& \in_array(strtoupper($request->server->get('REQUEST_METHOD', 'GET')), ['PUT', 'DELETE', 'PATCH', 'QUERY'], true)
285-
) {
283+
if (!\in_array($request->server->get('REQUEST_METHOD', 'GET'), ['PUT', 'DELETE', 'PATCH', 'QUERY'], true)) {
284+
return $request;
285+
}
286+
287+
if (\PHP_VERSION_ID >= 80400) {
288+
try {
289+
[$post, $files] = request_parse_body();
290+
291+
$request->request->add($post);
292+
$request->files->add($files);
293+
} catch (\RequestParseBodyException) {
294+
}
295+
} elseif (str_starts_with($request->headers->get('CONTENT_TYPE', ''), 'application/x-www-form-urlencoded')) {
286296
parse_str($request->getContent(), $data);
287297
$request->request = new InputBag($data);
288298
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
use Symfony\Component\HttpFoundation\File\UploadedFile;
4+
use Symfony\Component\HttpFoundation\JsonResponse;
5+
use Symfony\Component\HttpFoundation\Request;
6+
7+
$parent = __DIR__;
8+
while (!@file_exists($parent.'/vendor/autoload.php')) {
9+
if (!@file_exists($parent)) {
10+
// open_basedir restriction in effect
11+
break;
12+
}
13+
if ($parent === dirname($parent)) {
14+
echo "vendor/autoload.php not found\n";
15+
exit(1);
16+
}
17+
18+
$parent = dirname($parent);
19+
}
20+
21+
require $parent.'/vendor/autoload.php';
22+
23+
error_reporting(-1);
24+
ini_set('html_errors', 0);
25+
ini_set('display_errors', 1);
26+
27+
if (filter_var(ini_get('xdebug.default_enable'), \FILTER_VALIDATE_BOOL)) {
28+
xdebug_disable();
29+
}
30+
31+
$request = Request::createFromGlobals();
32+
33+
$r = new JsonResponse([
34+
'request' => $request->request->all(),
35+
'files' => array_map(
36+
static fn (UploadedFile $file) => [
37+
'clientOriginalName' => $file->getClientOriginalName(),
38+
'clientMimeType' => $file->getClientMimeType(),
39+
'content' => $file->getContent(),
40+
],
41+
$request->files->all()
42+
),
43+
]);
44+
45+
$r->send();

Tests/RequestFunctionalTest.php

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpFoundation\Tests;
13+
14+
use PHPUnit\Framework\Attributes\DataProvider;
15+
use PHPUnit\Framework\Attributes\RequiresPhp;
16+
use PHPUnit\Framework\TestCase;
17+
18+
#[RequiresPhp('>=8.4')]
19+
class RequestFunctionalTest extends TestCase
20+
{
21+
/** @var resource|false */
22+
private static $server;
23+
24+
public static function setUpBeforeClass(): void
25+
{
26+
$spec = [
27+
1 => ['file', '/dev/null', 'w'],
28+
2 => ['file', '/dev/null', 'w'],
29+
];
30+
if (!self::$server = @proc_open('exec '.\PHP_BINARY.' -S localhost:8054', $spec, $pipes, __DIR__.'/Fixtures/request-functional')) {
31+
self::markTestSkipped('PHP server unable to start.');
32+
}
33+
sleep(1);
34+
}
35+
36+
public static function tearDownAfterClass(): void
37+
{
38+
if (self::$server) {
39+
proc_terminate(self::$server);
40+
proc_close(self::$server);
41+
}
42+
}
43+
44+
public static function provideMethodsRequiringExplicitBodyParsing()
45+
{
46+
return [
47+
['PUT'],
48+
['DELETE'],
49+
['PATCH'],
50+
// PHP’s built-in server doesn’t support QUERY
51+
];
52+
}
53+
54+
#[DataProvider('provideMethodsRequiringExplicitBodyParsing')]
55+
public function testFormUrlEncodedBodyParsing(string $method)
56+
{
57+
$response = file_get_contents('http://localhost:8054/', false, stream_context_create([
58+
'http' => [
59+
'header' => 'Content-type: application/x-www-form-urlencoded',
60+
'method' => $method,
61+
'content' => http_build_query(['foo' => 'bar']),
62+
],
63+
]));
64+
65+
$this->assertSame(['foo' => 'bar'], json_decode($response, true)['request']);
66+
}
67+
68+
#[DataProvider('provideMethodsRequiringExplicitBodyParsing')]
69+
public function testMultipartFormDataBodyParsing(string $method)
70+
{
71+
$response = file_get_contents('http://localhost:8054/', false, stream_context_create([
72+
'http' => [
73+
'header' => 'Content-Type: multipart/form-data; boundary=boundary',
74+
'method' => $method,
75+
'content' => "--boundary\r\n".
76+
"Content-Disposition: form-data; name=foo\r\n".
77+
"\r\n".
78+
"bar\r\n".
79+
"--boundary\r\n".
80+
"Content-Disposition: form-data; name=baz; filename=baz.txt\r\n".
81+
"Content-Type: text/plain\r\n".
82+
"\r\n".
83+
"qux\r\n".
84+
'--boundary--',
85+
],
86+
]));
87+
88+
$data = json_decode($response, true);
89+
90+
$this->assertSame(['foo' => 'bar'], $data['request']);
91+
$this->assertSame(['baz' => [
92+
'clientOriginalName' => 'baz.txt',
93+
'clientMimeType' => 'text/plain',
94+
'content' => 'qux',
95+
]], $data['files']);
96+
}
97+
}

Tests/RequestTest.php

Lines changed: 14 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,7 @@
1919
use Symfony\Component\HttpFoundation\Exception\ConflictingHeadersException;
2020
use Symfony\Component\HttpFoundation\Exception\JsonException;
2121
use Symfony\Component\HttpFoundation\Exception\SuspiciousOperationException;
22-
use Symfony\Component\HttpFoundation\InputBag;
2322
use Symfony\Component\HttpFoundation\IpUtils;
24-
use Symfony\Component\HttpFoundation\ParameterBag;
2523
use Symfony\Component\HttpFoundation\Request;
2624
use Symfony\Component\HttpFoundation\Session\Session;
2725
use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
@@ -1279,18 +1277,6 @@ public static function getContentCanBeCalledTwiceWithResourcesProvider()
12791277
];
12801278
}
12811279

1282-
public static function provideOverloadedMethods()
1283-
{
1284-
return [
1285-
['PUT'],
1286-
['DELETE'],
1287-
['PATCH'],
1288-
['put'],
1289-
['delete'],
1290-
['patch'],
1291-
];
1292-
}
1293-
12941280
public function testToArrayEmpty()
12951281
{
12961282
$req = new Request();
@@ -1329,49 +1315,29 @@ public function testGetPayload()
13291315
$this->assertSame([], $req->getPayload()->all());
13301316
}
13311317

1332-
#[DataProvider('provideOverloadedMethods')]
1333-
public function testCreateFromGlobals($method)
1318+
public function testCreateFromGlobals()
13341319
{
1335-
$normalizedMethod = strtoupper($method);
1336-
13371320
$_GET['foo1'] = 'bar1';
13381321
$_POST['foo2'] = 'bar2';
13391322
$_COOKIE['foo3'] = 'bar3';
13401323
$_FILES['foo4'] = ['bar4'];
13411324
$_SERVER['foo5'] = 'bar5';
13421325

13431326
$request = Request::createFromGlobals();
1344-
$this->assertEquals('bar1', $request->query->get('foo1'), '::fromGlobals() uses values from $_GET');
1345-
$this->assertEquals('bar2', $request->request->get('foo2'), '::fromGlobals() uses values from $_POST');
1346-
$this->assertEquals('bar3', $request->cookies->get('foo3'), '::fromGlobals() uses values from $_COOKIE');
1347-
$this->assertEquals(['bar4'], $request->files->get('foo4'), '::fromGlobals() uses values from $_FILES');
1348-
$this->assertEquals('bar5', $request->server->get('foo5'), '::fromGlobals() uses values from $_SERVER');
1349-
$this->assertInstanceOf(InputBag::class, $request->request);
1350-
$this->assertInstanceOf(ParameterBag::class, $request->request);
1351-
1352-
unset($_GET['foo1'], $_POST['foo2'], $_COOKIE['foo3'], $_FILES['foo4'], $_SERVER['foo5']);
1353-
1354-
$_SERVER['REQUEST_METHOD'] = $method;
1355-
$_SERVER['CONTENT_TYPE'] = 'application/x-www-form-urlencoded';
1356-
$request = RequestContentProxy::createFromGlobals();
1357-
$this->assertEquals($normalizedMethod, $request->getMethod());
1358-
$this->assertEquals('mycontent', $request->request->get('content'));
1359-
$this->assertInstanceOf(InputBag::class, $request->request);
1360-
$this->assertInstanceOf(ParameterBag::class, $request->request);
1361-
1362-
unset($_SERVER['REQUEST_METHOD'], $_SERVER['CONTENT_TYPE']);
1363-
1364-
Request::createFromGlobals();
1327+
$this->assertEquals('bar1', $request->query->get('foo1'), '::createFromGlobals() uses values from $_GET');
1328+
$this->assertEquals('bar2', $request->request->get('foo2'), '::createFromGlobals() uses values from $_POST');
1329+
$this->assertEquals('bar3', $request->cookies->get('foo3'), '::createFromGlobals() uses values from $_COOKIE');
1330+
$this->assertEquals(['bar4'], $request->files->get('foo4'), '::createFromGlobals() uses values from $_FILES');
1331+
$this->assertEquals('bar5', $request->server->get('foo5'), '::createFromGlobals() uses values from $_SERVER');
1332+
}
1333+
1334+
public function testGetRealMethod()
1335+
{
13651336
Request::enableHttpMethodParameterOverride();
1366-
$_POST['_method'] = $method;
1367-
$_POST['foo6'] = 'bar6';
1368-
$_SERVER['REQUEST_METHOD'] = 'PoSt';
1369-
$request = Request::createFromGlobals();
1370-
$this->assertEquals($normalizedMethod, $request->getMethod());
1371-
$this->assertEquals('POST', $request->getRealMethod());
1372-
$this->assertEquals('bar6', $request->request->get('foo6'));
1337+
$request = new Request(request: ['_method' => 'PUT'], server: ['REQUEST_METHOD' => 'PoSt']);
1338+
1339+
$this->assertEquals('POST', $request->getRealMethod(), '->getRealMethod() returns the uppercased request method, even if it has been overridden');
13731340

1374-
unset($_POST['_method'], $_POST['foo6'], $_SERVER['REQUEST_METHOD']);
13751341
$this->disableHttpMethodParameterOverride();
13761342
}
13771343

@@ -2652,7 +2618,7 @@ class RequestContentProxy extends Request
26522618
{
26532619
public function getContent($asResource = false)
26542620
{
2655-
return http_build_query(['_method' => 'PUT', 'content' => 'mycontent'], '', '&');
2621+
return http_build_query(['content' => 'mycontent'], '', '&');
26562622
}
26572623
}
26582624

0 commit comments

Comments
 (0)