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