Mercurial > hg > isophonics-drupal-site
comparison core/modules/jsonapi/src/Routing/Routes.php @ 18:af1871eacc83
Update to Drupal core 8.7.1
author | Chris Cannam |
---|---|
date | Thu, 09 May 2019 15:33:08 +0100 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
17:129ea1e6d783 | 18:af1871eacc83 |
---|---|
1 <?php | |
2 | |
3 namespace Drupal\jsonapi\Routing; | |
4 | |
5 use Drupal\Core\DependencyInjection\ContainerInjectionInterface; | |
6 use Drupal\jsonapi\Access\RelationshipFieldAccess; | |
7 use Drupal\jsonapi\Controller\EntryPoint; | |
8 use Drupal\jsonapi\ParamConverter\ResourceTypeConverter; | |
9 use Drupal\jsonapi\ResourceType\ResourceType; | |
10 use Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface; | |
11 use Symfony\Cmf\Component\Routing\RouteObjectInterface; | |
12 use Symfony\Component\DependencyInjection\ContainerInterface; | |
13 use Symfony\Component\Routing\Route; | |
14 use Symfony\Component\Routing\RouteCollection; | |
15 | |
16 /** | |
17 * Defines dynamic routes. | |
18 * | |
19 * @internal JSON:API maintains no PHP API since its API is the HTTP API. This | |
20 * class may change at any time and this will break any dependencies on it. | |
21 * | |
22 * @see https://www.drupal.org/project/jsonapi/issues/3032787 | |
23 * @see jsonapi.api.php | |
24 */ | |
25 class Routes implements ContainerInjectionInterface { | |
26 | |
27 /** | |
28 * The service name for the primary JSON:API controller. | |
29 * | |
30 * All resources except the entrypoint are served by this controller. | |
31 * | |
32 * @var string | |
33 */ | |
34 const CONTROLLER_SERVICE_NAME = 'jsonapi.entity_resource'; | |
35 | |
36 /** | |
37 * A key with which to flag a route as belonging to the JSON:API module. | |
38 * | |
39 * @var string | |
40 */ | |
41 const JSON_API_ROUTE_FLAG_KEY = '_is_jsonapi'; | |
42 | |
43 /** | |
44 * The route default key for the route's resource type information. | |
45 * | |
46 * @var string | |
47 */ | |
48 const RESOURCE_TYPE_KEY = 'resource_type'; | |
49 | |
50 /** | |
51 * The JSON:API resource type repository. | |
52 * | |
53 * @var \Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface | |
54 */ | |
55 protected $resourceTypeRepository; | |
56 | |
57 /** | |
58 * List of providers. | |
59 * | |
60 * @var string[] | |
61 */ | |
62 protected $providerIds; | |
63 | |
64 /** | |
65 * The JSON:API base path. | |
66 * | |
67 * @var string | |
68 */ | |
69 protected $jsonApiBasePath; | |
70 | |
71 /** | |
72 * Instantiates a Routes object. | |
73 * | |
74 * @param \Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface $resource_type_repository | |
75 * The JSON:API resource type repository. | |
76 * @param string[] $authentication_providers | |
77 * The authentication providers, keyed by ID. | |
78 * @param string $jsonapi_base_path | |
79 * The JSON:API base path. | |
80 */ | |
81 public function __construct(ResourceTypeRepositoryInterface $resource_type_repository, array $authentication_providers, $jsonapi_base_path) { | |
82 $this->resourceTypeRepository = $resource_type_repository; | |
83 $this->providerIds = array_keys($authentication_providers); | |
84 assert(is_string($jsonapi_base_path)); | |
85 assert( | |
86 $jsonapi_base_path[0] === '/', | |
87 sprintf('The provided base path should contain a leading slash "/". Given: "%s".', $jsonapi_base_path) | |
88 ); | |
89 assert( | |
90 substr($jsonapi_base_path, -1) !== '/', | |
91 sprintf('The provided base path should not contain a trailing slash "/". Given: "%s".', $jsonapi_base_path) | |
92 ); | |
93 $this->jsonApiBasePath = $jsonapi_base_path; | |
94 } | |
95 | |
96 /** | |
97 * {@inheritdoc} | |
98 */ | |
99 public static function create(ContainerInterface $container) { | |
100 return new static( | |
101 $container->get('jsonapi.resource_type.repository'), | |
102 $container->getParameter('authentication_providers'), | |
103 $container->getParameter('jsonapi.base_path') | |
104 ); | |
105 } | |
106 | |
107 /** | |
108 * {@inheritdoc} | |
109 */ | |
110 public function routes() { | |
111 $routes = new RouteCollection(); | |
112 $upload_routes = new RouteCollection(); | |
113 | |
114 // JSON:API's routes: entry point + routes for every resource type. | |
115 foreach ($this->resourceTypeRepository->all() as $resource_type) { | |
116 $routes->addCollection(static::getRoutesForResourceType($resource_type, $this->jsonApiBasePath)); | |
117 $upload_routes->addCollection(static::getFileUploadRoutesForResourceType($resource_type, $this->jsonApiBasePath)); | |
118 } | |
119 $routes->add('jsonapi.resource_list', static::getEntryPointRoute($this->jsonApiBasePath)); | |
120 | |
121 // Require the JSON:API media type header on every route, except on file | |
122 // upload routes, where we require `application/octet-stream`. | |
123 $routes->addRequirements(['_content_type_format' => 'api_json']); | |
124 $upload_routes->addRequirements(['_content_type_format' => 'bin']); | |
125 | |
126 $routes->addCollection($upload_routes); | |
127 | |
128 // Enable all available authentication providers. | |
129 $routes->addOptions(['_auth' => $this->providerIds]); | |
130 | |
131 // Flag every route as belonging to the JSON:API module. | |
132 $routes->addDefaults([static::JSON_API_ROUTE_FLAG_KEY => TRUE]); | |
133 | |
134 // All routes serve only the JSON:API media type. | |
135 $routes->addRequirements(['_format' => 'api_json']); | |
136 | |
137 return $routes; | |
138 } | |
139 | |
140 /** | |
141 * Gets applicable resource routes for a JSON:API resource type. | |
142 * | |
143 * @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type | |
144 * The JSON:API resource type for which to get the routes. | |
145 * @param string $path_prefix | |
146 * The root path prefix. | |
147 * | |
148 * @return \Symfony\Component\Routing\RouteCollection | |
149 * A collection of routes for the given resource type. | |
150 */ | |
151 protected static function getRoutesForResourceType(ResourceType $resource_type, $path_prefix) { | |
152 // Internal resources have no routes. | |
153 if ($resource_type->isInternal()) { | |
154 return new RouteCollection(); | |
155 } | |
156 | |
157 $routes = new RouteCollection(); | |
158 | |
159 // Collection route like `/jsonapi/node/article`. | |
160 if ($resource_type->isLocatable()) { | |
161 $collection_route = new Route("/{$resource_type->getPath()}"); | |
162 $collection_route->addDefaults([RouteObjectInterface::CONTROLLER_NAME => static::CONTROLLER_SERVICE_NAME . ':getCollection']); | |
163 $collection_route->setMethods(['GET']); | |
164 // Allow anybody access because "view" and "view label" access are checked | |
165 // in the controller. | |
166 $collection_route->setRequirement('_access', 'TRUE'); | |
167 $routes->add(static::getRouteName($resource_type, 'collection'), $collection_route); | |
168 } | |
169 | |
170 // Creation route. | |
171 if ($resource_type->isMutable()) { | |
172 $collection_create_route = new Route("/{$resource_type->getPath()}"); | |
173 $collection_create_route->addDefaults([RouteObjectInterface::CONTROLLER_NAME => static::CONTROLLER_SERVICE_NAME . ':createIndividual']); | |
174 $collection_create_route->setMethods(['POST']); | |
175 $create_requirement = sprintf("%s:%s", $resource_type->getEntityTypeId(), $resource_type->getBundle()); | |
176 $collection_create_route->setRequirement('_entity_create_access', $create_requirement); | |
177 $collection_create_route->setRequirement('_csrf_request_header_token', 'TRUE'); | |
178 $routes->add(static::getRouteName($resource_type, 'collection.post'), $collection_create_route); | |
179 } | |
180 | |
181 // Individual routes like `/jsonapi/node/article/{uuid}` or | |
182 // `/jsonapi/node/article/{uuid}/relationships/uid`. | |
183 $routes->addCollection(static::getIndividualRoutesForResourceType($resource_type)); | |
184 | |
185 // Add the resource type as a parameter to every resource route. | |
186 foreach ($routes as $route) { | |
187 static::addRouteParameter($route, static::RESOURCE_TYPE_KEY, ['type' => ResourceTypeConverter::PARAM_TYPE_ID]); | |
188 $route->addDefaults([static::RESOURCE_TYPE_KEY => $resource_type->getTypeName()]); | |
189 } | |
190 | |
191 // Resource routes all have the same base path. | |
192 $routes->addPrefix($path_prefix); | |
193 | |
194 return $routes; | |
195 } | |
196 | |
197 /** | |
198 * Gets the file upload route collection for the given resource type. | |
199 * | |
200 * @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type | |
201 * The resource type for which the route collection should be created. | |
202 * @param string $path_prefix | |
203 * The root path prefix. | |
204 * | |
205 * @return \Symfony\Component\Routing\RouteCollection | |
206 * The route collection. | |
207 */ | |
208 protected static function getFileUploadRoutesForResourceType(ResourceType $resource_type, $path_prefix) { | |
209 $routes = new RouteCollection(); | |
210 | |
211 // Internal resources have no routes; individual routes require locations. | |
212 if ($resource_type->isInternal() || !$resource_type->isLocatable()) { | |
213 return $routes; | |
214 } | |
215 | |
216 // File upload routes are only necessary for resource types that have file | |
217 // fields. | |
218 $has_file_field = array_reduce($resource_type->getRelatableResourceTypes(), function ($carry, array $target_resource_types) { | |
219 return $carry || static::hasNonInternalFileTargetResourceTypes($target_resource_types); | |
220 }, FALSE); | |
221 if (!$has_file_field) { | |
222 return $routes; | |
223 } | |
224 | |
225 if ($resource_type->isMutable()) { | |
226 $path = $resource_type->getPath(); | |
227 $entity_type_id = $resource_type->getEntityTypeId(); | |
228 | |
229 $new_resource_file_upload_route = new Route("/{$path}/{file_field_name}"); | |
230 $new_resource_file_upload_route->addDefaults([RouteObjectInterface::CONTROLLER_NAME => 'jsonapi.file_upload:handleFileUploadForNewResource']); | |
231 $new_resource_file_upload_route->setMethods(['POST']); | |
232 $new_resource_file_upload_route->setRequirement('_csrf_request_header_token', 'TRUE'); | |
233 $routes->add(static::getFileUploadRouteName($resource_type, 'new_resource'), $new_resource_file_upload_route); | |
234 | |
235 $existing_resource_file_upload_route = new Route("/{$path}/{entity}/{file_field_name}"); | |
236 $existing_resource_file_upload_route->addDefaults([RouteObjectInterface::CONTROLLER_NAME => 'jsonapi.file_upload:handleFileUploadForExistingResource']); | |
237 $existing_resource_file_upload_route->setMethods(['POST']); | |
238 $existing_resource_file_upload_route->setRequirement('_csrf_request_header_token', 'TRUE'); | |
239 $routes->add(static::getFileUploadRouteName($resource_type, 'existing_resource'), $existing_resource_file_upload_route); | |
240 | |
241 // Add entity parameter conversion to every route. | |
242 $routes->addOptions(['parameters' => ['entity' => ['type' => 'entity:' . $entity_type_id]]]); | |
243 | |
244 // Add the resource type as a parameter to every resource route. | |
245 foreach ($routes as $route) { | |
246 static::addRouteParameter($route, static::RESOURCE_TYPE_KEY, ['type' => ResourceTypeConverter::PARAM_TYPE_ID]); | |
247 $route->addDefaults([static::RESOURCE_TYPE_KEY => $resource_type->getTypeName()]); | |
248 } | |
249 } | |
250 | |
251 // File upload routes all have the same base path. | |
252 $routes->addPrefix($path_prefix); | |
253 | |
254 return $routes; | |
255 } | |
256 | |
257 /** | |
258 * Determines if the given request is for a JSON:API generated route. | |
259 * | |
260 * @param array $defaults | |
261 * The request's route defaults. | |
262 * | |
263 * @return bool | |
264 * Whether the request targets a generated route. | |
265 */ | |
266 public static function isJsonApiRequest(array $defaults) { | |
267 return isset($defaults[RouteObjectInterface::CONTROLLER_NAME]) | |
268 && strpos($defaults[RouteObjectInterface::CONTROLLER_NAME], static::CONTROLLER_SERVICE_NAME) === 0; | |
269 } | |
270 | |
271 /** | |
272 * Gets a route collection for the given resource type. | |
273 * | |
274 * @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type | |
275 * The resource type for which the route collection should be created. | |
276 * | |
277 * @return \Symfony\Component\Routing\RouteCollection | |
278 * The route collection. | |
279 */ | |
280 protected static function getIndividualRoutesForResourceType(ResourceType $resource_type) { | |
281 if (!$resource_type->isLocatable()) { | |
282 return new RouteCollection(); | |
283 } | |
284 | |
285 $routes = new RouteCollection(); | |
286 | |
287 $path = $resource_type->getPath(); | |
288 $entity_type_id = $resource_type->getEntityTypeId(); | |
289 | |
290 // Individual read, update and remove. | |
291 $individual_route = new Route("/{$path}/{entity}"); | |
292 $individual_route->addDefaults([RouteObjectInterface::CONTROLLER_NAME => static::CONTROLLER_SERVICE_NAME . ':getIndividual']); | |
293 $individual_route->setMethods(['GET']); | |
294 // No _entity_access requirement because "view" and "view label" access are | |
295 // checked in the controller. So it's safe to allow anybody access. | |
296 $individual_route->setRequirement('_access', 'TRUE'); | |
297 $routes->add(static::getRouteName($resource_type, 'individual'), $individual_route); | |
298 if ($resource_type->isMutable()) { | |
299 $individual_update_route = new Route($individual_route->getPath()); | |
300 $individual_update_route->addDefaults([RouteObjectInterface::CONTROLLER_NAME => static::CONTROLLER_SERVICE_NAME . ':patchIndividual']); | |
301 $individual_update_route->setMethods(['PATCH']); | |
302 $individual_update_route->setRequirement('_entity_access', "entity.update"); | |
303 $individual_update_route->setRequirement('_csrf_request_header_token', 'TRUE'); | |
304 $routes->add(static::getRouteName($resource_type, 'individual.patch'), $individual_update_route); | |
305 $individual_remove_route = new Route($individual_route->getPath()); | |
306 $individual_remove_route->addDefaults([RouteObjectInterface::CONTROLLER_NAME => static::CONTROLLER_SERVICE_NAME . ':deleteIndividual']); | |
307 $individual_remove_route->setMethods(['DELETE']); | |
308 $individual_remove_route->setRequirement('_entity_access', "entity.delete"); | |
309 $individual_remove_route->setRequirement('_csrf_request_header_token', 'TRUE'); | |
310 $routes->add(static::getRouteName($resource_type, 'individual.delete'), $individual_remove_route); | |
311 } | |
312 | |
313 foreach ($resource_type->getRelatableResourceTypes() as $relationship_field_name => $target_resource_types) { | |
314 // Read, update, add, or remove an individual resources relationships to | |
315 // other resources. | |
316 $relationship_route = new Route("/{$path}/{entity}/relationships/{$relationship_field_name}"); | |
317 $relationship_route->addDefaults(['_on_relationship' => TRUE]); | |
318 $relationship_route->addDefaults(['related' => $relationship_field_name]); | |
319 $relationship_route->setRequirement(RelationshipFieldAccess::ROUTE_REQUIREMENT_KEY, $relationship_field_name); | |
320 $relationship_route->setRequirement('_csrf_request_header_token', 'TRUE'); | |
321 $relationship_route_methods = $resource_type->isMutable() | |
322 ? ['GET', 'POST', 'PATCH', 'DELETE'] | |
323 : ['GET']; | |
324 $relationship_controller_methods = [ | |
325 'GET' => 'getRelationship', | |
326 'POST' => 'addToRelationshipData', | |
327 'PATCH' => 'replaceRelationshipData', | |
328 'DELETE' => 'removeFromRelationshipData', | |
329 ]; | |
330 foreach ($relationship_route_methods as $method) { | |
331 $method_specific_relationship_route = clone $relationship_route; | |
332 $method_specific_relationship_route->addDefaults([RouteObjectInterface::CONTROLLER_NAME => static::CONTROLLER_SERVICE_NAME . ":{$relationship_controller_methods[$method]}"]); | |
333 $method_specific_relationship_route->setMethods($method); | |
334 $routes->add(static::getRouteName($resource_type, sprintf("%s.relationship.%s", $relationship_field_name, strtolower($method))), $method_specific_relationship_route); | |
335 } | |
336 | |
337 // Only create routes for related routes that target at least one | |
338 // non-internal resource type. | |
339 if (static::hasNonInternalTargetResourceTypes($target_resource_types)) { | |
340 // Get an individual resource's related resources. | |
341 $related_route = new Route("/{$path}/{entity}/{$relationship_field_name}"); | |
342 $related_route->setMethods(['GET']); | |
343 $related_route->addDefaults([RouteObjectInterface::CONTROLLER_NAME => static::CONTROLLER_SERVICE_NAME . ':getRelated']); | |
344 $related_route->addDefaults(['related' => $relationship_field_name]); | |
345 $related_route->setRequirement(RelationshipFieldAccess::ROUTE_REQUIREMENT_KEY, $relationship_field_name); | |
346 $routes->add(static::getRouteName($resource_type, "$relationship_field_name.related"), $related_route); | |
347 } | |
348 } | |
349 | |
350 // Add entity parameter conversion to every route. | |
351 $routes->addOptions(['parameters' => ['entity' => ['type' => 'entity:' . $entity_type_id]]]); | |
352 | |
353 return $routes; | |
354 } | |
355 | |
356 /** | |
357 * Provides the entry point route. | |
358 * | |
359 * @param string $path_prefix | |
360 * The root path prefix. | |
361 * | |
362 * @return \Symfony\Component\Routing\Route | |
363 * The entry point route. | |
364 */ | |
365 protected function getEntryPointRoute($path_prefix) { | |
366 $entry_point = new Route("/{$path_prefix}"); | |
367 $entry_point->addDefaults([RouteObjectInterface::CONTROLLER_NAME => EntryPoint::class . '::index']); | |
368 $entry_point->setRequirement('_access', 'TRUE'); | |
369 $entry_point->setMethods(['GET']); | |
370 return $entry_point; | |
371 } | |
372 | |
373 /** | |
374 * Adds a parameter option to a route, overrides options of the same name. | |
375 * | |
376 * The Symfony Route class only has a method for adding options which | |
377 * overrides any previous values. Therefore, it is tedious to add a single | |
378 * parameter while keeping those that are already set. | |
379 * | |
380 * @param \Symfony\Component\Routing\Route $route | |
381 * The route to which the parameter is to be added. | |
382 * @param string $name | |
383 * The name of the parameter. | |
384 * @param mixed $parameter | |
385 * The parameter's options. | |
386 */ | |
387 protected static function addRouteParameter(Route $route, $name, $parameter) { | |
388 $parameters = $route->getOption('parameters') ?: []; | |
389 $parameters[$name] = $parameter; | |
390 $route->setOption('parameters', $parameters); | |
391 } | |
392 | |
393 /** | |
394 * Get a unique route name for the JSON:API resource type and route type. | |
395 * | |
396 * @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type | |
397 * The resource type for which the route collection should be created. | |
398 * @param string $route_type | |
399 * The route type. E.g. 'individual' or 'collection'. | |
400 * | |
401 * @return string | |
402 * The generated route name. | |
403 */ | |
404 public static function getRouteName(ResourceType $resource_type, $route_type) { | |
405 return sprintf('jsonapi.%s.%s', $resource_type->getTypeName(), $route_type); | |
406 } | |
407 | |
408 /** | |
409 * Get a unique route name for the file upload resource type and route type. | |
410 * | |
411 * @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type | |
412 * The resource type for which the route collection should be created. | |
413 * @param string $route_type | |
414 * The route type. E.g. 'individual' or 'collection'. | |
415 * | |
416 * @return string | |
417 * The generated route name. | |
418 */ | |
419 protected static function getFileUploadRouteName(ResourceType $resource_type, $route_type) { | |
420 return sprintf('jsonapi.%s.%s.%s', $resource_type->getTypeName(), 'file_upload', $route_type); | |
421 } | |
422 | |
423 /** | |
424 * Determines if an array of resource types has any non-internal ones. | |
425 * | |
426 * @param \Drupal\jsonapi\ResourceType\ResourceType[] $resource_types | |
427 * The resource types to check. | |
428 * | |
429 * @return bool | |
430 * TRUE if there is at least one non-internal resource type in the given | |
431 * array; FALSE otherwise. | |
432 */ | |
433 protected static function hasNonInternalTargetResourceTypes(array $resource_types) { | |
434 return array_reduce($resource_types, function ($carry, ResourceType $target) { | |
435 return $carry || !$target->isInternal(); | |
436 }, FALSE); | |
437 } | |
438 | |
439 /** | |
440 * Determines if an array of resource types lists non-internal "file" ones. | |
441 * | |
442 * @param \Drupal\jsonapi\ResourceType\ResourceType[] $resource_types | |
443 * The resource types to check. | |
444 * | |
445 * @return bool | |
446 * TRUE if there is at least one non-internal "file" resource type in the | |
447 * given array; FALSE otherwise. | |
448 */ | |
449 protected static function hasNonInternalFileTargetResourceTypes(array $resource_types) { | |
450 return array_reduce($resource_types, function ($carry, ResourceType $target) { | |
451 return $carry || (!$target->isInternal() && $target->getEntityTypeId() === 'file'); | |
452 }, FALSE); | |
453 } | |
454 | |
455 /** | |
456 * Gets the resource type from a route or request's parameters. | |
457 * | |
458 * @param array $parameters | |
459 * An array of parameters. These may be obtained from a route's | |
460 * parameter defaults or from a request object. | |
461 * | |
462 * @return \Drupal\jsonapi\ResourceType\ResourceType|null | |
463 * The resource type, NULL if one cannot be found from the given parameters. | |
464 */ | |
465 public static function getResourceTypeNameFromParameters(array $parameters) { | |
466 if (isset($parameters[static::JSON_API_ROUTE_FLAG_KEY]) && $parameters[static::JSON_API_ROUTE_FLAG_KEY]) { | |
467 return isset($parameters[static::RESOURCE_TYPE_KEY]) ? $parameters[static::RESOURCE_TYPE_KEY] : NULL; | |
468 } | |
469 return NULL; | |
470 } | |
471 | |
472 /** | |
473 * Invalidates any JSON:API resource type dependent responses and routes. | |
474 */ | |
475 public static function rebuild() { | |
476 \Drupal::service('cache_tags.invalidator')->invalidateTags(['jsonapi_resource_types']); | |
477 \Drupal::service('router.builder')->setRebuildNeeded(); | |
478 } | |
479 | |
480 } |