Commit c67de74409c for woocommerce
commit c67de74409cf4c6a3523506062d0498236919330
Author: Anuj Singh <80690679+Anuj-Rathore24@users.noreply.github.com>
Date: Tue Mar 10 17:22:22 2026 +0530
fix: allow math expressions in flat rate shipping cost field. (#63453)
diff --git a/plugins/woocommerce/changelog/fix-flat-rate-shipping-math-expression-validation b/plugins/woocommerce/changelog/fix-flat-rate-shipping-math-expression-validation
new file mode 100644
index 00000000000..88b75cdb8b5
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-flat-rate-shipping-math-expression-validation
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Fix math expressions being rejected in the flat rate shipping cost field
diff --git a/plugins/woocommerce/includes/shipping/flat-rate/class-wc-shipping-flat-rate.php b/plugins/woocommerce/includes/shipping/flat-rate/class-wc-shipping-flat-rate.php
index 7b63514e98d..f3ad1192c03 100644
--- a/plugins/woocommerce/includes/shipping/flat-rate/class-wc-shipping-flat-rate.php
+++ b/plugins/woocommerce/includes/shipping/flat-rate/class-wc-shipping-flat-rate.php
@@ -273,6 +273,32 @@ class WC_Shipping_Flat_Rate extends WC_Shipping_Method {
return $found_shipping_classes;
}
+ /**
+ * Check if a value is a math expression.
+ *
+ * Matches two or more numeric operands separated by arithmetic operators.
+ *
+ * @param string $value Value to test.
+ * @return bool True if value is a well-formed math expression.
+ */
+ protected function is_math_expression( string $value ): bool {
+ $decimal_separator = wc_get_price_decimal_separator();
+
+ // Use preg_quote() to safely escaping separator.
+ $separator = preg_quote( $decimal_separator, '/' );
+
+ // Breakdown:
+ // [\d{sep}\s]+ - First operand: digits, separator, or spaces.
+ // ( - Begin repeating group:
+ // [+\-*\/] - An arithmetic operator (+, -, *, /).
+ // [\d{sep}\s]+ - Next operand: digits, separator, or spaces.
+ // )+ - One or more operator+operand pairs (ensures no trailing operator).
+ return (bool) preg_match(
+ '/^[\d' . $separator . '\s]+([+\-*\/][\d' . $separator . '\s]+)+$/',
+ trim( $value )
+ );
+ }
+
/**
* Sanitize the cost field.
*
@@ -287,7 +313,8 @@ class WC_Shipping_Flat_Rate extends WC_Shipping_Method {
$value = str_replace( array( get_woocommerce_currency_symbol(), html_entity_decode( get_woocommerce_currency_symbol() ) ), '', $value );
$contains_shortcodes = false !== strpos( $value, '[' ) || false !== strpos( $value, ']' );
- if ( ! $contains_shortcodes ) {
+
+ if ( ! $contains_shortcodes && ! $this->is_math_expression( $value ) ) {
$value = \Automattic\WooCommerce\Utilities\NumberUtil::sanitize_cost_in_current_locale( $value );
}
diff --git a/plugins/woocommerce/tests/php/includes/shipping/flat-rate/class-wc-shipping-flat-rate-test.php b/plugins/woocommerce/tests/php/includes/shipping/flat-rate/class-wc-shipping-flat-rate-test.php
index 5ff8c925575..7d8fcffe831 100644
--- a/plugins/woocommerce/tests/php/includes/shipping/flat-rate/class-wc-shipping-flat-rate-test.php
+++ b/plugins/woocommerce/tests/php/includes/shipping/flat-rate/class-wc-shipping-flat-rate-test.php
@@ -1,4 +1,5 @@
<?php
+declare( strict_types = 1 );
// phpcs:disable Squiz.Classes.ClassFileName.NoMatch, Squiz.Classes.ValidClassName.NotCamelCaps -- backcompat nomenclature.
@@ -17,6 +18,11 @@ class WC_Shipping_Flat_Rate_Test extends WC_Unit_Test_Case {
*/
private $call_evaluate_cost;
+ /**
+ * @var Closure Function to call public method sanitize_cost.
+ */
+ private $call_sanitize_cost;
+
/**
* Set up test case.
*
@@ -28,6 +34,9 @@ class WC_Shipping_Flat_Rate_Test extends WC_Unit_Test_Case {
$this->call_evaluate_cost = function ( $sum, $args ) {
return $this->evaluate_cost( $sum, $args );
};
+ $this->call_sanitize_cost = function ( $value ) {
+ return $this->sanitize_cost( $value );
+ };
update_option( 'woocommerce_price_decimal_sep', ',' );
update_option( 'woocommerce_price_thousand_sep', '.' );
}
@@ -151,4 +160,92 @@ class WC_Shipping_Flat_Rate_Test extends WC_Unit_Test_Case {
);
$this->assertEquals( 10, $val );
}
+
+ /**
+ * @testDox sanitize_cost() accepts and preserves valid math expressions.
+ *
+ * @dataProvider provider_valid_math_expressions
+ *
+ * @param string $value Value to test.
+ * @param string $decimal_sep Decimal separator to use.
+ * @param string $thousand_sep Thousand separator to use.
+ */
+ public function test_sanitize_cost_accepts_math_expressions( string $value, string $decimal_sep, string $thousand_sep ): void {
+ update_option( 'woocommerce_price_decimal_sep', $decimal_sep );
+ update_option( 'woocommerce_price_thousand_sep', $thousand_sep );
+
+ $result = $this->call_sanitize_cost->call( $this->sut, $value );
+ $this->assertEquals( $value, trim( $result ) );
+ }
+
+ /**
+ * @testDox sanitize_cost() rejects invalid math expressions.
+ *
+ * @dataProvider provider_invalid_math_expressions
+ *
+ * @param string $value Value to sanitize.
+ * @param string $decimal_sep Decimal separator to use.
+ * @param string $thousand_sep Thousand separator to use.
+ */
+ public function test_sanitize_cost_rejects_invalid_expressions( string $value, string $decimal_sep, string $thousand_sep ): void {
+ update_option( 'woocommerce_price_decimal_sep', $decimal_sep );
+ update_option( 'woocommerce_price_thousand_sep', $thousand_sep );
+
+ $this->expectException( Exception::class );
+ $this->call_sanitize_cost->call( $this->sut, $value );
+ }
+
+ /**
+ * Valid math expression cases.
+ *
+ * Format: [ value, decimal_separator, thousand_separator ]
+ */
+ public function provider_valid_math_expressions(): array {
+ return array(
+ 'plain number' => array( '10.00', '.', ',' ),
+ 'empty string' => array( '', '.', ',' ),
+ 'shortcode qty' => array( '[qty]', '.', ',' ),
+ 'shortcode expression' => array( '10.00 * [qty]', '.', ',' ),
+
+ // period decimal, comma thousand.
+ 'simple division' => array( '3.50 / 1.21', '.', ',' ),
+ 'simple multiplication' => array( '10.00 * 1.21', '.', ',' ),
+ 'simple addition' => array( '10 + 5', '.', ',' ),
+ 'simple subtraction' => array( '20 - 3.50', '.', ',' ),
+ 'chained operators' => array( '10 * 2 + 5', '.', ',' ),
+
+ // comma decimal, period thousand.
+ 'EU locale division' => array( '3,50 / 1,21', ',', '.' ),
+ 'EU locale multiplication' => array( '10,00 * 1,21', ',', '.' ),
+
+ // No thousand separator locale.
+ 'no thousand separator simple' => array( '3.50 / 1.21', '.', '' ),
+ 'no thousand separator chained' => array( '10 * 2 + 5', '.', '' ),
+ );
+ }
+
+ /**
+ * Invalid math expression cases.
+ *
+ * Format: [ value, decimal_separator, thousand_separator ]
+ */
+ public function provider_invalid_math_expressions(): array {
+ return array(
+ // Thousand-separated operands must not be used in math expressions
+ // as evaluate_cost() normalises all separators to ".", causing
+ // "10,000" to be evaluated as "10.0" instead of "10000".
+ 'thousand separated operand' => array( '10,500 * 3000', '.', ',' ),
+ 'EU thousand separated operand' => array( '10.500 * 3000', ',', '.' ),
+
+ // Trailing operator — incomplete expressions.
+ 'trailing plus' => array( '20 +', '.', ',' ),
+ 'trailing minus' => array( '20 -', '.', ',' ),
+ 'trailing multiply' => array( '20 *', '.', ',' ),
+ 'trailing divide' => array( '3.50 /', '.', ',' ),
+
+ // Invalid characters.
+ 'alphabetic string' => array( 'abc', '.', ',' ),
+ 'alphanumeric' => array( '10abc', '.', ',' ),
+ );
+ }
}