mirror of
https://github.com/WordPress/WordPress.git
synced 2026-06-19 07:37:07 +00:00
Abilities API: Introduce server-side registry and REST API endpoints
Feature proposal at https://make.wordpress.org/ai/2025/07/17/abilities-api/. Project developed in https://github.com/WordPress/abilities-api. Introduces a new Abilities API that allows WordPress plugins and themes to register and execute custom abilities with built-in permission checking, input/output validation via JSON Schema, and REST API integration. ## Public Functions ### Ability Management - `wp_register_ability( string $name, array $args ): ?WP_Ability` - Registers a new ability (must be called on `wp_abilities_api_init` hook) - `wp_unregister_ability( string $name ): ?WP_Ability` - Unregisters an ability - `wp_has_ability( string $name ): bool` - Checks if an ability is registered - `wp_get_ability( string $name ): ?WP_Ability` - Retrieves a registered ability - `wp_get_abilities(): array` - Retrieves all registered abilities ### Ability Category Management - `wp_register_ability_category( string $slug, array $args ): ?WP_Ability_Category` - Registers an ability category (must be called on `wp_abilities_api_categories_init` hook) - `wp_unregister_ability_category( string $slug ): ?WP_Ability_Category` - Unregisters an ability category - `wp_has_ability_category( string $slug ): bool` - Checks if an ability category is registered - `wp_get_ability_category( string $slug ): ?WP_Ability_Category` - Retrieves a registered ability category - `wp_get_ability_categories(): array` - Retrieves all registered ability categories ## Public Classes - `WP_Ability` - Encapsulates ability properties and methods (execute, check_permission, validate_input, etc.) - `WP_Ability_Category` - Encapsulates ability category properties - `WP_Abilities_Registry` - Manages ability registration and lookup (private, accessed via functions) - `WP_Ability_Categories_Registry` - Manages ability category registration (private, accessed via functions) - `WP_REST_Abilities_V1_List_Controller` - REST controller for listing abilities - `WP_REST_Abilities_V1_Run_Controller` - REST controller for executing abilities ## REST API Endpoints ### Namespace: `wp-abilities/v1` #### List Abilities - `GET /wp-abilities/v1/abilities` - Retrieve all registered abilities - Query parameters: `page`, `per_page`, `category` #### Get Single Ability - `GET /wp-abilities/v1/abilities/(?P<name>[a-zA-Z0-9\-\/]+)` - Retrieve a specific ability by name #### Execute Ability - `GET|POST|DELETE /wp-abilities/v1/abilities/(?P<name>[a-zA-Z0-9\-\/]+)/run` - Execute an ability - Supports multiple HTTP methods based on ability annotations - Validates input against ability's input schema - Validates output against ability's output schema - Performs permission checks via ability's permission callback ## Hooks ### Actions - `wp_abilities_api_categories_init` - Fired when ability categories registry is initialized (register categories here) - `wp_abilities_api_init` - Fired when abilities registry is initialized (register abilities here) - `wp_before_execute_ability` - Fired before an ability gets executed, after input validation and permissions check - `wp_after_execute_ability` - Fires immediately after an ability finished executing ### Filters - `wp_register_ability_category_args` - Filters ability category arguments before registration - `wp_register_ability_args` - Filters ability arguments before registration Developed in https://github.com/WordPress/wordpress-develop/pull/9410. Props gziolo, jorbin, justlevine, westonruter, jason_the_adams, flixos90, karmatosed, timothyblynjacobs. Fixes #64098. Built from https://develop.svn.wordpress.org/trunk@61032 git-svn-id: http://core.svn.wordpress.org/trunk@60368 1a063a9b-81f0-0310-95a4-ce76da25c4cd
This commit is contained in:
@@ -0,0 +1,260 @@
|
||||
<?php
|
||||
/**
|
||||
* Abilities API
|
||||
*
|
||||
* Defines functions for managing abilities in WordPress.
|
||||
*
|
||||
* @package WordPress
|
||||
* @subpackage Abilities_API
|
||||
* @since 6.9.0
|
||||
*/
|
||||
|
||||
declare( strict_types = 1 );
|
||||
|
||||
/**
|
||||
* Registers a new ability using Abilities API.
|
||||
*
|
||||
* Note: Should only be used on the {@see 'wp_abilities_api_init'} hook.
|
||||
*
|
||||
* @since 6.9.0
|
||||
*
|
||||
* @see WP_Abilities_Registry::register()
|
||||
*
|
||||
* @param string $name The name of the ability. The name must be a string containing a namespace
|
||||
* prefix, i.e. `my-plugin/my-ability`. It can only contain lowercase
|
||||
* alphanumeric characters, dashes and the forward slash.
|
||||
* @param array<string, mixed> $args {
|
||||
* An associative array of arguments for the ability.
|
||||
*
|
||||
* @type string $label The human-readable label for the ability.
|
||||
* @type string $description A detailed description of what the ability does.
|
||||
* @type string $category The ability category slug this ability belongs to.
|
||||
* @type callable $execute_callback A callback function to execute when the ability is invoked.
|
||||
* Receives optional mixed input and returns mixed result or WP_Error.
|
||||
* @type callable $permission_callback A callback function to check permissions before execution.
|
||||
* Receives optional mixed input and returns bool or WP_Error.
|
||||
* @type array<string, mixed> $input_schema Optional. JSON Schema definition for the ability's input.
|
||||
* @type array<string, mixed> $output_schema Optional. JSON Schema definition for the ability's output.
|
||||
* @type array<string, mixed> $meta {
|
||||
* Optional. Additional metadata for the ability.
|
||||
*
|
||||
* @type array<string, null|bool> $annotations Optional. Annotation metadata for the ability.
|
||||
* @type bool $show_in_rest Optional. Whether to expose this ability in the REST API. Default false.
|
||||
* }
|
||||
* @type string $ability_class Optional. Custom class to instantiate instead of WP_Ability.
|
||||
* }
|
||||
* @return WP_Ability|null An instance of registered ability on success, null on failure.
|
||||
*/
|
||||
function wp_register_ability( string $name, array $args ): ?WP_Ability {
|
||||
if ( ! did_action( 'wp_abilities_api_init' ) ) {
|
||||
_doing_it_wrong(
|
||||
__FUNCTION__,
|
||||
sprintf(
|
||||
/* translators: 1: abilities_api_init, 2: string value of the ability name. */
|
||||
esc_html__( 'Abilities must be registered on the %1$s action. The ability %2$s was not registered.' ),
|
||||
'<code>abilities_api_init</code>',
|
||||
'<code>' . esc_html( $name ) . '</code>'
|
||||
),
|
||||
'6.9.0'
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
$registry = WP_Abilities_Registry::get_instance();
|
||||
if ( null === $registry ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $registry->register( $name, $args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters an ability from the Abilities API.
|
||||
*
|
||||
* @since 6.9.0
|
||||
*
|
||||
* @see WP_Abilities_Registry::unregister()
|
||||
*
|
||||
* @param string $name The name of the registered ability, with its namespace.
|
||||
* @return WP_Ability|null The unregistered ability instance on success, null on failure.
|
||||
*/
|
||||
function wp_unregister_ability( string $name ): ?WP_Ability {
|
||||
$registry = WP_Abilities_Registry::get_instance();
|
||||
if ( null === $registry ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $registry->unregister( $name );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an ability is registered.
|
||||
*
|
||||
* @since 6.9.0
|
||||
*
|
||||
* @see WP_Abilities_Registry::is_registered()
|
||||
*
|
||||
* @param string $name The name of the registered ability, with its namespace.
|
||||
* @return bool True if the ability is registered, false otherwise.
|
||||
*/
|
||||
function wp_has_ability( string $name ): bool {
|
||||
$registry = WP_Abilities_Registry::get_instance();
|
||||
if ( null === $registry ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $registry->is_registered( $name );
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a registered ability using Abilities API.
|
||||
*
|
||||
* @since 6.9.0
|
||||
*
|
||||
* @see WP_Abilities_Registry::get_registered()
|
||||
*
|
||||
* @param string $name The name of the registered ability, with its namespace.
|
||||
* @return WP_Ability|null The registered ability instance, or null if it is not registered.
|
||||
*/
|
||||
function wp_get_ability( string $name ): ?WP_Ability {
|
||||
$registry = WP_Abilities_Registry::get_instance();
|
||||
if ( null === $registry ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $registry->get_registered( $name );
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all registered abilities using Abilities API.
|
||||
*
|
||||
* @since 6.9.0
|
||||
*
|
||||
* @see WP_Abilities_Registry::get_all_registered()
|
||||
*
|
||||
* @return WP_Ability[] The array of registered abilities.
|
||||
*/
|
||||
function wp_get_abilities(): array {
|
||||
$registry = WP_Abilities_Registry::get_instance();
|
||||
if ( null === $registry ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
return $registry->get_all_registered();
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a new ability category.
|
||||
*
|
||||
* @since 6.9.0
|
||||
*
|
||||
* @see WP_Ability_Categories_Registry::register()
|
||||
*
|
||||
* @param string $slug The unique slug for the ability category. Must contain only lowercase
|
||||
* alphanumeric characters and dashes.
|
||||
* @param array<string, mixed> $args {
|
||||
* An associative array of arguments for the ability category.
|
||||
*
|
||||
* @type string $label The human-readable label for the ability category.
|
||||
* @type string $description A description of the ability category.
|
||||
* @type array<string, mixed> $meta Optional. Additional metadata for the ability category.
|
||||
* }
|
||||
* @return WP_Ability_Category|null The registered ability category instance on success, null on failure.
|
||||
*/
|
||||
function wp_register_ability_category( string $slug, array $args ): ?WP_Ability_Category {
|
||||
if ( ! did_action( 'wp_abilities_api_categories_init' ) ) {
|
||||
_doing_it_wrong(
|
||||
__METHOD__,
|
||||
sprintf(
|
||||
/* translators: 1: abilities_api_categories_init, 2: ability category slug. */
|
||||
__( 'Ability categories must be registered on the %1$s action. The ability category %2$s was not registered.' ),
|
||||
'<code>wp_abilities_api_categories_init</code>',
|
||||
'<code>' . esc_html( $slug ) . '</code>'
|
||||
),
|
||||
'6.9.0'
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
$registry = WP_Ability_Categories_Registry::get_instance();
|
||||
if ( null === $registry ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $registry->register( $slug, $args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters an ability category.
|
||||
*
|
||||
* @since 6.9.0
|
||||
*
|
||||
* @see WP_Ability_Categories_Registry::unregister()
|
||||
*
|
||||
* @param string $slug The slug of the registered ability category.
|
||||
* @return WP_Ability_Category|null The unregistered ability category instance on success, null on failure.
|
||||
*/
|
||||
function wp_unregister_ability_category( string $slug ): ?WP_Ability_Category {
|
||||
$registry = WP_Ability_Categories_Registry::get_instance();
|
||||
if ( null === $registry ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $registry->unregister( $slug );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an ability category is registered.
|
||||
*
|
||||
* @since 6.9.0
|
||||
*
|
||||
* @see WP_Ability_Categories_Registry::is_registered()
|
||||
*
|
||||
* @param string $slug The slug of the ability category.
|
||||
* @return bool True if the ability category is registered, false otherwise.
|
||||
*/
|
||||
function wp_has_ability_category( string $slug ): bool {
|
||||
$registry = WP_Ability_Categories_Registry::get_instance();
|
||||
if ( null === $registry ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $registry->is_registered( $slug );
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a registered ability category.
|
||||
*
|
||||
* @since 6.9.0
|
||||
*
|
||||
* @see WP_Ability_Categories_Registry::get_registered()
|
||||
*
|
||||
* @param string $slug The slug of the registered ability category.
|
||||
* @return WP_Ability_Category|null The registered ability category instance, or null if it is not registered.
|
||||
*/
|
||||
function wp_get_ability_category( string $slug ): ?WP_Ability_Category {
|
||||
$registry = WP_Ability_Categories_Registry::get_instance();
|
||||
if ( null === $registry ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $registry->get_registered( $slug );
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all registered ability categories.
|
||||
*
|
||||
* @since 6.9.0
|
||||
*
|
||||
* @see WP_Ability_Categories_Registry::get_all_registered()
|
||||
*
|
||||
* @return WP_Ability_Category[] The array of registered ability categories.
|
||||
*/
|
||||
function wp_get_ability_categories(): array {
|
||||
$registry = WP_Ability_Categories_Registry::get_instance();
|
||||
if ( null === $registry ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
return $registry->get_all_registered();
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
<?php
|
||||
/**
|
||||
* Abilities API
|
||||
*
|
||||
* Defines WP_Abilities_Registry class.
|
||||
*
|
||||
* @package WordPress
|
||||
* @subpackage Abilities API
|
||||
* @since 6.9.0
|
||||
*/
|
||||
|
||||
declare( strict_types = 1 );
|
||||
|
||||
/**
|
||||
* Manages the registration and lookup of abilities.
|
||||
*
|
||||
* @since 6.9.0
|
||||
* @access private
|
||||
*/
|
||||
final class WP_Abilities_Registry {
|
||||
/**
|
||||
* The singleton instance of the registry.
|
||||
*
|
||||
* @since 6.9.0
|
||||
* @var self|null
|
||||
*/
|
||||
private static $instance = null;
|
||||
|
||||
/**
|
||||
* Holds the registered abilities.
|
||||
*
|
||||
* @since 6.9.0
|
||||
* @var WP_Ability[]
|
||||
*/
|
||||
private $registered_abilities = array();
|
||||
|
||||
/**
|
||||
* Registers a new ability.
|
||||
*
|
||||
* Do not use this method directly. Instead, use the `wp_register_ability()` function.
|
||||
*
|
||||
* @since 6.9.0
|
||||
*
|
||||
* @see wp_register_ability()
|
||||
*
|
||||
* @param string $name The name of the ability. The name must be a string containing a namespace
|
||||
* prefix, i.e. `my-plugin/my-ability`. It can only contain lowercase
|
||||
* alphanumeric characters, dashes and the forward slash.
|
||||
* @param array<string, mixed> $args {
|
||||
* An associative array of arguments for the ability.
|
||||
*
|
||||
* @type string $label The human-readable label for the ability.
|
||||
* @type string $description A detailed description of what the ability does.
|
||||
* @type string $category The ability category slug this ability belongs to.
|
||||
* @type callable $execute_callback A callback function to execute when the ability is invoked.
|
||||
* Receives optional mixed input and returns mixed result or WP_Error.
|
||||
* @type callable $permission_callback A callback function to check permissions before execution.
|
||||
* Receives optional mixed input and returns bool or WP_Error.
|
||||
* @type array<string, mixed> $input_schema Optional. JSON Schema definition for the ability's input.
|
||||
* @type array<string, mixed> $output_schema Optional. JSON Schema definition for the ability's output.
|
||||
* @type array<string, mixed> $meta {
|
||||
* Optional. Additional metadata for the ability.
|
||||
*
|
||||
* @type array<string, null|bool> $annotations Optional. Annotation metadata for the ability.
|
||||
* @type bool $show_in_rest Optional. Whether to expose this ability in the REST API. Default false.
|
||||
* }
|
||||
* @type string $ability_class Optional. Custom class to instantiate instead of WP_Ability.
|
||||
* }
|
||||
* @return WP_Ability|null The registered ability instance on success, null on failure.
|
||||
*/
|
||||
public function register( string $name, array $args ): ?WP_Ability {
|
||||
if ( ! preg_match( '/^[a-z0-9-]+\/[a-z0-9-]+$/', $name ) ) {
|
||||
_doing_it_wrong(
|
||||
__METHOD__,
|
||||
__(
|
||||
'Ability name must be a string containing a namespace prefix, i.e. "my-plugin/my-ability". It can only contain lowercase alphanumeric characters, dashes and the forward slash.'
|
||||
),
|
||||
'6.9.0'
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
if ( $this->is_registered( $name ) ) {
|
||||
_doing_it_wrong(
|
||||
__METHOD__,
|
||||
/* translators: %s: Ability name. */
|
||||
sprintf( __( 'Ability "%s" is already registered.' ), esc_html( $name ) ),
|
||||
'6.9.0'
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the ability arguments before they are validated and used to instantiate the ability.
|
||||
*
|
||||
* @since 6.9.0
|
||||
*
|
||||
* @param array<string, mixed> $args {
|
||||
* An associative array of arguments for the ability.
|
||||
*
|
||||
* @type string $label The human-readable label for the ability.
|
||||
* @type string $description A detailed description of what the ability does.
|
||||
* @type string $category The ability category slug this ability belongs to.
|
||||
* @type callable $execute_callback A callback function to execute when the ability is invoked.
|
||||
* Receives optional mixed input and returns mixed result or WP_Error.
|
||||
* @type callable $permission_callback A callback function to check permissions before execution.
|
||||
* Receives optional mixed input and returns bool or WP_Error.
|
||||
* @type array<string, mixed> $input_schema Optional. JSON Schema definition for the ability's input.
|
||||
* @type array<string, mixed> $output_schema Optional. JSON Schema definition for the ability's output.
|
||||
* @type array<string, mixed> $meta {
|
||||
* Optional. Additional metadata for the ability.
|
||||
*
|
||||
* @type array<string, bool|string> $annotations Optional. Annotation metadata for the ability.
|
||||
* @type bool $show_in_rest Optional. Whether to expose this ability in the REST API. Default false.
|
||||
* }
|
||||
* @type string $ability_class Optional. Custom class to instantiate instead of WP_Ability.
|
||||
* }
|
||||
* @param string $name The name of the ability, with its namespace.
|
||||
*/
|
||||
$args = apply_filters( 'wp_register_ability_args', $args, $name );
|
||||
|
||||
// Validate ability category exists if provided (will be validated as required in WP_Ability).
|
||||
if ( isset( $args['category'] ) ) {
|
||||
$category_registry = WP_Ability_Categories_Registry::get_instance();
|
||||
if ( ! $category_registry->is_registered( $args['category'] ) ) {
|
||||
_doing_it_wrong(
|
||||
__METHOD__,
|
||||
sprintf(
|
||||
/* translators: %1$s: ability category slug, %2$s: ability name */
|
||||
__( 'Ability category "%1$s" is not registered. Please register the ability category before assigning it to ability "%2$s".' ),
|
||||
esc_html( $args['category'] ),
|
||||
esc_html( $name )
|
||||
),
|
||||
'6.9.0'
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// The class is only used to instantiate the ability, and is not a property of the ability itself.
|
||||
if ( isset( $args['ability_class'] ) && ! is_a( $args['ability_class'], WP_Ability::class, true ) ) {
|
||||
_doing_it_wrong(
|
||||
__METHOD__,
|
||||
__( 'The ability args should provide a valid `ability_class` that extends WP_Ability.' ),
|
||||
'6.9.0'
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @var class-string<WP_Ability> */
|
||||
$ability_class = $args['ability_class'] ?? WP_Ability::class;
|
||||
unset( $args['ability_class'] );
|
||||
|
||||
try {
|
||||
// WP_Ability::prepare_properties() will throw an exception if the properties are invalid.
|
||||
$ability = new $ability_class( $name, $args );
|
||||
} catch ( InvalidArgumentException $e ) {
|
||||
_doing_it_wrong(
|
||||
__METHOD__,
|
||||
$e->getMessage(),
|
||||
'6.9.0'
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->registered_abilities[ $name ] = $ability;
|
||||
return $ability;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters an ability.
|
||||
*
|
||||
* Do not use this method directly. Instead, use the `wp_unregister_ability()` function.
|
||||
*
|
||||
* @since 6.9.0
|
||||
*
|
||||
* @see wp_unregister_ability()
|
||||
*
|
||||
* @param string $name The name of the registered ability, with its namespace.
|
||||
* @return WP_Ability|null The unregistered ability instance on success, null on failure.
|
||||
*/
|
||||
public function unregister( string $name ): ?WP_Ability {
|
||||
if ( ! $this->is_registered( $name ) ) {
|
||||
_doing_it_wrong(
|
||||
__METHOD__,
|
||||
/* translators: %s: Ability name. */
|
||||
sprintf( __( 'Ability "%s" not found.' ), esc_html( $name ) ),
|
||||
'6.9.0'
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
$unregistered_ability = $this->registered_abilities[ $name ];
|
||||
unset( $this->registered_abilities[ $name ] );
|
||||
|
||||
return $unregistered_ability;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the list of all registered abilities.
|
||||
*
|
||||
* Do not use this method directly. Instead, use the `wp_get_abilities()` function.
|
||||
*
|
||||
* @since 6.9.0
|
||||
*
|
||||
* @see wp_get_abilities()
|
||||
*
|
||||
* @return WP_Ability[] The array of registered abilities.
|
||||
*/
|
||||
public function get_all_registered(): array {
|
||||
return $this->registered_abilities;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an ability is registered.
|
||||
*
|
||||
* Do not use this method directly. Instead, use the `wp_has_ability()` function.
|
||||
*
|
||||
* @since 6.9.0
|
||||
*
|
||||
* @see wp_has_ability()
|
||||
*
|
||||
* @param string $name The name of the registered ability, with its namespace.
|
||||
* @return bool True if the ability is registered, false otherwise.
|
||||
*/
|
||||
public function is_registered( string $name ): bool {
|
||||
return isset( $this->registered_abilities[ $name ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a registered ability.
|
||||
*
|
||||
* Do not use this method directly. Instead, use the `wp_get_ability()` function.
|
||||
*
|
||||
* @since 6.9.0
|
||||
*
|
||||
* @see wp_get_ability()
|
||||
*
|
||||
* @param string $name The name of the registered ability, with its namespace.
|
||||
* @return ?WP_Ability The registered ability instance, or null if it is not registered.
|
||||
*/
|
||||
public function get_registered( string $name ): ?WP_Ability {
|
||||
if ( ! $this->is_registered( $name ) ) {
|
||||
_doing_it_wrong(
|
||||
__METHOD__,
|
||||
/* translators: %s: Ability name. */
|
||||
sprintf( esc_html__( 'Ability "%s" not found.' ), esc_attr( $name ) ),
|
||||
'6.9.0'
|
||||
);
|
||||
return null;
|
||||
}
|
||||
return $this->registered_abilities[ $name ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method to retrieve the main instance of the registry class.
|
||||
*
|
||||
* The instance will be created if it does not exist yet.
|
||||
*
|
||||
* @since 6.9.0
|
||||
*
|
||||
* @return WP_Abilities_Registry|null The main registry instance, or null when `init` action has not fired.
|
||||
*/
|
||||
public static function get_instance(): ?self {
|
||||
if ( ! did_action( 'init' ) ) {
|
||||
_doing_it_wrong(
|
||||
__METHOD__,
|
||||
sprintf(
|
||||
__( 'Ability API should not be initialized before the <code>init</code> action has fired' )
|
||||
),
|
||||
'6.9.0'
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
if ( null === self::$instance ) {
|
||||
self::$instance = new self();
|
||||
|
||||
// Ensure ability category registry is initialized first to allow categories to be registered
|
||||
// before abilities that depend on them.
|
||||
WP_Ability_Categories_Registry::get_instance();
|
||||
|
||||
/**
|
||||
* Fires when preparing abilities registry.
|
||||
*
|
||||
* Abilities should be created and register their hooks on this action rather
|
||||
* than another action to ensure they're only loaded when needed.
|
||||
*
|
||||
* @since 6.9.0
|
||||
*
|
||||
* @param WP_Abilities_Registry $instance Abilities registry object.
|
||||
*/
|
||||
do_action( 'wp_abilities_api_init', self::$instance );
|
||||
}
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wakeup magic method.
|
||||
*
|
||||
* @since 6.9.0
|
||||
* @throws LogicException If the registry object is unserialized.
|
||||
* This is a security hardening measure to prevent unserialization of the registry.
|
||||
*/
|
||||
public function __wakeup(): void {
|
||||
throw new LogicException( __CLASS__ . ' should never be unserialized.' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Sleep magic method.
|
||||
*
|
||||
* @since 6.9.0
|
||||
* @throws LogicException If the registry object is serialized.
|
||||
* This is a security hardening measure to prevent serialization of the registry.
|
||||
*/
|
||||
public function __sleep(): array {
|
||||
throw new LogicException( __CLASS__ . ' should never be serialized' );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
<?php
|
||||
/**
|
||||
* Abilities API
|
||||
*
|
||||
* Defines WP_Ability_Categories_Registry class.
|
||||
*
|
||||
* @package WordPress
|
||||
* @subpackage Abilities API
|
||||
* @since 6.9.0
|
||||
*/
|
||||
|
||||
declare( strict_types = 1 );
|
||||
|
||||
/**
|
||||
* Manages the registration and lookup of ability categories.
|
||||
*
|
||||
* @since 6.9.0
|
||||
* @access private
|
||||
*/
|
||||
final class WP_Ability_Categories_Registry {
|
||||
/**
|
||||
* The singleton instance of the registry.
|
||||
*
|
||||
* @since 6.9.0
|
||||
* @var self|null
|
||||
*/
|
||||
private static $instance = null;
|
||||
|
||||
/**
|
||||
* Holds the registered ability categories.
|
||||
*
|
||||
* @since 6.9.0
|
||||
* @var WP_Ability_Category[]
|
||||
*/
|
||||
private $registered_categories = array();
|
||||
|
||||
/**
|
||||
* Registers a new ability category.
|
||||
*
|
||||
* Do not use this method directly. Instead, use the `wp_register_ability_category()` function.
|
||||
*
|
||||
* @since 6.9.0
|
||||
*
|
||||
* @see wp_register_ability_category()
|
||||
*
|
||||
* @param string $slug The unique slug for the ability category. Must contain only lowercase
|
||||
* alphanumeric characters and dashes.
|
||||
* @param array<string, mixed> $args {
|
||||
* An associative array of arguments for the ability category.
|
||||
*
|
||||
* @type string $label The human-readable label for the ability category.
|
||||
* @type string $description A description of the ability category.
|
||||
* @type array<string, mixed> $meta Optional. Additional metadata for the ability category.
|
||||
* }
|
||||
* @return WP_Ability_Category|null The registered ability category instance on success, null on failure.
|
||||
*/
|
||||
public function register( string $slug, array $args ): ?WP_Ability_Category {
|
||||
if ( $this->is_registered( $slug ) ) {
|
||||
_doing_it_wrong(
|
||||
__METHOD__,
|
||||
/* translators: %s: Ability category slug. */
|
||||
sprintf( __( 'Ability category "%s" is already registered.' ), esc_html( $slug ) ),
|
||||
'6.9.0'
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
if ( ! preg_match( '/^[a-z0-9]+(?:-[a-z0-9]+)*$/', $slug ) ) {
|
||||
_doing_it_wrong(
|
||||
__METHOD__,
|
||||
__( 'Ability category slug must contain only lowercase alphanumeric characters and dashes.' ),
|
||||
'6.9.0'
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the ability category arguments before they are validated and used to instantiate the ability category.
|
||||
*
|
||||
* @since 6.9.0
|
||||
*
|
||||
* @param array<string, mixed> $args {
|
||||
* The arguments used to instantiate the ability category.
|
||||
*
|
||||
* @type string $label The human-readable label for the ability category.
|
||||
* @type string $description A description of the ability category.
|
||||
* @type array<string, mixed> $meta Optional. Additional metadata for the ability category.
|
||||
* }
|
||||
* @param string $slug The slug of the ability category.
|
||||
*/
|
||||
$args = apply_filters( 'wp_register_ability_category_args', $args, $slug );
|
||||
|
||||
try {
|
||||
// WP_Ability_Category::prepare_properties() will throw an exception if the properties are invalid.
|
||||
$category = new WP_Ability_Category( $slug, $args );
|
||||
} catch ( InvalidArgumentException $e ) {
|
||||
_doing_it_wrong(
|
||||
__METHOD__,
|
||||
$e->getMessage(),
|
||||
'6.9.0'
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->registered_categories[ $slug ] = $category;
|
||||
return $category;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters an ability category.
|
||||
*
|
||||
* Do not use this method directly. Instead, use the `wp_unregister_ability_category()` function.
|
||||
*
|
||||
* @since 6.9.0
|
||||
*
|
||||
* @see wp_unregister_ability_category()
|
||||
*
|
||||
* @param string $slug The slug of the registered ability category.
|
||||
* @return WP_Ability_Category|null The unregistered ability category instance on success, null on failure.
|
||||
*/
|
||||
public function unregister( string $slug ): ?WP_Ability_Category {
|
||||
if ( ! $this->is_registered( $slug ) ) {
|
||||
_doing_it_wrong(
|
||||
__METHOD__,
|
||||
/* translators: %s: Ability category slug. */
|
||||
sprintf( __( 'Ability category "%s" not found.' ), esc_html( $slug ) ),
|
||||
'6.9.0'
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
$unregistered_category = $this->registered_categories[ $slug ];
|
||||
unset( $this->registered_categories[ $slug ] );
|
||||
|
||||
return $unregistered_category;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the list of all registered ability categories.
|
||||
*
|
||||
* Do not use this method directly. Instead, use the `wp_get_ability_categories()` function.
|
||||
*
|
||||
* @since 6.9.0
|
||||
*
|
||||
* @see wp_get_ability_categories()
|
||||
*
|
||||
* @return array<string, WP_Ability_Category> The array of registered ability categories.
|
||||
*/
|
||||
public function get_all_registered(): array {
|
||||
return $this->registered_categories;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an ability category is registered.
|
||||
*
|
||||
* Do not use this method directly. Instead, use the `wp_has_ability_category()` function.
|
||||
*
|
||||
* @since 6.9.0
|
||||
*
|
||||
* @see wp_has_ability_category()
|
||||
*
|
||||
* @param string $slug The slug of the ability category.
|
||||
* @return bool True if the ability category is registered, false otherwise.
|
||||
*/
|
||||
public function is_registered( string $slug ): bool {
|
||||
return isset( $this->registered_categories[ $slug ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a registered ability category.
|
||||
*
|
||||
* Do not use this method directly. Instead, use the `wp_get_ability_category()` function.
|
||||
*
|
||||
* @since 6.9.0
|
||||
*
|
||||
* @see wp_get_ability_category()
|
||||
*
|
||||
* @param string $slug The slug of the registered ability category.
|
||||
* @return WP_Ability_Category|null The registered ability category instance, or null if it is not registered.
|
||||
*/
|
||||
public function get_registered( string $slug ): ?WP_Ability_Category {
|
||||
if ( ! $this->is_registered( $slug ) ) {
|
||||
_doing_it_wrong(
|
||||
__METHOD__,
|
||||
/* translators: %s: Ability category slug. */
|
||||
sprintf( __( 'Ability category "%s" not found.' ), esc_html( $slug ) ),
|
||||
'6.9.0'
|
||||
);
|
||||
return null;
|
||||
}
|
||||
return $this->registered_categories[ $slug ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method to retrieve the main instance of the registry class.
|
||||
*
|
||||
* The instance will be created if it does not exist yet.
|
||||
*
|
||||
* @since 6.9.0
|
||||
*
|
||||
* @return WP_Ability_Categories_Registry|null The main registry instance, or null when `init` action has not fired.
|
||||
*/
|
||||
public static function get_instance(): ?self {
|
||||
if ( ! did_action( 'init' ) ) {
|
||||
_doing_it_wrong(
|
||||
__METHOD__,
|
||||
sprintf(
|
||||
__( 'Ability API should not be initialized before the <code>init</code> action has fired' )
|
||||
),
|
||||
'6.9.0'
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
if ( null === self::$instance ) {
|
||||
self::$instance = new self();
|
||||
|
||||
/**
|
||||
* Fires when preparing ability categories registry.
|
||||
*
|
||||
* Ability categories should be registered on this action to ensure they're available when needed.
|
||||
*
|
||||
* @since 6.9.0
|
||||
*
|
||||
* @param WP_Ability_Categories_Registry $instance Ability categories registry object.
|
||||
*/
|
||||
do_action( 'wp_abilities_api_categories_init', self::$instance );
|
||||
}
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wakeup magic method.
|
||||
*
|
||||
* @since 6.9.0
|
||||
* @throws LogicException If the registry object is unserialized.
|
||||
* This is a security hardening measure to prevent unserialization of the registry.
|
||||
*/
|
||||
public function __wakeup(): void {
|
||||
throw new LogicException( __CLASS__ . ' should never be unserialized.' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Sleep magic method.
|
||||
*
|
||||
* @since 6.9.0
|
||||
* @throws LogicException If the registry object is serialized.
|
||||
* This is a security hardening measure to prevent serialization of the registry.
|
||||
*/
|
||||
public function __sleep(): array {
|
||||
throw new LogicException( __CLASS__ . ' should never be serialized' );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
<?php
|
||||
/**
|
||||
* Abilities API
|
||||
*
|
||||
* Defines WP_Ability_Category class.
|
||||
*
|
||||
* @package WordPress
|
||||
* @subpackage Abilities API
|
||||
* @since 6.9.0
|
||||
*/
|
||||
|
||||
declare( strict_types = 1 );
|
||||
|
||||
/**
|
||||
* Encapsulates the properties and methods related to a specific ability category.
|
||||
*
|
||||
* @since 6.9.0
|
||||
*
|
||||
* @see WP_Ability_Categories_Registry
|
||||
*/
|
||||
final class WP_Ability_Category {
|
||||
|
||||
/**
|
||||
* The unique slug for the ability category.
|
||||
*
|
||||
* @since 6.9.0
|
||||
* @var string
|
||||
*/
|
||||
protected $slug;
|
||||
|
||||
/**
|
||||
* The human-readable ability category label.
|
||||
*
|
||||
* @since 6.9.0
|
||||
* @var string
|
||||
*/
|
||||
protected $label;
|
||||
|
||||
/**
|
||||
* The detailed ability category description.
|
||||
*
|
||||
* @since 6.9.0
|
||||
* @var string
|
||||
*/
|
||||
protected $description;
|
||||
|
||||
/**
|
||||
* The optional ability category metadata.
|
||||
*
|
||||
* @since 6.9.0
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
protected $meta = array();
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* Do not use this constructor directly. Instead, use the `wp_register_ability_category()` function.
|
||||
*
|
||||
* @access private
|
||||
*
|
||||
* @since 6.9.0
|
||||
*
|
||||
* @see wp_register_ability_category()
|
||||
*
|
||||
* @param string $slug The unique slug for the ability category.
|
||||
* @param array<string, mixed> $args {
|
||||
* An associative array of arguments for the ability category.
|
||||
*
|
||||
* @type string $label The human-readable label for the ability category.
|
||||
* @type string $description A description of the ability category.
|
||||
* @type array<string, mixed> $meta Optional. Additional metadata for the ability category.
|
||||
* }
|
||||
*/
|
||||
public function __construct( string $slug, array $args ) {
|
||||
if ( empty( $slug ) ) {
|
||||
throw new InvalidArgumentException(
|
||||
esc_html__( 'The ability category slug cannot be empty.' )
|
||||
);
|
||||
}
|
||||
|
||||
$this->slug = $slug;
|
||||
|
||||
$properties = $this->prepare_properties( $args );
|
||||
|
||||
foreach ( $properties as $property_name => $property_value ) {
|
||||
if ( ! property_exists( $this, $property_name ) ) {
|
||||
_doing_it_wrong(
|
||||
__METHOD__,
|
||||
sprintf(
|
||||
/* translators: %s: Property name. */
|
||||
__( 'Property "%1$s" is not a valid property for ability category "%2$s". Please check the %3$s class for allowed properties.' ),
|
||||
'<code>' . esc_html( $property_name ) . '</code>',
|
||||
'<code>' . esc_html( $this->slug ) . '</code>',
|
||||
'<code>' . __CLASS__ . '</code>'
|
||||
),
|
||||
'6.9.0'
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->$property_name = $property_value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares and validates the properties used to instantiate the ability category.
|
||||
*
|
||||
* @since 6.9.0
|
||||
*
|
||||
* @param array<string, mixed> $args $args {
|
||||
* An associative array of arguments used to instantiate the ability category class.
|
||||
*
|
||||
* @type string $label The human-readable label for the ability category.
|
||||
* @type string $description A description of the ability category.
|
||||
* @type array<string, mixed> $meta Optional. Additional metadata for the ability category.
|
||||
* }
|
||||
* @return array<string, mixed> $args {
|
||||
* An associative array with validated and prepared ability category properties.
|
||||
*
|
||||
* @type string $label The human-readable label for the ability category.
|
||||
* @type string $description A description of the ability category.
|
||||
* @type array<string, mixed> $meta Optional. Additional metadata for the ability category.
|
||||
* }
|
||||
* @throws InvalidArgumentException if an argument is invalid.
|
||||
*/
|
||||
protected function prepare_properties( array $args ): array {
|
||||
// Required args must be present and of the correct type.
|
||||
if ( empty( $args['label'] ) || ! is_string( $args['label'] ) ) {
|
||||
throw new InvalidArgumentException(
|
||||
__( 'The ability category properties must contain a `label` string.' )
|
||||
);
|
||||
}
|
||||
|
||||
if ( empty( $args['description'] ) || ! is_string( $args['description'] ) ) {
|
||||
throw new InvalidArgumentException(
|
||||
__( 'The ability category properties must contain a `description` string.' )
|
||||
);
|
||||
}
|
||||
|
||||
// Optional args only need to be of the correct type if they are present.
|
||||
if ( isset( $args['meta'] ) && ! is_array( $args['meta'] ) ) {
|
||||
throw new InvalidArgumentException(
|
||||
__( 'The ability category properties should provide a valid `meta` array.' )
|
||||
);
|
||||
}
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the slug of the ability category.
|
||||
*
|
||||
* @since 6.9.0
|
||||
*
|
||||
* @return string The ability category slug.
|
||||
*/
|
||||
public function get_slug(): string {
|
||||
return $this->slug;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the human-readable label for the ability category.
|
||||
*
|
||||
* @since 6.9.0
|
||||
*
|
||||
* @return string The human-readable ability category label.
|
||||
*/
|
||||
public function get_label(): string {
|
||||
return $this->label;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the detailed description for the ability category.
|
||||
*
|
||||
* @since 6.9.0
|
||||
*
|
||||
* @return string The detailed description for the ability category.
|
||||
*/
|
||||
public function get_description(): string {
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the metadata for the ability category.
|
||||
*
|
||||
* @since 6.9.0
|
||||
*
|
||||
* @return array<string,mixed> The metadata for the ability category.
|
||||
*/
|
||||
public function get_meta(): array {
|
||||
return $this->meta;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wakeup magic method.
|
||||
*
|
||||
* @since 6.9.0
|
||||
* @throws LogicException If the ability category object is unserialized.
|
||||
* This is a security hardening measure to prevent unserialization of the ability category.
|
||||
*/
|
||||
public function __wakeup(): void {
|
||||
throw new LogicException( __CLASS__ . ' should never be unserialized.' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Sleep magic method.
|
||||
*
|
||||
* @since 6.9.0
|
||||
* @throws LogicException If the ability category object is serialized.
|
||||
* This is a security hardening measure to prevent serialization of the ability category.
|
||||
*/
|
||||
public function __sleep(): array {
|
||||
throw new LogicException( __CLASS__ . ' should never be serialized' );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,617 @@
|
||||
<?php
|
||||
/**
|
||||
* Abilities API
|
||||
*
|
||||
* Defines WP_Ability class.
|
||||
*
|
||||
* @package WordPress
|
||||
* @subpackage Abilities API
|
||||
* @since 6.9.0
|
||||
*/
|
||||
|
||||
declare( strict_types = 1 );
|
||||
|
||||
/**
|
||||
* Encapsulates the properties and methods related to a specific ability in the registry.
|
||||
*
|
||||
* @since 6.9.0
|
||||
*
|
||||
* @see WP_Abilities_Registry
|
||||
*/
|
||||
class WP_Ability {
|
||||
|
||||
/**
|
||||
* The default value for the `show_in_rest` meta.
|
||||
*
|
||||
* @since 6.9.0
|
||||
* @var bool
|
||||
*/
|
||||
protected const DEFAULT_SHOW_IN_REST = false;
|
||||
|
||||
/**
|
||||
* The default ability annotations.
|
||||
* They are not guaranteed to provide a faithful description of ability behavior.
|
||||
*
|
||||
* @since 6.9.0
|
||||
* @var array<string, (null|bool)>
|
||||
*/
|
||||
protected static $default_annotations = array(
|
||||
// If true, the ability does not modify its environment.
|
||||
'readonly' => null,
|
||||
/*
|
||||
* If true, the ability may perform destructive updates to its environment.
|
||||
* If false, the ability performs only additive updates.
|
||||
*/
|
||||
'destructive' => null,
|
||||
/*
|
||||
* If true, calling the ability repeatedly with the same arguments will have no additional effect
|
||||
* on its environment.
|
||||
*/
|
||||
'idempotent' => null,
|
||||
);
|
||||
|
||||
/**
|
||||
* The name of the ability, with its namespace.
|
||||
* Example: `my-plugin/my-ability`.
|
||||
*
|
||||
* @since 6.9.0
|
||||
* @var string
|
||||
*/
|
||||
protected $name;
|
||||
|
||||
/**
|
||||
* The human-readable ability label.
|
||||
*
|
||||
* @since 6.9.0
|
||||
* @var string
|
||||
*/
|
||||
protected $label;
|
||||
|
||||
/**
|
||||
* The detailed ability description.
|
||||
*
|
||||
* @since 6.9.0
|
||||
* @var string
|
||||
*/
|
||||
protected $description;
|
||||
|
||||
/**
|
||||
* The ability category.
|
||||
*
|
||||
* @since 6.9.0
|
||||
* @var string
|
||||
*/
|
||||
protected $category;
|
||||
|
||||
/**
|
||||
* The optional ability input schema.
|
||||
*
|
||||
* @since 6.9.0
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
protected $input_schema = array();
|
||||
|
||||
/**
|
||||
* The optional ability output schema.
|
||||
*
|
||||
* @since 6.9.0
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
protected $output_schema = array();
|
||||
|
||||
/**
|
||||
* The ability execute callback.
|
||||
*
|
||||
* @since 6.9.0
|
||||
* @var callable( mixed $input= ): (mixed|WP_Error)
|
||||
*/
|
||||
protected $execute_callback;
|
||||
|
||||
/**
|
||||
* The optional ability permission callback.
|
||||
*
|
||||
* @since 6.9.0
|
||||
* @var callable( mixed $input= ): (bool|WP_Error)
|
||||
*/
|
||||
protected $permission_callback;
|
||||
|
||||
/**
|
||||
* The optional ability metadata.
|
||||
*
|
||||
* @since 6.9.0
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
protected $meta;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* Do not use this constructor directly. Instead, use the `wp_register_ability()` function.
|
||||
*
|
||||
* @access private
|
||||
*
|
||||
* @since 6.9.0
|
||||
*
|
||||
* @see wp_register_ability()
|
||||
*
|
||||
* @param string $name The name of the ability, with its namespace.
|
||||
* @param array<string, mixed> $args {
|
||||
* An associative array of arguments for the ability.
|
||||
*
|
||||
* @type string $label The human-readable label for the ability.
|
||||
* @type string $description A detailed description of what the ability does.
|
||||
* @type string $category The ability category slug this ability belongs to.
|
||||
* @type callable $execute_callback A callback function to execute when the ability is invoked.
|
||||
* Receives optional mixed input and returns mixed result or WP_Error.
|
||||
* @type callable $permission_callback A callback function to check permissions before execution.
|
||||
* Receives optional mixed input and returns bool or WP_Error.
|
||||
* @type array<string, mixed> $input_schema Optional. JSON Schema definition for the ability's input.
|
||||
* @type array<string, mixed> $output_schema Optional. JSON Schema definition for the ability's output.
|
||||
* @type array<string, mixed> $meta {
|
||||
* Optional. Additional metadata for the ability.
|
||||
*
|
||||
* @type array<string, null|bool> $annotations Optional. Annotation metadata for the ability.
|
||||
* @type bool $show_in_rest Optional. Whether to expose this ability in the REST API. Default false.
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
public function __construct( string $name, array $args ) {
|
||||
$this->name = $name;
|
||||
|
||||
$properties = $this->prepare_properties( $args );
|
||||
|
||||
foreach ( $properties as $property_name => $property_value ) {
|
||||
if ( ! property_exists( $this, $property_name ) ) {
|
||||
_doing_it_wrong(
|
||||
__METHOD__,
|
||||
sprintf(
|
||||
/* translators: %s: Property name. */
|
||||
__( 'Property "%1$s" is not a valid property for ability "%2$s". Please check the %3$s class for allowed properties.' ),
|
||||
'<code>' . esc_html( $property_name ) . '</code>',
|
||||
'<code>' . esc_html( $this->name ) . '</code>',
|
||||
'<code>' . self::class . '</code>'
|
||||
),
|
||||
'6.9.0'
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->$property_name = $property_value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares and validates the properties used to instantiate the ability.
|
||||
*
|
||||
* Errors are thrown as exceptions instead of WP_Errors to allow for simpler handling and overloading. They are then
|
||||
* caught and converted to a WP_Error when by WP_Abilities_Registry::register().
|
||||
*
|
||||
* @since 6.9.0
|
||||
*
|
||||
* @see WP_Abilities_Registry::register()
|
||||
*
|
||||
* @param array<string, mixed> $args {
|
||||
* An associative array of arguments used to instantiate the ability class.
|
||||
*
|
||||
* @type string $label The human-readable label for the ability.
|
||||
* @type string $description A detailed description of what the ability does.
|
||||
* @type string $category The ability category slug this ability belongs to.
|
||||
* @type callable $execute_callback A callback function to execute when the ability is invoked.
|
||||
* Receives optional mixed input and returns mixed result or WP_Error.
|
||||
* @type callable $permission_callback A callback function to check permissions before execution.
|
||||
* Receives optional mixed input and returns bool or WP_Error.
|
||||
* @type array<string, mixed> $input_schema Optional. JSON Schema definition for the ability's input. Required if ability accepts an input.
|
||||
* @type array<string, mixed> $output_schema Optional. JSON Schema definition for the ability's output.
|
||||
* @type array<string, mixed> $meta {
|
||||
* Optional. Additional metadata for the ability.
|
||||
*
|
||||
* @type array<string, null|bool> $annotations Optional. Annotation metadata for the ability.
|
||||
* @type bool $show_in_rest Optional. Whether to expose this ability in the REST API. Default false.
|
||||
* }
|
||||
* }
|
||||
* @return array<string, mixed> {
|
||||
* An associative array of arguments with validated and prepared properties for the ability class.
|
||||
*
|
||||
* @type string $label The human-readable label for the ability.
|
||||
* @type string $description A detailed description of what the ability does.
|
||||
* @type string $category The ability category slug this ability belongs to.
|
||||
* @type callable $execute_callback A callback function to execute when the ability is invoked.
|
||||
* Receives optional mixed input and returns mixed result or WP_Error.
|
||||
* @type callable $permission_callback A callback function to check permissions before execution.
|
||||
* Receives optional mixed input and returns bool or WP_Error.
|
||||
* @type array<string, mixed> $input_schema Optional. JSON Schema definition for the ability's input.
|
||||
* @type array<string, mixed> $output_schema Optional. JSON Schema definition for the ability's output.
|
||||
* @type array<string, mixed> $meta {
|
||||
* Additional metadata for the ability.
|
||||
*
|
||||
* @type array<string, null|bool> $annotations Optional. Annotation metadata for the ability.
|
||||
* @type bool $show_in_rest Whether to expose this ability in the REST API. Default false.
|
||||
* }
|
||||
* }
|
||||
* @throws InvalidArgumentException if an argument is invalid.
|
||||
*/
|
||||
protected function prepare_properties( array $args ): array {
|
||||
// Required args must be present and of the correct type.
|
||||
if ( empty( $args['label'] ) || ! is_string( $args['label'] ) ) {
|
||||
throw new InvalidArgumentException(
|
||||
__( 'The ability properties must contain a `label` string.' )
|
||||
);
|
||||
}
|
||||
|
||||
if ( empty( $args['description'] ) || ! is_string( $args['description'] ) ) {
|
||||
throw new InvalidArgumentException(
|
||||
__( 'The ability properties must contain a `description` string.' )
|
||||
);
|
||||
}
|
||||
|
||||
if ( empty( $args['category'] ) || ! is_string( $args['category'] ) ) {
|
||||
throw new InvalidArgumentException(
|
||||
__( 'The ability properties must contain a `category` string.' )
|
||||
);
|
||||
}
|
||||
|
||||
if ( empty( $args['execute_callback'] ) || ! is_callable( $args['execute_callback'] ) ) {
|
||||
throw new InvalidArgumentException(
|
||||
__( 'The ability properties must contain a valid `execute_callback` function.' )
|
||||
);
|
||||
}
|
||||
|
||||
if ( empty( $args['permission_callback'] ) || ! is_callable( $args['permission_callback'] ) ) {
|
||||
throw new InvalidArgumentException(
|
||||
__( 'The ability properties must provide a valid `permission_callback` function.' )
|
||||
);
|
||||
}
|
||||
|
||||
// Optional args only need to be of the correct type if they are present.
|
||||
if ( isset( $args['input_schema'] ) && ! is_array( $args['input_schema'] ) ) {
|
||||
throw new InvalidArgumentException(
|
||||
__( 'The ability properties should provide a valid `input_schema` definition.' )
|
||||
);
|
||||
}
|
||||
|
||||
if ( isset( $args['output_schema'] ) && ! is_array( $args['output_schema'] ) ) {
|
||||
throw new InvalidArgumentException(
|
||||
__( 'The ability properties should provide a valid `output_schema` definition.' )
|
||||
);
|
||||
}
|
||||
|
||||
if ( isset( $args['meta'] ) && ! is_array( $args['meta'] ) ) {
|
||||
throw new InvalidArgumentException(
|
||||
__( 'The ability properties should provide a valid `meta` array.' )
|
||||
);
|
||||
}
|
||||
|
||||
if ( isset( $args['meta']['annotations'] ) && ! is_array( $args['meta']['annotations'] ) ) {
|
||||
throw new InvalidArgumentException(
|
||||
__( 'The ability meta should provide a valid `annotations` array.' )
|
||||
);
|
||||
}
|
||||
|
||||
if ( isset( $args['meta']['show_in_rest'] ) && ! is_bool( $args['meta']['show_in_rest'] ) ) {
|
||||
throw new InvalidArgumentException(
|
||||
__( 'The ability meta should provide a valid `show_in_rest` boolean.' )
|
||||
);
|
||||
}
|
||||
|
||||
// Set defaults for optional meta.
|
||||
$args['meta'] = wp_parse_args(
|
||||
$args['meta'] ?? array(),
|
||||
array(
|
||||
'annotations' => static::$default_annotations,
|
||||
'show_in_rest' => self::DEFAULT_SHOW_IN_REST,
|
||||
)
|
||||
);
|
||||
$args['meta']['annotations'] = wp_parse_args(
|
||||
$args['meta']['annotations'],
|
||||
static::$default_annotations
|
||||
);
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the name of the ability, with its namespace.
|
||||
* Example: `my-plugin/my-ability`.
|
||||
*
|
||||
* @since 6.9.0
|
||||
*
|
||||
* @return string The ability name, with its namespace.
|
||||
*/
|
||||
public function get_name(): string {
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the human-readable label for the ability.
|
||||
*
|
||||
* @since 6.9.0
|
||||
*
|
||||
* @return string The human-readable ability label.
|
||||
*/
|
||||
public function get_label(): string {
|
||||
return $this->label;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the detailed description for the ability.
|
||||
*
|
||||
* @since 6.9.0
|
||||
*
|
||||
* @return string The detailed description for the ability.
|
||||
*/
|
||||
public function get_description(): string {
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the ability category for the ability.
|
||||
*
|
||||
* @since 6.9.0
|
||||
*
|
||||
* @return string The ability category for the ability.
|
||||
*/
|
||||
public function get_category(): string {
|
||||
return $this->category;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the input schema for the ability.
|
||||
*
|
||||
* @since 6.9.0
|
||||
*
|
||||
* @return array<string, mixed> The input schema for the ability.
|
||||
*/
|
||||
public function get_input_schema(): array {
|
||||
return $this->input_schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the output schema for the ability.
|
||||
*
|
||||
* @since 6.9.0
|
||||
*
|
||||
* @return array<string, mixed> The output schema for the ability.
|
||||
*/
|
||||
public function get_output_schema(): array {
|
||||
return $this->output_schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the metadata for the ability.
|
||||
*
|
||||
* @since 6.9.0
|
||||
*
|
||||
* @return array<string, mixed> The metadata for the ability.
|
||||
*/
|
||||
public function get_meta(): array {
|
||||
return $this->meta;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a specific metadata item for the ability.
|
||||
*
|
||||
* @since 6.9.0
|
||||
*
|
||||
* @param string $key The metadata key to retrieve.
|
||||
* @param mixed $default_value Optional. The default value to return if the metadata item is not found. Default `null`.
|
||||
* @return mixed The value of the metadata item, or the default value if not found.
|
||||
*/
|
||||
public function get_meta_item( string $key, $default_value = null ) {
|
||||
return array_key_exists( $key, $this->meta ) ? $this->meta[ $key ] : $default_value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates input data against the input schema.
|
||||
*
|
||||
* @since 6.9.0
|
||||
*
|
||||
* @param mixed $input Optional. The input data to validate. Default `null`.
|
||||
* @return true|WP_Error Returns true if valid or the WP_Error object if validation fails.
|
||||
*/
|
||||
public function validate_input( $input = null ) {
|
||||
$input_schema = $this->get_input_schema();
|
||||
if ( empty( $input_schema ) ) {
|
||||
if ( null === $input ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return new WP_Error(
|
||||
'ability_missing_input_schema',
|
||||
sprintf(
|
||||
/* translators: %s ability name. */
|
||||
__( 'Ability "%s" does not define an input schema required to validate the provided input.' ),
|
||||
$this->name
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$valid_input = rest_validate_value_from_schema( $input, $input_schema, 'input' );
|
||||
if ( is_wp_error( $valid_input ) ) {
|
||||
return new WP_Error(
|
||||
'ability_invalid_input',
|
||||
sprintf(
|
||||
/* translators: %1$s ability name, %2$s error message. */
|
||||
__( 'Ability "%1$s" has invalid input. Reason: %2$s' ),
|
||||
$this->name,
|
||||
$valid_input->get_error_message()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes a callable, ensuring the input is passed through only if the input schema is defined.
|
||||
*
|
||||
* @since 6.9.0
|
||||
*
|
||||
* @param callable $callback The callable to invoke.
|
||||
* @param mixed $input Optional. The input data for the ability. Default `null`.
|
||||
* @return mixed The result of the callable execution.
|
||||
*/
|
||||
protected function invoke_callback( callable $callback, $input = null ) {
|
||||
$args = array();
|
||||
if ( ! empty( $this->get_input_schema() ) ) {
|
||||
$args[] = $input;
|
||||
}
|
||||
|
||||
return $callback( ...$args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the ability has the necessary permissions.
|
||||
*
|
||||
* Please note that input is not automatically validated against the input schema.
|
||||
* Use `validate_input()` method to validate input before calling this method if needed.
|
||||
*
|
||||
* @since 6.9.0
|
||||
*
|
||||
* @see validate_input()
|
||||
*
|
||||
* @param mixed $input Optional. The valid input data for permission checking. Default `null`.
|
||||
* @return bool|WP_Error Whether the ability has the necessary permission.
|
||||
*/
|
||||
public function check_permissions( $input = null ) {
|
||||
return $this->invoke_callback( $this->permission_callback, $input );
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the ability callback.
|
||||
*
|
||||
* @since 6.9.0
|
||||
*
|
||||
* @param mixed $input Optional. The input data for the ability. Default `null`.
|
||||
* @return mixed|WP_Error The result of the ability execution, or WP_Error on failure.
|
||||
*/
|
||||
protected function do_execute( $input = null ) {
|
||||
if ( ! is_callable( $this->execute_callback ) ) {
|
||||
return new WP_Error(
|
||||
'ability_invalid_execute_callback',
|
||||
/* translators: %s ability name. */
|
||||
sprintf( __( 'Ability "%s" does not have a valid execute callback.' ), $this->name )
|
||||
);
|
||||
}
|
||||
|
||||
return $this->invoke_callback( $this->execute_callback, $input );
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates output data against the output schema.
|
||||
*
|
||||
* @since 6.9.0
|
||||
*
|
||||
* @param mixed $output The output data to validate.
|
||||
* @return true|WP_Error Returns true if valid, or a WP_Error object if validation fails.
|
||||
*/
|
||||
protected function validate_output( $output ) {
|
||||
$output_schema = $this->get_output_schema();
|
||||
if ( empty( $output_schema ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$valid_output = rest_validate_value_from_schema( $output, $output_schema, 'output' );
|
||||
if ( is_wp_error( $valid_output ) ) {
|
||||
return new WP_Error(
|
||||
'ability_invalid_output',
|
||||
sprintf(
|
||||
/* translators: %1$s ability name, %2$s error message. */
|
||||
__( 'Ability "%1$s" has invalid output. Reason: %2$s' ),
|
||||
$this->name,
|
||||
$valid_output->get_error_message()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the ability after input validation and running a permission check.
|
||||
* Before returning the return value, it also validates the output.
|
||||
*
|
||||
* @since 6.9.0
|
||||
*
|
||||
* @param mixed $input Optional. The input data for the ability. Default `null`.
|
||||
* @return mixed|WP_Error The result of the ability execution, or WP_Error on failure.
|
||||
*/
|
||||
public function execute( $input = null ) {
|
||||
$is_valid = $this->validate_input( $input );
|
||||
if ( is_wp_error( $is_valid ) ) {
|
||||
return $is_valid;
|
||||
}
|
||||
|
||||
$has_permissions = $this->check_permissions( $input );
|
||||
if ( true !== $has_permissions ) {
|
||||
if ( is_wp_error( $has_permissions ) ) {
|
||||
// Don't leak the permission check error to someone without the correct perms.
|
||||
_doing_it_wrong(
|
||||
__METHOD__,
|
||||
esc_html( $has_permissions->get_error_message() ),
|
||||
'6.9.0'
|
||||
);
|
||||
}
|
||||
|
||||
return new WP_Error(
|
||||
'ability_invalid_permissions',
|
||||
/* translators: %s ability name. */
|
||||
sprintf( __( 'Ability "%s" does not have necessary permission.' ), $this->name )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fires before an ability gets executed, after input validation and permissions check.
|
||||
*
|
||||
* @since 6.9.0
|
||||
*
|
||||
* @param string $ability_name The name of the ability.
|
||||
* @param mixed $input The input data for the ability.
|
||||
*/
|
||||
do_action( 'wp_before_execute_ability', $this->name, $input );
|
||||
|
||||
$result = $this->do_execute( $input );
|
||||
if ( is_wp_error( $result ) ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$is_valid = $this->validate_output( $result );
|
||||
if ( is_wp_error( $is_valid ) ) {
|
||||
return $is_valid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fires immediately after an ability finished executing.
|
||||
*
|
||||
* @since 6.9.0
|
||||
*
|
||||
* @param string $ability_name The name of the ability.
|
||||
* @param mixed $input The input data for the ability.
|
||||
* @param mixed $result The result of the ability execution.
|
||||
*/
|
||||
do_action( 'wp_after_execute_ability', $this->name, $input, $result );
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wakeup magic method.
|
||||
*
|
||||
* @since 6.9.0
|
||||
* @throws LogicException If the ability object is unserialized.
|
||||
* This is a security hardening measure to prevent unserialization of the ability.
|
||||
*/
|
||||
public function __wakeup(): void {
|
||||
throw new LogicException( __CLASS__ . ' should never be unserialized.' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Sleep magic method.
|
||||
*
|
||||
* @since 6.9.0
|
||||
* @throws LogicException If the ability object is serialized.
|
||||
* This is a security hardening measure to prevent serialization of the ability.
|
||||
*/
|
||||
public function __sleep(): array {
|
||||
throw new LogicException( __CLASS__ . ' should never be serialized' );
|
||||
}
|
||||
}
|
||||
@@ -483,6 +483,12 @@ function create_initial_rest_routes() {
|
||||
// Font Collections.
|
||||
$font_collections_controller = new WP_REST_Font_Collections_Controller();
|
||||
$font_collections_controller->register_routes();
|
||||
|
||||
// Abilities.
|
||||
$abilities_run_controller = new WP_REST_Abilities_V1_Run_Controller();
|
||||
$abilities_run_controller->register_routes();
|
||||
$abilities_list_controller = new WP_REST_Abilities_V1_List_Controller();
|
||||
$abilities_list_controller->register_routes();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,343 @@
|
||||
<?php
|
||||
/**
|
||||
* REST API list controller for Abilities API.
|
||||
*
|
||||
* @package WordPress
|
||||
* @subpackage Abilities_API
|
||||
* @since 6.9.0
|
||||
*/
|
||||
|
||||
declare( strict_types = 1 );
|
||||
|
||||
/**
|
||||
* Core controller used to access abilities via the REST API.
|
||||
*
|
||||
* @since 6.9.0
|
||||
*
|
||||
* @see WP_REST_Controller
|
||||
*/
|
||||
class WP_REST_Abilities_V1_List_Controller extends WP_REST_Controller {
|
||||
|
||||
/**
|
||||
* REST API namespace.
|
||||
*
|
||||
* @since 6.9.0
|
||||
* @var string
|
||||
*/
|
||||
protected $namespace = 'wp-abilities/v1';
|
||||
|
||||
/**
|
||||
* REST API base route.
|
||||
*
|
||||
* @since 6.9.0
|
||||
* @var string
|
||||
*/
|
||||
protected $rest_base = 'abilities';
|
||||
|
||||
/**
|
||||
* Registers the routes for abilities.
|
||||
*
|
||||
* @since 6.9.0
|
||||
*
|
||||
* @see register_rest_route()
|
||||
*/
|
||||
public function register_routes(): void {
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base,
|
||||
array(
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( $this, 'get_items' ),
|
||||
'permission_callback' => array( $this, 'get_items_permissions_check' ),
|
||||
'args' => $this->get_collection_params(),
|
||||
),
|
||||
'schema' => array( $this, 'get_public_item_schema' ),
|
||||
)
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/(?P<name>[a-zA-Z0-9\-\/]+)',
|
||||
array(
|
||||
'args' => array(
|
||||
'name' => array(
|
||||
'description' => __( 'Unique identifier for the ability.' ),
|
||||
'type' => 'string',
|
||||
'pattern' => '^[a-zA-Z0-9\-\/]+$',
|
||||
),
|
||||
),
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( $this, 'get_item' ),
|
||||
'permission_callback' => array( $this, 'get_item_permissions_check' ),
|
||||
),
|
||||
'schema' => array( $this, 'get_public_item_schema' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all abilities.
|
||||
*
|
||||
* @since 6.9.0
|
||||
*
|
||||
* @param WP_REST_Request $request Full details about the request.
|
||||
* @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' );
|
||||
}
|
||||
);
|
||||
|
||||
// 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 );
|
||||
}
|
||||
|
||||
$page = $request['page'];
|
||||
$per_page = $request['per_page'];
|
||||
$offset = ( $page - 1 ) * $per_page;
|
||||
|
||||
$total_abilities = count( $abilities );
|
||||
$max_pages = ceil( $total_abilities / $per_page );
|
||||
|
||||
if ( $request->get_method() === 'HEAD' ) {
|
||||
$response = new WP_REST_Response( array() );
|
||||
} else {
|
||||
$abilities = array_slice( $abilities, $offset, $per_page );
|
||||
|
||||
$data = array();
|
||||
foreach ( $abilities as $ability ) {
|
||||
$item = $this->prepare_item_for_response( $ability, $request );
|
||||
$data[] = $this->prepare_response_for_collection( $item );
|
||||
}
|
||||
|
||||
$response = rest_ensure_response( $data );
|
||||
}
|
||||
|
||||
$response->header( 'X-WP-Total', (string) $total_abilities );
|
||||
$response->header( 'X-WP-TotalPages', (string) $max_pages );
|
||||
|
||||
$query_params = $request->get_query_params();
|
||||
$base = add_query_arg( urlencode_deep( $query_params ), rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ) );
|
||||
|
||||
if ( $page > 1 ) {
|
||||
$prev_page = $page - 1;
|
||||
$prev_link = add_query_arg( 'page', $prev_page, $base );
|
||||
$response->link_header( 'prev', $prev_link );
|
||||
}
|
||||
|
||||
if ( $page < $max_pages ) {
|
||||
$next_page = $page + 1;
|
||||
$next_link = add_query_arg( 'page', $next_page, $base );
|
||||
$response->link_header( 'next', $next_link );
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a specific ability.
|
||||
*
|
||||
* @since 6.9.0
|
||||
*
|
||||
* @param WP_REST_Request $request Full details about the request.
|
||||
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
|
||||
*/
|
||||
public function get_item( $request ) {
|
||||
$ability = wp_get_ability( $request['name'] );
|
||||
if ( ! $ability || ! $ability->get_meta_item( 'show_in_rest' ) ) {
|
||||
return new WP_Error(
|
||||
'rest_ability_not_found',
|
||||
__( 'Ability not found.' ),
|
||||
array( 'status' => 404 )
|
||||
);
|
||||
}
|
||||
|
||||
$data = $this->prepare_item_for_response( $ability, $request );
|
||||
return rest_ensure_response( $data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given request has access to read ability items.
|
||||
*
|
||||
* @since 6.9.0
|
||||
*
|
||||
* @param WP_REST_Request $request Full details about the request.
|
||||
* @return bool True if the request has read access.
|
||||
*/
|
||||
public function get_items_permissions_check( $request ) {
|
||||
return current_user_can( 'read' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given request has access to read an ability item.
|
||||
*
|
||||
* @since 6.9.0
|
||||
*
|
||||
* @param WP_REST_Request $request Full details about the request.
|
||||
* @return bool True if the request has read access.
|
||||
*/
|
||||
public function get_item_permissions_check( $request ) {
|
||||
return current_user_can( 'read' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares an ability for response.
|
||||
*
|
||||
* @since 6.9.0
|
||||
*
|
||||
* @param WP_Ability $ability The ability object.
|
||||
* @param WP_REST_Request $request Request object.
|
||||
* @return WP_REST_Response Response object.
|
||||
*/
|
||||
public function prepare_item_for_response( $ability, $request ) {
|
||||
$data = array(
|
||||
'name' => $ability->get_name(),
|
||||
'label' => $ability->get_label(),
|
||||
'description' => $ability->get_description(),
|
||||
'category' => $ability->get_category(),
|
||||
'input_schema' => $ability->get_input_schema(),
|
||||
'output_schema' => $ability->get_output_schema(),
|
||||
'meta' => $ability->get_meta(),
|
||||
);
|
||||
|
||||
$context = $request['context'] ?? 'view';
|
||||
$data = $this->add_additional_fields_to_object( $data, $request );
|
||||
$data = $this->filter_response_by_context( $data, $context );
|
||||
|
||||
$response = rest_ensure_response( $data );
|
||||
|
||||
$fields = $this->get_fields_for_response( $request );
|
||||
if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
|
||||
$links = array(
|
||||
'self' => array(
|
||||
'href' => rest_url( sprintf( '%s/%s/%s', $this->namespace, $this->rest_base, $ability->get_name() ) ),
|
||||
),
|
||||
'collection' => array(
|
||||
'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ),
|
||||
),
|
||||
);
|
||||
|
||||
$links['wp:action-run'] = array(
|
||||
'href' => rest_url( sprintf( '%s/%s/%s/run', $this->namespace, $this->rest_base, $ability->get_name() ) ),
|
||||
);
|
||||
|
||||
$response->add_links( $links );
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the ability's schema, conforming to JSON Schema.
|
||||
*
|
||||
* @since 6.9.0
|
||||
*
|
||||
* @return array<string, mixed> Item schema data.
|
||||
*/
|
||||
public function get_item_schema(): array {
|
||||
$schema = array(
|
||||
'$schema' => 'http://json-schema.org/draft-04/schema#',
|
||||
'title' => 'ability',
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'name' => array(
|
||||
'description' => __( 'Unique identifier for the ability.' ),
|
||||
'type' => 'string',
|
||||
'context' => array( 'view', 'edit', 'embed' ),
|
||||
'readonly' => true,
|
||||
),
|
||||
'label' => array(
|
||||
'description' => __( 'Display label for the ability.' ),
|
||||
'type' => 'string',
|
||||
'context' => array( 'view', 'edit', 'embed' ),
|
||||
'readonly' => true,
|
||||
),
|
||||
'description' => array(
|
||||
'description' => __( 'Description of the ability.' ),
|
||||
'type' => 'string',
|
||||
'context' => array( 'view', 'edit' ),
|
||||
'readonly' => true,
|
||||
),
|
||||
'category' => array(
|
||||
'description' => __( 'Ability category this ability belongs to.' ),
|
||||
'type' => 'string',
|
||||
'context' => array( 'view', 'edit', 'embed' ),
|
||||
'readonly' => true,
|
||||
),
|
||||
'input_schema' => array(
|
||||
'description' => __( 'JSON Schema for the ability input.' ),
|
||||
'type' => 'object',
|
||||
'context' => array( 'view', 'edit' ),
|
||||
'readonly' => true,
|
||||
),
|
||||
'output_schema' => array(
|
||||
'description' => __( 'JSON Schema for the ability output.' ),
|
||||
'type' => 'object',
|
||||
'context' => array( 'view', 'edit' ),
|
||||
'readonly' => true,
|
||||
),
|
||||
'meta' => array(
|
||||
'description' => __( 'Meta information about the ability.' ),
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'annotations' => array(
|
||||
'description' => __( 'Annotations for the ability.' ),
|
||||
'type' => array( 'boolean', 'null' ),
|
||||
'default' => null,
|
||||
),
|
||||
),
|
||||
'context' => array( 'view', 'edit' ),
|
||||
'readonly' => true,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return $this->add_additional_fields_schema( $schema );
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the query params for collections.
|
||||
*
|
||||
* @since 6.9.0
|
||||
*
|
||||
* @return array<string, mixed> Collection parameters.
|
||||
*/
|
||||
public function get_collection_params(): array {
|
||||
return array(
|
||||
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
|
||||
'page' => array(
|
||||
'description' => __( 'Current page of the collection.' ),
|
||||
'type' => 'integer',
|
||||
'default' => 1,
|
||||
'minimum' => 1,
|
||||
),
|
||||
'per_page' => array(
|
||||
'description' => __( 'Maximum number of items to be returned in result set.' ),
|
||||
'type' => 'integer',
|
||||
'default' => 50,
|
||||
'minimum' => 1,
|
||||
'maximum' => 100,
|
||||
),
|
||||
'category' => array(
|
||||
'description' => __( 'Limit results to abilities in specific ability category.' ),
|
||||
'type' => 'string',
|
||||
'sanitize_callback' => 'sanitize_key',
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
<?php
|
||||
/**
|
||||
* REST API run controller for Abilities API.
|
||||
*
|
||||
* @package WordPress
|
||||
* @subpackage Abilities_API
|
||||
* @since 6.9.0
|
||||
*/
|
||||
|
||||
declare( strict_types = 1 );
|
||||
|
||||
/**
|
||||
* Core controller used to execute abilities via the REST API.
|
||||
*
|
||||
* @since 6.9.0
|
||||
*
|
||||
* @see WP_REST_Controller
|
||||
*/
|
||||
class WP_REST_Abilities_V1_Run_Controller extends WP_REST_Controller {
|
||||
|
||||
/**
|
||||
* REST API namespace.
|
||||
*
|
||||
* @since 6.9.0
|
||||
* @var string
|
||||
*/
|
||||
protected $namespace = 'wp-abilities/v1';
|
||||
|
||||
/**
|
||||
* REST API base route.
|
||||
*
|
||||
* @since 6.9.0
|
||||
* @var string
|
||||
*/
|
||||
protected $rest_base = 'abilities';
|
||||
|
||||
/**
|
||||
* Registers the routes for ability execution.
|
||||
*
|
||||
* @since 6.9.0
|
||||
*
|
||||
* @see register_rest_route()
|
||||
*/
|
||||
public function register_routes(): void {
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/(?P<name>[a-zA-Z0-9\-\/]+?)/run',
|
||||
array(
|
||||
'args' => array(
|
||||
'name' => array(
|
||||
'description' => __( 'Unique identifier for the ability.' ),
|
||||
'type' => 'string',
|
||||
'pattern' => '^[a-zA-Z0-9\-\/]+$',
|
||||
),
|
||||
),
|
||||
|
||||
// TODO: We register ALLMETHODS because at route registration time, we don't know which abilities
|
||||
// exist or their annotations (`destructive`, `idempotent`, `readonly`). This is due to WordPress
|
||||
// load order - routes are registered early, before plugins have registered their abilities.
|
||||
// This approach works but could be improved with lazy route registration or a different
|
||||
// architecture that allows type-specific routes after abilities are registered.
|
||||
// This was the same issue that we ended up seeing with the Feature API.
|
||||
array(
|
||||
'methods' => WP_REST_Server::ALLMETHODS,
|
||||
'callback' => array( $this, 'execute_ability' ),
|
||||
'permission_callback' => array( $this, 'check_ability_permissions' ),
|
||||
'args' => $this->get_run_args(),
|
||||
),
|
||||
'schema' => array( $this, 'get_run_schema' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes an ability.
|
||||
*
|
||||
* @since 6.9.0
|
||||
*
|
||||
* @param WP_REST_Request $request Full details about the request.
|
||||
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
|
||||
*/
|
||||
public function execute_ability( $request ) {
|
||||
$ability = wp_get_ability( $request['name'] );
|
||||
if ( ! $ability ) {
|
||||
return new WP_Error(
|
||||
'rest_ability_not_found',
|
||||
__( 'Ability not found.' ),
|
||||
array( 'status' => 404 )
|
||||
);
|
||||
}
|
||||
|
||||
$input = $this->get_input_from_request( $request );
|
||||
$result = $ability->execute( $input );
|
||||
if ( is_wp_error( $result ) ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
return rest_ensure_response( $result );
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates if the HTTP method matches the expected method for the ability based on its annotations.
|
||||
*
|
||||
* @since 6.9.0
|
||||
*
|
||||
* @param string $request_method The HTTP method of the request.
|
||||
* @param array<string, (null|bool)> $annotations The ability annotations.
|
||||
* @return true|WP_Error True on success, or WP_Error object on failure.
|
||||
*/
|
||||
public function validate_request_method( string $request_method, array $annotations ) {
|
||||
$expected_method = 'POST';
|
||||
if ( ! empty( $annotations['readonly'] ) ) {
|
||||
$expected_method = 'GET';
|
||||
} elseif ( ! empty( $annotations['destructive'] ) && ! empty( $annotations['idempotent'] ) ) {
|
||||
$expected_method = 'DELETE';
|
||||
}
|
||||
|
||||
if ( $expected_method === $request_method ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$error_message = __( 'Abilities that perform updates require POST method.' );
|
||||
if ( 'GET' === $expected_method ) {
|
||||
$error_message = __( 'Read-only abilities require GET method.' );
|
||||
} elseif ( 'DELETE' === $expected_method ) {
|
||||
$error_message = __( 'Abilities that perform destructive actions require DELETE method.' );
|
||||
}
|
||||
return new WP_Error(
|
||||
'rest_ability_invalid_method',
|
||||
$error_message,
|
||||
array( 'status' => 405 )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given request has permission to execute a specific ability.
|
||||
*
|
||||
* @since 6.9.0
|
||||
*
|
||||
* @param WP_REST_Request $request Full details about the request.
|
||||
* @return true|WP_Error True if the request has execution permission, WP_Error object otherwise.
|
||||
*/
|
||||
public function check_ability_permissions( $request ) {
|
||||
$ability = wp_get_ability( $request['name'] );
|
||||
if ( ! $ability || ! $ability->get_meta_item( 'show_in_rest' ) ) {
|
||||
return new WP_Error(
|
||||
'rest_ability_not_found',
|
||||
__( 'Ability not found.' ),
|
||||
array( 'status' => 404 )
|
||||
);
|
||||
}
|
||||
|
||||
$is_valid = $this->validate_request_method(
|
||||
$request->get_method(),
|
||||
$ability->get_meta_item( 'annotations' )
|
||||
);
|
||||
if ( is_wp_error( $is_valid ) ) {
|
||||
return $is_valid;
|
||||
}
|
||||
|
||||
$input = $this->get_input_from_request( $request );
|
||||
$is_valid = $ability->validate_input( $input );
|
||||
if ( is_wp_error( $is_valid ) ) {
|
||||
$is_valid->add_data( array( 'status' => 400 ) );
|
||||
return $is_valid;
|
||||
}
|
||||
|
||||
$result = $ability->check_permissions( $input );
|
||||
if ( is_wp_error( $result ) ) {
|
||||
$result->add_data( array( 'status' => rest_authorization_required_code() ) );
|
||||
return $result;
|
||||
}
|
||||
if ( ! $result ) {
|
||||
return new WP_Error(
|
||||
'rest_ability_cannot_execute',
|
||||
__( 'Sorry, you are not allowed to execute this ability.' ),
|
||||
array( 'status' => rest_authorization_required_code() )
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts input parameters from the request.
|
||||
*
|
||||
* @since 6.9.0
|
||||
*
|
||||
* @param WP_REST_Request $request The request object.
|
||||
* @return mixed|null The input parameters.
|
||||
*/
|
||||
private function get_input_from_request( $request ) {
|
||||
if ( in_array( $request->get_method(), array( 'GET', 'DELETE' ), true ) ) {
|
||||
// For GET and DELETE requests, look for 'input' query parameter.
|
||||
$query_params = $request->get_query_params();
|
||||
return $query_params['input'] ?? null;
|
||||
}
|
||||
|
||||
// For POST requests, look for 'input' in JSON body.
|
||||
$json_params = $request->get_json_params();
|
||||
return $json_params['input'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the arguments for ability execution endpoint.
|
||||
*
|
||||
* @since 6.9.0
|
||||
*
|
||||
* @return array<string, mixed> Arguments for the run endpoint.
|
||||
*/
|
||||
public function get_run_args(): array {
|
||||
return array(
|
||||
'input' => array(
|
||||
'description' => __( 'Input parameters for the ability execution.' ),
|
||||
'type' => array( 'integer', 'number', 'boolean', 'string', 'array', 'object', 'null' ),
|
||||
'default' => null,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the schema for ability execution endpoint.
|
||||
*
|
||||
* @since 6.9.0
|
||||
*
|
||||
* @return array<string, mixed> Schema for the run endpoint.
|
||||
*/
|
||||
public function get_run_schema(): array {
|
||||
return array(
|
||||
'$schema' => 'http://json-schema.org/draft-04/schema#',
|
||||
'title' => 'ability-execution',
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'result' => array(
|
||||
'description' => __( 'The result of the ability execution.' ),
|
||||
'type' => array( 'integer', 'number', 'boolean', 'string', 'array', 'object', 'null' ),
|
||||
'context' => array( 'view', 'edit' ),
|
||||
'readonly' => true,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@
|
||||
*
|
||||
* @global string $wp_version
|
||||
*/
|
||||
$wp_version = '6.9-alpha-61031';
|
||||
$wp_version = '6.9-alpha-61032';
|
||||
|
||||
/**
|
||||
* Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.
|
||||
|
||||
@@ -285,6 +285,11 @@ require ABSPATH . WPINC . '/nav-menu-template.php';
|
||||
require ABSPATH . WPINC . '/nav-menu.php';
|
||||
require ABSPATH . WPINC . '/admin-bar.php';
|
||||
require ABSPATH . WPINC . '/class-wp-application-passwords.php';
|
||||
require ABSPATH . WPINC . '/abilities-api/class-wp-ability-category.php';
|
||||
require ABSPATH . WPINC . '/abilities-api/class-wp-ability-categories-registry.php';
|
||||
require ABSPATH . WPINC . '/abilities-api/class-wp-ability.php';
|
||||
require ABSPATH . WPINC . '/abilities-api/class-wp-abilities-registry.php';
|
||||
require ABSPATH . WPINC . '/abilities-api.php';
|
||||
require ABSPATH . WPINC . '/rest-api.php';
|
||||
require ABSPATH . WPINC . '/rest-api/class-wp-rest-server.php';
|
||||
require ABSPATH . WPINC . '/rest-api/class-wp-rest-response.php';
|
||||
@@ -331,6 +336,8 @@ require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-navigation-fallback
|
||||
require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-font-families-controller.php';
|
||||
require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-font-faces-controller.php';
|
||||
require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-font-collections-controller.php';
|
||||
require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-abilities-v1-list-controller.php';
|
||||
require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-abilities-v1-run-controller.php';
|
||||
require ABSPATH . WPINC . '/rest-api/fields/class-wp-rest-meta-fields.php';
|
||||
require ABSPATH . WPINC . '/rest-api/fields/class-wp-rest-comment-meta-fields.php';
|
||||
require ABSPATH . WPINC . '/rest-api/fields/class-wp-rest-post-meta-fields.php';
|
||||
|
||||
Reference in New Issue
Block a user