diff -r 7b1b88e27a20 -r 48c4eec2b7e6 wp/wp-includes/user.php --- a/wp/wp-includes/user.php Thu Sep 29 08:06:27 2022 +0200 +++ b/wp/wp-includes/user.php Fri Sep 05 18:40:08 2025 +0200 @@ -25,14 +25,28 @@ * @since 2.5.0 * * @global string $auth_secure_cookie - * - * @param array $credentials Optional. User info in order to sign on. + * @global wpdb $wpdb WordPress database abstraction object. + * + * @param array $credentials { + * Optional. User info in order to sign on. + * + * @type string $user_login Username. + * @type string $user_password User password. + * @type bool $remember Whether to 'remember' the user. Increases the time + * that the cookie will be kept. Default false. + * } * @param string|bool $secure_cookie Optional. Whether to use secure cookie. * @return WP_User|WP_Error WP_User on success, WP_Error on failure. */ function wp_signon( $credentials = array(), $secure_cookie = '' ) { + global $auth_secure_cookie, $wpdb; + if ( empty( $credentials ) ) { - $credentials = array(); // Back-compat for plugins passing an empty string. + $credentials = array( + 'user_login' => '', + 'user_password' => '', + 'remember' => false, + ); if ( ! empty( $_POST['log'] ) ) { $credentials['user_login'] = wp_unslash( $_POST['log'] ); @@ -87,7 +101,7 @@ */ $secure_cookie = apply_filters( 'secure_signon_cookie', $secure_cookie, $credentials ); - global $auth_secure_cookie; // XXX ugly hack to pass this to wp_authenticate_cookie(). + // XXX ugly hack to pass this to wp_authenticate_cookie(). $auth_secure_cookie = $secure_cookie; add_filter( 'authenticate', 'wp_authenticate_cookie', 30, 3 ); @@ -99,6 +113,20 @@ } wp_set_auth_cookie( $user->ID, $credentials['remember'], $secure_cookie ); + + // Clear `user_activation_key` after a successful login. + if ( ! empty( $user->user_activation_key ) ) { + $wpdb->update( + $wpdb->users, + array( + 'user_activation_key' => '', + ), + array( 'ID' => $user->ID ) + ); + + $user->user_activation_key = ''; + } + /** * Fires after the user has successfully logged in. * @@ -108,6 +136,7 @@ * @param WP_User $user WP_User object of the logged-in user. */ do_action( 'wp_login', $user->user_login, $user ); + return $user; } @@ -134,11 +163,11 @@ $error = new WP_Error(); if ( empty( $username ) ) { - $error->add( 'empty_username', __( 'Error: The username field is empty.' ) ); + $error->add( 'empty_username', __( 'Error: The username field is empty.' ) ); } if ( empty( $password ) ) { - $error->add( 'empty_password', __( 'Error: The password field is empty.' ) ); + $error->add( 'empty_password', __( 'Error: The password field is empty.' ) ); } return $error; @@ -151,7 +180,7 @@ 'invalid_username', sprintf( /* translators: %s: User name. */ - __( 'Error: The username %s is not registered on this site. If you are unsure of your username, try your email address instead.' ), + __( 'Error: The username %s is not registered on this site. If you are unsure of your username, try your email address instead.' ), $username ) ); @@ -176,7 +205,7 @@ 'incorrect_password', sprintf( /* translators: %s: User name. */ - __( 'Error: The password you entered for the username %s is incorrect.' ), + __( 'Error: The password you entered for the username %s is incorrect.' ), '' . $username . '' ) . ' ' . @@ -213,11 +242,11 @@ if ( empty( $email ) ) { // Uses 'empty_username' for back-compat with wp_signon(). - $error->add( 'empty_username', __( 'Error: The email field is empty.' ) ); + $error->add( 'empty_username', __( 'Error: The email field is empty.' ) ); } if ( empty( $password ) ) { - $error->add( 'empty_password', __( 'Error: The password field is empty.' ) ); + $error->add( 'empty_password', __( 'Error: The password field is empty.' ) ); } return $error; @@ -248,7 +277,7 @@ 'incorrect_password', sprintf( /* translators: %s: Email address. */ - __( 'Error: The password you entered for the email address %s is incorrect.' ), + __( 'Error: The password you entered for the email address %s is incorrect.' ), '' . $email . '' ) . ' ' . @@ -273,6 +302,8 @@ * @return WP_User|WP_Error WP_User on success, WP_Error on failure. */ function wp_authenticate_cookie( $user, $username, $password ) { + global $auth_secure_cookie; + if ( $user instanceof WP_User ) { return $user; } @@ -283,8 +314,6 @@ return new WP_User( $user_id ); } - global $auth_secure_cookie; - if ( $auth_secure_cookie ) { $auth_cookie = SECURE_AUTH_COOKIE; } else { @@ -322,6 +351,7 @@ return $input_user; } + // The 'REST_REQUEST' check here may happen too early for the constant to be available. $is_api_request = ( ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST ) || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ); /** @@ -351,12 +381,12 @@ if ( is_email( $username ) ) { $error = new WP_Error( 'invalid_email', - __( 'Error: Unknown email address. Check again or try your username.' ) + __( 'Error: Unknown email address. Check again or try your username.' ) ); } else { $error = new WP_Error( 'invalid_username', - __( 'Error: Unknown username. Check again or try your email address.' ) + __( 'Error: Unknown username. Check again or try your email address.' ) ); } } elseif ( ! wp_is_application_passwords_available() ) { @@ -503,7 +533,7 @@ $spammed = apply_filters( 'check_is_user_spammed', is_user_spammy( $user ), $user ); if ( $spammed ) { - return new WP_Error( 'spammer_account', __( 'Error: Your account has been marked as a spammer.' ) ); + return new WP_Error( 'spammer_account', __( 'Error: Your account has been marked as a spammer.' ) ); } } return $user; @@ -691,7 +721,7 @@ * Updates user option with global blog capability. * * User options are just like user metadata except that they have support for - * global blog options. If the 'global' parameter is false, which it is by default + * global blog options. If the 'is_global' parameter is false, which it is by default, * it will prepend the WordPress table prefix to the option name. * * Deletes the user option if $newvalue is empty. @@ -703,15 +733,15 @@ * @param int $user_id User ID. * @param string $option_name User option name. * @param mixed $newvalue User option value. - * @param bool $global Optional. Whether option name is global or blog specific. + * @param bool $is_global Optional. Whether option name is global or blog specific. * Default false (blog specific). * @return int|bool User meta ID if the option didn't exist, true on successful update, * false on failure. */ -function update_user_option( $user_id, $option_name, $newvalue, $global = false ) { +function update_user_option( $user_id, $option_name, $newvalue, $is_global = false ) { global $wpdb; - if ( ! $global ) { + if ( ! $is_global ) { $option_name = $wpdb->get_blog_prefix() . $option_name; } @@ -722,7 +752,7 @@ * Deletes user option with global blog capability. * * User options are just like user metadata except that they have support for - * global blog options. If the 'global' parameter is false, which it is by default + * global blog options. If the 'is_global' parameter is false, which it is by default, * it will prepend the WordPress table prefix to the option name. * * @since 3.0.0 @@ -731,16 +761,17 @@ * * @param int $user_id User ID * @param string $option_name User option name. - * @param bool $global Optional. Whether option name is global or blog specific. + * @param bool $is_global Optional. Whether option name is global or blog specific. * Default false (blog specific). * @return bool True on success, false on failure. */ -function delete_user_option( $user_id, $option_name, $global = false ) { +function delete_user_option( $user_id, $option_name, $is_global = false ) { global $wpdb; - if ( ! $global ) { + if ( ! $is_global ) { $option_name = $wpdb->get_blog_prefix() . $option_name; } + return delete_user_meta( $user_id, $option_name ); } @@ -811,75 +842,91 @@ 'include' => '', ); - $args = wp_parse_args( $args, $defaults ); + $parsed_args = wp_parse_args( $args, $defaults ); $return = ''; - $query_args = wp_array_slice_assoc( $args, array( 'orderby', 'order', 'number', 'exclude', 'include' ) ); + $query_args = wp_array_slice_assoc( $parsed_args, array( 'orderby', 'order', 'number', 'exclude', 'include' ) ); $query_args['fields'] = 'ids'; - $users = get_users( $query_args ); + + /** + * Filters the query arguments for the list of all users of the site. + * + * @since 6.1.0 + * + * @param array $query_args The query arguments for get_users(). + * @param array $parsed_args The arguments passed to wp_list_users() combined with the defaults. + */ + $query_args = apply_filters( 'wp_list_users_args', $query_args, $parsed_args ); + + $users = get_users( $query_args ); foreach ( $users as $user_id ) { $user = get_userdata( $user_id ); - if ( $args['exclude_admin'] && 'admin' === $user->display_name ) { + if ( $parsed_args['exclude_admin'] && 'admin' === $user->display_name ) { continue; } - if ( $args['show_fullname'] && '' !== $user->first_name && '' !== $user->last_name ) { - $name = "$user->first_name $user->last_name"; + if ( $parsed_args['show_fullname'] && '' !== $user->first_name && '' !== $user->last_name ) { + $name = sprintf( + /* translators: 1: User's first name, 2: Last name. */ + _x( '%1$s %2$s', 'Display name based on first name and last name' ), + $user->first_name, + $user->last_name + ); } else { $name = $user->display_name; } - if ( ! $args['html'] ) { + if ( ! $parsed_args['html'] ) { $return .= $name . ', '; continue; // No need to go further to process HTML. } - if ( 'list' === $args['style'] ) { + if ( 'list' === $parsed_args['style'] ) { $return .= '
  • '; } $row = $name; - if ( ! empty( $args['feed_image'] ) || ! empty( $args['feed'] ) ) { + if ( ! empty( $parsed_args['feed_image'] ) || ! empty( $parsed_args['feed'] ) ) { $row .= ' '; - if ( empty( $args['feed_image'] ) ) { + if ( empty( $parsed_args['feed_image'] ) ) { $row .= '('; } - $row .= 'ID, $parsed_args['feed_type'] ) . '"'; $alt = ''; - if ( ! empty( $args['feed'] ) ) { - $alt = ' alt="' . esc_attr( $args['feed'] ) . '"'; - $name = $args['feed']; + if ( ! empty( $parsed_args['feed'] ) ) { + $alt = ' alt="' . esc_attr( $parsed_args['feed'] ) . '"'; + $name = $parsed_args['feed']; } $row .= '>'; - if ( ! empty( $args['feed_image'] ) ) { - $row .= ''; + if ( ! empty( $parsed_args['feed_image'] ) ) { + $row .= ''; } else { $row .= $name; } $row .= ''; - if ( empty( $args['feed_image'] ) ) { + if ( empty( $parsed_args['feed_image'] ) ) { $row .= ')'; } } $return .= $row; - $return .= ( 'list' === $args['style'] ) ? '
  • ' : ', '; + $return .= ( 'list' === $parsed_args['style'] ) ? '' : ', '; } $return = rtrim( $return, ', ' ); - if ( ! $args['echo'] ) { + if ( ! $parsed_args['echo'] ) { return $return; } echo $return; @@ -935,7 +982,7 @@ if ( ! is_multisite() ) { $site_id = get_current_blog_id(); - $sites = array( $site_id => new stdClass ); + $sites = array( $site_id => new stdClass() ); $sites[ $site_id ]->userblog_id = $site_id; $sites[ $site_id ]->blogname = get_option( 'blogname' ); $sites[ $site_id ]->domain = ''; @@ -958,10 +1005,10 @@ $keys = array_keys( $keys ); foreach ( $keys as $key ) { - if ( 'capabilities' !== substr( $key, -12 ) ) { + if ( ! str_ends_with( $key, 'capabilities' ) ) { continue; } - if ( $wpdb->base_prefix && 0 !== strpos( $key, $wpdb->base_prefix ) ) { + if ( $wpdb->base_prefix && ! str_starts_with( $key, $wpdb->base_prefix ) ) { continue; } $site_id = str_replace( array( $wpdb->base_prefix, '_capabilities' ), '', $key ); @@ -976,9 +1023,8 @@ if ( ! empty( $site_ids ) ) { $args = array( - 'number' => '', - 'site__in' => $site_ids, - 'update_site_meta_cache' => false, + 'number' => '', + 'site__in' => $site_ids, ); if ( ! $all ) { $args['archived'] = 0; @@ -1038,8 +1084,10 @@ $user_id = get_current_user_id(); } - // Technically not needed, but does save calls to get_site() and get_user_meta() - // in the event that the function is called when a user isn't logged in. + /* + * Technically not needed, but does save calls to get_site() and get_user_meta() + * in the event that the function is called when a user isn't logged in. + */ if ( empty( $user_id ) ) { return false; } else { @@ -1072,7 +1120,7 @@ $base_capabilities_key = $wpdb->base_prefix . 'capabilities'; $site_capabilities_key = $wpdb->base_prefix . $blog_id . '_capabilities'; - if ( isset( $keys[ $base_capabilities_key ] ) && 1 == $blog_id ) { + if ( isset( $keys[ $base_capabilities_key ] ) && 1 === $blog_id ) { return true; } @@ -1208,10 +1256,10 @@ * * @since 5.1.0 * - * @param null|string $result The value to return instead. Default null to continue with the query. - * @param string $strategy Optional. The computational strategy to use when counting the users. - * Accepts either 'time' or 'memory'. Default 'time'. - * @param int|null $site_id Optional. The site ID to count users for. Defaults to the current site. + * @param null|array $result The value to return instead. Default null to continue with the query. + * @param string $strategy Optional. The computational strategy to use when counting the users. + * Accepts either 'time' or 'memory'. Default 'time'. + * @param int $site_id The site ID to count users for. */ $pre = apply_filters( 'pre_count_users', null, $strategy, $site_id ); @@ -1223,7 +1271,7 @@ $result = array(); if ( 'time' === $strategy ) { - if ( is_multisite() && get_current_blog_id() != $site_id ) { + if ( is_multisite() && get_current_blog_id() !== $site_id ) { switch_to_blog( $site_id ); $avail_roles = wp_roles()->get_names(); restore_current_blog(); @@ -1287,11 +1335,11 @@ continue; } if ( empty( $b_roles ) ) { - $avail_roles['none']++; + ++$avail_roles['none']; } foreach ( $b_roles as $b_role => $val ) { if ( isset( $avail_roles[ $b_role ] ) ) { - $avail_roles[ $b_role ]++; + ++$avail_roles[ $b_role ]; } else { $avail_roles[ $b_role ] = 1; } @@ -1510,54 +1558,70 @@ * * @since 2.3.0 * @since 4.5.0 Added the 'display_name_with_login' value for 'show'. - * @since 4.7.0 Added the `$role`, `$role__in`, and `$role__not_in` parameters. + * @since 4.7.0 Added the 'role', 'role__in', and 'role__not_in' parameters. + * @since 5.9.0 Added the 'capability', 'capability__in', and 'capability__not_in' parameters. + * Deprecated the 'who' parameter. * * @param array|string $args { * Optional. Array or string of arguments to generate a drop-down of users. * See WP_User_Query::prepare_query() for additional available arguments. * - * @type string $show_option_all Text to show as the drop-down default (all). - * Default empty. - * @type string $show_option_none Text to show as the drop-down default when no - * users were found. Default empty. - * @type int|string $option_none_value Value to use for $show_option_non when no users - * were found. Default -1. - * @type string $hide_if_only_one_author Whether to skip generating the drop-down - * if only one user was found. Default empty. - * @type string $orderby Field to order found users by. Accepts user fields. - * Default 'display_name'. - * @type string $order Whether to order users in ascending or descending - * order. Accepts 'ASC' (ascending) or 'DESC' (descending). - * Default 'ASC'. - * @type int[]|string $include Array or comma-separated list of user IDs to include. - * Default empty. - * @type int[]|string $exclude Array or comma-separated list of user IDs to exclude. - * Default empty. - * @type bool|int $multi Whether to skip the ID attribute on the 'select' element. - * Accepts 1|true or 0|false. Default 0|false. - * @type string $show User data to display. If the selected item is empty - * then the 'user_login' will be displayed in parentheses. - * Accepts any user field, or 'display_name_with_login' to show - * the display name with user_login in parentheses. - * Default 'display_name'. - * @type int|bool $echo Whether to echo or return the drop-down. Accepts 1|true (echo) - * or 0|false (return). Default 1|true. - * @type int $selected Which user ID should be selected. Default 0. - * @type bool $include_selected Whether to always include the selected user ID in the drop- - * down. Default false. - * @type string $name Name attribute of select element. Default 'user'. - * @type string $id ID attribute of the select element. Default is the value of $name. - * @type string $class Class attribute of the select element. Default empty. - * @type int $blog_id ID of blog (Multisite only). Default is ID of the current blog. - * @type string $who Which type of users to query. Accepts only an empty string or - * 'authors'. Default empty. - * @type string|array $role An array or a comma-separated list of role names that users must - * match to be included in results. Note that this is an inclusive - * list: users must match *each* role. Default empty. - * @type string[] $role__in An array of role names. Matched users must have at least one of - * these roles. Default empty array. - * @type string[] $role__not_in An array of role names to exclude. Users matching one or more of - * these roles will not be included in results. Default empty array. + * @type string $show_option_all Text to show as the drop-down default (all). + * Default empty. + * @type string $show_option_none Text to show as the drop-down default when no + * users were found. Default empty. + * @type int|string $option_none_value Value to use for `$show_option_none` when no users + * were found. Default -1. + * @type string $hide_if_only_one_author Whether to skip generating the drop-down + * if only one user was found. Default empty. + * @type string $orderby Field to order found users by. Accepts user fields. + * Default 'display_name'. + * @type string $order Whether to order users in ascending or descending + * order. Accepts 'ASC' (ascending) or 'DESC' (descending). + * Default 'ASC'. + * @type int[]|string $include Array or comma-separated list of user IDs to include. + * Default empty. + * @type int[]|string $exclude Array or comma-separated list of user IDs to exclude. + * Default empty. + * @type bool|int $multi Whether to skip the ID attribute on the 'select' element. + * Accepts 1|true or 0|false. Default 0|false. + * @type string $show User data to display. If the selected item is empty + * then the 'user_login' will be displayed in parentheses. + * Accepts any user field, or 'display_name_with_login' to show + * the display name with user_login in parentheses. + * Default 'display_name'. + * @type int|bool $echo Whether to echo or return the drop-down. Accepts 1|true (echo) + * or 0|false (return). Default 1|true. + * @type int $selected Which user ID should be selected. Default 0. + * @type bool $include_selected Whether to always include the selected user ID in the drop- + * down. Default false. + * @type string $name Name attribute of select element. Default 'user'. + * @type string $id ID attribute of the select element. Default is the value of `$name`. + * @type string $class Class attribute of the select element. Default empty. + * @type int $blog_id ID of blog (Multisite only). Default is ID of the current blog. + * @type string $who Deprecated, use `$capability` instead. + * Which type of users to query. Accepts only an empty string or + * 'authors'. Default empty (all users). + * @type string|string[] $role An array or a comma-separated list of role names that users + * must match to be included in results. Note that this is + * an inclusive list: users must match *each* role. Default empty. + * @type string[] $role__in An array of role names. Matched users must have at least one + * of these roles. Default empty array. + * @type string[] $role__not_in An array of role names to exclude. Users matching one or more + * of these roles will not be included in results. Default empty array. + * @type string|string[] $capability An array or a comma-separated list of capability names that users + * must match to be included in results. Note that this is + * an inclusive list: users must match *each* capability. + * Does NOT work for capabilities not in the database or filtered + * via {@see 'map_meta_cap'}. Default empty. + * @type string[] $capability__in An array of capability names. Matched users must have at least one + * of these capabilities. + * Does NOT work for capabilities not in the database or filtered + * via {@see 'map_meta_cap'}. Default empty array. + * @type string[] $capability__not_in An array of capability names to exclude. Users matching one or more + * of these capabilities will not be included in results. + * Does NOT work for capabilities not in the database or filtered + * via {@see 'map_meta_cap'}. Default empty array. * } * @return string HTML dropdown list of users. */ @@ -1738,7 +1802,7 @@ return $value; } - $prefixed = false !== strpos( $field, 'user_' ); + $prefixed = str_contains( $field, 'user_' ); if ( 'edit' === $context ) { if ( $prefixed ) { @@ -1845,8 +1909,11 @@ wp_cache_add( $user->ID, $user, 'users' ); wp_cache_add( $user->user_login, $user->ID, 'userlogins' ); - wp_cache_add( $user->user_email, $user->ID, 'useremail' ); wp_cache_add( $user->user_nicename, $user->ID, 'userslugs' ); + + if ( ! empty( $user->user_email ) ) { + wp_cache_add( $user->user_email, $user->ID, 'useremail' ); + } } /** @@ -1854,15 +1921,11 @@ * * @since 3.0.0 * @since 4.4.0 'clean_user_cache' action was added. - * @since 5.8.0 Refreshes the global user instance if cleaning the user cache for the current user. - * - * @global WP_User $current_user The current user object which holds the user data. + * @since 6.2.0 User metadata caches are now cleared. * * @param WP_User|int $user User object or ID to be cleaned from the cache */ function clean_user_cache( $user ) { - global $current_user; - if ( is_numeric( $user ) ) { $user = new WP_User( $user ); } @@ -1873,9 +1936,15 @@ wp_cache_delete( $user->ID, 'users' ); wp_cache_delete( $user->user_login, 'userlogins' ); - wp_cache_delete( $user->user_email, 'useremail' ); wp_cache_delete( $user->user_nicename, 'userslugs' ); + if ( ! empty( $user->user_email ) ) { + wp_cache_delete( $user->user_email, 'useremail' ); + } + + wp_cache_delete( $user->ID, 'user_meta' ); + wp_cache_set_users_last_changed(); + /** * Fires immediately after the given user's cache is cleaned. * @@ -1885,13 +1954,6 @@ * @param WP_User $user User object. */ do_action( 'clean_user_cache', $user->ID, $user ); - - // Refresh the global user instance if the cleaning current user. - if ( get_current_user_id() === (int) $user->ID ) { - $user_id = (int) $user->ID; - $current_user = null; - wp_set_current_user( $user_id, '' ); - } } /** @@ -1969,7 +2031,7 @@ */ function validate_username( $username ) { $sanitized = sanitize_user( $username, true ); - $valid = ( $sanitized == $username && ! empty( $sanitized ) ); + $valid = ( $sanitized === $username && ! empty( $sanitized ) ); /** * Filters whether the provided username is valid. @@ -2005,7 +2067,8 @@ * An array, object, or WP_User object of user data arguments. * * @type int $ID User ID. If supplied, the user will be updated. - * @type string $user_pass The plain-text user password. + * @type string $user_pass The plain-text user password for new users. + * Hashed password for existing users. * @type string $user_login The user's login username. * @type string $user_nicename The URL-friendly user name. * @type string $user_url The user URL. @@ -2067,6 +2130,9 @@ return new WP_Error( 'invalid_user_id', __( 'Invalid user ID.' ) ); } + // Slash current user email to compare it later with slashed new user email. + $old_user_data->user_email = wp_slash( $old_user_data->user_email ); + // Hashed in wp_update_user(), plaintext if called directly. $user_pass = ! empty( $userdata['user_pass'] ) ? $userdata['user_pass'] : $old_user_data->user_pass; } else { @@ -2149,7 +2215,7 @@ $base_length = 49 - mb_strlen( $suffix ); $alt_user_nicename = mb_substr( $user_nicename, 0, $base_length ) . "-$suffix"; $user_nicename_check = $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM $wpdb->users WHERE user_nicename = %s AND user_login != %s LIMIT 1", $alt_user_nicename, $user_login ) ); - $suffix++; + ++$suffix; } $user_nicename = $alt_user_nicename; } @@ -2242,8 +2308,12 @@ if ( $update ) { $display_name = $user_login; } elseif ( $meta['first_name'] && $meta['last_name'] ) { - /* translators: 1: User's first name, 2: Last name. */ - $display_name = sprintf( _x( '%1$s %2$s', 'Display name based on first name and last name' ), $meta['first_name'], $meta['last_name'] ); + $display_name = sprintf( + /* translators: 1: User's first name, 2: Last name. */ + _x( '%1$s %2$s', 'Display name based on first name and last name' ), + $meta['first_name'], + $meta['last_name'] + ); } elseif ( $meta['first_name'] ) { $display_name = $meta['first_name']; } elseif ( $meta['last_name'] ) { @@ -2399,9 +2469,16 @@ $meta = array_merge( $meta, $custom_meta ); - // Update user meta. - foreach ( $meta as $key => $value ) { - update_user_meta( $user_id, $key, $value ); + if ( $update ) { + // Update user meta. + foreach ( $meta as $key => $value ) { + update_user_meta( $user_id, $key, $value ); + } + } else { + // Add user meta. + foreach ( $meta as $key => $value ) { + add_user_meta( $user_id, $key, $value ); + } } foreach ( wp_get_user_contact_methods( $user ) as $key => $value ) { @@ -2431,8 +2508,8 @@ */ do_action( 'profile_update', $user_id, $old_user_data, $userdata ); - if ( isset( $userdata['spam'] ) && $userdata['spam'] != $old_user_data->spam ) { - if ( 1 == $userdata['spam'] ) { + if ( isset( $userdata['spam'] ) && $userdata['spam'] !== $old_user_data->spam ) { + if ( '1' === $userdata['spam'] ) { /** * Fires after the user is marked as a SPAM user. * @@ -2491,6 +2568,8 @@ $userdata = $userdata->to_array(); } + $userdata_raw = $userdata; + $user_id = isset( $userdata['ID'] ) ? (int) $userdata['ID'] : 0; if ( ! $user_id ) { return new WP_Error( 'invalid_user_id', __( 'Invalid user ID.' ) ); @@ -2560,7 +2639,7 @@ $switched_locale = false; if ( ! empty( $send_password_change_email ) || ! empty( $send_email_change_email ) ) { - $switched_locale = switch_to_locale( get_user_locale( $user_id ) ); + $switched_locale = switch_to_user_locale( $user_id ); } if ( ! empty( $send_password_change_email ) ) { @@ -2687,16 +2766,20 @@ // Update the cookies if the password changed. $current_user = wp_get_current_user(); - if ( $current_user->ID == $user_id ) { + if ( $current_user->ID === $user_id ) { if ( isset( $plaintext_pass ) ) { wp_clear_auth_cookie(); - // Here we calculate the expiration length of the current auth cookie and compare it to the default expiration. - // If it's greater than this, then we know the user checked 'Remember Me' when they logged in. + /* + * Here we calculate the expiration length of the current auth cookie and compare it to the default expiration. + * If it's greater than this, then we know the user checked 'Remember Me' when they logged in. + */ $logged_in_cookie = wp_parse_auth_cookie( '', 'logged_in' ); /** This filter is documented in wp-includes/pluggable.php */ $default_cookie_life = apply_filters( 'auth_cookie_expiration', ( 2 * DAY_IN_SECONDS ), $user_id, false ); - $remember = false; + + $remember = false; + if ( false !== $logged_in_cookie && ( $logged_in_cookie['expiration'] - time() ) > $default_cookie_life ) { $remember = true; } @@ -2705,6 +2788,17 @@ } } + /** + * Fires after the user has been updated and emails have been sent. + * + * @since 6.3.0 + * + * @param int $user_id The ID of the user that was just updated. + * @param array $userdata The array of user data that was updated. + * @param array $userdata_raw The unedited array of user data that was updated. + */ + do_action( 'wp_update_user', $user_id, $userdata, $userdata_raw ); + return $user_id; } @@ -2821,7 +2915,7 @@ * * @since 4.4.0 * - * @global PasswordHash $wp_hasher Portable PHP password hashing framework. + * @global PasswordHash $wp_hasher Portable PHP password hashing framework instance. * * @param WP_User $user User to retrieve password reset key for. * @return string|WP_Error Password reset key on success. WP_Error on error. @@ -2830,7 +2924,7 @@ global $wp_hasher; if ( ! ( $user instanceof WP_User ) ) { - return new WP_Error( 'invalidcombo', __( 'Error: There is no account with that username or email address.' ) ); + return new WP_Error( 'invalidcombo', __( 'Error: There is no account with that username or email address.' ) ); } /** @@ -2854,25 +2948,11 @@ */ do_action( 'retrieve_password', $user->user_login ); - $allow = true; - if ( is_multisite() && is_user_spammy( $user ) ) { - $allow = false; - } - - /** - * Filters whether to allow a password to be reset. - * - * @since 2.7.0 - * - * @param bool $allow Whether to allow the password to be reset. Default true. - * @param int $user_id The ID of the user attempting to reset a password. - */ - $allow = apply_filters( 'allow_password_reset', $allow, $user->ID ); - - if ( ! $allow ) { + $password_reset_allowed = wp_is_password_reset_allowed_for_user( $user ); + if ( ! $password_reset_allowed ) { return new WP_Error( 'no_password_reset', __( 'Password reset is not allowed for this user' ) ); - } elseif ( is_wp_error( $allow ) ) { - return $allow; + } elseif ( is_wp_error( $password_reset_allowed ) ) { + return $password_reset_allowed; } // Generate something random for a password reset key. @@ -2920,7 +3000,6 @@ * * @since 3.1.0 * - * @global wpdb $wpdb WordPress database object for queries. * @global PasswordHash $wp_hasher Portable PHP password hashing framework instance. * * @param string $key Hash to validate sending user's password. @@ -2928,7 +3007,7 @@ * @return WP_User|WP_Error WP_User object on success, WP_Error object for invalid or expired keys. */ function check_password_reset_key( $key, $login ) { - global $wpdb, $wp_hasher; + global $wp_hasher; $key = preg_replace( '/[^a-z0-9]/i', '', $key ); @@ -2960,7 +3039,7 @@ */ $expiration_duration = apply_filters( 'password_reset_expiration', DAY_IN_SECONDS ); - if ( false !== strpos( $user->user_activation_key, ':' ) ) { + if ( str_contains( $user->user_activation_key, ':' ) ) { list( $pass_request_time, $pass_key ) = explode( ':', $user->user_activation_key, 2 ); $expiration_time = $pass_request_time + $expiration_duration; } else { @@ -3008,8 +3087,8 @@ * @since 2.5.0 * @since 5.7.0 Added `$user_login` parameter. * - * @global wpdb $wpdb WordPress database abstraction object. - * @global PasswordHash $wp_hasher Portable PHP password hashing framework. + * @global wpdb $wpdb WordPress database abstraction object. + * @global PasswordHash $wp_hasher Portable PHP password hashing framework instance. * * @param string $user_login Optional. Username to send a password retrieval email for. * Defaults to `$_POST['user_login']` if not set. @@ -3024,15 +3103,22 @@ $user_login = $_POST['user_login']; } + $user_login = trim( wp_unslash( $user_login ) ); + if ( empty( $user_login ) ) { - $errors->add( 'empty_username', __( 'Error: Please enter a username or email address.' ) ); + $errors->add( 'empty_username', __( 'Error: Please enter a username or email address.' ) ); } elseif ( strpos( $user_login, '@' ) ) { - $user_data = get_user_by( 'email', trim( wp_unslash( $user_login ) ) ); + $user_data = get_user_by( 'email', $user_login ); + if ( empty( $user_data ) ) { - $errors->add( 'invalid_email', __( 'Error: There is no account with that username or email address.' ) ); + $user_data = get_user_by( 'login', $user_login ); + } + + if ( empty( $user_data ) ) { + $errors->add( 'invalid_email', __( 'Error: There is no account with that username or email address.' ) ); } } else { - $user_data = get_user_by( 'login', trim( wp_unslash( $user_login ) ) ); + $user_data = get_user_by( 'login', $user_login ); } /** @@ -3083,7 +3169,7 @@ } if ( ! $user_data ) { - $errors->add( 'invalidcombo', __( 'Error: There is no account with that username or email address.' ) ); + $errors->add( 'invalidcombo', __( 'Error: There is no account with that username or email address.' ) ); return $errors; } @@ -3114,7 +3200,7 @@ // Localize password reset message content for user. $locale = get_user_locale( $user_data ); - $switched_locale = switch_to_locale( $locale ); + $switched_locale = switch_to_user_locale( $user_data->ID ); if ( is_multisite() ) { $site_name = get_network()->site_name; @@ -3205,9 +3291,9 @@ * @type string $message The body of the email. * @type string $headers The headers of the email. * } - * @type string $key The activation key. - * @type string $user_login The username for the user. - * @type WP_User $user_data WP_User object. + * @param string $key The activation key. + * @param string $user_login The username for the user. + * @param WP_User $user_data WP_User object. */ $notification_email = apply_filters( 'retrieve_password_notification_email', $defaults, $key, $user_login, $user_data ); @@ -3231,8 +3317,8 @@ 'retrieve_password_email_failure', sprintf( /* translators: %s: Documentation URL. */ - __( 'Error: The email could not be sent. Your site may not be correctly configured to send emails. Get support for resetting your password.' ), - esc_url( __( 'https://wordpress.org/support/article/resetting-your-password/' ) ) + __( 'Error: The email could not be sent. Your site may not be correctly configured to send emails. Get support for resetting your password.' ), + esc_url( __( 'https://wordpress.org/documentation/article/reset-your-password/' ) ) ) ); return $errors; @@ -3298,26 +3384,25 @@ // Check the username. if ( '' === $sanitized_user_login ) { - $errors->add( 'empty_username', __( 'Error: Please enter a username.' ) ); + $errors->add( 'empty_username', __( 'Error: Please enter a username.' ) ); } elseif ( ! validate_username( $user_login ) ) { - $errors->add( 'invalid_username', __( 'Error: This username is invalid because it uses illegal characters. Please enter a valid username.' ) ); + $errors->add( 'invalid_username', __( 'Error: This username is invalid because it uses illegal characters. Please enter a valid username.' ) ); $sanitized_user_login = ''; } elseif ( username_exists( $sanitized_user_login ) ) { - $errors->add( 'username_exists', __( 'Error: This username is already registered. Please choose another one.' ) ); - + $errors->add( 'username_exists', __( 'Error: This username is already registered. Please choose another one.' ) ); } else { /** This filter is documented in wp-includes/user.php */ $illegal_user_logins = (array) apply_filters( 'illegal_user_logins', array() ); if ( in_array( strtolower( $sanitized_user_login ), array_map( 'strtolower', $illegal_user_logins ), true ) ) { - $errors->add( 'invalid_username', __( 'Error: Sorry, that username is not allowed.' ) ); + $errors->add( 'invalid_username', __( 'Error: Sorry, that username is not allowed.' ) ); } } // Check the email address. if ( '' === $user_email ) { - $errors->add( 'empty_email', __( 'Error: Please type your email address.' ) ); + $errors->add( 'empty_email', __( 'Error: Please type your email address.' ) ); } elseif ( ! is_email( $user_email ) ) { - $errors->add( 'invalid_email', __( 'Error: The email address is not correct.' ) ); + $errors->add( 'invalid_email', __( 'Error: The email address is not correct.' ) ); $user_email = ''; } elseif ( email_exists( $user_email ) ) { $errors->add( @@ -3372,7 +3457,7 @@ 'registerfail', sprintf( /* translators: %s: Admin email address. */ - __( 'Error: Could not register you… please contact the site admin!' ), + __( 'Error: Could not register you… please contact the site admin!' ), get_option( 'admin_email' ) ) ); @@ -3484,6 +3569,8 @@ * @since 4.4.0 * @since 4.9.0 The `$site_id` parameter was added to support multisite. * + * @global wpdb $wpdb WordPress database abstraction object. + * * @param int|null $site_id Optional. The site ID to get users with no role for. Defaults to the current site. * @return string[] Array of user IDs as strings. */ @@ -3496,7 +3583,7 @@ $prefix = $wpdb->get_blog_prefix( $site_id ); - if ( is_multisite() && get_current_blog_id() != $site_id ) { + if ( is_multisite() && get_current_blog_id() !== $site_id ) { switch_to_blog( $site_id ); $role_names = wp_roles()->get_names(); restore_current_blog(); @@ -3508,12 +3595,10 @@ $regex = preg_replace( '/[^a-zA-Z_\|-]/', '', $regex ); $users = $wpdb->get_col( $wpdb->prepare( - " - SELECT user_id - FROM $wpdb->usermeta - WHERE meta_key = '{$prefix}capabilities' - AND meta_value NOT REGEXP %s - ", + "SELECT user_id + FROM $wpdb->usermeta + WHERE meta_key = '{$prefix}capabilities' + AND meta_value NOT REGEXP %s", $regex ) ); @@ -3607,15 +3692,15 @@ $errors = new WP_Error(); } - if ( $current_user->ID != $_POST['user_id'] ) { + if ( $current_user->ID !== (int) $_POST['user_id'] ) { return false; } - if ( $current_user->user_email != $_POST['email'] ) { + if ( $current_user->user_email !== $_POST['email'] ) { if ( ! is_email( $_POST['email'] ) ) { $errors->add( 'user_email', - __( 'Error: The email address is not correct.' ), + __( 'Error: The email address is not correct.' ), array( 'form-field' => 'email', ) @@ -3627,7 +3712,7 @@ if ( email_exists( $_POST['email'] ) ) { $errors->add( 'user_email', - __( 'Error: The email address is already used.' ), + __( 'Error: The email address is already used.' ), array( 'form-field' => 'email', ) @@ -3689,7 +3774,7 @@ $content = apply_filters( 'new_user_email_content', $email_text, $new_user_email ); $content = str_replace( '###USERNAME###', $current_user->user_login, $content ); - $content = str_replace( '###ADMIN_URL###', esc_url( admin_url( 'profile.php?newuseremail=' . $hash ) ), $content ); + $content = str_replace( '###ADMIN_URL###', esc_url( self_admin_url( 'profile.php?newuseremail=' . $hash ) ), $content ); $content = str_replace( '###EMAIL###', $_POST['email'], $content ); $content = str_replace( '###SITENAME###', $sitename, $content ); $content = str_replace( '###SITEURL###', home_url(), $content ); @@ -3716,8 +3801,12 @@ if ( 'profile.php' === $pagenow && isset( $_GET['updated'] ) ) { $email = get_user_meta( get_current_user_id(), '_new_email', true ); if ( $email ) { - /* translators: %s: New email address. */ - echo '

    ' . sprintf( __( 'Your email address has not been updated yet. Please check your inbox at %s for a confirmation email.' ), '' . esc_html( $email['newemail'] ) . '' ) . '

    '; + $message = sprintf( + /* translators: %s: New email address. */ + __( 'Your email address has not been updated yet. Please check your inbox at %s for a confirmation email.' ), + '' . esc_html( $email['newemail'] ) . '' + ); + wp_admin_notice( $message, array( 'type' => 'info' ) ); } } } @@ -3728,7 +3817,7 @@ * @since 4.9.6 * @access private * - * @return array List of core privacy action types. + * @return string[] List of core privacy action types. */ function _wp_privacy_action_request_types() { return array( @@ -3742,8 +3831,8 @@ * * @since 4.9.6 * - * @param array $exporters An array of personal data exporters. - * @return array An array of personal data exporters. + * @param array[] $exporters An array of personal data exporters. + * @return array[] An array of personal data exporters. */ function wp_register_user_personal_data_exporter( $exporters ) { $exporters['wordpress-user'] = array( @@ -3762,7 +3851,12 @@ * @since 5.4.0 Added 'Session Tokens' group to the export data. * * @param string $email_address The user's email address. - * @return array An array of personal data. + * @return array { + * An array of personal data. + * + * @type array[] $data An array of personal data arrays. + * @type bool $done Whether the exporter is finished. + * } */ function wp_user_personal_data_exporter( $email_address ) { $email_address = trim( $email_address ); @@ -3849,7 +3943,7 @@ // Remove items that use reserved names. $extra_data = array_filter( $_extra_data, - static function( $item ) use ( $reserved_names ) { + static function ( $item ) use ( $reserved_names ) { return ! in_array( $item['name'], $reserved_names, true ); } ); @@ -3993,7 +4087,7 @@ function _wp_privacy_send_request_confirmation_notification( $request_id ) { $request = wp_get_user_request( $request_id ); - if ( ! is_a( $request, 'WP_User_Request' ) || 'request-confirmed' !== $request->status ) { + if ( ! ( $request instanceof WP_User_Request ) || 'request-confirmed' !== $request->status ) { return; } @@ -4158,8 +4252,8 @@ $content = str_replace( '###SITENAME###', $email_data['sitename'], $content ); $content = str_replace( '###USER_EMAIL###', $email_data['user_email'], $content ); $content = str_replace( '###DESCRIPTION###', $email_data['description'], $content ); - $content = str_replace( '###MANAGE_URL###', esc_url_raw( $email_data['manage_url'] ), $content ); - $content = str_replace( '###SITEURL###', esc_url_raw( $email_data['siteurl'] ), $content ); + $content = str_replace( '###MANAGE_URL###', sanitize_url( $email_data['manage_url'] ), $content ); + $content = str_replace( '###SITEURL###', sanitize_url( $email_data['siteurl'] ), $content ); $headers = ''; @@ -4205,7 +4299,7 @@ function _wp_privacy_send_erasure_fulfillment_notification( $request_id ) { $request = wp_get_user_request( $request_id ); - if ( ! is_a( $request, 'WP_User_Request' ) || 'request-completed' !== $request->status ) { + if ( ! ( $request instanceof WP_User_Request ) || 'request-completed' !== $request->status ) { return; } @@ -4217,12 +4311,10 @@ // Localize message content for user; fallback to site default for visitors. if ( ! empty( $request->user_id ) ) { - $locale = get_user_locale( $request->user_id ); + $switched_locale = switch_to_user_locale( $request->user_id ); } else { - $locale = get_locale(); - } - - $switched_locale = switch_to_locale( $locale ); + $switched_locale = switch_to_locale( get_locale() ); + } /** * Filters the recipient of the data erasure fulfillment notification. @@ -4399,7 +4491,7 @@ $content = str_replace( '###SITENAME###', $email_data['sitename'], $content ); $content = str_replace( '###PRIVACY_POLICY_URL###', $email_data['privacy_policy_url'], $content ); - $content = str_replace( '###SITEURL###', esc_url_raw( $email_data['siteurl'] ), $content ); + $content = str_replace( '###SITEURL###', sanitize_url( $email_data['siteurl'] ), $content ); $headers = ''; @@ -4628,12 +4720,10 @@ // Localize message content for user; fallback to site default for visitors. if ( ! empty( $request->user_id ) ) { - $locale = get_user_locale( $request->user_id ); + $switched_locale = switch_to_user_locale( $request->user_id ); } else { - $locale = get_locale(); - } - - $switched_locale = switch_to_locale( $locale ); + $switched_locale = switch_to_locale( get_locale() ); + } $email_data = array( 'request' => $request, @@ -4720,10 +4810,10 @@ $content = apply_filters( 'user_request_action_email_content', $content, $email_data ); $content = str_replace( '###DESCRIPTION###', $email_data['description'], $content ); - $content = str_replace( '###CONFIRM_URL###', esc_url_raw( $email_data['confirm_url'] ), $content ); + $content = str_replace( '###CONFIRM_URL###', sanitize_url( $email_data['confirm_url'] ), $content ); $content = str_replace( '###EMAIL###', $email_data['email'], $content ); $content = str_replace( '###SITENAME###', $email_data['sitename'], $content ); - $content = str_replace( '###SITEURL###', esc_url_raw( $email_data['siteurl'] ), $content ); + $content = str_replace( '###SITEURL###', sanitize_url( $email_data['siteurl'] ), $content ); $headers = ''; @@ -4767,6 +4857,8 @@ * * @since 4.9.6 * + * @global PasswordHash $wp_hasher Portable PHP password hashing framework instance. + * * @param int $request_id Request ID. * @return string Confirmation key. */ @@ -4798,6 +4890,8 @@ * * @since 4.9.6 * + * @global PasswordHash $wp_hasher Portable PHP password hashing framework instance. + * * @param string $request_id ID of the request being confirmed. * @param string $key Provided key to validate. * @return true|WP_Error True on success, WP_Error on failure. @@ -4936,3 +5030,91 @@ */ return apply_filters( 'wp_is_application_passwords_available_for_user', true, $user ); } + +/** + * Registers the user meta property for persisted preferences. + * + * This property is used to store user preferences across page reloads and is + * currently used by the block editor for preferences like 'fullscreenMode' and + * 'fixedToolbar'. + * + * @since 6.1.0 + * @access private + * + * @global wpdb $wpdb WordPress database abstraction object. + */ +function wp_register_persisted_preferences_meta() { + /* + * Create a meta key that incorporates the blog prefix so that each site + * on a multisite can have distinct user preferences. + */ + global $wpdb; + $meta_key = $wpdb->get_blog_prefix() . 'persisted_preferences'; + + register_meta( + 'user', + $meta_key, + array( + 'type' => 'object', + 'single' => true, + 'show_in_rest' => array( + 'name' => 'persisted_preferences', + 'type' => 'object', + 'schema' => array( + 'type' => 'object', + 'context' => array( 'edit' ), + 'properties' => array( + '_modified' => array( + 'description' => __( 'The date and time the preferences were updated.' ), + 'type' => 'string', + 'format' => 'date-time', + 'readonly' => false, + ), + ), + 'additionalProperties' => true, + ), + ), + ) + ); +} + +/** + * Sets the last changed time for the 'users' cache group. + * + * @since 6.3.0 + */ +function wp_cache_set_users_last_changed() { + wp_cache_set_last_changed( 'users' ); +} + +/** + * Checks if password reset is allowed for a specific user. + * + * @since 6.3.0 + * + * @param int|WP_User $user The user to check. + * @return bool|WP_Error True if allowed, false or WP_Error otherwise. + */ +function wp_is_password_reset_allowed_for_user( $user ) { + if ( ! is_object( $user ) ) { + $user = get_userdata( $user ); + } + + if ( ! $user || ! $user->exists() ) { + return false; + } + $allow = true; + if ( is_multisite() && is_user_spammy( $user ) ) { + $allow = false; + } + + /** + * Filters whether to allow a password to be reset. + * + * @since 2.7.0 + * + * @param bool $allow Whether to allow the password to be reset. Default true. + * @param int $user_id The ID of the user attempting to reset a password. + */ + return apply_filters( 'allow_password_reset', $allow, $user->ID ); +}