Commit 6463741e8b for wordpress.org

commit 6463741e8ba4084053542be45242ede87d6d6ee1
Author: gziolo <gziolo@git.wordpress.org>
Date:   Tue Nov 10 05:19:01 2026 +0000

    Abilities API: Replace `coerce_input()` with `prepare_input()`

    Add a public `WP_Ability::prepare_input()` method that normalizes, validates, and sanitizes input in one call. Use it in `WP_Ability::execute()` and the REST run controller.

    Rename `coerce_input()` to the protected `sanitize_input()`, so sanitization cannot run on input that was not validated. `coerce_input()` was not released yet, so this is not a breaking change.

    Developed in https://github.com/WordPress/wordpress-develop/pull/15685.
    Follow-up to [65044], [65306].

    Props gziolo, jorgefilipecosta, justlevine, zaguiini.
    See #66383.
    Fixes #66482.



    Built from https://develop.svn.wordpress.org/trunk@65370


    git-svn-id: http://core.svn.wordpress.org/trunk@64509 1a063a9b-81f0-0310-95a4-ce76da25c4cd

diff --git a/wp-includes/abilities-api.php b/wp-includes/abilities-api.php
index 6647aa7c2b..50f4b6ac05 100644
--- a/wp-includes/abilities-api.php
+++ b/wp-includes/abilities-api.php
@@ -200,10 +200,10 @@ declare( strict_types = 1 );
  * #### Execute Callback
  *
  * The execute callback performs the ability's core functionality. It receives
