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..455cd02e942c 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 + */ + 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); @@ -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/Superglobals.php b/system/Superglobals.php index ac0ea289bd77..1691c65a57a6 100644 --- a/system/Superglobals.php +++ b/system/Superglobals.php @@ -387,6 +387,46 @@ public function setRequestArray(array $array): self return $this; } + /** + * 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 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 + { + $requestOrder ??= ini_get('request_order'); + + if ($requestOrder === false || $requestOrder === '') { + $requestOrder = ini_get('variables_order'); + } + + if ($requestOrder === false || $requestOrder === '') { + $requestOrder = '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 $request; + } + /** * Get all $_FILES values. * diff --git a/tests/system/HTTP/IncomingRequestTest.php b/tests/system/HTTP/IncomingRequestTest.php index 464b46e2ce2c..8d73a9745c67 100644 --- a/tests/system/HTTP/IncomingRequestTest.php +++ b/tests/system/HTTP/IncomingRequestTest.php @@ -70,12 +70,23 @@ 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')); } + 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'); @@ -525,8 +536,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/SuperglobalsTest.php b/tests/system/SuperglobalsTest.php index e4b8b23b2b28..a4c24081d1a7 100644 --- a/tests/system/SuperglobalsTest.php +++ b/tests/system/SuperglobalsTest.php @@ -302,6 +302,63 @@ public function testRequestSetArray(): void $this->assertSame($data, $_REQUEST); } + public function testGetRequestDataMergesGetAndPost(): void + { + $this->superglobals->setGetArray(['get_key' => 'get_value']); + $this->superglobals->setPostArray(['post_key' => 'post_value']); + + $data = $this->superglobals->getRequestData(); + + $this->assertSame('get_value', $data['get_key']); + $this->assertSame('post_value', $data['post_key']); + } + + public function testGetRequestDataReflectsGetChanges(): void + { + $this->superglobals->setGetArray(['key' => 'old']); + + $this->assertSame('old', $this->superglobals->getRequestData()['key']); + + // Simulate SiteURIFactory updating $_GET after the request started. + $this->superglobals->setGetArray(['key' => 'new']); + + $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 { 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', ]);