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.