@@ -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/**
0 commit comments