From ec4cd2313f37f8264c739156811912aa2a78cf40 Mon Sep 17 00:00:00 2001 From: rahul05ranjan Date: Fri, 25 Sep 2026 17:01:44 +0530 Subject: [PATCH 01/14] fix: sync \ after \ is updated in SiteURIFactory SiteURIFactory updates \ (and \['QUERY_STRING']) when it detects the route path, but \ was left stale. Since PHP populates \ only once at the start of the request, getVar() (which reads \) returned outdated values, breaking \->withRequest() for GET parameters. Add Superglobals::syncRequest() to rebuild \ from \, \, and \ according to request_order/variables_order, and call it from SiteURIFactory after setGetArray(). Fixes #9872 --- system/HTTP/SiteURIFactory.php | 4 +-- system/Superglobals.php | 28 +++++++++++++++++++ .../SiteURIFactoryDetectRoutePathTest.php | 1 + tests/system/SuperglobalsTest.php | 28 +++++++++++++++++++ 4 files changed, 59 insertions(+), 2 deletions(-) diff --git a/system/HTTP/SiteURIFactory.php b/system/HTTP/SiteURIFactory.php index 11dccce6c540..a06a944d1331 100644 --- a/system/HTTP/SiteURIFactory.php +++ b/system/HTTP/SiteURIFactory.php @@ -171,7 +171,7 @@ private function parseRequestURI(): string // Update our global GET for values likely to have been changed parse_str($this->superglobals->server('QUERY_STRING'), $get); - $this->superglobals->setGetArray($get); + $this->superglobals->setGetArray($get)->syncRequest(); return URI::removeDotSegments($path); } @@ -203,7 +203,7 @@ private function parseQueryString(): string // Update our global GET for values likely to have been changed parse_str($this->superglobals->server('QUERY_STRING'), $get); - $this->superglobals->setGetArray($get); + $this->superglobals->setGetArray($get)->syncRequest(); return URI::removeDotSegments($path); } diff --git a/system/Superglobals.php b/system/Superglobals.php index ac0ea289bd77..407e98a90b9b 100644 --- a/system/Superglobals.php +++ b/system/Superglobals.php @@ -387,6 +387,34 @@ public function setRequestArray(array $array): self return $this; } + /** + * Rebuilds $_REQUEST from $_GET, $_POST, and $_COOKIE according to the + * `request_order` (or `variables_order`) ini setting. + * + * PHP populates $_REQUEST only once at the start of the request. When + * $_GET is modified later (e.g. by SiteURIFactory), $_REQUEST becomes + * stale. This method re-synchronizes $_REQUEST with the current values. + * + * @return self + */ + public function syncRequest(): self + { + $requestOrder = ini_get('request_order') ?: ini_get('variables_order') ?: 'GP'; + + $request = []; + + foreach (str_split($requestOrder) as $type) { + match ($type) { + 'G' => $request = array_merge($request, $this->get), + 'P' => $request = array_merge($request, $this->post), + 'C' => $request = array_merge($request, $this->cookie), + default => null, + }; + } + + return $this->setRequestArray($request); + } + /** * Get all $_FILES values. * diff --git a/tests/system/HTTP/SiteURIFactoryDetectRoutePathTest.php b/tests/system/HTTP/SiteURIFactoryDetectRoutePathTest.php index 4bc29fbecc4d..314fcf77ee80 100644 --- a/tests/system/HTTP/SiteURIFactoryDetectRoutePathTest.php +++ b/tests/system/HTTP/SiteURIFactoryDetectRoutePathTest.php @@ -256,6 +256,7 @@ public function testQueryStringWithQueryString(): void $this->assertSame($expected, $factory->detectRoutePath('QUERY_STRING')); $this->assertSame('code=good', $_SERVER['QUERY_STRING']); // @phpstan-ignore codeigniter.superglobalsOffsetAccess (checks the live superglobal, not the snapshot service) $this->assertSame(['code' => 'good'], $_GET); + $this->assertSame(['code' => 'good'], $_REQUEST); // @phpstan-ignore codeigniter.superglobalsOffsetAccess (checks the live superglobal, not the snapshot service) } public function testQueryStringEmpty(): void diff --git a/tests/system/SuperglobalsTest.php b/tests/system/SuperglobalsTest.php index e4b8b23b2b28..3007cf74ad2f 100644 --- a/tests/system/SuperglobalsTest.php +++ b/tests/system/SuperglobalsTest.php @@ -302,6 +302,34 @@ public function testRequestSetArray(): void $this->assertSame($data, $_REQUEST); } + public function testSyncRequestRebuildsRequestFromGetPostCookie(): void + { + $this->superglobals->setGetArray(['get_key' => 'get_value']); + $this->superglobals->setPostArray(['post_key' => 'post_value']); + $this->superglobals->setCookieArray(['cookie_key' => 'cookie_value']); + + $this->superglobals->syncRequest(); + + $this->assertSame('get_value', $this->superglobals->request('get_key')); + $this->assertSame('post_value', $this->superglobals->request('post_key')); + $this->assertSame('cookie_value', $this->superglobals->request('cookie_key')); + $this->assertSame('get_value', $_REQUEST['get_key']); // @phpstan-ignore codeigniter.superglobalsOffsetAccess (checks the live superglobal, not the snapshot service) + } + + public function testSyncRequestReflectsGetChanges(): void + { + $this->superglobals->setGetArray(['key' => 'old']); + $this->superglobals->syncRequest(); + + $this->assertSame('old', $this->superglobals->request('key')); + + // Simulate SiteURIFactory updating $_GET after the request started. + $this->superglobals->setGetArray(['key' => 'new']); + $this->superglobals->syncRequest(); + + $this->assertSame('new', $this->superglobals->request('key')); + } + // $_FILES tests public function testFilesGetArray(): void { From 487fb1bc3c792dcb41975af965d4f6686b876b96 Mon Sep 17 00:00:00 2001 From: rahul05ranjan Date: Sat, 26 Sep 2026 18:06:04 +0530 Subject: [PATCH 02/14] fix: make getVar() read merged superglobals instead of stale $_REQUEST $_REQUEST is populated only once at the start of the request, so it becomes stale when SiteURIFactory updates $_GET during URI parsing. getVar() previously read the stale $_REQUEST, breaking withRequest() for GET parameters. Instead of mutating $_REQUEST (the approach rejected in #10205), this change makes getVar() return a merged view of $_GET, $_POST, and $_COOKIE according to request_order, leaving $_REQUEST untouched. - Add Superglobals::getRequestData() returning the merged data. - Extract RequestTrait::fetchFromArray() to reuse the filtering logic. - Update getVar() to use getRequestData() + fetchFromArray(). - Revert the SiteURIFactory syncRequest() calls. - Update tests. Fixes #9872 --- system/HTTP/IncomingRequest.php | 14 +++++++--- system/HTTP/RequestTrait.php | 26 +++++++++++++++---- system/HTTP/SiteURIFactory.php | 4 +-- system/Superglobals.php | 14 +++++----- tests/system/HTTP/IncomingRequestTest.php | 6 ++--- .../SiteURIFactoryDetectRoutePathTest.php | 1 - tests/system/SuperglobalsTest.php | 19 ++++++-------- tests/system/Validation/ValidationTest.php | 6 ++--- 8 files changed, 55 insertions(+), 35 deletions(-) diff --git a/system/HTTP/IncomingRequest.php b/system/HTTP/IncomingRequest.php index cc66e35f4ddc..b9fb942ae23a 100644 --- a/system/HTTP/IncomingRequest.php +++ b/system/HTTP/IncomingRequest.php @@ -368,9 +368,10 @@ public function getDefaultLocale(): string } /** - * Fetch an item from JSON input stream with fallback to $_REQUEST object. This is the simplest way - * to grab data from the request object and can be used in lieu of the - * other get* methods in most cases. + * Fetch an item from JSON input stream with fallback to the merged + * $_GET, $_POST, and $_COOKIE data. This is the simplest way to grab data + * from the request object and can be used in lieu of the other get* + * methods in most cases. * * @param list|string|null $index * @param int|null $filter Filter constant @@ -387,7 +388,12 @@ public function getVar($index = null, $filter = null, $flags = null) return $this->getJsonVar($index, false, $filter, $flags); } - return $this->fetchGlobal('request', $index, $filter, $flags); + // $_REQUEST is populated only once at the start of the request, so it + // can become stale when $_GET is modified later (e.g. by SiteURIFactory). + // Merge the current superglobals instead of reading the stale $_REQUEST. + $data = service('superglobals')->getRequestData(); + + return $this->fetchFromArray($data, $index, $filter, $flags); } /** diff --git a/system/HTTP/RequestTrait.php b/system/HTTP/RequestTrait.php index 973757c8559e..1d10d18b3658 100644 --- a/system/HTTP/RequestTrait.php +++ b/system/HTTP/RequestTrait.php @@ -291,6 +291,22 @@ public function fetchGlobal(string $name, $index = null, ?int $filter = null, $f $this->populateGlobals($name); } + return $this->fetchFromArray($this->globals[$name], $index, $filter, $flags); + } + + /** + * Fetches one or more items from an array, applying the same filtering + * and index resolution as fetchGlobal(). + * + * @param array $data + * @param int|list|string|null $index + * @param int|null $filter Filter constant + * @param array|int|null $flags Options + * + * @return mixed + */ + private function fetchFromArray(array $data, $index = null, ?int $filter = null, $flags = null) + { // Null filters cause null values to return. $filter ??= FILTER_UNSAFE_RAW; $flags = is_array($flags) ? $flags : (is_numeric($flags) ? (int) $flags : 0); @@ -299,9 +315,9 @@ public function fetchGlobal(string $name, $index = null, ?int $filter = null, $f if ($index === null) { $values = []; - foreach ($this->globals[$name] as $key => $value) { + foreach ($data as $key => $value) { $values[$key] = is_array($value) - ? $this->fetchGlobal($name, $key, $filter, $flags) + ? $this->fetchFromArray($data, $key, $filter, $flags) : filter_var($value, $filter, $flags); } @@ -313,7 +329,7 @@ public function fetchGlobal(string $name, $index = null, ?int $filter = null, $f $output = []; foreach ($index as $key) { - $output[$key] = $this->fetchGlobal($name, $key, $filter, $flags); + $output[$key] = $this->fetchFromArray($data, $key, $filter, $flags); } return $output; @@ -321,7 +337,7 @@ public function fetchGlobal(string $name, $index = null, ?int $filter = null, $f // Does the index contain array notation? if (is_string($index) && ($count = preg_match_all('/(?:^[^\[]+)|\[[^]]*\]/', $index, $matches)) > 1) { - $value = $this->globals[$name]; + $value = $data; for ($i = 0; $i < $count; $i++) { $key = trim($matches[0][$i], '[]'); @@ -338,7 +354,7 @@ public function fetchGlobal(string $name, $index = null, ?int $filter = null, $f } } - $value ??= $this->globals[$name][$index] ?? null; + $value ??= $data[$index] ?? null; if (is_array($value) && ( diff --git a/system/HTTP/SiteURIFactory.php b/system/HTTP/SiteURIFactory.php index a06a944d1331..11dccce6c540 100644 --- a/system/HTTP/SiteURIFactory.php +++ b/system/HTTP/SiteURIFactory.php @@ -171,7 +171,7 @@ private function parseRequestURI(): string // Update our global GET for values likely to have been changed parse_str($this->superglobals->server('QUERY_STRING'), $get); - $this->superglobals->setGetArray($get)->syncRequest(); + $this->superglobals->setGetArray($get); return URI::removeDotSegments($path); } @@ -203,7 +203,7 @@ private function parseQueryString(): string // Update our global GET for values likely to have been changed parse_str($this->superglobals->server('QUERY_STRING'), $get); - $this->superglobals->setGetArray($get)->syncRequest(); + $this->superglobals->setGetArray($get); return URI::removeDotSegments($path); } diff --git a/system/Superglobals.php b/system/Superglobals.php index 407e98a90b9b..c8bc5ada863c 100644 --- a/system/Superglobals.php +++ b/system/Superglobals.php @@ -388,16 +388,18 @@ public function setRequestArray(array $array): self } /** - * Rebuilds $_REQUEST from $_GET, $_POST, and $_COOKIE according to the - * `request_order` (or `variables_order`) ini setting. + * Returns the merged $_GET, $_POST, and $_COOKIE data according to the + * `request_order` (or `variables_order`) ini setting, without mutating + * $_REQUEST. * * PHP populates $_REQUEST only once at the start of the request. When * $_GET is modified later (e.g. by SiteURIFactory), $_REQUEST becomes - * stale. This method re-synchronizes $_REQUEST with the current values. + * stale. This method returns the current merged values so callers can + * read up-to-date request data without relying on the stale $_REQUEST. * - * @return self + * @return array */ - public function syncRequest(): self + public function getRequestData(): array { $requestOrder = ini_get('request_order') ?: ini_get('variables_order') ?: 'GP'; @@ -412,7 +414,7 @@ public function syncRequest(): self }; } - return $this->setRequestArray($request); + return $request; } /** diff --git a/tests/system/HTTP/IncomingRequestTest.php b/tests/system/HTTP/IncomingRequestTest.php index 464b46e2ce2c..add88bfaef3a 100644 --- a/tests/system/HTTP/IncomingRequestTest.php +++ b/tests/system/HTTP/IncomingRequestTest.php @@ -70,7 +70,7 @@ private function createRequest(?App $config = null, false|string|null $body = nu public function testCanGrabRequestVars(): void { - service('superglobals')->setRequest('TEST', '5'); + service('superglobals')->setGet('TEST', '5'); $this->assertSame('5', $this->request->getVar('TEST')); $this->assertNull($this->request->getVar('TESTY')); @@ -525,8 +525,8 @@ public function testGetVarWorksWithJsonAndGetParams(): void $config->baseURL = 'http://example.com/'; // GET method - service('superglobals')->setRequest('foo', 'bar'); - service('superglobals')->setRequest('fizz', 'buzz'); + service('superglobals')->setGet('foo', 'bar'); + service('superglobals')->setGet('fizz', 'buzz'); $request = $this->createRequest($config); $request = $request->withMethod('GET'); diff --git a/tests/system/HTTP/SiteURIFactoryDetectRoutePathTest.php b/tests/system/HTTP/SiteURIFactoryDetectRoutePathTest.php index 314fcf77ee80..4bc29fbecc4d 100644 --- a/tests/system/HTTP/SiteURIFactoryDetectRoutePathTest.php +++ b/tests/system/HTTP/SiteURIFactoryDetectRoutePathTest.php @@ -256,7 +256,6 @@ public function testQueryStringWithQueryString(): void $this->assertSame($expected, $factory->detectRoutePath('QUERY_STRING')); $this->assertSame('code=good', $_SERVER['QUERY_STRING']); // @phpstan-ignore codeigniter.superglobalsOffsetAccess (checks the live superglobal, not the snapshot service) $this->assertSame(['code' => 'good'], $_GET); - $this->assertSame(['code' => 'good'], $_REQUEST); // @phpstan-ignore codeigniter.superglobalsOffsetAccess (checks the live superglobal, not the snapshot service) } public function testQueryStringEmpty(): void diff --git a/tests/system/SuperglobalsTest.php b/tests/system/SuperglobalsTest.php index 3007cf74ad2f..7eca2beb64fc 100644 --- a/tests/system/SuperglobalsTest.php +++ b/tests/system/SuperglobalsTest.php @@ -302,32 +302,29 @@ public function testRequestSetArray(): void $this->assertSame($data, $_REQUEST); } - public function testSyncRequestRebuildsRequestFromGetPostCookie(): void + public function testGetRequestDataMergesGetPostCookie(): void { $this->superglobals->setGetArray(['get_key' => 'get_value']); $this->superglobals->setPostArray(['post_key' => 'post_value']); $this->superglobals->setCookieArray(['cookie_key' => 'cookie_value']); - $this->superglobals->syncRequest(); + $data = $this->superglobals->getRequestData(); - $this->assertSame('get_value', $this->superglobals->request('get_key')); - $this->assertSame('post_value', $this->superglobals->request('post_key')); - $this->assertSame('cookie_value', $this->superglobals->request('cookie_key')); - $this->assertSame('get_value', $_REQUEST['get_key']); // @phpstan-ignore codeigniter.superglobalsOffsetAccess (checks the live superglobal, not the snapshot service) + $this->assertSame('get_value', $data['get_key']); + $this->assertSame('post_value', $data['post_key']); + $this->assertSame('cookie_value', $data['cookie_key']); } - public function testSyncRequestReflectsGetChanges(): void + public function testGetRequestDataReflectsGetChanges(): void { $this->superglobals->setGetArray(['key' => 'old']); - $this->superglobals->syncRequest(); - $this->assertSame('old', $this->superglobals->request('key')); + $this->assertSame('old', $this->superglobals->getRequestData()['key']); // Simulate SiteURIFactory updating $_GET after the request started. $this->superglobals->setGetArray(['key' => 'new']); - $this->superglobals->syncRequest(); - $this->assertSame('new', $this->superglobals->request('key')); + $this->assertSame('new', $this->superglobals->getRequestData()['key']); } // $_FILES tests diff --git a/tests/system/Validation/ValidationTest.php b/tests/system/Validation/ValidationTest.php index 49baeaba0c79..70adb1a16d18 100644 --- a/tests/system/Validation/ValidationTest.php +++ b/tests/system/Validation/ValidationTest.php @@ -1289,7 +1289,7 @@ public function testRulesForSingleRuleWithAsteriskWillReturnNoError(): void $config = new App(); $config->baseURL = 'http://example.com/'; - service('superglobals')->setRequestArray([ + service('superglobals')->setPostArray([ 'id_user' => [ 1, 3, @@ -1316,7 +1316,7 @@ public function testRulesForSingleRuleWithAsteriskWillReturnError(): void $config = new App(); $config->baseURL = 'http://example.com/'; - service('superglobals')->setRequestArray([ + service('superglobals')->setPostArray([ 'id_user' => [ '1dfd', 3, @@ -1366,7 +1366,7 @@ public function testRulesForSingleRuleWithSingleValue(): void $config = new App(); $config->baseURL = 'http://example.com/'; - service('superglobals')->setRequestArray([ + service('superglobals')->setPostArray([ 'id_user' => 'gh', ]); From 91e42c75fc946c7977a10d43dc5ceaecef16b234 Mon Sep 17 00:00:00 2001 From: rahul05ranjan Date: Sat, 26 Sep 2026 18:49:23 +0530 Subject: [PATCH 03/14] fix: make fetchFromArray protected and avoid short ternary - fetchFromArray() must be protected so IncomingRequest (a subclass) can call it. - Replace the short ternary in getRequestData() with explicit checks to satisfy the static analysis rules. - Drop the cookie assertion from the test since request_order defaults to GP (no cookies). --- system/HTTP/RequestTrait.php | 2 +- system/Superglobals.php | 8 +++++++- tests/system/SuperglobalsTest.php | 4 +--- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/system/HTTP/RequestTrait.php b/system/HTTP/RequestTrait.php index 1d10d18b3658..1dd635be0ac8 100644 --- a/system/HTTP/RequestTrait.php +++ b/system/HTTP/RequestTrait.php @@ -305,7 +305,7 @@ public function fetchGlobal(string $name, $index = null, ?int $filter = null, $f * * @return mixed */ - private function fetchFromArray(array $data, $index = null, ?int $filter = null, $flags = null) + protected function fetchFromArray(array $data, $index = null, ?int $filter = null, $flags = null) { // Null filters cause null values to return. $filter ??= FILTER_UNSAFE_RAW; diff --git a/system/Superglobals.php b/system/Superglobals.php index c8bc5ada863c..3d7abda0680c 100644 --- a/system/Superglobals.php +++ b/system/Superglobals.php @@ -401,7 +401,13 @@ public function setRequestArray(array $array): self */ public function getRequestData(): array { - $requestOrder = ini_get('request_order') ?: ini_get('variables_order') ?: 'GP'; + $requestOrder = ini_get('request_order'); + if ($requestOrder === false || $requestOrder === '') { + $requestOrder = ini_get('variables_order'); + } + if ($requestOrder === false || $requestOrder === '') { + $requestOrder = 'GP'; + } $request = []; diff --git a/tests/system/SuperglobalsTest.php b/tests/system/SuperglobalsTest.php index 7eca2beb64fc..547493878dab 100644 --- a/tests/system/SuperglobalsTest.php +++ b/tests/system/SuperglobalsTest.php @@ -302,17 +302,15 @@ public function testRequestSetArray(): void $this->assertSame($data, $_REQUEST); } - public function testGetRequestDataMergesGetPostCookie(): void + public function testGetRequestDataMergesGetAndPost(): void { $this->superglobals->setGetArray(['get_key' => 'get_value']); $this->superglobals->setPostArray(['post_key' => 'post_value']); - $this->superglobals->setCookieArray(['cookie_key' => 'cookie_value']); $data = $this->superglobals->getRequestData(); $this->assertSame('get_value', $data['get_key']); $this->assertSame('post_value', $data['post_key']); - $this->assertSame('cookie_value', $data['cookie_key']); } public function testGetRequestDataReflectsGetChanges(): void From 22008ea1a41db3d2fa281626c0876b111815cc7d Mon Sep 17 00:00:00 2001 From: rahul05ranjan Date: Sat, 26 Sep 2026 21:29:56 +0530 Subject: [PATCH 04/14] style: fix PHP CS Fixer alignment issues - Align phpdoc @param annotations in RequestTrait::fetchFromArray. - Align match arm => operators in Superglobals::getRequestData. --- system/HTTP/RequestTrait.php | 8 ++++---- system/Superglobals.php | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/system/HTTP/RequestTrait.php b/system/HTTP/RequestTrait.php index 1dd635be0ac8..455cd02e942c 100644 --- a/system/HTTP/RequestTrait.php +++ b/system/HTTP/RequestTrait.php @@ -298,10 +298,10 @@ public function fetchGlobal(string $name, $index = null, ?int $filter = null, $f * Fetches one or more items from an array, applying the same filtering * and index resolution as fetchGlobal(). * - * @param array $data - * @param int|list|string|null $index - * @param int|null $filter Filter constant - * @param array|int|null $flags Options + * @param array $data + * @param int|list|string|null $index + * @param int|null $filter Filter constant + * @param array|int|null $flags Options * * @return mixed */ diff --git a/system/Superglobals.php b/system/Superglobals.php index 3d7abda0680c..7ff67fb52416 100644 --- a/system/Superglobals.php +++ b/system/Superglobals.php @@ -413,9 +413,9 @@ public function getRequestData(): array foreach (str_split($requestOrder) as $type) { match ($type) { - 'G' => $request = array_merge($request, $this->get), - 'P' => $request = array_merge($request, $this->post), - 'C' => $request = array_merge($request, $this->cookie), + 'G' => $request = array_merge($request, $this->get), + 'P' => $request = array_merge($request, $this->post), + 'C' => $request = array_merge($request, $this->cookie), default => null, }; } From 4721a7cf04f351f98734a430f36c6de8e4400eb3 Mon Sep 17 00:00:00 2001 From: rahul05ranjan Date: Sun, 27 Sep 2026 14:44:06 +0530 Subject: [PATCH 05/14] test: cover cookie merge, request_order precedence, and stale $_REQUEST - Make Superglobals::getRequestData() accept an optional request_order override so precedence and cookie branches can be tested deterministically. - Add tests for cookie merging, order-sensitive overwrite behavior, and unknown order types. - Add a regression test proving getVar() reflects $_GET changes even when $_REQUEST is stale. --- system/Superglobals.php | 10 +++++-- tests/system/HTTP/IncomingRequestTest.php | 11 ++++++++ tests/system/SuperglobalsTest.php | 34 +++++++++++++++++++++++ 3 files changed, 53 insertions(+), 2 deletions(-) diff --git a/system/Superglobals.php b/system/Superglobals.php index 7ff67fb52416..17d3573b2d25 100644 --- a/system/Superglobals.php +++ b/system/Superglobals.php @@ -397,14 +397,20 @@ public function setRequestArray(array $array): self * stale. This method returns the current merged values so callers can * read up-to-date request data without relying on the stale $_REQUEST. * + * @param string|null $requestOrder Overrides the ini setting for testing. + * * @return array */ - public function getRequestData(): array + public function getRequestData(?string $requestOrder = null): array { - $requestOrder = ini_get('request_order'); + if ($requestOrder === null) { + $requestOrder = ini_get('request_order'); + } + if ($requestOrder === false || $requestOrder === '') { $requestOrder = ini_get('variables_order'); } + if ($requestOrder === false || $requestOrder === '') { $requestOrder = 'GP'; } diff --git a/tests/system/HTTP/IncomingRequestTest.php b/tests/system/HTTP/IncomingRequestTest.php index add88bfaef3a..8d73a9745c67 100644 --- a/tests/system/HTTP/IncomingRequestTest.php +++ b/tests/system/HTTP/IncomingRequestTest.php @@ -76,6 +76,17 @@ public function testCanGrabRequestVars(): void $this->assertNull($this->request->getVar('TESTY')); } + public function testGetVarReflectsGetChangesWhenRequestIsStale(): void + { + // Simulate the state after SiteURIFactory updates $_GET: $_REQUEST + // still holds the original value while $_GET has been refreshed. + service('superglobals') + ->setGetArray(['code' => 'good']) + ->setRequestArray(['code' => 'stale']); + + $this->assertSame('good', $this->request->getVar('code')); + } + public function testCanGrabGetVars(): void { service('superglobals')->setGet('TEST', '5'); diff --git a/tests/system/SuperglobalsTest.php b/tests/system/SuperglobalsTest.php index 547493878dab..a4c24081d1a7 100644 --- a/tests/system/SuperglobalsTest.php +++ b/tests/system/SuperglobalsTest.php @@ -325,6 +325,40 @@ public function testGetRequestDataReflectsGetChanges(): void $this->assertSame('new', $this->superglobals->getRequestData()['key']); } + public function testGetRequestDataMergesCookie(): void + { + $this->superglobals->setGetArray(['get_key' => 'get_value']); + $this->superglobals->setPostArray(['post_key' => 'post_value']); + $this->superglobals->setCookieArray(['cookie_key' => 'cookie_value']); + + $data = $this->superglobals->getRequestData('GPC'); + + $this->assertSame('get_value', $data['get_key']); + $this->assertSame('post_value', $data['post_key']); + $this->assertSame('cookie_value', $data['cookie_key']); + } + + public function testGetRequestDataRespectsOrder(): void + { + $this->superglobals->setGetArray(['shared' => 'get']); + $this->superglobals->setPostArray(['shared' => 'post']); + $this->superglobals->setCookieArray(['shared' => 'cookie']); + + // Later sources overwrite earlier ones, matching PHP's request_order. + $this->assertSame('post', $this->superglobals->getRequestData('GP')['shared']); + $this->assertSame('cookie', $this->superglobals->getRequestData('GPC')['shared']); + $this->assertSame('get', $this->superglobals->getRequestData('PG')['shared']); + } + + public function testGetRequestDataIgnoresUnknownOrderTypes(): void + { + $this->superglobals->setGetArray(['get_key' => 'get_value']); + + $data = $this->superglobals->getRequestData('GX'); + + $this->assertSame(['get_key' => 'get_value'], $data); + } + // $_FILES tests public function testFilesGetArray(): void { From 65c4c62cec640d018bb9848ab24317255dee1ab1 Mon Sep 17 00:00:00 2001 From: rahul05ranjan Date: Mon, 28 Sep 2026 18:31:24 +0530 Subject: [PATCH 06/14] style: use null coalescing assignment in getRequestData() Rector's IfToNullCoalescingAssignRector flags the if-null block. --- system/Superglobals.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/system/Superglobals.php b/system/Superglobals.php index 17d3573b2d25..1691c65a57a6 100644 --- a/system/Superglobals.php +++ b/system/Superglobals.php @@ -403,9 +403,7 @@ public function setRequestArray(array $array): self */ public function getRequestData(?string $requestOrder = null): array { - if ($requestOrder === null) { - $requestOrder = ini_get('request_order'); - } + $requestOrder ??= ini_get('request_order'); if ($requestOrder === false || $requestOrder === '') { $requestOrder = ini_get('variables_order'); From 4627d7ed4b34886eee3f911e063821d9aab82fa4 Mon Sep 17 00:00:00 2001 From: rahul05ranjan Date: Tue, 29 Sep 2026 12:09:23 +0530 Subject: [PATCH 07/14] fix: match PHP $_REQUEST merge semantics in getRequestData() Use array_replace_recursive() instead of array_merge() so numeric keys are preserved and array values are merged recursively, matching PHP's php_autoglobal_merge. Add tests for both behaviors, update the getVar() user guide docs, and add a changelog entry. --- system/Superglobals.php | 9 ++++++--- tests/system/SuperglobalsTest.php | 19 +++++++++++++++++++ user_guide_src/source/changelogs/v4.7.5.rst | 1 + .../source/incoming/incomingrequest.rst | 8 +++++--- 4 files changed, 31 insertions(+), 6 deletions(-) diff --git a/system/Superglobals.php b/system/Superglobals.php index 1691c65a57a6..f93cf1133857 100644 --- a/system/Superglobals.php +++ b/system/Superglobals.php @@ -417,9 +417,12 @@ public function getRequestData(?string $requestOrder = null): array foreach (str_split($requestOrder) as $type) { match ($type) { - 'G' => $request = array_merge($request, $this->get), - 'P' => $request = array_merge($request, $this->post), - 'C' => $request = array_merge($request, $this->cookie), + // array_replace_recursive() matches PHP's own $_REQUEST merge + // (php_autoglobal_merge): numeric keys are preserved and + // array values are merged recursively. + 'G' => $request = array_replace_recursive($request, $this->get), + 'P' => $request = array_replace_recursive($request, $this->post), + 'C' => $request = array_replace_recursive($request, $this->cookie), default => null, }; } diff --git a/tests/system/SuperglobalsTest.php b/tests/system/SuperglobalsTest.php index a4c24081d1a7..41ce8e85fdfa 100644 --- a/tests/system/SuperglobalsTest.php +++ b/tests/system/SuperglobalsTest.php @@ -359,6 +359,25 @@ public function testGetRequestDataIgnoresUnknownOrderTypes(): void $this->assertSame(['get_key' => 'get_value'], $data); } + public function testGetRequestDataPreservesNumericKeys(): void + { + $this->superglobals->setGetArray([100 => 'foo']); // @phpstan-ignore argument.type (numeric keys are valid in superglobals, e.g. ?100=foo) + + $data = $this->superglobals->getRequestData('G'); + + $this->assertSame([100 => 'foo'], $data); + } + + public function testGetRequestDataMergesRecursively(): void + { + $this->superglobals->setGetArray(['a' => ['x' => 'get']]); + $this->superglobals->setPostArray(['a' => ['y' => 'post']]); + + $data = $this->superglobals->getRequestData('GP'); + + $this->assertSame(['a' => ['x' => 'get', 'y' => 'post']], $data); + } + // $_FILES tests public function testFilesGetArray(): void { diff --git a/user_guide_src/source/changelogs/v4.7.5.rst b/user_guide_src/source/changelogs/v4.7.5.rst index deabaf4b7ab4..70a29bbfa624 100644 --- a/user_guide_src/source/changelogs/v4.7.5.rst +++ b/user_guide_src/source/changelogs/v4.7.5.rst @@ -61,6 +61,7 @@ Bugs Fixed - **I18n:** Fixed a bug where ``Time::today()``, ``Time::yesterday()``, and ``Time::tomorrow()`` ignored the specified ``$timezone`` and ``setTestNow()`` when calculating the day. - **Logger:** Fixed a bug where interpolating a log message with array or non-stringable context values could raise PHP warnings or errors. - **Validation:** Fixed a bug where ``valid_cc_number`` accepted non-digit characters (e.g., a decimal point) in the card number. Such values could pass the Luhn check and triggered an ``Undefined array key`` warning inside it; the number is now checked with ``ctype_digit()``. +- **IncomingRequest:** Fixed a bug where ``getVar()`` returned stale data after ``$_GET`` was updated during URI parsing. It now returns a merged view of ``$_GET``, ``$_POST``, and ``$_COOKIE`` (respecting the ``request_order`` ini setting) instead of reading the stale ``$_REQUEST``. See the repo's `CHANGELOG.md `_ diff --git a/user_guide_src/source/incoming/incomingrequest.rst b/user_guide_src/source/incoming/incomingrequest.rst index 99c5762129d6..a07c8dd27adb 100644 --- a/user_guide_src/source/incoming/incomingrequest.rst +++ b/user_guide_src/source/incoming/incomingrequest.rst @@ -162,7 +162,7 @@ getVar() in new projects. Even if you are already using it, we recommend that you use another, more appropriate method. -The ``getVar()`` method will pull from ``$_REQUEST``, so will return any data from ``$_GET``, ``$_POST``, or ``$_COOKIE`` (depending on php.ini `request-order `_). +The ``getVar()`` method returns a merged view of ``$_GET``, ``$_POST``, and ``$_COOKIE`` (depending on php.ini `request-order `_). It does not read or modify ``$_REQUEST``. .. warning:: If you want to validate POST data only, don't use ``getVar()``. Newer values override older values. POST values may be overridden by the @@ -373,14 +373,16 @@ The methods provided by the parent classes that are available are: `Types of filters `__. :param int $flags: Flags to apply. A list of flags can be found in `Filter flags `__. - :returns: ``$_REQUEST`` if no parameters supplied, otherwise the REQUEST value if found, or null if not + :returns: The merged ``$_GET``, ``$_POST``, and ``$_COOKIE`` data if no parameters supplied, otherwise the value if found, or null if not :rtype: array|bool|float|int|object|string|null .. important:: This method exists only for backward compatibility. Do not use it in new projects. Even if you are already using it, we recommend that you use another, more appropriate method. - This method is identical to ``getGet()``, only it fetches REQUEST data. + This method is identical to ``getGet()``, only it fetches the merged + ``$_GET``, ``$_POST``, and ``$_COOKIE`` data (respecting the + ``request_order`` ini setting) instead of ``$_REQUEST``. .. php:method:: getGet([$index = null[, $filter = null[, $flags = null]]]) From c658c0621a99061bbd0ec27ab0c31094dd227095 Mon Sep 17 00:00:00 2001 From: rahul05ranjan Date: Tue, 29 Sep 2026 16:32:50 +0530 Subject: [PATCH 08/14] refactor: drop request_order param from getRequestData() and use ini_set in tests Address review feedback: remove the test-only \ parameter and cast ini_get() results to string so they compare against the empty string. Tests now set request_order via ini_set(). Also reorder the changelog entry alphabetically under HTTP. --- system/Superglobals.php | 12 ++++----- tests/system/SuperglobalsTest.php | 27 +++++++++++++++------ user_guide_src/source/changelogs/v4.7.5.rst | 2 +- 3 files changed, 26 insertions(+), 15 deletions(-) diff --git a/system/Superglobals.php b/system/Superglobals.php index f93cf1133857..33565855b872 100644 --- a/system/Superglobals.php +++ b/system/Superglobals.php @@ -397,19 +397,17 @@ public function setRequestArray(array $array): self * stale. This method returns the current merged values so callers can * read up-to-date request data without relying on the stale $_REQUEST. * - * @param string|null $requestOrder Overrides the ini setting for testing. - * * @return array */ - public function getRequestData(?string $requestOrder = null): array + public function getRequestData(): array { - $requestOrder ??= ini_get('request_order'); + $requestOrder = (string) ini_get('request_order'); - if ($requestOrder === false || $requestOrder === '') { - $requestOrder = ini_get('variables_order'); + if ($requestOrder === '') { + $requestOrder = (string) ini_get('variables_order'); } - if ($requestOrder === false || $requestOrder === '') { + if ($requestOrder === '') { $requestOrder = 'GP'; } diff --git a/tests/system/SuperglobalsTest.php b/tests/system/SuperglobalsTest.php index 41ce8e85fdfa..0971b8c8080d 100644 --- a/tests/system/SuperglobalsTest.php +++ b/tests/system/SuperglobalsTest.php @@ -327,11 +327,13 @@ public function testGetRequestDataReflectsGetChanges(): void public function testGetRequestDataMergesCookie(): void { + ini_set('request_order', 'GPC'); + $this->superglobals->setGetArray(['get_key' => 'get_value']); $this->superglobals->setPostArray(['post_key' => 'post_value']); $this->superglobals->setCookieArray(['cookie_key' => 'cookie_value']); - $data = $this->superglobals->getRequestData('GPC'); + $data = $this->superglobals->getRequestData(); $this->assertSame('get_value', $data['get_key']); $this->assertSame('post_value', $data['post_key']); @@ -345,35 +347,46 @@ public function testGetRequestDataRespectsOrder(): void $this->superglobals->setCookieArray(['shared' => 'cookie']); // Later sources overwrite earlier ones, matching PHP's request_order. - $this->assertSame('post', $this->superglobals->getRequestData('GP')['shared']); - $this->assertSame('cookie', $this->superglobals->getRequestData('GPC')['shared']); - $this->assertSame('get', $this->superglobals->getRequestData('PG')['shared']); + ini_set('request_order', 'GP'); + $this->assertSame('post', $this->superglobals->getRequestData()['shared']); + + ini_set('request_order', 'GPC'); + $this->assertSame('cookie', $this->superglobals->getRequestData()['shared']); + + ini_set('request_order', 'PG'); + $this->assertSame('get', $this->superglobals->getRequestData()['shared']); } public function testGetRequestDataIgnoresUnknownOrderTypes(): void { + ini_set('request_order', 'GX'); + $this->superglobals->setGetArray(['get_key' => 'get_value']); - $data = $this->superglobals->getRequestData('GX'); + $data = $this->superglobals->getRequestData(); $this->assertSame(['get_key' => 'get_value'], $data); } public function testGetRequestDataPreservesNumericKeys(): void { + ini_set('request_order', 'G'); + $this->superglobals->setGetArray([100 => 'foo']); // @phpstan-ignore argument.type (numeric keys are valid in superglobals, e.g. ?100=foo) - $data = $this->superglobals->getRequestData('G'); + $data = $this->superglobals->getRequestData(); $this->assertSame([100 => 'foo'], $data); } public function testGetRequestDataMergesRecursively(): void { + ini_set('request_order', 'GP'); + $this->superglobals->setGetArray(['a' => ['x' => 'get']]); $this->superglobals->setPostArray(['a' => ['y' => 'post']]); - $data = $this->superglobals->getRequestData('GP'); + $data = $this->superglobals->getRequestData(); $this->assertSame(['a' => ['x' => 'get', 'y' => 'post']], $data); } diff --git a/user_guide_src/source/changelogs/v4.7.5.rst b/user_guide_src/source/changelogs/v4.7.5.rst index 70a29bbfa624..ffe5f64735af 100644 --- a/user_guide_src/source/changelogs/v4.7.5.rst +++ b/user_guide_src/source/changelogs/v4.7.5.rst @@ -58,10 +58,10 @@ Bugs Fixed - **Files:** Fixed a bug where ``File::move()`` and ``UploadedFile::move()`` set executable and overly permissive file permissions (``0777 & ~umask()`` instead of ``0666 & ~umask()``), and ``UploadedFile::move()`` targeted the parent directory instead of the destination file for ``chmod()``. - **Helpers:** Fixed a bug where ``get_dir_file_info()`` returned incomplete entries for subdirectories and missing files instead of omitting them. - **Honeypot:** Fixed a bug where bot detection returned an HTTP 500 response instead of 403 (Forbidden). +- **HTTP:** Fixed a bug where ``IncomingRequest::getVar()`` returned stale data after ``$_GET`` was updated during URI parsing. It now returns a merged view of ``$_GET``, ``$_POST``, and ``$_COOKIE`` (respecting the ``request_order`` ini setting) instead of reading the stale ``$_REQUEST``. - **I18n:** Fixed a bug where ``Time::today()``, ``Time::yesterday()``, and ``Time::tomorrow()`` ignored the specified ``$timezone`` and ``setTestNow()`` when calculating the day. - **Logger:** Fixed a bug where interpolating a log message with array or non-stringable context values could raise PHP warnings or errors. - **Validation:** Fixed a bug where ``valid_cc_number`` accepted non-digit characters (e.g., a decimal point) in the card number. Such values could pass the Luhn check and triggered an ``Undefined array key`` warning inside it; the number is now checked with ``ctype_digit()``. -- **IncomingRequest:** Fixed a bug where ``getVar()`` returned stale data after ``$_GET`` was updated during URI parsing. It now returns a merged view of ``$_GET``, ``$_POST``, and ``$_COOKIE`` (respecting the ``request_order`` ini setting) instead of reading the stale ``$_REQUEST``. See the repo's `CHANGELOG.md `_ From b96b30ae9cfcf4b1e01391e3d76f8bb8868e6237 Mon Sep 17 00:00:00 2001 From: rahul05ranjan Date: Tue, 29 Sep 2026 19:33:41 +0530 Subject: [PATCH 09/14] fix: make getRequestData() accept request_order override for tests --- system/Superglobals.php | 10 ++++++++-- tests/system/SuperglobalsTest.php | 27 +++++++-------------------- 2 files changed, 15 insertions(+), 22 deletions(-) diff --git a/system/Superglobals.php b/system/Superglobals.php index 33565855b872..8b45133fb1dd 100644 --- a/system/Superglobals.php +++ b/system/Superglobals.php @@ -397,11 +397,17 @@ public function setRequestArray(array $array): self * stale. This method returns the current merged values so callers can * read up-to-date request data without relying on the stale $_REQUEST. * + * @param string|null $requestOrder Overrides the `request_order` ini + * setting. Useful for testing, since the + * ini setting cannot be changed at runtime. + * * @return array */ - public function getRequestData(): array + public function getRequestData(?string $requestOrder = null): array { - $requestOrder = (string) ini_get('request_order'); + if ($requestOrder === null) { + $requestOrder = (string) ini_get('request_order'); + } if ($requestOrder === '') { $requestOrder = (string) ini_get('variables_order'); diff --git a/tests/system/SuperglobalsTest.php b/tests/system/SuperglobalsTest.php index 0971b8c8080d..41ce8e85fdfa 100644 --- a/tests/system/SuperglobalsTest.php +++ b/tests/system/SuperglobalsTest.php @@ -327,13 +327,11 @@ public function testGetRequestDataReflectsGetChanges(): void public function testGetRequestDataMergesCookie(): void { - ini_set('request_order', 'GPC'); - $this->superglobals->setGetArray(['get_key' => 'get_value']); $this->superglobals->setPostArray(['post_key' => 'post_value']); $this->superglobals->setCookieArray(['cookie_key' => 'cookie_value']); - $data = $this->superglobals->getRequestData(); + $data = $this->superglobals->getRequestData('GPC'); $this->assertSame('get_value', $data['get_key']); $this->assertSame('post_value', $data['post_key']); @@ -347,46 +345,35 @@ public function testGetRequestDataRespectsOrder(): void $this->superglobals->setCookieArray(['shared' => 'cookie']); // Later sources overwrite earlier ones, matching PHP's request_order. - ini_set('request_order', 'GP'); - $this->assertSame('post', $this->superglobals->getRequestData()['shared']); - - ini_set('request_order', 'GPC'); - $this->assertSame('cookie', $this->superglobals->getRequestData()['shared']); - - ini_set('request_order', 'PG'); - $this->assertSame('get', $this->superglobals->getRequestData()['shared']); + $this->assertSame('post', $this->superglobals->getRequestData('GP')['shared']); + $this->assertSame('cookie', $this->superglobals->getRequestData('GPC')['shared']); + $this->assertSame('get', $this->superglobals->getRequestData('PG')['shared']); } public function testGetRequestDataIgnoresUnknownOrderTypes(): void { - ini_set('request_order', 'GX'); - $this->superglobals->setGetArray(['get_key' => 'get_value']); - $data = $this->superglobals->getRequestData(); + $data = $this->superglobals->getRequestData('GX'); $this->assertSame(['get_key' => 'get_value'], $data); } public function testGetRequestDataPreservesNumericKeys(): void { - ini_set('request_order', 'G'); - $this->superglobals->setGetArray([100 => 'foo']); // @phpstan-ignore argument.type (numeric keys are valid in superglobals, e.g. ?100=foo) - $data = $this->superglobals->getRequestData(); + $data = $this->superglobals->getRequestData('G'); $this->assertSame([100 => 'foo'], $data); } public function testGetRequestDataMergesRecursively(): void { - ini_set('request_order', 'GP'); - $this->superglobals->setGetArray(['a' => ['x' => 'get']]); $this->superglobals->setPostArray(['a' => ['y' => 'post']]); - $data = $this->superglobals->getRequestData(); + $data = $this->superglobals->getRequestData('GP'); $this->assertSame(['a' => ['x' => 'get', 'y' => 'post']], $data); } From d0522b82417244530c7fbe9a37c9244f0eaf6001 Mon Sep 17 00:00:00 2001 From: rahul05ranjan Date: Tue, 29 Sep 2026 19:40:12 +0530 Subject: [PATCH 10/14] style: apply Rector IfToNullCoalescingAssignRector --- system/Superglobals.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/system/Superglobals.php b/system/Superglobals.php index 8b45133fb1dd..8c2287b8fcf1 100644 --- a/system/Superglobals.php +++ b/system/Superglobals.php @@ -405,9 +405,7 @@ public function setRequestArray(array $array): self */ public function getRequestData(?string $requestOrder = null): array { - if ($requestOrder === null) { - $requestOrder = (string) ini_get('request_order'); - } + $requestOrder ??= (string) ini_get('request_order'); if ($requestOrder === '') { $requestOrder = (string) ini_get('variables_order'); From 34d46b9a6c86500128fe8231eb9fa3e46876e368 Mon Sep 17 00:00:00 2001 From: rahul05ranjan Date: Mon, 5 Oct 2026 13:52:52 +0530 Subject: [PATCH 11/14] fix: preserve request overrides and PHP request-order semantics Reuse fetchGlobal for merged request data, preserve explicit request globals, and clear temporary data after filtering. Remove the added protected helper to avoid subclass signature collisions. Normalize request-order case and process each source once. --- system/HTTP/IncomingRequest.php | 19 ++++-- system/HTTP/RequestTrait.php | 26 ++------ system/Superglobals.php | 2 +- tests/system/HTTP/IncomingRequestTest.php | 60 +++++++++++++++++++ tests/system/SuperglobalsTest.php | 11 ++++ .../source/incoming/incomingrequest.rst | 4 +- 6 files changed, 93 insertions(+), 29 deletions(-) diff --git a/system/HTTP/IncomingRequest.php b/system/HTTP/IncomingRequest.php index b9fb942ae23a..0946f3232d3c 100644 --- a/system/HTTP/IncomingRequest.php +++ b/system/HTTP/IncomingRequest.php @@ -388,12 +388,21 @@ public function getVar($index = null, $filter = null, $flags = null) return $this->getJsonVar($index, false, $filter, $flags); } - // $_REQUEST is populated only once at the start of the request, so it - // can become stale when $_GET is modified later (e.g. by SiteURIFactory). - // Merge the current superglobals instead of reading the stale $_REQUEST. - $data = service('superglobals')->getRequestData(); + // Preserve request data explicitly supplied through setGlobal(). + if (isset($this->globals['request'])) { + return $this->fetchGlobal('request', $index, $filter, $flags); + } + + // $_REQUEST can become stale when SiteURIFactory updates $_GET. + // Use the existing filtering path with a fresh merged view, without + // caching it between calls or modifying the superglobals. + $this->globals['request'] = service('superglobals')->getRequestData(); - return $this->fetchFromArray($data, $index, $filter, $flags); + try { + return $this->fetchGlobal('request', $index, $filter, $flags); + } finally { + unset($this->globals['request']); + } } /** diff --git a/system/HTTP/RequestTrait.php b/system/HTTP/RequestTrait.php index 455cd02e942c..973757c8559e 100644 --- a/system/HTTP/RequestTrait.php +++ b/system/HTTP/RequestTrait.php @@ -291,22 +291,6 @@ public function fetchGlobal(string $name, $index = null, ?int $filter = null, $f $this->populateGlobals($name); } - return $this->fetchFromArray($this->globals[$name], $index, $filter, $flags); - } - - /** - * Fetches one or more items from an array, applying the same filtering - * and index resolution as fetchGlobal(). - * - * @param array $data - * @param int|list|string|null $index - * @param int|null $filter Filter constant - * @param array|int|null $flags Options - * - * @return mixed - */ - protected function fetchFromArray(array $data, $index = null, ?int $filter = null, $flags = null) - { // Null filters cause null values to return. $filter ??= FILTER_UNSAFE_RAW; $flags = is_array($flags) ? $flags : (is_numeric($flags) ? (int) $flags : 0); @@ -315,9 +299,9 @@ protected function fetchFromArray(array $data, $index = null, ?int $filter = nul if ($index === null) { $values = []; - foreach ($data as $key => $value) { + foreach ($this->globals[$name] as $key => $value) { $values[$key] = is_array($value) - ? $this->fetchFromArray($data, $key, $filter, $flags) + ? $this->fetchGlobal($name, $key, $filter, $flags) : filter_var($value, $filter, $flags); } @@ -329,7 +313,7 @@ protected function fetchFromArray(array $data, $index = null, ?int $filter = nul $output = []; foreach ($index as $key) { - $output[$key] = $this->fetchFromArray($data, $key, $filter, $flags); + $output[$key] = $this->fetchGlobal($name, $key, $filter, $flags); } return $output; @@ -337,7 +321,7 @@ protected function fetchFromArray(array $data, $index = null, ?int $filter = nul // Does the index contain array notation? if (is_string($index) && ($count = preg_match_all('/(?:^[^\[]+)|\[[^]]*\]/', $index, $matches)) > 1) { - $value = $data; + $value = $this->globals[$name]; for ($i = 0; $i < $count; $i++) { $key = trim($matches[0][$i], '[]'); @@ -354,7 +338,7 @@ protected function fetchFromArray(array $data, $index = null, ?int $filter = nul } } - $value ??= $data[$index] ?? null; + $value ??= $this->globals[$name][$index] ?? null; if (is_array($value) && ( diff --git a/system/Superglobals.php b/system/Superglobals.php index 8c2287b8fcf1..21909e432b0e 100644 --- a/system/Superglobals.php +++ b/system/Superglobals.php @@ -417,7 +417,7 @@ public function getRequestData(?string $requestOrder = null): array $request = []; - foreach (str_split($requestOrder) as $type) { + foreach (array_unique(str_split(strtoupper($requestOrder))) as $type) { match ($type) { // array_replace_recursive() matches PHP's own $_REQUEST merge // (php_autoglobal_merge): numeric keys are preserved and diff --git a/tests/system/HTTP/IncomingRequestTest.php b/tests/system/HTTP/IncomingRequestTest.php index 8d73a9745c67..cc0e753ea741 100644 --- a/tests/system/HTTP/IncomingRequestTest.php +++ b/tests/system/HTTP/IncomingRequestTest.php @@ -29,6 +29,7 @@ use PHPUnit\Framework\Attributes\PreserveGlobalState; use PHPUnit\Framework\Attributes\RunInSeparateProcess; use PHPUnit\Framework\Attributes\WithoutErrorHandler; +use RuntimeException; /** * @internal @@ -87,6 +88,65 @@ public function testGetVarReflectsGetChangesWhenRequestIsStale(): void $this->assertSame('good', $this->request->getVar('code')); } + public function testGetVarPreservesExplicitRequestOverride(): void + { + service('superglobals')->setGetArray(['foo' => 'get', 'extra' => 'value']); + $this->request->setGlobal('request', ['foo' => 'bar']); + + $this->assertSame('bar', $this->request->getVar('foo')); + $this->assertNull($this->request->getVar('extra')); + $this->assertSame(['foo' => 'bar'], $this->request->getVar()); + + $this->request->setGlobal('request', []); + + $this->assertNull($this->request->getVar('foo')); + } + + public function testGetVarRefreshesMergedDataAfterFirstRead(): void + { + service('superglobals')->setGetArray(['foo' => 'old']); + $this->assertSame('old', $this->request->getVar('foo')); + + service('superglobals')->setGet('foo', 'new'); + + $this->assertSame('new', $this->request->getVar('foo')); + } + + public function testGetVarClearsMergedDataAfterFilterThrows(): void + { + service('superglobals')->setGetArray(['foo' => 'old']); + + try { + $this->request->getVar('foo', FILTER_CALLBACK, [ + 'options' => static function (): never { + throw new RuntimeException('Filter failed'); + }, + ]); + $this->fail('The filter should throw an exception.'); + } catch (RuntimeException $e) { + $this->assertSame('Filter failed', $e->getMessage()); + } + + service('superglobals')->setGet('foo', 'new'); + + $this->assertSame('new', $this->request->getVar('foo')); + } + + public function testGetVarAllowsSubclassWithFetchFromArrayMethod(): void + { + $config = new App(); + $request = new class ($config, new SiteURI($config), null, new UserAgent()) extends IncomingRequest { + protected function fetchFromArray(): string + { + return 'application helper'; + } + }; + + service('superglobals')->setGetArray(['foo' => 'bar']); + + $this->assertSame('bar', $request->getVar('foo')); + } + public function testCanGrabGetVars(): void { service('superglobals')->setGet('TEST', '5'); diff --git a/tests/system/SuperglobalsTest.php b/tests/system/SuperglobalsTest.php index 41ce8e85fdfa..d1d5529c0773 100644 --- a/tests/system/SuperglobalsTest.php +++ b/tests/system/SuperglobalsTest.php @@ -350,6 +350,17 @@ public function testGetRequestDataRespectsOrder(): void $this->assertSame('get', $this->superglobals->getRequestData('PG')['shared']); } + public function testGetRequestDataNormalizesOrderAndIgnoresDuplicates(): void + { + $this->superglobals->setGetArray(['shared' => 'get']); + $this->superglobals->setPostArray(['shared' => 'post']); + $this->superglobals->setCookieArray(['shared' => 'cookie']); + + $this->assertSame('post', $this->superglobals->getRequestData('gp')['shared']); + $this->assertSame('post', $this->superglobals->getRequestData('GPG')['shared']); + $this->assertSame('cookie', $this->superglobals->getRequestData('gPcGpC')['shared']); + } + public function testGetRequestDataIgnoresUnknownOrderTypes(): void { $this->superglobals->setGetArray(['get_key' => 'get_value']); diff --git a/user_guide_src/source/incoming/incomingrequest.rst b/user_guide_src/source/incoming/incomingrequest.rst index a07c8dd27adb..27d3a5505af1 100644 --- a/user_guide_src/source/incoming/incomingrequest.rst +++ b/user_guide_src/source/incoming/incomingrequest.rst @@ -162,7 +162,7 @@ getVar() in new projects. Even if you are already using it, we recommend that you use another, more appropriate method. -The ``getVar()`` method returns a merged view of ``$_GET``, ``$_POST``, and ``$_COOKIE`` (depending on php.ini `request-order `_). It does not read or modify ``$_REQUEST``. +The ``getVar()`` method returns a merged view of ``$_GET``, ``$_POST``, and ``$_COOKIE`` (depending on php.ini `request-order `_). It does not read or modify ``$_REQUEST``. Data explicitly supplied through ``setGlobal('request', ...)`` takes precedence over the merged view. .. warning:: If you want to validate POST data only, don't use ``getVar()``. Newer values override older values. POST values may be overridden by the @@ -170,7 +170,7 @@ The ``getVar()`` method returns a merged view of ``$_GET``, ``$_POST``, and ``$_ `request-order `_. .. note:: If the incoming request has a ``Content-Type`` header set to ``application/json``, - the ``getVar()`` method returns the JSON data instead of ``$_REQUEST`` data. + the ``getVar()`` method returns the JSON data instead of the merged superglobal data. .. _incomingrequest-getting-json-data: From 9d7d1431665f8dcdc2b8fa63bdd13af7c4b72a3b Mon Sep 17 00:00:00 2001 From: rahul05ranjan Date: Thu, 8 Oct 2026 13:23:07 +0530 Subject: [PATCH 12/14] fix: distinguish request overrides from cached globals --- system/HTTP/IncomingRequest.php | 23 +++++++++++++++++++++-- tests/system/HTTP/IncomingRequestTest.php | 15 +++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/system/HTTP/IncomingRequest.php b/system/HTTP/IncomingRequest.php index 0946f3232d3c..1205b1c84dfc 100644 --- a/system/HTTP/IncomingRequest.php +++ b/system/HTTP/IncomingRequest.php @@ -49,6 +49,9 @@ */ class IncomingRequest extends Request { + /** Distinguishes an explicit request override from fetchGlobal()'s cache. */ + private bool $requestGlobalWasExplicitlySet = false; + /** * The URI for this request. * @@ -389,20 +392,36 @@ public function getVar($index = null, $filter = null, $flags = null) } // Preserve request data explicitly supplied through setGlobal(). - if (isset($this->globals['request'])) { + if ($this->requestGlobalWasExplicitlySet) { return $this->fetchGlobal('request', $index, $filter, $flags); } // $_REQUEST can become stale when SiteURIFactory updates $_GET. // Use the existing filtering path with a fresh merged view, without // caching it between calls or modifying the superglobals. + $cachedRequest = $this->globals['request'] ?? null; $this->globals['request'] = service('superglobals')->getRequestData(); try { return $this->fetchGlobal('request', $index, $filter, $flags); } finally { - unset($this->globals['request']); + if ($cachedRequest === null) { + unset($this->globals['request']); + } else { + $this->globals['request'] = $cachedRequest; + } + } + } + + public function setGlobal(string $name, $value) + { + parent::setGlobal($name, $value); + + if ($name === 'request') { + $this->requestGlobalWasExplicitlySet = true; } + + return $this; } /** diff --git a/tests/system/HTTP/IncomingRequestTest.php b/tests/system/HTTP/IncomingRequestTest.php index cc0e753ea741..f88061980123 100644 --- a/tests/system/HTTP/IncomingRequestTest.php +++ b/tests/system/HTTP/IncomingRequestTest.php @@ -112,6 +112,21 @@ public function testGetVarRefreshesMergedDataAfterFirstRead(): void $this->assertSame('new', $this->request->getVar('foo')); } + public function testGetVarDoesNotTreatFetchGlobalCacheAsRequestOverride(): void + { + service('superglobals') + ->setGetArray(['foo' => 'fresh']) + ->setRequestArray(['foo' => 'stale']); + + $this->assertSame('stale', $this->request->fetchGlobal('request', 'foo')); + $this->assertSame('fresh', $this->request->getVar('foo')); + $this->assertSame('stale', $this->request->fetchGlobal('request', 'foo')); + + $this->request->setGlobal('request', ['foo' => 'explicit']); + + $this->assertSame('explicit', $this->request->getVar('foo')); + } + public function testGetVarClearsMergedDataAfterFilterThrows(): void { service('superglobals')->setGetArray(['foo' => 'old']); From ae938158ebb7d0e5368322190611ac1cf49be611 Mon Sep 17 00:00:00 2001 From: rahul05ranjan Date: Thu, 8 Oct 2026 13:24:40 +0530 Subject: [PATCH 13/14] test: cover empty request order fallback --- tests/system/SuperglobalsTest.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/system/SuperglobalsTest.php b/tests/system/SuperglobalsTest.php index d1d5529c0773..309acbf8eec6 100644 --- a/tests/system/SuperglobalsTest.php +++ b/tests/system/SuperglobalsTest.php @@ -350,6 +350,18 @@ public function testGetRequestDataRespectsOrder(): void $this->assertSame('get', $this->superglobals->getRequestData('PG')['shared']); } + public function testGetRequestDataEmptyOrderFallsBackToVariablesOrder(): void + { + $this->superglobals->setGetArray(['get' => 'value']); + $this->superglobals->setPostArray(['post' => 'value']); + $this->superglobals->setCookieArray(['cookie' => 'value']); + + $this->assertSame( + $this->superglobals->getRequestData((string) ini_get('variables_order')), + $this->superglobals->getRequestData(''), + ); + } + public function testGetRequestDataNormalizesOrderAndIgnoresDuplicates(): void { $this->superglobals->setGetArray(['shared' => 'get']); From 59ff334a24398e05d36640d96032b0dbf88cfc01 Mon Sep 17 00:00:00 2001 From: rahul05ranjan Date: Thu, 8 Oct 2026 15:22:24 +0530 Subject: [PATCH 14/14] style: format request override property docblock --- system/HTTP/IncomingRequest.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/system/HTTP/IncomingRequest.php b/system/HTTP/IncomingRequest.php index 1205b1c84dfc..908ba02abcb9 100644 --- a/system/HTTP/IncomingRequest.php +++ b/system/HTTP/IncomingRequest.php @@ -49,7 +49,9 @@ */ class IncomingRequest extends Request { - /** Distinguishes an explicit request override from fetchGlobal()'s cache. */ + /** + * Distinguishes an explicit request override from fetchGlobal()'s cache. + */ private bool $requestGlobalWasExplicitlySet = false; /**