Commit e84d5b7170b for woocommerce
commit e84d5b7170ba83c34de4091b0457ddb40552dd3b
Author: Mario Santos <34552881+SantosGuillamot@users.noreply.github.com>
Date: Tue Mar 31 07:55:51 2026 +0200
Fix: Product title with "$" breaks Add to cart with options (#63653)
* Use `preg_replace_callback` in quantity steppers
* Add changefile(s) from automation for the following project(s): woocommerce
* Update `phpstan`
* Add unit test
* Reverse callbacks order
* Fix phpstan errors
* Change also return
* Apply changes to AddToCartForm
---------
Co-authored-by: woocommercebot <woocommercebot@users.noreply.github.com>
diff --git a/plugins/woocommerce/changelog/63653-fix-page-title-with-$-breaks-add-to-cart-with-options b/plugins/woocommerce/changelog/63653-fix-page-title-with-$-breaks-add-to-cart-with-options
new file mode 100644
index 00000000000..7a0b33c601f
--- /dev/null
+++ b/plugins/woocommerce/changelog/63653-fix-page-title-with-$-breaks-add-to-cart-with-options
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Fix product title with $ breaking Add to cart with options
\ No newline at end of file
diff --git a/plugins/woocommerce/phpstan-baseline.neon b/plugins/woocommerce/phpstan-baseline.neon
index 3566c30f31f..0b4aafe57bd 100644
--- a/plugins/woocommerce/phpstan-baseline.neon
+++ b/plugins/woocommerce/phpstan-baseline.neon
@@ -51375,12 +51375,6 @@ parameters:
count: 1
path: src/Blocks/BlockTypes/AddToCartForm.php
- -
- message: '#^Method Automattic\\WooCommerce\\Blocks\\BlockTypes\\AddToCartForm\:\:add_steppers\(\) should return string but returns string\|null\.$#'
- identifier: return.type
- count: 1
- path: src/Blocks/BlockTypes/AddToCartForm.php
-
-
message: '#^Method Automattic\\WooCommerce\\Blocks\\BlockTypes\\AddToCartForm\:\:enqueue_assets\(\) has no return type specified\.$#'
identifier: missingType.return
@@ -51405,12 +51399,6 @@ parameters:
count: 1
path: src/Blocks/BlockTypes/AddToCartForm.php
- -
- message: '#^Parameter \#3 \$subject of function preg_replace expects array\<float\|int\|string\>\|string, string\|null given\.$#'
- identifier: argument.type
- count: 1
- path: src/Blocks/BlockTypes/AddToCartForm.php
-
-
message: '#^Parameter \$block of method Automattic\\WooCommerce\\Blocks\\BlockTypes\\AddToCartForm\:\:enqueue_assets\(\) has invalid type Automattic\\WooCommerce\\Blocks\\BlockTypes\\WP_Block\.$#'
identifier: class.notFound
@@ -51591,12 +51579,6 @@ parameters:
count: 1
path: src/Blocks/BlockTypes/AddToCartWithOptions/Utils.php
- -
- message: '#^Method Automattic\\WooCommerce\\Blocks\\BlockTypes\\AddToCartWithOptions\\Utils\:\:add_quantity_steppers\(\) should return string but returns string\|null\.$#'
- identifier: return.type
- count: 1
- path: src/Blocks/BlockTypes/AddToCartWithOptions/Utils.php
-
-
message: '#^Parameter \#1 \$haystack of function strpos expects string, string\|true\|null given\.$#'
identifier: argument.type
@@ -51609,12 +51591,6 @@ parameters:
count: 1
path: src/Blocks/BlockTypes/AddToCartWithOptions/Utils.php
- -
- message: '#^Parameter \#3 \$subject of function preg_replace expects array\<float\|int\|string\>\|string, string\|null given\.$#'
- identifier: argument.type
- count: 1
- path: src/Blocks/BlockTypes/AddToCartWithOptions/Utils.php
-
-
message: '#^Parameter \$block of method Automattic\\WooCommerce\\Blocks\\BlockTypes\\AddToCartWithOptions\\VariationDescription\:\:render\(\) has invalid type Automattic\\WooCommerce\\Blocks\\BlockTypes\\AddToCartWithOptions\\WP_Block\.$#'
identifier: class.notFound
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartForm.php b/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartForm.php
index ff5f78321ef..1895b6ce896 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartForm.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartForm.php
@@ -77,14 +77,26 @@ class AddToCartForm extends AbstractBlock {
// Regex pattern to match the <input> element with id starting with 'quantity_'.
$pattern = '/(<input[^>]*id="quantity_[^"]*"[^>]*\/>)/';
// Replacement string to add button AFTER the matched <input> element.
- /* translators: %s refers to the item name in the cart. */
- $minus_button = '$1<button aria-label="' . esc_attr( sprintf( __( 'Reduce quantity of %s', 'woocommerce' ), $product_name ) ) . '" type="button" data-wp-on--click="actions.removeQuantity" class="wc-block-components-quantity-selector__button wc-block-components-quantity-selector__button--minus">−</button>';
- // Replacement string to add button AFTER the matched <input> element.
- /* translators: %s refers to the item name in the cart. */
- $plus_button = '$1<button aria-label="' . esc_attr( sprintf( __( 'Increase quantity of %s', 'woocommerce' ), $product_name ) ) . '" type="button" data-wp-on--click="actions.addQuantity" class="wc-block-components-quantity-selector__button wc-block-components-quantity-selector__button--plus">+</button>';
- $new_html = preg_replace( $pattern, $plus_button, $product_html );
- $new_html = preg_replace( $pattern, $minus_button, $new_html );
- return $new_html;
+ // Use preg_replace_callback to avoid backreference interpretation of $, \ sequences in product names.
+ $new_html = preg_replace_callback(
+ $pattern,
+ function ( $matches ) use ( $product_name ) {
+ /* translators: %s refers to the item name in the cart. */
+ $plus_aria = esc_attr( sprintf( __( 'Increase quantity of %s', 'woocommerce' ), $product_name ) );
+ return $matches[1] . '<button aria-label="' . $plus_aria . '" type="button" data-wp-on--click="actions.addQuantity" class="wc-block-components-quantity-selector__button wc-block-components-quantity-selector__button--plus">+</button>';
+ },
+ $product_html ?? ''
+ );
+ $new_html = preg_replace_callback(
+ $pattern,
+ function ( $matches ) use ( $product_name ) {
+ /* translators: %s refers to the item name in the cart. */
+ $minus_aria = esc_attr( sprintf( __( 'Reduce quantity of %s', 'woocommerce' ), $product_name ) );
+ return $matches[1] . '<button aria-label="' . $minus_aria . '" type="button" data-wp-on--click="actions.removeQuantity" class="wc-block-components-quantity-selector__button wc-block-components-quantity-selector__button--minus">−</button>';
+ },
+ $new_html ?? ''
+ );
+ return $new_html ?? '';
}
/**
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptions/Utils.php b/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptions/Utils.php
index 2a8f8d90346..117a06a16bf 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptions/Utils.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptions/Utils.php
@@ -42,14 +42,26 @@ class Utils {
// Regex pattern to match the <input> element with id starting with 'quantity_'.
$pattern = '/(<input[^>]*id="quantity_[^"]*"[^>]*\/>)/';
// Replacement string to add button AFTER the matched <input> element.
- /* translators: %s refers to the item name in the cart. */
- $minus_button = '$1<button aria-label="' . esc_attr( sprintf( __( 'Reduce quantity of %s', 'woocommerce' ), $product_name ) ) . '" type="button" data-wp-on--click="actions.decreaseQuantity" data-wp-bind--disabled="!state.allowsDecrease" class="wc-block-components-quantity-selector__button wc-block-components-quantity-selector__button--minus">−</button>';
- // Replacement string to add button AFTER the matched <input> element.
- /* translators: %s refers to the item name in the cart. */
- $plus_button = '$1<button aria-label="' . esc_attr( sprintf( __( 'Increase quantity of %s', 'woocommerce' ), $product_name ) ) . '" type="button" data-wp-on--click="actions.increaseQuantity" data-wp-bind--disabled="!state.allowsIncrease" class="wc-block-components-quantity-selector__button wc-block-components-quantity-selector__button--plus">+</button>';
- $new_html = preg_replace( $pattern, $plus_button, $quantity_html );
- $new_html = preg_replace( $pattern, $minus_button, $new_html );
- return $new_html;
+ // Use preg_replace_callback to avoid backreference interpretation of $, \ sequences in product names.
+ $new_html = preg_replace_callback(
+ $pattern,
+ function ( $matches ) use ( $product_name ) {
+ /* translators: %s refers to the item name in the cart. */
+ $plus_aria = esc_attr( sprintf( __( 'Increase quantity of %s', 'woocommerce' ), $product_name ) );
+ return $matches[1] . '<button aria-label="' . $plus_aria . '" type="button" data-wp-on--click="actions.increaseQuantity" data-wp-bind--disabled="!state.allowsIncrease" class="wc-block-components-quantity-selector__button wc-block-components-quantity-selector__button--plus">+</button>';
+ },
+ $quantity_html ?? ''
+ );
+ $new_html = preg_replace_callback(
+ $pattern,
+ function ( $matches ) use ( $product_name ) {
+ /* translators: %s refers to the item name in the cart. */
+ $minus_aria = esc_attr( sprintf( __( 'Reduce quantity of %s', 'woocommerce' ), $product_name ) );
+ return $matches[1] . '<button aria-label="' . $minus_aria . '" type="button" data-wp-on--click="actions.decreaseQuantity" data-wp-bind--disabled="!state.allowsDecrease" class="wc-block-components-quantity-selector__button wc-block-components-quantity-selector__button--minus">−</button>';
+ },
+ $new_html ?? ''
+ );
+ return $new_html ?? '';
}
/**
diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/AddToCartWithOptions.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/AddToCartWithOptions.php
index dd82bb347f9..b2040fafe30 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/AddToCartWithOptions.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/AddToCartWithOptions.php
@@ -444,6 +444,32 @@ class AddToCartWithOptions extends \WP_UnitTestCase {
);
}
+ /**
+ * Tests that the stepper buttons render with correct aria labels when the product name contains a dollar sign.
+ */
+ public function test_stepper_renders_correctly_with_dollar_sign_in_product_name() {
+ $simple_product = new \WC_Product_Simple();
+ $simple_product->set_regular_price( 10 );
+ $simple_product->set_name( 'CANADA, $1' );
+ $simple_product->set_manage_stock( true );
+ $simple_product->set_stock_quantity( 10 );
+ $simple_product_id = $simple_product->save();
+
+ $markup = do_blocks( '<!-- wp:woocommerce/single-product {"productId":' . $simple_product_id . '} --><!-- wp:woocommerce/add-to-cart-with-options /--><!-- /wp:woocommerce/single-product -->' );
+
+ $this->assertStringContainsString( 'wc-block-components-quantity-selector__button--minus', $markup, 'The minus stepper button is rendered.' );
+ $this->assertStringContainsString( 'wc-block-components-quantity-selector__button--plus', $markup, 'The plus stepper button is rendered.' );
+ $this->assertStringContainsString( 'Reduce quantity of CANADA, $1', $markup, 'The minus button aria-label contains the full product name with dollar sign.' );
+ $this->assertStringContainsString( 'Increase quantity of CANADA, $1', $markup, 'The plus button aria-label contains the full product name with dollar sign.' );
+
+ // Verify $1 was not interpreted as a backreference (which would inject the captured <input> HTML into the aria-label).
+ $this->assertDoesNotMatchRegularExpression(
+ '/aria-label="[^"]*<input[^"]*"/',
+ $markup,
+ 'The aria-label should not contain HTML from backreference expansion.'
+ );
+ }
+
/**
* Tests that the quantity selector and its steppers are hidden when
* a filter sets min and max quantity to the same value for a product.