Skip to content

Commit 93c0d2c

Browse files
[HttpFoundation][HttpKernel][WebProfilerBundle] Add support for the QUERY HTTP method
1 parent b065b9a commit 93c0d2c

File tree

10 files changed

+202
-8
lines changed

10 files changed

+202
-8
lines changed

src/Symfony/Bundle/WebProfilerBundle/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

4+
7.4
5+
---
6+
7+
* Add support for the `QUERY` HTTP method in the profiler
8+
49
7.3
510
---
611

src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/search.html.twig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
{% if 'command' == profile_type %}
2626
{% set methods = ['BATCH', 'INTERACTIVE'] %}
2727
{% else %}
28-
{% set methods = ['DELETE', 'GET', 'HEAD', 'PATCH', 'POST', 'PUT'] %}
28+
{% set methods = ['DELETE', 'GET', 'HEAD', 'PATCH', 'POST', 'PUT', 'QUERY'] %}
2929
{% endif %}
3030
{% for m in methods %}
3131
<option {{ m == method ? 'selected="selected"' }}>{{ m }}</option>

src/Symfony/Component/HttpFoundation/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ CHANGELOG
55
---
66

77
* Deprecate using `Request::sendHeaders()` after headers have already been sent; use a `StreamedResponse` instead
8+
* Add support for the `QUERY` HTTP method
89

910
7.3
1011
---

src/Symfony/Component/HttpFoundation/Request.php

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ class Request
6262
public const METHOD_OPTIONS = 'OPTIONS';
6363
public const METHOD_TRACE = 'TRACE';
6464
public const METHOD_CONNECT = 'CONNECT';
65+
public const METHOD_QUERY = 'QUERY';
6566

6667
/**
6768
* @var string[]
@@ -254,7 +255,7 @@ public static function createFromGlobals(): static
254255
$request = self::createRequestFromFactory($_GET, $_POST, [], $_COOKIE, $_FILES, $_SERVER);
255256

256257
if (str_starts_with($request->headers->get('CONTENT_TYPE', ''), 'application/x-www-form-urlencoded')
257-
&& \in_array(strtoupper($request->server->get('REQUEST_METHOD', 'GET')), ['PUT', 'DELETE', 'PATCH'], true)
258+
&& \in_array(strtoupper($request->server->get('REQUEST_METHOD', 'GET')), ['PUT', 'DELETE', 'PATCH', 'QUERY'], true)
258259
) {
259260
parse_str($request->getContent(), $data);
260261
$request->request = new InputBag($data);
@@ -350,6 +351,7 @@ public static function create(string $uri, string $method = 'GET', array $parame
350351
case 'POST':
351352
case 'PUT':
352353
case 'DELETE':
354+
case 'QUERY':
353355
if (!isset($server['CONTENT_TYPE'])) {
354356
$server['CONTENT_TYPE'] = 'application/x-www-form-urlencoded';
355357
}
@@ -1175,7 +1177,7 @@ public function getMethod(): string
11751177

11761178
$method = strtoupper($method);
11771179

1178-
if (\in_array($method, ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT', 'OPTIONS', 'PATCH', 'PURGE', 'TRACE'], true)) {
1180+
if (\in_array($method, ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT', 'OPTIONS', 'PATCH', 'PURGE', 'TRACE', 'QUERY'], true)) {
11791181
return $this->method = $method;
11801182
}
11811183

@@ -1351,15 +1353,15 @@ public function isMethod(string $method): bool
13511353
*/
13521354
public function isMethodSafe(): bool
13531355
{
1354-
return \in_array($this->getMethod(), ['GET', 'HEAD', 'OPTIONS', 'TRACE'], true);
1356+
return \in_array($this->getMethod(), ['GET', 'HEAD', 'OPTIONS', 'TRACE', 'QUERY'], true);
13551357
}
13561358

