Commit d86e2079ed for wordpress.org

commit d86e2079edbf869a0393ee99a91da19351420461
Author: jorgefilipecosta <jorgefilipecosta@git.wordpress.org>
Date:   Thu Oct 22 11:12:53 2026 +0000

    Abilities API: Replace the `author_slug` filter of `core/content-query` with an `author` object.

    The `core/content-query` ability filtered posts by author through a `where.author_slug` string, while each post returns its author as an `author` object with `id`, `name`, and `slug`. The REST API posts collection filters by author ID, which the ability did not take.
    Replace the filter with a `where.author` object that takes the `id` or the `slug` the `author` field returns, so a result's author can be given back as a filter.

    Developed in https://github.com/WordPress/wordpress-develop/pull/14673.

    Follow-up to [64388].

    Props jorgefilipecosta, gziolo.
    See #66268.
    Built from https://develop.svn.wordpress.org/trunk@64680


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

diff --git a/wp-includes/abilities/class-wp-abilities-content.php b/wp-includes/abilities/class-wp-abilities-content.php
index 725301fd6a..cf64f0a5f0 100644
--- a/wp-includes/abilities/class-wp-abilities-content.php
+++ b/wp-includes/abilities/class-wp-abilities-content.php
@@ -450,24 +450,72 @@ final class WP_Abilities_Content {
 			}
 		}

