Skip to content

Commit c967518

Browse files
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
1 parent 7a634b9 commit c967518

2 files changed

Lines changed: 65 additions & 12 deletions

File tree

‎wp-includes/abilities/class-wp-abilities-content.php‎

Lines changed: 64 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
* Registers the read-only `core/content-query` ability, which queries the readable posts of a
1616
* post type exposed to abilities via `show_in_abilities`, filtered by any of post IDs, slugs,
1717
* status, author, and parent. Raw fields are only returned for posts the current user can
18-
* edit.
18+
* edit, and the author's name and slug only for authors the current user may see.
1919
*
2020
* Unlike the core abilities registered directly in wp_register_core_abilities() as
2121
* self-contained closures, the content ability lives in a dedicated class because
@@ -92,6 +92,14 @@ final class WP_Abilities_Content {
9292
*/
9393
private const LOOP_GLOBALS = array( 'post', 'id', 'authordata', 'currentday', 'currentmonth', 'page', 'pages', 'multipage', 'more', 'numpages' );
9494

95+
/**
96+
* Whether each author seen in the current query has posts the current user may read.
97+
*
98+
* @since 7.2.0
99+
* @var array<int, bool>
100+
*/
101+
private $visible_authors = array();
102+
95103
/**
96104
* Registers all content abilities.
97105
*
@@ -397,6 +405,9 @@ private function execute_content_query( $input = array() ) {
397405
$input = rest_sanitize_object( $input );
398406
$fields = $this->normalize_fields( $input );
399407

408+
// The author visibility memo is per query; see get_visible_author().
409+
$this->visible_authors = array();
410+
400411
/*
401412
* A `where` that is not an object cannot be honored. Reject it rather than reading it
402413
* as no filter at all.
@@ -506,10 +517,13 @@ private function execute_content_query( $input = array() ) {
506517
/*
507518
* The slug is looked up as given, like the `slug` of the users endpoint: the
508519
* collation of the users table decides whether the case matters. A slug that
509-
* names no user, or names another user than `id`, matches no post, as a post
510-
* must match every given filter.
520+
* names no user, or a user the current user may not see, or names another user
521+
* than `id`, matches no post, as a post must match every given filter. The
522+
* visibility rule is the one the `author` field follows, so a slug the field
523+
* withholds cannot be confirmed through the filter.
511524
*/
512525
$user = get_user_by( 'slug', $author_filter['slug'] );
526+
$user = $user ? $this->get_visible_author( $user->ID ) : null;
513527
if ( ! $user || ( null !== $author_id && $user->ID !== $author_id ) ) {
514528
$author_id = false;
515529
} else {
@@ -618,10 +632,10 @@ private function execute_content_query( $input = array() ) {
618632
}
619633

620634
/*
621-
* A slug that names no user, or an ID and a slug that name different users, match no
622-
* post, like IDs and slugs that name no post, rather than failing the query. An `id` is
623-
* not looked up: the query matches the stored author ID, so the posts of a deleted user
624-
* still match it.
635+
* A slug that names no user or a user the current user may not see, or an ID and a slug
636+
* that name different users, match no post, like IDs and slugs that name no post, rather
637+
* than failing the query. An `id` is not looked up: the query matches the stored author
638+
* ID, so the posts of a deleted user still match it.
625639
*/
626640
if ( false === $author_id ) {
627641
return $this->prepare_output( array(), 0, 0 );
@@ -1081,11 +1095,11 @@ private function get_post_properties(): array {
10811095
),
10821096
'name' => array(
10831097
'type' => array( 'string', 'null' ),
1084-
'description' => __( "The author's display name, as the REST API users endpoint returns it in `name`. Null when the author no longer exists." ),
1098+
'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." ),
10851099
),
10861100
'slug' => array(
10871101
'type' => array( 'string', 'null' ),
1088-
'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." ),
1102+
'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." ),
10891103
),
10901104
),
10911105
'description' => __( 'The post author. Present when the post type supports authors.' ),
@@ -1225,7 +1239,7 @@ private function get_content_query_input_schema( array $post_types, array $statu
12251239
'description' => __( "The author's user slug (nicename), as `author.slug` is returned and as the REST API users endpoint returns it in `slug`." ),
12261240
),
12271241
),
1228-
'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.' ),
1242+
'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.' ),
12291243
),
12301244
'parent' => array(
12311245
'type' => 'integer',
@@ -1324,6 +1338,42 @@ private function get_content_query_output_schema(): array {
13241338
);
13251339
}
13261340

1341+
/**
1342+
* Returns a user when the current user may see them as an author.
1343+
*
1344+
* The current user may see themselves, the users they can edit, every user when they can
1345+
* list users, and otherwise the authors of posts they can read in a post type exposed to
1346+
* abilities: this ability returns those posts with their author, so the author is already
1347+
* in view. Which post types the REST API exposes plays no part. A user the current user
1348+
* may not see is reported like a missing one.
1349+
*
1350+
* The `author` field and the `author` slug filter share this rule, so the filter never
1351+
* confirms a slug the field withholds. The post count is kept per author for the query,
1352+
* since count_user_posts() is not cached.
1353+
*
1354+
* @since 7.2.0
1355+
*
1356+
* @param int $user_id The user ID.
1357+
* @return WP_User|null The user, or null when they no longer exist or may not be seen.
1358+
*/
1359+
private function get_visible_author( int $user_id ): ?WP_User {
1360+
$user = get_userdata( $user_id );
1361+
if ( ! $user ) {
1362+
return null;
1363+
}
1364+
1365+
if ( get_current_user_id() === $user_id || current_user_can( 'list_users' ) || current_user_can( 'edit_user', $user_id ) ) {
1366+
return $user;
1367+
}
1368+
1369+
if ( ! isset( $this->visible_authors[ $user_id ] ) ) {
1370+
$post_types = get_post_types( array( 'show_in_abilities' => true ), 'names' );
1371+
$this->visible_authors[ $user_id ] = (int) count_user_posts( $user_id, $post_types ) > 0;
1372+
}
1373+
1374+
return $this->visible_authors[ $user_id ] ? $user : null;
1375+
}
1376+
13271377
/**
13281378
* Formats a post into the ability output shape.
13291379
*
@@ -1469,7 +1519,10 @@ private function build_post_fields( WP_Post $post, array $fields, bool $can_edit
14691519
}
14701520

14711521
if ( $this->is_field_requested( 'author', $fields ) && post_type_supports( $post_type, 'author' ) ) {
1472-
$user = get_userdata( (int) $post->post_author );
1522+
$user = null;
1523+
if ( $this->is_field_requested( 'author.name', $fields ) || $this->is_field_requested( 'author.slug', $fields ) ) {
1524+
$user = $this->get_visible_author( (int) $post->post_author );
1525+
}
14731526
$author = array();
14741527
if ( $this->is_field_requested( 'author.id', $fields ) ) {
14751528
$author['id'] = (int) $post->post_author;

‎wp-includes/version.php‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
*
1717
* @global string $wp_version
1818
*/
19-
$wp_version = '7.2-beta1-64687';
19+
$wp_version = '7.2-beta1-64688';
2020

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

0 commit comments

Comments
 (0)