Skip to content

Commit be9e9a4

Browse files
gzioloclaude
andcommitted
Abilities: Address review feedback for ability deprecation
- Support `true` as a shorthand for the `deprecated` meta filter in `wp_get_abilities()`, so REST API and PHP callers can request only deprecated abilities. - Validate that the deprecation `replacement` detail is a namespaced ability name. - Handle the `deprecated_ability_run` action while serving REST API requests. The notice becomes an `X-WP-DeprecatedAbility` header instead of corrupting the JSON response when `WP_DEBUG` is enabled. - Document the `deprecated` collection parameter details in the REST API schema. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent fd8cbce commit be9e9a4

9 files changed

Lines changed: 129 additions & 9 deletions

File tree

src/wp-includes/abilities-api.php

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -539,9 +539,10 @@ function _deprecated_ability( string $ability_name, string $version = '', string
539539
* 'meta' => array( 'deprecated' => false ),
540540
* ) );
541541
*
542-
* // Return only deprecated abilities.
542+
* // Return only deprecated abilities. Passing `true` matches any deprecated
543+
* // ability. An array of details narrows the results further.
543544
* $abilities = wp_get_abilities( array(
544-
* 'meta' => array( 'deprecated' => array() ),
545+
* 'meta' => array( 'deprecated' => true ),
545546
* ) );
546547
*
547548
* // Filter by category.
@@ -624,6 +625,15 @@ function wp_get_abilities( array $args = array() ): array {
624625
$item_include_callback = isset( $args['item_include_callback'] ) && is_callable( $args['item_include_callback'] ) ? $args['item_include_callback'] : null;
625626
$result_callback = isset( $args['result_callback'] ) && is_callable( $args['result_callback'] ) ? $args['result_callback'] : null;
626627

628+
/*
629+
* Normalize the `deprecated` meta filter shorthand. Stored values are `false`
630+
* or an array of details, so `true` becomes an empty set of conditions that
631+
* matches any deprecated ability.
632+
*/
633+
if ( isset( $meta['deprecated'] ) && true === $meta['deprecated'] ) {
634+
$meta['deprecated'] = array();
635+
}
636+
627637
$matched = array();
628638

629639
foreach ( $abilities as $name => $ability ) {

src/wp-includes/abilities-api/class-wp-ability.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,12 @@ protected function prepare_properties( array $args ): array {
401401
);
402402
}
403403

404+
if ( 'replacement' === $key && ! preg_match( '/^[a-z0-9-]+\/[a-z0-9-]+$/', $args['meta']['deprecated'][ $key ] ) ) {
405+
throw new InvalidArgumentException(
406+
__( 'The ability deprecation `replacement` value should be a namespaced ability name, i.e. "my-plugin/my-ability". It can only contain lowercase alphanumeric characters, dashes and the forward slash.' )
407+
);
408+
}
409+
404410
$has_deprecation_details = true;
405411
}
406412

src/wp-includes/rest-api.php

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,8 @@ function rest_api_default_filters() {
244244
add_filter( 'deprecated_function_trigger_error', '__return_false' );
245245
add_action( 'deprecated_argument_run', 'rest_handle_deprecated_argument', 10, 3 );
246246
add_filter( 'deprecated_argument_trigger_error', '__return_false' );
247+
add_action( 'deprecated_ability_run', 'rest_handle_deprecated_ability', 10, 4 );
248+
add_filter( 'deprecated_ability_trigger_error', '__return_false' );
247249
add_action( 'doing_it_wrong_run', 'rest_handle_doing_it_wrong', 10, 3 );
248250
add_filter( 'doing_it_wrong_trigger_error', '__return_false' );
249251
}
@@ -767,6 +769,42 @@ function rest_handle_deprecated_argument( $function_name, $message, $version ) {
767769
header( sprintf( 'X-WP-DeprecatedParam: %s', $string ) );
768770
}
769771

