diff --git a/core/modules/image/image.admin.inc b/core/modules/image/image.admin.inc index e43bd487..b4df6bce 100644 --- a/core/modules/image/image.admin.inc +++ b/core/modules/image/image.admin.inc @@ -47,13 +47,32 @@ function template_preprocess_image_style_preview(&$variables) { $variables['preview']['original']['width'] = round($variables['preview']['original']['height'] / $variables['original']['height'] * $variables['original']['width']); } - // Set up derivative file information. - $preview_file = $style->buildUri($original_path); + // Set up derivative file pipeline. + $pipeline = \Drupal::service('image.processor')->createInstance('derivative') + ->setImageStyle($style) + ->setSourceImageUri($original_path); + $preview_file = $pipeline->getDerivativeImageUri(); + // Create derivative if necessary. if (!file_exists($preview_file)) { - $style->createDerivative($original_path, $preview_file); + $pipeline->buildDerivativeImage(); + } + + // The resulting image dimensions may be available from the Image object in + // the pipeline. If not (the image was already processed, or the state of + // dimensions from Image object is not determined), then load the Image + // object form the file. + $load_from_file = TRUE; + if ($pipeline->hasImage()) { + $preview_image = $pipeline->getImage(); + if ($preview_image->getWidth() && $preview_image->getHeight()) { + $load_from_file = FALSE; + } + } + if ($load_from_file) { + $preview_image = $image_factory->get($preview_file); } - $preview_image = $image_factory->get($preview_file); + $variables['derivative'] = [ 'url' => file_url_transform_relative(file_create_url($preview_file)), 'width' => $preview_image->getWidth(), diff --git a/core/modules/image/image.module b/core/modules/image/image.module index dbed41c9..d18ab71b 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, + ]) + ); } /** @@ -251,18 +255,15 @@ function image_style_options($include_empty = TRUE) { function template_preprocess_image_style(&$variables) { $style = ImageStyle::load($variables['style_name']); - // Determine the dimensions of the styled image. - $dimensions = [ - 'width' => $variables['width'], - 'height' => $variables['height'], - ]; - - $style->transformDimensions($dimensions, $variables['uri']); + $pipeline = \Drupal::service('image.processor')->createInstance('derivative') + ->setImageStyle($style) + ->setSourceImageUri($variables['uri']) + ->setSourceImageDimensions($variables['width'], $variables['height']); $variables['image'] = [ '#theme' => 'image', - '#width' => $dimensions['width'], - '#height' => $dimensions['height'], + '#width' => $pipeline->getDerivativeImageWidth(), + '#height' => $pipeline->getDerivativeImageHeight(), '#attributes' => $variables['attributes'], '#style_name' => $variables['style_name'], ]; @@ -270,8 +271,8 @@ function template_preprocess_image_style(&$variables) { // If the current image toolkit supports this file type, prepare the URI for // the derivative image. If not, just use the original image resized to the // dimensions specified by the style. - if ($style->supportsUri($variables['uri'])) { - $variables['image']['#uri'] = $style->buildUrl($variables['uri']); + if ($pipeline->isSourceImageProcessable()) { + $variables['image']['#uri'] = $pipeline->getDerivativeImageUrl()->toString(); } else { $variables['image']['#uri'] = $variables['uri']; diff --git a/core/modules/image/image.services.yml b/core/modules/image/image.services.yml index 2f17bb5c..aa05f321 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 00000000..0a691b39 --- /dev/null +++ b/core/modules/image/src/Annotation/ImageProcessPipeline.php @@ -0,0 +1,32 @@ +lock = $lock; $this->imageFactory = $image_factory; + $this->imageProcessor = $image_processor; $this->logger = $this->getLogger('image'); } @@ -66,7 +77,8 @@ public static function create(ContainerInterface $container) { return new static( $container->get('lock'), $container->get('image.factory'), - $container->get('stream_wrapper_manager') + $container->get('stream_wrapper_manager'), + $container->get('image.processor') ); } @@ -96,20 +108,31 @@ public function deliver(Request $request, $scheme, ImageStyleInterface $image_st $target = $request->query->get('file'); $image_uri = $scheme . '://' . $target; - // Check that the style is defined, the scheme is valid, and the image - // derivative token is valid. Sites which require image derivatives to be - // generated without a token can set the - // 'image.settings:allow_insecure_derivatives' configuration to TRUE to - // bypass the latter check, but this will increase the site's vulnerability - // to denial-of-service attacks. To prevent this variable from leaving the - // site vulnerable to the most serious attacks, a token is always required - // when a derivative of a style is requested. + // Check that the style is defined, return a 404 (Page Not Found) if + // missing. + if (empty($image_style)) { + throw new NotFoundHttpException(); + } + + // Create an image process pipeline. + $pipeline = $this->imageProcessor->createInstance('derivative'); + $pipeline + ->setImageStyle($image_style) + ->setSourceImageUri($image_uri); + + // Check that the scheme is valid, and the image derivative token is valid. + // Sites which require image derivatives to be generated without a token + // can set the 'image.settings:allow_insecure_derivatives' configuration to + // TRUE to bypass the latter check, but this will increase the site's + // vulnerability to denial-of-service attacks. To prevent this variable + // from leaving the site vulnerable to the most serious attacks, a token is + // always required when a derivative of a style is requested. // The $target variable for a derivative of a style has // styles//... as structure, so we check if the $target variable // starts with styles/. - $valid = !empty($image_style) && $this->streamWrapperManager->isValidScheme($scheme); + $valid = $this->streamWrapperManager->isValidScheme($scheme); if (!$this->config('image.settings')->get('allow_insecure_derivatives') || strpos(ltrim($target, '\/'), 'styles/') === 0) { - $valid &= hash_equals($image_style->getPathToken($image_uri), $request->query->get(IMAGE_DERIVATIVE_TOKEN, '')); + $valid &= hash_equals($pipeline->getDerivativeImageUrlSecurityToken(), $request->query->get(IMAGE_DERIVATIVE_TOKEN, '')); } if (!$valid) { // Return a 404 (Page Not Found) rather than a 403 (Access Denied) as the @@ -119,7 +142,7 @@ public function deliver(Request $request, $scheme, ImageStyleInterface $image_st throw new NotFoundHttpException(); } - $derivative_uri = $image_style->buildUri($image_uri); + $derivative_uri = $pipeline->getDerivativeImageUri(); $headers = []; // If using the private scheme, let other modules provide headers and @@ -163,7 +186,7 @@ public function deliver(Request $request, $scheme, ImageStyleInterface $image_st // Try to generate the image, unless another thread just did it while we // were acquiring the lock. - $success = file_exists($derivative_uri) || $image_style->createDerivative($image_uri, $derivative_uri); + $success = file_exists($derivative_uri) || $pipeline->buildDerivativeImage(); if (!empty($lock_acquired)) { $this->lock->release($lock_name); diff --git a/core/modules/image/src/Entity/ImageStyle.php b/core/modules/image/src/Entity/ImageStyle.php index 39e7263f..1239c972 100644 --- a/core/modules/image/src/Entity/ImageStyle.php +++ b/core/modules/image/src/Entity/ImageStyle.php @@ -2,24 +2,17 @@ namespace Drupal\image\Entity; -use Drupal\Core\Cache\Cache; use Drupal\Core\Config\Entity\ConfigEntityBase; use Drupal\Core\Entity\Entity\EntityFormDisplay; use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Entity\EntityWithPluginCollectionInterface; -use Drupal\Core\File\Exception\FileException; -use Drupal\Core\File\FileSystemInterface; -use Drupal\Core\Routing\RequestHelper; use Drupal\Core\Site\Settings; -use Drupal\Core\StreamWrapper\StreamWrapperManager; -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; -use Drupal\Component\Utility\Crypt; -use Drupal\Component\Utility\UrlHelper; -use Drupal\Core\StreamWrapper\StreamWrapperInterface; -use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException; use Drupal\Core\Entity\Entity\EntityViewDisplay; /** @@ -174,130 +167,40 @@ protected static function replaceImageStyle(ImageStyleInterface $style) { * {@inheritdoc} */ public function buildUri($uri) { - $source_scheme = $scheme = StreamWrapperManager::getScheme($uri); - $default_scheme = $this->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); + return \Drupal::service('image.processor')->createInstance('derivative') + ->setImageStyle($this) + ->setSourceImageUri($path) + ->setCleanUrl($clean_urls) + ->getDerivativeImageUrl() + ->toString(); } /** * {@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 +208,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 +267,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 +347,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 +360,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 +375,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 +396,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 +416,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 +430,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 00000000..a7b0141e --- /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 00000000..c40de29b --- /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 00000000..35a8cee0 --- /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)) { + $validated_uri = 'base:' . $this->streamWrapperManager->getViaUri($derivative_uri)->getDirectoryPath() . '/' . $this->streamWrapperManager->getTarget($derivative_uri); + } + else { + // Using clean URLs. + $validated_uri = file_create_url($derivative_uri); + } + + $pipeline->setVariable('derivativeImageUrl', Url::fromUri($validated_uri, [ + 'absolute' => TRUE, + 'query' => $token_query, + ]) + ); + } + + /** + * 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->hasImage()) { + // If the source file doesn't exist or is invalid, throw an exception. + $image = $this->imageFactory->get($pipeline->getVariable('sourceImageUri'), $pipeline->getVariable('imageToolkitId')); + if (!$image->isValid()) { + throw new ImageProcessException('Missing or invalid source image file ' . $pipeline->getVariable('sourceImageUri')); + } + $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->getImage()->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 { + $event->getPipeline() + ->dispatch(ImageDerivativePipelineEvents::RESOLVE_SOURCE_IMAGE_PROCESSABILITY) + ->dispatch(ImageDerivativePipelineEvents::LOAD_SOURCE_IMAGE) + ->dispatch(ImageDerivativePipelineEvents::APPLY_IMAGE_STYLE) + ->dispatch(ImageDerivativePipelineEvents::SAVE_DERIVATIVE_IMAGE); + } + + /** + * 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->getImage()); + } + + /** + * 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 00000000..009a8218 --- /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 d3306e5d..34a9d694 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/Field/FieldFormatter/ImageFormatter.php b/core/modules/image/src/Plugin/Field/FieldFormatter/ImageFormatter.php index ee99b0f8..70009707 100644 --- a/core/modules/image/src/Plugin/Field/FieldFormatter/ImageFormatter.php +++ b/core/modules/image/src/Plugin/Field/FieldFormatter/ImageFormatter.php @@ -10,6 +10,7 @@ use Drupal\Core\Session\AccountInterface; use Drupal\Core\Url; use Drupal\image\Entity\ImageStyle; +use Drupal\image\ImageProcessor; use Symfony\Component\DependencyInjection\ContainerInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Cache\Cache; @@ -44,6 +45,13 @@ class ImageFormatter extends ImageFormatterBase implements ContainerFactoryPlugi */ protected $imageStyleStorage; + /** + * The image processor service. + * + * @var \Drupal\image\ImageProcessor + */ + protected $imageProcessor; + /** * Constructs an ImageFormatter object. * @@ -65,11 +73,14 @@ class ImageFormatter extends ImageFormatterBase implements ContainerFactoryPlugi * The current user. * @param \Drupal\Core\Entity\EntityStorageInterface $image_style_storage * The image style storage. + * @param \Drupal\image\ImageProcessor $image_processor + * The image processor service. */ - public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, $label, $view_mode, array $third_party_settings, AccountInterface $current_user, EntityStorageInterface $image_style_storage) { + public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, $label, $view_mode, array $third_party_settings, AccountInterface $current_user, EntityStorageInterface $image_style_storage, ImageProcessor $image_processor) { parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $label, $view_mode, $third_party_settings); $this->currentUser = $current_user; $this->imageStyleStorage = $image_style_storage; + $this->imageProcessor = $image_processor; } /** @@ -85,7 +96,8 @@ public static function create(ContainerInterface $container, array $configuratio $configuration['view_mode'], $configuration['third_party_settings'], $container->get('current_user'), - $container->get('entity_type.manager')->getStorage('image_style') + $container->get('entity_type.manager')->getStorage('image_style'), + $container->get('image.processor') ); } diff --git a/core/modules/image/src/Plugin/Field/FieldFormatter/ImageUrlFormatter.php b/core/modules/image/src/Plugin/Field/FieldFormatter/ImageUrlFormatter.php index 7109627a..f6522d19 100644 --- a/core/modules/image/src/Plugin/Field/FieldFormatter/ImageUrlFormatter.php +++ b/core/modules/image/src/Plugin/Field/FieldFormatter/ImageUrlFormatter.php @@ -64,7 +64,16 @@ public function viewElements(FieldItemListInterface $items, $langcode) { /** @var \Drupal\file\FileInterface[] $images */ foreach ($images as $delta => $image) { $image_uri = $image->getFileUri(); - $url = $image_style ? $image_style->buildUrl($image_uri) : file_create_url($image_uri); + if ($image_style) { + $url = $this->imageProcessor->createInstance('derivative') + ->setImageStyle($image_style) + ->setSourceImageUri($image_uri) + ->getDerivativeImageUrl() + ->toString(); + } + else { + $url = file_create_url($image_uri); + } $url = file_url_transform_relative($url); // Add cacheability metadata from the image and image style. 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 00000000..8081428e --- /dev/null +++ b/core/modules/image/src/Plugin/ImageProcessPipeline/Derivative.php @@ -0,0 +1,248 @@ +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; + } + + /** + * {@inheritdoc} + */ + public function setImage(ImageInterface $image): ImageProcessPipelineInterface { + if (!$this->hasVariable('sourceImageUri')) { + $this->setVariable('sourceImageUri', NULL); + } + return parent::setImage($image); + } + + /** + * 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 \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(): Url { + $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 { + try { + $this->dispatch(ImageDerivativePipelineEvents::BUILD_DERIVATIVE_IMAGE); + return TRUE; + } + catch (ImageProcessException $e) { + return FALSE; + } + } + +} 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 00000000..3a3ddd7f --- /dev/null +++ b/core/modules/image/src/Plugin/ImageProcessPipeline/ImageProcessPipelineBase.php @@ -0,0 +1,138 @@ +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 setImage(ImageInterface $image): ImageProcessPipelineInterface { + $this->image = $image; + return $this; + } + + /** + * {@inheritdoc} + */ + public function getImage(): ImageInterface { + return $this->image; + } + + /** + * {@inheritdoc} + */ + public function hasImage(): bool { + return (bool) $this->image; + } + + /** + * {@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/Functional/FileMoveTest.php b/core/modules/image/tests/src/Functional/FileMoveTest.php index c2a3a1bb..f460d755 100644 --- a/core/modules/image/tests/src/Functional/FileMoveTest.php +++ b/core/modules/image/tests/src/Functional/FileMoveTest.php @@ -34,8 +34,13 @@ class FileMoveTest extends BrowserTestBase { /** * Tests moving a randomly generated image. + * + * @group legacy + * + * @expectedDeprecation The Drupal\image\Entity\ImageStyle::buildUri method is deprecated since version 9.x.x and will be removed in y.y.y. + * @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 testNormal() { + public function testNormalLegacy() { // Pick a file for testing. $file = File::create((array) current($this->drupalGetTestFiles('image'))); @@ -61,4 +66,36 @@ public function testNormal() { $this->assertFalse(file_exists($derivative_uri), 'Make sure derivative image has been flushed.'); } + /** + * Tests moving a randomly generated image. + */ + public function testNormal() { + // Pick a file for testing. + $file = File::create((array) current($this->drupalGetTestFiles('image'))); + + // Create derivative image. + $styles = ImageStyle::loadMultiple(); + $style = reset($styles); + $original_uri = $file->getFileUri(); + $pipeline = \Drupal::service('image.processor')->createInstance('derivative') + ->setImageStyle($style) + ->setSourceImageUri($original_uri); + $derivative_uri = $pipeline->getDerivativeImageUri(); + $pipeline->buildDerivativeImage(); + + // Check if derivative image exists. + $this->assertFileExists($derivative_uri, 'Make sure derivative image is generated successfully.'); + + // Clone the object so we don't have to worry about the function changing + // our reference copy. + $desired_filepath = 'public://' . $this->randomMachineName(); + $result = file_move(clone $file, $desired_filepath, FileSystemInterface::EXISTS_ERROR); + + // Check if image has been moved. + $this->assertFileExists($result->getFileUri(), 'Make sure image is moved successfully.'); + + // Check if derivative image has been flushed. + $this->assertFileNotExists($derivative_uri, 'Make sure derivative image has been flushed.'); + } + } diff --git a/core/modules/image/tests/src/Functional/ImageAdminStylesTest.php b/core/modules/image/tests/src/Functional/ImageAdminStylesTest.php index 46c96ca2..0325226f 100644 --- a/core/modules/image/tests/src/Functional/ImageAdminStylesTest.php +++ b/core/modules/image/tests/src/Functional/ImageAdminStylesTest.php @@ -42,7 +42,10 @@ public function createSampleImage(ImageStyleInterface $style) { $file_path = \Drupal::service('file_system')->copy($file->uri, 'public://'); } - return $style->buildUrl($file_path) ? $file_path : FALSE; + $pipeline = \Drupal::service('image.processor')->createInstance('derivative') + ->setImageStyle($style) + ->setSourceImageUri($file_path); + return $pipeline->getDerivativeImageUrl() ? $file_path : FALSE; } /** @@ -339,7 +342,10 @@ public function testStyleReplacement() { // Test that image is displayed using newly created style. $this->drupalGet('node/' . $nid); - $this->assertRaw(file_url_transform_relative($style->buildUrl($original_uri)), new FormattableMarkup('Image displayed using style @style.', ['@style' => $style_name])); + $pipeline = \Drupal::service('image.processor')->createInstance('derivative') + ->setImageStyle($style) + ->setSourceImageUri($original_uri); + $this->assertRaw(file_url_transform_relative($pipeline->getDerivativeImageUrl()->toString()), new FormattableMarkup('Image displayed using style @style.', ['@style' => $style_name])); // Rename the style and make sure the image field is updated. $new_style_name = strtolower($this->randomMachineName(10)); @@ -354,7 +360,10 @@ public function testStyleReplacement() { // Reload the image style using the new name. $style = ImageStyle::load($new_style_name); - $this->assertRaw(file_url_transform_relative($style->buildUrl($original_uri)), 'Image displayed using style replacement style.'); + $pipeline = \Drupal::service('image.processor')->createInstance('derivative') + ->setImageStyle($style) + ->setSourceImageUri($original_uri); + $this->assertRaw(file_url_transform_relative($pipeline->getDerivativeImageUrl()->toString()), 'Image displayed using style replacement style.'); // Delete the style and choose a replacement style. $edit = [ @@ -366,7 +375,10 @@ public function testStyleReplacement() { $replacement_style = ImageStyle::load('thumbnail'); $this->drupalGet('node/' . $nid); - $this->assertRaw(file_url_transform_relative($replacement_style->buildUrl($original_uri)), 'Image displayed using style replacement style.'); + $pipeline = \Drupal::service('image.processor')->createInstance('derivative') + ->setImageStyle($replacement_style) + ->setSourceImageUri($original_uri); + $this->assertRaw(file_url_transform_relative($pipeline->getDerivativeImageUrl()->toString()), 'Image displayed using style replacement style.'); } /** @@ -436,8 +448,10 @@ public function testFlushUserInterface() { // Create an image to make sure it gets flushed. $files = $this->drupalGetTestFiles('image'); $image_uri = $files[0]->uri; - $derivative_uri = $style->buildUri($image_uri); - $this->assertTrue($style->createDerivative($image_uri, $derivative_uri)); + $pipeline = \Drupal::service('image.processor')->createInstance('derivative') + ->setImageStyle($style) + ->setSourceImageUri($image_uri); + $this->assertTrue($pipeline->buildDerivativeImage()); $this->assertEqual($this->getImageCount($style), 1); // Go to image styles list page and check if the flush operation link @@ -485,7 +499,10 @@ public function testConfigImport() { // Test that image is displayed using newly created style. $this->drupalGet('node/' . $nid); - $this->assertRaw(file_url_transform_relative($style->buildUrl($original_uri)), new FormattableMarkup('Image displayed using style @style.', ['@style' => $style_name])); + $pipeline = \Drupal::service('image.processor')->createInstance('derivative') + ->setImageStyle($style) + ->setSourceImageUri($original_uri); + $this->assertRaw(file_url_transform_relative($pipeline->getDerivativeImageUrl()->toString()), new FormattableMarkup('Image displayed using style @style.', ['@style' => $style_name])); // Copy config to sync, and delete the image style. $sync = $this->container->get('config.storage.sync'); diff --git a/core/modules/image/tests/src/Functional/ImageDimensionsTest.php b/core/modules/image/tests/src/Functional/ImageDimensionsTest.php index 6a969e57..c1471b75 100644 --- a/core/modules/image/tests/src/Functional/ImageDimensionsTest.php +++ b/core/modules/image/tests/src/Functional/ImageDimensionsTest.php @@ -35,8 +35,12 @@ class ImageDimensionsTest extends BrowserTestBase { /** * Test styled image dimensions cumulatively. + * + * @group legacy + * + * @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 testImageDimensions() { + public function testLegacyImageDimensions() { $image_factory = $this->container->get('image.factory'); // Create a working copy of the file. $files = $this->drupalGetTestFiles('image'); @@ -291,6 +295,276 @@ public function testImageDimensions() { $this->assertEqual($image_file->getHeight(), 50); } + /** + * Test styled image dimensions cumulatively. + */ + public function testImageDimensions() { + $image_factory = $this->container->get('image.factory'); + // Create a working copy of the file. + $files = $this->drupalGetTestFiles('image'); + $file = reset($files); + /** @var \Drupal\Core\File\FileSystemInterface $file_system */ + $file_system = \Drupal::service('file_system'); + $original_uri = $file_system->copy($file->uri, 'public://', FileSystemInterface::EXISTS_RENAME); + + // Create a style. + /** @var $style \Drupal\image\ImageStyleInterface */ + $style = ImageStyle::create(['name' => 'test', 'label' => 'Test']); + $style->save(); + + $pipeline = \Drupal::service('image.processor')->createInstance('derivative') + ->setImageStyle($style) + ->setSourceImageUri($original_uri); + + $generated_uri = 'public://styles/test/public/' . $file_system->basename($original_uri); + $url = file_url_transform_relative($pipeline->getDerivativeImageUrl()->toString()); + + $variables = [ + '#theme' => 'image_style', + '#style_name' => 'test', + '#uri' => $original_uri, + '#width' => 40, + '#height' => 20, + ]; + // Verify that the original image matches the hard-coded values. + $image_file = $image_factory->get($original_uri); + $this->assertSame($variables['#width'], $image_file->getWidth()); + $this->assertSame($variables['#height'], $image_file->getHeight()); + + // Scale an image that is wider than it is high. + $effect = [ + 'id' => 'image_scale', + 'data' => [ + 'width' => 120, + 'height' => 90, + 'upscale' => TRUE, + ], + 'weight' => 0, + ]; + + $style->addImageEffect($effect); + $style->save(); + $this->assertSame('', $this->getImageTag($variables)); + $this->assertFileNotExists($generated_uri, 'Generated file does not exist.'); + $this->drupalGet($this->getAbsoluteUrl($url)); + $this->assertResponse(200, 'Image was generated at the URL.'); + $this->assertFileExists($generated_uri, 'Generated file does exist after we accessed it.'); + $image_file = $image_factory->get($generated_uri); + $this->assertSame(120, $image_file->getWidth()); + $this->assertSame(60, $image_file->getHeight()); + + // Rotate 90 degrees anticlockwise. + $effect = [ + 'id' => 'image_rotate', + 'data' => [ + 'degrees' => -90, + 'random' => FALSE, + ], + 'weight' => 1, + ]; + + $style->addImageEffect($effect); + $style->save(); + $this->assertSame('', $this->getImageTag($variables)); + $this->assertFileNotExists($generated_uri, 'Generated file does not exist.'); + $this->drupalGet($this->getAbsoluteUrl($url)); + $this->assertResponse(200, 'Image was generated at the URL.'); + $this->assertFileExists($generated_uri, 'Generated file does exist after we accessed it.'); + $image_file = $image_factory->get($generated_uri); + $this->assertSame(60, $image_file->getWidth()); + $this->assertSame(120, $image_file->getHeight()); + + // Scale an image that is higher than it is wide (rotated by previous effect). + $effect = [ + 'id' => 'image_scale', + 'data' => [ + 'width' => 120, + 'height' => 90, + 'upscale' => TRUE, + ], + 'weight' => 2, + ]; + + $style->addImageEffect($effect); + $style->save(); + $this->assertSame('', $this->getImageTag($variables)); + $this->assertFileNotExists($generated_uri, 'Generated file does not exist.'); + $this->drupalGet($this->getAbsoluteUrl($url)); + $this->assertResponse(200, 'Image was generated at the URL.'); + $this->assertFileExists($generated_uri, 'Generated file does exist after we accessed it.'); + $image_file = $image_factory->get($generated_uri); + $this->assertSame(45, $image_file->getWidth()); + $this->assertSame(90, $image_file->getHeight()); + + // Test upscale disabled. + $effect = [ + 'id' => 'image_scale', + 'data' => [ + 'width' => 400, + 'height' => 200, + 'upscale' => FALSE, + ], + 'weight' => 3, + ]; + + $style->addImageEffect($effect); + $style->save(); + $this->assertSame('', $this->getImageTag($variables)); + $this->assertFileNotExists($generated_uri, 'Generated file does not exist.'); + $this->drupalGet($this->getAbsoluteUrl($url)); + $this->assertResponse(200, 'Image was generated at the URL.'); + $this->assertFileExists($generated_uri, 'Generated file does exist after we accessed it.'); + $image_file = $image_factory->get($generated_uri); + $this->assertSame(45, $image_file->getWidth()); + $this->assertSame(90, $image_file->getHeight()); + + // Add a desaturate effect. + $effect = [ + 'id' => 'image_desaturate', + 'data' => [], + 'weight' => 4, + ]; + + $style->addImageEffect($effect); + $style->save(); + $this->assertSame('', $this->getImageTag($variables)); + $this->assertFileNotExists($generated_uri, 'Generated file does not exist.'); + $this->drupalGet($this->getAbsoluteUrl($url)); + $this->assertResponse(200, 'Image was generated at the URL.'); + $this->assertFileExists($generated_uri, 'Generated file does exist after we accessed it.'); + $image_file = $image_factory->get($generated_uri); + $this->assertSame(45, $image_file->getWidth()); + $this->assertSame(90, $image_file->getHeight()); + + // Add a random rotate effect. + $effect = [ + 'id' => 'image_rotate', + 'data' => [ + 'degrees' => 180, + 'random' => TRUE, + ], + 'weight' => 5, + ]; + + $style->addImageEffect($effect); + $style->save(); + $this->assertSame('', $this->getImageTag($variables)); + $this->assertFileNotExists($generated_uri, 'Generated file does not exist.'); + $this->drupalGet($this->getAbsoluteUrl($url)); + $this->assertResponse(200, 'Image was generated at the URL.'); + $this->assertFileExists($generated_uri, 'Generated file does exist after we accessed it.'); + + // Add a crop effect. + $effect = [ + 'id' => 'image_crop', + 'data' => [ + 'width' => 30, + 'height' => 30, + 'anchor' => 'center-center', + ], + 'weight' => 6, + ]; + + $style->addImageEffect($effect); + $style->save(); + $this->assertEqual('', $this->getImageTag($variables)); + $this->assertFileNotExists($generated_uri, 'Generated file does not exist.'); + $this->drupalGet($this->getAbsoluteUrl($url)); + $this->assertResponse(200, 'Image was generated at the URL.'); + $this->assertFileExists($generated_uri, 'Generated file does exist after we accessed it.'); + $image_file = $image_factory->get($generated_uri); + $this->assertSame(30, $image_file->getWidth()); + $this->assertSame(30, $image_file->getHeight()); + + // Rotate to a non-multiple of 90 degrees. + $effect = [ + 'id' => 'image_rotate', + 'data' => [ + 'degrees' => 57, + 'random' => FALSE, + ], + 'weight' => 7, + ]; + + $effect_id = $style->addImageEffect($effect); + $style->save(); + // @todo Uncomment this once + // https://www.drupal.org/project/drupal/issues/2670966 is resolved. + // $this->assertSame('', $this->getImageTag($variables)); + $this->assertFileNotExists($generated_uri, 'Generated file does not exist.'); + $this->drupalGet($this->getAbsoluteUrl($url)); + $this->assertResponse(200, 'Image was generated at the URL.'); + $this->assertFileExists($generated_uri, 'Generated file does exist after we accessed it.'); + $image_file = $image_factory->get($generated_uri); + // @todo Uncomment this once + // https://www.drupal.org/project/drupal/issues/2670966 is resolved. + // $this->assertSame(41, $image_file->getWidth()); + // $this->assertSame(41, $image_file->getHeight()); + + $effect_plugin = $style->getEffect($effect_id); + $style->deleteImageEffect($effect_plugin); + + // Ensure that an effect can unset dimensions. + $effect = [ + 'id' => 'image_module_test_null', + 'data' => [], + 'weight' => 8, + ]; + + $style->addImageEffect($effect); + $style->save(); + $this->assertSame('', $this->getImageTag($variables)); + + // Test URI dependent image effect. + $style = ImageStyle::create(['name' => 'test_uri', 'label' => 'Test URI']); + $effect = [ + 'id' => 'image_module_test_uri_dependent', + 'data' => [], + 'weight' => 0, + ]; + $style->addImageEffect($effect); + $style->save(); + $variables = [ + '#theme' => 'image_style', + '#style_name' => 'test_uri', + '#uri' => $original_uri, + '#width' => 40, + '#height' => 20, + ]; + // PNG original image. Should be resized to 100x100. + $pipeline = \Drupal::service('image.processor')->createInstance('derivative') + ->setImageStyle($style) + ->setSourceImageUri($original_uri); + $generated_uri = 'public://styles/test_uri/public/' . $file_system->basename($original_uri); + $url = file_url_transform_relative($pipeline->getDerivativeImageUrl()->toString()); + + $this->assertSame('', $this->getImageTag($variables)); + $this->assertFileNotExists($generated_uri, 'Generated file does not exist.'); + $this->drupalGet($this->getAbsoluteUrl($url)); + $this->assertResponse(200, 'Image was generated at the URL.'); + $this->assertFileExists($generated_uri, 'Generated file does exist after we accessed it.'); + $image_file = $image_factory->get($generated_uri); + $this->assertSame(100, $image_file->getWidth()); + $this->assertSame(100, $image_file->getHeight()); + // GIF original image. Should be resized to 50x50. + $file = $files[1]; + $original_uri = $file_system->copy($file->uri, 'public://', FileSystemInterface::EXISTS_RENAME); + $pipeline = \Drupal::service('image.processor')->createInstance('derivative') + ->setImageStyle($style) + ->setSourceImageUri($original_uri); + $generated_uri = 'public://styles/test_uri/public/' . $file_system->basename($original_uri); + $url = file_url_transform_relative($pipeline->getDerivativeImageUrl()->toString()); + $variables['#uri'] = $original_uri; + $this->assertSame('', $this->getImageTag($variables)); + $this->assertFileNotExists($generated_uri, 'Generated file does not exist.'); + $this->drupalGet($this->getAbsoluteUrl($url)); + $this->assertResponse(200, 'Image was generated at the URL.'); + $this->assertFileExists($generated_uri, 'Generated file does exist after we accessed it.'); + $image_file = $image_factory->get($generated_uri); + $this->assertSame(50, $image_file->getWidth()); + $this->assertSame(50, $image_file->getHeight()); + } + /** * Render an image style element. * diff --git a/core/modules/image/tests/src/Functional/ImageFieldDisplayTest.php b/core/modules/image/tests/src/Functional/ImageFieldDisplayTest.php index 96baad54..ef8f8a10 100644 --- a/core/modules/image/tests/src/Functional/ImageFieldDisplayTest.php +++ b/core/modules/image/tests/src/Functional/ImageFieldDisplayTest.php @@ -195,7 +195,10 @@ public function _testImageFieldFormatters($scheme) { // Ensure the derivative image is generated so we do not have to deal with // image style callback paths. - $this->drupalGet(ImageStyle::load('thumbnail')->buildUrl($image_uri)); + $pipeline = \Drupal::service('image.processor')->createInstance('derivative') + ->setImageStyle(ImageStyle::load('thumbnail')) + ->setSourceImageUri($image_uri); + $this->drupalGet($pipeline->getDerivativeImageUrl()->toString()); $image_style = [ '#theme' => 'image_style', '#uri' => $image_uri, @@ -213,7 +216,7 @@ public function _testImageFieldFormatters($scheme) { if ($scheme == 'private') { // Log out and try to access the file. $this->drupalLogout(); - $this->drupalGet(ImageStyle::load('thumbnail')->buildUrl($image_uri)); + $this->drupalGet($pipeline->getDerivativeImageUrl()->toString()); $this->assertResponse('403', 'Access denied to image style thumbnail as anonymous user.'); } @@ -227,7 +230,7 @@ public function _testImageFieldFormatters($scheme) { // Test the image URL formatter with an image style. $display_options['settings']['image_style'] = 'thumbnail'; - $expected_url = file_url_transform_relative(ImageStyle::load('thumbnail')->buildUrl($image_uri)); + $expected_url = file_url_transform_relative($pipeline->getDerivativeImageUrl()->toString()); $this->assertEqual($expected_url, $node->{$field_name}->view($display_options)[0]['#markup']); } @@ -286,7 +289,10 @@ public function testImageFieldSettings() { $node = $node_storage->load($nid); $file = $node->{$field_name}->entity; - $url = file_url_transform_relative(ImageStyle::load('medium')->buildUrl($file->getFileUri())); + $pipeline = \Drupal::service('image.processor')->createInstance('derivative') + ->setImageStyle(ImageStyle::load('medium')) + ->setSourceImageUri($file->getFileUri()); + $url = file_url_transform_relative($pipeline->getDerivativeImageUrl()->toString()); $this->assertSession()->elementExists('css', 'img[width=40][height=20][class=image-style-medium][src="' . $url . '"]'); // Add alt/title fields to the image and verify that they are displayed. diff --git a/core/modules/image/tests/src/Functional/ImageStyleFlushTest.php b/core/modules/image/tests/src/Functional/ImageStyleFlushTest.php index a5036eec..28aab49a 100644 --- a/core/modules/image/tests/src/Functional/ImageStyleFlushTest.php +++ b/core/modules/image/tests/src/Functional/ImageStyleFlushTest.php @@ -37,8 +37,11 @@ public function createSampleImage($style, $wrapper) { // Make sure we have an image in our wrapper testing file directory. $source_uri = \Drupal::service('file_system')->copy($file->uri, $wrapper . '://'); // Build the derivative image. - $derivative_uri = $style->buildUri($source_uri); - $derivative = $style->createDerivative($source_uri, $derivative_uri); + $pipeline = \Drupal::service('image.processor')->createInstance('derivative') + ->setImageStyle($style) + ->setSourceImageUri($source_uri); + $derivative_uri = $pipeline->getDerivativeImageUri(); + $derivative = $pipeline->buildDerivativeImage(); return $derivative ? $derivative_uri : FALSE; } diff --git a/core/modules/image/tests/src/Functional/ImageStylesPathAndUrlTest.php b/core/modules/image/tests/src/Functional/ImageStylesPathAndUrlTest.php index 194f6e57..417f5128 100644 --- a/core/modules/image/tests/src/Functional/ImageStylesPathAndUrlTest.php +++ b/core/modules/image/tests/src/Functional/ImageStylesPathAndUrlTest.php @@ -58,8 +58,12 @@ protected function setUp() { /** * Tests \Drupal\image\ImageStyleInterface::buildUri(). + * + * @group legacy + * + * @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 testImageStylePath() { + public function testLegacyImageStylePath() { $scheme = 'public'; $actual = $this->style->buildUri("$scheme://foo/bar.gif"); $expected = "$scheme://styles/" . $this->style->id() . "/$scheme/foo/bar.gif"; @@ -72,57 +76,96 @@ public function testImageStylePath() { /** * Tests an image style URL using the "public://" scheme. + * + * @group legacy + * + * @expectedDeprecation The Drupal\image\Entity\ImageStyle::buildUri method is deprecated since version 9.x.x and will be removed in y.y.y. + * @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 testImageStyleUrlAndPathPublic() { - $this->doImageStyleUrlAndPathTests('public'); + public function testLegacyImageStyleUrlAndPathPublic() { + $this->doLegacyImageStyleUrlAndPathTests('public'); } /** * Tests an image style URL using the "private://" scheme. + * + * @group legacy + * + * @expectedDeprecation The Drupal\image\Entity\ImageStyle::buildUri method is deprecated since version 9.x.x and will be removed in y.y.y. + * @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 testImageStyleUrlAndPathPrivate() { - $this->doImageStyleUrlAndPathTests('private'); + public function testLegacyImageStyleUrlAndPathPrivate() { + $this->doLegacyImageStyleUrlAndPathTests('private'); } /** * Tests an image style URL with the "public://" scheme and unclean URLs. + * + * @group legacy + * + * @expectedDeprecation The Drupal\image\Entity\ImageStyle::buildUri method is deprecated since version 9.x.x and will be removed in y.y.y. + * @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 testImageStyleUrlAndPathPublicUnclean() { - $this->doImageStyleUrlAndPathTests('public', FALSE); + public function testLegacyImageStyleUrlAndPathPublicUnclean() { + $this->doLegacyImageStyleUrlAndPathTests('public', FALSE); } /** * Tests an image style URL with the "private://" schema and unclean URLs. + * + * @group legacy + * + * @expectedDeprecation The Drupal\image\Entity\ImageStyle::buildUri method is deprecated since version 9.x.x and will be removed in y.y.y. + * @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 testImageStyleUrlAndPathPrivateUnclean() { - $this->doImageStyleUrlAndPathTests('private', FALSE); + public function testLegacyImageStyleUrlAndPathPrivateUnclean() { + $this->doLegacyImageStyleUrlAndPathTests('private', FALSE); } /** * Tests an image style URL with the "public://" schema and language prefix. + * + * @group legacy + * + * @expectedDeprecation The Drupal\image\Entity\ImageStyle::buildUri method is deprecated since version 9.x.x and will be removed in y.y.y. + * @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 testImageStyleUrlAndPathPublicLanguage() { - $this->doImageStyleUrlAndPathTests('public', TRUE, TRUE, 'fr'); + public function testLegacyImageStyleUrlAndPathPublicLanguage() { + $this->doLegacyImageStyleUrlAndPathTests('public', TRUE, TRUE, 'fr'); } /** * Tests an image style URL with the "private://" schema and language prefix. + * + * @group legacy + * + * @expectedDeprecation The Drupal\image\Entity\ImageStyle::buildUri method is deprecated since version 9.x.x and will be removed in y.y.y. + * @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 testImageStyleUrlAndPathPrivateLanguage() { - $this->doImageStyleUrlAndPathTests('private', TRUE, TRUE, 'fr'); + public function testLegacyImageStyleUrlAndPathPrivateLanguage() { + $this->doLegacyImageStyleUrlAndPathTests('private', TRUE, TRUE, 'fr'); } /** * Tests an image style URL with a file URL that has an extra slash in it. + * + * @group legacy + * + * @expectedDeprecation The Drupal\image\Entity\ImageStyle::buildUri method is deprecated since version 9.x.x and will be removed in y.y.y. + * @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 testImageStyleUrlExtraSlash() { - $this->doImageStyleUrlAndPathTests('public', TRUE, TRUE); + public function testLegacyImageStyleUrlExtraSlash() { + $this->doLegacyImageStyleUrlAndPathTests('public', TRUE, TRUE); } /** * Tests that an invalid source image returns a 404. + * + * @group legacy + * + * @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 testImageStyleUrlForMissingSourceImage() { + public function testLegacyImageStyleUrlForMissingSourceImage() { $non_existent_uri = 'public://foo.png'; $generated_url = $this->style->buildUrl($non_existent_uri); $this->drupalGet($generated_url); @@ -131,8 +174,10 @@ public function testImageStyleUrlForMissingSourceImage() { /** * Tests building an image style URL. + * + * @todo This is a legacy method, remove in y.y.y. */ - public function doImageStyleUrlAndPathTests($scheme, $clean_url = TRUE, $extra_slash = FALSE, $langcode = FALSE) { + public function doLegacyImageStyleUrlAndPathTests($scheme, $clean_url = TRUE, $extra_slash = FALSE, $langcode = FALSE) { $this->prepareRequestForGenerator($clean_url); // Make the default scheme neither "public" nor "private" to verify the @@ -164,7 +209,7 @@ public function doImageStyleUrlAndPathTests($scheme, $clean_url = TRUE, $extra_s // Get the URL of a file that has not been generated and try to create it. $generated_uri = $this->style->buildUri($original_uri); - $this->assertFalse(file_exists($generated_uri), 'Generated file does not exist.'); + $this->assertFileNotExists($generated_uri); $generate_url = $this->style->buildUrl($original_uri, $clean_url); // Make sure that language prefix is never added to the image style URL. @@ -326,4 +371,273 @@ public function doImageStyleUrlAndPathTests($scheme, $clean_url = TRUE, $extra_s $this->assertFalse(file_exists($directory), 'New directory was not created in the filesystem when requesting an unauthorized image.'); } + /** + * @covers \Drupal\image\Plugin\ImageProcessPipeline\Derivative::getDerivativeImageUri + */ + public function testImageStylePath() { + $scheme = 'public'; + + $pipeline = \Drupal::service('image.processor')->createInstance('derivative') + ->setImageStyle($this->style) + ->setSourceImageUri("$scheme://foo/bar.gif"); + // Got the path for a file URI. + $this->assertSame("$scheme://styles/" . $this->style->id() . "/$scheme/foo/bar.gif", $pipeline->getDerivativeImageUri()); + + $pipeline = \Drupal::service('image.processor')->createInstance('derivative') + ->setImageStyle($this->style) + ->setSourceImageUri('foo/bar.gif'); + // Got the path for a relative file path. + $this->assertSame("$scheme://styles/" . $this->style->id() . "/$scheme/foo/bar.gif", $pipeline->getDerivativeImageUri()); + } + + /** + * Tests that an invalid source image returns a 404. + */ + public function testImageStyleUrlForMissingSourceImage() { + $non_existent_uri = 'public://foo.png'; + $generated_url = \Drupal::service('image.processor')->createInstance('derivative') + ->setImageStyle($this->style) + ->setSourceImageUri($non_existent_uri) + ->getDerivativeImageUrl() + ->toString(); + $this->drupalGet($generated_url); + $this->assertResponse(404, 'Accessing an image style URL with a source image that does not exist provides a 404 error response.'); + } + + /** + * Provides setup data for the test method. + */ + public function providerImageStyleUrlAndPath() { + return [ + 'public scheme' => ['public', TRUE, FALSE, NULL], + 'private scheme' => ['private', TRUE, FALSE, NULL], + 'public scheme, unclean URLs' => ['public', FALSE, FALSE, NULL], + 'private scheme, unclean URLs' => ['private', FALSE, FALSE, NULL], + 'public scheme, language prefix' => ['public', TRUE, TRUE, 'fr'], + 'private scheme, language prefix' => ['private', TRUE, TRUE, 'fr'], + 'public scheme, extra slash' => ['public', TRUE, TRUE, NULL], + ]; + } + + /** + * Tests building image style URLs. + * + * @dataProvider providerImageStyleUrlAndPath + */ + public function testImageStyleUrlAndPath(string $scheme, bool $clean_url, bool $extra_slash, ?string $langcode) { + $this->prepareRequestForGenerator($clean_url); + + // Make the default scheme neither "public" nor "private" to verify the + // functions work for other than the default scheme. + $this->config('system.file')->set('default_scheme', 'temporary')->save(); + + // Create the directories for the styles. + $directory = $scheme . '://styles/' . $this->style->id(); + $this->assertNotFalse(\Drupal::service('file_system')->prepareDirectory($directory, FileSystemInterface::CREATE_DIRECTORY)); + + // Override the language to build the URL for the correct language. + if ($langcode) { + $language_manager = \Drupal::service('language_manager'); + $language = $language_manager->getLanguage($langcode); + $language_manager->setConfigOverrideLanguage($language); + } + + // Create a working copy of the file. + $files = $this->drupalGetTestFiles('image'); + $file = array_shift($files); + /** @var \Drupal\Core\File\FileSystemInterface $file_system */ + $file_system = \Drupal::service('file_system'); + $original_uri = $file_system->copy($file->uri, $scheme . '://', FileSystemInterface::EXISTS_RENAME); + // Let the image_module_test module know about this file, so it can claim + // ownership in hook_file_download(). + \Drupal::state()->set('image.test_file_download', $original_uri); + $this->assertNotFalse($original_uri, 'Created the generated image file.'); + + // Get the URL of a file that has not been generated and try to create it. + $pipeline = \Drupal::service('image.processor')->createInstance('derivative') + ->setImageStyle($this->style) + ->setSourceImageUri($original_uri) + ->setCleanUrl($clean_url); + $generated_uri = $pipeline->getDerivativeImageUri(); + $this->assertFileNotExists($generated_uri); + $generate_url = $pipeline->getDerivativeImageUrl()->toString(); + $this->assertFileNotExists($generated_uri); + + // Make sure that language prefix is never added to the image style URL. + if ($langcode) { + $this->assertNotContains("/$langcode/", $generate_url); + } + + // Ensure that the tests still pass when the file is generated by accessing + // a poorly constructed (but still valid) file URL that has an extra slash + // in it. + if ($extra_slash) { + $modified_uri = str_replace('://', ':///', $original_uri); + // Check that an extra slash was added to the generated file URI. + $this->assertNotSame($original_uri, $modified_uri); + $pipeline = \Drupal::service('image.processor')->createInstance('derivative') + ->setImageStyle($this->style) + ->setSourceImageUri($modified_uri) + ->setCleanUrl($clean_url); + $generate_url = $pipeline->getDerivativeImageUrl()->toString(); + } + + // When using non-clean URLS, the system path contains the script name. + if (!$clean_url) { + $this->assertContains('index.php/', $generate_url); + } + + // Add some extra chars to the token. + $this->drupalGet(str_replace(IMAGE_DERIVATIVE_TOKEN . '=', IMAGE_DERIVATIVE_TOKEN . '=Zo', $generate_url)); + $this->assertResponse(404, 'Image was inaccessible at the URL with an invalid token.'); + // Change the parameter name so the token is missing. + $this->drupalGet(str_replace(IMAGE_DERIVATIVE_TOKEN . '=', 'wrongparam=', $generate_url)); + $this->assertResponse(404, 'Image was inaccessible at the URL with a missing token.'); + + // Check that the generated URL is the same when we pass in a relative path + // rather than a URI. We need to temporarily switch the default scheme to + // match the desired scheme before testing this, then switch it back to the + // "temporary" scheme used throughout this test afterwards. + $this->config('system.file')->set('default_scheme', $scheme)->save(); + $relative_path = StreamWrapperManager::getTarget($original_uri); + $pipeline = \Drupal::service('image.processor')->createInstance('derivative') + ->setImageStyle($this->style) + ->setSourceImageUri($relative_path) + ->setCleanUrl($clean_url); + $generate_url_from_relative_path = $pipeline->getDerivativeImageUrl()->toString(); + $this->assertSame($generate_url, $generate_url_from_relative_path); + $this->config('system.file')->set('default_scheme', 'temporary')->save(); + + // Fetch the URL that generates the file. + $this->drupalGet($generate_url); + $this->assertResponse(200, 'Image was generated at the URL.'); + $this->assertFileExists($generated_uri); + // assertRaw can't be used with string containing non UTF-8 chars. + $this->assertNotEmpty(file_get_contents($generated_uri), 'URL returns expected file.'); + $image = $this->container->get('image.factory')->get($generated_uri); + $this->assertSame($image->getMimeType(), $this->drupalGetHeader('Content-Type'), 'Expected Content-Type was reported.'); + $this->assertSame($image->getFileSize(), (int) $this->drupalGetHeader('Content-Length'), 'Expected Content-Length was reported.'); + + // Check that we did not download the original file. + $original_image = $this->container->get('image.factory') + ->get($original_uri); + $this->assertNotSame($original_image->getFileSize(), (int) $this->drupalGetHeader('Content-Length')); + + if ($scheme == 'private') { + $this->assertSame('Sun, 19 Nov 1978 05:00:00 GMT', $this->drupalGetHeader('Expires')); + $this->assertContains('no-cache', $this->drupalGetHeader('Cache-Control')); + $this->assertSame('image_module_test', $this->drupalGetHeader('X-Image-Owned-By')); + + // Make sure that a second request to the already existing derivative + // works too. + $this->drupalGet($generate_url); + $this->assertResponse(200, 'Image was generated at the URL.'); + + // Check that the second request also returned the generated image. + $this->assertSame($image->getFileSize(), (int) $this->drupalGetHeader('Content-Length')); + + // Check that we did not download the original file. + $this->assertNotSame($original_image->getFileSize(), (int) $this->drupalGetHeader('Content-Length')); + + // Make sure that access is denied for existing style files if we do not + // have access. + \Drupal::state()->delete('image.test_file_download'); + $this->drupalGet($generate_url); + $this->assertResponse(403, 'Confirmed that access is denied for the private image style.'); + + // Repeat this with a different file that we do not have access to and + // make sure that access is denied. + $file_noaccess = array_shift($files); + $original_uri_noaccess = $file_system->copy($file_noaccess->uri, $scheme . '://', FileSystemInterface::EXISTS_RENAME); + $generated_uri_noaccess = $scheme . '://styles/' . $this->style->id() . '/' . $scheme . '/' . $file_system->basename($original_uri_noaccess); + $this->assertFileNotExists($generated_uri_noaccess); + $pipeline = \Drupal::service('image.processor')->createInstance('derivative') + ->setImageStyle($this->style) + ->setSourceImageUri($original_uri_noaccess); + $generate_url_noaccess = $pipeline->getDerivativeImageUrl()->toString(); + + $this->drupalGet($generate_url_noaccess); + $this->assertResponse(403, 'Confirmed that access is denied for the private image style.'); + // Verify that images are not appended to the response. + // Currently this test only uses PNG images. + $this->assertContains('.png', $generate_url, 'Confirming that private image styles are not appended require PNG file.'); + // Check for PNG-Signature + // (cf. http://www.libpng.org/pub/png/book/chapter08.html#png.ch08.div.2) + // in the response body. + $raw = $this->getSession()->getPage()->getContent(); + $this->assertNotContains(chr(137) . chr(80) . chr(78) . chr(71) . chr(13) . chr(10) . chr(26) . chr(10), $raw); + } + else { + $this->assertSame('Sun, 19 Nov 1978 05:00:00 GMT', $this->drupalGetHeader('Expires'), 'Expires header was sent.'); + $this->assertNotContains('no-cache', $this->drupalGetHeader('Cache-Control')); + + if ($clean_url) { + // Add some extra chars to the token. + $this->drupalGet(str_replace(IMAGE_DERIVATIVE_TOKEN . '=', IMAGE_DERIVATIVE_TOKEN . '=Zo', $generate_url)); + $this->assertResponse(200, 'Existing image was accessible at the URL with an invalid token.'); + } + } + + // Allow insecure image derivatives to be created for the remainder of this + // test. + $this->config('image.settings') + ->set('allow_insecure_derivatives', TRUE) + ->save(); + + // Create another working copy of the file. + $files = $this->drupalGetTestFiles('image'); + $file = array_shift($files); + $original_uri = $file_system->copy($file->uri, $scheme . '://', FileSystemInterface::EXISTS_RENAME); + // Let the image_module_test module know about this file, so it can claim + // ownership in hook_file_download(). + \Drupal::state()->set('image.test_file_download', $original_uri); + + // Suppress the security token in the URL, then get the URL of a file that + // has not been created and try to create it. Check that the security token + // is not present in the URL but that the image is still accessible. + $this->config('image.settings')->set('suppress_itok_output', TRUE)->save(); + $pipeline = \Drupal::service('image.processor')->createInstance('derivative') + ->setImageStyle($this->style) + ->setSourceImageUri($original_uri) + ->setCleanUrl($clean_url); + $generated_uri = $pipeline->getDerivativeImageUri(); + $this->assertFileNotExists($generated_uri, 'Generated file does not exist.'); + $generate_url = $pipeline->getDerivativeImageUrl()->toString(); + $this->assertNotContains(IMAGE_DERIVATIVE_TOKEN . '=', $generate_url, 'The security token does not appear in the image style URL.'); + $this->drupalGet($generate_url); + $this->assertResponse(200, 'Image was accessible at the URL with a missing token.'); + + // Stop suppressing the security token in the URL. + $this->config('image.settings')->set('suppress_itok_output', FALSE)->save(); + // Ensure allow_insecure_derivatives is enabled. + $this->assertTrue($this->config('image.settings')->get('allow_insecure_derivatives')); + // Check that a security token is still required when generating a second + // image derivative using the first one as a source. + $pipeline = \Drupal::service('image.processor')->createInstance('derivative') + ->setImageStyle($this->style) + ->setSourceImageUri($generated_uri) + ->setCleanUrl($clean_url); + $nested_url = $pipeline->getDerivativeImageUrl()->toString(); + $matches_expected_url_format = (boolean) preg_match('/styles\/' . $this->style->id() . '\/' . $scheme . '\/styles\/' . $this->style->id() . '\/' . $scheme . '/', $nested_url); + $this->assertTrue($matches_expected_url_format, "URL for a derivative of an image style matches expected format."); + $nested_url_with_wrong_token = str_replace(IMAGE_DERIVATIVE_TOKEN . '=', 'wrongparam=', $nested_url); + $this->drupalGet($nested_url_with_wrong_token); + $this->assertResponse(404, 'Image generated from an earlier derivative was inaccessible at the URL with a missing token.'); + // Check that this restriction cannot be bypassed by adding extra slashes + // to the URL. + $this->drupalGet(substr_replace($nested_url_with_wrong_token, '//styles/', strrpos($nested_url_with_wrong_token, '/styles/'), strlen('/styles/'))); + $this->assertResponse(404, 'Image generated from an earlier derivative was inaccessible at the URL with a missing token, even with an extra forward slash in the URL.'); + $this->drupalGet(substr_replace($nested_url_with_wrong_token, '////styles/', strrpos($nested_url_with_wrong_token, '/styles/'), strlen('/styles/'))); + $this->assertResponse(404, 'Image generated from an earlier derivative was inaccessible at the URL with a missing token, even with multiple forward slashes in the URL.'); + // Make sure the image can still be generated if a correct token is used. + $this->drupalGet($nested_url); + $this->assertResponse(200, 'Image was accessible when a correct token was provided in the URL.'); + + // Check that requesting a nonexistent image does not create any new + // directories in the file system. + $directory = $scheme . '://styles/' . $this->style->id() . '/' . $scheme . '/' . $this->randomMachineName(); + $this->drupalGet(file_create_url($directory . '/' . $this->randomString())); + $this->assertFileNotExists($directory, 'New directory was not created in the filesystem when requesting an unauthorized image.'); + } + } 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 00000000..24f7c5ba --- /dev/null +++ b/core/modules/image/tests/src/Kernel/DerivativeImageProcessTest.php @@ -0,0 +1,214 @@ +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 ::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 an URI, file missing. + $pipeline = $this->imageProcessor->createInstance('derivative') + ->setImageStyle($this->imageStyle) + ->setSourceImageUri('public://missing.png'); + $this->assertFalse($pipeline->buildDerivativeImage()); + $this->assertFalse(file_exists('public://styles/thumbnail/public/missing.png')); + + // 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/ImageStyleCustomStreamWrappersTest.php b/core/modules/image/tests/src/Kernel/ImageStyleCustomStreamWrappersTest.php index 3e0b345a..8b16c728 100644 --- a/core/modules/image/tests/src/Kernel/ImageStyleCustomStreamWrappersTest.php +++ b/core/modules/image/tests/src/Kernel/ImageStyleCustomStreamWrappersTest.php @@ -75,7 +75,11 @@ public function register(ContainerBuilder $container) { * The derivative expected stream wrapper scheme. */ public function testCustomStreamWrappers($source_scheme, $expected_scheme) { - $derivative_uri = $this->imageStyle->buildUri("$source_scheme://some/path/image.png"); + // Create an image process pipeline. + $pipeline = \Drupal::service('image.processor')->createInstance('derivative') + ->setImageStyle($this->imageStyle) + ->setSourceImageUri("$source_scheme://some/path/image.png"); + $derivative_uri = $pipeline->getDerivativeImageUri(); $derivative_scheme = StreamWrapperManager::getScheme($derivative_uri); // Check that the derivative scheme is the expected scheme. 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 00000000..3f6e7700 --- /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/Kernel/ImageThemeFunctionTest.php b/core/modules/image/tests/src/Kernel/ImageThemeFunctionTest.php index 0d219694..adaa5e46 100644 --- a/core/modules/image/tests/src/Kernel/ImageThemeFunctionTest.php +++ b/core/modules/image/tests/src/Kernel/ImageThemeFunctionTest.php @@ -86,7 +86,12 @@ public function testImageFormatterTheme() { // Create a style. $style = ImageStyle::create(['name' => 'test', 'label' => 'Test']); $style->save(); - $url = file_url_transform_relative($style->buildUrl($original_uri)); + + // Create an image process pipeline. + $pipeline = \Drupal::service('image.processor')->createInstance('derivative') + ->setImageStyle($style) + ->setSourceImageUri($original_uri); + $url = file_url_transform_relative($pipeline->getDerivativeImageUrl()->toString()); // Create a test entity with the image field set. $entity = EntityTest::create(); @@ -148,7 +153,12 @@ public function testImageStyleTheme() { // Create a style. $style = ImageStyle::create(['name' => 'image_test', 'label' => 'Test']); $style->save(); - $url = file_url_transform_relative($style->buildUrl($original_uri)); + + // Create an image process pipeline. + $pipeline = \Drupal::service('image.processor')->createInstance('derivative') + ->setImageStyle($style) + ->setSourceImageUri($original_uri); + $url = file_url_transform_relative($pipeline->getDerivativeImageUrl()->toString()); // Create the base element that we'll use in the tests below. $base_element = [ 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 f02e3eef..00000000 --- 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/modules/rdf/tests/src/Functional/ImageFieldAttributesTest.php b/core/modules/rdf/tests/src/Functional/ImageFieldAttributesTest.php index faa55de8..93cc3ba0 100644 --- a/core/modules/rdf/tests/src/Functional/ImageFieldAttributesTest.php +++ b/core/modules/rdf/tests/src/Functional/ImageFieldAttributesTest.php @@ -114,7 +114,11 @@ public function testNodeTeaser() { // Construct the node and image URIs for testing. $node_uri = $this->node->toUrl('canonical', ['absolute' => TRUE])->toString(); - $image_uri = ImageStyle::load('medium')->buildUrl($this->file->getFileUri()); + $image_uri = \Drupal::service('image.processor')->createInstance('derivative') + ->setImageStyle(ImageStyle::load('medium')) + ->setSourceImageUri($this->file->getFileUri()) + ->getDerivativeImageUrl() + ->toString(); // Test relations from node to image. $expected_value = [ diff --git a/core/modules/rdf/tests/src/Functional/StandardProfileTest.php b/core/modules/rdf/tests/src/Functional/StandardProfileTest.php index 279e92f5..32813447 100644 --- a/core/modules/rdf/tests/src/Functional/StandardProfileTest.php +++ b/core/modules/rdf/tests/src/Functional/StandardProfileTest.php @@ -172,7 +172,11 @@ protected function setUp() { // Set URIs. // Image. $image_file = $this->article->get('field_image')->entity; - $this->imageUri = ImageStyle::load('large')->buildUrl($image_file->getFileUri()); + $this->imageUri = \Drupal::service('image.processor')->createInstance('derivative') + ->setImageStyle(ImageStyle::load('large')) + ->setSourceImageUri($image_file->getFileUri()) + ->getDerivativeImageUrl() + ->toString(); // Term. $this->termUri = $this->term->toUrl('canonical', ['absolute' => TRUE])->toString(); // Article. diff --git a/core/modules/responsive_image/responsive_image.module b/core/modules/responsive_image/responsive_image.module index 36ea84d2..47ae6e9a 100644 --- a/core/modules/responsive_image/responsive_image.module +++ b/core/modules/responsive_image/responsive_image.module @@ -448,7 +448,13 @@ function responsive_image_get_image_dimensions($image_style_name, array $dimensi ]; } elseif ($entity = ImageStyle::load($image_style_name)) { - $entity->transformDimensions($dimensions, $uri); + $pipeline = \Drupal::service('image.processor')->createInstance('derivative'); + $pipeline + ->setImageStyle($entity) + ->setSourceImageUri($uri) + ->setSourceImageDimensions($dimensions['width'] ?? NULL, $dimensions['height'] ?? NULL); + $dimensions['width'] = $pipeline->getDerivativeImageWidth(); + $dimensions['height'] = $pipeline->getDerivativeImageHeight(); } return $dimensions; @@ -475,7 +481,10 @@ function responsive_image_get_mime_type($image_style_name, $extension) { $fake_path = 'responsive_image.' . $extension; } else { - $fake_path = 'responsive_image.' . ImageStyle::load($image_style_name)->getDerivativeExtension($extension); + $pipeline = \Drupal::service('image.processor')->createInstance('derivative') + ->setImageStyle(ImageStyle::load($image_style_name)) + ->setSourceImageFileExtension($extension); + $fake_path = 'responsive_image.' . $pipeline->getDerivativeImageFileExtension(); } return Drupal::service('file.mime_type.guesser.extension')->guess($fake_path); } @@ -491,7 +500,11 @@ function _responsive_image_image_style_url($style_name, $path) { } $entity = ImageStyle::load($style_name); if ($entity instanceof ImageStyle) { - return file_url_transform_relative($entity->buildUrl($path)); + $pipeline = \Drupal::service('image.processor')->createInstance('derivative'); + $pipeline + ->setImageStyle($entity) + ->setSourceImageUri($path); + return file_url_transform_relative($pipeline->getDerivativeImageUrl()->toString()); } return file_url_transform_relative(file_create_url($path)); } diff --git a/core/modules/responsive_image/tests/src/Functional/ResponsiveImageFieldDisplayTest.php b/core/modules/responsive_image/tests/src/Functional/ResponsiveImageFieldDisplayTest.php index 2e94a1da..82057c3c 100644 --- a/core/modules/responsive_image/tests/src/Functional/ResponsiveImageFieldDisplayTest.php +++ b/core/modules/responsive_image/tests/src/Functional/ResponsiveImageFieldDisplayTest.php @@ -280,8 +280,10 @@ protected function doTestResponsiveImageFieldFormatters($scheme, $empty_styles = ->save(); // Create a derivative so at least one MIME type will be known. - $large_style = ImageStyle::load('large'); - $large_style->createDerivative($image_uri, $large_style->buildUri($image_uri)); + $large_style_pipeline = \Drupal::service('image.processor')->createInstance('derivative') + ->setImageStyle(ImageStyle::load('large')) + ->setSourceImageUri($image_uri); + $large_style_pipeline->buildDerivativeImage(); // Output should contain all image styles and all breakpoints. $this->drupalGet('node/' . $nid); @@ -289,9 +291,11 @@ protected function doTestResponsiveImageFieldFormatters($scheme, $empty_styles = $this->assertRaw('/styles/medium/'); // Assert the empty image is present. $this->assertRaw('data:image/gif;base64,R0lGODlhAQABAIABAP///wAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='); - $thumbnail_style = ImageStyle::load('thumbnail'); + $thumbnail_style_pipeline = \Drupal::service('image.processor')->createInstance('derivative') + ->setImageStyle(ImageStyle::load('thumbnail')) + ->setSourceImageUri($image_uri); // Assert the output of the 'srcset' attribute (small multipliers first). - $this->assertRaw('data:image/gif;base64,R0lGODlhAQABAIABAP///wAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw== 1x, ' . file_url_transform_relative($thumbnail_style->buildUrl($image_uri)) . ' 1.5x'); + $this->assertRaw('data:image/gif;base64,R0lGODlhAQABAIABAP///wAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw== 1x, ' . file_url_transform_relative($thumbnail_style_pipeline->getDerivativeImageUrl()->toString()) . ' 1.5x'); $this->assertRaw('/styles/medium/'); // Assert the output of the original image. $this->assertRaw(file_url_transform_relative(file_create_url($image_uri)) . ' 3x'); @@ -302,8 +306,10 @@ protected function doTestResponsiveImageFieldFormatters($scheme, $empty_styles = $this->assertRaw('sizes="(min-width: 700px) 700px, 100vw"'); $this->assertPattern('/media="\(min-width: 560px\)".+?sizes="\(min-width: 700px\) 700px, 100vw"/'); // Assert the output of the 'srcset' attribute (small images first). - $medium_style = ImageStyle::load('medium'); - $this->assertRaw(file_url_transform_relative($medium_style->buildUrl($image_uri)) . ' 220w, ' . file_url_transform_relative($large_style->buildUrl($image_uri)) . ' 360w'); + $medium_style_pipeline = \Drupal::service('image.processor')->createInstance('derivative') + ->setImageStyle(ImageStyle::load('medium')) + ->setSourceImageUri($image_uri); + $this->assertRaw(file_url_transform_relative($medium_style_pipeline->getDerivativeImageUrl()->toString()) . ' 220w, ' . file_url_transform_relative($large_style_pipeline->getDerivativeImageUrl()->toString()) . ' 360w'); $this->assertRaw('media="(min-width: 851px)"'); } $this->assertRaw('/styles/large/'); @@ -318,10 +324,13 @@ protected function doTestResponsiveImageFieldFormatters($scheme, $empty_styles = // Test the fallback image style. $image = \Drupal::service('image.factory')->get($image_uri); + $large_style_pipeline = \Drupal::service('image.processor')->createInstance('derivative') + ->setImageStyle(ImageStyle::load('large')) + ->setSourceImageUri($image->getSource()); $fallback_image = [ '#theme' => 'image', '#alt' => $alt, - '#uri' => file_url_transform_relative($large_style->buildUrl($image->getSource())), + '#uri' => file_url_transform_relative($large_style_pipeline->getDerivativeImageUrl()->toString()), ]; // The image.html.twig template has a newline after the tag but // responsive-image.html.twig doesn't have one after the fallback image, so @@ -332,7 +341,10 @@ protected function doTestResponsiveImageFieldFormatters($scheme, $empty_styles = if ($scheme == 'private') { // Log out and try to access the file. $this->drupalLogout(); - $this->drupalGet($large_style->buildUrl($image_uri)); + $large_style_pipeline = \Drupal::service('image.processor')->createInstance('derivative') + ->setImageStyle(ImageStyle::load('large')) + ->setSourceImageUri($image_uri); + $this->drupalGet($large_style_pipeline->getDerivativeImageUrl()->toString()); $this->assertResponse('403', 'Access denied to image style large as anonymous user.'); $cache_tags_header = $this->drupalGetHeader('X-Drupal-Cache-Tags'); $this->assertTrue(!preg_match('/ image_style\:/', $cache_tags_header), 'No image style cache tag found.'); @@ -399,10 +411,12 @@ public function testResponsiveImageFieldFormattersEmptyMediaQuery() { $this->assertSession()->responseNotMatches('@srcset="data:image/gif;base64,R0lGODlhAQABAIABAP///wAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw== 1x".+?media=".+?/>load($nid); $image_uri = File::load($node->{$field_name}->target_id)->getFileUri(); - $this->assertPattern('/srcset="' . preg_quote(file_url_transform_relative($thumbnail_style->buildUrl($image_uri)), '/') . ' 1x".+?media="\(min-width: 0px\)"/'); + $thumbnail_style_pipeline = \Drupal::service('image.processor')->createInstance('derivative') + ->setImageStyle(ImageStyle::load('thumbnail')) + ->setSourceImageUri($image_uri); + $this->assertPattern('/srcset="' . preg_quote(file_url_transform_relative($thumbnail_style_pipeline->getDerivativeImageUrl()->toString()), '/') . ' 1x".+?media="\(min-width: 0px\)"/'); } /** @@ -445,11 +459,15 @@ public function testResponsiveImageFieldFormattersOneSource() { $this->drupalGet('node/' . $nid); // Assert the media attribute is present if it has a value. - $large_style = ImageStyle::load('large'); - $medium_style = ImageStyle::load('medium'); $node = $node_storage->load($nid); $image_uri = File::load($node->{$field_name}->target_id)->getFileUri(); - $this->assertRaw('createInstance('derivative') + ->setImageStyle(ImageStyle::load('large')) + ->setSourceImageUri($image_uri); + $medium_style_pipeline = \Drupal::service('image.processor')->createInstance('derivative') + ->setImageStyle(ImageStyle::load('medium')) + ->setSourceImageUri($image_uri); + $this->assertRaw('save(); // Create a derivative so at least one MIME type will be known. - $large_style = ImageStyle::load('large'); $image_uri = File::load($node->{$field_name}->target_id)->getFileUri(); - $large_style->createDerivative($image_uri, $large_style->buildUri($image_uri)); + \Drupal::service('image.processor')->createInstance('derivative') + ->setImageStyle(ImageStyle::load('large')) + ->setSourceImageUri($image_uri) + ->buildDerivativeImage(); // Output should contain all image styles and all breakpoints. $this->drupalGet('node/' . $nid); diff --git a/core/modules/user/tests/src/Functional/UserPictureTest.php b/core/modules/user/tests/src/Functional/UserPictureTest.php index 9aa5e45a..6936e0a9 100644 --- a/core/modules/user/tests/src/Functional/UserPictureTest.php +++ b/core/modules/user/tests/src/Functional/UserPictureTest.php @@ -113,8 +113,10 @@ public function testPictureOnNodeComment() { $this->config('system.theme.global')->set('features.node_user_picture', TRUE)->save(); $image_style_id = $this->config('core.entity_view_display.user.user.compact')->get('content.user_picture.settings.image_style'); - $style = ImageStyle::load($image_style_id); - $image_url = file_url_transform_relative($style->buildUrl($file->getfileUri())); + $pipeline = \Drupal::service('image.processor')->createInstance('derivative') + ->setImageStyle(ImageStyle::load($image_style_id)) + ->setSourceImageUri($file->getfileUri()); + $image_url = file_url_transform_relative($pipeline->getDerivativeImageUrl()->toString()); $alt_text = 'Profile picture for user ' . $this->webUser->getAccountName(); // Verify that the image is displayed on the node page.