Skip to content

Commit e29e51d

Browse files
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 #14673. Follow-up to [64388]. Props jorgefilipecosta, gziolo. See #66268. git-svn-id: https://develop.svn.wordpress.org/trunk@64680 602fd350-edb4-49c9-b593-d223f7449a82
1 parent 9063498 commit e29e51d

2 files changed

Lines changed: 809 additions & 189 deletions

File tree

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

Lines changed: 94 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -450,24 +450,72 @@ private function execute_content_query( $input = array() ) {
450450
}
451451
}
452452

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

463-
$author = $this->get_author_by_slug( $where['author_slug'], $post_type_object );
464-
if ( ! $author ) {
468+
/*
469+
* An object without `id` or `slug`, such as `{}` or an empty string, and one with
470+
* an unknown key, such as a list or a misspelled property, name no user and must not
471+
* fall through to an unrestricted query, like an empty `ids` filter.
472+
*/
473+
$author_filter = $this->get_input_object( $where, 'author' );
474+
if ( empty( $author_filter ) || ! empty( array_diff_key( $author_filter, array_flip( array( 'id', 'slug' ) ) ) ) ) {
465475
return $this->invalid_filter_error(
466-
'where[author_slug]',
476+
'where[author]',
467477
/* translators: %s: Parameter. */
468-
sprintf( __( 'The %s filter must be the slug of an existing user.' ), 'author_slug' )
478+
sprintf( __( 'The %s filter must be an object with an id or a slug.' ), 'author' )
469479
);
470480
}
481+
482+
/*
483+
* An `id` of 0 must not reach the query: WP_Query reads `author => 0` as no author
484+
* filter, which would return every post of the type.
485+
*/
486+
if ( array_key_exists( 'id', $author_filter ) ) {
487+
$author_id = $this->parse_filter_int( $author_filter['id'], 1 );
488+
if ( null === $author_id ) {
489+
return $this->invalid_filter_error(
490+
'where[author][id]',
491+
/* translators: %s: Parameter. */
492+
sprintf( __( 'The %s ID must be a positive integer.' ), 'author' )
493+
);
494+
}
495+
}
496+
497+
if ( array_key_exists( 'slug', $author_filter ) ) {
498+
if ( ! is_string( $author_filter['slug'] ) || '' === $author_filter['slug'] ) {
499+
return $this->invalid_filter_error(
500+
'where[author][slug]',
501+
/* translators: %s: Parameter. */
502+
sprintf( __( 'The %s slug must be a non-empty string.' ), 'author' )
503+
);
504+
}
505+
506+
/*
507+
* The slug is looked up as given, like the `slug` of the users endpoint: the
508+
* 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.
511+
*/
512+
$user = get_user_by( 'slug', $author_filter['slug'] );
513+
if ( ! $user || ( null !== $author_id && $user->ID !== $author_id ) ) {
514+
$author_id = false;
515+
} else {
516+
$author_id = $user->ID;
517+
}
518+
}
471519
}
472520

473521
/*
@@ -561,14 +609,24 @@ private function execute_content_query( $input = array() ) {
561609
$query_args['post_name__in'] = $slugs;
562610
}
563611

564-
if ( null !== $author ) {
565-
$query_args['author'] = $author->ID;
612+
if ( is_int( $author_id ) ) {
613+
$query_args['author'] = $author_id;
566614
}
567615

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

620+
/*
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.
625+
*/
626+
if ( false === $author_id ) {
627+
return $this->prepare_output( array(), 0, 0 );
628+
}
629+
572630
$query = new WP_Query( $query_args );
573631
$total = $this->get_query_total( $query, $query_args, $page );
574632

@@ -745,50 +803,6 @@ private function is_field_requested( string $path, array $fields ): bool {
745803
return rest_is_field_included( $path, $fields );
746804
}
747805

