From 31f9e8a9c642b861161401c9de005a752bf6f201 Mon Sep 17 00:00:00 2001
From: Alex Pott <alex.a.pott@googlemail.com>
Date: Tue, 9 Apr 2024 23:33:15 +0100
Subject: [PATCH] =?UTF-8?q?Issue=20#2786735=20by=20recrit,=20eiriksm,=20ra?=
 =?UTF-8?q?njith=5Fkumar=5Fk=5Fu,=20immaculatexavier,=20ravi.shankar,=20za?=
 =?UTF-8?q?porylie,=20smustgrave,=20TrevorBradley,=20johnle,=20Ga=C3=ABlG,?=
 =?UTF-8?q?=20jefuri,=20alexpott,=20poker10:=20Image=20derivative=20genera?=
 =?UTF-8?q?tion=20does=20not=20work=20if=20effect=20"Convert"=20in=20use?=
 =?UTF-8?q?=20and=20file=20stored=20in=20private=20filesystem?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 core/modules/image/image.module               | 13 ++++
 .../ImageStyleDownloadController.php          | 65 ++++++++++++++-----
 .../Functional/ImageStylesPathAndUrlTest.php  | 16 +++++
 3 files changed, 76 insertions(+), 18 deletions(-)

diff --git a/core/modules/image/image.module b/core/modules/image/image.module
index 4203045b5952..0d8bd4b1f265 100644
--- a/core/modules/image/image.module
+++ b/core/modules/image/image.module
@@ -13,6 +13,7 @@
 use Drupal\field\FieldConfigInterface;
 use Drupal\field\FieldStorageConfigInterface;
 use Drupal\file\FileInterface;
