diff --git a/src/wp-includes/abilities-api/class-wp-ability.php b/src/wp-includes/abilities-api/class-wp-ability.php index 0c7310f0343bb..71b2ea25f1885 100644 --- a/src/wp-includes/abilities-api/class-wp-ability.php +++ b/src/wp-includes/abilities-api/class-wp-ability.php @@ -400,6 +400,31 @@ public function get_meta_item( string $key, $default_value = null ) { return array_key_exists( $key, $this->meta ) ? $this->meta[ $key ] : $default_value; } + /** + * Normalizes the input for the ability, applying the default value from the input schema when needed. + * + * When no input is provided and the input schema is defined with a top-level `default` key, this method returns + * the value of that key. If the input schema does not define a `default`, or if the input schema is empty, + * this method returns null. If input is provided, it is returned as-is. + * + * @since 6.9.0 + * + * @param mixed $input Optional. The raw input provided for the ability. Default `null`. + * @return mixed The same input, or the default from schema, or `null` if default not set. + */ + public function normalize_input( $input = null ) { + if ( null !== $input ) { + return $input; + } + + $input_schema = $this->get_input_schema(); + if ( ! empty( $input_schema ) && array_key_exists( 'default', $input_schema ) ) { + return $input_schema['default']; + } + + return null; + } + /** * Validates input data against the input schema. * @@ -536,6 +561,7 @@ protected function validate_output( $output ) { * @return mixed|WP_Error The result of the ability execution, or WP_Error on failure. */ public function execute( $input = null ) { + $input = $this->normalize_input( $input ); $is_valid = $this->validate_input( $input ); if ( is_wp_error( $is_valid ) ) { return $is_valid; diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-abilities-v1-run-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-abilities-v1-run-controller.php index c8243526be723..a58888bebd9ea 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-abilities-v1-run-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-abilities-v1-run-controller.php @@ -159,6 +159,7 @@ public function check_ability_permissions( $request ) { } $input = $this->get_input_from_request( $request ); + $input = $ability->normalize_input( $input ); $is_valid = $ability->validate_input( $input ); if ( is_wp_error( $is_valid ) ) { $is_valid->add_data( array( 'status' => 400 ) ); diff --git a/tests/phpunit/tests/rest-api/wpRestAbilitiesV1RunController.php b/tests/phpunit/tests/rest-api/wpRestAbilitiesV1RunController.php index f610bf6a026e3..40372fe57b4be 100644 --- a/tests/phpunit/tests/rest-api/wpRestAbilitiesV1RunController.php +++ b/tests/phpunit/tests/rest-api/wpRestAbilitiesV1RunController.php @@ -921,12 +921,11 @@ public function test_ability_without_annotations_defaults_to_post_method(): void } /** - * Test edge case with empty input for both GET and POST methods. + * Test edge case with empty input for GET method. * * @ticket 64098 */ - public function test_empty_input_handling(): void { - // Registers abilities for empty input testing. + public function test_empty_input_handling_get_method(): void { wp_register_ability( 'test/read-only-empty', array( @@ -946,6 +945,55 @@ public function test_empty_input_handling(): void { ) ); + // Tests GET with no input parameter. + $get_request = new WP_REST_Request( 'GET', '/wp-abilities/v1/abilities/test/read-only-empty/run' ); + $get_response = $this->server->dispatch( $get_request ); + $this->assertEquals( 200, $get_response->get_status() ); + $this->assertTrue( $get_response->get_data()['input_was_empty'] ); + } + + /** + * Test edge case with empty input for GET method, and normalized input using schema. + * + * @ticket 64098 + */ + public function test_empty_input_handling_get_method_with_normalized_input(): void { + wp_register_ability( + 'test/read-only-empty-array', + array( + 'label' => 'Read-only Empty Array', + 'description' => 'Read-only with inferred empty array input from schema.', + 'category' => 'general', + 'input_schema' => array( + 'type' => 'array', + 'default' => array(), + ), + 'execute_callback' => static function ( $input ) { + return is_array( $input ) && empty( $input ); + }, + 'permission_callback' => '__return_true', + 'meta' => array( + 'annotations' => array( + 'readonly' => true, + ), + 'show_in_rest' => true, + ), + ) + ); + + // Tests GET with no input parameter. + $get_request = new WP_REST_Request( 'GET', '/wp-abilities/v1/abilities/test/read-only-empty-array/run' ); + $get_response = $this->server->dispatch( $get_request ); + $this->assertEquals( 200, $get_response->get_status() ); + $this->assertTrue( $get_response->get_data() ); + } + + /** + * Test edge case with empty input for POST method. + * + * @ticket 64098 + */ + public function test_empty_input_handling_post_method(): void { wp_register_ability( 'test/regular-empty', array( @@ -962,12 +1010,6 @@ public function test_empty_input_handling(): void { ) ); - // Tests GET with no input parameter. - $get_request = new WP_REST_Request( 'GET', '/wp-abilities/v1/abilities/test/read-only-empty/run' ); - $get_response = $this->server->dispatch( $get_request ); - $this->assertEquals( 200, $get_response->get_status() ); - $this->assertTrue( $get_response->get_data()['input_was_empty'] ); - // Tests POST with no body. $post_request = new WP_REST_Request( 'POST', '/wp-abilities/v1/abilities/test/regular-empty/run' ); $post_request->set_header( 'Content-Type', 'application/json' );