diff --git a/core/modules/jsonapi/jsonapi.api.php b/core/modules/jsonapi/jsonapi.api.php index c011e77f79..fa9be05fa3 100644 --- a/core/modules/jsonapi/jsonapi.api.php +++ b/core/modules/jsonapi/jsonapi.api.php @@ -349,6 +349,59 @@ function hook_jsonapi_entity_field_filter_access(\Drupal\Core\Field\FieldDefinit return AccessResult::neutral(); } +/** + * Allow access to revisions of content. + * + * Normally JSON API forbids access to all revisions of entities apart from core + * entity types Node and Media since these have interfaces for properly + * determining access. + * + * If you know what you are doing and understand the risks you can implement + * this hook to provide custom logic for other Entity types to allow revision + * access. + * + * The example below allows access to revisions of paragraph entities by + * checking if the user has access to the parent node revision the paragraph + * revision is on. + * + * @return \Drupal\Core\Access\AccessResultInterface + */ +function hook_jsonapi_revision_view_access(\Drupal\Core\Entity\EntityInterface $entity, \Drupal\Core\Session\AccountInterface $account) { + /** @var \Drupal\paragraphs\Entity\Paragraph $entity */ + if (!$entity instanceof \Drupal\paragraphs\ParagraphInterface) { + return AccessResult::neutral(); + } + + $parent = $entity->getParentEntity(); + + if ($parent instanceof \Drupal\node\NodeInterface) { + $field_name = $entity->get('parent_field_name')[0]->getValue(); + + $database = \Drupal::database(); + $node_revision = $database + ->select('node_revision__' . $field_name['value'], 'r') + ->fields('r', ['revision_id']) + ->condition('r.' . $field_name['value'] . '_target_id', $entity->id()) + ->condition('r.' . $field_name['value'] . '_target_revision_id', $entity->getRevisionId()) + ->execute() + ->fetch(); + + if ($parent->getRevisionId() !== $node_revision->revision_id) { + $parent = node_revision_load($node_revision->revision_id); + } + + /** @var \Drupal\node\Access\NodeRevisionAccessCheck $node_revision_access_service */ + $node_revision_access_service = \Drupal::service('access_check.node.revision'); + + return AccessResult::allowedIf($node_revision_access_service + ->checkAccess($parent, $account, 'view')) + ->cachePerPermissions() + ->addCacheableDependency($entity); + } + + return AccessResult::neutral('Paragraphs revisions are only supported on nodes with JSON:API'); +} + /** * @} End of "addtogroup hooks". */ diff --git a/core/modules/jsonapi/src/Access/EntityAccessChecker.php b/core/modules/jsonapi/src/Access/EntityAccessChecker.php index d3d4fa06e0..455306c06a 100644 --- a/core/modules/jsonapi/src/Access/EntityAccessChecker.php +++ b/core/modules/jsonapi/src/Access/EntityAccessChecker.php @@ -256,10 +256,22 @@ protected function checkRevisionViewAccess(EntityInterface $entity, AccountInter break; default: - $reason = 'Only node and media revisions are supported by JSON:API.'; - $reason .= ' For context, see https://www.drupal.org/project/jsonapi/issues/2992833#comment-12818258.'; - $reason .= ' To contribute, see https://www.drupal.org/project/drupal/issues/2350939 and https://www.drupal.org/project/drupal/issues/2809177.'; - $access = AccessResult::neutral($reason); + // Allow modules that know what they are doing a way to provide custom revision access. + $custom_access_list = \Drupal::moduleHandler()->invokeAll('jsonapi_revision_view_access', [$entity, $account]); + + if (empty($custom_access_list)) { + $reason = 'Only node and media revisions are supported by JSON:API.'; + $reason .= ' For context, see https://www.drupal.org/project/jsonapi/issues/2992833#comment-12818258.'; + $reason .= ' To contribute, see https://www.drupal.org/project/drupal/issues/2350939 and https://www.drupal.org/project/drupal/issues/2809177.'; + $access = AccessResult::neutral($reason); + } + else { + /** @var \Drupal\Core\Access\AccessResultInterface $access */ + $access = array_shift($custom_access_list); + foreach ($custom_access_list as $other) { + $access = $access->orIf($other); + } + } } // Apply content_moderation's additional access logic. // @see \Drupal\content_moderation\Access\LatestRevisionCheck::access() diff --git a/core/modules/jsonapi/src/IncludeResolver.php b/core/modules/jsonapi/src/IncludeResolver.php index d8a283df0d..4469104d4a 100644 --- a/core/modules/jsonapi/src/IncludeResolver.php +++ b/core/modules/jsonapi/src/IncludeResolver.php @@ -139,12 +139,24 @@ protected function resolveIncludeTree(array $include_tree, Data $data, Data $inc assert(!empty($target_type)); foreach ($field_list as $field_item) { assert($field_item instanceof EntityReferenceItem); - $references[$target_type][] = $field_item->get($field_item::mainPropertyName())->getValue(); + if ($resource_object->getResourceType()->isVersionable()) { + // Load the revision id. + $references[$target_type . ':revision_ids'][] = $field_item->get('target_revision_id')->getValue(); + } + else { + $references[$target_type . ':ids'][] = $field_item->get($field_item::mainPropertyName())->getValue(); + } } } - foreach ($references as $target_type => $ids) { + foreach ($references as $target_type_and_rev => $ids) { + list($target_type, $revision_type) = explode(':', $target_type_and_rev); + $entity_storage = $this->entityTypeManager->getStorage($target_type); - $targeted_entities = $entity_storage->loadMultiple(array_unique($ids)); + + $targeted_entities = ($revision_type === 'revision_ids') + ? $entity_storage->loadMultipleRevisions($ids) + : $entity_storage->loadMultiple(array_unique($ids)); + $access_checked_entities = array_map(function (EntityInterface $entity) { return $this->entityAccessChecker->getAccessCheckedResourceObject($entity); }, $targeted_entities);