diff --git a/core/modules/media/config/schema/media.schema.yml b/core/modules/media/config/schema/media.schema.yml index 14d40a9065..69740f75ee 100644 --- a/core/modules/media/config/schema/media.schema.yml +++ b/core/modules/media/config/schema/media.schema.yml @@ -44,6 +44,10 @@ media.source.*: type: mapping label: 'Media source settings' +media.source.file: + type: media.source.field_aware + label: '"File" media source configuration' + media.source.field_aware: type: mapping mapping: diff --git a/core/modules/media/images/icons/generic.png b/core/modules/media/images/icons/generic.png index 2050a78a4f..13090d8d0d 100644 --- a/core/modules/media/images/icons/generic.png +++ b/core/modules/media/images/icons/generic.png @@ -1,3 +1,4 @@ -PNG +PNG  - IHDRYfIDATx@@}7J0@YwwwwwwU9fM/, 4hРA4hРA 4hРA 4hРA Z,+h]~˽YX/6@G5$:deDT4z 󦤇Eh; mj` _;> ,E]%e{EH\$Хb]Zt77؇թEs3d K-Z jkULR1ךgoٱ}]SB(>N-mwRA.=!sm4O"Dه=uckw̕ӎI--BtChР5Nvze]{JCo6Og$>tq.ޔ43b\h4hР[)In :5O,s.$z z ]k6t6d35Z7~zӖ]Klо< !A 4hРA 4hРA 4hРA 4M+FEIENDB` \ No newline at end of file + IHDRYdIDATx0@ޠ(333333,[D߉y$hР 4hI4hРA 4hРA 4hРAkۀ}6 ņ訆Dllр6\C!{ޒvHmG5AQ Q_.4" +3Y3׋20zBf֐i"3s]Γ@*⋹vi"h>N-z\[%eY*hi=7bM`7[\GD s}v۷ou=%RAђ{'|ˣD{*44hРAVoّ#]к<-kAjMuˣX./~4hйFVg*W+VͷV}}ۧ+2Ft*B}8Ճ[7v\gOL9I|hQo# ZjWE+ٻUzy:#񡋋/̤#DРANI4Et4hOǀˀ-}keks!#Ѡ7ȵǎ^n#;xk#8quW7#պ˓@` 4hРA 4hРA 4hРA 4hР7 +F*IENDB` \ No newline at end of file diff --git a/core/modules/media/src/Plugin/media/Source/File.php b/core/modules/media/src/Plugin/media/Source/File.php new file mode 100644 index 0000000000..b3ab119f63 --- /dev/null +++ b/core/modules/media/src/Plugin/media/Source/File.php @@ -0,0 +1,94 @@ +get($this->configuration['source_field'])->entity; + // If the source field is not required, it may be empty. + if (!$file) { + return parent::getMetadata($media, $attribute_name); + } + switch ($attribute_name) { + case 'default_name': + return $file->getFilename(); + + case 'thumbnail_uri': + return $this->getThumbnail($file) ?: parent::getMetadata($media, $attribute_name); + + default: + return parent::getMetadata($media, $attribute_name); + } + } + + /** + * Gets the thumbnail image URI based on a file entity. + * + * @param \Drupal\file\FileInterface $file + * A file entity. + * + * @return string + * File URI of the thumbnail image or NULL if there is no specific icon. + */ + protected function getThumbnail(FileInterface $file) { + $icon_base = $this->configFactory->get('media.settings')->get('icon_base_uri'); + + // We try to automatically use the most specific icon present in the + // $icon_base directory, based on the MIME type. For instance, if an + // icon file named "pdf.png" is present, it will be used if the file + // matches this MIME type. + $mimetype = $file->getMimeType(); + $mimetype = explode('/', $mimetype); + + $icon_names = [ + $mimetype[0] . '--' . $mimetype[1], + $mimetype[1], + $mimetype[0], + ]; + foreach ($icon_names as $icon_name) { + $thumbnail = $icon_base . '/' . $icon_name . '.png'; + if (is_file($thumbnail)) { + return $thumbnail; + } + } + + return NULL; + } + + /** + * {@inheritdoc} + */ + public function createSourceField(MediaTypeInterface $type) { + return parent::createSourceField($type)->set('settings', ['file_extensions' => 'txt doc docx pdf']); + } + +} diff --git a/core/modules/media/tests/src/FunctionalJavascript/MediaSourceFileTest.php b/core/modules/media/tests/src/FunctionalJavascript/MediaSourceFileTest.php new file mode 100644 index 0000000000..7dd03efc87 --- /dev/null +++ b/core/modules/media/tests/src/FunctionalJavascript/MediaSourceFileTest.php @@ -0,0 +1,56 @@ +getSession(); + $page = $session->getPage(); + $assert_session = $this->assertSession(); + + $this->doTestCreateMediaType($media_type_id, 'file'); + + // Hide the name field widget to test default name generation. + $this->hideMediaTypeFieldWidget('name', $media_type_id); + + // Create a media item. + $this->drupalGet("media/add/{$media_type_id}"); + $page->attachFileToField("files[{$source_field_id}_0]", \Drupal::root() . '/sites/README.txt'); + $assert_session->assertWaitOnAjaxRequest(); + $page->pressButton('Save and publish'); + + $assert_session->addressEquals('media/1'); + + // Make sure the thumbnail is displayed. + $assert_session->elementAttributeContains('css', '.image-style-thumbnail', 'src', 'generic.png'); + + // Load the media and check if the label was properly populated. + $media = Media::load(1); + $this->assertEquals('README.txt', $media->label()); + + // Test the MIME type icon. + $icon_base = \Drupal::config('media.settings')->get('icon_base_uri'); + file_unmanaged_copy($icon_base . '/generic.png', $icon_base . '/text--plain.png'); + $this->drupalGet("media/add/{$media_type_id}"); + $page->attachFileToField("files[{$source_field_id}_0]", \Drupal::root() . '/sites/README.txt'); + $assert_session->assertWaitOnAjaxRequest(); + $page->pressButton('Save and publish'); + $assert_session->elementAttributeContains('css', '.image-style-thumbnail', 'src', 'text--plain.png'); + } + +} diff --git a/core/modules/media/tests/src/FunctionalJavascript/MediaSourceTestBase.php b/core/modules/media/tests/src/FunctionalJavascript/MediaSourceTestBase.php new file mode 100644 index 0000000000..5af6d5c143 --- /dev/null +++ b/core/modules/media/tests/src/FunctionalJavascript/MediaSourceTestBase.php @@ -0,0 +1,125 @@ + $field_name, + 'entity_type' => 'media', + 'type' => $field_type, + ]); + $storage->save(); + + FieldConfig::create([ + 'field_storage' => $storage, + 'bundle' => $media_type_id, + ])->save(); + + // Make the field widget visible in the form display. + $component = \Drupal::service('plugin.manager.field.widget') + ->prepareConfiguration($field_type, []); + /** @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface $entity_form_display */ + $entity_form_display = entity_get_form_display('media', $media_type_id, 'default'); + $entity_form_display->setComponent($field_name, $component) + ->save(); + } + + /** + * Create a set of fields in a media type. + * + * @param array $fields + * An associative array where keys are field names and values field types. + * @param string $media_type_id + * The media type config entity ID. + */ + protected function createMediaTypeFields(array $fields, $media_type_id) { + foreach ($fields as $field_name => $field_type) { + $this->createMediaTypeField($field_name, $field_type, $media_type_id); + } + } + + /** + * Hides a widget in the default form display config. + * + * @param string $field_name + * The field name. + * @param string $media_type_id + * The media type config entity ID. + */ + protected function hideMediaTypeFieldWidget($field_name, $media_type_id) { + /** @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface $entity_form_display */ + $entity_form_display = entity_get_form_display('media', $media_type_id, 'default'); + if ($entity_form_display->getComponent($field_name)) { + $entity_form_display->removeComponent($field_name)->save(); + } + } + + /** + * Test generic media type creation. + * + * @param string $media_type_id + * The media type config entity ID. + * @param string $source_id + * The media source ID. + * @param array $provided_fields + * (optional) An array of field machine names this type provides. + * + * @return \Drupal\media\MediaTypeInterface + * The created media type. + */ + public function doTestCreateMediaType($media_type_id, $source_id, array $provided_fields = []) { + $session = $this->getSession(); + $page = $session->getPage(); + $assert_session = $this->assertSession(); + + $this->drupalGet('admin/structure/media/add'); + $page->fillField('label', $media_type_id); + $this->getSession() + ->wait(5000, "jQuery('.machine-name-value').text() === '{$media_type_id}'"); + + // Make sure the source is available. + $assert_session->fieldExists('Media source'); + $assert_session->optionExists('Media source', $source_id); + $page->selectFieldOption('Media source', $source_id); + $assert_session->assertWaitOnAjaxRequest(); + + // Make sure the provided fields are visible on the form. + foreach ($provided_fields as $provided_field) { + $assert_session->selectExists("field_map[{$provided_field}]"); + } + + // Save the form to create the type. + $page->pressButton('Save'); + $assert_session->statusCodeEquals(200); + $assert_session->pageTextContains('The media type ' . $media_type_id . ' has been added.'); + $this->drupalGet('admin/structure/media'); + $assert_session->pageTextContains($media_type_id); + + // Bundle definitions are statically cached in the context of the test, we + // need to make sure we have updated information before proceeding with the + // actions on the UI. + \Drupal::service('entity_type.bundle.info')->clearCachedBundles(); + + return MediaType::load($media_type_id); + } + +} diff --git a/core/modules/media/tests/src/Kernel/MediaKernelTestBase.php b/core/modules/media/tests/src/Kernel/MediaKernelTestBase.php index 0daec876af..5d75bed69b 100644 --- a/core/modules/media/tests/src/Kernel/MediaKernelTestBase.php +++ b/core/modules/media/tests/src/Kernel/MediaKernelTestBase.php @@ -2,8 +2,12 @@ namespace Drupal\Tests\media\Kernel; +use Drupal\file\Entity\File; use Drupal\KernelTests\KernelTestBase; +use Drupal\media\Entity\Media; use Drupal\media\Entity\MediaType; +use Drupal\media\MediaTypeInterface; +use org\bovigo\vfs\vfsStream; /** * Base class for Media kernel tests. @@ -88,4 +92,42 @@ protected function createMediaType($media_source_name) { return $media_type; } + /** + * Helper to generate media entity. + * + * @param string $filename + * String filename with extension. + * @param \Drupal\media\MediaTypeInterface $media_type + * The the media type. + * + * @return \Drupal\media\Entity\Media + * A media entity. + */ + protected function generateMedia($filename, MediaTypeInterface $media_type) { + vfsStream::setup('drupal_root'); + vfsStream::create([ + 'sites' => [ + 'default' => [ + 'files' => [ + $filename => str_repeat('a', 3000), + ], + ], + ], + ]); + + $file = File::create([ + 'uri' => 'vfs://drupal_root/sites/default/files/' . $filename, + ]); + $file->setPermanent(); + $file->save(); + + return Media::create([ + 'bundle' => $media_type->id(), + 'name' => 'Mr. Jones', + 'field_media_file' => [ + 'target_id' => $file->id(), + ], + ]); + } + } diff --git a/core/modules/media/tests/src/Kernel/MediaSourceFileTest.php b/core/modules/media/tests/src/Kernel/MediaSourceFileTest.php new file mode 100644 index 0000000000..65d472324b --- /dev/null +++ b/core/modules/media/tests/src/Kernel/MediaSourceFileTest.php @@ -0,0 +1,30 @@ +createMediaType('file'); + // Create a random file that should fail. + $media = $this->generateMedia('test.patch', $mediaType); + $result = $media->validate(); + $this->assertCount(1, $result); + $this->assertEquals('field_media_file.0', $result->get(0)->getPropertyPath()); + $this->assertContains('Only files with the following extensions are allowed:', (string) $result->get(0)->getMessage()); + + // Create a random file that should pass. + $media = $this->generateMedia('test.txt', $mediaType); + $result = $media->validate(); + $this->assertCount(0, $result); + } + +} diff --git a/core/profiles/standard/config/optional/core.entity_form_display.media.file.default.yml b/core/profiles/standard/config/optional/core.entity_form_display.media.file.default.yml new file mode 100644 index 0000000000..15de2acd90 --- /dev/null +++ b/core/profiles/standard/config/optional/core.entity_form_display.media.file.default.yml @@ -0,0 +1,44 @@ +langcode: en +status: true +dependencies: + config: + - field.field.media.file.field_media_file + - media.type.file + module: + - file +id: media.file.default +targetEntityType: media +bundle: file +mode: default +content: + created: + type: datetime_timestamp + weight: 10 + region: content + settings: { } + third_party_settings: { } + field_media_file: + settings: + progress_indicator: throbber + third_party_settings: { } + type: file_generic + weight: 26 + region: content + name: + type: string_textfield + weight: -5 + region: content + settings: + size: 60 + placeholder: '' + third_party_settings: { } + uid: + type: entity_reference_autocomplete + weight: 5 + settings: + match_operator: CONTAINS + size: 60 + placeholder: '' + region: content + third_party_settings: { } +hidden: { } diff --git a/core/profiles/standard/config/optional/core.entity_view_display.media.file.default.yml b/core/profiles/standard/config/optional/core.entity_view_display.media.file.default.yml new file mode 100644 index 0000000000..697f117c6e --- /dev/null +++ b/core/profiles/standard/config/optional/core.entity_view_display.media.file.default.yml @@ -0,0 +1,50 @@ +langcode: en +status: true +dependencies: + config: + - field.field.media.file.field_media_file + - image.style.thumbnail + - media.type.file + module: + - file + - image + - user +id: media.file.default +targetEntityType: media +bundle: file +mode: default +content: + created: + label: hidden + type: timestamp + weight: 0 + region: content + settings: + date_format: medium + custom_date_format: '' + timezone: '' + third_party_settings: { } + field_media_file: + label: above + settings: { } + third_party_settings: { } + type: file_default + weight: 6 + region: content + thumbnail: + type: image + weight: 5 + label: hidden + settings: + image_style: thumbnail + image_link: '' + region: content + third_party_settings: { } + uid: + label: hidden + type: author + weight: 0 + region: content + settings: { } + third_party_settings: { } +hidden: { } diff --git a/core/profiles/standard/config/optional/field.field.media.file.field_media_file.yml b/core/profiles/standard/config/optional/field.field.media.file.field_media_file.yml new file mode 100644 index 0000000000..2d2e17937b --- /dev/null +++ b/core/profiles/standard/config/optional/field.field.media.file.field_media_file.yml @@ -0,0 +1,29 @@ +langcode: en +status: true +dependencies: + config: + - field.storage.media.field_media_file + - media.type.file + module: + - file + enforced: + module: + - media +id: media.file.field_media_file +field_name: field_media_file +entity_type: media +bundle: file +label: File +description: '' +required: true +translatable: true +default_value: { } +default_value_callback: '' +settings: + file_directory: '[date:custom:Y]-[date:custom:m]' + file_extensions: 'txt doc docx pdf' + max_filesize: '' + handler: 'default:file' + handler_settings: { } + description_field: false +field_type: file diff --git a/core/profiles/standard/config/optional/field.storage.media.field_media_file.yml b/core/profiles/standard/config/optional/field.storage.media.field_media_file.yml new file mode 100644 index 0000000000..efe7954dc7 --- /dev/null +++ b/core/profiles/standard/config/optional/field.storage.media.field_media_file.yml @@ -0,0 +1,25 @@ +langcode: en +status: true +dependencies: + module: + - file + - media + enforced: + module: + - media +id: media.field_media_file +field_name: field_media_file +entity_type: media +type: file +settings: + uri_scheme: public + target_type: file + display_field: false + display_default: false +module: file +locked: true +cardinality: 1 +translatable: true +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/core/profiles/standard/config/optional/media.type.file.yml b/core/profiles/standard/config/optional/media.type.file.yml new file mode 100644 index 0000000000..0fc270a083 --- /dev/null +++ b/core/profiles/standard/config/optional/media.type.file.yml @@ -0,0 +1,14 @@ +langcode: en +status: true +dependencies: + module: + - media +id: file +label: File +description: "Use local files for reusable media." +source: file +queue_thumbnail_downloads: false +new_revision: true +source_configuration: + source_field: field_media_file +field_map: { }