Commit b39ce3a2aa1 for woocommerce

commit b39ce3a2aa1d58832e6111ae9bccc098007b90c6
Author: Mustafa Bharmal <92981596+Mustafabharmal@users.noreply.github.com>
Date:   Thu Oct 22 17:53:31 2026 +0530

    Products: Avoid duplicating the featured image in REST API and Store API images (#70134)

diff --git a/plugins/woocommerce/changelog/38462-fix-duplicate-featured-image-in-product-images b/plugins/woocommerce/changelog/38462-fix-duplicate-featured-image-in-product-images
new file mode 100644
index 00000000000..2dd5c820da4
--- /dev/null
+++ b/plugins/woocommerce/changelog/38462-fix-duplicate-featured-image-in-product-images
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Stop the product REST API and Store API from listing the featured image twice when it is also in the product gallery.
diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-products-v2-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-products-v2-controller.php
index a7b3e0843bf..d3cb94cccef 100644
--- a/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-products-v2-controller.php
+++ b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-products-v2-controller.php
@@ -554,8 +554,8 @@ class WC_REST_Products_V2_Controller extends WC_REST_CRUD_Controller {
 			$attachment_ids[] = $product->get_image_id();
 		}

-		// Add gallery images.
-		$attachment_ids = array_merge( $attachment_ids, $product->get_gallery_image_ids() );
+		// Add gallery images, skipping the featured image if it is also in the gallery.
+		$attachment_ids = array_merge( $attachment_ids, array_diff( $product->get_gallery_image_ids(), $attachment_ids ) );

 		if ( ! empty( $attachment_ids ) ) {
 			// Prime caches to reduce future queries.
diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php
index 09b221e5710..077ed424283 100644
--- a/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php
+++ b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php
@@ -184,8 +184,8 @@ class WC_REST_Products_Controller extends WC_REST_Products_V2_Controller {
 			$attachment_ids[] = $product->get_image_id();
 		}

-		// Add gallery images.
-		$attachment_ids = array_merge( $attachment_ids, $product->get_gallery_image_ids() );
+		// Add gallery images, skipping the featured image if it is also in the gallery.
+		$attachment_ids = array_merge( $attachment_ids, array_diff( $product->get_gallery_image_ids(), $attachment_ids ) );

 		if ( ! empty( $attachment_ids ) ) {
 			// Prime caches to reduce future queries.
diff --git a/plugins/woocommerce/src/Internal/RestApi/Routes/V4/Products/Controller.php b/plugins/woocommerce/src/Internal/RestApi/Routes/V4/Products/Controller.php
index e449959c135..2c7b566b489 100644
--- a/plugins/woocommerce/src/Internal/RestApi/Routes/V4/Products/Controller.php
+++ b/plugins/woocommerce/src/Internal/RestApi/Routes/V4/Products/Controller.php
@@ -298,8 +298,8 @@ class Controller extends WC_REST_Products_V2_Controller {
 			$attachment_ids[] = $product->get_image_id();
 		}

-		// Add gallery images.
-		$attachment_ids = array_merge( $attachment_ids, $product->get_gallery_image_ids() );
+		// Add gallery images, skipping the featured image if it is also in the gallery.
+		$attachment_ids = array_merge( $attachment_ids, array_diff( $product->get_gallery_image_ids(), $attachment_ids ) );

 		if ( ! empty( $attachment_ids ) ) {
 			// Prime caches to reduce future queries.
diff --git a/plugins/woocommerce/src/StoreApi/Schemas/V1/ProductSchema.php b/plugins/woocommerce/src/StoreApi/Schemas/V1/ProductSchema.php
index ca2ae649e41..486e2a7628f 100644
--- a/plugins/woocommerce/src/StoreApi/Schemas/V1/ProductSchema.php
+++ b/plugins/woocommerce/src/StoreApi/Schemas/V1/ProductSchema.php
@@ -691,7 +691,7 @@ class ProductSchema extends AbstractSchema {
 	 * @return array
 	 */
 	protected function get_images( \WC_Product $product ) {
-		$attachment_ids = array_filter( array_merge( [ $product->get_image_id() ], $product->get_gallery_image_ids() ) );
+		$attachment_ids = array_unique( array_filter( array_merge( [ $product->get_image_id() ], $product->get_gallery_image_ids() ) ) );
 		if ( ! empty( $attachment_ids ) ) {
 			// Prime caches to reduce future queries.
 			_prime_post_caches( $attachment_ids );
diff --git a/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller-tests.php b/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller-tests.php
index ccd73a6a10c..8eacee85f2d 100644
--- a/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller-tests.php
+++ b/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller-tests.php
@@ -2895,4 +2895,27 @@ class WC_REST_Products_Controller_Tests extends WC_Unit_Test_Case {
 		$this->assertSame( 'rest_invalid_param', $response->get_data()['code'], 'Schema validation should reject the non-string value.' );
 		$this->assertFalse( metadata_exists( 'post', $product->get_id(), '_customs_commodity_code' ), 'A rejected commodity code should not be stored.' );
 	}
+
+	/**
+	 * @testdox The featured image is returned only once when it is also part of the product gallery.
+	 */
+	public function test_get_product_does_not_duplicate_featured_image_in_gallery(): void {
+		$image_file  = WC_Unit_Tests_Bootstrap::instance()->tests_dir . '/data/Dr1Bczxq4q.png';
+		$featured_id = self::factory()->attachment->create_upload_object( $image_file );
+		$gallery_id  = self::factory()->attachment->create_upload_object( $image_file );
+
+		$product = WC_Helper_Product::create_simple_product();
+		$product->set_image_id( $featured_id );
+		$product->set_gallery_image_ids( array( $featured_id, $gallery_id ) );
+		$product->save();
+
+		$response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/products/' . $product->get_id() ) );
+
+		$this->assertSame( 200, $response->get_status() );
+		$this->assertSame(
+			array( $featured_id, $gallery_id ),
+			wp_list_pluck( $response->get_data()['images'], 'id' ),
+			'The featured image should be listed first and only once.'
+		);
+	}
 }
diff --git a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/Products.php b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/Products.php
index 09950d28347..1dc28ca8c6c 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/Products.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/Products.php
@@ -396,6 +396,31 @@ class Products extends ControllerTestCase {
 		$this->assertCount( 0, $data['images'] );
 	}

+	/**
+	 * @testdox The featured image is returned only once when it is also part of the product gallery.
+	 */
+	public function test_featured_image_in_gallery_is_not_duplicated(): void {
+		$fixtures    = new FixtureData();
+		$featured_id = $fixtures->create_image_attachment();
+		$gallery_id  = $fixtures->create_image_attachment();
+		$product     = $fixtures->get_simple_product(
+			array(
+				'name'              => 'Test Product 1',
+				'regular_price'     => 10,
+				'image_id'          => $featured_id,
+				'gallery_image_ids' => array( $featured_id, $gallery_id ),
+			)
+		);
+
+		$response = rest_get_server()->dispatch( new \WP_REST_Request( 'GET', '/wc/store/v1/products/' . $product->get_id() ) );
+
+		$this->assertSame(
+			array( $featured_id, $gallery_id ),
+			wp_list_pluck( $response->get_data()['images'], 'id' ),
+			'The featured image should be listed first and only once.'
+		);
+	}
+
 	/**
 	 * Test product category image return types.
 	 */