Commit 25f31872565 for woocommerce

commit 25f3187256500aef460a9247b03b73a71819bfaf
Author: Karol Manijak <20098064+kmanijak@users.noreply.github.com>
Date:   Tue Jul 21 11:49:56 2026 +0200

    Add placeholder image source filtering (#66717)

    * Fix placeholder image source filtering

    * Add changelog entry for placeholder image filtering

    * Preserve responsive placeholder markup

    * Fix placeholder image source linting

diff --git a/plugins/woocommerce/changelog/fix-placeholder-img-src-filter b/plugins/woocommerce/changelog/fix-placeholder-img-src-filter
new file mode 100644
index 00000000000..311519e66d2
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-placeholder-img-src-filter
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Ensure the woocommerce_placeholder_img_src filter applies to attachment-based product placeholders.
diff --git a/plugins/woocommerce/includes/wc-product-functions.php b/plugins/woocommerce/includes/wc-product-functions.php
index e3f39cdf942..56a7c3dab08 100644
--- a/plugins/woocommerce/includes/wc-product-functions.php
+++ b/plugins/woocommerce/includes/wc-product-functions.php
@@ -444,8 +444,15 @@ function wc_placeholder_img_src( $size = 'woocommerce_thumbnail' ) {
  * @return string
  */
 function wc_placeholder_img( $size = 'woocommerce_thumbnail', $attr = '' ) {
-	$dimensions        = wc_get_image_size( $size );
-	$placeholder_image = get_option( 'woocommerce_placeholder_image', 0 );
+	$dimensions           = wc_get_image_size( $size );
+	$placeholder_image    = get_option( 'woocommerce_placeholder_image', 0 );
+	$use_attachment_image = wp_attachment_is_image( $placeholder_image );
+	$image                = null;
+
+	if ( $use_attachment_image && has_filter( 'woocommerce_placeholder_img_src' ) ) {
+		$image                = wc_placeholder_img_src( $size );
+		$use_attachment_image = wp_get_attachment_image_url( $placeholder_image, $size ) === $image;
+	}

 	$default_attr = array(
 		'class' => 'woocommerce-placeholder wp-post-image',
@@ -454,7 +461,7 @@ function wc_placeholder_img( $size = 'woocommerce_thumbnail', $attr = '' ) {

 	$attr = wp_parse_args( $attr, $default_attr );

-	if ( wp_attachment_is_image( $placeholder_image ) ) {
+	if ( $use_attachment_image ) {
 		$image_html = wp_get_attachment_image(
 			$placeholder_image,
 			$size,
@@ -462,7 +469,8 @@ function wc_placeholder_img( $size = 'woocommerce_thumbnail', $attr = '' ) {
 			$attr
 		);
 	} else {
-		$image      = wc_placeholder_img_src( $size );
+		// A changed source cannot use the attachment's srcset, as the browser could select it instead.
+		$image      = $image ?? wc_placeholder_img_src( $size );
 		$hwstring   = image_hwstring( $dimensions['width'], $dimensions['height'] );
 		$attributes = array();

diff --git a/plugins/woocommerce/tests/legacy/unit-tests/product/functions.php b/plugins/woocommerce/tests/legacy/unit-tests/product/functions.php
index 4acae40fb66..ac1cbbfb5d5 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/product/functions.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/product/functions.php
@@ -1015,6 +1015,72 @@ class WC_Tests_Product_Functions extends WC_Unit_Test_Case {
 		$this->assertStringContainsString( 'class="custom-class"', wc_placeholder_img( 'woocommerce_thumbnail', $attr ) );
 	}

+	/**
+	 * @testdox Should use the filtered placeholder source for an attachment placeholder.
+	 */
+	public function test_wc_placeholder_img_uses_filtered_src_for_attachment_placeholder() {
+		$option_name                = 'woocommerce_placeholder_image';
+		$original_placeholder_image = get_option( $option_name, false );
+		$placeholder_image_id       = self::factory()->attachment->create(
+			array(
+				'file'           => 'placeholder.png',
+				'guid'           => 'https://example.com/wp-content/uploads/placeholder.png',
+				'post_mime_type' => 'image/png',
+			)
+		);
+		$placeholder_src            = 'https://example.com/custom-placeholder.png';
+		$filter                     = function () use ( $placeholder_src ) {
+			return $placeholder_src;
+		};
+		$passthrough_filter         = function ( $src ) {
+			return $src;
+		};
+
+		wp_update_attachment_metadata(
+			$placeholder_image_id,
+			array(
+				'width'  => 186,
+				'height' => 144,
+				'file'   => 'placeholder.png',
+			)
+		);
+		update_option( $option_name, $placeholder_image_id );
+		add_filter( 'woocommerce_placeholder_img_src', $filter );
+
+		try {
+			$image_html = wc_placeholder_img();
+
+			$this->assertStringContainsString( 'src="' . $placeholder_src . '"', $image_html );
+			$this->assertStringNotContainsString( 'srcset=', $image_html );
+
+			remove_filter( 'woocommerce_placeholder_img_src', $filter );
+			add_filter( 'woocommerce_placeholder_img_src', $passthrough_filter );
+
+			$this->assertSame(
+				wp_get_attachment_image(
+					$placeholder_image_id,
+					'woocommerce_thumbnail',
+					false,
+					array(
+						'class' => 'woocommerce-placeholder wp-post-image',
+						'alt'   => __( 'Placeholder', 'woocommerce' ),
+					)
+				),
+				wc_placeholder_img()
+			);
+		} finally {
+			remove_filter( 'woocommerce_placeholder_img_src', $filter );
+			remove_filter( 'woocommerce_placeholder_img_src', $passthrough_filter );
+			wp_delete_attachment( $placeholder_image_id, true );
+
+			if ( false === $original_placeholder_image ) {
+				delete_option( $option_name );
+			} else {
+				update_option( $option_name, $original_placeholder_image );
+			}
+		}
+	}
+
 	/**
 	 * Test wc_get_product_types().
 	 *