Chris@18: resourceTypeRepository = $resource_type_repository; Chris@18: $this->router = $router; Chris@18: $this->currentUser = $account; Chris@18: $this->entityRepository = $entity_repository; Chris@18: } Chris@18: Chris@18: /** Chris@18: * Sets the node revision access check service. Chris@18: * Chris@18: * This is only called when node module is installed. Chris@18: * Chris@18: * @param \Drupal\node\Access\NodeRevisionAccessCheck $node_revision_access_check Chris@18: * The node revision access check service. Chris@18: */ Chris@18: public function setNodeRevisionAccessCheck(NodeRevisionAccessCheck $node_revision_access_check) { Chris@18: $this->nodeRevisionAccessCheck = $node_revision_access_check; Chris@18: } Chris@18: Chris@18: /** Chris@18: * Sets the media revision access check service. Chris@18: * Chris@18: * This is only called when media module is installed. Chris@18: * Chris@18: * @param \Drupal\media\Access\MediaRevisionAccessCheck $media_revision_access_check Chris@18: * The media revision access check service. Chris@18: */ Chris@18: public function setMediaRevisionAccessCheck(MediaRevisionAccessCheck $media_revision_access_check) { Chris@18: $this->mediaRevisionAccessCheck = $media_revision_access_check; Chris@18: } Chris@18: Chris@18: /** Chris@18: * Sets the media revision access check service. Chris@18: * Chris@18: * This is only called when content_moderation module is installed. Chris@18: * Chris@18: * @param \Drupal\content_moderation\Access\LatestRevisionCheck $latest_revision_check Chris@18: * The latest revision access check service provided by the Chris@18: * content_moderation module. Chris@18: * Chris@18: * @see self::$latestRevisionCheck Chris@18: */ Chris@18: public function setLatestRevisionCheck(LatestRevisionCheck $latest_revision_check) { Chris@18: $this->latestRevisionCheck = $latest_revision_check; Chris@18: } Chris@18: Chris@18: /** Chris@18: * Get the object to normalize and the access based on the provided entity. Chris@18: * Chris@18: * @param \Drupal\Core\Entity\EntityInterface $entity Chris@18: * The entity to test access for. Chris@18: * @param \Drupal\Core\Session\AccountInterface $account Chris@18: * (optional) The account with which access should be checked. Defaults to Chris@18: * the current user. Chris@18: * Chris@18: * @return \Drupal\jsonapi\JsonApiResource\ResourceObject|\Drupal\jsonapi\JsonApiResource\LabelOnlyResourceObject|\Drupal\jsonapi\Exception\EntityAccessDeniedHttpException Chris@18: * The ResourceObject, a LabelOnlyResourceObject or an Chris@18: * EntityAccessDeniedHttpException object if neither is accessible. All Chris@18: * three possible return values carry the access result cacheability. Chris@18: */ Chris@18: public function getAccessCheckedResourceObject(EntityInterface $entity, AccountInterface $account = NULL) { Chris@18: $account = $account ?: $this->currentUser; Chris@18: $resource_type = $this->resourceTypeRepository->get($entity->getEntityTypeId(), $entity->bundle()); Chris@18: $entity = $this->entityRepository->getTranslationFromContext($entity, NULL, ['operation' => 'entity_upcast']); Chris@18: $access = $this->checkEntityAccess($entity, 'view', $account); Chris@18: $entity->addCacheableDependency($access); Chris@18: if (!$access->isAllowed()) { Chris@18: // If this is the default revision or the entity is not revisionable, then Chris@18: // check access to the entity label. Revision support is all or nothing. Chris@18: if (!$entity->getEntityType()->isRevisionable() || $entity->isDefaultRevision()) { Chris@18: $label_access = $entity->access('view label', NULL, TRUE); Chris@18: $entity->addCacheableDependency($label_access); Chris@18: if ($label_access->isAllowed()) { Chris@18: return LabelOnlyResourceObject::createFromEntity($resource_type, $entity); Chris@18: } Chris@18: $access = $access->orIf($label_access); Chris@18: } Chris@18: return new EntityAccessDeniedHttpException($entity, $access, '/data', 'The current user is not allowed to GET the selected resource.'); Chris@18: } Chris@18: return ResourceObject::createFromEntity($resource_type, $entity); Chris@18: } Chris@18: Chris@18: /** Chris@18: * Checks access to the given entity. Chris@18: * Chris@18: * @param \Drupal\Core\Entity\EntityInterface $entity Chris@18: * The entity for which access should be evaluated. Chris@18: * @param string $operation Chris@18: * The entity operation for which access should be evaluated. Chris@18: * @param \Drupal\Core\Session\AccountInterface $account Chris@18: * (optional) The account with which access should be checked. Defaults to Chris@18: * the current user. Chris@18: * Chris@18: * @return \Drupal\Core\Access\AccessResultInterface|\Drupal\Core\Access\AccessResultReasonInterface Chris@18: * The access check result. Chris@18: */ Chris@18: public function checkEntityAccess(EntityInterface $entity, $operation, AccountInterface $account) { Chris@18: $access = $entity->access($operation, $account, TRUE); Chris@18: if ($entity->getEntityType()->isRevisionable()) { Chris@18: $access = AccessResult::neutral()->addCacheContexts(['url.query_args:' . JsonApiSpec::VERSION_QUERY_PARAMETER])->orIf($access); Chris@18: if (!$entity->isDefaultRevision()) { Chris@18: assert($operation === 'view', 'JSON:API does not yet support mutable operations on revisions.'); Chris@18: $revision_access = $this->checkRevisionViewAccess($entity, $account); Chris@18: $access = $access->andIf($revision_access); Chris@18: // The revision access reason should trump the primary access reason. Chris@18: if (!$access->isAllowed()) { Chris@18: $reason = $access instanceof AccessResultReasonInterface ? $access->getReason() : ''; Chris@18: $access->setReason(trim('The user does not have access to the requested version. ' . $reason)); Chris@18: } Chris@18: } Chris@18: } Chris@18: return $access; Chris@18: } Chris@18: Chris@18: /** Chris@18: * Checks access to the given revision entity. Chris@18: * Chris@18: * This should only be called for non-default revisions. Chris@18: * Chris@18: * There is no standardized API for revision access checking in Drupal core Chris@18: * and this method shims that missing API. Chris@18: * Chris@18: * @param \Drupal\Core\Entity\EntityInterface $entity Chris@18: * The revised entity for which to check access. Chris@18: * @param \Drupal\Core\Session\AccountInterface $account Chris@18: * (optional) The account with which access should be checked. Defaults to Chris@18: * the current user. Chris@18: * Chris@18: * @return \Drupal\Core\Access\AccessResultInterface|\Drupal\Core\Access\AccessResultReasonInterface Chris@18: * The access check result. Chris@18: * Chris@18: * @todo: remove when a generic revision access API exists in Drupal core, and Chris@18: * also remove the injected "node" and "media" services. Chris@18: * @see https://www.drupal.org/project/jsonapi/issues/2992833#comment-12818386 Chris@18: */ Chris@18: protected function checkRevisionViewAccess(EntityInterface $entity, AccountInterface $account) { Chris@18: assert($entity instanceof RevisionableInterface); Chris@18: assert(!$entity->isDefaultRevision(), 'It is not necessary to check revision access when the entity is the default revision.'); Chris@18: $entity_type = $entity->getEntityType(); Chris@18: switch ($entity_type->id()) { Chris@18: case 'node': Chris@18: assert($entity instanceof NodeInterface); Chris@18: $access = AccessResult::allowedIf($this->nodeRevisionAccessCheck->checkAccess($entity, $account, 'view'))->cachePerPermissions()->addCacheableDependency($entity); Chris@18: break; Chris@18: Chris@18: case 'media': Chris@18: assert($entity instanceof MediaInterface); Chris@18: $access = AccessResult::allowedIf($this->mediaRevisionAccessCheck->checkAccess($entity, $account, 'view'))->cachePerPermissions()->addCacheableDependency($entity); Chris@18: break; Chris@18: Chris@18: default: Chris@18: $reason = 'Only node and media revisions are supported by JSON:API.'; Chris@18: $reason .= ' For context, see https://www.drupal.org/project/jsonapi/issues/2992833#comment-12818258.'; Chris@18: $reason .= ' To contribute, see https://www.drupal.org/project/drupal/issues/2350939 and https://www.drupal.org/project/drupal/issues/2809177.'; Chris@18: $access = AccessResult::neutral($reason); Chris@18: } Chris@18: // Apply content_moderation's additional access logic. Chris@18: // @see \Drupal\content_moderation\Access\LatestRevisionCheck::access() Chris@18: if ($entity_type->getLinkTemplate('latest-version') && $entity->isLatestRevision() && isset($this->latestRevisionCheck)) { Chris@18: // The latest revision access checker only expects to be invoked by the Chris@18: // routing system, which makes it necessary to fake a route match. Chris@18: $routes = $this->router->getRouteCollection(); Chris@18: $resource_type = $this->resourceTypeRepository->get($entity->getEntityTypeId(), $entity->bundle()); Chris@18: $route_name = sprintf('jsonapi.%s.individual', $resource_type->getTypeName()); Chris@18: $route = $routes->get($route_name); Chris@18: $route->setOption('_content_moderation_entity_type', 'entity'); Chris@18: $route_match = new RouteMatch($route_name, $route, ['entity' => $entity], ['entity' => $entity->uuid()]); Chris@18: $moderation_access_result = $this->latestRevisionCheck->access($route, $route_match, $account); Chris@18: $access = $access->andIf($moderation_access_result); Chris@18: } Chris@18: return $access; Chris@18: } Chris@18: Chris@18: }