Mercurial > hg > cmmr2012-drupal-site
comparison 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 |
comparison
equal
deleted
inserted
replaced
4:a9cd425dd02b | 5:12f9dff5fda9 |
---|---|
1 <?php | |
2 | |
3 namespace Drupal\jsonapi\Revisions; | |
4 | |
5 use Drupal\Core\Cache\CacheableMetadata; | |
6 use Drupal\Core\Entity\EntityInterface; | |
7 use Drupal\Core\Http\Exception\CacheableBadRequestHttpException; | |
8 use Drupal\Core\Http\Exception\CacheableHttpException; | |
9 use Drupal\Core\Routing\EnhancerInterface; | |
10 use Drupal\jsonapi\Routing\Routes; | |
11 use Symfony\Cmf\Component\Routing\RouteObjectInterface; | |
12 use Symfony\Component\HttpFoundation\Request; | |
13 use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; | |
14 | |
15 /** | |
16 * Loads an appropriate revision for the requested resource version. | |
17 * | |
18 * @internal JSON:API maintains no PHP API since its API is the HTTP API. This | |
19 * class may change at any time and this will break any dependencies on it. | |
20 * | |
21 * @see https://www.drupal.org/project/jsonapi/issues/3032787 | |
22 * @see jsonapi.api.php | |
23 */ | |
24 final class ResourceVersionRouteEnhancer implements EnhancerInterface { | |
25 | |
26 /** | |
27 * The route default parameter name. | |
28 * | |
29 * @var string | |
30 */ | |
31 const REVISION_ID_KEY = 'revision_id'; | |
32 | |
33 /** | |
34 * The query parameter for providing a version (revision) value. | |
35 * | |
36 * @var string | |
37 */ | |
38 const RESOURCE_VERSION_QUERY_PARAMETER = 'resourceVersion'; | |
39 | |
40 /** | |
41 * A route parameter key which indicates that working copies were requested. | |
42 * | |
43 * @var string | |
44 */ | |
45 const WORKING_COPIES_REQUESTED = 'working_copies_requested'; | |
46 | |
47 /** | |
48 * The cache context by which vary the loaded entity revision. | |
49 * | |
50 * @var string | |
51 * | |
52 * @todo When D8 requires PHP >=5.6, convert to expression using the RESOURCE_VERSION_QUERY_PARAMETER constant. | |
53 */ | |
54 const CACHE_CONTEXT = 'url.query_args:resourceVersion'; | |
55 | |
56 /** | |
57 * Resource version validation regex. | |
58 * | |
59 * @var string | |
60 * | |
61 * @todo When D8 requires PHP >=5.6, convert to expression using the VersionNegotiator::SEPARATOR constant. | |
62 */ | |
63 const VERSION_IDENTIFIER_VALIDATOR = '/^[a-z]+[a-z_]*[a-z]+:[a-zA-Z0-9\-]+(:[a-zA-Z0-9\-]+)*$/'; | |
64 | |
65 /** | |
66 * The revision ID negotiator. | |
67 * | |
68 * @var \Drupal\jsonapi\Revisions\VersionNegotiator | |
69 */ | |
70 protected $versionNegotiator; | |
71 | |
72 /** | |
73 * ResourceVersionRouteEnhancer constructor. | |
74 * | |
75 * @param \Drupal\jsonapi\Revisions\VersionNegotiator $version_negotiator_manager | |
76 * The version negotiator. | |
77 */ | |
78 public function __construct(VersionNegotiator $version_negotiator_manager) { | |
79 $this->versionNegotiator = $version_negotiator_manager; | |
80 } | |
81 | |
82 /** | |
83 * {@inheritdoc} | |
84 */ | |
85 public function enhance(array $defaults, Request $request) { | |
86 if (!Routes::isJsonApiRequest($defaults) || !($resource_type = Routes::getResourceTypeNameFromParameters($defaults))) { | |
87 return $defaults; | |
88 } | |
89 | |
90 $has_version_param = $request->query->has(static::RESOURCE_VERSION_QUERY_PARAMETER); | |
91 | |
92 // If the resource type is not versionable, then nothing needs to be | |
93 // enhanced. | |
94 if (!$resource_type->isVersionable()) { | |
95 // If the query parameter was provided but the resource type is not | |
96 // versionable, provide a helpful error. | |
97 if ($has_version_param) { | |
98 // Until Drupal core has a generic revision access API, it is only safe | |
99 // to support the `node` and `media` entity types because they are the | |
100 // only // entity types that have revision access checks for forward | |
101 // revisions that are not the default and not the latest revision. | |
102 $cacheability = (new CacheableMetadata())->addCacheContexts(['url.path', static::CACHE_CONTEXT]); | |
103 /* Uncomment the next line and remove the following one when https://www.drupal.org/project/drupal/issues/3002352 lands in core. */ | |
104 /* throw new CacheableHttpException($cacheability, 501, 'Resource versioning is not yet supported for this resource type.'); */ | |
105 $message = 'JSON:API does not yet support resource versioning for this resource type.'; | |
106 $message .= ' For context, see https://www.drupal.org/project/jsonapi/issues/2992833#comment-12818258.'; | |
107 $message .= ' To contribute, see https://www.drupal.org/project/drupal/issues/2350939 and https://www.drupal.org/project/drupal/issues/2809177.'; | |
108 throw new CacheableHttpException($cacheability, 501, $message, NULL, []); | |
109 } | |
110 return $defaults; | |
111 } | |
112 | |
113 // Since the resource type is versionable, responses must always vary by the | |
114 // requested version, without regard for whether a version query parameter | |
115 // was provided or not. | |
116 if (isset($defaults['entity'])) { | |
117 assert($defaults['entity'] instanceof EntityInterface); | |
118 $defaults['entity']->addCacheContexts([static::CACHE_CONTEXT]); | |
119 } | |
120 | |
121 // If no version was specified, nothing is left to enhance. | |
122 if (!$has_version_param) { | |
123 return $defaults; | |
124 } | |
125 | |
126 // Provide a helpful error when a version is specified with an unsafe | |
127 // method. | |
128 if (!$request->isMethodCacheable()) { | |
129 throw new BadRequestHttpException(sprintf('%s requests with a `%s` query parameter are not supported.', $request->getMethod(), static::RESOURCE_VERSION_QUERY_PARAMETER)); | |
130 } | |
131 | |
132 $resource_version_identifier = $request->query->get(static::RESOURCE_VERSION_QUERY_PARAMETER); | |
133 | |
134 if (!static::isValidVersionIdentifier($resource_version_identifier)) { | |
135 $cacheability = (new CacheableMetadata())->addCacheContexts([static::CACHE_CONTEXT]); | |
136 $message = sprintf('A resource version identifier was provided in an invalid format: `%s`', $resource_version_identifier); | |
137 throw new CacheableBadRequestHttpException($cacheability, $message); | |
138 } | |
139 | |
140 // Determine if the request is for a collection resource. | |
141 if ($defaults[RouteObjectInterface::CONTROLLER_NAME] === Routes::CONTROLLER_SERVICE_NAME . ':getCollection') { | |
142 $latest_version_identifier = 'rel' . VersionNegotiator::SEPARATOR . 'latest-version'; | |
143 $working_copy_identifier = 'rel' . VersionNegotiator::SEPARATOR . 'working-copy'; | |
144 // Until Drupal core has a revision access API that works on entity | |
145 // queries, filtering is not permitted on non-default revisions. | |
146 if ($request->query->has('filter') && $resource_version_identifier !== $latest_version_identifier) { | |
147 $cache_contexts = [ | |
148 'url.path', | |
149 static::CACHE_CONTEXT, | |
150 'url.query_args:filter', | |
151 ]; | |
152 $cacheability = (new CacheableMetadata())->addCacheContexts($cache_contexts); | |
153 $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.'; | |
154 throw new CacheableHttpException($cacheability, 501, $message, NULL, []); | |
155 } | |
156 // 'latest-version' and 'working-copy' are the only acceptable version | |
157 // identifiers for a collection resource. | |
158 if (!in_array($resource_version_identifier, [$latest_version_identifier, $working_copy_identifier])) { | |
159 $cacheability = (new CacheableMetadata())->addCacheContexts(['url.path', static::CACHE_CONTEXT]); | |
160 $message = sprintf('Collection resources only support the following resource version identifiers: %s', implode(', ', [ | |
161 $latest_version_identifier, | |
162 $working_copy_identifier, | |
163 ])); | |
164 throw new CacheableBadRequestHttpException($cacheability, $message); | |
165 } | |
166 // Whether the collection to be loaded should include only working copies. | |
167 $defaults[static::WORKING_COPIES_REQUESTED] = $resource_version_identifier === $working_copy_identifier; | |
168 return $defaults; | |
169 } | |
170 | |
171 /** @var \Drupal\Core\Entity\EntityInterface $entity */ | |
172 $entity = $defaults['entity']; | |
173 | |
174 /** @var \Drupal\jsonapi\Revisions\VersionNegotiatorInterface $negotiator */ | |
175 $resolved_revision = $this->versionNegotiator->getRevision($entity, $resource_version_identifier); | |
176 // Ensure none of the original entity cacheability is lost, especially the | |
177 // query argument's cache context. | |
178 $resolved_revision->addCacheableDependency($entity); | |
179 return ['entity' => $resolved_revision] + $defaults; | |
180 } | |
181 | |
182 /** | |
183 * Validates the user input. | |
184 * | |
185 * @param string $resource_version | |
186 * The requested resource version identifier. | |
187 * | |
188 * @return bool | |
189 * TRUE if the received resource version value is valid, FALSE otherwise. | |
190 */ | |
191 protected static function isValidVersionIdentifier($resource_version) { | |
192 return preg_match(static::VERSION_IDENTIFIER_VALIDATOR, $resource_version) === 1; | |
193 } | |
194 | |
195 } |