diff -r 48c4eec2b7e6 -r 8c2e4d02f4ef wp/wp-includes/user.php --- a/wp/wp-includes/user.php Fri Sep 05 18:40:08 2025 +0200 +++ b/wp/wp-includes/user.php Fri Sep 05 18:52:52 2025 +0200 @@ -48,10 +48,10 @@ 'remember' => false, ); - if ( ! empty( $_POST['log'] ) ) { + if ( ! empty( $_POST['log'] ) && is_string( $_POST['log'] ) ) { $credentials['user_login'] = wp_unslash( $_POST['log'] ); } - if ( ! empty( $_POST['pwd'] ) ) { + if ( ! empty( $_POST['pwd'] ) && is_string( $_POST['pwd'] ) ) { $credentials['user_password'] = $_POST['pwd']; } if ( ! empty( $_POST['rememberme'] ) ) { @@ -150,7 +150,12 @@ * @param string $password Password for authentication. * @return WP_User|WP_Error WP_User on success, WP_Error on failure. */ -function wp_authenticate_username_password( $user, $username, $password ) { +function wp_authenticate_username_password( + $user, + $username, + #[\SensitiveParameter] + $password +) { if ( $user instanceof WP_User ) { return $user; } @@ -200,7 +205,9 @@ return $user; } - if ( ! wp_check_password( $password, $user->user_pass, $user->ID ) ) { + $valid = wp_check_password( $password, $user->user_pass, $user->ID ); + + if ( ! $valid ) { return new WP_Error( 'incorrect_password', sprintf( @@ -214,6 +221,10 @@ ); } + if ( wp_password_needs_rehash( $user->user_pass, $user->ID ) ) { + wp_set_password( $password, $user->ID ); + } + return $user; } @@ -228,7 +239,12 @@ * @param string $password Password for authentication. * @return WP_User|WP_Error WP_User on success, WP_Error on failure. */ -function wp_authenticate_email_password( $user, $email, $password ) { +function wp_authenticate_email_password( + $user, + $email, + #[\SensitiveParameter] + $password +) { if ( $user instanceof WP_User ) { return $user; } @@ -272,7 +288,9 @@ return $user; } - if ( ! wp_check_password( $password, $user->user_pass, $user->ID ) ) { + $valid = wp_check_password( $password, $user->user_pass, $user->ID ); + + if ( ! $valid ) { return new WP_Error( 'incorrect_password', sprintf( @@ -286,6 +304,10 @@ ); } + if ( wp_password_needs_rehash( $user->user_pass, $user->ID ) ) { + wp_set_password( $password, $user->ID ); + } + return $user; } @@ -301,7 +323,12 @@ * @param string $password Password. If not empty, cancels the cookie authentication. * @return WP_User|WP_Error WP_User on success, WP_Error on failure. */ -function wp_authenticate_cookie( $user, $username, $password ) { +function wp_authenticate_cookie( + $user, + $username, + #[\SensitiveParameter] + $password +) { global $auth_secure_cookie; if ( $user instanceof WP_User ) { @@ -342,7 +369,12 @@ * @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 ) { +function wp_authenticate_application_password( + $input_user, + $username, + #[\SensitiveParameter] + $password +) { if ( $input_user instanceof WP_User ) { return $input_user; } @@ -425,7 +457,7 @@ $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 ) ) { + if ( ! WP_Application_Passwords::check_password( $password, $item['password'] ) ) { continue; } @@ -584,9 +616,19 @@ function count_user_posts( $userid, $post_type = 'post', $public_only = false ) { global $wpdb; + $post_type = array_unique( (array) $post_type ); + sort( $post_type ); + $where = get_posts_by_author_sql( $post_type, true, $userid, $public_only ); - - $count = $wpdb->get_var( "SELECT COUNT(*) FROM $wpdb->posts $where" ); + $query = "SELECT COUNT(*) FROM $wpdb->posts $where"; + + $last_changed = wp_cache_get_last_changed( 'posts' ); + $cache_key = 'count_user_posts:' . md5( $query ) . ':' . $last_changed; + $count = wp_cache_get( $cache_key, 'post-queries' ); + if ( false === $count ) { + $count = $wpdb->get_var( $query ); + wp_cache_set( $cache_key, $count, 'post-queries' ); + } /** * Filters the number of posts a user has written. @@ -618,25 +660,38 @@ function count_many_users_posts( $users, $post_type = 'post', $public_only = false ) { global $wpdb; - $count = array(); if ( empty( $users ) || ! is_array( $users ) ) { - return $count; + return array(); + } + + /** + * Filters whether to short-circuit performing the post counts. + * + * When filtering, return an array of posts counts as strings, keyed + * by the user ID. + * + * @since 6.8.0 + * + * @param string[]|null $count The post counts. Return a non-null value to short-circuit. + * @param int[] $users Array of user IDs. + * @param string|string[] $post_type Single post type or array of post types to check. + * @param bool $public_only Whether to only return counts for public posts. + */ + $pre = apply_filters( 'pre_count_many_users_posts', null, $users, $post_type, $public_only ); + if ( null !== $pre ) { + return $pre; } $userlist = implode( ',', array_map( 'absint', $users ) ); $where = get_posts_by_author_sql( $post_type, true, null, $public_only ); $result = $wpdb->get_results( "SELECT post_author, COUNT(*) FROM $wpdb->posts $where AND post_author IN ($userlist) GROUP BY post_author", ARRAY_N ); + + $count = array_fill_keys( $users, 0 ); foreach ( $result as $row ) { $count[ $row[0] ] = $row[1]; } - foreach ( $users as $id ) { - if ( ! isset( $count[ $id ] ) ) { - $count[ $id ] = 0; - } - } - return $count; } @@ -776,6 +831,19 @@ } /** + * Retrieves user info by user ID. + * + * @since 6.7.0 + * + * @param int $user_id User ID. + * + * @return WP_User|false WP_User object on success, false on failure. + */ +function get_user( $user_id ) { + return get_user_by( 'id', $user_id ); +} + +/** * Retrieves list of users matching criteria. * * @since 3.1.0 @@ -1138,7 +1206,13 @@ * * @param int $user_id User ID. * @param string $meta_key Metadata name. - * @param mixed $meta_value Metadata value. Must be serializable if non-scalar. + * @param mixed $meta_value Metadata value. Arrays and objects are stored as serialized data and + * will be returned as the same type when retrieved. Other data types will + * be stored as strings in the database: + * - false is stored and retrieved as an empty string ('') + * - true is stored and retrieved as '1' + * - numbers (both integer and float) are stored and retrieved as strings + * Must be serializable if non-scalar. * @param bool $unique Optional. Whether the same key should not be added. * Default false. * @return int|false Meta ID on success, false on failure. @@ -1185,7 +1259,13 @@ * @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. + * An empty array if a valid but non-existing user ID is passed and `$single` is false. + * An empty string if a valid but non-existing user ID is passed and `$single` is true. + * Note: Non-serialized values are returned as strings: + * - false values are returned as empty strings ('') + * - true values are returned as '1' + * - numbers (both integer and float) are returned as strings + * Arrays and objects retain their original type. */ function get_user_meta( $user_id, $key = '', $single = false ) { return get_metadata( 'user', $user_id, $key, $single ); @@ -1549,12 +1629,10 @@ /** * 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 - * need to be used; all users will be displayed in that case. Only one can be - * used, either 'include' or 'exclude', but not both. - * - * The available arguments are as follows: + * The content can either be displayed, which it is by default, or retrieved by + * setting the 'echo' argument to false. The 'include' and 'exclude' arguments + * are optional; if they are not specified, all users will be displayed. Only one + * can be used in a single call, either 'include' or 'exclude', but not both. * * @since 2.3.0 * @since 4.5.0 Added the 'display_name_with_login' value for 'show'. @@ -2354,7 +2432,7 @@ $admin_color = empty( $userdata['admin_color'] ) ? 'fresh' : $userdata['admin_color']; $meta['admin_color'] = preg_replace( '|[^a-z0-9 _.\-@]|i', '', $admin_color ); - $meta['use_ssl'] = empty( $userdata['use_ssl'] ) ? 0 : (bool) $userdata['use_ssl']; + $meta['use_ssl'] = empty( $userdata['use_ssl'] ) ? '0' : '1'; $meta['show_admin_bar_front'] = empty( $userdata['show_admin_bar_front'] ) ? 'true' : $userdata['show_admin_bar_front']; @@ -2378,6 +2456,7 @@ * * @since 4.9.0 * @since 5.8.0 The `$userdata` parameter was added. + * @since 6.8.0 The user's password is now hashed using bcrypt by default instead of phpass. * * @param array $data { * Values and keys for the user. @@ -2386,7 +2465,7 @@ * @type string $user_pass The user's password. * @type string $user_email The user's email. * @type string $user_url The user's url. - * @type string $user_nicename The user's nice name. Defaults to a URL-safe version of user's login + * @type string $user_nicename The user's nice name. Defaults to a URL-safe version of user's login. * @type string $display_name The user's display name. * @type string $user_registered MySQL timestamp describing the moment when the user registered. Defaults to * the current UTC timestamp. @@ -2768,8 +2847,6 @@ $current_user = wp_get_current_user(); if ( $current_user->ID === $user_id ) { if ( isset( $plaintext_pass ) ) { - wp_clear_auth_cookie(); - /* * Here we calculate the expiration length of the current auth cookie and compare it to the default expiration. * If it's greater than this, then we know the user checked 'Remember Me' when they logged in. @@ -2778,13 +2855,20 @@ /** This filter is documented in wp-includes/pluggable.php */ $default_cookie_life = apply_filters( 'auth_cookie_expiration', ( 2 * DAY_IN_SECONDS ), $user_id, false ); + wp_clear_auth_cookie(); + $remember = false; - - if ( false !== $logged_in_cookie && ( $logged_in_cookie['expiration'] - time() ) > $default_cookie_life ) { + $token = ''; + + if ( false !== $logged_in_cookie ) { + $token = $logged_in_cookie['token']; + } + + if ( false !== $logged_in_cookie && ( (int) $logged_in_cookie['expiration'] - time() ) > $default_cookie_life ) { $remember = true; } - wp_set_auth_cookie( $user_id, $remember ); + wp_set_auth_cookie( $user_id, $remember, '', $token ); } } @@ -2818,7 +2902,12 @@ * @return int|WP_Error The newly created user's ID or a WP_Error object if the user could not * be created. */ -function wp_create_user( $username, $password, $email = '' ) { +function wp_create_user( + $username, + #[\SensitiveParameter] + $password, + $email = '' +) { $user_login = wp_slash( $username ); $user_email = wp_slash( $email ); $user_pass = $password; @@ -2915,14 +3004,10 @@ * * @since 4.4.0 * - * @global PasswordHash $wp_hasher Portable PHP password hashing framework instance. - * * @param WP_User $user User to retrieve password reset key for. * @return string|WP_Error Password reset key on success. WP_Error on error. */ function get_password_reset_key( $user ) { - global $wp_hasher; - if ( ! ( $user instanceof WP_User ) ) { return new WP_Error( 'invalidcombo', __( 'Error: There is no account with that username or email address.' ) ); } @@ -2968,13 +3053,7 @@ */ do_action( 'retrieve_password_key', $user->user_login, $key ); - // Now insert the key, hashed, into the DB. - if ( empty( $wp_hasher ) ) { - require_once ABSPATH . WPINC . '/class-phpass.php'; - $wp_hasher = new PasswordHash( 8, true ); - } - - $hashed = time() . ':' . $wp_hasher->HashPassword( $key ); + $hashed = time() . ':' . wp_fast_hash( $key ); $key_saved = wp_update_user( array( @@ -3000,15 +3079,15 @@ * * @since 3.1.0 * - * @global PasswordHash $wp_hasher Portable PHP password hashing framework instance. - * - * @param string $key Hash to validate sending user's password. + * @param string $key The password reset key. * @param string $login The user login. * @return WP_User|WP_Error WP_User object on success, WP_Error object for invalid or expired keys. */ -function check_password_reset_key( $key, $login ) { - global $wp_hasher; - +function check_password_reset_key( + #[\SensitiveParameter] + $key, + $login +) { $key = preg_replace( '/[^a-z0-9]/i', '', $key ); if ( empty( $key ) || ! is_string( $key ) ) { @@ -3025,11 +3104,6 @@ return new WP_Error( 'invalid_key', __( 'Invalid key.' ) ); } - if ( empty( $wp_hasher ) ) { - require_once ABSPATH . WPINC . '/class-phpass.php'; - $wp_hasher = new PasswordHash( 8, true ); - } - /** * Filters the expiration time of password reset keys. * @@ -3051,7 +3125,7 @@ return new WP_Error( 'invalid_key', __( 'Invalid key.' ) ); } - $hash_is_correct = $wp_hasher->CheckPassword( $key, $pass_key ); + $hash_is_correct = wp_verify_fast_hash( $key, $pass_key ); if ( $hash_is_correct && $expiration_time && time() < $expiration_time ) { return $user; @@ -3066,7 +3140,7 @@ /** * Filters the return value of check_password_reset_key() when an - * old-style key is used. + * old-style key or an expired key is used. * * @since 3.7.0 Previously plain-text keys were stored in the database. * @since 4.3.0 Previously key hashes were stored without an expiration time. @@ -3087,14 +3161,13 @@ * @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 instance. + * @global wpdb $wpdb WordPress database abstraction object. * * @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 ) { +function retrieve_password( $user_login = '' ) { $errors = new WP_Error(); $user_data = false; @@ -3219,7 +3292,15 @@ $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"; + + /* + * Since some user login names end in a period, this could produce ambiguous URLs that + * end in a period. To avoid the ambiguity, ensure that the login is not the last query + * arg in the URL. If moving it to the end, a trailing period will need to be escaped. + * + * @see https://core.trac.wordpress.org/tickets/42957 + */ + $message .= network_site_url( 'wp-login.php?login=' . rawurlencode( $user_login ) . "&key=$key&action=rp", 'login' ) . '&wp_lang=' . $locale . "\r\n\r\n"; if ( ! is_user_logged_in() ) { $requester_ip = $_SERVER['REMOTE_ADDR']; @@ -3335,7 +3416,11 @@ * @param WP_User $user The user * @param string $new_pass New password for the user in plaintext */ -function reset_password( $user, $new_pass ) { +function reset_password( + $user, + #[\SensitiveParameter] + $new_pass +) { /** * Fires before the user's password is reset. * @@ -4148,7 +4233,7 @@ * 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 $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. @@ -4199,7 +4284,7 @@ * 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 $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. @@ -4239,7 +4324,7 @@ * 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 $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. @@ -4270,7 +4355,7 @@ * 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 $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. @@ -4857,28 +4942,19 @@ * * @since 4.9.6 * - * @global PasswordHash $wp_hasher Portable PHP password hashing framework instance. - * * @param int $request_id Request ID. * @return string Confirmation key. */ function wp_generate_user_request_key( $request_id ) { - global $wp_hasher; - // Generate something random for a confirmation key. $key = wp_generate_password( 20, false ); - // Return the key, hashed. - if ( empty( $wp_hasher ) ) { - require_once ABSPATH . WPINC . '/class-phpass.php'; - $wp_hasher = new PasswordHash( 8, true ); - } - + // Save the key, hashed. wp_update_post( array( 'ID' => $request_id, 'post_status' => 'request-pending', - 'post_password' => $wp_hasher->HashPassword( $key ), + 'post_password' => wp_fast_hash( $key ), ) ); @@ -4890,15 +4966,15 @@ * * @since 4.9.6 * - * @global PasswordHash $wp_hasher Portable PHP password hashing framework instance. - * * @param string $request_id ID of the request being confirmed. * @param string $key Provided key to validate. * @return true|WP_Error True on success, WP_Error on failure. */ -function wp_validate_user_request_key( $request_id, $key ) { - global $wp_hasher; - +function wp_validate_user_request_key( + $request_id, + #[\SensitiveParameter] + $key +) { $request_id = absint( $request_id ); $request = wp_get_user_request( $request_id ); $saved_key = $request->confirm_key; @@ -4916,11 +4992,6 @@ return new WP_Error( 'missing_key', __( 'The confirmation key is missing from this personal data request.' ) ); } - if ( empty( $wp_hasher ) ) { - require_once ABSPATH . WPINC . '/class-phpass.php'; - $wp_hasher = new PasswordHash( 8, true ); - } - /** * Filters the expiration time of confirm keys. * @@ -4931,7 +5002,7 @@ $expiration_duration = (int) apply_filters( 'user_request_key_expiration', DAY_IN_SECONDS ); $expiration_time = $key_request_time + $expiration_duration; - if ( ! $wp_hasher->CheckPassword( $key, $saved_key ) ) { + if ( ! wp_verify_fast_hash( $key, $saved_key ) ) { return new WP_Error( 'invalid_key', __( 'The confirmation key is invalid for this personal data request.' ) ); }