In order to support alternative stream wrappers like S3 and maintain performance, contributed modules often have to reimplement significant parts of the image generation process. For example, with S3 we want to first serve an image locally, and then upload it in the background to mask the several seconds it takes to upload an image.

This patch takes the bare minimum approach, and refactors deliver() into several protected methods. This significantly simplifies the implementation for the Flysystem module, and helps avoid bugs in image token and request validation.

A more complete approach would be to refactor most of the new methods into a separate class that can be unit tested, decoupling generation from a Request object. It looks like that's underway over at #2359443: Allow creating image derivatives from an Image object, though that patch doesn't address the issues with the deliver() method.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

deviantintegral created an issue. See original summary.

deviantintegral’s picture

Status: Active » Needs review
FileSize
9.58 KB
deviantintegral’s picture

twang’s picture

  1. +++ b/core/modules/image/src/Controller/ImageStyleDownloadController.php
    @@ -131,23 +115,97 @@ public function deliver(Request $request, $scheme, ImageStyleInterface $image_st
    +      $image_uri = $this->validateSource($image_uri);
    

    Break the original method into several methods makes the code much easier to read!

  2. +++ b/core/modules/image/src/Controller/ImageStyleDownloadController.php
    @@ -131,23 +115,97 @@ public function deliver(Request $request, $scheme, ImageStyleInterface $image_st
    +      $this->logger->notice('Source image at %source_image_path not found while trying to generate derivative image at %derivative_path.', array('%source_image_path' => $image_uri, '%derivative_path' => $derivative_uri));
    

    Does this long line ok with phpcs rules?

  3. +++ b/core/modules/image/src/Controller/ImageStyleDownloadController.php
    @@ -131,23 +115,97 @@ public function deliver(Request $request, $scheme, ImageStyleInterface $image_st
    +      return new Response($this->t('Error generating image.'), 500);
    

    Feel like the response better include the same msg logged above, rather than just saying "Error generating image".

deviantintegral’s picture

Does this long line ok with phpcs rules?

It does! The use statement is actually used in an @var declaration.

phpcs --standard=Drupal ImageStyleDownloadController.php

FILE: ...re/modules/image/src/Controller/ImageStyleDownloadController.php
----------------------------------------------------------------------
FOUND 0 ERRORS AND 1 WARNING AFFECTING 1 LINE
----------------------------------------------------------------------
 15 | WARNING | [x] Unused use statement
----------------------------------------------------------------------
PHPCBF CAN FIX THE 1 MARKED SNIFF VIOLATIONS AUTOMATICALLY
----------------------------------------------------------------------

Time: 202ms; Memory: 7.25Mb

What would you suggest for the error message? I didn't change the text in this patch. The image style only returns TRUE or FALSE based on if the image could be generated or not, without any additional context. It'd be nice to fix this, but I think that's probably better done at #2359443: Allow creating image derivatives from an Image object?

mondrake’s picture

Status: Needs review » Reviewed & tested by the community

Looks good to me, contrib will be able to extend the class and change only some of the methods instead of rewriting the entire deliver() method.

#4.3 changing the existing error string would be out of scope here.

mondrake’s picture

Status: Reviewed & tested by the community » Needs work

Second thinking :)

Actually since you mention #2359443: Allow creating image derivatives from an Image object, in order for this to work better with it later, I would suggest

  1. +++ b/core/modules/image/src/Controller/ImageStyleDownloadController.php
    @@ -131,23 +115,97 @@ public function deliver(Request $request, $scheme, ImageStyleInterface $image_st
    +  protected function send($scheme, $derivative_uri, $headers = array()) {
    +    $image = $this->imageFactory->get($derivative_uri);
    +    $uri = $image->getSource();
    

    pass $image as a parameter to send() instead of $derivative_uri

  2. +++ b/core/modules/image/src/Controller/ImageStyleDownloadController.php
    @@ -131,23 +115,97 @@ public function deliver(Request $request, $scheme, ImageStyleInterface $image_st
    +   * @return bool
    +   *   TRUE if the image exists or was generated, FALSE otherwise.
    +   */
    +  protected function generate(ImageStyleInterface $image_style, $image_uri, $derivative_uri) {
         // Don't start generating the image if the derivative already exists or if
    

    return an Image object instead of a bool - the image object of the generated derivative image, that can then be passed to send() - or FALSE/NULL in case of failure.

deviantintegral’s picture

Status: Needs work » Needs review
FileSize
10.17 KB
7.7 KB

Good points. I've addressed them in this patch, and changed generate() from returning booleans to throwing an exception.

mondrake’s picture

Status: Needs review » Reviewed & tested by the community

Looks good to me. It's purely refactoring within the ImageStyleDownloadController class - with introduction of some protected methods. Not sure whether it would need additional tests for that - let's bring to the right eyes and see feedback :)

RTBC

alexpott’s picture

Status: Reviewed & tested by the community » Needs work
+++ b/core/modules/image/src/Controller/ImageStyleDownloadController.php
@@ -157,27 +214,54 @@ public function deliver(Request $request, $scheme, ImageStyleInterface $image_st
+      throw new \RuntimeException(sprintf('%s was unable to be generated', $derivative_uri));

Let's add a specific exception for this and we need an @throws for this.

twistor’s picture

Status: Needs work » Needs review
FileSize
2.18 KB
10.97 KB
dawehner’s picture

Nice refactoring in general!

+++ b/core/modules/image/src/Controller/ImageStyleDownloadController.php
@@ -125,23 +111,100 @@ public function deliver(Request $request, $scheme, ImageStyleInterface $image_st
+    catch (\RuntimeException $e) {
+      $this->logger->notice('Unable to generate the derived image located at %path.', array('%path' => $derivative_uri));
+      return new Response($this->t('Error generating image.'), 500);
+    }

Is there a reason we catch any kind of exception and not just the more specific one like in the snippet above?

twistor’s picture

Because I forgot to change it?

Status: Needs review » Needs work

The last submitted patch, 13: support_delivering-2661588-25.patch, failed testing.

twistor’s picture

Entirely wrong patch. Time for bed.

mondrake’s picture

#10 has been addressed. RTBC again.

mondrake’s picture

Status: Needs review » Reviewed & tested by the community
claudiu.cristea’s picture

Status: Reviewed & tested by the community » Needs work

Nice cleanup!

Few nits:

  1. +++ b/core/modules/image/src/Controller/ImageStyleDownloadController.php
    @@ -125,23 +111,100 @@ public function deliver(Request $request, $scheme, ImageStyleInterface $image_st
    +      $this->logger->notice('Source image at %source_image_path not found while trying to generate derivative image at %derivative_path.', array('%source_image_path' => $image_uri, '%derivative_path' => $derivative_uri));
    ...
    +      $this->logger->notice('Unable to generate the derived image located at %path.', array('%path' => $derivative_uri));
    ...
    +  protected function send($scheme, ImageInterface $image, $headers = array()) {
    ...
    +    $headers += array(
    ...
    +    );
    

    Lets' use modern square brackets syntax for arrays.

  2. +++ b/core/modules/image/src/Controller/ImageStyleDownloadController.php
    @@ -157,27 +220,54 @@ public function deliver(Request $request, $scheme, ImageStyleInterface $image_st
    +      throw new ImageGenerationFailedException(sprintf('%s was unable to be generated', $derivative_uri));
    

    In such cases, by using double quotes, you can simply inject $derivative_uri into string and avoid any function call like spintf().

  3. +++ b/core/modules/image/src/Controller/ImageStyleDownloadController.php
    @@ -157,27 +220,54 @@ public function deliver(Request $request, $scheme, ImageStyleInterface $image_st
    +    if (!$this->config('image.settings')
    

    I would prefer to move $this->config('image.settings')->get('allow_insecure_derivatives') in variable just for if (...) readability.

Version: 8.2.x-dev » 8.3.x-dev

Drupal 8.2.0-beta1 was released on August 3, 2016, which means new developments and disruptive changes should now be targeted against the 8.3.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.3.x-dev » 8.4.x-dev

Drupal 8.3.0-alpha1 will be released the week of January 30, 2017, which means new developments and disruptive changes should now be targeted against the 8.4.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.4.x-dev » 8.5.x-dev

Drupal 8.4.0-alpha1 will be released the week of July 31, 2017, which means new developments and disruptive changes should now be targeted against the 8.5.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.5.x-dev » 8.6.x-dev

Drupal 8.5.0-alpha1 will be released the week of January 17, 2018, which means new developments and disruptive changes should now be targeted against the 8.6.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.6.x-dev » 8.7.x-dev

Drupal 8.6.0-alpha1 will be released the week of July 16, 2018, which means new developments and disruptive changes should now be targeted against the 8.7.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.7.x-dev » 8.8.x-dev

Drupal 8.7.0-alpha1 will be released the week of March 11, 2019, which means new developments and disruptive changes should now be targeted against the 8.8.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.8.x-dev » 8.9.x-dev

Drupal 8.8.0-alpha1 will be released the week of October 14th, 2019, which means new developments and disruptive changes should now be targeted against the 8.9.x-dev branch. (Any changes to 8.9.x will also be committed to 9.0.x in preparation for Drupal 9’s release, but some changes like significant feature additions will be deferred to 9.1.x.). For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 8.9.x-dev » 9.1.x-dev

Drupal 8.9.0-beta1 was released on March 20, 2020. 8.9.x is the final, long-term support (LTS) minor release of Drupal 8, which means new developments and disruptive changes should now be targeted against the 9.1.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 9.1.x-dev » 9.2.x-dev

Drupal 9.1.0-alpha1 will be released the week of October 19, 2020, which means new developments and disruptive changes should now be targeted for the 9.2.x-dev branch. For more information see the Drupal 9 minor version schedule and the Allowed changes during the Drupal 9 release cycle.

Version: 9.2.x-dev » 9.3.x-dev

Drupal 9.2.0-alpha1 will be released the week of May 3, 2021, which means new developments and disruptive changes should now be targeted for the 9.3.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.3.x-dev » 9.4.x-dev

Drupal 9.3.0-rc1 was released on November 26, 2021, which means new developments and disruptive changes should now be targeted for the 9.4.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.4.x-dev » 9.5.x-dev

Drupal 9.4.0-alpha1 was released on May 6, 2022, which means new developments and disruptive changes should now be targeted for the 9.5.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.5.x-dev » 10.1.x-dev

Drupal 9.5.0-beta2 and Drupal 10.0.0-beta2 were released on September 29, 2022, which means new developments and disruptive changes should now be targeted for the 10.1.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 10.1.x-dev » 11.x-dev

Drupal core is moving towards using a “main” branch. As an interim step, a new 11.x branch has been opened, as Drupal.org infrastructure cannot currently fully support a branch named main. New developments and disruptive changes should now be targeted for the 11.x branch, which currently accepts only minor-version allowed changes. For more information, see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

mstrelan’s picture

This is nice, but not help when more than one module needs to override the controller, e.g. stage_file_proxy and webp and avif. I'm wondering if it would be possible to have a service that can be decorated by multiple modules instead? See https://symfony.com/doc/current/service_container/service_decoration.html for details on service decoration in symfony. Then the ::deliver method can just call a method on the service and each module can decorate it with it's own behaviour.