Commit eb8694b4fe for wordpress.org
commit eb8694b4feeff0d5156a2c358692bae82454e2dd
Author: gziolo <gziolo@git.wordpress.org>
Date: Fri Nov 6 08:38:09 2026 +0000
Abilities API: Document input preparation for direct permission checks
`WP_Ability::check_permissions()` does not prepare the input. Callers outside of `WP_Ability::execute()` must run `normalize_input()`, `validate_input()`, and `coerce_input()` first, or the permission callback may get different input than during `execute()`.
Developed in https://github.com/WordPress/wordpress-develop/pull/15514.
Props euvoor, gziolo, zaguiini, jorbin.
See #66482.
Built from https://develop.svn.wordpress.org/trunk@65306
git-svn-id: http://core.svn.wordpress.org/trunk@64446 1a063a9b-81f0-0310-95a4-ce76da25c4cd
diff --git a/wp-includes/abilities-api.php b/wp-includes/abilities-api.php
index 764261d331..f50531e5b5 100644
--- a/wp-includes/abilities-api.php
+++ b/wp-includes/abilities-api.php
@@ -199,9 +199,10 @@ declare( strict_types = 1 );
* #### Execute Callback
*
* The execute callback performs the ability's core functionality. It receives
- * the normalized, validated 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 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`.
*
* function my_plugin_analyze_text( string $input ): string|WP_Error {
* $score = My_Plugin::perform_sentiment_analysis( $input );
@@ -224,12 +225,12 @@ declare( strict_types = 1 );
* For an ability that everyone may execute, use `__return_true` as the callback
* with no required capabilities.
*
- * The permission callback determines whether the ability can be executed.
- * It receives the same input as the execute callback and must return a
- * boolean or `WP_Error`. 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:
+ * 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:
*
* function my_plugin_can_analyze_text( string $input ): bool|WP_Error {
* // Only administrators can analyze long texts.
@@ -241,9 +242,13 @@ declare( strict_types = 1 );
*
* ### Execution Lifecycle
*
- * Call `WP_Ability::execute()` to normalize and validate 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 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.
+ *
+ * Integrations that call `WP_Ability::check_permissions()` directly must prepare
+ * the input first. See `WP_Ability::check_permissions()` for the required steps.
*
* Hooks run in this order, and processing stops at the first failure:
*
@@ -251,7 +256,9 @@ declare( strict_types = 1 );
* 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.
+ * 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.
diff --git a/wp-includes/abilities-api/class-wp-ability.php b/wp-includes/abilities-api/class-wp-ability.php
index a9f53586d4..d27d4c2cb5 100644
--- a/wp-includes/abilities-api/class-wp-ability.php
+++ b/wp-includes/abilities-api/class-wp-ability.php
@@ -566,9 +566,9 @@ class WP_Ability {
* Coerces validated input to the types declared in 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().
+ * `text-field` and sanitizing URLs for `uri`, using `rest_sanitize_value_from_schema()`.
*
- * Call validate_input() before this method. Coercing unvalidated input can
+ * 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.
*
@@ -577,10 +577,10 @@ class WP_Ability {
*
* @since 7.2.0
*
- * @see validate_input()
+ * @see WP_Ability::validate_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 Coerced input, or `WP_Error` when it cannot be safely coerced.
*/
public function coerce_input( $input ) {
$schema = $this->get_input_schema();
@@ -751,8 +751,20 @@ class WP_Ability {
/**
* Checks whether the ability has the necessary permissions.
*
- * Please note that input is not automatically validated against the input schema.
- * Use `validate_input()` method to validate input before calling this method if needed.
+ * 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.
*
* The checks run in this order:
*
@@ -768,9 +780,13 @@ class WP_Ability {
* @since 7.1.0 Added the `wp_ability_permission_result` filter.
* @since 7.2.0 Added the `required_capabilities` check.
*
- * @see validate_input()
+ * @see WP_Ability::normalize_input()
+ * @see WP_Ability::validate_input()
+ * @see WP_Ability::coerce_input()
+ * @see WP_Ability::execute()
*
- * @param mixed $input Optional. The valid input data for permission checking. Default `null`.
+ * @param mixed $input Optional. The normalized, validated, and coerced input data for permission checking.
+ * Default `null`.
* @return bool|WP_Error Whether the ability has the necessary permission.
*/
public function check_permissions( $input = null ) {
diff --git a/wp-includes/version.php b/wp-includes/version.php
index b92ea5aa39..a0870d4cb8 100644
--- a/wp-includes/version.php
+++ b/wp-includes/version.php
@@ -16,7 +16,7 @@
*
* @global string $wp_version
*/
-$wp_version = '7.2-beta4-65303';
+$wp_version = '7.2-beta4-65306';
/**
* Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.