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 2f17bb5c8e..aa05f3210b 100644 --- a/core/modules/image/image.services.yml +++ b/core/modules/image/image.services.yml @@ -4,6 +4,16 @@ services: arguments: ['@stream_wrapper_manager'] tags: - { name: path_processor_inbound, priority: 300 } + image.processor: + class: Drupal\image\ImageProcessor + 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/Annotation/ImageProcessPipeline.php b/core/modules/image/src/Annotation/ImageProcessPipeline.php new file mode 100644 index 0000000000..0a691b39a3 --- /dev/null +++ b/core/modules/image/src/Annotation/ImageProcessPipeline.php @@ -0,0 +1,32 @@ +fileDefaultScheme(); - - if ($source_scheme) { - $path = StreamWrapperManager::getTarget($uri); - // 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->getStreamWrapperManager()->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 = $uri; - $source_scheme = $scheme = $default_scheme; - } - return "$scheme://styles/{$this->id()}/$source_scheme/{$this->addExtension($path)}"; + @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('derivative') + ->setImageStyle($this) + ->setSourceImageUri($uri) + ->getDerivativeImageUri(); } /** * {@inheritdoc} */ public function buildUrl($path, $clean_urls = NULL) { - $uri = $this->buildUri($path); - - /** @var \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface $stream_wrapper_manager */ - $stream_wrapper_manager = \Drupal::service('stream_wrapper_manager'); - - // 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 (!\Drupal::config('image.settings')->get('suppress_itok_output')) { - // The passed $path variable can be either a relative path or a full URI. - $original_uri = $stream_wrapper_manager::getScheme($path) ? $stream_wrapper_manager->normalizeUri($path) : file_build_uri($path); - $token_query = [IMAGE_DERIVATIVE_TOKEN => $this->getPathToken($original_uri)]; - } - - if ($clean_urls === NULL) { - // Assume clean URLs unless the request tells us otherwise. - $clean_urls = TRUE; - try { - $request = \Drupal::request(); - $clean_urls = RequestHelper::isCleanUrl($request); - } - 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 && $stream_wrapper_manager::getScheme($uri) == 'public' && !file_exists($uri)) { - $directory_path = $stream_wrapper_manager->getViaUri($uri)->getDirectoryPath(); - return Url::fromUri('base:' . $directory_path . '/' . $stream_wrapper_manager::getTarget($uri), ['absolute' => TRUE, 'query' => $token_query])->toString(); - } - - $file_url = file_create_url($uri); - // Append the query string with the token, if necessary. - if ($token_query) { - $file_url .= (strpos($file_url, '?') !== FALSE ? '&' : '?') . UrlHelper::buildQuery($token_query); - } - - return $file_url; + @trigger_error('The ' . __METHOD__ . ' method is deprecated since version 9.x.x and will be removed in y.y.y.', E_USER_DEPRECATED); + $url = \Drupal::service('image.processor')->createInstance('derivative') + ->setImageStyle($this) + ->setSourceImageUri($path) + ->setCleanUrl($clean_urls) + ->getDerivativeImageUrl(); + return $url instanceof Url ? $url->toString() : $url; } /** * {@inheritdoc} */ public function flush($path = NULL) { - // A specific image path has been provided. Flush only that derivative. - /** @var \Drupal\Core\File\FileSystemInterface $file_system */ - $file_system = \Drupal::service('file_system'); if (isset($path)) { - $derivative_uri = $this->buildUri($path); - if (file_exists($derivative_uri)) { - try { - $file_system->delete($derivative_uri); - } - catch (FileException $e) { - // Ignore failed deletes. - } - } - return $this; + // A specific image path has been provided. Flush only that derivative. + $pipeline = \Drupal::service('image.processor')->createInstance('derivative') + ->setImageStyle($this) + ->setSourceImageUri($path) + ->dispatch(ImageDerivativePipelineEvents::REMOVE_DERIVATIVE_IMAGE); } - - // Delete the style directory in each registered wrapper. - $wrappers = $this->getStreamWrapperManager()->getWrappers(StreamWrapperInterface::WRITE_VISIBLE); - foreach ($wrappers as $wrapper => $wrapper_data) { - if (file_exists($directory = $wrapper . '://styles/' . $this->id())) { - try { - $file_system->deleteRecursive($directory); - } - catch (FileException $e) { - // Ignore failed deletes. - } - } + else { + \Drupal::service('event_dispatcher')->dispatch(ImageStyleEvents::FLUSH, new ImageStyleEvent($this)); } - - // Let other modules update as necessary on flush. - $module_handler = \Drupal::moduleHandler(); - $module_handler->invokeAll('image_style_flush', [$this]); - - // Clear caches so that formatters may be added for this style. - drupal_theme_rebuild(); - - Cache::invalidateTags($this->getCacheTagsToInvalidate()); - return $this; } @@ -305,60 +209,50 @@ public function flush($path = NULL) { * {@inheritdoc} */ public function createDerivative($original_uri, $derivative_uri) { - // If the source file doesn't exist, return FALSE without creating folders. - $image = $this->getImageFactory()->get($original_uri); - if (!$image->isValid()) { - return FALSE; - } - - // Get the folder for the final location of this style. - $directory = \Drupal::service('file_system')->dirname($derivative_uri); - - // Build the destination folder tree if it doesn't already exist. - if (!\Drupal::service('file_system')->prepareDirectory($directory, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS)) { - \Drupal::logger('image')->error('Failed to create style directory: %directory', ['%directory' => $directory]); - return FALSE; - } - - foreach ($this->getEffects() as $effect) { - $effect->applyEffect($image); - } - - if (!$image->save($derivative_uri)) { - if (file_exists($derivative_uri)) { - \Drupal::logger('image')->error('Cached image file %destination already exists. There may be an issue with your rewrite configuration.', ['%destination' => $derivative_uri]); - } - return FALSE; - } - - return TRUE; + @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('derivative') + ->setImageStyle($this) + ->setSourceImageUri($original_uri) + ->setDerivativeImageUri($derivative_uri) + ->buildDerivativeImage(); } /** * {@inheritdoc} */ public function transformDimensions(array &$dimensions, $uri) { - foreach ($this->getEffects() as $effect) { - $effect->transformDimensions($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('derivative'); + $pipeline + ->setImageStyle($this) + ->setSourceImageUri($uri) + ->setSourceImageDimensions($dimensions['width'] ?? NULL, $dimensions['height'] ?? NULL) + ->dispatch(ImageDerivativePipelineEvents::RESOLVE_DERIVATIVE_IMAGE_DIMENSIONS); + $dimensions['width'] = $pipeline->getVariable('derivativeImageWidth'); + $dimensions['height'] = $pipeline->getVariable('derivativeImageHeight'); + return; } /** * {@inheritdoc} */ public function getDerivativeExtension($extension) { - foreach ($this->getEffects() as $effect) { - $extension = $effect->getDerivativeExtension($extension); - } - return $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('derivative') + ->setImageStyle($this) + ->setSourceImageFileExtension($extension) + ->getDerivativeImageFileExtension(); } /** * {@inheritdoc} */ public function getPathToken($uri) { - // Return the first 8 characters. - return substr(Crypt::hmacBase64($this->id() . ':' . $this->addExtension($uri), $this->getPrivateKey() . $this->getHashSalt()), 0, 8); + @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('derivative') + ->setImageStyle($this) + ->setSourceImageUri($uri) + ->getDerivativeImageUrlSecurityToken(); } /** @@ -374,12 +268,11 @@ public function deleteImageEffect(ImageEffectInterface $effect) { * {@inheritdoc} */ public function supportsUri($uri) { - // Only support the URI if its extension is supported by the current image - // toolkit. - return in_array( - mb_strtolower(pathinfo($uri, PATHINFO_EXTENSION)), - $this->getImageFactory()->getSupportedExtensions() - ); + @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('derivative') + ->setImageStyle($this) + ->setSourceImageUri($uri) + ->isSourceImageProcessable(); } /** @@ -455,8 +348,11 @@ protected function getImageEffectPluginManager() { * * @return \Drupal\Core\Image\ImageFactory * The image factory. + * + * @deprecated since version 9.x.x and will be removed in y.y.y. */ protected function getImageFactory() { + @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.factory'); } @@ -465,8 +361,11 @@ protected function getImageFactory() { * * @return string * The Drupal private key. + * + * @deprecated since version 9.x.x and will be removed in y.y.y. */ protected function getPrivateKey() { + @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('private_key')->get(); } @@ -477,8 +376,11 @@ protected function getPrivateKey() { * A salt based on information in settings.php, not in the database. * * @throws \RuntimeException + * + * @deprecated since version 9.x.x and will be removed in y.y.y. */ protected function getHashSalt() { + @trigger_error('The ' . __METHOD__ . ' method is deprecated since version 9.x.x and will be removed in y.y.y.', E_USER_DEPRECATED); return Settings::getHashSalt(); } @@ -495,8 +397,11 @@ protected function getHashSalt() { * @return string * The given path if this image style doesn't change its extension, or the * path with the added extension if it does. + * + * @deprecated since version 9.x.x and will be removed in y.y.y. */ protected function addExtension($path) { + @trigger_error('The ' . __METHOD__ . ' method is deprecated since version 9.x.x and will be removed in y.y.y.', E_USER_DEPRECATED); $original_extension = pathinfo($path, PATHINFO_EXTENSION); $extension = $this->getDerivativeExtension($original_extension); if ($original_extension !== $extension) { @@ -512,8 +417,11 @@ protected function addExtension($path) { * * @return string * 'public', 'private' or any other file scheme defined as the default. + * + * @deprecated since version 9.x.x and will be removed in y.y.y. */ protected function fileDefaultScheme() { + @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::config('system.file')->get('default_scheme'); } @@ -523,9 +431,10 @@ protected function fileDefaultScheme() { * @return \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface * The stream wrapper manager service * - * @todo Properly inject this service in Drupal 9.0.x. + * @deprecated since version 9.x.x and will be removed in y.y.y. */ protected function getStreamWrapperManager() { + @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('stream_wrapper_manager'); } 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'); + $this->setCacheBackend($cache_backend, 'image_process_pipeline_plugins'); + } + +} diff --git a/core/modules/image/src/ImageStyleInterface.php b/core/modules/image/src/ImageStyleInterface.php index d3306e5d15..34a9d69430 100644 --- a/core/modules/image/src/ImageStyleInterface.php +++ b/core/modules/image/src/ImageStyleInterface.php @@ -40,6 +40,8 @@ public function setName($name); * * @return string * The URI to the image derivative for this style. + * + * @deprecated since version 9.x.x and will be removed in y.y.y. */ public function buildUri($uri); @@ -57,6 +59,8 @@ public function buildUri($uri); * * @see \Drupal\image\Controller\ImageStyleDownloadController::deliver() * @see file_url_transform_relative() + * + * @deprecated since version 9.x.x and will be removed in y.y.y. */ public function buildUrl($path, $clean_urls = NULL); @@ -72,6 +76,8 @@ public function buildUrl($path, $clean_urls = NULL); * @return string * An eight-character token which can be used to protect image style * derivatives against denial-of-service attacks. + * + * @deprecated since version 9.x.x and will be removed in y.y.y. */ public function getPathToken($uri); @@ -100,6 +106,8 @@ public function flush($path = NULL); * @return bool * TRUE if an image derivative was generated, or FALSE if the image * derivative could not be generated. + * + * @deprecated since version 9.x.x and will be removed in y.y.y. */ public function createDerivative($original_uri, $derivative_uri); @@ -124,6 +132,8 @@ public function createDerivative($original_uri, $derivative_uri); * performance. * * @see ImageEffectInterface::transformDimensions + * + * @deprecated since version 9.x.x and will be removed in y.y.y. */ public function transformDimensions(array &$dimensions, $uri); @@ -136,6 +146,8 @@ public function transformDimensions(array &$dimensions, $uri); * @return string * The extension the derivative image will have, given the extension of the * original. + * + * @deprecated since version 9.x.x and will be removed in y.y.y. */ public function getDerivativeExtension($extension); @@ -187,6 +199,8 @@ public function deleteImageEffect(ImageEffectInterface $effect); * * @return bool * TRUE if the image is supported, FALSE otherwise. + * + * @deprecated since version 9.x.x and will be removed in y.y.y. */ public function supportsUri($uri); 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/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/ImageStyleLegacyTest.php b/core/modules/image/tests/src/Kernel/ImageStyleLegacyTest.php new file mode 100644 index 0000000000..3f6e7700ef --- /dev/null +++ b/core/modules/image/tests/src/Kernel/ImageStyleLegacyTest.php @@ -0,0 +1,109 @@ +imageStyle = ImageStyle::create([ + 'name' => 'test', + ]); + $this->imageStyle->addImageEffect(['id' => 'image_module_test_null']); + $this->imageStyle->save(); + + \Drupal::service('file_system')->copy($this->root . '/core/misc/druplicon.png', 'public://test.png'); + } + + /** + * @covers ::buildUri + * @expectedDeprecation The Drupal\image\Entity\ImageStyle::buildUri method is deprecated since version 9.x.x and will be removed in y.y.y. + */ + public function testBuildUri() { + $this->assertSame('public://styles/test/public/test.png', $this->imageStyle->buildUri('public://test.png')); + } + + /** + * @covers ::buildUrl + * @expectedDeprecation The Drupal\image\Entity\ImageStyle::buildUrl method is deprecated since version 9.x.x and will be removed in y.y.y. + */ + public function testBuildUrl() { + $this->assertContains('files/styles/test/public/test.png?itok=', $this->imageStyle->buildUrl('public://test.png')); + } + + /** + * @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. + */ + public function testCreateDerivative() { + $this->assertInternalType('bool', $this->imageStyle->createDerivative('public://test.png', 'public://test_derivative.png')); + } + + /** + * @covers ::transformDimensions + * @expectedDeprecation The Drupal\image\Entity\ImageStyle::transformDimensions method is deprecated since version 9.x.x and will be removed in y.y.y. + */ + public function testTransformDimensions() { + $dimensions = ['width' => 100, 'height' => 200]; + $this->assertNull($this->imageStyle->transformDimensions($dimensions, 'public://test.png')); + $this->assertEquals([ + 'width' => NULL, + 'height' => NULL, + ], $dimensions); + } + + /** + * @covers ::getDerivativeExtension + * @expectedDeprecation The Drupal\image\Entity\ImageStyle::getDerivativeExtension method is deprecated since version 9.x.x and will be removed in y.y.y. + */ + public function testGetDerivativeExtension() { + $this->assertSame('png', $this->imageStyle->getDerivativeExtension('png')); + } + + /** + * @covers ::getPathToken + * @expectedDeprecation The Drupal\image\Entity\ImageStyle::getPathToken method is deprecated since version 9.x.x and will be removed in y.y.y. + */ + public function testGetPathToken() { + $this->assertNotEmpty($this->imageStyle->getPathToken('public://test.png')); + } + + /** + * @covers ::supportsUri + * @expectedDeprecation The Drupal\image\Entity\ImageStyle::supportsUri method is deprecated since version 9.x.x and will be removed in y.y.y. + */ + public function testSupportsUri() { + $this->assertTrue($this->imageStyle->supportsUri('public://test.png')); + } + +} diff --git a/core/modules/image/tests/src/Unit/ImageStyleTest.php b/core/modules/image/tests/src/Unit/ImageStyleTest.php deleted file mode 100644 index f02e3eeff4..0000000000 --- a/core/modules/image/tests/src/Unit/ImageStyleTest.php +++ /dev/null @@ -1,207 +0,0 @@ -getMockBuilder('\Drupal\image\ImageEffectManager') - ->disableOriginalConstructor() - ->getMock(); - $effectManager->expects($this->any()) - ->method('createInstance') - ->with($image_effect_id) - ->will($this->returnValue($image_effect)); - $default_stubs = [ - 'getImageEffectPluginManager', - 'fileUriScheme', - 'fileUriTarget', - 'fileDefaultScheme', - ]; - $image_style = $this->getMockBuilder('\Drupal\image\Entity\ImageStyle') - ->setConstructorArgs([ - ['effects' => [$image_effect_id => ['id' => $image_effect_id]]], - $this->entityTypeId, - ]) - ->setMethods(array_merge($default_stubs, $stubs)) - ->getMock(); - - $image_style->expects($this->any()) - ->method('getImageEffectPluginManager') - ->will($this->returnValue($effectManager)); - $image_style->expects($this->any()) - ->method('fileDefaultScheme') - ->will($this->returnCallback([$this, 'fileDefaultScheme'])); - - return $image_style; - } - - /** - * {@inheritdoc} - */ - protected function setUp() { - $this->entityTypeId = $this->randomMachineName(); - $this->provider = $this->randomMachineName(); - $this->entityType = $this->createMock('\Drupal\Core\Entity\EntityTypeInterface'); - $this->entityType->expects($this->any()) - ->method('getProvider') - ->will($this->returnValue($this->provider)); - $this->entityTypeManager = $this->createMock('\Drupal\Core\Entity\EntityTypeManagerInterface'); - $this->entityTypeManager->expects($this->any()) - ->method('getDefinition') - ->with($this->entityTypeId) - ->will($this->returnValue($this->entityType)); - } - - /** - * @covers ::getDerivativeExtension - */ - public function testGetDerivativeExtension() { - $image_effect_id = $this->randomMachineName(); - $logger = $this->getMockBuilder('\Psr\Log\LoggerInterface')->getMock(); - $image_effect = $this->getMockBuilder('\Drupal\image\ImageEffectBase') - ->setConstructorArgs([[], $image_effect_id, [], $logger]) - ->getMock(); - $image_effect->expects($this->any()) - ->method('getDerivativeExtension') - ->will($this->returnValue('png')); - - $image_style = $this->getImageStyleMock($image_effect_id, $image_effect); - - $extensions = ['jpeg', 'gif', 'png']; - foreach ($extensions as $extension) { - $extensionReturned = $image_style->getDerivativeExtension($extension); - $this->assertEquals($extensionReturned, 'png'); - } - } - - /** - * @covers ::buildUri - */ - public function testBuildUri() { - // Image style that changes the extension. - $image_effect_id = $this->randomMachineName(); - $logger = $this->getMockBuilder('\Psr\Log\LoggerInterface')->getMock(); - $image_effect = $this->getMockBuilder('\Drupal\image\ImageEffectBase') - ->setConstructorArgs([[], $image_effect_id, [], $logger]) - ->getMock(); - $image_effect->expects($this->any()) - ->method('getDerivativeExtension') - ->will($this->returnValue('png')); - - $image_style = $this->getImageStyleMock($image_effect_id, $image_effect); - $this->assertEquals($image_style->buildUri('public://test.jpeg'), 'public://styles/' . $image_style->id() . '/public/test.jpeg.png'); - - // Image style that doesn't change the extension. - $image_effect_id = $this->randomMachineName(); - $image_effect = $this->getMockBuilder('\Drupal\image\ImageEffectBase') - ->setConstructorArgs([[], $image_effect_id, [], $logger]) - ->getMock(); - $image_effect->expects($this->any()) - ->method('getDerivativeExtension') - ->will($this->returnArgument(0)); - - $image_style = $this->getImageStyleMock($image_effect_id, $image_effect); - $this->assertEquals($image_style->buildUri('public://test.jpeg'), 'public://styles/' . $image_style->id() . '/public/test.jpeg'); - } - - /** - * @covers ::getPathToken - */ - public function testGetPathToken() { - $logger = $this->getMockBuilder('\Psr\Log\LoggerInterface')->getMock(); - $private_key = $this->randomMachineName(); - $hash_salt = $this->randomMachineName(); - - // Image style that changes the extension. - $image_effect_id = $this->randomMachineName(); - $image_effect = $this->getMockBuilder('\Drupal\image\ImageEffectBase') - ->setConstructorArgs([[], $image_effect_id, [], $logger]) - ->getMock(); - $image_effect->expects($this->any()) - ->method('getDerivativeExtension') - ->will($this->returnValue('png')); - - $image_style = $this->getImageStyleMock($image_effect_id, $image_effect, ['getPrivateKey', 'getHashSalt']); - $image_style->expects($this->any()) - ->method('getPrivateKey') - ->will($this->returnValue($private_key)); - $image_style->expects($this->any()) - ->method('getHashSalt') - ->will($this->returnValue($hash_salt)); - - // Assert the extension has been added to the URI before creating the token. - $this->assertEquals($image_style->getPathToken('public://test.jpeg.png'), $image_style->getPathToken('public://test.jpeg')); - $this->assertEquals(substr(Crypt::hmacBase64($image_style->id() . ':' . 'public://test.jpeg.png', $private_key . $hash_salt), 0, 8), $image_style->getPathToken('public://test.jpeg')); - $this->assertNotEquals(substr(Crypt::hmacBase64($image_style->id() . ':' . 'public://test.jpeg', $private_key . $hash_salt), 0, 8), $image_style->getPathToken('public://test.jpeg')); - - // Image style that doesn't change the extension. - $image_effect_id = $this->randomMachineName(); - $image_effect = $this->getMockBuilder('\Drupal\image\ImageEffectBase') - ->setConstructorArgs([[], $image_effect_id, [], $logger]) - ->getMock(); - $image_effect->expects($this->any()) - ->method('getDerivativeExtension') - ->will($this->returnArgument(0)); - - $image_style = $this->getImageStyleMock($image_effect_id, $image_effect, ['getPrivateKey', 'getHashSalt']); - $image_style->expects($this->any()) - ->method('getPrivateKey') - ->will($this->returnValue($private_key)); - $image_style->expects($this->any()) - ->method('getHashSalt') - ->will($this->returnValue($hash_salt)); - // Assert no extension has been added to the uri before creating the token. - $this->assertNotEquals($image_style->getPathToken('public://test.jpeg.png'), $image_style->getPathToken('public://test.jpeg')); - $this->assertNotEquals(substr(Crypt::hmacBase64($image_style->id() . ':' . 'public://test.jpeg.png', $private_key . $hash_salt), 0, 8), $image_style->getPathToken('public://test.jpeg')); - $this->assertEquals(substr(Crypt::hmacBase64($image_style->id() . ':' . 'public://test.jpeg', $private_key . $hash_salt), 0, 8), $image_style->getPathToken('public://test.jpeg')); - } - - /** - * Mock function for ImageStyle::fileDefaultScheme(). - */ - public function fileDefaultScheme() { - return 'public'; - } - -} diff --git a/core/tests/Drupal/Tests/Listeners/DeprecationListenerTrait.php b/core/tests/Drupal/Tests/Listeners/DeprecationListenerTrait.php index e4ec6c9653..ce6ed7a68d 100644 --- a/core/tests/Drupal/Tests/Listeners/DeprecationListenerTrait.php +++ b/core/tests/Drupal/Tests/Listeners/DeprecationListenerTrait.php @@ -140,6 +140,13 @@ public static function isDeprecationSkipped($message) { */ public static function getSkippedDeprecations() { return [ + '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::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.', + 'The Drupal\image\Entity\ImageStyle::supportsUri method is deprecated since version 9.x.x and will be removed in y.y.y.', 'The Symfony\Component\ClassLoader\ApcClassLoader class is deprecated since Symfony 3.3 and will be removed in 4.0. Use `composer install --apcu-autoloader` instead.', // The following deprecation is not triggered by DrupalCI testing since it // is a Windows only deprecation. Remove when core no longer uses