Skip to content

Commit ce66582

Browse files
authored
Merge branch 'WordPress:trunk' into trunk
2 parents 92a7d59 + 9645ea0 commit ce66582

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)