diff --git a/core/modules/media/media.install b/core/modules/media/media.install index 7d8764c03e..65b82f806f 100644 --- a/core/modules/media/media.install +++ b/core/modules/media/media.install @@ -9,10 +9,13 @@ 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\image\Plugin\Field\FieldType\ImageItem; +use Drupal\Core\StringTranslation\TranslatableMarkup; /** * Implements hook_install(). @@ -118,6 +121,59 @@ function media_requirements($phase) { ]; } } + + // When a new media type with an image source is created we're configuring + // the default entity view display using the 'large' image style. + // Unfortunately, if a site builder has deleted the 'large' image style, + // we need some other image style to use, but at this point, we can't + // really know the site builder's intentions. So rather than do something + // surprising, we're leaving the embedded media without an image style and + // adding a warning that the site builder might want to add an image style. + // @see Drupal\media\Plugin\media\Source\Image::prepareViewDisplay + $module_handler = \Drupal::service('module_handler'); + foreach (MediaType::loadMultiple() as $type) { + $display = \Drupal::service('entity_display.repository') + ->getViewDisplay('media', $type->id(), 'default'); + // Do not add a warning for not-yet-configured displays. + if ($display->isNew()) { + continue; + } + + $source = $type->getSource(); + $source_field_definition = $source->getSourceFieldDefinition($type); + if (!is_a($source_field_definition->getItemDefinition()->getClass(), ImageItem::class, TRUE)) { + continue; + } + + $component = $display->getComponent($source_field_definition->getName()); + if (empty($component) || $component['type'] !== 'image' || !empty($component['settings']['image_style'])) { + continue; + } + + $action_item = ''; + if ($module_handler->moduleExists('field_ui') && \Drupal::currentUser()->hasPermission('administer media display')) { + $url = Url::fromRoute('entity.entity_view_display.media.default', [ + 'media_type' => $type->id(), + ])->toString(); + $action_item = new TranslatableMarkup('If you would like to change this, add an image style to the %field_name field.', + [ + '%field_name' => $source_field_definition->label(), + ':display' => $url, + ]); + } + $requirements['media_default_image_style_' . $type->id()] = [ + 'title' => t('Media'), + 'description' => new TranslatableMarkup('The default display for the %type media type is not currently using an image style on the %field_name field. Not using an image style can lead to much larger file downloads. @action_item', + [ + '%field_name' => $source_field_definition->label(), + '@action_item' => $action_item, + '%type' => $type->label(), + ] + ), + 'severity' => REQUIREMENT_WARNING, + ]; + } + } return $requirements; diff --git a/core/modules/media/src/Plugin/media/Source/Image.php b/core/modules/media/src/Plugin/media/Source/Image.php index 34b565c054..6439ad8fa2 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,35 @@ public function createSourceField(MediaTypeInterface $type) { return $field->set('settings', $settings); } + /** + * {@inheritdoc} + */ + public function prepareViewDisplay(MediaTypeInterface $type, EntityViewDisplayInterface $display) { + parent::prepareViewDisplay($type, $display); + + $field_name = $this->getSourceFieldDefinition($type)->getName(); + + if ($display->getMode() === 'default') { + // Remove components other than the source field. + foreach (array_keys($display->getComponents()) as $name) { + if ($name !== $field_name) { + $display->removeComponent($name); + } + } + } + + // Use the `large` image style and do not link the image to anything. + // This will prevent the out-of-the-box configuration from outputting very + // large raw images. If the `large` image style has been deleted, do not + // set an image style. + $component = $display->getComponent($field_name); + $component['label'] = 'visually_hidden'; + $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/FunctionalJavascript/MediaDisplayTest.php b/core/modules/media/tests/src/FunctionalJavascript/MediaDisplayTest.php index bb3d6c5e68..315deecedc 100644 --- a/core/modules/media/tests/src/FunctionalJavascript/MediaDisplayTest.php +++ b/core/modules/media/tests/src/FunctionalJavascript/MediaDisplayTest.php @@ -101,10 +101,13 @@ public function testMediaDisplay() { // Assert the image is present inside the media element. $media_item = $assert_session->elementExists('css', '.media--type-image > div'); $assert_session->elementExists('css', 'img', $media_item); - // Assert that the image src is the original image and not an image style. + // Assert that the image src uses the large image style, the label is + // visually hidden, and there is no link to the image file. $media_image = $assert_session->elementExists('css', '.media--type-image img'); - $expected_image_src = file_url_transform_relative(file_create_url(\Drupal::token()->replace('public://[date:custom:Y]-[date:custom:m]/example_1.jpeg'))); - $this->assertSame($expected_image_src, $media_image->getAttribute('src')); + $expected_image_src = file_url_transform_relative(file_create_url(\Drupal::token()->replace('public://styles/large/public/[date:custom:Y]-[date:custom:m]/example_1.jpeg'))); + $this->assertContains($expected_image_src, $media_image->getAttribute('src')); + $assert_session->elementExists('css', '.field__label.visually-hidden'); + $assert_session->elementNotExists('css', '.field--name-field-media-image a'); $test_filename = $this->randomMachineName() . '.txt'; $test_filepath = 'public://' . $test_filename; diff --git a/core/modules/media/tests/src/FunctionalJavascript/MediaSourceImageTest.php b/core/modules/media/tests/src/FunctionalJavascript/MediaSourceImageTest.php index cb013805ad..fe0129fd64 100644 --- a/core/modules/media/tests/src/FunctionalJavascript/MediaSourceImageTest.php +++ b/core/modules/media/tests/src/FunctionalJavascript/MediaSourceImageTest.php @@ -2,8 +2,13 @@ namespace Drupal\Tests\media\FunctionalJavascript; +use Drupal\Core\Entity\Entity\EntityViewDisplay; +use Drupal\image\Entity\ImageStyle; use Drupal\media\Entity\Media; +use Drupal\media\Entity\MediaType; use Drupal\media\Plugin\media\Source\Image; +use Drupal\user\Entity\Role; +use Drupal\user\RoleInterface; /** * Tests the image media source. @@ -57,16 +62,118 @@ public function testMediaImageSource() { // Get the media entity view URL from the creation message. $this->drupalGet($this->assertLinkToCreatedMedia()); - // Make sure the thumbnail is displayed from uploaded image. - $assert_session->elementAttributeContains('css', '.image-style-thumbnail', 'src', 'example_1.jpeg'); - // Ensure the thumbnail has the correct alt attribute. - $assert_session->elementAttributeContains('css', '.image-style-thumbnail', 'alt', 'Image Alt Text 1'); + // Assert the image element is present inside the media element and that its + // src attribute uses the large image style, the label is visually hidden, + // and there is no link to the image file. + $image_element = $assert_session->elementExists('css', '.field--name-field-media-image img'); + $expected_image_src = file_url_transform_relative(file_create_url(\Drupal::token()->replace('public://styles/large/public/[date:custom:Y]-[date:custom:m]/example_1.jpeg'))); + $this->assertContains($expected_image_src, $image_element->getAttribute('src')); + $assert_session->elementExists('css', '.field--name-field-media-image .field__label.visually-hidden'); + $assert_session->elementNotExists('css', '.field--name-field-media-image a'); + + // Ensure the image has the correct alt attribute. + $assert_session->elementAttributeContains('css', '.field--name-field-media-image img', 'alt', 'Image Alt Text 1'); // Load the media and check that all fields are properly populated. $media = Media::load(1); $this->assertSame('example_1.jpeg', $media->getName()); $this->assertSame('200', $media->get('field_string_width')->value); $this->assertSame('89', $media->get('field_string_height')->value); + + // Tests the warning when the default display's image style is missing. + $this->drupalLogin($this->drupalCreateUser([ + 'administer site configuration', + 'access media overview', + 'administer media', + 'administer media types', + 'administer media display', + 'view media', + // We need 'access content' for system.machine_name_transliterate. + 'access content', + ])); + + $page = $this->getSession()->getPage(); + $assert_session = $this->assertSession(); + // If for some reason a site builder deletes the 'large' image style, do + // not add an image style to the new entity view display's image field. + // Instead, add a warning on the 'Status report' page. + 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 the form to complete with AJAX. + $this->assertNotEmpty($assert_session->waitForText('Field mapping')); + $page->pressButton('Save'); + $this->assertViewDisplayConfigured('madame_bonacieux'); + + // Create user without the 'administer media display' permission. + $this->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', + ])); + // Test that hook_requirements adds warning about the lack of an image + // style. + $this->drupalGet('/admin/reports/status'); + // The image style warning should not include an action link when the + // current user lacks the permission 'administer media display'. + $assert_session->pageTextContains('The default display for the Madame Bonacieux media type is not currently using an image style on the Image field. Not using an image style can lead to much larger file downloads.'); + $assert_session->linkNotExists('add an image style to the Image field'); + $assert_session->linkByHrefNotExists('/admin/structure/media/manage/madame_bonacieux/display'); + + // The image style warning should include an action link when the current + // user has the permission 'administer media display'. + Role::load(RoleInterface::AUTHENTICATED_ID) + ->grantPermission('administer media display') + ->save(); + $this->drupalGet('/admin/reports/status'); + $assert_session->pageTextContains('The default display for the Madame Bonacieux media type is not currently using an image style on the Image field. Not using an image style can lead to much larger file downloads. If you would like to change this, add an image style to the Image field.'); + $assert_session->linkExists('add an image style to the Image field'); + $assert_session->linkByHrefExists('/admin/structure/media/manage/madame_bonacieux/display'); + + // The image style warning should not include an action link when the + // Field UI module is uninstalled. + $this->container->get('module_installer')->uninstall(['field_ui']); + $this->drupalGet('/admin/reports/status'); + $assert_session->pageTextContains('The default display for the Madame Bonacieux media type is not currently using an image style on the Image field. Not using an image style can lead to much larger file downloads.'); + $assert_session->linkNotExists('add an image style to the Image field'); + $assert_session->linkByHrefNotExists('/admin/structure/media/manage/madame_bonacieux/display'); + } + + /** + * Asserts the proper entity view display components for a media type. + * + * @param string $media_type_id + * The media type ID. + */ + protected function assertViewDisplayConfigured($media_type_id) { + $assert_session = $this->assertSession(); + $type = MediaType::load($media_type_id); + $display = EntityViewDisplay::load('media.' . $media_type_id . '.default'); + $this->assertInstanceOf(EntityViewDisplay::class, $display); + $source_field_definition = $type->getSource()->getSourceFieldDefinition($type); + $component = $display->getComponent($source_field_definition->getName()); + $this->assertSame('visually_hidden', $component['label']); + if (ImageStyle::load('large')) { + $this->assertSame('large', $component['settings']['image_style']); + } + else { + $this->assertEmpty($component['settings']['image_style']); + } + $this->assertEmpty($component['settings']['image_link']); + + // Since components that aren't explicitly hidden can show up on the + // display edit form, check that only the image field appears enabled on + // the display edit form. + $this->drupalGet('/admin/structure/media/manage/' . $media_type_id . '/display'); + // Assert that only the source field is enabled. + $assert_session->elementExists('css', 'input[name="' . $source_field_definition->getName() . '_settings_edit"]'); + $assert_session->elementsCount('css', 'input[name$="_settings_edit"]', 1); } } diff --git a/core/modules/media/tests/src/FunctionalJavascript/MediaStandardProfileTest.php b/core/modules/media/tests/src/FunctionalJavascript/MediaStandardProfileTest.php index 3a6e4089c6..b7ddb9ed7e 100644 --- a/core/modules/media/tests/src/FunctionalJavascript/MediaStandardProfileTest.php +++ b/core/modules/media/tests/src/FunctionalJavascript/MediaStandardProfileTest.php @@ -230,10 +230,13 @@ protected function imageTest() { $assert_session->elementsCount('css', 'article.media--type-image > *', 1); // Assert the image element is present inside the media element and that its - // src attribute matches the image. + // src attribute uses the large image style, the label is visually hidden, + // and there is no link to the image file. $image_element = $assert_session->elementExists('css', 'article.media--type-image img'); - $expected_image_src = file_url_transform_relative(file_create_url(\Drupal::token()->replace('public://[date:custom:Y]-[date:custom:m]/' . $image_media_name))); - $this->assertSame($expected_image_src, $image_element->getAttribute('src')); + $expected_image_src = file_url_transform_relative(file_create_url(\Drupal::token()->replace('public://styles/large/public/[date:custom:Y]-[date:custom:m]/' . $image_media_name))); + $this->assertContains($expected_image_src, $image_element->getAttribute('src')); + $assert_session->elementExists('css', '.field--name-field-media-image .field__label.visually-hidden'); + $assert_session->elementNotExists('css', '.field--name-field-media-image a'); // Assert the media name is updated through the field mapping when changing // the source field. @@ -259,10 +262,13 @@ protected function imageTest() { $assert_session->elementsCount('css', 'article.media--type-image > *', 1); // Assert the image element is present inside the media element and that its - // src attribute matches the updated image. + // src attribute uses the large image style, the label is visually hidden, + // and there is no link to the image file. $image_element = $assert_session->elementExists('css', 'article.media--type-image img'); - $expected_image_src = file_url_transform_relative(file_create_url(\Drupal::token()->replace('public://[date:custom:Y]-[date:custom:m]/' . $image_media_name_updated))); - $this->assertSame($expected_image_src, $image_element->getAttribute('src')); + $expected_image_src = file_url_transform_relative(file_create_url(\Drupal::token()->replace('public://styles/large/public/[date:custom:Y]-[date:custom:m]/' . $image_media_name_updated))); + $this->assertContains($expected_image_src, $image_element->getAttribute('src')); + $assert_session->elementExists('css', '.field--name-field-media-image .field__label.visually-hidden'); + $assert_session->elementNotExists('css', '.field--name-field-media-image a'); } /** diff --git a/core/profiles/demo_umami/config/install/core.entity_view_display.media.image.default.yml b/core/profiles/demo_umami/config/install/core.entity_view_display.media.image.default.yml index 2d41440159..35bc622952 100644 --- a/core/profiles/demo_umami/config/install/core.entity_view_display.media.image.default.yml +++ b/core/profiles/demo_umami/config/install/core.entity_view_display.media.image.default.yml @@ -3,6 +3,7 @@ status: true dependencies: config: - field.field.media.image.field_media_image + - image.style.large - media.type.image module: - image @@ -14,8 +15,8 @@ content: field_media_image: label: visually_hidden settings: - image_style: '' - image_link: file + image_style: 'large' + image_link: '' third_party_settings: { } type: image weight: 1 diff --git a/core/profiles/standard/config/optional/core.entity_view_display.media.image.default.yml b/core/profiles/standard/config/optional/core.entity_view_display.media.image.default.yml index 728150f4e7..aeaebb2ecf 100644 --- a/core/profiles/standard/config/optional/core.entity_view_display.media.image.default.yml +++ b/core/profiles/standard/config/optional/core.entity_view_display.media.image.default.yml @@ -3,7 +3,7 @@ status: true dependencies: config: - field.field.media.image.field_media_image - - image.style.medium + - image.style.large - media.type.image module: - image @@ -15,8 +15,8 @@ content: field_media_image: label: visually_hidden settings: - image_style: '' - image_link: file + image_style: 'large' + image_link: '' third_party_settings: { } type: image weight: 1 diff --git a/core/profiles/standard/tests/src/Functional/StandardTest.php b/core/profiles/standard/tests/src/Functional/StandardTest.php index f62e183fca..1f7c111edf 100644 --- a/core/profiles/standard/tests/src/Functional/StandardTest.php +++ b/core/profiles/standard/tests/src/Functional/StandardTest.php @@ -4,6 +4,7 @@ use Drupal\Component\Utility\Html; use Drupal\media\Entity\MediaType; +use Drupal\media\Plugin\media\Source\Image; use Drupal\Tests\SchemaCheckTestTrait; use Drupal\contact\Entity\ContactForm; use Drupal\Core\Url; @@ -232,6 +233,7 @@ public function testStandard() { 'label' => 'Admin media', ]); $role->grantPermission('administer media'); + $role->grantPermission('administer media display'); $role->save(); $this->adminUser->addRole($role->id()); $this->adminUser->save(); @@ -256,6 +258,17 @@ 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)); + if (is_a($media_type->getSource(), Image::class, TRUE)) { + // Assert the default entity view display is configured with an image + // style. + $this->drupalGet('/admin/structure/media/manage/' . $media_type->id() . '/display'); + $assert_session->fieldValueEquals('fields[field_media_image][type]', 'image'); + $assert_session->elementTextContains('css', 'tr[data-drupal-selector="edit-fields-field-media-image"]', 'Image style: Large (480×480)'); + // By default for media types with an image source, only the image + // component should be enabled. + $assert_session->elementsCount('css', 'input[name$="_settings_edit"]', 1); + } + } }