From 03e381100b2b454871c1900ca04d09d23cc38772 Mon Sep 17 00:00:00 2001 From: robelsust Date: Wed, 7 Oct 2026 12:25:04 +0600 Subject: [PATCH 1/3] Media: Add unit tests for `pre_wp_get_image_alttext` filter. Introduces unit tests in `tests/phpunit/tests/image/alttext.php` covering short-circuiting with a custom string, disabling alt text generation with an empty string, falling through when null is returned, passing the file path argument to the callback, and bypassing file access requirements. Follow-up to #66248 and PR #14024. Trac ticket: https://core.trac.wordpress.org/ticket/66248 --- src/wp-admin/includes/image.php | 20 ++++++ tests/phpunit/tests/image/alttext.php | 92 +++++++++++++++++++++++++++ 2 files changed, 112 insertions(+) diff --git a/src/wp-admin/includes/image.php b/src/wp-admin/includes/image.php index a05531c6d9557..38bd1681e25d2 100644 --- a/src/wp-admin/includes/image.php +++ b/src/wp-admin/includes/image.php @@ -1086,6 +1086,26 @@ function wp_read_image_metadata( $file ) { * @return string Embedded alternative text, empty when there is no alt text or DOM extension is not installed. */ function wp_get_image_alttext( $file ) { + /** + * Filters whether to short-circuit the parsing of alt text from an image. + * + * Returning a non-null value will short-circuit the image parsing, + * returning the passed value instead. + * + * This could be used simply to turn off the alt text generation + * (by returning an empty string from the filter instead of null). + * Or it could be used to run custom code for generating alt text. + * + * @since 7.2.0 + * + * @param string|null $override_alt_text Short-circuit return value. + * @param string $file File path to the image. + */ + $override_alt_text = apply_filters( 'pre_wp_get_image_alttext', null, $file ); + if ( null !== $override_alt_text ) { + return $override_alt_text; + } + $alt_text = ''; if ( ! class_exists( 'DOMDocument', false ) ) { diff --git a/tests/phpunit/tests/image/alttext.php b/tests/phpunit/tests/image/alttext.php index 8d22353ee090d..5465850ce383c 100644 --- a/tests/phpunit/tests/image/alttext.php +++ b/tests/phpunit/tests/image/alttext.php @@ -199,4 +199,96 @@ public function test_wp_get_image_alttext_returns_different_alt_text_based_on_si switch_to_locale( 'ja_JP' ); $this->assert_alttext_matches_or_empty_without_dom( 'Default description', wp_get_image_alttext( $file ) ); } + + /** + * Tests that pre_wp_get_image_alttext filter short-circuits and returns a custom string. + * + * @ticket 66248 + */ + public function test_pre_wp_get_image_alttext_short_circuits_with_custom_string() { + $filter = function () { + return 'Overridden custom alt text'; + }; + add_filter( 'pre_wp_get_image_alttext', $filter ); + + $alt = wp_get_image_alttext( DIR_TESTDATA . '/images/IPTC-PhotometadataRef-Std2025.1.jpg' ); + remove_filter( 'pre_wp_get_image_alttext', $filter ); + + $this->assertSame( 'Overridden custom alt text', $alt ); + } + + /** + * Tests that pre_wp_get_image_alttext filter short-circuits with an empty string to disable alt text generation. + * + * @ticket 66248 + */ + public function test_pre_wp_get_image_alttext_short_circuits_with_empty_string() { + $filter = '__return_empty_string'; + add_filter( 'pre_wp_get_image_alttext', $filter ); + + $alt = wp_get_image_alttext( DIR_TESTDATA . '/images/IPTC-PhotometadataRef-Std2025.1.jpg' ); + remove_filter( 'pre_wp_get_image_alttext', $filter ); + + $this->assertSame( '', $alt ); + } + + /** + * Tests that pre_wp_get_image_alttext returns default parsed alt text when the filter returns null. + * + * @ticket 66248 + */ + public function test_pre_wp_get_image_alttext_returns_default_when_filter_returns_null() { + $filter = '__return_null'; + add_filter( 'pre_wp_get_image_alttext', $filter ); + + $alt = wp_get_image_alttext( DIR_TESTDATA . '/images/IPTC-PhotometadataRef-Std2025.1.jpg' ); + remove_filter( 'pre_wp_get_image_alttext', $filter ); + + $this->assert_alttext_matches_or_empty_without_dom( + 'This is the Alt Text description to support accessibility in 2025.1', + $alt + ); + } + + /** + * Tests that pre_wp_get_image_alttext passes the file path parameter to the filter callback. + * + * @ticket 66248 + */ + public function test_pre_wp_get_image_alttext_passes_file_path_parameter() { + $target_file = DIR_TESTDATA . '/images/IPTC-PhotometadataRef-Std2025.1.jpg'; + $passed_args = array(); + + $filter = function ( $override, $file ) use ( &$passed_args ) { + $passed_args = array( + 'override' => $override, + 'file' => $file, + ); + return $override; + }; + add_filter( 'pre_wp_get_image_alttext', $filter, 10, 2 ); + + wp_get_image_alttext( $target_file ); + remove_filter( 'pre_wp_get_image_alttext', $filter, 10 ); + + $this->assertNull( $passed_args['override'] ); + $this->assertSame( $target_file, $passed_args['file'] ); + } + + /** + * Tests that pre_wp_get_image_alttext short-circuits without checking file existence or DOM availability. + * + * @ticket 66248 + */ + public function test_pre_wp_get_image_alttext_short_circuits_without_file_access() { + $filter = function () { + return 'Alt text from database or external API'; + }; + add_filter( 'pre_wp_get_image_alttext', $filter ); + + $alt = wp_get_image_alttext( '/non/existent/path/to/image.jpg' ); + remove_filter( 'pre_wp_get_image_alttext', $filter ); + + $this->assertSame( 'Alt text from database or external API', $alt ); + } } From 8df4afffa514dcf2a0cb2c6473e0d27d63da0a41 Mon Sep 17 00:00:00 2001 From: robelsust Date: Thu, 8 Oct 2026 09:31:39 +0600 Subject: [PATCH 2/3] Media: Add `wp_read_image_metadata_allowed_keys` filter in `wp_read_image_metadata()`. Allows developers to customize or disable extraction of specific metadata fields (such as 'alt', 'caption', 'title', etc.) or disable metadata parsing entirely via a single filter, as proposed by @joedolson on Trac #66248. Guards IPTC, XMP ('alt'), and EXIF extraction based on the filtered allowed keys to avoid redundant disk I/O and XML/DOMDocument parsing overhead when fields are not needed. Maintains the complete default 13-key return array structure to ensure strict backward compatibility for all callers. Includes dedicated unit tests in `readImageMetadataAllowedKeys.php`. See #66248. --- src/wp-admin/includes/image.php | 131 ++++++---- .../image/readImageMetadataAllowedKeys.php | 226 ++++++++++++++++++ 2 files changed, 309 insertions(+), 48 deletions(-) create mode 100644 tests/phpunit/tests/image/readImageMetadataAllowedKeys.php diff --git a/src/wp-admin/includes/image.php b/src/wp-admin/includes/image.php index 38bd1681e25d2..971bd08b9eedc 100644 --- a/src/wp-admin/includes/image.php +++ b/src/wp-admin/includes/image.php @@ -857,13 +857,37 @@ function wp_read_image_metadata( $file ) { 'alt' => '', ); + /** + * Filters the list of allowed metadata keys to extract from an image. + * + * Allows customizing or disabling extraction of specific metadata fields + * (such as 'alt', 'caption', 'title', 'copyright', etc.), or disabling metadata extraction entirely. + * + * @since 7.2.0 + * + * @param string[] $allowed_keys Array of metadata keys to extract. Defaults to all keys: + * 'aperture', 'credit', 'camera', 'caption', 'created_timestamp', + * 'copyright', 'focal_length', 'iso', 'shutter_speed', 'title', + * 'orientation', 'keywords', 'alt'. + * @param string $file Path to the image file. + * @param int $image_type Type of image, one of the `IMAGETYPE_*` constants. + */ + $allowed_keys = apply_filters( 'wp_read_image_metadata_allowed_keys', array_keys( $meta ), $file, $image_type ); + + if ( ! is_array( $allowed_keys ) ) { + $allowed_keys = array(); + } + + $iptc_keys = array( 'title', 'caption', 'credit', 'created_timestamp', 'copyright', 'keywords' ); + $extract_iptc = ! empty( array_intersect( $iptc_keys, $allowed_keys ) ); + $iptc = array(); $info = array(); /* * Read IPTC first, since it might contain data not available in exif such * as caption, description etc. */ - if ( is_callable( 'iptcparse' ) ) { + if ( $extract_iptc && is_callable( 'iptcparse' ) ) { wp_getimagesize( $file, $info ); if ( ! empty( $info['APP13'] ) ) { @@ -882,24 +906,26 @@ function wp_read_image_metadata( $file ) { } // Headline, "A brief synopsis of the caption". - if ( ! empty( $iptc['2#105'][0] ) ) { - $meta['title'] = trim( $iptc['2#105'][0] ); - /* - * Title, "Many use the Title field to store the filename of the image, - * though the field may be used in many ways". - */ - } elseif ( ! empty( $iptc['2#005'][0] ) ) { - $meta['title'] = trim( $iptc['2#005'][0] ); + if ( in_array( 'title', $allowed_keys, true ) ) { + if ( ! empty( $iptc['2#105'][0] ) ) { + $meta['title'] = trim( $iptc['2#105'][0] ); + /* + * Title, "Many use the Title field to store the filename of the image, + * though the field may be used in many ways". + */ + } elseif ( ! empty( $iptc['2#005'][0] ) ) { + $meta['title'] = trim( $iptc['2#005'][0] ); + } } - if ( ! empty( $iptc['2#120'][0] ) ) { // Description / legacy caption. + if ( in_array( 'caption', $allowed_keys, true ) && ! empty( $iptc['2#120'][0] ) ) { // Description / legacy caption. $caption = trim( $iptc['2#120'][0] ); mbstring_binary_safe_encoding(); $caption_length = strlen( $caption ); reset_mbstring_encoding(); - if ( empty( $meta['title'] ) && $caption_length < 80 ) { + if ( in_array( 'title', $allowed_keys, true ) && empty( $meta['title'] ) && $caption_length < 80 ) { // Assume the title is stored in 2:120 if it's short. $meta['title'] = $caption; } @@ -907,27 +933,34 @@ function wp_read_image_metadata( $file ) { $meta['caption'] = $caption; } - if ( ! empty( $iptc['2#110'][0] ) ) { // Credit. - $meta['credit'] = trim( $iptc['2#110'][0] ); - } elseif ( ! empty( $iptc['2#080'][0] ) ) { // Creator / legacy byline. - $meta['credit'] = trim( $iptc['2#080'][0] ); + if ( in_array( 'credit', $allowed_keys, true ) ) { + if ( ! empty( $iptc['2#110'][0] ) ) { // Credit. + $meta['credit'] = trim( $iptc['2#110'][0] ); + } elseif ( ! empty( $iptc['2#080'][0] ) ) { // Creator / legacy byline. + $meta['credit'] = trim( $iptc['2#080'][0] ); + } } - if ( ! empty( $iptc['2#055'][0] ) && ! empty( $iptc['2#060'][0] ) ) { // Created date and time. + if ( in_array( 'created_timestamp', $allowed_keys, true ) && ! empty( $iptc['2#055'][0] ) && ! empty( $iptc['2#060'][0] ) ) { // Created date and time. $meta['created_timestamp'] = strtotime( $iptc['2#055'][0] . ' ' . $iptc['2#060'][0] ); } - if ( ! empty( $iptc['2#116'][0] ) ) { // Copyright. + if ( in_array( 'copyright', $allowed_keys, true ) && ! empty( $iptc['2#116'][0] ) ) { // Copyright. $meta['copyright'] = trim( $iptc['2#116'][0] ); } - if ( ! empty( $iptc['2#025'][0] ) ) { // Keywords array. + if ( in_array( 'keywords', $allowed_keys, true ) && ! empty( $iptc['2#025'][0] ) ) { // Keywords array. $meta['keywords'] = $iptc['2#025']; } } } - $meta['alt'] = wp_get_image_alttext( $file ); + if ( in_array( 'alt', $allowed_keys, true ) ) { + $meta['alt'] = wp_get_image_alttext( $file ); + } + + $exif_keys = array( 'aperture', 'credit', 'camera', 'caption', 'created_timestamp', 'copyright', 'focal_length', 'iso', 'shutter_speed', 'title', 'orientation' ); + $extract_exif = ! empty( array_intersect( $exif_keys, $allowed_keys ) ); $exif = array(); @@ -941,7 +974,7 @@ function wp_read_image_metadata( $file ) { */ $exif_image_types = apply_filters( 'wp_read_image_metadata_types', array( IMAGETYPE_JPEG, IMAGETYPE_TIFF_II, IMAGETYPE_TIFF_MM ) ); - if ( is_callable( 'exif_read_data' ) && in_array( $image_type, $exif_image_types, true ) ) { + if ( $extract_exif && is_callable( 'exif_read_data' ) && in_array( $image_type, $exif_image_types, true ) ) { // Don't silence errors when in debug mode, unless running unit tests. if ( defined( 'WP_DEBUG' ) && WP_DEBUG && ! defined( 'WP_RUN_CORE_TESTS' ) @@ -970,43 +1003,45 @@ function wp_read_image_metadata( $file ) { mbstring_binary_safe_encoding(); $description_length = strlen( $exif_description ); reset_mbstring_encoding(); - if ( empty( $meta['title'] ) && $description_length < 80 ) { + if ( in_array( 'title', $allowed_keys, true ) && empty( $meta['title'] ) && $description_length < 80 ) { // Assume the title is stored in ImageDescription. $meta['title'] = $exif_description; } - // If both user comments and description are present. - if ( empty( $meta['caption'] ) && $exif_usercomment ) { - if ( ! empty( $meta['title'] ) && $exif_description === $meta['title'] ) { - $caption = $exif_usercomment; - } else { - if ( $exif_description === $exif_usercomment ) { - $caption = $exif_description; + if ( in_array( 'caption', $allowed_keys, true ) ) { + // If both user comments and description are present. + if ( empty( $meta['caption'] ) && $exif_usercomment ) { + if ( ! empty( $meta['title'] ) && $exif_description === $meta['title'] ) { + $caption = $exif_usercomment; } else { - $caption = trim( $exif_description . ' ' . $exif_usercomment ); + if ( $exif_description === $exif_usercomment ) { + $caption = $exif_description; + } else { + $caption = trim( $exif_description . ' ' . $exif_usercomment ); + } } + $meta['caption'] = $caption; } - $meta['caption'] = $caption; - } - if ( empty( $meta['caption'] ) && $exif_usercomment ) { - $meta['caption'] = $exif_usercomment; - } + if ( empty( $meta['caption'] ) && $exif_usercomment ) { + $meta['caption'] = $exif_usercomment; + } - if ( empty( $meta['caption'] ) ) { - $meta['caption'] = $exif_description; + if ( empty( $meta['caption'] ) ) { + $meta['caption'] = $exif_description; + } } - } elseif ( empty( $meta['caption'] ) && $exif_usercomment ) { + } elseif ( in_array( 'caption', $allowed_keys, true ) && empty( $meta['caption'] ) && $exif_usercomment ) { $meta['caption'] = $exif_usercomment; $description_length = strlen( $exif_usercomment ); - if ( empty( $meta['title'] ) && $description_length < 80 ) { + if ( in_array( 'title', $allowed_keys, true ) && empty( $meta['title'] ) && $description_length < 80 ) { $meta['title'] = trim( $exif_usercomment ); } - } elseif ( empty( $meta['caption'] ) && ! empty( $exif['Comments'] ) ) { + } elseif ( in_array( 'caption', $allowed_keys, true ) && empty( $meta['caption'] ) && ! empty( $exif['Comments'] ) ) { $meta['caption'] = trim( $exif['Comments'] ); } - if ( empty( $meta['credit'] ) ) { + if ( in_array( 'credit', $allowed_keys, true ) && empty( $meta['credit'] ) ) { if ( ! empty( $exif['Artist'] ) ) { $meta['credit'] = trim( $exif['Artist'] ); } elseif ( ! empty( $exif['Author'] ) ) { @@ -1014,35 +1049,35 @@ function wp_read_image_metadata( $file ) { } } - if ( empty( $meta['copyright'] ) && ! empty( $exif['Copyright'] ) ) { + if ( in_array( 'copyright', $allowed_keys, true ) && empty( $meta['copyright'] ) && ! empty( $exif['Copyright'] ) ) { $meta['copyright'] = trim( $exif['Copyright'] ); } - if ( ! empty( $exif['FNumber'] ) && is_scalar( $exif['FNumber'] ) ) { + if ( in_array( 'aperture', $allowed_keys, true ) && ! empty( $exif['FNumber'] ) && is_scalar( $exif['FNumber'] ) ) { $meta['aperture'] = round( wp_exif_frac2dec( $exif['FNumber'] ), 2 ); } - if ( ! empty( $exif['Model'] ) ) { + if ( in_array( 'camera', $allowed_keys, true ) && ! empty( $exif['Model'] ) ) { $meta['camera'] = trim( $exif['Model'] ); } - if ( empty( $meta['created_timestamp'] ) && ! empty( $exif['DateTimeDigitized'] ) ) { + if ( in_array( 'created_timestamp', $allowed_keys, true ) && empty( $meta['created_timestamp'] ) && ! empty( $exif['DateTimeDigitized'] ) ) { $meta['created_timestamp'] = wp_exif_date2ts( $exif['DateTimeDigitized'] ); } - if ( ! empty( $exif['FocalLength'] ) ) { + if ( in_array( 'focal_length', $allowed_keys, true ) && ! empty( $exif['FocalLength'] ) ) { $meta['focal_length'] = (string) $exif['FocalLength']; if ( is_scalar( $exif['FocalLength'] ) ) { $meta['focal_length'] = (string) wp_exif_frac2dec( $exif['FocalLength'] ); } } - if ( ! empty( $exif['ISOSpeedRatings'] ) ) { + if ( in_array( 'iso', $allowed_keys, true ) && ! empty( $exif['ISOSpeedRatings'] ) ) { $meta['iso'] = is_array( $exif['ISOSpeedRatings'] ) ? reset( $exif['ISOSpeedRatings'] ) : $exif['ISOSpeedRatings']; $meta['iso'] = trim( $meta['iso'] ); } - if ( ! empty( $exif['ExposureTime'] ) ) { + if ( in_array( 'shutter_speed', $allowed_keys, true ) && ! empty( $exif['ExposureTime'] ) ) { $meta['shutter_speed'] = (string) $exif['ExposureTime']; if ( is_scalar( $exif['ExposureTime'] ) ) { $meta['shutter_speed'] = (string) wp_exif_frac2dec( $exif['ExposureTime'] ); } } - if ( ! empty( $exif['Orientation'] ) ) { + if ( in_array( 'orientation', $allowed_keys, true ) && ! empty( $exif['Orientation'] ) ) { $meta['orientation'] = $exif['Orientation']; } } diff --git a/tests/phpunit/tests/image/readImageMetadataAllowedKeys.php b/tests/phpunit/tests/image/readImageMetadataAllowedKeys.php new file mode 100644 index 0000000000000..7861bfa8e4d76 --- /dev/null +++ b/tests/phpunit/tests/image/readImageMetadataAllowedKeys.php @@ -0,0 +1,226 @@ +assertIsArray( $meta ); + $this->assertSame( 'NIKON D70', $meta['camera'] ); + $this->assertSame( '6.3', $meta['aperture'] ); + $this->assertSame( '27', $meta['focal_length'] ); + $this->assertSame( '400', $meta['iso'] ); + } + + /** + * Tests that disallowing the 'alt' key prevents alt text extraction while allowing other keys. + * + * @ticket 66248 + */ + public function test_disallowing_alt_key_skips_alt_extraction() { + add_filter( + 'wp_read_image_metadata_allowed_keys', + static function ( $keys ) { + return array_diff( $keys, array( 'alt' ) ); + } + ); + + $meta = wp_read_image_metadata( self::$iptc_xmp_image ); + + $this->assertIsArray( $meta ); + $this->assertSame( '', $meta['alt'], "Alt text should remain empty when 'alt' is not in allowed keys." ); + $this->assertNotEmpty( $meta['caption'], 'Caption should still be extracted from IPTC.' ); + } + + /** + * Tests that passing an empty array of allowed keys skips all metadata extraction + * while preserving the complete default array structure. + * + * @ticket 66248 + */ + public function test_empty_allowed_keys_skips_all_extraction_and_preserves_structure() { + add_filter( 'wp_read_image_metadata_allowed_keys', '__return_empty_array' ); + + $meta = wp_read_image_metadata( self::$exif_image ); + + $expected = array( + 'aperture' => '0', + 'credit' => '', + 'camera' => '', + 'caption' => '', + 'created_timestamp' => '0', + 'copyright' => '', + 'focal_length' => '0', + 'iso' => '0', + 'shutter_speed' => '0', + 'title' => '', + 'orientation' => '0', + 'keywords' => array(), + 'alt' => '', + ); + + $this->assertSame( $expected, $meta ); + } + + /** + * Tests that non-array return values from the filter are safely treated as an empty array. + * + * @ticket 66248 + * + * @dataProvider data_non_array_allowed_keys + * + * @param mixed $non_array_value Non-array filter return value. + */ + public function test_non_array_allowed_keys_treated_as_empty_array( $non_array_value ) { + add_filter( + 'wp_read_image_metadata_allowed_keys', + static function () use ( $non_array_value ) { + return $non_array_value; + } + ); + + $meta = wp_read_image_metadata( self::$exif_image ); + + $this->assertSame( '0', $meta['aperture'] ); + $this->assertSame( '', $meta['camera'] ); + $this->assertSame( '', $meta['alt'] ); + } + + /** + * Data provider for non-array allowed keys filter return values. + * + * @return array[] + */ + public static function data_non_array_allowed_keys() { + return array( + 'false' => array( false ), + 'null' => array( null ), + 'string' => array( 'invalid' ), + 'int' => array( 123 ), + ); + } + + /** + * Tests whitelisting specific metadata keys only extracts those fields. + * + * @ticket 66248 + */ + public function test_whitelist_specific_keys_only_extracts_those_fields() { + add_filter( + 'wp_read_image_metadata_allowed_keys', + static function () { + return array( 'camera', 'iso' ); + } + ); + + $meta = wp_read_image_metadata( self::$exif_image ); + + // Whitelisted fields should be extracted. + $this->assertSame( 'NIKON D70', $meta['camera'] ); + $this->assertSame( '400', $meta['iso'] ); + + // Non-whitelisted fields should remain defaults. + $this->assertSame( '0', $meta['aperture'] ); + $this->assertSame( '0', $meta['focal_length'] ); + $this->assertSame( '0', $meta['shutter_speed'] ); + $this->assertSame( '', $meta['alt'] ); + } + + /** + * Tests that the filter receives all expected parameters. + * + * @ticket 66248 + */ + public function test_allowed_keys_filter_receives_expected_arguments() { + $captured_args = array(); + + add_filter( + 'wp_read_image_metadata_allowed_keys', + static function ( $allowed_keys, $file, $image_type ) use ( &$captured_args ) { + $captured_args = array( + 'allowed_keys' => $allowed_keys, + 'file' => $file, + 'image_type' => $image_type, + ); + return $allowed_keys; + }, + 10, + 3 + ); + + wp_read_image_metadata( self::$exif_image ); + + $expected_keys = array( + 'aperture', + 'credit', + 'camera', + 'caption', + 'created_timestamp', + 'copyright', + 'focal_length', + 'iso', + 'shutter_speed', + 'title', + 'orientation', + 'keywords', + 'alt', + ); + + $this->assertSame( $expected_keys, $captured_args['allowed_keys'] ); + $this->assertSame( self::$exif_image, $captured_args['file'] ); + $this->assertSame( IMAGETYPE_JPEG, $captured_args['image_type'] ); + } +} From 7e24989573b4e5f74467ae2a1f48c4c6a01af482 Mon Sep 17 00:00:00 2001 From: robelsust Date: Wed, 14 Oct 2026 19:40:18 +0600 Subject: [PATCH 3/3] Media: Decouple title and caption in image metadata extraction and drop pre_wp_get_image_alttext filter. - Decouple 'title' from 'caption' in both IPTC and EXIF parsing within wp_read_image_metadata() so that allowing only 'title' still extracts titles stored in IPTC 2#120 or EXIF UserComment. - Remove pre_wp_get_image_alttext filter in favor of the unified wp_read_image_metadata_allowed_keys filter. - Add unit tests for independent title and caption extraction in readImageMetadataAllowedKeys.php. See #66248. --- src/wp-admin/includes/image.php | 32 ++----- tests/phpunit/tests/image/alttext.php | 92 ------------------- .../image/readImageMetadataAllowedKeys.php | 70 ++++++++++++++ 3 files changed, 78 insertions(+), 116 deletions(-) diff --git a/src/wp-admin/includes/image.php b/src/wp-admin/includes/image.php index 971bd08b9eedc..8aa4d66a28938 100644 --- a/src/wp-admin/includes/image.php +++ b/src/wp-admin/includes/image.php @@ -918,7 +918,7 @@ function wp_read_image_metadata( $file ) { } } - if ( in_array( 'caption', $allowed_keys, true ) && ! empty( $iptc['2#120'][0] ) ) { // Description / legacy caption. + if ( ( in_array( 'title', $allowed_keys, true ) || in_array( 'caption', $allowed_keys, true ) ) && ! empty( $iptc['2#120'][0] ) ) { // Description / legacy caption. $caption = trim( $iptc['2#120'][0] ); mbstring_binary_safe_encoding(); @@ -930,7 +930,9 @@ function wp_read_image_metadata( $file ) { $meta['title'] = $caption; } - $meta['caption'] = $caption; + if ( in_array( 'caption', $allowed_keys, true ) ) { + $meta['caption'] = $caption; + } } if ( in_array( 'credit', $allowed_keys, true ) ) { @@ -1031,8 +1033,10 @@ function wp_read_image_metadata( $file ) { $meta['caption'] = $exif_description; } } - } elseif ( in_array( 'caption', $allowed_keys, true ) && empty( $meta['caption'] ) && $exif_usercomment ) { - $meta['caption'] = $exif_usercomment; + } elseif ( $exif_usercomment && ( ( in_array( 'caption', $allowed_keys, true ) && empty( $meta['caption'] ) ) || ( in_array( 'title', $allowed_keys, true ) && empty( $meta['title'] ) ) ) ) { + if ( in_array( 'caption', $allowed_keys, true ) && empty( $meta['caption'] ) ) { + $meta['caption'] = $exif_usercomment; + } $description_length = strlen( $exif_usercomment ); if ( in_array( 'title', $allowed_keys, true ) && empty( $meta['title'] ) && $description_length < 80 ) { $meta['title'] = trim( $exif_usercomment ); @@ -1121,26 +1125,6 @@ function wp_read_image_metadata( $file ) { * @return string Embedded alternative text, empty when there is no alt text or DOM extension is not installed. */ function wp_get_image_alttext( $file ) { - /** - * Filters whether to short-circuit the parsing of alt text from an image. - * - * Returning a non-null value will short-circuit the image parsing, - * returning the passed value instead. - * - * This could be used simply to turn off the alt text generation - * (by returning an empty string from the filter instead of null). - * Or it could be used to run custom code for generating alt text. - * - * @since 7.2.0 - * - * @param string|null $override_alt_text Short-circuit return value. - * @param string $file File path to the image. - */ - $override_alt_text = apply_filters( 'pre_wp_get_image_alttext', null, $file ); - if ( null !== $override_alt_text ) { - return $override_alt_text; - } - $alt_text = ''; if ( ! class_exists( 'DOMDocument', false ) ) { diff --git a/tests/phpunit/tests/image/alttext.php b/tests/phpunit/tests/image/alttext.php index 5465850ce383c..8d22353ee090d 100644 --- a/tests/phpunit/tests/image/alttext.php +++ b/tests/phpunit/tests/image/alttext.php @@ -199,96 +199,4 @@ public function test_wp_get_image_alttext_returns_different_alt_text_based_on_si switch_to_locale( 'ja_JP' ); $this->assert_alttext_matches_or_empty_without_dom( 'Default description', wp_get_image_alttext( $file ) ); } - - /** - * Tests that pre_wp_get_image_alttext filter short-circuits and returns a custom string. - * - * @ticket 66248 - */ - public function test_pre_wp_get_image_alttext_short_circuits_with_custom_string() { - $filter = function () { - return 'Overridden custom alt text'; - }; - add_filter( 'pre_wp_get_image_alttext', $filter ); - - $alt = wp_get_image_alttext( DIR_TESTDATA . '/images/IPTC-PhotometadataRef-Std2025.1.jpg' ); - remove_filter( 'pre_wp_get_image_alttext', $filter ); - - $this->assertSame( 'Overridden custom alt text', $alt ); - } - - /** - * Tests that pre_wp_get_image_alttext filter short-circuits with an empty string to disable alt text generation. - * - * @ticket 66248 - */ - public function test_pre_wp_get_image_alttext_short_circuits_with_empty_string() { - $filter = '__return_empty_string'; - add_filter( 'pre_wp_get_image_alttext', $filter ); - - $alt = wp_get_image_alttext( DIR_TESTDATA . '/images/IPTC-PhotometadataRef-Std2025.1.jpg' ); - remove_filter( 'pre_wp_get_image_alttext', $filter ); - - $this->assertSame( '', $alt ); - } - - /** - * Tests that pre_wp_get_image_alttext returns default parsed alt text when the filter returns null. - * - * @ticket 66248 - */ - public function test_pre_wp_get_image_alttext_returns_default_when_filter_returns_null() { - $filter = '__return_null'; - add_filter( 'pre_wp_get_image_alttext', $filter ); - - $alt = wp_get_image_alttext( DIR_TESTDATA . '/images/IPTC-PhotometadataRef-Std2025.1.jpg' ); - remove_filter( 'pre_wp_get_image_alttext', $filter ); - - $this->assert_alttext_matches_or_empty_without_dom( - 'This is the Alt Text description to support accessibility in 2025.1', - $alt - ); - } - - /** - * Tests that pre_wp_get_image_alttext passes the file path parameter to the filter callback. - * - * @ticket 66248 - */ - public function test_pre_wp_get_image_alttext_passes_file_path_parameter() { - $target_file = DIR_TESTDATA . '/images/IPTC-PhotometadataRef-Std2025.1.jpg'; - $passed_args = array(); - - $filter = function ( $override, $file ) use ( &$passed_args ) { - $passed_args = array( - 'override' => $override, - 'file' => $file, - ); - return $override; - }; - add_filter( 'pre_wp_get_image_alttext', $filter, 10, 2 ); - - wp_get_image_alttext( $target_file ); - remove_filter( 'pre_wp_get_image_alttext', $filter, 10 ); - - $this->assertNull( $passed_args['override'] ); - $this->assertSame( $target_file, $passed_args['file'] ); - } - - /** - * Tests that pre_wp_get_image_alttext short-circuits without checking file existence or DOM availability. - * - * @ticket 66248 - */ - public function test_pre_wp_get_image_alttext_short_circuits_without_file_access() { - $filter = function () { - return 'Alt text from database or external API'; - }; - add_filter( 'pre_wp_get_image_alttext', $filter ); - - $alt = wp_get_image_alttext( '/non/existent/path/to/image.jpg' ); - remove_filter( 'pre_wp_get_image_alttext', $filter ); - - $this->assertSame( 'Alt text from database or external API', $alt ); - } } diff --git a/tests/phpunit/tests/image/readImageMetadataAllowedKeys.php b/tests/phpunit/tests/image/readImageMetadataAllowedKeys.php index 7861bfa8e4d76..0c45574377e55 100644 --- a/tests/phpunit/tests/image/readImageMetadataAllowedKeys.php +++ b/tests/phpunit/tests/image/readImageMetadataAllowedKeys.php @@ -223,4 +223,74 @@ static function ( $allowed_keys, $file, $image_type ) use ( &$captured_args ) { $this->assertSame( self::$exif_image, $captured_args['file'] ); $this->assertSame( IMAGETYPE_JPEG, $captured_args['image_type'] ); } + + /** + * Tests that allowing only 'title' extracts the title from IPTC 2#120 + * without extracting 'caption'. + * + * @ticket 66248 + */ + public function test_allowed_keys_with_only_title_extracts_title_from_iptc_without_caption() { + if ( ! function_exists( 'iptcembed' ) ) { + $this->markTestSkipped( 'iptcembed() is required for this test.' ); + } + + $iptc_tag = chr( 0x1c ) . chr( 0x02 ) . chr( 120 ) . chr( 0x00 ) . chr( strlen( 'IPTC Fallback Title' ) ) . 'IPTC Fallback Title'; + $data = iptcembed( $iptc_tag, DIR_TESTDATA . '/images/2004-07-22-DSC_0008.jpg' ); + $temp_file = wp_tempnam( 'iptc-title' ); + file_put_contents( $temp_file, $data ); + + add_filter( + 'wp_read_image_metadata_allowed_keys', + static function () { + return array( 'title' ); + } + ); + + $meta = wp_read_image_metadata( $temp_file ); + unlink( $temp_file ); + + $this->assertSame( 'IPTC Fallback Title', $meta['title'], 'Title should be extracted from IPTC 2#120 when only title is allowed.' ); + $this->assertSame( '', $meta['caption'], 'Caption should remain empty when caption is not in allowed keys.' ); + } + + /** + * Tests that allowing only 'title' extracts the title from EXIF metadata + * without extracting 'caption'. + * + * @ticket 66248 + */ + public function test_allowed_keys_with_only_title_extracts_title_from_exif_without_caption() { + add_filter( + 'wp_read_image_metadata_allowed_keys', + static function () { + return array( 'title' ); + } + ); + + $meta = wp_read_image_metadata( DIR_TESTDATA . '/images/2007-06-17DSC_4173.JPG' ); + + $this->assertSame( 'Copyright Alex Shiels', $meta['title'], 'Title should be extracted from EXIF when only title is allowed.' ); + $this->assertSame( '', $meta['caption'], 'Caption should remain empty when caption is not in allowed keys.' ); + } + + /** + * Tests that allowing only 'caption' extracts the caption from IPTC 2#120 + * without extracting 'title'. + * + * @ticket 66248 + */ + public function test_allowed_keys_with_only_caption_extracts_caption_without_title() { + add_filter( + 'wp_read_image_metadata_allowed_keys', + static function () { + return array( 'caption' ); + } + ); + + $meta = wp_read_image_metadata( DIR_TESTDATA . '/images/2004-07-22-DSC_0007.jpg' ); + + $this->assertSame( 'IPTC Caption', $meta['caption'], 'Caption should be extracted when caption is allowed.' ); + $this->assertSame( '', $meta['title'], 'Title should remain empty when title is not in allowed keys.' ); + } }