You are here

final class ResourceVersionRouteEnhancer in Drupal 10

Same name and namespace in other branches
  1. 8 core/modules/jsonapi/src/Revisions/ResourceVersionRouteEnhancer.php \Drupal\jsonapi\Revisions\ResourceVersionRouteEnhancer
  2. 9 core/modules/jsonapi/src/Revisions/ResourceVersionRouteEnhancer.php \Drupal\jsonapi\Revisions\ResourceVersionRouteEnhancer

Loads an appropriate revision for the requested resource version.

@internal JSON:API maintains no PHP API since its API is the HTTP API. This class may change at any time and this will break any dependencies on it.

Hierarchy

Expanded class hierarchy of ResourceVersionRouteEnhancer

See also

https://www.drupal.org/project/drupal/issues/3032787

jsonapi.api.php

1 file declares its use of ResourceVersionRouteEnhancer
EntityResource.php in core/modules/jsonapi/src/Controller/EntityResource.php
1 string reference to 'ResourceVersionRouteEnhancer'
jsonapi.services.yml in core/modules/jsonapi/jsonapi.services.yml
core/modules/jsonapi/jsonapi.services.yml
1 service uses ResourceVersionRouteEnhancer
jsonapi.resource_version.route_enhancer in core/modules/jsonapi/jsonapi.services.yml
Drupal\jsonapi\Revisions\ResourceVersionRouteEnhancer

File

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

Namespace

Drupal\jsonapi\Revisions
View source
final class ResourceVersionRouteEnhancer implements EnhancerInterface {

  /**
   * The route default parameter name.
   *
   * @var string
   */
  const REVISION_ID_KEY = 'revision_id';

  /**
   * The query parameter for providing a version (revision) value.
   *
   * @var string
   */
  const RESOURCE_VERSION_QUERY_PARAMETER = 'resourceVersion';

  /**
   * A route parameter key which indicates that working copies were requested.
   *
   * @var string
   */
  const WORKING_COPIES_REQUESTED = 'working_copies_requested';

  /**
   * The cache context by which vary the loaded entity revision.
   *
   * @var string
   *
   * @todo When D8 requires PHP >=5.6, convert to expression using the RESOURCE_VERSION_QUERY_PARAMETER constant.
   */
  const CACHE_CONTEXT = 'url.query_args:resourceVersion';

  /**
   * Resource version validation regex.
   *
   * @var string
   *
   * @todo When D8 requires PHP >=5.6, convert to expression using the VersionNegotiator::SEPARATOR constant.
   */
  const VERSION_IDENTIFIER_VALIDATOR = '/^[a-z]+[a-z_]*[a-z]+:[a-zA-Z0-9\\-]+(:[a-zA-Z0-9\\-]+)*$/';

  /**
   * The revision ID negotiator.
   *
   * @var \Drupal\jsonapi\Revisions\VersionNegotiator
   */
  protected $versionNegotiator;

  /**
   * ResourceVersionRouteEnhancer constructor.
   *
   * @param \Drupal\jsonapi\Revisions\VersionNegotiator $version_negotiator_manager
   *   The version negotiator.
   */
  public function __construct(VersionNegotiator $version_negotiator_manager) {
    $this->versionNegotiator = $version_negotiator_manager;
  }

  /**
   * {@inheritdoc}
   */
  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) {
        $cacheability = (new CacheableMetadata())
          ->addCacheContexts([
          'url.path',
          static::CACHE_CONTEXT,
        ]);
        throw new CacheableHttpException($cacheability, 501, 'Resource versioning is not yet supported for this resource type.');
      }
      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);
      }

      // '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;
  }

  /**
   * Validates the user input.
   *
   * @param string $resource_version
   *   The requested resource version identifier.
   *
   * @return bool
   *   TRUE if the received resource version value is valid, FALSE otherwise.
   */
  protected static function isValidVersionIdentifier($resource_version) {
    return preg_match(static::VERSION_IDENTIFIER_VALIDATOR, $resource_version) === 1;
  }

}

Members