From 4dd53a5b6c1850d9344f1a1f9aa8f64fab363ede Mon Sep 17 00:00:00 2001
From: "Eirik S. Morland" <eirik@morland.no>
Date: Sun, 14 Jan 2024 20:48:26 +0100
Subject: [PATCH 01/22] Move into a service and add back the test module

---
 core/modules/image/image.services.yml         |   6 +
 core/modules/image/src/ImageFieldManager.php  | 126 ++++++++++++++++++
 ...play_test_default_private_storage.info.yml |   5 +
 ...isplay_test_default_private_storage.module |  22 +++
 .../src/Functional/ImageFieldDisplayTest.php  |  15 ++-
 5 files changed, 173 insertions(+), 1 deletion(-)
 create mode 100644 core/modules/image/src/ImageFieldManager.php
 create mode 100644 core/modules/image/tests/modules/image_field_display_test_default_private_storage/image_field_display_test_default_private_storage.info.yml
 create mode 100644 core/modules/image/tests/modules/image_field_display_test_default_private_storage/image_field_display_test_default_private_storage.module

diff --git a/core/modules/image/image.services.yml b/core/modules/image/image.services.yml
index eda90083af1b..926c540be058 100644
--- a/core/modules/image/image.services.yml
+++ b/core/modules/image/image.services.yml
@@ -15,3 +15,9 @@ services:
     public: false
     tags:
       - { name: page_cache_response_policy }
