Commit a2939fcd67b for woocommerce

commit a2939fcd67b8040b20f270d2461cb76204a62ad1
Author: Cvetan Cvetanov <cvetan.cvetanov@automattic.com>
Date:   Fri Aug 7 11:43:47 2026 +0300

    Fix product search filters applying to only a single OR group (#67420)

    * Fix product search filters applying only to the last OR group

    WC_Product_Data_Store_CPT::search_products() joins OR-term groups as
    AND (g1) OR (g2) without wrapping the disjunction in its own
    parentheses. Since AND binds tighter than OR, the include, exclude,
    status, product type and post type clauses appended after it apply only
    to the last group, so admin product pickers could suggest drafts,
    excluded products, non-downloadable products in downloadable searches,
    and even non-product post types for terms containing " or ".

    Wrap the whole disjunction in an extra pair of parentheses so every
    trailing filter applies to all groups. Single-group terms, the common
    case, are unaffected.

    The include and exclude leaks date to the introduction of those
    arguments in 3.6.0; the status, type and post type leaks go back to the
    OR splitting itself (~3.2).

    * Add changelog entry for product search OR group fix

    * Add regression test for the middle group of a three-group OR search

    The two-group tests cover the first and last OR groups, but under the
    old predicate a middle group in a three-plus-group term carried no
    constraint at all, not even the post type check. Pin that case with a
    draft product matched by the middle group of a published-only search.

    Verified red against the unfixed predicate and green with the fix.

    * Correct changelog wording on which OR group carried each filter

    The entry said all filters previously applied only to the last group,
    but the post type constraint bound to the first group and escaped from
    the last. Say "only a single group" instead of naming the wrong one.

    * Scope the changelog claim to rows matched by the OR groups

    The pickers can still re-suggest an excluded parent whose variation
    matches the term, via the post-query parent merge that PR #67308
    addresses. Scoping the claim to rows matched by the groups keeps the
    entry accurate on its own.

    * Restructure OR-group search tests per review feedback

    Move the OR-group tests and their helper next to
    test_variation_searches_parent_sku so all search_products() coverage
    sits together, which also moves the block away from the end of the
    file where PR #67308 adds its own tests. Rename the fixture products
    from "... widget" to "... product" to avoid reading as theme
    widgets; names stay multi-word so each OR group still produces
    several AND-joined LIKE terms.

    * Harden OR-term product search tests

    All in-core search_products() callers pass $include_variations = true,
    but the OR-group regression tests only covered the simple path. Add a
    test mirroring the downloadable-and-variations AJAX caller, which adds
    a LEFT JOIN and extra OR terms inside the changed parenthesization;
    it goes red with the fix reverted.

    Also assert the page fixture insert succeeded so a broken fixture
    fails with an accurate message instead of a misleading one.

diff --git a/plugins/woocommerce/changelog/67392-search-or-group-filters b/plugins/woocommerce/changelog/67392-search-or-group-filters
new file mode 100644
index 00000000000..8e823b59b36
--- /dev/null
+++ b/plugins/woocommerce/changelog/67392-search-or-group-filters
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Product searches with terms containing " or " now apply the include, exclude, status, product type and post type filters to every OR group instead of only a single group, so admin product pickers no longer suggest drafts, excluded products or wrong-type products matched by those groups.
diff --git a/plugins/woocommerce/includes/data-stores/class-wc-product-data-store-cpt.php b/plugins/woocommerce/includes/data-stores/class-wc-product-data-store-cpt.php
index 2961c4632a1..aa76f17798c 100644
--- a/plugins/woocommerce/includes/data-stores/class-wc-product-data-store-cpt.php
+++ b/plugins/woocommerce/includes/data-stores/class-wc-product-data-store-cpt.php
@@ -2067,7 +2067,7 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da
 		}

 		if ( ! empty( $search_queries ) ) {
-			$search_where = ' AND (' . implode( ') OR (', $search_queries ) . ') ';
+			$search_where = ' AND ((' . implode( ') OR (', $search_queries ) . ')) ';
 		}

 		if ( ! empty( $include ) && is_array( $include ) ) {
diff --git a/plugins/woocommerce/tests/php/includes/data-stores/class-wc-product-data-store-cpt-test.php b/plugins/woocommerce/tests/php/includes/data-stores/class-wc-product-data-store-cpt-test.php
index 27c688253fa..68fcaf60290 100644
--- a/plugins/woocommerce/tests/php/includes/data-stores/class-wc-product-data-store-cpt-test.php
+++ b/plugins/woocommerce/tests/php/includes/data-stores/class-wc-product-data-store-cpt-test.php
@@ -75,6 +75,156 @@ class WC_Product_Data_Store_CPT_Test extends WC_Unit_Test_Case {
 		$this->assertContains( $variation->get_id(), $results );
 	}

+	/**
+	 * Create a simple product with the given name and status, for OR-term search tests.
+	 *
+	 * @param string $name   Product name.
+	 * @param string $status Post status.
+	 * @return WC_Product_Simple The saved product.
+	 */
+	private function create_search_test_product( string $name, string $status = 'publish' ): WC_Product_Simple {
+		$product = new WC_Product_Simple();
+		$product->set_name( $name );
+		$product->set_status( $status );
+		$product->set_regular_price( '10' );
+		$product->save();
+
+		return $product;
+	}
+
+	/**
+	 * @testdox Search with an OR term should apply the exclude list to every OR group.
+	 */
+	public function test_search_products_or_term_applies_exclude(): void {
+		$alpha = $this->create_search_test_product( 'Searchable alpha product' );
+		$beta  = $this->create_search_test_product( 'Searchable beta product' );
+
+		$data_store = WC_Data_Store::load( 'product' );
+		$results    = $data_store->search_products( 'Searchable alpha product or Searchable beta product', '', false, true, null, null, array( $alpha->get_id() ) );
+
+		$this->assertNotContains( $alpha->get_id(), $results, 'Excluded product matched by the first OR group should not be returned' );
+		$this->assertContains( $beta->get_id(), $results, 'Non-excluded product should still be returned' );
+	}
+
+	/**
+	 * @testdox Search with an OR term should apply the include list to every OR group.
+	 */
+	public function test_search_products_or_term_applies_include(): void {
+		$alpha = $this->create_search_test_product( 'Searchable alpha product' );
+		$beta  = $this->create_search_test_product( 'Searchable beta product' );
+
+		$data_store = WC_Data_Store::load( 'product' );
+		$results    = $data_store->search_products( 'Searchable alpha product or Searchable beta product', '', false, true, null, array( $beta->get_id() ) );
+
+		$this->assertNotContains( $alpha->get_id(), $results, 'Product outside the include list matched by the first OR group should not be returned' );
+		$this->assertContains( $beta->get_id(), $results, 'Included product should be returned' );
+	}
+
+	/**
+	 * @testdox Search with an OR term should apply the status filter to every OR group.
+	 */
+	public function test_search_products_or_term_applies_status_filter(): void {
+		$draft = $this->create_search_test_product( 'Searchable gamma product', 'draft' );
+		$beta  = $this->create_search_test_product( 'Searchable beta product' );
+
+		$data_store = WC_Data_Store::load( 'product' );
+		$results    = $data_store->search_products( 'Searchable gamma product or Searchable beta product', '', false, false );
+
+		$this->assertNotContains( $draft->get_id(), $results, 'Draft product matched by the first OR group should not be returned when searching published only' );
+		$this->assertContains( $beta->get_id(), $results, 'Published product should be returned' );
+	}
+
+	/**
+	 * @testdox Search with an OR term should apply the product type filter to every OR group.
+	 */
+	public function test_search_products_or_term_applies_type_filter(): void {
+		$plain        = $this->create_search_test_product( 'Searchable alpha product' );
+		$downloadable = $this->create_search_test_product( 'Searchable beta product' );
+		$downloadable->set_downloadable( true );
+		$downloadable->save();
+
+		$data_store = WC_Data_Store::load( 'product' );
+		$results    = $data_store->search_products( 'Searchable alpha product or Searchable beta product', 'downloadable', false, true );
+
+		$this->assertNotContains( $plain->get_id(), $results, 'Non-downloadable product matched by the first OR group should not be returned for a downloadable search' );
+		$this->assertContains( $downloadable->get_id(), $results, 'Downloadable product should be returned' );
+	}
+
+	/**
+	 * @testdox Search with an OR term including variations should apply the type and status filters to every OR group.
+	 */
+	public function test_search_products_or_term_applies_filters_with_variations(): void {
+		$plain = $this->create_search_test_product( 'Searchable alpha product' );
+
+		$parent = new WC_Product_Variable();
+		$parent->set_name( 'Searchable beta product' );
+		$parent->save();
+
+		$variation = new WC_Product_Variation();
+		$variation->set_parent_id( $parent->get_id() );
+		$variation->set_regular_price( '10' );
+		$variation->set_downloadable( true );
+		$variation->save();
+
+		$data_store = WC_Data_Store::load( 'product' );
+		$results    = $data_store->search_products( 'Searchable alpha product or Searchable beta product', 'downloadable', true, false );
+
+		$this->assertNotContains( $plain->get_id(), $results, 'Non-downloadable product matched by the first OR group should not be returned for a downloadable variations search' );
+		$this->assertContains( $variation->get_id(), $results, 'Downloadable variation matched by the second OR group should be returned' );
+	}
+
+	/**
+	 * @testdox Search with an OR term should apply the post type constraint to every OR group.
+	 */
+	public function test_search_products_or_term_applies_post_type(): void {
+		$alpha   = $this->create_search_test_product( 'Searchable alpha product' );
+		$page_id = wp_insert_post(
+			array(
+				'post_type'   => 'page',
+				'post_status' => 'publish',
+				'post_title'  => 'Searchable beta product page',
+			)
+		);
+
+		$this->assertGreaterThan( 0, $page_id, 'Page fixture should have been created' );
+
+		$data_store = WC_Data_Store::load( 'product' );
+		$results    = $data_store->search_products( 'Searchable alpha product or Searchable beta product page', '', false, true );
+
+		$this->assertNotContains( $page_id, $results, 'A page matched by the last OR group should not be returned from a product search' );
+		$this->assertContains( $alpha->get_id(), $results, 'Product matched by the first OR group should be returned' );
+	}
+
+	/**
+	 * @testdox Search with a three-group OR term should apply the status filter to the middle group.
+	 */
+	public function test_search_products_or_term_applies_status_filter_to_middle_group(): void {
+		$alpha = $this->create_search_test_product( 'Searchable alpha product' );
+		$draft = $this->create_search_test_product( 'Searchable gamma product', 'draft' );
+		$beta  = $this->create_search_test_product( 'Searchable beta product' );
+
+		$data_store = WC_Data_Store::load( 'product' );
+		$results    = $data_store->search_products( 'Searchable alpha product or Searchable gamma product or Searchable beta product', '', false, false );
+
+		$this->assertNotContains( $draft->get_id(), $results, 'Draft product matched by the middle OR group should not be returned when searching published only' );
+		$this->assertContains( $alpha->get_id(), $results, 'Published product matched by the first OR group should be returned' );
+		$this->assertContains( $beta->get_id(), $results, 'Published product matched by the last OR group should be returned' );
+	}
+
+	/**
+	 * @testdox Search with an OR term without extra filters should return matches from every OR group.
+	 */
+	public function test_search_products_or_term_returns_all_groups(): void {
+		$alpha = $this->create_search_test_product( 'Searchable alpha product' );
+		$beta  = $this->create_search_test_product( 'Searchable beta product' );
+
+		$data_store = WC_Data_Store::load( 'product' );
+		$results    = $data_store->search_products( 'Searchable alpha product or Searchable beta product', '', false, true );
+
+		$this->assertContains( $alpha->get_id(), $results, 'Product matched by the first OR group should be returned' );
+		$this->assertContains( $beta->get_id(), $results, 'Product matched by the second OR group should be returned' );
+	}
+
 	/**
 	 * Ensure product rating counts are calculated correctly.
 	 *