jsonapi.services.yml | 1 + src/Controller/EntityResource.php | 30 +++++++++++++++++++-- src/Controller/RequestHandler.php | 16 +++++++++-- tests/src/Functional/JsonApiRegressionTest.php | 31 ++++++++++++++++++++++ tests/src/Kernel/Controller/EntityResourceTest.php | 12 ++++++--- 5 files changed, 82 insertions(+), 8 deletions(-) diff --git a/jsonapi.services.yml b/jsonapi.services.yml index f059898..59643ff 100644 --- a/jsonapi.services.yml +++ b/jsonapi.services.yml @@ -155,6 +155,7 @@ services: - '@entity_field.manager' - '@plugin.manager.field.field_type' - '@jsonapi.link_manager' + - '@renderer' # Event subscribers. jsonapi.custom_query_parameter_names_validator.subscriber: diff --git a/src/Controller/EntityResource.php b/src/Controller/EntityResource.php index 78ef277..dfd418d 100644 --- a/src/Controller/EntityResource.php +++ b/src/Controller/EntityResource.php @@ -14,6 +14,8 @@ use Drupal\Core\Entity\FieldableEntityInterface; use Drupal\Core\Field\EntityReferenceFieldItemListInterface; use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Field\FieldTypePluginManagerInterface; +use Drupal\Core\Render\RenderContext; +use Drupal\Core\Render\RendererInterface; use Drupal\jsonapi\Exception\EntityAccessDeniedHttpException; use Drupal\jsonapi\Exception\UnprocessableHttpEntityException; use Drupal\jsonapi\LabelOnlyEntity; @@ -82,6 +84,13 @@ class EntityResource { protected $resourceTypeRepository; /** + * The renderer. + * + * @var \Drupal\Core\Render\RendererInterface + */ + protected $renderer; + + /** * Instantiates a EntityResource object. * * @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type @@ -96,14 +105,17 @@ class EntityResource { * The link manager service. * @param \Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface $resource_type_repository * The link manager service. + * @param \Drupal\Core\Render\RendererInterface $renderer + * The renderer. */ - public function __construct(ResourceType $resource_type, EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $field_manager, FieldTypePluginManagerInterface $plugin_manager, LinkManager $link_manager, ResourceTypeRepositoryInterface $resource_type_repository) { + public function __construct(ResourceType $resource_type, EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $field_manager, FieldTypePluginManagerInterface $plugin_manager, LinkManager $link_manager, ResourceTypeRepositoryInterface $resource_type_repository, RendererInterface $renderer) { $this->resourceType = $resource_type; $this->entityTypeManager = $entity_type_manager; $this->fieldManager = $field_manager; $this->pluginManager = $plugin_manager; $this->linkManager = $link_manager; $this->resourceTypeRepository = $resource_type_repository; + $this->renderer = $renderer; } /** @@ -327,9 +339,22 @@ class EntityResource { $route_params = $request->attributes->get('_route_params'); $params = isset($route_params['_json_api_params']) ? $route_params['_json_api_params'] : []; $query = $this->getCollectionQuery($entity_type_id, $params); + $query_cacheability = new CacheableMetadata(); try { - $results = $query->execute(); + // Execute the query in a render context, to catch bubbled cacheability. + // @see node_query_node_access_alter() + // @see https://www.drupal.org/project/drupal/issues/2557815 + // @see https://www.drupal.org/project/drupal/issues/2794385 + // @todo Remove this when the query sytems's return value is able to carry + // cacheability. + $context = new RenderContext(); + $results = $this->renderer->executeInRenderContext($context, function () use ($query) { + return $query->execute(); + }); + if (!$context->isEmpty()) { + $query_cacheability->addCacheableDependency($context->pop()); + } } catch (\LogicException $e) { // Ensure good DX when an entity query involves a config entity type. @@ -369,6 +394,7 @@ class EntityResource { $response = $this->respondWithCollection($entity_collection, $entity_type_id); + $response->addCacheableDependency($query_cacheability); // Add cacheable metadata for the access result. $access_info = array_column($collection_data, 'access'); array_walk($access_info, function ($access) use ($response) { diff --git a/src/Controller/RequestHandler.php b/src/Controller/RequestHandler.php index ed015a5..eb480b3 100644 --- a/src/Controller/RequestHandler.php +++ b/src/Controller/RequestHandler.php @@ -5,6 +5,7 @@ namespace Drupal\jsonapi\Controller; use Drupal\Core\Entity\EntityFieldManagerInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Field\FieldTypePluginManagerInterface; +use Drupal\Core\Render\RendererInterface; use Drupal\jsonapi\LinkManager\LinkManager; use Drupal\jsonapi\ResourceType\ResourceType; use Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface; @@ -65,6 +66,13 @@ class RequestHandler { protected $linkManager; /** + * The renderer. + * + * @var \Drupal\Core\Render\RendererInterface + */ + protected $renderer; + + /** * Creates a new RequestHandler instance. * * @param \Symfony\Component\Serializer\SerializerInterface $serializer @@ -79,14 +87,17 @@ class RequestHandler { * The field type manager. * @param \Drupal\jsonapi\LinkManager\LinkManager $link_manager * The JSON API link manager. + * @param \Drupal\Core\Render\RendererInterface $renderer + * The renderer. */ - public function __construct(SerializerInterface $serializer, ResourceTypeRepositoryInterface $resource_type_repository, EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $field_manager, FieldTypePluginManagerInterface $field_type_manager, LinkManager $link_manager) { + public function __construct(SerializerInterface $serializer, ResourceTypeRepositoryInterface $resource_type_repository, EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $field_manager, FieldTypePluginManagerInterface $field_type_manager, LinkManager $link_manager, RendererInterface $renderer) { $this->serializer = $serializer; $this->resourceTypeRepository = $resource_type_repository; $this->entityTypeManager = $entity_type_manager; $this->fieldManager = $field_manager; $this->fieldTypeManager = $field_type_manager; $this->linkManager = $link_manager; + $this->renderer = $renderer; } /** @@ -239,7 +250,8 @@ class RequestHandler { $this->fieldManager, $this->fieldTypeManager, $this->linkManager, - $this->resourceTypeRepository + $this->resourceTypeRepository, + $this->renderer ); return $resource; } diff --git a/tests/src/Functional/JsonApiRegressionTest.php b/tests/src/Functional/JsonApiRegressionTest.php index 61dfb20..987ff5e 100644 --- a/tests/src/Functional/JsonApiRegressionTest.php +++ b/tests/src/Functional/JsonApiRegressionTest.php @@ -279,4 +279,35 @@ class JsonApiRegressionTest extends JsonApiFunctionalTestBase { $this->assertSame(200, $response->getStatusCode(), (string) $response->getBody()); } + /** + * Ensures GETting node collection + hook_node_grants() implementations works. + * + * @see https://www.drupal.org/project/jsonapi/issues/2984964 + */ + public function testGetNodeCollectionWithHookNodeGrantsImplementationsFromIssue2984964() { + // Set up data model. + $this->assertTrue($this->container->get('module_installer')->install(['node_access_test'], TRUE), 'Installed modules.'); + node_access_rebuild(); + $this->rebuildAll(); + + // Create data. + Node::create([ + 'title' => 'test article', + 'type' => 'article', + ])->save(); + + // Test. + $user = $this->drupalCreateUser([ + 'access content', + ]); + $response = $this->request('GET', Url::fromUri('internal:/jsonapi/node/article'), [ + RequestOptions::AUTH => [ + $user->getUsername(), + $user->pass_raw, + ], + ]); + $this->assertSame(200, $response->getStatusCode()); + $this->assertTrue(in_array('user.node_grants:view', explode(' ', $response->getHeader('X-Drupal-Cache-Contexts')[0]), TRUE)); + } + } diff --git a/tests/src/Kernel/Controller/EntityResourceTest.php b/tests/src/Kernel/Controller/EntityResourceTest.php index eb2960d..88db1dd 100644 --- a/tests/src/Kernel/Controller/EntityResourceTest.php +++ b/tests/src/Kernel/Controller/EntityResourceTest.php @@ -227,7 +227,8 @@ class EntityResourceTest extends JsonapiKernelTestBase { $this->container->get('entity_field.manager'), $this->container->get('plugin.manager.field.field_type'), $this->container->get('jsonapi.link_manager'), - $this->container->get('jsonapi.resource_type.repository') + $this->container->get('jsonapi.resource_type.repository'), + $this->container->get('renderer') ); // Get the response. @@ -262,7 +263,8 @@ class EntityResourceTest extends JsonapiKernelTestBase { $this->container->get('entity_field.manager'), $this->container->get('plugin.manager.field.field_type'), $this->container->get('jsonapi.link_manager'), - $this->container->get('jsonapi.resource_type.repository') + $this->container->get('jsonapi.resource_type.repository'), + $this->container->get('renderer') ); // Get the response. @@ -298,7 +300,8 @@ class EntityResourceTest extends JsonapiKernelTestBase { $this->container->get('entity_field.manager'), $this->container->get('plugin.manager.field.field_type'), $this->container->get('jsonapi.link_manager'), - $this->container->get('jsonapi.resource_type.repository') + $this->container->get('jsonapi.resource_type.repository'), + $this->container->get('renderer') ); // Get the response. @@ -809,7 +812,8 @@ class EntityResourceTest extends JsonapiKernelTestBase { $this->container->get('entity_field.manager'), $this->container->get('plugin.manager.field.field_type'), $this->container->get('jsonapi.link_manager'), - $this->container->get('jsonapi.resource_type.repository') + $this->container->get('jsonapi.resource_type.repository'), + $this->container->get('renderer') ); }