+use Drupal\image\Controller\ImageStyleDownloadController;
 use Drupal\image\Entity\ImageStyle;
 
 /**
@@ -151,6 +152,18 @@ function image_file_download($uri) {
     // Check that the file exists and is an image.
     $image = \Drupal::service('image.factory')->get($uri);
     if ($image->isValid()) {
+      // If the image style converted the extension, it has been added to the
+      // original file, resulting in filenames like image.png.jpeg. So to find
+      // the actual source image, we remove the extension and check if that
+      // image exists.
+      if (!file_exists($original_uri)) {
+        $converted_original_uri = ImageStyleDownloadController::getUriWithoutConvertedExtension($original_uri);
+        if ($converted_original_uri !== $original_uri && file_exists($converted_original_uri)) {
+          // The converted file does exist, use it as the source.
+          $original_uri = $converted_original_uri;
+        }
+      }
+
       // Check the permissions of the original to grant access to this image.
       $headers = \Drupal::moduleHandler()->invokeAll('file_download', [$original_uri]);
       // Confirm there's at least one module granting access and none denying access.
diff --git a/core/modules/image/src/Controller/ImageStyleDownloadController.php b/core/modules/image/src/Controller/ImageStyleDownloadController.php
index 543700115ee0..bfc5c985d8dc 100644
--- a/core/modules/image/src/Controller/ImageStyleDownloadController.php
+++ b/core/modules/image/src/Controller/ImageStyleDownloadController.php
@@ -173,6 +173,24 @@ public function deliver(Request $request, $scheme, ImageStyleInterface $image_st
 
     $headers = [];
 
+    // Don't try to generate file if source is missing.
+    if ($image_uri !== $sample_image_uri && !$this->sourceImageExists($image_uri, $token_is_valid)) {
+      // If the image style converted the extension, it has been added to the
+      // original file, resulting in filenames like image.png.jpeg. So to find
+      // the actual source image, we remove the extension and check if that
+      // image exists.
+      $converted_image_uri = static::getUriWithoutConvertedExtension($image_uri);
+      if ($converted_image_uri !== $image_uri &&
+          $this->sourceImageExists($converted_image_uri, $token_is_valid)) {
+        // The converted file does exist, use it as the source.
+        $image_uri = $converted_image_uri;
+      }
+      else {
+        $this->logger->notice('Source image at %source_image_path not found while trying to generate derivative image at %derivative_path.', ['%source_image_path' => $image_uri, '%derivative_path' => $derivative_uri]);
+        return new Response($this->t('Error generating image, missing source file.'), 404);
+      }
+    }
+
     // If not using a public scheme, let other modules provide headers and
     // control access to the file.
     if (!$is_public) {
@@ -183,28 +201,12 @@ public function deliver(Request $request, $scheme, ImageStyleInterface $image_st
     }
 
     // If it is default sample.png, ignore scheme.
+    // This value swap must be done after hook_file_download is called since
+    // the hooks are expecting a URI, not a file path.
     if ($image_uri === $sample_image_uri) {
       $image_uri = $target;
     }
 
-    // Don't try to generate file if source is missing.
-    if (!$this->sourceImageExists($image_uri, $token_is_valid)) {
-      // If the image style converted the extension, it has been added to the
-      // original file, resulting in filenames like image.png.jpeg. So to find
-      // the actual source image, we remove the extension and check if that
-      // image exists.
-      $path_info = pathinfo(StreamWrapperManager::getTarget($image_uri));
-      $converted_image_uri = sprintf('%s://%s%s%s', $this->streamWrapperManager->getScheme($derivative_uri), $path_info['dirname'], DIRECTORY_SEPARATOR, $path_info['filename']);
-      if (!$this->sourceImageExists($converted_image_uri, $token_is_valid)) {
-        $this->logger->notice('Source image at %source_image_path not found while trying to generate derivative image at %derivative_path.', ['%source_image_path' => $image_uri, '%derivative_path' => $derivative_uri]);
-        return new Response($this->t('Error generating image, missing source file.'), 404);
-      }
-      else {
-        // The converted file does exist, use it as the source.
-        $image_uri = $converted_image_uri;
-      }
-    }
-
     // Don't start generating the image if the derivative already exists or if
     // generation is in progress in another thread.
     if (!file_exists($derivative_uri)) {
@@ -283,4 +285,31 @@ private function sourceImageExists(string $image_uri, bool $token_is_valid): boo
     return TRUE;
   }
 
+  /**
+   * Get the file URI without the extension from any conversion image style.
+   *
+   * If the image style converted the image, then an extension has been added
+   * to the original file, resulting in filenames like image.png.jpeg.
+   *
+   * @param string $uri
+   *   The file URI.
+   *
+   * @return string
+   *   The file URI without the extension from any conversion image style.
+   */
+  public static function getUriWithoutConvertedExtension(string $uri): string {
+    $original_uri = $uri;
+    $path_info = pathinfo(StreamWrapperManager::getTarget($uri));
+    // Only convert the URI when the filename still has an extension.
+    if (!empty($path_info['filename']) && pathinfo($path_info['filename'], PATHINFO_EXTENSION)) {
+      $original_uri = StreamWrapperManager::getScheme($uri) . '://';
+      if (!empty($path_info['dirname']) && $path_info['dirname'] !== '.') {
+        $original_uri .= $path_info['dirname'] . DIRECTORY_SEPARATOR;
+      }
+      $original_uri .= $path_info['filename'];
+    }
+
+    return $original_uri;
+  }
+
 }
diff --git a/core/modules/image/tests/src/Functional/ImageStylesPathAndUrlTest.php b/core/modules/image/tests/src/Functional/ImageStylesPathAndUrlTest.php
index 306684fbe684..f82e52e39608 100644
--- a/core/modules/image/tests/src/Functional/ImageStylesPathAndUrlTest.php
+++ b/core/modules/image/tests/src/Functional/ImageStylesPathAndUrlTest.php
@@ -122,6 +122,22 @@ public function testImageStyleUrlExtraSlash() {
     $this->doImageStyleUrlAndPathTests('public', TRUE, TRUE);
   }
 
+  /**
+   * Test an image style URL with a private file that also gets converted.
+   */
+  public function testImageStylePrivateWithConversion(): void {
+    // Add the "convert" image style effect to our style.
+    $this->style->addImageEffect([
+      'uuid' => '',
+      'id' => 'image_convert',
+      'weight' => 1,
+      'data' => [
+        'extension' => 'jpeg',
+      ],
+    ]);
+    $this->doImageStyleUrlAndPathTests('private');
+  }
+
   /**
    * Tests that an invalid source image returns a 404.
    */
-- 
GitLab