diff --git a/core/modules/file/file.permissions.yml b/core/modules/file/file.permissions.yml index 4491590d7b..8575f20806 100644 --- a/core/modules/file/file.permissions.yml +++ b/core/modules/file/file.permissions.yml @@ -1,4 +1,2 @@ access files overview: title: 'Access the Files overview page' -access file upload endpoint: - title: 'Access the file upload endpoint' diff --git a/core/modules/file/file.routing.yml b/core/modules/file/file.routing.yml index 46c5db28f7..e182c97ee1 100644 --- a/core/modules/file/file.routing.yml +++ b/core/modules/file/file.routing.yml @@ -4,24 +4,3 @@ file.ajax_progress: _controller: '\Drupal\file\Controller\FileWidgetAjaxController::progress' requirements: _permission: 'access content' -file.upload_update: - path: '/file/upload/{file}' - methods: [POST] - defaults: - _controller: '\Drupal\file\Controller\FileUploadController::updateFile' - requirements: - _permission: 'access file upload endpoint' - options: - parameters: - file: - type: entity:file - _auth: [ 'basic_auth' ] -file.upload_create: - path: '/file/upload' - methods: [POST] - defaults: - _controller: '\Drupal\file\Controller\FileUploadController::newFile' - requirements: - _permission: 'access file upload endpoint' - options: - _auth: [ 'basic_auth' ] \ No newline at end of file diff --git a/core/modules/file/src/Controller/FileUploadController.php b/core/modules/file/src/Controller/FileUploadController.php deleted file mode 100644 index 0582ed340e..0000000000 --- a/core/modules/file/src/Controller/FileUploadController.php +++ /dev/null @@ -1,148 +0,0 @@ -fileSystem = $file_system; - $this->serializer = $serializer; - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container) { - return new static( - $container->get('file_system'), - $container->get('serializer') - ); - } - - - protected function validateOctetStream(Request $request) { - if ($request->headers->get('Content-Type') !== 'application/octet-stream') { - throw new HttpException(415, 'The "application/octet-stream" content type must be used to send binary file data'); - } - } - - /** - * Creates a file from endpoint. - * - * @param \Symfony\Component\HttpFoundation\Request $request - * @param \Drupal\file\FileInterface $file - */ - public function newFile(Request $request) { - $this->validateOctetStream($request); - - // Grab the filename as described in Content-Disposition header. - $content_disposition = $request->headers->get('Content-Disposition'); - $filename = uniqid(); - // @todo find a better way to do this. - preg_match('/filename=\"(.*?)\"/', $content_disposition, $matches); - if ($matches) { - $filename = $matches[1]; - } - - // Check the destination file path is writable. - $temporary_uri = 'temporary://'; - if (!is_writable($this->fileSystem->realpath($temporary_uri))) { - throw new HttpException(500, 'Destination file path is not writable'); - } - - // Create the file. - $file_uri = "{$temporary_uri}{$filename}"; - $file = File::create([ - 'uri' => $file_uri, - ]); - $this->streamUploadData($file_uri); - $file->save(); - - // @todo: This adds a dependency to Serialization module. - return new Response($this->serializer->serialize($file, 'json')); - } - - /** - * Handles binary file uploads. - * - * @param \Symfony\Component\HttpFoundation\Request $request - * @param \Drupal\file\FileInterface $file - */ - public function updateFile(Request $request, FileInterface $file) { - $this->validateOctetStream($request); - - // Check the destination file path is writable. - if (!is_writable($this->fileSystem->realpath($file->getFileUri()))) { - throw new HttpException(500, 'Destination file path is not writable'); - } - - $this->streamUploadData($file->getFileUri()); - - // Save the file so file size is re-calculated. - $file->save(); - - // @todo: This adds a dependency to Serialization module. - return new Response($this->serializer->serialize($file, 'json')); - } - - /** - * Streams file upload data to temporary file and moves to file destination. - * - * @param \Drupal\file\FileInterface $file - * - * @throws \Symfony\Component\HttpKernel\Exception\HttpException - */ - protected function streamUploadData($uri) { - // 'rb' is needed so reading works correctly on windows environments too. - $file_data = fopen('php://input','rb'); - - $temp_file_name = $this->fileSystem->tempnam('temporary://', 'file'); - - if ($temp_file = fopen($temp_file_name, 'wb')) { - while (!feof($file_data)) { - fwrite($temp_file, fread($file_data, 8192)); - } - - fclose($temp_file); - - // Move the file to the correct location based on the file entity, - // replacing any existing file. - if (!file_unmanaged_move($temp_file_name, $uri, FILE_EXISTS_REPLACE)) { - throw new HttpException(500, 'Temporary file could not be moved to file location'); - } - } - else { - $this->getLogger('file system')->error('Temporary file "%path" could not be opened for file upload', ['%path' => $temp_file_name]); - throw new HttpException(500, 'Temporary file could not be opened'); - } - - fclose($file_data); - } - -} diff --git a/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php new file mode 100644 index 0000000000..a3d3e18be4 --- /dev/null +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php @@ -0,0 +1,226 @@ +fileSystem = $file_system; + $this->entityFieldManager = $entity_field_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->getParameter('serializer.formats'), + $container->get('logger.factory')->get('rest'), + $container->get('file_system'), + $container->get('entity_field.manager') + ); + } + + /** + * {@inheritdoc} + */ + public function calculateDependencies() { + return [ + 'module' => ['file'] + ]; + } + + /** + * Creates a file from endpoint. + * + * @param \Symfony\Component\HttpFoundation\Request $request + * @param \Drupal\file\FileInterface $file + */ + public function post(Request $request, $entity_type_id, $bundle, $field_name) { + $this->validateOctetStream($request); + // @todo Validate for file name too. + + $field_definitions = $this->entityFieldManager->getFieldDefinitions($entity_type_id, $bundle); + + if (!isset($field_definitions[$field_name])) { + throw new BadRequestHttpException(sprintf('Field "%s" does not exist', $field_name)); + } + + // @todo check the definition is a file field. + $field_definition = $field_definitions[$field_name]; + + // Check access. + if (!$field_definition->access('create')) { + throw new AccessDeniedException(sprintf('Access denied for field "%s"', $field_name)); + } + + $destination = $this->getUploadLocation($field_definition->getFieldStorageDefinition()->getSettings()); + + // Grab the filename as described in Content-Disposition header. + $content_disposition = $request->headers->get('Content-Disposition'); + $filename = uniqid(); + // @todo find a better way to do this. + preg_match('/filename=\"(.*?)\"/', $content_disposition, $matches); + if ($matches) { + $filename = $matches[1]; + } + + // Check the destination file path is writable. + if (!is_writable($this->fileSystem->realpath($destination))) { + throw new HttpException(500, 'Destination file path is not writable'); + } + + // Create the file. + $file_uri = "{$destination}/{$filename}"; + + $file = File::create([ + 'uri' => $file_uri, + ]); + + $this->streamUploadData($file_uri); + + // @todo Also validate based on the above field definition. + $this->validate($file); + + $file->save(); + + return $file; + } + + /** + * Streams file upload data to temporary file and moves to file destination. + * + * @param string $destination_uri + * + * @throws \Symfony\Component\HttpKernel\Exception\HttpException + */ + protected function streamUploadData($destination_uri) { + // 'rb' is needed so reading works correctly on windows environments too. + $file_data = fopen('php://input','rb'); + + $temp_file_name = $this->fileSystem->tempnam('temporary://', 'file'); + + if ($temp_file = fopen($temp_file_name, 'wb')) { + while (!feof($file_data)) { + fwrite($temp_file, fread($file_data, 8192)); + } + + fclose($temp_file); + + // Move the file to the correct location based on the file entity, + // replacing any existing file. + if (!file_unmanaged_move($temp_file_name, $uri, FILE_EXISTS_REPLACE)) { + throw new HttpException(500, 'Temporary file could not be moved to file location'); + } + } + else { + $this->getLogger('file system')->error('Temporary file "%path" could not be opened for file upload', ['%path' => $temp_file_name]); + throw new HttpException(500, 'Temporary file could not be opened'); + } + + fclose($file_data); + } + + /** + * Validates the Content-Type header for the request. + * + * @param \Symfony\Component\HttpKernel\Request $request + * The request to validate. + * + * @throws \Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException + */ + protected function validateOctetStream(Request $request) { + if ($request->headers->get('Content-Type') !== 'application/octet-stream') { + throw new UnsupportedMediaTypeHttpException('The "application/octet-stream" content type must be used to send binary file data'); + } + } + + /** + * Determines the URI for a file field. + * + * @param array $settings + * The array of field settings. + * + * @return string + * An unsanitized file directory URI with tokens replaced. The result of + * the token replacement is then converted to plain text and returned. + */ + protected function getUploadLocation(array $settings) { + $destination = trim($settings['file_directory'], '/'); + + // Replace tokens. As the tokens might contain HTML we convert it to plain + // text. + $destination = PlainTextOutput::renderFromHtml(\Drupal::token()->replace($destination, $data)); + return $settings['uri_scheme'] . '://' . $destination; + } + +}