+  image.field_manager:
+    class: Drupal\image\ImageFieldManager
+    arguments:
+      - '@cache.default'
+      - '@entity_type.manager'
+      - '@entity.repository'
diff --git a/core/modules/image/src/ImageFieldManager.php b/core/modules/image/src/ImageFieldManager.php
new file mode 100644
index 000000000000..d6323838ecfd
--- /dev/null
+++ b/core/modules/image/src/ImageFieldManager.php
@@ -0,0 +1,126 @@
+<?php
+
+namespace Drupal\image;
+
+use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Entity\EntityRepositoryInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\file\FileInterface;
+
+/**
+ * Provides a service for managing image fields.
+ */
+class ImageFieldManager {
+
+  /**
+   * The cache backend.
+   *
+   * @var \Drupal\Core\Cache\CacheBackendInterface
+   */
+  protected CacheBackendInterface $cache;
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected EntityTypeManagerInterface $entityTypeManager;
+
+  /**
+   * The entity repository.
+   *
+   * @var \Drupal\Core\Entity\EntityRepositoryInterface
+   */
+  protected EntityRepositoryInterface $entityRepository;
+
+  /**
+   * The initialized array.
+   */
+  private ?array $cachedDefaults;
+
+  /**
+   * Construct a new image field manager.
+   *
+   * @param \Drupal\Core\Cache\CacheBackendInterface $cache
+   *   The cache backend.
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
+   *   The entity type manager.
+   * @param \Drupal\Core\Entity\EntityRepositoryInterface $entityRepository
+   *   The entity repository.
+   */
+  public function __construct(CacheBackendInterface $cache, EntityTypeManagerInterface $entityTypeManager, EntityRepositoryInterface $entityRepository) {
+    $this->cache = $cache;
+    $this->entityTypeManager = $entityTypeManager;
+    $this->entityRepository = $entityRepository;
+    $this->cachedDefaults = NULL;
+  }
+
+  /**
+   * Map default values for image fields, and those fields' configuration IDs.
+   *
+   * This is used in image_file_download() to determine whether to grant access to
+   * an image stored in the private file storage.
+   *
+   * @return array
+   *   An associative array, where the keys are image file URIs, and the values
+   *   are arrays of field configuration IDs which use that image file as their
+   *   default image. For example,
+   *
+   * @code [
+   *     'private://default_images/astronaut.jpg' => [
+   *       'node.article.field_image',
+   *       'user.user.field_portrait',
+   *     ],
+   *   ]
+   * @code
+   */
+  public function getDefaultImageFields() : array {
+    $cid = 'image:default_images';
+    if (!isset($this->cachedDefaults)) {
+      $cache = $this->cache->get($cid);
+      if ($cache) {
+        $this->cachedDefaults = $cache->data;
+      }
+      else {
+        // Save a map of all default image UUIDs and their corresponding field
+        // configuration IDs for quick lookup.
+        $defaults = [];
+        $fields = $this->entityTypeManager
+          ->getStorage('field_config')
+          ->loadMultiple();
+
+        foreach ($fields as $field) {
+          if ($field->getType() === 'image') {
+            // Check if there is a default image in the field config.
+            $field_uuid = $field->getSetting('default_image')['uuid'];
+            if ($field_uuid) {
+              $file = $this->entityRepository->loadEntityByUuid('file', $field_uuid);
+              if ($file instanceof FileInterface) {
+                // A default image could be used by multiple field configs.
+                $defaults[$file->getFileUri()][] = $field->get('id');
+              }
+            }
+
+            // Field storage config can also have a default image.
+            $storage_uuid = $field->getFieldStorageDefinition()->getSetting('default_image')['uuid'];
+            if ($storage_uuid) {
+              $file = $this->entityRepository->loadEntityByUuid('file', $storage_uuid);
+              if ($file instanceof FileInterface) {
+                // Use the field config id since that is what we'll be using to
+                // check access in image_file_download().
+                $defaults[$file->getFileUri()][] = $field->get('id');
+              }
+            }
+          }
+        }
+
+        // Cache the default image list.
+        $this->cache
+          ->set($cid, $defaults, CacheBackendInterface::CACHE_PERMANENT, ['image_default_images']);
+        $this->cachedDefaults = $defaults;
+      }
+    }
+    return $this->cachedDefaults;
+  }
+
+}
diff --git a/core/modules/image/tests/modules/image_field_display_test_default_private_storage/image_field_display_test_default_private_storage.info.yml b/core/modules/image/tests/modules/image_field_display_test_default_private_storage/image_field_display_test_default_private_storage.info.yml
new file mode 100644
index 000000000000..a7952d0183fc
--- /dev/null
+++ b/core/modules/image/tests/modules/image_field_display_test_default_private_storage/image_field_display_test_default_private_storage.info.yml
@@ -0,0 +1,5 @@
+name: 'Image field display test for default images in private file storage'
+type: module
+description: 'Provides an entity field access hook to deny view access to a field for the current user.'
+package: Testing
+version: VERSION
diff --git a/core/modules/image/tests/modules/image_field_display_test_default_private_storage/image_field_display_test_default_private_storage.module b/core/modules/image/tests/modules/image_field_display_test_default_private_storage/image_field_display_test_default_private_storage.module
new file mode 100644
index 000000000000..0f1f0c64db03
--- /dev/null
+++ b/core/modules/image/tests/modules/image_field_display_test_default_private_storage/image_field_display_test_default_private_storage.module
@@ -0,0 +1,22 @@
+<?php
+
+/**
+ * @file
+ * Image field display test for default images in private file storage.
+ */
+
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\Core\Access\AccessResult;
+
+/**
+ * Implements hook_entity_field_access().
+ */
+function image_field_display_test_default_private_storage_entity_field_access($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, FieldItemListInterface $items = NULL) {
+  if ($field_definition->getName() == 'field_default_private' && $operation == 'view') {
+    return AccessResult::forbidden();
+  }
+
+  return AccessResult::neutral();
+}
diff --git a/core/modules/image/tests/src/Functional/ImageFieldDisplayTest.php b/core/modules/image/tests/src/Functional/ImageFieldDisplayTest.php
index efeacd6ff165..fda4b185c347 100644
--- a/core/modules/image/tests/src/Functional/ImageFieldDisplayTest.php
+++ b/core/modules/image/tests/src/Functional/ImageFieldDisplayTest.php
@@ -566,7 +566,7 @@ public function testImageFieldDefaultImage(): void {
     $this->assertEmpty($default_image['uuid'], 'Default image removed from field.');
     // Create an image field that uses the private:// scheme and test that the
     // default image works as expected.
-    $private_field_name = $this->randomMachineName();
+    $private_field_name = 'field_default_private';
     $this->createImageField($private_field_name, 'node', 'article', ['uri_scheme' => 'private']);
     // Add a default image to the new field.
     $edit = [
@@ -606,6 +606,19 @@ public function testImageFieldDefaultImage(): void {
     // Default private image should be displayed when no user supplied image
     // is present.
     $this->assertSession()->responseContains($default_output);
+
+    // Check that the default image itself can be downloaded; i.e.: not just the
+    // HTML markup.
+    $urlForPrivateDefaultImageInNodeField = \Drupal::service('file_url_generator')->generateAbsoluteString($file->getFileUri());
+    // Check that a user can download the default image attached to a node field
+    // configured to store data in the private file storage.
+    $this->drupalGet($urlForPrivateDefaultImageInNodeField);
+    $this->assertSession()->statusCodeEquals(200);
+    // Now, install a module that denies access to the field; and check that the
+    // same user now receives a 403 Access Denied.
+    \Drupal::service('module_installer')->install(['image_field_display_test_default_private_storage']);
+    $this->drupalGet($urlForPrivateDefaultImageInNodeField);
+    $this->assertSession()->statusCodeEquals(403);
   }
 
 }
-- 
GitLab


From a141635deb9cdd476545085d8dbbb9d164715769 Mon Sep 17 00:00:00 2001
From: "joseph.olstad" <joseph.olstad@1321830.no-reply.drupal.org>
Date: Thu, 27 Jun 2024 18:10:47 -0400
Subject: [PATCH 02/22] Issue #2107455 by mparker17, ravi.shankar,
 yogeshmpawar, pooja saraah, joseph.olstad, claudiu.cristea, KapilV,
 ShaunDychko, andileco, drclaw, xjm, alexpott: Allow the $items parameter to
 be nullable thanks to code sniffer.  Image field default value not shown when
 upload destination set to private file storage

---
 .../image_field_display_test_default_private_storage.module     | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/core/modules/image/tests/modules/image_field_display_test_default_private_storage/image_field_display_test_default_private_storage.module b/core/modules/image/tests/modules/image_field_display_test_default_private_storage/image_field_display_test_default_private_storage.module
index 0f1f0c64db03..ccb51b9c863a 100644
--- a/core/modules/image/tests/modules/image_field_display_test_default_private_storage/image_field_display_test_default_private_storage.module
+++ b/core/modules/image/tests/modules/image_field_display_test_default_private_storage/image_field_display_test_default_private_storage.module
@@ -13,7 +13,7 @@
 /**
  * Implements hook_entity_field_access().
  */
-function image_field_display_test_default_private_storage_entity_field_access($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, FieldItemListInterface $items = NULL) {
+function image_field_display_test_default_private_storage_entity_field_access($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, ?FieldItemListInterface $items = NULL) {
   if ($field_definition->getName() == 'field_default_private' && $operation == 'view') {
     return AccessResult::forbidden();
   }
-- 
GitLab


From 7f7e1bf760d86e1524a03120afe830847cafb6ac Mon Sep 17 00:00:00 2001
From: "joseph.olstad" <joseph.olstad@1321830.no-reply.drupal.org>
Date: Thu, 27 Jun 2024 18:41:31 -0400
Subject: [PATCH 03/22] Issue #2107455 by mparker17, ravi.shankar,
 yogeshmpawar, pooja saraah, joseph.olstad, claudiu.cristea, KapilV,
 ShaunDychko, andileco, drclaw, xjm, alexpott: testCoreServiceAliases says
 move @image.field_manager to core/core.services.yml.  Image field default
 value not shown when upload destination set to private file storage

---
 core/core.services.yml                | 4 ++++
 core/modules/image/image.services.yml | 6 ------
 2 files changed, 4 insertions(+), 6 deletions(-)

diff --git a/core/core.services.yml b/core/core.services.yml
index 54fbf1579777..91c6fcffa1f4 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -1498,6 +1498,10 @@ services:
     class: Drupal\Core\StreamWrapper\TemporaryStream
     tags:
       - { name: stream_wrapper, scheme: temporary }
+  image.field_manager:
+    class: Drupal\image\ImageFieldManager
+    arguments: ['@cache.default', '@entity_type.manager', '@entity.repository']
+  Drupal\image\ImageFieldManager: '@image.field_manager'
   image.toolkit.manager:
     class: Drupal\Core\ImageToolkit\ImageToolkitManager
     arguments: ['@config.factory']
diff --git a/core/modules/image/image.services.yml b/core/modules/image/image.services.yml
index 926c540be058..eda90083af1b 100644
--- a/core/modules/image/image.services.yml
+++ b/core/modules/image/image.services.yml
@@ -15,9 +15,3 @@ services:
     public: false
     tags:
       - { name: page_cache_response_policy }
-  image.field_manager:
-    class: Drupal\image\ImageFieldManager
-    arguments:
-      - '@cache.default'
-      - '@entity_type.manager'
-      - '@entity.repository'
-- 
GitLab


From 8d25ae0313ccfb8ffde538e2b8dd3453669cc3c3 Mon Sep 17 00:00:00 2001
From: "joseph.olstad" <joseph.olstad@1321830.no-reply.drupal.org>
Date: Fri, 28 Jun 2024 16:29:42 -0400
Subject: [PATCH 04/22] Issue #2107455 by kksandr - In most cases formatted in
 the style (): array.

---
 core/modules/image/src/ImageFieldManager.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/core/modules/image/src/ImageFieldManager.php b/core/modules/image/src/ImageFieldManager.php
index d6323838ecfd..41826c77633d 100644
--- a/core/modules/image/src/ImageFieldManager.php
+++ b/core/modules/image/src/ImageFieldManager.php
@@ -74,7 +74,7 @@ public function __construct(CacheBackendInterface $cache, EntityTypeManagerInter
    *   ]
    * @code
    */
-  public function getDefaultImageFields() : array {
+  public function getDefaultImageFields(): array {
     $cid = 'image:default_images';
     if (!isset($this->cachedDefaults)) {
       $cache = $this->cache->get($cid);
-- 
GitLab


From c056651fd960ab28612a4832b1e4cc3ee932e0ae Mon Sep 17 00:00:00 2001
From: "joseph.olstad" <joseph.olstad@1321830.no-reply.drupal.org>
Date: Fri, 28 Jun 2024 16:33:15 -0400
Subject: [PATCH 05/22] Issue #2107455 by kksandr - the entity_field_info tag
 should be added so that it is reset when the field configuration is updated.

---
 core/modules/image/src/ImageFieldManager.php | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/core/modules/image/src/ImageFieldManager.php b/core/modules/image/src/ImageFieldManager.php
index 41826c77633d..5cee2ce0f323 100644
--- a/core/modules/image/src/ImageFieldManager.php
+++ b/core/modules/image/src/ImageFieldManager.php
@@ -116,7 +116,10 @@ public function getDefaultImageFields(): array {
 
         // Cache the default image list.
         $this->cache
-          ->set($cid, $defaults, CacheBackendInterface::CACHE_PERMANENT, ['image_default_images']);
+          ->set($cid, $defaults, CacheBackendInterface::CACHE_PERMANENT, [
+            'image_default_images',
+            'entity_field_info'
+          ]);
         $this->cachedDefaults = $defaults;
       }
     }
-- 
GitLab


From cc6557c62b0cf0e169a9da3ec00333810dee95c2 Mon Sep 17 00:00:00 2001
From: "joseph.olstad" <joseph.olstad@1321830.no-reply.drupal.org>
Date: Fri, 28 Jun 2024 16:37:26 -0400
Subject: [PATCH 06/22] Issue #2107455 by kksandr - Base fields can also have
 default images, for example they can be easily defined using the
 core.base_field_override.*.* configuration, so it is better to use
 entity_field.manager here.

---
 core/modules/image/src/ImageFieldManager.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/core/modules/image/src/ImageFieldManager.php b/core/modules/image/src/ImageFieldManager.php
index 5cee2ce0f323..2b74b0a32082 100644
--- a/core/modules/image/src/ImageFieldManager.php
+++ b/core/modules/image/src/ImageFieldManager.php
@@ -86,7 +86,7 @@ public function getDefaultImageFields(): array {
         // configuration IDs for quick lookup.
         $defaults = [];
         $fields = $this->entityTypeManager
-          ->getStorage('field_config')
+          ->getStorage('entity_field.manager')
           ->loadMultiple();
 
         foreach ($fields as $field) {
-- 
GitLab


From 970cc09f2fd7306add4882d150455ad4caecdb0e Mon Sep 17 00:00:00 2001
From: "joseph.olstad" <joseph.olstad@1321830.no-reply.drupal.org>
Date: Fri, 28 Jun 2024 16:47:55 -0400
Subject: [PATCH 07/22] Issue #2107455 by kksandr - the entity_field_info tag
 should be added so that it is reset when the field configuration is updated.

---
 core/modules/image/src/ImageFieldManager.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/core/modules/image/src/ImageFieldManager.php b/core/modules/image/src/ImageFieldManager.php
index 2b74b0a32082..afebe21c7fa6 100644
--- a/core/modules/image/src/ImageFieldManager.php
+++ b/core/modules/image/src/ImageFieldManager.php
@@ -118,7 +118,7 @@ public function getDefaultImageFields(): array {
         $this->cache
           ->set($cid, $defaults, CacheBackendInterface::CACHE_PERMANENT, [
             'image_default_images',
-            'entity_field_info'
+            'entity_field_info',
           ]);
         $this->cachedDefaults = $defaults;
       }
-- 
GitLab


From 2b888c7b7d65d6b6ddf23bbb2fa647b8980e0421 Mon Sep 17 00:00:00 2001
From: "joseph.olstad" <joseph.olstad@1321830.no-reply.drupal.org>
Date: Fri, 28 Jun 2024 17:08:41 -0400
Subject: [PATCH 08/22] Revert "Issue #2107455 by kksandr - Base fields can
 also have default images, for example they can be easily defined using the
 core.base_field_override.*.* configuration, so it is better to use
 entity_field.manager here."

This reverts commit 5158f6f62a72d5c95fd900c9ef646d90f563a2a4.
---
 core/modules/image/src/ImageFieldManager.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/core/modules/image/src/ImageFieldManager.php b/core/modules/image/src/ImageFieldManager.php
index afebe21c7fa6..e66ed9f2e7f4 100644
--- a/core/modules/image/src/ImageFieldManager.php
+++ b/core/modules/image/src/ImageFieldManager.php
@@ -86,7 +86,7 @@ public function getDefaultImageFields(): array {
         // configuration IDs for quick lookup.
         $defaults = [];
         $fields = $this->entityTypeManager
-          ->getStorage('entity_field.manager')
+          ->getStorage('field_config')
           ->loadMultiple();
 
         foreach ($fields as $field) {
-- 
GitLab


From 47d88d868c663db0ad76d6eda71e5d80f0f76a3f Mon Sep 17 00:00:00 2001
From: kksandr <ksandr754@gmail.com>
Date: Sat, 29 Jun 2024 13:25:47 +0300
Subject: [PATCH 09/22] Issue #2107455 by kksandr: moving access checks to the
 service and general refactoring

---
 core/core.services.yml                        |   4 +-
 core/modules/image/src/ImageFieldManager.php  | 169 +++++++++---------
 .../image/src/ImageFieldManagerInterface.php  |  48 +++++
 3 files changed, 139 insertions(+), 82 deletions(-)
 create mode 100644 core/modules/image/src/ImageFieldManagerInterface.php

diff --git a/core/core.services.yml b/core/core.services.yml
index 91c6fcffa1f4..704e4ab0ded2 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -1500,8 +1500,8 @@ services:
       - { name: stream_wrapper, scheme: temporary }
   image.field_manager:
     class: Drupal\image\ImageFieldManager
-    arguments: ['@cache.default', '@entity_type.manager', '@entity.repository']
-  Drupal\image\ImageFieldManager: '@image.field_manager'
+    autowire: true
+  Drupal\image\ImageFieldManagerInterface: '@image.field_manager'
   image.toolkit.manager:
     class: Drupal\Core\ImageToolkit\ImageToolkitManager
     arguments: ['@config.factory']
diff --git a/core/modules/image/src/ImageFieldManager.php b/core/modules/image/src/ImageFieldManager.php
index e66ed9f2e7f4..349eb629ef52 100644
--- a/core/modules/image/src/ImageFieldManager.php
+++ b/core/modules/image/src/ImageFieldManager.php
@@ -2,44 +2,34 @@
 
 namespace Drupal\image;
 
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Access\AccessResultInterface;
 use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Entity\EntityFieldManagerInterface;
 use Drupal\Core\Entity\EntityRepositoryInterface;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
-use Drupal\file\FileInterface;
+use Drupal\Core\Image\ImageInterface;
+use Drupal\Core\Session\AccountInterface;
+use Symfony\Component\DependencyInjection\Attribute\Autowire;
 
 /**
  * Provides a service for managing image fields.
+ *
+ * @todo perhaps rename to ImageDefaultAccess and make getDefaultImageFields() protected?
+ *   That is, this will be a service only for checking access to images that are used by default,
+ *   because managing fields is definitely not what this service does.
  */
-class ImageFieldManager {
+class ImageFieldManager implements ImageFieldManagerInterface {
 
   /**
-   * The cache backend.
+   * Initialized field cache for default images.
    *
-   * @var \Drupal\Core\Cache\CacheBackendInterface
+   * @var array<string, \Drupal\Core\Field\FieldDefinitionInterface[]>|null
    */
-  protected CacheBackendInterface $cache;
+  private array $cachedDefaults;
 
   /**
-   * The entity type manager.
-   *
-   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
-   */
-  protected EntityTypeManagerInterface $entityTypeManager;
-
-  /**
-   * The entity repository.
-   *
-   * @var \Drupal\Core\Entity\EntityRepositoryInterface
-   */
-  protected EntityRepositoryInterface $entityRepository;
-
-  /**
-   * The initialized array.
-   */
-  private ?array $cachedDefaults;
-
-  /**
-   * Construct a new image field manager.
+   * Constructs a new ImageFieldManager.
    *
    * @param \Drupal\Core\Cache\CacheBackendInterface $cache
    *   The cache backend.
@@ -47,83 +37,102 @@ class ImageFieldManager {
    *   The entity type manager.
    * @param \Drupal\Core\Entity\EntityRepositoryInterface $entityRepository
    *   The entity repository.
+   * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entityFieldManager
+   *   The entity field manager.
+   * @param \Drupal\Core\Session\AccountInterface $currentUser
+   *   The current user.
    */
-  public function __construct(CacheBackendInterface $cache, EntityTypeManagerInterface $entityTypeManager, EntityRepositoryInterface $entityRepository) {
-    $this->cache = $cache;
-    $this->entityTypeManager = $entityTypeManager;
-    $this->entityRepository = $entityRepository;
-    $this->cachedDefaults = NULL;
-  }
+  public function __construct(
+    #[Autowire(service: 'cache.default')]
+    protected readonly CacheBackendInterface $cache,
+    protected readonly EntityTypeManagerInterface $entityTypeManager,
+    protected readonly EntityRepositoryInterface $entityRepository,
+    protected readonly EntityFieldManagerInterface $entityFieldManager,
+    protected readonly AccountInterface $currentUser,
+  ) {}
 
   /**
-   * Map default values for image fields, and those fields' configuration IDs.
-   *
-   * This is used in image_file_download() to determine whether to grant access to
-   * an image stored in the private file storage.
-   *
-   * @return array
-   *   An associative array, where the keys are image file URIs, and the values
-   *   are arrays of field configuration IDs which use that image file as their
-   *   default image. For example,
-   *
-   * @code [
-   *     'private://default_images/astronaut.jpg' => [
-   *       'node.article.field_image',
-   *       'user.user.field_portrait',
-   *     ],
-   *   ]
-   * @code
+   * {@inheritdoc}
    */
   public function getDefaultImageFields(): array {
-    $cid = 'image:default_images';
     if (!isset($this->cachedDefaults)) {
-      $cache = $this->cache->get($cid);
-      if ($cache) {
+      $cid = 'image:default_images';
+      if ($cache = $this->cache->get($cid)) {
         $this->cachedDefaults = $cache->data;
       }
       else {
         // Save a map of all default image UUIDs and their corresponding field
-        // configuration IDs for quick lookup.
+        // definitions for quick lookup.
         $defaults = [];
-        $fields = $this->entityTypeManager
-          ->getStorage('field_config')
-          ->loadMultiple();
-
-        foreach ($fields as $field) {
-          if ($field->getType() === 'image') {
-            // Check if there is a default image in the field config.
-            $field_uuid = $field->getSetting('default_image')['uuid'];
-            if ($field_uuid) {
-              $file = $this->entityRepository->loadEntityByUuid('file', $field_uuid);
-              if ($file instanceof FileInterface) {
-                // A default image could be used by multiple field configs.
-                $defaults[$file->getFileUri()][] = $field->get('id');
-              }
+        $field_map = $this->entityFieldManager->getFieldMapByFieldType('image');
+        foreach ($field_map as $entity_type_id => $fields) {
+          $field_storages = $this->entityFieldManager->getFieldStorageDefinitions($entity_type_id);
+          foreach ($fields as $field_name => $field_info) {
+            // First, check if the default image is set on the field storage.
+            $uri_from_storage = NULL;
+            $file_uuid = $field_storages[$field_name]->getSetting('default_image')['uuid'];
+            if ($file_uuid && $file = $this->entityRepository->loadEntityByUuid('file', $file_uuid)) {
+              /** @var \Drupal\file\FileInterface $file */
+              $uri_from_storage = $file->getFileUri();
             }
 
-            // Field storage config can also have a default image.
-            $storage_uuid = $field->getFieldStorageDefinition()->getSetting('default_image')['uuid'];
-            if ($storage_uuid) {
-              $file = $this->entityRepository->loadEntityByUuid('file', $storage_uuid);
-              if ($file instanceof FileInterface) {
-                // Use the field config id since that is what we'll be using to
-                // check access in image_file_download().
-                $defaults[$file->getFileUri()][] = $field->get('id');
+            foreach ($field_info['bundles'] as $bundle) {
+              $field_definition = $this->entityFieldManager->getFieldDefinitions($entity_type_id, $bundle)[$field_name];
+              $default_uri = $uri_from_storage;
+              $file_uuid = $field_definition->getSetting('default_image')['uuid'];
+              // If the default image is overridden in the field definition, use
+              // that instead of the one set on the field storage.
+              if ($file_uuid && $file = $this->entityRepository->loadEntityByUuid('file', $file_uuid)) {
+                /** @var \Drupal\file\FileInterface $file */
+                $default_uri = $file->getFileUri();
+              }
+              // Finally, if a default image URI was found, add it to the list.
+              if ($default_uri) {
+                $defaults[$default_uri][] = $field_definition;
               }
             }
           }
         }
-
         // Cache the default image list.
-        $this->cache
-          ->set($cid, $defaults, CacheBackendInterface::CACHE_PERMANENT, [
-            'image_default_images',
-            'entity_field_info',
-          ]);
         $this->cachedDefaults = $defaults;
+        $this->cache->set($cid, $defaults, CacheBackendInterface::CACHE_PERMANENT, [
+          'image_default_images',
+          'entity_field_info',
+        ]);
       }
     }
     return $this->cachedDefaults;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function checkAccessToDefaultImage(ImageInterface $image, ?AccountInterface $account = NULL): AccessResultInterface {
+    if (!$image->isValid()) {
+      return AccessResult::forbidden();
+    }
+    $account ??= $this->currentUser;
+    // If the image being requested for download is being used as the default
+    // image for any fields, then grant access if the user has 'view' access to
+    // at least one of those fields.
+    $uri = $image->getSource();
+    $default_images = $this->getDefaultImageFields();
+    $access = AccessResult::neutral()->addCacheTags(['image_default_images', 'entity_field_info']);
+    if (isset($default_images[$uri])) {
+      foreach ($default_images[$uri] as $field_definition) {
+        $access_control_handler = $this->entityTypeManager->getAccessControlHandler($field_definition->getTargetEntityTypeId());
+        $field_access = $access_control_handler->fieldAccess('view', $field_definition, $account, NULL, TRUE);
+        // As long as the user has view access to at least one of the fields,
+        // that uses this image as a default, we can exit this foreach loop,
+        // and grant access.
+        if ($field_access->isAllowed()) {
+          return AccessResult::allowed()
+            ->addCacheableDependency($access)
+            ->addCacheableDependency($field_access);
+        }
+      }
+    }
+    return $access;
+  }
+
 }
diff --git a/core/modules/image/src/ImageFieldManagerInterface.php b/core/modules/image/src/ImageFieldManagerInterface.php
new file mode 100644
index 000000000000..9e613c598b6c
--- /dev/null
+++ b/core/modules/image/src/ImageFieldManagerInterface.php
@@ -0,0 +1,48 @@
+<?php
+
+namespace Drupal\image;
+
+use Drupal\Core\Access\AccessResultInterface;
+use Drupal\Core\Image\ImageInterface;
+use Drupal\Core\Session\AccountInterface;
+
+/**
+ * Provides an interface for an image field manager.
+ */
+interface ImageFieldManagerInterface {
+
+  /**
+   * Map default values for image fields, and those fields' configuration IDs.
+   *
+   * This is used in image_file_download() to determine whether to grant access
+   * to an image stored in the private file storage.
+   *
+   * @return array<string, \Drupal\Core\Field\FieldDefinitionInterface[]>
+   *   An associative array, where the keys are image file URIs, and the values
+   *   are arrays of field configuration IDs which use that image file as their
+   *   default image. For example,
+   *
+   * @code [
+   *     'private://default_images/astronaut.jpg' => [
+   *       'node.article.field_image',
+   *       'user.user.field_portrait',
+   *     ],
+   *   ]
+   * @code
+   */
+  public function getDefaultImageFields(): array;
+
+  /**
+   * Check access to a default image.
+   *
+   * @param \Drupal\Core\Image\ImageInterface $image
+   *   The image to check access to.
+   * @param \Drupal\Core\Session\AccountInterface|null $account
+   *   (optional) The account for which to check access.
+   *
+   * @return \Drupal\Core\Access\AccessResultInterface
+   *   The access result.
+   */
+  public function checkAccessToDefaultImage(ImageInterface $image, ?AccountInterface $account = NULL): AccessResultInterface;
+
+}
-- 
GitLab


From 3aeaa651a82915c1f9e5068373a07f94204d43dd Mon Sep 17 00:00:00 2001
From: kksandr <ksandr754@gmail.com>
Date: Sat, 29 Jun 2024 16:22:04 +0300
Subject: [PATCH 10/22] Issue #2107455 by kksandr: update docs

---
 core/modules/image/src/ImageFieldManager.php  |  4 ++--
 .../image/src/ImageFieldManagerInterface.php  | 21 +++++++------------
 2 files changed, 9 insertions(+), 16 deletions(-)

diff --git a/core/modules/image/src/ImageFieldManager.php b/core/modules/image/src/ImageFieldManager.php
index 349eb629ef52..62b51988e7a4 100644
--- a/core/modules/image/src/ImageFieldManager.php
+++ b/core/modules/image/src/ImageFieldManager.php
@@ -24,7 +24,7 @@ class ImageFieldManager implements ImageFieldManagerInterface {
   /**
    * Initialized field cache for default images.
    *
-   * @var array<string, \Drupal\Core\Field\FieldDefinitionInterface[]>|null
+   * @var array<string, \Drupal\Core\Field\FieldDefinitionInterface[]>
    */
   private array $cachedDefaults;
 
@@ -111,13 +111,13 @@ public function checkAccessToDefaultImage(ImageInterface $image, ?AccountInterfa
     if (!$image->isValid()) {
       return AccessResult::forbidden();
     }
-    $account ??= $this->currentUser;
     // If the image being requested for download is being used as the default
     // image for any fields, then grant access if the user has 'view' access to
     // at least one of those fields.
     $uri = $image->getSource();
     $default_images = $this->getDefaultImageFields();
     $access = AccessResult::neutral()->addCacheTags(['image_default_images', 'entity_field_info']);
+    $account ??= $this->currentUser;
     if (isset($default_images[$uri])) {
       foreach ($default_images[$uri] as $field_definition) {
         $access_control_handler = $this->entityTypeManager->getAccessControlHandler($field_definition->getTargetEntityTypeId());
diff --git a/core/modules/image/src/ImageFieldManagerInterface.php b/core/modules/image/src/ImageFieldManagerInterface.php
index 9e613c598b6c..ffa2e6f59c52 100644
--- a/core/modules/image/src/ImageFieldManagerInterface.php
+++ b/core/modules/image/src/ImageFieldManagerInterface.php
@@ -12,33 +12,26 @@
 interface ImageFieldManagerInterface {
 
   /**
-   * Map default values for image fields, and those fields' configuration IDs.
-   *
-   * This is used in image_file_download() to determine whether to grant access
-   * to an image stored in the private file storage.
+   * Map default values for image fields, and those fields' definitions.
    *
    * @return array<string, \Drupal\Core\Field\FieldDefinitionInterface[]>
    *   An associative array, where the keys are image file URIs, and the values
-   *   are arrays of field configuration IDs which use that image file as their
-   *   default image. For example,
-   *
-   * @code [
-   *     'private://default_images/astronaut.jpg' => [
-   *       'node.article.field_image',
-   *       'user.user.field_portrait',
-   *     ],
-   *   ]
-   * @code
+   *   are arrays of field definitions which use that image file as their
+   *   default image.
    */
   public function getDefaultImageFields(): array;
 
   /**
    * Check access to a default image.
    *
+   * This is used in image_file_download() to determine whether to grant access
+   * to an image stored in the private file storage.
+   *
    * @param \Drupal\Core\Image\ImageInterface $image
    *   The image to check access to.
    * @param \Drupal\Core\Session\AccountInterface|null $account
    *   (optional) The account for which to check access.
+   *   Defaults to the current user.
    *
    * @return \Drupal\Core\Access\AccessResultInterface
    *   The access result.
-- 
GitLab


From 5ed3b449f12180b2ba08edef846a9ed40f824182 Mon Sep 17 00:00:00 2001
From: kksandr <ksandr754@gmail.com>
Date: Sat, 29 Jun 2024 16:36:04 +0300
Subject: [PATCH 11/22] Issue #2107455 by kksandr: defined constant for default
 image directory

---
 core/modules/image/src/ImageFieldManagerInterface.php       | 5 +++++
 core/modules/image/src/Plugin/Field/FieldType/ImageItem.php | 3 ++-
 2 files changed, 7 insertions(+), 1 deletion(-)

diff --git a/core/modules/image/src/ImageFieldManagerInterface.php b/core/modules/image/src/ImageFieldManagerInterface.php
index ffa2e6f59c52..b5848eb941c7 100644
--- a/core/modules/image/src/ImageFieldManagerInterface.php
+++ b/core/modules/image/src/ImageFieldManagerInterface.php
@@ -11,6 +11,11 @@
  */
 interface ImageFieldManagerInterface {
 
+  /**
+   * The default image directory.
+   */
+  public const string DEFAULT_IMAGE_DIRECTORY = 'default_images';
+
   /**
    * Map default values for image fields, and those fields' definitions.
    *
diff --git a/core/modules/image/src/Plugin/Field/FieldType/ImageItem.php b/core/modules/image/src/Plugin/Field/FieldType/ImageItem.php
index 2cae36dc6cf6..732f03af973e 100644
--- a/core/modules/image/src/Plugin/Field/FieldType/ImageItem.php
+++ b/core/modules/image/src/Plugin/Field/FieldType/ImageItem.php
@@ -17,6 +17,7 @@
 use Drupal\file\Entity\File;
 use Drupal\file\Plugin\Field\FieldType\FileFieldItemList;
 use Drupal\file\Plugin\Field\FieldType\FileItem;
+use Drupal\image\ImageFieldManagerInterface;
 
 /**
  * Plugin implementation of the 'image' field type.
@@ -459,7 +460,7 @@ protected function defaultImageForm(array &$element, array $settings) {
       '#title' => $this->t('Image'),
       '#description' => $this->t('Image to be shown if no image is uploaded.'),
       '#default_value' => $fids,
-      '#upload_location' => $settings['uri_scheme'] . '://default_images/',
+      '#upload_location' => $settings['uri_scheme'] . '://' . ImageFieldManagerInterface::DEFAULT_IMAGE_DIRECTORY . '/',
       '#element_validate' => [
         '\Drupal\file\Element\ManagedFile::validateManagedFile',
         [static::class, 'validateDefaultImageForm'],
-- 
GitLab


From 14cdf0799082419e05ff708ded7e8ba682e6eea4 Mon Sep 17 00:00:00 2001
From: kksandr <ksandr754@gmail.com>
Date: Sun, 30 Jun 2024 17:54:16 +0300
Subject: [PATCH 12/22] Issue #2107455 by kksandr: added used files to cache
 tags

---
 core/modules/image/src/ImageFieldManager.php | 14 +++++++++-----
 1 file changed, 9 insertions(+), 5 deletions(-)

diff --git a/core/modules/image/src/ImageFieldManager.php b/core/modules/image/src/ImageFieldManager.php
index 62b51988e7a4..9afb7b324228 100644
--- a/core/modules/image/src/ImageFieldManager.php
+++ b/core/modules/image/src/ImageFieldManager.php
@@ -4,6 +4,7 @@
 
 use Drupal\Core\Access\AccessResult;
 use Drupal\Core\Access\AccessResultInterface;
+use Drupal\Core\Cache\Cache;
 use Drupal\Core\Cache\CacheBackendInterface;
 use Drupal\Core\Entity\EntityFieldManagerInterface;
 use Drupal\Core\Entity\EntityRepositoryInterface;
@@ -26,7 +27,7 @@ class ImageFieldManager implements ImageFieldManagerInterface {
    *
    * @var array<string, \Drupal\Core\Field\FieldDefinitionInterface[]>
    */
-  private array $cachedDefaults;
+  protected array $cachedDefaults;
 
   /**
    * Constructs a new ImageFieldManager.
@@ -65,6 +66,10 @@ public function getDefaultImageFields(): array {
         // definitions for quick lookup.
         $defaults = [];
         $field_map = $this->entityFieldManager->getFieldMapByFieldType('image');
+        $cache_tags = [
+          'image_default_images',
+          'entity_field_info',
+        ];
         foreach ($field_map as $entity_type_id => $fields) {
           $field_storages = $this->entityFieldManager->getFieldStorageDefinitions($entity_type_id);
           foreach ($fields as $field_name => $field_info) {
@@ -74,6 +79,7 @@ public function getDefaultImageFields(): array {
             if ($file_uuid && $file = $this->entityRepository->loadEntityByUuid('file', $file_uuid)) {
               /** @var \Drupal\file\FileInterface $file */
               $uri_from_storage = $file->getFileUri();
+              $cache_tags = Cache::mergeTags($cache_tags, $file->getCacheTags());
             }
 
             foreach ($field_info['bundles'] as $bundle) {
@@ -85,6 +91,7 @@ public function getDefaultImageFields(): array {
               if ($file_uuid && $file = $this->entityRepository->loadEntityByUuid('file', $file_uuid)) {
                 /** @var \Drupal\file\FileInterface $file */
                 $default_uri = $file->getFileUri();
+                $cache_tags = Cache::mergeTags($cache_tags, $file->getCacheTags());
               }
               // Finally, if a default image URI was found, add it to the list.
               if ($default_uri) {
@@ -95,10 +102,7 @@ public function getDefaultImageFields(): array {
         }
         // Cache the default image list.
         $this->cachedDefaults = $defaults;
-        $this->cache->set($cid, $defaults, CacheBackendInterface::CACHE_PERMANENT, [
-          'image_default_images',
-          'entity_field_info',
-        ]);
+        $this->cache->set($cid, $defaults, CacheBackendInterface::CACHE_PERMANENT, $cache_tags);
       }
     }
     return $this->cachedDefaults;
-- 
GitLab


From 495c771b4d70733b4c125e755f578d0e499e5577 Mon Sep 17 00:00:00 2001
From: kksandr <ksandr754@gmail.com>
Date: Sun, 30 Jun 2024 18:08:21 +0300
Subject: [PATCH 13/22] Issue #2107455 by kksandr: move new service into image
 module

---
 core/core.services.yml                | 4 ----
 core/modules/image/image.services.yml | 4 ++++
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/core/core.services.yml b/core/core.services.yml
index 704e4ab0ded2..54fbf1579777 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -1498,10 +1498,6 @@ services:
     class: Drupal\Core\StreamWrapper\TemporaryStream
     tags:
       - { name: stream_wrapper, scheme: temporary }
-  image.field_manager:
-    class: Drupal\image\ImageFieldManager
-    autowire: true
-  Drupal\image\ImageFieldManagerInterface: '@image.field_manager'
   image.toolkit.manager:
     class: Drupal\Core\ImageToolkit\ImageToolkitManager
     arguments: ['@config.factory']
diff --git a/core/modules/image/image.services.yml b/core/modules/image/image.services.yml
index eda90083af1b..485d91b9174e 100644
--- a/core/modules/image/image.services.yml
+++ b/core/modules/image/image.services.yml
@@ -15,3 +15,7 @@ services:
     public: false
     tags:
       - { name: page_cache_response_policy }
+  image.field_manager:
+    class: Drupal\image\ImageFieldManager
+    autowire: true
+  Drupal\image\ImageFieldManagerInterface: '@image.field_manager'
-- 
GitLab


From bf319feebad1fe2ed02b7d01a21fe772e9d91fdc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Matthias=20L=C3=BCnemann?= <matthias.luenemann@equinoxe.com>
Date: Mon, 27 Jan 2025 11:26:24 +0100
Subject: [PATCH 14/22] fix coding standards

---
 .../image_field_display_test_default_private_storage.module  | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/core/modules/image/tests/modules/image_field_display_test_default_private_storage/image_field_display_test_default_private_storage.module b/core/modules/image/tests/modules/image_field_display_test_default_private_storage/image_field_display_test_default_private_storage.module
index ccb51b9c863a..1f465fbaf2f1 100644
--- a/core/modules/image/tests/modules/image_field_display_test_default_private_storage/image_field_display_test_default_private_storage.module
+++ b/core/modules/image/tests/modules/image_field_display_test_default_private_storage/image_field_display_test_default_private_storage.module
@@ -5,6 +5,9 @@
  * Image field display test for default images in private file storage.
  */
 
+declare(strict_types=1);
+
+use Drupal\Core\Access\AccessResultInterface;
 use Drupal\Core\Field\FieldDefinitionInterface;
 use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\Field\FieldItemListInterface;
@@ -13,7 +16,7 @@
 /**
  * Implements hook_entity_field_access().
  */
-function image_field_display_test_default_private_storage_entity_field_access($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, ?FieldItemListInterface $items = NULL) {
+function image_field_display_test_default_private_storage_entity_field_access($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, ?FieldItemListInterface $items = NULL): AccessResultInterface {
   if ($field_definition->getName() == 'field_default_private' && $operation == 'view') {
     return AccessResult::forbidden();
   }
-- 
GitLab


From 3b4563f4dd94c00f6a56b671e13b08afca10085c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Matthias=20L=C3=BCnemann?=
 <24740-luenemam@users.noreply.drupalcode.org>
Date: Mon, 27 Jan 2025 11:08:57 +0000
Subject: [PATCH 15/22] Apply 1 suggestion(s) to 1 file(s)

Co-authored-by: Claudiu Cristea <7931-claudiucristea@users.noreply.drupalcode.org>
---
 core/modules/image/src/ImageFieldManagerInterface.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/core/modules/image/src/ImageFieldManagerInterface.php b/core/modules/image/src/ImageFieldManagerInterface.php
index b5848eb941c7..3877966f19f2 100644
--- a/core/modules/image/src/ImageFieldManagerInterface.php
+++ b/core/modules/image/src/ImageFieldManagerInterface.php
@@ -17,7 +17,7 @@ interface ImageFieldManagerInterface {
   public const string DEFAULT_IMAGE_DIRECTORY = 'default_images';
 
   /**
-   * Map default values for image fields, and those fields' definitions.
+   * Maps default values for image fields, and those fields' definitions.
    *
    * @return array<string, \Drupal\Core\Field\FieldDefinitionInterface[]>
    *   An associative array, where the keys are image file URIs, and the values
-- 
GitLab


From 02612234048edac967b1074bde85f60633312897 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Matthias=20L=C3=BCnemann?=
 <24740-luenemam@users.noreply.drupalcode.org>
Date: Mon, 27 Jan 2025 11:09:21 +0000
Subject: [PATCH 16/22] Apply 1 suggestion(s) to 1 file(s)

Co-authored-by: Claudiu Cristea <7931-claudiucristea@users.noreply.drupalcode.org>
---
 core/modules/image/src/ImageFieldManagerInterface.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/core/modules/image/src/ImageFieldManagerInterface.php b/core/modules/image/src/ImageFieldManagerInterface.php
index 3877966f19f2..408e24ae90a7 100644
--- a/core/modules/image/src/ImageFieldManagerInterface.php
+++ b/core/modules/image/src/ImageFieldManagerInterface.php
@@ -27,7 +27,7 @@ interface ImageFieldManagerInterface {
   public function getDefaultImageFields(): array;
 
   /**
-   * Check access to a default image.
+   * Checks the access to a default image.
    *
    * This is used in image_file_download() to determine whether to grant access
    * to an image stored in the private file storage.
-- 
GitLab


From 83faa182d2b13826fa3cddebaa7d8b43f0e53e60 Mon Sep 17 00:00:00 2001
From: kksandr <ksandr754@gmail.com>
Date: Mon, 27 Jan 2025 18:22:23 +0200
Subject: [PATCH 17/22] Issue #2107455 by kksandr: fix standards

---
 .../image/tests/src/Functional/ImageFieldDisplayTest.php    | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/core/modules/image/tests/src/Functional/ImageFieldDisplayTest.php b/core/modules/image/tests/src/Functional/ImageFieldDisplayTest.php
index fda4b185c347..edda3602feca 100644
--- a/core/modules/image/tests/src/Functional/ImageFieldDisplayTest.php
+++ b/core/modules/image/tests/src/Functional/ImageFieldDisplayTest.php
@@ -609,15 +609,15 @@ public function testImageFieldDefaultImage(): void {
 
     // Check that the default image itself can be downloaded; i.e.: not just the
     // HTML markup.
-    $urlForPrivateDefaultImageInNodeField = \Drupal::service('file_url_generator')->generateAbsoluteString($file->getFileUri());
+    $private_default_image_url = \Drupal::service('file_url_generator')->generateAbsoluteString($file->getFileUri());
     // Check that a user can download the default image attached to a node field
     // configured to store data in the private file storage.
-    $this->drupalGet($urlForPrivateDefaultImageInNodeField);
+    $this->drupalGet($private_default_image_url);
     $this->assertSession()->statusCodeEquals(200);
     // Now, install a module that denies access to the field; and check that the
     // same user now receives a 403 Access Denied.
     \Drupal::service('module_installer')->install(['image_field_display_test_default_private_storage']);
-    $this->drupalGet($urlForPrivateDefaultImageInNodeField);
+    $this->drupalGet($private_default_image_url);
     $this->assertSession()->statusCodeEquals(403);
   }
 
-- 
GitLab


From 1732f17640deaede5f65826422ac0dea9cb7c2f5 Mon Sep 17 00:00:00 2001
From: kksandr <ksandr754@gmail.com>
Date: Mon, 27 Jan 2025 20:21:17 +0200
Subject: [PATCH 18/22] Issue #2107455 by kksandr: replace new service with
 hook class

---
 core/modules/image/image.services.yml         |   4 -
 .../image/src/Hook/ImageDownloadFileHook.php  | 191 ++++++++++++++++++
 core/modules/image/src/Hook/ImageHooks.php    |  57 ------
 core/modules/image/src/ImageFieldManager.php  | 142 -------------
 .../image/src/ImageFieldManagerInterface.php  |  46 -----
 .../src/Plugin/Field/FieldType/ImageItem.php  |   8 +-
 6 files changed, 197 insertions(+), 251 deletions(-)
 create mode 100644 core/modules/image/src/Hook/ImageDownloadFileHook.php
 delete mode 100644 core/modules/image/src/ImageFieldManager.php
 delete mode 100644 core/modules/image/src/ImageFieldManagerInterface.php

diff --git a/core/modules/image/image.services.yml b/core/modules/image/image.services.yml
index 485d91b9174e..eda90083af1b 100644
--- a/core/modules/image/image.services.yml
+++ b/core/modules/image/image.services.yml
@@ -15,7 +15,3 @@ services:
     public: false
     tags:
       - { name: page_cache_response_policy }
-  image.field_manager:
-    class: Drupal\image\ImageFieldManager
-    autowire: true
-  Drupal\image\ImageFieldManagerInterface: '@image.field_manager'
diff --git a/core/modules/image/src/Hook/ImageDownloadFileHook.php b/core/modules/image/src/Hook/ImageDownloadFileHook.php
new file mode 100644
index 000000000000..80f983bc47fe
--- /dev/null
+++ b/core/modules/image/src/Hook/ImageDownloadFileHook.php
@@ -0,0 +1,191 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\image\Hook;
+
+use Drupal\Core\Cache\Cache;
+use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Entity\EntityFieldManagerInterface;
+use Drupal\Core\Entity\EntityRepositoryInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+use Drupal\Core\Image\ImageFactory;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\StreamWrapper\StreamWrapperManager;
+use Drupal\image\Controller\ImageStyleDownloadController;
+use Drupal\image\Plugin\Field\FieldType\ImageItem;
+use Symfony\Component\DependencyInjection\Attribute\Autowire;
+
+/**
+ * Implements hook_file_download().
+ */
+#[Hook('file_download')]
+class ImageDownloadFileHook {
+
+  /**
+   * Cache for private default images.
+   *
+   * @var array<string, \Drupal\Core\Field\FieldDefinitionInterface[]>
+   */
+  protected array $cachedPrivateDefaultImages;
+
+  public function __construct(
+    #[Autowire(service: 'cache.default')]
+    protected readonly CacheBackendInterface $cache,
+    protected readonly EntityTypeManagerInterface $entityTypeManager,
+    protected readonly EntityRepositoryInterface $entityRepository,
+    protected readonly EntityFieldManagerInterface $entityFieldManager,
+    protected readonly AccountInterface $currentUser,
+    protected readonly ImageFactory $imageFactory,
+    protected readonly ConfigFactoryInterface $configFactory,
+    protected readonly ModuleHandlerInterface $moduleHandler,
+  ) {}
+
+  /**
+   * Implements hook_file_download().
+   */
+  public function __invoke($uri): array|int|null {
+    $path = StreamWrapperManager::getTarget($uri);
+    // Private file access for image style derivatives.
+    if (str_starts_with($path, 'styles/')) {
+      $args = explode('/', $path);
+      // Discard "styles", style name, and scheme from the path
+      $args = array_slice($args, 3);
+      // Then the remaining parts are the path to the image.
+      $original_uri = StreamWrapperManager::getScheme($uri) . '://' . implode('/', $args);
+      // Check that the file exists and is an image.
+      $image = $this->imageFactory->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 = $this->moduleHandler->invokeAll('file_download', [$original_uri]);
+        // Confirm there's at least one module granting access and none denying access.
+        if (!empty($headers) && !in_array(-1, $headers)) {
+          return [
+            // Send headers describing the image's size, and MIME-type.
+            'Content-Type' => $image->getMimeType(),
+            'Content-Length' => $image->getFileSize(),
+          ];
+        }
+      }
+      return -1;
+    }
+    // If it is the sample image we need to grant access.
+    $samplePath = $this->configFactory->get('image.settings')->get('preview_image');
+    if ($path === $samplePath) {
+      $image = $this->imageFactory->get($samplePath);
+      return [
+        // Send headers describing the image's size, and MIME-type.
+        'Content-Type' => $image->getMimeType(),
+        'Content-Length' => $image->getFileSize(),
+      ];
+    }
+
+    // Private file access for image fields' default images. Default images are
+    // displayed as a fallback when an image is not uploaded to an image field.
+    if (str_starts_with($path, ImageItem::DEFAULT_IMAGE_DIRECTORY . DIRECTORY_SEPARATOR)) {
+      $image = $this->imageFactory->get($uri);
+      if ($image->isValid()) {
+        $private_default_images = $this->getPrivateDefaultImages();
+        if (isset($private_default_images[$uri])) {
+          foreach ($private_default_images[$uri] as $field_definition) {
+            $access_control_handler = $this->entityTypeManager->getAccessControlHandler($field_definition->getTargetEntityTypeId());
+            // As long as the user has view access to at least one of the fields,
+            // that uses this image as a default, we can exit this foreach loop,
+            // and grant access.
+            if ($access_control_handler->fieldAccess('view', $field_definition)) {
+              return [
+                // Send headers describing the image's size, and MIME-type.
+                'Content-Type' => $image->getMimeType(),
+                'Content-Length' => $image->getFileSize(),
+                // By not explicitly setting them here, this uses normal Drupal
+                // Expires, Cache-Control and ETag headers to prevent proxy or
+                // browser caching of private images.
+              ];
+            }
+          }
+        }
+      }
+      return -1;
+    }
+
+    return NULL;
+  }
+
+  /**
+   * Returns the mapping of private default images to field definitions.
+   *
+   * @return array<string, \Drupal\Core\Field\FieldDefinitionInterface[]>
+   *   An associative array where keys are private image URIs and values are
+   *   arrays of field definitions that reference these images as defaults.
+   */
+  protected function getPrivateDefaultImages(): array {
+    if (!isset($this->cachedPrivateDefaultImages)) {
+      $cid = 'image:default_images';
+      if ($cache = $this->cache->get($cid)) {
+        $this->cachedPrivateDefaultImages = $cache->data;
+      }
+      else {
+        // Save a map of all default image UUIDs and their corresponding field
+        // definitions for quick lookup.
+        $private_default_images = [];
+        $field_map = $this->entityFieldManager->getFieldMapByFieldType('image');
+        $cache_tags = [
+          'image_default_images',
+          'entity_field_info',
+        ];
+        foreach ($field_map as $entity_type_id => $fields) {
+          // Do not filter field storages by uri_scheme, as file entities may
+          // change regardless of the field storage configuration.
+          $field_storages = $this->entityFieldManager->getFieldStorageDefinitions($entity_type_id);
+          foreach ($fields as $field_name => $field_info) {
+            // First, check if the default image is set on the field storage.
+            $uri_from_storage = NULL;
+            $file_uuid = $field_storages[$field_name]->getSetting('default_image')['uuid'];
+            if ($file_uuid && $file = $this->entityRepository->loadEntityByUuid('file', $file_uuid)) {
+              /** @var \Drupal\file\FileInterface $file */
+              $uri_from_storage = $file->getFileUri();
+              $cache_tags = Cache::mergeTags($cache_tags, $file->getCacheTags());
+            }
+
+            foreach ($field_info['bundles'] as $bundle) {
+              $field_definition = $this->entityFieldManager->getFieldDefinitions($entity_type_id, $bundle)[$field_name];
+              $default_uri = $uri_from_storage;
+              $file_uuid = $field_definition->getSetting('default_image')['uuid'];
+              // If the default image is overridden in the field definition, use
+              // that instead of the one set on the field storage.
+              if ($file_uuid && $file = $this->entityRepository->loadEntityByUuid('file', $file_uuid)) {
+                /** @var \Drupal\file\FileInterface $file */
+                $default_uri = $file->getFileUri();
+                $cache_tags = Cache::mergeTags($cache_tags, $file->getCacheTags());
+              }
+              // Finally, if a private default image URI was found,
+              // add it to the list.
+              if ($default_uri && StreamWrapperManager::getScheme($default_uri) === 'private') {
+                $private_default_images[$default_uri][] = $field_definition;
+              }
+            }
+          }
+        }
+        // Cache the default image list.
+        $this->cachedPrivateDefaultImages = $private_default_images;
+        $this->cache->set($cid, $private_default_images, CacheBackendInterface::CACHE_PERMANENT, $cache_tags);
+      }
+    }
+    return $this->cachedPrivateDefaultImages;
+  }
+
+}
diff --git a/core/modules/image/src/Hook/ImageHooks.php b/core/modules/image/src/Hook/ImageHooks.php
index 2d6af269220b..06d2af69cb44 100644
--- a/core/modules/image/src/Hook/ImageHooks.php
+++ b/core/modules/image/src/Hook/ImageHooks.php
@@ -8,7 +8,6 @@
 use Drupal\field\FieldStorageConfigInterface;
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\file\FileInterface;
-use Drupal\image\Controller\ImageStyleDownloadController;
 use Drupal\Core\StreamWrapper\StreamWrapperManager;
 use Drupal\Core\Url;
 use Drupal\Core\Routing\RouteMatchInterface;
@@ -166,62 +165,6 @@ public function theme() : array {
     ];
   }
 
-  /**
-   * Implements hook_file_download().
-   *
-   * Control the access to files underneath the styles directory.
-   */
-  #[Hook('file_download')]
-  public function fileDownload($uri): array|int|null {
-    $path = StreamWrapperManager::getTarget($uri);
-    // Private file access for image style derivatives.
-    if (str_starts_with($path, 'styles/')) {
-      $args = explode('/', $path);
-      // Discard "styles", style name, and scheme from the path
-      $args = array_slice($args, 3);
-      // Then the remaining parts are the path to the image.
-      $original_uri = StreamWrapperManager::getScheme($uri) . '://' . implode('/', $args);
-      // 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.
-        if (!empty($headers) && !in_array(-1, $headers)) {
-          return [
-                // Send headers describing the image's size, and MIME-type.
-            'Content-Type' => $image->getMimeType(),
-            'Content-Length' => $image->getFileSize(),
-          ];
-        }
-      }
-      return -1;
-    }
-    // If it is the sample image we need to grant access.
-    $samplePath = \Drupal::config('image.settings')->get('preview_image');
-    if ($path === $samplePath) {
-      $image = \Drupal::service('image.factory')->get($samplePath);
-      return [
-            // Send headers describing the image's size, and MIME-type.
-        'Content-Type' => $image->getMimeType(),
-        'Content-Length' => $image->getFileSize(),
-      ];
-    }
-    return NULL;
-  }
-
   /**
    * Implements hook_file_move().
    */
diff --git a/core/modules/image/src/ImageFieldManager.php b/core/modules/image/src/ImageFieldManager.php
deleted file mode 100644
index 9afb7b324228..000000000000
--- a/core/modules/image/src/ImageFieldManager.php
+++ /dev/null
@@ -1,142 +0,0 @@
-<?php
-
-namespace Drupal\image;
-
-use Drupal\Core\Access\AccessResult;
-use Drupal\Core\Access\AccessResultInterface;
-use Drupal\Core\Cache\Cache;
-use Drupal\Core\Cache\CacheBackendInterface;
-use Drupal\Core\Entity\EntityFieldManagerInterface;
-use Drupal\Core\Entity\EntityRepositoryInterface;
-use Drupal\Core\Entity\EntityTypeManagerInterface;
-use Drupal\Core\Image\ImageInterface;
-use Drupal\Core\Session\AccountInterface;
-use Symfony\Component\DependencyInjection\Attribute\Autowire;
-
-/**
- * Provides a service for managing image fields.
- *
- * @todo perhaps rename to ImageDefaultAccess and make getDefaultImageFields() protected?
- *   That is, this will be a service only for checking access to images that are used by default,
- *   because managing fields is definitely not what this service does.
- */
-class ImageFieldManager implements ImageFieldManagerInterface {
-
-  /**
-   * Initialized field cache for default images.
-   *
-   * @var array<string, \Drupal\Core\Field\FieldDefinitionInterface[]>
-   */
-  protected array $cachedDefaults;
-
-  /**
-   * Constructs a new ImageFieldManager.
-   *
-   * @param \Drupal\Core\Cache\CacheBackendInterface $cache
-   *   The cache backend.
-   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
-   *   The entity type manager.
-   * @param \Drupal\Core\Entity\EntityRepositoryInterface $entityRepository
-   *   The entity repository.
-   * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entityFieldManager
-   *   The entity field manager.
-   * @param \Drupal\Core\Session\AccountInterface $currentUser
-   *   The current user.
-   */
-  public function __construct(
-    #[Autowire(service: 'cache.default')]
-    protected readonly CacheBackendInterface $cache,
-    protected readonly EntityTypeManagerInterface $entityTypeManager,
-    protected readonly EntityRepositoryInterface $entityRepository,
-    protected readonly EntityFieldManagerInterface $entityFieldManager,
-    protected readonly AccountInterface $currentUser,
-  ) {}
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getDefaultImageFields(): array {
-    if (!isset($this->cachedDefaults)) {
-      $cid = 'image:default_images';
-      if ($cache = $this->cache->get($cid)) {
-        $this->cachedDefaults = $cache->data;
-      }
-      else {
-        // Save a map of all default image UUIDs and their corresponding field
-        // definitions for quick lookup.
-        $defaults = [];
-        $field_map = $this->entityFieldManager->getFieldMapByFieldType('image');
-        $cache_tags = [
-          'image_default_images',
-          'entity_field_info',
-        ];
-        foreach ($field_map as $entity_type_id => $fields) {
-          $field_storages = $this->entityFieldManager->getFieldStorageDefinitions($entity_type_id);
-          foreach ($fields as $field_name => $field_info) {
-            // First, check if the default image is set on the field storage.
-            $uri_from_storage = NULL;
-            $file_uuid = $field_storages[$field_name]->getSetting('default_image')['uuid'];
-            if ($file_uuid && $file = $this->entityRepository->loadEntityByUuid('file', $file_uuid)) {
-              /** @var \Drupal\file\FileInterface $file */
-              $uri_from_storage = $file->getFileUri();
-              $cache_tags = Cache::mergeTags($cache_tags, $file->getCacheTags());
-            }
-
-            foreach ($field_info['bundles'] as $bundle) {
-              $field_definition = $this->entityFieldManager->getFieldDefinitions($entity_type_id, $bundle)[$field_name];
-              $default_uri = $uri_from_storage;
-              $file_uuid = $field_definition->getSetting('default_image')['uuid'];
-              // If the default image is overridden in the field definition, use
-              // that instead of the one set on the field storage.
-              if ($file_uuid && $file = $this->entityRepository->loadEntityByUuid('file', $file_uuid)) {
-                /** @var \Drupal\file\FileInterface $file */
-                $default_uri = $file->getFileUri();
-                $cache_tags = Cache::mergeTags($cache_tags, $file->getCacheTags());
-              }
-              // Finally, if a default image URI was found, add it to the list.
-              if ($default_uri) {
-                $defaults[$default_uri][] = $field_definition;
-              }
-            }
-          }
-        }
-        // Cache the default image list.
-        $this->cachedDefaults = $defaults;
-        $this->cache->set($cid, $defaults, CacheBackendInterface::CACHE_PERMANENT, $cache_tags);
-      }
-    }
-    return $this->cachedDefaults;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function checkAccessToDefaultImage(ImageInterface $image, ?AccountInterface $account = NULL): AccessResultInterface {
-    if (!$image->isValid()) {
-      return AccessResult::forbidden();
-    }
-    // If the image being requested for download is being used as the default
-    // image for any fields, then grant access if the user has 'view' access to
-    // at least one of those fields.
-    $uri = $image->getSource();
-    $default_images = $this->getDefaultImageFields();
-    $access = AccessResult::neutral()->addCacheTags(['image_default_images', 'entity_field_info']);
-    $account ??= $this->currentUser;
-    if (isset($default_images[$uri])) {
-      foreach ($default_images[$uri] as $field_definition) {
-        $access_control_handler = $this->entityTypeManager->getAccessControlHandler($field_definition->getTargetEntityTypeId());
-        $field_access = $access_control_handler->fieldAccess('view', $field_definition, $account, NULL, TRUE);
-        // As long as the user has view access to at least one of the fields,
-        // that uses this image as a default, we can exit this foreach loop,
-        // and grant access.
-        if ($field_access->isAllowed()) {
-          return AccessResult::allowed()
-            ->addCacheableDependency($access)
-            ->addCacheableDependency($field_access);
-        }
-      }
-    }
-    return $access;
-  }
-
-}
diff --git a/core/modules/image/src/ImageFieldManagerInterface.php b/core/modules/image/src/ImageFieldManagerInterface.php
deleted file mode 100644
index 408e24ae90a7..000000000000
--- a/core/modules/image/src/ImageFieldManagerInterface.php
+++ /dev/null
@@ -1,46 +0,0 @@
-<?php
-
-namespace Drupal\image;
-
-use Drupal\Core\Access\AccessResultInterface;
-use Drupal\Core\Image\ImageInterface;
-use Drupal\Core\Session\AccountInterface;
-
-/**
- * Provides an interface for an image field manager.
- */
-interface ImageFieldManagerInterface {
-
-  /**
-   * The default image directory.
-   */
-  public const string DEFAULT_IMAGE_DIRECTORY = 'default_images';
-
-  /**
-   * Maps default values for image fields, and those fields' definitions.
-   *
-   * @return array<string, \Drupal\Core\Field\FieldDefinitionInterface[]>
-   *   An associative array, where the keys are image file URIs, and the values
-   *   are arrays of field definitions which use that image file as their
-   *   default image.
-   */
-  public function getDefaultImageFields(): array;
-
-  /**
-   * Checks the access to a default image.
-   *
-   * This is used in image_file_download() to determine whether to grant access
-   * to an image stored in the private file storage.
-   *
-   * @param \Drupal\Core\Image\ImageInterface $image
-   *   The image to check access to.
-   * @param \Drupal\Core\Session\AccountInterface|null $account
-   *   (optional) The account for which to check access.
-   *   Defaults to the current user.
-   *
-   * @return \Drupal\Core\Access\AccessResultInterface
-   *   The access result.
-   */
-  public function checkAccessToDefaultImage(ImageInterface $image, ?AccountInterface $account = NULL): AccessResultInterface;
-
-}
diff --git a/core/modules/image/src/Plugin/Field/FieldType/ImageItem.php b/core/modules/image/src/Plugin/Field/FieldType/ImageItem.php
index 732f03af973e..038f8e080cab 100644
--- a/core/modules/image/src/Plugin/Field/FieldType/ImageItem.php
+++ b/core/modules/image/src/Plugin/Field/FieldType/ImageItem.php
@@ -17,7 +17,6 @@
 use Drupal\file\Entity\File;
 use Drupal\file\Plugin\Field\FieldType\FileFieldItemList;
 use Drupal\file\Plugin\Field\FieldType\FileItem;
-use Drupal\image\ImageFieldManagerInterface;
 
 /**
  * Plugin implementation of the 'image' field type.
@@ -61,6 +60,11 @@ class ImageItem extends FileItem {
 
   use LoggerChannelTrait;
 
+  /**
+   * The default image directory.
+   */
+  public const string DEFAULT_IMAGE_DIRECTORY = 'default_images';
+
   /**
    * {@inheritdoc}
    */
@@ -460,7 +464,7 @@ protected function defaultImageForm(array &$element, array $settings) {
       '#title' => $this->t('Image'),
       '#description' => $this->t('Image to be shown if no image is uploaded.'),
       '#default_value' => $fids,
-      '#upload_location' => $settings['uri_scheme'] . '://' . ImageFieldManagerInterface::DEFAULT_IMAGE_DIRECTORY . '/',
+      '#upload_location' => $settings['uri_scheme'] . '://' . static::DEFAULT_IMAGE_DIRECTORY . '/',
       '#element_validate' => [
         '\Drupal\file\Element\ManagedFile::validateManagedFile',
         [static::class, 'validateDefaultImageForm'],
-- 
GitLab


From 0ad8ede37177c4817c57b11ac177ae8e6415bc31 Mon Sep 17 00:00:00 2001
From: kksandr <ksandr754@gmail.com>
Date: Thu, 30 Jan 2025 15:44:58 +0200
Subject: [PATCH 19/22] Issue #2107455 by kksandr: replace test procedural hook
 with hook class

---
 ...isplay_test_default_private_storage.module | 25 -----------------
 ...dDisplayTestDefaultPrivateStorageHooks.php | 28 +++++++++++++++++++
 2 files changed, 28 insertions(+), 25 deletions(-)
 delete mode 100644 core/modules/image/tests/modules/image_field_display_test_default_private_storage/image_field_display_test_default_private_storage.module
 create mode 100644 core/modules/image/tests/modules/image_field_display_test_default_private_storage/src/Hook/ImageFieldDisplayTestDefaultPrivateStorageHooks.php

diff --git a/core/modules/image/tests/modules/image_field_display_test_default_private_storage/image_field_display_test_default_private_storage.module b/core/modules/image/tests/modules/image_field_display_test_default_private_storage/image_field_display_test_default_private_storage.module
deleted file mode 100644
index 1f465fbaf2f1..000000000000
--- a/core/modules/image/tests/modules/image_field_display_test_default_private_storage/image_field_display_test_default_private_storage.module
+++ /dev/null
@@ -1,25 +0,0 @@
-<?php
-
-/**
- * @file
- * Image field display test for default images in private file storage.
- */
-
-declare(strict_types=1);
-
-use Drupal\Core\Access\AccessResultInterface;
-use Drupal\Core\Field\FieldDefinitionInterface;
-use Drupal\Core\Session\AccountInterface;
-use Drupal\Core\Field\FieldItemListInterface;
-use Drupal\Core\Access\AccessResult;
-
-/**
- * Implements hook_entity_field_access().
- */
-function image_field_display_test_default_private_storage_entity_field_access($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, ?FieldItemListInterface $items = NULL): AccessResultInterface {
-  if ($field_definition->getName() == 'field_default_private' && $operation == 'view') {
-    return AccessResult::forbidden();
-  }
-
-  return AccessResult::neutral();
-}
diff --git a/core/modules/image/tests/modules/image_field_display_test_default_private_storage/src/Hook/ImageFieldDisplayTestDefaultPrivateStorageHooks.php b/core/modules/image/tests/modules/image_field_display_test_default_private_storage/src/Hook/ImageFieldDisplayTestDefaultPrivateStorageHooks.php
new file mode 100644
index 000000000000..884016149a7e
--- /dev/null
+++ b/core/modules/image/tests/modules/image_field_display_test_default_private_storage/src/Hook/ImageFieldDisplayTestDefaultPrivateStorageHooks.php
@@ -0,0 +1,28 @@
+<?php
+
+declare(strict_types=1);
+
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Access\AccessResultInterface;
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+use Drupal\Core\Session\AccountInterface;
+
+/**
+ * Implements hook_entity_field_access().
+ */
+#[Hook('entity_field_access')]
+class ImageFieldDisplayTestDefaultPrivateStorageHooks {
+
+  /**
+   * Implements hook_entity_field_access().
+   */
+  public function __invoke($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, ?FieldItemListInterface $items = NULL): AccessResultInterface {
+    if ($field_definition->getName() == 'field_default_private' && $operation == 'view') {
+      return AccessResult::forbidden();
+    }
+    return AccessResult::neutral();
+  }
+
+}
-- 
GitLab


From 47e414dccca32676e940fa5650acc662d36c714b Mon Sep 17 00:00:00 2001
From: kksandr <ksandr754@gmail.com>
Date: Thu, 30 Jan 2025 15:53:29 +0200
Subject: [PATCH 20/22] Issue #2107455 by kksandr: fix namespace

---
 .../Hook/ImageFieldDisplayTestDefaultPrivateStorageHooks.php    | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/core/modules/image/tests/modules/image_field_display_test_default_private_storage/src/Hook/ImageFieldDisplayTestDefaultPrivateStorageHooks.php b/core/modules/image/tests/modules/image_field_display_test_default_private_storage/src/Hook/ImageFieldDisplayTestDefaultPrivateStorageHooks.php
index 884016149a7e..cd8b45320a6f 100644
--- a/core/modules/image/tests/modules/image_field_display_test_default_private_storage/src/Hook/ImageFieldDisplayTestDefaultPrivateStorageHooks.php
+++ b/core/modules/image/tests/modules/image_field_display_test_default_private_storage/src/Hook/ImageFieldDisplayTestDefaultPrivateStorageHooks.php
@@ -2,6 +2,8 @@
 
 declare(strict_types=1);
 
+namespace Drupal\image_field_display_test_default_private_storage\Hook;
+
 use Drupal\Core\Access\AccessResult;
 use Drupal\Core\Access\AccessResultInterface;
 use Drupal\Core\Field\FieldDefinitionInterface;
-- 
GitLab


From 2453ee81d3824a82f38cbe2974ce6d2e0b7d97f6 Mon Sep 17 00:00:00 2001
From: kksandr <59277-kksandr@users.noreply.drupalcode.org>
Date: Fri, 28 Feb 2025 20:56:18 +0000
Subject: [PATCH 21/22] Apply 1 suggestion(s) to 1 file(s)

Co-authored-by: godotislate <15754-godotislate@users.noreply.drupalcode.org>
---
 core/modules/image/src/Hook/ImageDownloadFileHook.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/core/modules/image/src/Hook/ImageDownloadFileHook.php b/core/modules/image/src/Hook/ImageDownloadFileHook.php
index 80f983bc47fe..9b12ebbae3d2 100644
--- a/core/modules/image/src/Hook/ImageDownloadFileHook.php
+++ b/core/modules/image/src/Hook/ImageDownloadFileHook.php
@@ -47,7 +47,7 @@ public function __construct(
   /**
    * Implements hook_file_download().
    */
-  public function __invoke($uri): array|int|null {
+  public function __invoke(string $uri): array|int|null {
     $path = StreamWrapperManager::getTarget($uri);
     // Private file access for image style derivatives.
     if (str_starts_with($path, 'styles/')) {
-- 
GitLab


From 32a4fc9a1e8ccf8cc58578d12c03775a4149bf98 Mon Sep 17 00:00:00 2001
From: kksandr <ksandr754@gmail.com>
Date: Sun, 2 Mar 2025 22:52:51 +0200
Subject: [PATCH 22/22] Issue #2107455 by kksandr: improved test coverage

---
 .../image_field_display_test.schema.yml       |  7 +++
 .../image_field_display_test.info.yml}        |  0
 .../src/Hook/ImageFieldDisplayTestHooks.php   | 33 ++++++++++++
 ...dDisplayTestDefaultPrivateStorageHooks.php | 30 -----------
 .../src/Functional/ImageFieldDisplayTest.php  | 50 +++++++++++++++----
 5 files changed, 81 insertions(+), 39 deletions(-)
 create mode 100644 core/modules/image/tests/modules/image_field_display_test/config/schema/image_field_display_test.schema.yml
 rename core/modules/image/tests/modules/{image_field_display_test_default_private_storage/image_field_display_test_default_private_storage.info.yml => image_field_display_test/image_field_display_test.info.yml} (100%)
 create mode 100644 core/modules/image/tests/modules/image_field_display_test/src/Hook/ImageFieldDisplayTestHooks.php
 delete mode 100644 core/modules/image/tests/modules/image_field_display_test_default_private_storage/src/Hook/ImageFieldDisplayTestDefaultPrivateStorageHooks.php

diff --git a/core/modules/image/tests/modules/image_field_display_test/config/schema/image_field_display_test.schema.yml b/core/modules/image/tests/modules/image_field_display_test/config/schema/image_field_display_test.schema.yml
new file mode 100644
index 000000000000..1f42e8e78060
--- /dev/null
+++ b/core/modules/image/tests/modules/image_field_display_test/config/schema/image_field_display_test.schema.yml
@@ -0,0 +1,7 @@
+field.field.*.*.*.third_party.image_field_display_test:
+  type: mapping
+  label: 'Image field display test settings'
+  mapping:
+    access_denied:
+      type: boolean
+      label: 'Access denied'
diff --git a/core/modules/image/tests/modules/image_field_display_test_default_private_storage/image_field_display_test_default_private_storage.info.yml b/core/modules/image/tests/modules/image_field_display_test/image_field_display_test.info.yml
similarity index 100%
rename from core/modules/image/tests/modules/image_field_display_test_default_private_storage/image_field_display_test_default_private_storage.info.yml
rename to core/modules/image/tests/modules/image_field_display_test/image_field_display_test.info.yml
diff --git a/core/modules/image/tests/modules/image_field_display_test/src/Hook/ImageFieldDisplayTestHooks.php b/core/modules/image/tests/modules/image_field_display_test/src/Hook/ImageFieldDisplayTestHooks.php
new file mode 100644
index 000000000000..e03d58d57f1b
--- /dev/null
+++ b/core/modules/image/tests/modules/image_field_display_test/src/Hook/ImageFieldDisplayTestHooks.php
@@ -0,0 +1,33 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\image_field_display_test\Hook;
+
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Access\AccessResultInterface;
+use Drupal\Core\Field\FieldConfigInterface;
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+use Drupal\Core\Session\AccountInterface;
+
+/**
+ * Implements hook_entity_field_access().
+ */
+#[Hook('entity_field_access')]
+class ImageFieldDisplayTestHooks {
+
+  /**
+   * Implements hook_entity_field_access().
+   */
+  public function __invoke(string $operation, FieldDefinitionInterface $field_definition, AccountInterface $account, ?FieldItemListInterface $items = NULL): AccessResultInterface {
+    if ($operation === 'view'
+        && $field_definition instanceof FieldConfigInterface
+        && $field_definition->getThirdPartySetting('image_field_display_test', 'access_denied', FALSE)) {
+      return AccessResult::forbidden()->addCacheableDependency($field_definition);
+    }
+    return AccessResult::neutral();
+  }
+
+}
diff --git a/core/modules/image/tests/modules/image_field_display_test_default_private_storage/src/Hook/ImageFieldDisplayTestDefaultPrivateStorageHooks.php b/core/modules/image/tests/modules/image_field_display_test_default_private_storage/src/Hook/ImageFieldDisplayTestDefaultPrivateStorageHooks.php
deleted file mode 100644
index cd8b45320a6f..000000000000
--- a/core/modules/image/tests/modules/image_field_display_test_default_private_storage/src/Hook/ImageFieldDisplayTestDefaultPrivateStorageHooks.php
+++ /dev/null
@@ -1,30 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Drupal\image_field_display_test_default_private_storage\Hook;
-
-use Drupal\Core\Access\AccessResult;
-use Drupal\Core\Access\AccessResultInterface;
-use Drupal\Core\Field\FieldDefinitionInterface;
-use Drupal\Core\Field\FieldItemListInterface;
-use Drupal\Core\Hook\Attribute\Hook;
-use Drupal\Core\Session\AccountInterface;
-
-/**
- * Implements hook_entity_field_access().
- */
-#[Hook('entity_field_access')]
-class ImageFieldDisplayTestDefaultPrivateStorageHooks {
-
-  /**
-   * Implements hook_entity_field_access().
-   */
-  public function __invoke($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, ?FieldItemListInterface $items = NULL): AccessResultInterface {
-    if ($field_definition->getName() == 'field_default_private' && $operation == 'view') {
-      return AccessResult::forbidden();
-    }
-    return AccessResult::neutral();
-  }
-
-}
diff --git a/core/modules/image/tests/src/Functional/ImageFieldDisplayTest.php b/core/modules/image/tests/src/Functional/ImageFieldDisplayTest.php
index edda3602feca..3a410bec76ec 100644
--- a/core/modules/image/tests/src/Functional/ImageFieldDisplayTest.php
+++ b/core/modules/image/tests/src/Functional/ImageFieldDisplayTest.php
@@ -7,7 +7,9 @@
 use Drupal\Core\Field\FieldStorageDefinitionInterface;
 use Drupal\Core\StreamWrapper\StreamWrapperManager;
 use Drupal\Core\Url;
+use Drupal\field\Entity\FieldConfig;
 use Drupal\field\Entity\FieldStorageConfig;
+use Drupal\file\FileInterface;
 use Drupal\image\Entity\ImageStyle;
 use Drupal\Tests\system\Functional\Cache\AssertPageCacheContextsAndTagsTrait;
 use Drupal\Tests\TestFileCreationTrait;
@@ -566,8 +568,8 @@ public function testImageFieldDefaultImage(): void {
     $this->assertEmpty($default_image['uuid'], 'Default image removed from field.');
     // Create an image field that uses the private:// scheme and test that the
     // default image works as expected.
-    $private_field_name = 'field_default_private';
-    $this->createImageField($private_field_name, 'node', 'article', ['uri_scheme' => 'private']);
+    $private_field_name = $this->randomMachineName();
+    $private_field = $this->createImageField($private_field_name, 'node', 'article', ['uri_scheme' => 'private']);
     // Add a default image to the new field.
     $edit = [
       // Get the path of the 'image-test.gif' file.
@@ -583,6 +585,7 @@ public function testImageFieldDefaultImage(): void {
     $private_field_storage = FieldStorageConfig::loadByName('node', $private_field_name);
     $default_image = $private_field_storage->getSetting('default_image');
     $file = \Drupal::service('entity.repository')->loadEntityByUuid('file', $default_image['uuid']);
+    $this->assertInstanceOf(FileInterface::class, $file);
 
     $this->assertEquals('private', StreamWrapperManager::getScheme($file->getFileUri()), 'Default image uses private:// scheme.');
     $this->assertTrue($file->isPermanent(), 'The default image status is permanent.');
@@ -607,16 +610,45 @@ public function testImageFieldDefaultImage(): void {
     // is present.
     $this->assertSession()->responseContains($default_output);
 
-    // Check that the default image itself can be downloaded; i.e.: not just the
-    // HTML markup.
-    $private_default_image_url = \Drupal::service('file_url_generator')->generateAbsoluteString($file->getFileUri());
-    // Check that a user can download the default image attached to a node field
-    // configured to store data in the private file storage.
+    // Check if the default image set for the field storage can be downloaded;
+    // i.e., not just the HTML markup.
+    $this->container->get('module_installer')->install(['image_field_display_test']);
+    $private_default_image_url = $file->createFileUrl();
     $this->drupalGet($private_default_image_url);
     $this->assertSession()->statusCodeEquals(200);
-    // Now, install a module that denies access to the field; and check that the
+    // Now disable access to the field and check that the
     // same user now receives a 403 Access Denied.
-    \Drupal::service('module_installer')->install(['image_field_display_test_default_private_storage']);
+    $private_field->setThirdPartySetting('image_field_display_test', 'access_denied', TRUE)->save();
+    $this->drupalGet($private_default_image_url);
+    $this->assertSession()->statusCodeEquals(403);
+
+    // Override the default image with the field configuration for
+    // the next access check.
+    $edit = [
+      // Get the path of the 'image-test-transparent-out-of-range.gif' file.
+      'files[settings_default_image_uuid]' => $this->container->get('file_system')->realpath($images[3]->uri),
+      'settings[default_image][alt]' => $alt,
+      'settings[default_image][title]' => $title,
+    ];
+    $this->drupalGet("admin/structure/types/manage/article/fields/node.article.$private_field_name");
+    $this->submitForm($edit, 'Save');
+    // Clear the cache and reload the field configuration
+    // to retrieve the UUID of the default image file.
+    $this->container->get('entity_field.manager')->clearCachedFieldDefinitions();
+    $private_field = FieldConfig::loadByName('node', 'article', $private_field_name);
+    $file_uuid = $private_field->getSetting('default_image')['uuid'];
+    $file = $this->container->get('entity.repository')->loadEntityByUuid('file', $file_uuid);
+    $this->assertInstanceOf(FileInterface::class, $file);
+
+    // Check if the default image overridden by the field configuration
+    // can be downloaded.
+    $private_default_image_url = $file->createFileUrl();
+    $private_field->setThirdPartySetting('image_field_display_test', 'access_denied', FALSE)->save();
+    $this->drupalGet($private_default_image_url);
+    $this->assertSession()->statusCodeEquals(200);
+    // Now disable access to the field and check that the
+    // same user now receives a 403 Access Denied.
+    $private_field->setThirdPartySetting('image_field_display_test', 'access_denied', TRUE)->save();
     $this->drupalGet($private_default_image_url);
     $this->assertSession()->statusCodeEquals(403);
   }
-- 
GitLab