View source
<?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;
final class ResourceVersionRouteEnhancer implements EnhancerInterface {
const REVISION_ID_KEY = 'revision_id';
const RESOURCE_VERSION_QUERY_PARAMETER = 'resourceVersion';
const WORKING_COPIES_REQUESTED = 'working_copies_requested';
const CACHE_CONTEXT = 'url.query_args:resourceVersion';
const VERSION_IDENTIFIER_VALIDATOR = '/^[a-z]+[a-z_]*[a-z]+:[a-zA-Z0-9\\-]+(:[a-zA-Z0-9\\-]+)*$/';
protected $versionNegotiator;
public function __construct(VersionNegotiator $version_negotiator_manager) {
$this->versionNegotiator = $version_negotiator_manager;
}
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 (!$resource_type
->isVersionable()) {
if ($has_version_param) {
$cacheability = (new CacheableMetadata())
->addCacheContexts([
'url.path',
static::CACHE_CONTEXT,
]);
$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;
}
if (isset($defaults['entity'])) {
assert($defaults['entity'] instanceof EntityInterface);
$defaults['entity']
->addCacheContexts([
static::CACHE_CONTEXT,
]);
}
if (!$has_version_param) {
return $defaults;
}
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);
}
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';
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, []);
}
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);
}
$defaults[static::WORKING_COPIES_REQUESTED] = $resource_version_identifier === $working_copy_identifier;
return $defaults;
}
$entity = $defaults['entity'];
$resolved_revision = $this->versionNegotiator
->getRevision($entity, $resource_version_identifier);
$resolved_revision
->addCacheableDependency($entity);
return [
'entity' => $resolved_revision,
] + $defaults;
}
protected static function isValidVersionIdentifier($resource_version) {
return preg_match(static::VERSION_IDENTIFIER_VALIDATOR, $resource_version) === 1;
}
}