Commit 13707a2a9e3 for woocommerce
commit 13707a2a9e33c959c83794c326ab4c7c5aeb974c
Author: Karol Manijak <20098064+kmanijak@users.noreply.github.com>
Date: Tue Sep 8 11:28:01 2026 +0200
Fix password protection flag for variations so it follows parent product (#68431)
* Fix variation password protection flag
* Add changelog entry for variation password flag
* Clarify Store API password protection documentation
diff --git a/docs/apis/store-api/resources-endpoints/products.md b/docs/apis/store-api/resources-endpoints/products.md
index cd5163f8b76..f621e12b8da 100644
--- a/docs/apis/store-api/resources-endpoints/products.md
+++ b/docs/apis/store-api/resources-endpoints/products.md
@@ -10,7 +10,9 @@ Only published products are accessible via the Store API. Requesting a draft, pe
### Password-protected products
-Password-protected products are visible in the API, but their `description` and `short_description` fields are redacted (returned as empty strings) until the correct password has been submitted. The response includes an `is_password_protected` boolean field so clients can detect this state and prompt the user.
+Password-protected products, including variations whose parent product is password-protected, are visible in the API, but their `description` and `short_description` fields are redacted (returned as empty strings) until the correct password has been submitted.
+
+The `is_password_protected` field indicates whether the product or its parent has a configured password. It remains `true` after the current visitor has submitted the correct password and the descriptions become accessible.
Password verification uses WordPress's native `wp-postpass_*` cookie, set when a user submits the password form on the frontend. The Store API does not accept passwords directly.
diff --git a/plugins/woocommerce/changelog/fix-variation-password-protection-flag b/plugins/woocommerce/changelog/fix-variation-password-protection-flag
new file mode 100644
index 00000000000..4022fef3485
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-variation-password-protection-flag
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Report inherited password protection correctly for product variations in Store API responses.
diff --git a/plugins/woocommerce/src/StoreApi/Schemas/V1/ProductSchema.php b/plugins/woocommerce/src/StoreApi/Schemas/V1/ProductSchema.php
index 9341beb890d..9062905b778 100644
--- a/plugins/woocommerce/src/StoreApi/Schemas/V1/ProductSchema.php
+++ b/plugins/woocommerce/src/StoreApi/Schemas/V1/ProductSchema.php
@@ -578,7 +578,7 @@ class ProductSchema extends AbstractSchema {
],
],
'is_password_protected' => [
- 'description' => __( 'Whether the product requires a password to access its content.', 'woocommerce' ),
+ 'description' => __( 'Whether the product or its parent requires a password to access its content.', 'woocommerce' ),
'type' => 'boolean',
'context' => [ 'view', 'edit', 'embed' ],
'readonly' => true,
@@ -662,12 +662,28 @@ class ProductSchema extends AbstractSchema {
],
( new QuantityLimits() )->get_add_to_cart_limits( $product )
),
- 'is_password_protected' => '' !== $product->get_post_password(),
+ 'is_password_protected' => $this->is_password_protected( $product ),
self::EXTENDING_KEY => $this->get_extended_data( self::IDENTIFIER, $product ),
];
}
+ /**
+ * Whether the product or its parent is password-protected.
+ *
+ * @param \WC_Product $product Product instance.
+ * @return bool
+ */
+ private function is_password_protected( $product ) {
+ if ( '' !== $product->get_post_password() ) {
+ return true;
+ }
+
+ $parent_id = $product->get_parent_id();
+
+ return $parent_id && '' !== get_post_field( 'post_password', $parent_id, 'raw' );
+ }
+
/**
* Get list of product images.
*
diff --git a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/Products.php b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/Products.php
index 6c3d6e87fa5..13f9c617228 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/Products.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/Products.php
@@ -856,6 +856,50 @@ class Products extends ControllerTestCase {
$this->assertFalse( $data['is_password_protected'] );
}
+ /**
+ * @testdox Variations should inherit the parent product's password-protected flag regardless of access.
+ */
+ public function test_variation_inherits_parent_password_protected_flag(): void {
+ $password = 'secret';
+ $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( $password );
+ $product->save();
+
+ $request = new \WP_REST_Request( 'GET', '/wc/store/v1/products' );
+ $request->set_query_params(
+ array(
+ 'include' => array( $variation_id ),
+ 'parent' => array( $product->get_id() ),
+ 'type' => 'variation',
+ )
+ );
+
+ $protected_response = rest_get_server()->dispatch( $request );
+ $this->assertSame( 200, $protected_response->get_status() );
+ $protected_data = $protected_response->get_data()[0];
+ $this->assertTrue( $protected_data['is_password_protected'] );
+ $this->assertSame( '', $protected_data['description'] );
+
+ require_once ABSPATH . WPINC . '/class-phpass.php';
+ $hasher = new \PasswordHash( 8, true );
+ $_COOKIE[ 'wp-postpass_' . COOKIEHASH ] = $hasher->HashPassword( $password );
+
+ try {
+ $accessible_response = rest_get_server()->dispatch( $request );
+ } finally {
+ unset( $_COOKIE[ 'wp-postpass_' . COOKIEHASH ] );
+ }
+
+ $this->assertSame( 200, $accessible_response->get_status() );
+ $accessible_data = $accessible_response->get_data()[0];
+ $this->assertTrue( $accessible_data['is_password_protected'] );
+ $this->assertStringContainsString( 'Protected variation description', $accessible_data['description'] );
+ }
+
/**
* @testdox Related query parameter returns empty when no related products exist.
*/