13571359
/**
13581360
* Checks whether or not the method is idempotent.
13591361
*/
13601362
public function isMethodIdempotent(): bool
13611363
{
1362-
return \in_array($this->getMethod(), ['HEAD', 'GET', 'PUT', 'DELETE', 'TRACE', 'OPTIONS', 'PURGE'], true);
1364+
return \in_array($this->getMethod(), ['HEAD', 'GET', 'PUT', 'DELETE', 'TRACE', 'OPTIONS', 'PURGE', 'QUERY'], true);
13631365
}
13641366

13651367
/**
@@ -1369,7 +1371,7 @@ public function isMethodIdempotent(): bool
13691371
*/
13701372
public function isMethodCacheable(): bool
13711373
{
1372-
return \in_array($this->getMethod(), ['GET', 'HEAD'], true);
1374+
return \in_array($this->getMethod(), ['GET', 'HEAD', 'QUERY'], true);
13731375
}
13741376

13751377
/**

src/Symfony/Component/HttpFoundation/Tests/RequestTest.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2330,6 +2330,7 @@ public static function methodIdempotentProvider()
23302330
['OPTIONS', true],
23312331
['TRACE', true],
23322332
['CONNECT', false],
2333+
['QUERY', true],
23332334
];
23342335
}
23352336

@@ -2356,6 +2357,7 @@ public static function methodSafeProvider()
23562357
['OPTIONS', true],
23572358
['TRACE', true],
23582359
['CONNECT', false],
2360+
['QUERY', true],
23592361
];
23602362
}
23612363

@@ -2382,6 +2384,7 @@ public static function methodCacheableProvider()
23822384
['OPTIONS', false],
23832385
['TRACE', false],
23842386
['CONNECT', false],
2387+
['QUERY', true],
23852388
];
23862389
}
23872390

src/Symfony/Component/HttpKernel/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

4+
7.4
5+
---
6+
7+
* Add support for the `QUERY` HTTP method
8+
49
7.3
510
---
611

src/Symfony/Component/HttpKernel/HttpCache/Store.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -427,7 +427,15 @@ public function getPath(string $key): string
427427
*/
428428
protected function generateCacheKey(Request $request): string
429429
{
430-
return 'md'.hash('sha256', $request->getUri());
430+
$key = $request->getUri();
431+
432+
if ('QUERY' === $request->getMethod()) {
433+
// add null byte to separate the URI from the body and avoid boundary collisions
434+
// which could lead to cache poisoning
435+
$key .= "\0".$request->getContent();
436+
}
437+
438+
return 'md'.hash('sha256', $key);
431439
}
432440

