Mercurial > hg > cmmr2012-drupal-site
diff core/modules/jsonapi/src/Revisions/ResourceVersionRouteEnhancer.php @ 5:12f9dff5fda9 tip
Update to Drupal core 8.7.1
author | Chris Cannam |
---|---|
date | Thu, 09 May 2019 15:34:47 +0100 |
parents | |
children |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/core/modules/jsonapi/src/Revisions/ResourceVersionRouteEnhancer.php Thu May 09 15:34:47 2019 +0100 @@ -0,0 +1,195 @@ +<?php + +namespace Drupal\jsonapi\Revisions; + +use Drupal\Core\Cache\CacheableMetadata; +use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Http\Exception\CacheableBadRequestHttpException; +use Drupal\Core\Http\Exception\CacheableHttpException; +use Drupal\Core\Routing\EnhancerInterface; +use Drupal\jsonapi\Routing\Routes; +use Symfony\Cmf\Component\Routing\RouteObjectInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; + +/** + * 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. + * + * @see https://www.drupal.org/project/jsonapi/issues/3032787 + * @see jsonapi.api.php + */ +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) { + // 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/jsonapi/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; + } + + /** + * 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; + } + +}