You are here

public function ResourceVersionRouteEnhancer::enhance in Drupal 8

Same name and namespace in other branches
  1. 9 core/modules/jsonapi/src/Revisions/ResourceVersionRouteEnhancer.php \Drupal\jsonapi\Revisions\ResourceVersionRouteEnhancer::enhance()

File

core/modules/jsonapi/src/Revisions/ResourceVersionRouteEnhancer.php, line 85

Class

ResourceVersionRouteEnhancer
Loads an appropriate revision for the requested resource version.

Namespace

Drupal\jsonapi\Revisions

Code

public function enhance(array $defaults, Request $request) {
  if (!Routes::isJsonApiRequest($defaults) || !($resource_type = Routes::getResourceTypeNameFromParameters($defaults))) {
    return $defaults;
  }
  $has_version_param = $request->query
    ->has(static::RESOURCE_VERSION_QUERY_PARAMETER);

  // If the resource type is not versionable, then nothing needs to be
  // enhanced.
  if (!$resource_type
    ->isVersionable()) {

    // If the query parameter was provided but the resource type is not
    // versionable, provide a helpful error.
    if ($has_version_param) {

      // Until Drupal core has a generic revision access API, it is only safe
      // to support the `node` and `media` entity types because they are the
      // only // entity types that have revision access checks for forward
      // revisions that are not the default and not the latest revision.
      $cacheability = (new CacheableMetadata())
        ->addCacheContexts([
        'url.path',
        static::CACHE_CONTEXT,
      ]);

      /* Uncomment the next line and remove the following one when https://www.drupal.org/project/drupal/issues/3002352 lands in core. */

      /* throw new CacheableHttpException($cacheability, 501, 'Resource versioning is not yet supported for this resource type.'); */
      $message = 'JSON:API does not yet support resource versioning for this resource type.';
      $message .= ' For context, see https://www.drupal.org/project/drupal/issues/2992833#comment-12818258.';
      $message .= ' To contribute, see https://www.drupal.org/project/drupal/issues/2350939 and https://www.drupal.org/project/drupal/issues/2809177.';
      throw new CacheableHttpException($cacheability, 501, $message, NULL, []);
    }
    return $defaults;
  }

  // Since the resource type is versionable, responses must always vary by the
  // requested version, without regard for whether a version query parameter
  // was provided or not.
  if (isset($defaults['entity'])) {
    assert($defaults['entity'] instanceof EntityInterface);
    $defaults['entity']
      ->addCacheContexts([
      static::CACHE_CONTEXT,
    ]);
  }

  // If no version was specified, nothing is left to enhance.
  if (!$has_version_param) {
    return $defaults;
  }

  // Provide a helpful error when a version is specified with an unsafe
  // method.
  if (!$request
    ->isMethodCacheable()) {
    throw new BadRequestHttpException(sprintf('%s requests with a `%s` query parameter are not supported.', $request
      ->getMethod(), static::RESOURCE_VERSION_QUERY_PARAMETER));
  }
  $resource_version_identifier = $request->query
    ->get(static::RESOURCE_VERSION_QUERY_PARAMETER);
  if (!static::isValidVersionIdentifier($resource_version_identifier)) {
    $cacheability = (new CacheableMetadata())
      ->addCacheContexts([
      static::CACHE_CONTEXT,
    ]);
    $message = sprintf('A resource version identifier was provided in an invalid format: `%s`', $resource_version_identifier);
    throw new CacheableBadRequestHttpException($cacheability, $message);
  }

  // Determine if the request is for a collection resource.
  if ($defaults[RouteObjectInterface::CONTROLLER_NAME] === Routes::CONTROLLER_SERVICE_NAME . ':getCollection') {
    $latest_version_identifier = 'rel' . VersionNegotiator::SEPARATOR . 'latest-version';
    $working_copy_identifier = 'rel' . VersionNegotiator::SEPARATOR . 'working-copy';

    // Until Drupal core has a revision access API that works on entity
    // queries, filtering is not permitted on non-default revisions.
    if ($request->query
      ->has('filter') && $resource_version_identifier !== $latest_version_identifier) {
      $cache_contexts = [
        'url.path',
        static::CACHE_CONTEXT,
        'url.query_args:filter',
      ];
      $cacheability = (new CacheableMetadata())
        ->addCacheContexts($cache_contexts);
      $message = 'JSON:API does not support filtering on revisions other than the latest version because a secure Drupal core API does not yet exist to do so.';
      throw new CacheableHttpException($cacheability, 501, $message, NULL, []);
    }

    // 'latest-version' and 'working-copy' are the only acceptable version
    // identifiers for a collection resource.
    if (!in_array($resource_version_identifier, [
      $latest_version_identifier,
      $working_copy_identifier,
    ])) {
      $cacheability = (new CacheableMetadata())
        ->addCacheContexts([
        'url.path',
        static::CACHE_CONTEXT,
      ]);
      $message = sprintf('Collection resources only support the following resource version identifiers: %s', implode(', ', [
        $latest_version_identifier,
        $working_copy_identifier,
      ]));
      throw new CacheableBadRequestHttpException($cacheability, $message);
    }

    // Whether the collection to be loaded should include only working copies.
    $defaults[static::WORKING_COPIES_REQUESTED] = $resource_version_identifier === $working_copy_identifier;
    return $defaults;
  }

  /** @var \Drupal\Core\Entity\EntityInterface $entity */
  $entity = $defaults['entity'];

  /** @var \Drupal\jsonapi\Revisions\VersionNegotiatorInterface $negotiator */
  $resolved_revision = $this->versionNegotiator
    ->getRevision($entity, $resource_version_identifier);

  // Ensure none of the original entity cacheability is lost, especially the
  // query argument's cache context.
  $resolved_revision
    ->addCacheableDependency($entity);
  return [
    'entity' => $resolved_revision,
  ] + $defaults;
}