Commit 2066a65e861 for woocommerce

commit 2066a65e8613ec610d2be3da642480d4ef88c74f
Author: Chris Lilitsas <1105590+xristos3490@users.noreply.github.com>
Date:   Tue Jul 14 12:01:54 2026 +0300

    Fix CSV importer leaving removed gallery images on product (#66213)

    fix: clear removed gallery images on CSV product import

diff --git a/plugins/woocommerce/changelog/fix-34839-csv-import-gallery-images b/plugins/woocommerce/changelog/fix-34839-csv-import-gallery-images
new file mode 100644
index 00000000000..74b0abb7e3a
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-34839-csv-import-gallery-images
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+CSV importer: clear removed gallery images when reducing the number of images for an existing product.
diff --git a/plugins/woocommerce/includes/import/class-wc-product-csv-importer.php b/plugins/woocommerce/includes/import/class-wc-product-csv-importer.php
index 864f7be875f..0e506672e9b 100644
--- a/plugins/woocommerce/includes/import/class-wc-product-csv-importer.php
+++ b/plugins/woocommerce/includes/import/class-wc-product-csv-importer.php
@@ -885,7 +885,12 @@ class WC_Product_CSV_Importer extends WC_Product_Importer {
 			$images               = $data['images'];
 			$data['raw_image_id'] = array_shift( $images );

-			if ( ! empty( $images ) ) {
+			// When a featured image is provided, treat the remaining values as the full
+			// gallery. Setting the key even when no gallery images remain ensures that
+			// reducing the number of images in the CSV clears previously imported gallery
+			// images instead of leaving them in place.
+			// See https://github.com/woocommerce/woocommerce/issues/34839.
+			if ( ! empty( $data['raw_image_id'] ) ) {
 				$data['raw_gallery_image_ids'] = $images;
 			}
 			unset( $data['images'] );
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/importer/product.php b/plugins/woocommerce/tests/legacy/unit-tests/importer/product.php
index 3c9a21d293c..050362a92fe 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/importer/product.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/importer/product.php
@@ -224,6 +224,90 @@ class WC_Tests_Product_CSV_Importer extends WC_Unit_Test_Case {
 		$this->assertEquals( 20, $updated_product->get_price() );
 	}

+	/**
+	 * @testdox Reducing the number of images in a CSV clears previously imported gallery images.
+	 *
+	 * Regression test for https://github.com/woocommerce/woocommerce/issues/34839: replacing a
+	 * product's two images with a single image left the removed gallery image in place instead
+	 * of clearing it.
+	 */
+	public function test_reducing_images_clears_removed_gallery_images() {
+		$featured_url = 'http://example.com/featured.jpg';
+		$gallery_url  = 'http://example.com/gallery.jpg';
+		$new_url      = 'http://example.com/new.jpg';
+
+		$featured_id = $this->create_sourced_attachment( $featured_url );
+		$gallery_id  = $this->create_sourced_attachment( $gallery_url );
+		$new_id      = $this->create_sourced_attachment( $new_url );
+
+		$product = WC_Helper_Product::create_simple_product();
+		$product->set_image_id( $featured_id );
+		$product->set_gallery_image_ids( array( $gallery_id ) );
+		$product->save();
+
+		$expanded = $this->invoke_protected( $this->sut, 'expand_data', array( array( 'images' => array( $new_url ) ) ) );
+		$this->invoke_protected( $this->sut, 'set_image_data', array( &$product, $expanded ) );
+
+		$this->assertEquals( $new_id, $product->get_image_id(), 'Featured image should be replaced with the new image.' );
+		$this->assertEquals( array(), $product->get_gallery_image_ids(), 'Removed gallery images should be cleared.' );
+	}
+
+	/**
+	 * @testdox An empty images column leaves the existing featured image and gallery untouched.
+	 *
+	 * Guards against over-clearing: a blank or absent Images value must not wipe images that
+	 * were previously set on the product. See https://github.com/woocommerce/woocommerce/issues/34839.
+	 */
+	public function test_empty_images_column_preserves_existing_images() {
+		$featured_id = $this->create_sourced_attachment( 'http://example.com/featured.jpg' );
+		$gallery_id  = $this->create_sourced_attachment( 'http://example.com/gallery.jpg' );
+
+		$product = WC_Helper_Product::create_simple_product();
+		$product->set_image_id( $featured_id );
+		$product->set_gallery_image_ids( array( $gallery_id ) );
+		$product->save();
+
+		$expanded = $this->invoke_protected( $this->sut, 'expand_data', array( array( 'images' => array() ) ) );
+		$this->invoke_protected( $this->sut, 'set_image_data', array( &$product, $expanded ) );
+
+		$this->assertEquals( $featured_id, $product->get_image_id(), 'Featured image should be preserved.' );
+		$this->assertEquals( array( $gallery_id ), $product->get_gallery_image_ids(), 'Existing gallery should be preserved.' );
+	}
+
+	/**
+	 * Create an attachment whose source URL is recorded, so the importer can resolve it by URL.
+	 *
+	 * @param string $url Source URL of the image.
+	 * @return int Attachment ID.
+	 */
+	private function create_sourced_attachment( $url ) {
+		$attachment_id = wp_insert_attachment(
+			array(
+				'post_title'     => basename( $url ),
+				'post_mime_type' => 'image/jpeg',
+				'post_status'    => 'inherit',
+			)
+		);
+		update_post_meta( $attachment_id, '_wc_attachment_source', $url );
+
+		return $attachment_id;
+	}
+
+	/**
+	 * Invoke a protected method on an object via reflection.
+	 *
+	 * @param object $target      Object to invoke the method on.
+	 * @param string $method_name Method name.
+	 * @param array  $args        Arguments to pass to the method.
+	 * @return mixed Return value of the method.
+	 */
+	private function invoke_protected( $target, $method_name, $args ) {
+		$method = new ReflectionMethod( $target, $method_name );
+		$method->setAccessible( true );
+
+		return $method->invokeArgs( $target, $args );
+	}
+
 	/**
 	 * Test importing file located on another location on server.
 	 *
@@ -576,40 +660,41 @@ class WC_Tests_Product_CSV_Importer extends WC_Unit_Test_Case {
 				'menu_order'            => 3,
 			),
 			array(
-				'type'               => ProductType::VARIATION,
-				'sku'                => '',
-				'name'               => '',
-				'featured'           => '',
-				'catalog_visibility' => CatalogVisibility::VISIBLE,
-				'short_description'  => '',
-				'description'        => 'Lorem ipsum dolor sit amet, at exerci civibus appetere sit, iuvaret hendrerit mea no. Eam integre feugait liberavisse an.',
-				'date_on_sale_from'  => null,
-				'date_on_sale_to'    => null,
-				'tax_status'         => ProductTaxStatus::TAXABLE,
-				'tax_class'          => 'standard',
-				'stock_status'       => ProductStockStatus::IN_STOCK,
-				'stock_quantity'     => 6,
-				'backorders'         => 'no',
-				'sold_individually'  => '',
-				'weight'             => 1.0,
-				'length'             => 2.0,
-				'width'              => 25.0,
-				'height'             => 55.0,
-				'reviews_allowed'    => '',
-				'purchase_note'      => '',
-				'sale_price'         => '',
-				'regular_price'      => '20',
-				'shipping_class_id'  => 0,
-				'download_limit'     => '',
-				'download_expiry'    => '',
-				'product_url'        => '',
-				'button_text'        => '',
-				'status'             => ProductStatus::PUBLISH,
-				'raw_image_id'       => 'http://demo.woothemes.com/woocommerce/wp-content/uploads/sites/56/2013/06/T_4_front.jpg',
-				'virtual'            => false,
-				'downloadable'       => false,
-				'manage_stock'       => true,
-				'raw_attributes'     => array(
+				'type'                  => ProductType::VARIATION,
+				'sku'                   => '',
+				'name'                  => '',
+				'featured'              => '',
+				'catalog_visibility'    => CatalogVisibility::VISIBLE,
+				'short_description'     => '',
+				'description'           => 'Lorem ipsum dolor sit amet, at exerci civibus appetere sit, iuvaret hendrerit mea no. Eam integre feugait liberavisse an.',
+				'date_on_sale_from'     => null,
+				'date_on_sale_to'       => null,
+				'tax_status'            => ProductTaxStatus::TAXABLE,
+				'tax_class'             => 'standard',
+				'stock_status'          => ProductStockStatus::IN_STOCK,
+				'stock_quantity'        => 6,
+				'backorders'            => 'no',
+				'sold_individually'     => '',
+				'weight'                => 1.0,
+				'length'                => 2.0,
+				'width'                 => 25.0,
+				'height'                => 55.0,
+				'reviews_allowed'       => '',
+				'purchase_note'         => '',
+				'sale_price'            => '',
+				'regular_price'         => '20',
+				'shipping_class_id'     => 0,
+				'download_limit'        => '',
+				'download_expiry'       => '',
+				'product_url'           => '',
+				'button_text'           => '',
+				'status'                => ProductStatus::PUBLISH,
+				'raw_image_id'          => 'http://demo.woothemes.com/woocommerce/wp-content/uploads/sites/56/2013/06/T_4_front.jpg',
+				'raw_gallery_image_ids' => array(),
+				'virtual'               => false,
+				'downloadable'          => false,
+				'manage_stock'          => true,
+				'raw_attributes'        => array(
 					array(
 						'name' => 'Color',
 					),
@@ -618,43 +703,44 @@ class WC_Tests_Product_CSV_Importer extends WC_Unit_Test_Case {
 						'name'  => 'Size',
 					),
 				),
-				'menu_order'         => 1,
+				'menu_order'            => 1,
 			),
 			array(
-				'type'               => ProductType::VARIATION,
-				'sku'                => '',
-				'name'               => '',
-				'featured'           => '',
-				'catalog_visibility' => CatalogVisibility::VISIBLE,
-				'short_description'  => '',
-				'description'        => 'Lorem ipsum dolor sit amet, at exerci civibus appetere sit, iuvaret hendrerit mea no. Eam integre feugait liberavisse an.',
-				'date_on_sale_from'  => null,
-				'date_on_sale_to'    => null,
-				'tax_status'         => ProductTaxStatus::TAXABLE,
-				'tax_class'          => 'standard',
-				'stock_status'       => ProductStockStatus::IN_STOCK,
-				'stock_quantity'     => 10,
-				'backorders'         => 'yes',
-				'sold_individually'  => '',
-				'weight'             => 1.0,
-				'length'             => 2.0,
-				'width'              => 25.0,
-				'height'             => 55.0,
-				'reviews_allowed'    => '',
-				'purchase_note'      => '',
-				'sale_price'         => '17.99',
-				'regular_price'      => '20',
-				'shipping_class_id'  => 0,
-				'download_limit'     => '',
-				'download_expiry'    => '',
-				'product_url'        => '',
-				'button_text'        => '',
-				'status'             => ProductStatus::PUBLISH,
-				'raw_image_id'       => 'http://demo.woothemes.com/woocommerce/wp-content/uploads/sites/56/2013/06/T_3_front.jpg',
-				'virtual'            => false,
-				'downloadable'       => false,
-				'manage_stock'       => true,
-				'raw_attributes'     => array(
+				'type'                  => ProductType::VARIATION,
+				'sku'                   => '',
+				'name'                  => '',
+				'featured'              => '',
+				'catalog_visibility'    => CatalogVisibility::VISIBLE,
+				'short_description'     => '',
+				'description'           => 'Lorem ipsum dolor sit amet, at exerci civibus appetere sit, iuvaret hendrerit mea no. Eam integre feugait liberavisse an.',
+				'date_on_sale_from'     => null,
+				'date_on_sale_to'       => null,
+				'tax_status'            => ProductTaxStatus::TAXABLE,
+				'tax_class'             => 'standard',
+				'stock_status'          => ProductStockStatus::IN_STOCK,
+				'stock_quantity'        => 10,
+				'backorders'            => 'yes',
+				'sold_individually'     => '',
+				'weight'                => 1.0,
+				'length'                => 2.0,
+				'width'                 => 25.0,
+				'height'                => 55.0,
+				'reviews_allowed'       => '',
+				'purchase_note'         => '',
+				'sale_price'            => '17.99',
+				'regular_price'         => '20',
+				'shipping_class_id'     => 0,
+				'download_limit'        => '',
+				'download_expiry'       => '',
+				'product_url'           => '',
+				'button_text'           => '',
+				'status'                => ProductStatus::PUBLISH,
+				'raw_image_id'          => 'http://demo.woothemes.com/woocommerce/wp-content/uploads/sites/56/2013/06/T_3_front.jpg',
+				'raw_gallery_image_ids' => array(),
+				'virtual'               => false,
+				'downloadable'          => false,
+				'manage_stock'          => true,
+				'raw_attributes'        => array(
 					array(
 						'name' => 'Color',
 					),
@@ -663,7 +749,7 @@ class WC_Tests_Product_CSV_Importer extends WC_Unit_Test_Case {
 						'name'  => 'Size',
 					),
 				),
-				'menu_order'         => 2,
+				'menu_order'            => 2,
 			),
 			array(
 				'type'                  => ProductType::GROUPED,