Image field default value not shown when upload destination set to private file storage

Created on 8 October 2013, over 11 years ago
Updated 20 January 2023, about 2 years ago

Problem/Motivation

In an image field's configuration, you can specify a default image to be displayed as a fallback if the content editor does not upload an image to that field.

When an image field's Upload Destination is set to the Private file storage destination (i.e.: the private:// stream), attempting to view (i.e.: download) the default image returns an HTTP 403 Access Denied, resulting in a broken image on the page.

Steps to reproduce

  1. Add an image field to a content entity type. In the new image field's Field Settings, set its Upload destination to Private files. In its settings, upload a default image.
  2. Add an image field to a content entity type. In the new image field's Field Settings, set its Upload destination to Public files. In its settings, upload a default image.
  3. Create a new instance of that content entity. Leave both image fields empty.
    • Expected behavior: The default image for both fields are shown,
    • Actual behavior: The default image for the field whose Upload Destination is set to "Public files" is shown. The default image for the field whose Upload Destination is set to "Private files" is broken.

Inspection in the browser's Network Console shows Drupal responds to the browser's request with an HTTP/403 Access Denied response. Further inspection of the cause of the 403 shows that a \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException is thrown from \Drupal\image\Controller\ImageStyleDownloadController::deliver() because $this->moduleHandler()->invokeAll('file_download', [$image_uri]) fails to return any headers (i.e.: indicating access denied). In particular, the image module's implementation of hook_file_download() (i.e.: image_file_download()) does not handle a case for default images.

As of 2020-05-29, this happens on 9.1.x and 8.8.x.

Proposed resolution

Modify image_file_download() to handle the case for default images, by granting access if the image URI that is being requested happens to be the default image for at least one field that the current user has 'view' access to.

Remaining tasks

  1. Review and feedback - in particular, is the access check that we are making sound and complete?
  2. RTBC
  3. Maintainer review, feedback
  4. Commit
  5. Backport?

User interface changes

None.

API changes

None.

Data model changes

None.

Release notes snippet

To be determined.

Original report by claudiu.cristea

On an image field where the uploaded destination is set to private:// stream, the default image is returning 403.

If you encounter a WSOD while manually testing, see #2799837: WSOD when changing uri_scheme and setting a new default image at the same time for an image field. โ†’ .

๐Ÿ› Bug report
Status

Needs review

Version

10.1 โœจ

Component
Image moduleย  โ†’

Last updated 2 days ago

Created by

๐Ÿ‡ท๐Ÿ‡ดRomania claudiu.cristea Arad ๐Ÿ‡ท๐Ÿ‡ด

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Merge Requests

Comments & Activities

Not all content is available!

