diff --git a/system/Filters/PageCache.php b/system/Filters/PageCache.php index a4858c400c71..fe984c065992 100644 --- a/system/Filters/PageCache.php +++ b/system/Filters/PageCache.php @@ -16,6 +16,7 @@ use CodeIgniter\Cache\ResponseCache; use CodeIgniter\HTTP\CLIRequest; use CodeIgniter\HTTP\IncomingRequest; +use CodeIgniter\HTTP\Method; use CodeIgniter\HTTP\NonBufferedResponseInterface; use CodeIgniter\HTTP\RedirectResponse; use CodeIgniter\HTTP\RequestInterface; @@ -53,6 +54,10 @@ public function before(RequestInterface $request, $arguments = null) { assert($request instanceof CLIRequest || $request instanceof IncomingRequest); + if ($request instanceof IncomingRequest && $request->getMethod() === Method::QUERY) { + return null; + } + $response = service('response'); return $this->pageCache->get($request, $response); @@ -67,6 +72,10 @@ public function after(RequestInterface $request, ResponseInterface $response, $a { assert($request instanceof CLIRequest || $request instanceof IncomingRequest); + if ($request instanceof IncomingRequest && $request->getMethod() === Method::QUERY) { + return null; + } + if ( ! $response instanceof NonBufferedResponseInterface && ! $response instanceof RedirectResponse diff --git a/system/HTTP/CURLRequest.php b/system/HTTP/CURLRequest.php index 3c199fb9a309..0180c832860e 100644 --- a/system/HTTP/CURLRequest.php +++ b/system/HTTP/CURLRequest.php @@ -292,6 +292,16 @@ public function put(string $url, array $options = []): ResponseInterface return $this->request(Method::PUT, $url, $options); } + /** + * Convenience method for sending a QUERY request. + * + * @param array $options + */ + public function query(string $url, array $options = []): ResponseInterface + { + return $this->request(Method::QUERY, $url, $options); + } + /** * Set the HTTP Authentication. * diff --git a/system/HTTP/FormRequest.php b/system/HTTP/FormRequest.php index 881824e18acf..1a38eeae00f4 100644 --- a/system/HTTP/FormRequest.php +++ b/system/HTTP/FormRequest.php @@ -223,10 +223,10 @@ public function getValidatedInput(): ValidatedInput * multiple sources. By default, data is sourced from the appropriate * part of the request based on HTTP method and Content-Type: * - * - JSON (any method) - decoded JSON body - * - PUT / PATCH / DELETE - raw body (unless multipart/form-data) - * - GET / HEAD - query-string parameters - * - Everything else (POST) - POST body + * - JSON (any method) - decoded JSON body + * - PUT / PATCH / DELETE / QUERY - raw body (unless multipart/form-data) + * - GET / HEAD - query-string parameters + * - Everything else (POST) - POST body * * @return array */ @@ -239,7 +239,7 @@ protected function validationData(): array } if ( - in_array($this->request->getMethod(), [Method::PUT, Method::PATCH, Method::DELETE], true) + in_array($this->request->getMethod(), [Method::PUT, Method::PATCH, Method::DELETE, Method::QUERY], true) && ! str_contains($contentType, 'multipart/form-data') ) { return $this->request->getRawInput() ?? []; diff --git a/system/HTTP/IncomingRequest.php b/system/HTTP/IncomingRequest.php index 8cf8f8dfc693..3808935a4a09 100644 --- a/system/HTTP/IncomingRequest.php +++ b/system/HTTP/IncomingRequest.php @@ -503,7 +503,7 @@ public function getJsonVar($index = null, bool $assoc = false, ?int $filter = nu } /** - * A convenience method that grabs the raw input stream(send method in PUT, PATCH, DELETE) and decodes + * A convenience method that grabs the raw input stream (send method in PUT, PATCH, DELETE, QUERY) and decodes * the String into an array. * * @return array @@ -516,7 +516,7 @@ public function getRawInput() } /** - * Gets a specific variable from raw input stream (send method in PUT, PATCH, DELETE). + * Gets a specific variable from raw input stream (send method in PUT, PATCH, DELETE, QUERY). * * @param array|string|null $index The variable that you want which can use dot syntax for getting specific values. * @param int|null $filter Filter Constant diff --git a/system/HTTP/Method.php b/system/HTTP/Method.php index 09e1dbc95ecb..e86bcaed9b76 100644 --- a/system/HTTP/Method.php +++ b/system/HTTP/Method.php @@ -90,6 +90,15 @@ class Method */ public const PUT = 'PUT'; + /** + * Safe: Yes + * Idempotent: Yes + * Cacheable: Yes + * + * @see https://www.rfc-editor.org/rfc/rfc10008.html + */ + public const QUERY = 'QUERY'; + /** * Safe: Yes * Idempotent: Yes @@ -115,6 +124,7 @@ public static function all(): array self::PATCH, self::POST, self::PUT, + self::QUERY, self::TRACE, ]; } diff --git a/system/Router/RouteCollection.php b/system/Router/RouteCollection.php index 0710c6902836..2c2700046d00 100644 --- a/system/Router/RouteCollection.php +++ b/system/Router/RouteCollection.php @@ -143,6 +143,7 @@ class RouteCollection implements RouteCollectionInterface Method::OPTIONS => [], Method::GET => [], Method::HEAD => [], + Method::QUERY => [], Method::POST => [], Method::PATCH => [], Method::PUT => [], @@ -168,6 +169,7 @@ class RouteCollection implements RouteCollectionInterface Method::OPTIONS => [], Method::GET => [], Method::HEAD => [], + Method::QUERY => [], Method::POST => [], Method::PATCH => [], Method::PUT => [], @@ -1102,6 +1104,19 @@ public function head(string $from, $to, ?array $options = null): RouteCollection return $this; } + /** + * Specifies a route that is only available to QUERY requests. + * + * @param array|(Closure(mixed...): (ResponseInterface|string|void))|string $to + * @param array|null $options + */ + public function query(string $from, $to, ?array $options = null): RouteCollectionInterface + { + $this->create(Method::QUERY, $from, $to, $options); + + return $this; + } + /** * Specifies a route that is only available to PATCH requests. * diff --git a/system/Router/Router.php b/system/Router/Router.php index 924a1ae7ea5f..5755c0cf23fe 100644 --- a/system/Router/Router.php +++ b/system/Router/Router.php @@ -43,6 +43,7 @@ class Router implements RouterInterface public const HTTP_METHODS = [ Method::GET, Method::HEAD, + Method::QUERY, Method::POST, Method::PATCH, Method::PUT, diff --git a/system/Test/FeatureTestTrait.php b/system/Test/FeatureTestTrait.php index c45ed882699f..37d44b6d8ac1 100644 --- a/system/Test/FeatureTestTrait.php +++ b/system/Test/FeatureTestTrait.php @@ -296,6 +296,21 @@ public function options(string $path, ?array $params = null) return $this->call(Method::OPTIONS, $path, $params); } + /** + * Performs a QUERY request. + * + * @param array|null $params + * + * @return TestResponse + * + * @throws RedirectException + * @throws Exception + */ + public function query(string $path, ?array $params = null) + { + return $this->call(Method::QUERY, $path, $params); + } + /** * Setup a Request object to use so that CodeIgniter * won't try to auto-populate some of the items. diff --git a/system/Validation/Validation.php b/system/Validation/Validation.php index fa8522ac7799..7f53bb58c1f3 100644 --- a/system/Validation/Validation.php +++ b/system/Validation/Validation.php @@ -535,7 +535,7 @@ public function withRequest(RequestInterface $request): ValidationInterface return $this; } - if (in_array($request->getMethod(), [Method::PUT, Method::PATCH, Method::DELETE], true) + if (in_array($request->getMethod(), [Method::PUT, Method::PATCH, Method::DELETE, Method::QUERY], true) && ! str_contains($request->getHeaderLine('Content-Type'), 'multipart/form-data') ) { $this->data = $request->getRawInput(); diff --git a/tests/system/Commands/Utilities/RoutesTest.php b/tests/system/Commands/Utilities/RoutesTest.php index df54ece28a6c..20bceadc947a 100644 --- a/tests/system/Commands/Utilities/RoutesTest.php +++ b/tests/system/Commands/Utilities/RoutesTest.php @@ -32,6 +32,10 @@ protected function setUp(): void { $this->resetServices(); parent::setUp(); + + service('superglobals') + ->setServer('HTTP_HOST', 'example.com') + ->setServer('SERVER_NAME', 'example.com'); } protected function tearDown(): void @@ -68,6 +72,7 @@ public function testRoutesCommand(): void | GET | closure | » | (Closure) | | | | GET | testing | testing-index | \App\Controllers\TestController::index | | | | HEAD | testing | testing-index | \App\Controllers\TestController::index | | | + | QUERY | testing | testing-index | \App\Controllers\TestController::index | | | | POST | testing | testing-index | \App\Controllers\TestController::index | | | | PATCH | testing | testing-index | \App\Controllers\TestController::index | | | | PUT | testing | testing-index | \App\Controllers\TestController::index | | | @@ -104,6 +109,7 @@ public function testRoutesCommandSortByHandler(): void | GET | / | » | \App\Controllers\Home::index | | | | GET | testing | testing-index | \App\Controllers\TestController::index | | | | HEAD | testing | testing-index | \App\Controllers\TestController::index | | | + | QUERY | testing | testing-index | \App\Controllers\TestController::index | | | | POST | testing | testing-index | \App\Controllers\TestController::index | | | | PATCH | testing | testing-index | \App\Controllers\TestController::index | | | | PUT | testing | testing-index | \App\Controllers\TestController::index | | | @@ -133,6 +139,7 @@ public function testRoutesCommandHostHostname(): void | GET | all | » | \App\Controllers\AllDomain::index | | | | GET | testing | testing-index | \App\Controllers\TestController::index | | | | HEAD | testing | testing-index | \App\Controllers\TestController::index | | | + | QUERY | testing | testing-index | \App\Controllers\TestController::index | | | | POST | testing | testing-index | \App\Controllers\TestController::index | | | | PATCH | testing | testing-index | \App\Controllers\TestController::index | | | | PUT | testing | testing-index | \App\Controllers\TestController::index | | | @@ -162,6 +169,7 @@ public function testRoutesCommandHostSubdomain(): void | GET | all | » | \App\Controllers\AllDomain::index | | | | GET | testing | testing-index | \App\Controllers\TestController::index | | | | HEAD | testing | testing-index | \App\Controllers\TestController::index | | | + | QUERY | testing | testing-index | \App\Controllers\TestController::index | | | | POST | testing | testing-index | \App\Controllers\TestController::index | | | | PATCH | testing | testing-index | \App\Controllers\TestController::index | | | | PUT | testing | testing-index | \App\Controllers\TestController::index | | | @@ -193,6 +201,7 @@ public function testRoutesCommandAutoRouteImproved(): void | GET | closure | » | (Closure) | | | | GET | testing | testing-index | \App\Controllers\TestController::index | | | | HEAD | testing | testing-index | \App\Controllers\TestController::index | | | + | QUERY | testing | testing-index | \App\Controllers\TestController::index | | | | POST | testing | testing-index | \App\Controllers\TestController::index | | | | PATCH | testing | testing-index | \App\Controllers\TestController::index | | | | PUT | testing | testing-index | \App\Controllers\TestController::index | | | @@ -228,6 +237,7 @@ public function testRoutesCommandRouteLegacy(): void | GET | closure | » | (Closure) | | | | GET | testing | testing-index | \App\Controllers\TestController::index | | | | HEAD | testing | testing-index | \App\Controllers\TestController::index | | | + | QUERY | testing | testing-index | \App\Controllers\TestController::index | | | | POST | testing | testing-index | \App\Controllers\TestController::index | | | | PATCH | testing | testing-index | \App\Controllers\TestController::index | | | | PUT | testing | testing-index | \App\Controllers\TestController::index | | | diff --git a/tests/system/Filters/PageCacheTest.php b/tests/system/Filters/PageCacheTest.php index 7a45cc3641be..062c077d1db1 100644 --- a/tests/system/Filters/PageCacheTest.php +++ b/tests/system/Filters/PageCacheTest.php @@ -15,6 +15,7 @@ use CodeIgniter\HTTP\DownloadResponse; use CodeIgniter\HTTP\IncomingRequest; +use CodeIgniter\HTTP\Method; use CodeIgniter\HTTP\RedirectResponse; use CodeIgniter\HTTP\Response; use CodeIgniter\HTTP\ResponseInterface; @@ -31,14 +32,15 @@ #[Group('Others')] final class PageCacheTest extends CIUnitTestCase { - private function createRequest(): IncomingRequest + private function createRequest(string $method = Method::GET): IncomingRequest { $superglobals = service('superglobals'); $superglobals->setServer('REQUEST_URI', '/'); $siteUri = new SiteURI(new App()); - return new IncomingRequest(new App(), $siteUri, null, new UserAgent()); + return (new IncomingRequest(new App(), $siteUri, null, new UserAgent())) + ->withMethod($method); } public function testDefaultConfigCachesAllStatusCodes(): void @@ -141,6 +143,39 @@ public function testCustomCacheStatusCodes(): void $this->assertNotInstanceOf(ResponseInterface::class, $result); } + public function testQueryRequestIsNotCached(): void + { + $config = new Cache(); + $filter = new PageCache($config); + + $request = $this->createRequest(Method::QUERY); + $response = new Response(); + $response->setStatusCode(200); + $response->setBody('Success'); + + $result = $filter->after($request, $response); + $this->assertNotInstanceOf(ResponseInterface::class, $result); + } + + public function testQueryRequestDoesNotReturnCachedResponse(): void + { + $filter = new PageCache(new Cache()); + $request = $this->createRequest(Method::QUERY); + $key = service('responsecache')->generateCacheKey($request); + + service('cache')->save($key, serialize([ + 'headers' => [], + 'output' => 'Cached', + 'status' => 200, + 'reason' => 'OK', + ]), 60); + + $result = $filter->before($request); + $this->assertNotInstanceOf(ResponseInterface::class, $result); + + service('cache')->delete($key); + } + public function testDownloadResponseNotCached(): void { $config = new Cache(); diff --git a/tests/system/HTTP/CURLRequestTest.php b/tests/system/HTTP/CURLRequestTest.php index aece2bf26354..ff85437af1b9 100644 --- a/tests/system/HTTP/CURLRequestTest.php +++ b/tests/system/HTTP/CURLRequestTest.php @@ -175,6 +175,35 @@ public function testOptionsSetsCorrectMethod(): void $this->assertSame('OPTIONS', $options[CURLOPT_CUSTOMREQUEST]); } + public function testQuerySetsCorrectMethod(): void + { + $this->request->query('http://example.com'); + + $this->assertSame('QUERY', $this->request->getMethod()); + + $options = $this->request->curl_options; + + $this->assertArrayHasKey(CURLOPT_CUSTOMREQUEST, $options); + $this->assertSame('QUERY', $options[CURLOPT_CUSTOMREQUEST]); + } + + public function testQueryWithJsonSetsBodyAndContentType(): void + { + $params = [ + 'foo' => 'bar', + ]; + $this->request->query('http://example.com', [ + 'json' => $params, + ]); + + $options = $this->request->curl_options; + + $this->assertSame('QUERY', $this->request->getMethod()); + $this->assertSame('QUERY', $options[CURLOPT_CUSTOMREQUEST]); + $this->assertSame(json_encode($params), $options[CURLOPT_POSTFIELDS]); + $this->assertContains('Content-Type: application/json', $options[CURLOPT_HTTPHEADER]); + } + public function testOptionsBaseURIOption(): void { $options = ['baseURI' => 'http://www.foo.com/api/v1/']; diff --git a/tests/system/HTTP/FormRequestTest.php b/tests/system/HTTP/FormRequestTest.php index e01c6ebb3509..d10c2433d663 100644 --- a/tests/system/HTTP/FormRequestTest.php +++ b/tests/system/HTTP/FormRequestTest.php @@ -40,7 +40,7 @@ protected function setUp(): void parent::setUp(); $this->resetServices(); - Services::injectMock('superglobals', new Superglobals()); + Services::injectMock('superglobals', new Superglobals(get: [], post: [])); service('superglobals')->setServer('REQUEST_METHOD', 'POST'); service('superglobals')->setServer('SERVER_PROTOCOL', 'HTTP/1.1'); service('superglobals')->setServer('SERVER_NAME', 'example.com'); @@ -346,6 +346,23 @@ public function testResolveRequestReturns422ForJsonRequest(): void $this->assertSame(422, $response->getStatusCode()); } + public function testResolveRequestUsesQueryRawBody(): void + { + service('superglobals') + ->setServer('REQUEST_METHOD', Method::QUERY) + ->setServer('CONTENT_TYPE', 'application/x-www-form-urlencoded'); + + $formRequest = $this->makeFormRequest($this->makeRequest('title=Hello&body=World')); + + $response = $formRequest->resolveRequest(); + + $this->assertNotInstanceOf(ResponseInterface::class, $response); + $this->assertSame([ + 'title' => 'Hello', + 'body' => 'World', + ], $formRequest->getValidated()); + } + public function testResolveRequestReturns422WhenJsonIsPreferred(): void { service('superglobals')->setServer('HTTP_ACCEPT', 'application/json'); diff --git a/tests/system/HTTP/IncomingRequestTest.php b/tests/system/HTTP/IncomingRequestTest.php index 5fba02047c93..7b76a2765a10 100644 --- a/tests/system/HTTP/IncomingRequestTest.php +++ b/tests/system/HTTP/IncomingRequestTest.php @@ -700,6 +700,7 @@ public static function provideIsHTTPMethods(): iterable ['HEAD'], ['PATCH'], ['OPTIONS'], + ['QUERY'], ]; } diff --git a/tests/system/Router/RouteCollectionTest.php b/tests/system/Router/RouteCollectionTest.php index 6346697d206a..905affd1c827 100644 --- a/tests/system/Router/RouteCollectionTest.php +++ b/tests/system/Router/RouteCollectionTest.php @@ -839,12 +839,17 @@ public function testMatchSupportsMultipleMethods(): void $expected = ['here' => '\there']; - $routes->match(['GET', 'POST'], 'here', 'there'); + $routes->match(['GET', 'POST', 'QUERY'], 'here', 'there'); $this->assertSame($expected, $routes->getRoutes()); service('request')->setMethod(Method::POST); $routes = $this->getCollector(); - $routes->match(['GET', 'POST'], 'here', 'there'); + $routes->match(['GET', 'POST', 'QUERY'], 'here', 'there'); + $this->assertSame($expected, $routes->getRoutes()); + + service('request')->setMethod(Method::QUERY); + $routes = $this->getCollector(); + $routes->match(['GET', 'POST', 'QUERY'], 'here', 'there'); $this->assertSame($expected, $routes->getRoutes()); } @@ -915,6 +920,17 @@ public function testHead(): void $this->assertSame($expected, $routes->getRoutes()); } + public function testQuery(): void + { + $routes = $this->getCollector(); + $routes->setHTTPVerb(Method::QUERY); + + $expected = ['here' => '\there']; + + $routes->query('here', 'there'); + $this->assertSame($expected, $routes->getRoutes()); + } + public function testPatch(): void { $routes = $this->getCollector(); diff --git a/tests/system/Security/SecurityTest.php b/tests/system/Security/SecurityTest.php index 1785341531c1..a33ec2fef045 100644 --- a/tests/system/Security/SecurityTest.php +++ b/tests/system/Security/SecurityTest.php @@ -16,6 +16,7 @@ use CodeIgniter\Config\Factories; use CodeIgniter\Config\Services; use CodeIgniter\HTTP\IncomingRequest; +use CodeIgniter\HTTP\Method; use CodeIgniter\HTTP\SiteURI; use CodeIgniter\HTTP\UserAgent; use CodeIgniter\Security\Exceptions\SecurityException; @@ -104,6 +105,16 @@ public function testGetHashReturnsCsrfCookieWhenGetWithCsrfCookie(): void $this->assertSame(self::CORRECT_CSRF_HASH, $security->getHash()); } + public function testCsrfVerifyQueryReturnsSelf(): void + { + service('superglobals')->setServer('REQUEST_METHOD', Method::QUERY); + + $security = $this->createMockSecurity(); + $request = $this->createIncomingRequest(); + + $this->assertSame($security, $security->verify($request)); + } + public function testCsrfVerifyPostThrowsExceptionOnNoMatch(): void { service('superglobals') diff --git a/tests/system/Test/FeatureTestTraitTest.php b/tests/system/Test/FeatureTestTraitTest.php index 79a00104a079..665dc4b71361 100644 --- a/tests/system/Test/FeatureTestTraitTest.php +++ b/tests/system/Test/FeatureTestTraitTest.php @@ -538,6 +538,30 @@ public function testCallPutWithJsonRequest(): void $response->assertJSONExact($data); } + public function testCallQueryWithJsonRequest(): void + { + $this->withRoutes([ + [ + 'QUERY', + 'home', + '\Tests\Support\Controllers\Popcorn::echoJson', + ], + ]); + $data = [ + 'true' => true, + 'false' => false, + 'int' => 2, + 'null' => null, + 'float' => 1.23, + 'string' => 'foo', + ]; + $response = $this->withBodyFormat('json') + ->query('home', $data); + + $response->assertOK(); + $response->assertJSONExact($data); + } + public function testCallPutWithJsonRequestAndREQUEST(): void { $this->withRoutes([ diff --git a/tests/system/Validation/ValidationTest.php b/tests/system/Validation/ValidationTest.php index d0483a890ed8..6773f27d7274 100644 --- a/tests/system/Validation/ValidationTest.php +++ b/tests/system/Validation/ValidationTest.php @@ -974,7 +974,8 @@ public function testInvalidRule(): void $this->validation->run($data); } - public function testRawInput(): void + #[DataProvider('provideRawInput')] + public function testRawInput(string $method): void { $rawstring = 'username=admin001&role=administrator&usepass=0'; $config = new App(); @@ -984,13 +985,23 @@ public function testRawInput(): void $rules = [ 'role' => 'required|min_length[5]', ]; - $result = $this->validation->withRequest($request->withMethod('PATCH'))->setRules($rules)->run(); + $result = $this->validation->withRequest($request->withMethod($method))->setRules($rules)->run(); $this->assertTrue($result); $this->assertSame([], $this->validation->getErrors()); $this->assertSame(['role' => 'administrator'], $this->validation->getValidated()); } + /** + * @return iterable> + */ + public static function provideRawInput(): iterable + { + yield 'PATCH' => ['PATCH']; + + yield 'QUERY' => ['QUERY']; + } + public function testJsonInput(): void { service('superglobals')->setServer('CONTENT_TYPE', 'application/json'); diff --git a/user_guide_src/source/changelogs/v4.8.0.rst b/user_guide_src/source/changelogs/v4.8.0.rst index 3eeebc3d3c2b..f27b1fc2e65f 100644 --- a/user_guide_src/source/changelogs/v4.8.0.rst +++ b/user_guide_src/source/changelogs/v4.8.0.rst @@ -40,6 +40,7 @@ Behavior Changes - **Database:** The Postgre driver's ``$db->error()['code']`` previously always returned ``''``. It now returns the 5-character SQLSTATE string for query and transaction failures (e.g., ``'42P01'``), or ``'08006'`` for connection-level failures. Code that relied on ``$db->error()['code'] === ''`` will need updating. - **Filters:** HTTP method matching for method-based filters is now case-sensitive. The keys in ``Config\Filters::$methods`` must exactly match the request method (e.g., ``GET``, ``POST``). Lowercase method names (e.g., ``post``) will no longer match. +- **HTTP:** Routes defined with ``$routes->add()`` now also match HTTP ``QUERY`` requests. If the route has CSRF protection, remember that CSRF verification does not protect safe methods such as ``GET`` and ``QUERY``. - **Testing:** Tests using the ``FeatureTestTrait`` must now use uppercase HTTP method names when performing a request when using the ``call()`` method directly (e.g., ``$this->call('GET', '/path')`` instead of ``$this->call('get', '/path')``). Additionally, setting method-based routes using ``withRoutes()`` must also use uppercase method names (e.g., ``$this->withRoutes([['GET', 'home', 'Home::index']])``). @@ -306,6 +307,7 @@ Helpers and Functions HTTP ==== +- Added support for the HTTP ``QUERY`` method in routing, feature tests, ``CURLRequest``, and ``FormRequest`` input source selection. - Added the ``retry`` option to ``CURLRequest`` for retrying failed responses with configurable delays, retryable status codes, optional transient cURL error retries, and ``Retry-After`` support. See :ref:`curlrequest-request-options-retry`. - Added :ref:`Form Requests ` - a new ``FormRequest`` base class that encapsulates validation rules, custom error messages, and authorization logic for a single HTTP request. - Added ``IncomingRequest::input()`` to read GET, POST, JSON, and raw request data through ``InputData``. diff --git a/user_guide_src/source/general/caching.rst b/user_guide_src/source/general/caching.rst index a5a92e29fe6d..6c03cf3bf46a 100644 --- a/user_guide_src/source/general/caching.rst +++ b/user_guide_src/source/general/caching.rst @@ -90,8 +90,8 @@ Valid options are: caching errors that should be temporary. For example, a cached 404 page would remain cached even after the resource is created, until the cache expires. -.. note:: Regardless of this setting, ``DownloadResponse`` and ``RedirectResponse`` - instances are never cached by the ``PageCache`` filter. +.. note:: Regardless of this setting, ``DownloadResponse``, ``RedirectResponse``, + and HTTP ``QUERY`` responses are never cached by the ``PageCache`` filter. Enabling Caching ================ diff --git a/user_guide_src/source/incoming/form_requests.rst b/user_guide_src/source/incoming/form_requests.rst index 278703cd237e..6528ca33a8a0 100644 --- a/user_guide_src/source/incoming/form_requests.rst +++ b/user_guide_src/source/incoming/form_requests.rst @@ -185,7 +185,7 @@ By default ``validationData()`` selects the appropriate data source automatically based on the HTTP method and ``Content-Type`` header: * **JSON request** (``Content-Type: application/json``) -> decoded JSON body -* **PUT / PATCH / DELETE** (non-multipart) -> raw body via ``getRawInput()`` +* **PUT / PATCH / DELETE / QUERY** (non-multipart) -> raw body via ``getRawInput()`` * **GET / HEAD** -> query-string parameters via ``getGet()`` * **Everything else** (POST, multipart) -> POST body via ``getPost()`` diff --git a/user_guide_src/source/incoming/incomingrequest.rst b/user_guide_src/source/incoming/incomingrequest.rst index a5bd2719a827..9397c4568be2 100644 --- a/user_guide_src/source/incoming/incomingrequest.rst +++ b/user_guide_src/source/incoming/incomingrequest.rst @@ -231,8 +231,8 @@ pass true in the second parameter: .. _incomingrequest-retrieving-raw-data: -Retrieving Raw Data (PUT, PATCH, DELETE) -======================================== +Retrieving Raw Data (PUT, PATCH, DELETE, QUERY) +=============================================== Finally, you can grab the contents of ``php://input`` as a raw stream with ``getRawInput()``: diff --git a/user_guide_src/source/incoming/routing.rst b/user_guide_src/source/incoming/routing.rst index 0aad82af632c..9c62eb659ddf 100644 --- a/user_guide_src/source/incoming/routing.rst +++ b/user_guide_src/source/incoming/routing.rst @@ -78,6 +78,19 @@ You can use any standard HTTP verb (GET, POST, PUT, DELETE, OPTIONS, etc): .. literalinclude:: routing/003.php +``QUERY`` routes are for the HTTP QUERY method defined by RFC 10008. +``QUERY`` requests are safe and idempotent like GET, but send query content in the +request body. Read that body explicitly with request input methods such as +``$this->request->input()->json()`` or ``$this->request->input()->raw()``. Your +controller should reject missing or unsupported ``Content-Type`` values for the +query content it expects. Do not use ``QUERY`` for operations that create, +update, delete, or otherwise change server state. + +.. note:: Your web server, proxy, or CORS configuration may need to allow the + ``QUERY`` method before these requests can reach CodeIgniter. For + browser-based cross-origin requests, ensure ``QUERY`` is included in your + allowed CORS methods so preflight requests can succeed. + You can supply multiple verbs that a route should match by passing them in as an array to the ``match()`` method: .. literalinclude:: routing/004.php @@ -354,9 +367,9 @@ Routes with any HTTP verbs another, more appropriate method. .. warning:: While the ``add()`` method seems to be convenient, it is recommended to always use the HTTP-verb-based - routes, described above, as it is more secure. If you use the :doc:`CSRF protection `, it does not protect **GET** - requests. If the URI specified in the ``add()`` method is accessible by the GET method, the CSRF protection - will not work. + routes, described above, as it is more secure. If you use the :doc:`CSRF protection `, it does not protect + **GET** or **QUERY** requests. If the URI specified in the ``add()`` method is accessible by the GET or QUERY method, + the CSRF protection will not work. It is possible to define a route with any HTTP verbs. You can use the ``add()`` method: diff --git a/user_guide_src/source/incoming/routing/003.php b/user_guide_src/source/incoming/routing/003.php index 7351142dea77..50ac7be75c64 100644 --- a/user_guide_src/source/incoming/routing/003.php +++ b/user_guide_src/source/incoming/routing/003.php @@ -3,3 +3,4 @@ $routes->post('products', 'Product::feature'); $routes->put('products/1', 'Product::feature'); $routes->delete('products/1', 'Product::feature'); +$routes->query('products/search', 'Product::search'); diff --git a/user_guide_src/source/libraries/curlrequest.rst b/user_guide_src/source/libraries/curlrequest.rst index 61e73e702796..970a66959391 100644 --- a/user_guide_src/source/libraries/curlrequest.rst +++ b/user_guide_src/source/libraries/curlrequest.rst @@ -116,6 +116,11 @@ each take the URL as the first parameter and an array of options as the second: .. literalinclude:: curlrequest/007.php +The ``query()`` shortcut sends an HTTP QUERY request. QUERY requests usually +include query content, so use options such as ``json``, ``form_params``, or +``body`` to send the expected body. This is different from the ``query`` request +option, which adds URL query variables to the request URI. + Base URI -------- diff --git a/user_guide_src/source/libraries/curlrequest/007.php b/user_guide_src/source/libraries/curlrequest/007.php index d509f3e3fa29..ca6514e2b970 100644 --- a/user_guide_src/source/libraries/curlrequest/007.php +++ b/user_guide_src/source/libraries/curlrequest/007.php @@ -7,3 +7,4 @@ $client->patch('http://example.com'); $client->put('http://example.com'); $client->post('http://example.com'); +$client->query('http://example.com'); diff --git a/user_guide_src/source/libraries/validation.rst b/user_guide_src/source/libraries/validation.rst index c7d76d1c0409..17da5936c1f2 100644 --- a/user_guide_src/source/libraries/validation.rst +++ b/user_guide_src/source/libraries/validation.rst @@ -399,7 +399,7 @@ data to be validated: when the request is a JSON request (``Content-Type: application/json``), or gets Raw data from :ref:`$request->getRawInput() ` - when the request is a PUT, PATCH, DELETE request and + when the request is a PUT, PATCH, DELETE, or QUERY request and is not HTML form post (``Content-Type: multipart/form-data``), or gets data from :ref:`$request->getVar() `, and an attacker could change what data is validated. diff --git a/user_guide_src/source/testing/feature.rst b/user_guide_src/source/testing/feature.rst index bccefdf3b0df..84f8fc18effc 100644 --- a/user_guide_src/source/testing/feature.rst +++ b/user_guide_src/source/testing/feature.rst @@ -105,6 +105,9 @@ This is useful when testing JSON or XML APIs so that you can set the request in This will take the parameters passed into ``call()``, ``post()``, ``get()``... and assign them to the body of the request in the given format. +For example, use ``withBodyFormat('json')->query()`` to test a QUERY request +with a JSON body. + This will also set the `Content-Type` header for your request accordingly. .. literalinclude:: feature/008.php diff --git a/user_guide_src/source/testing/feature/003.php b/user_guide_src/source/testing/feature/003.php index 1905a2dcf110..859c9e348c9d 100644 --- a/user_guide_src/source/testing/feature/003.php +++ b/user_guide_src/source/testing/feature/003.php @@ -6,3 +6,4 @@ $this->patch($path, $params); $this->delete($path, $params); $this->options($path, $params); +$this->query($path, $params);