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 }