433441
/**

src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTest.php

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2070,6 +2070,93 @@ public function testTraceLevelShort()
20702070
$this->assertTrue($this->response->headers->has('X-Symfony-Cache'));
20712071
$this->assertEquals('miss', $this->response->headers->get('X-Symfony-Cache'));
20722072
}
2073+
2074+
public function testQueryMethodIsCacheable()
2075+
{
2076+
$this->setNextResponse(200, ['Cache-Control' => 'public, max-age=10000'], 'Query result', function (Request $request) {
2077+
$this->assertSame('QUERY', $request->getMethod());
2078+
2079+
return '{"query": "users"}' === $request->getContent();
2080+
});
2081+
2082+
$this->kernel->reset();
2083+
$this->store = $this->createStore();
2084+
$this->cacheConfig['debug'] = true;
2085+
$this->cache = new HttpCache($this->kernel, $this->store, null, $this->cacheConfig);
2086+
2087+
$request1 = Request::create('/', 'QUERY', [], [], [], [], '{"query": "users"}');
2088+
$this->response = $this->cache->handle($request1, HttpKernelInterface::MAIN_REQUEST, $this->catch);
2089+
2090+
$this->assertSame(200, $this->response->getStatusCode());
2091+
$this->assertTraceContains('miss');
2092+
$this->assertSame('Query result', $this->response->getContent());
2093+
2094+
$request2 = Request::create('/', 'QUERY', [], [], [], [], '{"query": "users"}');
2095+
$this->response = $this->cache->handle($request2, HttpKernelInterface::MAIN_REQUEST, $this->catch);
2096+
2097+
$this->assertSame(200, $this->response->getStatusCode());
2098+
$this->assertTrue($this->response->headers->has('Age'));
2099+
$this->assertSame('Query result', $this->response->getContent());
2100+
}
2101+
2102+
public function testQueryMethodDifferentBodiesCreateDifferentCacheEntries()
2103+
{
2104+
$this->setNextResponses([
2105+
[
2106+
'status' => 200,
2107+
'body' => 'Users result',
2108+
'headers' => ['Cache-Control' => 'public, max-age=10000'],
2109+
],
2110+
[
2111+
'status' => 200,
2112+
'body' => 'Posts result',
2113+
'headers' => ['Cache-Control' => 'public, max-age=10000'],
2114+
],
2115+
]);
2116+
2117+
$this->store = $this->createStore();
2118+
$this->cacheConfig['debug'] = true;
2119+
$this->cache = new HttpCache($this->kernel, $this->store, null, $this->cacheConfig);
2120+
2121+
$request1 = Request::create('/', 'QUERY', [], [], [], [], '{"query": "users"}');
2122+
$this->response = $this->cache->handle($request1, HttpKernelInterface::MAIN_REQUEST, $this->catch);
2123+
2124+
$this->assertSame('Users result', $this->response->getContent());
2125+
$this->assertTraceContains('miss');
2126+
2127+
$request2 = Request::create('/', 'QUERY', [], [], [], [], '{"query": "posts"}');
2128+
$this->response = $this->cache->handle($request2, HttpKernelInterface::MAIN_REQUEST, $this->catch);
2129+
2130+
$this->assertSame('Posts result', $this->response->getContent());
2131+
$this->assertTraceContains('miss');
2132+
2133+
$request3 = Request::create('/', 'QUERY', [], [], [], [], '{"query": "users"}');
2134+
$this->response = $this->cache->handle($request3, HttpKernelInterface::MAIN_REQUEST, $this->catch);
2135+
2136+
$this->assertSame('Users result', $this->response->getContent());
2137+
$this->assertTrue($this->response->headers->has('Age'));
2138+
}
2139+
2140+
public function testQueryMethodWithEmptyBodyIsCacheable()
2141+
{
2142+
$this->setNextResponse(200, ['Cache-Control' => 'public, max-age=10000'], 'Empty query result');
2143+
$this->kernel->reset();
2144+
$this->store = $this->createStore();
2145+
$this->cacheConfig['debug'] = true;
2146+
$this->cache = new HttpCache($this->kernel, $this->store, null, $this->cacheConfig);
2147+
2148+
$request1 = Request::create('/', 'QUERY', [], [], [], [], '');
2149+
$this->response = $this->cache->handle($request1, HttpKernelInterface::MAIN_REQUEST, $this->catch);
2150+
2151+
$this->assertSame(200, $this->response->getStatusCode());
2152+
$this->assertTraceContains('miss');
2153+
2154+
$request2 = Request::create('/', 'QUERY', [], [], [], [], '');
2155+
$this->response = $this->cache->handle($request2, HttpKernelInterface::MAIN_REQUEST, $this->catch);
2156+
2157+
$this->assertSame(200, $this->response->getStatusCode());
2158+
$this->assertTrue($this->response->headers->has('Age'));
2159+
}
20732160
}
20742161

20752162
class TestKernel implements HttpKernelInterface

src/Symfony/Component/HttpKernel/Tests/HttpCache/StoreTest.php

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,4 +378,87 @@ protected function getStorePath($key)
378378

379379
return $m->invoke($this->store, $key);
380380
}
381+
382+
public function testQueryMethodCacheKeyIncludesBody()
383+
{
384+
$response = new Response('test', 200, ['Cache-Control' => 'max-age=420']);
385+
386+
$request1 = Request::create('/', 'QUERY', [], [], [], [], '{"query": "users"}');
387+
$request2 = Request::create('/', 'QUERY', [], [], [], [], '{"query": "posts"}');
388+
$request3 = Request::create('/', 'QUERY', [], [], [], [], '{"query": "users"}');
389+
390+
$key1 = $this->store->write($request1, $response);
391+
$key2 = $this->store->write($request2, $response);
392+
$key3 = $this->store->write($request3, $response);
393+
394+
$this->assertNotSame($key1, $key2);
395+
$this->assertSame($key1, $key3);
396+
397+
$this->assertNotEmpty($this->getStoreMetadata($key1));
398+
$this->assertNotEmpty($this->getStoreMetadata($key2));
399+
400+
$this->assertNotNull($this->store->lookup($request1));
401+
$this->assertNotNull($this->store->lookup($request2));
402+
$this->assertNotNull($this->store->lookup($request3));
403+
}
404+
405+
public function testQueryMethodCacheKeyDiffersFromGet()
406+
{
407+
$response = new Response('test', 200, ['Cache-Control' => 'max-age=420']);
408+
409+
$getRequest = Request::create('/');
410+
$queryRequest = Request::create('/', 'QUERY', [], [], [], [], '{"query": "test"}');
411+
412+
$getKey = $this->store->write($getRequest, $response);
413+
$queryKey = $this->store->write($queryRequest, $response);
414+
415+
$this->assertNotSame($getKey, $queryKey);
416+
417+
$this->assertNotEmpty($this->getStoreMetadata($getKey));
418+
$this->assertNotEmpty($this->getStoreMetadata($queryKey));
419+
420+
$this->assertNotNull($this->store->lookup($getRequest));
421+
$this->assertNotNull($this->store->lookup($queryRequest));
422+
}
423+
424+
public function testOtherMethodsCacheKeyIgnoresBody()
425+
{
426+
$response1 = new Response('test 1', 200, ['Cache-Control' => 'max-age=420']);
427+
$response2 = new Response('test 2', 200, ['Cache-Control' => 'max-age=420']);
428+
429+
$getRequest1 = Request::create('/', 'GET', [], [], [], [], '{"data": "test"}');
430+
$getRequest2 = Request::create('/', 'GET', [], [], [], [], '{"data": "different"}');
431+
432+
$key1 = $this->store->write($getRequest1, $response1);
433+
$key2 = $this->store->write($getRequest2, $response2);
434+
435+
$this->assertSame($key1, $key2);
436+
437+
$lookup1 = $this->store->lookup($getRequest1);
438+
$lookup2 = $this->store->lookup($getRequest2);
439+
$this->assertNotNull($lookup1);
440+
$this->assertNotNull($lookup2);
441+
442+
$this->assertCount(1, $this->getStoreMetadata($key1));
443+
$this->assertSame($lookup1->getContent(), $lookup2->getContent());
444+
}
445+
446+
public function testQueryMethodCacheKeyAvoidsBoundaryCollisions()
447+
{
448+
$response = new Response('test', 200, ['Cache-Control' => 'max-age=420']);
449+
450+
$request1 = Request::create('/api/query', 'QUERY', [], [], [], [], 'test');
451+
$request2 = Request::create('/api/que', 'QUERY', [], [], [], [], 'rytest');
452+
453+
$key1 = $this->store->write($request1, $response);
454+
$key2 = $this->store->write($request2, $response);
455+
456+
$this->assertNotSame($key1, $key2);
457+
458+
$this->assertNotEmpty($this->getStoreMetadata($key1));
459+
$this->assertNotEmpty($this->getStoreMetadata($key2));
460+
461+
$this->assertNotNull($this->store->lookup($request1));
462+
$this->assertNotNull($this->store->lookup($request2));
463+
}
381464
}

src/Symfony/Component/HttpKernel/composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
"symfony/deprecation-contracts": "^2.5|^3",
2121
"symfony/error-handler": "^6.4|^7.0|^8.0",
2222
"symfony/event-dispatcher": "^7.3|^8.0",
23-
"symfony/http-foundation": "^7.3|^8.0",
23+
"symfony/http-foundation": "^7.4|^8.0",
2424
"symfony/polyfill-ctype": "^1.8",
2525
"psr/log": "^1|^2|^3"
2626
},

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy