Commit 72a6b65c7a for woocommerce

commit 72a6b65c7aebdbdb844381e2f1b94ca6b6e59476
Author: Luigi Teschio <gigitux@gmail.com>
Date:   Tue Dec 23 15:18:14 2025 +0100

    Fix URL decoding for product variation attributes in REST API (#62562)

    * Fix URL decoding for product variation attributes in REST API

    * add changelog

diff --git a/plugins/woocommerce/changelog/fix-variation-special-char-follow-up b/plugins/woocommerce/changelog/fix-variation-special-char-follow-up
new file mode 100644
index 0000000000..8032c28e43
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-variation-special-char-follow-up
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Fix decoding attribute option name
diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-product-variations-v2-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-product-variations-v2-controller.php
index 94bdcf7aee..b3ecfb77cb 100644
--- a/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-product-variations-v2-controller.php
+++ b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-product-variations-v2-controller.php
@@ -497,7 +497,7 @@ class WC_REST_Product_Variations_V2_Controller extends WC_REST_Products_V2_Contr
 				}

 				$attribute_key   = sanitize_title( $parent_attributes[ $attribute_name ]->get_name() );
-				$attribute_value = isset( $attribute['option'] ) ? wc_clean( stripslashes( $attribute['option'] ) ) : '';
+				$attribute_value = isset( $attribute['option'] ) ? wc_clean( rawurldecode( stripslashes( $attribute['option'] ) ) ) : '';

 				if ( $parent_attributes[ $attribute_name ]->is_taxonomy() ) {
 					// If dealing with a taxonomy, we need to get the slug from the name posted to the API.
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 4803c51428..c6beb25063 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
@@ -689,7 +689,7 @@ class WC_REST_Products_V2_Controller extends WC_REST_CRUD_Controller {
 						'id'     => wc_attribute_taxonomy_id_by_name( $name ),
 						'name'   => $this->get_attribute_taxonomy_name( $name, $_product ),
 						'slug'   => rawurldecode( $name ),
-						'option' => $option_term && ! is_wp_error( $option_term ) ? $option_term->name : $attribute,
+						'option' => $option_term && ! is_wp_error( $option_term ) ? rawurldecode( $option_term->name ) : rawurldecode( $attribute ),
 					);
 				} else {
 					$attributes[] = array(
@@ -1631,7 +1631,7 @@ class WC_REST_Products_V2_Controller extends WC_REST_CRUD_Controller {
 					$_attribute = $attributes[ $attribute_name ];

 					if ( $_attribute['is_variation'] ) {
-						$value = isset( $attribute['option'] ) ? wc_clean( stripslashes( $attribute['option'] ) ) : '';
+						$value = isset( $attribute['option'] ) ? wc_clean( rawurldecode( stripslashes( $attribute['option'] ) ) ) : '';

 						if ( ! empty( $_attribute['is_taxonomy'] ) ) {
 							// If dealing with a taxonomy, we need to get the slug from the name posted to the API.
diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-product-variations-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-product-variations-controller.php
index fc3d505938..3533f85cbf 100644
--- a/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-product-variations-controller.php
+++ b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-product-variations-controller.php
@@ -364,7 +364,7 @@ class WC_REST_Product_Variations_Controller extends WC_REST_Product_Variations_V
 				}

 				$attribute_key   = sanitize_title( $parent_attributes[ $attribute_name ]->get_name() );
-				$attribute_value = isset( $attribute['option'] ) ? wc_clean( stripslashes( $attribute['option'] ) ) : '';
+				$attribute_value = isset( $attribute['option'] ) ? wc_clean( rawurldecode( stripslashes( $attribute['option'] ) ) ) : '';

 				if ( $parent_attributes[ $attribute_name ]->is_taxonomy() ) {
 					// If dealing with a taxonomy, we need to get the slug from the name posted to the API.
diff --git a/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-product-variations-controller-tests.php b/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-product-variations-controller-tests.php
index a26374466e..e5937b4ffd 100644
--- a/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-product-variations-controller-tests.php
+++ b/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-product-variations-controller-tests.php
@@ -226,6 +226,65 @@ class WC_REST_Product_Variations_Controller_Tests extends WC_REST_Unit_Test_Case
 		$this->assertEquals( 'green', $retrieved_variation['attributes'][0]['option'], 'Variation should contain color option green' );
 	}

+	/**
+	 * Test that creating a variation with URL-encoded attribute option values
+	 * properly decodes and saves the attributes.
+	 *
+	 * This test verifies the fix for URL-encoded attribute options (e.g., special characters
+	 * like Persian/unicode) are properly decoded when sent via the REST API.
+	 */
+	public function test_create_variation_with_url_encoded_attribute_option() {
+		// Create a variable product with a Persian attribute name and options.
+		$product         = WC_Helper_Product::create_variation_product();
+		$color_attribute = WC_Helper_Product::create_product_attribute_object( 'رنگ', array( 'آبی', 'سبز' ) );
+		$product->set_attributes( array( $color_attribute ) );
+		$product->save();
+
+		// Create a variation via REST API with URL-encoded attribute option.
+		$request = new WP_REST_Request( 'POST', '/wc/v3/products/' . $product->get_id() . '/variations' );
+		$request->set_body_params(
+			array(
+				'regular_price'  => '0',
+				'sale_price'     => '0',
+				'manage_stock'   => true,
+				'stock_quantity' => 0,
+				'attributes'     => array(
+					array(
+						'id'     => $color_attribute->get_id(),
+						'option' => 'سبز', // URL-encoded Persian word for "green".
+					),
+				),
+			)
+		);
+
+		$response  = $this->server->dispatch( $request );
+		$variation = $response->get_data();
+
+		// Verify the variation was created successfully.
+		$this->assertEquals( 201, $response->get_status() );
+		$this->assertNotEmpty( $variation['id'] );
+		$this->assertEquals( 'variation', $variation['type'] );
+		$this->assertEquals( $product->get_id(), $variation['parent_id'] );
+
+		// Verify the attribute is properly set with decoded option.
+		$this->assertNotEmpty( $variation['attributes'], 'Attributes array should not be empty' );
+		$this->assertCount( 1, $variation['attributes'], 'Variation should have 1 attribute' );
+		$this->assertEquals( 'رنگ', $variation['attributes'][0]['name'], 'Variation should contain color attribute' );
+		$this->assertEquals( 'سبز', $variation['attributes'][0]['option'], 'Attribute option should be decoded from URL encoding' );
+
+		// Verify the variation can be retrieved and has the attribute data.
+		$get_request         = new WP_REST_Request( 'GET', '/wc/v3/products/' . $product->get_id() . '/variations/' . $variation['id'] );
+		$get_response        = $this->server->dispatch( $get_request );
+		$retrieved_variation = $get_response->get_data();
+
+		$this->assertEquals( 200, $get_response->get_status() );
+		$this->assertNotEmpty( $retrieved_variation['attributes'], 'Retrieved variation should have attributes' );
+		$this->assertCount( 1, $retrieved_variation['attributes'], 'Retrieved variation should have 1 attribute' );
+		$this->assertEquals( 'رنگ', $retrieved_variation['attributes'][0]['name'], 'Variation should contain color attribute' );
+		$this->assertEquals( 'pa_رنگ', $retrieved_variation['attributes'][0]['slug'], 'Variation should contain color attribute slug' );
+		$this->assertEquals( 'سبز', $retrieved_variation['attributes'][0]['option'], 'Retrieved attribute option should be decoded' );
+	}
+
 	/**
 	 * Test that the products endpoint can filter by global_unique_id and also return matched variations.
 	 *