--- 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/<locale code>/branches/<wp version>/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/<locale code>/branches/<wp version>/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;