diff --git a/core/modules/editor/editor.module b/core/modules/editor/editor.module index d6fa16c5c00f32c21fd317018049cc6c124ec637..010177974405a17f28308318969acce97c570b56 100644 --- a/core/modules/editor/editor.module +++ b/core/modules/editor/editor.module @@ -467,6 +467,78 @@ 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. So instead we must check if the current user is allowed to view any + // of the entities that reference the image using the 'editor' module. + if ($file->isPermanent()) { + $referencing_entity_is_accessible = FALSE; + $references = empty($usage_list['editor']) ? [] : $usage_list['editor']; + foreach ($references as $entity_type => $entity_ids) { + $referencing_entities = entity_load_multiple($entity_type, $entity_ids); + /** @var \Drupal\Core\Entity\EntityInterface $referencing_entity */ + foreach ($referencing_entities as $referencing_entity) { + if ($referencing_entity->access('view', NULL, TRUE)->isAllowed()) { + $referencing_entity_is_accessible = TRUE; + break 2; + } + } + } + if (!$referencing_entity_is_accessible) { + return -1; + } + } + + // 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..5f689b3f0d1759d9023efebc76481adc3d0b908c --- /dev/null +++ b/core/modules/editor/src/Tests/EditorPrivateFileReferenceFilterTest.php @@ -0,0 +1,100 @@ +drupalCreateUser(); + $this->drupalLogin($author); + + // Create a content type with a body field. + $this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']); + + // Create a file in the 'private:// ' stream. + $filename = 'test.png'; + $src = '/system/files/' . $filename; + /** @var \Drupal\file\FileInterface $file */ + $file = File::create([ + 'uri' => 'private://' . $filename, + ]); + $file->setTemporary(); + $file->setOwner($author); + // Create the file itself. + file_put_contents($file->getFileUri(), $this->randomString()); + $file->save(); + + // The image should be visible for its author. + $this->drupalGet($src); + $this->assertSession()->statusCodeEquals(200); + // The not-yet-permanent image should NOT be visible for anonymous. + $this->drupalLogout(); + $this->drupalGet($src); + $this->assertSession()->statusCodeEquals(403); + + // Resave the file to be permanent. + $file->setPermanent(); + $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', + ], + 'uid' => $author->id(), + ]); + + // Do the actual test. The image should be visible for anonymous users, + // because they can view the referencing entity. + $this->drupalGet($node->toUrl()); + $this->assertSession()->statusCodeEquals(200); + $this->drupalGet($src); + $this->assertSession()->statusCodeEquals(200); + + // Disallow anonymous users to view the entity, which then should also + // disallow them to view the image. + Role::load(RoleInterface::ANONYMOUS_ID) + ->revokePermission('access content') + ->save(); + $this->drupalGet($node->toUrl()); + $this->assertSession()->statusCodeEquals(403); + $this->drupalGet($src); + $this->assertSession()->statusCodeEquals(403); + } + +} 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