Skip to content
Closed
Changes from 1 commit
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
42556b4
Abilities API: Implement server-side registry
gziolo Aug 8, 2025
c0efd2c
Sync latest changes from Abilities API repo
gziolo Aug 18, 2025
a0c1633
Bring the latest changes from Abilities API repo
gziolo Aug 26, 2025
85f3a49
Bring another set of upstream changes planned for v0.2.0 release
gziolo Sep 8, 2025
dbaf583
Sync changes from Abilities API targeted for v0.2.0
gziolo Sep 29, 2025
a2b852f
Sync changes from the `v0.3.0` release
gziolo Oct 14, 2025
fb3ac2a
Update `@since` to the next WP version
gziolo Oct 15, 2025
4ee5446
Apply suggestions from code review
gziolo Oct 16, 2025
f1c1dc8
Update src/wp-includes/abilities-api/class-wp-abilities-category-regi…
gziolo Oct 16, 2025
fd37d48
Update src/wp-includes/rest-api/endpoints/class-wp-rest-abilities-lis…
gziolo Oct 16, 2025
38de7a7
Expand PHPDoc for array params
gziolo Oct 17, 2025
abc62fe
Address feedback from review regarding coding style
gziolo Oct 17, 2025
f5ad2b7
Fix PHPDoc for arrays
gziolo Oct 17, 2025
988f5e5
Fix Method ReflectionProperty::setAccessible() is deprecated since 8.…
gziolo Oct 17, 2025
61bf9fe
Prefix all filters with `wp_`
gziolo Oct 17, 2025
8e75dcb
Remove PHPStan comments in PHPDoc
gziolo Oct 17, 2025
4242735
Remove `instructions` annotation and change default for `destructive`…
gziolo Oct 17, 2025
bff3e7f
Add tests cases covering context and filtering by fields in REST API
gziolo Oct 17, 2025
533eb68
Use consistently ability category/categories
gziolo Oct 17, 2025
19f1a61
Introduce `wp_has_ability()` and `wp_has_ability_category()` public m…
gziolo Oct 18, 2025
de07c8c
Improve support for annotations, including DELETE handling in REST API
gziolo Oct 18, 2025
1e54797
Fix bug in the permission check for ability in REST API run controller
gziolo Oct 18, 2025
57d8eda
Improve handling for init hooks related to abilities and their catego…
gziolo Oct 19, 2025
6cb5d7c
Apply suggestions from code review
gziolo Oct 20, 2025
7f68dd9
Update src/wp-includes/abilities-api/class-wp-ability.php
gziolo Oct 20, 2025
b36ea85
Apply suggestions from code review
gziolo Oct 21, 2025
e124094
Rename REST routes and controllers
gziolo Oct 21, 2025
1dec6a7
Enforce `init` hook before using ability related registries
gziolo Oct 21, 2025
2dbde17
Increase testing code coverage for required `init` action check in ab…
gziolo Oct 21, 2025
bcc6ff9
Promote `validate_input` to public method and refactor usage
gziolo Oct 21, 2025
88b4a6f
Fix test for expected routes in REST API schema
gziolo Oct 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Fix bug in the permission check for ability in REST API run controller
  • Loading branch information
gziolo committed Oct 21, 2025
commit 1e54797715cc2ee7b7a2d1523f4db9e8e30b46bb
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ public function register_routes(): void {
// This was the same issue that we ended up seeing with the Feature API.
array(
'methods' => WP_REST_Server::ALLMETHODS,
'callback' => array( $this, 'run_ability_with_method_check' ),
'permission_callback' => array( $this, 'run_ability_permissions_check' ),
'callback' => array( $this, 'execute_ability' ),
'permission_callback' => array( $this, 'check_ability_permissions' ),
'args' => $this->get_run_args(),
),
'schema' => array( $this, 'get_run_schema' ),
Expand All @@ -72,16 +72,15 @@ public function register_routes(): void {
}

/**
* Executes an ability with HTTP method validation.
* Executes an ability.
*
* @since 6.9.0
*
* @param WP_REST_Request<array<string, mixed>> $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function run_ability_with_method_check( $request ) {
public function execute_ability( $request ) {
$ability = wp_get_ability( $request->get_param( 'name' ) );

if ( ! $ability ) {
return new WP_Error(
'rest_ability_not_found',
Expand All @@ -90,60 +89,50 @@ public function run_ability_with_method_check( $request ) {
);
}

// Check if the HTTP method matches the ability annotations.
$annotations = $ability->get_meta_item( '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->get_method() ) {
$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.' );
$input = $this->get_input_from_request( $request );
$result = $ability->execute( $input );
if ( is_wp_error( $result ) ) {
if ( 'ability_invalid_input' === $result->get_error_code() ) {
$result->add_data( array( 'status' => 400 ) );
}
return new WP_Error(
'rest_ability_invalid_method',
$error_message,
array( 'status' => 405 )
);
return $result;
}

return $this->run_ability( $request );
return rest_ensure_response( $result );
}

/**
* Executes an ability.
* Validates if the HTTP method matches the expected method for the ability based on its annotations.
*
* @since 6.9.0
*
* @param WP_REST_Request<array<string, mixed>> $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
* @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 run_ability( $request ) {
$ability = wp_get_ability( $request->get_param( 'name' ) );
if ( ! $ability ) {
return new WP_Error(
'rest_ability_not_found',
__( 'Ability not found.' ),
array( 'status' => 404 )
);
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';
}

$input = $this->get_input_from_request( $request );
$result = $ability->execute( $input );
if ( is_wp_error( $result ) ) {
if ( 'ability_invalid_input' === $result->get_error_code() ) {
$result->add_data( array( 'status' => 400 ) );
}
return $result;
if ( $expected_method === $request_method ) {
return true;
}

return rest_ensure_response( $result );
$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 )
);
}

/**
Expand All @@ -154,7 +143,7 @@ public function run_ability( $request ) {
* @param WP_REST_Request<array<string, mixed>> $request Full details about the request.
* @return true|WP_Error True if the request has execution permission, WP_Error object otherwise.
*/
public function run_ability_permissions_check( $request ) {
public function check_ability_permissions( $request ) {
$ability = wp_get_ability( $request->get_param( 'name' ) );
if ( ! $ability || ! $ability->get_meta_item( 'show_in_rest' ) ) {
return new WP_Error(
Expand All @@ -164,8 +153,23 @@ public function run_ability_permissions_check( $request ) {
);
}

$input = $this->get_input_from_request( $request );
if ( ! $ability->check_permissions( $input ) ) {
$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 );
$result = $ability->check_permissions( $input );
if ( is_wp_error( $result ) ) {
if ( 'ability_invalid_input' === $result->get_error_code() ) {
$result->add_data( array( 'status' => 400 ) );
}
return $result;
}
if ( ! $result ) {
return new WP_Error(
'rest_ability_cannot_execute',
__( 'Sorry, you are not allowed to execute this ability.' ),
Expand All @@ -185,7 +189,7 @@ public function run_ability_permissions_check( $request ) {
* @return mixed|null The input parameters.
*/
private function get_input_from_request( $request ) {
if ( in_array( $request->get_method(), array( 'GET', 'DELETE' ) ) ) {
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;
Expand Down