Skip to content

Commit 01debdf

Browse files
committed
Abilities API: Add filtering support to wp_get_abilities()
Extends `wp_get_abilities()` with an optional `$args` array, giving callers a shared primitive for filtering registered abilities by `category`, `namespace`, or `meta`. Two callback slots — `item_include_callback` (per ability) and `result_callback` (on the full matched array) — round out the caller-scoped pipeline. Two new filters, `wp_get_abilities_item_include` and `wp_get_abilities_result`, expose ecosystem-scoped extension points so plugins can participate in ability resolution without monkey-patching call sites. This replaces the ad-hoc `array_filter` passes that consumers (the REST list controller, the MCP adapter, WooCommerce) had each implemented independently. The REST list controller now delegates to the new primitive instead of running its own post-retrieval filtering, and gains a `namespace` query parameter alongside the existing `category` filter. Called without arguments, `wp_get_abilities()` behaves exactly as before — no backward compatibility break. Props sheldorofazeroth, gziolo. Fixes #64990. git-svn-id: https://develop.svn.wordpress.org/trunk@62420 602fd350-edb4-49c9-b593-d223f7449a82
1 parent 9765876 commit 01debdf

5 files changed

Lines changed: 1047 additions & 32 deletions

File tree

src/wp-includes/abilities-api.php

Lines changed: 182 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -396,33 +396,203 @@ function wp_get_ability( string $name ): ?WP_Ability {
396396
}
397397

