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