Skip to content
Snippets Groups Projects
Verified Commit 4d6f38f6 authored by Lee Rowlands's avatar Lee Rowlands
Browse files

Issue #3380345 by kim.pepper, smustgrave, quietone, larowlan: Create a...

Issue #3380345 by kim.pepper, smustgrave, quietone, larowlan: Create a InputStreamFileWriter for writing the input stream to a file
parent 8a739dc5
Branches calendar_sub
No related tags found
38 merge requests!8528Issue #3456871 by Tim Bozeman: Support NULL services,!8323Fix source code editing and in place front page site studio editing.,!6278Issue #3187770 by godotislate, smustgrave, catch, quietone: Views Rendered...,!54479.5.x SF update,!3878Removed unused condition head title for views,!38582585169-10.1.x,!3818Issue #2140179: $entity->original gets stale between updates,!3742Issue #3328429: Create item list field formatter for displaying ordered and unordered lists,!3731Claro: role=button on status report items,!3668Resolve #3347842 "Deprecate the trusted",!3651Issue #3347736: Create new SDC component for Olivero (header-search),!3546refactored dialog.pcss file,!3531Issue #3336994: StringFormatter always displays links to entity even if the user in context does not have access,!3502Issue #3335308: Confusing behavior with FormState::setFormState and FormState::setMethod,!3452Issue #3332701: Refactor Claro's tablesort-indicator stylesheet,!3355Issue #3209129: Scrolling problems when adding a block via layout builder,!3226Issue #2987537: Custom menu link entity type should not declare "bundle" entity key,!3154Fixes #2987987 - CSRF token validation broken on routes with optional parameters.,!3147Issue #3328457: Replace most substr($a, $i) where $i is negative with str_ends_with(),!3146Issue #3328456: Replace substr($a, 0, $i) with str_starts_with(),!3133core/modules/system/css/components/hidden.module.css,!2964Issue #2865710 : Dependencies from only one instance of a widget are used in display modes,!2812Issue #3312049: [Followup] Fix Drupal.Commenting.FunctionComment.MissingReturnType returns for NULL,!2614Issue #2981326: Replace non-test usages of \Drupal::logger() with IoC injection,!2378Issue #2875033: Optimize joins and table selection in SQL entity query implementation,!2334Issue #3228209: Add hasRole() method to AccountInterface,!2062Issue #3246454: Add weekly granularity to views date sort,!1255Issue #3238922: Refactor (if feasible) uses of the jQuery serialize function to use vanillaJS,!1105Issue #3025039: New non translatable field on translatable content throws error,!1073issue #3191727: Focus states on mobile second level navigation items fixed,!10223132456: Fix issue where views instances are emptied before an ajax request is complete,!877Issue #2708101: Default value for link text is not saved,!844Resolve #3036010 "Updaters",!673Issue #3214208: FinishResponseSubscriber could create duplicate headers,!617Issue #3043725: Provide a Entity Handler for user cancelation,!579Issue #2230909: Simple decimals fail to pass validation,!560Move callback classRemove outside of the loop,!555Issue #3202493
Pipeline #48465 failed
Pipeline: drupal