748-
/**
749-
* Looks up the user an author slug names.
750-
*
751-
* The slug is the user's nicename, which the REST API users endpoint returns as `slug`. It is
752-
* looked up as given, like that endpoint's `slug` filter, so the database collation decides
753-
* whether a variant in another case matches. A user the current user may not see is reported
754-
* like a missing one. The current user can see themselves, any user of the site when they can
755-
* list users, and authors with posts in a publicly viewable post type. A user who can edit
756-
* others' posts of the post type can also see any user of the site, since they may make any of
757-
* them the author, so for them the lookup does tell whether such an account exists.
758-
*
759-
* On multisite the lookup searches the whole network, so posts by authors who are not
760-
* members of the site, such as super admins, can still be filtered. Those users are only
761-
* visible as the current user or as authors with posts in a publicly viewable post type, so
762-
* the lookup does not tell whether an account exists elsewhere on the network.
763-
*
764-
* @since 7.2.0
765-
*
766-
* @param mixed $slug The author slug.
767-
* @param WP_Post_Type $post_type_object The post type the author is looked up for.
768-
* @return WP_User|null The user, or null when the slug does not name a visible user.
769-
*/
770-
private function get_author_by_slug( $slug, WP_Post_Type $post_type_object ): ?WP_User {
771-
$user = is_string( $slug ) ? get_user_by( 'slug', $slug ) : false;
772-
if ( ! $user ) {
773-
return null;
774-
}
775-
776-
if ( get_current_user_id() === $user->ID ) {
777-
return $user;
778-
}
779-
780-
// The capabilities only reveal users of the site, not of the whole network.
781-
if ( is_user_member_of_blog( $user->ID )
782-
&& ( current_user_can( 'list_users' ) || current_user_can( $post_type_object->cap->edit_others_posts ) )
783-
) {
784-
return $user;
785-
}
786-
787-
$public_post_types = array_values( array_filter( get_post_types(), 'is_post_type_viewable' ) );
788-
789-
return ! empty( $public_post_types ) && count_user_posts( $user->ID, $public_post_types ) > 0 ? $user : null;
790-
}
791-
792806
/**
793807
* Returns a post type exposed through the Abilities API.
794808
*
@@ -1063,15 +1077,15 @@ private function get_post_properties(): array {
10631077
'properties' => array(
10641078
'id' => array(
10651079
'type' => 'integer',
1066-
'description' => __( 'The user ID of the author.' ),
1080+
'description' => __( 'The user ID of the author, as the `author.id` filter takes it.' ),
10671081
),
10681082
'name' => array(
10691083
'type' => array( 'string', 'null' ),
10701084
'description' => __( "The author's display name, as the REST API users endpoint returns it in `name`. Null when the author no longer exists." ),
10711085
),
10721086
'slug' => array(
10731087
'type' => array( 'string', 'null' ),
1074-
'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." ),
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." ),
10751089
),
10761090
),
10771091
'description' => __( 'The post author. Present when the post type supports authors.' ),
@@ -1137,7 +1151,7 @@ private function get_fields_input_schema(): array {
11371151
* The input has three members: `where` selects the posts, `fields` selects the fields
11381152
* returned for each post, and `pagination` selects the page of results. `where` holds
11391153
* the post type to query in `type`, and any of the filters `ids`, `slugs`, `status`,
1140-
* `author_slug`, and `parent`, which all combine. Every object sets
1154+
* `author`, and `parent`, which all combine. Every object sets
11411155
* `additionalProperties: false`, so an unknown key, such as a misspelled filter, fails
11421156
* validation instead of being silently ignored.
11431157
*
@@ -1158,12 +1172,12 @@ private function get_content_query_input_schema( array $post_types, array $statu
11581172
'additionalProperties' => false,
11591173
'required' => array( 'type' ),
11601174
'properties' => array(
1161-
'type' => array(
1175+
'type' => array(
11621176
'type' => 'string',
11631177
'enum' => $post_types,
11641178
'description' => __( 'Post type to query for readable posts.' ),
11651179
),
1166-
'ids' => array(
1180+
'ids' => array(
11671181
'type' => 'array',
11681182
'uniqueItems' => true,
11691183
'minItems' => 1,
@@ -1174,7 +1188,7 @@ private function get_content_query_input_schema( array $post_types, array $statu
11741188
),
11751189
'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.' ),
11761190
),
1177-
'slugs' => array(
1191+
'slugs' => array(
11781192
'type' => 'array',
11791193
'uniqueItems' => true,
11801194
'minItems' => 1,
@@ -1185,7 +1199,7 @@ private function get_content_query_input_schema( array $post_types, array $statu
11851199
),
11861200
'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.' ),
11871201
),
1188-
'status' => array(
1202+
'status' => array(
11891203
'type' => 'array',
11901204
'uniqueItems' => true,
11911205
'minItems' => 1,
@@ -1195,18 +1209,31 @@ private function get_content_query_input_schema( array $post_types, array $statu
11951209
),
11961210
'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.' ),
11971211
),
1198-
'author_slug' => array(
1199-
'type' => 'string',
1200-
'minLength' => 1,
1201-
'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." ),
1212+
'author' => array(
1213+
'type' => 'object',
1214+
'additionalProperties' => false,
1215+
'minProperties' => 1,
1216+
'properties' => array(
1217+
'id' => array(
1218+
'type' => 'integer',
1219+
'minimum' => 1,
1220+
'description' => __( 'The user ID of the author, as `author.id` is returned.' ),
1221+
),
1222+
'slug' => array(
1223+
'type' => 'string',
1224+
'minLength' => 1,
1225+
'description' => __( "The author's user slug (nicename), as `author.slug` is returned and as the REST API users endpoint returns it in `slug`." ),
1226+
),
1227+
),
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.' ),
12021229
),
1203-
'parent' => array(
1230+
'parent' => array(
12041231
'type' => 'integer',
12051232
'minimum' => 0,
12061233
'description' => __( 'Filter by parent post ID. Only supported for hierarchical post types. Use 0 for top-level posts.' ),
12071234
),
12081235
),
1209-
'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.' ),
1236+
'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.' ),
12101237
),
12111238
'fields' => $this->get_fields_input_schema(),
12121239
'pagination' => array(

0 commit comments

Comments
 (0)