Commit 8398823ca4c for woocommerce

commit 8398823ca4cf58d44ff169c6f895846118612f1c
Author: Karol Manijak <20098064+kmanijak@users.noreply.github.com>
Date:   Mon Sep 7 15:39:54 2026 +0200

    Respect product passwords when rendering descriptions (#68371)

    * Fix product descriptions rendering before password entry

    * Add changelog entry for protected product descriptions

diff --git a/plugins/woocommerce/changelog/fix-protected-product-descriptions b/plugins/woocommerce/changelog/fix-protected-product-descriptions
new file mode 100644
index 00000000000..25987ddcb70
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-protected-product-descriptions
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Hide product and variation descriptions until the product password is entered.
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/FeaturedProduct.php b/plugins/woocommerce/src/Blocks/BlockTypes/FeaturedProduct.php
index ec91d0adc20..07c1977ff66 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/FeaturedProduct.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/FeaturedProduct.php
@@ -2,6 +2,7 @@
 declare( strict_types = 1 );
 namespace Automattic\WooCommerce\Blocks\BlockTypes;

+use Automattic\WooCommerce\Blocks\Utils\ProductDescriptionUtils;
 use Automattic\WooCommerce\Enums\ProductType;

 /**
@@ -111,9 +112,14 @@ class FeaturedProduct extends FeaturedItem {
 				! isset( $attributes['showDesc'] ) ||
 				( isset( $attributes['showDesc'] ) && false !== $attributes['showDesc'] )
 			) {
-				$desc_str = sprintf(
-					'<div class="wc-block-featured-product__description">%s</div>',
-					wc_format_content( wp_kses_post( $product->get_short_description() ? $product->get_short_description() : wc_trim_string( $product->get_description(), 400 ) ) )
+				$desc_str = ProductDescriptionUtils::guarded_format(
+					$product,
+					function () use ( $product ) {
+						return sprintf(
+							'<div class="wc-block-featured-product__description">%s</div>',
+							wc_format_content( wp_kses_post( $product->get_short_description() ? $product->get_short_description() : wc_trim_string( $product->get_description(), 400 ) ) )
+						);
+					}
 				);
 				$output  .= $desc_str;
 			}
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductSummary.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductSummary.php
index 9656ec0d0bf..5773975c002 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductSummary.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductSummary.php
@@ -196,7 +196,7 @@ class ProductSummary extends AbstractBlock {
 		$post_id = $block->context['postId'] ?? '';
 		$product = wc_get_product( $post_id );

-		if ( ! $product || post_password_required( $product->get_id() ) ) {
+		if ( ! $product ) {
 			return '';
 		}

diff --git a/plugins/woocommerce/src/Blocks/Utils/ProductDescriptionUtils.php b/plugins/woocommerce/src/Blocks/Utils/ProductDescriptionUtils.php
index 88d5fc96aa3..10d5dd1a261 100644
--- a/plugins/woocommerce/src/Blocks/Utils/ProductDescriptionUtils.php
+++ b/plugins/woocommerce/src/Blocks/Utils/ProductDescriptionUtils.php
@@ -16,7 +16,23 @@ class ProductDescriptionUtils {
 	private static array $formatting_product_descriptions = array();

 	/**
-	 * Format a product description while preventing same-product recursive formatting.
+	 * Whether the current visitor can access a product's description.
+	 *
+	 * @param \WC_Product $product Product object.
+	 * @return bool
+	 */
+	private static function is_description_accessible( \WC_Product $product ): bool {
+		if ( post_password_required( $product->get_id() ) ) {
+			return false;
+		}
+
+		$parent_id = $product->get_parent_id();
+
+		return ! $parent_id || ! post_password_required( $parent_id );
+	}
+
+	/**
+	 * Format an accessible product description while preventing same-product recursion.
 	 *
 	 * @param \WC_Product $product         Product object.
 	 * @param callable    $format_callback Callback that formats the product description.
@@ -25,7 +41,7 @@ class ProductDescriptionUtils {
 	public static function guarded_format( \WC_Product $product, callable $format_callback ): string {
 		$product_id = $product->get_id();

-		if ( isset( self::$formatting_product_descriptions[ $product_id ] ) ) {
+		if ( ! self::is_description_accessible( $product ) || isset( self::$formatting_product_descriptions[ $product_id ] ) ) {
 			return '';
 		}

diff --git a/plugins/woocommerce/src/StoreApi/Schemas/V1/ProductSchema.php b/plugins/woocommerce/src/StoreApi/Schemas/V1/ProductSchema.php
index a2ece51e115..9341beb890d 100644
--- a/plugins/woocommerce/src/StoreApi/Schemas/V1/ProductSchema.php
+++ b/plugins/woocommerce/src/StoreApi/Schemas/V1/ProductSchema.php
@@ -595,8 +595,7 @@ class ProductSchema extends AbstractSchema {
 	 */
 	public function get_item_response( $product ) {
 		$availability      = ProductAvailabilityUtils::get_product_availability( $product );
-		$password_required = post_password_required( $product->get_id() );
-		$short_description = $password_required ? '' : $this->prepare_html_response(
+		$short_description = $this->prepare_html_response(
 			ProductDescriptionUtils::guarded_format(
 				$product,
 				function () use ( $product ) {
@@ -604,7 +603,7 @@ class ProductSchema extends AbstractSchema {
 				}
 			)
 		);
-		$description       = $password_required ? '' : $this->prepare_html_response(
+		$description       = $this->prepare_html_response(
 			ProductDescriptionUtils::guarded_format(
 				$product,
 				function () use ( $product ) {
diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/FeaturedProductTest.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/FeaturedProductTest.php
index 533a019113f..30b15aa0f1b 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/FeaturedProductTest.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/FeaturedProductTest.php
@@ -80,6 +80,30 @@ class FeaturedProductTest extends WC_Unit_Test_Case {
 		);
 	}

+	/**
+	 * @testdox Should not render a password-protected description in the legacy Featured Product block.
+	 */
+	public function test_does_not_render_password_protected_legacy_description(): void {
+		$this->product = WC_Helper_Product::create_simple_product();
+		$this->product->set_description( 'Protected featured product description' );
+		$this->product->set_post_password( 'secret' );
+		$this->product->save();
+
+		$output = $this->render_featured_product(
+			array(
+				'productId' => $this->product->get_id(),
+				'editMode'  => false,
+				'showDesc'  => true,
+			)
+		);
+
+		$this->assertStringNotContainsString(
+			'Protected featured product description',
+			$output,
+			'Legacy Featured Product blocks should not render protected descriptions before password entry.'
+		);
+	}
+
 	/**
 	 * Render a Featured Product block with the given attributes.
 	 *
diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductDescriptionTest.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductDescriptionTest.php
new file mode 100644
index 00000000000..a009d1cefe9
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductDescriptionTest.php
@@ -0,0 +1,48 @@
+<?php
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Tests\Blocks\BlockTypes;
+
+use WC_Helper_Product;
+use WC_Unit_Test_Case;
+
+/**
+ * Tests for the Product Description block type.
+ */
+class ProductDescriptionTest extends WC_Unit_Test_Case {
+
+	/**
+	 * @testdox Should not render a password-protected product description.
+	 */
+	public function test_does_not_render_password_protected_product_description(): void {
+		$product = WC_Helper_Product::create_simple_product();
+		$product->set_description( 'Protected product description' );
+		$product->save();
+
+		$block = new \WP_Block(
+			array(
+				'blockName'    => 'woocommerce/product-description',
+				'attrs'        => array(),
+				'innerBlocks'  => array(),
+				'innerHTML'    => '',
+				'innerContent' => array(),
+			),
+			array( 'postId' => $product->get_id() )
+		);
+
+		$this->assertStringContainsString(
+			'Protected product description',
+			$block->render(),
+			'The test product description should render before password protection is enabled.'
+		);
+
+		$product->set_post_password( 'secret' );
+		$product->save();
+
+		$this->assertStringNotContainsString(
+			'Protected product description',
+			$block->render(),
+			'Password-protected product descriptions should not render before password entry.'
+		);
+	}
+}
diff --git a/plugins/woocommerce/tests/php/src/Blocks/SharedStores/ProductsStore.php b/plugins/woocommerce/tests/php/src/Blocks/SharedStores/ProductsStore.php
index d6d6b53b59d..7d2d2e9064f 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/SharedStores/ProductsStore.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/SharedStores/ProductsStore.php
@@ -140,6 +140,29 @@ class ProductsStore extends \WC_Unit_Test_Case {
 		$product->delete( true );
 	}

+	/**
+	 * @testdox load_variations() does not hydrate descriptions protected by the parent product password.
+	 */
+	public function test_load_variations_omits_parent_password_protected_descriptions(): void {
+		$product      = WC_Helper_Product::create_variation_product();
+		$variation_id = $product->get_children()[0];
+		$variation    = wc_get_product( $variation_id );
+		$variation->set_description( 'Protected variation description' );
+		$variation->save();
+		$product->set_post_password( 'secret' );
+		$product->save();
+
+		$result = TestedProductsStore::load_variations( $this->consent, $product->get_id() );
+
+		$this->assertSame(
+			'',
+			$result[ $variation_id ]['description'],
+			'Variation descriptions should be omitted until the parent product password is entered.'
+		);
+
+		$product->delete( true );
+	}
+
 	/**
 	 * @testdox load_variations() fetches each parent product from REST only once.
 	 */
diff --git a/plugins/woocommerce/tests/php/src/Blocks/Utils/ProductDescriptionUtilsTest.php b/plugins/woocommerce/tests/php/src/Blocks/Utils/ProductDescriptionUtilsTest.php
index 3a8a0254b17..8e56828d59d 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/Utils/ProductDescriptionUtilsTest.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/Utils/ProductDescriptionUtilsTest.php
@@ -61,4 +61,45 @@ class ProductDescriptionUtilsTest extends \WC_Unit_Test_Case {
 			WC_Helper_Product::delete_product( $product->get_id() );
 		}
 	}
+
+	/**
+	 * @testdox guarded_format() does not invoke the formatter for a password-protected product.
+	 */
+	public function test_guarded_format_blocks_password_protected_product_description(): void {
+		$product = WC_Helper_Product::create_simple_product();
+		$product->set_post_password( 'secret' );
+		$product->save();
+		$formatter_called = false;
+
+		$result = ProductDescriptionUtils::guarded_format(
+			$product,
+			function () use ( &$formatter_called ) {
+				$formatter_called = true;
+
+				return 'Protected description';
+			}
+		);
+
+		$this->assertSame( '', $result, 'Protected product descriptions should not be formatted before password entry.' );
+		$this->assertFalse( $formatter_called, 'The formatter should not receive the description before password entry.' );
+	}
+
+	/**
+	 * @testdox guarded_format() protects variation descriptions using the parent product password.
+	 */
+	public function test_guarded_format_blocks_description_when_parent_is_password_protected(): void {
+		$product = WC_Helper_Product::create_variation_product();
+		$product->set_post_password( 'secret' );
+		$product->save();
+		$variation = wc_get_product( $product->get_children()[0] );
+
+		$result = ProductDescriptionUtils::guarded_format(
+			$variation,
+			function () {
+				return 'Protected variation description';
+			}
+		);
+
+		$this->assertSame( '', $result, 'A variation description should not render until the parent product password is entered.' );
+	}
 }