diff --git a/core/modules/editor/editor.module b/core/modules/editor/editor.module index 2a052dce29bdb9532d09cfc0fda6c0352ead17a5..851bdbb679a76d7a2c7458ca2c4cf38d2b5caedc 100644 --- a/core/modules/editor/editor.module +++ b/core/modules/editor/editor.module @@ -453,6 +453,60 @@ function _editor_delete_file_usage(array $uuids, EntityInterface $entity, $count } } +/** + * Implements hook_file_download(). + * + * @see file_file_download() + * @see file_get_file_references() + */ +function editor_file_download($uri) { + // Get the file record based on the URI. If not in the database just return. + /** @var \Drupal\file\FileInterface[] $files */ + $files = \Drupal::entityTypeManager() + ->getStorage('file') + ->loadByProperties(['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; + } + + // Temporary files are handled by file_file_download(), so nothing to do here + // about them. + // @see file_file_download() + + // Find out if any editor-backed field contains the file. + $usage_list = \Drupal::service('file.usage')->listUsage($file); + + // 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 creation form) in which case, allow download by + // the file's owner. + if (empty($usage_list['editor']) && ($file->isPermanent() || $file->getOwnerId() != \Drupal::currentUser()->id())) { + return; + } + + // Editor.module MUST NOT call $file->access() here (like file_file_download() + // does) as checking the 'download' access to a file entity would end up in + // FileAccessControlHandler->checkAccess() and ->getFileReferences(), which + // calls file_get_file_references(). This latter one would allow downloading + // files only handled by the file.module, which is exactly not the case right + // here. + + // Access is granted. + $headers = file_get_content_headers($file); + return $headers; +} + /** * Finds all files referenced (data-entity-uuid) by formatted text fields. * diff --git a/core/modules/editor/src/Tests/EditorPrivateFileReferenceFilterTest.php b/core/modules/editor/src/Tests/EditorPrivateFileReferenceFilterTest.php new file mode 100644 index 0000000000000000000000000000000000000000..b5ff6db0306b7acf7bebb321e0c874ed0b9df09a --- /dev/null +++ b/core/modules/editor/src/Tests/EditorPrivateFileReferenceFilterTest.php @@ -0,0 +1,69 @@ +drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']); + + // Create a file in the 'private:// ' stream. + $filename = 'test.png'; + $src = '/system/files/' . $filename; + + $file = File::create([ + 'uri' => 'private://' . $filename, + 'status' => FILE_STATUS_PERMANENT, + ]); + // Create the file itself. + file_put_contents($file->getFileUri(), $this->randomString()); + $file->save(); + + // Create a node with its body field properly pointing to the just-created + // file. + $node = $this->drupalCreateNode([ + 'type' => 'page', + 'body' => [ + 'value' => 'alt', + 'format' => 'private_images', + ], + ]); + $this->drupalGet('/node/' . $node->id()); + + // Do the actual test. The image should be visible for anonymous. + $this->drupalGet($src); + $this->assertResponse(200, 'Image is downloadable as anonymous.'); + } + +} diff --git a/core/modules/editor/tests/editor_private_test/config/install/editor.editor.private_images.yml b/core/modules/editor/tests/editor_private_test/config/install/editor.editor.private_images.yml new file mode 100644 index 0000000000000000000000000000000000000000..2de126a1cea6c65955d97175fe71a24b35bc6816 --- /dev/null +++ b/core/modules/editor/tests/editor_private_test/config/install/editor.editor.private_images.yml @@ -0,0 +1,34 @@ +format: private_images +status: true +langcode: en +editor: ckeditor +settings: + toolbar: + rows: + - + - + name: Media + items: + - DrupalImage + - + name: Tools + items: + - Source + plugins: + language: + language_list: un + stylescombo: + styles: '' +image_upload: + status: true + scheme: private + directory: '' + max_size: '' + max_dimensions: + width: null + height: null +dependencies: + config: + - filter.format.private_images + module: + - ckeditor diff --git a/core/modules/editor/tests/editor_private_test/config/install/filter.format.private_images.yml b/core/modules/editor/tests/editor_private_test/config/install/filter.format.private_images.yml new file mode 100644 index 0000000000000000000000000000000000000000..261bd901797a40eb411add76527f1af64b4351fb --- /dev/null +++ b/core/modules/editor/tests/editor_private_test/config/install/filter.format.private_images.yml @@ -0,0 +1,23 @@ +format: private_images +name: 'Private images' +status: true +langcode: en +filters: + editor_file_reference: + id: editor_file_reference + provider: editor + status: true + weight: 0 + settings: { } + filter_html: + id: filter_html + provider: filter + status: false + weight: -10 + settings: + allowed_html: '' + filter_html_help: true + filter_html_nofollow: false +dependencies: + module: + - editor diff --git a/core/modules/editor/tests/editor_private_test/editor_private_test.info.yml b/core/modules/editor/tests/editor_private_test/editor_private_test.info.yml new file mode 100644 index 0000000000000000000000000000000000000000..474fda67228be1f6ca278900e7468981e789a579 --- /dev/null +++ b/core/modules/editor/tests/editor_private_test/editor_private_test.info.yml @@ -0,0 +1,9 @@ +name: 'Text Editor Private test' +type: module +description: 'Support module for the Text Editor Private module tests.' +core: 8.x +package: Testing +version: VERSION +dependencies: + - filter + - ckeditor