jsonapi.services.yml | 13 ++ .../jsonapi_file_upload.info.yml | 8 - .../jsonapi_file_upload.routing.yml | 2 - .../jsonapi_file_upload.services.yml | 14 -- .../src/JsonapiFileUploadServiceProvider.php | 25 --- modules/jsonapi_file_upload/src/Routing/Routes.php | 177 --------------------- .../Controller/FileUpload.php | 14 +- .../ForwardCompatibility}/FileUploader.php | 2 +- .../FileUploaderInterface.php | 2 +- src/JsonapiServiceProvider.php | 10 ++ src/Routing/Routes.php | 83 +++++++++- .../src/Functional/FileUploadTest.php | 6 +- 12 files changed, 115 insertions(+), 241 deletions(-) diff --git a/jsonapi.services.yml b/jsonapi.services.yml index b21b4ff..1eee76d 100644 --- a/jsonapi.services.yml +++ b/jsonapi.services.yml @@ -152,6 +152,14 @@ services: - '@renderer' - '@entity.repository' - '@jsonapi.include_resolver' + jsonapi.file_upload: + class: Drupal\jsonapi\Controller\FileUpload + arguments: + - '@current_user' + - '@entity_field.manager' + - '@file.uploader' + - '@http_kernel' + - '@jsonapi.link_manager' # Event subscribers. jsonapi.custom_query_parameter_names_validator.subscriber: @@ -189,3 +197,8 @@ services: tags: # Priority must be higher than serializer.normalizer.primitive_data. - { name: normalizer, priority: 20 } + # @todo Remove in Drupal 8.7 (assuming it contains https://www.drupal.org/project/drupal/issues/2940383) + file.uploader: + class: Drupal\jsonapi\ForwardCompatibility\FileUploader + public: false + arguments: ['@logger.channel.file', '@file_system', '@file.mime_type.guesser', '@token', '@lock', '@config.factory'] diff --git a/modules/jsonapi_file_upload/jsonapi_file_upload.info.yml b/modules/jsonapi_file_upload/jsonapi_file_upload.info.yml deleted file mode 100644 index 1088974..0000000 --- a/modules/jsonapi_file_upload/jsonapi_file_upload.info.yml +++ /dev/null @@ -1,8 +0,0 @@ -name: JSON API File Upload -type: module -description: Provides JSON API file upload support. -core: 8.x -package: Web services -dependencies: - - drupal:system (>=8.6.0) - - jsonapi diff --git a/modules/jsonapi_file_upload/jsonapi_file_upload.routing.yml b/modules/jsonapi_file_upload/jsonapi_file_upload.routing.yml deleted file mode 100644 index c82aa0d..0000000 --- a/modules/jsonapi_file_upload/jsonapi_file_upload.routing.yml +++ /dev/null @@ -1,2 +0,0 @@ -route_callbacks: - - '\Drupal\jsonapi_file_upload\Routing\Routes::routes' diff --git a/modules/jsonapi_file_upload/jsonapi_file_upload.services.yml b/modules/jsonapi_file_upload/jsonapi_file_upload.services.yml deleted file mode 100644 index 23742df..0000000 --- a/modules/jsonapi_file_upload/jsonapi_file_upload.services.yml +++ /dev/null @@ -1,14 +0,0 @@ -services: - jsonapi_file_upload.file_uploader: - class: Drupal\jsonapi_file_upload\Shim\FileUploader - public: false - arguments: ['@logger.channel.jsonapi_file_upload', '@file_system', '@file.mime_type.guesser', '@token', '@lock', '@config.factory'] - - logger.channel.jsonapi_file_upload: - parent: logger.channel_base - arguments: ['jsonapi_file_upload'] - - # Controllers. - jsonapi_file_upload.request_handler: - class: Drupal\jsonapi_file_upload\Controller\RequestHandler - arguments: ['@current_user', '@entity_field.manager', '@jsonapi_file_upload.file_uploader', '@http_kernel', '@jsonapi.link_manager'] diff --git a/modules/jsonapi_file_upload/src/JsonapiFileUploadServiceProvider.php b/modules/jsonapi_file_upload/src/JsonapiFileUploadServiceProvider.php deleted file mode 100644 index a2b65c2..0000000 --- a/modules/jsonapi_file_upload/src/JsonapiFileUploadServiceProvider.php +++ /dev/null @@ -1,25 +0,0 @@ -has('http_middleware.negotiation') && is_a($container->getDefinition('http_middleware.negotiation')->getClass(), NegotiationMiddleware::class, TRUE)) { - $container->getDefinition('http_middleware.negotiation')->addMethodCall('registerFormat', ['bin', ['application/octet-stream']]); - } - } - -} diff --git a/modules/jsonapi_file_upload/src/Routing/Routes.php b/modules/jsonapi_file_upload/src/Routing/Routes.php deleted file mode 100644 index df82193..0000000 --- a/modules/jsonapi_file_upload/src/Routing/Routes.php +++ /dev/null @@ -1,177 +0,0 @@ -resourceTypeRepository = $resource_type_repository; - $this->providerIds = array_keys($authentication_providers); - $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(); - - foreach ($this->resourceTypeRepository->all() as $resource_type) { - $routes->addCollection($this->getFileUploadRoutesForResourceType($resource_type)); - } - - // Enable all available authentication providers. - $routes->addOptions(['_auth' => $this->providerIds]); - - // Flag every route as belonging to the JSON API module. - $routes->addDefaults([JsonApiRoutes::JSON_API_ROUTE_FLAG_KEY => TRUE]); - - // Only accept 'Content-Type: application/octet-stream' requests. - $routes->addRequirements(['_content_type_format' => 'bin']); - - // Resource routes all have the same controller. - $routes->addPrefix($this->jsonApiBasePath); - - 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. - * - * @return \Symfony\Component\Routing\RouteCollection - * The route collection. - */ - protected function getFileUploadRoutesForResourceType(ResourceType $resource_type) { - $routes = new RouteCollection(); - - // Internal resources have no routes; individual routes require locations. - if ($resource_type->isInternal() || !$resource_type->isLocatable()) { - 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.request_handler: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.request_handler: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, JsonApiRoutes::RESOURCE_TYPE_KEY, ['type' => ResourceTypeConverter::PARAM_TYPE_ID]); - $route->addDefaults([JsonApiRoutes::RESOURCE_TYPE_KEY => $resource_type->getTypeName()]); - } - - } - - return $routes; - } - - /** - * 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 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); - } - -} diff --git a/modules/jsonapi_file_upload/src/Controller/RequestHandler.php b/src/Controller/FileUpload.php similarity index 96% rename from modules/jsonapi_file_upload/src/Controller/RequestHandler.php rename to src/Controller/FileUpload.php index b531ce4..b173622 100644 --- a/modules/jsonapi_file_upload/src/Controller/RequestHandler.php +++ b/src/Controller/FileUpload.php @@ -1,6 +1,6 @@ addMethodCall('registerFormat', [ 'api_json', ['application/vnd.api+json'], + ]) + ->addMethodCall('registerFormat', [ + 'bin', + ['application/octet-stream'], ]); } + + // @todo Remove this when JSON API requires Drupal >=8.6, see https://www.drupal.org/node/1927648. + if (!defined('FILE_INSECURE_EXTENSION_REGEX')) { + $container->removeDefinition('jsonapi.file_upload'); + $container->removeDefinition('file.uploader'); + } } /** diff --git a/src/Routing/Routes.php b/src/Routing/Routes.php index 7966fba..f38af6e 100644 --- a/src/Routing/Routes.php +++ b/src/Routing/Routes.php @@ -107,13 +107,22 @@ class Routes implements ContainerInjectionInterface { */ 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]); @@ -123,9 +132,6 @@ class Routes implements ContainerInjectionInterface { // All routes serve only the JSON API media type. $routes->addRequirements(['_format' => 'api_json']); - // Require the JSON API media type header on every route. - $routes->addRequirements(['_content_type_format' => 'api_json']); - return $routes; } @@ -188,6 +194,62 @@ class Routes implements ContainerInjectionInterface { } /** + * 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(); + + // @todo Remove this when JSON API requires Drupal >=8.6, see https://www.drupal.org/node/1927648. + if (!defined('FILE_INSECURE_EXTENSION_REGEX')) { + return $routes; + } + + // Internal resources have no routes; individual routes require locations. + if ($resource_type->isInternal() || !$resource_type->isLocatable()) { + 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 @@ -341,6 +403,21 @@ class Routes implements ContainerInjectionInterface { } /** + * 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 diff --git a/modules/jsonapi_file_upload/tests/src/Functional/FileUploadTest.php b/tests/src/Functional/FileUploadTest.php similarity index 99% rename from modules/jsonapi_file_upload/tests/src/Functional/FileUploadTest.php rename to tests/src/Functional/FileUploadTest.php index 86009d9..553eb98 100644 --- a/modules/jsonapi_file_upload/tests/src/Functional/FileUploadTest.php +++ b/tests/src/Functional/FileUploadTest.php @@ -1,6 +1,6 @@