From 3ea3cbb2ba25cbb7ead0fc3d9144e8d5e62ae09a Mon Sep 17 00:00:00 2001 From: Christian Fritsch Date: Mon, 19 Apr 2021 17:28:22 +0200 Subject: [PATCH 1/9] feat: add APQ plugin --- graphql.services.yml | 15 ++++ src/EventSubscriber/ApqSubscriber.php | 70 +++++++++++++++++++ .../AutomaticPersistedQuery.php | 56 +++++++++++++++ 3 files changed, 141 insertions(+) create mode 100644 src/EventSubscriber/ApqSubscriber.php create mode 100644 src/Plugin/GraphQL/PersistedQuery/AutomaticPersistedQuery.php diff --git a/graphql.services.yml b/graphql.services.yml index 8de9f5448..62c79a6d8 100644 --- a/graphql.services.yml +++ b/graphql.services.yml @@ -39,6 +39,14 @@ services: tags: - { name: cache.context } + # Cache bin for the persisted queries. + cache.graphql.apq: + class: Drupal\Core\Cache\CacheBackendInterface + tags: + - { name: cache.bin } + factory: cache_factory:get + arguments: [graphql_apq] + # Cache bin for the parsed sdl ast. cache.graphql.ast: class: Drupal\Core\Cache\CacheBackendInterface @@ -108,6 +116,13 @@ services: tags: - { name: event_subscriber } + # Cache the queries to be persistent. + graphql.apq_subscriber: + class: Drupal\graphql\EventSubscriber\ApqSubscriber + arguments: ['@cache.graphql.apq', '@request_stack'] + tags: + - { name: event_subscriber } + # Plugin manager for schemas plugin.manager.graphql.schema: class: Drupal\graphql\Plugin\SchemaPluginManager diff --git a/src/EventSubscriber/ApqSubscriber.php b/src/EventSubscriber/ApqSubscriber.php new file mode 100644 index 000000000..c6ecff693 --- /dev/null +++ b/src/EventSubscriber/ApqSubscriber.php @@ -0,0 +1,70 @@ +cache = $cache; + $this->request = $requestStack->getCurrentRequest(); + } + + /** + * Handle operation start events. + * + * @param \Drupal\graphql\Event\OperationEvent $event + * The kernel event object. + */ + public function onBeforeOperation(OperationEvent $event): void { + try { + $json = Json::decode($this->request->getContent()); + if (!empty($json['extensions']['persistedQuery']['sha256Hash']) && !empty($json['query'])) { + $this->cache->set($json['extensions']['persistedQuery']['sha256Hash'], $json['query']); + } + + } + catch (\Exception $exception) { + } + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() { + return [ + OperationEvent::GRAPHQL_OPERATION_BEFORE => 'onBeforeOperation', + ]; + } + +} diff --git a/src/Plugin/GraphQL/PersistedQuery/AutomaticPersistedQuery.php b/src/Plugin/GraphQL/PersistedQuery/AutomaticPersistedQuery.php new file mode 100644 index 000000000..db5d369da --- /dev/null +++ b/src/Plugin/GraphQL/PersistedQuery/AutomaticPersistedQuery.php @@ -0,0 +1,56 @@ +cache = $cache; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static($configuration, $plugin_id, $plugin_definition, $container->get('cache.graphql.apq')); + } + + /** + * {@inheritdoc} + */ + public function getQuery($id, OperationParams $operation) { + if ($query = $this->cache->get($id)) { + return $query->data; + } + throw new RequestError('PersistedQueryNotFound'); + } + +} From bfc90b7817cb48916dbd999a3c5abb5aa40f4a18 Mon Sep 17 00:00:00 2001 From: Christian Fritsch Date: Wed, 24 Nov 2021 16:46:45 +0100 Subject: [PATCH 2/9] test: add a test for the plugin --- .../Kernel/Framework/PersistedQueriesTest.php | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/tests/src/Kernel/Framework/PersistedQueriesTest.php b/tests/src/Kernel/Framework/PersistedQueriesTest.php index 43daeb249..f8d309f1b 100644 --- a/tests/src/Kernel/Framework/PersistedQueriesTest.php +++ b/tests/src/Kernel/Framework/PersistedQueriesTest.php @@ -3,6 +3,7 @@ namespace Drupal\Tests\graphql\Kernel\Framework; use Drupal\Tests\graphql\Kernel\GraphQLTestBase; +use Symfony\Component\HttpFoundation\Request; /** * Tests the entire query result pipeline when using persisted queries. @@ -51,6 +52,7 @@ protected function setUp(): void { $this->plugin_one = $manager->createInstance('persisted_query_plugin_one'); $this->plugin_two = $manager->createInstance('persisted_query_plugin_two'); $this->plugin_three = $manager->createInstance('persisted_query_plugin_three'); + $this->plugin_apq = $manager->createInstance('automatic_persisted_query'); } /** @@ -115,4 +117,38 @@ public function persistedQueriesDataProvider(): array { ]; } + /** + * Test the automatic persisted queries plugin. + */ + public function testAutomaticPersistedQueries(): void { + // Before adding the persisted query plugins to the server, we want to make + // sure that there are no existing plugins already there. + $this->server->removeAllPersistedQueryInstances(); + $this->server->addPersistedQueryInstance($this->plugin_apq); + $this->server->save(); + + $endpoint = $this->server->get('endpoint'); + + $parameters['extensions']['persistedQuery']['sha256Hash'] = 'hash'; + + // Check we get PersistedQueryNotFound. + $request = Request::create($endpoint, 'GET', $parameters); + $result = $this->container->get('http_kernel')->handle($request); + $this->assertSame(200, $result->getStatusCode()); + $this->assertSame(['errors' => [['message' => 'PersistedQueryNotFound', 'extensions' => ['category' => 'request']]]], json_decode($result->getContent(), TRUE)); + + // Post query to endpoint. + $content = json_encode(['query' => 'query { field_one } '] + $parameters); + $request = Request::create($endpoint, 'POST', [], [], [], [], $content); + $result = $this->container->get('http_kernel')->handle($request); + $this->assertSame(200, $result->getStatusCode()); + $this->assertSame(['data' => ['field_one' => 'this is the field one']], json_decode($result->getContent(), TRUE)); + + // Execute first request again. + $request = Request::create($endpoint, 'GET', $parameters); + $result = $this->container->get('http_kernel')->handle($request); + $this->assertSame(200, $result->getStatusCode()); + $this->assertSame(['data' => ['field_one' => 'this is the field one']], json_decode($result->getContent(), TRUE)); + } + } From 01a43ce6f5187d0471712c40fd4f4825365b46cd Mon Sep 17 00:00:00 2001 From: Christian Fritsch Date: Wed, 24 Nov 2021 17:22:21 +0100 Subject: [PATCH 3/9] fix: cs issues --- tests/src/Kernel/Framework/PersistedQueriesTest.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/src/Kernel/Framework/PersistedQueriesTest.php b/tests/src/Kernel/Framework/PersistedQueriesTest.php index f8d309f1b..af295d919 100644 --- a/tests/src/Kernel/Framework/PersistedQueriesTest.php +++ b/tests/src/Kernel/Framework/PersistedQueriesTest.php @@ -135,7 +135,14 @@ public function testAutomaticPersistedQueries(): void { $request = Request::create($endpoint, 'GET', $parameters); $result = $this->container->get('http_kernel')->handle($request); $this->assertSame(200, $result->getStatusCode()); - $this->assertSame(['errors' => [['message' => 'PersistedQueryNotFound', 'extensions' => ['category' => 'request']]]], json_decode($result->getContent(), TRUE)); + $this->assertSame([ + 'errors' => [ + [ + 'message' => 'PersistedQueryNotFound', + 'extensions' => ['category' => 'request'], + ], + ], + ], json_decode($result->getContent(), TRUE)); // Post query to endpoint. $content = json_encode(['query' => 'query { field_one } '] + $parameters); From c827324595085d59a65757c999755f79fa66083f Mon Sep 17 00:00:00 2001 From: Christian Fritsch Date: Thu, 25 Nov 2021 14:00:33 +0100 Subject: [PATCH 4/9] fix: make it more robust --- graphql.services.yml | 2 +- src/EventSubscriber/ApqSubscriber.php | 35 +++---- .../AutomaticPersistedQueriesTest.php | 96 +++++++++++++++++++ .../Kernel/Framework/PersistedQueriesTest.php | 43 --------- 4 files changed, 112 insertions(+), 64 deletions(-) create mode 100644 tests/src/Kernel/Framework/AutomaticPersistedQueriesTest.php diff --git a/graphql.services.yml b/graphql.services.yml index 62c79a6d8..cf6ac6659 100644 --- a/graphql.services.yml +++ b/graphql.services.yml @@ -119,7 +119,7 @@ services: # Cache the queries to be persistent. graphql.apq_subscriber: class: Drupal\graphql\EventSubscriber\ApqSubscriber - arguments: ['@cache.graphql.apq', '@request_stack'] + arguments: ['@cache.graphql.apq'] tags: - { name: event_subscriber } diff --git a/src/EventSubscriber/ApqSubscriber.php b/src/EventSubscriber/ApqSubscriber.php index c6ecff693..4473220bb 100644 --- a/src/EventSubscriber/ApqSubscriber.php +++ b/src/EventSubscriber/ApqSubscriber.php @@ -2,11 +2,10 @@ namespace Drupal\graphql\EventSubscriber; -use Drupal\Component\Serialization\Json; use Drupal\Core\Cache\CacheBackendInterface; use Drupal\graphql\Event\OperationEvent; use Symfony\Component\EventDispatcher\EventSubscriberInterface; -use Symfony\Component\HttpFoundation\RequestStack; +use GraphQL\Error\Error; /** * Save persisted queries to cache. @@ -20,24 +19,14 @@ class ApqSubscriber implements EventSubscriberInterface { */ protected $cache; - /** - * The current request. - * - * @var \Symfony\Component\HttpFoundation\Request|null - */ - protected $request; - /** * Constructs a ApqSubscriber object. * * @param \Drupal\Core\Cache\CacheBackendInterface $cache * The cache to store persisted queries. - * @param \Symfony\Component\HttpFoundation\RequestStack $requestStack - * The request stack. */ - public function __construct(CacheBackendInterface $cache, RequestStack $requestStack) { + public function __construct(CacheBackendInterface $cache) { $this->cache = $cache; - $this->request = $requestStack->getCurrentRequest(); } /** @@ -45,16 +34,22 @@ public function __construct(CacheBackendInterface $cache, RequestStack $requestS * * @param \Drupal\graphql\Event\OperationEvent $event * The kernel event object. + * + * @throws \GraphQL\Error\Error */ public function onBeforeOperation(OperationEvent $event): void { - try { - $json = Json::decode($this->request->getContent()); - if (!empty($json['extensions']['persistedQuery']['sha256Hash']) && !empty($json['query'])) { - $this->cache->set($json['extensions']['persistedQuery']['sha256Hash'], $json['query']); - } - + if (!in_array('automatic_persisted_query', array_keys($event->getContext()->getServer()->getPersistedQueryInstances() ?? []))) { + return; } - catch (\Exception $exception) { + $query = $event->getContext()->getOperation()->query; + $queryHash = $event->getContext()->getOperation()->extensions['persistedQuery']['sha256Hash'] ?? ''; + + if ($query && $queryHash) { + $computedQueryHash = hash('sha256', $query); + if ($queryHash !== $computedQueryHash) { + throw new Error('Provided sha does not match query'); + } + $this->cache->set($queryHash, $query); } } diff --git a/tests/src/Kernel/Framework/AutomaticPersistedQueriesTest.php b/tests/src/Kernel/Framework/AutomaticPersistedQueriesTest.php new file mode 100644 index 000000000..58593ae0c --- /dev/null +++ b/tests/src/Kernel/Framework/AutomaticPersistedQueriesTest.php @@ -0,0 +1,96 @@ +setUpSchema($schema); + $this->mockResolver('Query', 'field_one', 'this is the field one'); + + /** @var \Drupal\graphql\Plugin\DataProducerPluginManager $manager */ + $manager = $this->container->get('plugin.manager.graphql.persisted_query'); + + $this->plugin_apq = $manager->createInstance('automatic_persisted_query'); + } + + /** + * Test the automatic persisted queries plugin. + */ + public function testAutomaticPersistedQueries(): void { + // Before adding the persisted query plugins to the server, we want to make + // sure that there are no existing plugins already there. + $this->server->removeAllPersistedQueryInstances(); + $this->server->addPersistedQueryInstance($this->plugin_apq); + $this->server->save(); + + $endpoint = $this->server->get('endpoint'); + + $query = 'query { field_one } '; + $parameters['extensions']['persistedQuery']['sha256Hash'] = 'some random hash'; + + // Check we get PersistedQueryNotFound. + $request = Request::create($endpoint, 'GET', $parameters); + $result = $this->container->get('http_kernel')->handle($request); + $this->assertSame(200, $result->getStatusCode()); + $this->assertSame([ + 'errors' => [ + [ + 'message' => 'PersistedQueryNotFound', + 'extensions' => ['category' => 'request'], + ], + ], + ], json_decode($result->getContent(), TRUE)); + + // Post query to endpoint with a not matching hash. + $content = json_encode(['query' => $query] + $parameters); + $request = Request::create($endpoint, 'POST', [], [], [], [], $content); + $result = $this->container->get('http_kernel')->handle($request); + $this->assertSame(200, $result->getStatusCode()); + $this->assertSame([ + 'errors' => [ + [ + 'message' => 'Provided sha does not match query', + 'extensions' => ['category' => 'graphql'], + ], + ], + ], json_decode($result->getContent(), TRUE)); + + // Post query to endpoint to get the result and cache it. + $parameters['extensions']['persistedQuery']['sha256Hash'] = hash('sha256', $query); + + $content = json_encode(['query' => $query] + $parameters); + $request = Request::create($endpoint, 'POST', [], [], [], [], $content); + $result = $this->container->get('http_kernel')->handle($request); + $this->assertSame(200, $result->getStatusCode()); + $this->assertSame(['data' => ['field_one' => 'this is the field one']], json_decode($result->getContent(), TRUE)); + + // Execute first request again. + $request = Request::create($endpoint, 'GET', $parameters); + $result = $this->container->get('http_kernel')->handle($request); + $this->assertSame(200, $result->getStatusCode()); + $this->assertSame(['data' => ['field_one' => 'this is the field one']], json_decode($result->getContent(), TRUE)); + } + +} diff --git a/tests/src/Kernel/Framework/PersistedQueriesTest.php b/tests/src/Kernel/Framework/PersistedQueriesTest.php index af295d919..43daeb249 100644 --- a/tests/src/Kernel/Framework/PersistedQueriesTest.php +++ b/tests/src/Kernel/Framework/PersistedQueriesTest.php @@ -3,7 +3,6 @@ namespace Drupal\Tests\graphql\Kernel\Framework; use Drupal\Tests\graphql\Kernel\GraphQLTestBase; -use Symfony\Component\HttpFoundation\Request; /** * Tests the entire query result pipeline when using persisted queries. @@ -52,7 +51,6 @@ protected function setUp(): void { $this->plugin_one = $manager->createInstance('persisted_query_plugin_one'); $this->plugin_two = $manager->createInstance('persisted_query_plugin_two'); $this->plugin_three = $manager->createInstance('persisted_query_plugin_three'); - $this->plugin_apq = $manager->createInstance('automatic_persisted_query'); } /** @@ -117,45 +115,4 @@ public function persistedQueriesDataProvider(): array { ]; } - /** - * Test the automatic persisted queries plugin. - */ - public function testAutomaticPersistedQueries(): void { - // Before adding the persisted query plugins to the server, we want to make - // sure that there are no existing plugins already there. - $this->server->removeAllPersistedQueryInstances(); - $this->server->addPersistedQueryInstance($this->plugin_apq); - $this->server->save(); - - $endpoint = $this->server->get('endpoint'); - - $parameters['extensions']['persistedQuery']['sha256Hash'] = 'hash'; - - // Check we get PersistedQueryNotFound. - $request = Request::create($endpoint, 'GET', $parameters); - $result = $this->container->get('http_kernel')->handle($request); - $this->assertSame(200, $result->getStatusCode()); - $this->assertSame([ - 'errors' => [ - [ - 'message' => 'PersistedQueryNotFound', - 'extensions' => ['category' => 'request'], - ], - ], - ], json_decode($result->getContent(), TRUE)); - - // Post query to endpoint. - $content = json_encode(['query' => 'query { field_one } '] + $parameters); - $request = Request::create($endpoint, 'POST', [], [], [], [], $content); - $result = $this->container->get('http_kernel')->handle($request); - $this->assertSame(200, $result->getStatusCode()); - $this->assertSame(['data' => ['field_one' => 'this is the field one']], json_decode($result->getContent(), TRUE)); - - // Execute first request again. - $request = Request::create($endpoint, 'GET', $parameters); - $result = $this->container->get('http_kernel')->handle($request); - $this->assertSame(200, $result->getStatusCode()); - $this->assertSame(['data' => ['field_one' => 'this is the field one']], json_decode($result->getContent(), TRUE)); - } - } From b37e1448f9e6290ee67bcfc96c6e4b17b84a5da2 Mon Sep 17 00:00:00 2001 From: Christian Fritsch Date: Thu, 25 Nov 2021 14:19:22 +0100 Subject: [PATCH 5/9] fix: nobody should care about PHP 7.2 --- .github/workflows/testing.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index fde304ce0..7e56aebc1 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -17,7 +17,7 @@ jobs: drupal-core: ['9.2.x'] include: # Extra run to also test on latest Drupal 8 and PHP 7.2. - - php-versions: '7.2' + - php-versions: '7.4' drupal-core: '8.9.x' steps: - name: Checkout Drupal core From b16a770549eac7025e0f11c53f988e431a433e55 Mon Sep 17 00:00:00 2001 From: Christian Fritsch Date: Mon, 7 Mar 2022 13:46:30 +0100 Subject: [PATCH 6/9] fix: use array_key_exists --- src/EventSubscriber/ApqSubscriber.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/EventSubscriber/ApqSubscriber.php b/src/EventSubscriber/ApqSubscriber.php index 4473220bb..d8bb451a9 100644 --- a/src/EventSubscriber/ApqSubscriber.php +++ b/src/EventSubscriber/ApqSubscriber.php @@ -38,7 +38,7 @@ public function __construct(CacheBackendInterface $cache) { * @throws \GraphQL\Error\Error */ public function onBeforeOperation(OperationEvent $event): void { - if (!in_array('automatic_persisted_query', array_keys($event->getContext()->getServer()->getPersistedQueryInstances() ?? []))) { + if (!array_key_exists('automatic_persisted_query', $event->getContext()->getServer()->getPersistedQueryInstances() ?? [])) { return; } $query = $event->getContext()->getOperation()->query; From d00ece8dbeac3472aef213ff352284bbacf9e584 Mon Sep 17 00:00:00 2001 From: Christian Fritsch Date: Mon, 7 Mar 2022 13:54:26 +0100 Subject: [PATCH 7/9] fix: return type --- src/Entity/ServerInterface.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Entity/ServerInterface.php b/src/Entity/ServerInterface.php index 5ad8edc9b..050f14e7c 100644 --- a/src/Entity/ServerInterface.php +++ b/src/Entity/ServerInterface.php @@ -60,7 +60,7 @@ public function removeAllPersistedQueryInstances(); /** * Returns the current persisted queries set. * - * @return \Drupal\graphql\Plugin\PersistedQueryPluginInterface[] + * @return \Drupal\graphql\Plugin\PersistedQueryPluginInterface[]|NULL */ public function getPersistedQueryInstances(); From 656d9aaaeec39aaffc963f18539e392f644fcd8c Mon Sep 17 00:00:00 2001 From: Christian Fritsch Date: Mon, 7 Mar 2022 13:57:36 +0100 Subject: [PATCH 8/9] fix: cs --- src/Entity/ServerInterface.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Entity/ServerInterface.php b/src/Entity/ServerInterface.php index 050f14e7c..256c46df9 100644 --- a/src/Entity/ServerInterface.php +++ b/src/Entity/ServerInterface.php @@ -60,7 +60,7 @@ public function removeAllPersistedQueryInstances(); /** * Returns the current persisted queries set. * - * @return \Drupal\graphql\Plugin\PersistedQueryPluginInterface[]|NULL + * @return \Drupal\graphql\Plugin\PersistedQueryPluginInterface[]|null */ public function getPersistedQueryInstances(); From e13a696d29bf5205f6a3fef161422a37b99cee84 Mon Sep 17 00:00:00 2001 From: Alexander Varwijk Date: Wed, 28 Sep 2022 13:52:34 +0200 Subject: [PATCH 9/9] Make ApqSubscriber query and query_hash checks type explicit --- src/EventSubscriber/ApqSubscriber.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/EventSubscriber/ApqSubscriber.php b/src/EventSubscriber/ApqSubscriber.php index d8bb451a9..1dd3ca5a5 100644 --- a/src/EventSubscriber/ApqSubscriber.php +++ b/src/EventSubscriber/ApqSubscriber.php @@ -44,7 +44,7 @@ public function onBeforeOperation(OperationEvent $event): void { $query = $event->getContext()->getOperation()->query; $queryHash = $event->getContext()->getOperation()->extensions['persistedQuery']['sha256Hash'] ?? ''; - if ($query && $queryHash) { + if (is_string($query) && is_string($queryHash) && $queryHash !== '') { $computedQueryHash = hash('sha256', $query); if ($queryHash !== $computedQueryHash) { throw new Error('Provided sha does not match query');