@@ -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