diff -r be944660c56a -r 3d72ae0968f4 wp/wp-includes/media.php --- a/wp/wp-includes/media.php Wed Sep 21 18:19:35 2022 +0200 +++ b/wp/wp-includes/media.php Tue Sep 27 16:37:53 2022 +0200 @@ -393,7 +393,7 @@ */ $class = apply_filters( 'get_image_tag_class', $class, $id, $align, $size ); - $html = '' . esc_attr( $alt ) . ''; + $html = '' . esc_attr( $alt ) . ''; /** * Filters the HTML content for the image tag. @@ -882,7 +882,8 @@ * @uses wp_get_additional_image_sizes() * @uses get_intermediate_image_sizes() * - * @return array Associative array of the registered image sub-sizes. + * @return array[] Associative array of arrays of image sub-size information, + * keyed by image size name. */ function wp_get_registered_image_subsizes() { $additional_sizes = wp_get_additional_image_sizes(); @@ -1046,7 +1047,7 @@ // Add `loading` attribute. if ( wp_lazy_loading_enabled( 'img', 'wp_get_attachment_image' ) ) { - $default_attr['loading'] = 'lazy'; + $default_attr['loading'] = wp_get_loading_attr_default( 'wp_get_attachment_image' ); } $attr = wp_parse_args( $attr, $default_attr ); @@ -1129,7 +1130,7 @@ */ function wp_get_attachment_image_url( $attachment_id, $size = 'thumbnail', $icon = false ) { $image = wp_get_attachment_image_src( $attachment_id, $size, $icon ); - return isset( $image['0'] ) ? $image['0'] : false; + return isset( $image[0] ) ? $image[0] : false; } /** @@ -1820,39 +1821,68 @@ _prime_post_caches( $attachment_ids, false, true ); } - foreach ( $images as $image => $attachment_id ) { - $filtered_image = $image; - - // Add 'width' and 'height' attributes if applicable. - if ( $attachment_id > 0 && false === strpos( $filtered_image, ' width=' ) && false === strpos( $filtered_image, ' height=' ) ) { - $filtered_image = wp_img_tag_add_width_and_height_attr( $filtered_image, $context, $attachment_id ); - } - - // Add 'srcset' and 'sizes' attributes if applicable. - if ( $attachment_id > 0 && false === strpos( $filtered_image, ' srcset=' ) ) { - $filtered_image = wp_img_tag_add_srcset_and_sizes_attr( $filtered_image, $context, $attachment_id ); - } - - // Add 'loading' attribute if applicable. - if ( $add_img_loading_attr && false === strpos( $filtered_image, ' loading=' ) ) { - $filtered_image = wp_img_tag_add_loading_attr( $filtered_image, $context ); + // Iterate through the matches in order of occurrence as it is relevant for whether or not to lazy-load. + foreach ( $matches as $match ) { + // Filter an image match. + if ( isset( $images[ $match[0] ] ) ) { + $filtered_image = $match[0]; + $attachment_id = $images[ $match[0] ]; + + // Add 'width' and 'height' attributes if applicable. + if ( $attachment_id > 0 && false === strpos( $filtered_image, ' width=' ) && false === strpos( $filtered_image, ' height=' ) ) { + $filtered_image = wp_img_tag_add_width_and_height_attr( $filtered_image, $context, $attachment_id ); + } + + // Add 'srcset' and 'sizes' attributes if applicable. + if ( $attachment_id > 0 && false === strpos( $filtered_image, ' srcset=' ) ) { + $filtered_image = wp_img_tag_add_srcset_and_sizes_attr( $filtered_image, $context, $attachment_id ); + } + + // Add 'loading' attribute if applicable. + if ( $add_img_loading_attr && false === strpos( $filtered_image, ' loading=' ) ) { + $filtered_image = wp_img_tag_add_loading_attr( $filtered_image, $context ); + } + + /** + * Filters an img tag within the content for a given context. + * + * @since 6.0.0 + * + * @param string $filtered_image Full img tag with attributes that will replace the source img tag. + * @param string $context Additional context, like the current filter name or the function name from where this was called. + * @param int $attachment_id The image attachment ID. May be 0 in case the image is not an attachment. + */ + $filtered_image = apply_filters( 'wp_content_img_tag', $filtered_image, $context, $attachment_id ); + + if ( $filtered_image !== $match[0] ) { + $content = str_replace( $match[0], $filtered_image, $content ); + } + + /* + * Unset image lookup to not run the same logic again unnecessarily if the same image tag is used more than + * once in the same blob of content. + */ + unset( $images[ $match[0] ] ); } - if ( $filtered_image !== $image ) { - $content = str_replace( $image, $filtered_image, $content ); - } - } - - foreach ( $iframes as $iframe => $attachment_id ) { - $filtered_iframe = $iframe; - - // Add 'loading' attribute if applicable. - if ( $add_iframe_loading_attr && false === strpos( $filtered_iframe, ' loading=' ) ) { - $filtered_iframe = wp_iframe_tag_add_loading_attr( $filtered_iframe, $context ); - } - - if ( $filtered_iframe !== $iframe ) { - $content = str_replace( $iframe, $filtered_iframe, $content ); + // Filter an iframe match. + if ( isset( $iframes[ $match[0] ] ) ) { + $filtered_iframe = $match[0]; + + // Add 'loading' attribute if applicable. + if ( $add_iframe_loading_attr && false === strpos( $filtered_iframe, ' loading=' ) ) { + $filtered_iframe = wp_iframe_tag_add_loading_attr( $filtered_iframe, $context ); + } + + if ( $filtered_iframe !== $match[0] ) { + $content = str_replace( $match[0], $filtered_iframe, $content ); + } + + /* + * Unset iframe lookup to not run the same logic again unnecessarily if the same iframe tag is used more + * than once in the same blob of content. + */ + unset( $iframes[ $match[0] ] ); } } @@ -1869,6 +1899,10 @@ * @return string Converted `img` tag with `loading` attribute added. */ function wp_img_tag_add_loading_attr( $image, $context ) { + // Get loading attribute value to use. This must occur before the conditional check below so that even images that + // are ineligible for being lazy-loaded are considered. + $value = wp_get_loading_attr_default( $context ); + // Images should have source and dimension attributes for the `loading` attribute to be added. if ( false === strpos( $image, ' src="' ) || false === strpos( $image, ' width="' ) || false === strpos( $image, ' height="' ) ) { return $image; @@ -1883,11 +1917,11 @@ * @since 5.5.0 * * @param string|bool $value The `loading` attribute value. Returning a falsey value will result in - * the attribute being omitted for the image. Default 'lazy'. + * the attribute being omitted for the image. * @param string $image The HTML `img` tag to be filtered. * @param string $context Additional context about how the function was called or where the img tag is. */ - $value = apply_filters( 'wp_img_tag_add_loading_attr', 'lazy', $image, $context ); + $value = apply_filters( 'wp_img_tag_add_loading_attr', $value, $image, $context ); if ( $value ) { if ( ! in_array( $value, array( 'lazy', 'eager' ), true ) ) { @@ -1995,6 +2029,10 @@ return $iframe; } + // Get loading attribute value to use. This must occur before the conditional check below so that even iframes that + // are ineligible for being lazy-loaded are considered. + $value = wp_get_loading_attr_default( $context ); + // Iframes should have source and dimension attributes for the `loading` attribute to be added. if ( false === strpos( $iframe, ' src="' ) || false === strpos( $iframe, ' width="' ) || false === strpos( $iframe, ' height="' ) ) { return $iframe; @@ -2009,11 +2047,11 @@ * @since 5.7.0 * * @param string|bool $value The `loading` attribute value. Returning a falsey value will result in - * the attribute being omitted for the iframe. Default 'lazy'. + * the attribute being omitted for the iframe. * @param string $iframe The HTML `iframe` tag to be filtered. * @param string $context Additional context about how the function was called or where the iframe tag is. */ - $value = apply_filters( 'wp_iframe_tag_add_loading_attr', 'lazy', $iframe, $context ); + $value = apply_filters( 'wp_iframe_tag_add_loading_attr', $value, $iframe, $context ); if ( $value ) { if ( ! in_array( $value, array( 'lazy', 'eager' ), true ) ) { @@ -2085,6 +2123,7 @@ * @since 2.6.0 * @since 3.9.0 The `class` attribute was added. * @since 5.1.0 The `caption_id` attribute was added. + * @since 5.9.0 The `$content` parameter default value changed from `null` to `''`. * * @param array $attr { * Attributes of the caption shortcode. @@ -2097,10 +2136,10 @@ * @type string $caption The caption text. * @type string $class Additional class name(s) added to the caption container. * } - * @param string $content Shortcode content. + * @param string $content Optional. Shortcode content. Default empty string. * @return string HTML content to display the caption. */ -function img_caption_shortcode( $attr, $content = null ) { +function img_caption_shortcode( $attr, $content = '' ) { // New-style shortcode with the caption inside the shortcode with the link and image tags. if ( ! isset( $attr['caption'] ) ) { if ( preg_match( '#((?:]+>\s*)?]+>(?:\s*)?)(.*)#is', $content, $matches ) ) { @@ -4030,7 +4069,7 @@ if ( isset( $meta['filesize'] ) ) { $bytes = $meta['filesize']; } elseif ( file_exists( $attached_file ) ) { - $bytes = filesize( $attached_file ); + $bytes = wp_filesize( $attached_file ); } else { $bytes = ''; } @@ -4209,7 +4248,7 @@ * @param array $args { * Arguments for enqueuing media scripts. * - * @type int|WP_Post A post object or ID. + * @type int|WP_Post $post A post object or ID. * } */ function wp_enqueue_media( $args = array() ) { @@ -4455,7 +4494,7 @@ 'allDates' => __( 'All dates' ), 'noItemsFound' => __( 'No items found.' ), 'insertIntoPost' => $post_type_object->labels->insert_into_item, - 'unattached' => __( 'Unattached' ), + 'unattached' => _x( 'Unattached', 'media items' ), 'mine' => _x( 'Mine', 'media items' ), 'trash' => _x( 'Trash', 'noun' ), 'uploadedToThisPost' => $post_type_object->labels->uploaded_to_this_item, @@ -4466,6 +4505,7 @@ 'trashSelected' => __( 'Move to Trash' ), 'restoreSelected' => __( 'Restore from Trash' ), 'deletePermanently' => __( 'Delete permanently' ), + 'errorDeleting' => __( 'Error in deleting the attachment.' ), 'apply' => __( 'Apply' ), 'filterByDate' => __( 'Filter by date' ), 'filterByType' => __( 'Filter by type' ), @@ -4710,7 +4750,7 @@ return array(); } - if ( ! has_shortcode( $post->post_content, 'gallery' ) ) { + if ( ! has_shortcode( $post->post_content, 'gallery' ) && ! has_block( 'gallery', $post->post_content ) ) { return array(); } @@ -4752,6 +4792,98 @@ } } + if ( has_block( 'gallery', $post->post_content ) ) { + $post_blocks = parse_blocks( $post->post_content ); + + while ( $block = array_shift( $post_blocks ) ) { + $has_inner_blocks = ! empty( $block['innerBlocks'] ); + + // Skip blocks with no blockName and no innerHTML. + if ( ! $block['blockName'] ) { + continue; + } + + // Skip non-Gallery blocks. + if ( 'core/gallery' !== $block['blockName'] ) { + // Move inner blocks into the root array before skipping. + if ( $has_inner_blocks ) { + array_push( $post_blocks, ...$block['innerBlocks'] ); + } + continue; + } + + // New Gallery block format as HTML. + if ( $has_inner_blocks && $html ) { + $block_html = wp_list_pluck( $block['innerBlocks'], 'innerHTML' ); + $galleries[] = '
' . implode( ' ', $block_html ) . '
'; + continue; + } + + $srcs = array(); + + // New Gallery block format as an array. + if ( $has_inner_blocks ) { + $attrs = wp_list_pluck( $block['innerBlocks'], 'attrs' ); + $ids = wp_list_pluck( $attrs, 'id' ); + + foreach ( $ids as $id ) { + $url = wp_get_attachment_url( $id ); + + if ( is_string( $url ) && ! in_array( $url, $srcs, true ) ) { + $srcs[] = $url; + } + } + + $galleries[] = array( + 'ids' => implode( ',', $ids ), + 'src' => $srcs, + ); + + continue; + } + + // Old Gallery block format as HTML. + if ( $html ) { + $galleries[] = $block['innerHTML']; + continue; + } + + // Old Gallery block format as an array. + $ids = ! empty( $block['attrs']['ids'] ) ? $block['attrs']['ids'] : array(); + + // If present, use the image IDs from the JSON blob as canonical. + if ( ! empty( $ids ) ) { + foreach ( $ids as $id ) { + $url = wp_get_attachment_url( $id ); + + if ( is_string( $url ) && ! in_array( $url, $srcs, true ) ) { + $srcs[] = $url; + } + } + + $galleries[] = array( + 'ids' => implode( ',', $ids ), + 'src' => $srcs, + ); + + continue; + } + + // Otherwise, extract srcs from the innerHTML. + preg_match_all( '#src=([\'"])(.+?)\1#is', $block['innerHTML'], $found_srcs, PREG_SET_ORDER ); + + if ( ! empty( $found_srcs[0] ) ) { + foreach ( $found_srcs as $src ) { + if ( isset( $src[2] ) && ! in_array( $src[2], $srcs, true ) ) { + $srcs[] = $src[2]; + } + } + } + + $galleries[] = array( 'src' => $srcs ); + } + } + /** * Filters the list of all found galleries in the given post. * @@ -5091,7 +5223,7 @@ return array( $width, $height, - IMAGETYPE_WEBP, // phpcs:ignore PHPCompatibility.Constants.NewConstants.imagetype_webpFound + IMAGETYPE_WEBP, sprintf( 'width="%d" height="%d"', $width, @@ -5107,20 +5239,19 @@ } /** - * Extracts meta information about a webp file: width, height and type. + * Extracts meta information about a WebP file: width, height, and type. * * @since 5.8.0 * * @param string $filename Path to a WebP file. - * @return array $webp_info { + * @return array { * An array of WebP image information. * - * @type array $size { - * @type int|false $width Image width on success, false on failure. - * @type int|false $height Image height on success, false on failure. - * @type string|false $type The WebP type: one of 'lossy', 'lossless' or 'animated-alpha'. - * False on failure. - * } + * @type int|false $width Image width on success, false on failure. + * @type int|false $height Image height on success, false on failure. + * @type string|false $type The WebP type: one of 'lossy', 'lossless' or 'animated-alpha'. + * False on failure. + * } */ function wp_get_webp_info( $filename ) { $width = false; @@ -5131,48 +5262,139 @@ return compact( 'width', 'height', 'type' ); } - try { - $handle = fopen( $filename, 'rb' ); - if ( $handle ) { - $magic = fread( $handle, 40 ); - fclose( $handle ); - - // Make sure we got enough bytes. - if ( strlen( $magic ) < 40 ) { - return compact( 'width', 'height', 'type' ); - } - - // The headers are a little different for each of the three formats. - // Header values based on WebP docs, see https://developers.google.com/speed/webp/docs/riff_container. - switch ( substr( $magic, 12, 4 ) ) { - // Lossy WebP. - case 'VP8 ': - $parts = unpack( 'v2', substr( $magic, 26, 4 ) ); - $width = (int) ( $parts[1] & 0x3FFF ); - $height = (int) ( $parts[2] & 0x3FFF ); - $type = 'lossy'; - break; - // Lossless WebP. - case 'VP8L': - $parts = unpack( 'C4', substr( $magic, 21, 4 ) ); - $width = (int) ( $parts[1] | ( ( $parts[2] & 0x3F ) << 8 ) ) + 1; - $height = (int) ( ( ( $parts[2] & 0xC0 ) >> 6 ) | ( $parts[3] << 2 ) | ( ( $parts[4] & 0x03 ) << 10 ) ) + 1; - $type = 'lossless'; - break; - // Animated/alpha WebP. - case 'VP8X': - // Pad 24-bit int. - $width = unpack( 'V', substr( $magic, 24, 3 ) . "\x00" ); - $width = (int) ( $width[1] & 0xFFFFFF ) + 1; - // Pad 24-bit int. - $height = unpack( 'V', substr( $magic, 27, 3 ) . "\x00" ); - $height = (int) ( $height[1] & 0xFFFFFF ) + 1; - $type = 'animated-alpha'; - break; - } - } - } catch ( Exception $e ) { + $magic = file_get_contents( $filename, false, null, 0, 40 ); + + if ( false === $magic ) { + return compact( 'width', 'height', 'type' ); + } + + // Make sure we got enough bytes. + if ( strlen( $magic ) < 40 ) { + return compact( 'width', 'height', 'type' ); + } + + // The headers are a little different for each of the three formats. + // Header values based on WebP docs, see https://developers.google.com/speed/webp/docs/riff_container. + switch ( substr( $magic, 12, 4 ) ) { + // Lossy WebP. + case 'VP8 ': + $parts = unpack( 'v2', substr( $magic, 26, 4 ) ); + $width = (int) ( $parts[1] & 0x3FFF ); + $height = (int) ( $parts[2] & 0x3FFF ); + $type = 'lossy'; + break; + // Lossless WebP. + case 'VP8L': + $parts = unpack( 'C4', substr( $magic, 21, 4 ) ); + $width = (int) ( $parts[1] | ( ( $parts[2] & 0x3F ) << 8 ) ) + 1; + $height = (int) ( ( ( $parts[2] & 0xC0 ) >> 6 ) | ( $parts[3] << 2 ) | ( ( $parts[4] & 0x03 ) << 10 ) ) + 1; + $type = 'lossless'; + break; + // Animated/alpha WebP. + case 'VP8X': + // Pad 24-bit int. + $width = unpack( 'V', substr( $magic, 24, 3 ) . "\x00" ); + $width = (int) ( $width[1] & 0xFFFFFF ) + 1; + // Pad 24-bit int. + $height = unpack( 'V', substr( $magic, 27, 3 ) . "\x00" ); + $height = (int) ( $height[1] & 0xFFFFFF ) + 1; + $type = 'animated-alpha'; + break; } return compact( 'width', 'height', 'type' ); } + +/** + * Gets the default value to use for a `loading` attribute on an element. + * + * This function should only be called for a tag and context if lazy-loading is generally enabled. + * + * The function usually returns 'lazy', but uses certain heuristics to guess whether the current element is likely to + * appear above the fold, in which case it returns a boolean `false`, which will lead to the `loading` attribute being + * omitted on the element. The purpose of this refinement is to avoid lazy-loading elements that are within the initial + * viewport, which can have a negative performance impact. + * + * Under the hood, the function uses {@see wp_increase_content_media_count()} every time it is called for an element + * within the main content. If the element is the very first content element, the `loading` attribute will be omitted. + * This default threshold of 1 content element to omit the `loading` attribute for can be customized using the + * {@see 'wp_omit_loading_attr_threshold'} filter. + * + * @since 5.9.0 + * + * @param string $context Context for the element for which the `loading` attribute value is requested. + * @return string|bool The default `loading` attribute value. Either 'lazy', 'eager', or a boolean `false`, to indicate + * that the `loading` attribute should be skipped. + */ +function wp_get_loading_attr_default( $context ) { + // Only elements with 'the_content' or 'the_post_thumbnail' context have special handling. + if ( 'the_content' !== $context && 'the_post_thumbnail' !== $context ) { + return 'lazy'; + } + + // Only elements within the main query loop have special handling. + if ( is_admin() || ! in_the_loop() || ! is_main_query() ) { + return 'lazy'; + } + + // Increase the counter since this is a main query content element. + $content_media_count = wp_increase_content_media_count(); + + // If the count so far is below the threshold, return `false` so that the `loading` attribute is omitted. + if ( $content_media_count <= wp_omit_loading_attr_threshold() ) { + return false; + } + + // For elements after the threshold, lazy-load them as usual. + return 'lazy'; +} + +/** + * Gets the threshold for how many of the first content media elements to not lazy-load. + * + * This function runs the {@see 'wp_omit_loading_attr_threshold'} filter, which uses a default threshold value of 1. + * The filter is only run once per page load, unless the `$force` parameter is used. + * + * @since 5.9.0 + * + * @param bool $force Optional. If set to true, the filter will be (re-)applied even if it already has been before. + * Default false. + * @return int The number of content media elements to not lazy-load. + */ +function wp_omit_loading_attr_threshold( $force = false ) { + static $omit_threshold; + + // This function may be called multiple times. Run the filter only once per page load. + if ( ! isset( $omit_threshold ) || $force ) { + /** + * Filters the threshold for how many of the first content media elements to not lazy-load. + * + * For these first content media elements, the `loading` attribute will be omitted. By default, this is the case + * for only the very first content media element. + * + * @since 5.9.0 + * + * @param int $omit_threshold The number of media elements where the `loading` attribute will not be added. Default 1. + */ + $omit_threshold = apply_filters( 'wp_omit_loading_attr_threshold', 1 ); + } + + return $omit_threshold; +} + +/** + * Increases an internal content media count variable. + * + * @since 5.9.0 + * @access private + * + * @param int $amount Optional. Amount to increase by. Default 1. + * @return int The latest content media count, after the increase. + */ +function wp_increase_content_media_count( $amount = 1 ) { + static $content_media_count = 0; + + $content_media_count += $amount; + + return $content_media_count; +}