Skip to content

Commit 9645ea0

Browse files
Abilities API: Coerce REST run input to the ability input schema.
REST GET and DELETE requests deliver every query string parameter as a string ("10", "true") and comma-separated values as a single string, so without coercion an ability receives raw strings where its input schema declares integers, booleans, or arrays. This commit coerces the run input to the types declared by the ability's input schema before the ability executes. The coercion is registered as the `input` argument's `sanitize_callback`, so it runs once during `sanitize_params()` and both the permission and execution callbacks receive natively typed input from the same request object. Props jorgefilipecosta, gziolo, mukesh27. Fixes #65594. git-svn-id: https://develop.svn.wordpress.org/trunk@62674 602fd350-edb4-49c9-b593-d223f7449a82
1 parent 3ec6c92 commit 9645ea0

2 files changed

Lines changed: 615 additions & 3 deletions

File tree

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

Lines changed: 110 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,112 @@ private function get_input_from_request( $request ) {
223223
return $json_params['input'] ?? null;
224224
}
225225

226+
/**
227+
* Sanitizes the run input by coercing it to the ability's input schema.
228+
*
229+
* Registered as the `input` argument `sanitize_callback` so that both
230+
* `check_ability_permissions()` and `execute_ability()` receive natively typed input
231+
* regardless of transport.
232+
*
233+
* @since 7.1.0
234+
*
235+
* @see WP_REST_Abilities_V1_Run_Controller::coerce_input_to_schema()
236+
*
237+
* @param mixed $input Raw input extracted from the request.
238+
* @param WP_REST_Request $request The request object.
239+
* @return mixed Coerced input, or the raw input when it cannot be safely coerced.
240+
*/
241+
public function sanitize_input_for_ability( $input, $request ) {
242+
$ability = wp_get_ability( $request['name'] );
243+
if ( ! $ability instanceof WP_Ability ) {
244+
return $input;
245+
}
246+
247+
return $this->coerce_input_to_schema( $input, $ability );
248+
}
249+
250+
/**
251+
* Coerces raw request input to the types declared in the ability input schema.
252+
*
253+
* GET and DELETE deliver every scalar as a string ("10", "true") and a list as a single
254+
* comma-separated string, so without coercion an ability receives raw strings where its
255+
* schema declares integers, booleans, or arrays.
256+
*
257+
* Coercion never changes what validation accepts. Input is coerced only when
258+
* {@see WP_Ability::validate_input()} already accepts it, and any error surfaced while
259+
* sanitizing falls back to the raw input, so `validate_input()` stays the single authority
260+
* on what is rejected.
261+
*
262+
* @since 7.1.0
263+
*
264+
* @param mixed $input Raw input extracted from the request.
265+
* @param WP_Ability $ability The ability being executed.
266+
* @return mixed Coerced input, or the raw input when it cannot be safely coerced.
267+
*/
268+
private function coerce_input_to_schema( $input, WP_Ability $ability ) {
269+
if ( null === $input ) {
270+
return $input;
271+
}
272+
273+
$schema = $ability->get_input_schema();
274+
if ( empty( $schema ) ) {
275+
return $input;
276+
}
277+
278+
/*
279+
* Only coerce input that already validates. Sanitizing invalid input can silently
280+
* change which values are accepted -- `additionalProperties: false` strips unknown
281+
* keys, and a non-numeric string casts to 0 -- so leaving invalid input untouched
282+
* lets validate_input() reject it exactly as it does without coercion.
283+
*
284+
* validate_input() is asked rather than rest_validate_value_from_schema() so that the
285+
* `wp_ability_validate_input` filter decides what counts as valid here as well. A filter
286+
* that overrides a schema failure accepts the input, so the input is coerced; a filter
287+
* that rejects otherwise valid input leaves it untouched for validate_input() to report.
288+
*/
289+
if ( is_wp_error( $ability->validate_input( $input ) ) ) {
290+
return $input;
291+
}
292+
293+
$sanitized = rest_sanitize_value_from_schema( $input, $schema, 'input' );
294+
295+
/*
296+
* Sanitizing can still surface an error the lenient validation above did not, such as
297+
* items that are unique as strings but collide once cast to integers (`uniqueItems`).
298+
* The error may be returned at the top level or nested inside the returned array, so
299+
* scan recursively and fall back to the raw input on any error.
300+
*/
301+
if ( $this->input_contains_error( $sanitized ) ) {
302+
return $input;
303+
}
304+
305+
return $sanitized;
306+
}
307+
308+
/**
309+
* Determines whether a sanitized value is, or contains, a WP_Error.
310+
*
311+
* @since 7.1.0
312+
*
313+
* @param mixed $value The value to inspect.
314+
* @return bool True if the value is, or contains, a WP_Error.
315+
*/
316+
private function input_contains_error( $value ): bool {
317+
if ( is_wp_error( $value ) ) {
318+
return true;
319+
}
320+
321+
if ( is_array( $value ) ) {
322+
foreach ( $value as $item ) {
323+
if ( $this->input_contains_error( $item ) ) {
324+
return true;
325+
}
326+
}
327+
}
328+
329+
return false;
330+
}
331+
226332
/**
227333
* Retrieves the arguments for ability execution endpoint.
228334
*
@@ -233,9 +339,10 @@ private function get_input_from_request( $request ) {
233339
public function get_run_args(): array {
234340
return array(
235341
'input' => array(
236-
'description' => __( 'Input parameters for the ability execution.' ),
237-
'type' => array( 'integer', 'number', 'boolean', 'string', 'array', 'object', 'null' ),
238-
'default' => null,
342+
'description' => __( 'Input parameters for the ability execution.' ),
343+
'type' => array( 'integer', 'number', 'boolean', 'string', 'array', 'object', 'null' ),
344+
'default' => null,
345+
'sanitize_callback' => array( $this, 'sanitize_input_for_ability' ),
239346
),
240347
);
241348
}

0 commit comments

Comments
 (0)