diff --git a/core/modules/media/config/schema/media.schema.yml b/core/modules/media/config/schema/media.schema.yml index 4783f6d0ca..945db57aac 100644 --- a/core/modules/media/config/schema/media.schema.yml +++ b/core/modules/media/config/schema/media.schema.yml @@ -36,6 +36,10 @@ media.type.*: sequence: type: string +media.handler.file: + type: media.handler.field_aware + label: 'File handler configuration' + action.configuration.media_delete_action: type: action_configuration_default label: 'Delete media configuration' 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..e9290aa7fb --- /dev/null +++ b/core/modules/media/src/Plugin/media/Source/File.php @@ -0,0 +1,116 @@ + $this->t('MIME type'), + 'size' => $this->t('Size'), + ]; + } + + /** + * {@inheritdoc} + */ + public function getMetadata(MediaInterface $media, $name) { + $file = $this->getSourceFile($media); + + switch ($name) { + case 'mime': + return $file->getMimeType() ?: FALSE; + + case 'size': + $size = $file->getSize(); + return is_numeric($size) ? $size : FALSE; + } + + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function getThumbnail(MediaInterface $media) { + $file = $this->getSourceFile($media); + $icon_base = $this->configFactory->get('media.settings')->get('icon_base'); + $thumbnail = FALSE; + + // We try to magically use the most specific icon present in the $icon_base + // directory, based on the MIME information. For instance, if an icon file + // named "pdf.png" is present, it will be used if the file matches this + // MIME type. + if ($file) { + $mimetype = $file->getMimeType(); + $mimetype = explode('/', $mimetype); + $thumbnail = $icon_base . "/{$mimetype[0]}--{$mimetype[1]}.png"; + + if (!is_file($thumbnail)) { + $thumbnail = $icon_base . "/{$mimetype[1]}.png"; + } + } + + if (!is_file($thumbnail)) { + $thumbnail = $icon_base . '/generic.png'; + } + + return $thumbnail; + } + + /** + * {@inheritdoc} + */ + public function getDefaultName(MediaInterface $media) { + // The default name will be the filename of the source_field. + return $this->getSourceFile($media)->getFilename(); + } + + /** + * Get source field file entity. + * + * @param \Drupal\media\MediaInterface $media + * The media entity. + * + * @return \Drupal\file\FileInterface + * The file entity present in the source field. + * + * @throws \RuntimeException + * If the source field for the handler is not defined, or if the + * source field does not contain a file entity. + */ + protected function getSourceFile(MediaInterface $media) { + $source_field = $this->configuration['source_field']; + + if (empty($source_field)) { + throw new \RuntimeException('Source field for media file handler is not defined.'); + } + + $file = $media->get($source_field)->entity; + if (empty($file)) { + throw new \RuntimeException('The source field does not contain a file entity.'); + } + + return $file; + } + +} diff --git a/core/modules/media/tests/src/FunctionalJavascript/FileTest.php b/core/modules/media/tests/src/FunctionalJavascript/FileTest.php new file mode 100644 index 0000000000..070520210c --- /dev/null +++ b/core/modules/media/tests/src/FunctionalJavascript/FileTest.php @@ -0,0 +1,62 @@ +getSession(); + $page = $session->getPage(); + $assert_session = $this->assertSession(); + + // Create image media handler. + $this->createMediaTypeTest($type_name, 'file', $provided_fields); + + // Create a supported and a non-supported field. + $fields = [ + 'field_string_mime' => 'string', + ]; + $this->createMediaFields($fields, $type_name); + + // Hide the media name to test default name generation. + $this->hideMediaField('name', $type_name); + + $this->drupalGet("admin/structure/media/manage/$type_name"); + $page->selectFieldOption("field_map[mime]", 'field_string_mime'); + $page->pressButton('Save'); + + // Create a media item. + $this->drupalGet("media/add/{$type_name}"); + $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 created. + $assert_session->elementAttributeContains('css', '.image-style-thumbnail', 'src', 'generic.png'); + + // Load the media and check that all fields are properly populated. + $media = Media::load(1); + $this->assertEquals('README.txt', $media->label()); + $this->assertEquals('text/plain', $media->get('field_string_mime')->value); + } + +} diff --git a/core/modules/media/tests/src/FunctionalJavascript/MediaHandlerTestBase.php b/core/modules/media/tests/src/FunctionalJavascript/MediaHandlerTestBase.php new file mode 100644 index 0000000000..bd2911079e --- /dev/null +++ b/core/modules/media/tests/src/FunctionalJavascript/MediaHandlerTestBase.php @@ -0,0 +1,123 @@ + $field_name, + 'entity_type' => 'media', + 'type' => $field_type, + ]); + $storage->save(); + + $config = FieldConfig::create([ + 'field_storage' => $storage, + 'bundle' => $type_id, + ]); + $config->save(); + + // Make the field visible in the form display. + /** @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface $form_display */ + $component = \Drupal::service('plugin.manager.field.widget') + ->prepareConfiguration($field_type, []); + + entity_get_form_display('media', $type_id, 'default') + ->setComponent($field_name, $component) + ->save(); + } + + /** + * Helper to 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 $type_id + * The type machine name. + */ + protected function createMediaFields(array $fields, $type_id) { + foreach ($fields as $field_name => $field_type) { + $this->createMediaField($field_name, $field_type, $type_id); + } + } + + /** + * Hide a component from the default form display config. + * + * @param string $field_name + * The field name. + * @param string $type_name + * The media type machine name. + */ + protected function hideMediaField($field_name, $type_name) { + $form_display = entity_get_form_display('media', $type_name, 'default'); + $form_display->removeComponent($field_name)->save(); + } + + /** + * Helper to test a generic type (bundle) creation. + * + * @param string $type_name + * The type machine name. + * @param string $type_id + * The bundle type ID. + * @param array $provided_fields + * (optional) An array of field machine names this type provides. + * + * @return \Drupal\media\MediaTypeInterface + * The type created. + */ + public function createMediaTypeTest($type_name, $type_id, array $provided_fields = []) { + $session = $this->getSession(); + $page = $session->getPage(); + $assert_session = $this->assertSession(); + + $this->drupalGet('admin/structure/media/add'); + $page->fillField('label', $type_name); + $this->assertJsCondition("jQuery('.machine-name-value').text() === '$type_name'"); + + // Make sure the type is available. + $assert_session->optionExists('handler', $type_id); + $page->selectFieldOption('handler', $type_id); + $assert_session->assertWaitOnAjaxRequest(); + + // Make sure the provided fields are visible on the form. + if (!empty($provided_fields)) { + foreach ($provided_fields as $provided_field) { + $assert_session->selectExists("field_map[$provided_field]"); + } + } + + // Save the page to create the type. + $page->pressButton('Save'); + $assert_session->pageTextContains('The media type ' . $type_name . ' has been added.'); + $this->drupalGet('admin/structure/media'); + $assert_session->pageTextContains($type_name); + + // 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($type_name); + } + +} 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..740d5df5bb --- /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 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..a830fa89c4 --- /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: false +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..109d801dd7 --- /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 the "File" media type for uploading local files.' +source: file +queue_thumbnail_downloads: false +new_revision: false +source_configuration: + source_field: field_media_file +field_map: { }