diff --git a/core/modules/config/config.module b/core/modules/config/config.module index 59498ce..c4189e0 100644 --- a/core/modules/config/config.module +++ b/core/modules/config/config.module @@ -48,10 +48,9 @@ function config_permission() { } /** - * Implements hook_file_download_headers(). + * Implements hook_unmanaged_file_download_headers(). */ -function config_file_download_headers(FileInterface $file) { - $uri = $file->getFileUri(); +function config_unmanaged_file_download_headers($uri) { $scheme = file_uri_scheme($uri); $target = file_uri_target($uri); if ($scheme == 'temporary' && $target == 'config.tar.gz') { diff --git a/core/modules/file/file.module b/core/modules/file/file.module index 96d9f48..0ec2388 100644 --- a/core/modules/file/file.module +++ b/core/modules/file/file.module @@ -549,26 +549,6 @@ function file_save_data($data, $destination = NULL, $replace = FILE_EXISTS_RENAM } /** - * Examines a file entity and returns appropriate content headers for download. - * - * @param \Drupal\file\FileInterface $file - * A file entity. - * - * @return - * An associative array of headers, as expected by - * \Symfony\Component\HttpFoundation\StreamedResponse. - */ -function file_get_content_headers(FileInterface $file) { - $type = mime_header_encode($file->getMimeType()); - - return array( - 'Content-Type' => $type, - 'Content-Length' => $file->getSize(), - 'Cache-Control' => 'private', - ); -} - -/** * Implements hook_theme(). */ function file_theme() { @@ -603,25 +583,6 @@ function file_theme() { } /** - * Implements hook_file_download_headers(). - */ -function file_file_download_headers(FileInterface $file) { - // Find out which (if any) fields of this type contain the file. - $references = file_get_file_references($file, NULL, EntityStorageInterface::FIELD_LOAD_CURRENT, NULL); - - // Stop processing if there are no references in order to avoid returning - // headers for files controlled by other modules. Make an exception for - // temporary files where the host entity has not yet been saved (for example, - // an image preview on a node/add form) in which case, allow download by the - // file's owner. - if (empty($references) && ($file->isPermanent() || $file->getOwnerId() != \Drupal::currentUser()->id())) { - return; - } - - return file_get_content_headers($file); -} - -/** * Implements file_cron() */ function file_cron() { diff --git a/core/modules/file/src/Entity/File.php b/core/modules/file/src/Entity/File.php index 59a5421..9a7d015 100644 --- a/core/modules/file/src/Entity/File.php +++ b/core/modules/file/src/Entity/File.php @@ -7,6 +7,7 @@ namespace Drupal\file\Entity; +use Drupal\Component\Utility\Unicode; use Drupal\Core\Entity\ContentEntityBase; use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Entity\EntityTypeInterface; @@ -127,6 +128,19 @@ public function getChangedTime() { /** * {@inheritdoc} */ + public function getContentHeaders() { + $type = Unicode::mimeHeaderEncode($this->getMimeType()); + + return array( + 'Content-Type' => $type, + 'Content-Length' => $this->getSize(), + 'Cache-Control' => 'private', + ); + } + + /** + * {@inheritdoc} + */ public function getOwner() { return $this->get('uid')->entity; } diff --git a/core/modules/file/src/FileAccessController.php b/core/modules/file/src/FileAccessController.php index 08972ab..7782589 100644 --- a/core/modules/file/src/FileAccessController.php +++ b/core/modules/file/src/FileAccessController.php @@ -23,6 +23,13 @@ class FileAccessController extends EntityAccessController { protected function checkAccess(EntityInterface $entity, $operation, $langcode, AccountInterface $account) { if ($operation == 'download') { + + // Allow access to non permanent files only to the file owner. I.e. to the + // preview of a just uploaded image. + if (!$entity->isPermanent() && $entity->getOwnerId() != $account->id()) { + return FALSE; + } + foreach ($this->getFileReferences($entity) as $field_name => $entity_map) { foreach ($entity_map as $referencing_entity_type => $referencing_entities) { /** @var \Drupal\Core\Entity\EntityInterface $referencing_entity */ diff --git a/core/modules/file/src/FileInterface.php b/core/modules/file/src/FileInterface.php index be94aaa..d4b956d 100644 --- a/core/modules/file/src/FileInterface.php +++ b/core/modules/file/src/FileInterface.php @@ -119,4 +119,13 @@ public function setTemporary(); * Creation timestamp of the node. */ public function getCreatedTime(); + + /** + * Returns the file headers for download. + * + * @return array + * An associative array of headers, as expected by + * \Symfony\Component\HttpFoundation\StreamedResponse. + */ + public function getContentHeaders(); } diff --git a/core/modules/file/tests/file_test/file_test.module b/core/modules/file/tests/file_test/file_test.module index 4bbdfe0..32a1626 100644 --- a/core/modules/file/tests/file_test/file_test.module +++ b/core/modules/file/tests/file_test/file_test.module @@ -9,7 +9,7 @@ */ use Drupal\file\Entity\File; -use Drupal\file\Entity\FileInterface; +use Drupal\file\FileInterface; const FILE_URL_TEST_CDN_1 = 'http://cdn1.example.com'; const FILE_URL_TEST_CDN_2 = 'http://cdn2.example.com'; @@ -171,10 +171,10 @@ function file_test_file_validate(File $file) { } /** - * Implements hook_file_download_headers(). + * Implements hook_unmanaged_file_download_headers(). */ -function file_test_file_download_headers(FileInterface $file) { - _file_test_log_call('download', array($file->getFileUri())); +function file_test_unmanaged_file_download_headers($uri) { + _file_test_log_call('download', array($uri)); return _file_test_get_return('download'); } diff --git a/core/modules/image/image.module b/core/modules/image/image.module index 1e19be7..83681c7 100644 --- a/core/modules/image/image.module +++ b/core/modules/image/image.module @@ -8,6 +8,7 @@ use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\file\Entity\File; +use Drupal\file\FileInterface; use Drupal\field\FieldStorageConfigInterface; use Drupal\field\FieldInstanceConfigInterface; @@ -169,26 +170,6 @@ function image_permission() { } /** - * Implements hook_file_download_headers(). - */ -function image_file_download_headers(FileInterface $file) { - $uri = $file->getFileUri(); - - // Check that the file exists and is an image. - $image = \Drupal::service('image.factory')->get($uri); - if ($image->isValid() && $file->access('download')) { - return array( - // 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. - ); - } -} - -/** * Implements hook_file_move(). */ function image_file_move(File $file, File $source) { diff --git a/core/modules/image/src/Controller/ImageStyleDownloadController.php b/core/modules/image/src/Controller/ImageStyleDownloadController.php index 98b9195..77b4d2a 100644 --- a/core/modules/image/src/Controller/ImageStyleDownloadController.php +++ b/core/modules/image/src/Controller/ImageStyleDownloadController.php @@ -10,6 +10,7 @@ use Drupal\Component\Utility\Crypt; use Drupal\Core\Image\ImageFactory; use Drupal\Core\Lock\LockBackendInterface; +use Drupal\file\Entity\File; use Drupal\image\ImageStyleInterface; use Drupal\system\FileDownloadController; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -17,6 +18,7 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException; /** @@ -108,18 +110,12 @@ public function deliver(Request $request, $scheme, ImageStyleInterface $image_st if (file_exists($derivative_uri)) { return parent::download($request, $scheme); } - else { - $files = $this->entityManager() - ->getStorage('file') - ->loadByProperties(array('uri' => $image_uri)); - $image = reset($files); - debug($image->access('download')); - if ($image && $image->access('download')) { - $headers = $this->moduleHandler()->invokeAll('file_download_headers', array($image)); - } - else { - throw new AccessDeniedHttpException(); - } + + $headers = $this->getContentHeaders($image_uri); + // If we failed to get valid headers it means the access has not been + // granted. + if (empty($headers)) { + throw new AccessDeniedHttpException(); } } diff --git a/core/modules/image/tests/modules/image_module_test/image_module_test.module b/core/modules/image/tests/modules/image_module_test/image_module_test.module index 0d4dad5..4ca0431 100644 --- a/core/modules/image/tests/modules/image_module_test/image_module_test.module +++ b/core/modules/image/tests/modules/image_module_test/image_module_test.module @@ -5,14 +5,12 @@ * Provides Image module hook implementations for testing purposes. */ -use \Drupal\file\FileInterface; - /** * Implements hook_file_download_headers(). */ -function image_module_test_file_download_headers(FileInterface $file) { - $default_uri = \Drupal::state()->get('image.test_file_download') ?: FALSE; - if ($default_uri == $file->getFileUri()) { +function image_module_test_unmanaged_file_download_headers($uri) { + $default_uri = \Drupal::state()->get('image.test_file_download') ? : FALSE; + if ($default_uri == $uri) { return array('X-Image-Owned-By' => 'image_module_test'); } } diff --git a/core/modules/responsive_image/src/Tests/ResponsiveImageFieldDisplayTest.php b/core/modules/responsive_image/src/Tests/ResponsiveImageFieldDisplayTest.php index d6a09bc..8da867e 100644 --- a/core/modules/responsive_image/src/Tests/ResponsiveImageFieldDisplayTest.php +++ b/core/modules/responsive_image/src/Tests/ResponsiveImageFieldDisplayTest.php @@ -112,6 +112,7 @@ public function _testResponsiveImageFieldFormatters($scheme) { $this->createImageField($field_name, 'article', array('uri_scheme' => $scheme)); // Create a new node with an image attached. $test_image = current($this->drupalGetTestFiles('image')); + debug($test_image); $nid = $this->uploadNodeImage($test_image, $field_name, 'article'); $node = node_load($nid, TRUE); diff --git a/core/modules/system/src/FileDownloadController.php b/core/modules/system/src/FileDownloadController.php index 09577c5..f1a7e6d 100644 --- a/core/modules/system/src/FileDownloadController.php +++ b/core/modules/system/src/FileDownloadController.php @@ -8,6 +8,7 @@ namespace Drupal\system; use Drupal\Core\Controller\ControllerBase; +use Drupal\file\FileInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; @@ -49,30 +50,47 @@ public function download(Request $request, $scheme = 'private') { $uri = $scheme . '://' . $target; if (file_stream_wrapper_valid_scheme($scheme) && file_exists($uri)) { - $files = $this->entityManager() - ->getStorage('file') - ->loadByProperties(array('uri' => $uri)); - $file = reset($files); - if ($file && $file->access('download')) { - // Let other modules provide headers and controls access to the file. - $headers = $this->moduleHandler()->invokeAll('file_download_headers', array($file)); + if ($headers = $this->getContentHeaders($uri)) { + return new BinaryFileResponse($uri, 200, $headers); + } - foreach ($headers as $result) { - if ($result == -1) { - throw new AccessDeniedHttpException(); - } - } + throw new AccessDeniedHttpException(); + } - if (count($headers)) { - return new BinaryFileResponse($uri, 200, $headers); - } + throw new NotFoundHttpException(); + } + + /** + * Gets headers for the provided file uri. + * + * It first tries to load managed file based on the uri, if not it will invoke + * the hook_unmanaged_file_download_headers(). + * + * @param string $file_uri + * The file uri for which to get the headers. + * + * @return array|NULL + * If access to the file is granted an associative array of headers as + * expected by \Symfony\Component\HttpFoundation\StreamedResponse, NULL + * otherwise. + */ + protected function getContentHeaders($file_uri) { + if ($this->moduleHandler()->moduleExists('file')) { + $files = $this->entityManager() + ->getStorage('file') + ->loadByProperties(array('uri' => $file_uri)); + /** @var \Drupal\file\FileInterface $file */ + $file = reset($files); - throw new AccessDeniedHttpException(); + if (!empty($file) && $file->access('download')) { + return $file->getContentHeaders(); } } - throw new NotFoundHttpException(); + if (empty($headers)) { + return $this->moduleHandler()->invokeAll('unmanaged_file_download_headers', array($file_uri)); + } } } diff --git a/core/modules/system/system.api.php b/core/modules/system/system.api.php index 218608b..69d7488 100644 --- a/core/modules/system/system.api.php +++ b/core/modules/system/system.api.php @@ -1497,21 +1497,20 @@ function hook_stream_wrappers_alter(&$wrappers) { } /** - * Specify HTTP headers for a file's download. + * Specify HTTP headers for a unmanaged file's download. * * Modules can provide headers to specify information like the file's name or * MIME type. * - * @param \Drupal\file\FileInterface $file - * The file being downloaded. + * @param string $uri + * The file uri being downloaded. * @return array|null * An array with the appropriate headers. If the file is not controlled by the * current module, the return value should be NULL. * - * @see file_download() + * @see FileDownloadController::download() */ -function hook_file_download_headers(\Drupal\file\FileInterface $file) { - $uri = $file->getFileUri(); +function hook_unmanaged_file_download_headers($uri) { // Check to see if this is a config download. $scheme = file_uri_scheme($uri); $target = file_uri_target($uri);