diff 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
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/core/modules/jsonapi/src/Routing/Routes.php	Thu May 09 15:33:08 2019 +0100
@@ -0,0 +1,480 @@
+<?php
+
+namespace Drupal\jsonapi\Routing;
+
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\jsonapi\Access\RelationshipFieldAccess;
+use Drupal\jsonapi\Controller\EntryPoint;
+use Drupal\jsonapi\ParamConverter\ResourceTypeConverter;
+use Drupal\jsonapi\ResourceType\ResourceType;
+use Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface;
+use Symfony\Cmf\Component\Routing\RouteObjectInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\Routing\Route;
+use Symfony\Component\Routing\RouteCollection;
+
+/**
+ * Defines dynamic routes.
+ *
+ * @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
+ */
+class Routes implements ContainerInjectionInterface {
+
+  /**
+   * The service name for the primary JSON:API controller.
+   *
+   * All resources except the entrypoint are served by this controller.
+   *
+   * @var string
+   */
+  const CONTROLLER_SERVICE_NAME = 'jsonapi.entity_resource';
+
+  /**
+   * A key with which to flag a route as belonging to the JSON:API module.
+   *
+   * @var string
+   */
+  const JSON_API_ROUTE_FLAG_KEY = '_is_jsonapi';
+
+  /**
+   * The route default key for the route's resource type information.
+   *
+   * @var string
+   */
+  const RESOURCE_TYPE_KEY = 'resource_type';
+
+  /**
+   * The JSON:API resource type repository.
+   *
+   * @var \Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface
+   */
+  protected $resourceTypeRepository;
+
+  /**
+   * List of providers.
+   *
+   * @var string[]
+   */
+  protected $providerIds;
+
+  /**
+   * The JSON:API base path.
+   *
+   * @var string
+   */
+  protected $jsonApiBasePath;
+
+  /**
+   * Instantiates a Routes object.
+   *
+   * @param \Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface $resource_type_repository
+   *   The JSON:API resource type repository.
+   * @param string[] $authentication_providers
+   *   The authentication providers, keyed by ID.
+   * @param string $jsonapi_base_path
+   *   The JSON:API base path.
+   */
+  public function __construct(ResourceTypeRepositoryInterface $resource_type_repository, array $authentication_providers, $jsonapi_base_path) {
+    $this->resourceTypeRepository = $resource_type_repository;
+    $this->providerIds = array_keys($authentication_providers);
+    assert(is_string($jsonapi_base_path));
+    assert(
+      $jsonapi_base_path[0] === '/',
+      sprintf('The provided base path should contain a leading slash "/". Given: "%s".', $jsonapi_base_path)
+    );
+    assert(
+      substr($jsonapi_base_path, -1) !== '/',
+      sprintf('The provided base path should not contain a trailing slash "/". Given: "%s".', $jsonapi_base_path)
+    );
+    $this->jsonApiBasePath = $jsonapi_base_path;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('jsonapi.resource_type.repository'),
+      $container->getParameter('authentication_providers'),
+      $container->getParameter('jsonapi.base_path')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function routes() {
+    $routes = new RouteCollection();
+    $upload_routes = new RouteCollection();
+
+    // JSON:API's routes: entry point + routes for every resource type.
+    foreach ($this->resourceTypeRepository->all() as $resource_type) {
+      $routes->addCollection(static::getRoutesForResourceType($resource_type, $this->jsonApiBasePath));
+      $upload_routes->addCollection(static::getFileUploadRoutesForResourceType($resource_type, $this->jsonApiBasePath));
+    }
+    $routes->add('jsonapi.resource_list', static::getEntryPointRoute($this->jsonApiBasePath));
+
+    // Require the JSON:API media type header on every route, except on file
+    // upload routes, where we require `application/octet-stream`.
+    $routes->addRequirements(['_content_type_format' => 'api_json']);
+    $upload_routes->addRequirements(['_content_type_format' => 'bin']);
+
+    $routes->addCollection($upload_routes);
+
+    // Enable all available authentication providers.
+    $routes->addOptions(['_auth' => $this->providerIds]);
+
+    // Flag every route as belonging to the JSON:API module.
+    $routes->addDefaults([static::JSON_API_ROUTE_FLAG_KEY => TRUE]);
+
+    // All routes serve only the JSON:API media type.
+    $routes->addRequirements(['_format' => 'api_json']);
+
+    return $routes;
+  }
+
+  /**
+   * Gets applicable resource routes for a JSON:API resource type.
+   *
+   * @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type
+   *   The JSON:API resource type for which to get the routes.
+   * @param string $path_prefix
+   *   The root path prefix.
+   *
+   * @return \Symfony\Component\Routing\RouteCollection
+   *   A collection of routes for the given resource type.
+   */
+  protected static function getRoutesForResourceType(ResourceType $resource_type, $path_prefix) {
+    // Internal resources have no routes.
+    if ($resource_type->isInternal()) {
+      return new RouteCollection();
+    }
+
+    $routes = new RouteCollection();
+
+    // Collection route like `/jsonapi/node/article`.
+    if ($resource_type->isLocatable()) {
+      $collection_route = new Route("/{$resource_type->getPath()}");
+      $collection_route->addDefaults([RouteObjectInterface::CONTROLLER_NAME => static::CONTROLLER_SERVICE_NAME . ':getCollection']);
+      $collection_route->setMethods(['GET']);
+      // Allow anybody access because "view" and "view label" access are checked
+      // in the controller.
+      $collection_route->setRequirement('_access', 'TRUE');
+      $routes->add(static::getRouteName($resource_type, 'collection'), $collection_route);
+    }
+
+    // Creation route.
+    if ($resource_type->isMutable()) {
+      $collection_create_route = new Route("/{$resource_type->getPath()}");
+      $collection_create_route->addDefaults([RouteObjectInterface::CONTROLLER_NAME => static::CONTROLLER_SERVICE_NAME . ':createIndividual']);
+      $collection_create_route->setMethods(['POST']);
+      $create_requirement = sprintf("%s:%s", $resource_type->getEntityTypeId(), $resource_type->getBundle());
+      $collection_create_route->setRequirement('_entity_create_access', $create_requirement);
+      $collection_create_route->setRequirement('_csrf_request_header_token', 'TRUE');
+      $routes->add(static::getRouteName($resource_type, 'collection.post'), $collection_create_route);
+    }
+
+    // Individual routes like `/jsonapi/node/article/{uuid}` or
+    // `/jsonapi/node/article/{uuid}/relationships/uid`.
+    $routes->addCollection(static::getIndividualRoutesForResourceType($resource_type));
+
+    // Add the resource type as a parameter to every resource route.
+    foreach ($routes as $route) {
+      static::addRouteParameter($route, static::RESOURCE_TYPE_KEY, ['type' => ResourceTypeConverter::PARAM_TYPE_ID]);
+      $route->addDefaults([static::RESOURCE_TYPE_KEY => $resource_type->getTypeName()]);
+    }
+
+    // Resource routes all have the same base path.
+    $routes->addPrefix($path_prefix);
+
+    return $routes;
+  }
+
+  /**
+   * Gets the file upload route collection for the given resource type.
+   *
+   * @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type
+   *   The resource type for which the route collection should be created.
+   * @param string $path_prefix
+   *   The root path prefix.
+   *
+   * @return \Symfony\Component\Routing\RouteCollection
+   *   The route collection.
+   */
+  protected static function getFileUploadRoutesForResourceType(ResourceType $resource_type, $path_prefix) {
+    $routes = new RouteCollection();
+
+    // Internal resources have no routes; individual routes require locations.
+    if ($resource_type->isInternal() || !$resource_type->isLocatable()) {
+      return $routes;
+    }
+
+    // File upload routes are only necessary for resource types that have file
+    // fields.
+    $has_file_field = array_reduce($resource_type->getRelatableResourceTypes(), function ($carry, array $target_resource_types) {
+      return $carry || static::hasNonInternalFileTargetResourceTypes($target_resource_types);
+    }, FALSE);
+    if (!$has_file_field) {
+      return $routes;
+    }
+
+    if ($resource_type->isMutable()) {
+      $path = $resource_type->getPath();
+      $entity_type_id = $resource_type->getEntityTypeId();
+
+      $new_resource_file_upload_route = new Route("/{$path}/{file_field_name}");
+      $new_resource_file_upload_route->addDefaults([RouteObjectInterface::CONTROLLER_NAME => 'jsonapi.file_upload:handleFileUploadForNewResource']);
+      $new_resource_file_upload_route->setMethods(['POST']);
+      $new_resource_file_upload_route->setRequirement('_csrf_request_header_token', 'TRUE');
+      $routes->add(static::getFileUploadRouteName($resource_type, 'new_resource'), $new_resource_file_upload_route);
+
+      $existing_resource_file_upload_route = new Route("/{$path}/{entity}/{file_field_name}");
+      $existing_resource_file_upload_route->addDefaults([RouteObjectInterface::CONTROLLER_NAME => 'jsonapi.file_upload:handleFileUploadForExistingResource']);
+      $existing_resource_file_upload_route->setMethods(['POST']);
+      $existing_resource_file_upload_route->setRequirement('_csrf_request_header_token', 'TRUE');
+      $routes->add(static::getFileUploadRouteName($resource_type, 'existing_resource'), $existing_resource_file_upload_route);
+
+      // Add entity parameter conversion to every route.
+      $routes->addOptions(['parameters' => ['entity' => ['type' => 'entity:' . $entity_type_id]]]);
+
+      // Add the resource type as a parameter to every resource route.
+      foreach ($routes as $route) {
+        static::addRouteParameter($route, static::RESOURCE_TYPE_KEY, ['type' => ResourceTypeConverter::PARAM_TYPE_ID]);
+        $route->addDefaults([static::RESOURCE_TYPE_KEY => $resource_type->getTypeName()]);
+      }
+    }
+
+    // File upload routes all have the same base path.
+    $routes->addPrefix($path_prefix);
+
+    return $routes;
+  }
+
+  /**
+   * Determines if the given request is for a JSON:API generated route.
+   *
+   * @param array $defaults
+   *   The request's route defaults.
+   *
+   * @return bool
+   *   Whether the request targets a generated route.
+   */
+  public static function isJsonApiRequest(array $defaults) {
+    return isset($defaults[RouteObjectInterface::CONTROLLER_NAME])
+      && strpos($defaults[RouteObjectInterface::CONTROLLER_NAME], static::CONTROLLER_SERVICE_NAME) === 0;
+  }
+
+  /**
+   * Gets a route collection for the given resource type.
+   *
+   * @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type
+   *   The resource type for which the route collection should be created.
+   *
+   * @return \Symfony\Component\Routing\RouteCollection
+   *   The route collection.
+   */
+  protected static function getIndividualRoutesForResourceType(ResourceType $resource_type) {
+    if (!$resource_type->isLocatable()) {
+      return new RouteCollection();
+    }
+
+    $routes = new RouteCollection();
+
+    $path = $resource_type->getPath();
+    $entity_type_id = $resource_type->getEntityTypeId();
+
+    // Individual read, update and remove.
+    $individual_route = new Route("/{$path}/{entity}");
+    $individual_route->addDefaults([RouteObjectInterface::CONTROLLER_NAME => static::CONTROLLER_SERVICE_NAME . ':getIndividual']);
+    $individual_route->setMethods(['GET']);
+    // No _entity_access requirement because "view" and "view label" access are
+    // checked in the controller. So it's safe to allow anybody access.
+    $individual_route->setRequirement('_access', 'TRUE');
+    $routes->add(static::getRouteName($resource_type, 'individual'), $individual_route);
+    if ($resource_type->isMutable()) {
+      $individual_update_route = new Route($individual_route->getPath());
+      $individual_update_route->addDefaults([RouteObjectInterface::CONTROLLER_NAME => static::CONTROLLER_SERVICE_NAME . ':patchIndividual']);
+      $individual_update_route->setMethods(['PATCH']);
+      $individual_update_route->setRequirement('_entity_access', "entity.update");
+      $individual_update_route->setRequirement('_csrf_request_header_token', 'TRUE');
+      $routes->add(static::getRouteName($resource_type, 'individual.patch'), $individual_update_route);
+      $individual_remove_route = new Route($individual_route->getPath());
+      $individual_remove_route->addDefaults([RouteObjectInterface::CONTROLLER_NAME => static::CONTROLLER_SERVICE_NAME . ':deleteIndividual']);
+      $individual_remove_route->setMethods(['DELETE']);
+      $individual_remove_route->setRequirement('_entity_access', "entity.delete");
+      $individual_remove_route->setRequirement('_csrf_request_header_token', 'TRUE');
+      $routes->add(static::getRouteName($resource_type, 'individual.delete'), $individual_remove_route);
+    }
+
+    foreach ($resource_type->getRelatableResourceTypes() as $relationship_field_name => $target_resource_types) {
+      // Read, update, add, or remove an individual resources relationships to
+      // other resources.
+      $relationship_route = new Route("/{$path}/{entity}/relationships/{$relationship_field_name}");
+      $relationship_route->addDefaults(['_on_relationship' => TRUE]);
+      $relationship_route->addDefaults(['related' => $relationship_field_name]);
+      $relationship_route->setRequirement(RelationshipFieldAccess::ROUTE_REQUIREMENT_KEY, $relationship_field_name);
+      $relationship_route->setRequirement('_csrf_request_header_token', 'TRUE');
+      $relationship_route_methods = $resource_type->isMutable()
+        ? ['GET', 'POST', 'PATCH', 'DELETE']
+        : ['GET'];
+      $relationship_controller_methods = [
+        'GET' => 'getRelationship',
+        'POST' => 'addToRelationshipData',
+        'PATCH' => 'replaceRelationshipData',
+        'DELETE' => 'removeFromRelationshipData',
+      ];
+      foreach ($relationship_route_methods as $method) {
+        $method_specific_relationship_route = clone $relationship_route;
+        $method_specific_relationship_route->addDefaults([RouteObjectInterface::CONTROLLER_NAME => static::CONTROLLER_SERVICE_NAME . ":{$relationship_controller_methods[$method]}"]);
+        $method_specific_relationship_route->setMethods($method);
+        $routes->add(static::getRouteName($resource_type, sprintf("%s.relationship.%s", $relationship_field_name, strtolower($method))), $method_specific_relationship_route);
+      }
+
+      // Only create routes for related routes that target at least one
+      // non-internal resource type.
+      if (static::hasNonInternalTargetResourceTypes($target_resource_types)) {
+        // Get an individual resource's related resources.
+        $related_route = new Route("/{$path}/{entity}/{$relationship_field_name}");
+        $related_route->setMethods(['GET']);
+        $related_route->addDefaults([RouteObjectInterface::CONTROLLER_NAME => static::CONTROLLER_SERVICE_NAME . ':getRelated']);
+        $related_route->addDefaults(['related' => $relationship_field_name]);
+        $related_route->setRequirement(RelationshipFieldAccess::ROUTE_REQUIREMENT_KEY, $relationship_field_name);
+        $routes->add(static::getRouteName($resource_type, "$relationship_field_name.related"), $related_route);
+      }
+    }
+
+    // Add entity parameter conversion to every route.
+    $routes->addOptions(['parameters' => ['entity' => ['type' => 'entity:' . $entity_type_id]]]);
+
+    return $routes;
+  }
+
+  /**
+   * Provides the entry point route.
+   *
+   * @param string $path_prefix
+   *   The root path prefix.
+   *
+   * @return \Symfony\Component\Routing\Route
+   *   The entry point route.
+   */
+  protected function getEntryPointRoute($path_prefix) {
+    $entry_point = new Route("/{$path_prefix}");
+    $entry_point->addDefaults([RouteObjectInterface::CONTROLLER_NAME => EntryPoint::class . '::index']);
+    $entry_point->setRequirement('_access', 'TRUE');
+    $entry_point->setMethods(['GET']);
+    return $entry_point;
+  }
+
+  /**
+   * Adds a parameter option to a route, overrides options of the same name.
+   *
+   * The Symfony Route class only has a method for adding options which
+   * overrides any previous values. Therefore, it is tedious to add a single
+   * parameter while keeping those that are already set.
+   *
+   * @param \Symfony\Component\Routing\Route $route
+   *   The route to which the parameter is to be added.
+   * @param string $name
+   *   The name of the parameter.
+   * @param mixed $parameter
+   *   The parameter's options.
+   */
+  protected static function addRouteParameter(Route $route, $name, $parameter) {
+    $parameters = $route->getOption('parameters') ?: [];
+    $parameters[$name] = $parameter;
+    $route->setOption('parameters', $parameters);
+  }
+
+  /**
+   * Get a unique route name for the JSON:API resource type and route type.
+   *
+   * @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type
+   *   The resource type for which the route collection should be created.
+   * @param string $route_type
+   *   The route type. E.g. 'individual' or 'collection'.
+   *
+   * @return string
+   *   The generated route name.
+   */
+  public static function getRouteName(ResourceType $resource_type, $route_type) {
+    return sprintf('jsonapi.%s.%s', $resource_type->getTypeName(), $route_type);
+  }
+
+  /**
+   * Get a unique route name for the file upload resource type and route type.
+   *
+   * @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type
+   *   The resource type for which the route collection should be created.
+   * @param string $route_type
+   *   The route type. E.g. 'individual' or 'collection'.
+   *
+   * @return string
+   *   The generated route name.
+   */
+  protected static function getFileUploadRouteName(ResourceType $resource_type, $route_type) {
+    return sprintf('jsonapi.%s.%s.%s', $resource_type->getTypeName(), 'file_upload', $route_type);
+  }
+
+  /**
+   * Determines if an array of resource types has any non-internal ones.
+   *
+   * @param \Drupal\jsonapi\ResourceType\ResourceType[] $resource_types
+   *   The resource types to check.
+   *
+   * @return bool
+   *   TRUE if there is at least one non-internal resource type in the given
+   *   array; FALSE otherwise.
+   */
+  protected static function hasNonInternalTargetResourceTypes(array $resource_types) {
+    return array_reduce($resource_types, function ($carry, ResourceType $target) {
+      return $carry || !$target->isInternal();
+    }, FALSE);
+  }
+
+  /**
+   * Determines if an array of resource types lists non-internal "file" ones.
+   *
+   * @param \Drupal\jsonapi\ResourceType\ResourceType[] $resource_types
+   *   The resource types to check.
+   *
+   * @return bool
+   *   TRUE if there is at least one non-internal "file" resource type in the
+   *   given array; FALSE otherwise.
+   */
+  protected static function hasNonInternalFileTargetResourceTypes(array $resource_types) {
+    return array_reduce($resource_types, function ($carry, ResourceType $target) {
+      return $carry || (!$target->isInternal() && $target->getEntityTypeId() === 'file');
+    }, FALSE);
+  }
+
+  /**
+   * Gets the resource type from a route or request's parameters.
+   *
+   * @param array $parameters
+   *   An array of parameters. These may be obtained from a route's
+   *   parameter defaults or from a request object.
+   *
+   * @return \Drupal\jsonapi\ResourceType\ResourceType|null
+   *   The resource type, NULL if one cannot be found from the given parameters.
+   */
+  public static function getResourceTypeNameFromParameters(array $parameters) {
+    if (isset($parameters[static::JSON_API_ROUTE_FLAG_KEY]) && $parameters[static::JSON_API_ROUTE_FLAG_KEY]) {
+      return isset($parameters[static::RESOURCE_TYPE_KEY]) ? $parameters[static::RESOURCE_TYPE_KEY] : NULL;
+    }
+    return NULL;
+  }
+
+  /**
+   * Invalidates any JSON:API resource type dependent responses and routes.
+   */
+  public static function rebuild() {
+    \Drupal::service('cache_tags.invalidator')->invalidateTags(['jsonapi_resource_types']);
+    \Drupal::service('router.builder')->setRebuildNeeded();
+  }
+
+}