Commit 371d32ed785 for woocommerce
commit 371d32ed785b85b82c11bcf0980d6a185e720d84
Author: Luigi Teschio <gigitux@gmail.com>
Date: Wed Nov 11 12:59:56 2026 +0100
Preserve product context for invalid add-to-cart shortcodes (#70847)
* Preserve product context for invalid add-to-cart shortcodes
* Add changelog entry for shortcode product context fix
diff --git a/plugins/woocommerce/changelog/fix-add-to-cart-invalid-product-context b/plugins/woocommerce/changelog/fix-add-to-cart-invalid-product-context
new file mode 100644
index 00000000000..194a7836e98
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-add-to-cart-invalid-product-context
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Prevent invalid add-to-cart shortcodes from clearing the surrounding product context.
diff --git a/plugins/woocommerce/includes/class-wc-shortcodes.php b/plugins/woocommerce/includes/class-wc-shortcodes.php
index 7210f0dfb86..93e363285b9 100644
--- a/plugins/woocommerce/includes/class-wc-shortcodes.php
+++ b/plugins/woocommerce/includes/class-wc-shortcodes.php
@@ -342,7 +342,11 @@ class WC_Shortcodes {
$product = is_object( $product_data ) && in_array( $product_data->post_type, array( 'product', 'product_variation' ), true ) ? wc_setup_product_data( $product_data ) : false;
- if ( ! $product || ! $product->is_viewable() ) {
+ if ( ! $product instanceof WC_Product ) {
+ return '';
+ }
+
+ if ( ! $product->is_viewable() ) {
wc_setup_product_data( $post );
return '';
}
diff --git a/plugins/woocommerce/tests/php/includes/class-wc-shortcodes-test.php b/plugins/woocommerce/tests/php/includes/class-wc-shortcodes-test.php
index 4d12b390550..92a37619d22 100644
--- a/plugins/woocommerce/tests/php/includes/class-wc-shortcodes-test.php
+++ b/plugins/woocommerce/tests/php/includes/class-wc-shortcodes-test.php
@@ -477,6 +477,63 @@ class WC_Shortcodes_Test extends WC_Unit_Test_Case {
$this->assertMatchesRegularExpression( '/This content is password[- ]protected/', $product_page );
}
+ /**
+ * @testdox An invalid add-to-cart shortcode preserves the surrounding product on a non-product page.
+ * @testWith ["missing_id"]
+ * ["non_product_id"]
+ * ["missing_sku"]
+ * @param string $reference The invalid product reference to use.
+ */
+ public function test_product_add_to_cart_invalid_reference_preserves_product( string $reference ): void {
+ $previous_product = $GLOBALS['product'] ?? null;
+
+ try {
+ $post_id = self::factory()->post->create();
+ $this->go_to( get_permalink( $post_id ) );
+
+ $outer_product = WC_Helper_Product::create_simple_product();
+ $outer_product = wc_setup_product_data( $outer_product->get_id() );
+ $references = array(
+ 'missing_id' => array( 'id' => PHP_INT_MAX ),
+ 'non_product_id' => array( 'id' => $post_id ),
+ 'missing_sku' => array( 'sku' => 'nonexistent-shortcode-product' ),
+ );
+
+ $this->assertSame( '', WC_Shortcodes::product_add_to_cart( $references[ $reference ] ) );
+ $this->assertSame( $outer_product, $GLOBALS['product'] ?? null );
+ } finally {
+ $GLOBALS['product'] = $previous_product;
+ }
+ }
+
+ /**
+ * @testdox The add-to-cart shortcode only renders viewable products for visitors.
+ * @testWith [true]
+ * [false]
+ * @param bool $published Whether the product is published.
+ */
+ public function test_product_add_to_cart_respects_visibility( bool $published ): void {
+ $previous_product = $GLOBALS['product'] ?? null;
+
+ try {
+ $this->go_to( get_permalink( self::factory()->post->create() ) );
+ wp_set_current_user( 0 );
+ $shortcode_product = WC_Helper_Product::create_simple_product();
+ $shortcode_product->set_status( $published ? ProductStatus::PUBLISH : ProductStatus::DRAFT );
+ $shortcode_product->save();
+
+ $output = WC_Shortcodes::product_add_to_cart( array( 'id' => $shortcode_product->get_id() ) );
+
+ if ( $published ) {
+ $this->assertStringContainsString( 'add_to_cart_button', $output );
+ } else {
+ $this->assertSame( '', $output );
+ }
+ } finally {
+ $GLOBALS['product'] = $previous_product;
+ }
+ }
+
/**
* @testdox The shop_messages shortcode should render queued notices inside the shared notices wrapper.
*/