Commit c967518292 for wordpress.org
commit c9675182926de3c788cd7c594705111a920403d3
Author: jorgefilipecosta <jorgefilipecosta@git.wordpress.org>
Date: Fri Oct 23 10:06:54 2026 +0000
Abilities API: Name the author of a `core/content-query` post only to users who may see them.
The `core/content-query` ability returned the author's display name and slug for every post the current user can read, and its `author` slug filter named any user of the site.
Return them only to users who may see the author: the author themselves, users who can edit them or list users, and otherwise users who can read a published post of theirs in a post type exposed to abilities, where the ability already returns the author.
Follow-up to [64388], [64680].
Props jorgefilipecosta, gziolo.
See #66268.
Built from https://develop.svn.wordpress.org/trunk@64688
git-svn-id: http://core.svn.wordpress.org/trunk@63838 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 cf64f0a5f0..8d75cb3f14 100644
--- a/wp-includes/abilities/class-wp-abilities-content.php
+++ b/wp-includes/abilities/class-wp-abilities-content.php
@@ -15,7 +15,7 @@ declare( strict_types = 1 );
* Registers the read-only `core/content-query` ability, which queries the readable posts of a
* post type exposed to abilities via `show_in_abilities`, filtered by any of post IDs, slugs,
* status, author, and parent. Raw fields are only returned for posts the current user can
- * edit.
+ * edit, and the author's name and slug only for authors the current user may see.
*
* Unlike the core abilities registered directly in wp_register_core_abilities() as
* self-contained closures, the content ability lives in a dedicated class because
@@ -92,6 +92,14 @@ final class WP_Abilities_Content {
*/
private const LOOP_GLOBALS = array( 'post', 'id', 'authordata', 'currentday', 'currentmonth', 'page', 'pages', 'multipage', 'more', 'numpages' );
+ /**
+ * Whether each author seen in the current query has posts the current user may read.
+ *
+ * @since 7.2.0
+ * @var array<int, bool>
+ */
+ private $visible_authors = array();
+
/**
* Registers all content abilities.
*
@@ -397,6 +405,9 @@ final class WP_Abilities_Content {
$input = rest_sanitize_object( $input );
$fields = $this->normalize_fields( $input );
+ // The author visibility memo is per query; see get_visible_author().
+ $this->visible_authors = array();
+
/*
* A `where` that is not an object cannot be honored. Reject it rather than reading it
* as no filter at all.
@@ -506,10 +517,13 @@ final class WP_Abilities_Content {
/*
* 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.
+ * names no user, or a user the current user may not see, or names another user
+ * than `id`, matches no post, as a post must match every given filter. The
+ * visibility rule is the one the `author` field follows, so a slug the field
+ * withholds cannot be confirmed through the filter.
*/
$user = get_user_by( 'slug', $author_filter['slug'] );
+ $user = $user ? $this->get_visible_author( $user->ID ) : null;
if ( ! $user || ( null !== $author_id && $user->ID !== $author_id ) ) {
$author_id = false;
} else {
@@ -618,10 +632,10 @@ final class WP_Abilities_Content {
}
/*
- * 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.
+ * A slug that names no user or a user the current user may not see, 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 );
@@ -1081,11 +1095,11 @@ final class WP_Abilities_Content {
),
'name' => array(
'type' => array( 'string', 'null' ),
- 'description' => __( "The author's display name, as the REST API users endpoint returns it in `name`. Null when the author no longer exists." ),
+ 'description' => __( "The author's display name, as the REST API users endpoint returns it in `name`. Null when the author no longer exists or the current user may not see them." ),
),
'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 or the current user may not see them." ),
),
),
'description' => __( 'The post author. Present when the post type supports authors.' ),
@@ -1225,7 +1239,7 @@ final class WP_Abilities_Content {
'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.' ),
+ '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 a user the current user may not see, as the `author` field reports them, or an `id` and a `slug` that name different users, match no post. Only supported for post types that support authors.' ),
),
'parent' => array(
'type' => 'integer',
@@ -1324,6 +1338,42 @@ final class WP_Abilities_Content {
);
}
+ /**
+ * Returns a user when the current user may see them as an author.
+ *
+ * The current user may see themselves, the users they can edit, every user when they can
+ * list users, and otherwise the authors of posts they can read in a post type exposed to
+ * abilities: this ability returns those posts with their author, so the author is already
+ * in view. Which post types the REST API exposes plays no part. A user the current user
+ * may not see is reported like a missing one.
+ *
+ * The `author` field and the `author` slug filter share this rule, so the filter never
+ * confirms a slug the field withholds. The post count is kept per author for the query,
+ * since count_user_posts() is not cached.
+ *
+ * @since 7.2.0
+ *
+ * @param int $user_id The user ID.
+ * @return WP_User|null The user, or null when they no longer exist or may not be seen.
+ */
+ private function get_visible_author( int $user_id ): ?WP_User {
+ $user = get_userdata( $user_id );
+ if ( ! $user ) {
+ return null;
+ }
+
+ if ( get_current_user_id() === $user_id || current_user_can( 'list_users' ) || current_user_can( 'edit_user', $user_id ) ) {
+ return $user;
+ }
+
+ if ( ! isset( $this->visible_authors[ $user_id ] ) ) {
+ $post_types = get_post_types( array( 'show_in_abilities' => true ), 'names' );
+ $this->visible_authors[ $user_id ] = (int) count_user_posts( $user_id, $post_types ) > 0;
+ }
+
+ return $this->visible_authors[ $user_id ] ? $user : null;
+ }
+
/**
* Formats a post into the ability output shape.
*
@@ -1469,7 +1519,10 @@ final class WP_Abilities_Content {
}
if ( $this->is_field_requested( 'author', $fields ) && post_type_supports( $post_type, 'author' ) ) {
- $user = get_userdata( (int) $post->post_author );
+ $user = null;
+ if ( $this->is_field_requested( 'author.name', $fields ) || $this->is_field_requested( 'author.slug', $fields ) ) {
+ $user = $this->get_visible_author( (int) $post->post_author );
+ }
$author = array();
if ( $this->is_field_requested( 'author.id', $fields ) ) {
$author['id'] = (int) $post->post_author;
diff --git a/wp-includes/version.php b/wp-includes/version.php
index cb6f831cba..5bd26c27f5 100644
--- a/wp-includes/version.php
+++ b/wp-includes/version.php
@@ -16,7 +16,7 @@
*
* @global string $wp_version
*/
-$wp_version = '7.2-beta1-64687';
+$wp_version = '7.2-beta1-64688';
/**
* Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.