From c9f39b6bea176a1e51cc9ad15f82d737df27963a Mon Sep 17 00:00:00 2001 From: Matei Stanca Date: Mon, 28 Mar 2022 13:52:53 -0400 Subject: [PATCH] Added all patches locally to mitigate potential security issues: https://github.com/cweagans/composer-patches/issues/347 --- composer.json | 6 +- patches/drupal/core/3042423-43.patch | 156 ++++ patches/drupal/core/3192234-116.patch | 740 ++++++++++++++++++ ...tion_required_alt_required_3181263-1.patch | 15 + 4 files changed, 914 insertions(+), 3 deletions(-) create mode 100644 patches/drupal/core/3042423-43.patch create mode 100644 patches/drupal/core/3192234-116.patch create mode 100644 patches/drupal/image_field_caption/image_field_caption_caption_required_alt_required_3181263-1.patch diff --git a/composer.json b/composer.json index 845c0ed..47bbf3b 100644 --- a/composer.json +++ b/composer.json @@ -21,11 +21,11 @@ "extra": { "patches": { "drupal/core": { - "Add a hook to modify oEmbed resource data (core 9.3.x) [#3042423]: https://www.drupal.org/project/drupal/issues/3042423#comment-14333467": "https://www.drupal.org/files/issues/2021-12-08/3042423-43.patch", - "Apply width and height attributes to responsive image tag (core 9.3.x) [#3192234]: https://www.drupal.org/project/drupal/issues/3192234#comment-14296101": "https://www.drupal.org/files/issues/2021-11-18/3192234-116.patch" + "Add a hook to modify oEmbed resource data (core 9.3.x) [#3042423]: https://www.drupal.org/project/drupal/issues/3042423#comment-14333467": "https://raw.githubusercontent.com/Ambient-Impact/drupal-modules/4.x/ambientimpact_media/patches/drupal/core/3042423-43.patch", + "Apply width and height attributes to responsive image tag (core 9.3.x) [#3192234]: https://www.drupal.org/project/drupal/issues/3192234#comment-14296101": "https://raw.githubusercontent.com/Ambient-Impact/drupal-modules/4.x/ambientimpact_media/patches/drupal/core/3192234-116.patch" }, "drupal/image_field_caption": { - "Caption required incorrectly based on alt field required https://www.drupal.org/project/image_field_caption/issues/3181263 ": "https://www.drupal.org/files/issues/2020-11-07/image_field_caption_caption_required_alt_required_3181263-1.patch" + "Caption required incorrectly based on alt field required: https://www.drupal.org/project/image_field_caption/issues/3181263#comment-13895775": "https://raw.githubusercontent.com/Ambient-Impact/drupal-modules/4.x/ambientimpact_media/patches/drupal/image_field_caption/image_field_caption_caption_required_alt_required_3181263-1.patch" } } } diff --git a/patches/drupal/core/3042423-43.patch b/patches/drupal/core/3042423-43.patch new file mode 100644 index 0000000..c277293 --- /dev/null +++ b/patches/drupal/core/3042423-43.patch @@ -0,0 +1,156 @@ +diff --git a/core/misc/cspell/dictionary.txt b/core/misc/cspell/dictionary.txt +index 40ebb8e9d1..059401acbe 100644 +--- a/core/misc/cspell/dictionary.txt ++++ b/core/misc/cspell/dictionary.txt +@@ -632,6 +632,7 @@ hookname + horizontalrule + hosters + hostnames ++hqdefault + hreflang + hreflangs + hrefs +@@ -807,6 +808,7 @@ maxage + maxdepth + maximumred + maxlifetime ++maxresdefault + maxsize + maynot + mbytes +diff --git a/core/modules/media/media.api.php b/core/modules/media/media.api.php +index 93244f58a8..5f7b4c7aec 100644 +--- a/core/modules/media/media.api.php ++++ b/core/modules/media/media.api.php +@@ -20,6 +20,21 @@ function hook_media_source_info_alter(array &$sources) { + $sources['youtube']['label'] = t('Youtube rocks!'); + } + ++/** ++ * Alters the information provided by the oEmbed resource url. ++ * ++ * @param array $data ++ * Data provided by the oEmbed resource. ++ * @param $url ++ * The oEmbed resource URL. ++ */ ++function hook_oembed_resource_data_alter(array &$data, $url) { ++ if (strpos($url, 'youtube.com/oembed') !== FALSE) { ++ // Get the maximum resolution thumbnail from YouTube. ++ $data['thumbnail_url'] = str_replace('hqdefault', 'maxresdefault', $data['thumbnail_url']); ++ } ++} ++ + /** + * Alters an oEmbed resource URL before it is fetched. + * +diff --git a/core/modules/media/media.services.yml b/core/modules/media/media.services.yml +index 847e9e3d48..b4119f1de0 100644 +--- a/core/modules/media/media.services.yml ++++ b/core/modules/media/media.services.yml +@@ -16,7 +16,7 @@ services: + arguments: ['@http_client', '@config.factory', '@datetime.time', '@keyvalue', '@logger.factory'] + media.oembed.resource_fetcher: + class: Drupal\media\OEmbed\ResourceFetcher +- arguments: ['@http_client', '@media.oembed.provider_repository', '@cache.default'] ++ arguments: ['@http_client', '@media.oembed.provider_repository', '@module_handler', '@cache.default'] + media.oembed.iframe_url_helper: + class: Drupal\media\IFrameUrlHelper + arguments: ['@router.request_context', '@private_key'] +diff --git a/core/modules/media/src/OEmbed/ResourceFetcher.php b/core/modules/media/src/OEmbed/ResourceFetcher.php +index 39e7dd147f..4e0c40b9bc 100644 +--- a/core/modules/media/src/OEmbed/ResourceFetcher.php ++++ b/core/modules/media/src/OEmbed/ResourceFetcher.php +@@ -4,6 +4,7 @@ + + use Drupal\Component\Serialization\Json; + use Drupal\Core\Cache\CacheBackendInterface; ++use Drupal\Core\Extension\ModuleHandlerInterface; + use GuzzleHttp\ClientInterface; + use GuzzleHttp\Exception\TransferException; + use GuzzleHttp\RequestOptions; +@@ -27,6 +28,13 @@ class ResourceFetcher implements ResourceFetcherInterface { + */ + protected $providers; + ++ /** ++ * The module handler service. ++ * ++ * @var \Drupal\Core\Extension\ModuleHandlerInterface ++ */ ++ protected $moduleHandler; ++ + /** + * The cache backend. + * +@@ -41,10 +49,12 @@ class ResourceFetcher implements ResourceFetcherInterface { + * The HTTP client. + * @param \Drupal\media\OEmbed\ProviderRepositoryInterface $providers + * The oEmbed provider repository service. ++ * @param \Drupal\Core\Extension\ModuleHandlerInterface $moduleHandler ++ * The module handler service. + * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend + * The cache backend. + */ +- public function __construct(ClientInterface $http_client, ProviderRepositoryInterface $providers, CacheBackendInterface $cache_backend = NULL) { ++ public function __construct(ClientInterface $http_client, ProviderRepositoryInterface $providers, ModuleHandlerInterface $moduleHandler, CacheBackendInterface $cache_backend = NULL) { + $this->httpClient = $http_client; + $this->providers = $providers; + if (empty($cache_backend)) { +@@ -52,6 +62,7 @@ public function __construct(ClientInterface $http_client, ProviderRepositoryInte + @trigger_error('Passing NULL as the $cache_backend parameter to ' . __METHOD__ . '() is deprecated in drupal:9.3.0 and is removed from drupal:10.0.0. See https://www.drupal.org/node/3223594', E_USER_DEPRECATED); + } + $this->cacheBackend = $cache_backend; ++ $this->moduleHandler = $moduleHandler; + } + + /** +@@ -92,6 +103,8 @@ public function fetchResource($url) { + throw new ResourceException('The oEmbed resource could not be decoded.', $url); + } + ++ $this->moduleHandler->alter('oembed_resource_data', $data, $url); ++ + $this->cacheBackend->set($cache_id, $data); + + return $this->createResource($data, $url); +diff --git a/core/modules/media/tests/src/Kernel/ResourceFetcherTest.php b/core/modules/media/tests/src/Kernel/ResourceFetcherTest.php +index e68810f348..6e408f8517 100644 +--- a/core/modules/media/tests/src/Kernel/ResourceFetcherTest.php ++++ b/core/modules/media/tests/src/Kernel/ResourceFetcherTest.php +@@ -21,7 +21,8 @@ public function testDeprecations(): void { + $this->expectDeprecation('Passing NULL as the $cache_backend parameter to Drupal\media\OEmbed\ResourceFetcher::__construct() is deprecated in drupal:9.3.0 and is removed from drupal:10.0.0. See https://www.drupal.org/node/3223594'); + new ResourceFetcher( + $this->container->get('http_client'), +- $this->createMock('\Drupal\media\OEmbed\ProviderRepositoryInterface') ++ $this->createMock('\Drupal\media\OEmbed\ProviderRepositoryInterface'), ++ $this->createMock('\Drupal\Core\Extension\ModuleHandlerInterface') + ); + } + +diff --git a/core/modules/media/tests/src/Unit/ResourceFetcherTest.php b/core/modules/media/tests/src/Unit/ResourceFetcherTest.php +index 8cd96683c9..327a628a65 100644 +--- a/core/modules/media/tests/src/Unit/ResourceFetcherTest.php ++++ b/core/modules/media/tests/src/Unit/ResourceFetcherTest.php +@@ -43,6 +43,7 @@ public function testFetchTimeout(): void { + $fetcher = new ResourceFetcher( + $client->reveal(), + $this->createMock('\Drupal\media\OEmbed\ProviderRepositoryInterface'), ++ $this->createMock('\Drupal\Core\Extension\ModuleHandlerInterface'), + new NullBackend('default') + ); + $fetcher->fetchResource($url); +@@ -80,7 +81,12 @@ public function testUnknownContentTypeHeader(): void { + ]); + $providers = $this->createMock('\Drupal\media\OEmbed\ProviderRepositoryInterface'); + +- $fetcher = new ResourceFetcher($client, $providers, new NullBackend('default')); ++ $fetcher = new ResourceFetcher( ++ $client, ++ $providers, ++ $this->createMock('\Drupal\Core\Extension\ModuleHandlerInterface'), ++ new NullBackend('default') ++ ); + /** @var \Drupal\media\OEmbed\Resource $resource */ + $resource = $fetcher->fetchResource('valid'); + // The resource should have been successfully decoded as JSON. diff --git a/patches/drupal/core/3192234-116.patch b/patches/drupal/core/3192234-116.patch new file mode 100644 index 0000000..1a65761 --- /dev/null +++ b/patches/drupal/core/3192234-116.patch @@ -0,0 +1,740 @@ +diff --git a/core/modules/responsive_image/config/schema/responsive_image.schema.yml b/core/modules/responsive_image/config/schema/responsive_image.schema.yml +index f05a8e2608..8fae617593 100644 +--- a/core/modules/responsive_image/config/schema/responsive_image.schema.yml ++++ b/core/modules/responsive_image/config/schema/responsive_image.schema.yml +@@ -67,3 +67,10 @@ field.formatter.settings.responsive_image: + image_link: + type: string + label: 'Link image to' ++ image_loading: ++ type: mapping ++ label: 'Image loading settings' ++ mapping: ++ attribute: ++ type: string ++ label: 'Loading attribute' +diff --git a/core/modules/responsive_image/responsive_image.module b/core/modules/responsive_image/responsive_image.module +index 6bd0e51f28..69523d2e7e 100644 +--- a/core/modules/responsive_image/responsive_image.module ++++ b/core/modules/responsive_image/responsive_image.module +@@ -32,11 +32,11 @@ function responsive_image_help($route_name, RouteMatchInterface $route_match) { + $output .= '
' . t('Fallback image style') . '
'; + $output .= '
' . t('The fallback image style is typically the smallest size image you expect to appear in this space. Because the responsive images module uses the Picturefill library so that responsive images can work in older browsers, the fallback image should only appear on a site if an error occurs.') . '
'; + $output .= '
' . t('Breakpoint groups: viewport sizing vs art direction') . '
'; +- $output .= '
' . t('The breakpoint group typically only needs a single breakpoint with an empty media query in order to do viewport sizing. Multiple breakpoints are used for changing the crop or aspect ratio of images at different viewport sizes, which is often referred to as art direction. Once you select a breakpoint group, you can choose which breakpoints to use for the responsive image style. By default, the option do not use this breakpoint is selected for each breakpoint. See the help page of the Breakpoint module for more information.', [':breakpoint_help' => Url::fromRoute('help.page', ['name' => 'breakpoint'])->toString()]) . '
'; ++ $output .= '
' . t('The breakpoint group typically only needs a single breakpoint with an empty media query in order to do viewport sizing. Multiple breakpoints are used for changing the crop or aspect ratio of images at different viewport sizes, which is often referred to as art direction. A new breakpoint group should be created for each aspect ratio to avoid content shift. Once you select a breakpoint group, you can choose which breakpoints to use for the responsive image style. By default, the option do not use this breakpoint is selected for each breakpoint. See the help page of the Breakpoint module for more information.', [':breakpoint_help' => Url::fromRoute('help.page', ['name' => 'breakpoint'])->toString()]) . '
'; + $output .= '
' . t('Breakpoint settings: sizes vs image styles') . '
'; +- $output .= '
' . t('While you have the option to provide only one image style per breakpoint, the sizes option allows you to provide more options to browsers as to which image file it can display, even when using multiple breakpoints for art direction. Breakpoints are defined in the configuration files of the theme.') . '
'; ++ $output .= '
' . t('While you have the option to provide only one image style per breakpoint, the sizes attribute allows you to provide more options to browsers as to which image file it can display. If using sizes field and art direction, all selected image styles should use the same aspect ratio to avoid content shifting. Breakpoints are defined in the configuration files of the theme.') . '
'; + $output .= '
' . t('Sizes field') . '
'; +- $output .= '
' . t('Once the sizes option is selected, you can let the browser know the size of this image in relation to the site layout, using the Sizes field. For a hero image that always fills the entire screen, you could simply enter 100vw, which means 100% of the viewport width. For an image that fills 90% of the screen for small viewports, but only fills 40% of the screen when the viewport is larger than 40em (typically 640px), you could enter "(min-width: 40em) 40vw, 90vw" in the Sizes field. The last item in the comma-separated list is the smallest viewport size: other items in the comma-separated list should have a media condition paired with an image width. Media conditions are similar to a media query, often a min-width paired with a viewport width using em or px units: e.g. (min-width: 640px) or (min-width: 40em). This is paired with the image width at that viewport size using px, em or vw units. The vw unit is viewport width and is used instead of a percentage because the percentage always refers to the width of the entire viewport.') . '
'; ++ $output .= '
' . t('The sizes attribute paired with the srcset attribute provides information on how much space these images take up within the viewport at different browser breakpoints, but the aspect ratios should remain the same across those breakpoints. Once the sizes option is selected, you can let the browser know the size of this image in relation to the site layout, using the Sizes field. For a hero image that always fills the entire screen, you could simply enter 100vw, which means 100% of the viewport width. For an image that fills 90% of the screen for small viewports, but only fills 40% of the screen when the viewport is larger than 40em (typically 640px), you could enter "(min-width: 40em) 40vw, 90vw" in the Sizes field. The last item in the comma-separated list is the smallest viewport size: other items in the comma-separated list should have a media condition paired with an image width. Media conditions are similar to a media query, often a min-width paired with a viewport width using em or px units: e.g. (min-width: 640px) or (min-width: 40em). This is paired with the image width at that viewport size using px, em or vw units. The vw unit is viewport width and is used instead of a percentage because the percentage always refers to the width of the entire viewport.') . '
'; + $output .= '
' . t('Image styles for sizes') . '
'; + $output .= '
' . t('Below the Sizes field you can choose multiple image styles so the browser can choose the best image file size to fill the space defined in the Sizes field. Typically you will want to use image styles that resize your image to have options that range from the smallest px width possible for the space the image will appear in to the largest px width possible, with a variety of widths in between. You may want to provide image styles with widths that are 1.5x to 2x the space available in the layout to account for high resolution screens. Image styles can be defined on the Image styles page that is provided by the Image module.', [':image_styles' => Url::fromRoute('entity.image_style.collection')->toString(), ':image_help' => Url::fromRoute('help.page', ['name' => 'image'])->toString()]) . '
'; + $output .= ''; +@@ -212,6 +212,15 @@ function template_preprocess_responsive_image(&$variables) { + } + $variables['img_element']['#attributes'] = $variables['attributes']; + } ++ ++ // Get width and height from fallback responsive image style and transfer them ++ // to img tag so browser can do aspect ratio calculation and prevent ++ // recalculation of layout on image load. ++ if (isset($variables['width'], $variables['height'])) { ++ $dimensions = responsive_image_get_image_dimensions($responsive_image_style->getFallbackImageStyle(), ['width' => $variables['width'], 'height' => $variables['height']], $variables['uri']); ++ $variables['img_element']['#width'] = $dimensions['width']; ++ $variables['img_element']['#height'] = $dimensions['height']; ++ } + } + + /** +@@ -403,13 +412,15 @@ function _responsive_image_build_source_attributes(array $variables, BreakpointI + // be used. We multiply it by 100 so multipliers with up to two decimals + // can be used. + $srcset[intval(mb_substr($multiplier, 0, -1) * 100)] = _responsive_image_image_style_url($image_style_mapping['image_mapping'], $variables['uri']) . ' ' . $multiplier; ++ $dimensions = responsive_image_get_image_dimensions($image_style_mapping['image_mapping'], ['width' => $width, 'height' => $height], $variables['uri']); + break; + } + } + // Sort the srcset from small to large image width or multiplier. + ksort($srcset); ++ $srcset = array_unique($srcset); + $source_attributes = new Attribute([ +- 'srcset' => implode(', ', array_unique($srcset)), ++ 'srcset' => implode(', ', $srcset), + ]); + $media_query = trim($breakpoint->getMediaQuery()); + if (!empty($media_query)) { +@@ -421,6 +432,21 @@ function _responsive_image_build_source_attributes(array $variables, BreakpointI + if (!empty($sizes)) { + $source_attributes->setAttribute('sizes', implode(',', array_unique($sizes))); + } ++ // The images used in a particular srcset attribute should all have the same ++ // aspect ratio. The sizes attribute paired with the srcset attribute provides ++ // information on how much space these images take up within the viewport at ++ // different breakpoints, but the aspect ratios should remain the same across ++ // those breakpoints. Multiple source elements can be used for art direction, ++ // where aspect ratios should change at particular breakpoints. Each source ++ // element can still have srcset and sizes attributes to handle variations for ++ // that particular aspect ratio. Because the same aspect ratio is assumed for ++ // all images in a srcset, dimensions are always added to the source ++ // attribute. Within srcset, images are sorted from largest to smallest in ++ // terms of the real dimension of the image. ++ if (!empty($dimensions['width']) && !empty($dimensions['height'])) { ++ $source_attributes->setAttribute('width', $dimensions['width']); ++ $source_attributes->setAttribute('height', $dimensions['height']); ++ } + return $source_attributes; + } + +diff --git a/core/modules/responsive_image/responsive_image.post_update.php b/core/modules/responsive_image/responsive_image.post_update.php +index 70093a9eda..4fc6f00cac 100644 +--- a/core/modules/responsive_image/responsive_image.post_update.php ++++ b/core/modules/responsive_image/responsive_image.post_update.php +@@ -13,3 +13,24 @@ function responsive_image_removed_post_updates() { + 'responsive_image_post_update_recreate_dependencies' => '9.0.0', + ]; + } ++ ++/** ++ * Add the image loading settings to responsive image field formatter instances. ++ */ ++function responsive_image_post_update_image_loading_attribute() { ++ $storage = \Drupal::entityTypeManager()->getStorage('entity_view_display'); ++ foreach ($storage->loadMultiple() as $id => $view_display) { ++ $changed = FALSE; ++ /** @var \Drupal\Core\Entity\Display\EntityViewDisplayInterface $view_display */ ++ foreach ($view_display->getComponents() as $field => $component) { ++ if (isset($component['type']) && ($component['type'] === 'responsive_image')) { ++ $component['settings']['image_loading']['attribute'] = 'lazy'; ++ $view_display->setComponent($field, $component); ++ $changed = TRUE; ++ } ++ } ++ if ($changed) { ++ $view_display->save(); ++ } ++ } ++} +diff --git a/core/modules/responsive_image/src/Plugin/Field/FieldFormatter/ResponsiveImageFormatter.php b/core/modules/responsive_image/src/Plugin/Field/FieldFormatter/ResponsiveImageFormatter.php +index 331c278e0c..ff9dd44ae9 100644 +--- a/core/modules/responsive_image/src/Plugin/Field/FieldFormatter/ResponsiveImageFormatter.php ++++ b/core/modules/responsive_image/src/Plugin/Field/FieldFormatter/ResponsiveImageFormatter.php +@@ -118,6 +118,9 @@ public static function defaultSettings() { + return [ + 'responsive_image_style' => '', + 'image_link' => '', ++ 'image_loading' => [ ++ 'attribute' => 'lazy', ++ ], + ] + parent::defaultSettings(); + } + +@@ -125,6 +128,8 @@ public static function defaultSettings() { + * {@inheritdoc} + */ + public function settingsForm(array $form, FormStateInterface $form_state) { ++ $elements = parent::settingsForm($form, $form_state); ++ + $responsive_image_options = []; + $responsive_image_styles = $this->responsiveImageStyleStorage->loadMultiple(); + uasort($responsive_image_styles, '\Drupal\responsive_image\Entity\ResponsiveImageStyle::sort'); +@@ -148,6 +153,27 @@ public function settingsForm(array $form, FormStateInterface $form_state) { + ], + ]; + ++ $image_loading = $this->getSetting('image_loading'); ++ $elements['image_loading'] = [ ++ '#type' => 'details', ++ '#title' => $this->t('Image loading'), ++ '#weight' => 10, ++ '#description' => $this->t('Image assets are rendered with native browser loading attribute of (loading="lazy") by default. This improves performance by allowing modern browsers to lazily load images without JavaScript. It is sometimes desirable to override this default to force browsers to download an image as soon as possible using the "eager" value instead.'), ++ ]; ++ $loading_attribute_options = [ ++ 'lazy' => $this->t('Lazy'), ++ 'eager' => $this->t('Eager'), ++ ]; ++ $elements['image_loading']['attribute'] = [ ++ '#title' => $this->t('Lazy loading attribute'), ++ '#type' => 'select', ++ '#default_value' => $image_loading['attribute'], ++ '#options' => $loading_attribute_options, ++ '#description' => $this->t('Select the lazy loading attribute for images. Learn more.', [ ++ ':link' => 'https://html.spec.whatwg.org/multipage/urls-and-fetching.html#lazy-loading-attributes', ++ ]), ++ ]; ++ + $link_types = [ + 'content' => t('Content'), + 'file' => t('File'), +@@ -186,7 +212,12 @@ public function settingsSummary() { + $summary[] = t('Select a responsive image style.'); + } + +- return $summary; ++ $image_loading = $this->getSetting('image_loading'); ++ $summary[] = $this->t('Loading attribute: @attribute', [ ++ '@attribute' => $image_loading['attribute'], ++ ]); ++ ++ return array_merge($summary, parent::settingsSummary()); + } + + /** +@@ -239,6 +270,9 @@ public function viewElements(FieldItemListInterface $items, $langcode) { + $item_attributes = $item->_attributes; + unset($item->_attributes); + ++ $image_loading_settings = $this->getSetting('image_loading'); ++ $item_attributes['loading'] = $image_loading_settings['attribute']; ++ + $elements[$delta] = [ + '#theme' => 'responsive_image_formatter', + '#item' => $item, +diff --git a/core/modules/responsive_image/tests/modules/responsive_image_test_module/config/schema/responsive_image_test_module.schema.yml b/core/modules/responsive_image/tests/modules/responsive_image_test_module/config/schema/responsive_image_test_module.schema.yml +index 6208012735..549a1bb928 100644 +--- a/core/modules/responsive_image/tests/modules/responsive_image_test_module/config/schema/responsive_image_test_module.schema.yml ++++ b/core/modules/responsive_image/tests/modules/responsive_image_test_module/config/schema/responsive_image_test_module.schema.yml +@@ -9,3 +9,10 @@ field.formatter.settings.responsive_image_test: + image_link: + type: string + label: 'Link image to' ++ image_loading: ++ type: mapping ++ label: 'Image loading settings' ++ mapping: ++ attribute: ++ type: string ++ label: 'Loading attribute' +diff --git a/core/modules/responsive_image/tests/src/Functional/ResponsiveImageFieldDisplayTest.php b/core/modules/responsive_image/tests/src/Functional/ResponsiveImageFieldDisplayTest.php +index b56b027fec..73142c793b 100644 +--- a/core/modules/responsive_image/tests/src/Functional/ResponsiveImageFieldDisplayTest.php ++++ b/core/modules/responsive_image/tests/src/Functional/ResponsiveImageFieldDisplayTest.php +@@ -318,6 +318,11 @@ protected function doTestResponsiveImageFieldFormatters($scheme, $empty_styles = + $medium_style = ImageStyle::load('medium'); + $this->assertSession()->responseContains($this->fileUrlGenerator->transformRelative($medium_style->buildUrl($image_uri)) . ' 220w, ' . $this->fileUrlGenerator->transformRelative($large_style->buildUrl($image_uri)) . ' 360w'); + $this->assertSession()->responseContains('media="(min-width: 851px)"'); ++ // Assert the output of the 'width' attribute. ++ $this->assertSession()->responseContains('width="360"'); ++ // Assert the output of the 'height' attribute. ++ $this->assertSession()->responseContains('height="240"'); ++ $this->assertSession()->responseContains('loading="lazy"'); + } + $this->assertSession()->responseContains('/styles/large/'); + $this->assertSession()->responseHeaderContains('X-Drupal-Cache-Tags', 'config:responsive_image.styles.style_one'); +@@ -334,6 +339,9 @@ protected function doTestResponsiveImageFieldFormatters($scheme, $empty_styles = + '#theme' => 'image', + '#alt' => $alt, + '#uri' => $this->fileUrlGenerator->transformRelative($large_style->buildUrl($image->getSource())), ++ '#width' => 360, ++ '#height' => 240, ++ '#attributes' => ['loading' => 'lazy'], + ]; + // The image.html.twig template has a newline after the tag but + // responsive-image.html.twig doesn't have one after the fallback image, so +@@ -417,9 +425,9 @@ public function testResponsiveImageFieldFormattersEmptyMediaQuery() { + } + + /** +- * Tests responsive image formatter on node display with one source. ++ * Tests responsive image formatter on node display with one and two sources. + */ +- public function testResponsiveImageFieldFormattersOneSource() { ++ public function testResponsiveImageFieldFormattersMultipleSources() { + $this->responsiveImgStyle + // Test the output of an empty media query. + ->addImageStyleMapping('responsive_image_test_module.empty', '1x', [ +@@ -445,6 +453,10 @@ public function testResponsiveImageFieldFormattersOneSource() { + 'settings' => [ + 'image_link' => '', + 'responsive_image_style' => 'style_one', ++ 'image_loading' => [ ++ // Test the image loading default option can be overridden. ++ 'attribute' => 'eager', ++ ], + ], + ]; + $display = \Drupal::service('entity_display.repository') +@@ -455,12 +467,26 @@ public function testResponsiveImageFieldFormattersOneSource() { + // View the node. + $this->drupalGet('node/' . $nid); + +- // Assert the media attribute is present if it has a value. ++ // Assert the img tag has medium and large images. + $large_style = ImageStyle::load('large'); + $medium_style = ImageStyle::load('medium'); + $node = $node_storage->load($nid); + $image_uri = File::load($node->{$field_name}->target_id)->getFileUri(); +- $this->assertSession()->responseContains('fileUrlGenerator->transformRelative($medium_style->buildUrl($image_uri)); ++ $large_transform_url = $this->fileUrlGenerator->transformRelative($large_style->buildUrl($image_uri)); ++ $this->assertSession()->responseMatches('/\w+/'); ++ ++ $this->responsiveImgStyle ++ // Test the output of an empty media query. ++ ->addImageStyleMapping('responsive_image_test_module.wide', '1x', [ ++ 'image_mapping_type' => 'image_style', ++ 'image_mapping' => 'large', ++ ]) ++ ->save(); ++ ++ // Assert the picture tag has source tags that include dimensions. ++ $this->drupalGet('node/' . $nid); ++ $this->assertSession()->responseMatches('/\s+' . '\s+\s+\w+\s+<\/picture>/'); + } + + /** +diff --git a/core/modules/responsive_image/tests/src/Functional/ResponsiveImageLazyLoadUpdateTest.php b/core/modules/responsive_image/tests/src/Functional/ResponsiveImageLazyLoadUpdateTest.php +new file mode 100644 +index 0000000000..6cba77f744 +--- /dev/null ++++ b/core/modules/responsive_image/tests/src/Functional/ResponsiveImageLazyLoadUpdateTest.php +@@ -0,0 +1,76 @@ ++fieldName = mb_strtolower($this->randomMachineName()); ++ $this->createImageField($this->fieldName, 'article'); ++ ++ // Legacy display options without any image loading options. ++ $display_options = [ ++ 'type' => 'responsive_image', ++ 'settings' => [ ++ 'responsive_image_style' => '', ++ 'image_link' => '', ++ 'image_loading' => [], ++ ], ++ ]; ++ $display = $this->container->get('entity_type.manager') ++ ->getStorage('entity_view_display') ++ ->load('node.article.default'); ++ if (!$display) { ++ $values = [ ++ 'targetEntityType' => 'node', ++ 'bundle' => 'article', ++ 'mode' => 'default', ++ 'status' => TRUE, ++ ]; ++ $display = $this->container->get('entity_type.manager')->getStorage('entity_view_display')->create($values); ++ } ++ $display->setComponent($this->fieldName, $display_options)->save(); ++ $component = $display->getComponent($this->fieldName); ++ $this->assertArrayHasKey('image_loading', $component['settings']); ++ $this->assertArrayNotHasKey('attribute', $component['settings']['image_loading']); ++ } ++ ++ /** ++ * {@inheritdoc} ++ */ ++ protected function setDatabaseDumpFiles(): void { ++ $this->databaseDumpFiles = [ ++ __DIR__ . '/../../../../system/tests/fixtures/update/drupal-8.8.0.filled.standard.php.gz', ++ ]; ++ } ++ ++ /** ++ * Test new lazy-load setting upgrade path. ++ * ++ * @see responsive_image_post_update_image_loading_attribute ++ */ ++ public function testUpdate(): void { ++ $storage = \Drupal::entityTypeManager()->getStorage('entity_view_display'); ++ $this->runUpdates(); ++ /** @var \Drupal\Core\Entity\Display\EntityViewDisplayInterface $view_display */ ++ $view_display = $storage->load('node.article.default'); ++ $component = $view_display->getComponent($this->fieldName); ++ $this->assertArrayHasKey('image_loading', $component['settings']); ++ $this->assertEquals('lazy', $component['settings']['image_loading']['attribute']); ++ } ++ ++} +diff --git a/core/modules/responsive_image/tests/src/FunctionalJavascript/ResponsiveImageFieldUiTest.php b/core/modules/responsive_image/tests/src/FunctionalJavascript/ResponsiveImageFieldUiTest.php +index 7ecfd5fbec..9d722a6c8b 100644 +--- a/core/modules/responsive_image/tests/src/FunctionalJavascript/ResponsiveImageFieldUiTest.php ++++ b/core/modules/responsive_image/tests/src/FunctionalJavascript/ResponsiveImageFieldUiTest.php +@@ -95,7 +95,7 @@ public function testResponsiveImageFormatterUi() { + $field_image_type->setValue('responsive_image'); + + $summary_text = $assert_session->waitForElement('xpath', $this->cssSelectToXpath('#field-image .ajax-new-content .field-plugin-summary')); +- $this->assertEquals('Select a responsive image style.', $summary_text->getText()); ++ $this->assertEquals('Select a responsive image style. Loading attribute: lazy', $summary_text->getText()); + + $page->pressButton('Save'); + $assert_session->responseContains("Select a responsive image style."); +diff --git a/core/modules/views/src/ViewsConfigUpdater.php b/core/modules/views/src/ViewsConfigUpdater.php +index dfb89e338a..dcf262a5a0 100644 +--- a/core/modules/views/src/ViewsConfigUpdater.php ++++ b/core/modules/views/src/ViewsConfigUpdater.php +@@ -480,6 +480,50 @@ protected function mapOperatorFromSingleToMultiple($single_operator) { + } + } + ++ /** ++ * Add lazy load options to all responsive_image type field configurations. ++ * ++ * @param \Drupal\views\ViewEntityInterface $view ++ * The View to update. ++ * ++ * @return bool ++ * Whether the view was updated. ++ */ ++ public function needsResponsiveImageLazyLoadFieldUpdate(ViewEntityInterface $view): bool { ++ return $this->processDisplayHandlers($view, TRUE, function (&$handler, $handler_type) use ($view) { ++ return $this->processResponsiveImageLazyLoadFieldHandler($handler, $handler_type, $view); ++ }); ++ } ++ ++ /** ++ * Processes responsive_image type fields. ++ * ++ * @param array $handler ++ * A display handler. ++ * @param string $handler_type ++ * The handler type. ++ * @param \Drupal\views\ViewEntityInterface $view ++ * The View being updated. ++ * ++ * @return bool ++ * Whether the handler was updated. ++ */ ++ protected function processResponsiveImageLazyLoadFieldHandler(array &$handler, string $handler_type, ViewEntityInterface $view): bool { ++ $changed = FALSE; ++ ++ // Add any missing settings for lazy loading. ++ if (($handler_type === 'field') ++ && isset($handler['plugin_id'], $handler['type']) ++ && $handler['plugin_id'] === 'field' ++ && $handler['type'] === 'responsive_image' ++ && !isset($handler['settings']['image_loading'])) { ++ $handler['settings']['image_loading'] = ['attribute' => 'lazy']; ++ $changed = TRUE; ++ } ++ ++ return $changed; ++ } ++ + /** + * Updates the sort handlers by adding default sort field identifiers. + * +diff --git a/core/modules/views/tests/fixtures/update/views.view.test_responsive_images.yml b/core/modules/views/tests/fixtures/update/views.view.test_responsive_images.yml +new file mode 100644 +index 0000000000..6537c27184 +--- /dev/null ++++ b/core/modules/views/tests/fixtures/update/views.view.test_responsive_images.yml +@@ -0,0 +1,224 @@ ++uuid: 6a7eb126-7ba9-493f-a209-e3aa0672b8f5 ++langcode: en ++status: true ++dependencies: ++ config: ++ - field.storage.entity_test.bar ++ - responsive_image.styles.responsive_image_style_id ++ module: ++ - entity_test ++ - responsive_image ++id: test_responsive_images ++label: 'Responsive Images' ++module: views ++description: '' ++tag: '' ++base_table: entity_test ++base_field: id ++display: ++ default: ++ display_plugin: default ++ id: default ++ display_title: Default ++ position: 0 ++ display_options: ++ access: ++ type: none ++ options: { } ++ cache: ++ type: tag ++ options: { } ++ query: ++ type: views_query ++ options: ++ disable_sql_rewrite: false ++ distinct: false ++ replica: false ++ query_comment: '' ++ query_tags: { } ++ exposed_form: ++ type: basic ++ options: ++ submit_button: Apply ++ reset_button: false ++ reset_button_label: Reset ++ exposed_sorts_label: 'Sort by' ++ expose_sort_order: true ++ sort_asc_label: Asc ++ sort_desc_label: Desc ++ pager: ++ type: mini ++ options: ++ items_per_page: 10 ++ offset: 0 ++ id: 0 ++ total_pages: null ++ expose: ++ items_per_page: false ++ items_per_page_label: 'Items per page' ++ items_per_page_options: '5, 10, 25, 50' ++ items_per_page_options_all: false ++ items_per_page_options_all_label: '- All -' ++ offset: false ++ offset_label: Offset ++ tags: ++ previous: ‹‹ ++ next: ›› ++ style: ++ type: default ++ options: ++ grouping: { } ++ row_class: '' ++ default_row_class: true ++ uses_fields: false ++ row: ++ type: fields ++ options: ++ inline: { } ++ separator: '' ++ hide_empty: false ++ default_field_elements: true ++ fields: ++ name: ++ table: entity_test ++ field: name ++ id: name ++ entity_type: null ++ entity_field: name ++ plugin_id: field ++ relationship: none ++ group_type: group ++ admin_label: '' ++ label: '' ++ exclude: false ++ alter: ++ alter_text: false ++ text: '' ++ make_link: false ++ path: '' ++ absolute: false ++ external: false ++ replace_spaces: false ++ path_case: none ++ trim_whitespace: false ++ alt: '' ++ rel: '' ++ link_class: '' ++ prefix: '' ++ suffix: '' ++ target: '' ++ nl2br: false ++ max_length: 0 ++ word_boundary: true ++ ellipsis: true ++ more_link: false ++ more_link_text: '' ++ more_link_path: '' ++ strip_tags: false ++ trim: false ++ preserve_tags: '' ++ html: false ++ element_type: '' ++ element_class: '' ++ element_label_type: '' ++ element_label_class: '' ++ element_label_colon: true ++ element_wrapper_type: '' ++ element_wrapper_class: '' ++ element_default_classes: true ++ empty: '' ++ hide_empty: false ++ empty_zero: false ++ hide_alter_empty: true ++ click_sort_column: value ++ type: string ++ settings: { } ++ group_column: value ++ group_columns: { } ++ group_rows: true ++ delta_limit: 0 ++ delta_offset: 0 ++ delta_reversed: false ++ delta_first_last: false ++ multi_type: separator ++ separator: ', ' ++ field_api_classes: false ++ bar: ++ id: bar ++ table: entity_test__bar ++ field: bar ++ relationship: none ++ group_type: group ++ admin_label: '' ++ label: '' ++ exclude: false ++ alter: ++ alter_text: false ++ text: '' ++ make_link: false ++ path: '' ++ absolute: false ++ external: false ++ replace_spaces: false ++ path_case: none ++ trim_whitespace: false ++ alt: '' ++ rel: '' ++ link_class: '' ++ prefix: '' ++ suffix: '' ++ target: '' ++ nl2br: false ++ max_length: 0 ++ word_boundary: true ++ ellipsis: true ++ more_link: false ++ more_link_text: '' ++ more_link_path: '' ++ strip_tags: false ++ trim: false ++ preserve_tags: '' ++ html: false ++ element_type: '' ++ element_class: '' ++ element_label_type: '' ++ element_label_class: '' ++ element_label_colon: false ++ element_wrapper_type: '' ++ element_wrapper_class: '' ++ element_default_classes: true ++ empty: '' ++ hide_empty: false ++ empty_zero: false ++ hide_alter_empty: true ++ click_sort_column: target_id ++ type: responsive_image ++ settings: ++ responsive_image_style: responsive_image_style_id ++ image_link: '' ++ group_column: '' ++ group_columns: { } ++ group_rows: true ++ delta_limit: 0 ++ delta_offset: 0 ++ delta_reversed: false ++ delta_first_last: false ++ multi_type: separator ++ separator: ', ' ++ field_api_classes: false ++ plugin_id: field ++ filters: { } ++ sorts: { } ++ header: { } ++ footer: { } ++ empty: { } ++ relationships: { } ++ arguments: { } ++ display_extenders: { } ++ cache_metadata: ++ max-age: -1 ++ contexts: ++ - entity_test_view_grants ++ - 'languages:language_content' ++ - 'languages:language_interface' ++ - url.query_args +diff --git a/core/modules/views/tests/src/Kernel/ViewsConfigUpdaterTest.php b/core/modules/views/tests/src/Kernel/ViewsConfigUpdaterTest.php +index 2c4b507eac..554f108364 100644 +--- a/core/modules/views/tests/src/Kernel/ViewsConfigUpdaterTest.php ++++ b/core/modules/views/tests/src/Kernel/ViewsConfigUpdaterTest.php +@@ -3,6 +3,10 @@ + namespace Drupal\Tests\views\Kernel; + + use Drupal\Core\Config\FileStorage; ++use Drupal\field\Entity\FieldConfig; ++use Drupal\field\Entity\FieldStorageConfig; ++use Drupal\responsive_image\Entity\ResponsiveImageStyle; ++use Drupal\Tests\responsive_image\Functional\ViewsIntegrationTest; + use Drupal\views\ViewsConfigUpdater; + + /** +@@ -23,7 +27,16 @@ class ViewsConfigUpdaterTest extends ViewsKernelTestBase { + /** + * {@inheritdoc} + */ +- protected static $modules = ['views_config_entity_test']; ++ protected static $modules = [ ++ 'views_config_entity_test', ++ 'entity_test', ++ 'breakpoint', ++ 'field', ++ 'file', ++ 'image', ++ 'responsive_image', ++ 'responsive_image_test_module', ++ ]; + + /** + * {@inheritdoc} +@@ -34,6 +47,24 @@ protected function setUp($import_test_views = TRUE): void { + $this->configUpdater = $this->container + ->get('class_resolver') + ->getInstanceFromDefinition(ViewsConfigUpdater::class); ++ ++ // Create a responsive image style. ++ ResponsiveImageStyle::create([ ++ 'id' => ViewsIntegrationTest::RESPONSIVE_IMAGE_STYLE_ID, ++ 'label' => 'Foo', ++ 'breakpoint_group' => 'responsive_image_test_module', ++ ]); ++ // Create an image field to be used with a responsive image formatter. ++ FieldStorageConfig::create([ ++ 'type' => 'image', ++ 'entity_type' => 'entity_test', ++ 'field_name' => 'bar', ++ ])->save(); ++ FieldConfig::create([ ++ 'entity_type' => 'entity_test', ++ 'bundle' => 'entity_test', ++ 'field_name' => 'bar', ++ ])->save(); + } + + /** +@@ -119,6 +150,15 @@ public function testNeedsFieldNamesForMultivalueBaseUpdateFieldsDeprecation() { + $this->assertTrue($needs_update); + } + ++ /** ++ * @covers ::needsResponsiveImageLazyLoadFieldUpdate ++ */ ++ public function testNeedsResponsiveImageLazyLoadFieldUpdate(): void { ++ $test_view = $this->loadTestView('views.view.test_responsive_images'); ++ $needs_update = $this->configUpdater->needsResponsiveImageLazyLoadFieldUpdate($test_view); ++ $this->assertTrue($needs_update); ++ } ++ + /** + * @covers ::updateAll + */ diff --git a/patches/drupal/image_field_caption/image_field_caption_caption_required_alt_required_3181263-1.patch b/patches/drupal/image_field_caption/image_field_caption_caption_required_alt_required_3181263-1.patch new file mode 100644 index 0000000..e793b3c --- /dev/null +++ b/patches/drupal/image_field_caption/image_field_caption_caption_required_alt_required_3181263-1.patch @@ -0,0 +1,15 @@ +diff --git a/image_field_caption.module b/image_field_caption.module +index f4fa48d..d79c70a 100644 +--- a/image_field_caption.module ++++ b/image_field_caption.module +@@ -76,8 +76,8 @@ function _image_field_caption_widget_process($element, &$form_state, $form) { + '#default_value' => (!empty($element['#value']['caption'])) ? $element['#value']['caption'] : (!empty($element['#value']['image_field_caption']) ? $element['#value']['image_field_caption']['value'] : ''), + '#access' => (bool) $element['#value']['fids'], + '#format' => (!empty($field_value['format'])) ? $field_value['format'] : ((!empty($element['#value']['caption_format'])) ? $element['#value']['caption_format'] : 'plain_text'), +- '#required' => $element['#alt_field_required'], +- '#element_validate' => $element['#alt_field_required'] ? ['_image_field_caption_validate_required'] : [], ++ '#required' => $element['#caption_field_required'], ++ '#element_validate' => $element['#caption_field_required'] ? ['_image_field_caption_validate_required'] : [], + ]; + + return $element;