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.