Commit b0b212c03d2 for woocommerce
commit b0b212c03d2647e58f94683dcb9e5fe2b42134a4
Author: Jorge A. Torres <jorge.torres@automattic.com>
Date: Thu Sep 17 21:57:01 2026 +0100
Keep the price separator sanitize filters returning a string (#68798)
diff --git a/plugins/woocommerce/changelog/fix-price-separator-sanitize-null b/plugins/woocommerce/changelog/fix-price-separator-sanitize-null
new file mode 100644
index 00000000000..6fe7ede85be
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-price-separator-sanitize-null
@@ -0,0 +1,4 @@
+Significance: patch
+Type: dev
+
+Always return a string from the price separator sanitize filters.
diff --git a/plugins/woocommerce/includes/wc-formatting-functions.php b/plugins/woocommerce/includes/wc-formatting-functions.php
index d3482879539..a09c43640c5 100644
--- a/plugins/woocommerce/includes/wc-formatting-functions.php
+++ b/plugins/woocommerce/includes/wc-formatting-functions.php
@@ -1206,12 +1206,12 @@ function wc_format_product_short_description( $content ) {
* Validates and sanitizes currency separators when saved in settings.
*
* @param mixed $value Option value passed through earlier filters.
- * @param array $option Option data including 'id' and 'default'.
+ * @param array $option Option data including 'id'.
* @param mixed $raw_value Raw request value, null when the field was not submitted.
- * @return mixed
+ * @return string
*/
function wc_format_option_price_separators( $value, $option, $raw_value ) {
- return wc_get_container()->get( OptionSanitizer::class )->sanitize_price_separator_setting( $value, $raw_value );
+ return wc_get_container()->get( OptionSanitizer::class )->sanitize_price_separator_setting( $option['id'], $raw_value );
}
add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_price_decimal_sep', 'wc_format_option_price_separators', 10, 3 );
add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_price_thousand_sep', 'wc_format_option_price_separators', 10, 3 );
diff --git a/plugins/woocommerce/src/Internal/Settings/OptionSanitizer.php b/plugins/woocommerce/src/Internal/Settings/OptionSanitizer.php
index 5cbad4fd221..3583f79927d 100644
--- a/plugins/woocommerce/src/Internal/Settings/OptionSanitizer.php
+++ b/plugins/woocommerce/src/Internal/Settings/OptionSanitizer.php
@@ -68,20 +68,16 @@ class OptionSanitizer {
/**
* Rejects thousand and decimal separators that contain a number.
- * On rejection it adds a settings error and returns null, so the stored value is left untouched.
+ * On rejection it adds a settings error and returns the stored separator, or an empty string when nothing is stored.
*
* @since 11.2.0
- * @param mixed $value Option value.
- * @param mixed $raw_value Raw request value, null when the field was not submitted.
- * @return mixed
+ * @param string $option_id Name of the option being saved.
+ * @param mixed $raw_value Raw request value, null when the field was not submitted.
+ * @return string
*
* @internal For exclusive usage of WooCommerce core, backwards compatibility not guaranteed.
*/
- public function sanitize_price_separator_setting( $value, $raw_value ) {
- if ( null === $raw_value ) {
- return $value;
- }
-
+ public function sanitize_price_separator_setting( $option_id, $raw_value ) {
if ( is_string( $raw_value ) ) {
$separator = wp_kses( $raw_value, array() );
$decoded = html_entity_decode( $separator, ENT_QUOTES | ENT_HTML5, 'UTF-8' );
@@ -91,8 +87,12 @@ class OptionSanitizer {
}
}
- \WC_Admin_Settings::add_error( __( 'Thousand and decimal separators cannot contain numbers.', 'woocommerce' ) );
+ if ( null !== $raw_value ) {
+ \WC_Admin_Settings::add_error( __( 'Thousand and decimal separators cannot contain numbers.', 'woocommerce' ) );
+ }
+
+ $stored = get_option( $option_id, '' );
- return null;
+ return is_string( $stored ) ? $stored : '';
}
}
diff --git a/plugins/woocommerce/tests/php/includes/wc-formatting-functions-test.php b/plugins/woocommerce/tests/php/includes/wc-formatting-functions-test.php
index fbba65571b6..22abf9b9479 100644
--- a/plugins/woocommerce/tests/php/includes/wc-formatting-functions-test.php
+++ b/plugins/woocommerce/tests/php/includes/wc-formatting-functions-test.php
@@ -12,6 +12,14 @@ declare( strict_types = 1 );
*/
class WC_Formatting_Functions_Test extends \WC_Unit_Test_Case {
+ /**
+ * Starts each test with an empty WC_Admin_Settings errors array.
+ */
+ public function setUp(): void {
+ parent::setUp();
+ $this->wc_admin_settings_errors_property()->setValue( null, array() );
+ }
+
/**
* Resets the static WC_Admin_Settings errors so they do not leak into other tests.
*/
@@ -108,85 +116,82 @@ class WC_Formatting_Functions_Test extends \WC_Unit_Test_Case {
*/
public function data_provider_wc_format_option_price_separators(): array {
return array(
- 'thousand sep: comma' => array( 'woocommerce_price_thousand_sep', ',', ',' ),
- 'thousand sep: period' => array( 'woocommerce_price_thousand_sep', '.', '.' ),
- 'thousand sep: space' => array( 'woocommerce_price_thousand_sep', ' ', ' ' ),
- 'thousand sep: two spaces' => array( 'woocommerce_price_thousand_sep', ' ', ' ' ),
- 'thousand sep: empty' => array( 'woocommerce_price_thousand_sep', '', '' ),
- 'thousand sep: nbsp entity' => array( 'woocommerce_price_thousand_sep', ' ', ' ' ),
- 'thousand sep: comma entity' => array( 'woocommerce_price_thousand_sep', ',', ',' ),
- 'thousand sep: zero digit' => array( 'woocommerce_price_thousand_sep', '0', null ),
- 'thousand sep: single digit' => array( 'woocommerce_price_thousand_sep', '1', null ),
- 'thousand sep: digit+symbol' => array( 'woocommerce_price_thousand_sep', '1,', null ),
- 'thousand sep: digit entity' => array( 'woocommerce_price_thousand_sep', '1', null ),
- 'thousand sep: fullwidth digit' => array( 'woocommerce_price_thousand_sep', '1', null ),
- 'decimal sep: period' => array( 'woocommerce_price_decimal_sep', '.', '.' ),
- 'decimal sep: single digit' => array( 'woocommerce_price_decimal_sep', '2', null ),
- 'decimal sep: arabic digit' => array( 'woocommerce_price_decimal_sep', '٢', null ),
+ 'thousand sep: comma' => array( 'woocommerce_price_thousand_sep', ',', ',', false ),
+ 'thousand sep: period' => array( 'woocommerce_price_thousand_sep', '.', '.', false ),
+ 'thousand sep: space' => array( 'woocommerce_price_thousand_sep', ' ', ' ', false ),
+ 'thousand sep: two spaces' => array( 'woocommerce_price_thousand_sep', ' ', ' ', false ),
+ 'thousand sep: empty' => array( 'woocommerce_price_thousand_sep', '', '', false ),
+ 'thousand sep: nbsp entity' => array( 'woocommerce_price_thousand_sep', ' ', ' ', false ),
+ 'thousand sep: comma entity' => array( 'woocommerce_price_thousand_sep', ',', ',', false ),
+ 'thousand sep: not submitted' => array( 'woocommerce_price_thousand_sep', null, '|', false ),
+ 'thousand sep: single digit' => array( 'woocommerce_price_thousand_sep', '1', '|', true ),
+ 'thousand sep: digit+symbol' => array( 'woocommerce_price_thousand_sep', '1,', '|', true ),
+ 'thousand sep: digit entity' => array( 'woocommerce_price_thousand_sep', '1', '|', true ),
+ 'thousand sep: fullwidth digit' => array( 'woocommerce_price_thousand_sep', '1', '|', true ),
+ 'thousand sep: array' => array( 'woocommerce_price_thousand_sep', array( '1' ), '|', true ),
+ 'decimal sep: period' => array( 'woocommerce_price_decimal_sep', '.', '.', false ),
+ 'decimal sep: single digit' => array( 'woocommerce_price_decimal_sep', '2', '|', true ),
+ 'decimal sep: arabic digit' => array( 'woocommerce_price_decimal_sep', '٢', '|', true ),
);
}
/**
- * @testdox wc_format_option_price_separators should reject values containing digits by adding an error and returning null.
+ * @testdox wc_format_option_price_separators should keep valid separators and fall back to the stored one when the input is rejected.
*
* @dataProvider data_provider_wc_format_option_price_separators
*
- * @param string $option_id The option being saved.
- * @param string $raw_value The raw input being saved.
- * @param string|null $expected The value the filter should return, null when the input is rejected.
+ * @param string $option_id The option being saved.
+ * @param mixed $raw_value The raw input being saved, null when the field was not submitted.
+ * @param mixed $expected The value the filter should return.
+ * @param bool $expects_error Whether the input should be rejected with a settings error.
*/
- public function test_wc_format_option_price_separators( string $option_id, string $raw_value, ?string $expected ): void {
- $option = array(
- 'id' => $option_id,
- 'default' => ',',
- );
+ public function test_wc_format_option_price_separators( string $option_id, $raw_value, $expected, bool $expects_error ): void {
+ update_option( $option_id, '|' );
- $errors_before = $this->get_wc_admin_settings_errors();
- $result = wc_format_option_price_separators( $raw_value, $option, $raw_value );
- $errors_after = $this->get_wc_admin_settings_errors();
+ $result = wc_format_option_price_separators( $raw_value, array( 'id' => $option_id ), $raw_value );
$this->assertSame( $expected, $result );
- if ( null === $expected ) {
- $this->assertCount( count( $errors_before ) + 1, $errors_after, 'An error should be added when a numeric separator is rejected.' );
- $this->assertStringContainsString( 'cannot contain numbers', end( $errors_after ), 'Error message should mention numbers.' );
- } else {
- $this->assertCount( count( $errors_before ), $errors_after, 'No error should be added for valid separators.' );
+ $errors = $this->get_wc_admin_settings_errors();
+
+ $this->assertCount( $expects_error ? 1 : 0, $errors, 'An error should be added only when the separator is rejected.' );
+
+ if ( $expects_error ) {
+ $this->assertStringContainsString( 'cannot contain numbers', end( $errors ), 'Error message should mention numbers.' );
}
}
/**
- * @testdox wc_format_option_price_separators should reject a non-string raw value.
+ * @testdox wc_format_option_price_separators should return an empty string when nothing is stored.
*/
- public function test_wc_format_option_price_separators_rejects_non_string_input(): void {
- $option = array(
- 'id' => 'woocommerce_price_thousand_sep',
- 'default' => ',',
- );
+ public function test_wc_format_option_price_separators_returns_empty_string_when_nothing_stored(): void {
+ delete_option( 'woocommerce_price_thousand_sep' );
- $errors_before = $this->get_wc_admin_settings_errors();
- $result = wc_format_option_price_separators( ',', $option, array( '1' ) );
- $errors_after = $this->get_wc_admin_settings_errors();
+ $result = wc_format_option_price_separators( '1', array( 'id' => 'woocommerce_price_thousand_sep' ), '1' );
- $this->assertNull( $result, 'An array raw value should be rejected.' );
- $this->assertCount( count( $errors_before ) + 1, $errors_after, 'An error should be added when the raw value is not a string.' );
+ $this->assertSame( '', $result, 'A rejected separator should return an empty string when the option is not stored.' );
}
/**
- * @testdox wc_format_option_price_separators should leave the value alone when the field was not submitted.
+ * @testdox wc_format_option_price_separators should keep a stored empty separator on rejection.
*/
- public function test_wc_format_option_price_separators_skips_missing_field(): void {
- $option = array(
- 'id' => 'woocommerce_price_thousand_sep',
- 'default' => ',',
- );
+ public function test_wc_format_option_price_separators_keeps_stored_empty_separator(): void {
+ update_option( 'woocommerce_price_thousand_sep', '' );
+
+ $result = wc_format_option_price_separators( '1', array( 'id' => 'woocommerce_price_thousand_sep' ), '1' );
+
+ $this->assertSame( '', $result, 'A deliberately empty separator should survive a rejected save.' );
+ }
+
+ /**
+ * @testdox wc_format_option_price_separators should discard a stored value that is not a string.
+ */
+ public function test_wc_format_option_price_separators_discards_non_string_stored_value(): void {
+ update_option( 'woocommerce_price_thousand_sep', array( '1' ) );
- $errors_before = $this->get_wc_admin_settings_errors();
- $result = wc_format_option_price_separators( null, $option, null );
- $errors_after = $this->get_wc_admin_settings_errors();
+ $result = wc_format_option_price_separators( '1', array( 'id' => 'woocommerce_price_thousand_sep' ), '1' );
- $this->assertNull( $result, 'A null raw value means the field was not submitted, so nothing should be saved.' );
- $this->assertCount( count( $errors_before ), $errors_after, 'No error should be added for a field that was not submitted.' );
+ $this->assertSame( '', $result, 'A stored value that is not a string should not be handed back.' );
}
/**