diff -r 34716fd837a4 -r be944660c56a wp/wp-includes/user.php --- a/wp/wp-includes/user.php Tue Dec 15 15:52:01 2020 +0100 +++ b/wp/wp-includes/user.php Wed Sep 21 18:19:35 2022 +0200 @@ -149,7 +149,11 @@ if ( ! $user ) { return new WP_Error( 'invalid_username', - __( 'Unknown username. Check again or try your email address.' ) + 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.' ), + $username + ) ); } @@ -298,6 +302,186 @@ } /** + * Authenticates the user using an application password. + * + * @since 5.6.0 + * + * @param WP_User|WP_Error|null $input_user WP_User or WP_Error object if a previous + * callback failed authentication. + * @param string $username Username for authentication. + * @param string $password Password for authentication. + * @return WP_User|WP_Error|null WP_User on success, WP_Error on failure, null if + * null is passed in and this isn't an API request. + */ +function wp_authenticate_application_password( $input_user, $username, $password ) { + if ( $input_user instanceof WP_User ) { + return $input_user; + } + + if ( ! WP_Application_Passwords::is_in_use() ) { + return $input_user; + } + + $is_api_request = ( ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST ) || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ); + + /** + * Filters whether this is an API request that Application Passwords can be used on. + * + * By default, Application Passwords is available for the REST API and XML-RPC. + * + * @since 5.6.0 + * + * @param bool $is_api_request If this is an acceptable API request. + */ + $is_api_request = apply_filters( 'application_password_is_api_request', $is_api_request ); + + if ( ! $is_api_request ) { + return $input_user; + } + + $error = null; + $user = get_user_by( 'login', $username ); + + if ( ! $user && is_email( $username ) ) { + $user = get_user_by( 'email', $username ); + } + + // If the login name is invalid, short circuit. + if ( ! $user ) { + if ( is_email( $username ) ) { + $error = new WP_Error( + 'invalid_email', + __( '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.' ) + ); + } + } elseif ( ! wp_is_application_passwords_available() ) { + $error = new WP_Error( + 'application_passwords_disabled', + __( 'Application passwords are not available.' ) + ); + } elseif ( ! wp_is_application_passwords_available_for_user( $user ) ) { + $error = new WP_Error( + 'application_passwords_disabled_for_user', + __( 'Application passwords are not available for your account. Please contact the site administrator for assistance.' ) + ); + } + + if ( $error ) { + /** + * Fires when an application password failed to authenticate the user. + * + * @since 5.6.0 + * + * @param WP_Error $error The authentication error. + */ + do_action( 'application_password_failed_authentication', $error ); + + return $error; + } + + /* + * Strip 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. + */ + $password = preg_replace( '/[^a-z\d]/i', '', $password ); + + $hashed_passwords = WP_Application_Passwords::get_user_application_passwords( $user->ID ); + + foreach ( $hashed_passwords as $key => $item ) { + if ( ! wp_check_password( $password, $item['password'], $user->ID ) ) { + continue; + } + + $error = new WP_Error(); + + /** + * Fires when an application password has been successfully checked as valid. + * + * This allows for plugins to add additional constraints to prevent an application password from being used. + * + * @since 5.6.0 + * + * @param WP_Error $error The error object. + * @param WP_User $user The user authenticating. + * @param array $item The details about the application password. + * @param string $password The raw supplied password. + */ + do_action( 'wp_authenticate_application_password_errors', $error, $user, $item, $password ); + + if ( is_wp_error( $error ) && $error->has_errors() ) { + /** This action is documented in wp-includes/user.php */ + do_action( 'application_password_failed_authentication', $error ); + + return $error; + } + + WP_Application_Passwords::record_application_password_usage( $user->ID, $item['uuid'] ); + + /** + * Fires after an application password was used for authentication. + * + * @since 5.6.0 + * + * @param WP_User $user The user who was authenticated. + * @param array $item The application password used. + */ + do_action( 'application_password_did_authenticate', $user, $item ); + + return $user; + } + + $error = new WP_Error( + 'incorrect_password', + __( 'The provided password is an invalid application password.' ) + ); + + /** This action is documented in wp-includes/user.php */ + do_action( 'application_password_failed_authentication', $error ); + + return $error; +} + +/** + * Validates the application password credentials passed via Basic Authentication. + * + * @since 5.6.0 + * + * @param int|false $input_user User ID if one has been determined, false otherwise. + * @return int|false The authenticated user ID if successful, false otherwise. + */ +function wp_validate_application_password( $input_user ) { + // Don't authenticate twice. + if ( ! empty( $input_user ) ) { + return $input_user; + } + + if ( ! wp_is_application_passwords_available() ) { + return $input_user; + } + + // Both $_SERVER['PHP_AUTH_USER'] and $_SERVER['PHP_AUTH_PW'] must be set in order to attempt authentication. + if ( ! isset( $_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW'] ) ) { + return $input_user; + } + + $authenticated = wp_authenticate_application_password( null, $_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW'] ); + + if ( $authenticated instanceof WP_User ) { + return $authenticated->ID; + } + + // If it wasn't a user what got returned, just pass on what we had received originally. + return $input_user; +} + +/** * For Multisite blogs, check if the authenticated user has been marked as a * spammer, or if the user's primary blog has been marked as spam. * @@ -335,8 +519,8 @@ * * @since 3.9.0 * - * @param int|bool $user_id The user ID (or false) as received from - * the `determine_current_user` filter. + * @param int|false $user_id The user ID (or false) as received from + * the `determine_current_user` filter. * @return int|false User ID if validated, false otherwise. If a user ID from * an earlier filter callback is received, that value is returned. */ @@ -567,7 +751,7 @@ * * @see WP_User_Query * - * @param array $args Optional. Arguments to retrieve users. See WP_User_Query::prepare_query(). + * @param array $args Optional. Arguments to retrieve users. See WP_User_Query::prepare_query() * for more information on accepted arguments. * @return array List of users. */ @@ -592,8 +776,8 @@ * @param int $user_id User ID * @param bool $all Whether to retrieve all sites, or only sites that are not * marked as deleted, archived, or spam. - * @return array A list of the user's sites. An empty array if the user doesn't exist - * or belongs to no sites. + * @return object[] A list of the user's sites. An empty array if the user doesn't exist + * or belongs to no sites. */ function get_blogs_of_user( $user_id, $all = false ) { global $wpdb; @@ -613,10 +797,10 @@ * * @since 4.6.0 * - * @param null|array $sites An array of site objects of which the user is a member. - * @param int $user_id User ID. - * @param bool $all Whether the returned array should contain all sites, including - * those marked 'deleted', 'archived', or 'spam'. Default false. + * @param null|object[] $sites An array of site objects of which the user is a member. + * @param int $user_id User ID. + * @param bool $all Whether the returned array should contain all sites, including + * those marked 'deleted', 'archived', or 'spam'. Default false. */ $sites = apply_filters( 'pre_get_blogs_of_user', null, $user_id, $all ); @@ -705,10 +889,10 @@ * * @since MU (3.0.0) * - * @param array $sites An array of site objects belonging to the user. - * @param int $user_id User ID. - * @param bool $all Whether the returned sites array should contain all sites, including - * those marked 'deleted', 'archived', or 'spam'. Default false. + * @param object[] $sites An array of site objects belonging to the user. + * @param int $user_id User ID. + * @param bool $all Whether the returned sites array should contain all sites, including + * those marked 'deleted', 'archived', or 'spam'. Default false. */ return apply_filters( 'get_blogs_of_user', $sites, $user_id, $all ); } @@ -828,10 +1012,12 @@ * @param string $key Optional. The meta key to retrieve. By default, * returns data for all keys. * @param bool $single Optional. Whether to return a single value. - * This parameter has no effect if $key is not specified. + * This parameter has no effect if `$key` is not specified. * Default false. - * @return mixed An array if $single is false. The value of meta data field - * if $single is true. False for an invalid $user_id. + * @return mixed An array of values if `$single` is false. + * The value of meta data field if `$single` is true. + * False for an invalid `$user_id` (non-numeric, zero, or negative value). + * An empty string if a valid but non-existing user ID is passed. */ function get_user_meta( $user_id, $key = '', $single = false ) { return get_metadata( 'user', $user_id, $key, $single ); @@ -896,8 +1082,9 @@ } /** - * Filter the user count before queries are run. Return a non-null value to cause count_users() - * to return early. + * Filters the user count before queries are run. + * + * Return a non-null value to cause count_users() to return early. * * @since 5.1.0 * @@ -1078,9 +1265,9 @@ * @type string $order Whether to order users in ascending or descending * order. Accepts 'ASC' (ascending) or 'DESC' (descending). * Default 'ASC'. - * @type array|string $include Array or comma-separated list of user IDs to include. + * @type int[]|string $include Array or comma-separated list of user IDs to include. * Default empty. - * @type array|string $exclude Array or comma-separated list of user IDs to exclude. + * @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. @@ -1103,9 +1290,9 @@ * @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 array $role__in An array of role names. Matched users must have at least one of + * @type string[] $role__in An array of role names. Matched users must have at least one of * these roles. Default empty array. - * @type array $role__not_in An array of role names to exclude. Users matching one or more of + * @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. * } * @return string HTML dropdown list of users. @@ -1190,6 +1377,7 @@ if ( $parsed_args['include_selected'] && ( $parsed_args['selected'] > 0 ) ) { $found_selected = false; $parsed_args['selected'] = (int) $parsed_args['selected']; + foreach ( (array) $users as $user ) { $user->ID = (int) $user->ID; if ( $user->ID === $parsed_args['selected'] ) { @@ -1198,7 +1386,10 @@ } if ( ! $found_selected ) { - $users[] = get_userdata( $parsed_args['selected'] ); + $selected_user = get_userdata( $parsed_args['selected'] ); + if ( $selected_user ) { + $users[] = $selected_user; + } } } @@ -1343,6 +1534,12 @@ } elseif ( 'js' === $context ) { $value = esc_js( $value ); } + + // Restore the type for integer fields after esc_attr(). + if ( in_array( $field, $int_fields, true ) ) { + $value = (int) $value; + } + return $value; } @@ -1351,8 +1548,8 @@ * * @since 3.0.0 * - * @param WP_User $user User object to be cached - * @return bool|null Returns false on failure. + * @param object|WP_User $user User object or database row to be cached + * @return void|false Void on success, false on failure. */ function update_user_caches( $user ) { if ( $user instanceof WP_User ) { @@ -1374,10 +1571,15 @@ * * @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. * * @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 ); } @@ -1400,6 +1602,13 @@ * @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, '' ); + } } /** @@ -1411,8 +1620,8 @@ * * @since 2.0.0 * - * @param string $username Username. - * @return int|false The user's ID on success, and false on failure. + * @param string $username The username to check for existence. + * @return int|false The user ID on success, false on failure. */ function username_exists( $username ) { $user = get_user_by( 'login', $username ); @@ -1423,12 +1632,13 @@ } /** - * Filters whether the given username exists or not. + * Filters whether the given username exists. * * @since 4.9.0 * - * @param int|false $user_id The user's ID on success, and false on failure. - * @param string $username Username to check. + * @param int|false $user_id The user ID associated with the username, + * or false if the username does not exist. + * @param string $username The username to check for existence. */ return apply_filters( 'username_exists', $user_id, $username ); } @@ -1442,32 +1652,44 @@ * * @since 2.1.0 * - * @param string $email Email. - * @return int|false The user's ID on success, and false on failure. + * @param string $email The email to check for existence. + * @return int|false The user ID on success, false on failure. */ function email_exists( $email ) { $user = get_user_by( 'email', $email ); if ( $user ) { - return $user->ID; + $user_id = $user->ID; + } else { + $user_id = false; } - return false; + + /** + * Filters whether the given email exists. + * + * @since 5.6.0 + * + * @param int|false $user_id The user ID associated with the email, + * or false if the email does not exist. + * @param string $email The email to check for existence. + */ + return apply_filters( 'email_exists', $user_id, $email ); } /** * Checks whether a username is valid. * * @since 2.0.1 - * @since 4.4.0 Empty sanitized usernames are now considered invalid + * @since 4.4.0 Empty sanitized usernames are now considered invalid. * * @param string $username Username. - * @return bool Whether username given is valid + * @return bool Whether username given is valid. */ function validate_username( $username ) { $sanitized = sanitize_user( $username, true ); $valid = ( $sanitized == $username && ! empty( $sanitized ) ); /** - * Filters whether the provided username is valid or not. + * Filters whether the provided username is valid. * * @since 2.0.1 * @@ -1658,7 +1880,7 @@ /* * If there is no update, just check for `email_exists`. If there is an update, - * check if current email and new email are the same, or not, and check `email_exists` + * check if current email and new email are the same, and check `email_exists` * accordingly. */ if ( ( ! $update || ( ! empty( $old_user_data ) && 0 !== strcasecmp( $user_email, $old_user_data->user_email ) ) ) @@ -1791,9 +2013,10 @@ /** * Filters user data before the record is created or updated. * - * It only includes data in the wp_users table wp_user, not any user metadata. + * 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. * * @param array $data { * Values and keys for the user. @@ -1807,10 +2030,11 @@ * @type string $user_registered MySQL timestamp describing the moment when the user registered. Defaults to * 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 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 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 ); + $data = apply_filters( 'wp_pre_insert_user_data', $data, $update, ( $update ? (int) $ID : null ), $userdata ); if ( empty( $data ) || ! is_array( $data ) ) { return new WP_Error( 'empty_data', __( 'Not enough data to create this user.' ) ); @@ -1836,6 +2060,7 @@ * Does not include contact methods. These are added using `wp_get_user_contact_methods( $user )`. * * @since 4.4.0 + * @since 5.8.0 The $userdata parameter was added. * * @param array $meta { * Default meta values and keys for the user. @@ -1854,10 +2079,11 @@ * Default 'true'. * @type string $locale User's locale. Default empty. * } - * @param WP_User $user User object. - * @param bool $update Whether the user is being updated rather than created. + * @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(). */ - $meta = apply_filters( 'insert_user_meta', $meta, $user, $update ); + $meta = apply_filters( 'insert_user_meta', $meta, $user, $update, $userdata ); // Update user meta. foreach ( $meta as $key => $value ) { @@ -1883,11 +2109,13 @@ * Fires immediately after an existing user is updated. * * @since 2.0.0 + * @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. + * @param array $userdata The raw array of data passed to wp_insert_user(). */ - do_action( 'profile_update', $user_id, $old_user_data ); + do_action( 'profile_update', $user_id, $old_user_data, $userdata ); if ( isset( $userdata['spam'] ) && $userdata['spam'] != $old_user_data->spam ) { if ( 1 == $userdata['spam'] ) { @@ -1915,10 +2143,12 @@ * Fires immediately after a new user is registered. * * @since 1.5.0 + * @since 5.8.0 The $userdata parameter was added. * - * @param int $user_id User ID. + * @param int $user_id User ID. + * @param array $userdata The raw array of data passed to wp_insert_user(). */ - do_action( 'user_register', $user_id ); + do_action( 'user_register', $user_id, $userdata ); } return $user_id; @@ -2048,19 +2278,19 @@ * @since 4.3.0 * * @param array $pass_change_email { - * Used to build wp_mail(). + * 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. - * } + * @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. */ @@ -2106,20 +2336,20 @@ * @since 4.3.0 * * @param array $email_change_email { - * Used to build wp_mail(). + * 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. - * } + * @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. */ @@ -2458,6 +2688,184 @@ } /** + * Handles sending a password retrieval email to a user. + * + * @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. + * + * @param string $user_login Optional. Username to send a password retrieval email for. + * Defaults to `$_POST['user_login']` if not set. + * @return true|WP_Error True when finished, WP_Error object on error. + */ +function retrieve_password( $user_login = null ) { + $errors = new WP_Error(); + $user_data = false; + + // Use the passed $user_login if available, otherwise use $_POST['user_login']. + if ( ! $user_login && ! empty( $_POST['user_login'] ) ) { + $user_login = $_POST['user_login']; + } + + if ( empty( $user_login ) ) { + $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 ) ) ); + 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 ) ) ); + } + + /** + * Filters the user data during a password reset request. + * + * Allows, for example, custom validation using data other than username or email address. + * + * @since 5.7.0 + * + * @param WP_User|false $user_data WP_User object if found, false if the user does not exist. + * @param WP_Error $errors A WP_Error object containing any errors generated + * by using invalid credentials. + */ + $user_data = apply_filters( 'lostpassword_user_data', $user_data, $errors ); + + /** + * Fires before errors are returned from a password reset request. + * + * @since 2.1.0 + * @since 4.4.0 Added the `$errors` parameter. + * @since 5.4.0 Added the `$user_data` parameter. + * + * @param WP_Error $errors A WP_Error object containing any errors generated + * by using invalid credentials. + * @param WP_User|false $user_data WP_User object if found, false if the user does not exist. + */ + do_action( 'lostpassword_post', $errors, $user_data ); + + /** + * Filters the errors encountered on a password reset request. + * + * The filtered WP_Error object may, for example, contain errors for an invalid + * username or email address. A WP_Error object should always be returned, + * but may or may not contain errors. + * + * If any errors are present in $errors, this will abort the password reset request. + * + * @since 5.5.0 + * + * @param WP_Error $errors A WP_Error object containing any errors generated + * by using invalid credentials. + * @param WP_User|false $user_data WP_User object if found, false if the user does not exist. + */ + $errors = apply_filters( 'lostpassword_errors', $errors, $user_data ); + + if ( $errors->has_errors() ) { + return $errors; + } + + if ( ! $user_data ) { + $errors->add( 'invalidcombo', __( 'Error: There is no account with that username or email address.' ) ); + return $errors; + } + + // Redefining user_login ensures we return the right case in the email. + $user_login = $user_data->user_login; + $user_email = $user_data->user_email; + $key = get_password_reset_key( $user_data ); + + if ( is_wp_error( $key ) ) { + return $key; + } + + // Localize password reset message content for user. + $locale = get_user_locale( $user_data ); + + $switched_locale = switch_to_locale( $locale ); + + if ( is_multisite() ) { + $site_name = get_network()->site_name; + } else { + /* + * The blogname option is escaped with esc_html on the way into the database + * in sanitize_option. We want to reverse this for the plain text arena of emails. + */ + $site_name = wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES ); + } + + $message = __( 'Someone has requested a password reset for the following account:' ) . "\r\n\r\n"; + /* translators: %s: Site name. */ + $message .= sprintf( __( 'Site Name: %s' ), $site_name ) . "\r\n\r\n"; + /* translators: %s: User login. */ + $message .= sprintf( __( 'Username: %s' ), $user_login ) . "\r\n\r\n"; + $message .= __( 'If this was a mistake, ignore this email and nothing will happen.' ) . "\r\n\r\n"; + $message .= __( 'To reset your password, visit the following address:' ) . "\r\n\r\n"; + $message .= network_site_url( "wp-login.php?action=rp&key=$key&login=" . rawurlencode( $user_login ), 'login' ) . '&wp_lang=' . $locale . "\r\n\r\n"; + + if ( ! is_user_logged_in() ) { + $requester_ip = $_SERVER['REMOTE_ADDR']; + if ( $requester_ip ) { + $message .= sprintf( + /* translators: %s: IP address of password reset requester. */ + __( 'This password reset request originated from the IP address %s.' ), + $requester_ip + ) . "\r\n"; + } + } + + /* translators: Password reset notification email subject. %s: Site title. */ + $title = sprintf( __( '[%s] Password Reset' ), $site_name ); + + /** + * Filters the subject of the password reset email. + * + * @since 2.8.0 + * @since 4.4.0 Added the `$user_login` and `$user_data` parameters. + * + * @param string $title Email subject. + * @param string $user_login The username for the user. + * @param WP_User $user_data WP_User object. + */ + $title = apply_filters( 'retrieve_password_title', $title, $user_login, $user_data ); + + /** + * Filters the message body of the password reset mail. + * + * If the filtered message is empty, the password reset email will not be sent. + * + * @since 2.8.0 + * @since 4.1.0 Added `$user_login` and `$user_data` parameters. + * + * @param string $message Email message. + * @param string $key The activation key. + * @param string $user_login The username for the user. + * @param WP_User $user_data WP_User object. + */ + $message = apply_filters( 'retrieve_password_message', $message, $key, $user_login, $user_data ); + + if ( $switched_locale ) { + restore_previous_locale(); + } + + if ( $message && ! wp_mail( $user_email, wp_specialchars_decode( $title ), $message ) ) { + $errors->add( + '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/' ) ) + ) + ); + return $errors; + } + + return true; +} + +/** * Handles resetting the user's password. * * @since 2.5.0 @@ -2477,7 +2885,7 @@ do_action( 'password_reset', $user, $new_pass ); wp_set_password( $new_pass, $user->ID ); - update_user_option( $user->ID, 'default_password_nag', false, true ); + update_user_meta( $user->ID, 'default_password_nag', false ); /** * Fires after the user's password is reset. @@ -2588,7 +2996,7 @@ return $errors; } - update_user_option( $user_id, 'default_password_nag', true, true ); // Set up the password change nag. + update_user_meta( $user_id, 'default_password_nag', true ); // Set up the password change nag. /** * Fires after a new user registration has been recorded. @@ -2780,7 +3188,7 @@ * * @since 3.9.0 * - * @param int|bool $user_id User ID if one has been determined, false otherwise. + * @param int|false $user_id User ID if one has been determined, false otherwise. */ $user_id = apply_filters( 'determine_current_user', false ); if ( ! $user_id ) { @@ -2925,7 +3333,7 @@ } /** - * Get all user privacy request types. + * Get all personal data request types. * * @since 4.9.6 * @access private @@ -3238,59 +3646,6 @@ 'admin_email' => $admin_email, ); - /* translators: Do not translate SITENAME, USER_EMAIL, DESCRIPTION, MANAGE_URL, SITEURL; those are placeholders. */ - $email_text = __( - 'Howdy, - -A user data privacy request has been confirmed on ###SITENAME###: - -User: ###USER_EMAIL### -Request: ###DESCRIPTION### - -You can view and manage these data privacy requests here: - -###MANAGE_URL### - -Regards, -All at ###SITENAME### -###SITEURL###' - ); - - /** - * Filters the body of the user request confirmation email. - * - * The email is sent to an administrator when an user request is confirmed. - * The following strings have a special meaning and will get replaced dynamically: - * - * ###SITENAME### The name of the site. - * ###USER_EMAIL### The user email for the request. - * ###DESCRIPTION### Description of the action being performed so the user knows what the email is for. - * ###MANAGE_URL### The URL to manage requests. - * ###SITEURL### The URL to the site. - * - * @since 4.9.6 - * - * @param string $email_text Text in the email. - * @param array $email_data { - * Data relating to the account action email. - * - * @type WP_User_Request $request User request object. - * @type string $user_email The email address confirming a request - * @type string $description Description of the action being performed so the user knows what the email is for. - * @type string $manage_url The link to click manage privacy requests of this type. - * @type string $sitename The site name sending the mail. - * @type string $siteurl The site URL sending the mail. - * @type string $admin_email The administrator email receiving the mail. - * } - */ - $content = apply_filters( 'user_confirmed_action_email_content', $email_text, $email_data ); - - $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 ); - $subject = sprintf( /* translators: Privacy data request confirmed notification email subject. 1: Site title, 2: Name of the confirmed action. */ __( '[%1$s] Action Confirmed: %2$s' ), @@ -3319,6 +3674,103 @@ */ $subject = apply_filters( 'user_request_confirmed_email_subject', $subject, $email_data['sitename'], $email_data ); + /* translators: Do not translate SITENAME, USER_EMAIL, DESCRIPTION, MANAGE_URL, SITEURL; those are placeholders. */ + $content = __( + 'Howdy, + +A user data privacy request has been confirmed on ###SITENAME###: + +User: ###USER_EMAIL### +Request: ###DESCRIPTION### + +You can view and manage these data privacy requests here: + +###MANAGE_URL### + +Regards, +All at ###SITENAME### +###SITEURL###' + ); + + /** + * Filters the body of the user request confirmation email. + * + * The email is sent to an administrator when a user request is confirmed. + * + * The following strings have a special meaning and will get replaced dynamically: + * + * ###SITENAME### The name of the site. + * ###USER_EMAIL### The user email for the request. + * ###DESCRIPTION### Description of the action being performed so the user knows what the email is for. + * ###MANAGE_URL### The URL to manage requests. + * ###SITEURL### The URL to the site. + * + * @since 4.9.6 + * @deprecated 5.8.0 Use {@see 'user_request_confirmed_email_content'} instead. + * For user erasure fulfillment email content + * use {@see 'user_erasure_fulfillment_email_content'} instead. + * + * @param string $content The email content. + * @param array $email_data { + * Data relating to the account action email. + * + * @type WP_User_Request $request User request object. + * @type string $user_email The email address confirming a request + * @type string $description Description of the action being performed + * so the user knows what the email is for. + * @type string $manage_url The link to click manage privacy requests of this type. + * @type string $sitename The site name sending the mail. + * @type string $siteurl The site URL sending the mail. + * @type string $admin_email The administrator email receiving the mail. + * } + */ + $content = apply_filters_deprecated( + 'user_confirmed_action_email_content', + array( $content, $email_data ), + '5.8.0', + sprintf( + /* translators: 1 & 2: Deprecation replacement options. */ + __( '%1$s or %2$s' ), + 'user_request_confirmed_email_content', + 'user_erasure_fulfillment_email_content' + ) + ); + + /** + * Filters the body of the user request confirmation email. + * + * The email is sent to an administrator when a user request is confirmed. + * The following strings have a special meaning and will get replaced dynamically: + * + * ###SITENAME### The name of the site. + * ###USER_EMAIL### The user email for the request. + * ###DESCRIPTION### Description of the action being performed so the user knows what the email is for. + * ###MANAGE_URL### The URL to manage requests. + * ###SITEURL### The URL to the site. + * + * @since 5.8.0 + * + * @param string $content The email content. + * @param array $email_data { + * Data relating to the account action email. + * + * @type WP_User_Request $request User request object. + * @type string $user_email The email address confirming a request + * @type string $description Description of the action being performed so the user knows what the email is for. + * @type string $manage_url The link to click manage privacy requests of this type. + * @type string $sitename The site name sending the mail. + * @type string $siteurl The site URL sending the mail. + * @type string $admin_email The administrator email receiving the mail. + * } + */ + $content = apply_filters( 'user_request_confirmed_email_content', $content, $email_data ); + + $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 ); + $headers = ''; /** @@ -3410,6 +3862,7 @@ * Filters the subject of the email sent when an erasure request is completed. * * @since 4.9.8 + * @deprecated 5.8.0 Use {@see 'user_erasure_fulfillment_email_subject'} instead. * * @param string $subject The email subject. * @param string $sitename The name of the site. @@ -3425,12 +3878,37 @@ * @type string $siteurl The site URL sending the mail. * } */ - $subject = apply_filters( 'user_erasure_complete_email_subject', $subject, $email_data['sitename'], $email_data ); - - if ( empty( $email_data['privacy_policy_url'] ) ) { - /* translators: Do not translate SITENAME, SITEURL; those are placeholders. */ - $email_text = __( - 'Howdy, + $subject = apply_filters_deprecated( + 'user_erasure_complete_email_subject', + array( $subject, $email_data['sitename'], $email_data ), + '5.8.0', + 'user_erasure_fulfillment_email_subject' + ); + + /** + * Filters the subject of the email sent when an erasure request is completed. + * + * @since 5.8.0 + * + * @param string $subject The email subject. + * @param string $sitename The name of the site. + * @param array $email_data { + * Data relating to the account action email. + * + * @type WP_User_Request $request User request object. + * @type string $message_recipient The address that the email will be sent to. Defaults + * to the value of `$request->email`, but can be changed + * by the `user_erasure_fulfillment_email_to` filter. + * @type string $privacy_policy_url Privacy policy URL. + * @type string $sitename The site name sending the mail. + * @type string $siteurl The site URL sending the mail. + * } + */ + $subject = apply_filters( 'user_erasure_fulfillment_email_subject', $subject, $email_data['sitename'], $email_data ); + + /* translators: Do not translate SITENAME, SITEURL; those are placeholders. */ + $content = __( + 'Howdy, Your request to erase your personal data on ###SITENAME### has been completed. @@ -3439,10 +3917,11 @@ Regards, All at ###SITENAME### ###SITEURL###' - ); - } else { + ); + + if ( ! empty( $email_data['privacy_policy_url'] ) ) { /* translators: Do not translate SITENAME, SITEURL, PRIVACY_POLICY_URL; those are placeholders. */ - $email_text = __( + $content = __( 'Howdy, Your request to erase your personal data on ###SITENAME### has been completed. @@ -3460,7 +3939,7 @@ /** * Filters the body of the data erasure fulfillment notification. * - * The email is sent to a user when a their data erasure request is fulfilled + * The email is sent to a user when their data erasure request is fulfilled * by an administrator. * * The following strings have a special meaning and will get replaced dynamically: @@ -3470,8 +3949,11 @@ * ###SITEURL### The URL to the site. * * @since 4.9.6 + * @deprecated 5.8.0 Use {@see 'user_erasure_fulfillment_email_content'} instead. + * For user request confirmation email content + * use {@see 'user_request_confirmed_email_content'} instead. * - * @param string $email_text Text in the email. + * @param string $content The email content. * @param array $email_data { * Data relating to the account action email. * @@ -3484,7 +3966,46 @@ * @type string $siteurl The site URL sending the mail. * } */ - $content = apply_filters( 'user_confirmed_action_email_content', $email_text, $email_data ); + $content = apply_filters_deprecated( + 'user_confirmed_action_email_content', + array( $content, $email_data ), + '5.8.0', + sprintf( + /* translators: 1 & 2: Deprecation replacement options. */ + __( '%1$s or %2$s' ), + 'user_erasure_fulfillment_email_content', + 'user_request_confirmed_email_content' + ) + ); + + /** + * Filters the body of the data erasure fulfillment notification. + * + * The email is sent to a user when their data erasure request is fulfilled + * by an administrator. + * + * The following strings have a special meaning and will get replaced dynamically: + * + * ###SITENAME### The name of the site. + * ###PRIVACY_POLICY_URL### Privacy policy page URL. + * ###SITEURL### The URL to the site. + * + * @since 5.8.0 + * + * @param string $content The email content. + * @param array $email_data { + * Data relating to the account action email. + * + * @type WP_User_Request $request User request object. + * @type string $message_recipient The address that the email will be sent to. Defaults + * to the value of `$request->email`, but can be changed + * by the `user_erasure_fulfillment_email_to` filter. + * @type string $privacy_policy_url Privacy policy URL. + * @type string $sitename The site name sending the mail. + * @type string $siteurl The site URL sending the mail. + * } + */ + $content = apply_filters( 'user_erasure_fulfillment_email_content', $content, $email_data ); $content = str_replace( '###SITENAME###', $email_data['sitename'], $content ); $content = str_replace( '###PRIVACY_POLICY_URL###', $email_data['privacy_policy_url'], $content ); @@ -3496,6 +4017,7 @@ * Filters the headers of the data erasure fulfillment notification. * * @since 5.4.0 + * @deprecated 5.8.0 Use {@see 'user_erasure_fulfillment_email_headers'} instead. * * @param string|array $headers The email headers. * @param string $subject The email subject. @@ -3513,7 +4035,35 @@ * @type string $siteurl The site URL sending the mail. * } */ - $headers = apply_filters( 'user_erasure_complete_email_headers', $headers, $subject, $content, $request_id, $email_data ); + $headers = apply_filters_deprecated( + 'user_erasure_complete_email_headers', + array( $headers, $subject, $content, $request_id, $email_data ), + '5.8.0', + 'user_erasure_fulfillment_email_headers' + ); + + /** + * Filters the headers of the data erasure fulfillment notification. + * + * @since 5.8.0 + * + * @param string|array $headers The email headers. + * @param string $subject The email subject. + * @param string $content The email content. + * @param int $request_id The request ID. + * @param array $email_data { + * Data relating to the account action email. + * + * @type WP_User_Request $request User request object. + * @type string $message_recipient The address that the email will be sent to. Defaults + * to the value of `$request->email`, but can be changed + * by the `user_erasure_fulfillment_email_to` filter. + * @type string $privacy_policy_url Privacy policy URL. + * @type string $sitename The site name sending the mail. + * @type string $siteurl The site URL sending the mail. + * } + */ + $headers = apply_filters( 'user_erasure_fulfillment_email_headers', $headers, $subject, $content, $request_id, $email_data ); $email_sent = wp_mail( $user_email, $subject, $content, $headers ); @@ -3571,13 +4121,17 @@ * users on the site, or guests without a user account. * * @since 4.9.6 + * @since 5.7.0 Added the `$status` parameter. * - * @param string $email_address User email address. This can be the address of a registered or non-registered user. - * @param string $action_name Name of the action that is being confirmed. Required. - * @param array $request_data Misc data you want to send with the verification request and pass to the actions once the request is confirmed. - * @return int|WP_Error Returns the request ID if successful, or a WP_Error object on failure. + * @param string $email_address User email address. This can be the address of a registered + * or non-registered user. + * @param string $action_name Name of the action that is being confirmed. Required. + * @param array $request_data Misc data you want to send with the verification request and pass + * to the actions once the request is confirmed. + * @param string $status Optional request status (pending or confirmed). Default 'pending'. + * @return int|WP_Error Returns the request ID if successful, or a WP_Error object on failure. */ -function wp_create_user_request( $email_address = '', $action_name = '', $request_data = array() ) { +function wp_create_user_request( $email_address = '', $action_name = '', $request_data = array(), $status = 'pending' ) { $email_address = sanitize_email( $email_address ); $action_name = sanitize_key( $action_name ); @@ -3585,10 +4139,14 @@ return new WP_Error( 'invalid_email', __( 'Invalid email address.' ) ); } - if ( ! $action_name ) { + if ( ! in_array( $action_name, _wp_privacy_action_request_types(), true ) ) { return new WP_Error( 'invalid_action', __( 'Invalid action name.' ) ); } + if ( ! in_array( $status, array( 'pending', 'confirmed' ), true ) ) { + return new WP_Error( 'invalid_status', __( 'Invalid request status.' ) ); + } + $user = get_user_by( 'email', $email_address ); $user_id = $user && ! is_wp_error( $user ) ? $user->ID : 0; @@ -3607,7 +4165,7 @@ ); if ( $requests_query->found_posts ) { - return new WP_Error( 'duplicate_request', __( 'An incomplete request for this email address already exists.' ) ); + return new WP_Error( 'duplicate_request', __( 'An incomplete personal data request for this email address already exists.' ) ); } $request_id = wp_insert_post( @@ -3616,7 +4174,7 @@ 'post_name' => $action_name, 'post_title' => $email_address, 'post_content' => wp_json_encode( $request_data ), - 'post_status' => 'request-pending', + 'post_status' => 'request-' . $status, 'post_type' => 'user_request', 'post_date' => current_time( 'mysql', false ), 'post_date_gmt' => current_time( 'mysql', true ), @@ -3668,14 +4226,14 @@ * @since 4.9.6 * * @param string $request_id ID of the request created via wp_create_user_request(). - * @return bool|WP_Error True on success, `WP_Error` on failure. + * @return true|WP_Error True on success, `WP_Error` on failure. */ function wp_send_user_request( $request_id ) { $request_id = absint( $request_id ); $request = wp_get_user_request( $request_id ); if ( ! $request ) { - return new WP_Error( 'invalid_request', __( 'Invalid user request.' ) ); + return new WP_Error( 'invalid_request', __( 'Invalid personal data request.' ) ); } // Localize message content for user; fallback to site default for visitors. @@ -3703,8 +4261,31 @@ 'siteurl' => home_url(), ); + /* translators: Confirm privacy data request notification email subject. 1: Site title, 2: Name of the action. */ + $subject = sprintf( __( '[%1$s] Confirm Action: %2$s' ), $email_data['sitename'], $email_data['description'] ); + + /** + * Filters the subject of the email sent when an account action is attempted. + * + * @since 4.9.6 + * + * @param string $subject The email subject. + * @param string $sitename The name of the site. + * @param array $email_data { + * Data relating to the account action email. + * + * @type WP_User_Request $request User request object. + * @type string $email The email address this is being sent to. + * @type string $description Description of the action being performed so the user knows what the email is for. + * @type string $confirm_url The link to click on to confirm the account action. + * @type string $sitename The site name sending the mail. + * @type string $siteurl The site URL sending the mail. + * } + */ + $subject = apply_filters( 'user_request_action_email_subject', $subject, $email_data['sitename'], $email_data ); + /* translators: Do not translate DESCRIPTION, CONFIRM_URL, SITENAME, SITEURL: those are placeholders. */ - $email_text = __( + $content = __( 'Howdy, A request has been made to perform the following action on your account: @@ -3734,7 +4315,7 @@ * * @since 4.9.6 * - * @param string $email_text Text in the email. + * @param string $content Text in the email. * @param array $email_data { * Data relating to the account action email. * @@ -3746,7 +4327,7 @@ * @type string $siteurl The site URL sending the mail. * } */ - $content = apply_filters( 'user_request_action_email_content', $email_text, $email_data ); + $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 ); @@ -3754,29 +4335,6 @@ $content = str_replace( '###SITENAME###', $email_data['sitename'], $content ); $content = str_replace( '###SITEURL###', esc_url_raw( $email_data['siteurl'] ), $content ); - /* translators: Confirm privacy data request notification email subject. 1: Site title, 2: Name of the action. */ - $subject = sprintf( __( '[%1$s] Confirm Action: %2$s' ), $email_data['sitename'], $email_data['description'] ); - - /** - * Filters the subject of the email sent when an account action is attempted. - * - * @since 4.9.6 - * - * @param string $subject The email subject. - * @param string $sitename The name of the site. - * @param array $email_data { - * Data relating to the account action email. - * - * @type WP_User_Request $request User request object. - * @type string $email The email address this is being sent to. - * @type string $description Description of the action being performed so the user knows what the email is for. - * @type string $confirm_url The link to click on to confirm the account action. - * @type string $sitename The site name sending the mail. - * @type string $siteurl The site URL sending the mail. - * } - */ - $subject = apply_filters( 'user_request_action_email_subject', $subject, $email_data['sitename'], $email_data ); - $headers = ''; /** @@ -3852,24 +4410,26 @@ * * @param string $request_id ID of the request being confirmed. * @param string $key Provided key to validate. - * @return bool|WP_Error True on success, WP_Error on failure. + * @return true|WP_Error True on success, WP_Error on failure. */ function wp_validate_user_request_key( $request_id, $key ) { global $wp_hasher; - $request_id = absint( $request_id ); - $request = wp_get_user_request( $request_id ); - - if ( ! $request ) { - return new WP_Error( 'invalid_request', __( 'Invalid request.' ) ); + $request_id = absint( $request_id ); + $request = wp_get_user_request( $request_id ); + $saved_key = $request->confirm_key; + $key_request_time = $request->modified_timestamp; + + if ( ! $request || ! $saved_key || ! $key_request_time ) { + return new WP_Error( 'invalid_request', __( 'Invalid personal data request.' ) ); } if ( ! in_array( $request->status, array( 'request-pending', 'request-failed' ), true ) ) { - return new WP_Error( 'expired_link', __( 'This link has expired.' ) ); + return new WP_Error( 'expired_request', __( 'This personal data request has expired.' ) ); } if ( empty( $key ) ) { - return new WP_Error( 'missing_key', __( 'Missing confirm key.' ) ); + return new WP_Error( 'missing_key', __( 'The confirmation key is missing from this personal data request.' ) ); } if ( empty( $wp_hasher ) ) { @@ -3877,17 +4437,6 @@ $wp_hasher = new PasswordHash( 8, true ); } - $key_request_time = $request->modified_timestamp; - $saved_key = $request->confirm_key; - - if ( ! $saved_key ) { - return new WP_Error( 'invalid_key', __( 'Invalid key.' ) ); - } - - if ( ! $key_request_time ) { - return new WP_Error( 'invalid_key', __( 'Invalid action.' ) ); - } - /** * Filters the expiration time of confirm keys. * @@ -3899,11 +4448,11 @@ $expiration_time = $key_request_time + $expiration_duration; if ( ! $wp_hasher->CheckPassword( $key, $saved_key ) ) { - return new WP_Error( 'invalid_key', __( 'Invalid key.' ) ); + return new WP_Error( 'invalid_key', __( 'The confirmation key is invalid for this personal data request.' ) ); } if ( ! $expiration_time || time() > $expiration_time ) { - return new WP_Error( 'expired_key', __( 'The confirmation email has expired.' ) ); + return new WP_Error( 'expired_key', __( 'The confirmation key has expired for this personal data request.' ) ); } return true; @@ -3927,3 +4476,61 @@ return new WP_User_Request( $post ); } + +/** + * 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. + * + * @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. + * + * @since 5.6.0 + * + * @param bool $available True if available, false otherwise. + */ + return apply_filters( 'wp_is_application_passwords_available', $available ); +} + +/** + * Checks if Application Passwords is available for a specific user. + * + * By default all users can use Application Passwords. Use {@see 'wp_is_application_passwords_available_for_user'} + * to restrict availability to certain users. + * + * @since 5.6.0 + * + * @param int|WP_User $user The user to check. + * @return bool + */ +function wp_is_application_passwords_available_for_user( $user ) { + if ( ! wp_is_application_passwords_available() ) { + return false; + } + + if ( ! is_object( $user ) ) { + $user = get_userdata( $user ); + } + + if ( ! $user || ! $user->exists() ) { + return false; + } + + /** + * Filters whether Application Passwords is available for a specific user. + * + * @since 5.6.0 + * + * @param bool $available True if available, false otherwise. + * @param WP_User $user The user to check. + */ + return apply_filters( 'wp_is_application_passwords_available_for_user', true, $user ); +}