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