interdiff impossible; taking evasive action reverted: --- b/core/modules/editor/editor.module +++ a/core/modules/editor/editor.module @@ -16,8 +16,6 @@ use Drupal\Core\Entity\EntityInterface; use Drupal\filter\FilterFormatInterface; use Drupal\filter\Plugin\FilterInterface; -use Drupal\Core\Entity\EntityStorageInterface; -use Drupal\file\FileInterface; /** * Implements hook_help(). @@ -456,132 +454,6 @@ } /** - * 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 = 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; - } - - // Temporary files are handled by file_file_download(), so nothing to do here - // about them. - - // Find out which (if any) fields of this type contain the file. - $references = editor_get_file_references($file, EntityStorageInterface::FIELD_LOAD_CURRENT); - - // 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; - } - - // 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() again. This latter one would allow - // downloading files only handled by the file.module, which is exactly not the - // case right here. -// if (!($return = $file->access('download', NULL, TRUE))) { -// return -1; -// } - - // Access is granted. - $headers = file_get_content_headers($file); - return $headers; -} - -/** - * Retrieves a list of references to a file uploaded via EditorImageDialog. - * - * @param \Drupal\file\FileInterface $file - * A file entity. - * @param int $age - * (optional) A constant that specifies which references to count. Use - * EntityStorageInterface::FIELD_LOAD_REVISION (the default) to retrieve all - * references within all revisions or - * EntityStorageInterface::FIELD_LOAD_CURRENT to retrieve references only in - * the current revisions of all entities that have references to this file. - * - * @return array - * A multidimensional array. The keys are field_name, entity_type, - * entity_id and the value is an entity referencing this file. - * - * @ingroup file - * @see file_get_file_references() - */ -function editor_get_file_references(FileInterface $file, $age = EntityStorageInterface::FIELD_LOAD_REVISION) { - $references = &drupal_static(__FUNCTION__, array()); - $field_columns = &drupal_static(__FUNCTION__ . ':field_columns', array()); - - // Fill the static cache, disregard $field and $field_type for now. - if (!isset($references[$file->id()][$age])) { - $references[$file->id()][$age] = array(); - $usage_list = \Drupal::service('file.usage')->listUsage($file); - $file_usage_list = isset($usage_list['editor']) ? $usage_list['editor'] : array(); - foreach ($file_usage_list as $entity_type_id => $entity_ids) { - $entities = entity_load_multiple($entity_type_id, array_keys($entity_ids)); - /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */ - foreach ($entities as $entity) { - $bundle = $entity->bundle(); - - // We need to find editor fields for this entity type and bundle. - if (!isset($editor_fields[$entity_type_id][$bundle])) { - $editor_fields[$entity_type_id][$bundle] = _editor_get_formatted_text_fields($entity); - } - foreach ($editor_fields[$entity_type_id][$bundle] as $field_name) { - // Iterate over the field items to find the referenced file and field - // name. This will fail if the usage checked is in a non-current - // revision because field items are from the current - // revision. - // We also iterate over all translations because a file can be linked - // to a language other than the default. - foreach ($entity->getTranslationLanguages() as $langcode => $language) { - /** @var \Drupal\text\Plugin\Field\FieldType\TextItemBase $item */ - foreach ($entity->getTranslation($langcode)->get($field_name) as $item) { - // Check if the file is referenced in either - // $item->values['value'] or $item->values['summary'] (if the - // latter exists at all). - $value = $item->getValue(); - foreach (['summary', 'value'] as $subfield) { - if (isset($value[$subfield])) { - $xpath = new \DOMXPath(Html::load($value['value'])); - /** @var DOMNodeList $query */ - $query = $xpath->query('//*[@data-entity-type="file" and @data-entity-uuid="' . $file->uuid() . '"]'); - if ($query->length) { - $references[$file->id()][$age][$field_name][$entity_type_id][$entity->id()] = $entity; - break; - } - } - } - } - } - } - } - } - } - return $references[$file->id()][$age]; -} - -/** * Finds all files referenced (data-entity-uuid) by formatted text fields. * * @param EntityInterface $entity unchanged: --- a/core/modules/editor/editor.module +++ b/core/modules/editor/editor.module @@ -16,6 +16,8 @@ use Drupal\Core\Entity\EntityInterface; use Drupal\filter\FilterFormatInterface; use Drupal\filter\Plugin\FilterInterface; +use Drupal\Core\Entity\EntityStorageInterface; +use Drupal\file\FileInterface; /** * Implements hook_help(). @@ -468,6 +470,131 @@ 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 = 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; + } + + // Temporary files are handled by file_file_download(), so nothing to do here + // about them. + + // Find out which (if any) fields of this type contain the file. + $references = editor_get_file_references($file, EntityStorageInterface::FIELD_LOAD_CURRENT); + + // 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; + } + + // 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() again. This latter one would allow + // downloading files only handled by the file.module, which is exactly not the + // case right here. +// if (!($return = $file->access('download', NULL, TRUE))) { +// return -1; +// } + + // Access is granted. + $headers = file_get_content_headers($file); + return $headers; +} + +/** + * Retrieves a list of references to a file uploaded via EditorImageDialog. + * + * @param \Drupal\file\FileInterface $file + * A file entity. + * @param int $age + * (optional) A constant that specifies which references to count. Use + * EntityStorageInterface::FIELD_LOAD_REVISION (the default) to retrieve all + * references within all revisions or + * EntityStorageInterface::FIELD_LOAD_CURRENT to retrieve references only in + * the current revisions of all entities that have references to this file. + * + * @return array + * A multidimensional array. The keys are field_name, entity_type, + * entity_id and the value is an entity referencing this file. + * + * @ingroup file + * @see file_get_file_references() + */ +function editor_get_file_references(FileInterface $file, $age = EntityStorageInterface::FIELD_LOAD_REVISION) { + $references = &drupal_static(__FUNCTION__, array()); + + // Fill the static cache, disregard $field and $field_type for now. + if (!isset($references[$file->id()][$age])) { + $references[$file->id()][$age] = array(); + $usage_list = \Drupal::service('file.usage')->listUsage($file); + $file_usage_list = isset($usage_list['editor']) ? $usage_list['editor'] : array(); + foreach ($file_usage_list as $entity_type_id => $entity_ids) { + $entities = entity_load_multiple($entity_type_id, array_keys($entity_ids)); + /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */ + foreach ($entities as $entity) { + $bundle = $entity->bundle(); + + // We need to find editor fields for this entity type and bundle. + if (!isset($editor_fields[$entity_type_id][$bundle])) { + $editor_fields[$entity_type_id][$bundle] = _editor_get_formatted_text_fields($entity); + } + foreach ($editor_fields[$entity_type_id][$bundle] as $field_name) { + // Iterate over the field items to find the referenced file and field + // name. This will fail if the usage checked is in a non-current + // revision because field items are from the current + // revision. + // We also iterate over all translations because a file can be linked + // to a language other than the default. + foreach ($entity->getTranslationLanguages() as $langcode => $language) { + /** @var \Drupal\text\Plugin\Field\FieldType\TextItemBase $item */ + foreach ($entity->getTranslation($langcode)->get($field_name) as $item) { + // Check if the file is referenced in either + // $item->values['value'] or $item->values['summary'] (if the + // latter exists at all). + $value = $item->getValue(); + foreach (['summary', 'value'] as $subfield) { + if (isset($value[$subfield])) { + $xpath = new \DOMXPath(Html::load($value['value'])); + /** @var DOMNodeList $query */ + $query = $xpath->query('//*[@data-entity-type="file" and @data-entity-uuid="' . $file->uuid() . '"]'); + if ($query->length) { + $references[$file->id()][$age][$field_name][$entity_type_id][$entity->id()] = $entity; + break; + } + } + } + } + } + } + } + } + } + return $references[$file->id()][$age]; +} + +/** * Finds all files referenced (data-entity-uuid) by formatted text fields. * * @param EntityInterface $entity only in patch2: unchanged: --- /dev/null +++ b/core/modules/editor/src/Tests/EditorPrivateFileReferenceFilterTest.php @@ -0,0 +1,70 @@ +drupalCreateContentType(array('type' => 'page', 'name' => 'Basic page')); + + // Create a file in the 'private:// ' stream. + $filename = 'test.png'; + $src = '/system/files/' . $filename; + /** @var FileInterface $file */ + $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.'); + } + +} only in patch2: unchanged: --- /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 only in patch2: unchanged: --- /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 only in patch2: unchanged: --- /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