It's likely this issue predates Contrib.social: some issue and comment data are missing.

  • Status changed to RTBC about 2 years ago
  • ๐Ÿ‡บ๐Ÿ‡ธUnited States xjm

    This is quite a lot of procedural code, plus the &drupal_static() in there. This looks like D7 code. Is there a reason it has to be added to image.module rather than being better encapsulated?

    Tagging for framework manager feedback on the architecture. This could also use @claudiu.cristea's feedback as the subsystem maintainer -- their patch was the original prototype back in 2013, but the codebase has changed quite a lot since then.

  • Status changed to Needs work about 2 years ago
  • ๐Ÿ‡ฌ๐Ÿ‡งUnited Kingdom alexpott ๐Ÿ‡ช๐Ÿ‡บ๐ŸŒ
    1. +++ b/core/modules/image/image.module
      @@ -167,6 +168,115 @@ function image_file_download($uri) {
      +  if (strpos($path, 'default_images/') === 0) {
      

      Can use str_starts_with() here.

    2. +++ b/core/modules/image/image.module
      @@ -167,6 +168,115 @@ function image_file_download($uri) {
      +/**
      + * 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
      + */
      +function image_get_default_image_fields() {
      +  $defaults = &drupal_static(__FUNCTION__);
      +  $cid = 'image:default_images';
      +
      +  if (!isset($defaults)) {
      +    if ($cache = \Drupal::cache()->get($cid)) {
      +      $defaults = $cache->data;
      +    }
      +    else {
      +      // Save a map of all default image UUIDs and their corresponding field
      +      // configuration IDs for quick lookup.
      +      $defaults = [];
      +      $fields = \Drupal::entityTypeManager()
      +        ->getStorage('field_config')
      +        ->loadMultiple();
      +
      +      foreach ($fields as $field) {
      +        if ($field->getType() == 'image') {
      +          // Check if there is a default image in the field config.
      +          if ($field_uuid = $field->getSetting('default_image')['uuid']) {
      +            $file = \Drupal::service('entity.repository')
      +              ->loadEntityByUuid('file', $field_uuid);
      +
      +            // 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.
      +          if ($storage_uuid = $field->getFieldStorageDefinition()->getSetting('default_image')['uuid']) {
      +            $file = \Drupal::service('entity.repository')
      +              ->loadEntityByUuid('file', $storage_uuid);
      +            if ($file) {
      +              // 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.
      +      \Drupal::cache()
      +        ->set($cid, $defaults, CacheBackendInterface::CACHE_PERMANENT, ['image_default_images']);
      +    }
      +  }
      +
      +  return $defaults;
       }
      

      I agree with @xjm - this code should be a service and the if we do have a cache then it'll need to be cleared when a field is changed.

    3. When we add the service we can also move the code from image_file_download there too
  • ๐Ÿ‡ฉ๐Ÿ‡ชGermany jan kellermann

    Reworks patch #74 for Drupal 9.4.13

  • ๐Ÿ‡บ๐Ÿ‡ธUnited States xjm

    Hiding the D9.4 patch; we don't need that. We need #82 and #83 addressed. Thanks!

  • ๐Ÿ‡บ๐Ÿ‡ธUnited States xjm
  • ๐Ÿ‡บ๐Ÿ‡ธUnited States bas123

    I just updated to Social (social-11.8.12) and the ugly Default Profile image wouldn't display once again and I couldn't get any of the patches to work until I tried #84 ๐Ÿ› Image field default value not shown when upload destination set to private file storage Needs work ( 2107455-84.patch โ†’ )

    At least for now that one solved it in four Drupal Open Social websites! so Kudos to Jan Kellermann โ†’ !

    I just wish this problem which has persisted for several years and incarnations would ge committed to core...

  • 50:14
    46:59
    Running
  • ๐Ÿ‡ฉ๐Ÿ‡ชGermany metalbote Aachen

    Just reroll for 10.1.6

  • last update about 1 year ago
    Patch Failed to Apply
  • First commit to issue fork.
  • last update about 1 year ago
    29,717 pass, 2 fail
  • ๐Ÿ‡บ๐Ÿ‡ธUnited States yospyn

    Upgrading to core 10.2.1 caused #79 (and #89) to fail to apply.

    I was using #79 successfully with 10.1.7.

  • First commit to issue fork.
  • Open in Jenkins โ†’ Open on Drupal.org โ†’
    Environment: PHP 8.1 & MariaDB 10.3.22
    last update about 1 year ago
    Patch Failed to Apply
  • ๐Ÿ‡ณ๐Ÿ‡ดNorway eiriksm Norway

    eiriksm โ†’ changed the visibility of the branch 11.x to hidden.

  • ๐Ÿ‡ณ๐Ÿ‡ดNorway eiriksm Norway

    Here is a patch for 10.2

    Going to try to take a swing at the feedback in #82 and #83

  • ๐Ÿ‡ณ๐Ÿ‡ดNorway eiriksm Norway

    eiriksm โ†’ changed the visibility of the branch 11.x to active.

  • Status changed to Needs review about 1 year ago
  • ๐Ÿ‡ณ๐Ÿ‡ดNorway eiriksm Norway

    Opened a MR (against 11.x) where i added back the test module needed to actually run the tests in the patch.

    Also refactored the procedural style code into a service.

    I'm pretty sure we are missing a bit of test coverage on this one though (for example the code branch concerning "A default image could be used by multiple field configs."), but since it's a bug it might not be the main priority. The test certainly demonstrates the bug at least, so that's the most important part for me.

    Anyway. the feedback in #82 and #83 should be addressed in this MR

  • Pipeline finished with Failed
    about 1 year ago
    Total: 756s
    #77036
  • Status changed to Needs work about 1 year ago
  • The Needs Review Queue Bot โ†’ tested this issue.

    While you are making the above changes, we recommend that you convert this patch to a merge request โ†’ . Merge requests are preferred over patches. Be sure to hide the old patch files as well. (Converting an issue to a merge request without other contributions to the issue will not receive credit.)

  • ๐Ÿ‡บ๐Ÿ‡ธUnited States yospyn

    Patch #94 via eiriksm worked for me on 10.2.3, many thanks.

  • Status changed to Needs review about 1 year ago
  • ๐Ÿ‡ณ๐Ÿ‡ดNorway eiriksm Norway

    Guess it should have status needs review?

  • Status changed to Needs work about 1 year ago
  • ๐Ÿ‡บ๐Ÿ‡ธUnited States smustgrave

    Assuming the MR, appears to have test failures.

  • ๐Ÿ‡บ๐Ÿ‡ธUnited States yospyn

    Patch #94 not working with 10.3, so sticking to 10.2.7 for sites involving this.

  • ๐Ÿ‡จ๐Ÿ‡ฆCanada joseph.olstad

    @yospyn , are there any watchdog /php log messages or helpful console exceptions that provide some insight?

  • ๐Ÿ‡จ๐Ÿ‡ฆCanada joseph.olstad

    @yospyn,

    The MR was updated, it now rolls with fuzz on 10.3.x, might work for you.

    https://git.drupalcode.org/project/drupal/-/merge_requests/6158.diff

    Might work with 10.3.x

  • Pipeline finished with Failed
    8 months ago
    Total: 167s
    #210174
  • Pipeline finished with Failed
    8 months ago
    Total: 508s
    #210196
  • Pipeline finished with Success
    8 months ago
    Total: 658s
    #210230
  • Status changed to Needs review 8 months ago
  • ๐Ÿ‡จ๐Ÿ‡ฆCanada joseph.olstad

    w00t! All passing as a service now.

  • ๐Ÿ‡ฉ๐Ÿ‡ชGermany luenemann Sรผdbaden, Germany
  • Status changed to RTBC 8 months ago
  • ๐Ÿ‡จ๐Ÿ‡ฆCanada joseph.olstad

    Rationale for RTBC:

    • Requested change to a service is completed (see comments #82,#83)
    • Merge request is passing tests.
    • Tests Only test is failing which proves the need for the fix
  • Pipeline finished with Failed
    8 months ago
    Total: 171s
    #211167
  • ๐Ÿ‡จ๐Ÿ‡ฆCanada joseph.olstad

    @kksandr , I'll leave the other suggested changes for others:

    @todo:

    • Services should be based on interfaces so they can be decorated or overridden.
    • Move this into a new service method image.field_manager something like checkAccessToDefaultImage(string $uri): bool so that it uses dependency injection, and can be overridden by a service override or decoration.
    • Check access only after the image is recognized as valid, that is, this should all be under
      if ($image->isValid()) {
      
    • It could be part of the checkAccessToDefaultImage method which will do all the work regarding the $uri access check.
  • Pipeline finished with Failed
    8 months ago
    #211173
  • ๐Ÿ‡จ๐Ÿ‡ฆCanada joseph.olstad

    Reverted 5158f6f6, it didn't seem right to change the storage from field_config to entity_field.manager as suggested by @kksandr, so backing it off. We'll see if this greens it up.

  • Pipeline finished with Success
    8 months ago
    Total: 2741s
    #211201
  • Assigned to kksandr
  • Status changed to Needs work 8 months ago
  • kksandr โ†’ changed the visibility of the branch 2107455-image-field-default to hidden.

  • kksandr โ†’ changed the visibility of the branch 2107455-102 to hidden.

  • Pipeline finished with Success
    8 months ago
    Total: 644s
    #211686
  • Issue was unassigned.
  • Changes that I made:

    1. Auto-wiring of dependencies
    2. Dependencies were moved to the properties of the constructor
    3. An interface was defined for the new service
    4. Access check was moved to the service itself
    5. The service uses entity_field.manager to get fields instead of loading only configuration fields
    6. The access check now returns an AccessResultInterface, which makes it possible to provide cache metadata if it is to be used somewhere other than a file access check

    I also left a few details for consideration tagged @todo.

  • Pipeline finished with Canceled
    8 months ago
    Total: 68s
    #211708
  • Pipeline finished with Success
    8 months ago
    Total: 598s
    #211711
  • Pipeline finished with Success
    8 months ago
    Total: 481s
    #211725
  • Status changed to Needs review 8 months ago
  • Pipeline finished with Success
    8 months ago
    Total: 520s
    #212249
  • I moved the new service to the images module because it handles loading default images and checking access to them.

  • Pipeline finished with Success
    8 months ago
    Total: 544s
    #212257
  • Pipeline finished with Success
    8 months ago
    #212259
  • ๐Ÿ‡บ๐Ÿ‡ธUnited States yospyn

    @joseph.olstad thanks for working on this, when I apply the patch in #104 I get this error:
    ParseError: syntax error, unexpected identifier "DEFAULT_IMAGE_DIRECTORY", expecting "=" in Composer\Autoload\{closure}() (line 17 of /app/web/core/modules/image/src/ImageFieldManagerInterface.php).

  • @yospyn this fix targets the 11.x branch, which depends on PHP 8.3, so I used typed constants that are available in PHP 8.3. You can open a separate merge request for 10.3.x and lower the PHP requirements. You can also always update PHP on your server.

  • ๐Ÿ‡บ๐Ÿ‡ธUnited States yospyn

    @kksandr success! Patch #104 worked with PHP 8.3 on 10.3. No errors in logs. Thank you

  • First commit to issue fork.
  • ๐Ÿ‡ง๐Ÿ‡ทBrazil viniciusrp

    I created a merge request for Drupal 10.3.x version and I fixed the PHP8.1 compatibility.

  • Pipeline finished with Failed
    6 months ago
    Total: 120s
    #255955
  • Pipeline finished with Success
    6 months ago
    Total: 532s
    #255964
  • Status changed to Needs work 6 months ago
  • ๐Ÿ‡ท๐Ÿ‡ดRomania claudiu.cristea Arad ๐Ÿ‡ท๐Ÿ‡ด

    I have some remarks. See the MR

  • ๐Ÿ‡ฉ๐Ÿ‡ชGermany luenemann Sรผdbaden, Germany

    luenemann โ†’ changed the visibility of the branch 2107455-image-field-default to active.

  • ๐Ÿ‡ฉ๐Ÿ‡ชGermany luenemann Sรผdbaden, Germany

    luenemann โ†’ changed the visibility of the branch 2107455-image-field-default to hidden.

  • Pipeline finished with Failed
    26 days ago
    Total: 150s
    #407093
  • ๐Ÿ‡ฉ๐Ÿ‡ชGermany luenemann Sรผdbaden, Germany

    Just rebased onto 55bab340, just before the ๐Ÿ“Œ [ignore] Convert everything everywhere all at once Active .

    I removed some out of scope comment changes to simplify the rebase.

  • Pipeline finished with Success
    26 days ago
    Total: 1068s
    #407117
  • ๐Ÿ‡ฉ๐Ÿ‡ชGermany luenemann Sรผdbaden, Germany

    CI pipline is green, Test-only changes fails as expected.

    Still "Needs work" for #123 and hook conversion.

  • Pipeline finished with Canceled
    26 days ago
    Total: 70s
    #407156
  • Pipeline finished with Canceled
    26 days ago
    Total: 69s
    #407157
  • Pipeline finished with Canceled
    26 days ago
    Total: 71s
    #407158
  • Pipeline finished with Success
    26 days ago
    Total: 581s
    #407159
  • Pipeline finished with Success
    26 days ago
    Total: 516s
    #407441
  • Pipeline finished with Failed
    26 days ago
    Total: 435s
    #407477
  • Pipeline finished with Failed
    26 days ago
    Total: 186s
    #407550
  • Pipeline finished with Running
    26 days ago
    #407561
  • ๐Ÿ‡ฌ๐Ÿ‡งUnited Kingdom oily Greater London

    oily โ†’ changed the visibility of the branch 10.3.x to hidden.

  • ๐Ÿ‡ฎ๐Ÿ‡ณIndia dhaval_panara

    dhaval_panara โ†’ changed the visibility of the branch 10.3.x to active.

  • Adding the patch file instead of using git MR in composer json.

  • ๐Ÿ‡ฉ๐Ÿ‡ชGermany luenemann Sรผdbaden, Germany

    Hooks in test modules should also be converted to OOP.
    IMHO that is the only thing that's left.

  • ๐Ÿ‡ฉ๐Ÿ‡ชGermany luenemann Sรผdbaden, Germany

    There is no more subsystem maintainer for image.module since ๐Ÿ“Œ Remove claudiu.cristea from MAINTAINERS.txt Active . How to proceed?

  • Pipeline finished with Failed
    23 days ago
    Total: 408s
    #410347
  • Pipeline finished with Success
    23 days ago
    Total: 394s
    #410360
  • Thanks for your work. It works for me too for 10.4

  • Pipeline finished with Success
    16 days ago
    Total: 681s
    #416679
Production build 0.71.5 2024