diff --git a/src/Illuminate/Contracts/Routing/Registrar.php b/src/Illuminate/Contracts/Routing/Registrar.php index 57e327231dcd..999fcd6a0f20 100644 --- a/src/Illuminate/Contracts/Routing/Registrar.php +++ b/src/Illuminate/Contracts/Routing/Registrar.php @@ -58,6 +58,15 @@ public function patch($uri, $action); */ public function options($uri, $action); + /** + * Register a new QUERY route with the router. + * + * @param string $uri + * @param array|string|callable $action + * @return \Illuminate\Routing\Route + */ + public function query($uri, $action); + /** * Register a new route with the given verbs. * diff --git a/src/Illuminate/Foundation/Console/RouteListCommand.php b/src/Illuminate/Foundation/Console/RouteListCommand.php index fa86f4a329c6..12d39f8ec98e 100644 --- a/src/Illuminate/Foundation/Console/RouteListCommand.php +++ b/src/Illuminate/Foundation/Console/RouteListCommand.php @@ -66,6 +66,7 @@ class RouteListCommand extends Command 'GET' => 'blue', 'HEAD' => '#6C7280', 'OPTIONS' => '#6C7280', + 'QUERY' => '#6C7280', 'POST' => 'yellow', 'PUT' => 'yellow', 'PATCH' => 'yellow', @@ -394,7 +395,7 @@ protected function forCli($routes) $routes = $routes->map( fn ($route) => array_merge($route, [ 'action' => $this->formatActionForCli($route), - 'method' => $route['method'] == 'GET|HEAD|POST|PUT|PATCH|DELETE|OPTIONS' ? 'ANY' : $route['method'], + 'method' => $route['method'] == implode('|', Router::$verbs) ? 'ANY' : $route['method'], 'uri' => $route['domain'] ? ($route['domain'].'/'.ltrim($route['uri'], '/')) : $route['uri'], ]), ); diff --git a/src/Illuminate/Foundation/Http/Middleware/PreventRequestForgery.php b/src/Illuminate/Foundation/Http/Middleware/PreventRequestForgery.php index dc2474267943..527722caa4a8 100644 --- a/src/Illuminate/Foundation/Http/Middleware/PreventRequestForgery.php +++ b/src/Illuminate/Foundation/Http/Middleware/PreventRequestForgery.php @@ -119,7 +119,7 @@ public function handle($request, Closure $next) */ protected function isReading($request) { - return in_array($request->method(), ['HEAD', 'GET', 'OPTIONS']); + return in_array($request->method(), ['HEAD', 'GET', 'OPTIONS', 'QUERY']); } /** diff --git a/src/Illuminate/Routing/RouteRegistrar.php b/src/Illuminate/Routing/RouteRegistrar.php index be73cd9a98d4..61de06ce0993 100644 --- a/src/Illuminate/Routing/RouteRegistrar.php +++ b/src/Illuminate/Routing/RouteRegistrar.php @@ -18,6 +18,7 @@ * @method \Illuminate\Routing\Route patch(string $uri, \Closure|array|string|null $action = null) * @method \Illuminate\Routing\Route post(string $uri, \Closure|array|string|null $action = null) * @method \Illuminate\Routing\Route put(string $uri, \Closure|array|string|null $action = null) + * @method \Illuminate\Routing\Route query(string $uri, \Closure|array|string|null $action = null) * @method \Illuminate\Routing\RouteRegistrar as(string $value) * @method \Illuminate\Routing\RouteRegistrar can(\UnitEnum|string $ability, array|string $models = []) * @method \Illuminate\Routing\RouteRegistrar controller(string $controller) @@ -60,7 +61,7 @@ class RouteRegistrar * @var string[] */ protected $passthru = [ - 'get', 'post', 'put', 'patch', 'delete', 'options', 'any', + 'get', 'post', 'put', 'patch', 'delete', 'options', 'query', 'any', ]; /** diff --git a/src/Illuminate/Routing/Router.php b/src/Illuminate/Routing/Router.php index 24ef495b398b..ae602cd9ee80 100644 --- a/src/Illuminate/Routing/Router.php +++ b/src/Illuminate/Routing/Router.php @@ -133,7 +133,7 @@ class Router implements BindingRegistrar, RegistrarContract * * @var string[] */ - public static $verbs = ['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS']; + public static $verbs = ['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'QUERY']; /** * Create a new Router instance. @@ -220,6 +220,18 @@ public function options($uri, $action = null) return $this->addRoute('OPTIONS', $uri, $action); } + /** + * Register a new QUERY route with the router. + * + * @param string $uri + * @param array|string|callable|null $action + * @return \Illuminate\Routing\Route + */ + public function query($uri, $action = null) + { + return $this->addRoute('QUERY', $uri, $action); + } + /** * Register a new route responding to all verbs. * diff --git a/src/Illuminate/Support/Facades/Route.php b/src/Illuminate/Support/Facades/Route.php index f6a8232d7a0c..312b093f8235 100755 --- a/src/Illuminate/Support/Facades/Route.php +++ b/src/Illuminate/Support/Facades/Route.php @@ -9,6 +9,7 @@ * @method static \Illuminate\Routing\Route patch(string $uri, array|string|callable|null $action = null) * @method static \Illuminate\Routing\Route delete(string $uri, array|string|callable|null $action = null) * @method static \Illuminate\Routing\Route options(string $uri, array|string|callable|null $action = null) + * @method static \Illuminate\Routing\Route query(string $uri, array|string|callable|null $action = null) * @method static \Illuminate\Routing\Route any(string $uri, array|string|callable|null $action = null) * @method static \Illuminate\Routing\Route fallback(array|string|callable|null $action) * @method static \Illuminate\Routing\Route redirect(string $uri, string $destination, int $status = 302) diff --git a/tests/Http/Middleware/PreventRequestForgeryTest.php b/tests/Http/Middleware/PreventRequestForgeryTest.php index 656443e05b5a..1edbdb909575 100644 --- a/tests/Http/Middleware/PreventRequestForgeryTest.php +++ b/tests/Http/Middleware/PreventRequestForgeryTest.php @@ -32,6 +32,16 @@ public function test_same_origin_header_passes() $this->assertSame('OK', $response->getContent()); } + public function test_query_requests_pass_without_token() + { + $middleware = $this->createMiddleware(); + $request = $this->createRequest(method: 'QUERY'); + + $response = $middleware->handle($request, fn () => new Response('OK')); + + $this->assertSame('OK', $response->getContent()); + } + public function test_same_site_header_rejected_by_default() { $middleware = $this->createMiddleware(); @@ -121,11 +131,11 @@ public function test_origin_only_mode_passes_same_origin() $this->assertSame('OK', $response->getContent()); } - protected function createRequest(array $server = [], ?string $token = null) + protected function createRequest(array $server = [], ?string $token = null, string $method = 'POST') { $request = Request::create( 'http://example.com/test', - 'POST', + $method, $token ? ['_token' => $token] : [], [], [], diff --git a/tests/Integration/Routing/Fixtures/query_routes.php b/tests/Integration/Routing/Fixtures/query_routes.php new file mode 100644 index 000000000000..42663f641a79 --- /dev/null +++ b/tests/Integration/Routing/Fixtures/query_routes.php @@ -0,0 +1,11 @@ +json([ + 'method' => request()->method(), + 'term' => request()->query('term'), + 'filter' => request()->input('filter'), + ]); +}); diff --git a/tests/Integration/Routing/RouteCachingTest.php b/tests/Integration/Routing/RouteCachingTest.php index 1d070772854c..ea749e648c71 100644 --- a/tests/Integration/Routing/RouteCachingTest.php +++ b/tests/Integration/Routing/RouteCachingTest.php @@ -23,6 +23,17 @@ public function testRedirectRoutes() $this->get('/foo/1')->assertRedirect('/foo/1/bar'); } + public function testQueryRoutes() + { + $this->routes(__DIR__.'/Fixtures/query_routes.php'); + + $this->call('QUERY', '/search?term=laravel', ['filter' => 'framework'])->assertExactJson([ + 'method' => 'QUERY', + 'term' => 'laravel', + 'filter' => 'framework', + ]); + } + protected function routes(string $file) { $this->defineCacheRoutes(file_get_contents($file)); diff --git a/tests/Routing/RouteRegistrarTest.php b/tests/Routing/RouteRegistrarTest.php index 4194d691bbf3..6c6f077a301f 100644 --- a/tests/Routing/RouteRegistrarTest.php +++ b/tests/Routing/RouteRegistrarTest.php @@ -216,6 +216,17 @@ public function testCanRegisterPostRouteWithClosureAction() $this->seeMiddleware('post-middleware'); } + public function testCanRegisterQueryRouteWithClosureAction() + { + $this->router->middleware('query-middleware')->query('users', function () { + return 'found'; + }); + + $this->assertTrue($this->getRoute()->matches(Request::create('users', 'QUERY'))); + $this->assertSame(['QUERY'], $this->getRoute()->methods()); + $this->seeMiddleware('query-middleware'); + } + public function testCanRegisterAnyRouteWithClosureAction() { $this->router->middleware('test-middleware')->any('users', function () { diff --git a/tests/Routing/RoutingRouteTest.php b/tests/Routing/RoutingRouteTest.php index 5804ffe4fdac..9e9abc3fe5f9 100644 --- a/tests/Routing/RoutingRouteTest.php +++ b/tests/Routing/RoutingRouteTest.php @@ -86,8 +86,12 @@ public function testBasicDispatchingOfRoutes() $router->post('foo/bar', function () { return 'post hello'; }); + $router->query('foo/bar', function () { + return 'query hello'; + }); $this->assertSame('hello', $router->dispatch(Request::create('foo/bar', 'GET'))->getContent()); $this->assertSame('post hello', $router->dispatch(Request::create('foo/bar', 'POST'))->getContent()); + $this->assertSame('query hello', $router->dispatch(Request::create('foo/bar', 'QUERY'))->getContent()); $router = $this->getRouter(); $router->get('foo/{bar}', function ($name) { @@ -157,6 +161,7 @@ public function testBasicDispatchingOfRoutes() return 'hello'; }); $this->assertEmpty($router->dispatch(Request::create('foo/bar', 'HEAD'))->getContent()); + $this->assertSame('hello', $router->dispatch(Request::create('foo/bar', 'QUERY'))->getContent()); $router = $this->getRouter(); $router->get('foo/bar', function () {