772+
/**
773+
* Handles _deprecated_ability() errors.
774+
*
775+
* @since 7.2.0
776+
*
777+
* @param string $ability_name The ability that was executed.
778+
* @param string $replacement The ability that should be used as a replacement.
779+
* @param string $version The version of the ability provider that deprecated the ability.
780+
* @param string $message Additional migration guidance.
781+
*/
782+
function rest_handle_deprecated_ability( $ability_name, $replacement, $version, $message ) {
783+
if ( ! WP_DEBUG || headers_sent() ) {
784+
return;
785+
}
786+
787+
if ( $version && $replacement ) {
788+
/* translators: 1: Ability name, 2: Version number, 3: Alternative ability name. */
789+
$string = sprintf( __( '%1$s (since %2$s; use %3$s instead)' ), $ability_name, $version, $replacement );
790+
} elseif ( $version ) {
791+
/* translators: 1: Ability name, 2: Version number. */
792+
$string = sprintf( __( '%1$s (since %2$s; no alternative available)' ), $ability_name, $version );
793+
} elseif ( $replacement ) {
794+
/* translators: 1: Ability name, 2: Alternative ability name. */
795+
$string = sprintf( __( '%1$s (use %2$s instead)' ), $ability_name, $replacement );
796+
} else {
797+
/* translators: %s: Ability name. */
798+
$string = sprintf( __( '%s (no alternative available)' ), $ability_name );
799+
}
800+
801+
if ( $message ) {
802+
$string .= ' ' . $message;
803+
}
804+
805+
header( sprintf( 'X-WP-DeprecatedAbility: %s', $string ) );
806+
}
807+
770808
/**
771809
* Handles _doing_it_wrong errors.
772810
*

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

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -408,17 +408,20 @@ public function get_collection_params(): array {
408408
'additionalProperties' => true,
409409
),
410410
'deprecated' => array(
411-
'description' => __( 'Limit results by deprecation status or details.' ),
411+
'description' => __( 'Limit results by deprecation status or details. Use true to return only deprecated abilities, false to return only active abilities, or an object to match specific deprecation details.' ),
412412
'type' => array( 'boolean', 'object' ),
413413
'properties' => array(
414414
'since' => array(
415-
'type' => 'string',
415+
'description' => __( 'Version of the ability provider that deprecated the ability.' ),
416+
'type' => 'string',
416417
),
417418
'replacement' => array(
418-
'type' => 'string',
419+
'description' => __( 'Namespaced ability to use instead.' ),
420+
'type' => 'string',
419421
),
420422
'message' => array(
421-
'type' => 'string',
423+
'description' => __( 'Additional migration guidance.' ),
424+
'type' => 'string',
422425
),
423426
),
424427
'additionalProperties' => true,

tests/phpunit/includes/abstract-testcase.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -760,8 +760,6 @@ public function setExpectedException( $exception, $message = '', $code = null )
760760
*/
761761
public function deprecated_function_run( $function_name, $replacement, $version, $message = '' ) {
762762
if ( ! isset( $this->caught_deprecated[ $function_name ] ) ) {
763-
$additional_message = $message;
764-
765763
switch ( current_action() ) {
766764
case 'deprecated_function_run':
767765
if ( $replacement ) {
@@ -849,6 +847,8 @@ public function deprecated_function_run( $function_name, $replacement, $version,
849847
break;
850848

851849
case 'deprecated_ability_run':
850+
$additional_message = $message;
851+
852852
if ( $version ) {
853853
if ( $replacement ) {
854854
$message = sprintf(

tests/phpunit/tests/abilities-api/wpAbility.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,21 @@ public function test_meta_deprecated_rejects_invalid_detail(): void {
471471
new WP_Ability( self::$test_ability_name, $args );
472472
}
473473

474+
/**
475+
* Tests that the `replacement` detail must be a namespaced ability name.
476+
*
477+
* @ticket 64209
478+
*/
479+
public function test_meta_deprecated_rejects_invalid_replacement(): void {
480+
$args = self::$test_ability_properties;
481+
$args['meta']['deprecated'] = array( 'replacement' => 'Not An Ability' );
482+
483+
$this->expectException( InvalidArgumentException::class );
484+
$this->expectExceptionMessage( 'The ability deprecation `replacement` value should be a namespaced ability name' );
485+
486+
new WP_Ability( self::$test_ability_name, $args );
487+
}
488+
474489
/**
475490
* Tests that executing a deprecated ability emits its structured details.
476491
*

tests/phpunit/tests/abilities-api/wpGetAbilities.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,30 @@ public function test_deprecated_abilities_can_be_filtered_exclusively(): void {
195195
$this->assertArrayHasKey( 'test/deprecated-ability', $result );
196196
}
197197

198+
/**
199+
* Tests that `true` works as a shorthand for any deprecated ability.
200+
*
201+
* @ticket 64209
202+
*/
203+
public function test_deprecated_abilities_can_be_filtered_with_true_shorthand(): void {
204+
$this->simulate_wp_abilities_init();
205+
206+
$this->register_test_ability( 'test/active-ability' );
207+
$this->register_test_ability(
208+
'test/deprecated-ability',
209+
array(
210+
'meta' => array(
211+
'deprecated' => array( 'replacement' => 'test/active-ability' ),
212+
),
213+
)
214+
);
215+
216+
$result = wp_get_abilities( array( 'meta' => array( 'deprecated' => true ) ) );
217+
218+
$this->assertArrayNotHasKey( 'test/active-ability', $result );
219+
$this->assertArrayHasKey( 'test/deprecated-ability', $result );
220+
}
221+
198222
// -------------------------------------------------------------------------
199223
// Category filter
200224
// -------------------------------------------------------------------------

tests/phpunit/tests/rest-api/wpRestAbilitiesV1ListController.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,27 @@ public function test_get_items_can_exclude_deprecated_abilities(): void {
384384
$this->assertNotContains( 'test/deprecated-calculator', wp_list_pluck( $response->get_data(), 'name' ) );
385385
}
386386

387+
/**
388+
* Tests that collection discovery can return only deprecated abilities.
389+
*
390+
* @ticket 64209
391+
*/
392+
public function test_get_items_can_filter_only_deprecated_abilities(): void {
393+
$this->register_deprecated_ability();
394+
395+
$request = new WP_REST_Request( 'GET', '/wp-abilities/v1/abilities' );
396+
$request->set_param( 'per_page', 100 );
397+
$request->set_param( 'meta', array( 'deprecated' => 'true' ) );
398+
$response = $this->server->dispatch( $request );
399+
400+
$this->assertSame( 200, $response->get_status() );
401+
402+
$ability_names = wp_list_pluck( $response->get_data(), 'name' );
403+
404+
$this->assertContains( 'test/deprecated-calculator', $ability_names );
405+
$this->assertNotContains( 'test/calculator', $ability_names );
406+
}
407+
387408
/**
388409
* Test getting a specific ability.
389410
*

tests/qunit/fixtures/wp-api-generated.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12743,19 +12743,22 @@ mockedApiResponse.Schema = {
1274312743
"additionalProperties": true
1274412744
},
1274512745
"deprecated": {
12746-
"description": "Limit results by deprecation status or details.",
12746+
"description": "Limit results by deprecation status or details. Use true to return only deprecated abilities, false to return only active abilities, or an object to match specific deprecation details.",
1274712747
"type": [
1274812748
"boolean",
1274912749
"object"
1275012750
],
1275112751
"properties": {
1275212752
"since": {
12753+
"description": "Version of the ability provider that deprecated the ability.",
1275312754
"type": "string"
1275412755
},
1275512756
"replacement": {
12757+
"description": "Namespaced ability to use instead.",
1275612758
"type": "string"
1275712759
},
1275812760
"message": {
12761+
"description": "Additional migration guidance.",
1275912762
"type": "string"
1276012763
}
1276112764
},

0 commit comments

Comments
 (0)