Repository navigation
feat: Add support for Ability Deprecation #13852
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: trunk
Are you sure you want to change the base?
Changes from all commits
7f76eea
4d118f9
9607ae3
910cf50
763010d
712d0fd
f878eee
575c8f0
344ceb3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -229,9 +229,21 @@ | |
| * 'show_in_rest' => false, | ||
| * ), | ||
| * | ||
| * Mark an ability as deprecated while keeping exact-name retrieval and execution | ||
| * available for backward compatibility: | ||
| * | ||
| * 'meta' => array( | ||
| * 'deprecated' => array( | ||
| * 'since' => '2.0.0', | ||
| * 'replacement' => 'my-plugin/new-ability', | ||
| * 'message' => __( 'The replacement supports the new data format.', 'my-plugin' ), | ||
| * ), | ||
| * ), | ||
| * | ||
| * @since 6.9.0 | ||
| * @since 7.1.0 Added the `public` meta argument. | ||
| * @since 7.2.0 The `category` argument is now optional and defaults to `uncategorized`. | ||
| * @since 7.2.0 Added the `deprecated` meta property. | ||
| * @since 7.2.0 Added the `required_capabilities` argument. | ||
| * | ||
| * @see WP_Abilities_Registry::register() | ||
|
|
@@ -288,6 +300,15 @@ | |
| * clients such as the REST API, MCP, or AI agents. Seeds | ||
| * the default for per-channel flags like `$show_in_rest`. | ||
| * Defaults to false. | ||
| * @type null|array<string, string> $deprecated { | ||
| * Optional. Deprecation details. If set, mark the ability as deprecated. Deprecated abilities are hidden | ||
| * from discovery by default, but can be retrieved by exact name or explicitly included in discovery | ||
| * with the `include_deprecated` argument. Default null. | ||
| * | ||
| * @type string $since Version of the ability provider that deprecated the ability. | ||
| * @type string $replacement Optional. Namespaced ability to use instead. | ||
| * @type string $message Optional. Additional migration guidance. | ||
| * } | ||
| * @type bool $show_in_rest Optional. Whether to expose this ability in the REST API. | ||
| * When true, the ability can be invoked via HTTP requests. | ||
| * Default is the value of `$public` when set, false otherwise. | ||
|
|
@@ -424,13 +445,13 @@ function wp_get_ability( string $name ): ?WP_Ability { | |
| /** | ||
| * Retrieves registered abilities, optionally filtered by the given arguments. | ||
| * | ||
| * When called without arguments, returns all registered abilities. When called | ||
| * with an $args array, returns only abilities that match every specified condition. | ||
| * When called without arguments, returns all registered abilities that are not deprecated. When called with an $args | ||
| * array, returns only abilities that match every specified condition. | ||
| * | ||
| * Filtering pipeline (executed in order): | ||
| * | ||
| * 1. Declarative filters (`category`, `namespace`, `meta`) — per-item, AND logic between | ||
| * arg types. | ||
| * 1. Declarative filters ( `category`, `include_deprecated`, `namespace`, `meta`) - per-item, AND logic between 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. | ||
|
|
@@ -441,9 +462,19 @@ function wp_get_ability( string $name ): ?WP_Ability { | |
| * | ||
| * Examples: | ||
| * | ||
| * // All abilities (unchanged behaviour). | ||
| * // All (non-deprecated) abilities. | ||
| * $abilities = wp_get_abilities(); | ||
| * | ||
| * // All abilities, including deprecated ones. | ||
| * $abilities = wp_get_abilities( array( | ||
| * 'include_deprecated' => true, | ||
| * ) ); | ||
| * | ||
| * // Only deprecated abilities. An empty array matches any deprecation details. | ||
| * $abilities = wp_get_abilities( array( | ||
| * 'meta' => array( 'deprecated' => array() ), | ||
| * ) ); | ||
| * | ||
| * // Filter by category. | ||
| * $abilities = wp_get_abilities( array( 'category' => 'content' ) ); | ||
| * | ||
|
|
@@ -483,21 +514,27 @@ function wp_get_ability( string $name ): ?WP_Ability { | |
| * | ||
| * @since 6.9.0 | ||
| * @since 7.1.0 Added the `$args` parameter for filtering support. | ||
| * @since 7.2.0 Deprecated abilities are excluded by default. Added the `include_deprecated` argument. | ||
| * @since 7.2.0 Added the `has_required_capabilities` argument. | ||
| * | ||
| * @see WP_Abilities_Registry::get_all_registered() | ||
| * | ||
| * @param array $args { | ||
| * Optional. Arguments to filter the returned abilities. Default empty array (returns all). | ||
| * | ||
| * @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'`. | ||
| * @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 ) )`. | ||
| * Optional. Arguments to filter the returned abilities. Default empty array (returns all abilities | ||
| * that are not deprecated). | ||
| * | ||
| * @type string $category Filter by category slug. Only abilities whose category | ||
| * exactly matches the given slug are included. | ||
| * @type bool $include_deprecated Whether to include deprecated abilities. Deprecated abilities | ||
| * retrieved this way do not trigger deprecation notices until | ||
| * they are executed. Default false. | ||
| * @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 ) )`. | ||
| * A `deprecated` condition implies `include_deprecated`. | ||
| * @type bool $has_required_capabilities Whether to leave out abilities whose required capabilities | ||
| * the current user doesn't have. Abilities without required | ||
| * capabilities are always included. Permission callbacks | ||
|
|
@@ -523,27 +560,33 @@ function wp_get_abilities( array $args = array() ): array { | |
|
|
||
| $abilities = $registry->get_all_registered(); | ||
|
|
||
| $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(); | ||
| $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(); | ||
| $include_deprecated = ! empty( $args['include_deprecated'] ) || array_key_exists( 'deprecated', $meta ); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could we support if ( true === $meta['deprecated'] ) {
$meta['deprecated'] = array();
} |
||
| $has_required_capabilities = ! empty( $args['has_required_capabilities'] ); | ||
| $item_include_callback = isset( $args['item_include_callback'] ) && is_callable( $args['item_include_callback'] ) ? $args['item_include_callback'] : null; | ||
| $result_callback = isset( $args['result_callback'] ) && is_callable( $args['result_callback'] ) ? $args['result_callback'] : null; | ||
| $item_include_callback = isset( $args['item_include_callback'] ) && is_callable( $args['item_include_callback'] ) ? $args['item_include_callback'] : null; | ||
| $result_callback = isset( $args['result_callback'] ) && is_callable( $args['result_callback'] ) ? $args['result_callback'] : null; | ||
|
|
||
| $matched = array(); | ||
|
|
||
| foreach ( $abilities as $name => $ability ) { | ||
| // Step 1a: Filter by category. | ||
| // Step 1a: Exclude deprecated abilities unless requested via `include_deprecated` or a `deprecated` meta condition. | ||
| if ( ! $include_deprecated && null !== $ability->get_meta_item( 'deprecated' ) ) { | ||
| continue; | ||
| } | ||
|
|
||
| // Step 1b: Filter by category. | ||
| if ( '' !== $category && $ability->get_category() !== $category ) { | ||
| continue; | ||
| } | ||
|
|
||
| // Step 1b: Filter by namespace prefix. | ||
| // Step 1c: 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). | ||
| // Step 1d: Filter by meta key/value pairs (AND logic, supports nested arrays). | ||
| if ( ! empty( $meta ) && ! _wp_get_abilities_match_meta( $ability->get_meta(), $meta ) ) { | ||
| continue; | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -42,6 +42,7 @@ final class WP_Abilities_Registry { | |
| * @since 6.9.0 | ||
| * @since 7.1.0 Added the `public` meta argument. | ||
| * @since 7.2.0 The `category` argument is now optional and defaults to `uncategorized`. | ||
| * @since 7.2.0 Added the `deprecated` meta property. | ||
| * @since 7.2.0 Added the `required_capabilities` argument. | ||
| * | ||
| * @see wp_register_ability() | ||
|
|
@@ -82,6 +83,15 @@ final class WP_Abilities_Registry { | |
| * to clients such as the REST API, MCP, or AI agents. | ||
| * Seeds the default for per-channel flags like | ||
| * `$show_in_rest`. Defaults to false. | ||
| * @type null|array<string, string> $deprecated { | ||
| * Optional. Deprecation details. If set, mark the ability as deprecated. Deprecated abilities are hidden | ||
| * from discovery by default, but can be retrieved by exact name or explicitly included in discovery | ||
| * with the `include_deprecated` argument. Default null. | ||
| * | ||
| * @type string $since Version of the ability provider that deprecated the ability. | ||
| * @type string $replacement Optional. Namespaced ability to use instead. | ||
| * @type string $message Optional. Additional migration guidance. | ||
| * } | ||
| * @type bool $show_in_rest Optional. Whether to expose this ability in the REST API. | ||
| * Default is the value of `$public` when set, false otherwise. | ||
| * } | ||
|
|
@@ -119,6 +129,7 @@ public function register( string $name, array $args ): ?WP_Ability { | |
| * @since 6.9.0 | ||
| * @since 7.1.0 Added the `public` meta argument. | ||
| * @since 7.2.0 The `category` argument is now optional and defaults to `uncategorized`. | ||
| * @since 7.2.0 Added the `deprecated` meta property. | ||
| * @since 7.2.0 Added the `required_capabilities` argument. | ||
| * | ||
| * @param array<string, mixed> $args { | ||
|
|
@@ -145,6 +156,15 @@ public function register( string $name, array $args ): ?WP_Ability { | |
| * available to clients such as the REST API, MCP, or AI | ||
| * agents. Seeds the default for per-channel flags like | ||
| * `$show_in_rest`. Defaults to false. | ||
| * @type null|array<string, string> $deprecated { | ||
| * Optional. Deprecation details. If set, mark the ability as deprecated. Deprecated abilities are hidden | ||
| * from discovery by default, but can be retrieved by exact name or explicitly included in discovery | ||
| * with the `include_deprecated` argument. Default null. | ||
| * | ||
| * @type string $since Version of the ability provider that deprecated the ability. | ||
| * @type string $replacement Optional. Namespaced ability to use instead. | ||
| * @type string $message Optional. Additional migration guidance. | ||
| * } | ||
| * @type bool $show_in_rest Optional. Whether to expose this ability in the REST API. | ||
| * Default is the value of `$public` when set, false otherwise. | ||
| * } | ||
|
|
@@ -277,6 +297,7 @@ public function is_registered( string $name ): bool { | |
| * Do not use this method directly. Instead, use the `wp_get_ability()` function. | ||
| * | ||
| * @since 6.9.0 | ||
| * @since 7.2.0 Added deprecation notices for abilities with the `deprecated` meta property. | ||
| * | ||
| * @see wp_get_ability() | ||
| * | ||
|
|
@@ -293,7 +314,13 @@ public function get_registered( string $name ): ?WP_Ability { | |
| ); | ||
| return null; | ||
| } | ||
| return $this->registered_abilities[ $name ]; | ||
|
|
||
| $ability = $this->registered_abilities[ $name ]; | ||
|
|
||
| // Check if the ability is deprecated and handle it accordingly. | ||
| $ability->_handle_ability_deprecation(); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The run controller calls wp_get_ability three times per request (permission check, input sanitization and execute), so the deprecated_ability_run action and the REST header handler run three times for a single POST to the run endpoint. Should we only trigger the notice on execute, or also apply the "once per lifecycle" logic to the action and not just to the error?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (My gut reaction without having the source code open in front of me is to fix the run controller in a separate PR to reuse the same instance. Will look at this with fresh eyes in the morning) |
||
|
|
||
| return $ability; | ||
| } | ||
|
|
||
| /** | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nitpick: there is an extra space after the opening parenthesis, and this line uses a hyphen while the other steps keep the em dash.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
great eyes!!