From e439d601ddaca3c19a64716e947885b6ff08a5e2 Mon Sep 17 00:00:00 2001 From: Bob van de Vijver Date: Wed, 26 Nov 2025 16:30:11 +0100 Subject: [PATCH 1/3] Fix Request getPathInfo docblock --- Request.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Request.php b/Request.php index 922014133..b624675d0 100644 --- a/Request.php +++ b/Request.php @@ -846,7 +846,7 @@ public function getScriptName(): string * * 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' From 9d18f1c620b45bfca4c3637b23115884029c3ebb Mon Sep 17 00:00:00 2001 From: MiBo Date: Mon, 24 Nov 2025 18:37:51 +0100 Subject: [PATCH 2/3] [HttpFoundation] Fix Expires response header for EventStream --- EventStreamResponse.php | 2 +- Tests/EventStreamResponseTest.php | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/EventStreamResponse.php b/EventStreamResponse.php index fe1a2872e..e739c309c 100644 --- a/EventStreamResponse.php +++ b/EventStreamResponse.php @@ -48,7 +48,7 @@ public function __construct(?callable $callback = null, int $status = 200, array 'Cache-Control' => 'private, no-cache, no-store, must-revalidate, max-age=0', 'X-Accel-Buffering' => 'no', 'Pragma' => 'no-cache', - 'Expire' => '0', + 'Expires' => '0', ]; parent::__construct($callback, $status, $headers); diff --git a/Tests/EventStreamResponseTest.php b/Tests/EventStreamResponseTest.php index f3b5ae910..a941bc40e 100644 --- a/Tests/EventStreamResponseTest.php +++ b/Tests/EventStreamResponseTest.php @@ -29,6 +29,14 @@ public function testInitializationWithDefaultValues() $this->assertNull($response->getRetry()); } + public function testPresentOfExpiresHeader() + { + $response = new EventStreamResponse(); + + $this->assertTrue($response->headers->has('Expires')); + $this->assertSame('0', $response->headers->get('Expires')); + } + public function testStreamSingleEvent() { $response = new EventStreamResponse(function () { From bd1af1e425811d6f077db240c3a588bdb405cd27 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Fri, 5 Dec 2025 13:24:18 +0100 Subject: [PATCH 3/3] [HttpFoundation] Improve logic in Request::createFromGlobals() --- Request.php | 42 +++++++++++++++-------------- Tests/RequestTest.php | 63 +++++++++++++++++++++++++++++++++++++++---- 2 files changed, 80 insertions(+), 25 deletions(-) diff --git a/Request.php b/Request.php index b5ca911b0..dc21ee16b 100644 --- a/Request.php +++ b/Request.php @@ -285,26 +285,30 @@ public function initialize(array $query = [], array $request = [], array $attrib */ public static function createFromGlobals(): static { - $request = self::createRequestFromFactory($_GET, $_POST, [], $_COOKIE, $_FILES, $_SERVER); - - if (!\in_array($request->server->get('REQUEST_METHOD', 'GET'), ['PUT', 'DELETE', 'PATCH', 'QUERY'], true)) { - return $request; + if (!\in_array($_SERVER['REQUEST_METHOD'] ?? null, ['PUT', 'DELETE', 'PATCH', 'QUERY'], true)) { + return self::createRequestFromFactory($_GET, $_POST, [], $_COOKIE, $_FILES, $_SERVER); } - if (\PHP_VERSION_ID >= 80400) { - try { - [$post, $files] = request_parse_body(); - - $request->request->add($post); - $request->files->add($files); - } catch (\RequestParseBodyException) { + 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; } - } elseif (str_starts_with($request->headers->get('CONTENT_TYPE', ''), 'application/x-www-form-urlencoded')) { - parse_str($request->getContent(), $data); - $request->request = new InputBag($data); + + return self::createRequestFromFactory($_GET, $post, [], $_COOKIE, $_FILES, $_SERVER, $content); } - return $request; + try { + [$post, $files] = request_parse_body(); + } catch (\RequestParseBodyException) { + $post = $_POST; + $files = $_FILES; + } + + return self::createRequestFromFactory($_GET, $post, [], $_COOKIE, $files, $_SERVER); } /** @@ -1538,10 +1542,8 @@ public function getProtocolVersion(): ?string */ 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; @@ -1561,7 +1563,7 @@ public function getContent(bool $asResource = false) return fopen('php://input', 'r'); } - if ($currentContentIsResource) { + if (\is_resource($this->content)) { rewind($this->content); return stream_get_contents($this->content); diff --git a/Tests/RequestTest.php b/Tests/RequestTest.php index 707a2708b..cc3008b57 100644 --- a/Tests/RequestTest.php +++ b/Tests/RequestTest.php @@ -1431,9 +1431,17 @@ public function testFormUrlEncodedBodyParsing(string $method) $_SERVER['REQUEST_METHOD'] = $method; $_SERVER['CONTENT_TYPE'] = 'application/x-www-form-urlencoded'; - $request = RequestContentProxy::createFromGlobals(); + MockPhpStreamWrapper::setInputContent('content=mycontent'); + stream_wrapper_unregister('php'); + stream_wrapper_register('php', MockPhpStreamWrapper::class); - $this->assertEquals('mycontent', $request->request->get('content')); + try { + $request = Request::createFromGlobals(); + + $this->assertSame('mycontent', $request->request->get('content')); + } finally { + stream_wrapper_restore('php'); + } } public function testOverrideGlobals() @@ -3050,11 +3058,56 @@ public static function providePreferredFormatRfc9110(): iterable } } -class RequestContentProxy extends Request +class MockPhpStreamWrapper { - public function getContent($asResource = false) + /** @var resource|null */ + public $context; + + private static string $inputContent = ''; + private string $content = ''; + private int $position = 0; + + public static function setInputContent(string $content): void + { + self::$inputContent = $content; + } + + public function stream_open(string $path, string $mode, int $options, ?string &$opened_path): bool + { + if ('php://input' === $path) { + $this->content = self::$inputContent; + $this->position = 0; + + return true; + } + + return false; + } + + public function stream_read(int $count): string + { + $result = substr($this->content, $this->position, $count); + $this->position += \strlen($result); + + return $result; + } + + public function stream_eof(): bool + { + return $this->position >= \strlen($this->content); + } + + public function stream_stat(): array { - return http_build_query(['content' => 'mycontent'], '', '&'); + return [ + 'size' => \strlen($this->content), + 'mode' => 0, + 'uid' => 0, + 'gid' => 0, + 'atime' => 0, + 'mtime' => 0, + 'ctime' => 0, + ]; } }