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

Issue #3388985 by kim.pepper, Wim Leers: Make CKEditor5ImageController reuse FileUploadHandler

parent 48a7cf12
No related branches found
No related tags found
33 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...,!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),!3531Issue #3336994: StringFormatter always displays links to entity even if the user in context does not have access,!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,!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 #67167 passed with warnings
Pipeline: drupal

#67171

    Pipeline: drupal

    #67170

      Pipeline: drupal

      #67169

        +1
        <?php <?php
        declare(strict_types = 1); declare(strict_types=1);
        namespace Drupal\ckeditor5\Controller; namespace Drupal\ckeditor5\Controller;
        ...@@ -9,20 +9,21 @@ ...@@ -9,20 +9,21 @@
        use Drupal\Component\Utility\Environment; use Drupal\Component\Utility\Environment;
        use Drupal\Core\Access\AccessResult; use Drupal\Core\Access\AccessResult;
        use Drupal\Core\Controller\ControllerBase; use Drupal\Core\Controller\ControllerBase;
        use Drupal\Core\File\Event\FileUploadSanitizeNameEvent;
        use Drupal\Core\File\Exception\FileException; use Drupal\Core\File\Exception\FileException;
        use Drupal\Core\File\FileSystemInterface; use Drupal\Core\File\FileSystemInterface;
        use Drupal\Core\Lock\LockBackendInterface; use Drupal\Core\Lock\LockBackendInterface;
        use Drupal\Core\Session\AccountInterface; use Drupal\Core\Session\AccountInterface;
        use Drupal\editor\Entity\Editor; use Drupal\editor\Entity\Editor;
        use Drupal\file\Entity\File; use Drupal\file\Upload\FileUploadHandler;
        use Drupal\file\FileInterface; use Drupal\file\Upload\FormUploadedFile;
        use Drupal\file\Validation\FileValidatorInterface; use Drupal\file\Validation\FileValidatorInterface;
        use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\ContainerInterface;
        use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\JsonResponse;
        use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
        use Symfony\Component\HttpFoundation\Response;
        use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\Exception\HttpException;
        use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException; use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
        use Symfony\Component\Lock\Exception\LockAcquiringException;
        use Symfony\Component\Mime\MimeTypeGuesserInterface; use Symfony\Component\Mime\MimeTypeGuesserInterface;
        use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
        ...@@ -35,74 +36,62 @@ ...@@ -35,74 +36,62 @@
        class CKEditor5ImageController extends ControllerBase { class CKEditor5ImageController extends ControllerBase {
        /** /**
        * The file system service. * The default allowed image extensions.
        *
        * @var \Drupal\Core\File\FileSystem
        */
        protected $fileSystem;
        /**
        * The currently authenticated user.
        *
        * @var \Drupal\Core\Session\AccountInterface
        */
        protected $currentUser;
        /**
        * The MIME type guesser.
        *
        * @var \Symfony\Component\Mime\MimeTypeGuesserInterface
        */ */
        protected $mimeTypeGuesser; const DEFAULT_IMAGE_EXTENSIONS = 'gif png jpg jpeg';
        /** /**
        * The lock service. * The file system service.
        *
        * @var \Drupal\Core\Lock\LockBackendInterface
        */ */
        protected $lock; protected FileSystemInterface $fileSystem;
        /** /**
        * The event dispatcher. * The lock.
        *
        * @var \Symfony\Contracts\EventDispatcher\EventDispatcherInterface
        */ */
        protected $eventDispatcher; protected LockBackendInterface $lock;
        /** /**
        * The file validator. * The file upload handler.
        *
        * @var \Drupal\file\Validation\FileValidatorInterface
        */ */
        protected FileValidatorInterface $fileValidator; protected FileUploadHandler $fileUploadHandler;
        /** /**
        * Constructs a new CKEditor5ImageController. * Constructs a new CKEditor5ImageController.
        * *
        * @param \Drupal\Core\File\FileSystemInterface $file_system * @param \Drupal\Core\File\FileSystemInterface $fileSystem
        * The file system service. * The file upload handler.
        * @param \Drupal\Core\Session\AccountInterface $current_user * @param \Drupal\Core\Session\AccountInterface|\Drupal\file\Upload\FileUploadHandler $fileUploadHandler
        * The currently authenticated user. * The currently authenticated user.
        * @param \Symfony\Component\Mime\MimeTypeGuesserInterface $mime_type_guesser * @param \Symfony\Component\Mime\MimeTypeGuesserInterface|\Drupal\Core\Lock\LockBackendInterface $mime_type_guesser
        * The MIME type guesser. * The MIME type guesser.
        * @param \Drupal\Core\Lock\LockBackendInterface $lock * @param \Drupal\Core\Lock\LockBackendInterface|null $lock
        * The lock service. * The lock service.
        * @param \Symfony\Contracts\EventDispatcher\EventDispatcherInterface $event_dispatcher * @param \Symfony\Contracts\EventDispatcher\EventDispatcherInterface|null $event_dispatcher
        * The event dispatcher. * 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.
        */ */
        public function __construct(FileSystemInterface $file_system, AccountInterface $current_user, MimeTypeGuesserInterface $mime_type_guesser, LockBackendInterface $lock, EventDispatcherInterface $event_dispatcher, FileValidatorInterface $file_validator = NULL) { public function __construct(FileSystemInterface $fileSystem, AccountInterface | FileUploadHandler $fileUploadHandler, MimeTypeGuesserInterface | LockBackendInterface $mime_type_guesser, LockBackendInterface $lock = NULL, EventDispatcherInterface $event_dispatcher = NULL, FileValidatorInterface $file_validator = NULL) {
        $this->fileSystem = $file_system; $this->fileSystem = $fileSystem;
        $this->currentUser = $current_user; if ($fileUploadHandler instanceof AccountInterface) {
        $this->mimeTypeGuesser = $mime_type_guesser; @trigger_error('Calling ' . __METHOD__ . '() with the $current_user argument is deprecated in drupal:10.3.0 and is removed from drupal:11.0.0. See https://www.drupal.org/node/3388990', E_USER_DEPRECATED);
        $this->lock = $lock; $fileUploadHandler = \Drupal::service('file.upload_handler');
        $this->eventDispatcher = $event_dispatcher; }
        if (!$file_validator) { $this->fileUploadHandler = $fileUploadHandler;
        @trigger_error('Calling ' . __METHOD__ . '() without the $file_validator argument is deprecated in drupal:10.2.0 and is required in drupal:11.0.0. See https://www.drupal.org/node/3363700', E_USER_DEPRECATED); if ($mime_type_guesser instanceof MimeTypeGuesserInterface) {
        $file_validator = \Drupal::service('file.validator'); @trigger_error('Calling ' . __METHOD__ . '() with the $mime_type_guesser argument is deprecated in drupal:10.3.0 and is replaced with $lock from drupal:11.0.0. See https://www.drupal.org/node/3388990', E_USER_DEPRECATED);
        $mime_type_guesser = \Drupal::service('lock');
        }
        $this->lock = $mime_type_guesser;
        if ($lock) {
        @trigger_error('Calling ' . __METHOD__ . '() with the $lock argument in position 4 is deprecated in drupal:10.3.0 and is removed from drupal:11.0.0. See https://www.drupal.org/node/3388990', E_USER_DEPRECATED);
        }
        if ($event_dispatcher) {
        @trigger_error('Calling ' . __METHOD__ . '() with the $event_dispatcher argument is deprecated in drupal:10.3.0 and is removed from drupal:11.0.0. See https://www.drupal.org/node/3388990', E_USER_DEPRECATED);
        }
        if ($file_validator) {
        @trigger_error('Calling ' . __METHOD__ . '() with the $file_validator argument is deprecated in drupal:10.3.0 and is removed from drupal:11.0.0. See https://www.drupal.org/node/3388990', E_USER_DEPRECATED);
        } }
        $this->fileValidator = $file_validator;
        } }
        /** /**
        ...@@ -111,11 +100,8 @@ public function __construct(FileSystemInterface $file_system, AccountInterface $ ...@@ -111,11 +100,8 @@ public function __construct(FileSystemInterface $file_system, AccountInterface $
        public static function create(ContainerInterface $container) { public static function create(ContainerInterface $container) {
        return new static( return new static(
        $container->get('file_system'), $container->get('file_system'),
        $container->get('current_user'), $container->get('file.upload_handler'),
        $container->get('file.mime_type.guesser'), $container->get('lock')
        $container->get('lock'),
        $container->get('event_dispatcher'),
        $container->get('file.validator')
        ); );
        } }
        ...@@ -132,52 +118,26 @@ public static function create(ContainerInterface $container) { ...@@ -132,52 +118,26 @@ public static function create(ContainerInterface $container) {
        * Thrown when file system errors occur. * Thrown when file system errors occur.
        * @throws \Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException * @throws \Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException
        * Thrown when validation errors occur. * Thrown when validation errors occur.
        * @throws \Drupal\Core\Entity\EntityStorageException
        * Thrown when file entity could not be saved.
        */ */
        public function upload(Request $request) { public function upload(Request $request): Response {
        // Getting the UploadedFile directly from the request. // Getting the UploadedFile directly from the request.
        /** @var \Symfony\Component\HttpFoundation\File\UploadedFile $upload */
        $upload = $request->files->get('upload'); $upload = $request->files->get('upload');
        $filename = $upload->getClientOriginalName(); $filename = $upload->getClientOriginalName();
        /** @var \Drupal\editor\EditorInterface $editor */
        $editor = $request->attributes->get('editor'); $editor = $request->attributes->get('editor');
        $image_upload = $editor->getImageUploadSettings(); $settings = $editor->getImageUploadSettings();
        $destination = $image_upload['scheme'] . '://' . $image_upload['directory']; $destination = $settings['scheme'] . '://' . $settings['directory'];
        // Check the destination file path is writable. // Check the destination file path is writable.
        if (!$this->fileSystem->prepareDirectory($destination, FileSystemInterface::CREATE_DIRECTORY)) { if (!$this->fileSystem->prepareDirectory($destination, FileSystemInterface::CREATE_DIRECTORY)) {
        throw new HttpException(500, 'Destination file path is not writable'); throw new HttpException(500, 'Destination file path is not writable');
        } }
        $max_filesize = min(Bytes::toNumber($image_upload['max_size']), Environment::getUploadMaxSize()); $validators = $this->getImageUploadValidators($settings);
        if (!empty($image_upload['max_dimensions']['width']) || !empty($image_upload['max_dimensions']['height'])) {
        $max_dimensions = $image_upload['max_dimensions']['width'] . 'x' . $image_upload['max_dimensions']['height'];
        }
        else {
        $max_dimensions = 0;
        }
        $allowed_extensions = 'gif png jpg jpeg';
        $validators = [
        'FileExtension' => [
        'extensions' => $allowed_extensions,
        ],
        'FileSizeLimit' => [
        'fileLimit' => $max_filesize,
        ],
        'FileImageDimensions' => [
        'maxDimensions' => $max_dimensions,
        ],
        ];
        $prepared_filename = $this->prepareFilename($filename, $allowed_extensions);
        // Create the file.
        $file_uri = "{$destination}/{$prepared_filename}";
        // Using the UploadedFile method instead of streamUploadData.
        $temp_file_path = $upload->getRealPath();
        $file_uri = "{$destination}/{$filename}";
        $file_uri = $this->fileSystem->getDestinationFilename($file_uri, FileSystemInterface::EXISTS_RENAME); $file_uri = $this->fileSystem->getDestinationFilename($file_uri, FileSystemInterface::EXISTS_RENAME);
        // Lock based on the prepared file URI. // Lock based on the prepared file URI.
        ...@@ -187,31 +147,23 @@ public function upload(Request $request) { ...@@ -187,31 +147,23 @@ public function upload(Request $request) {
        throw new HttpException(503, sprintf('File "%s" is already locked for writing.', $file_uri), NULL, ['Retry-After' => 1]); throw new HttpException(503, sprintf('File "%s" is already locked for writing.', $file_uri), NULL, ['Retry-After' => 1]);
        } }
        // Begin building file entity.
        $file = File::create([]);
        $file->setOwnerId($this->currentUser->id());
        $file->setFilename($prepared_filename);
        $file->setMimeType($this->mimeTypeGuesser->guessMimeType($prepared_filename));
        $file->setFileUri($file_uri);
        $file->setSize(@filesize($temp_file_path));
        $violations = $this->validate($file, $validators);
        if ($violations->count() > 0) {
        throw new UnprocessableEntityHttpException($violations->__toString());
        }
        try { try {
        $this->fileSystem->move($temp_file_path, $file_uri, FileSystemInterface::EXISTS_ERROR); $uploadedFile = new FormUploadedFile($upload);
        $uploadResult = $this->fileUploadHandler->handleFileUpload($uploadedFile, $validators, $destination, FileSystemInterface::EXISTS_RENAME, FALSE);
        if ($uploadResult->hasViolations()) {
        throw new UnprocessableEntityHttpException((string) $uploadResult->getViolations());
        }
        } }
        catch (FileException $e) { catch (FileException $e) {
        throw new HttpException(500, 'Temporary file could not be moved to file location'); throw new HttpException(500, 'File could not be saved');
        }
        catch (LockAcquiringException $e) {
        throw new HttpException(503, sprintf('File "%s" is already locked for writing.', $upload->getClientOriginalName()), NULL, ['Retry-After' => 1]);
        } }
        $file->save();
        $this->lock->release($lock_id); $this->lock->release($lock_id);
        $file = $uploadResult->getFile();
        return new JsonResponse([ return new JsonResponse([
        'url' => $file->createFileUrl(), 'url' => $file->createFileUrl(),
        'uuid' => $file->uuid(), 'uuid' => $file->uuid(),
        ...@@ -219,6 +171,28 @@ public function upload(Request $request) { ...@@ -219,6 +171,28 @@ public function upload(Request $request) {
        ], 201); ], 201);
        } }
        /**
        * Gets the image upload validators.
        */
        protected function getImageUploadValidators(array $settings): array {
        $max_filesize = min(Bytes::toNumber($settings['max_size']), Environment::getUploadMaxSize());
        $max_dimensions = 0;
        if (!empty($settings['max_dimensions']['width']) || !empty($settings['max_dimensions']['height'])) {
        $max_dimensions = $settings['max_dimensions']['width'] . 'x' . $settings['max_dimensions']['height'];
        }
        return [
        'FileExtension' => [
        'extensions' => self::DEFAULT_IMAGE_EXTENSIONS,
        ],
        'FileSizeLimit' => [
        'fileLimit' => $max_filesize,
        ],
        'FileImageDimensions' => [
        'maxDimensions' => $max_dimensions,
        ],
        ];
        }
        /** /**
        * Access check based on whether image upload is enabled or not. * Access check based on whether image upload is enabled or not.
        * *
        ...@@ -239,48 +213,6 @@ public function imageUploadEnabledAccess(Editor $editor) { ...@@ -239,48 +213,6 @@ public function imageUploadEnabledAccess(Editor $editor) {
        return AccessResult::allowed(); return AccessResult::allowed();
        } }
        /**
        * Validates the file.
        *
        * @param \Drupal\file\FileInterface $file
        * The file entity to validate.
        * @param array $validators
        * An array of upload validators to pass to the FileValidator.
        *
        * @return \Drupal\Core\Entity\EntityConstraintViolationListInterface
        * The list of constraint violations, if any.
        */
        protected function validate(FileInterface $file, array $validators) {
        $violations = $file->validate();
        // Remove violations of inaccessible fields as they cannot stem from our
        // changes.
        $violations->filterByFieldAccess();
        // Validate the file based on the field definition configuration.
        $violations->addAll($this->fileValidator->validate($file, $validators));
        return $violations;
        }
        /**
        * Prepares the filename to strip out any malicious extensions.
        *
        * @param string $filename
        * The file name.
        * @param string $allowed_extensions
        * The allowed extensions.
        *
        * @return string
        * The prepared/munged filename.
        */
        protected function prepareFilename(string $filename, string $allowed_extensions): string {
        $event = new FileUploadSanitizeNameEvent($filename, $allowed_extensions);
        $this->eventDispatcher->dispatch($event);
        return $event->getFilename();
        }
        /** /**
        * Generates a lock ID based on the file URI. * Generates a lock ID based on the file URI.
        * *
        ......
        0% Loading or .
        You are about to add 0 people to the discussion. Proceed with caution.
        Finish editing this message first!
        Please register or to comment