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
17 changes: 13 additions & 4 deletions src/wp-includes/author-template.php
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ function get_the_modified_author( $post = null ) {
if ( ! $last_id ) {
return null;
}
$last_user = get_userdata( $last_id );
$last_user = get_authordata( $last_id );

/**
* Filters the display name of the author who last edited the current post.
Expand Down Expand Up @@ -162,6 +162,9 @@ function the_modified_author() {
*
* @since 2.8.0
* @since 6.9.0 Removed `aim`, `jabber`, and `yim` as valid values for the `$field` parameter.
* @since 7.2.0 `$authordata` may now be a WP_User instance with capability and role data
* not yet loaded; requesting the (undocumented) `caps`, `roles`, or `allcaps`
* fields now loads that data on demand.
*
* @global WP_User|false|null $authordata The current author's data.
*
Expand All @@ -176,13 +179,19 @@ function get_the_author_meta( $field = '', $user_id = false ) {
global $authordata;
$user_id = $authordata->ID ?? 0;
} else {
$authordata = get_userdata( $user_id );
$authordata = get_authordata( $user_id );
}

if ( in_array( $field, array( 'login', 'pass', 'nicename', 'email', 'url', 'registered', 'activation_key', 'status' ), true ) ) {
$field = 'user_' . $field;
}

// These aren't part of the documented $field list above, but are read directly off
// $authordata by third-party code; ensure they're loaded if $authordata is short-init.
if ( in_array( $field, array( 'caps', 'roles', 'allcaps' ), true ) && $authordata instanceof WP_User ) {
$authordata->get_role_caps();
}

$value = $authordata->$field ?? '';

/**
Expand Down Expand Up @@ -399,7 +408,7 @@ function get_author_posts_url( $author_id, $author_nicename = '' ) {
$link = $file . '?author=' . $author_id;
} else {
if ( '' === $author_nicename ) {
$user = get_userdata( $author_id );
$user = get_authordata( $author_id );
if ( ! empty( $user->user_nicename ) ) {
$author_nicename = $user->user_nicename;
}
Expand Down Expand Up @@ -534,7 +543,7 @@ function wp_list_authors( $args = '' ) {
continue;
}

$author = get_userdata( $author_id );
$author = get_authordata( $author_id );

if ( $parsed_args['exclude_admin'] && 'admin' === $author->display_name ) {
continue;
Expand Down
4 changes: 2 additions & 2 deletions src/wp-includes/class-wp-query.php
Original file line number Diff line number Diff line change
Expand Up @@ -4066,7 +4066,7 @@ public function get_queried_object() {
}

if ( $this->queried_object_id ) {
$user = get_userdata( $this->queried_object_id );
$user = get_authordata( $this->queried_object_id );
if ( $user ) {
$this->queried_object = $user;
}
Expand Down Expand Up @@ -4925,7 +4925,7 @@ public function generate_postdata( $post ) {

$id = (int) $post->ID;

$authordata = get_userdata( $post->post_author );
$authordata = get_authordata( (int) $post->post_author );

$currentday = false;
$currentmonth = false;
Expand Down
83 changes: 76 additions & 7 deletions src/wp-includes/class-wp-user.php
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,20 @@ class WP_User {
*/
private static $back_compat_keys;

/**
* Whether this instance was initialized without loading capability data.
*
* When true, `$caps`, `$roles`, and `$allcaps` have not yet been populated from
* the database. They are populated on first access via `load_capability_data()`,
* called from `has_cap()`, `get_role_caps()`, `add_cap()`, `remove_cap()`,
* `add_role()`, `remove_role()`, and `set_role()`. They can be loaded explicitly
* with `load_capabilities()`.
*
* @since 7.2.0
* @var bool
*/
private $short_init = false;

/**
* Constructor.
*
Expand Down Expand Up @@ -176,17 +190,22 @@ public function __construct( $id = 0, $name = '', $site_id = 0 ) {
* Sets up object properties, including capabilities.
*
* @since 3.3.0
* @since 7.2.0 Added the `$short_init` parameter.
*
* @param object $data User DB row object.
* @param int $site_id Optional. The site ID to initialize for.
* @param object $data User DB row object.
* @param int $site_id Optional. The site ID to initialize for.
* @param bool $short_init Optional. Whether to skip loading capability and role data
* until it is first needed. Default false.
*/
public function init( $data, $site_id = 0 ) {
public function init( $data, $site_id = 0, $short_init = false ) {
if ( ! isset( $data->ID ) ) {
$data->ID = 0;
}
$this->data = $data;
$this->ID = (int) $data->ID;

$this->short_init = $short_init;

$this->for_site( $site_id );
}

Expand Down Expand Up @@ -492,9 +511,7 @@ protected function _init_caps( $cap_key = '' ) {
$this->cap_key = $cap_key;
}

$this->caps = $this->get_caps_data();

$this->get_role_caps();
$this->load_capabilities();
}

/**
Expand All @@ -511,6 +528,8 @@ protected function _init_caps( $cap_key = '' ) {
* and boolean values represent whether the user has that capability.
*/
public function get_role_caps() {
$this->load_capability_data();

$switch_site = false;
if ( is_multisite() && get_current_blog_id() !== $this->site_id ) {
$switch_site = true;
Expand Down Expand Up @@ -560,6 +579,8 @@ public function add_role( $role ) {
return;
}

$this->load_capability_data();

if ( in_array( $role, $this->roles, true ) ) {
return;
}
Expand Down Expand Up @@ -588,6 +609,8 @@ public function add_role( $role ) {
* @param string $role Role name.
*/
public function remove_role( $role ) {
$this->load_capability_data();

if ( ! in_array( $role, $this->roles, true ) ) {
return;
}
Expand Down Expand Up @@ -620,6 +643,8 @@ public function remove_role( $role ) {
* @param string $role Role name.
*/
public function set_role( $role ) {
$this->load_capability_data();

if ( 1 === count( $this->roles ) && current( $this->roles ) === $role ) {
return;
}
Expand Down Expand Up @@ -721,6 +746,8 @@ public function update_user_level_from_caps() {
* @param bool $grant Whether to grant capability to user.
*/
public function add_cap( $cap, $grant = true ) {
$this->load_capability_data();

$this->caps[ $cap ] = $grant;
update_user_meta( $this->ID, $this->cap_key, $this->caps );
$this->get_role_caps();
Expand All @@ -735,6 +762,8 @@ public function add_cap( $cap, $grant = true ) {
* @param string $cap Capability name.
*/
public function remove_cap( $cap ) {
$this->load_capability_data();

if ( ! isset( $this->caps[ $cap ] ) ) {
return;
}
Expand All @@ -753,7 +782,8 @@ public function remove_cap( $cap ) {
*/
public function remove_all_caps() {
global $wpdb;
$this->caps = array();
$this->short_init = false;
$this->caps = array();
delete_user_meta( $this->ID, $this->cap_key );
delete_user_meta( $this->ID, $wpdb->get_blog_prefix() . 'user_level' );
$this->get_role_caps();
Expand Down Expand Up @@ -787,6 +817,8 @@ public function remove_all_caps() {
* the given capability for that object.
*/
public function has_cap( $cap, ...$args ) {
$this->load_capability_data();

if ( is_numeric( $cap ) ) {
_deprecated_argument( __FUNCTION__, '2.0.0', __( 'Usage of user levels is deprecated. Use capabilities instead.' ) );
$cap = $this->translate_level_to_cap( $cap );
Expand Down Expand Up @@ -867,6 +899,8 @@ public function for_blog( $blog_id = 0 ) {
* Sets the site to operate on. Defaults to the current site.
*
* @since 4.9.0
* @since 7.2.0 Capability and role data is no longer loaded here if this instance was
* constructed with `$short_init`; it is loaded on demand instead.
*
* @global wpdb $wpdb WordPress database abstraction object.
*
Expand All @@ -883,6 +917,10 @@ public function for_site( $site_id = 0 ) {

$this->cap_key = $wpdb->get_blog_prefix( $this->site_id ) . 'capabilities';

if ( $this->short_init ) {
return;
}

$this->caps = $this->get_caps_data();

$this->get_role_caps();
Expand Down Expand Up @@ -916,4 +954,35 @@ private function get_caps_data() {

return $caps;
}

/**
* Loads capability and role data if this instance was constructed with `$short_init`
* and the data has not been loaded yet.
*
* @since 7.2.0
*/
private function load_capability_data() {
if ( ! $this->short_init ) {
return;
}

$this->load_capabilities();
}

/**
* Loads the user's capability and role data from the database for the current site.
*
* Capability data is loaded on demand for instances constructed with `$short_init`.
* This method forces it to load immediately, and can also be used to reload it.
*
* @since 7.2.0
*/
public function load_capabilities() {
// Must be set before get_role_caps(), which calls load_capability_data().
$this->short_init = false;

$this->caps = $this->get_caps_data();

$this->get_role_caps();
}
}
2 changes: 1 addition & 1 deletion src/wp-includes/class-wp.php
Original file line number Diff line number Diff line change
Expand Up @@ -677,7 +677,7 @@ public function register_globals() {
}

if ( $wp_query->is_author() ) {
$GLOBALS['authordata'] = get_userdata( get_queried_object_id() );
$GLOBALS['authordata'] = get_authordata( get_queried_object_id() );
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/wp-includes/link-template.php
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ function get_permalink( $post = 0, $leavename = false ) {

$author = '';
if ( str_contains( $permalink, '%author%' ) ) {
$authordata = get_userdata( $post->post_author );
$authordata = get_authordata( (int) $post->post_author );
$author = $authordata->user_nicename;
}

Expand Down
32 changes: 32 additions & 0 deletions src/wp-includes/user.php
Original file line number Diff line number Diff line change
Expand Up @@ -2028,6 +2028,38 @@ function sanitize_user_field( $field, $value, $user_id, $context ) {
return $value;
}

/**
* Retrieves user info by user ID, without loading capability or role data.
*
* This is a lighter-weight alternative to get_userdata() for contexts that only need
* display data (name, avatar, bio, etc.) and never check the user's capabilities or
* roles, such as author archives and the `$authordata` global set up in the Loop.
*
* Capability and role data (`$caps`, `$roles`, `$allcaps`) is not queried until it is
* actually needed: calling `has_cap()`, `get_role_caps()`, or any of the role/capability
* mutator methods on the returned WP_User will transparently load it at that point.
* Reading the `->caps`, `->roles`, or `->allcaps` properties directly, without going
* through one of those methods first, will see an empty array rather than triggering
* a load — use get_userdata() instead if that is required.
*
* @since 7.2.0
*
* @param int $user_id User ID.
* @return WP_User|false WP_User object on success, false on failure.
*/
function get_authordata( $user_id ) {
$userdata = WP_User::get_data_by( 'id', $user_id );

if ( ! $userdata ) {
return false;
}

$user = new WP_User();
$user->init( $userdata, 0, true );

return $user;
}

/**
* Updates all user caches.
*
Expand Down
10 changes: 0 additions & 10 deletions tests/phpstan/baselines/argument.type.neon
Original file line number Diff line number Diff line change
Expand Up @@ -778,11 +778,6 @@ parameters:
identifier: argument.type
count: 1
path: ../../../src/wp-includes/class-wp-query.php
-
message: '#^Parameter \#1 \$user_id of function get_userdata expects int, string given\.$#'
identifier: argument.type
count: 1
path: ../../../src/wp-includes/class-wp-query.php
-
message: '#^Parameter \#1 \$pages of function get_page_hierarchy expects array\<WP_Post\>, list\<stdClass\>\|null given\.$#'
identifier: argument.type
Expand Down Expand Up @@ -973,11 +968,6 @@ parameters:
identifier: argument.type
count: 1
path: ../../../src/wp-includes/general-template.php
-
message: '#^Parameter \#1 \$user_id of function get_userdata expects int, string given\.$#'
identifier: argument.type
count: 1
path: ../../../src/wp-includes/link-template.php
-
message: '#^Parameter \#2 \$replace of function str_replace expects array\<string\>\|string, array\<int, mixed\> given\.$#'
identifier: argument.type
Expand Down
10 changes: 9 additions & 1 deletion tests/phpunit/tests/query/generatePostdata.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,16 @@ public function test_authordata() {
$data = generate_postdata( $p );

$this->assertNotEmpty( $data['authordata'] );

// As of #58001, 'authordata' is populated via get_authordata(), a short-init WP_User
// that doesn't eagerly load capability/role data (unlike get_userdata()). Compare
// display data directly, and confirm capability data still loads correctly on demand.
$this->assertSame( $u->ID, $data['authordata']->ID );
$this->assertSame( $u->user_login, $data['authordata']->user_login );
$this->assertSame( $u->display_name, $data['authordata']->display_name );
// Keep assertEquals() because the objects are intentionally compared by value.
$this->assertEquals( $u, $data['authordata'] );
$this->assertEquals( get_authordata( $u->ID ), $data['authordata'] );
$this->assertSame( $u->has_cap( 'read' ), $data['authordata']->has_cap( 'read' ) );
}

/**
Expand Down
Loading
Loading