Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 6 additions & 10 deletions src/wp-includes/abilities-api.php
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,7 @@ function wp_get_ability( string $name ): ?WP_Ability {
* Filtering pipeline (executed in order):
*
* 1. Declarative filters (`category`, `namespace`, `meta`) — per-item, AND logic between
* arg types, OR logic within multi-value `category` arrays.
* arg types.
* 2. `item_include_callback` — per-item, caller-scoped. Return true to include, false to exclude.
* 3. `wp_get_abilities_item_include` filter — per-item, ecosystem-scoped. Plugins can enforce
* universal inclusion rules regardless of what the caller passed.
Expand All @@ -421,9 +421,6 @@ function wp_get_ability( string $name ): ?WP_Ability {
* // Filter by category.
* $abilities = wp_get_abilities( array( 'category' => 'content' ) );
*
* // Filter by multiple categories (OR logic).
* $abilities = wp_get_abilities( array( 'category' => array( 'content', 'settings' ) ) );
*
* // Filter by namespace.
* $abilities = wp_get_abilities( array( 'namespace' => 'woocommerce' ) );
*
Expand Down Expand Up @@ -466,9 +463,8 @@ function wp_get_ability( string $name ): ?WP_Ability {
* @param array $args {
* Optional. Arguments to filter the returned abilities. Default empty array (returns all).
*
* @type string|string[] $category Filter by category slug. A single string or an array of
* slugs — abilities matching any of the given slugs are
* included (OR logic within this arg type).
* @type string $category Filter by category slug. Only abilities whose category
* exactly matches the given slug are included.
* @type string $namespace Filter by ability namespace prefix. Pass the namespace
* without a trailing slash, e.g. `'woocommerce'` matches
* `'woocommerce/create-order'`.
Expand All @@ -495,7 +491,7 @@ function wp_get_abilities( array $args = array() ): array {

$abilities = $registry->get_all_registered();

$category = isset( $args['category'] ) ? (array) $args['category'] : array();
$category = isset( $args['category'] ) && is_string( $args['category'] ) ? $args['category'] : '';
$namespace = isset( $args['namespace'] ) && is_string( $args['namespace'] ) ? rtrim( $args['namespace'], '/' ) . '/' : '';
$meta = isset( $args['meta'] ) && is_array( $args['meta'] ) ? $args['meta'] : array();
$item_include_callback = isset( $args['item_include_callback'] ) && is_callable( $args['item_include_callback'] ) ? $args['item_include_callback'] : null;
Expand All @@ -504,8 +500,8 @@ function wp_get_abilities( array $args = array() ): array {
$matched = array();

foreach ( $abilities as $name => $ability ) {
// Step 1a: Filter by category (OR logic within the arg).
if ( ! empty( $category ) && ! in_array( $ability->get_category(), $category, true ) ) {
// Step 1a: Filter by category.
if ( '' !== $category && $ability->get_category() !== $category ) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

did you verify that the logic remains the same?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned in the ticket and the description of this PR, this is an anticipated change to align with the current handling of namespace and category through the REST API. I committed the initial implementation during this release cycle, so there’s still room for refinement in the implementation.

continue;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,11 @@ public function get_items( $request ) {
$query_args['namespace'] = $request['namespace'];
}

if ( ! empty( $request['meta'] ) ) {
// Merge caller meta first so the forced show_in_rest filter wins. This keeps a caller from using meta to reveal abilities hidden from REST.
$query_args['meta'] = array_merge( $request['meta'], $query_args['meta'] );
}

$abilities = wp_get_abilities( $query_args );

$page = $request['page'];
Expand Down Expand Up @@ -447,9 +452,23 @@ public function get_item_schema(): array {
'type' => 'object',
'properties' => array(
'annotations' => array(
'description' => __( 'Annotations for the ability.' ),
'type' => array( 'boolean', 'null' ),
'default' => null,
'description' => __( 'Behavioral annotations for the ability.' ),
'type' => 'object',
'properties' => array(
'readonly' => array(
'description' => __( 'Whether the ability does not modify its environment.' ),
'type' => array( 'boolean', 'null' ),
),
'destructive' => array(
'description' => __( 'Whether the ability may perform destructive updates to its environment.' ),
'type' => array( 'boolean', 'null' ),
),
'idempotent' => array(
'description' => __( 'Whether repeated calls with the same arguments have no additional effect.' ),
'type' => array( 'boolean', 'null' ),
),
),
'additionalProperties' => true,
),
),
'context' => array( 'view', 'edit' ),
Expand All @@ -469,7 +488,7 @@ public function get_item_schema(): array {
* @return array<string, mixed> Collection parameters.
*/
public function get_collection_params(): array {
return array(
$query_params = array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
'page' => array(
'description' => __( 'Current page of the collection.' ),
Expand All @@ -496,6 +515,46 @@ public function get_collection_params(): array {
'sanitize_callback' => 'sanitize_key',
'validate_callback' => 'rest_validate_request_arg',
),
'meta' => array(
'description' => __( 'Limit results to abilities matching all of the given meta fields.' ),
'type' => 'object',
'properties' => array(
// show_in_rest is omitted on purpose. It is forced on and cannot be filtered by a caller.
'annotations' => array(
'description' => __( 'Limit results to abilities matching the given behavioral annotations.' ),
'type' => 'object',
'properties' => array(
'readonly' => array(
'description' => __( 'Whether the ability does not modify its environment.' ),
'type' => array( 'boolean', 'null' ),
),
'destructive' => array(
'description' => __( 'Whether the ability may perform destructive updates to its environment.' ),
'type' => array( 'boolean', 'null' ),
),
'idempotent' => array(
'description' => __( 'Whether repeated calls with the same arguments have no additional effect.' ),
'type' => array( 'boolean', 'null' ),
),
),
'additionalProperties' => true,
),
),
'additionalProperties' => true,
),
);

/**
* Filters REST API collection parameters for the abilities controller.
*
* Use this to declare the schema type of a custom meta key. A declared
* type lets REST coerce a query-string value, for example "true" to a
* boolean, before the meta filter matches it.
*
* @since 7.1.0
*
* @param array $query_params JSON Schema-formatted collection parameters.
*/
return apply_filters( 'rest_abilities_collection_params', $query_params );
}
}
17 changes: 15 additions & 2 deletions tests/phpunit/tests/abilities-api/wpGetAbilities.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@ public function set_up(): void {
'description' => 'Text operations.',
)
);
wp_register_ability_category(
'media',
array(
'label' => 'Media',
'description' => 'Media operations.',
)
);
}

/**
Expand All @@ -53,6 +60,7 @@ public function tear_down(): void {

wp_unregister_ability_category( 'math' );
wp_unregister_ability_category( 'text' );
wp_unregister_ability_category( 'media' );

parent::tear_down();
}
Expand Down Expand Up @@ -135,15 +143,19 @@ static function ( WP_Ability $a ) {
}

/**
* Tests that passing an array of categories uses OR logic.
* Tests that a non-string category is ignored rather than treated as a multi-value filter.
*
* The declarative filters accept a single slug. Anything other than a string is ignored,
* matching how the `namespace` and `meta` args guard their own types.
*
* @ticket 64990
*/
public function test_filter_by_category_array_uses_or_logic(): void {
public function test_filter_by_non_string_category_is_ignored(): void {
$this->simulate_wp_abilities_init();

$this->register_test_ability( 'test/math-add', array( 'category' => 'math' ) );
$this->register_test_ability( 'test/text-upper', array( 'category' => 'text' ) );
$this->register_test_ability( 'test/media-crop', array( 'category' => 'media' ) );

$result = wp_get_abilities( array( 'category' => array( 'math', 'text' ) ) );
$names = array_map(
Expand All @@ -155,6 +167,7 @@ static function ( WP_Ability $a ) {

$this->assertContains( 'test/math-add', $names );
$this->assertContains( 'test/text-upper', $names );
$this->assertContains( 'test/media-crop', $names );
}

/**
Expand Down
Loading
Loading