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.
*