diff -r 48c4eec2b7e6 -r 8c2e4d02f4ef wp/wp-includes/pluggable.php --- a/wp/wp-includes/pluggable.php Fri Sep 05 18:40:08 2025 +0200 +++ b/wp/wp-includes/pluggable.php Fri Sep 05 18:52:52 2025 +0200 @@ -251,7 +251,8 @@ require_once ABSPATH . WPINC . '/PHPMailer/PHPMailer.php'; require_once ABSPATH . WPINC . '/PHPMailer/SMTP.php'; require_once ABSPATH . WPINC . '/PHPMailer/Exception.php'; - $phpmailer = new PHPMailer\PHPMailer\PHPMailer( true ); + require_once ABSPATH . WPINC . '/class-wp-phpmailer.php'; + $phpmailer = new WP_PHPMailer( true ); $phpmailer::$validator = static function ( $email ) { return (bool) is_email( $email ); @@ -597,7 +598,11 @@ * @return WP_User|WP_Error WP_User object if the credentials are valid, * otherwise WP_Error. */ - function wp_authenticate( $username, $password ) { + function wp_authenticate( + $username, + #[\SensitiveParameter] + $password + ) { $username = sanitize_user( $username ); $password = trim( $password ); @@ -688,6 +693,7 @@ * * @param string $cookie Optional. If used, will validate contents instead of cookie's. * @param string $scheme Optional. The cookie scheme to use: 'auth', 'secure_auth', or 'logged_in'. + * Note: This does *not* default to 'auth' like other cookie functions. * @return int|false User ID if valid cookie, false if invalid. */ function wp_validate_auth_cookie( $cookie = '', $scheme = '' ) { @@ -710,9 +716,10 @@ $username = $cookie_elements['username']; $hmac = $cookie_elements['hmac']; $token = $cookie_elements['token']; - $expired = $cookie_elements['expiration']; $expiration = $cookie_elements['expiration']; + $expired = (int) $expiration; + // Allow a grace period for POST and Ajax requests. if ( wp_doing_ajax() || 'POST' === $_SERVER['REQUEST_METHOD'] ) { $expired += HOUR_IN_SECONDS; @@ -762,13 +769,17 @@ return false; } - $pass_frag = substr( $user->user_pass, 8, 4 ); + if ( str_starts_with( $user->user_pass, '$P$' ) || str_starts_with( $user->user_pass, '$2y$' ) ) { + // Retain previous behaviour of phpass or vanilla bcrypt hashed passwords. + $pass_frag = substr( $user->user_pass, 8, 4 ); + } else { + // Otherwise, use a substring from the end of the hash to avoid dealing with potentially long hash prefixes. + $pass_frag = substr( $user->user_pass, -4 ); + } $key = wp_hash( $username . '|' . $pass_frag . '|' . $expiration . '|' . $token, $scheme ); - // If ext/hash is not present, compat.php's hash_hmac() does not support sha256. - $algo = function_exists( 'hash' ) ? 'sha256' : 'sha1'; - $hash = hash_hmac( $algo, $username . '|' . $expiration . '|' . $token, $key ); + $hash = hash_hmac( 'sha256', $username . '|' . $expiration . '|' . $token, $key ); if ( ! hash_equals( $hash, $hmac ) ) { /** @@ -865,13 +876,17 @@ $token = $manager->create( $expiration ); } - $pass_frag = substr( $user->user_pass, 8, 4 ); + if ( str_starts_with( $user->user_pass, '$P$' ) || str_starts_with( $user->user_pass, '$2y$' ) ) { + // Retain previous behaviour of phpass or vanilla bcrypt hashed passwords. + $pass_frag = substr( $user->user_pass, 8, 4 ); + } else { + // Otherwise, use a substring from the end of the hash to avoid dealing with potentially long hash prefixes. + $pass_frag = substr( $user->user_pass, -4 ); + } $key = wp_hash( $user->user_login . '|' . $pass_frag . '|' . $expiration . '|' . $token, $scheme ); - // If ext/hash is not present, compat.php's hash_hmac() does not support sha256. - $algo = function_exists( 'hash' ) ? 'sha256' : 'sha1'; - $hash = hash_hmac( $algo, $user->user_login . '|' . $expiration . '|' . $token, $key ); + $hash = hash_hmac( 'sha256', $user->user_login . '|' . $expiration . '|' . $token, $key ); $cookie = $user->user_login . '|' . $expiration . '|' . $token . '|' . $hash; @@ -1719,8 +1734,6 @@ $emails = array_flip( $emails ); } - $switched_locale = switch_to_locale( get_locale() ); - $comment_author_domain = ''; if ( WP_Http::is_ip_address( $comment->comment_author_IP ) ) { $comment_author_domain = gethostbyaddr( $comment->comment_author_IP ); @@ -1733,74 +1746,6 @@ $blogname = wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES ); $comment_content = wp_specialchars_decode( $comment->comment_content ); - switch ( $comment->comment_type ) { - case 'trackback': - /* translators: %s: Post title. */ - $notify_message = sprintf( __( 'New trackback on your post "%s"' ), $post->post_title ) . "\r\n"; - /* translators: 1: Trackback/pingback website name, 2: Website IP address, 3: Website hostname. */ - $notify_message .= sprintf( __( 'Website: %1$s (IP address: %2$s, %3$s)' ), $comment->comment_author, $comment->comment_author_IP, $comment_author_domain ) . "\r\n"; - /* translators: %s: Trackback/pingback/comment author URL. */ - $notify_message .= sprintf( __( 'URL: %s' ), $comment->comment_author_url ) . "\r\n"; - /* translators: %s: Comment text. */ - $notify_message .= sprintf( __( 'Comment: %s' ), "\r\n" . $comment_content ) . "\r\n\r\n"; - $notify_message .= __( 'You can see all trackbacks on this post here:' ) . "\r\n"; - /* translators: Trackback notification email subject. 1: Site title, 2: Post title. */ - $subject = sprintf( __( '[%1$s] Trackback: "%2$s"' ), $blogname, $post->post_title ); - break; - - case 'pingback': - /* translators: %s: Post title. */ - $notify_message = sprintf( __( 'New pingback on your post "%s"' ), $post->post_title ) . "\r\n"; - /* translators: 1: Trackback/pingback website name, 2: Website IP address, 3: Website hostname. */ - $notify_message .= sprintf( __( 'Website: %1$s (IP address: %2$s, %3$s)' ), $comment->comment_author, $comment->comment_author_IP, $comment_author_domain ) . "\r\n"; - /* translators: %s: Trackback/pingback/comment author URL. */ - $notify_message .= sprintf( __( 'URL: %s' ), $comment->comment_author_url ) . "\r\n"; - /* translators: %s: Comment text. */ - $notify_message .= sprintf( __( 'Comment: %s' ), "\r\n" . $comment_content ) . "\r\n\r\n"; - $notify_message .= __( 'You can see all pingbacks on this post here:' ) . "\r\n"; - /* translators: Pingback notification email subject. 1: Site title, 2: Post title. */ - $subject = sprintf( __( '[%1$s] Pingback: "%2$s"' ), $blogname, $post->post_title ); - break; - - default: // Comments. - /* translators: %s: Post title. */ - $notify_message = sprintf( __( 'New comment on your post "%s"' ), $post->post_title ) . "\r\n"; - /* translators: 1: Comment author's name, 2: Comment author's IP address, 3: Comment author's hostname. */ - $notify_message .= sprintf( __( 'Author: %1$s (IP address: %2$s, %3$s)' ), $comment->comment_author, $comment->comment_author_IP, $comment_author_domain ) . "\r\n"; - /* translators: %s: Comment author email. */ - $notify_message .= sprintf( __( 'Email: %s' ), $comment->comment_author_email ) . "\r\n"; - /* translators: %s: Trackback/pingback/comment author URL. */ - $notify_message .= sprintf( __( 'URL: %s' ), $comment->comment_author_url ) . "\r\n"; - - if ( $comment->comment_parent && user_can( $post->post_author, 'edit_comment', $comment->comment_parent ) ) { - /* translators: Comment moderation. %s: Parent comment edit URL. */ - $notify_message .= sprintf( __( 'In reply to: %s' ), admin_url( "comment.php?action=editcomment&c={$comment->comment_parent}#wpbody-content" ) ) . "\r\n"; - } - - /* translators: %s: Comment text. */ - $notify_message .= sprintf( __( 'Comment: %s' ), "\r\n" . $comment_content ) . "\r\n\r\n"; - $notify_message .= __( 'You can see all comments on this post here:' ) . "\r\n"; - /* translators: Comment notification email subject. 1: Site title, 2: Post title. */ - $subject = sprintf( __( '[%1$s] Comment: "%2$s"' ), $blogname, $post->post_title ); - break; - } - - $notify_message .= get_permalink( $comment->comment_post_ID ) . "#comments\r\n\r\n"; - /* translators: %s: Comment URL. */ - $notify_message .= sprintf( __( 'Permalink: %s' ), get_comment_link( $comment ) ) . "\r\n"; - - if ( user_can( $post->post_author, 'edit_comment', $comment->comment_ID ) ) { - if ( EMPTY_TRASH_DAYS ) { - /* translators: Comment moderation. %s: Comment action URL. */ - $notify_message .= sprintf( __( 'Trash it: %s' ), admin_url( "comment.php?action=trash&c={$comment->comment_ID}#wpbody-content" ) ) . "\r\n"; - } else { - /* translators: Comment moderation. %s: Comment action URL. */ - $notify_message .= sprintf( __( 'Delete it: %s' ), admin_url( "comment.php?action=delete&c={$comment->comment_ID}#wpbody-content" ) ) . "\r\n"; - } - /* translators: Comment moderation. %s: Comment action URL. */ - $notify_message .= sprintf( __( 'Spam it: %s' ), admin_url( "comment.php?action=spam&c={$comment->comment_ID}#wpbody-content" ) ) . "\r\n"; - } - $wp_email = 'wordpress@' . preg_replace( '#^www\.#', '', wp_parse_url( network_home_url(), PHP_URL_HOST ) ); if ( '' === $comment->comment_author ) { @@ -1823,26 +1768,6 @@ } /** - * Filters the comment notification email text. - * - * @since 1.5.2 - * - * @param string $notify_message The comment notification email text. - * @param string $comment_id Comment ID as a numeric string. - */ - $notify_message = apply_filters( 'comment_notification_text', $notify_message, $comment->comment_ID ); - - /** - * Filters the comment notification email subject. - * - * @since 1.5.2 - * - * @param string $subject The comment notification email subject. - * @param string $comment_id Comment ID as a numeric string. - */ - $subject = apply_filters( 'comment_notification_subject', $subject, $comment->comment_ID ); - - /** * Filters the comment notification email headers. * * @since 1.5.2 @@ -1853,11 +1778,107 @@ $message_headers = apply_filters( 'comment_notification_headers', $message_headers, $comment->comment_ID ); foreach ( $emails as $email ) { + $user = get_user_by( 'email', $email ); + + if ( $user ) { + $switched_locale = switch_to_user_locale( $user->ID ); + } else { + $switched_locale = switch_to_locale( get_locale() ); + } + + switch ( $comment->comment_type ) { + case 'trackback': + /* translators: %s: Post title. */ + $notify_message = sprintf( __( 'New trackback on your post "%s"' ), $post->post_title ) . "\r\n"; + /* translators: 1: Trackback/pingback website name, 2: Website IP address, 3: Website hostname. */ + $notify_message .= sprintf( __( 'Website: %1$s (IP address: %2$s, %3$s)' ), $comment->comment_author, $comment->comment_author_IP, $comment_author_domain ) . "\r\n"; + /* translators: %s: Trackback/pingback/comment author URL. */ + $notify_message .= sprintf( __( 'URL: %s' ), $comment->comment_author_url ) . "\r\n"; + /* translators: %s: Comment text. */ + $notify_message .= sprintf( __( 'Comment: %s' ), "\r\n" . $comment_content ) . "\r\n\r\n"; + $notify_message .= __( 'You can see all trackbacks on this post here:' ) . "\r\n"; + /* translators: Trackback notification email subject. 1: Site title, 2: Post title. */ + $subject = sprintf( __( '[%1$s] Trackback: "%2$s"' ), $blogname, $post->post_title ); + break; + + case 'pingback': + /* translators: %s: Post title. */ + $notify_message = sprintf( __( 'New pingback on your post "%s"' ), $post->post_title ) . "\r\n"; + /* translators: 1: Trackback/pingback website name, 2: Website IP address, 3: Website hostname. */ + $notify_message .= sprintf( __( 'Website: %1$s (IP address: %2$s, %3$s)' ), $comment->comment_author, $comment->comment_author_IP, $comment_author_domain ) . "\r\n"; + /* translators: %s: Trackback/pingback/comment author URL. */ + $notify_message .= sprintf( __( 'URL: %s' ), $comment->comment_author_url ) . "\r\n"; + /* translators: %s: Comment text. */ + $notify_message .= sprintf( __( 'Comment: %s' ), "\r\n" . $comment_content ) . "\r\n\r\n"; + $notify_message .= __( 'You can see all pingbacks on this post here:' ) . "\r\n"; + /* translators: Pingback notification email subject. 1: Site title, 2: Post title. */ + $subject = sprintf( __( '[%1$s] Pingback: "%2$s"' ), $blogname, $post->post_title ); + break; + + default: // Comments. + /* translators: %s: Post title. */ + $notify_message = sprintf( __( 'New comment on your post "%s"' ), $post->post_title ) . "\r\n"; + /* translators: 1: Comment author's name, 2: Comment author's IP address, 3: Comment author's hostname. */ + $notify_message .= sprintf( __( 'Author: %1$s (IP address: %2$s, %3$s)' ), $comment->comment_author, $comment->comment_author_IP, $comment_author_domain ) . "\r\n"; + /* translators: %s: Comment author email. */ + $notify_message .= sprintf( __( 'Email: %s' ), $comment->comment_author_email ) . "\r\n"; + /* translators: %s: Trackback/pingback/comment author URL. */ + $notify_message .= sprintf( __( 'URL: %s' ), $comment->comment_author_url ) . "\r\n"; + + if ( $comment->comment_parent && user_can( $post->post_author, 'edit_comment', $comment->comment_parent ) ) { + /* translators: Comment moderation. %s: Parent comment edit URL. */ + $notify_message .= sprintf( __( 'In reply to: %s' ), admin_url( "comment.php?action=editcomment&c={$comment->comment_parent}#wpbody-content" ) ) . "\r\n"; + } + + /* translators: %s: Comment text. */ + $notify_message .= sprintf( __( 'Comment: %s' ), "\r\n" . $comment_content ) . "\r\n\r\n"; + $notify_message .= __( 'You can see all comments on this post here:' ) . "\r\n"; + /* translators: Comment notification email subject. 1: Site title, 2: Post title. */ + $subject = sprintf( __( '[%1$s] Comment: "%2$s"' ), $blogname, $post->post_title ); + break; + } + + $notify_message .= get_permalink( $comment->comment_post_ID ) . "#comments\r\n\r\n"; + /* translators: %s: Comment URL. */ + $notify_message .= sprintf( __( 'Permalink: %s' ), get_comment_link( $comment ) ) . "\r\n"; + + if ( user_can( $post->post_author, 'edit_comment', $comment->comment_ID ) ) { + if ( EMPTY_TRASH_DAYS ) { + /* translators: Comment moderation. %s: Comment action URL. */ + $notify_message .= sprintf( __( 'Trash it: %s' ), admin_url( "comment.php?action=trash&c={$comment->comment_ID}#wpbody-content" ) ) . "\r\n"; + } else { + /* translators: Comment moderation. %s: Comment action URL. */ + $notify_message .= sprintf( __( 'Delete it: %s' ), admin_url( "comment.php?action=delete&c={$comment->comment_ID}#wpbody-content" ) ) . "\r\n"; + } + /* translators: Comment moderation. %s: Comment action URL. */ + $notify_message .= sprintf( __( 'Spam it: %s' ), admin_url( "comment.php?action=spam&c={$comment->comment_ID}#wpbody-content" ) ) . "\r\n"; + } + + /** + * Filters the comment notification email text. + * + * @since 1.5.2 + * + * @param string $notify_message The comment notification email text. + * @param string $comment_id Comment ID as a numeric string. + */ + $notify_message = apply_filters( 'comment_notification_text', $notify_message, $comment->comment_ID ); + + /** + * Filters the comment notification email subject. + * + * @since 1.5.2 + * + * @param string $subject The comment notification email subject. + * @param string $comment_id Comment ID as a numeric string. + */ + $subject = apply_filters( 'comment_notification_subject', $subject, $comment->comment_ID ); + wp_mail( $email, wp_specialchars_decode( $subject ), $notify_message, $message_headers ); - } - - if ( $switched_locale ) { - restore_previous_locale(); + + if ( $switched_locale ) { + restore_previous_locale(); + } } return true; @@ -1908,8 +1929,6 @@ } } - $switched_locale = switch_to_locale( get_locale() ); - $comment_author_domain = ''; if ( WP_Http::is_ip_address( $comment->comment_author_IP ) ) { $comment_author_domain = gethostbyaddr( $comment->comment_author_IP ); @@ -1924,77 +1943,6 @@ $blogname = wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES ); $comment_content = wp_specialchars_decode( $comment->comment_content ); - switch ( $comment->comment_type ) { - case 'trackback': - /* translators: %s: Post title. */ - $notify_message = sprintf( __( 'A new trackback on the post "%s" is waiting for your approval' ), $post->post_title ) . "\r\n"; - $notify_message .= get_permalink( $comment->comment_post_ID ) . "\r\n\r\n"; - /* translators: 1: Trackback/pingback website name, 2: Website IP address, 3: Website hostname. */ - $notify_message .= sprintf( __( 'Website: %1$s (IP address: %2$s, %3$s)' ), $comment->comment_author, $comment->comment_author_IP, $comment_author_domain ) . "\r\n"; - /* translators: %s: Trackback/pingback/comment author URL. */ - $notify_message .= sprintf( __( 'URL: %s' ), $comment->comment_author_url ) . "\r\n"; - $notify_message .= __( 'Trackback excerpt: ' ) . "\r\n" . $comment_content . "\r\n\r\n"; - break; - - case 'pingback': - /* translators: %s: Post title. */ - $notify_message = sprintf( __( 'A new pingback on the post "%s" is waiting for your approval' ), $post->post_title ) . "\r\n"; - $notify_message .= get_permalink( $comment->comment_post_ID ) . "\r\n\r\n"; - /* translators: 1: Trackback/pingback website name, 2: Website IP address, 3: Website hostname. */ - $notify_message .= sprintf( __( 'Website: %1$s (IP address: %2$s, %3$s)' ), $comment->comment_author, $comment->comment_author_IP, $comment_author_domain ) . "\r\n"; - /* translators: %s: Trackback/pingback/comment author URL. */ - $notify_message .= sprintf( __( 'URL: %s' ), $comment->comment_author_url ) . "\r\n"; - $notify_message .= __( 'Pingback excerpt: ' ) . "\r\n" . $comment_content . "\r\n\r\n"; - break; - - default: // Comments. - /* translators: %s: Post title. */ - $notify_message = sprintf( __( 'A new comment on the post "%s" is waiting for your approval' ), $post->post_title ) . "\r\n"; - $notify_message .= get_permalink( $comment->comment_post_ID ) . "\r\n\r\n"; - /* translators: 1: Comment author's name, 2: Comment author's IP address, 3: Comment author's hostname. */ - $notify_message .= sprintf( __( 'Author: %1$s (IP address: %2$s, %3$s)' ), $comment->comment_author, $comment->comment_author_IP, $comment_author_domain ) . "\r\n"; - /* translators: %s: Comment author email. */ - $notify_message .= sprintf( __( 'Email: %s' ), $comment->comment_author_email ) . "\r\n"; - /* translators: %s: Trackback/pingback/comment author URL. */ - $notify_message .= sprintf( __( 'URL: %s' ), $comment->comment_author_url ) . "\r\n"; - - if ( $comment->comment_parent ) { - /* translators: Comment moderation. %s: Parent comment edit URL. */ - $notify_message .= sprintf( __( 'In reply to: %s' ), admin_url( "comment.php?action=editcomment&c={$comment->comment_parent}#wpbody-content" ) ) . "\r\n"; - } - - /* translators: %s: Comment text. */ - $notify_message .= sprintf( __( 'Comment: %s' ), "\r\n" . $comment_content ) . "\r\n\r\n"; - break; - } - - /* translators: Comment moderation. %s: Comment action URL. */ - $notify_message .= sprintf( __( 'Approve it: %s' ), admin_url( "comment.php?action=approve&c={$comment_id}#wpbody-content" ) ) . "\r\n"; - - if ( EMPTY_TRASH_DAYS ) { - /* translators: Comment moderation. %s: Comment action URL. */ - $notify_message .= sprintf( __( 'Trash it: %s' ), admin_url( "comment.php?action=trash&c={$comment_id}#wpbody-content" ) ) . "\r\n"; - } else { - /* translators: Comment moderation. %s: Comment action URL. */ - $notify_message .= sprintf( __( 'Delete it: %s' ), admin_url( "comment.php?action=delete&c={$comment_id}#wpbody-content" ) ) . "\r\n"; - } - - /* translators: Comment moderation. %s: Comment action URL. */ - $notify_message .= sprintf( __( 'Spam it: %s' ), admin_url( "comment.php?action=spam&c={$comment_id}#wpbody-content" ) ) . "\r\n"; - - $notify_message .= sprintf( - /* translators: Comment moderation. %s: Number of comments awaiting approval. */ - _n( - 'Currently %s comment is waiting for approval. Please visit the moderation panel:', - 'Currently %s comments are waiting for approval. Please visit the moderation panel:', - $comments_waiting - ), - number_format_i18n( $comments_waiting ) - ) . "\r\n"; - $notify_message .= admin_url( 'edit-comments.php?comment_status=moderated#wpbody-content' ) . "\r\n"; - - /* translators: Comment moderation notification email subject. 1: Site title, 2: Post title. */ - $subject = sprintf( __( '[%1$s] Please moderate: "%2$s"' ), $blogname, $post->post_title ); $message_headers = ''; /** @@ -2008,26 +1956,6 @@ $emails = apply_filters( 'comment_moderation_recipients', $emails, $comment_id ); /** - * Filters the comment moderation email text. - * - * @since 1.5.2 - * - * @param string $notify_message Text of the comment moderation email. - * @param int $comment_id Comment ID. - */ - $notify_message = apply_filters( 'comment_moderation_text', $notify_message, $comment_id ); - - /** - * Filters the comment moderation email subject. - * - * @since 1.5.2 - * - * @param string $subject Subject of the comment moderation email. - * @param int $comment_id Comment ID. - */ - $subject = apply_filters( 'comment_moderation_subject', $subject, $comment_id ); - - /** * Filters the comment moderation email headers. * * @since 2.8.0 @@ -2038,11 +1966,111 @@ $message_headers = apply_filters( 'comment_moderation_headers', $message_headers, $comment_id ); foreach ( $emails as $email ) { + $user = get_user_by( 'email', $email ); + + if ( $user ) { + $switched_locale = switch_to_user_locale( $user->ID ); + } else { + $switched_locale = switch_to_locale( get_locale() ); + } + + switch ( $comment->comment_type ) { + case 'trackback': + /* translators: %s: Post title. */ + $notify_message = sprintf( __( 'A new trackback on the post "%s" is waiting for your approval' ), $post->post_title ) . "\r\n"; + $notify_message .= get_permalink( $comment->comment_post_ID ) . "\r\n\r\n"; + /* translators: 1: Trackback/pingback website name, 2: Website IP address, 3: Website hostname. */ + $notify_message .= sprintf( __( 'Website: %1$s (IP address: %2$s, %3$s)' ), $comment->comment_author, $comment->comment_author_IP, $comment_author_domain ) . "\r\n"; + /* translators: %s: Trackback/pingback/comment author URL. */ + $notify_message .= sprintf( __( 'URL: %s' ), $comment->comment_author_url ) . "\r\n"; + $notify_message .= __( 'Trackback excerpt: ' ) . "\r\n" . $comment_content . "\r\n\r\n"; + break; + + case 'pingback': + /* translators: %s: Post title. */ + $notify_message = sprintf( __( 'A new pingback on the post "%s" is waiting for your approval' ), $post->post_title ) . "\r\n"; + $notify_message .= get_permalink( $comment->comment_post_ID ) . "\r\n\r\n"; + /* translators: 1: Trackback/pingback website name, 2: Website IP address, 3: Website hostname. */ + $notify_message .= sprintf( __( 'Website: %1$s (IP address: %2$s, %3$s)' ), $comment->comment_author, $comment->comment_author_IP, $comment_author_domain ) . "\r\n"; + /* translators: %s: Trackback/pingback/comment author URL. */ + $notify_message .= sprintf( __( 'URL: %s' ), $comment->comment_author_url ) . "\r\n"; + $notify_message .= __( 'Pingback excerpt: ' ) . "\r\n" . $comment_content . "\r\n\r\n"; + break; + + default: // Comments. + /* translators: %s: Post title. */ + $notify_message = sprintf( __( 'A new comment on the post "%s" is waiting for your approval' ), $post->post_title ) . "\r\n"; + $notify_message .= get_permalink( $comment->comment_post_ID ) . "\r\n\r\n"; + /* translators: 1: Comment author's name, 2: Comment author's IP address, 3: Comment author's hostname. */ + $notify_message .= sprintf( __( 'Author: %1$s (IP address: %2$s, %3$s)' ), $comment->comment_author, $comment->comment_author_IP, $comment_author_domain ) . "\r\n"; + /* translators: %s: Comment author email. */ + $notify_message .= sprintf( __( 'Email: %s' ), $comment->comment_author_email ) . "\r\n"; + /* translators: %s: Trackback/pingback/comment author URL. */ + $notify_message .= sprintf( __( 'URL: %s' ), $comment->comment_author_url ) . "\r\n"; + + if ( $comment->comment_parent ) { + /* translators: Comment moderation. %s: Parent comment edit URL. */ + $notify_message .= sprintf( __( 'In reply to: %s' ), admin_url( "comment.php?action=editcomment&c={$comment->comment_parent}#wpbody-content" ) ) . "\r\n"; + } + + /* translators: %s: Comment text. */ + $notify_message .= sprintf( __( 'Comment: %s' ), "\r\n" . $comment_content ) . "\r\n\r\n"; + break; + } + + /* translators: Comment moderation. %s: Comment action URL. */ + $notify_message .= sprintf( __( 'Approve it: %s' ), admin_url( "comment.php?action=approve&c={$comment_id}#wpbody-content" ) ) . "\r\n"; + + if ( EMPTY_TRASH_DAYS ) { + /* translators: Comment moderation. %s: Comment action URL. */ + $notify_message .= sprintf( __( 'Trash it: %s' ), admin_url( "comment.php?action=trash&c={$comment_id}#wpbody-content" ) ) . "\r\n"; + } else { + /* translators: Comment moderation. %s: Comment action URL. */ + $notify_message .= sprintf( __( 'Delete it: %s' ), admin_url( "comment.php?action=delete&c={$comment_id}#wpbody-content" ) ) . "\r\n"; + } + + /* translators: Comment moderation. %s: Comment action URL. */ + $notify_message .= sprintf( __( 'Spam it: %s' ), admin_url( "comment.php?action=spam&c={$comment_id}#wpbody-content" ) ) . "\r\n"; + + $notify_message .= sprintf( + /* translators: Comment moderation. %s: Number of comments awaiting approval. */ + _n( + 'Currently %s comment is waiting for approval. Please visit the moderation panel:', + 'Currently %s comments are waiting for approval. Please visit the moderation panel:', + $comments_waiting + ), + number_format_i18n( $comments_waiting ) + ) . "\r\n"; + $notify_message .= admin_url( 'edit-comments.php?comment_status=moderated#wpbody-content' ) . "\r\n"; + + /* translators: Comment moderation notification email subject. 1: Site title, 2: Post title. */ + $subject = sprintf( __( '[%1$s] Please moderate: "%2$s"' ), $blogname, $post->post_title ); + + /** + * Filters the comment moderation email text. + * + * @since 1.5.2 + * + * @param string $notify_message Text of the comment moderation email. + * @param int $comment_id Comment ID. + */ + $notify_message = apply_filters( 'comment_moderation_text', $notify_message, $comment_id ); + + /** + * Filters the comment moderation email subject. + * + * @since 1.5.2 + * + * @param string $subject Subject of the comment moderation email. + * @param int $comment_id Comment ID. + */ + $subject = apply_filters( 'comment_moderation_subject', $subject, $comment_id ); + wp_mail( $email, wp_specialchars_decode( $subject ), $notify_message, $message_headers ); - } - - if ( $switched_locale ) { - restore_previous_locale(); + + if ( $switched_locale ) { + restore_previous_locale(); + } } return true; @@ -2063,6 +2091,15 @@ * but check to see if it's the admin whose password we're changing, and skip this. */ if ( 0 !== strcasecmp( $user->user_email, get_option( 'admin_email' ) ) ) { + + $admin_user = get_user_by( 'email', get_option( 'admin_email' ) ); + + if ( $admin_user ) { + $switched_locale = switch_to_user_locale( $admin_user->ID ); + } else { + $switched_locale = switch_to_locale( get_locale() ); + } + /* translators: %s: User name. */ $message = sprintf( __( 'Password changed for user: %s' ), $user->user_login ) . "\r\n"; /* @@ -2103,6 +2140,10 @@ $wp_password_change_notification_email['message'], $wp_password_change_notification_email['headers'] ); + + if ( $switched_locale ) { + restore_previous_locale(); + } } } endif; @@ -2152,7 +2193,14 @@ $send_notification_to_admin = apply_filters( 'wp_send_new_user_notification_to_admin', true, $user ); if ( 'user' !== $notify && true === $send_notification_to_admin ) { - $switched_locale = switch_to_locale( get_locale() ); + + $admin_user = get_user_by( 'email', get_option( 'admin_email' ) ); + + if ( $admin_user ) { + $switched_locale = switch_to_user_locale( $admin_user->ID ); + } else { + $switched_locale = switch_to_locale( get_locale() ); + } /* translators: %s: Site title. */ $message = sprintf( __( 'New user registration on your site %s:' ), $blogname ) . "\r\n\r\n"; @@ -2224,7 +2272,15 @@ /* translators: %s: User login. */ $message = sprintf( __( 'Username: %s' ), $user->user_login ) . "\r\n\r\n"; $message .= __( 'To set your password, visit the following address:' ) . "\r\n\r\n"; - $message .= network_site_url( "wp-login.php?action=rp&key=$key&login=" . rawurlencode( $user->user_login ), 'login' ) . "\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->user_login ) . "&key=$key&action=rp", 'login' ) . "\r\n\r\n"; $message .= wp_login_url() . "\r\n"; @@ -2300,7 +2356,7 @@ /** * Verifies that a correct security nonce was used with time limit. * - * A nonce is valid for 24 hours (by default). + * A nonce is valid for between 12 and 24 hours (by default). * * @since 2.0.3 * @@ -2438,16 +2494,7 @@ static $duplicated_keys; if ( null === $duplicated_keys ) { - $duplicated_keys = array( - 'put your unique phrase here' => true, - ); - - /* - * translators: This string should only be translated if wp-config-sample.php is localized. - * You can check the localized release package or - * https://i18n.svn.wordpress.org//branches//dist/wp-config-sample.php - */ - $duplicated_keys[ __( 'put your unique phrase here' ) ] = true; + $duplicated_keys = array(); foreach ( array( 'AUTH', 'SECURE_AUTH', 'LOGGED_IN', 'NONCE', 'SECRET' ) as $first ) { foreach ( array( 'KEY', 'SALT' ) as $second ) { @@ -2458,6 +2505,50 @@ $duplicated_keys[ $value ] = isset( $duplicated_keys[ $value ] ); } } + + $duplicated_keys['put your unique phrase here'] = true; + + /* + * translators: This string should only be translated if wp-config-sample.php is localized. + * You can check the localized release package or + * https://i18n.svn.wordpress.org//branches//dist/wp-config-sample.php + */ + $duplicated_keys[ __( 'put your unique phrase here' ) ] = true; + } + + /* + * Determine which options to prime. + * + * If the salt keys are undefined, use a duplicate value or the + * default `put your unique phrase here` value the salt will be + * generated via `wp_generate_password()` and stored as a site + * option. These options will be primed to avoid repeated + * database requests for undefined salts. + */ + $options_to_prime = array(); + foreach ( array( 'auth', 'secure_auth', 'logged_in', 'nonce' ) as $key ) { + foreach ( array( 'key', 'salt' ) as $second ) { + $const = strtoupper( "{$key}_{$second}" ); + if ( ! defined( $const ) || true === $duplicated_keys[ constant( $const ) ] ) { + $options_to_prime[] = "{$key}_{$second}"; + } + } + } + + if ( ! empty( $options_to_prime ) ) { + /* + * Also prime `secret_key` used for undefined salting schemes. + * + * If the scheme is unknown, the default value for `secret_key` will be + * used too for the salt. This should rarely happen, so the option is only + * primed if other salts are undefined. + * + * At this point of execution it is known that a database call will be made + * to prime salts, so the `secret_key` option can be primed regardless of the + * constants status. + */ + $options_to_prime[] = 'secret_key'; + wp_prime_site_option_caches( $options_to_prime ); } $values = array( @@ -2504,110 +2595,246 @@ if ( ! function_exists( 'wp_hash' ) ) : /** - * Gets hash of given string. + * Gets the hash of the given string. + * + * The default algorithm is md5 but can be changed to any algorithm supported by + * `hash_hmac()`. Use the `hash_hmac_algos()` function to check the supported + * algorithms. * * @since 2.0.3 + * @since 6.8.0 The `$algo` parameter was added. + * + * @throws InvalidArgumentException if the hashing algorithm is not supported. * * @param string $data Plain text to hash. * @param string $scheme Authentication scheme (auth, secure_auth, logged_in, nonce). + * @param string $algo Hashing algorithm to use. Default: 'md5'. * @return string Hash of $data. */ - function wp_hash( $data, $scheme = 'auth' ) { + function wp_hash( $data, $scheme = 'auth', $algo = 'md5' ) { $salt = wp_salt( $scheme ); - return hash_hmac( 'md5', $data, $salt ); + // Ensure the algorithm is supported by the hash_hmac function. + if ( ! in_array( $algo, hash_hmac_algos(), true ) ) { + throw new InvalidArgumentException( + sprintf( + /* translators: 1: Name of a cryptographic hash algorithm. 2: List of supported algorithms. */ + __( 'Unsupported hashing algorithm: %1$s. Supported algorithms are: %2$s' ), + $algo, + implode( ', ', hash_hmac_algos() ) + ) + ); + } + + return hash_hmac( $algo, $data, $salt ); } endif; if ( ! function_exists( 'wp_hash_password' ) ) : /** - * Creates a hash (encrypt) of a plain text password. + * Creates a hash of a plain text password. * * For integration with other applications, this function can be overwritten to - * instead use the other package password checking algorithm. + * instead use the other package password hashing algorithm. * * @since 2.5.0 + * @since 6.8.0 The password is now hashed using bcrypt by default instead of phpass. * - * @global PasswordHash $wp_hasher PHPass object. + * @global PasswordHash $wp_hasher phpass object. * * @param string $password Plain text user password to hash. * @return string The hash string of the password. */ - function wp_hash_password( $password ) { + function wp_hash_password( + #[\SensitiveParameter] + $password + ) { global $wp_hasher; - if ( empty( $wp_hasher ) ) { - require_once ABSPATH . WPINC . '/class-phpass.php'; - // By default, use the portable hash from phpass. - $wp_hasher = new PasswordHash( 8, true ); + if ( ! empty( $wp_hasher ) ) { + return $wp_hasher->HashPassword( trim( $password ) ); + } + + if ( strlen( $password ) > 4096 ) { + return '*'; } - return $wp_hasher->HashPassword( trim( $password ) ); + /** + * Filters the hashing algorithm to use in the password_hash() and password_needs_rehash() functions. + * + * The default is the value of the `PASSWORD_BCRYPT` constant which means bcrypt is used. + * + * **Important:** The only password hashing algorithm that is guaranteed to be available across PHP + * installations is bcrypt. If you use any other algorithm you must make sure that it is available on + * the server. The `password_algos()` function can be used to check which hashing algorithms are available. + * + * The hashing options can be controlled via the {@see 'wp_hash_password_options'} filter. + * + * Other available constants include: + * + * - `PASSWORD_ARGON2I` + * - `PASSWORD_ARGON2ID` + * - `PASSWORD_DEFAULT` + * + * @since 6.8.0 + * + * @param string $algorithm The hashing algorithm. Default is the value of the `PASSWORD_BCRYPT` constant. + */ + $algorithm = apply_filters( 'wp_hash_password_algorithm', PASSWORD_BCRYPT ); + + /** + * Filters the options passed to the password_hash() and password_needs_rehash() functions. + * + * The default hashing algorithm is bcrypt, but this can be changed via the {@see 'wp_hash_password_algorithm'} + * filter. You must ensure that the options are appropriate for the algorithm in use. + * + * @since 6.8.0 + * + * @param array $options Array of options to pass to the password hashing functions. + * By default this is an empty array which means the default + * options will be used. + * @param string $algorithm The hashing algorithm in use. + */ + $options = apply_filters( 'wp_hash_password_options', array(), $algorithm ); + + // Algorithms other than bcrypt don't need to use pre-hashing. + if ( PASSWORD_BCRYPT !== $algorithm ) { + return password_hash( $password, $algorithm, $options ); + } + + // Use SHA-384 to retain entropy from a password that's longer than 72 bytes, and a `wp-sha384` key for domain separation. + $password_to_hash = base64_encode( hash_hmac( 'sha384', trim( $password ), 'wp-sha384', true ) ); + + // Add a prefix to facilitate distinguishing vanilla bcrypt hashes. + return '$wp' . password_hash( $password_to_hash, $algorithm, $options ); } endif; if ( ! function_exists( 'wp_check_password' ) ) : /** - * Checks the plaintext password against the encrypted Password. + * Checks a plaintext password against a hashed password. * - * Maintains compatibility between old version and the new cookie authentication - * protocol using PHPass library. The $hash parameter is the encrypted password - * and the function compares the plain text password when encrypted similarly - * against the already encrypted password to see if they match. + * Note that this function may be used to check a value that is not a user password. + * A plugin may use this function to check a password of a different type, and there + * may not always be a user ID associated with the password. * * For integration with other applications, this function can be overwritten to - * instead use the other package password checking algorithm. + * instead use the other package password hashing algorithm. * * @since 2.5.0 + * @since 6.8.0 Passwords in WordPress are now hashed with bcrypt by default. A + * password that wasn't hashed with bcrypt will be checked with phpass. * - * @global PasswordHash $wp_hasher PHPass object used for checking the password - * against the $hash + $password. - * @uses PasswordHash::CheckPassword + * @global PasswordHash $wp_hasher phpass object. Used as a fallback for verifying + * passwords that were hashed with phpass. * - * @param string $password Plaintext user's password. - * @param string $hash Hash of the user's password to check against. - * @param string|int $user_id Optional. User ID. + * @param string $password Plaintext password. + * @param string $hash Hash of the password to check against. + * @param string|int $user_id Optional. ID of a user associated with the password. * @return bool False, if the $password does not match the hashed password. */ - function wp_check_password( $password, $hash, $user_id = '' ) { + function wp_check_password( + #[\SensitiveParameter] + $password, + $hash, + $user_id = '' + ) { global $wp_hasher; - // If the hash is still md5... if ( strlen( $hash ) <= 32 ) { + // Check the hash using md5 regardless of the current hashing mechanism. $check = hash_equals( $hash, md5( $password ) ); - if ( $check && $user_id ) { - // Rehash using new hash. - wp_set_password( $password, $user_id ); - $hash = wp_hash_password( $password ); - } - - /** - * Filters whether the plaintext password matches the encrypted password. - * - * @since 2.5.0 - * - * @param bool $check Whether the passwords match. - * @param string $password The plaintext password. - * @param string $hash The hashed password. - * @param string|int $user_id User ID. Can be empty. - */ - return apply_filters( 'check_password', $check, $password, $hash, $user_id ); + } elseif ( ! empty( $wp_hasher ) ) { + // Check the password using the overridden hasher. + $check = $wp_hasher->CheckPassword( $password, $hash ); + } elseif ( strlen( $password ) > 4096 ) { + // Passwords longer than 4096 characters are not supported. + $check = false; + } elseif ( str_starts_with( $hash, '$wp' ) ) { + // Check the password using the current prefixed hash. + $password_to_verify = base64_encode( hash_hmac( 'sha384', $password, 'wp-sha384', true ) ); + $check = password_verify( $password_to_verify, substr( $hash, 3 ) ); + } elseif ( str_starts_with( $hash, '$P$' ) ) { + // Check the password using phpass. + require_once ABSPATH . WPINC . '/class-phpass.php'; + $check = ( new PasswordHash( 8, true ) )->CheckPassword( $password, $hash ); + } else { + // Check the password using compat support for any non-prefixed hash. + $check = password_verify( $password, $hash ); } - /* - * If the stored hash is longer than an MD5, - * presume the new style phpass portable hash. + /** + * Filters whether the plaintext password matches the hashed password. + * + * @since 2.5.0 + * @since 6.8.0 Passwords are now hashed with bcrypt by default. + * Old passwords may still be hashed with phpass or md5. + * + * @param bool $check Whether the passwords match. + * @param string $password The plaintext password. + * @param string $hash The hashed password. + * @param string|int $user_id Optional ID of a user associated with the password. + * Can be empty. */ - if ( empty( $wp_hasher ) ) { - require_once ABSPATH . WPINC . '/class-phpass.php'; - // By default, use the portable hash from phpass. - $wp_hasher = new PasswordHash( 8, true ); + return apply_filters( 'check_password', $check, $password, $hash, $user_id ); + } +endif; + +if ( ! function_exists( 'wp_password_needs_rehash' ) ) : + /** + * Checks whether a password hash needs to be rehashed. + * + * Passwords are hashed with bcrypt using the default cost. A password hashed in a prior version + * of WordPress may still be hashed with phpass and will need to be rehashed. If the default cost + * or algorithm is changed in PHP or WordPress then a password hashed in a previous version will + * need to be rehashed. + * + * Note that, just like wp_check_password(), this function may be used to check a value that is + * not a user password. A plugin may use this function to check a password of a different type, + * and there may not always be a user ID associated with the password. + * + * @since 6.8.0 + * + * @global PasswordHash $wp_hasher phpass object. + * + * @param string $hash Hash of a password to check. + * @param string|int $user_id Optional. ID of a user associated with the password. + * @return bool Whether the hash needs to be rehashed. + */ + function wp_password_needs_rehash( $hash, $user_id = '' ) { + global $wp_hasher; + + if ( ! empty( $wp_hasher ) ) { + return false; } - $check = $wp_hasher->CheckPassword( $password, $hash ); - /** This filter is documented in wp-includes/pluggable.php */ - return apply_filters( 'check_password', $check, $password, $hash, $user_id ); + $algorithm = apply_filters( 'wp_hash_password_algorithm', PASSWORD_BCRYPT ); + + /** This filter is documented in wp-includes/pluggable.php */ + $options = apply_filters( 'wp_hash_password_options', array(), $algorithm ); + + $prefixed = str_starts_with( $hash, '$wp' ); + + if ( ( PASSWORD_BCRYPT === $algorithm ) && ! $prefixed ) { + // If bcrypt is in use and the hash is not prefixed then it needs to be rehashed. + $needs_rehash = true; + } else { + // Otherwise check the hash minus its prefix if necessary. + $hash_to_check = $prefixed ? substr( $hash, 3 ) : $hash; + $needs_rehash = password_needs_rehash( $hash_to_check, $algorithm, $options ); + } + + /** + * Filters whether the password hash needs to be rehashed. + * + * @since 6.8.0 + * + * @param bool $needs_rehash Whether the password hash needs to be rehashed. + * @param string $hash The password hash. + * @param string|int $user_id Optional. ID of a user associated with the password. + */ + return apply_filters( 'password_needs_rehash', $needs_rehash, $hash, $user_id ); } endif; @@ -2749,7 +2976,7 @@ if ( ! function_exists( 'wp_set_password' ) ) : /** - * Updates the user's password with a new encrypted one. + * Updates the user's password with a new hashed one. * * For integration with other applications, this function can be overwritten to * instead use the other package password checking algorithm. @@ -2759,15 +2986,22 @@ * of password resets if precautions are not taken to ensure it does not execute on every page load. * * @since 2.5.0 + * @since 6.8.0 The password is now hashed using bcrypt by default instead of phpass. * * @global wpdb $wpdb WordPress database abstraction object. * * @param string $password The plaintext new user password. * @param int $user_id User ID. */ - function wp_set_password( $password, $user_id ) { + function wp_set_password( + #[\SensitiveParameter] + $password, + $user_id + ) { global $wpdb; + $old_user_data = get_userdata( $user_id ); + $hash = wp_hash_password( $password ); $wpdb->update( $wpdb->users, @@ -2784,11 +3018,13 @@ * Fires after the user password is set. * * @since 6.2.0 + * @since 6.7.0 The `$old_user_data` parameter was added. * - * @param string $password The plaintext password just set. - * @param int $user_id The ID of the user whose password was just set. + * @param string $password The plaintext password just set. + * @param int $user_id The ID of the user whose password was just set. + * @param WP_User $old_user_data Object containing user's data prior to update. */ - do_action( 'wp_set_password', $password, $user_id ); + do_action( 'wp_set_password', $password, $user_id, $old_user_data ); } endif;