Commit c87d0c1cc30 for woocommerce
commit c87d0c1cc30794503dff88143d85d662bfad6258
Author: Luis Felipe Zaguini <26530524+zaguiini@users.noreply.github.com>
Date: Fri Oct 16 20:33:07 2026 -0300
Check order notes permission against the actual order type (#69956)
The `woocommerce/order-notes-query` ability checks read access with the `shop_order` capability for every order ID. The other order abilities check the capability of the order's actual type. An extension can register an order type with its own read capability. For that type, the notes ability asks for the wrong capability.
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
diff --git a/plugins/woocommerce/changelog/fix-order-notes-query-permission b/plugins/woocommerce/changelog/fix-order-notes-query-permission
new file mode 100644
index 00000000000..6ded7fe6ced
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-order-notes-query-permission
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Check the order notes query ability permission against the actual order type.
diff --git a/plugins/woocommerce/src/Internal/Abilities/Domain/OrderNotesQuery.php b/plugins/woocommerce/src/Internal/Abilities/Domain/OrderNotesQuery.php
index 5a6ec652312..562e0f6eb14 100644
--- a/plugins/woocommerce/src/Internal/Abilities/Domain/OrderNotesQuery.php
+++ b/plugins/woocommerce/src/Internal/Abilities/Domain/OrderNotesQuery.php
@@ -103,20 +103,6 @@ class OrderNotesQuery extends AbstractDomainAbility implements AbilityDefinition
);
}
- /**
- * Check order read access for an ability input payload.
- *
- * @param mixed $input Ability input.
- * @return bool
- *
- * @since 11.3.0
- */
- public static function can_view_order( $input = array() ): bool {
- $order_id = self::get_id_from_input( $input );
-
- return $order_id > 0 && wc_rest_check_post_permissions( 'shop_order', 'read', $order_id );
- }
-
/**
* Get the ability input schema.
*
diff --git a/plugins/woocommerce/src/Internal/Abilities/Domain/Traits/OrderAbilityTrait.php b/plugins/woocommerce/src/Internal/Abilities/Domain/Traits/OrderAbilityTrait.php
index be62be7cc7e..fb3137600eb 100644
--- a/plugins/woocommerce/src/Internal/Abilities/Domain/Traits/OrderAbilityTrait.php
+++ b/plugins/woocommerce/src/Internal/Abilities/Domain/Traits/OrderAbilityTrait.php
@@ -241,6 +241,20 @@ trait OrderAbilityTrait {
return $order_id > 0 && self::check_order_permission( $order_id, 'edit' );
}
+ /**
+ * Check order read access for an ability input payload.
+ *
+ * @param mixed $input Ability input.
+ * @return bool
+ *
+ * @since 11.3.0
+ */
+ public static function can_view_order( $input = array() ): bool {
+ $order_id = self::get_id_from_input( $input );
+
+ return $order_id > 0 && self::check_order_permission( $order_id, 'read' );
+ }
+
/**
* Check a permission against the actual type of an order. An order whose
* type the abilities do not support fails. An ID with no order is checked
diff --git a/plugins/woocommerce/tests/php/src/Internal/Abilities/AbilitiesLoaderTest.php b/plugins/woocommerce/tests/php/src/Internal/Abilities/AbilitiesLoaderTest.php
index 7e769993e98..99266929d32 100644
--- a/plugins/woocommerce/tests/php/src/Internal/Abilities/AbilitiesLoaderTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/Abilities/AbilitiesLoaderTest.php
@@ -2156,6 +2156,67 @@ class AbilitiesLoaderTest extends \WC_Unit_Test_Case {
$this->assertSame( 'woocommerce_order_not_found', $result->get_error_code() );
}
+ /**
+ * @testdox Should check the order-notes-query permission against the actual type of the order ID.
+ */
+ public function test_order_notes_query_refuses_id_of_unsupported_order_type(): void {
+ $order = \WC_Helper_Order::create_order();
+ $this->created_order_ids[] = $order->get_id();
+ $refund = wc_create_refund(
+ array(
+ 'order_id' => $order->get_id(),
+ 'amount' => 1,
+ )
+ );
+
+ $result = wp_get_ability( 'woocommerce/order-notes-query' )->execute( array( 'id' => $refund->get_id() ) );
+
+ $this->assertWPError( $result );
+ $this->assertSame( 'ability_invalid_permissions', $result->get_error_code() );
+ }
+
+ /**
+ * @testdox Should check the order-notes-query permission against the capabilities of an order type that an extension registers.
+ */
+ public function test_order_notes_query_uses_the_capabilities_of_the_order_type(): void {
+ global $wc_order_types, $post_type_meta_caps, $wpdb;
+
+ $meta_caps = $post_type_meta_caps;
+ wc_register_order_type(
+ 'shop_test_private',
+ array(
+ 'capability_type' => 'shop_test_private',
+ 'exclude_from_order_views' => true,
+ 'add_order_meta_boxes' => false,
+ )
+ );
+ $order = \WC_Helper_Order::create_order();
+ $this->created_order_ids[] = $order->get_id();
+ $wpdb->update( $wpdb->posts, array( 'post_type' => 'shop_test_private' ), array( 'ID' => $order->get_id() ) );
+ $wpdb->update( OrdersTableDataStore::get_orders_table_name(), array( 'type' => 'shop_test_private' ), array( 'id' => $order->get_id() ) );
+ clean_post_cache( $order->get_id() );
+ wp_cache_flush();
+ wp_set_current_user( self::factory()->user->create( array( 'role' => 'shop_manager' ) ) );
+ $ability = wp_get_ability( 'woocommerce/order-notes-query' );
+
+ try {
+ $this->assertSame( 'shop_test_private', OrderUtil::get_order_type( $order->get_id() ) );
+
+ $refused = $ability->execute( array( 'id' => $order->get_id() ) );
+
+ $this->assertWPError( $refused );
+ $this->assertSame( 'ability_invalid_permissions', $refused->get_error_code() );
+
+ wp_get_current_user()->add_cap( 'read_private_shop_test_privates' );
+
+ $this->assertIsArray( $ability->execute( array( 'id' => $order->get_id() ) ) );
+ } finally {
+ unset( $wc_order_types['shop_test_private'] );
+ unregister_post_type( 'shop_test_private' );
+ $post_type_meta_caps = $meta_caps; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- unregister_post_type() removes the shop_order meta caps too.
+ }
+ }
+
/**
* @testdox Should exclude checkout-draft orders from default order queries.
*