Commit 5a7f984ccf5 for woocommerce

commit 5a7f984ccf536dbb5eafdca424d99a0cedfbfd34
Author: Ahmed Ismail <105542236+ahmedevsaad@users.noreply.github.com>
Date:   Thu Nov 5 17:13:47 2026 +0200

    Fix extra backslashes when updating a review through the REST API (#69295)

    * Fix extra backslashes when updating a review through the REST API

    * Keep backslashes when a review is updated through the v1 and v2 REST API

    update_item() passed unslashed data to wp_update_comment(), which unslashes
    it again, so backslashes were stripped. Slash it like create_item() does.
    This also covers the v2 route and batch endpoint, which inherit the method.

diff --git a/plugins/woocommerce/changelog/34193-fix-rest-review-update-slashes b/plugins/woocommerce/changelog/34193-fix-rest-review-update-slashes
new file mode 100644
index 00000000000..a7a7321d3ea
--- /dev/null
+++ b/plugins/woocommerce/changelog/34193-fix-rest-review-update-slashes
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Keep quotes and backslashes as entered when a product review is edited through the REST API.
diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-product-reviews-v1-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-product-reviews-v1-controller.php
index 9eaa821ac3b..ff07b391db8 100644
--- a/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-product-reviews-v1-controller.php
+++ b/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-product-reviews-v1-controller.php
@@ -399,8 +399,9 @@ class WC_REST_Product_Reviews_V1_Controller extends WC_REST_Controller {
 		/*
 		 * Zero means no comment-table row changed; false is the actual update failure. A rating-only
 		 * request legitimately returns zero while still storing comment_meta and refreshing counts.
+		 * wp_update_comment() expects slashed data, like wp_insert_comment() in create_item().
 		 */
-		$updated = wp_update_comment( $prepared_review );
+		$updated = wp_update_comment( wp_slash( $prepared_review ) );
 		if ( false === $updated ) {
 			return new WP_Error( 'rest_product_review_failed_edit', __( 'Updating product review failed.', 'woocommerce' ), array( 'status' => 500 ) );
 		}
diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-product-reviews-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-product-reviews-controller.php
index 168880ad192..27a8a2fa426 100644
--- a/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-product-reviews-controller.php
+++ b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-product-reviews-controller.php
@@ -949,7 +949,7 @@ class WC_REST_Product_Reviews_Controller extends WC_REST_Controller {
 					'type'        => 'string',
 					'context'     => array( 'view', 'edit' ),
 					'arg_options' => array(
-						'sanitize_callback' => 'wp_filter_post_kses',
+						'sanitize_callback' => 'wp_kses_post',
 					),
 				),
 				'rating'           => array(
diff --git a/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version2/class-wc-rest-product-reviews-v2-controller-tests.php b/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version2/class-wc-rest-product-reviews-v2-controller-tests.php
index 2000d4c7de7..fa40e55e828 100644
--- a/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version2/class-wc-rest-product-reviews-v2-controller-tests.php
+++ b/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version2/class-wc-rest-product-reviews-v2-controller-tests.php
@@ -87,6 +87,89 @@ class WC_REST_Product_Reviews_V2_Controller_Test extends WC_REST_Unit_Test_case
 		$this->assertSame( 3, (int) get_comment_meta( $review_id, 'rating', true ) );
 	}

+	/**
+	 * Review content that must be stored exactly as sent.
+	 *
+	 * @return array
+	 */
+	public function provide_review_content_with_slashes(): array {
+		return array(
+			'quotes'    => array( 'It\'s "great"' ),
+			'backslash' => array( 'Cable 1\\2 inch' ),
+			'mixed'     => array( 'It\'s a 1\\2" cable' ),
+		);
+	}
+
+	/**
+	 * @testdox The wc/v2 update route stores quotes and backslashes in review content as sent.
+	 * @dataProvider provide_review_content_with_slashes
+	 *
+	 * @param string $content Review content.
+	 */
+	public function test_wc_v2_update_item_stores_review_content_as_sent( string $content ): void {
+		wp_set_current_user( $this->shop_manager_id );
+		$product_id = ProductHelper::create_simple_product()->get_id();
+		$review_id  = $this->create_v2_review( $product_id );
+
+		$update = new WP_REST_Request( 'PUT', '/wc/v2/products/' . $product_id . '/reviews/' . $review_id );
+		$update->set_body_params( array( 'review' => $content ) );
+		$response = $this->server->dispatch( $update );
+
+		$this->assertSame( 200, $response->get_status() );
+		$this->assertSame( $content, get_comment( $review_id )->comment_content );
+	}
+
+	/**
+	 * @testdox The wc/v2 batch route stores quotes and backslashes in updated review content as sent.
+	 * @dataProvider provide_review_content_with_slashes
+	 *
+	 * @param string $content Review content.
+	 */
+	public function test_wc_v2_batch_update_stores_review_content_as_sent( string $content ): void {
+		wp_set_current_user( $this->shop_manager_id );
+		$product_id = ProductHelper::create_simple_product()->get_id();
+		$review_id  = $this->create_v2_review( $product_id );
+
+		$batch = new WP_REST_Request( 'POST', '/wc/v2/products/' . $product_id . '/reviews/batch' );
+		$batch->set_body_params(
+			array(
+				'update' => array(
+					array(
+						'id'     => $review_id,
+						'review' => $content,
+					),
+				),
+			)
+		);
+		$response = $this->server->dispatch( $batch );
+
+		$this->assertSame( 200, $response->get_status() );
+		$this->assertSame( $content, get_comment( $review_id )->comment_content );
+	}
+
+	/**
+	 * Create a review through the wc/v2 route.
+	 *
+	 * @param int $product_id Product ID.
+	 * @return int Review ID.
+	 */
+	private function create_v2_review( int $product_id ): int {
+		$create = new WP_REST_Request( 'POST', '/wc/v2/products/' . $product_id . '/reviews' );
+		$create->set_body_params(
+			array(
+				'review' => 'First draft.',
+				'name'   => 'Jane Smith',
+				'email'  => 'jane.smith@example.org',
+				'rating' => 5,
+			)
+		);
+		$created = $this->server->dispatch( $create );
+
+		$this->assertSame( 201, $created->get_status() );
+
+		return (int) $created->get_data()['id'];
+	}
+
 	/**
 	 * @testdox Creating a review eagerly populates the verified-owner meta at creation time, not lazily during response preparation.
 	 */
diff --git a/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-product-reviews-controller-tests.php b/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-product-reviews-controller-tests.php
index 75f7762bfa7..2695cdc3b4b 100644
--- a/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-product-reviews-controller-tests.php
+++ b/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-product-reviews-controller-tests.php
@@ -300,6 +300,38 @@ class WC_REST_Product_Reviews_Controller_Tests extends WC_REST_Unit_Test_Case {
 		$this->assertEquals( 2, $product->get_review_count(), 'An unrated review is still counted as a review.' );
 	}

+	/**
+	 * Review content that must be stored exactly as sent.
+	 *
+	 * @return array
+	 */
+	public function provide_review_content_with_slashes(): array {
+		return array(
+			'quotes'    => array( 'It\'s "great"' ),
+			'backslash' => array( 'Cable 1\\2 inch' ),
+			'mixed'     => array( 'It\'s a 1\\2" cable' ),
+		);
+	}
+
+	/**
+	 * @testdox Updating review content stores quotes and backslashes as sent.
+	 * @dataProvider provide_review_content_with_slashes
+	 *
+	 * @param string $content Review content.
+	 */
+	public function test_update_item_stores_review_content_as_sent( string $content ): void {
+		wp_set_current_user( $this->shop_manager_id );
+		$product_id = ProductHelper::create_simple_product()->get_id();
+		$review_id  = $this->create_review( $product_id, 'First draft.', 5 )->get_data()['id'];
+
+		$request = new WP_REST_Request( 'PUT', '/wc/v3/products/reviews/' . $review_id );
+		$request->set_body_params( array( 'review' => $content ) );
+		$response = $this->server->dispatch( $request );
+
+		$this->assertEquals( 200, $response->get_status() );
+		$this->assertSame( $content, get_comment( $review_id )->comment_content );
+	}
+
 	/**
 	 * @testdox A rating-only edit stores the rating and refreshes aggregates in one product save.
 	 */