#48470

    Pipeline: drupal

    #48469

      Pipeline: drupal

      #48468

        +1
        ...@@ -32,3 +32,7 @@ services: ...@@ -32,3 +32,7 @@ services:
        class: Drupal\file\Validation\UploadedFileValidator class: Drupal\file\Validation\UploadedFileValidator
        arguments: ['@validation.basic_recursive_validator_factory'] arguments: ['@validation.basic_recursive_validator_factory']
        Drupal\file\Validation\UploadedFileValidatorInterface: '@file.uploaded_file_validator' Drupal\file\Validation\UploadedFileValidatorInterface: '@file.uploaded_file_validator'
        file.input_stream_file_writer:
        class: Drupal\file\Upload\InputStreamFileWriter
        arguments: ['@file_system']
        Drupal\file\Upload\InputStreamFileWriterInterface: '@file.input_stream_file_writer'
        ...@@ -17,6 +17,7 @@ ...@@ -17,6 +17,7 @@
        use Drupal\Core\Session\AccountInterface; use Drupal\Core\Session\AccountInterface;
        use Drupal\Core\Utility\Token; use Drupal\Core\Utility\Token;
        use Drupal\file\Entity\File; use Drupal\file\Entity\File;
        use Drupal\file\Upload\InputStreamFileWriterInterface;
        use Drupal\file\Validation\FileValidatorInterface; use Drupal\file\Validation\FileValidatorInterface;
        use Drupal\rest\ModifiedResourceResponse; use Drupal\rest\ModifiedResourceResponse;
        use Drupal\rest\Plugin\ResourceBase; use Drupal\rest\Plugin\ResourceBase;
        ...@@ -24,6 +25,9 @@ ...@@ -24,6 +25,9 @@
        use Drupal\rest\RequestHandler; use Drupal\rest\RequestHandler;
        use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
        use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\ContainerInterface;
        use Symfony\Component\HttpFoundation\File\Exception\CannotWriteFileException;
        use Symfony\Component\HttpFoundation\File\Exception\NoFileException;
        use Symfony\Component\HttpFoundation\File\Exception\UploadException;
        use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
        use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
        use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
        ...@@ -141,6 +145,11 @@ class FileUploadResource extends ResourceBase { ...@@ -141,6 +145,11 @@ class FileUploadResource extends ResourceBase {
        */ */
        protected FileValidatorInterface $fileValidator; protected FileValidatorInterface $fileValidator;
        /**
        * The input stream file writer.
        */
        protected InputStreamFileWriterInterface $inputStreamFileWriter;
        /** /**
        * Constructs a FileUploadResource instance. * Constructs a FileUploadResource instance.
        * *
        ...@@ -174,8 +183,10 @@ class FileUploadResource extends ResourceBase { ...@@ -174,8 +183,10 @@ class FileUploadResource extends ResourceBase {
        * The event dispatcher service. * The event dispatcher service.
        * @param \Drupal\file\Validation\FileValidatorInterface|null $file_validator * @param \Drupal\file\Validation\FileValidatorInterface|null $file_validator
        * The file validator service. * The file validator service.
        * @param \Drupal\file\Upload\InputStreamFileWriterInterface|null $input_stream_file_writer
        * The input stream file writer.
        */ */
        public function __construct(array $configuration, $plugin_id, $plugin_definition, $serializer_formats, LoggerInterface $logger, FileSystemInterface $file_system, EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager, AccountInterface $current_user, $mime_type_guesser, Token $token, LockBackendInterface $lock, Config $system_file_config, EventDispatcherInterface $event_dispatcher, FileValidatorInterface $file_validator = NULL) { public function __construct(array $configuration, $plugin_id, $plugin_definition, $serializer_formats, LoggerInterface $logger, FileSystemInterface $file_system, EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager, AccountInterface $current_user, $mime_type_guesser, Token $token, LockBackendInterface $lock, Config $system_file_config, EventDispatcherInterface $event_dispatcher, FileValidatorInterface $file_validator = NULL, InputStreamFileWriterInterface $input_stream_file_writer = NULL) {
        parent::__construct($configuration, $plugin_id, $plugin_definition, $serializer_formats, $logger); parent::__construct($configuration, $plugin_id, $plugin_definition, $serializer_formats, $logger);
        $this->fileSystem = $file_system; $this->fileSystem = $file_system;
        $this->entityTypeManager = $entity_type_manager; $this->entityTypeManager = $entity_type_manager;
        ...@@ -191,6 +202,11 @@ public function __construct(array $configuration, $plugin_id, $plugin_definition ...@@ -191,6 +202,11 @@ public function __construct(array $configuration, $plugin_id, $plugin_definition
        $file_validator = \Drupal::service('file.validator'); $file_validator = \Drupal::service('file.validator');
        } }
        $this->fileValidator = $file_validator; $this->fileValidator = $file_validator;
        if (!$input_stream_file_writer) {
        @trigger_error('Calling ' . __METHOD__ . '() without the $input_stream_file_writer argument is deprecated in drupal:10.3.0 and is required in drupal:11.0.0. See https://www.drupal.org/node/123', E_USER_DEPRECATED);
        $input_stream_file_writer = \Drupal::service('file.input_stream_file_writer');
        }
        $this->inputStreamFileWriter = $input_stream_file_writer;
        } }
        /** /**
        ...@@ -212,7 +228,8 @@ public static function create(ContainerInterface $container, array $configuratio ...@@ -212,7 +228,8 @@ public static function create(ContainerInterface $container, array $configuratio
        $container->get('lock'), $container->get('lock'),
        $container->get('config.factory')->get('system.file'), $container->get('config.factory')->get('system.file'),
        $container->get('event_dispatcher'), $container->get('event_dispatcher'),
        $container->get('file.validator') $container->get('file.validator'),
        $container->get('file.input_stream_file_writer')
        ); );
        } }
        ...@@ -341,48 +358,23 @@ public function post(Request $request, $entity_type_id, $bundle, $field_name) { ...@@ -341,48 +358,23 @@ public function post(Request $request, $entity_type_id, $bundle, $field_name) {
        * Thrown when input data cannot be read, the temporary file cannot be * Thrown when input data cannot be read, the temporary file cannot be
        * opened, or the temporary file cannot be written. * opened, or the temporary file cannot be written.
        */ */
        protected function streamUploadData() { protected function streamUploadData(): string {
        // 'rb' is needed so reading works correctly on Windows environments too. // Catch and throw the exceptions that REST expects.
        $file_data = fopen('php://input', 'rb'); try {
        $temp_file_path = $this->inputStreamFileWriter->writeStreamToFile();
        $temp_file_path = $this->fileSystem->tempnam('temporary://', 'file');
        $temp_file = fopen($temp_file_path, 'wb');
        if ($temp_file) {
        while (!feof($file_data)) {
        $read = fread($file_data, static::BYTES_TO_READ);
        if ($read === FALSE) {
        // Close the file streams.
        fclose($temp_file);
        fclose($file_data);
        $this->logger->error('Input data could not be read');
        throw new HttpException(500, 'Input file data could not be read');
        }
        if (fwrite($temp_file, $read) === FALSE) {
        // Close the file streams.
        fclose($temp_file);
        fclose($file_data);
        $this->logger->error('Temporary file data for "%path" could not be written', ['%path' => $temp_file_path]);
        throw new HttpException(500, 'Temporary file data could not be written');
        }
        }
        // Close the temp file stream.
        fclose($temp_file);
        } }
        else { catch (UploadException $e) {
        // Close the input file stream since we can't proceed with the upload. $this->logger->error('Input data could not be read');
        // Don't try to close $temp_file since it's FALSE at this point. throw new HttpException(500, 'Input file data could not be read', $e);
        fclose($file_data); }
        $this->logger->error('Temporary file "%path" could not be opened for file upload', ['%path' => $temp_file_path]); catch (CannotWriteFileException $e) {
        throw new HttpException(500, 'Temporary file could not be opened'); $this->logger->error('Temporary file data for could not be written');
        throw new HttpException(500, 'Temporary file data could not be written', $e);
        }
        catch (NoFileException $e) {
        $this->logger->error('Temporary file could not be opened for file upload');
        throw new HttpException(500, 'Temporary file could not be opened', $e);
        } }
        // Close the input stream.
        fclose($file_data);
        return $temp_file_path; return $temp_file_path;
        } }
        ......
        <?php
        namespace Drupal\file\Upload;
        use Drupal\Core\File\FileSystemInterface;
        use Symfony\Component\HttpFoundation\File\Exception\CannotWriteFileException;
        use Symfony\Component\HttpFoundation\File\Exception\NoFileException;
        use Symfony\Component\HttpFoundation\File\Exception\UploadException;
        /**
        * Writes files from a input stream to a temporary file.
        */
        class InputStreamFileWriter implements InputStreamFileWriterInterface {
        /**
        * Creates a new InputStreamFileUploader.
        */
        public function __construct(
        protected FileSystemInterface $fileSystem,
        ) {}
        /**
        * {@inheritdoc}
        */
        public function writeStreamToFile(string $stream = self::DEFAULT_STREAM, int $bytesToRead = self::DEFAULT_BYTES_TO_READ): string {
        // 'rb' is needed so reading works correctly on Windows environments too.
        $fileData = fopen($stream, 'rb');
        $tempFilePath = $this->fileSystem->tempnam('temporary://', 'file');
        $tempFile = fopen($tempFilePath, 'wb');
        if ($tempFile) {
        while (!feof($fileData)) {
        $read = fread($fileData, $bytesToRead);
        if ($read === FALSE) {
        // Close the file streams.
        fclose($tempFile);
        fclose($fileData);
        throw new UploadException('Input file data could not be read');
        }
        if (fwrite($tempFile, $read) === FALSE) {
        // Close the file streams.
        fclose($tempFile);
        fclose($fileData);
        throw new CannotWriteFileException(sprintf('Temporary file data for "%s" could not be written', $tempFilePath));
        }
        }
        // Close the temp file stream.
        fclose($tempFile);
        }
        else {
        // Close the input file stream since we can't proceed with the upload.
        // Don't try to close $tempFile since it's FALSE at this point.
        fclose($fileData);
        throw new NoFileException(sprintf('Temporary file "%s" could not be opened for file upload', $tempFilePath));
        }
        // Close the input stream.
        fclose($fileData);
        return $tempFilePath;
        }
        }
        <?php
        namespace Drupal\file\Upload;
        /**
        * Uploads files from a stream.
        */
        interface InputStreamFileWriterInterface {
        /**
        * The length of bytes to read in each iteration when streaming file data.
        */
        const DEFAULT_BYTES_TO_READ = 8192;
        /**
        * The default stream.
        */
        const DEFAULT_STREAM = "php://input";
        /**
        * Write the input stream to a temporary file.
        *
        * @param string $stream
        * (optional) The input stream.
        * @param int $bytesToRead
        * (optional) The length of bytes to read in each iteration.
        *
        * @return string
        * The temporary file path.
        */
        public function writeStreamToFile(string $stream = self::DEFAULT_STREAM, int $bytesToRead = self::DEFAULT_BYTES_TO_READ): string;
        }
        <?php
        declare(strict_types=1);
        namespace Drupal\Tests\file\Kernel\Upload;
        use Drupal\KernelTests\KernelTestBase;
        use org\bovigo\vfs\vfsStream;
        /**
        * Tests the stream file uploader.
        *
        * @group file
        * @coversDefaultClass \Drupal\file\Upload\InputStreamFileWriter
        */
        class StreamFileUploaderTest extends KernelTestBase {
        /**
        * {@inheritdoc}
        */
        protected static $modules = ['file'];
        /**
        * @covers ::writeStreamToFile
        */
        public function testWriteStreamToFileSuccess(): void {
        vfsStream::newFile('foo.txt')
        ->at($this->vfsRoot)
        ->withContent('bar');
        $fileWriter = $this->container->get('file.input_stream_file_writer');
        $filename = $fileWriter->writeStreamToFile(vfsStream::url('root/foo.txt'));
        $this->assertStringStartsWith('temporary://', $filename);
        $this->assertStringEqualsFile($filename, 'bar');
        }
        /**
        * @covers ::writeStreamToFile
        */
        public function testWriteStreamToFileWithSmallerBytes(): void {
        $content = $this->randomString(2048);
        vfsStream::newFile('foo.txt')
        ->at($this->vfsRoot)
        ->withContent($content);
        $fileWriter = $this->container->get('file.input_stream_file_writer');
        $filename = $fileWriter->writeStreamToFile(
        stream: vfsStream::url('root/foo.txt'),
        bytesToRead: 1024,
        );
        $this->assertStringStartsWith('temporary://', $filename);
        $this->assertStringEqualsFile($filename, $content);
        }
        }
        ...@@ -261,5 +261,5 @@ services: ...@@ -261,5 +261,5 @@ services:
        jsonapi.file.uploader.field: jsonapi.file.uploader.field:
        class: Drupal\jsonapi\Controller\TemporaryJsonapiFileFieldUploader class: Drupal\jsonapi\Controller\TemporaryJsonapiFileFieldUploader
        public: false public: false
        arguments: ['@logger.channel.file', '@file_system', '@file.mime_type.guesser', '@token', '@lock', '@config.factory', '@event_dispatcher', '@file.validator'] arguments: ['@logger.channel.file', '@file_system', '@file.mime_type.guesser', '@token', '@lock', '@config.factory', '@event_dispatcher', '@file.validator', '@file.input_stream_file_writer']
        Drupal\jsonapi\Controller\TemporaryJsonapiFileFieldUploader: '@jsonapi.file.uploader.field' Drupal\jsonapi\Controller\TemporaryJsonapiFileFieldUploader: '@jsonapi.file.uploader.field'
        ...@@ -19,8 +19,12 @@ ...@@ -19,8 +19,12 @@
        use Drupal\file\Entity\File; use Drupal\file\Entity\File;
        use Drupal\file\FileInterface; use Drupal\file\FileInterface;
        use Drupal\file\Plugin\Field\FieldType\FileFieldItemList; use Drupal\file\Plugin\Field\FieldType\FileFieldItemList;
        use Drupal\file\Upload\InputStreamFileWriterInterface;
        use Drupal\file\Validation\FileValidatorInterface; use Drupal\file\Validation\FileValidatorInterface;
        use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
        use Symfony\Component\HttpFoundation\File\Exception\CannotWriteFileException;
        use Symfony\Component\HttpFoundation\File\Exception\NoFileException;
        use Symfony\Component\HttpFoundation\File\Exception\UploadException;
        use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
        use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
        use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\Exception\HttpException;
        ...@@ -111,6 +115,11 @@ class TemporaryJsonapiFileFieldUploader { ...@@ -111,6 +115,11 @@ class TemporaryJsonapiFileFieldUploader {
        */ */
        protected FileValidatorInterface $fileValidator; protected FileValidatorInterface $fileValidator;
        /**
        * The input stream file writer.
        */
        protected InputStreamFileWriterInterface $inputStreamFileWriter;
        /** /**
        * Constructs a FileUploadResource instance. * Constructs a FileUploadResource instance.
        * *
        ...@@ -126,12 +135,14 @@ class TemporaryJsonapiFileFieldUploader { ...@@ -126,12 +135,14 @@ class TemporaryJsonapiFileFieldUploader {
        * The lock service. * The lock service.
        * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
        * The config factory. * The config factory.
        * @param \Symfony\Contracts\EventDispatcher\EventDispatcherInterface $event_dispatcher * @param \Symfony\Contracts\EventDispatcher\EventDispatcherInterface|null $event_dispatcher
        * (optional) The event dispatcher. * (optional) The event dispatcher.
        * @param \Drupal\file\Validation\FileValidatorInterface|null $file_validator * @param \Drupal\file\Validation\FileValidatorInterface|null $file_validator
        * The file validator. * The file validator.
        * @param \Drupal\file\Upload\InputStreamFileWriterInterface|null $input_stream_file_writer
        * The stream file uploader.
        */ */
        public function __construct(LoggerInterface $logger, FileSystemInterface $file_system, $mime_type_guesser, Token $token, LockBackendInterface $lock, ConfigFactoryInterface $config_factory, EventDispatcherInterface $event_dispatcher = NULL, FileValidatorInterface $file_validator = NULL) { public function __construct(LoggerInterface $logger, FileSystemInterface $file_system, $mime_type_guesser, Token $token, LockBackendInterface $lock, ConfigFactoryInterface $config_factory, EventDispatcherInterface $event_dispatcher = NULL, FileValidatorInterface $file_validator = NULL, InputStreamFileWriterInterface $input_stream_file_writer = NULL) {
        $this->logger = $logger; $this->logger = $logger;
        $this->fileSystem = $file_system; $this->fileSystem = $file_system;
        $this->mimeTypeGuesser = $mime_type_guesser; $this->mimeTypeGuesser = $mime_type_guesser;
        ...@@ -147,6 +158,11 @@ public function __construct(LoggerInterface $logger, FileSystemInterface $file_s ...@@ -147,6 +158,11 @@ public function __construct(LoggerInterface $logger, FileSystemInterface $file_s
        $file_validator = \Drupal::service('file.validator'); $file_validator = \Drupal::service('file.validator');
        } }
        $this->fileValidator = $file_validator; $this->fileValidator = $file_validator;
        if (!$input_stream_file_writer) {
        @trigger_error('Calling ' . __METHOD__ . '() without the $input_stream_file_writer argument is deprecated in drupal:10.3.0 and is required in drupal:11.0.0. See https://www.drupal.org/node/123', E_USER_DEPRECATED);
        $input_stream_file_writer = \Drupal::service('file.input_stream_file_writer');
        }
        $this->inputStreamFileWriter = $input_stream_file_writer;
        } }
        /** /**
        ...@@ -333,51 +349,22 @@ public static function checkFileUploadAccess(AccountInterface $account, FieldDef ...@@ -333,51 +349,22 @@ public static function checkFileUploadAccess(AccountInterface $account, FieldDef
        * opened, or the temporary file cannot be written. * opened, or the temporary file cannot be written.
        */ */
        protected function streamUploadData() { protected function streamUploadData() {
        // 'rb' is needed so reading works correctly on Windows environments too. // Catch and throw the exceptions that JSON API module expects.
        $file_data = fopen('php://input', 'rb'); try {
        $temp_file_path = $this->inputStreamFileWriter->writeStreamToFile();
        $temp_file_path = $this->fileSystem->tempnam('temporary://', 'file');
        if ($temp_file_path === FALSE) {
        $this->logger->error('Temporary file could not be created for file upload.');
        throw new HttpException(500, 'Temporary file could not be created');
        } }
        $temp_file = fopen($temp_file_path, 'wb'); catch (UploadException $e) {
        $this->logger->error('Input data could not be read');
        if ($temp_file) { throw new HttpException(500, 'Input file data could not be read', $e);
        while (!feof($file_data)) {
        $read = fread($file_data, static::BYTES_TO_READ);
        if ($read === FALSE) {
        // Close the file streams.
        fclose($temp_file);
        fclose($file_data);
        $this->logger->error('Input data could not be read');
        throw new HttpException(500, 'Input file data could not be read.');
        }
        if (fwrite($temp_file, $read) === FALSE) {
        // Close the file streams.
        fclose($temp_file);
        fclose($file_data);
        $this->logger->error('Temporary file data for "%path" could not be written', ['%path' => $temp_file_path]);
        throw new HttpException(500, 'Temporary file data could not be written.');
        }
        }
        // Close the temp file stream.
        fclose($temp_file);
        } }
        else { catch (CannotWriteFileException $e) {
        // Close the input file stream since we can't proceed with the upload. $this->logger->error('Temporary file data for could not be written');
        // Don't try to close $temp_file since it's FALSE at this point. throw new HttpException(500, 'Temporary file data could not be written', $e);
        fclose($file_data); }
        $this->logger->error('Temporary file "%path" could not be opened for file upload.', ['%path' => $temp_file_path]); catch (NoFileException $e) {
        throw new HttpException(500, 'Temporary file could not be opened'); $this->logger->error('Temporary file could not be opened for file upload');
        throw new HttpException(500, 'Temporary file could not be opened', $e);
        } }
        // Close the input stream.
        fclose($file_data);
        return $temp_file_path; return $temp_file_path;
        } }
        ......
        0% Loading or .
        You are about to add 0 people to the discussion. Proceed with caution.
        Please register or to comment