From 643e40579396104d7c9915247bb838015ea746f9 Mon Sep 17 00:00:00 2001 From: Vedanshmini26 <97348795+Vedanshmini26@users.noreply.github.com> Date: Thu, 9 Apr 2026 16:04:21 +0530 Subject: [PATCH 1/7] Feature #64990: Add filtering support to wp_get_abilities() via \$args array --- src/wp-includes/abilities-api.php | 192 ++++++++++++++++-- ...s-wp-rest-abilities-v1-list-controller.php | 34 ++-- 2 files changed, 197 insertions(+), 29 deletions(-) diff --git a/src/wp-includes/abilities-api.php b/src/wp-includes/abilities-api.php index 73ba658f3f10d..76deefb1320ec 100644 --- a/src/wp-includes/abilities-api.php +++ b/src/wp-includes/abilities-api.php @@ -396,33 +396,201 @@ function wp_get_ability( string $name ): ?WP_Ability { } /** - * Retrieves all registered abilities. + * Retrieves registered abilities, optionally filtered by the given arguments. * - * Returns an array of all ability instances currently registered in the system. - * Use this for discovery, debugging, or building administrative interfaces. + * When called without arguments, returns all registered abilities. When called + * with an $args array, returns only abilities that match every specified condition. * - * Example: + * 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. + * 2. `match_callback` — per-item, caller-scoped. Return true to include, false to exclude. + * 3. `wp_get_abilities_match` filter — per-item, ecosystem-scoped. Plugins can enforce + * universal inclusion rules regardless of what the caller passed. + * 4. `result_callback` — on the full matched array, caller-scoped. Sort, slice, or reshape. + * 5. `wp_get_abilities_result` filter — on the full array, ecosystem-scoped. + * + * Steps 1–3 run inside a single loop over the registry — no extra iteration. + * + * Examples: * - * // Prints information about all available abilities. + * // All abilities (unchanged behaviour). * $abilities = wp_get_abilities(); - * foreach ( $abilities as $ability ) { - * echo $ability->get_label() . ': ' . $ability->get_description() . "\n"; - * } + * + * // 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' ) ); + * + * // Filter by meta. + * $abilities = wp_get_abilities( array( 'meta' => array( 'show_in_rest' => true ) ) ); + * + * // Combine filters (AND logic between arg types). + * $abilities = wp_get_abilities( array( + * 'category' => 'content', + * 'namespace' => 'core', + * 'meta' => array( 'show_in_rest' => true ), + * ) ); + * + * // Caller-scoped per-item callback. + * $abilities = wp_get_abilities( array( + * 'match_callback' => function ( WP_Ability $ability ) { + * return current_user_can( 'manage_options' ); + * }, + * ) ); + * + * // Caller-scoped result callback (sort + paginate). + * $abilities = wp_get_abilities( array( + * 'result_callback' => function ( array $abilities ) { + * usort( $abilities, fn( $a, $b ) => strcasecmp( $a->get_label(), $b->get_label() ) ); + * return array_slice( $abilities, 0, 10 ); + * }, + * ) ); * * @since 6.9.0 + * @since 7.1.0 Added the `$args` parameter for filtering support. * * @see WP_Abilities_Registry::get_all_registered() * - * @return WP_Ability[] An array of registered WP_Ability instances. Returns an empty - * array if no abilities are registered or if the registry is unavailable. + * @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 $namespace Filter by ability namespace prefix. Pass the namespace + * without a trailing slash, e.g. `'woocommerce'` matches + * `'woocommerce/create-order'`. + * @type array $meta Filter by meta key/value pairs. All conditions must + * match (AND logic). Supports nested arrays for structured + * meta, e.g. `array( 'mcp' => array( 'public' => true ) )`. + * @type callable $match_callback Optional. A callback invoked per ability after declarative + * filters. Receives a WP_Ability instance, returns bool. + * Return true to include, false to exclude. + * @type callable $result_callback Optional. A callback invoked once on the full matched + * array. Receives WP_Ability[], must return WP_Ability[]. + * Use for sorting, slicing, or reshaping the result. + * } + * @return WP_Ability[] An array of registered WP_Ability instances matching the given args. + * Returns an empty array if no abilities are registered, the registry is + * unavailable, or no abilities match the given args. */ -function wp_get_abilities(): array { +function wp_get_abilities( array $args = array() ): array { $registry = WP_Abilities_Registry::get_instance(); if ( null === $registry ) { return array(); } - return $registry->get_all_registered(); + $abilities = $registry->get_all_registered(); + + // Bail early when no filtering is requested. + if ( empty( $args ) ) { + return $abilities; + } + + $category = isset( $args['category'] ) ? (array) $args['category'] : array(); + $namespace = isset( $args['namespace'] ) && is_string( $args['namespace'] ) ? rtrim( $args['namespace'], '/' ) . '/' : ''; + $meta = isset( $args['meta'] ) && is_array( $args['meta'] ) ? $args['meta'] : array(); + $match_callback = isset( $args['match_callback'] ) && is_callable( $args['match_callback'] ) ? $args['match_callback'] : null; + $result_callback = isset( $args['result_callback'] ) && is_callable( $args['result_callback'] ) ? $args['result_callback'] : null; + + $matched = array(); + + foreach ( $abilities as $ability ) { + // Step 1a: Filter by category (OR logic within the arg). + if ( ! empty( $category ) && ! in_array( $ability->get_category(), $category, true ) ) { + continue; + } + + // Step 1b: Filter by namespace prefix. + if ( '' !== $namespace && ! str_starts_with( $ability->get_name(), $namespace ) ) { + continue; + } + + // Step 1c: Filter by meta key/value pairs (AND logic, supports nested arrays). + if ( ! empty( $meta ) && ! wp_get_abilities_match_meta( $ability->get_meta(), $meta ) ) { + continue; + } + + // Step 2: Caller-scoped per-item callback. + $include = true; + if ( null !== $match_callback ) { + $include = (bool) call_user_func( $match_callback, $ability ); + } + + /** + * Filters whether an individual ability should be included in the result set. + * + * Fires after the declarative filters and the caller-scoped match_callback. + * Plugins can use this to enforce universal inclusion rules regardless of + * what the caller passed in $args. + * + * @since 7.1.0 + * + * @param bool $include Whether to include the ability. Default true (after declarative filters pass). + * @param WP_Ability $ability The ability instance being evaluated. + * @param array $args The full $args array passed to wp_get_abilities(). + */ + $include = (bool) apply_filters( 'wp_get_abilities_match', $include, $ability, $args ); + + if ( $include ) { + $matched[] = $ability; + } + } + + // Step 4: Caller-scoped result callback. + if ( null !== $result_callback ) { + $matched = (array) call_user_func( $result_callback, $matched ); + } + + /** + * Filters the full list of matched abilities after all per-item filtering is complete. + * + * Fires after the caller-scoped result_callback. Plugins can use this to sort, + * paginate, or reshape the final result set universally. + * + * @since 7.1.0 + * + * @param WP_Ability[] $matched The matched abilities after all filtering. + * @param array $args The full $args array passed to wp_get_abilities(). + */ + return (array) apply_filters( 'wp_get_abilities_result', $matched, $args ); +} + +/** + * Checks whether an ability's meta array matches a set of required key/value conditions. + * + * All conditions must match (AND logic). Supports nested arrays for structured meta, + * e.g. `array( 'mcp' => array( 'public' => true ) )`. + * + * @since 7.1.0 + * @access private + * + * @param array $meta The ability's meta array. + * @param array $conditions The required key/value conditions to match against. + * @return bool True if all conditions match, false otherwise. + */ +function wp_get_abilities_match_meta( array $meta, array $conditions ): bool { + foreach ( $conditions as $key => $value ) { + if ( ! array_key_exists( $key, $meta ) ) { + return false; + } + + if ( is_array( $value ) ) { + if ( ! is_array( $meta[ $key ] ) || ! wp_get_abilities_match_meta( $meta[ $key ], $value ) ) { + return false; + } + } elseif ( $meta[ $key ] !== $value ) { + return false; + } + } + + return true; } /** diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-abilities-v1-list-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-abilities-v1-list-controller.php index e3ce0c4f2e03e..080d6c52ede34 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-abilities-v1-list-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-abilities-v1-list-controller.php @@ -86,26 +86,20 @@ public function register_routes(): void { * @return WP_REST_Response Response object on success. */ public function get_items( $request ) { - $abilities = array_filter( - wp_get_abilities(), - static function ( $ability ) { - return $ability->get_meta_item( 'show_in_rest' ); - } + $query_args = array( + 'meta' => array( 'show_in_rest' => true ), ); - // Filter by ability category if specified. - $category = $request['category']; - if ( ! empty( $category ) ) { - $abilities = array_filter( - $abilities, - static function ( $ability ) use ( $category ) { - return $ability->get_category() === $category; - } - ); - // Reset array keys after filtering. - $abilities = array_values( $abilities ); + if ( ! empty( $request['category'] ) ) { + $query_args['category'] = $request['category']; + } + + if ( ! empty( $request['namespace'] ) ) { + $query_args['namespace'] = $request['namespace']; } + $abilities = wp_get_abilities( $query_args ); + $page = $request['page']; $per_page = $request['per_page']; $offset = ( $page - 1 ) * $per_page; @@ -432,12 +426,18 @@ public function get_collection_params(): array { 'minimum' => 1, 'maximum' => 100, ), - 'category' => array( + 'category' => array( 'description' => __( 'Limit results to abilities in specific ability category.' ), 'type' => 'string', 'sanitize_callback' => 'sanitize_key', 'validate_callback' => 'rest_validate_request_arg', ), + 'namespace' => array( + 'description' => __( 'Limit results to abilities in a specific namespace.' ), + 'type' => 'string', + 'sanitize_callback' => 'sanitize_key', + 'validate_callback' => 'rest_validate_request_arg', + ), ); } } From 0e6f5cf03062eef71ab0aa2fd5a1b0dfb5351287 Mon Sep 17 00:00:00 2001 From: Vedanshmini26 <97348795+Vedanshmini26@users.noreply.github.com> Date: Mon, 13 Apr 2026 11:03:19 +0530 Subject: [PATCH 2/7] Update wp-api-generated.js fixture for namespace param in abilities endpoint --- tests/qunit/fixtures/wp-api-generated.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/qunit/fixtures/wp-api-generated.js b/tests/qunit/fixtures/wp-api-generated.js index 003dc397ae305..730ed29d3a799 100644 --- a/tests/qunit/fixtures/wp-api-generated.js +++ b/tests/qunit/fixtures/wp-api-generated.js @@ -12582,6 +12582,11 @@ mockedApiResponse.Schema = { "description": "Limit results to abilities in specific ability category.", "type": "string", "required": false + }, + "namespace": { + "description": "Limit results to abilities in a specific namespace.", + "type": "string", + "required": false } } } From 0350d8ead0ed4f028f3b5181b96593260442e5b1 Mon Sep 17 00:00:00 2001 From: Vedanshmini26 <97348795+Vedanshmini26@users.noreply.github.com> Date: Wed, 29 Apr 2026 11:18:46 +0530 Subject: [PATCH 3/7] Trigger CI From ba955089b4c9a18e675106c2993d0637c2901125 Mon Sep 17 00:00:00 2001 From: Vedanshmini26 <97348795+Vedanshmini26@users.noreply.github.com> Date: Wed, 29 Apr 2026 12:02:12 +0530 Subject: [PATCH 4/7] Tests/Feature #64990: Add unit tests for wp_get_abilities() filtering and rename private helper to _wp_get_abilities_match_meta - Rename wp_get_abilities_match_meta() to _wp_get_abilities_match_meta() per WordPress private function naming convention - Add tests/phpunit/tests/abilities-api/wpGetAbilities.php covering: - category filter (single string, array OR logic, non-existent) - namespace filter (prefix match, trailing-slash normalisation, non-existent) - meta filter (single key, AND logic across keys, nested arrays, missing key) - match_callback per-item inclusion/exclusion and argument passing - wp_get_abilities_match filter hook exclusion and argument passing - result_callback result reshaping and argument passing - wp_get_abilities_result filter hook reshaping and argument passing - Combined category + meta and namespace + match_callback filters --- src/wp-includes/abilities-api.php | 6 +- .../tests/abilities-api/wpGetAbilities.php | 685 ++++++++++++++++++ 2 files changed, 688 insertions(+), 3 deletions(-) create mode 100644 tests/phpunit/tests/abilities-api/wpGetAbilities.php diff --git a/src/wp-includes/abilities-api.php b/src/wp-includes/abilities-api.php index 76deefb1320ec..a5b894f47a4bf 100644 --- a/src/wp-includes/abilities-api.php +++ b/src/wp-includes/abilities-api.php @@ -513,7 +513,7 @@ function wp_get_abilities( array $args = array() ): array { } // Step 1c: Filter by meta key/value pairs (AND logic, supports nested arrays). - if ( ! empty( $meta ) && ! wp_get_abilities_match_meta( $ability->get_meta(), $meta ) ) { + if ( ! empty( $meta ) && ! _wp_get_abilities_match_meta( $ability->get_meta(), $meta ) ) { continue; } @@ -575,14 +575,14 @@ function wp_get_abilities( array $args = array() ): array { * @param array $conditions The required key/value conditions to match against. * @return bool True if all conditions match, false otherwise. */ -function wp_get_abilities_match_meta( array $meta, array $conditions ): bool { +function _wp_get_abilities_match_meta( array $meta, array $conditions ): bool { foreach ( $conditions as $key => $value ) { if ( ! array_key_exists( $key, $meta ) ) { return false; } if ( is_array( $value ) ) { - if ( ! is_array( $meta[ $key ] ) || ! wp_get_abilities_match_meta( $meta[ $key ], $value ) ) { + if ( ! is_array( $meta[ $key ] ) || ! _wp_get_abilities_match_meta( $meta[ $key ], $value ) ) { return false; } } elseif ( $meta[ $key ] !== $value ) { diff --git a/tests/phpunit/tests/abilities-api/wpGetAbilities.php b/tests/phpunit/tests/abilities-api/wpGetAbilities.php new file mode 100644 index 0000000000000..59993e93ed318 --- /dev/null +++ b/tests/phpunit/tests/abilities-api/wpGetAbilities.php @@ -0,0 +1,685 @@ + 'Math', + 'description' => 'Mathematical operations.', + ) + ); + wp_register_ability_category( + 'text', + array( + 'label' => 'Text', + 'description' => 'Text operations.', + ) + ); + } + + /** + * Tear down after each test. + */ + public function tear_down(): void { + foreach ( $this->registered_ability_names as $name ) { + wp_unregister_ability( $name ); + } + + $this->registered_ability_names = array(); + + wp_unregister_ability_category( 'math' ); + wp_unregister_ability_category( 'text' ); + + parent::tear_down(); + } + + /** + * Simulates the `wp_abilities_api_init` action. + */ + private function simulate_wp_abilities_init(): void { + global $wp_current_filter; + + $wp_current_filter[] = 'wp_abilities_api_init'; + } + + /** + * Registers a test ability and tracks its name for teardown. + * + * @param string $name The ability name. + * @param array $overrides Optional args to merge into the defaults. + * @return WP_Ability|null The registered ability, or null on failure. + */ + private function register_test_ability( string $name, array $overrides = array() ): ?WP_Ability { + $args = array_merge( + array( + 'label' => 'Test Ability', + 'description' => 'A test ability.', + 'category' => 'math', + 'input_schema' => array( + 'type' => 'object', + 'properties' => array(), + ), + 'output_schema' => array( + 'type' => 'object', + 'properties' => array(), + ), + 'execute_callback' => static function (): array { + return array(); + }, + 'permission_callback' => static function (): bool { + return true; + }, + 'meta' => array(), + ), + $overrides + ); + + $ability = wp_register_ability( $name, $args ); + + if ( null !== $ability ) { + $this->registered_ability_names[] = $name; + } + + return $ability; + } + + // ------------------------------------------------------------------------- + // Category filter + // ------------------------------------------------------------------------- + + /** + * Tests filtering by a single category string. + * + * @ticket 64990 + */ + public function test_filter_by_single_category(): 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' ) ); + + $result = wp_get_abilities( array( 'category' => 'math' ) ); + $names = array_map( + static function ( WP_Ability $a ) { + return $a->get_name(); + }, + $result + ); + + $this->assertContains( 'test/math-add', $names ); + $this->assertNotContains( 'test/text-upper', $names ); + } + + /** + * Tests that passing an array of categories uses OR logic. + * + * @ticket 64990 + */ + public function test_filter_by_category_array_uses_or_logic(): 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' ) ); + + $result = wp_get_abilities( array( 'category' => array( 'math', 'text' ) ) ); + $names = array_map( + static function ( WP_Ability $a ) { + return $a->get_name(); + }, + $result + ); + + $this->assertContains( 'test/math-add', $names ); + $this->assertContains( 'test/text-upper', $names ); + } + + /** + * Tests that filtering by a non-existent category returns an empty array. + * + * @ticket 64990 + */ + public function test_filter_by_nonexistent_category_returns_empty(): void { + $this->simulate_wp_abilities_init(); + + $this->register_test_ability( 'test/math-add', array( 'category' => 'math' ) ); + + $result = wp_get_abilities( array( 'category' => 'nonexistent' ) ); + + $this->assertSame( array(), $result ); + } + + // ------------------------------------------------------------------------- + // Namespace filter + // ------------------------------------------------------------------------- + + /** + * Tests filtering by namespace prefix. + * + * @ticket 64990 + */ + public function test_filter_by_namespace(): void { + $this->simulate_wp_abilities_init(); + + $this->register_test_ability( 'test/ability-one' ); + $this->register_test_ability( 'other/ability-two', array( 'category' => 'text' ) ); + + $result = wp_get_abilities( array( 'namespace' => 'test' ) ); + $names = array_map( + static function ( WP_Ability $a ) { + return $a->get_name(); + }, + $result + ); + + $this->assertContains( 'test/ability-one', $names ); + $this->assertNotContains( 'other/ability-two', $names ); + } + + /** + * Tests that a namespace with a trailing slash produces the same result as one without. + * + * @ticket 64990 + */ + public function test_filter_by_namespace_trailing_slash_is_normalized(): void { + $this->simulate_wp_abilities_init(); + + $this->register_test_ability( 'test/ability-one' ); + + $get_names = static function ( array $abilities ): array { + return array_map( + static function ( WP_Ability $a ) { + return $a->get_name(); + }, + $abilities + ); + }; + $names_without_slash = $get_names( wp_get_abilities( array( 'namespace' => 'test' ) ) ); + $names_with_slash = $get_names( wp_get_abilities( array( 'namespace' => 'test/' ) ) ); + + $this->assertSame( $names_without_slash, $names_with_slash ); + } + + /** + * Tests that a non-matching namespace returns an empty array. + * + * @ticket 64990 + */ + public function test_filter_by_nonexistent_namespace_returns_empty(): void { + $this->simulate_wp_abilities_init(); + + $this->register_test_ability( 'test/ability-one' ); + + $result = wp_get_abilities( array( 'namespace' => 'nonexistent' ) ); + + $this->assertSame( array(), $result ); + } + + // ------------------------------------------------------------------------- + // Meta filter + // ------------------------------------------------------------------------- + + /** + * Tests filtering by a single meta key/value pair. + * + * @ticket 64990 + */ + public function test_filter_by_meta_single_key(): void { + $this->simulate_wp_abilities_init(); + + $this->register_test_ability( + 'test/ability-rest', + array( 'meta' => array( 'show_in_rest' => true ) ) + ); + $this->register_test_ability( + 'test/ability-no-rest', + array( 'meta' => array( 'show_in_rest' => false ) ) + ); + + $result = wp_get_abilities( array( 'meta' => array( 'show_in_rest' => true ) ) ); + $names = array_map( + static function ( WP_Ability $a ) { + return $a->get_name(); + }, + $result + ); + + $this->assertContains( 'test/ability-rest', $names ); + $this->assertNotContains( 'test/ability-no-rest', $names ); + } + + /** + * Tests that multiple meta conditions use AND logic. + * + * @ticket 64990 + */ + public function test_filter_by_meta_multiple_keys_uses_and_logic(): void { + $this->simulate_wp_abilities_init(); + + $this->register_test_ability( + 'test/ability-both', + array( 'meta' => array( 'show_in_rest' => true, 'public' => true ) ) + ); + $this->register_test_ability( + 'test/ability-one-key', + array( 'meta' => array( 'show_in_rest' => true, 'public' => false ) ) + ); + + $result = wp_get_abilities( + array( 'meta' => array( 'show_in_rest' => true, 'public' => true ) ) + ); + $names = array_map( + static function ( WP_Ability $a ) { + return $a->get_name(); + }, + $result + ); + + $this->assertContains( 'test/ability-both', $names ); + $this->assertNotContains( 'test/ability-one-key', $names ); + } + + /** + * Tests filtering by nested meta arrays. + * + * @ticket 64990 + */ + public function test_filter_by_nested_meta(): void { + $this->simulate_wp_abilities_init(); + + $this->register_test_ability( + 'test/ability-public-mcp', + array( 'meta' => array( 'mcp' => array( 'public' => true ) ) ) + ); + $this->register_test_ability( + 'test/ability-private-mcp', + array( 'meta' => array( 'mcp' => array( 'public' => false ) ) ) + ); + + $result = wp_get_abilities( array( 'meta' => array( 'mcp' => array( 'public' => true ) ) ) ); + $names = array_map( + static function ( WP_Ability $a ) { + return $a->get_name(); + }, + $result + ); + + $this->assertContains( 'test/ability-public-mcp', $names ); + $this->assertNotContains( 'test/ability-private-mcp', $names ); + } + + /** + * Tests that an ability without the required meta key is excluded. + * + * @ticket 64990 + */ + public function test_filter_by_missing_meta_key_excludes_ability(): void { + $this->simulate_wp_abilities_init(); + + $this->register_test_ability( 'test/ability-no-meta', array( 'meta' => array() ) ); + + $result = wp_get_abilities( array( 'meta' => array( 'show_in_rest' => true ) ) ); + $names = array_map( + static function ( WP_Ability $a ) { + return $a->get_name(); + }, + $result + ); + + $this->assertNotContains( 'test/ability-no-meta', $names ); + } + + // ------------------------------------------------------------------------- + // match_callback + // ------------------------------------------------------------------------- + + /** + * Tests that match_callback can include or exclude abilities per item. + * + * @ticket 64990 + */ + public function test_match_callback_filters_per_item(): void { + $this->simulate_wp_abilities_init(); + + $this->register_test_ability( 'test/ability-alpha' ); + $this->register_test_ability( 'test/ability-beta' ); + + $result = wp_get_abilities( + array( + 'match_callback' => static function ( WP_Ability $ability ): bool { + return 'test/ability-alpha' === $ability->get_name(); + }, + ) + ); + $names = array_map( + static function ( WP_Ability $a ) { + return $a->get_name(); + }, + $result + ); + + $this->assertContains( 'test/ability-alpha', $names ); + $this->assertNotContains( 'test/ability-beta', $names ); + } + + /** + * Tests that match_callback returning false for all abilities yields an empty result. + * + * @ticket 64990 + */ + public function test_match_callback_returning_false_yields_empty_result(): void { + $this->simulate_wp_abilities_init(); + + $this->register_test_ability( 'test/ability-one' ); + + $result = wp_get_abilities( + array( + 'match_callback' => static function ( WP_Ability $ability ): bool { + return false; + }, + ) + ); + + $this->assertSame( array(), $result ); + } + + /** + * Tests that match_callback receives a WP_Ability instance. + * + * @ticket 64990 + */ + public function test_match_callback_receives_ability_instance(): void { + $this->simulate_wp_abilities_init(); + + $this->register_test_ability( 'test/ability-one' ); + + $received = null; + wp_get_abilities( + array( + 'namespace' => 'test', + 'match_callback' => static function ( WP_Ability $ability ) use ( &$received ): bool { + $received = $ability; + return true; + }, + ) + ); + + $this->assertInstanceOf( WP_Ability::class, $received ); + $this->assertSame( 'test/ability-one', $received->get_name() ); + } + + // ------------------------------------------------------------------------- + // wp_get_abilities_match filter hook + // ------------------------------------------------------------------------- + + /** + * Tests that wp_get_abilities_match filter can exclude an ability. + * + * @ticket 64990 + */ + public function test_wp_get_abilities_match_filter_can_exclude_ability(): void { + $this->simulate_wp_abilities_init(); + + $this->register_test_ability( 'test/ability-one' ); + $this->register_test_ability( 'test/ability-two' ); + + $filter = static function ( bool $include, WP_Ability $ability ): bool { + return 'test/ability-two' !== $ability->get_name(); + }; + + add_filter( 'wp_get_abilities_match', $filter, 10, 2 ); + $result = wp_get_abilities( array( 'namespace' => 'test' ) ); + remove_filter( 'wp_get_abilities_match', $filter, 10 ); + + $names = array_map( + static function ( WP_Ability $a ) { + return $a->get_name(); + }, + $result + ); + + $this->assertContains( 'test/ability-one', $names ); + $this->assertNotContains( 'test/ability-two', $names ); + } + + /** + * Tests that wp_get_abilities_match filter receives the ability and original args. + * + * @ticket 64990 + */ + public function test_wp_get_abilities_match_filter_receives_ability_and_args(): void { + $this->simulate_wp_abilities_init(); + + $this->register_test_ability( 'test/ability-one' ); + + $received_ability = null; + $received_args = null; + $query_args = array( 'namespace' => 'test' ); + + $filter = static function ( + bool $include, + WP_Ability $ability, + array $args + ) use ( &$received_ability, &$received_args ): bool { + $received_ability = $ability; + $received_args = $args; + return $include; + }; + + add_filter( 'wp_get_abilities_match', $filter, 10, 3 ); + wp_get_abilities( $query_args ); + remove_filter( 'wp_get_abilities_match', $filter, 10 ); + + $this->assertInstanceOf( WP_Ability::class, $received_ability ); + $this->assertSame( $query_args, $received_args ); + } + + // ------------------------------------------------------------------------- + // result_callback + // ------------------------------------------------------------------------- + + /** + * Tests that result_callback receives the full array of matched abilities. + * + * @ticket 64990 + */ + public function test_result_callback_receives_matched_abilities(): void { + $this->simulate_wp_abilities_init(); + + $this->register_test_ability( 'test/ability-one' ); + $this->register_test_ability( 'test/ability-two' ); + + $received = null; + wp_get_abilities( + array( + 'namespace' => 'test', + 'result_callback' => static function ( array $abilities ) use ( &$received ): array { + $received = $abilities; + return $abilities; + }, + ) + ); + + $this->assertIsArray( $received ); + $this->assertCount( 2, $received ); + $this->assertContainsOnlyInstancesOf( WP_Ability::class, $received ); + } + + /** + * Tests that result_callback can reshape the result array. + * + * @ticket 64990 + */ + public function test_result_callback_can_reshape_result(): void { + $this->simulate_wp_abilities_init(); + + $this->register_test_ability( 'test/ability-one' ); + $this->register_test_ability( 'test/ability-two' ); + + $result = wp_get_abilities( + array( + 'namespace' => 'test', + 'result_callback' => static function ( array $abilities ): array { + return array_slice( $abilities, 0, 1 ); + }, + ) + ); + + $this->assertCount( 1, $result ); + } + + // ------------------------------------------------------------------------- + // wp_get_abilities_result filter hook + // ------------------------------------------------------------------------- + + /** + * Tests that wp_get_abilities_result filter can reshape the final result. + * + * @ticket 64990 + */ + public function test_wp_get_abilities_result_filter_can_reshape_result(): void { + $this->simulate_wp_abilities_init(); + + $this->register_test_ability( 'test/ability-one' ); + $this->register_test_ability( 'test/ability-two' ); + + $filter = static function ( array $abilities ): array { + return array_slice( $abilities, 0, 1 ); + }; + + add_filter( 'wp_get_abilities_result', $filter ); + $result = wp_get_abilities( array( 'namespace' => 'test' ) ); + remove_filter( 'wp_get_abilities_result', $filter ); + + $this->assertCount( 1, $result ); + } + + /** + * Tests that wp_get_abilities_result filter receives the matched abilities and original args. + * + * @ticket 64990 + */ + public function test_wp_get_abilities_result_filter_receives_abilities_and_args(): void { + $this->simulate_wp_abilities_init(); + + $this->register_test_ability( 'test/ability-one' ); + + $received_abilities = null; + $received_args = null; + $query_args = array( 'namespace' => 'test' ); + + $filter = static function ( + array $abilities, + array $args + ) use ( &$received_abilities, &$received_args ): array { + $received_abilities = $abilities; + $received_args = $args; + return $abilities; + }; + + add_filter( 'wp_get_abilities_result', $filter, 10, 2 ); + wp_get_abilities( $query_args ); + remove_filter( 'wp_get_abilities_result', $filter, 10 ); + + $this->assertIsArray( $received_abilities ); + $this->assertSame( $query_args, $received_args ); + } + + // ------------------------------------------------------------------------- + // Combined filters + // ------------------------------------------------------------------------- + + /** + * Tests that category and meta filters are combined with AND logic between them. + * + * @ticket 64990 + */ + public function test_combined_category_and_meta_filters(): void { + $this->simulate_wp_abilities_init(); + + $this->register_test_ability( + 'test/math-rest', + array( 'category' => 'math', 'meta' => array( 'show_in_rest' => true ) ) + ); + $this->register_test_ability( + 'test/math-no-rest', + array( 'category' => 'math', 'meta' => array( 'show_in_rest' => false ) ) + ); + $this->register_test_ability( + 'test/text-rest', + array( 'category' => 'text', 'meta' => array( 'show_in_rest' => true ) ) + ); + + $result = wp_get_abilities( + array( + 'category' => 'math', + 'meta' => array( 'show_in_rest' => true ), + ) + ); + $names = array_map( + static function ( WP_Ability $a ) { + return $a->get_name(); + }, + $result + ); + + $this->assertContains( 'test/math-rest', $names ); + $this->assertNotContains( 'test/math-no-rest', $names ); + $this->assertNotContains( 'test/text-rest', $names ); + } + + /** + * Tests that namespace and match_callback filters are applied together. + * + * @ticket 64990 + */ + public function test_combined_namespace_and_match_callback_filters(): void { + $this->simulate_wp_abilities_init(); + + $this->register_test_ability( 'test/ability-alpha' ); + $this->register_test_ability( 'test/ability-beta' ); + $this->register_test_ability( 'other/ability-gamma', array( 'category' => 'text' ) ); + + $result = wp_get_abilities( + array( + 'namespace' => 'test', + 'match_callback' => static function ( WP_Ability $ability ): bool { + return 'test/ability-alpha' === $ability->get_name(); + }, + ) + ); + $names = array_map( + static function ( WP_Ability $a ) { + return $a->get_name(); + }, + $result + ); + + $this->assertContains( 'test/ability-alpha', $names ); + $this->assertNotContains( 'test/ability-beta', $names ); + $this->assertNotContains( 'other/ability-gamma', $names ); + } +} From 38fec64ee90d3a9ab68c6d0c5f050c1fca509696 Mon Sep 17 00:00:00 2001 From: Vedanshmini26 <97348795+Vedanshmini26@users.noreply.github.com> Date: Wed, 29 Apr 2026 12:07:44 +0530 Subject: [PATCH 5/7] Tests #64990: Fix PHPCS - expand multi-key inline arrays and multi-variable use declarations to one-per-line --- .../tests/abilities-api/wpGetAbilities.php | 41 +++++++++++++++---- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/tests/phpunit/tests/abilities-api/wpGetAbilities.php b/tests/phpunit/tests/abilities-api/wpGetAbilities.php index 59993e93ed318..e7f18e53ef412 100644 --- a/tests/phpunit/tests/abilities-api/wpGetAbilities.php +++ b/tests/phpunit/tests/abilities-api/wpGetAbilities.php @@ -281,15 +281,30 @@ public function test_filter_by_meta_multiple_keys_uses_and_logic(): void { $this->register_test_ability( 'test/ability-both', - array( 'meta' => array( 'show_in_rest' => true, 'public' => true ) ) + array( + 'meta' => array( + 'show_in_rest' => true, + 'public' => true, + ), + ) ); $this->register_test_ability( 'test/ability-one-key', - array( 'meta' => array( 'show_in_rest' => true, 'public' => false ) ) + array( + 'meta' => array( + 'show_in_rest' => true, + 'public' => false, + ), + ) ); $result = wp_get_abilities( - array( 'meta' => array( 'show_in_rest' => true, 'public' => true ) ) + array( + 'meta' => array( + 'show_in_rest' => true, + 'public' => true, + ), + ) ); $names = array_map( static function ( WP_Ability $a ) { @@ -483,7 +498,10 @@ public function test_wp_get_abilities_match_filter_receives_ability_and_args(): bool $include, WP_Ability $ability, array $args - ) use ( &$received_ability, &$received_args ): bool { + ) use ( + &$received_ability, + &$received_args + ): bool { $received_ability = $ability; $received_args = $args; return $include; @@ -622,15 +640,24 @@ public function test_combined_category_and_meta_filters(): void { $this->register_test_ability( 'test/math-rest', - array( 'category' => 'math', 'meta' => array( 'show_in_rest' => true ) ) + array( + 'category' => 'math', + 'meta' => array( 'show_in_rest' => true ), + ) ); $this->register_test_ability( 'test/math-no-rest', - array( 'category' => 'math', 'meta' => array( 'show_in_rest' => false ) ) + array( + 'category' => 'math', + 'meta' => array( 'show_in_rest' => false ), + ) ); $this->register_test_ability( 'test/text-rest', - array( 'category' => 'text', 'meta' => array( 'show_in_rest' => true ) ) + array( + 'category' => 'text', + 'meta' => array( 'show_in_rest' => true ), + ) ); $result = wp_get_abilities( From 4a58fdd01bafce0aa417bcf288986cf57015e4d3 Mon Sep 17 00:00:00 2001 From: Vedanshmini26 <97348795+Vedanshmini26@users.noreply.github.com> Date: Wed, 29 Apr 2026 12:10:41 +0530 Subject: [PATCH 6/7] Tests #64990: Fix PHPCS - split multi-variable use declaration to one-per-line in result filter test --- tests/phpunit/tests/abilities-api/wpGetAbilities.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/phpunit/tests/abilities-api/wpGetAbilities.php b/tests/phpunit/tests/abilities-api/wpGetAbilities.php index e7f18e53ef412..3e61e03ddf22a 100644 --- a/tests/phpunit/tests/abilities-api/wpGetAbilities.php +++ b/tests/phpunit/tests/abilities-api/wpGetAbilities.php @@ -612,7 +612,10 @@ public function test_wp_get_abilities_result_filter_receives_abilities_and_args( $filter = static function ( array $abilities, array $args - ) use ( &$received_abilities, &$received_args ): array { + ) use ( + &$received_abilities, + &$received_args + ): array { $received_abilities = $abilities; $received_args = $args; return $abilities; From 19a528e5f263b0998fc534686349f02b02a7492b Mon Sep 17 00:00:00 2001 From: Vedanshmini26 <97348795+Vedanshmini26@users.noreply.github.com> Date: Wed, 29 Apr 2026 12:12:39 +0530 Subject: [PATCH 7/7] Tests #64990: Fix PHPCS - rename reserved keyword $include to $should_include in filter callbacks --- tests/phpunit/tests/abilities-api/wpGetAbilities.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/phpunit/tests/abilities-api/wpGetAbilities.php b/tests/phpunit/tests/abilities-api/wpGetAbilities.php index 3e61e03ddf22a..6d583249d1e92 100644 --- a/tests/phpunit/tests/abilities-api/wpGetAbilities.php +++ b/tests/phpunit/tests/abilities-api/wpGetAbilities.php @@ -461,7 +461,7 @@ public function test_wp_get_abilities_match_filter_can_exclude_ability(): void { $this->register_test_ability( 'test/ability-one' ); $this->register_test_ability( 'test/ability-two' ); - $filter = static function ( bool $include, WP_Ability $ability ): bool { + $filter = static function ( bool $should_include, WP_Ability $ability ): bool { return 'test/ability-two' !== $ability->get_name(); }; @@ -495,7 +495,7 @@ public function test_wp_get_abilities_match_filter_receives_ability_and_args(): $query_args = array( 'namespace' => 'test' ); $filter = static function ( - bool $include, + bool $should_include, WP_Ability $ability, array $args ) use ( @@ -504,7 +504,7 @@ public function test_wp_get_abilities_match_filter_receives_ability_and_args(): ): bool { $received_ability = $ability; $received_args = $args; - return $include; + return $should_include; }; add_filter( 'wp_get_abilities_match', $filter, 10, 3 );