diff --git a/core/modules/config/config.module b/core/modules/config/config.module index 84e0210..e5c9943 100644 --- a/core/modules/config/config.module +++ b/core/modules/config/config.module @@ -28,9 +28,9 @@ function config_help($route_name, RouteMatchInterface $route_match) { } /** - * Implements hook_file_download(). + * Implements hook_unmanaged_file_download_headers(). */ -function config_file_download($uri) { +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 0bc0584..9b27927 100644 --- a/core/modules/file/file.module +++ b/core/modules/file/file.module @@ -523,26 +523,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 = Unicode::mimeHeaderEncode($file->getMimeType()); - - return array( - 'Content-Type' => $type, - 'Content-Length' => $file->getSize(), - 'Cache-Control' => 'private', - ); -} - -/** * Implements hook_theme(). */ function file_theme() { @@ -572,48 +552,6 @@ function file_theme() { } /** - * Implements hook_file_download(). - */ -function file_file_download($uri) { - // Get the file record based on the URI. If not in the database just return. - /** @var \Drupal\file\FileInterface[] $files */ - $files = entity_load_multiple_by_properties('file', array('uri' => $uri)); - if (count($files)) { - foreach ($files as $item) { - // Since some database servers sometimes use a case-insensitive comparison - // by default, double check that the filename is an exact match. - if ($item->getFileUri() === $uri) { - $file = $item; - break; - } - } - } - if (!isset($file)) { - return; - } - - // 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; - } - - if (!$file->access('download')) { - return -1; - } - - // Access is granted. - $headers = file_get_content_headers($file); - return $headers; -} - -/** * 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 2dbbf31..d28c41e 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; @@ -119,6 +120,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/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/src/Tests/DownloadTest.php b/core/modules/file/src/Tests/DownloadTest.php index 9de1b3a..9290573 100644 --- a/core/modules/file/src/Tests/DownloadTest.php +++ b/core/modules/file/src/Tests/DownloadTest.php @@ -85,10 +85,10 @@ protected function doPrivateFileTransferTest() { // Test that the file transferred correctly. $this->assertEqual($contents, $this->content, 'Contents of the file are correct.'); - // Deny access to all downloads via a -1 header. - file_test_set_return('download', -1); + // Deny access to all downloads without header. + file_test_set_return('download', NULL); $this->drupalHead($url); - $this->assertResponse(403, 'Correctly denied access to a file when file_test sets the header to -1.'); + $this->assertResponse(403, 'Correctly denied access to a file when no headers are sent.'); // Try non-existent file. $url = file_create_url('private://' . $this->randomMachineName()); diff --git a/core/modules/file/tests/file_test/file_test.module b/core/modules/file/tests/file_test/file_test.module index 2c0b21e..284cc9c 100644 --- a/core/modules/file/tests/file_test/file_test.module +++ b/core/modules/file/tests/file_test/file_test.module @@ -147,9 +147,9 @@ function file_test_file_validate(File $file) { } /** - * Implements hook_file_download(). + * Implements hook_unmanaged_file_download_headers(). */ -function file_test_file_download($uri) { +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 0418c72..a030c33 100644 --- a/core/modules/image/image.module +++ b/core/modules/image/image.module @@ -152,45 +152,6 @@ function image_theme() { } /** - * Implements hook_file_download(). - * - * Control the access to files underneath the styles directory. - */ -function image_file_download($uri) { - $path = file_uri_target($uri); - - // Private file access for image style derivatives. - if (strpos($path, 'styles/') === 0) { - $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 = file_uri_scheme($uri) . '://' . implode('/', $args); - - // Check that the file exists and is an image. - $image = \Drupal::service('image.factory')->get($uri); - if ($image->isValid()) { - // Check the permissions of the original to grant access to this image. - $headers = \Drupal::moduleHandler()->invokeAll('file_download', array($original_uri)); - // Confirm there's at least one module granting access and none denying access. - if (!empty($headers) && !in_array(-1, $headers)) { - 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. - ); - } - } - return -1; - } -} - -/** * 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 2b2d89f..59494ee 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 Psr\Log\LoggerInterface; @@ -18,6 +19,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; /** @@ -120,11 +122,12 @@ public function deliver(Request $request, $scheme, ImageStyleInterface $image_st if (file_exists($derivative_uri)) { return parent::download($request, $scheme); } - else { - $headers = $this->moduleHandler()->invokeAll('file_download', array($image_uri)); - if (in_array(-1, $headers) || empty($headers)) { - 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 df71375..d42945e 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 @@ -7,8 +7,11 @@ use Drupal\image\ImageStyleInterface; -function image_module_test_file_download($uri) { - $default_uri = \Drupal::state()->get('image.test_file_download') ?: FALSE; +/** + * Implements hook_file_download_headers(). + */ +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/system/src/FileDownloadController.php b/core/modules/system/src/FileDownloadController.php index a65342d..e1357c5 100644 --- a/core/modules/system/src/FileDownloadController.php +++ b/core/modules/system/src/FileDownloadController.php @@ -49,16 +49,7 @@ public function download(Request $request, $scheme = 'private') { $uri = $scheme . '://' . $target; if (file_stream_wrapper_valid_scheme($scheme) && file_exists($uri)) { - // Let other modules provide headers and controls access to the file. - $headers = $this->moduleHandler()->invokeAll('file_download', array($uri)); - - foreach ($headers as $result) { - if ($result == -1) { - throw new AccessDeniedHttpException(); - } - } - - if (count($headers)) { + if ($headers = $this->getContentHeaders($uri)) { return new BinaryFileResponse($uri, 200, $headers); } @@ -68,4 +59,36 @@ public function download(Request $request, $scheme = 'private') { 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); + + if (!empty($file) && $file->access('download')) { + return $file->getContentHeaders(); + } + } + + if (empty($headers)) { + return $this->moduleHandler()->invokeAll('unmanaged_file_download_headers', array($file_uri)); + } + } + }