-
Notifications
You must be signed in to change notification settings - Fork 3.3k
Add: Get user ability. #10775
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
jorgefilipecosta
wants to merge
17
commits into
WordPress:trunk
Choose a base branch
from
jorgefilipecosta:add/get-current-user-ability
base: trunk
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Add: Get user ability. #10775
Changes from all commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
824ecab
Add: Get current user ability.
jorgefilipecosta 7a5cc0d
fix unexpected inclusion of meta or properties
jorgefilipecosta 08bb968
remove unrequired code
jorgefilipecosta 44bb39a
bug fix
jorgefilipecosta 7b36b5d
renamte get user
jorgefilipecosta 0985dbf
lint fix
jorgefilipecosta a0fab0f
post rebase fix
jorgefilipecosta b933772
fix since versions
jorgefilipecosta 3171d5a
minor condition improvement
jorgefilipecosta f18ccaf
Improve schemas
jorgefilipecosta 4c8e76a
improve permissions
jorgefilipecosta 408b78c
Adjust required output fields for core/get-user
jorgefilipecosta 69a38fe
Sort core/get-user output schema properties alphabetically
jorgefilipecosta 76b6c9a
reolver level field filtering
jorgefilipecosta aa40a4a
Abilities: normalize core/get-user output coercion
jorgefilipecosta 58931a3
test fixes
jorgefilipecosta a8218c3
remove optinal fields and capabilities
jorgefilipecosta File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,354 @@ | ||
| <?php | ||
| /** | ||
| * Registers core user abilities. | ||
| * | ||
| * This is a utility class to encapsulate the registration of user-related abilities. | ||
| * It is not intended to be instantiated or consumed directly by any other code or plugin. | ||
| * | ||
| * @package WordPress | ||
| * @subpackage Abilities_API | ||
| * @since 7.0.0 | ||
| * | ||
| * @internal This class is not part of the public API. | ||
| * @access private | ||
| */ | ||
|
|
||
| declare( strict_types=1 ); | ||
|
|
||
| /** | ||
| * Registers core user abilities. | ||
| * | ||
| * @since 7.0.0 | ||
| * @access private | ||
| */ | ||
| class WP_Users_Abilities { | ||
|
|
||
| /** | ||
| * Registers all user-related abilities. | ||
| * | ||
| * @since 7.0.0 | ||
| * | ||
| * @return void | ||
| */ | ||
| public static function register(): void { | ||
| self::register_get_user(); | ||
| } | ||
|
|
||
| /** | ||
| * Registers the core/get-user ability. | ||
| * | ||
| * @since 7.0.0 | ||
| * | ||
| * @return void | ||
| */ | ||
| private static function register_get_user(): void { | ||
| wp_register_ability( | ||
| 'core/get-user', | ||
| array( | ||
| 'label' => __( 'Get User' ), | ||
| 'description' => __( 'Returns comprehensive profile details for a user by id, username, or email.' ), | ||
| 'category' => 'user', | ||
| 'input_schema' => self::get_user_input_schema(), | ||
| 'output_schema' => self::get_user_output_schema(), | ||
| 'execute_callback' => array( __CLASS__, 'execute_get_user' ), | ||
| 'permission_callback' => array( __CLASS__, 'check_get_user_permission' ), | ||
| 'meta' => array( | ||
| 'annotations' => array( | ||
| 'readonly' => true, | ||
| 'destructive' => false, | ||
| 'idempotent' => true, | ||
| ), | ||
| 'show_in_rest' => true, | ||
| ), | ||
| ) | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Gets the input schema for the get-user ability. | ||
| * | ||
| * @since 7.0.0 | ||
| * | ||
| * @return array<string, mixed> The input schema. | ||
| */ | ||
| private static function get_user_input_schema(): array { | ||
| return array( | ||
| 'type' => 'object', | ||
| 'oneOf' => array( | ||
| array( 'required' => array( 'id' ) ), | ||
| array( 'required' => array( 'username' ) ), | ||
| array( 'required' => array( 'email' ) ), | ||
| ), | ||
| 'properties' => array( | ||
| 'email' => array( | ||
| 'type' => 'string', | ||
| 'format' => 'email', | ||
| 'description' => __( 'User email address.' ), | ||
| ), | ||
| 'id' => array( | ||
| 'type' => 'integer', | ||
| 'description' => __( 'User ID.' ), | ||
| ), | ||
| 'username' => array( | ||
| 'type' => 'string', | ||
| 'description' => __( 'User login name.' ), | ||
| ), | ||
| ), | ||
| 'additionalProperties' => false, | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Determines whether sensitive fields can be returned for a user. | ||
| * | ||
| * @since 7.0.0 | ||
| * | ||
| * @param WP_User $user The target user. | ||
| * @return bool Whether sensitive fields can be returned. | ||
| */ | ||
| private static function can_view_sensitive_user_fields( WP_User $user ): bool { | ||
| return get_current_user_id() === $user->ID || current_user_can( 'edit_user', $user->ID ); | ||
| } | ||
|
|
||
| /** | ||
| * Determines whether roles can be returned for a user. | ||
| * | ||
| * @since 7.0.0 | ||
| * | ||
| * @param WP_User $user The target user. | ||
| * @return bool Whether roles can be returned. | ||
| */ | ||
| private static function can_view_user_roles( WP_User $user ): bool { | ||
| return current_user_can( 'list_users' ) || current_user_can( 'edit_user', $user->ID ); | ||
| } | ||
|
|
||
| /** | ||
| * Gets the output schema for the get-user ability. | ||
| * | ||
| * @since 7.0.0 | ||
| * | ||
| * @return array<string, mixed> The output schema. | ||
| */ | ||
| private static function get_user_output_schema(): array { | ||
| $avatar_urls_schema = array( | ||
| 'type' => 'object', | ||
| 'description' => __( 'Avatar URLs for the user.' ), | ||
| ); | ||
|
|
||
| if ( get_option( 'show_avatars' ) ) { | ||
| $avatar_properties = array(); | ||
|
|
||
| foreach ( rest_get_avatar_sizes() as $size ) { | ||
| $avatar_properties[ $size ] = array( | ||
| /* translators: %d: Avatar image size in pixels. */ | ||
| 'description' => sprintf( __( 'Avatar URL with image size of %d pixels.' ), $size ), | ||
| 'type' => 'string', | ||
| 'format' => 'uri', | ||
| ); | ||
| } | ||
|
|
||
| $avatar_urls_schema['properties'] = $avatar_properties; | ||
| } | ||
|
|
||
| return array( | ||
| 'type' => 'object', | ||
| 'required' => array( | ||
| 'id', | ||
| 'username', | ||
| ), | ||
| 'properties' => array( | ||
| 'avatar_urls' => $avatar_urls_schema, | ||
| 'description' => array( | ||
| 'type' => 'string', | ||
| 'description' => __( 'Description of the user.' ), | ||
| ), | ||
| 'display_name' => array( | ||
| 'type' => 'string', | ||
| 'description' => __( 'Display name for the user.' ), | ||
| ), | ||
| 'email' => array( | ||
| 'type' => 'string', | ||
| 'format' => 'email', | ||
| 'description' => __( 'The email address for the user.' ), | ||
| ), | ||
| 'first_name' => array( | ||
| 'type' => 'string', | ||
| 'description' => __( 'First name for the user.' ), | ||
| ), | ||
| 'id' => array( | ||
| 'type' => 'integer', | ||
| 'description' => __( 'Unique identifier for the user.' ), | ||
| ), | ||
| 'last_name' => array( | ||
| 'type' => 'string', | ||
| 'description' => __( 'Last name for the user.' ), | ||
| ), | ||
| 'link' => array( | ||
| 'type' => 'string', | ||
| 'format' => 'uri', | ||
| 'description' => __( 'Author URL of the user.' ), | ||
| ), | ||
| 'locale' => array( | ||
| 'type' => 'string', | ||
| 'enum' => array_merge( array( '', 'en_US' ), get_available_languages() ), | ||
| 'description' => __( 'Locale for the user.' ), | ||
| ), | ||
| 'nickname' => array( | ||
| 'type' => 'string', | ||
| 'description' => __( 'The nickname for the user.' ), | ||
| ), | ||
| 'registered_date' => array( | ||
| 'type' => 'string', | ||
| 'format' => 'date-time', | ||
| 'description' => __( 'Registration date for the user in ISO 8601 format.' ), | ||
| ), | ||
| 'roles' => array( | ||
| 'type' => 'array', | ||
| 'description' => __( 'Roles assigned to the user when the current user can view them.' ), | ||
| 'items' => array( | ||
| 'type' => 'string', | ||
| ), | ||
| ), | ||
| 'slug' => array( | ||
| 'type' => 'string', | ||
| 'description' => __( 'An alphanumeric identifier for the user.' ), | ||
| ), | ||
| 'url' => array( | ||
| 'type' => 'string', | ||
| 'format' => 'uri', | ||
| 'description' => __( 'URL of the user.' ), | ||
| ), | ||
| 'username' => array( | ||
| 'type' => 'string', | ||
| 'description' => __( 'Login name for the user.' ), | ||
| ), | ||
| ), | ||
| 'additionalProperties' => false, | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Normalizes a list of values into an array of strings. | ||
| * | ||
| * @since 7.0.0 | ||
| * | ||
| * @param mixed $values Values to normalize. | ||
| * @return array<int, string> Normalized string list. | ||
| */ | ||
| private static function normalize_string_list( $values ): array { | ||
| $normalized = array(); | ||
|
|
||
| foreach ( (array) $values as $value ) { | ||
| if ( null === $value || is_scalar( $value ) || ( is_object( $value ) && is_callable( array( $value, '__toString' ) ) ) ) { | ||
| $normalized[] = (string) $value; | ||
| } | ||
| } | ||
|
|
||
| return $normalized; | ||
| } | ||
|
|
||
| /** | ||
| * Finds a user by id, username, or email from input parameters. | ||
| * | ||
| * @since 7.0.0 | ||
| * | ||
| * @param array<string, mixed> $input The input parameters. | ||
| * @return WP_User|false The user object if found, false otherwise. | ||
| */ | ||
| private static function find_user( array $input ) { | ||
| if ( ! empty( $input['id'] ) && $input['id'] > 0 ) { | ||
| return get_user_by( 'ID', (int) $input['id'] ); | ||
| } | ||
|
|
||
| if ( ! empty( $input['username'] ) ) { | ||
| return get_user_by( 'login', sanitize_user( $input['username'] ) ); | ||
| } | ||
|
|
||
| if ( ! empty( $input['email'] ) ) { | ||
| return get_user_by( 'email', sanitize_email( $input['email'] ) ); | ||
| } | ||
|
|
||
| return false; | ||
| } | ||
|
|
||
| /** | ||
| * Permission callback for the get-user ability. | ||
| * | ||
| * @since 7.0.0 | ||
| * | ||
| * @param array<string, mixed> $input The input parameters. | ||
| * @return bool Whether the user has permission. | ||
| */ | ||
| public static function check_get_user_permission( array $input = array() ): bool { | ||
| // Must be logged in. | ||
| if ( ! is_user_logged_in() ) { | ||
| return false; | ||
| } | ||
|
|
||
| $user = self::find_user( $input ); | ||
|
|
||
| // Allow privileged users to query unknown targets and receive not-found responses. | ||
| if ( ! $user || ! $user->exists() ) { | ||
| return current_user_can( 'edit_users' ); | ||
| } | ||
|
|
||
| // Users can view their own profile. | ||
| if ( get_current_user_id() === $user->ID ) { | ||
| return true; | ||
| } | ||
|
|
||
| // Otherwise require permission to edit the specific target user. | ||
| return current_user_can( 'edit_user', $user->ID ); | ||
| } | ||
|
|
||
| /** | ||
| * Executes the get-user ability. | ||
| * | ||
| * @since 7.0.0 | ||
| * | ||
| * @param array<string, mixed> $input The input parameters. | ||
| * @return array<string, mixed>|WP_Error The user data or error. | ||
| */ | ||
| public static function execute_get_user( array $input = array() ) { | ||
jorgefilipecosta marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| $input = is_array( $input ) ? $input : array(); | ||
|
|
||
| $user = self::find_user( $input ); | ||
|
|
||
| if ( ! $user || ! $user->exists() ) { | ||
| return new WP_Error( 'user_not_found', __( 'User not found.' ), array( 'status' => 404 ) ); | ||
| } | ||
|
|
||
| $user_id = (int) $user->ID; | ||
| $can_view_sensitive_user_fields = self::can_view_sensitive_user_fields( $user ); | ||
|
|
||
| $result = array( | ||
jorgefilipecosta marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 'id' => $user_id, | ||
| 'display_name' => (string) $user->display_name, | ||
| 'description' => (string) $user->description, | ||
| 'url' => (string) $user->user_url, | ||
| 'link' => (string) get_author_posts_url( $user_id, $user->user_nicename ), | ||
| 'slug' => (string) $user->user_nicename, | ||
| 'avatar_urls' => rest_get_avatar_urls( $user ), | ||
| ); | ||
|
|
||
| if ( $can_view_sensitive_user_fields ) { | ||
| $result['username'] = (string) $user->user_login; | ||
| $result['email'] = (string) $user->user_email; | ||
| $result['first_name'] = (string) $user->first_name; | ||
| $result['last_name'] = (string) $user->last_name; | ||
| $result['nickname'] = (string) $user->nickname; | ||
| $result['locale'] = (string) get_user_locale( $user ); | ||
|
|
||
| $registered_timestamp = strtotime( (string) $user->user_registered ); | ||
| if ( false !== $registered_timestamp ) { | ||
| $result['registered_date'] = gmdate( 'c', $registered_timestamp ); | ||
| } | ||
| } | ||
|
|
||
| if ( self::can_view_user_roles( $user ) ) { | ||
| $result['roles'] = self::normalize_string_list( $user->roles ); | ||
| } | ||
|
|
||
| return $result; | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What makes these "required"? It's clearly not a promise of content since we're not coercing empty values at all...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It means the keys will always be on the returned object even if they may be empty. But maybe we don't need to make them required. I guess we could consider only id and username as required. Will do an update.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is that helpful on
outputschema?Input I understand, if it's
requiredthen a consumer minimally needs to an explicitnull, but is knowing akeyexists but may benull|undefined|empty-string|beneficial?I could see the benefit of using required to choreograph if an output field will always have a value (like id/username) whereas fields that are coerced to
( (cast-type ) $value ) ?: nullare "optional".( Otherwise, then we should put all our output keys as required and let it be a guarantee that the prop exists. Don't see any Core use case of
optionalif it's just about us guaranteeing key presence)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The more complete the schema is the better, in this case we know any valid user must have at least an id and username, so we should provide that information in the schema.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Works for me. If we want to promise that then we need to enforce it though and not just that a
{key}is present