Commit bb5b474bd2b for woocommerce

commit bb5b474bd2b5d96eee4e1a931f714deac303410a
Author: Christopher Hertel <mail@christopher-hertel.de>
Date:   Fri Nov 13 11:51:07 2026 +0100

    Honour truthy REST permission grants in abilities (#70924)

    * Honour truthy REST permission grants in abilities

    The review abilities compared woocommerce_rest_check_permissions and
    woocommerce_rest_product_review_trashable with true ===, so a callback
    returning 1 or 'yes' was denied although REST allows it.

    Other abilities returned the raw wc_rest_check_*() result from a
    bool-typed method in a strict_types file, so the same callback threw a
    TypeError instead. All of them now cast the filtered result to bool,
    the truthiness REST applies.

    * Honour truthy REST permission grants in the customers query

    Test the grants as a subscriber, so they override missing capabilities.

    * Test truthy REST permission grants in the product term write abilities

diff --git a/plugins/woocommerce/changelog/fix-abilities-truthy-rest-permission b/plugins/woocommerce/changelog/fix-abilities-truthy-rest-permission
new file mode 100644
index 00000000000..889ddaa48f8
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-abilities-truthy-rest-permission
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Let connected agents act when an extension grants REST access with a truthy value other than true.
diff --git a/plugins/woocommerce/src/Internal/Abilities/Domain/CustomersQuery.php b/plugins/woocommerce/src/Internal/Abilities/Domain/CustomersQuery.php
index a250877e3a3..a6b49b646de 100644
--- a/plugins/woocommerce/src/Internal/Abilities/Domain/CustomersQuery.php
+++ b/plugins/woocommerce/src/Internal/Abilities/Domain/CustomersQuery.php
@@ -71,7 +71,7 @@ class CustomersQuery extends AbstractDomainAbility implements AbilityDefinition
 	 * @return bool
 	 */
 	public static function can_query_customers( $input = null ): bool {
-		return wc_rest_check_user_permissions( 'read' );
+		return (bool) wc_rest_check_user_permissions( 'read' );
 	}

 	/**
diff --git a/plugins/woocommerce/src/Internal/Abilities/Domain/OrdersQuery.php b/plugins/woocommerce/src/Internal/Abilities/Domain/OrdersQuery.php
index a88e92c64f6..96e42472695 100644
--- a/plugins/woocommerce/src/Internal/Abilities/Domain/OrdersQuery.php
+++ b/plugins/woocommerce/src/Internal/Abilities/Domain/OrdersQuery.php
@@ -265,7 +265,7 @@ class OrdersQuery extends AbstractDomainAbility implements AbilityDefinition {

 		$type = self::get_order_type_from_input( $input );

-		return wc_rest_check_post_permissions( $type, 'read' );
+		return (bool) wc_rest_check_post_permissions( $type, 'read' );
 	}

 	/**
diff --git a/plugins/woocommerce/src/Internal/Abilities/Domain/PaymentGatewaysQuery.php b/plugins/woocommerce/src/Internal/Abilities/Domain/PaymentGatewaysQuery.php
index 9ef500ff6d4..b59f488f133 100644
--- a/plugins/woocommerce/src/Internal/Abilities/Domain/PaymentGatewaysQuery.php
+++ b/plugins/woocommerce/src/Internal/Abilities/Domain/PaymentGatewaysQuery.php
@@ -116,7 +116,7 @@ class PaymentGatewaysQuery extends AbstractDomainAbility implements AbilityDefin
 	 * @since 11.4.0
 	 */
 	public static function can_query_payment_gateways(): bool {
-		return wc_rest_check_manager_permissions( 'payment_gateways', 'read' );
+		return (bool) wc_rest_check_manager_permissions( 'payment_gateways', 'read' );
 	}

 	/**
diff --git a/plugins/woocommerce/src/Internal/Abilities/Domain/ProductReviewUpdate.php b/plugins/woocommerce/src/Internal/Abilities/Domain/ProductReviewUpdate.php
index f527cd274ea..dc51289f9d7 100644
--- a/plugins/woocommerce/src/Internal/Abilities/Domain/ProductReviewUpdate.php
+++ b/plugins/woocommerce/src/Internal/Abilities/Domain/ProductReviewUpdate.php
@@ -170,7 +170,7 @@ class ProductReviewUpdate extends AbstractDomainAbility implements AbilityDefini
 			 * @param bool        $supports_trash Whether reviews can be trashed.
 			 * @param \WP_Comment $review         Review, or reply to a review, to trash.
 			 */
-			$trashable = true === apply_filters( 'woocommerce_rest_product_review_trashable', ( EMPTY_TRASH_DAYS > 0 ), $review );
+			$trashable = (bool) apply_filters( 'woocommerce_rest_product_review_trashable', ( EMPTY_TRASH_DAYS > 0 ), $review );

 			// wp_trash_comment() deletes permanently when trash is off, whatever the filter says.
 			if ( ! $trashable || EMPTY_TRASH_DAYS <= 0 ) {
diff --git a/plugins/woocommerce/src/Internal/Abilities/Domain/ProductsQuery.php b/plugins/woocommerce/src/Internal/Abilities/Domain/ProductsQuery.php
index 1a048b4eac1..bdd7859aac2 100644
--- a/plugins/woocommerce/src/Internal/Abilities/Domain/ProductsQuery.php
+++ b/plugins/woocommerce/src/Internal/Abilities/Domain/ProductsQuery.php
@@ -258,7 +258,7 @@ class ProductsQuery extends AbstractDomainAbility implements AbilityDefinition {
 	public static function can_query_products( $input = array() ): bool {
 		$product_id = self::get_product_permission_id( self::get_id_from_input( $input ) );

-		return wc_rest_check_post_permissions( 'product', 'read', $product_id );
+		return (bool) wc_rest_check_post_permissions( 'product', 'read', $product_id );
 	}

 	/**
diff --git a/plugins/woocommerce/src/Internal/Abilities/Domain/ShippingZonesQuery.php b/plugins/woocommerce/src/Internal/Abilities/Domain/ShippingZonesQuery.php
index 413123d7358..146aa76d572 100644
--- a/plugins/woocommerce/src/Internal/Abilities/Domain/ShippingZonesQuery.php
+++ b/plugins/woocommerce/src/Internal/Abilities/Domain/ShippingZonesQuery.php
@@ -119,7 +119,7 @@ class ShippingZonesQuery extends AbstractDomainAbility implements AbilityDefinit
 	 * @since 11.4.0
 	 */
 	public static function can_query_shipping(): bool {
-		return wc_rest_check_manager_permissions( 'settings', 'read' );
+		return (bool) wc_rest_check_manager_permissions( 'settings', 'read' );
 	}

 	/**
diff --git a/plugins/woocommerce/src/Internal/Abilities/Domain/TaxRatesQuery.php b/plugins/woocommerce/src/Internal/Abilities/Domain/TaxRatesQuery.php
index 195ee1c23e4..06c838b793e 100644
--- a/plugins/woocommerce/src/Internal/Abilities/Domain/TaxRatesQuery.php
+++ b/plugins/woocommerce/src/Internal/Abilities/Domain/TaxRatesQuery.php
@@ -180,7 +180,7 @@ class TaxRatesQuery extends AbstractDomainAbility implements AbilityDefinition {
 	 * @since 11.4.0
 	 */
 	public static function can_query_taxes(): bool {
-		return wc_rest_check_manager_permissions( 'settings', 'read' );
+		return (bool) wc_rest_check_manager_permissions( 'settings', 'read' );
 	}

 	/**
diff --git a/plugins/woocommerce/src/Internal/Abilities/Domain/Traits/OrderAbilityTrait.php b/plugins/woocommerce/src/Internal/Abilities/Domain/Traits/OrderAbilityTrait.php
index 7025fed7a3d..1dd93332377 100644
--- a/plugins/woocommerce/src/Internal/Abilities/Domain/Traits/OrderAbilityTrait.php
+++ b/plugins/woocommerce/src/Internal/Abilities/Domain/Traits/OrderAbilityTrait.php
@@ -368,7 +368,7 @@ trait OrderAbilityTrait {
 		$class_names = \WC_Order_Factory::get_class_names_for_order_ids( array( $order_id ) );

 		if ( empty( $class_names[ $order_id ] ) ) {
-			return wc_rest_check_post_permissions( 'shop_order', 'edit' === $context ? 'batch' : $context );
+			return (bool) wc_rest_check_post_permissions( 'shop_order', 'edit' === $context ? 'batch' : $context );
 		}

 		$type = OrderUtil::get_order_type( $order_id );
diff --git a/plugins/woocommerce/src/Internal/Abilities/Domain/Traits/ProductAttributeAbilityTrait.php b/plugins/woocommerce/src/Internal/Abilities/Domain/Traits/ProductAttributeAbilityTrait.php
index 30f4cbfcda4..441ea257950 100644
--- a/plugins/woocommerce/src/Internal/Abilities/Domain/Traits/ProductAttributeAbilityTrait.php
+++ b/plugins/woocommerce/src/Internal/Abilities/Domain/Traits/ProductAttributeAbilityTrait.php
@@ -21,7 +21,7 @@ trait ProductAttributeAbilityTrait {
 	 * @return bool
 	 */
 	private static function can_manage_product_attributes( string $context ): bool {
-		return wc_rest_check_manager_permissions( 'attributes', $context );
+		return (bool) wc_rest_check_manager_permissions( 'attributes', $context );
 	}

 	/**
diff --git a/plugins/woocommerce/src/Internal/Abilities/Domain/Traits/ProductReviewAbilityTrait.php b/plugins/woocommerce/src/Internal/Abilities/Domain/Traits/ProductReviewAbilityTrait.php
index 194324c6e9a..6b37b63712f 100644
--- a/plugins/woocommerce/src/Internal/Abilities/Domain/Traits/ProductReviewAbilityTrait.php
+++ b/plugins/woocommerce/src/Internal/Abilities/Domain/Traits/ProductReviewAbilityTrait.php
@@ -168,7 +168,7 @@ trait ProductReviewAbilityTrait {
 		 * @param int    $object_id   Review ID.
 		 * @param string $object_type Always 'product_review'.
 		 */
-		return true === apply_filters( 'woocommerce_rest_check_permissions', current_user_can( 'edit_products' ), $context, $review_id, 'product_review' );
+		return (bool) apply_filters( 'woocommerce_rest_check_permissions', current_user_can( 'edit_products' ), $context, $review_id, 'product_review' );
 	}

 	/**
diff --git a/plugins/woocommerce/src/Internal/Abilities/Domain/Traits/ProductTermAbilityTrait.php b/plugins/woocommerce/src/Internal/Abilities/Domain/Traits/ProductTermAbilityTrait.php
index 67682a735f5..bfcc8c54369 100644
--- a/plugins/woocommerce/src/Internal/Abilities/Domain/Traits/ProductTermAbilityTrait.php
+++ b/plugins/woocommerce/src/Internal/Abilities/Domain/Traits/ProductTermAbilityTrait.php
@@ -41,9 +41,9 @@ trait ProductTermAbilityTrait {
 	private static function can_manage_product_terms( string $taxonomy, string $context, int $object_id = 0 ): bool {
 		if ( ! taxonomy_exists( $taxonomy ) ) {
 			// Let execute() tell a store manager that the attribute does not exist, rather than refuse access.
-			return wc_rest_check_manager_permissions( 'attributes', 'read' );
+			return (bool) wc_rest_check_manager_permissions( 'attributes', 'read' );
 		}
-		return wc_rest_check_product_term_permissions( $taxonomy, $context, $object_id );
+		return (bool) wc_rest_check_product_term_permissions( $taxonomy, $context, $object_id );
 	}

 	/**
diff --git a/plugins/woocommerce/tests/php/src/Internal/Abilities/AbilitiesLoaderTest.php b/plugins/woocommerce/tests/php/src/Internal/Abilities/AbilitiesLoaderTest.php
index fc9d29ee1d4..c816a566a50 100644
--- a/plugins/woocommerce/tests/php/src/Internal/Abilities/AbilitiesLoaderTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/Abilities/AbilitiesLoaderTest.php
@@ -1151,6 +1151,63 @@ class AbilitiesLoaderTest extends \WC_Unit_Test_Case {
 		);
 	}

+	/**
+	 * @testdox Should grant a subscriber access when woocommerce_rest_check_permissions returns a truthy non-boolean, as REST does.
+	 *
+	 * @dataProvider provider_abilities_checking_rest_permissions
+	 *
+	 * @param string $ability_id Ability ID.
+	 * @param array  $input      Input that reaches the REST permission check.
+	 */
+	public function test_abilities_accept_truthy_rest_permission( string $ability_id, array $input ): void {
+		wp_set_current_user( $this->factory->user->create( array( 'role' => 'subscriber' ) ) );
+		add_filter( 'woocommerce_rest_check_permissions', fn() => 1 );
+
+		$this->assertTrue( wp_get_ability( $ability_id )->check_permissions( $input ) );
+	}
+
+	/**
+	 * Provides one input per path through which an ability returns a REST permission check.
+	 *
+	 * @return array<string, array{0: string, 1: array}>
+	 */
+	public function provider_abilities_checking_rest_permissions(): array {
+		$missing_id = array( 'id' => PHP_INT_MAX );
+
+		return array(
+			'products-query'                 => array( 'woocommerce/products-query', $missing_id ),
+			'orders-query by type'           => array( 'woocommerce/orders-query', array() ),
+			'orders-query by missing order'  => array( 'woocommerce/orders-query', $missing_id ),
+			'payment-gateways-query'         => array( 'woocommerce/payment-gateways-query', array() ),
+			'customers-query'                => array( 'woocommerce/customers-query', array() ),
+			'shipping-zones-query'           => array( 'woocommerce/shipping-zones-query', array() ),
+			'tax-rates-query'                => array( 'woocommerce/tax-rates-query', array() ),
+			'product-attributes-query'       => array( 'woocommerce/product-attributes-query', array() ),
+			'product-attribute-create'       => array( 'woocommerce/product-attribute-create', array( 'name' => 'Size' ) ),
+			'product-terms-query'            => array( 'woocommerce/product-terms-query', array( 'taxonomy' => 'product_cat' ) ),
+			'product-terms-query of unknown' => array( 'woocommerce/product-terms-query', array( 'taxonomy' => 'pa_missing' ) ),
+			'product-term-create'            => array(
+				'woocommerce/product-term-create',
+				array(
+					'taxonomy' => 'product_cat',
+					'name'     => 'Granted',
+				),
+			),
+			'product-term-update'            => array(
+				'woocommerce/product-term-update',
+				$missing_id + array( 'taxonomy' => 'product_cat' ),
+			),
+			'product-review-update'          => array(
+				'woocommerce/product-review-update',
+				$missing_id + array( 'status' => 'approved' ),
+			),
+			'product-review-reply'           => array(
+				'woocommerce/product-review-reply',
+				$missing_id + array( 'content' => 'Thanks!' ),
+			),
+		);
+	}
+
 	/**
 	 * @testdox Should require object edit and delete permissions for product mutations.
 	 */
diff --git a/plugins/woocommerce/tests/php/src/Internal/Abilities/Domain/ProductReviewUpdateTest.php b/plugins/woocommerce/tests/php/src/Internal/Abilities/Domain/ProductReviewUpdateTest.php
index 69ff058492a..c79c598fb90 100644
--- a/plugins/woocommerce/tests/php/src/Internal/Abilities/Domain/ProductReviewUpdateTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/Abilities/Domain/ProductReviewUpdateTest.php
@@ -302,6 +302,24 @@ class ProductReviewUpdateTest extends \WC_Unit_Test_Case {
 		$this->assertStringNotContainsString( 'Restore the product', $result->get_error_message(), 'Only a trashed product needs restoring.' );
 	}

+	/**
+	 * @testdox Should trash a review when the trashable filter returns a truthy non-boolean, as REST does.
+	 */
+	public function test_truthy_trashable_filter_allows_trash(): void {
+		$id = $this->review( 'approved' );
+		add_filter( 'woocommerce_rest_product_review_trashable', fn() => 1 );
+
+		$result = $this->sut->execute(
+			array(
+				'id'     => $id,
+				'status' => 'trash',
+			)
+		);
+
+		$this->assertIsArray( $result );
+		$this->assertSame( 'trash', get_comment( $id )->comment_approved );
+	}
+
 	/**
 	 * @testdox Should moderate a direct reply to a review, but not a reply to a reply.
 	 */