diff -r be944660c56a -r 3d72ae0968f4 wp/wp-includes/user.php --- a/wp/wp-includes/user.php Wed Sep 21 18:19:35 2022 +0200 +++ b/wp/wp-includes/user.php Tue Sep 27 16:37:53 2022 +0200 @@ -112,7 +112,7 @@ } /** - * Authenticate a user, confirming the username and password are valid. + * Authenticates a user, confirming the username and password are valid. * * @since 2.8.0 * @@ -158,7 +158,7 @@ } /** - * Filters whether the given user can be authenticated with the provided $password. + * Filters whether the given user can be authenticated with the provided password. * * @since 2.5.0 * @@ -261,7 +261,7 @@ } /** - * Authenticate the user using the WordPress auth cookie. + * Authenticates the user using the WordPress auth cookie. * * @since 2.8.0 * @@ -385,7 +385,7 @@ } /* - * Strip out anything non-alphanumeric. This is so passwords can be used with + * Strips out anything non-alphanumeric. This is so passwords can be used with * or without spaces to indicate the groupings for readability. * * Generated application passwords are exclusively alphanumeric. @@ -482,7 +482,7 @@ } /** - * For Multisite blogs, check if the authenticated user has been marked as a + * For Multisite blogs, checks if the authenticated user has been marked as a * spammer, or if the user's primary blog has been marked as spam. * * @since 3.7.0 @@ -537,7 +537,7 @@ } /** - * Number of posts user has written. + * Gets the number of posts a user has written. * * @since 3.0.0 * @since 4.1.0 Added `$post_type` argument. @@ -574,7 +574,7 @@ } /** - * Number of posts written by a list of users. + * Gets the number of posts written by a list of users. * * @since 3.0.0 * @@ -615,7 +615,7 @@ // /** - * Get the current user's ID + * Gets the current user's ID. * * @since MU (3.0.0) * @@ -630,7 +630,7 @@ } /** - * Retrieve user option that can be either per Site or per Network. + * Retrieves user option that can be either per Site or per Network. * * If the user ID is not given, then the current user will be used instead. If * the user ID is given, then the user data will be retrieved. The filter for @@ -688,7 +688,7 @@ } /** - * Update user option with global blog capability. + * 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 @@ -719,7 +719,7 @@ } /** - * Delete user option with global blog capability. + * 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 @@ -745,7 +745,7 @@ } /** - * Retrieve list of users matching criteria. + * Retrieves list of users matching criteria. * * @since 3.1.0 * @@ -766,7 +766,127 @@ } /** - * Get the sites a user belongs to. + * Lists all the users of the site, with several options available. + * + * @since 5.9.0 + * + * @param string|array $args { + * Optional. Array or string of default arguments. + * + * @type string $orderby How to sort the users. Accepts 'nicename', 'email', 'url', 'registered', + * 'user_nicename', 'user_email', 'user_url', 'user_registered', 'name', + * 'display_name', 'post_count', 'ID', 'meta_value', 'user_login'. Default 'name'. + * @type string $order Sorting direction for $orderby. Accepts 'ASC', 'DESC'. Default 'ASC'. + * @type int $number Maximum users to return or display. Default empty (all users). + * @type bool $exclude_admin Whether to exclude the 'admin' account, if it exists. Default false. + * @type bool $show_fullname Whether to show the user's full name. Default false. + * @type string $feed If not empty, show a link to the user's feed and use this text as the alt + * parameter of the link. Default empty. + * @type string $feed_image If not empty, show a link to the user's feed and use this image URL as + * clickable anchor. Default empty. + * @type string $feed_type The feed type to link to, such as 'rss2'. Defaults to default feed type. + * @type bool $echo Whether to output the result or instead return it. Default true. + * @type string $style If 'list', each user is wrapped in an `
  • ` element, otherwise the users + * will be separated by commas. + * @type bool $html Whether to list the items in HTML form or plaintext. Default true. + * @type string $exclude An array, comma-, or space-separated list of user IDs to exclude. Default empty. + * @type string $include An array, comma-, or space-separated list of user IDs to include. Default empty. + * } + * @return string|null The output if echo is false. Otherwise null. + */ +function wp_list_users( $args = array() ) { + $defaults = array( + 'orderby' => 'name', + 'order' => 'ASC', + 'number' => '', + 'exclude_admin' => true, + 'show_fullname' => false, + 'feed' => '', + 'feed_image' => '', + 'feed_type' => '', + 'echo' => true, + 'style' => 'list', + 'html' => true, + 'exclude' => '', + 'include' => '', + ); + + $args = wp_parse_args( $args, $defaults ); + + $return = ''; + + $query_args = wp_array_slice_assoc( $args, array( 'orderby', 'order', 'number', 'exclude', 'include' ) ); + $query_args['fields'] = 'ids'; + $users = get_users( $query_args ); + + foreach ( $users as $user_id ) { + $user = get_userdata( $user_id ); + + if ( $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"; + } else { + $name = $user->display_name; + } + + if ( ! $args['html'] ) { + $return .= $name . ', '; + + continue; // No need to go further to process HTML. + } + + if ( 'list' === $args['style'] ) { + $return .= '
  • '; + } + + $row = $name; + + if ( ! empty( $args['feed_image'] ) || ! empty( $args['feed'] ) ) { + $row .= ' '; + if ( empty( $args['feed_image'] ) ) { + $row .= '('; + } + + $row .= ''; + } else { + $row .= $name; + } + + $row .= ''; + + if ( empty( $args['feed_image'] ) ) { + $row .= ')'; + } + } + + $return .= $row; + $return .= ( 'list' === $args['style'] ) ? '
  • ' : ', '; + } + + $return = rtrim( $return, ', ' ); + + if ( ! $args['echo'] ) { + return $return; + } + echo $return; +} + +/** + * Gets the sites a user belongs to. * * @since 3.0.0 * @since 4.7.0 Converted to use `get_sites()`. @@ -898,7 +1018,7 @@ } /** - * Find out whether a user is a member of a given blog. + * Finds out whether a user is a member of a given blog. * * @since MU (3.0.0) * @@ -980,7 +1100,7 @@ } /** - * Remove metadata matching criteria from a user. + * Removes metadata matching criteria from a user. * * You can match based on the key, or key and value. Removing based on key and * value, will keep from removing duplicate metadata with the same key. It also @@ -1002,7 +1122,7 @@ } /** - * Retrieve user meta field for a user. + * Retrieves user meta field for a user. * * @since 3.0.0 * @@ -1024,7 +1144,7 @@ } /** - * Update user meta field based on user ID. + * Updates user meta field based on user ID. * * Use the $prev_value parameter to differentiate between meta fields with the * same key and user ID. @@ -1050,7 +1170,7 @@ } /** - * Count number of users who have each of the user roles. + * Counts number of users who have each of the user roles. * * Assumes there are neither duplicated nor orphaned capabilities meta_values. * Assumes role names are unique phrases. Same assumption made by WP_User_Query::prepare_query() @@ -1185,12 +1305,156 @@ return $result; } +/** + * Returns the number of active users in your installation. + * + * Note that on a large site the count may be cached and only updated twice daily. + * + * @since MU (3.0.0) + * @since 4.8.0 The `$network_id` parameter has been added. + * @since 6.0.0 Moved to wp-includes/user.php. + * + * @param int|null $network_id ID of the network. Defaults to the current network. + * @return int Number of active users on the network. + */ +function get_user_count( $network_id = null ) { + if ( ! is_multisite() && null !== $network_id ) { + _doing_it_wrong( + __FUNCTION__, + sprintf( + /* translators: %s: $network_id */ + __( 'Unable to pass %s if not using multisite.' ), + '$network_id' + ), + '6.0.0' + ); + } + + return (int) get_network_option( $network_id, 'user_count', -1 ); +} + +/** + * Updates the total count of users on the site if live user counting is enabled. + * + * @since 6.0.0 + * + * @param int|null $network_id ID of the network. Defaults to the current network. + * @return bool Whether the update was successful. + */ +function wp_maybe_update_user_counts( $network_id = null ) { + if ( ! is_multisite() && null !== $network_id ) { + _doing_it_wrong( + __FUNCTION__, + sprintf( + /* translators: %s: $network_id */ + __( 'Unable to pass %s if not using multisite.' ), + '$network_id' + ), + '6.0.0' + ); + } + + $is_small_network = ! wp_is_large_user_count( $network_id ); + /** This filter is documented in wp-includes/ms-functions.php */ + if ( ! apply_filters( 'enable_live_network_counts', $is_small_network, 'users' ) ) { + return false; + } + + return wp_update_user_counts( $network_id ); +} + +/** + * Updates the total count of users on the site. + * + * @global wpdb $wpdb WordPress database abstraction object. + * @since 6.0.0 + * + * @param int|null $network_id ID of the network. Defaults to the current network. + * @return bool Whether the update was successful. + */ +function wp_update_user_counts( $network_id = null ) { + global $wpdb; + + if ( ! is_multisite() && null !== $network_id ) { + _doing_it_wrong( + __FUNCTION__, + sprintf( + /* translators: %s: $network_id */ + __( 'Unable to pass %s if not using multisite.' ), + '$network_id' + ), + '6.0.0' + ); + } + + $query = "SELECT COUNT(ID) as c FROM $wpdb->users"; + if ( is_multisite() ) { + $query .= " WHERE spam = '0' AND deleted = '0'"; + } + + $count = $wpdb->get_var( $query ); + + return update_network_option( $network_id, 'user_count', $count ); +} + +/** + * Schedules a recurring recalculation of the total count of users. + * + * @since 6.0.0 + */ +function wp_schedule_update_user_counts() { + if ( ! is_main_site() ) { + return; + } + + if ( ! wp_next_scheduled( 'wp_update_user_counts' ) && ! wp_installing() ) { + wp_schedule_event( time(), 'twicedaily', 'wp_update_user_counts' ); + } +} + +/** + * Determines whether the site has a large number of users. + * + * The default criteria for a large site is more than 10,000 users. + * + * @since 6.0.0 + * + * @param int|null $network_id ID of the network. Defaults to the current network. + * @return bool Whether the site has a large number of users. + */ +function wp_is_large_user_count( $network_id = null ) { + if ( ! is_multisite() && null !== $network_id ) { + _doing_it_wrong( + __FUNCTION__, + sprintf( + /* translators: %s: $network_id */ + __( 'Unable to pass %s if not using multisite.' ), + '$network_id' + ), + '6.0.0' + ); + } + + $count = get_user_count( $network_id ); + + /** + * Filters whether the site is considered large, based on its number of users. + * + * @since 6.0.0 + * + * @param bool $is_large_user_count Whether the site has a large number of users. + * @param int $count The total number of users. + * @param int|null $network_id ID of the network. `null` represents the current network. + */ + return apply_filters( 'wp_is_large_user_count', $count > 10000, $count, $network_id ); +} + // // Private helper functions. // /** - * Set up global user vars. + * Sets up global user vars. * * Used by wp_set_current_user() for back compat. Might be deprecated in the future. * @@ -1235,7 +1499,7 @@ } /** - * Create dropdown HTML content of users. + * Creates dropdown HTML content of users. * * The content can either be displayed, which it is by default or retrieved by * setting the 'echo' argument. The 'include' and 'exclude' arguments do not @@ -1320,13 +1584,32 @@ 'role' => '', 'role__in' => array(), 'role__not_in' => array(), + 'capability' => '', + 'capability__in' => array(), + 'capability__not_in' => array(), ); $defaults['selected'] = is_author() ? get_query_var( 'author' ) : 0; $parsed_args = wp_parse_args( $args, $defaults ); - $query_args = wp_array_slice_assoc( $parsed_args, array( 'blog_id', 'include', 'exclude', 'orderby', 'order', 'who', 'role', 'role__in', 'role__not_in' ) ); + $query_args = wp_array_slice_assoc( + $parsed_args, + array( + 'blog_id', + 'include', + 'exclude', + 'orderby', + 'order', + 'who', + 'role', + 'role__in', + 'role__not_in', + 'capability', + 'capability__in', + 'capability__not_in', + ) + ); $fields = array( 'ID', 'user_login' ); @@ -1426,7 +1709,7 @@ } /** - * Sanitize user field based on context. + * Sanitizes user field based on context. * * Possible context values are: 'raw', 'edit', 'db', 'display', 'attribute' and 'js'. The * 'display' context is used by default. 'attribute' and 'js' contexts are treated like 'display' @@ -1544,7 +1827,7 @@ } /** - * Update all user caches + * Updates all user caches. * * @since 3.0.0 * @@ -1567,7 +1850,7 @@ } /** - * Clean all user caches + * Cleans all user caches. * * @since 3.0.0 * @since 4.4.0 'clean_user_cache' action was added. @@ -1700,7 +1983,7 @@ } /** - * Insert a user into the database. + * Inserts a user into the database. * * Most of the `$userdata` array fields have filters associated with the values. Exceptions are * 'ID', 'rich_editing', 'syntax_highlighting', 'comment_shortcuts', 'admin_color', 'use_ssl', @@ -1711,9 +1994,10 @@ * @since 2.0.0 * @since 3.6.0 The `aim`, `jabber`, and `yim` fields were removed as default user contact * methods for new installations. See wp_get_user_contact_methods(). - * @since 4.7.0 The user's locale can be passed to `$userdata`. + * @since 4.7.0 The `locale` field can be passed to `$userdata`. * @since 5.3.0 The `user_activation_key` field can be passed to `$userdata`. * @since 5.3.0 The `spam` field can be passed to `$userdata` (Multisite only). + * @since 5.9.0 The `meta_input` field can be passed to `$userdata` to allow addition of user meta data. * * @global wpdb $wpdb WordPress database abstraction object. * @@ -1749,7 +2033,7 @@ * @type string $admin_color Admin color scheme for the user. Default 'fresh'. * @type bool $use_ssl Whether the user should always access the admin over * https. Default false. - * @type string $user_registered Date the user registered. Format is 'Y-m-d H:i:s'. + * @type string $user_registered Date the user registered in UTC. Format is 'Y-m-d H:i:s'. * @type string $user_activation_key Password reset key. Default empty. * @type bool $spam Multisite only. Whether the user is marked as spam. * Default false. @@ -1758,6 +2042,8 @@ * as a string literal, not boolean. Default 'true'. * @type string $role User's role. * @type string $locale User's locale. Default empty. + * @type array $meta_input Array of custom user meta values keyed by meta key. + * Default empty. * } * @return int|WP_Error The newly created user's ID or a WP_Error object if the user could not * be created. @@ -1773,9 +2059,9 @@ // Are we updating or creating? if ( ! empty( $userdata['ID'] ) ) { - $ID = (int) $userdata['ID']; + $user_id = (int) $userdata['ID']; $update = true; - $old_user_data = get_userdata( $ID ); + $old_user_data = get_userdata( $user_id ); if ( ! $old_user_data ) { return new WP_Error( 'invalid_user_id', __( 'Invalid user ID.' ) ); @@ -1835,9 +2121,6 @@ */ if ( ! empty( $userdata['user_nicename'] ) ) { $user_nicename = sanitize_user( $userdata['user_nicename'], true ); - if ( mb_strlen( $user_nicename ) > 50 ) { - return new WP_Error( 'user_nicename_too_long', __( 'Nicename may not be longer than 50 characters.' ) ); - } } else { $user_nicename = mb_substr( $user_login, 0, 50 ); } @@ -1853,6 +2136,10 @@ */ $user_nicename = apply_filters( 'pre_user_nicename', $user_nicename ); + if ( mb_strlen( $user_nicename ) > 50 ) { + return new WP_Error( 'user_nicename_too_long', __( 'Nicename may not be longer than 50 characters.' ) ); + } + $user_nicename_check = $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM $wpdb->users WHERE user_nicename = %s AND user_login != %s LIMIT 1", $user_nicename, $user_login ) ); if ( $user_nicename_check ) { @@ -1901,6 +2188,10 @@ */ $user_url = apply_filters( 'pre_user_url', $raw_user_url ); + if ( mb_strlen( $user_url ) > 100 ) { + return new WP_Error( 'user_url_too_long', __( 'User URL may not be longer than 100 characters.' ) ); + } + $user_registered = empty( $userdata['user_registered'] ) ? gmdate( 'Y-m-d H:i:s' ) : $userdata['user_registered']; $user_activation_key = empty( $userdata['user_activation_key'] ) ? '' : $userdata['user_activation_key']; @@ -2016,7 +2307,7 @@ * It only includes data in the users table, not any user metadata. * * @since 4.9.0 - * @since 5.8.0 The $userdata parameter was added. + * @since 5.8.0 The `$userdata` parameter was added. * * @param array $data { * Values and keys for the user. @@ -2031,10 +2322,10 @@ * the current UTC timestamp. * } * @param bool $update Whether the user is being updated rather than created. - * @param int|null $id ID of the user to be updated, or NULL if the user is being created. + * @param int|null $user_id ID of the user to be updated, or NULL if the user is being created. * @param array $userdata The raw array of data passed to wp_insert_user(). */ - $data = apply_filters( 'wp_pre_insert_user_data', $data, $update, ( $update ? (int) $ID : null ), $userdata ); + $data = apply_filters( 'wp_pre_insert_user_data', $data, $update, ( $update ? $user_id : null ), $userdata ); if ( empty( $data ) || ! is_array( $data ) ) { return new WP_Error( 'empty_data', __( 'Not enough data to create this user.' ) ); @@ -2044,8 +2335,7 @@ if ( $user_email !== $old_user_data->user_email || $user_pass !== $old_user_data->user_pass ) { $data['user_activation_key'] = ''; } - $wpdb->update( $wpdb->users, $data, compact( 'ID' ) ); - $user_id = (int) $ID; + $wpdb->update( $wpdb->users, $data, array( 'ID' => $user_id ) ); } else { $wpdb->insert( $wpdb->users, $data ); $user_id = (int) $wpdb->insert_id; @@ -2059,8 +2349,10 @@ * * Does not include contact methods. These are added using `wp_get_user_contact_methods( $user )`. * + * For custom meta fields, see the {@see 'insert_custom_user_meta'} filter. + * * @since 4.4.0 - * @since 5.8.0 The $userdata parameter was added. + * @since 5.8.0 The `$userdata` parameter was added. * * @param array $meta { * Default meta values and keys for the user. @@ -2085,6 +2377,28 @@ */ $meta = apply_filters( 'insert_user_meta', $meta, $user, $update, $userdata ); + $custom_meta = array(); + if ( array_key_exists( 'meta_input', $userdata ) && is_array( $userdata['meta_input'] ) && ! empty( $userdata['meta_input'] ) ) { + $custom_meta = $userdata['meta_input']; + } + + /** + * Filters a user's custom meta values and keys immediately after the user is created or updated + * and before any user meta is inserted or updated. + * + * For non-custom meta fields, see the {@see 'insert_user_meta'} filter. + * + * @since 5.9.0 + * + * @param array $custom_meta Array of custom user meta values keyed by meta key. + * @param WP_User $user User object. + * @param bool $update Whether the user is being updated rather than created. + * @param array $userdata The raw array of data passed to wp_insert_user(). + */ + $custom_meta = apply_filters( 'insert_custom_user_meta', $custom_meta, $user, $update, $userdata ); + + $meta = array_merge( $meta, $custom_meta ); + // Update user meta. foreach ( $meta as $key => $value ) { update_user_meta( $user_id, $key, $value ); @@ -2109,7 +2423,7 @@ * Fires immediately after an existing user is updated. * * @since 2.0.0 - * @since 5.8.0 The $userdata parameter was added. + * @since 5.8.0 The `$userdata` parameter was added. * * @param int $user_id User ID. * @param WP_User $old_user_data Object containing user's data prior to update. @@ -2143,7 +2457,7 @@ * Fires immediately after a new user is registered. * * @since 1.5.0 - * @since 5.8.0 The $userdata parameter was added. + * @since 5.8.0 The `$userdata` parameter was added. * * @param int $user_id User ID. * @param array $userdata The raw array of data passed to wp_insert_user(). @@ -2155,7 +2469,7 @@ } /** - * Update a user in the database. + * Updates a user in the database. * * It is possible to update a user's password by specifying the 'user_pass' * value in the $userdata parameter array. @@ -2177,13 +2491,13 @@ $userdata = $userdata->to_array(); } - $ID = isset( $userdata['ID'] ) ? (int) $userdata['ID'] : 0; - if ( ! $ID ) { + $user_id = isset( $userdata['ID'] ) ? (int) $userdata['ID'] : 0; + if ( ! $user_id ) { return new WP_Error( 'invalid_user_id', __( 'Invalid user ID.' ) ); } // First, get all of the original fields. - $user_obj = get_userdata( $ID ); + $user_obj = get_userdata( $user_id ); if ( ! $user_obj ) { return new WP_Error( 'invalid_user_id', __( 'Invalid user ID.' ) ); } @@ -2192,7 +2506,7 @@ // Add additional custom fields. foreach ( _get_additional_user_keys( $user_obj ) as $key ) { - $user[ $key ] = get_user_meta( $ID, $key, true ); + $user[ $key ] = get_user_meta( $user_id, $key, true ); } // Escape data pulled from DB. @@ -2238,19 +2552,21 @@ $userdata = array_merge( $user, $userdata ); $user_id = wp_insert_user( $userdata ); - if ( ! is_wp_error( $user_id ) ) { - - $blog_name = wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES ); - - $switched_locale = false; - if ( ! empty( $send_password_change_email ) || ! empty( $send_email_change_email ) ) { - $switched_locale = switch_to_locale( get_user_locale( $user_id ) ); - } - - if ( ! empty( $send_password_change_email ) ) { - /* translators: Do not translate USERNAME, ADMIN_EMAIL, EMAIL, SITENAME, SITEURL: those are placeholders. */ - $pass_change_text = __( - 'Hi ###USERNAME###, + if ( is_wp_error( $user_id ) ) { + return $user_id; + } + + $blog_name = wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES ); + + $switched_locale = false; + if ( ! empty( $send_password_change_email ) || ! empty( $send_email_change_email ) ) { + $switched_locale = switch_to_locale( get_user_locale( $user_id ) ); + } + + if ( ! empty( $send_password_change_email ) ) { + /* translators: Do not translate USERNAME, ADMIN_EMAIL, EMAIL, SITENAME, SITEURL: those are placeholders. */ + $pass_change_text = __( + 'Hi ###USERNAME###, This notice confirms that your password was changed on ###SITENAME###. @@ -2262,53 +2578,53 @@ Regards, All at ###SITENAME### ###SITEURL###' - ); - - $pass_change_email = array( - 'to' => $user['user_email'], - /* translators: Password change notification email subject. %s: Site title. */ - 'subject' => __( '[%s] Password Changed' ), - 'message' => $pass_change_text, - 'headers' => '', - ); - - /** - * Filters the contents of the email sent when the user's password is changed. - * - * @since 4.3.0 - * - * @param array $pass_change_email { - * Used to build wp_mail(). - * - * @type string $to The intended recipients. Add emails in a comma separated string. - * @type string $subject The subject of the email. - * @type string $message The content of the email. - * The following strings have a special meaning and will get replaced dynamically: - * - ###USERNAME### The current user's username. - * - ###ADMIN_EMAIL### The admin email in case this was unexpected. - * - ###EMAIL### The user's email address. - * - ###SITENAME### The name of the site. - * - ###SITEURL### The URL to the site. - * @type string $headers Headers. Add headers in a newline (\r\n) separated string. - * } - * @param array $user The original user array. - * @param array $userdata The updated user array. - */ - $pass_change_email = apply_filters( 'password_change_email', $pass_change_email, $user, $userdata ); - - $pass_change_email['message'] = str_replace( '###USERNAME###', $user['user_login'], $pass_change_email['message'] ); - $pass_change_email['message'] = str_replace( '###ADMIN_EMAIL###', get_option( 'admin_email' ), $pass_change_email['message'] ); - $pass_change_email['message'] = str_replace( '###EMAIL###', $user['user_email'], $pass_change_email['message'] ); - $pass_change_email['message'] = str_replace( '###SITENAME###', $blog_name, $pass_change_email['message'] ); - $pass_change_email['message'] = str_replace( '###SITEURL###', home_url(), $pass_change_email['message'] ); - - wp_mail( $pass_change_email['to'], sprintf( $pass_change_email['subject'], $blog_name ), $pass_change_email['message'], $pass_change_email['headers'] ); - } - - if ( ! empty( $send_email_change_email ) ) { - /* translators: Do not translate USERNAME, ADMIN_EMAIL, NEW_EMAIL, EMAIL, SITENAME, SITEURL: those are placeholders. */ - $email_change_text = __( - 'Hi ###USERNAME###, + ); + + $pass_change_email = array( + 'to' => $user['user_email'], + /* translators: Password change notification email subject. %s: Site title. */ + 'subject' => __( '[%s] Password Changed' ), + 'message' => $pass_change_text, + 'headers' => '', + ); + + /** + * Filters the contents of the email sent when the user's password is changed. + * + * @since 4.3.0 + * + * @param array $pass_change_email { + * Used to build wp_mail(). + * + * @type string $to The intended recipients. Add emails in a comma separated string. + * @type string $subject The subject of the email. + * @type string $message The content of the email. + * The following strings have a special meaning and will get replaced dynamically: + * - ###USERNAME### The current user's username. + * - ###ADMIN_EMAIL### The admin email in case this was unexpected. + * - ###EMAIL### The user's email address. + * - ###SITENAME### The name of the site. + * - ###SITEURL### The URL to the site. + * @type string $headers Headers. Add headers in a newline (\r\n) separated string. + * } + * @param array $user The original user array. + * @param array $userdata The updated user array. + */ + $pass_change_email = apply_filters( 'password_change_email', $pass_change_email, $user, $userdata ); + + $pass_change_email['message'] = str_replace( '###USERNAME###', $user['user_login'], $pass_change_email['message'] ); + $pass_change_email['message'] = str_replace( '###ADMIN_EMAIL###', get_option( 'admin_email' ), $pass_change_email['message'] ); + $pass_change_email['message'] = str_replace( '###EMAIL###', $user['user_email'], $pass_change_email['message'] ); + $pass_change_email['message'] = str_replace( '###SITENAME###', $blog_name, $pass_change_email['message'] ); + $pass_change_email['message'] = str_replace( '###SITEURL###', home_url(), $pass_change_email['message'] ); + + wp_mail( $pass_change_email['to'], sprintf( $pass_change_email['subject'], $blog_name ), $pass_change_email['message'], $pass_change_email['headers'] ); + } + + if ( ! empty( $send_email_change_email ) ) { + /* translators: Do not translate USERNAME, ADMIN_EMAIL, NEW_EMAIL, EMAIL, SITENAME, SITEURL: those are placeholders. */ + $email_change_text = __( + 'Hi ###USERNAME###, This notice confirms that your email address on ###SITENAME### was changed to ###NEW_EMAIL###. @@ -2320,59 +2636,58 @@ Regards, All at ###SITENAME### ###SITEURL###' - ); - - $email_change_email = array( - 'to' => $user['user_email'], - /* translators: Email change notification email subject. %s: Site title. */ - 'subject' => __( '[%s] Email Changed' ), - 'message' => $email_change_text, - 'headers' => '', - ); - - /** - * Filters the contents of the email sent when the user's email is changed. - * - * @since 4.3.0 - * - * @param array $email_change_email { - * Used to build wp_mail(). - * - * @type string $to The intended recipients. - * @type string $subject The subject of the email. - * @type string $message The content of the email. - * The following strings have a special meaning and will get replaced dynamically: - * - ###USERNAME### The current user's username. - * - ###ADMIN_EMAIL### The admin email in case this was unexpected. - * - ###NEW_EMAIL### The new email address. - * - ###EMAIL### The old email address. - * - ###SITENAME### The name of the site. - * - ###SITEURL### The URL to the site. - * @type string $headers Headers. - * } - * @param array $user The original user array. - * @param array $userdata The updated user array. - */ - $email_change_email = apply_filters( 'email_change_email', $email_change_email, $user, $userdata ); - - $email_change_email['message'] = str_replace( '###USERNAME###', $user['user_login'], $email_change_email['message'] ); - $email_change_email['message'] = str_replace( '###ADMIN_EMAIL###', get_option( 'admin_email' ), $email_change_email['message'] ); - $email_change_email['message'] = str_replace( '###NEW_EMAIL###', $userdata['user_email'], $email_change_email['message'] ); - $email_change_email['message'] = str_replace( '###EMAIL###', $user['user_email'], $email_change_email['message'] ); - $email_change_email['message'] = str_replace( '###SITENAME###', $blog_name, $email_change_email['message'] ); - $email_change_email['message'] = str_replace( '###SITEURL###', home_url(), $email_change_email['message'] ); - - wp_mail( $email_change_email['to'], sprintf( $email_change_email['subject'], $blog_name ), $email_change_email['message'], $email_change_email['headers'] ); - } - - if ( $switched_locale ) { - restore_previous_locale(); - } + ); + + $email_change_email = array( + 'to' => $user['user_email'], + /* translators: Email change notification email subject. %s: Site title. */ + 'subject' => __( '[%s] Email Changed' ), + 'message' => $email_change_text, + 'headers' => '', + ); + + /** + * Filters the contents of the email sent when the user's email is changed. + * + * @since 4.3.0 + * + * @param array $email_change_email { + * Used to build wp_mail(). + * + * @type string $to The intended recipients. + * @type string $subject The subject of the email. + * @type string $message The content of the email. + * The following strings have a special meaning and will get replaced dynamically: + * - ###USERNAME### The current user's username. + * - ###ADMIN_EMAIL### The admin email in case this was unexpected. + * - ###NEW_EMAIL### The new email address. + * - ###EMAIL### The old email address. + * - ###SITENAME### The name of the site. + * - ###SITEURL### The URL to the site. + * @type string $headers Headers. + * } + * @param array $user The original user array. + * @param array $userdata The updated user array. + */ + $email_change_email = apply_filters( 'email_change_email', $email_change_email, $user, $userdata ); + + $email_change_email['message'] = str_replace( '###USERNAME###', $user['user_login'], $email_change_email['message'] ); + $email_change_email['message'] = str_replace( '###ADMIN_EMAIL###', get_option( 'admin_email' ), $email_change_email['message'] ); + $email_change_email['message'] = str_replace( '###NEW_EMAIL###', $userdata['user_email'], $email_change_email['message'] ); + $email_change_email['message'] = str_replace( '###EMAIL###', $user['user_email'], $email_change_email['message'] ); + $email_change_email['message'] = str_replace( '###SITENAME###', $blog_name, $email_change_email['message'] ); + $email_change_email['message'] = str_replace( '###SITEURL###', home_url(), $email_change_email['message'] ); + + wp_mail( $email_change_email['to'], sprintf( $email_change_email['subject'], $blog_name ), $email_change_email['message'], $email_change_email['headers'] ); + } + + if ( $switched_locale ) { + restore_previous_locale(); } // Update the cookies if the password changed. $current_user = wp_get_current_user(); - if ( $current_user->ID == $ID ) { + if ( $current_user->ID == $user_id ) { if ( isset( $plaintext_pass ) ) { wp_clear_auth_cookie(); @@ -2380,13 +2695,13 @@ // 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 ), $ID, false ); + $default_cookie_life = apply_filters( 'auth_cookie_expiration', ( 2 * DAY_IN_SECONDS ), $user_id, false ); $remember = false; if ( false !== $logged_in_cookie && ( $logged_in_cookie['expiration'] - time() ) > $default_cookie_life ) { $remember = true; } - wp_set_auth_cookie( $ID, $remember ); + wp_set_auth_cookie( $user_id, $remember ); } } @@ -2394,7 +2709,7 @@ } /** - * A simpler way of inserting a user into the database. + * Provides a simpler way of inserting a user into the database. * * Creates a new user with just the username, password, and email. For more * complex user creation use wp_insert_user() to specify more information. @@ -2436,13 +2751,13 @@ } /** - * Set up the user contact methods. + * Sets up the user contact methods. * * Default contact methods were removed in 3.6. A filter dictates contact methods. * * @since 3.7.0 * - * @param WP_User $user Optional. WP_User object. + * @param WP_User|null $user Optional. WP_User object. * @return string[] Array of contact method labels keyed by contact method. */ function wp_get_user_contact_methods( $user = null ) { @@ -2460,8 +2775,8 @@ * * @since 2.9.0 * - * @param string[] $methods Array of contact method labels keyed by contact method. - * @param WP_User $user WP_User object. + * @param string[] $methods Array of contact method labels keyed by contact method. + * @param WP_User|null $user WP_User object or null if none was provided. */ return apply_filters( 'user_contactmethods', $methods, $user ); } @@ -2474,7 +2789,7 @@ * @since 2.9.0 * @access private * - * @param WP_User $user Optional. WP_User object. Default null. + * @param WP_User|null $user Optional. WP_User object. Default null. * @return string[] Array of contact method labels keyed by contact method. */ function _wp_get_user_contactmethods( $user = null ) { @@ -2549,8 +2864,8 @@ * * @since 2.7.0 * - * @param bool $allow Whether to allow the password to be reset. Default true. - * @param int $ID The ID of the user attempting to reset a password. + * @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 ); @@ -2596,7 +2911,7 @@ } /** - * Retrieves a user row based on password reset key and login + * Retrieves a user row based on password reset key and login. * * A key is considered 'expired' if it exactly matches the value of the * user_activation_key field, rather than being matched after going through the @@ -2772,6 +3087,21 @@ return $errors; } + /** + * Filters whether to send the retrieve password email. + * + * Return false to disable sending the email. + * + * @since 6.0.0 + * + * @param bool $send Whether to send the email. + * @param string $user_login The username for the user. + * @param WP_User $user_data WP_User object. + */ + if ( ! apply_filters( 'send_retrieve_password_email', true, $user_login, $user_data ) ) { + return true; + } + // Redefining user_login ensures we return the right case in the email. $user_login = $user_data->user_login; $user_email = $user_data->user_email; @@ -2846,11 +3176,57 @@ */ $message = apply_filters( 'retrieve_password_message', $message, $key, $user_login, $user_data ); + // Short-circuit on falsey $message value for backwards compatibility. + if ( ! $message ) { + return true; + } + + /* + * Wrap the single notification email arguments in an array + * to pass them to the retrieve_password_notification_email filter. + */ + $defaults = array( + 'to' => $user_email, + 'subject' => $title, + 'message' => $message, + 'headers' => '', + ); + + /** + * Filters the contents of the reset password notification email sent to the user. + * + * @since 6.0.0 + * + * @param array $defaults { + * The default notification email arguments. Used to build wp_mail(). + * + * @type string $to The intended recipient - user email address. + * @type string $subject The subject of the email. + * @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. + */ + $notification_email = apply_filters( 'retrieve_password_notification_email', $defaults, $key, $user_login, $user_data ); + if ( $switched_locale ) { restore_previous_locale(); } - if ( $message && ! wp_mail( $user_email, wp_specialchars_decode( $title ), $message ) ) { + if ( is_array( $notification_email ) ) { + // Force key order and merge defaults in case any value is missing in the filtered array. + $notification_email = array_merge( $defaults, $notification_email ); + } else { + $notification_email = $defaults; + } + + list( $to, $subject, $message, $headers ) = array_values( $notification_email ); + + $subject = wp_specialchars_decode( $subject ); + + if ( ! wp_mail( $to, $subject, $message, $headers ) ) { $errors->add( 'retrieve_password_email_failure', sprintf( @@ -2941,10 +3317,17 @@ if ( '' === $user_email ) { $errors->add( 'empty_email', __( 'Error: Please type your email address.' ) ); } elseif ( ! is_email( $user_email ) ) { - $errors->add( 'invalid_email', __( 'Error: The email address isn’t correct.' ) ); + $errors->add( 'invalid_email', __( 'Error: The email address is not correct.' ) ); $user_email = ''; } elseif ( email_exists( $user_email ) ) { - $errors->add( 'email_exists', __( 'Error: This email is already registered. Please choose another one.' ) ); + $errors->add( + 'email_exists', + sprintf( + /* translators: %s: Link to the login page. */ + __( 'Error: This email address is already registered. Log in with this address or choose another one.' ), + wp_login_url() + ) + ); } /** @@ -2989,7 +3372,7 @@ 'registerfail', sprintf( /* translators: %s: Admin email address. */ - __( 'Error: Couldn’t register you… please contact the site admin!' ), + __( 'Error: Could not register you… please contact the site admin!' ), get_option( 'admin_email' ) ) ); @@ -2998,6 +3381,13 @@ update_user_meta( $user_id, 'default_password_nag', true ); // Set up the password change nag. + if ( ! empty( $_COOKIE['wp_lang'] ) ) { + $wp_lang = sanitize_text_field( $_COOKIE['wp_lang'] ); + if ( in_array( $wp_lang, get_available_languages(), true ) ) { + update_user_meta( $user_id, 'locale', $wp_lang ); // Set user locale if defined on registration. + } + } + /** * Fires after a new user registration has been recorded. * @@ -3029,7 +3419,7 @@ } /** - * Retrieve the current session token from the logged_in cookie. + * Retrieves the current session token from the logged_in cookie. * * @since 4.0.0 * @@ -3041,7 +3431,7 @@ } /** - * Retrieve a list of sessions for the current user. + * Retrieves a list of sessions for the current user. * * @since 4.0.0 * @@ -3053,7 +3443,7 @@ } /** - * Remove the current session token from the database. + * Removes the current session token from the database. * * @since 4.0.0 */ @@ -3066,7 +3456,7 @@ } /** - * Remove all but the current session token for the current user for the database. + * Removes all but the current session token for the current user for the database. * * @since 4.0.0 */ @@ -3079,7 +3469,7 @@ } /** - * Remove all session tokens for the current user from the database. + * Removes all session tokens for the current user from the database. * * @since 4.0.0 */ @@ -3089,7 +3479,7 @@ } /** - * Get the user IDs of all users with no role on this site. + * Gets the user IDs of all users with no role on this site. * * @since 4.4.0 * @since 4.9.0 The `$site_id` parameter was added to support multisite. @@ -3202,7 +3592,7 @@ } /** - * Send a confirmation request email when a change of user email address is attempted. + * Sends a confirmation request email when a change of user email address is attempted. * * @since 3.0.0 * @since 4.9.0 This function was moved from wp-admin/includes/ms.php so it's no longer Multisite specific. @@ -3225,7 +3615,7 @@ if ( ! is_email( $_POST['email'] ) ) { $errors->add( 'user_email', - __( 'Error: The email address isn’t correct.' ), + __( 'Error: The email address is not correct.' ), array( 'form-field' => 'email', ) @@ -3318,7 +3708,7 @@ * @since 3.0.0 * @since 4.9.0 This function was moved from wp-admin/includes/ms.php so it's no longer Multisite specific. * - * @global string $pagenow + * @global string $pagenow The filename of the current screen. */ function new_user_email_admin_notice() { global $pagenow; @@ -3333,7 +3723,7 @@ } /** - * Get all personal data request types. + * Gets all personal data request types. * * @since 4.9.6 * @access private @@ -3439,7 +3829,7 @@ $reserved_names = array_values( $user_props_to_export ); /** - * Filter to extend the user's profile data for the privacy exporter. + * Filters the user's profile data for the privacy exporter. * * @since 5.4.0 * @@ -3459,7 +3849,7 @@ // Remove items that use reserved names. $extra_data = array_filter( $_extra_data, - function( $item ) use ( $reserved_names ) { + static function( $item ) use ( $reserved_names ) { return ! in_array( $item['name'], $reserved_names, true ); } ); @@ -3563,7 +3953,7 @@ } /** - * Update log when privacy request is confirmed. + * Updates log when privacy request is confirmed. * * @since 4.9.6 * @access private @@ -3591,7 +3981,7 @@ } /** - * Notify the site administrator via email when a request is confirmed. + * Notifies the site administrator via email when a request is confirmed. * * Without this, the admin would have to manually check the site to see if any * action was needed on their part yet. @@ -3804,7 +4194,7 @@ } /** - * Notify the user when their erasure request is fulfilled. + * Notifies the user when their erasure request is fulfilled. * * Without this, the user would never know if their data was actually erased. * @@ -4077,7 +4467,7 @@ } /** - * Return request confirmation message HTML. + * Returns request confirmation message HTML. * * @since 4.9.6 * @access private @@ -4115,7 +4505,7 @@ } /** - * Create and log a user request to perform a specific action. + * Creates and logs a user request to perform a specific action. * * Requests are stored inside a post type named `user_request` since they can apply to both * users on the site, or guests without a user account. @@ -4186,7 +4576,7 @@ } /** - * Get action description from the name and return a string. + * Gets action description from the name and return a string. * * @since 4.9.6 * @@ -4404,7 +4794,7 @@ } /** - * Validate a user request by comparing the key with the request's key. + * Validates a user request by comparing the key with the request's key. * * @since 4.9.6 * @@ -4459,7 +4849,7 @@ } /** - * Return the user request object for the specified request ID. + * Returns the user request object for the specified request ID. * * @since 4.9.6 * @@ -4478,18 +4868,30 @@ } /** + * Checks if Application Passwords is supported. + * + * Application Passwords is supported only by sites using SSL or local environments + * but may be made available using the {@see 'wp_is_application_passwords_available'} filter. + * + * @since 5.9.0 + * + * @return bool + */ +function wp_is_application_passwords_supported() { + return is_ssl() || 'local' === wp_get_environment_type(); +} + +/** * Checks if Application Passwords is globally available. * * By default, Application Passwords is available to all sites using SSL or to local environments. - * Use {@see 'wp_is_application_passwords_available'} to adjust its availability. + * Use the {@see 'wp_is_application_passwords_available'} filter to adjust its availability. * * @since 5.6.0 * * @return bool */ function wp_is_application_passwords_available() { - $available = is_ssl() || 'local' === wp_get_environment_type(); - /** * Filters whether Application Passwords is available. * @@ -4497,7 +4899,7 @@ * * @param bool $available True if available, false otherwise. */ - return apply_filters( 'wp_is_application_passwords_available', $available ); + return apply_filters( 'wp_is_application_passwords_available', wp_is_application_passwords_supported() ); } /**