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