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.' );
+ }
}