From 4edb9f2c22a572af6fd1fd5b78d626c1c8a67155 Mon Sep 17 00:00:00 2001 From: Grzegorz Ziolkowski Date: Thu, 29 Oct 2026 15:33:08 +0100 Subject: [PATCH 1/2] Abilities API: Restrict author visibility to viewable post types. --- .../abilities/class-wp-abilities-content.php | 10 +-- .../wpRegisterCoreContentAbility.php | 74 +++++++++++++++++-- 2 files changed, 73 insertions(+), 11 deletions(-) diff --git a/src/wp-includes/abilities/class-wp-abilities-content.php b/src/wp-includes/abilities/class-wp-abilities-content.php index 28e03177dd62f..6be7e065e0988 100644 --- a/src/wp-includes/abilities/class-wp-abilities-content.php +++ b/src/wp-includes/abilities/class-wp-abilities-content.php @@ -1336,10 +1336,10 @@ private function get_content_query_output_schema(): array { * 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. + * list users, and otherwise the authors of posts they can read in a publicly viewable 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, @@ -1361,7 +1361,7 @@ private function get_visible_author( int $user_id ): ?WP_User { } if ( ! isset( $this->visible_authors[ $user_id ] ) ) { - $post_types = get_post_types( array( 'show_in_abilities' => true ), 'names' ); + $post_types = array_values( array_filter( get_post_types( array( 'show_in_abilities' => true ), 'names' ), 'is_post_type_viewable' ) ); $this->visible_authors[ $user_id ] = (int) count_user_posts( $user_id, $post_types ) > 0; } diff --git a/tests/phpunit/tests/abilities-api/wpRegisterCoreContentAbility.php b/tests/phpunit/tests/abilities-api/wpRegisterCoreContentAbility.php index a625215ddb823..398ab7146a287 100644 --- a/tests/phpunit/tests/abilities-api/wpRegisterCoreContentAbility.php +++ b/tests/phpunit/tests/abilities-api/wpRegisterCoreContentAbility.php @@ -2583,7 +2583,7 @@ static function ( $count, $user_id ) use ( &$looked_up ) { } /** - * An author is visible once they have a published post in a post type exposed to + * An author is visible once they have a published post in a viewable post type exposed to * abilities, whether or not that post type is exposed to the REST API, and not for a * published post in a post type exposed to the REST API only. * @@ -2603,10 +2603,11 @@ public function test_author_visibility_follows_the_post_types_exposed_to_abiliti register_post_type( 'abilities_cpt', array( - 'public' => false, - 'show_in_rest' => false, - 'show_in_abilities' => true, - 'supports' => array( 'title', 'author' ), + 'public' => false, + 'publicly_queryable' => true, + 'show_in_rest' => false, + 'show_in_abilities' => true, + 'supports' => array( 'title', 'author' ), ) ); @@ -2658,7 +2659,7 @@ public function test_author_visibility_follows_the_post_types_exposed_to_abiliti $post_ids['abilities_cpt'] => 'abilities_cpt-author', ), $slugs, - 'Only the author with a published post in a post type exposed to abilities should be named.' + 'Only the author with a published post in a viewable post type exposed to abilities should be named.' ); $filter = static function ( string $slug ) use ( $ability, $post_ids ) { @@ -2678,6 +2679,67 @@ public function test_author_visibility_follows_the_post_types_exposed_to_abiliti $this->assertSame( array( $post_ids['abilities_cpt'] ), wp_list_pluck( $filter( 'abilities_cpt-author' )['items'], 'id' ), 'The slug of an author published in a post type exposed to abilities should match their post.' ); } + /** + * Published posts in a non-viewable post type must not make an author's slug visible. + * + * @since 7.2.0 + */ + public function test_author_slug_is_not_exposed_by_posts_in_a_non_viewable_post_type(): void { + register_post_type( + 'hidden_author_cpt', + array( + 'public' => true, + 'publicly_queryable' => false, + 'show_in_abilities' => true, + 'supports' => array( 'title', 'author' ), + ) + ); + $author = self::factory()->user->create_and_get( array( 'role' => 'author' ) ); + self::factory()->post->create( + array( + 'post_type' => 'hidden_author_cpt', + 'post_author' => $author->ID, + 'post_status' => 'publish', + ) + ); + + $this->login_as( 'subscriber' ); + $this->register_ability(); + $ability = wp_get_ability( 'core/content-query' ); + + foreach ( array( $author->user_nicename, 'no-such-user' ) as $slug ) { + $result = $ability->execute( + array( + 'where' => array( + 'type' => 'hidden_author_cpt', + 'author' => array( 'slug' => $slug ), + ), + ) + ); + $this->assertSame( self::EMPTY_RESULT, $result, 'A hidden author slug should return zero totals, just like an unknown slug.' ); + } + + $post_id = self::factory()->post->create( + array( + 'post_author' => $author->ID, + 'post_status' => 'publish', + ) + ); + $result = $ability->execute( + array( + 'where' => array( + 'type' => 'post', + 'ids' => array( $post_id ), + 'author' => array( 'slug' => $author->user_nicename ), + ), + 'fields' => array( 'author' ), + ) + ); + $this->assertCount( 1, $result['items'], 'A published post in a viewable type should make the author visible.' ); + $this->assertSame( $author->display_name, $result['items'][0]['author']['name'], 'The visible author name should be returned.' ); + $this->assertSame( $author->user_nicename, $result['items'][0]['author']['slug'], 'The visible author slug should be returned.' ); + } + /** * Requested objects that do not apply to the post type are left out, and the post keeps its * ID. From 0572680f016fa023e83d7e51d0b58829e3dc272d Mon Sep 17 00:00:00 2001 From: Grzegorz Ziolkowski Date: Thu, 29 Oct 2026 15:38:51 +0100 Subject: [PATCH 2/2] Tests: Focus hidden author regression on slug visibility. --- .../wpRegisterCoreContentAbility.php | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/tests/phpunit/tests/abilities-api/wpRegisterCoreContentAbility.php b/tests/phpunit/tests/abilities-api/wpRegisterCoreContentAbility.php index 398ab7146a287..8e8772eb7776b 100644 --- a/tests/phpunit/tests/abilities-api/wpRegisterCoreContentAbility.php +++ b/tests/phpunit/tests/abilities-api/wpRegisterCoreContentAbility.php @@ -2718,26 +2718,6 @@ public function test_author_slug_is_not_exposed_by_posts_in_a_non_viewable_post_ ); $this->assertSame( self::EMPTY_RESULT, $result, 'A hidden author slug should return zero totals, just like an unknown slug.' ); } - - $post_id = self::factory()->post->create( - array( - 'post_author' => $author->ID, - 'post_status' => 'publish', - ) - ); - $result = $ability->execute( - array( - 'where' => array( - 'type' => 'post', - 'ids' => array( $post_id ), - 'author' => array( 'slug' => $author->user_nicename ), - ), - 'fields' => array( 'author' ), - ) - ); - $this->assertCount( 1, $result['items'], 'A published post in a viewable type should make the author visible.' ); - $this->assertSame( $author->display_name, $result['items'][0]['author']['name'], 'The visible author name should be returned.' ); - $this->assertSame( $author->user_nicename, $result['items'][0]['author']['slug'], 'The visible author slug should be returned.' ); } /**