diff --git a/core/modules/image/image.module b/core/modules/image/image.module index dbed41c93a..73bb4630d0 100644 --- a/core/modules/image/image.module +++ b/core/modules/image/image.module @@ -14,6 +14,8 @@ use Drupal\field\FieldStorageConfigInterface; use Drupal\file\FileInterface; use Drupal\image\Entity\ImageStyle; +use Drupal\image\Event\ImageStyleEvent; +use Drupal\image\Event\ImageStyleEvents; /** * The name of the query parameter for image derivative tokens. @@ -192,10 +194,12 @@ function image_file_predelete(FileInterface $file) { * The Drupal file path to the original image. */ function image_path_flush($path) { - $styles = ImageStyle::loadMultiple(); - foreach ($styles as $style) { - $style->flush($path); - } + \Drupal::service('event_dispatcher')->dispatch( + ImageStyleEvents::FLUSH_FROM_SOURCE_IMAGE_URI, + new ImageStyleEvent(NULL, [ + 'sourceImageUri' => $path, + ]) + ); } /** diff --git a/core/modules/image/image.services.yml b/core/modules/image/image.services.yml index bbddf3ee8b..aa05f3210b 100644 --- a/core/modules/image/image.services.yml +++ b/core/modules/image/image.services.yml @@ -9,6 +9,11 @@ services: parent: default_plugin_manager tags: - { name: plugin_manager_cache_clear } + image.pipeline.derivative.event_subscriber: + class: Drupal\image\EventSubscriber\ImageDerivativeSubscriber + arguments: ['@image.factory', '@image.processor', '@stream_wrapper_manager', '@private_key', '@module_handler','@config.factory', '@request_stack', '@logger.channel.image', '@file_system'] + tags: + - { name: 'event_subscriber' } plugin.manager.image.effect: class: Drupal\image\ImageEffectManager parent: default_plugin_manager diff --git a/core/modules/image/src/Entity/ImageStyle.php b/core/modules/image/src/Entity/ImageStyle.php index dda0ced5a4..fef6e7cc6d 100644 --- a/core/modules/image/src/Entity/ImageStyle.php +++ b/core/modules/image/src/Entity/ImageStyle.php @@ -7,6 +7,10 @@ use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Entity\EntityWithPluginCollectionInterface; use Drupal\Core\Site\Settings; +use Drupal\Core\Url; +use Drupal\image\Event\ImageDerivativePipelineEvents; +use Drupal\image\Event\ImageStyleEvent; +use Drupal\image\Event\ImageStyleEvents; use Drupal\image\ImageEffectPluginCollection; use Drupal\image\ImageEffectInterface; use Drupal\image\ImageStyleInterface; @@ -165,10 +169,10 @@ protected static function replaceImageStyle(ImageStyleInterface $style) { */ public function buildUri($uri) { @trigger_error('The ' . __METHOD__ . ' method is deprecated since version 9.x.x and will be removed in y.y.y.', E_USER_DEPRECATED); - return \Drupal::service('image.processor')->createInstance('core') + return \Drupal::service('image.processor')->createInstance('derivative') ->setImageStyle($this) ->setSourceImageUri($uri) - ->getDerivedImageUri(); + ->getDerivativeImageUri(); } /** @@ -176,25 +180,28 @@ public function buildUri($uri) { */ public function buildUrl($path, $clean_urls = NULL) { @trigger_error('The ' . __METHOD__ . ' method is deprecated since version 9.x.x and will be removed in y.y.y.', E_USER_DEPRECATED); - return \Drupal::service('image.processor')->createInstance('core') + $url = \Drupal::service('image.processor')->createInstance('derivative') ->setImageStyle($this) ->setSourceImageUri($path) - ->setDerivedImageCleanUrl($clean_urls) - ->getDerivedImageUrl(); + ->setCleanUrl($clean_urls) + ->getDerivativeImageUrl(); + return $url instanceof Url ? $url->toString() : $url; } /** * {@inheritdoc} */ public function flush($path = NULL) { - @trigger_error('The ' . __METHOD__ . ' method is deprecated since version 9.x.x and will be removed in y.y.y.', E_USER_DEPRECATED); - $pipeline = \Drupal::service('image.processor')->createInstance('core'); - $pipeline->setImageStyle($this); if (isset($path)) { // A specific image path has been provided. Flush only that derivative. - $pipeline->setSourceImageUri($path); + $pipeline = \Drupal::service('image.processor')->createInstance('derivative') + ->setImageStyle($this) + ->setSourceImageUri($path) + ->dispatch(ImageDerivativePipelineEvents::REMOVE_DERIVATIVE_IMAGE); + } + else { + \Drupal::service('event_dispatcher')->dispatch(ImageStyleEvents::FLUSH, new ImageStyleEvent($this)); } - $pipeline->removeDerivedImagesFromStorage(); return $this; } @@ -203,11 +210,11 @@ public function flush($path = NULL) { */ public function createDerivative($original_uri, $derivative_uri) { @trigger_error('The ' . __METHOD__ . ' method is deprecated since version 9.x.x and will be removed in y.y.y.', E_USER_DEPRECATED); - return \Drupal::service('image.processor')->createInstance('core') + return \Drupal::service('image.processor')->createInstance('derivative') ->setImageStyle($this) ->setSourceImageUri($original_uri) - ->setDerivedImageUri($derivative_uri) - ->produceDerivedImage(); + ->setDerivativeImageUri($derivative_uri) + ->buildDerivativeImage(); } /** @@ -215,13 +222,14 @@ public function createDerivative($original_uri, $derivative_uri) { */ public function transformDimensions(array &$dimensions, $uri) { @trigger_error('The ' . __METHOD__ . ' method is deprecated since version 9.x.x and will be removed in y.y.y.', E_USER_DEPRECATED); - $pipeline = \Drupal::service('image.processor')->createInstance('core'); + $pipeline = \Drupal::service('image.processor')->createInstance('derivative'); $pipeline ->setImageStyle($this) ->setSourceImageUri($uri) - ->setSourceImageDimensions($dimensions['width'] ?? NULL, $dimensions['height'] ?? NULL); - $dimensions['width'] = $pipeline->getDerivedImageWidth(); - $dimensions['height'] = $pipeline->getDerivedImageHeight(); + ->setSourceImageDimensions($dimensions['width'] ?? NULL, $dimensions['height'] ?? NULL) + ->dispatch(ImageDerivativePipelineEvents::RESOLVE_DERIVATIVE_IMAGE_DIMENSIONS); + $dimensions['width'] = $pipeline->getVariable('derivativeImageWidth'); + $dimensions['height'] = $pipeline->getVariable('derivativeImageHeight'); return; } @@ -230,10 +238,10 @@ public function transformDimensions(array &$dimensions, $uri) { */ public function getDerivativeExtension($extension) { @trigger_error('The ' . __METHOD__ . ' method is deprecated since version 9.x.x and will be removed in y.y.y.', E_USER_DEPRECATED); - return \Drupal::service('image.processor')->createInstance('core') + return \Drupal::service('image.processor')->createInstance('derivative') ->setImageStyle($this) ->setSourceImageFileExtension($extension) - ->getDerivedImageFileExtension(); + ->getDerivativeImageFileExtension(); } /** @@ -241,10 +249,10 @@ public function getDerivativeExtension($extension) { */ public function getPathToken($uri) { @trigger_error('The ' . __METHOD__ . ' method is deprecated since version 9.x.x and will be removed in y.y.y.', E_USER_DEPRECATED); - return \Drupal::service('image.processor')->createInstance('core') + return \Drupal::service('image.processor')->createInstance('derivative') ->setImageStyle($this) ->setSourceImageUri($uri) - ->getDerivedImageUrlSecurityToken(); + ->getDerivativeImageUrlSecurityToken(); } /** @@ -261,7 +269,7 @@ public function deleteImageEffect(ImageEffectInterface $effect) { */ public function supportsUri($uri) { @trigger_error('The ' . __METHOD__ . ' method is deprecated since version 9.x.x and will be removed in y.y.y.', E_USER_DEPRECATED); - return \Drupal::service('image.processor')->createInstance('core') + return \Drupal::service('image.processor')->createInstance('derivative') ->setImageStyle($this) ->setSourceImageUri($uri) ->isSourceImageProcessable(); diff --git a/core/modules/image/src/Event/ImageDerivativePipelineEvents.php b/core/modules/image/src/Event/ImageDerivativePipelineEvents.php new file mode 100644 index 0000000000..a7b0141ead --- /dev/null +++ b/core/modules/image/src/Event/ImageDerivativePipelineEvents.php @@ -0,0 +1,162 @@ +getSubject(); + } + +} diff --git a/core/modules/image/src/Event/ImageStyleEvent.php b/core/modules/image/src/Event/ImageStyleEvent.php new file mode 100644 index 0000000000..c40de29b87 --- /dev/null +++ b/core/modules/image/src/Event/ImageStyleEvent.php @@ -0,0 +1,23 @@ +getSubject(); + } + +} diff --git a/core/modules/image/src/Event/ImageStyleEvents.php b/core/modules/image/src/Event/ImageStyleEvents.php new file mode 100644 index 0000000000..35a8cee043 --- /dev/null +++ b/core/modules/image/src/Event/ImageStyleEvents.php @@ -0,0 +1,32 @@ +imageFactory = $image_factory; + $this->imageProcessor = $image_processor; + $this->streamWrapperManager = $stream_wrapper_manager; + $this->privateKey = $private_key->get(); + $this->moduleHandler = $module_handler; + $this->configFactory = $config_factory; + $this->currentRequest = $request_stack->getCurrentRequest(); + $this->logger = $logger; + $this->fileSystem = $file_system; + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() { + return [ + ImageDerivativePipelineEvents::RESOLVE_SOURCE_IMAGE_FORMAT => 'resolveSourceImageFormat', + ImageDerivativePipelineEvents::RESOLVE_SOURCE_IMAGE_PROCESSABILITY => 'resolveSourceImageProcessability', + ImageDerivativePipelineEvents::RESOLVE_DERIVATIVE_IMAGE_FORMAT => 'resolveDerivativeImageFormat', + ImageDerivativePipelineEvents::RESOLVE_DERIVATIVE_IMAGE_DIMENSIONS => 'resolveDerivativeImageDimensions', + ImageDerivativePipelineEvents::RESOLVE_DERIVATIVE_IMAGE_URI => 'resolveDerivativeImageUri', + ImageDerivativePipelineEvents::RESOLVE_DERIVATIVE_IMAGE_URL_PROTECTION => 'resolveDerivativeImageUrlProtection', + ImageDerivativePipelineEvents::RESOLVE_DERIVATIVE_IMAGE_URL => 'resolveDerivativeImageUrl', + ImageDerivativePipelineEvents::BUILD_DERIVATIVE_IMAGE => 'buildDerivativeImage', + ImageDerivativePipelineEvents::LOAD_SOURCE_IMAGE => 'loadSourceImage', + ImageDerivativePipelineEvents::APPLY_IMAGE_STYLE => 'applyImageStyle', + ImageDerivativePipelineEvents::APPLY_IMAGE_EFFECT => 'applyImageEffect', + ImageDerivativePipelineEvents::SAVE_DERIVATIVE_IMAGE => 'saveDerivativeImage', + ImageStyleEvents::FLUSH => 'flushImageStyle', + ImageStyleEvents::FLUSH_FROM_SOURCE_IMAGE_URI => 'flushFromSourceImageUri', + ImageDerivativePipelineEvents::REMOVE_DERIVATIVE_IMAGE => 'removeDerivativeImage', + ]; + } + + /** + * Determines the format of a source image. + * + * @param \Drupal\image\Event\ImageProcessEvent $event + * The image process event, carrying the process pipeline object. + */ + public function resolveSourceImageFormat(ImageProcessEvent $event): void { + $pipeline = $event->getPipeline(); + + // Get the image file extension from the URI if not already specified. + if (!$pipeline->hasVariable('sourceImageFileExtension')) { + $pipeline->setSourceImageFileExtension(mb_strtolower(pathinfo($pipeline->getVariable('sourceImageUri'), PATHINFO_EXTENSION))); + } + } + + /** + * Verifies that an image can be processed into a derivative. + * + * @param \Drupal\image\Event\ImageProcessEvent $event + * The image process event, carrying the process pipeline object. + */ + public function resolveSourceImageProcessability(ImageProcessEvent $event): void { + $pipeline = $event->getPipeline(); + + // Determine the image toolkit. + if (!$pipeline->hasVariable('imageToolkitId')) { + $pipeline->setImageToolkitId($this->imageFactory->getToolkitId()); + } + + // Ensure we know the format of the source image. + try { + $pipeline->dispatch(ImageDerivativePipelineEvents::RESOLVE_SOURCE_IMAGE_FORMAT); + } + catch (ImageProcessException $e) { + $pipeline->setVariable('isSourceImageProcessable', FALSE); + return; + } + + // The source image can be processed if the image toolkit supports its + // format. + $pipeline->setVariable( + 'isSourceImageProcessable', + in_array( + $pipeline->getVariable('sourceImageFileExtension'), + $this->imageFactory->getSupportedExtensions($pipeline->getVariable('imageToolkitId')) + ) + ); + } + + /** + * Determines the format of a derivative image. + * + * @param \Drupal\image\Event\ImageProcessEvent $event + * The image process event, carrying the process pipeline object. + */ + public function resolveDerivativeImageFormat(ImageProcessEvent $event): void { + $pipeline = $event->getPipeline(); + + // Ensure we can process the source image. + $pipeline->dispatch(ImageDerivativePipelineEvents::RESOLVE_SOURCE_IMAGE_PROCESSABILITY); + if (!$pipeline->getVariable('isSourceImageProcessable')) { + throw new ImageProcessException('Cannot determine derivative image format, source image not processable'); + } + + // Determine the derivative image file extension by looping through the + // image effects' ::getDerivativeExtension methods. + $extension = $pipeline->getVariable('sourceImageFileExtension'); + foreach ($pipeline->getVariable('imageStyle')->getEffects() as $effect) { + $extension = $effect->getDerivativeExtension($extension); + } + $pipeline->setVariable('derivativeImageFileExtension', $extension); + } + + /** + * Determines the dimensions of a derivative image. + * + * Takes the source URI, the image style, and the starting dimensions to + * determine the expected dimensions of the derivative image. The source URI + * is used to allow effects to optionally use this information to retrieve + * additional image metadata to determine output dimensions. The key + * objective is to calculate derivative image dimensions without performing + * actual image operations, so be aware that performing I/O on the URI may + * lead to decrease in performance. + * + * @param \Drupal\image\Event\ImageProcessEvent $event + * The image process event, carrying the process pipeline object. + */ + public function resolveDerivativeImageDimensions(ImageProcessEvent $event): void { + $pipeline = $event->getPipeline(); + + // It's still possible to calculate dimensions even if the image at source + // is not processable but we have input dimensions. + $pipeline->dispatch(ImageDerivativePipelineEvents::RESOLVE_SOURCE_IMAGE_PROCESSABILITY); + if (!$pipeline->getVariable('isSourceImageProcessable') && (!$pipeline->hasVariable('sourceImageWidth') || !$pipeline->hasVariable('sourceImageHeight'))) { + return; + } + + // Determine the derivative image dimensions by looping through the image + // style effects' ::transformDimensions methods. + $dimensions = [ + 'width' => $pipeline->getVariable('sourceImageWidth'), + 'height' => $pipeline->getVariable('sourceImageHeight'), + ]; + foreach ($pipeline->getVariable('imageStyle')->getEffects() as $effect) { + $effect->transformDimensions($dimensions, $pipeline->getVariable('sourceImageUri')); + } + $pipeline->setVariable('derivativeImageWidth', $dimensions['width']); + $pipeline->setVariable('derivativeImageHeight', $dimensions['height']); + } + + /** + * Determines the URI of a derivative image. + * + * @param \Drupal\image\Event\ImageProcessEvent $event + * The image process event, carrying the process pipeline object. + */ + public function resolveDerivativeImageUri(ImageProcessEvent $event): void { + $pipeline = $event->getPipeline(); + + // Return if we already have the derivative URI. + if ($pipeline->hasVariable('derivativeImageUri')) { + return; + } + + // Ensure we can process the source image. + $pipeline->dispatch(ImageDerivativePipelineEvents::RESOLVE_DERIVATIVE_IMAGE_FORMAT); + if (!$pipeline->getVariable('isSourceImageProcessable')) { + throw new ImageProcessException('Cannot determine derivative image URI, source image not processable'); + } + + // Determine derivative image URI. + $source_scheme = $scheme = $this->streamWrapperManager->getScheme($pipeline->getVariable('sourceImageUri')); + $default_scheme = $this->configFactory->get('system.file')->get('default_scheme'); + + if ($source_scheme) { + $path = $this->streamWrapperManager->getTarget($pipeline->getVariable('sourceImageUri')); + // The scheme of derivative image files only needs to be computed for + // source files not stored in the default scheme. + if ($source_scheme != $default_scheme) { + $class = $this->streamWrapperManager->getClass($source_scheme); + $is_writable = NULL; + if ($class) { + $is_writable = $class::getType() & StreamWrapperInterface::WRITE; + } + + // Compute the derivative URI scheme. Derivatives created from writable + // source stream wrappers will inherit the scheme. Derivatives created + // from read-only stream wrappers will fall-back to the default scheme. + $scheme = $is_writable ? $source_scheme : $default_scheme; + } + } + else { + $path = $pipeline->getVariable('sourceImageUri'); + $source_scheme = $scheme = $default_scheme; + } + $path = $pipeline->getVariable('derivativeImageFileExtension') === $pipeline->getVariable('sourceImageFileExtension') ? $path : $path . '.' . $pipeline->getVariable('derivativeImageFileExtension'); + $pipeline->setVariable('derivativeImageUri', "$scheme://styles/{$pipeline->getVariable('imageStyle')->id()}/$source_scheme/$path"); + } + + /** + * Determines the URL protection token of a derivative image. + * + * @param \Drupal\image\Event\ImageProcessEvent $event + * The image process event, carrying the process pipeline object. + */ + public function resolveDerivativeImageUrlProtection(ImageProcessEvent $event): void { + $pipeline = $event->getPipeline(); + + // Ensure we have the derivative image URI. + $pipeline->dispatch(ImageDerivativePipelineEvents::RESOLVE_DERIVATIVE_IMAGE_URI); + + // The token query is added even if the + // 'image.settings:allow_insecure_derivatives' configuration is TRUE, so + // that the emitted links remain valid if it is changed back to the default + // FALSE. However, sites which need to prevent the token query from being + // emitted at all can additionally set the + // 'image.settings:suppress_itok_output' configuration to TRUE to achieve + // that (if both are set, the security token will neither be emitted in the + // image derivative URL nor checked for in + // \Drupal\image\ImageStyleInterface::deliver()). + $token_query = []; + $suppress_itok_output = $this->configFactory->get('image.settings')->get('suppress_itok_output'); + if (!$suppress_itok_output) { + // The sourceUri property can be either a relative path or a full URI. + $original_uri_normalised = $this->streamWrapperManager->getScheme($pipeline->getVariable('sourceImageUri')) ? $this->streamWrapperManager->normalizeUri($pipeline->getVariable('sourceImageUri')) : file_build_uri($pipeline->getVariable('sourceImageUri')); + $cryptable_uri = $pipeline->getVariable('derivativeImageFileExtension') === $pipeline->getVariable('sourceImageFileExtension') ? $original_uri_normalised : $original_uri_normalised . '.' . $pipeline->getVariable('derivativeImageFileExtension'); + // Return the first 8 characters. + $token_query = [IMAGE_DERIVATIVE_TOKEN => substr(Crypt::hmacBase64($pipeline->getVariable('imageStyle')->id() . ':' . $cryptable_uri, $this->privateKey . Settings::getHashSalt()), 0, 8)]; + $pipeline->setVariable('derivativeImageUrlProtection', $token_query); + } + } + + /** + * Determines the URL of a derivative image. + * + * Including the security token if specified. + * + * @param \Drupal\image\Event\ImageProcessEvent $event + * The image process event, carrying the process pipeline object. + */ + public function resolveDerivativeImageUrl(ImageProcessEvent $event): void { + $pipeline = $event->getPipeline(); + + // Ensure we have the derivative image URI and the URL protection token. + $pipeline->dispatch(ImageDerivativePipelineEvents::RESOLVE_DERIVATIVE_IMAGE_URI); + $pipeline->dispatch(ImageDerivativePipelineEvents::RESOLVE_DERIVATIVE_IMAGE_URL_PROTECTION); + + $clean_urls = $pipeline->hasVariable('setCleanUrl') ? $pipeline->getVariable('setCleanUrl') : NULL; + $derivative_uri = $pipeline->getVariable('derivativeImageUri'); + $token_query = $pipeline->hasVariable('derivativeImageUrlProtection') ? $pipeline->getVariable('derivativeImageUrlProtection') : []; + + // Determine whether clean URLs can be used. + if ($clean_urls === NULL) { + // Assume clean URLs unless the request tells us otherwise. + $clean_urls = TRUE; + try { + $clean_urls = RequestHelper::isCleanUrl($this->currentRequest); + } + catch (ServiceNotFoundException $e) { + } + } + + // If not using clean URLs, the image derivative callback is only available + // with the script path. If the file does not exist, use Url::fromUri() to + // ensure that it is included. Once the file exists it's fine to fall back + // to the actual file path, this avoids bootstrapping PHP once the files are + // built. + if ($clean_urls === FALSE && $this->streamWrapperManager->getScheme($derivative_uri) == 'public' && !file_exists($derivative_uri)) { + $directory_path = $this->streamWrapperManager->getViaUri($derivative_uri)->getDirectoryPath(); + $pipeline->setVariable('derivativeImageUrl', Url::fromUri( + 'base:' . $directory_path . '/' . $this->streamWrapperManager->getTarget($derivative_uri), [ + 'absolute' => TRUE, + 'query' => $token_query, + ]) + ); + return; + } + + // Append the query string with the token, if necessary. + $file_url = file_create_url($derivative_uri); + if ($token_query) { + $file_url .= (strpos($file_url, '?') !== FALSE ? '&' : '?') . UrlHelper::buildQuery($token_query); + } + + $pipeline->setVariable('derivativeImageUrl', $file_url); + } + + /** + * Loads an Image object for subsequent processing into a derivative. + * + * @param \Drupal\image\Event\ImageProcessEvent $event + * The image process event, carrying the process pipeline object. + */ + public function loadSourceImage(ImageProcessEvent $event): void { + $pipeline = $event->getPipeline(); + + if ($pipeline->hasVariable('sourceImageUri') && !$pipeline->hasVariable('image')) { + // If the source file doesn't exist, return FALSE without creating folders. + $image = $this->imageFactory->get($pipeline->getVariable('sourceImageUri'), $pipeline->getVariable('imageToolkitId')); + if (!$image->isValid()) { + return; + } + $pipeline->setImage($image); + } + } + + /** + * Stores a transformed image at the derivative URI. + * + * @param \Drupal\image\Event\ImageProcessEvent $event + * The image process event, carrying the process pipeline object. + */ + public function saveDerivativeImage(ImageProcessEvent $event): void { + $pipeline = $event->getPipeline(); + + $pipeline->dispatch(ImageDerivativePipelineEvents::RESOLVE_DERIVATIVE_IMAGE_URI); + + // Get the folder for the final location of this style. + $directory = $this->fileSystem->dirname($pipeline->getVariable('derivativeImageUri')); + + // Build the destination folder tree if it doesn't already exist. + if (!$this->fileSystem->prepareDirectory($directory, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS)) { + $this->logger->error('Failed to create style directory: %directory', ['%directory' => $directory]); + throw new ImageProcessException('Failed to create style directory'); + } + + if (!$pipeline->getVariable('image')->save($pipeline->getVariable('derivativeImageUri'))) { + if (file_exists($pipeline->getVariable('derivativeImageUri'))) { + $this->logger->error('Cached image file %destination already exists. There may be an issue with your rewrite configuration.', ['%destination' => $pipeline->getVariable('derivativeImageUri')]); + } + throw new ImageProcessException('Cached image file already exists'); + } + } + + /** + * Produces an image derivative. + * + * @param \Drupal\image\Event\ImageProcessEvent $event + * The image process event, carrying the process pipeline object. + */ + public function buildDerivativeImage(ImageProcessEvent $event): void { + $pipeline = $event->getPipeline(); + + try { + $pipeline + ->dispatch(ImageDerivativePipelineEvents::RESOLVE_SOURCE_IMAGE_PROCESSABILITY) + ->dispatch(ImageDerivativePipelineEvents::LOAD_SOURCE_IMAGE) + ->dispatch(ImageDerivativePipelineEvents::APPLY_IMAGE_STYLE) + ->dispatch(ImageDerivativePipelineEvents::SAVE_DERIVATIVE_IMAGE) + ->setVariable('derivativeImageBuilt', TRUE); + } + catch (ImageProcessException $e) { + // Do nothing. + } + } + + /** + * Applies an image style to the image object. + * + * @param \Drupal\image\Event\ImageProcessEvent $event + * The image process event, carrying the process pipeline object. + */ + public function applyImageStyle(ImageProcessEvent $event): void { + $pipeline = $event->getPipeline(); + + // Apply the image effects to the image object. + foreach ($pipeline->getVariable('imageStyle')->getEffects() as $effect) { + $pipeline->dispatch( + ImageDerivativePipelineEvents::APPLY_IMAGE_EFFECT, [ + 'imageEffect' => $effect, + ]); + } + } + + /** + * Applies a single image effect to the image object. + * + * @param \Drupal\image\Event\ImageProcessEvent $event + * The image process event, carrying the process pipeline object. + */ + public function applyImageEffect(ImageProcessEvent $event): void { + $pipeline = $event->getPipeline(); + $effect = $event->getArgument('imageEffect'); + $effect->applyEffect($pipeline->getVariable('image')); + } + + /** + * Removes an image derivative file based on its source file URI. + * + * @param \Drupal\image\Event\ImageProcessEvent $event + * The image process event, carrying the process pipeline object. + */ + public function removeDerivativeImage(ImageProcessEvent $event): void { + $pipeline = $event->getPipeline(); + + try { + // Remove a single image derivative. + $pipeline->dispatch(ImageDerivativePipelineEvents::RESOLVE_DERIVATIVE_IMAGE_URI); + if ($pipeline->hasVariable('derivativeImageUri') && file_exists($pipeline->getVariable('derivativeImageUri'))) { + $this->fileSystem->delete($pipeline->getVariable('derivativeImageUri')); + } + } + catch (ImageProcessException $e) { + // Do nothing if derivative is non determinable. + } + } + + /** + * Flushes all image derivatives for the specified image style. + * + * @param \Drupal\image\Event\ImageStyleEvent $event + * The image style event. + */ + public function flushImageStyle(ImageStyleEvent $event): void { + $image_style = $event->getImageStyle(); + + // Delete the style directory in each registered wrapper. + $wrappers = $this->streamWrapperManager->getWrappers(StreamWrapperInterface::WRITE_VISIBLE); + foreach ($wrappers as $wrapper => $wrapper_data) { + if (file_exists($directory = $wrapper . '://styles/' . $image_style->id())) { + $this->fileSystem->deleteRecursive($directory); + } + } + + // Let other modules update as necessary on flush. + $this->moduleHandler->invokeAllDeprecated("is deprecated since version 9.x.x and will be removed in y.y.y.", 'image_style_flush', [$image_style]); + + // Clear caches so that formatters may be added for this style. + drupal_theme_rebuild(); + + Cache::invalidateTags($image_style->getCacheTagsToInvalidate()); + } + + /** + * Flushes all derivative versions of a specific file in all styles. + * + * @param \Drupal\image\Event\ImageStyleEvent $event + * The image style event. + */ + public function flushFromSourceImageUri(ImageStyleEvent $event): void { + foreach (ImageStyle::loadMultiple() as $style) { + $this->imageProcessor->createInstance('derivative') + ->setImageStyle($style) + ->setSourceImageUri($event->getArgument('sourceImageUri')) + ->dispatch(ImageDerivativePipelineEvents::REMOVE_DERIVATIVE_IMAGE); + } + } + +} diff --git a/core/modules/image/src/ImageProcessException.php b/core/modules/image/src/ImageProcessException.php new file mode 100644 index 0000000000..009a8218d7 --- /dev/null +++ b/core/modules/image/src/ImageProcessException.php @@ -0,0 +1,8 @@ +alterInfo('image_process_pipeline_plugin_info'); diff --git a/core/modules/image/src/ImageStyleInterface.php b/core/modules/image/src/ImageStyleInterface.php index 1006de7988..34a9d69430 100644 --- a/core/modules/image/src/ImageStyleInterface.php +++ b/core/modules/image/src/ImageStyleInterface.php @@ -89,8 +89,6 @@ public function getPathToken($uri); * image derivative will be flushed. * * @return $this - * - * @deprecated since version 9.x.x and will be removed in y.y.y. */ public function flush($path = NULL); diff --git a/core/modules/image/src/Plugin/ImageProcessPipeline/Core.php b/core/modules/image/src/Plugin/ImageProcessPipeline/Core.php deleted file mode 100644 index 582d2acfb1..0000000000 --- a/core/modules/image/src/Plugin/ImageProcessPipeline/Core.php +++ /dev/null @@ -1,699 +0,0 @@ -variables = new MemoryStorage('image_pipeline_variables'); - $this->imageFactory = $image_factory; - $this->streamWrapperManager = $stream_wrapper_manager; - $this->privateKey = $private_key->get(); - $this->moduleHandler = $module_handler; - $this->configFactory = $config_factory; - $this->currentRequest = $request_stack->getCurrentRequest(); - $this->logger = $logger; - $this->fileSystem = $file_system; - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { - return new static( - $configuration, - $plugin_id, - $plugin_definition, - $container->get('image.factory'), - $container->get('stream_wrapper_manager'), - $container->get('private_key'), - $container->get('module_handler'), - $container->get('config.factory'), - $container->get('request_stack'), - $container->get('logger.channel.image'), - $container->get('file_system') - ); - } - - /** - * Sets a variable to a specified value. - * - * @param string $variable - * The variable to set. - * @param mixed $value - * The value to set. - * - * @return $this - */ - public function setVariable(string $variable, $value): self { - $this->variables->set($variable, $value); - return $this; - } - - /** - * Returns the value of a variable. - * - * @return mixed - * The value of the variable. - * - * @throws \RuntimeException - * If the variable is not set. - */ - public function getVariable(string $variable) { - if (!$this->variables->has($variable)) { - throw new \RuntimeException("Variable {$variable} not set"); - } - return $this->variables->get($variable); - } - - /** - * Returns whether a variable is set. - * - * @return bool - * TRUE if the variable is set, FALSE otherwise. - */ - public function hasVariable(string $variable): bool { - return $this->variables->has($variable); - } - - /** - * - */ - public function runTask(string $task): void { - if (isset($this->pipelineState[$task])) { - switch ($task) { - case 'done': - return; - - case 'processing': - throw new \RuntimeException("$task is already in process"); - - } - } - $this->pipelineState[$task] = 'processing'; - - try { - $this->$task(); - } - catch (\RuntimeException $e) { - throw new \RuntimeException("Failed '$task': " . $e->getMessage(), $e->getCode(), $e); - } - - $this->pipelineState[$task] = 'done'; - } - - /** - * Sets the 'sourceImageUri' variable. - * - * @param string $uri - * The URI of the source image file. - * - * @return $this - */ - public function setSourceImageUri(string $uri): self { - $this->setVariable('sourceImageUri', $uri); - return $this; - } - - /** - * - */ - public function setSourceImageFileExtension(string $extension): self { - $this->setVariable('sourceImageFileExtension', $extension); - return $this; - } - - /** - * Sets the 'imageStyle' variable. - * - * @param \Drupal\image\ImageStyleInterface $image_style - * The ImageStyle config entity to use for derivative creation. - * - * @return $this - */ - public function setImageStyle(ImageStyleInterface $image_style): self { - $this->setVariable('imageStyle', $image_style); - return $this; - } - - /** - * Sets the 'image' variable. - * - * @param \Drupal\Core\Image\ImageInterface $image - * The ImageInterface object to be derived. - * - * @return $this - */ - public function setImage(ImageInterface $image): self { - if (!$this->hasVariable('sourceImageUri')) { - $this->setVariable('sourceImageUri', NULL); - } - $this->setVariable('image', $image); - return $this; - } - - /** - * Sets the 'imageDimensions' variable. - * - * @param array $dimensions - * The starting image dimensions as an associative array with the following - * keys: - * - 'width': Integer with the starting image width. - * - 'height': Integer with the starting image height. - * - * @return $this - */ - public function setSourceImageDimensions(?int $width, ?int $height): self { - $this->setVariable('sourceImageWidth', $width); - $this->setVariable('sourceImageHeight', $height); - return $this; - } - - /** - * - */ - public function setDerivedImageUri(string $uri): self { - $this->setVariable('derivedImageUri', $uri); - return $this; - } - - /** - * - */ - public function setDerivedImageCleanUrl(?bool $clean_url): self { - $this->setVariable('derivedImageCleanUrl', $clean_url); - return $this; - } - - /** - * - */ - public function isSourceImageProcessable(): bool { - $this->runTask('determineSourceImageProcessability'); - return $this->getVariable('isSourceImageProcessable'); - } - - /** - * - */ - public function getDerivedImageFileExtension(): string { - $this->runTask('determineDerivedImageFormat'); - return $this->getVariable('derivedImageFileExtension'); - } - - /** - * - */ - public function getDerivedImageWidth(): ?int { - $this->runTask('determineDerivedImageDimensions'); - return $this->getVariable('derivedImageWidth'); - } - - /** - * - */ - public function getDerivedImageHeight(): ?int { - $this->runTask('determineDerivedImageDimensions'); - return $this->getVariable('derivedImageHeight'); - } - - /** - * - */ - public function getDerivedImageUri(): string { - $this->runTask('determineDerivedImageUri'); - return $this->getVariable('derivedImageUri'); - } - - /** - * - */ - public function getDerivedImageUrl(): string { - $this->runTask('determineDerivedImageUrl'); - return $this->getVariable('derivedImageUrl'); - } - - /** - * - */ - public function getDerivedImageUrlSecurityToken(): ?string { - $this->runTask('determineDerivedImageUrlProtection'); - - if (!$this->suppressImageSecurityTokenOutput() && $this->hasVariable('derivedImageUrlProtection')) { - $token_query = $this->getVariable('derivedImageUrlProtection'); - return $token_query[IMAGE_DERIVATIVE_TOKEN]; - } - - return NULL; - } - - /** - * - */ - protected function determineSourceImageFormat(): void { - // Only support the URI if its extension is supported by the current image - // toolkit. - if (!$this->hasVariable('sourceImageFileExtension')) { - $this->setSourceImageFileExtension(mb_strtolower(pathinfo($this->getVariable('sourceImageUri'), PATHINFO_EXTENSION))); - } - } - - /** - * - */ - protected function determineSourceImageProcessability(): void { - $this->runTask('determineSourceImageFormat'); - - $this->setVariable('isSourceImageProcessable', in_array($this->getVariable('sourceImageFileExtension'), $this->imageFactory->getSupportedExtensions())); - } - - /** - * - */ - protected function determineDerivedImageFormat(): void { - $this->runTask('determineSourceImageProcessability'); - - if (!$this->getVariable('isSourceImageProcessable')) { - return; - } - - // Determine derived image file extension. - $extension = $this->getVariable('sourceImageFileExtension'); - foreach ($this->getVariable('imageStyle')->getEffects() as $effect) { - $extension = $effect->getDerivativeExtension($extension); - } - $this->setVariable('derivedImageFileExtension', $extension); - } - - /** - * - */ - protected function determineDerivedImageDimensions(): void { - $this->runTask('determineSourceImageProcessability'); - - // It's still possible to calculate dimensions even if the image at source - // is not processable but we have input dimensions. - if (!$this->getVariable('isSourceImageProcessable') && (!$this->hasVariable('sourceImageWidth') || !$this->hasVariable('sourceImageHeight'))) { - return; - } - - $dimensions = [ - 'width' => $this->getVariable('sourceImageWidth'), - 'height' => $this->getVariable('sourceImageHeight'), - ]; - foreach ($this->getVariable('imageStyle')->getEffects() as $effect) { - $effect->transformDimensions($dimensions, $this->getVariable('sourceImageUri')); - } - $this->setVariable('derivedImageWidth', $dimensions['width']); - $this->setVariable('derivedImageHeight', $dimensions['height']); - } - - /** - * - */ - protected function determineDerivedImageUri(): void { - $this->runTask('determineDerivedImageFormat'); - - if ($this->hasVariable('derivedImageUri')) { - return; - } - if (!$this->getVariable('isSourceImageProcessable')) { - return; - } - - // Determine derived image URI. - $source_scheme = $scheme = $this->fileUriScheme($this->getVariable('sourceImageUri')); - $default_scheme = $this->fileDefaultScheme(); - - if ($source_scheme) { - $path = $this->fileUriTarget($this->getVariable('sourceImageUri')); - // The scheme of derivative image files only needs to be computed for - // source files not stored in the default scheme. - if ($source_scheme != $default_scheme) { - $class = $this->streamWrapperManager->getClass($source_scheme); - $is_writable = NULL; - if ($class) { - $is_writable = $class::getType() & StreamWrapperInterface::WRITE; - } - - // Compute the derivative URI scheme. Derivatives created from writable - // source stream wrappers will inherit the scheme. Derivatives created - // from read-only stream wrappers will fall-back to the default scheme. - $scheme = $is_writable ? $source_scheme : $default_scheme; - } - } - else { - $path = $this->getVariable('sourceImageUri'); - $source_scheme = $scheme = $default_scheme; - } - $path = $this->getVariable('derivedImageFileExtension') === $this->getVariable('sourceImageFileExtension') ? $path : $path . '.' . $this->getVariable('derivedImageFileExtension'); - $this->setVariable('derivedImageUri', "$scheme://styles/{$this->getVariable('imageStyle')->id()}/$source_scheme/$path"); - } - - /** - * - */ - protected function determineDerivedImageUrlProtection(): void { - $this->runTask('determineDerivedImageUri'); - - // The token query is added even if the - // 'image.settings:allow_insecure_derivatives' configuration is TRUE, so - // that the emitted links remain valid if it is changed back to the default - // FALSE. However, sites which need to prevent the token query from being - // emitted at all can additionally set the - // 'image.settings:suppress_itok_output' configuration to TRUE to achieve - // that (if both are set, the security token will neither be emitted in the - // image derivative URL nor checked for in - // \Drupal\image\ImageStyleInterface::deliver()). - $token_query = []; - if (!$this->suppressImageSecurityTokenOutput()) { - // The sourceUri property can be either a relative path or a full URI. - $original_uri_normalised = $this->fileUriScheme($this->getVariable('sourceImageUri')) ? $this->streamWrapperManager->normalizeUri($this->getVariable('sourceImageUri')) : file_build_uri($this->getVariable('sourceImageUri')); - $cryptable_uri = $this->getVariable('derivedImageFileExtension') === $this->getVariable('sourceImageFileExtension') ? $original_uri_normalised : $original_uri_normalised . '.' . $this->getVariable('derivedImageFileExtension'); - // Return the first 8 characters. - $token_query = [IMAGE_DERIVATIVE_TOKEN => substr(Crypt::hmacBase64($this->getVariable('imageStyle')->id() . ':' . $cryptable_uri, $this->privateKey . $this->getHashSalt()), 0, 8)]; - $this->setVariable('derivedImageUrlProtection', $token_query); - } - } - - /** - * - */ - protected function determineDerivedImageUrl(): void { - $this->runTask('determineDerivedImageUri'); - $this->runTask('determineDerivedImageUrlProtection'); - - $clean_urls = $this->getVariable('derivedImageCleanUrl'); - $derivative_uri = $this->getVariable('derivedImageUri'); - $token_query = $this->hasVariable('derivedImageUrlProtection') ? $this->getVariable('derivedImageUrlProtection') : []; - - // Determine whether clean URLs can be used. - if ($clean_urls === NULL) { - // Assume clean URLs unless the request tells us otherwise. - $clean_urls = TRUE; - try { - $clean_urls = RequestHelper::isCleanUrl($this->currentRequest); - } - catch (ServiceNotFoundException $e) { - } - } - - // If not using clean URLs, the image derivative callback is only available - // with the script path. If the file does not exist, use Url::fromUri() to - // ensure that it is included. Once the file exists it's fine to fall back - // to the actual file path, this avoids bootstrapping PHP once the files are - // built. - if ($clean_urls === FALSE && $this->streamWrapperManager->getScheme($derivative_uri) == 'public' && !file_exists($derivative_uri)) { - $directory_path = $this->streamWrapperManager->getViaUri($derivative_uri)->getDirectoryPath(); - $url = Url::fromUri('base:' . $directory_path . '/' . $this->streamWrapperManager->getTarget($derivative_uri), ['absolute' => TRUE, 'query' => $token_query])->toString(); - $this->setVariable('derivedImageUrl', $url); - return; - } - - // Append the query string with the token, if necessary. - $file_url = file_create_url($derivative_uri); - if ($token_query) { - $file_url .= (strpos($file_url, '?') !== FALSE ? '&' : '?') . UrlHelper::buildQuery($token_query); - } - - $this->setVariable('derivedImageUrl', $file_url); - } - - /** - * - */ - protected function loadSourceImage(): void { - if ($this->hasVariable('sourceImageUri') && !$this->hasVariable('image')) { - // If the source file doesn't exist, return FALSE without creating folders. - $image = $this->imageFactory->get($this->getVariable('sourceImageUri')); - if (!$image->isValid()) { - return; - } - $this->setImage($image); - } - } - - /** - * - */ - protected function saveDerivedImage(): bool { - $this->runTask('determineDerivedImageUri'); - - // Get the folder for the final location of this style. - $directory = $this->fileSystem->dirname($this->getVariable('derivedImageUri')); - - // Build the destination folder tree if it doesn't already exist. - if (!$this->fileSystem->prepareDirectory($directory, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS)) { - $this->logger->error('Failed to create style directory: %directory', ['%directory' => $directory]); - return FALSE; - } - - if (!$this->getVariable('image')->save($this->getVariable('derivedImageUri'))) { - if (file_exists($this->getVariable('derivedImageUri'))) { - $this->logger->error('Cached image file %destination already exists. There may be an issue with your rewrite configuration.', ['%destination' => $this->getVariable('derivedImageUri')]); - } - return FALSE; - } - - return TRUE; - } - - /** - * {@inheritdoc} - */ - public function removeDerivedImagesFromStorage(): void { - if ($this->hasVariable('sourceImageUri')) { - // Remove a single image derivative. - $this->runTask('determineDerivedImageUri'); - - if ($this->hasVariable('derivedImageUri')) { - $derivative_uri = $this->getVariable('derivedImageUri'); - if (file_exists($derivative_uri)) { - $this->fileSystem->delete($derivative_uri); - } - } - } - else { - // Delete the style directory in each registered wrapper. - $wrappers = $this->streamWrapperManager->getWrappers(StreamWrapperInterface::WRITE_VISIBLE); - foreach ($wrappers as $wrapper => $wrapper_data) { - if (file_exists($directory = $wrapper . '://styles/' . $this->getVariable('imageStyle')->id())) { - $this->fileSystem->deleteRecursive($directory); - } - } - - // Let other modules update as necessary on flush. - $this->moduleHandler->invokeAllDeprecated("is deprecated since version 9.x.x and will be removed in y.y.y.", 'image_style_flush', [$this->getVariable('imageStyle')]); - - // Clear caches so that formatters may be added for this style. - drupal_theme_rebuild(); - - Cache::invalidateTags($this->getVariable('imageStyle')->getCacheTagsToInvalidate()); - } - } - - /** - * {@inheritdoc} - */ - public function produceDerivedImage(): bool { - $this->loadSourceImage(); - - // Apply the image effects to the image object. - foreach ($this->getVariable('imageStyle')->getEffects() as $effect) { - $effect->applyEffect($this->getVariable('image')); - } - - return $this->saveDerivedImage(); - } - - /** - * Gets a salt useful for hardening against SQL injection. - * - * @return string - * A salt based on information in settings.php, not in the database. - * - * @throws \RuntimeException - */ - protected function getHashSalt() { - return Settings::getHashSalt(); - } - - /** - * Provides a wrapper for file_uri_scheme() to allow unit testing. - * - * Returns the scheme of a URI (e.g. a stream). - * - * @param string $uri - * A stream, referenced as "scheme://target" or "data:target". - * - * @return string - * A string containing the name of the scheme, or FALSE if none. For - * example, the URI "public://example.txt" would return "public". - */ - protected function fileUriScheme($uri) { - return $this->streamWrapperManager->getScheme($uri); - } - - /** - * Provides a wrapper for file_uri_target() to allow unit testing. - * - * Returns the part of a URI after the schema. - * - * @param string $uri - * A stream, referenced as "scheme://target" or "data:target". - * - * @return string|bool - * A string containing the target (path), or FALSE if none. - * For example, the URI "public://sample/test.txt" would return - * "sample/test.txt". - */ - protected function fileUriTarget($uri) { - return $this->streamWrapperManager->getTarget($uri); - } - - /** - * Provides a wrapper for file_default_scheme() to allow unit testing. - * - * Gets the default file stream implementation. - * - * @return string - * 'public', 'private' or any other file scheme defined as the default. - */ - protected function fileDefaultScheme() { - return $this->configFactory->get('system.file')->get('default_scheme'); - } - - /** - * - */ - protected function suppressImageSecurityTokenOutput() { - return $this->configFactory->get('image.settings')->get('suppress_itok_output'); - } - -} diff --git a/core/modules/image/src/Plugin/ImageProcessPipeline/Derivative.php b/core/modules/image/src/Plugin/ImageProcessPipeline/Derivative.php new file mode 100644 index 0000000000..426fd4fc26 --- /dev/null +++ b/core/modules/image/src/Plugin/ImageProcessPipeline/Derivative.php @@ -0,0 +1,247 @@ +setVariable('imageToolkitId', $toolkit_id); + return $this; + } + + /** + * Sets the 'sourceImageUri' variable. + * + * @param string $uri + * The URI of the source image file. + * + * @return $this + */ + public function setSourceImageUri(string $uri): self { + $this->setVariable('sourceImageUri', $uri); + return $this; + } + + /** + * Sets the 'sourceImageFileExtension' variable. + * + * Normally this method should not be called, the pipeline will determine the + * image file extension based on the source URI. This method will override + * that. + * + * @param string $extension + * An image file extension. + * + * @return $this + */ + public function setSourceImageFileExtension(string $extension): self { + $this->setVariable('sourceImageFileExtension', $extension); + return $this; + } + + /** + * Sets the 'imageStyle' variable. + * + * @param \Drupal\image\ImageStyleInterface $image_style + * The ImageStyle config entity to use for derivative creation. + * + * @return $this + */ + public function setImageStyle(ImageStyleInterface $image_style): self { + $this->setVariable('imageStyle', $image_style); + return $this; + } + + /** + * Sets the 'image' variable. + * + * @param \Drupal\Core\Image\ImageInterface $image + * The ImageInterface object to be derived. + * + * @return $this + */ + public function setImage(ImageInterface $image): self { + if (!$this->hasVariable('sourceImageUri')) { + $this->setVariable('sourceImageUri', NULL); + } + $this->setVariable('image', $image); + return $this; + } + + /** + * Sets the 'sourceImageWidth' and 'sourceImageHeight' variables. + * + * @param int|null $width + * (Optional) Integer with the starting image width. + * @param int|null $height + * (Optional) Integer with the starting image height. + * + * @return $this + */ + public function setSourceImageDimensions(?int $width, ?int $height): self { + $this->setVariable('sourceImageWidth', $width); + $this->setVariable('sourceImageHeight', $height); + return $this; + } + + /** + * Sets the 'derivativeImageUri' variable. + * + * Normally this method should not be called, the pipeline will determine the + * derivative URI based on the source URI. This method will override that. + * + * @param string $uri + * Derivative image file URI. + * + * @return $this + */ + public function setDerivativeImageUri(string $uri): self { + $this->setVariable('derivativeImageUri', $uri); + return $this; + } + + /** + * Sets the 'setCleanUrl' variable. + * + * @param bool|null $clean_url + * (Optional) Whether clean URLs are in use. + * + * @return $this + */ + public function setCleanUrl(?bool $clean_url): self { + $this->setVariable('setCleanUrl', $clean_url); + return $this; + } + + /** + * Determines if the source image at URI can be derived. + * + * Takes the source URI and the image style to determine if the image file + * can be loaded, transformed and saved as a derivative image. + * + * @return bool + * TRUE if the image is supported, FALSE otherwise. + */ + public function isSourceImageProcessable(): bool { + $this->dispatch(ImageDerivativePipelineEvents::RESOLVE_SOURCE_IMAGE_PROCESSABILITY); + return $this->getVariable('isSourceImageProcessable'); + } + + /** + * Determines the extension of the derivative image. + * + * @return string + * The extension the derivative image will have, given the extension of the + * original. + */ + public function getDerivativeImageFileExtension(): string { + $this->dispatch(ImageDerivativePipelineEvents::RESOLVE_DERIVATIVE_IMAGE_FORMAT); + return $this->getVariable('derivativeImageFileExtension'); + } + + /** + * Determines the width of the derivative image. + * + * @return int|null + * The width of the derivative image, or NULL if it cannot be calculated. + */ + public function getDerivativeImageWidth(): ?int { + $this->dispatch(ImageDerivativePipelineEvents::RESOLVE_DERIVATIVE_IMAGE_DIMENSIONS); + return $this->getVariable('derivativeImageWidth'); + } + + /** + * Determines the height of the derivative image. + * + * @return int|null + * The height of the derivative image, or NULL if it cannot be calculated. + */ + public function getDerivativeImageHeight(): ?int { + $this->dispatch(ImageDerivativePipelineEvents::RESOLVE_DERIVATIVE_IMAGE_DIMENSIONS); + return $this->getVariable('derivativeImageHeight'); + } + + /** + * Returns the URI of the derivative image file. + * + * Takes the source URI and the image style to determine the derivative URI. + * The path returned by this function may not exist. The default generation + * method only creates images when they are requested by a user's browser. + * Plugins may implement this method to decide where to place derivatives. + * + * @return string + * The URI to the image derivative for this style. + */ + public function getDerivativeImageUri(): string { + $this->dispatch(ImageDerivativePipelineEvents::RESOLVE_DERIVATIVE_IMAGE_URI); + return $this->getVariable('derivativeImageUri'); + } + + /** + * Returns the URL of the derivative image file. + * + * Takes the source URI and the image style to determine the derivative URL. + * + * @return string|\Drupal\Core\Url + * The absolute URL where a style image can be downloaded, suitable for use + * in an tag. + * + * @see \Drupal\image\Controller\ImageStyleDownloadController::deliver() + * @see file_url_transform_relative() + */ + public function getDerivativeImageUrl() { + $this->dispatch(ImageDerivativePipelineEvents::RESOLVE_DERIVATIVE_IMAGE_URL); + return $this->getVariable('derivativeImageUrl'); + } + + /** + * Returns a token to protect an image style derivative. + * + * This prevents unauthorized generation of an image style derivative, + * which can be costly both in CPU time and disk space. + * + * @return string + * An eight-character token which can be used to protect image style + * derivatives against denial-of-service attacks. + */ + public function getDerivativeImageUrlSecurityToken(): ?string { + $this->dispatch(ImageDerivativePipelineEvents::RESOLVE_DERIVATIVE_IMAGE_URL_PROTECTION); + return $this->hasVariable('derivativeImageUrlProtection') ? $this->getVariable('derivativeImageUrlProtection')[IMAGE_DERIVATIVE_TOKEN] : NULL; + } + + /** + * Transform an image based on the image style settings. + * + * Generates an image derivative applying all image effects. Takes the source + * URI or an ImageInterface object and the image style to process the image. + * + * @return bool + * TRUE if the image was transformed, or FALSE in case of failure. + */ + public function buildDerivativeImage(): bool { + $this->deleteVariable('derivativeImageBuilt'); + $this->dispatch(ImageDerivativePipelineEvents::BUILD_DERIVATIVE_IMAGE); + return $this->getVariable('derivativeImageBuilt'); + } + +} diff --git a/core/modules/image/src/Plugin/ImageProcessPipeline/ImageProcessPipelineBase.php b/core/modules/image/src/Plugin/ImageProcessPipeline/ImageProcessPipelineBase.php new file mode 100644 index 0000000000..286bd4c885 --- /dev/null +++ b/core/modules/image/src/Plugin/ImageProcessPipeline/ImageProcessPipelineBase.php @@ -0,0 +1,108 @@ +variables = new MemoryStorage('image_pipeline_variables'); + $this->eventDispatcher = $dispatcher; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('event_dispatcher') + ); + } + + /** + * {@inheritdoc} + */ + public function setVariable(string $variable, $value): ImageProcessPipelineInterface { + $this->variables->set($variable, $value); + return $this; + } + + /** + * {@inheritdoc} + */ + public function getVariable(string $variable) { + if (!$this->variables->has($variable)) { + throw new ImageProcessException("Variable {$variable} not set"); + } + return $this->variables->get($variable); + } + + /** + * {@inheritdoc} + */ + public function hasVariable(string $variable): bool { + return $this->variables->has($variable); + } + + /** + * {@inheritdoc} + */ + public function deleteVariable(string $variable): ImageProcessPipelineInterface { + $this->variables->delete($variable); + return $this; + } + + /** + * {@inheritdoc} + */ + public function dispatch(string $event, array $arguments = []): ImageProcessPipelineInterface { + try { + $this->eventDispatcher->dispatch($event, new ImageProcessEvent($this, $arguments)); + } + catch (ImageProcessException $e) { + throw new ImageProcessException("Failure processing '$event': " . $e->getMessage(), $e->getCode(), $e); + } + return $this; + } + +} diff --git a/core/modules/image/src/Plugin/ImageProcessPipelinePluginInterface.php b/core/modules/image/src/Plugin/ImageProcessPipelinePluginInterface.php deleted file mode 100644 index ce5c324cfe..0000000000 --- a/core/modules/image/src/Plugin/ImageProcessPipelinePluginInterface.php +++ /dev/null @@ -1,149 +0,0 @@ - tag. Requesting the URL will cause the image to be created. - * - * @see \Drupal\image\Controller\ImageStyleDownloadController::deliver() - * @see file_url_transform_relative() - */ - //public function buildDerivativeUrl(); - - /** - * Flushes an image derivative for the specified image style. - * - * Takes the source URI and the image style to determine the derivative to be - * removed. - * - * @return bool - * TRUE if all the image derivatives were removed from the file system, and - * the image style cache tags were invalidated. - */ -// public function removeSourceUriDerivative(); - - /** - * Flushes all image derivatives for the specified image style. - * - * @return bool - * TRUE if all the image derivatives were removed from the file system, and - * the image style cache tags were invalidated. - */ -// public function removeAllImageStyleDerivatives(); - - /** - * Transform an image based on the image style settings. - * - * Generates an image derivative applying all image effects, without saving - * the result to the file system. Takes the source URI or an ImageInterface - * object and the image style to process the image. - * - * @return bool - * TRUE if the image was transformed, or FALSE in case of failure. - * - * @see \Drupal\image\Plugin\ImageProcessPipelinePluginInterface::saveImage() - */ - //public function transformImage(); - - /** - * Saves a transformed image to the derivative URI. - * - * @param string $uri - * Derivative image file URI. - * - * @return bool - * TRUE if the image derivative was saved, or FALSE in case of failure. - * - * @see \Drupal\image\Plugin\ImageProcessPipelinePluginInterface::transformImage() - */ - //public function saveImage($uri); - - /** - * Determines the dimensions of the derivative image. - * - * Takes the source URI, the image style, and the starting dimensions to - * determine the expected dimensions of the derivative image. The source URI - * is used to allow effects to optionally use this information to retrieve - * additional image metadata to determine output dimensions. The key - * objective is to calculate derivative image dimensions without performing - * actual image operations, so be aware that performing I/O on the URI may - * lead to decrease in performance. - * The starting dimensions are defined through ::setImageDimensions as an - * associative array. Implementations have to provide values for the - * following keys: - * - 'width': Integer with the starting image width. - * - 'height': Integer with the starting image height. - * - * @see ImageEffectInterface::transformDimensions - * @see \Drupal\image\Plugin\ImageProcessPipelinePluginInterface::setImageDimensions() - */ - //public function getDerivativeImageDimensions(); - - /** - * Determines the extension of the derivative image. - * - * @param string $extension - * The file extension of the original image. - * - * @return string - * The extension the derivative image will have, given the extension of the - * original. - */ - //public function getDerivedImageFileExtension($extension); - - /** - * Generates a token to protect an image style derivative. - * - * This prevents unauthorized generation of an image style derivative, - * which can be costly both in CPU time and disk space. - * - * @param string $uri - * The URI of the original image of this style. - * - * @return string - * An eight-character token which can be used to protect image style - * derivatives against denial-of-service attacks. - */ - //public function getPathToken($uri); - - /** - * Determines if the source image at URI can be derived. - * - * Takes the source URI and the image style to determine if the image file - * can be loaded, transformed and saved as a derivative image. - * - * @return bool - * TRUE if the image is supported, FALSE otherwise. - */ -// public function isSourceImageProcessable(); - -} diff --git a/core/modules/image/tests/src/Functional/FileMoveTest.php b/core/modules/image/tests/src/Functional/FileMoveTest.php index d070bfc4b6..c2a3a1bbe4 100644 --- a/core/modules/image/tests/src/Functional/FileMoveTest.php +++ b/core/modules/image/tests/src/Functional/FileMoveTest.php @@ -61,50 +61,4 @@ public function testNormal() { $this->assertFalse(file_exists($derivative_uri), 'Make sure derivative image has been flushed.'); } - /** - * Tests creating a derivative from scratch. - */ - public function testBuildDerivativeFromImageObject() { - // Create scratch image. - $image = \Drupal::service('image.factory')->get(); - $this->assertSame('', $image->getSource()); - $this->assertSame('', $image->getMimeType()); - $this->assertNull($image->getFileSize()); - $image->createNew(600, 300, 'png'); - $this->assertSame('', $image->getSource()); - $this->assertSame('image/png', $image->getMimeType()); - $this->assertNull($image->getFileSize()); - - // Create derivative. - $pipeline = \Drupal::service('image.processor')->createInstance('core'); - $style = ImageStyle::load('medium'); - $desired_filepath = 'public://' . $this->randomMachineName(); - $derivative_uri = $desired_filepath . '/test_0.png'; - $pipeline - ->setImageStyle($style) - ->setImage($image) - ->setSourceImageFileExtension('png') - ->setDerivedImageUri($derivative_uri); - $this->assertTrue($pipeline->produceDerivedImage()); - - // Check if derivative image exists. - $this->assertTrue(file_exists($derivative_uri)); - - // Check derivative image after saving, with old object. - $this->assertSame(220, $image->getWidth()); - $this->assertSame(110, $image->getHeight()); - $this->assertSame($derivative_uri, $image->getSource()); - $this->assertSame('image/png', $image->getMimeType()); - $file_size = $image->getFileSize(); - $this->assertGreaterThan(0, $file_size); - - // Check derivative image after reloading from saved image file. - $image_r = \Drupal::service('image.factory')->get($derivative_uri); - $this->assertSame(220, $image_r->getWidth()); - $this->assertSame(110, $image_r->getHeight()); - $this->assertSame($derivative_uri, $image_r->getSource()); - $this->assertSame('image/png', $image_r->getMimeType()); - $this->assertSame($file_size, $image_r->getFileSize()); - } - } diff --git a/core/modules/image/tests/src/Kernel/DerivativeImageProcessTest.php b/core/modules/image/tests/src/Kernel/DerivativeImageProcessTest.php new file mode 100644 index 0000000000..3feb935062 --- /dev/null +++ b/core/modules/image/tests/src/Kernel/DerivativeImageProcessTest.php @@ -0,0 +1,227 @@ +installConfig(['system', 'image']); + $this->imageProcessor = \Drupal::service('image.processor'); + $this->imageStyle = ImageStyle::load('thumbnail'); + \Drupal::service('file_system')->copy('core/tests/fixtures/files/image-1.png', 'public://test.png'); + } + + /** + * @covers ::setImageStyle + * @covers ::setSourceImageUri + * @covers ::isSourceImageProcessable + */ + public function testIsSourceImageProcessable() { + // Starting off from a valid image file. + $pipeline = $this->imageProcessor->createInstance('derivative') + ->setImageStyle($this->imageStyle) + ->setSourceImageUri('public://test.png'); + $this->assertTrue($pipeline->isSourceImageProcessable()); + + // Starting off from non-image file. + $pipeline = $this->imageProcessor->createInstance('derivative') + ->setImageStyle($this->imageStyle) + ->setSourceImageUri('public://test.csv'); + $this->assertFalse($pipeline->isSourceImageProcessable()); + } + + /** + * @covers ::setImageStyle + * @covers ::setSourceImageUri + * @covers ::setSourceImageFileExtension + * @covers ::getDerivativeImageFileExtension + */ + public function testGetDerivativeImageFileExtension() { + // Starting off from a real source. + $pipeline = $this->imageProcessor->createInstance('derivative') + ->setImageStyle($this->imageStyle) + ->setSourceImageUri('public://test.png'); + $this->assertSame('png', $pipeline->getDerivativeImageFileExtension()); + + // Starting off from the image file extension. + $pipeline = $this->imageProcessor->createInstance('derivative') + ->setImageStyle($this->imageStyle) + ->setSourceImageFileExtension('jpg'); + $this->assertSame('jpg', $pipeline->getDerivativeImageFileExtension()); + } + + /** + * @covers ::setImageStyle + * @covers ::setSourceImageUri + * @covers ::setSourceImageDimensions + * @covers ::getDerivativeImageWidth + * @covers ::getDerivativeImageHeight + */ + public function testGetDerivativeImageDimensions() { + // Starting off from a real source. + $pipeline = $this->imageProcessor->createInstance('derivative') + ->setImageStyle($this->imageStyle) + ->setSourceImageUri('public://test.png') + ->setSourceImageDimensions(360, 240); + $this->assertSame(100, $pipeline->getDerivativeImageWidth()); + $this->assertSame(67, $pipeline->getDerivativeImageHeight()); + + // Starting off from a non-existent source, only dimensions. + $pipeline = $this->imageProcessor->createInstance('derivative') + ->setImageStyle($this->imageStyle) + ->setSourceImageUri('') + ->setSourceImageDimensions(100, 200); + $this->assertSame(50, $pipeline->getDerivativeImageWidth()); + $this->assertSame(100, $pipeline->getDerivativeImageHeight()); + } + + /** + * @covers ::setImageStyle + * @covers ::setSourceImageUri + * @covers ::getDerivativeImageUri + */ + public function testGetDerivativeImageUri() { + // Starting off from an URI. + $pipeline = $this->imageProcessor->createInstance('derivative') + ->setImageStyle($this->imageStyle) + ->setSourceImageUri('public://test.png'); + $this->assertEquals('public://styles/thumbnail/public/test.png', $pipeline->getDerivativeImageUri()); + + // Starting off from a path. + $pipeline = $this->imageProcessor->createInstance('derivative') + ->setImageStyle($this->imageStyle) + ->setSourceImageUri('core/modules/image/sample.png'); + $this->assertEquals('public://styles/thumbnail/public/core/modules/image/sample.png', $pipeline->getDerivativeImageUri()); + } + + /** + * @covers ::setImageStyle + * @covers ::setSourceImageUri + * @covers ::getDerivativeImageUrl + * @covers ::getDerivativeImageUrlSecurityToken + */ + public function testGetDerivativeImageUrl() { + // Starting off from an URI. + $pipeline = $this->imageProcessor->createInstance('derivative') + ->setImageStyle($this->imageStyle) + ->setSourceImageUri('public://test.png'); + $this->assertContains('files/styles/thumbnail/public/test.png?itok=' . $pipeline->getDerivativeImageUrlSecurityToken(), $pipeline->getDerivativeImageUrl()); + + // Starting off from a path. + $pipeline = $this->imageProcessor->createInstance('derivative') + ->setImageStyle($this->imageStyle) + ->setSourceImageUri('core/modules/image/sample.png'); + $this->assertContains('files/styles/thumbnail/public/core/modules/image/sample.png?itok=' . $pipeline->getDerivativeImageUrlSecurityToken(), $pipeline->getDerivativeImageUrl()); + } + + /** + * @covers ::setImageStyle + * @covers ::setSourceImageUri + * @covers ::buildDerivativeImage + */ + public function testBuildDerivativeImageFromFile() { + // Starting off from an URI. + $pipeline = $this->imageProcessor->createInstance('derivative') + ->setImageStyle($this->imageStyle) + ->setSourceImageUri('public://test.png'); + $this->assertTrue($pipeline->buildDerivativeImage()); + $this->assertTrue(file_exists('public://styles/thumbnail/public/test.png')); + $image = \Drupal::service('image.factory')->get('public://styles/thumbnail/public/test.png'); + $this->assertSame(100, $image->getWidth()); + $this->assertSame(67, $image->getHeight()); + + // Starting off from a path. + $pipeline = $this->imageProcessor->createInstance('derivative') + ->setImageStyle($this->imageStyle) + ->setSourceImageUri('core/modules/image/sample.png'); + $this->assertTrue($pipeline->buildDerivativeImage()); + $this->assertTrue(file_exists('public://styles/thumbnail/public/core/modules/image/sample.png')); + $image = \Drupal::service('image.factory')->get('public://styles/thumbnail/public/core/modules/image/sample.png'); + $this->assertSame(100, $image->getWidth()); + $this->assertSame(75, $image->getHeight()); + } + + /** + * Tests creating a derivative straight from an Image object. + * + * @covers ::setImageStyle + * @covers ::setImage + * @covers ::setSourceImageFileExtension + * @covers ::setDerivativeImageUri + * @covers ::buildDerivativeImage + */ + public function testBuildDerivativeImageFromImageObject() { + // Create scratch image. + $image = \Drupal::service('image.factory')->get(); + $this->assertSame('', $image->getSource()); + $this->assertSame('', $image->getMimeType()); + $this->assertNull($image->getFileSize()); + $image->createNew(600, 450, 'png'); + $this->assertSame('', $image->getSource()); + $this->assertSame('image/png', $image->getMimeType()); + $this->assertNull($image->getFileSize()); + + // Create derivative. + $pipeline = $this->imageProcessor->createInstance('derivative'); + $derivative_uri = 'public://test_0.png'; + $pipeline + ->setImageStyle($this->imageStyle) + ->setImage($image) + ->setSourceImageFileExtension('png') + ->setDerivativeImageUri($derivative_uri); + $this->assertTrue($pipeline->buildDerivativeImage()); + + // Check if derivative image exists. + $this->assertTrue(file_exists($derivative_uri)); + + // Check derivative image after saving, with old object. + $this->assertSame(100, $image->getWidth()); + $this->assertSame(75, $image->getHeight()); + $this->assertSame($derivative_uri, $image->getSource()); + $this->assertSame('image/png', $image->getMimeType()); + $file_size = $image->getFileSize(); + $this->assertGreaterThan(0, $file_size); + + // Check derivative image after reloading from saved image file. + $image_r = \Drupal::service('image.factory')->get($derivative_uri); + $this->assertSame(100, $image_r->getWidth()); + $this->assertSame(75, $image_r->getHeight()); + $this->assertSame($derivative_uri, $image_r->getSource()); + $this->assertSame('image/png', $image_r->getMimeType()); + $this->assertSame($file_size, $image_r->getFileSize()); + } + +} diff --git a/core/modules/image/tests/src/Kernel/ImageStyleTest.php b/core/modules/image/tests/src/Kernel/ImageStyleLegacyTest.php similarity index 90% rename from core/modules/image/tests/src/Kernel/ImageStyleTest.php rename to core/modules/image/tests/src/Kernel/ImageStyleLegacyTest.php index fef43a4998..3f6e7700ef 100644 --- a/core/modules/image/tests/src/Kernel/ImageStyleTest.php +++ b/core/modules/image/tests/src/Kernel/ImageStyleLegacyTest.php @@ -4,16 +4,16 @@ use Drupal\KernelTests\KernelTestBase; use Drupal\image\Entity\ImageStyle; -use Drupal\image\ImageStyleInterface; /** * Legacy test for deprecated ImageStyle methods. * * @coversDefaultClass \Drupal\image\Entity\ImageStyle * + * @group image * @group legacy */ -class ImageStyleTest extends KernelTestBase { +class ImageStyleLegacyTest extends KernelTestBase { /** * Modules to enable. @@ -61,14 +61,6 @@ public function testBuildUrl() { $this->assertContains('files/styles/test/public/test.png?itok=', $this->imageStyle->buildUrl('public://test.png')); } - /** - * @covers ::flush - * @expectedDeprecation The Drupal\image\Entity\ImageStyle::flush method is deprecated since version 9.x.x and will be removed in y.y.y. - */ - public function testFlush() { - $this->assertInstanceOf(ImageStyleInterface::class, $this->imageStyle->flush()); - } - /** * @covers ::createDerivative * @expectedDeprecation The Drupal\image\Entity\ImageStyle::createDerivative method is deprecated since version 9.x.x and will be removed in y.y.y. diff --git a/core/modules/image/tests/src/Unit/CoreImageProcessPipelinePluginTest.php b/core/modules/image/tests/src/Unit/CoreImageProcessPipelinePluginTest.php deleted file mode 100644 index 1a80427559..0000000000 --- a/core/modules/image/tests/src/Unit/CoreImageProcessPipelinePluginTest.php +++ /dev/null @@ -1,295 +0,0 @@ -getMockBuilder(ImageEffectManager::class) - ->disableOriginalConstructor() - ->getMock(); - $effectManager->expects($this->any()) - ->method('createInstance') - ->with('testEffectId') - ->will($this->returnValue($image_effect)); - - $default_stubs = [ - 'getImageEffectPluginManager', - 'id', - ]; - $image_style = $this->getMockBuilder(ImageStyle::class) - ->setConstructorArgs([ - ['effects' => ['testEffectId' => ['id' => 'testEffectId']]], - 'testImageStyleEntityId', - ]) - ->setMethods(array_merge($default_stubs, $stubs)) - ->getMock(); - - $image_style->expects($this->any()) - ->method('getImageEffectPluginManager') - ->will($this->returnValue($effectManager)); - $image_style->expects($this->any()) - ->method('id') - ->will($this->returnValue('testImageStyleEntityId')); - - return $image_style; - } - - /** - * Gets a mocked Core ImageProcessPipelinePlugin for testing. - * - * @return \Drupal\image\ImageStyleInterface - * The mocked image style. - */ - protected function getCoreImageProcessPipelinePluginMock($stubs = []) { - $image_factory = $this->getMockBuilder('\Drupal\Core\Image\ImageFactory') - ->disableOriginalConstructor() - ->getMock(); - $image_factory->expects($this->any()) - ->method('getSupportedExtensions') - ->will($this->returnValue(['gif', 'png', 'jpeg'])); - - $private_key = $this->getMockBuilder('\Drupal\Core\PrivateKey') - ->disableOriginalConstructor() - ->getMock(); - $private_key->expects($this->once()) - ->method('get') - ->will($this->returnValue('testPrivateKey')); - - $default_stubs = [ - 'getHashSalt', - 'fileUriScheme', - 'fileUriTarget', - 'fileDefaultScheme', - 'suppressImageSecurityTokenOutput', - ]; - $pipeline = $this->getMockBuilder('\Drupal\image\Plugin\ImageProcessPipeline\Core') - ->setConstructorArgs([ - [], - 'core', - [], - $image_factory, - $this->createMock('\Drupal\Core\StreamWrapper\StreamWrapperManagerInterface'), - $private_key, - $this->createMock('\Drupal\Core\Extension\ModuleHandlerInterface'), - $this->createMock('\Drupal\Core\Config\ConfigFactoryInterface'), - $this->createMock('\Symfony\Component\HttpFoundation\RequestStack'), - $this->createMock('\Psr\Log\LoggerInterface'), - $this->createMock('\Drupal\Core\File\FileSystemInterface'), - ]) - ->setMethods(array_merge($default_stubs, $stubs)) - ->getMock(); - $pipeline->expects($this->any()) - ->method('getHashSalt') - ->will($this->returnValue('testHashSalt')); - $pipeline->expects($this->any()) - ->method('fileDefaultScheme') - ->will($this->returnValue('public')); - $pipeline->expects($this->any()) - ->method('fileUriScheme') - ->will($this->returnCallback([$this, 'fileUriScheme'])); - $pipeline->expects($this->any()) - ->method('fileUriTarget') - ->will($this->returnCallback([$this, 'fileUriTarget'])); - $pipeline->expects($this->any()) - ->method('suppressImageSecurityTokenOutput') - ->will($this->returnValue(FALSE)); - - return $pipeline; - } - - /** - * @covers ::getDerivedImageUri - */ - public function testGetDerivedImageUri() { - // Image style that changes the extension. - $logger = $this->getMockBuilder('\Psr\Log\LoggerInterface')->getMock(); - $image_effect = $this->getMockBuilder('\Drupal\image\ImageEffectBase') - ->setConstructorArgs([[], 'testEffectId', [], $logger]) - ->getMock(); - $image_effect->expects($this->any()) - ->method('getDerivativeExtension') - ->will($this->returnValue('png')); - $image_style = $this->getImageStyleMock($image_effect); - - $pipeline = $this->getCoreImageProcessPipelinePluginMock(); - $pipeline->setImageStyle($image_style)->setSourceImageUri('public://test.jpeg'); - $this->assertEquals('public://styles/testImageStyleEntityId/public/test.jpeg.png', $pipeline->getDerivedImageUri()); - - // Image style that doesn't change the extension. - $logger = $this->getMockBuilder('\Psr\Log\LoggerInterface')->getMock(); - $image_effect = $this->getMockBuilder('\Drupal\image\ImageEffectBase') - ->setConstructorArgs([[], 'testEffectId', [], $logger]) - ->getMock(); - $image_effect->expects($this->any()) - ->method('getDerivativeExtension') - ->will($this->returnArgument(0)); - $image_style = $this->getImageStyleMock($image_effect); - - $pipeline = $this->getCoreImageProcessPipelinePluginMock(); - $pipeline->setImageStyle($image_style)->setSourceImageUri('public://test.jpeg'); - $this->assertEquals('public://styles/testImageStyleEntityId/public/test.jpeg', $pipeline->getDerivedImageUri()); - } - - /** - * @covers ::getDerivedImageFileExtension - */ - public function testGetDerivedImageFileExtension() { - // Image style that changes the extension. - $logger = $this->getMockBuilder('\Psr\Log\LoggerInterface')->getMock(); - $image_effect = $this->getMockBuilder('\Drupal\image\ImageEffectBase') - ->setConstructorArgs([[], 'testEffectId', [], $logger]) - ->getMock(); - $image_effect->expects($this->any()) - ->method('getDerivativeExtension') - ->will($this->returnValue('png')); - $image_style = $this->getImageStyleMock($image_effect); - - // The mock ImageEffect::getDerivativeExtension method is always returning - // 'png'. - $extensions = ['jpeg', 'gif', 'png']; - foreach ($extensions as $extension) { - $pipeline = $this->getCoreImageProcessPipelinePluginMock(); - $pipeline - ->setSourceImageFileExtension($extension) - ->setImageStyle($image_style); - $this->assertSame('png', $pipeline->getDerivedImageFileExtension()); - } - } - - /** - * @covers ::getDerivedImageWidth - * @covers ::getDerivedImageHeight - */ - public function testGetDerivedImageDimensions() { - $logger = $this->getMockBuilder('\Psr\Log\LoggerInterface')->getMock(); - $image_effect = $this->getMockBuilder('\Drupal\image\ImageEffectBase') - ->setConstructorArgs([[], 'testEffectId', [], $logger]) - ->getMock(); - $image_effect->expects($this->any()) - ->method('transformDimensions') - ->will($this->returnCallback([$this, 'transformDimensions'])); - $image_style = $this->getImageStyleMock($image_effect); - - // The mock ImageEffect::transformDimensions method is halving each - // dimension. - $pipeline = $this->getCoreImageProcessPipelinePluginMock(); - $pipeline - ->setImageStyle($image_style) - ->setSourceImageUri('noUri') - ->setSourceImageDimensions(100, 200); - $this->assertSame(50, $pipeline->getDerivedImageWidth()); - $this->assertSame(100, $pipeline->getDerivedImageHeight()); - } - - /** - * @covers ::isSourceImageProcessable - */ - public function testIsSourceImageProcessable() { - // The mock ImageFactory::getSupportedExtensions() method is returning - // ['gif', 'png']. - $pipeline = $this->getCoreImageProcessPipelinePluginMock(); - $pipeline->setSourceImageUri('bingo.png'); - $this->assertTrue($pipeline->isSourceImageProcessable()); - - $pipeline = $this->getCoreImageProcessPipelinePluginMock(); - $pipeline->setSourceImageUri('bingo.svg'); - $this->assertFalse($pipeline->isSourceImageProcessable()); - } - - /** - * @covers ::getDerivedImageUrlSecurityToken - */ - public function testGetDerivedImageUrlSecurityToken() { - // Image style that changes the extension. - $logger = $this->getMockBuilder('\Psr\Log\LoggerInterface')->getMock(); - $image_effect = $this->getMockBuilder('\Drupal\image\ImageEffectBase') - ->setConstructorArgs([[], 'testEffectId', [], $logger]) - ->getMock(); - $image_effect->expects($this->any()) - ->method('getDerivativeExtension') - ->will($this->returnValue('png')); - $image_style = $this->getImageStyleMock($image_effect); - - // Assert the extension has been added. - $pipeline = $this->getCoreImageProcessPipelinePluginMock(); - $pipeline - ->setSourceImageUri('public://test.jpeg') - ->setImageStyle($image_style); - $this->assertSame('R2xWQ5uR', $pipeline->getDerivedImageUrlSecurityToken()); - $this->assertSame('public://styles/testImageStyleEntityId/public/test.jpeg.png', $pipeline->getDerivedImageUri()); - - // Image style that doesn't change the extension. - $logger = $this->getMockBuilder('\Psr\Log\LoggerInterface')->getMock(); - $image_effect = $this->getMockBuilder('\Drupal\image\ImageEffectBase') - ->setConstructorArgs([[], 'testEffectId', [], $logger]) - ->getMock(); - $image_effect->expects($this->any()) - ->method('getDerivativeExtension') - ->will($this->returnArgument(0)); - $image_style = $this->getImageStyleMock($image_effect); - - // Assert no extension has been added. - $pipeline = $this->getCoreImageProcessPipelinePluginMock(); - $pipeline - ->setSourceImageUri('public://test.jpeg') - ->setImageStyle($image_style); - $this->assertSame('1U1MYok-', $pipeline->getDerivedImageUrlSecurityToken()); - $this->assertSame('public://styles/testImageStyleEntityId/public/test.jpeg', $pipeline->getDerivedImageUri()); - } - - /** - * Mock function for ImageEffect::transformDimensions(). - */ - public function transformDimensions(array &$dimensions, $uri) { - $dimensions['width'] /= 2; - $dimensions['height'] /= 2; - } - - /** - * Mock function for ImageStyle::fileUriScheme(). - */ - public function fileUriScheme($uri) { - if (preg_match('/^([\w\-]+):\/\/|^(data):/', $uri, $matches)) { - // The scheme will always be the last element in the matches array. - return array_pop($matches); - } - - return FALSE; - } - - /** - * Mock function for ImageStyle::fileUriTarget(). - */ - public function fileUriTarget($uri) { - // Remove the scheme from the URI and remove erroneous leading or trailing, - // forward-slashes and backslashes. - $target = trim(preg_replace('/^[\w\-]+:\/\/|^data:/', '', $uri), '\/'); - - // If nothing was replaced, the URI doesn't have a valid scheme. - return $target !== $uri ? $target : FALSE; - } - -} diff --git a/core/tests/Drupal/Tests/Listeners/DeprecationListenerTrait.php b/core/tests/Drupal/Tests/Listeners/DeprecationListenerTrait.php index 7fa2962894..ce6ed7a68d 100644 --- a/core/tests/Drupal/Tests/Listeners/DeprecationListenerTrait.php +++ b/core/tests/Drupal/Tests/Listeners/DeprecationListenerTrait.php @@ -143,7 +143,6 @@ public static function getSkippedDeprecations() { 'The Drupal\image\Entity\ImageStyle::buildUri method is deprecated since version 9.x.x and will be removed in y.y.y.', 'The Drupal\image\Entity\ImageStyle::buildUrl method is deprecated since version 9.x.x and will be removed in y.y.y.', 'The Drupal\image\Entity\ImageStyle::getPathToken method is deprecated since version 9.x.x and will be removed in y.y.y.', - 'The Drupal\image\Entity\ImageStyle::flush method is deprecated since version 9.x.x and will be removed in y.y.y.', 'The Drupal\image\Entity\ImageStyle::createDerivative method is deprecated since version 9.x.x and will be removed in y.y.y.', 'The Drupal\image\Entity\ImageStyle::transformDimensions method is deprecated since version 9.x.x and will be removed in y.y.y.', 'The Drupal\image\Entity\ImageStyle::getDerivativeExtension method is deprecated since version 9.x.x and will be removed in y.y.y.',