Conversation
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message. To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
✅ WordPress Plugin Check Report
📊 ReportAll checks passed! No errors or warnings found. 🤖 Generated by WordPress Plugin Check Action • Learn more about Plugin Check |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## develop #774 +/- ##
=============================================
+ Coverage 75.50% 76.67% +1.17%
- Complexity 2086 2198 +112
=============================================
Files 99 100 +1
Lines 8626 9078 +452
=============================================
+ Hits 6513 6961 +448
- Misses 2113 2117 +4
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
@jorgefilipecosta, great work so far. I left my initial feedback to evaluate. The logic is quite complex, or rather sensitive, as it has several security concerns that we need to ensure are addressed correctly from the start. |
gziolo
left a comment
There was a problem hiding this comment.
A few more points from my read, on top of the inline notes above. They are about behavior and one small docs fix, so none of them block the PR.
|
I added three more comments discovered with the help of Claude Code review. |
989b58f to
7132486
Compare
Looks like a lint failure that will need cleaned up but otherwise I'm okay proceeding, though may be worth waiting for @peterwilsoncc to chime back in from his initial review |
- Sanitize ability input against the input schema in both callbacks, since
the Abilities API validates but does not coerce REST GET input (booleans
arrive as 'true', arrays as CSV strings) and the strict normalizers were
silently dropping those filters.
- Return a fully redacted user as an object so it serializes as {} rather
than [] and honors the declared output type.
- Always declare avatar_urls in the schemas and honor show_avatars when
formatting instead, since the registered schemas are a registration-time
snapshot while the option is re-read at call time.
- Extract resolve_readable_user() so the single-user authorization decision
is implemented once and shared by the permission and execute callbacks.
Adversarial review of the previous commit surfaced defects in the schema-based sanitize pass, so coercion now lives in the normalizers: - rest_sanitize_value_from_schema() embeds property-level WP_Errors (an include list of '1' and '01' validates as unique strings but sanitizes to a WP_Error after integer coercion, silently dropping the filter), discards schema-valid stdClass input, falls back to raw input when the call-time schema drifts from the registration-time snapshot, and rebuilds the input schema on every call. - normalize_string_list() and normalize_include() now accept the CSV strings REST GET delivers (the latter via wp_parse_id_list(), which also deduplicates string-distinct IDs), normalize_has_published_posts() accepts the string forms of true, and stdClass input is treated as its array form. - The id field is always included, matching the REST users controller where id is present in every context; a fully redacted result now returns the user ID instead of an empty object, which REST post-processing (_fields/_embed) cannot handle at the top level. - Coverage extended: string-duplicate include IDs, numeric-string scalars, and object input.
A prior commit removed normalize_per_page(), normalize_string_list(), and normalize_include() but left their call sites, so PHPStan and the PHP/E2E suites failed with "Call to undefined method". Restore the normalizers. Also normalize the username lookup with sanitize_user() so every single-user lookup field is normalized alongside id (absint) and email (sanitize_email), keep the slug lookup on the raw nicename to match the REST users controller (non-canonical nicenames must resolve), and note that collection total/ total_pages may exceed the returned users when the multisite membership guard skips rows.
The normalizer restore reverted a `phpcs:ignore` that suppresses a Slevomat false positive (a multi-line array constant read as a multi-constant definition) on DEFAULT_FIELDS. Restore the annotation so PHPCS passes.
The roles field was returned to any caller with list_users, even for users they cannot edit. list_users grants no edit rights, so this disclosed the site's privilege map to a list_users-only role. Gate roles on the same is-self-or-can-edit check as the other sensitive fields, matching the REST users controller, where roles is an edit-context field and non-editable rows are dropped from collections.
The output schema's roles enum was captured at ability registration. A role registered afterwards is still held by a returned user, so format_user emits it, but the frozen enum rejects it during output validation and fails the whole call with ability_invalid_output. Drop the enum from the output roles items (plain strings, matching the REST users controller); the input roles enum, which is re-resolved at call time, is unaffected.
The collection loop post-filtered rows by is_user_member_of_site() while total/total_pages came from the unfiltered WP_User_Query. On multisite a bare collection query (no roles/has_published_posts) is network-wide, so a list_users caller got an inflated total and short, near-empty pages. Drop the per-row membership filter so users, total and total_pages all derive from the same query, matching WP_REST_Users_Controller::get_items (no per-row membership check, get_total() reported directly). Single-user lookups keep the membership check, matching the controller's get_user(). No-op on single site, where the guard was already always-true.
The comment claimed a bare collection query is network-wide on multisite, but WP_User_Query scopes results to the current blog via the capabilities meta clause it adds whenever blog_id (defaulted to the current blog) is set, even with no role/has_published_posts filter. Correct the note to describe the actual site-scoping; no behavior change.
|
Hi @gziolo and @peterwilsoncc thank you a lot for your reviews everything was addressed. |
Hi @dkotter, sorry that failure was temporary during the development it is addressed. |
The ability validates its result against the output schema, so a value that does not match the declared type fails the whole call. Two values WordPress stores can trigger that. A user can have no email at all. `wp_insert_user()` rejects an empty login and an empty nicename, but never an empty email. `sanitize_email()` can also repair an address into a shape `is_email()` rejects, so saving `a@@b.c` stores `a@b.c`. `get_avatar_url()` returns false when it cannot build a URL, which is how plugins that suppress avatars opt out. Since `avatar_urls` is part of the default field set, that failed every call, not only the ones that ask for it. Both fields now accept null, and `format_user()` maps every unusable value to it. The email field keeps its `email` format, so a non-null value is always a valid address. In collection mode a single bad row no longer rejects the whole page. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The `is_public_author()` docblock said the method checks for published posts. That is not what it does. `count_user_posts()` defaults to `$public_only = false`, so a private post also counts for a caller who holds `read_private_posts` for that post type. Collection mode is filtered by `WP_User_Query` with `has_published_posts`, which matches published posts only. So an editor can resolve an author whose posts are all private through a single-user lookup, but will never find that author in a collection. This keeps the current behavior, which matches the REST users controller. It exposes nothing new, because an editor who can read the private post already sees its author through the post itself. The docblock now describes the rule, and a new test pins all three legs: the editor resolves the author, the collection omits them, and a subscriber is denied. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The comment above the `include` query argument said the default ordering is kept "regardless of the requested order". There is no order input, so that clause described something the ability does not have. It was left over from dropping `orderby => include` in e01b49d. The comment now says what is actually being avoided. A new test locks the behavior in: it runs the same include set in both orders and expects the results in user login order both times. Adding `orderby => include` back makes it fail. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Peter pointed out that `public => true` does not mean a post type can be viewed on the front end, so the ability switched to is_post_type_viewable(). One path still slipped through. Passing `has_published_posts => true` straight to WP_User_Query makes core resolve the list itself, as `get_post_types( array( 'public' => true ) )`, which is the very set Peter warned about. The effect: an author whose only published post used a post type registered with `public => true, publicly_queryable => false` was matched by a boolean filter, even though the ability rejects that post type in the input enum, in collection mode for callers who cannot list users, and in the single-user lookup. Both branches now resolve the post types the same way, so `true` and an explicit list mean the same thing. Resolving at call time also picks up post types that were registered or unregistered after the input schema was built, which was Peter's other point. A new test registers the post type from his gist and checks all three paths. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
gziolo
left a comment
There was a problem hiding this comment.
Thank you @jorgefilipecosta and @peterwilsoncc. I pushed six follow-up commits. Here is what each one does:
00f98114— Bug fix.emailandavatar_urlscan now benull. WordPress can store a user with no email, andget_avatar_url()can returnfalse. Both failed output validation, and in collection mode one bad row failed the whole page.110b37f3— Docs and a test.is_public_author()also counts private posts for a caller who can read them, so a single-user lookup and a collection can disagree. This matches the REST users controller, so we keep it and pin it with a test.a8caabfb— Test fix. Newer WordPress versions sanitize an invalid email in a different way.d70c24b6— Docs only. The input enums are a snapshot taken when the ability is registered. This is on purpose, so the schema stays a stable contract.00f9fd6c— Docs and a test. Theincludeorder is not used for sorting. The test fails iforderby => includeever comes back.4bb161eb— Bug fix.has_published_posts: truewas passed toWP_User_Query, which resolves it withpublic => true. That is the gotcha @peterwilsoncc described. The ability now resolves the post types itself, sotrueand an explicit list mean the same thing.
PHPUnit, PHPCS, and PHPStan all pass.
The core/get-user-info field naming question is a follow-up, not a blocker. In the long run core/get-user-info could become a preset of this ability for the current user.
Approving. Thank you both for the careful work on a sensitive ability.
What?
core/read-usersability with single-user lookup byid,user_email,user_login, oruser_nicename.users,total, andtotal_pages.roles,has_published_posts,include,page,per_page, andfields; omitted REST input defaults to empty collection mode.fieldsis omitted, while keeping sensitive identifiers and fields opt-in and permission-gated.user_login,user_email,user_nicename,user_url, anduser_registered, to align with Core’s existingcore/get-user-infoability.get_option( 'show_avatars' ).Testing
php -l includes/Abilities/Users/Users.phpphp -l tests/Integration/Includes/Abilities/Users/UsersTest.phpvendor/bin/phpcs --standard=phpcs.xml.distvendor/bin/phpstan analyse --memory-limit=1Gnpx wp-scripts lint-js tests/e2e/specs/abilities/core-read-users.spec.jsnpm run lint:jsnpm run typechecknpm run buildnpm run test:php -- --filter UsersTestnpm run test:phpnpm run test:e2e -- tests/e2e/specs/abilities/core-read-users.spec.js --project=chromiumChangelog entry
Added - New core/read-users Ability; retrieves a single user by ID, email, login, or nicename, or a filtered and paginated users collection, with sensitive fields opt-in and permission-gated.