398398
/**
399-
* Retrieves all registered abilities.
399+
* Retrieves registered abilities, optionally filtered by the given arguments.
400400
*
401-
* Returns an array of all ability instances currently registered in the system.
402-
* Use this for discovery, debugging, or building administrative interfaces.
401+
* When called without arguments, returns all registered abilities. When called
402+
* with an $args array, returns only abilities that match every specified condition.
403403
*
404-
* Example:
404+
* Filtering pipeline (executed in order):
405+
*
406+
* 1. Declarative filters (`category`, `namespace`, `meta`) — per-item, AND logic between
407+
* arg types, OR logic within multi-value `category` arrays.
408+
* 2. `item_include_callback` — per-item, caller-scoped. Return true to include, false to exclude.
409+
* 3. `wp_get_abilities_item_include` filter — per-item, ecosystem-scoped. Plugins can enforce
410+
* universal inclusion rules regardless of what the caller passed.
411+
* 4. `result_callback` — on the full matched array, caller-scoped. Sort, slice, or reshape.
412+
* 5. `wp_get_abilities_result` filter — on the full array, ecosystem-scoped.
413+
*
414+
* Steps 1–3 run inside a single loop over the registry — no extra iteration.
405415
*
406-
* // Prints information about all available abilities.
416+
* Examples:
417+
*
418+
* // All abilities (unchanged behaviour).
407419
* $abilities = wp_get_abilities();
408-
* foreach ( $abilities as $ability ) {
409-
* echo $ability->get_label() . ': ' . $ability->get_description() . "\n";
410-
* }
420+
*
421+
* // Filter by category.
422+
* $abilities = wp_get_abilities( array( 'category' => 'content' ) );
423+
*
424+
* // Filter by multiple categories (OR logic).
425+
* $abilities = wp_get_abilities( array( 'category' => array( 'content', 'settings' ) ) );
426+
*
427+
* // Filter by namespace.
428+
* $abilities = wp_get_abilities( array( 'namespace' => 'woocommerce' ) );
429+
*
430+
* // Filter by meta.
431+
* $abilities = wp_get_abilities( array( 'meta' => array( 'show_in_rest' => true ) ) );
432+
*
433+
* // Combine filters (AND logic between arg types).
434+
* $abilities = wp_get_abilities( array(
435+
* 'category' => 'content',
436+
* 'namespace' => 'core',
437+
* 'meta' => array( 'show_in_rest' => true ),
438+
* ) );
439+
*
440+
* // Caller-scoped per-item callback.
441+
* $abilities = wp_get_abilities( array(
442+
* 'item_include_callback' => function ( WP_Ability $ability ) {
443+
* return current_user_can( 'manage_options' );
444+
* },
445+
* ) );
446+
*
447+
* // Caller-scoped result callback (sort + paginate).
448+
* $abilities = wp_get_abilities( array(
449+
* 'result_callback' => function ( array $abilities ) {
450+
* usort( $abilities, fn( $a, $b ) => strcasecmp( $a->get_label(), $b->get_label() ) );
451+
* return array_slice( $abilities, 0, 10 );
452+
* },
453+
* ) );
454+
*
455+
* The pipeline always runs, even when called with no arguments. This ensures that the
456+
* `wp_get_abilities_item_include` and `wp_get_abilities_result` filters always fire,
457+
* giving plugins a reliable place to enforce universal inclusion or shaping rules.
458+
* For raw, unfiltered registry data that bypasses the filter pipeline entirely, use
459+
* {@see WP_Abilities_Registry::get_all_registered()} directly.
411460
*
412461
* @since 6.9.0
462+
* @since 7.1.0 Added the `$args` parameter for filtering support.
413463
*
414464
* @see WP_Abilities_Registry::get_all_registered()
415465
*
416-
* @return WP_Ability[] An array of registered WP_Ability instances. Returns an empty
417-
* array if no abilities are registered or if the registry is unavailable.
466+
* @param array $args {
467+
* Optional. Arguments to filter the returned abilities. Default empty array (returns all).
468+
*
469+
* @type string|string[] $category Filter by category slug. A single string or an array of
470+
* slugs — abilities matching any of the given slugs are
471+
* included (OR logic within this arg type).
472+
* @type string $namespace Filter by ability namespace prefix. Pass the namespace
473+
* without a trailing slash, e.g. `'woocommerce'` matches
474+
* `'woocommerce/create-order'`.
475+
* @type array $meta Filter by meta key/value pairs. All conditions must
476+
* match (AND logic). Supports nested arrays for structured
477+
* meta, e.g. `array( 'mcp' => array( 'public' => true ) )`.
478+
* @type callable $item_include_callback Optional. A callback invoked per ability after declarative
479+
* filters. Receives a WP_Ability instance, returns bool.
480+
* Return true to include, false to exclude.
481+
* @type callable $result_callback Optional. A callback invoked once on the full matched
482+
* array. Receives WP_Ability[], must return WP_Ability[].
483+
* Use for sorting, slicing, or reshaping the result.
484+
* }
485+
* @return WP_Ability[] An array of registered WP_Ability instances matching the given args,
486+
* keyed by ability name. Returns an empty array if no abilities are
487+
* registered, the registry is unavailable, or no abilities match the
488+
* given args.
418489
*/
419-
function wp_get_abilities(): array {
490+
function wp_get_abilities( array $args = array() ): array {
420491
$registry = WP_Abilities_Registry::get_instance();
421492
if ( null === $registry ) {
422493
return array();
423494
}
424495

425-
return $registry->get_all_registered();
496+
$abilities = $registry->get_all_registered();
497+
498+
$category = isset( $args['category'] ) ? (array) $args['category'] : array();
499+
$namespace = isset( $args['namespace'] ) && is_string( $args['namespace'] ) ? rtrim( $args['namespace'], '/' ) . '/' : '';
500+
$meta = isset( $args['meta'] ) && is_array( $args['meta'] ) ? $args['meta'] : array();
501+
$item_include_callback = isset( $args['item_include_callback'] ) && is_callable( $args['item_include_callback'] ) ? $args['item_include_callback'] : null;
502+
$result_callback = isset( $args['result_callback'] ) && is_callable( $args['result_callback'] ) ? $args['result_callback'] : null;
503+
504+
$matched = array();
505+
506+
foreach ( $abilities as $name => $ability ) {
507+
// Step 1a: Filter by category (OR logic within the arg).
508+
if ( ! empty( $category ) && ! in_array( $ability->get_category(), $category, true ) ) {
509+
continue;
510+
}
511+
512+
// Step 1b: Filter by namespace prefix.
513+
if ( '' !== $namespace && ! str_starts_with( $ability->get_name(), $namespace ) ) {
514+
continue;
515+
}
516+
517+
// Step 1c: Filter by meta key/value pairs (AND logic, supports nested arrays).
518+
if ( ! empty( $meta ) && ! _wp_get_abilities_match_meta( $ability->get_meta(), $meta ) ) {
519+
continue;
520+
}
521+
522+
// Step 2: Caller-scoped per-item callback.
523+
$include = true;
524+
if ( null !== $item_include_callback ) {
525+
$include = (bool) call_user_func( $item_include_callback, $ability );
526+
}
527+
528+
/**
529+
* Filters whether an individual ability should be included in the result set.
530+
*
531+
* Fires after the declarative filters and the caller-scoped item_include_callback.
532+
* Plugins can use this to enforce universal inclusion rules regardless of
533+
* what the caller passed in $args.
534+
*
535+
* @since 7.1.0
536+
*
537+
* @param bool $include Whether to include the ability. Default true (after declarative filters pass).
538+
* @param WP_Ability $ability The ability instance being evaluated.
539+
* @param array $args The full $args array passed to wp_get_abilities().
540+
*/
541+
$include = (bool) apply_filters( 'wp_get_abilities_item_include', $include, $ability, $args );
542+
543+
if ( $include ) {
544+
$matched[ $name ] = $ability;
545+
}
546+
}
547+
548+
// Step 4: Caller-scoped result callback.
549+
if ( null !== $result_callback ) {
550+
$matched = (array) call_user_func( $result_callback, $matched );
551+
}
552+
553+
/**
554+
* Filters the full list of matched abilities after all per-item filtering is complete.
555+
*
556+
* Fires after the caller-scoped result_callback. Plugins can use this to sort,
557+
* paginate, or reshape the final result set universally.
558+
*
559+
* @since 7.1.0
560+
*
561+
* @param WP_Ability[] $matched The matched abilities after all filtering.
562+
* @param array $args The full $args array passed to wp_get_abilities().
563+
*/
564+
return (array) apply_filters( 'wp_get_abilities_result', $matched, $args );
565+
}
566+
567+
/**
568+
* Checks whether an ability's meta array matches a set of required key/value conditions.
569+
*
570+
* All conditions must match (AND logic). Supports nested arrays for structured meta,
571+
* e.g. `array( 'mcp' => array( 'public' => true ) )`.
572+
*
573+
* @since 7.1.0
574+
* @access private
575+
*
576+
* @param array $meta The ability's meta array.
577+
* @param array $conditions The required key/value conditions to match against.
578+
* @return bool True if all conditions match, false otherwise.
579+
*/
580+
function _wp_get_abilities_match_meta( array $meta, array $conditions ): bool {
581+
foreach ( $conditions as $key => $value ) {
582+
if ( ! array_key_exists( $key, $meta ) ) {
583+
return false;
584+
}
585+
586+
if ( is_array( $value ) ) {
587+
if ( ! is_array( $meta[ $key ] ) || ! _wp_get_abilities_match_meta( $meta[ $key ], $value ) ) {
588+
return false;
589+
}
590+
} elseif ( $meta[ $key ] !== $value ) {
591+
return false;
592+
}
593+
}
594+
595+
return true;
426596
}
427597

428598
/**

src/wp-includes/rest-api/endpoints/class-wp-rest-abilities-v1-list-controller.php

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -86,26 +86,20 @@ public function register_routes(): void {
8686
* @return WP_REST_Response Response object on success.
8787
*/
8888
public function get_items( $request ) {
89-
$abilities = array_filter(
90-
wp_get_abilities(),
91-
static function ( $ability ) {
92-
return $ability->get_meta_item( 'show_in_rest' );
93-
}
89+
$query_args = array(
90+
'meta' => array( 'show_in_rest' => true ),
9491
);
9592

96-
// Filter by ability category if specified.
97-
$category = $request['category'];
98-
if ( ! empty( $category ) ) {
99-
$abilities = array_filter(
100-
$abilities,
101-
static function ( $ability ) use ( $category ) {
102-
return $ability->get_category() === $category;
103-
}
104-
);
105-
// Reset array keys after filtering.
106-
$abilities = array_values( $abilities );
93+
if ( ! empty( $request['category'] ) ) {
94+
$query_args['category'] = $request['category'];
95+
}
96+
97+
if ( ! empty( $request['namespace'] ) ) {
98+
$query_args['namespace'] = $request['namespace'];
10799
}
108100

101+
$abilities = wp_get_abilities( $query_args );
102+
109103
$page = $request['page'];
110104
$per_page = $request['per_page'];
111105
$offset = ( $page - 1 ) * $per_page;
@@ -418,26 +412,32 @@ public function get_item_schema(): array {
418412
*/
419413
public function get_collection_params(): array {
420414
return array(
421-
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
422-
'page' => array(
415+
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
416+
'page' => array(
423417
'description' => __( 'Current page of the collection.' ),
424418
'type' => 'integer',
425419
'default' => 1,
426420
'minimum' => 1,
427421
),
428-
'per_page' => array(
422+
'per_page' => array(
429423
'description' => __( 'Maximum number of items to be returned in result set.' ),
430424
'type' => 'integer',
431425
'default' => 50,
432426
'minimum' => 1,
433427
'maximum' => 100,
434428
),
435-
'category' => array(
429+
'category' => array(
436430
'description' => __( 'Limit results to abilities in specific ability category.' ),
437431
'type' => 'string',
438432
'sanitize_callback' => 'sanitize_key',
439433
'validate_callback' => 'rest_validate_request_arg',
440434
),
435+
'namespace' => array(
436+
'description' => __( 'Limit results to abilities in a specific namespace.' ),
437+
'type' => 'string',
438+
'sanitize_callback' => 'sanitize_key',
439+
'validate_callback' => 'rest_validate_request_arg',
440+
),
441441
);
442442
}
443443
}

0 commit comments

Comments
 (0)