Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/wp-includes/abilities.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

declare( strict_types = 1 );

require_once __DIR__ . '/abilities/class-wp-users-abilities.php';
/**
* Registers the core ability categories.
*
Expand Down Expand Up @@ -257,4 +258,5 @@ function wp_register_core_abilities(): void {
),
)
);
WP_Users_Abilities::register();
}
354 changes: 354 additions & 0 deletions src/wp-includes/abilities/class-wp-users-abilities.php
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(

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...

Copy link
Member Author

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.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that helpful on output schema?

Input I understand, if it's required then a consumer minimally needs to an explicit null, but is knowing a key exists but may be null|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 ) ?: null are "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 optional if it's just about us guaranteeing key presence)

Copy link
Member Author

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.

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

'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() ) {
$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(
'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;
}
}
Loading
Loading