-		$author = null;
-		if ( isset( $where['author_slug'] ) ) {
+		/*
+		 * The author filter is an object with an `id` or a `slug`, the keys the `author`
+		 * field returns. Null, int, or false: no filter, the user ID to query, or a filter
+		 * that names no user, which matches no post (see below).
+		 */
+		$author_id = null;
+		if ( array_key_exists( 'author', $where ) ) {
 			if ( ! post_type_supports( $post_type, 'author' ) ) {
 				return $this->invalid_filter_error(
-					'where[author_slug]',
+					'where[author]',
 					/* translators: %s: Parameter. */
-					sprintf( __( 'The %s filter is only supported for post types that support authors.' ), 'author_slug' )
+					sprintf( __( 'The %s filter is only supported for post types that support authors.' ), 'author' )
 				);
 			}

-			$author = $this->get_author_by_slug( $where['author_slug'], $post_type_object );
-			if ( ! $author ) {
+			/*
+			 * An object without `id` or `slug`, such as `{}` or an empty string, and one with
+			 * an unknown key, such as a list or a misspelled property, name no user and must not
+			 * fall through to an unrestricted query, like an empty `ids` filter.
+			 */
+			$author_filter = $this->get_input_object( $where, 'author' );
+			if ( empty( $author_filter ) || ! empty( array_diff_key( $author_filter, array_flip( array( 'id', 'slug' ) ) ) ) ) {
 				return $this->invalid_filter_error(
-					'where[author_slug]',
+					'where[author]',
 					/* translators: %s: Parameter. */
-					sprintf( __( 'The %s filter must be the slug of an existing user.' ), 'author_slug' )
+					sprintf( __( 'The %s filter must be an object with an id or a slug.' ), 'author' )
 				);
 			}
+
+			/*
+			 * An `id` of 0 must not reach the query: WP_Query reads `author => 0` as no author
+			 * filter, which would return every post of the type.
+			 */
+			if ( array_key_exists( 'id', $author_filter ) ) {
+				$author_id = $this->parse_filter_int( $author_filter['id'], 1 );
+				if ( null === $author_id ) {
+					return $this->invalid_filter_error(
+						'where[author][id]',
+						/* translators: %s: Parameter. */
+						sprintf( __( 'The %s ID must be a positive integer.' ), 'author' )
+					);
+				}
+			}
+
+			if ( array_key_exists( 'slug', $author_filter ) ) {
+				if ( ! is_string( $author_filter['slug'] ) || '' === $author_filter['slug'] ) {
+					return $this->invalid_filter_error(
+						'where[author][slug]',
+						/* translators: %s: Parameter. */
+						sprintf( __( 'The %s slug must be a non-empty string.' ), 'author' )
+					);
+				}
+
+				/*
+				 * The slug is looked up as given, like the `slug` of the users endpoint: the
+				 * collation of the users table decides whether the case matters. A slug that
+				 * names no user, or names another user than `id`, matches no post, as a post
+				 * must match every given filter.
+				 */
+				$user = get_user_by( 'slug', $author_filter['slug'] );
+				if ( ! $user || ( null !== $author_id && $user->ID !== $author_id ) ) {
+					$author_id = false;
+				} else {
+					$author_id = $user->ID;
+				}
+			}
 		}

 		/*
@@ -561,14 +609,24 @@ final class WP_Abilities_Content {
 			$query_args['post_name__in'] = $slugs;
 		}

-		if ( null !== $author ) {
-			$query_args['author'] = $author->ID;
+		if ( is_int( $author_id ) ) {
+			$query_args['author'] = $author_id;
 		}

 		if ( null !== $parent ) {
 			$query_args['post_parent'] = $parent;
 		}

+		/*
+		 * A slug that names no user, or an ID and a slug that name different users, match no
+		 * post, like IDs and slugs that name no post, rather than failing the query. An `id` is
+		 * not looked up: the query matches the stored author ID, so the posts of a deleted user
+		 * still match it.
+		 */
+		if ( false === $author_id ) {
+			return $this->prepare_output( array(), 0, 0 );
+		}
+
 		$query = new WP_Query( $query_args );
 		$total = $this->get_query_total( $query, $query_args, $page );

@@ -745,50 +803,6 @@ final class WP_Abilities_Content {
 		return rest_is_field_included( $path, $fields );
 	}

-	/**
-	 * Looks up the user an author slug names.
-	 *
-	 * The slug is the user's nicename, which the REST API users endpoint returns as `slug`. It is
-	 * looked up as given, like that endpoint's `slug` filter, so the database collation decides
-	 * whether a variant in another case matches. A user the current user may not see is reported
-	 * like a missing one. The current user can see themselves, any user of the site when they can
-	 * list users, and authors with posts in a publicly viewable post type. A user who can edit
-	 * others' posts of the post type can also see any user of the site, since they may make any of
-	 * them the author, so for them the lookup does tell whether such an account exists.
-	 *
-	 * On multisite the lookup searches the whole network, so posts by authors who are not
-	 * members of the site, such as super admins, can still be filtered. Those users are only
-	 * visible as the current user or as authors with posts in a publicly viewable post type, so
-	 * the lookup does not tell whether an account exists elsewhere on the network.
-	 *
-	 * @since 7.2.0
-	 *
-	 * @param mixed        $slug             The author slug.
-	 * @param WP_Post_Type $post_type_object The post type the author is looked up for.
-	 * @return WP_User|null The user, or null when the slug does not name a visible user.
-	 */
-	private function get_author_by_slug( $slug, WP_Post_Type $post_type_object ): ?WP_User {
-		$user = is_string( $slug ) ? get_user_by( 'slug', $slug ) : false;
-		if ( ! $user ) {
-			return null;
-		}
-
-		if ( get_current_user_id() === $user->ID ) {
-			return $user;
-		}
-
-		// The capabilities only reveal users of the site, not of the whole network.
-		if ( is_user_member_of_blog( $user->ID )
-			&& ( current_user_can( 'list_users' ) || current_user_can( $post_type_object->cap->edit_others_posts ) )
-		) {
-			return $user;
-		}
-
-		$public_post_types = array_values( array_filter( get_post_types(), 'is_post_type_viewable' ) );
-
-		return ! empty( $public_post_types ) && count_user_posts( $user->ID, $public_post_types ) > 0 ? $user : null;
-	}
-
 	/**
 	 * Returns a post type exposed through the Abilities API.
 	 *
@@ -1063,7 +1077,7 @@ final class WP_Abilities_Content {
 				'properties'           => array(
 					'id'   => array(
 						'type'        => 'integer',
-						'description' => __( 'The user ID of the author.' ),
+						'description' => __( 'The user ID of the author, as the `author.id` filter takes it.' ),
 					),
 					'name' => array(
 						'type'        => array( 'string', 'null' ),
@@ -1071,7 +1085,7 @@ final class WP_Abilities_Content {
 					),
 					'slug' => array(
 						'type'        => array( 'string', 'null' ),
-						'description' => __( "The author's user slug (nicename), as the REST API users endpoint returns it in `slug`, and as the `author_slug` filter takes it. Null when the author no longer exists." ),
+						'description' => __( "The author's user slug (nicename), as the REST API users endpoint returns it in `slug`, and as the `author.slug` filter takes it. Null when the author no longer exists." ),
 					),
 				),
 				'description'          => __( 'The post author. Present when the post type supports authors.' ),
@@ -1137,7 +1151,7 @@ final class WP_Abilities_Content {
 	 * The input has three members: `where` selects the posts, `fields` selects the fields
 	 * returned for each post, and `pagination` selects the page of results. `where` holds
 	 * the post type to query in `type`, and any of the filters `ids`, `slugs`, `status`,
-	 * `author_slug`, and `parent`, which all combine. Every object sets
+	 * `author`, and `parent`, which all combine. Every object sets
 	 * `additionalProperties: false`, so an unknown key, such as a misspelled filter, fails
 	 * validation instead of being silently ignored.
 	 *
@@ -1158,12 +1172,12 @@ final class WP_Abilities_Content {
 					'additionalProperties' => false,
 					'required'             => array( 'type' ),
 					'properties'           => array(
-						'type'        => array(
+						'type'   => array(
 							'type'        => 'string',
 							'enum'        => $post_types,
 							'description' => __( 'Post type to query for readable posts.' ),
 						),
-						'ids'         => array(
+						'ids'    => array(
 							'type'        => 'array',
 							'uniqueItems' => true,
 							'minItems'    => 1,
@@ -1174,7 +1188,7 @@ final class WP_Abilities_Content {
 							),
 							'description' => __( 'Limit the query to these post IDs. The order of the IDs does not affect the order of the results. If `pagination.per_page` is omitted, every matching post is returned on the first page.' ),
 						),
-						'slugs'       => array(
+						'slugs'  => array(
 							'type'        => 'array',
 							'uniqueItems' => true,
 							'minItems'    => 1,
@@ -1185,7 +1199,7 @@ final class WP_Abilities_Content {
 							),
 							'description' => __( 'Limit the query to posts with these slugs. Each slug is sanitized as a post slug is, so `Hello-World` matches `hello-world`. A slug can match several posts, such as pages under different parents, and every match is returned. The order of the slugs does not affect the order of the results. If `pagination.per_page` is omitted, every matching post is returned on the first page, up to the `per_page` maximum.' ),
 						),
-						'status'      => array(
+						'status' => array(
 							'type'        => 'array',
 							'uniqueItems' => true,
 							'minItems'    => 1,
@@ -1195,18 +1209,31 @@ final class WP_Abilities_Content {
 							),
 							'description' => __( 'Filter readable posts by one or more post statuses. Defaults to publish, or, when `ids` or `slugs` is given, to every status the current user may query. Statuses that are not publicly viewable, other than private, require edit access.' ),
 						),
-						'author_slug' => array(
-							'type'        => 'string',
-							'minLength'   => 1,
-							'description' => __( "Filter by the author's user slug (nicename), as the REST API users endpoint returns it in `slug`. Only supported for post types that support authors." ),
+						'author' => array(
+							'type'                 => 'object',
+							'additionalProperties' => false,
+							'minProperties'        => 1,
+							'properties'           => array(
+								'id'   => array(
+									'type'        => 'integer',
+									'minimum'     => 1,
+									'description' => __( 'The user ID of the author, as `author.id` is returned.' ),
+								),
+								'slug' => array(
+									'type'        => 'string',
+									'minLength'   => 1,
+									'description' => __( "The author's user slug (nicename), as `author.slug` is returned and as the REST API users endpoint returns it in `slug`." ),
+								),
+							),
+							'description'          => __( 'Filter by the post author, given as the `id` or the `slug` the `author` field of a post returns. An `id` matches the author ID stored on the post, even when that user no longer exists. A `slug` that names no user, or an `id` and a `slug` that name different users, match no post. Only supported for post types that support authors.' ),
 						),
-						'parent'      => array(
+						'parent' => array(
 							'type'        => 'integer',
 							'minimum'     => 0,
 							'description' => __( 'Filter by parent post ID. Only supported for hierarchical post types. Use 0 for top-level posts.' ),
 						),
 					),
-					'description'          => __( 'Selects the posts to return: the readable posts of a `type`, filtered by any of `ids`, `slugs`, `status`, `author_slug`, and `parent`. A post must match every given filter.' ),
+					'description'          => __( 'Selects the posts to return: the readable posts of a `type`, filtered by any of `ids`, `slugs`, `status`, `author`, and `parent`. A post must match every given filter.' ),
 				),
 				'fields'     => $this->get_fields_input_schema(),
 				'pagination' => array(
diff --git a/wp-includes/version.php b/wp-includes/version.php
index d6554d4b53..8095e5622f 100644
--- a/wp-includes/version.php
+++ b/wp-includes/version.php
@@ -16,7 +16,7 @@
  *
  * @global string $wp_version
  */
-$wp_version = '7.2-beta1-64679';
+$wp_version = '7.2-beta1-64680';

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