diff --git a/core/modules/media/config/install/core.entity_view_mode.media.embed.yml b/core/modules/media/config/install/core.entity_view_mode.media.embed.yml new file mode 100644 index 0000000000..e45e4f3eca --- /dev/null +++ b/core/modules/media/config/install/core.entity_view_mode.media.embed.yml @@ -0,0 +1,12 @@ +langcode: en +status: true +dependencies: + enforced: + module: + - media + module: + - media +id: media.embed +label: 'Embed' +targetEntityType: media +cache: true diff --git a/core/modules/media/media.install b/core/modules/media/media.install index 7d8764c03e..9a806dcf9c 100644 --- a/core/modules/media/media.install +++ b/core/modules/media/media.install @@ -9,10 +9,14 @@ use Drupal\Core\File\Exception\FileException; use Drupal\Core\File\FileSystemInterface; use Drupal\Core\Url; +use Drupal\media\Entity\MediaType; use Drupal\media\MediaTypeInterface; use Drupal\media\Plugin\media\Source\OEmbedInterface; use Drupal\user\Entity\Role; use Drupal\user\RoleInterface; +use Drupal\media\Plugin\media\Source\Image; +use Drupal\Core\StringTranslation\TranslatableMarkup; +use Drupal\Core\Entity\Entity\EntityViewMode; /** * Implements hook_install(). @@ -118,6 +122,36 @@ function media_requirements($phase) { ]; } } + + $view_mode = EntityViewMode::load('media.embed'); + if (!empty($view_mode)) { + $display_repository = \Drupal::service('entity_display.repository'); + $module_handler = \Drupal::service('module_handler'); + foreach (MediaType::loadMultiple() as $type) { + $source = $type->getSource(); + if (is_a($source, Image::class, TRUE)) { + /** @var \Drupal\Core\Entity\Entity\EntityViewDisplay $display */ + $display = $display_repository->getViewDisplay('media', $type->id(), 'embed'); + $source_field = $source->getSourceFieldDefinition($type); + $field_name = $source_field->getName(); + if ($component = $display->getComponent($field_name)) { + if ($component['settings']['image_style'] === '') { + $action_item = ''; + if ($module_handler->moduleExists('field_ui')) { + $url = Url::fromRoute('entity.entity_view_display.media.view_mode', ['media_type' => $type->id(), 'view_mode_name' => 'embed'])->toString(); + $action_item = new TranslatableMarkup('If you would like to change this, add an image style to the field %field_name.', ['%field_name' => $source_field->label(), ':display' => $url]); + } + + $requirements['media_embed_image_style_' . $type->id()] = [ + 'title' => t('Media'), + 'description' => new TranslatableMarkup('The %view_mode_label display for the media type %type is not currently using an image style on the field %field_name. Not using an image style can lead to much larger file downloads. @action_item', ['%view_mode_label' => $view_mode->label(), '%field_name' => $source_field->label(), '@action_item' => $action_item, '%type' => $type->label()]), + 'severity' => REQUIREMENT_WARNING, + ]; + } + } + } + } + } } return $requirements; diff --git a/core/modules/media/media.module b/core/modules/media/media.module index 4553f283ec..18a36c2c36 100644 --- a/core/modules/media/media.module +++ b/core/modules/media/media.module @@ -16,7 +16,10 @@ use Drupal\Core\Template\Attribute; use Drupal\Core\Url; use Drupal\field\FieldConfigInterface; +use Drupal\media\MediaTypeInterface; use Drupal\media\Plugin\media\Source\OEmbedInterface; +use Drupal\Core\Entity\EntityStorageException; +use Drupal\Core\Entity\Entity\EntityViewMode; /** * Implements hook_help(). @@ -508,3 +511,64 @@ function media_field_widget_form_alter(&$element, FormStateInterface $form_state $element['#attributes']['data-media-embed-host-entity-langcode'] = $context['items']->getLangcode(); } } + +/** + * Ensures that the given media type has an embed view display. + * + * Embeds need a special view display to make sure that the out-of-the-box + * configuration doesn't output very large raw images. + * + * @param \Drupal\media\MediaTypeInterface $type + * The media type to configure. + * + * @return bool + * Returns TRUE if the entity view display was configured correctly, FALSE + * otherwise. + */ +function _media_configure_embed_view_display(MediaTypeInterface $type) { + // If a site builder has deleted the 'Embed' view mode, do not configure the + // display. + $view_mode = EntityViewMode::load('media.embed'); + if (!$view_mode) { + return FALSE; + } + + $display = \Drupal::service('entity_display.repository') + ->getViewDisplay('media', $type->id(), 'embed'); + + // If the embedded entity view display has already been configured, we don't + // need to redo it. + if (!$display->isNew()) { + return FALSE; + } + // Remove any defaults. + $display->set('content', []); + $type->getSource()->prepareViewDisplay($type, $display); + + return $display->save(); +} + +/** + * Implements hook_form_FORM_ID_alter(). + */ +function media_form_media_type_add_form_alter(&$form, FormStateInterface $form_state, $form_id) { + $form['actions']['submit']['#submit'][] = '_media_type_add_form_submit'; +} + +/** + * Submit callback for media_type_add_form. + */ +function _media_type_add_form_submit(array &$form, FormStateInterface $form_state) { + $type = $form_state->getFormObject()->getEntity(); + try { + if (_media_configure_embed_view_display($type)) { + \Drupal::messenger()->addStatus(t('The Embed view display has been created for the %type media type.', [ + '%type' => $type->label(), + ])); + } + } + catch (EntityStorageException $e) { + \Drupal::messenger()->addError($e->getMessage()); + return; + } +} diff --git a/core/modules/media/media.post_update.php b/core/modules/media/media.post_update.php index f2a6c375f5..2599a29d37 100644 --- a/core/modules/media/media.post_update.php +++ b/core/modules/media/media.post_update.php @@ -5,6 +5,39 @@ * Post update functions for Media. */ +use Drupal\Core\Entity\Entity\EntityViewMode; +use Drupal\media\Entity\MediaType; + +/** + * Ensure that all existing media types have an embed display. + */ +function media_post_update_entity_view_displays() { + if (!EntityViewMode::load('media.embed')) { + EntityViewMode::create([ + 'id' => 'media.embed', + 'targetEntityType' => 'media', + 'label' => t('Embed'), + 'dependencies' => [ + 'enforced' => [ + 'module' => ['media'], + ], + ], + ])->save(); + } + + $types = []; + foreach (MediaType::loadMultiple() as $type) { + if (_media_configure_embed_view_display($type)) { + $types[] = $type->label(); + } + } + if ($types) { + return t('The Embed view display has been created for the following media types: @types.', [ + '@types' => implode(', ', $types), + ]); + } +} + /** * Clear caches due to changes in local tasks and action links. */ diff --git a/core/modules/media/src/Plugin/Filter/MediaEmbed.php b/core/modules/media/src/Plugin/Filter/MediaEmbed.php index 45f2a5ab45..e2cf4c0c61 100644 --- a/core/modules/media/src/Plugin/Filter/MediaEmbed.php +++ b/core/modules/media/src/Plugin/Filter/MediaEmbed.php @@ -29,7 +29,7 @@ * description = @Translation("Embeds media items using a custom tag, <drupal-media>. If used in conjunction with the 'Align/Caption' filters, make sure this filter is configured to run after them."), * type = Drupal\filter\Plugin\FilterInterface::TYPE_TRANSFORM_REVERSIBLE, * settings = { - * "default_view_mode" = "full", + * "default_view_mode" = "embed", * }, * weight = 100, * ) diff --git a/core/modules/media/src/Plugin/media/Source/Image.php b/core/modules/media/src/Plugin/media/Source/Image.php index 34b565c054..ecc552f888 100644 --- a/core/modules/media/src/Plugin/media/Source/Image.php +++ b/core/modules/media/src/Plugin/media/Source/Image.php @@ -3,6 +3,7 @@ namespace Drupal\media\Plugin\media\Source; use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\Entity\Display\EntityViewDisplayInterface; use Drupal\Core\Entity\EntityFieldManagerInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Field\FieldTypePluginManagerInterface; @@ -161,4 +162,27 @@ public function createSourceField(MediaTypeInterface $type) { return $field->set('settings', $settings); } + /** + * {@inheritdoc} + */ + public function prepareViewDisplay(MediaTypeInterface $type, EntityViewDisplayInterface $display) { + parent::prepareViewDisplay($type, $display); + + // For the `embed` display mode, use the `large` image style and do not + // link the image to anything. This is done in order to ensure the image + // does not become too unwieldy when embedded into formatted text, and to + // allow it to wrapped by an author-created link. If the `large` image + // style has been deleted, do not set an image style. + if ($display->getMode() === 'embed') { + $field_name = $this->getSourceFieldDefinition($type)->getName(); + $component = $display->getComponent($field_name); + $component['settings']['image_link'] = ''; + $component['settings']['image_style'] = ''; + if ($this->entityTypeManager->getStorage('image_style')->load('large')) { + $component['settings']['image_style'] = 'large'; + } + $display->setComponent($field_name, $component); + } + } + } diff --git a/core/modules/media/tests/src/Functional/Update/MediaUpdateTest.php b/core/modules/media/tests/src/Functional/Update/MediaUpdateTest.php index ee97fe5ccd..64f3858979 100644 --- a/core/modules/media/tests/src/Functional/Update/MediaUpdateTest.php +++ b/core/modules/media/tests/src/Functional/Update/MediaUpdateTest.php @@ -2,7 +2,9 @@ namespace Drupal\Tests\media\Functional\Update; +use Drupal\Core\Entity\Entity\EntityViewDisplay; use Drupal\FunctionalTests\Update\UpdatePathTestBase; +use Drupal\image\Entity\ImageStyle; use Drupal\media\Entity\Media; use Drupal\Tests\media\Traits\MediaTypeCreationTrait; use Drupal\user\Entity\Role; @@ -114,4 +116,19 @@ public function testEnableStandaloneUrl() { $this->assertSession()->statusCodeEquals(200); } + /** + * Tests the media module post update for embed view display. + * + * @see media_post_update_entity_view_displays() + */ + public function testPostUpdateEntityViewDisplays() { + $this->assertNull(ImageStyle::load('embed')); + $this->assertNull(EntityViewDisplay::load('media.file.embed')); + $this->assertNull(EntityViewDisplay::load('media.image.embed')); + $this->runUpdates(); + $this->assertInstanceOf(EntityViewDisplay::class, EntityViewDisplay::load('media.file.embed')); + $this->assertInstanceOf(EntityViewDisplay::class, EntityViewDisplay::load('media.image.embed')); + $this->assertSession()->pageTextContains('The Embed view display has been created for the following media types: File, Image.'); + } + } diff --git a/core/modules/media/tests/src/FunctionalJavascript/MediaEmbedDisplayModeTest.php b/core/modules/media/tests/src/FunctionalJavascript/MediaEmbedDisplayModeTest.php new file mode 100644 index 0000000000..05488c9af5 --- /dev/null +++ b/core/modules/media/tests/src/FunctionalJavascript/MediaEmbedDisplayModeTest.php @@ -0,0 +1,149 @@ +drupalLogin($this->drupalCreateUser([ + 'administer site configuration', + 'access media overview', + 'administer media', + 'administer media types', + 'view media', + // We need 'access content' for system.machine_name_transliterate. + 'access content', + ])); + } + + /** + * Tests that media can automatically configure entity view displays. + */ + public function testEmbedEntityViewDisplays() { + $page = $this->getSession()->getPage(); + $assert_session = $this->assertSession(); + // The 'Embed' display is not automatically created when creating a + // media type programmatically, only when installing the module or when + // creating a media type via the UI. + $this->createMediaType('image', [ + 'id' => 'aramis', + 'field_map' => ['name' => Image::METADATA_ATTRIBUTE_NAME], + ]); + $this->assertNull(EntityViewDisplay::load('media.aramis.embed')); + + // Create a non-image media type through the UI. + $this->drupalGet('admin/structure/media/add'); + $page->fillField('label', 'Athos'); + $this->assertNotEmpty($assert_session->waitForText('Machine name: athos')); + $page->selectFieldOption('source', 'file'); + // Wait for form to complete with AJAX. + $this->assertNotEmpty($assert_session->waitForText('Field mapping')); + $page->pressButton('Save'); + $assert_session->pageTextContains("The Embed view display has been created for the Athos media type."); + $this->assertViewDisplayConfigured('athos'); + + // Create an image media through the UI. + $this->drupalGet('admin/structure/media/add'); + $page->fillField('label', 'Porthos'); + $this->assertNotEmpty($assert_session->waitForText('Machine name: porthos')); + $page->selectFieldOption('source', 'image'); + // Wait for form to complete with AJAX. + $this->assertNotEmpty($assert_session->waitForText('Field mapping')); + $page->pressButton('Save'); + $assert_session->pageTextContains("The Embed view display has been created for the Porthos media type."); + $this->assertViewDisplayConfigured('porthos'); + + // Delete a view display. + EntityViewDisplay::load("media.porthos.embed")->delete(); + // Test that the entity view display is not regenerated when saving + // existing media types after the entity view display has been deleted. + // We only want to create it on the `add` form, not the `edit` form. + $this->drupalGet('admin/structure/media/manage/porthos'); + $page->pressButton('Save'); + $this->assertNull(EntityViewDisplay::load('media.porthos.embed')); + + // If for some reason a site builder deletes the 'large' image style, it + // should be recreated when adding a new media type with an image source. + ImageStyle::load('large')->delete(); + $this->drupalGet('admin/structure/media/add'); + $page->fillField('label', 'Madame Bonacieux'); + $this->assertNotEmpty($assert_session->waitForText('Machine name: madame_bonacieux')); + $page->selectFieldOption('source', 'image'); + // Wait for form to complete with AJAX. + $this->assertNotEmpty($assert_session->waitForText('Field mapping')); + $page->pressButton('Save'); + $assert_session->pageTextContains("The Embed view display has been created for the Madame Bonacieux media type."); + $this->assertViewDisplayConfigured('madame_bonacieux'); + // Test that hook_requirements adds warning about the lack of an image style. + $this->drupalGet('/admin/reports/status'); + $assert_session->pageTextContains('The Embed display for the media type Madame Bonacieux is not currently using an image style on the field Image.'); + + // If for some reason a site builder deletes the embed view mode, an + // entity view display should not be configured when creating a media type + // through the UI. + EntityViewMode::load('media.embed')->delete(); + $this->drupalGet('admin/structure/media/add'); + $page->fillField('label', "D'Artagnan"); + $this->assertNotEmpty($assert_session->waitForText('Machine name: d_artagnan')); + $page->selectFieldOption('source', 'image'); + // Wait for form to complete with AJAX. + $this->assertNotEmpty($assert_session->waitForText('Field mapping')); + $page->pressButton('Save'); + $assert_session->pageTextContains("The media type d'Artagnan has been added."); + $assert_session->pageTextNotContains("The Embed view display has been created for the d'Artagnan media type."); + $this->assertNull(EntityViewDisplay::load('media.d_artagnan.embed')); + } + + /** + * Asserts the embed view display components for a media type. + * + * @param string $type_id + * The media type ID. + */ + protected function assertViewDisplayConfigured($type_id) { + $type = MediaType::load($type_id); + $display = EntityViewDisplay::load('media.' . $type_id . '.embed'); + $this->assertInstanceOf(EntityViewDisplay::class, $display); + $source_field_definition = $type->getSource()->getSourceFieldDefinition($type); + $component = $display->getComponent($source_field_definition->getName()); + $this->assertInternalType('array', $component); + if ($source_field_definition->getType() === 'image') { + if (is_a(ImageStyle::load('large'), ImageStyleInterface::class, TRUE)) { + $this->assertSame('large', $component['settings']['image_style']); + } + else { + $this->assertEmpty($component['settings']['image_style']); + } + $this->assertEmpty($component['settings']['image_link']); + } + } + +} diff --git a/core/modules/media/tests/src/Kernel/MediaEmbedFilterTest.php b/core/modules/media/tests/src/Kernel/MediaEmbedFilterTest.php index abe4442e12..2b2734d158 100644 --- a/core/modules/media/tests/src/Kernel/MediaEmbedFilterTest.php +++ b/core/modules/media/tests/src/Kernel/MediaEmbedFilterTest.php @@ -44,7 +44,7 @@ public function testBasics(array $embed_attributes, $expected_view_mode, array $ * Data provider for testBasics(). */ public function providerTestBasics() { - $expected_cacheability_full = (new CacheableMetadata()) + $expected_cacheability = (new CacheableMetadata()) ->setCacheTags([ '_media_test_filter_access:media:1', '_media_test_filter_access:user:2', @@ -58,14 +58,14 @@ public function providerTestBasics() { ->setCacheMaxAge(Cache::PERMANENT); return [ - 'data-entity-uuid only ⇒ default view mode "full" used' => [ + 'data-entity-uuid only ⇒ default view mode "embed" used' => [ [ 'data-entity-type' => 'media', 'data-entity-uuid' => static::EMBEDDED_ENTITY_UUID, ], - 'full', + 'embed', [], - $expected_cacheability_full, + $expected_cacheability, ], 'data-entity-uuid + data-view-mode=full ⇒ specified view mode used' => [ [ @@ -75,7 +75,17 @@ public function providerTestBasics() { ], 'full', [], - $expected_cacheability_full, + $expected_cacheability, + ], + 'data-entity-uuid + data-view-mode=embed ⇒ specified view mode used' => [ + [ + 'data-entity-type' => 'media', + 'data-entity-uuid' => static::EMBEDDED_ENTITY_UUID, + 'data-view-mode' => 'embed', + ], + 'embed', + [], + $expected_cacheability, ], 'data-entity-uuid + data-view-mode=foobar ⇒ specified view mode used' => [ [ @@ -103,12 +113,12 @@ public function providerTestBasics() { 'data-entity-type' => 'media', 'data-entity-uuid' => static::EMBEDDED_ENTITY_UUID, ], - 'full', + 'embed', [ 'data-foo' => 'bar', 'foo' => 'bar', ], - $expected_cacheability_full, + $expected_cacheability, ], ]; } @@ -138,7 +148,7 @@ public function testAccessUnpublished($allowed_to_view_unpublished, $expected_re $this->assertEmpty($this->getRawContent()); } else { - $this->assertCount(1, $this->cssSelect('div[data-media-embed-test-view-mode="full"]')); + $this->assertCount(1, $this->cssSelect('div[data-media-embed-test-view-mode="embed"]')); } $this->assertSame($expected_cacheability->getCacheTags(), $result->getCacheTags()); @@ -375,7 +385,7 @@ public function testRecursionProtection() { // Render and verify the presence of the embedded entity 20 times. for ($i = 0; $i < 20; $i++) { $this->applyFilter($text); - $this->assertCount(1, $this->cssSelect('div[data-media-embed-test-view-mode="full"]')); + $this->assertCount(1, $this->cssSelect('div[data-media-embed-test-view-mode="embed"]')); } // Render a 21st time, this is exceeding the recursion limit. The entity @@ -399,7 +409,7 @@ public function testFilterIntegration(array $filter_ids, array $additional_attri $result = $this->processText($content, 'en', $filter_ids); $this->setRawContent($result->getProcessedText()); $this->assertCount($expected_verification_success ? 1 : 0, $this->cssSelect($verification_selector)); - $this->assertCount(1, $this->cssSelect('div[data-media-embed-test-view-mode="full"]')); + $this->assertCount(1, $this->cssSelect('div[data-media-embed-test-view-mode="embed"]')); $this->assertSame([ '_media_test_filter_access:media:1', '_media_test_filter_access:user:2', diff --git a/core/profiles/demo_umami/config/install/core.entity_view_display.media.image.embed.yml b/core/profiles/demo_umami/config/install/core.entity_view_display.media.image.embed.yml new file mode 100644 index 0000000000..92a96dc0c0 --- /dev/null +++ b/core/profiles/demo_umami/config/install/core.entity_view_display.media.image.embed.yml @@ -0,0 +1,30 @@ +langcode: en +status: true +dependencies: + config: + - core.entity_view_mode.media.embed + - field.field.media.image.field_media_image + - image.style.large + - media.type.image + module: + - image +id: media.image.embed +targetEntityType: media +bundle: image +mode: embed +content: + field_media_image: + label: visually_hidden + settings: + image_style: large + image_link: '' + third_party_settings: { } + type: image + weight: 1 + region: content +hidden: + created: true + langcode: true + name: true + thumbnail: true + uid: true diff --git a/core/profiles/standard/config/optional/core.entity_view_display.media.image.embed.yml b/core/profiles/standard/config/optional/core.entity_view_display.media.image.embed.yml new file mode 100644 index 0000000000..92a96dc0c0 --- /dev/null +++ b/core/profiles/standard/config/optional/core.entity_view_display.media.image.embed.yml @@ -0,0 +1,30 @@ +langcode: en +status: true +dependencies: + config: + - core.entity_view_mode.media.embed + - field.field.media.image.field_media_image + - image.style.large + - media.type.image + module: + - image +id: media.image.embed +targetEntityType: media +bundle: image +mode: embed +content: + field_media_image: + label: visually_hidden + settings: + image_style: large + image_link: '' + third_party_settings: { } + type: image + weight: 1 + region: content +hidden: + created: true + langcode: true + name: true + thumbnail: true + uid: true diff --git a/core/profiles/standard/tests/src/Functional/StandardTest.php b/core/profiles/standard/tests/src/Functional/StandardTest.php index f62e183fca..6dc6be2863 100644 --- a/core/profiles/standard/tests/src/Functional/StandardTest.php +++ b/core/profiles/standard/tests/src/Functional/StandardTest.php @@ -256,6 +256,19 @@ public function testStandard() { $date_field = $assert_session->fieldExists('Date', $form)->getOuterHtml(); $published_checkbox = $assert_session->fieldExists('Published', $form)->getOuterHtml(); $this->assertTrue(strpos($form_html, $published_checkbox) > strpos($form_html, $date_field)); + // Assert an "embed" entity view display option exists for each media + // type, and that it is preconfigured for the "Image" media type. + $this->drupalGet('/admin/structure/media/manage/' . $media_type->id() . '/display'); + $page->hasUncheckedField('display_modes_custom[full]'); + if ($media_type === 'image') { + $page->hasCheckedField('display_modes_custom[embed]'); + $this->drupalGet('admin/structure/media/manage/image/display/embed'); + $this->assertTrue($assert_session->optionExists('fields[field_media_image][type]', 'image')->isSelected()); + $assert_session->elementTextContains('css', 'tr[data-drupal-selector="edit-fields-field-media-image"]', 'Image style: Large (480×480)'); + } + else { + $page->hasUncheckedField('display_modes_custom[embed]'); + } } }