- * the normalized, validated, and coerced input when an input schema is defined,
- * and no arguments otherwise. It returns either a result or `WP_Error` on failure.
- * Exceptions thrown by execute or permission callbacks are caught and converted
- * to a `WP_Error`.
+ * the input prepared by `WP_Ability::prepare_input()` when an input schema is
+ * defined, and no arguments otherwise. It returns either a result or `WP_Error`
+ * on failure. Exceptions thrown by execute or permission callbacks are caught
+ * and converted to a `WP_Error`.
  *
  *     function my_plugin_analyze_text( string $input ): string|WP_Error {
  *         $score = My_Plugin::perform_sentiment_analysis( $input );
@@ -228,10 +228,10 @@ declare( strict_types = 1 );
  *
  * The permission callback determines whether the ability can be executed and
  * must return a boolean or `WP_Error`. When called through `WP_Ability::execute()`,
- * it receives the same normalized, validated, and coerced input as the execute
- * callback. Common use cases include checking user capabilities, validating API
- * keys, or verifying system state. In the example above, every execution requires
- * `edit_posts`, and the callback only adds the check that depends on the input:
+ * it receives the same prepared input as the execute callback. Common use cases
+ * include checking user capabilities, validating API keys, or verifying system
+ * state. In the example above, every execution requires `edit_posts`, and the
+ * callback only adds the check that depends on the input:
  *
  *     function my_plugin_can_analyze_text( string $input ): bool|WP_Error {
  *         // Only administrators can analyze long texts.
@@ -243,26 +243,24 @@ declare( strict_types = 1 );
  *
  * ### Execution Lifecycle
  *
- * Call `WP_Ability::execute()` to normalize, validate, and coerce input, check
- * permissions, invoke the execute callback, and validate output. It returns the
- * result or a `WP_Error` on failure. Custom ability classes can override this
- * behavior.
+ * Call `WP_Ability::execute()` to prepare the input, check permissions, invoke
+ * the execute callback, and validate output. It returns the result or a
+ * `WP_Error` on failure. Custom ability classes can override this behavior.
  *
- * Integrations that call `WP_Ability::check_permissions()` directly must prepare
- * the input first. See `WP_Ability::check_permissions()` for the required steps.
+ * Integrations that call `WP_Ability::check_permissions()` directly must pass it
+ * the input returned by `WP_Ability::prepare_input()`.
  *
  * Hooks run in this order, and processing stops at the first failure:
  *
  *  1. `wp_ability_invoked` action, with the raw input.
  *  2. `wp_pre_execute_ability` filter, which can short-circuit the rest of execution.
- *  3. `wp_ability_normalize_input`, `wp_ability_validate_input`, and
- *     `wp_ability_permission_result` filters. The input validation filter runs only
- *     when a non-empty input schema is defined. Input coercion has no filter. It runs
- *     between validation and the permission check, only when a non-empty input
- *     schema is defined.
- *  4. `wp_before_execute_ability` action.
- *  5. `wp_ability_execute_result` and `wp_ability_validate_output` filters.
- *  6. `wp_after_execute_ability` action.
+ *  3. `wp_ability_normalize_input` and `wp_ability_validate_input` filters, while the
+ *     input is prepared. The input validation filter runs only when a non-empty input
+ *     schema is defined. Sanitization has no filter.
+ *  4. `wp_ability_permission_result` filter.
+ *  5. `wp_before_execute_ability` action.
+ *  6. `wp_ability_execute_result` and `wp_ability_validate_output` filters.
+ *  7. `wp_after_execute_ability` action.
  *
  * ### Client Exposure
  *
diff --git a/wp-includes/abilities-api/class-wp-ability.php b/wp-includes/abilities-api/class-wp-ability.php
index d27d4c2cb5..6903f66d66 100644
--- a/wp-includes/abilities-api/class-wp-ability.php
+++ b/wp-includes/abilities-api/class-wp-ability.php
@@ -563,26 +563,26 @@ class WP_Ability {
 	}

 	/**
-	 * Coerces validated input to the types declared in the input schema.
+	 * Sanitizes validated input against the input schema.
 	 *
-	 * Also applies schema format sanitization, such as stripping tags for
-	 * `text-field` and sanitizing URLs for `uri`, using `rest_sanitize_value_from_schema()`.
+	 * Converts values to the types declared in the input schema, and applies schema
+	 * format sanitization, such as stripping tags for `text-field` and sanitizing
+	 * URLs for `uri`, using `rest_sanitize_value_from_schema()`.
 	 *
-	 * Call `validate_input()` before this method. Coercing unvalidated input can
-	 * remove unknown properties or cast invalid values, hiding validation errors.
-	 * This method does not apply defaults or run input validation filters.
+	 * The input must be validated first. Sanitizing unvalidated input can remove
+	 * unknown properties or cast invalid values, hiding validation errors.
 	 *
 	 * If sanitization produces an error, including in a nested value, an invalid
-	 * input error is returned to prevent execution with uncoerced values.
+	 * input error is returned to prevent execution with unsanitized values.
 	 *
 	 * @since 7.2.0
 	 *
-	 * @see WP_Ability::validate_input()
+	 * @see WP_Ability::prepare_input()
 	 *
 	 * @param mixed $input Validated ability input.
-	 * @return mixed|WP_Error Coerced input, or `WP_Error` when it cannot be safely coerced.
+	 * @return mixed|WP_Error Sanitized input, or `WP_Error` when it cannot be safely sanitized.
 	 */
-	public function coerce_input( $input ) {
+	protected function sanitize_input( $input ) {
 		$schema = $this->get_input_schema();
 		if ( null === $input || empty( $schema ) ) {
 			return $input;
@@ -590,7 +590,7 @@ class WP_Ability {

 		$sanitized = rest_sanitize_value_from_schema( $input, $schema, 'input' );

-		// Values unique before coercion can collide after casting, even in nested arrays.
+		// Values unique before sanitization can collide after casting, even in nested arrays.
 		$error = self::get_input_error( $sanitized );
 		if ( is_wp_error( $error ) ) {
 			return new WP_Error(
@@ -698,6 +698,37 @@ class WP_Ability {
 		return true;
 	}

+	/**
+	 * Prepares input for the ability.
+	 *
+	 * Normalizes the input, validates it against the input schema, and then sanitizes
+	 * it. `execute()` passes the result to the permission and execute callbacks.
+	 * Validation runs before sanitization, so sanitization cannot make invalid input
+	 * valid.
+	 *
+	 * @since 7.2.0
+	 *
+	 * @see WP_Ability::normalize_input()
+	 * @see WP_Ability::validate_input()
+	 * @see WP_Ability::sanitize_input()
+	 *
+	 * @param mixed $input Optional. The raw input provided for the ability. Default `null`.
+	 * @return mixed|WP_Error The prepared input, or `WP_Error` when any step fails.
+	 */
+	public function prepare_input( $input = null ) {
+		$input = $this->normalize_input( $input );
+		if ( is_wp_error( $input ) ) {
+			return $input;
+		}
+
+		$is_valid = $this->validate_input( $input );
+		if ( is_wp_error( $is_valid ) ) {
+			return $is_valid;
+		}
+
+		return $this->sanitize_input( $input );
+	}
+
 	/**
 	 * Checks whether a value is a list of non-empty capability strings.
 	 *
@@ -754,17 +785,10 @@ class WP_Ability {
 	 * This method does not prepare the input. Use `execute()` to run the full ability
 	 * lifecycle, which prepares the input before calling this method.
 	 *
-	 * When calling this method directly, prepare the input in the same order as
-	 * `execute()`, and stop if any step returns a `WP_Error`:
-	 *
-	 *  1. `normalize_input()` applies the schema default and returns the input.
-	 *  2. `validate_input()` checks the input against the schema and returns `true`.
-	 *  3. `coerce_input()` converts values to the schema types and returns the input
-	 *     to pass to this method.
-	 *
-	 * Skipping these steps means the permission callback may receive different input
-	 * than it does during `execute()`, such as a missing schema default or a string
-	 * instead of an integer.
+	 * When calling this method directly, pass the input returned by `prepare_input()`,
+	 * and stop if it returns a `WP_Error`. Skipping this step means the permission
+	 * callback may receive different input than it does during `execute()`, such as
+	 * a missing schema default or a string instead of an integer.
 	 *
 	 * The checks run in this order:
 	 *
@@ -780,12 +804,10 @@ class WP_Ability {
 	 * @since 7.1.0 Added the `wp_ability_permission_result` filter.
 	 * @since 7.2.0 Added the `required_capabilities` check.
 	 *
-	 * @see WP_Ability::normalize_input()
-	 * @see WP_Ability::validate_input()
-	 * @see WP_Ability::coerce_input()
+	 * @see WP_Ability::prepare_input()
 	 * @see WP_Ability::execute()
 	 *
-	 * @param mixed $input Optional. The normalized, validated, and coerced input data for permission checking.
+	 * @param mixed $input Optional. The input prepared by `prepare_input()` for permission checking.
 	 *                     Default `null`.
 	 * @return bool|WP_Error Whether the ability has the necessary permission.
 	 */
@@ -884,7 +906,7 @@ class WP_Ability {
 		 * @param mixed      $result       The result returned by the registered `execute_callback`,
 		 *                                 or a `WP_Error` if execution failed.
 		 * @param string     $ability_name The name of the ability.
-		 * @param mixed      $input        The normalized input data.
+		 * @param mixed      $input        The prepared input data.
 		 * @param WP_Ability $ability      The ability instance.
 		 */
 		return apply_filters( 'wp_ability_execute_result', $result, $this->name, $input, $this );
@@ -946,13 +968,13 @@ class WP_Ability {
 	}

 	/**
-	 * Executes the ability after input validation, coercion, and a permission check.
+	 * Executes the ability after preparing the input and checking permissions.
 	 * Before returning the return value, it also validates the output.
 	 *
 	 * @since 6.9.0
 	 * @since 7.1.0 Added the `wp_ability_invoked` action.
 	 * @since 7.1.0 Added the `wp_pre_execute_ability` filter.
-	 * @since 7.2.0 Coerces validated input before permission checks and execution.
+	 * @since 7.2.0 Prepares the input with `prepare_input()`, which also sanitizes it.
 	 *
 	 * @param mixed $input Optional. The input data for the ability. Default `null`.
 	 * @return mixed|WP_Error The result of the ability execution, or WP_Error on failure.
@@ -979,7 +1001,7 @@ class WP_Ability {
 		 * Filters whether to short-circuit ability execution.
 		 *
 		 * Returning a value other than the received default bypasses the rest of `execute()` —
-		 * input normalization, input validation, permission checks, the registered execute callback,
+		 * input preparation, permission checks, the registered execute callback,
 		 * output validation, and the surrounding actions — and the value is returned to the caller
 		 * as-is. Useful for cached responses, rate limiting, maintenance mode, and test mocking.
 		 *
@@ -1002,17 +1024,7 @@ class WP_Ability {
 			return $pre;
 		}

-		$input = $this->normalize_input( $input );
-		if ( is_wp_error( $input ) ) {
-			return $input;
-		}
-
-		$is_valid = $this->validate_input( $input );
-		if ( is_wp_error( $is_valid ) ) {
-			return $is_valid;
-		}
-
-		$input = $this->coerce_input( $input );
+		$input = $this->prepare_input( $input );
 		if ( is_wp_error( $input ) ) {
 			return $input;
 		}
@@ -1036,13 +1048,13 @@ class WP_Ability {
 		}

 		/**
-		 * Fires before an ability gets executed, after input validation and permissions check.
+		 * Fires before an ability gets executed, after input preparation and permissions check.
 		 *
 		 * @since 6.9.0
 		 * @since 7.1.0 Added the `$ability` parameter.
 		 *
 		 * @param string     $ability_name The name of the ability.
-		 * @param mixed      $input        The validated and coerced input data for the ability.
+		 * @param mixed      $input        The prepared input data for the ability.
 		 * @param WP_Ability $ability      The ability instance.
 		 */
 		do_action( 'wp_before_execute_ability', $this->name, $input, $this );
@@ -1064,7 +1076,7 @@ class WP_Ability {
 		 * @since 7.1.0 Added the `$ability` parameter.
 		 *
 		 * @param string     $ability_name The name of the ability.
-		 * @param mixed      $input        The validated and coerced input data for the ability.
+		 * @param mixed      $input        The prepared input data for the ability.
 		 * @param mixed      $result       The result of the ability execution.
 		 * @param WP_Ability $ability      The ability instance.
 		 */
diff --git a/wp-includes/rest-api/endpoints/class-wp-rest-abilities-v1-run-controller.php b/wp-includes/rest-api/endpoints/class-wp-rest-abilities-v1-run-controller.php
index 631bc59bf1..8ed345bbc2 100644
--- a/wp-includes/rest-api/endpoints/class-wp-rest-abilities-v1-run-controller.php
+++ b/wp-includes/rest-api/endpoints/class-wp-rest-abilities-v1-run-controller.php
@@ -172,7 +172,7 @@ class WP_REST_Abilities_V1_Run_Controller extends WP_REST_Controller {
 			return $is_valid;
 		}

-		$input = $this->prepare_input_for_ability( $ability, $this->get_input_from_request( $request ) );
+		$input = $ability->prepare_input( $this->get_input_from_request( $request ) );
 		if ( is_wp_error( $input ) ) {
 			return $this->ensure_error_status( $input, 400 );
 		}
@@ -249,30 +249,6 @@ class WP_REST_Abilities_V1_Run_Controller extends WP_REST_Controller {
 		return $input;
 	}

-	/**
-	 * Normalizes, validates, and coerces input for an ability.
-	 *
-	 * @since 7.2.0
-	 *
-	 * @param WP_Ability $ability The ability to prepare input for.
-	 * @param mixed      $input   Input to prepare.
-	 * @return mixed|WP_Error Prepared input, or `WP_Error` when normalization, validation, or coercion fails.
-	 */
-	private function prepare_input_for_ability( WP_Ability $ability, $input ) {
-		$input = $ability->normalize_input( $input );
-		if ( is_wp_error( $input ) ) {
-			return $input;
-		}
-
-		// Validate before coercion so sanitization cannot rescue invalid input.
-		$is_valid = $ability->validate_input( $input );
-		if ( is_wp_error( $is_valid ) ) {
-			return $is_valid;
-		}
-
-		return $ability->coerce_input( $input );
-	}
-
 	/**
 	 * Retrieves the arguments for ability execution endpoint.
 	 *
diff --git a/wp-includes/version.php b/wp-includes/version.php
index b020f0e145..dba5cd40e6 100644
--- a/wp-includes/version.php
+++ b/wp-includes/version.php
@@ -16,7 +16,7 @@
  *
  * @global string $wp_version
  */
-$wp_version = '7.2-beta4-65369';
+$wp_version = '7.2-beta4-65370';

 /**
  * Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.