|
15 | 15 | * Registers the read-only `core/content-query` ability, which queries the readable posts of a |
16 | 16 | * post type exposed to abilities via `show_in_abilities`, filtered by any of post IDs, slugs, |
17 | 17 | * 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. |
19 | 19 | * |
20 | 20 | * Unlike the core abilities registered directly in wp_register_core_abilities() as |
21 | 21 | * self-contained closures, the content ability lives in a dedicated class because |
@@ -92,6 +92,14 @@ final class WP_Abilities_Content { |
92 | 92 | */ |
93 | 93 | private const LOOP_GLOBALS = array( 'post', 'id', 'authordata', 'currentday', 'currentmonth', 'page', 'pages', 'multipage', 'more', 'numpages' ); |
94 | 94 |
|
| 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 | + |
95 | 103 | /** |
96 | 104 | * Registers all content abilities. |
97 | 105 | * |
@@ -397,6 +405,9 @@ private function execute_content_query( $input = array() ) { |
397 | 405 | $input = rest_sanitize_object( $input ); |
398 | 406 | $fields = $this->normalize_fields( $input ); |
399 | 407 |
|
| 408 | + // The author visibility memo is per query; see get_visible_author(). |
| 409 | + $this->visible_authors = array(); |
| 410 | + |
400 | 411 | /* |
401 | 412 | * A `where` that is not an object cannot be honored. Reject it rather than reading it |
402 | 413 | * as no filter at all. |
@@ -506,10 +517,13 @@ private function execute_content_query( $input = array() ) { |
506 | 517 | /* |
507 | 518 | * The slug is looked up as given, like the `slug` of the users endpoint: the |
508 | 519 | * 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. |
511 | 524 | */ |
512 | 525 | $user = get_user_by( 'slug', $author_filter['slug'] ); |
| 526 | + $user = $user ? $this->get_visible_author( $user->ID ) : null; |
513 | 527 | if ( ! $user || ( null !== $author_id && $user->ID !== $author_id ) ) { |
514 | 528 | $author_id = false; |
515 | 529 | } else { |
@@ -618,10 +632,10 @@ private function execute_content_query( $input = array() ) { |
618 | 632 | } |
619 | 633 |
|
620 | 634 | /* |
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. |
625 | 639 | */ |
626 | 640 | if ( false === $author_id ) { |
627 | 641 | return $this->prepare_output( array(), 0, 0 ); |
@@ -1081,11 +1095,11 @@ private function get_post_properties(): array { |
1081 | 1095 | ), |
1082 | 1096 | 'name' => array( |
1083 | 1097 | '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." ), |
1085 | 1099 | ), |
1086 | 1100 | 'slug' => array( |
1087 | 1101 | '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." ), |
1089 | 1103 | ), |
1090 | 1104 | ), |
1091 | 1105 | '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 |
1225 | 1239 | 'description' => __( "The author's user slug (nicename), as `author.slug` is returned and as the REST API users endpoint returns it in `slug`." ), |
1226 | 1240 | ), |
1227 | 1241 | ), |
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.' ), |
1229 | 1243 | ), |
1230 | 1244 | 'parent' => array( |
1231 | 1245 | 'type' => 'integer', |
@@ -1324,6 +1338,42 @@ private function get_content_query_output_schema(): array { |
1324 | 1338 | ); |
1325 | 1339 | } |
1326 | 1340 |
|
| 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 | + |
1327 | 1377 | /** |
1328 | 1378 | * Formats a post into the ability output shape. |
1329 | 1379 | * |
@@ -1469,7 +1519,10 @@ private function build_post_fields( WP_Post $post, array $fields, bool $can_edit |
1469 | 1519 | } |
1470 | 1520 |
|
1471 | 1521 | 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 | + } |
1473 | 1526 | $author = array(); |
1474 | 1527 | if ( $this->is_field_requested( 'author.id', $fields ) ) { |
1475 | 1528 | $author['id'] = (int) $post->post_author; |
|
0 commit comments