Score:1

Is it possible to allow anonymous users to view a temporary managed file via hook_file_download()?

ng flag

I am creating a live image preview system for customizable products that needs to send a user's temporary file to a remote server.

I have a token-based system to protect the file but the problem seems to be that the file is temporary. I've created a hook_file_download() to return the headers as required to allow access when approved but the file access is still being denied somewhere.

It does not seem to be a module weight issue as I've made my custom module have the lowest weight and verified via debug code that it fires after core file_file_download()

EDIT: Further troubleshooting shows the the headers array is getting a key value set to "-1" somewhere after hook_file_download() and core "FileDownloadController" which is where the file is getting denied. Any ideas where this [0] = -1 header value is getting set and how to override it?

If I strip it down to the basics for testing as shown below the 'allow' headers are returned but the file is still being blocked when viewed from an anonymous browser:

function MYMODULE_file_download($uri) {

  // Check to see if this is a config download.
  $scheme = StreamWrapperManager::getScheme($uri);

  if ($scheme == 'temporary'){
    if ($files = \Drupal::entityTypeManager()->getStorage('file')->loadByProperties(['uri' => $uri])){

      $file = reset($files) ?: NULL;
      
      // Access is granted.
      $headers = file_get_content_headers($file);
      return $headers;
    }
  }
}
Score:1
us flag

Temporary files are handled by file_file_download(), an implementation of hook_file_download() done from the File module. The code it uses is the following one.

  // Find out if a temporary file is still used in the system.
  if ($file->isTemporary()) {
    $usage = \Drupal::service('file.usage')->listUsage($file);
    if (empty($usage) && $file->getOwnerId() != \Drupal::currentUser()
      ->id()) {
      // Deny access to temporary files without usage that are not owned by the
      // same user. This prevents the security issue that a private file that
      // was protected by field permissions becomes available after its usage
      // was removed and before it is actually deleted from the file system.
      // Modules that depend on this behavior should make the file permanent
      // instead.
      return -1;
    }
  }

Reading the comments, that is done on purpose to avoid a private file, protected by field permissions, is visible after the field permissions are changed, but before the file is deleted.

Looking at the code that invokes that hook, in FileDownloadController::download() for example, I don't see any way to avoid that, as the code doesn't use hook_file_download_alter().

A workaround could be setting that file as being used, since the code checks the file is not used, before blocking the access.

function mymodule_file_download($uri) {
  if (StreamWrapperManager::getScheme($uri) == 'temporary') {
    if ($files = \Drupal::entityTypeManager()->getStorage('file')->loadByProperties(['uri' => $uri])){
      if ($file = reset($files)) {
        // Access is granted.
        \Drupal::service('file.usage')->add($file, 'mymodule', 'unexisting_entity', 10);
        $headers = file_get_content_headers($file);
        return $headers;
      }
    }
  }
}

I used 'unexisting_entity' and 10 as entity type and entity ID. If you have real values for them, you should use those.

Note that FileUsageBase::add(), the DatabaseFileUsageBackend::add() parent method, changes the file to permanent, in the case it is not already.

// Make sure that a used file is permanent.
if (!$file->isPermanent()) {
  $file->setPermanent();
  $file->save();
} 

When a file usage is decremented and becomes 0, the file is changed to temporary from FileUsageBase::delete().

// If there are no more remaining usages of this file, mark it as temporary,
// which result in a delete through system_cron().
$usage = \Drupal::service('file.usage')->listUsage($file);
if (empty($usage)) {
  $file->setTemporary();
  $file->save();
}

I would rather increase the file usage, instead of making directly a file permanent, as decreasing the file usage doesn't conflict with other modules that could set the same file as permanent.
Alternatively, I would use the following code for hook_file_download().

function mymodule_file_download($uri) {
  if (StreamWrapperManager::getScheme($uri) == 'temporary') {
    if ($files = \Drupal::entityTypeManager()->getStorage('file')->loadByProperties(['uri' => $uri])){
      if ($file = reset($files)) {
        // Access is granted.
        if (!$file->isPermanent()) {
          $file->setPermanent();
          $file->save();
        }
        $headers = file_get_content_headers($file);
        return $headers;
      }
    }
  }
}

In this case, to make the file temporary again, I would use the following code.

// Store the file entity reference in $file.
$usage = \Drupal::service('file.usage')->listUsage($file);
if (empty($usage) && !$file->isTemporary()) {
  $file->setTemporary();
  $file->save();
}

To achieve what you want, it would be also possible to change the controller for the system.temporary route, but that seems excessive.

quantumized avatar
ng flag
Thank you, yes, that file_file_download() snippet is the culprit. I'm confused as to why I can't use hook_file_download() in my higher weighted module to override that.
quantumized avatar
ng flag
I found a workaround by setting the file to "isPermanent" but I don't like this. Do you know if that will cause files to not be deleted from the system when needed?
apaderno avatar
us flag
`hook_file_download()` only receives a URL, not the headers returned by other implementations; if this were the case, a hook implementation could override the decision of another implementation. The actual code invoking them just checks one of them returned -1 as header; it doesn't check -1 is the first returned value.
apaderno avatar
us flag
@quantumized Permanent files will not be removed during the file garbage collection process, but a module could delete them. If the module keeps a list of file it changed to permanent, that could be possible.
apaderno avatar
us flag
I also added a note about what happens when a file usage is increased: The Drupal core implementation of the *file.usage* service automatically makes a temporary file permanent.
mangohost

Post an answer

Most people don’t grasp that asking a lot of questions unlocks learning and improves interpersonal bonding. In Alison’s studies, for example, though people could accurately recall how many questions had been asked in their conversations, they didn’t intuit the link between questions and liking. Across four studies, in which participants were engaged in conversations themselves or read transcripts of others’ conversations, people tended not to realize that question asking would influence—or had influenced—the level of amity between the conversationalists.