Commit 8874024828e for woocommerce
commit 8874024828e85944d0a452c186f1ad14a25e0a1d
Author: Jorge A. Torres <jorge.torres@automattic.com>
Date: Thu Sep 24 13:36:08 2026 +0100
Fix published products with a missing parent being hidden from the catalog (#68988)
* Don't hide published products whose parent post is missing or not a product
* Add tests for products with missing or non-product parents
* Add changelog entry for the missing parent visibility fix
diff --git a/plugins/woocommerce/changelog/fix-missing-post-parent-hides-product b/plugins/woocommerce/changelog/fix-missing-post-parent-hides-product
new file mode 100644
index 00000000000..79e39b8c893
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-missing-post-parent-hides-product
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Restore visibility and purchasability of published products whose post_parent points at a missing or non-product post
diff --git a/plugins/woocommerce/includes/abstracts/abstract-wc-product.php b/plugins/woocommerce/includes/abstracts/abstract-wc-product.php
index 6296c0a1cf5..4580ce93415 100644
--- a/plugins/woocommerce/includes/abstracts/abstract-wc-product.php
+++ b/plugins/woocommerce/includes/abstracts/abstract-wc-product.php
@@ -1715,29 +1715,31 @@ class WC_Product extends WC_Abstract_Legacy_Product {
/**
* Whether the current user can view this product: it is published, or they can edit it.
- * A variation additionally requires its parent to be viewable.
+ * A variation additionally requires its parent product to be viewable.
*
* @since 11.1.0
* @return bool
*/
public function is_viewable() {
- $parent_id = $this->get_parent_id();
+ $parent_id = $this->get_parent_id();
+ $has_product_parent = $parent_id && in_array( get_post_type( $parent_id ), array( 'product', 'product_variation' ), true );
return ( ProductStatus::PUBLISH === $this->get_status() || current_user_can( 'edit_post', $this->get_id() ) )
- && ( ! $parent_id || ProductStatus::PUBLISH === get_post_status( $parent_id ) || current_user_can( 'edit_post', $parent_id ) );
+ && ( ! $has_product_parent || ProductStatus::PUBLISH === get_post_status( $parent_id ) || current_user_can( 'edit_post', $parent_id ) );
}
/**
- * Whether this product is publicly viewable: the product and its parent (if it has one) are published.
+ * Whether this product is publicly viewable: the product and its parent product (if it has one) are published.
*
* @since 11.1.0
* @return bool
*/
public function is_publicly_viewable() {
- $parent_id = $this->get_parent_id();
+ $parent_id = $this->get_parent_id();
+ $has_product_parent = $parent_id && in_array( get_post_type( $parent_id ), array( 'product', 'product_variation' ), true );
return ProductStatus::PUBLISH === $this->get_status()
- && ( ! $parent_id || ProductStatus::PUBLISH === get_post_status( $parent_id ) );
+ && ( ! $has_product_parent || ProductStatus::PUBLISH === get_post_status( $parent_id ) );
}
/**
diff --git a/plugins/woocommerce/src/Internal/ShopperLists/ShopperListItem.php b/plugins/woocommerce/src/Internal/ShopperLists/ShopperListItem.php
index 6fe986b5280..9168b9a2e02 100644
--- a/plugins/woocommerce/src/Internal/ShopperLists/ShopperListItem.php
+++ b/plugins/woocommerce/src/Internal/ShopperLists/ShopperListItem.php
@@ -3,7 +3,6 @@ declare( strict_types = 1 );
namespace Automattic\WooCommerce\Internal\ShopperLists;
-use Automattic\WooCommerce\Enums\ProductStatus;
use Automattic\WooCommerce\Enums\ProductType;
/**
@@ -134,7 +133,7 @@ class ShopperListItem {
*/
public static function from_product( int $product_or_variation_id, array $variation = array(), int $quantity = 1 ): ?self {
$product = wc_get_product( absint( $product_or_variation_id ) );
- if ( ! $product || ! self::product_is_live( $product ) ) {
+ if ( ! $product || ! $product->is_publicly_viewable() ) {
return null;
}
@@ -313,29 +312,7 @@ class ShopperListItem {
*/
public function is_live(): bool {
$product = $this->get_product();
- return $product instanceof \WC_Product && self::product_is_live( $product );
- }
-
- /**
- * Whether a resolved product (and its parent, for variations) is `publish`.
- *
- * @param \WC_Product $product Resolved product or variation.
- */
- private static function product_is_live( \WC_Product $product ): bool {
- if ( ProductStatus::PUBLISH !== $product->get_status() ) {
- return false;
- }
-
- $parent_id = $product->get_parent_id();
- if ( $parent_id > 0 ) {
- $parent = wc_get_product( $parent_id );
-
- if ( ! $parent instanceof \WC_Product || ProductStatus::PUBLISH !== $parent->get_status() ) {
- return false;
- }
- }
-
- return true;
+ return $product instanceof \WC_Product && $product->is_publicly_viewable();
}
/**
diff --git a/plugins/woocommerce/tests/php/includes/abstracts/class-wc-abstract-product-test.php b/plugins/woocommerce/tests/php/includes/abstracts/class-wc-abstract-product-test.php
index eb81dc5e3ec..58d636fce7b 100644
--- a/plugins/woocommerce/tests/php/includes/abstracts/class-wc-abstract-product-test.php
+++ b/plugins/woocommerce/tests/php/includes/abstracts/class-wc-abstract-product-test.php
@@ -1,5 +1,6 @@
<?php
+use Automattic\WooCommerce\Enums\ProductStatus;
use Automattic\WooCommerce\Internal\CostOfGoodsSold\CogsAwareUnitTestSuiteTrait;
use Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Register as Download_Directories;
@@ -541,4 +542,51 @@ class WC_Abstract_Product_Test extends WC_Unit_Test_Case {
$this->assertTrue( $product->is_viewable(), "A $status product is viewable by admins." );
$this->assertFalse( $product->is_publicly_viewable(), "A $status product is never publicly viewable, even for admins." );
}
+
+ /**
+ * @testdox A missing or non-product parent doesn't affect visibility, but an unpublished product parent hides the product from the public.
+ * @testWith ["missing", true]
+ * ["draft_page", true]
+ * ["draft_product", false]
+ * ["published_product", true]
+ * @param string $parent_kind Kind of post the parent ID points at.
+ * @param bool $visible_logged_out Whether the product should be visible to a logged-out user.
+ */
+ public function test_is_viewable_with_parent( $parent_kind, $visible_logged_out ) {
+ switch ( $parent_kind ) {
+ case 'missing':
+ $parent_id = 987654321;
+ break;
+ case 'draft_page':
+ $parent_id = self::factory()->post->create(
+ array(
+ 'post_type' => 'page',
+ 'post_status' => 'draft',
+ )
+ );
+ break;
+ case 'draft_product':
+ case 'published_product':
+ $parent = WC_Helper_Product::create_simple_product();
+ $parent->set_status( 'draft_product' === $parent_kind ? ProductStatus::DRAFT : ProductStatus::PUBLISH );
+ $parent->save();
+ $parent_id = $parent->get_id();
+ break;
+ default:
+ $this->fail( "Unhandled parent kind: $parent_kind." );
+ }
+
+ $product = WC_Helper_Product::create_simple_product();
+ $product->set_parent_id( $parent_id );
+ $product->save();
+
+ wp_set_current_user( 0 );
+ $this->assertSame( $visible_logged_out, $product->is_viewable(), "A product with a $parent_kind parent has unexpected is_viewable() when logged out." );
+ $this->assertSame( $visible_logged_out, $product->is_publicly_viewable(), "A product with a $parent_kind parent has unexpected is_publicly_viewable()." );
+ $this->assertSame( $visible_logged_out, $product->is_visible(), "A product with a $parent_kind parent has unexpected is_visible() when logged out." );
+ $this->assertSame( $visible_logged_out, $product->is_purchasable(), "A product with a $parent_kind parent has unexpected is_purchasable() when logged out." );
+
+ wp_set_current_user( $this->admin_user );
+ $this->assertTrue( $product->is_viewable(), "A product with a $parent_kind parent is viewable by admins." );
+ }
}
diff --git a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Schemas/ShopperListItemSchemaTest.php b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Schemas/ShopperListItemSchemaTest.php
index 89d9406675a..986c7357c6d 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Schemas/ShopperListItemSchemaTest.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Schemas/ShopperListItemSchemaTest.php
@@ -133,7 +133,7 @@ class ShopperListItemSchemaTest extends WC_Unit_Test_Case {
$product->set_catalog_visibility( $overrides['catalog_visibility'] );
$product->save();
}
- $post_overrides = array_intersect_key( $overrides, array_flip( array( 'post_status', 'post_password' ) ) );
+ $post_overrides = array_intersect_key( $overrides, array_flip( array( 'post_status', 'post_password', 'post_parent' ) ) );
if ( ! empty( $post_overrides ) ) {
// `wp_update_post` silently rewrites `future` back to `publish` when post_date is in the past,
// so a future date is needed to actually persist the status.
@@ -158,7 +158,7 @@ class ShopperListItemSchemaTest extends WC_Unit_Test_Case {
}
/**
- * @return array<string, array{0: array<string, string>, 1: bool}>
+ * @return array<string, array{0: array<string, string|int>, 1: bool}>
*/
public function provider_is_live_cases(): array {
return array(
@@ -166,6 +166,7 @@ class ShopperListItemSchemaTest extends WC_Unit_Test_Case {
// tombstone deliberately-saved OOS / catalog-hidden items.
'OOS, publish' => array( array( 'stock_status' => 'outofstock' ), true ),
'catalog_visibility=hidden' => array( array( 'catalog_visibility' => 'hidden' ), true ),
+ 'missing parent post' => array( array( 'post_parent' => 987654321 ), true ),
// Tombstone cases.
'draft' => array( array( 'post_status' => 'draft' ), false ),
'pending' => array( array( 'post_status' => 'pending' ), false ),