wp/wp-includes/user.php
changeset 19 3d72ae0968f4
parent 18 be944660c56a
child 21 48c4eec2b7e6
--- 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 `<li>` 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 .= '<li>';
+		}
+
+		$row = $name;
+
+		if ( ! empty( $args['feed_image'] ) || ! empty( $args['feed'] ) ) {
+			$row .= ' ';
+			if ( empty( $args['feed_image'] ) ) {
+				$row .= '(';
+			}
+
+			$row .= '<a href="' . get_author_feed_link( $user->ID, $args['feed_type'] ) . '"';
+
+			$alt = '';
+			if ( ! empty( $args['feed'] ) ) {
+				$alt  = ' alt="' . esc_attr( $args['feed'] ) . '"';
+				$name = $args['feed'];
+			}
+
+			$row .= '>';
+
+			if ( ! empty( $args['feed_image'] ) ) {
+				$row .= '<img src="' . esc_url( $args['feed_image'] ) . '" style="border: none;"' . $alt . ' />';
+			} else {
+				$row .= $name;
+			}
+
+			$row .= '</a>';
+
+			if ( empty( $args['feed_image'] ) ) {
+				$row .= ')';
+			}
+		}
+
+		$return .= $row;
+		$return .= ( 'list' === $args['style'] ) ? '</li>' : ', ';
+	}
+
+	$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.' ),
+				'<code>$network_id</code>'
+			),
+			'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.' ),
+				'<code>$network_id</code>'
+			),
+			'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.' ),
+				'<code>$network_id</code>'
+			),
+			'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.' ),
+				'<code>$network_id</code>'
+			),
+			'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', __( '<strong>Error</strong>: Please type your email address.' ) );
 	} elseif ( ! is_email( $user_email ) ) {
-		$errors->add( 'invalid_email', __( '<strong>Error</strong>: The email address isn&#8217;t correct.' ) );
+		$errors->add( 'invalid_email', __( '<strong>Error</strong>: The email address is not correct.' ) );
 		$user_email = '';
 	} elseif ( email_exists( $user_email ) ) {
-		$errors->add( 'email_exists', __( '<strong>Error</strong>: This email is already registered. Please choose another one.' ) );
+		$errors->add(
+			'email_exists',
+			sprintf(
+				/* translators: %s: Link to the login page. */
+				__( '<strong>Error:</strong> This email address is already registered. <a href="%s">Log in</a> with this address or choose another one.' ),
+				wp_login_url()
+			)
+		);
 	}
 
 	/**
@@ -2989,7 +3372,7 @@
 			'registerfail',
 			sprintf(
 				/* translators: %s: Admin email address. */
-				__( '<strong>Error</strong>: Couldn&#8217;t register you&hellip; please contact the <a href="mailto:%s">site admin</a>!' ),
+				__( '<strong>Error</strong>: Could not register you&hellip; please contact the <a href="mailto:%s">site admin</a>!' ),
 				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',
-				__( '<strong>Error</strong>: The email address isn&#8217;t correct.' ),
+				__( '<strong>Error</strong>: 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() );
 }
 
 /**