Skip to content

Commit 02cadfa

Browse files
committed
Revert [62482]: Add support for unicode email addresses.
This reverts [62482] based on this decision: > Because of the vast surface area Unicode introduces and the security implications we should keep core as it is, and put this work into a community plugin… - https://make.wordpress.org/core/2026/05/22/extending-unicode-support-in-email-addresses-usernames-and-slugs/#comment-49045 Developed in WordPress#12637. Follow-up to [62482]. See #31992. git-svn-id: https://develop.svn.wordpress.org/trunk@62829 602fd350-edb4-49c9-b593-d223f7449a82
1 parent 1f15812 commit 02cadfa

11 files changed

Lines changed: 175 additions & 836 deletions

File tree

src/wp-includes/class-wp-email-address.php

Lines changed: 0 additions & 405 deletions
This file was deleted.

src/wp-includes/default-filters.php

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -87,17 +87,6 @@
8787
add_filter( $filter, 'wp_filter_kses' );
8888
}
8989

90-
// Email addresses: Allow unicode if and only if as the database can
91-
// store them. This affects all addresses, including those entered
92-
// into contact forms.
93-
if ( 'utf8mb4' === $wpdb->charset ) {
94-
add_filter( 'is_email', 'wp_is_unicode_email', 10, 3 );
95-
add_filter( 'sanitize_email', 'wp_sanitize_unicode_email', 10, 3 );
96-
} else {
97-
add_filter( 'is_email', 'wp_is_ascii_email', 10, 3 );
98-
add_filter( 'sanitize_email', 'wp_sanitize_ascii_email', 10, 3 );
99-
}
100-
10190
// Display URL.
10291
foreach ( array( 'user_url', 'link_url', 'link_image', 'link_rss', 'comment_url', 'post_guid' ) as $filter ) {
10392
if ( is_admin() ) {

src/wp-includes/formatting.php

Lines changed: 160 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -2176,7 +2176,6 @@ function sanitize_user( $username, $strict = false ) {
21762176
return apply_filters( 'sanitize_user', $username, $raw_username, $strict );
21772177
}
21782178

2179-
21802179
/**
21812180
* Sanitizes a string key.
21822181
*
@@ -3602,14 +3601,7 @@ function convert_smilies( $text ) {
36023601
/**
36033602
* Verifies that an email is valid.
36043603
*
3605-
* This accepts the addresses that matches the WHATWG specifications,
3606-
* i.e. what browsers use for `<input type=email>`. It also accepts some
3607-
* additional addresses.
3608-
*
3609-
* By default this accepts addresses like info@grå.org (also accepted
3610-
* by Firefox) `<input type=email>`. You can disable Unicode support by
3611-
* using the wp_is_ascii_email filter instead of wp_is_unicode_email,
3612-
* which is the default.
3604+
* Does not grok i18n domains. Not RFC compliant.
36133605
*
36143606
* @since 0.71
36153607
*
@@ -3622,65 +3614,84 @@ function is_email( $email, $deprecated = false ) {
36223614
_deprecated_argument( __FUNCTION__, '3.0.0' );
36233615
}
36243616

3625-
/**
3626-
* Filters whether an email address is valid.
3627-
*
3628-
* This filter is evaluated under several different contexts, such as
3629-
* 'local_invalid_chars', 'domain_no_periods', or no specific context.
3630-
* Filters registered on this hook perform the actual validation; the
3631-
* default filter is registered in default-filters.php.
3632-
*
3633-
* @since 2.8.0
3634-
*
3635-
* @param string|false $is_email The email address if successfully passed the is_email() checks, false otherwise.
3636-
* @param string $email The email address being checked.
3637-
* @param string|null $context Context under which the email was tested, or null for the initial call.
3617+
// Test for the minimum length the email can be.
3618+
if ( strlen( $email ) < 6 ) {
3619+
/**
3620+
* Filters whether an email address is valid.
3621+
*
3622+
* This filter is evaluated under several different contexts, such as 'email_too_short',
3623+
* 'email_no_at', 'local_invalid_chars', 'domain_period_sequence', 'domain_period_limits',
3624+
* 'domain_no_periods', 'sub_hyphen_limits', 'sub_invalid_chars', or no specific context.
3625+
*
3626+
* @since 2.8.0
3627+
*
3628+
* @param string|false $is_email The email address if successfully passed the is_email() checks, false otherwise.
3629+
* @param string $email The email address being checked.
3630+
* @param string $context Context under which the email was tested.
3631+
*/
3632+
return apply_filters( 'is_email', false, $email, 'email_too_short' );
3633+
}
3634+
3635+
// Test for an @ character after the first position.
3636+
if ( false === strpos( $email, '@', 1 ) ) {
3637+
/** This filter is documented in wp-includes/formatting.php */
3638+
return apply_filters( 'is_email', false, $email, 'email_no_at' );
3639+
}
3640+
3641+
// Split out the local and domain parts.
3642+
list( $local, $domain ) = explode( '@', $email, 2 );
3643+
3644+
/*
3645+
* LOCAL PART
3646+
* Test for invalid characters.
36383647
*/
3639-
return apply_filters( 'is_email', false, $email, null );
3640-
}
3648+
if ( ! preg_match( '/^[a-zA-Z0-9!#$%&\'*+\/=?^_`{|}~\.-]+$/', $local ) ) {
3649+
/** This filter is documented in wp-includes/formatting.php */
3650+
return apply_filters( 'is_email', false, $email, 'local_invalid_chars' );
3651+
}
36413652

3642-
/**
3643-
* Default is_email filter for databases that support Unicode (db charset is utf8mb4).
3644-
*
3645-
* Validates the email address using {@see WP_Email_Address::from_string()} with Unicode enabled.
3646-
* Only acts when $context is null (which it is in the initial validation call); later rescue-context calls are passed through.
3647-
*
3648-
* @since 7.1.0
3649-
*
3650-
* @param string|false $value The current filter value.
3651-
* @param string $email The email address being checked.
3652-
* @param string|null $context Validation context, or null for the initial call.
3653-
* @return string|false The email address if valid, false otherwise.
3654-
*/
3655-
function wp_is_unicode_email( $value, $email, $context ) {
3656-
if ( null !== $context ) {
3657-
return $value;
3653+
/*
3654+
* DOMAIN PART
3655+
* Test for sequences of periods.
3656+
*/
3657+
if ( preg_match( '/\.{2,}/', $domain ) ) {
3658+
/** This filter is documented in wp-includes/formatting.php */
3659+
return apply_filters( 'is_email', false, $email, 'domain_period_sequence' );
36583660
}
36593661

3660-
$result = WP_Email_Address::from_string( $email, 'unicode' );
3661-
return $result ? $result->get_unicode_address() : false;
3662-
}
3662+
// Test for leading and trailing periods and whitespace.
3663+
if ( trim( $domain, " \t\n\r\0\x0B." ) !== $domain ) {
3664+
/** This filter is documented in wp-includes/formatting.php */
3665+
return apply_filters( 'is_email', false, $email, 'domain_period_limits' );
3666+
}
36633667

3664-
/**
3665-
* Default is_email filter for databases that do not support Unicode (db charset is not utf8mb4).
3666-
*
3667-
* Validates the email address using {@see WP_Email_Address::from_string()} with Unicode disabled.
3668-
* Only acts when $context is null (which it is in the initial validation call); later rescue-context calls are passed through.
3669-
*
3670-
* @since 7.1.0
3671-
*
3672-
* @param string|false $value The current filter value.
3673-
* @param string $email The email address being checked.
3674-
* @param string|null $context Validation context, or null for the initial call.
3675-
* @return string|false The email address if valid, false otherwise.
3676-
*/
3677-
function wp_is_ascii_email( $value, $email, $context ) {
3678-
if ( null !== $context ) {
3679-
return $value;
3668+
// Split the domain into subs.
3669+
$subs = explode( '.', $domain );
3670+
3671+
// Assume the domain will have at least two subs.
3672+
if ( 2 > count( $subs ) ) {
3673+
/** This filter is documented in wp-includes/formatting.php */
3674+
return apply_filters( 'is_email', false, $email, 'domain_no_periods' );
3675+
}
3676+
3677+
// Loop through each sub.
3678+
foreach ( $subs as $sub ) {
3679+
// Test for leading and trailing hyphens and whitespace.
3680+
if ( trim( $sub, " \t\n\r\0\x0B-" ) !== $sub ) {
3681+
/** This filter is documented in wp-includes/formatting.php */
3682+
return apply_filters( 'is_email', false, $email, 'sub_hyphen_limits' );
3683+
}
3684+
3685+
// Test for invalid characters.
3686+
if ( ! preg_match( '/^[a-z0-9-]+$/i', $sub ) ) {
3687+
/** This filter is documented in wp-includes/formatting.php */
3688+
return apply_filters( 'is_email', false, $email, 'sub_invalid_chars' );
3689+
}
36803690
}
36813691

3682-
$result = WP_Email_Address::from_string( $email, 'ascii' );
3683-
return $result ? $result->get_unicode_address() : false;
3692+
// Congratulations, your email made it!
3693+
/** This filter is documented in wp-includes/formatting.php */
3694+
return apply_filters( 'is_email', $email, $email, null );
36843695
}
36853696

36863697
/**
@@ -3809,96 +3820,109 @@ function iso8601_to_datetime( $date_string, $timezone = 'user' ) {
38093820
}
38103821

38113822
/**
3812-
* Sanitizes an email address.
3813-
*
3814-
* Strips stray whitespace from the input, then strips trailing dots from the domain.
3815-
* This is designed to recover from cut/paste mistakes without any risk of transforming
3816-
* the input into a different address than the user intended.
3817-
*
3818-
* Validation and final form are determined by the 'sanitize_email' filter; the default
3819-
* filter is registered in default-filters.php and delegates to {@see WP_Email_Address::from_string()}.
3823+
* Strips out all characters that are not allowable in an email.
38203824
*
38213825
* @since 1.5.0
3822-
* @since 7.1.0 Accepts Unicode email addresses on supporting platforms.
38233826
*
3824-
* @param string $email Email address to sanitize.
3825-
* @return string The sanitized email address, or an empty string if invalid.
3827+
* @param string $email Email address to filter.
3828+
* @return string Filtered email address.
38263829
*/
38273830
function sanitize_email( $email ) {
3828-
// Strip surrounding whitespace.
3829-
$email = trim( $email );
3831+
// Test for the minimum length the email can be.
3832+
if ( strlen( $email ) < 6 ) {
3833+
/**
3834+
* Filters a sanitized email address.
3835+
*
3836+
* This filter is evaluated under several contexts, including 'email_too_short',
3837+
* 'email_no_at', 'local_invalid_chars', 'domain_period_sequence', 'domain_period_limits',
3838+
* 'domain_no_periods', 'domain_no_valid_subs', or no context.
3839+
*
3840+
* @since 2.8.0
3841+
*
3842+
* @param string $sanitized_email The sanitized email address.
3843+
* @param string $email The email address, as provided to sanitize_email().
3844+
* @param string|null $message A message to pass to the user. null if email is sanitized.
3845+
*/
3846+
return apply_filters( 'sanitize_email', '', $email, 'email_too_short' );
3847+
}
38303848

3831-
// Extract the address from "Display Name <username@domain>" format.
3832-
if ( 1 === preg_match( '/<([^>]+)>$/', $email, $matches ) ) {
3833-
$email = $matches[1];
3849+
// Test for an @ character after the first position.
3850+
if ( false === strpos( $email, '@', 1 ) ) {
3851+
/** This filter is documented in wp-includes/formatting.php */
3852+
return apply_filters( 'sanitize_email', '', $email, 'email_no_at' );
38343853
}
38353854

3855+
// Split out the local and domain parts.
3856+
list( $local, $domain ) = explode( '@', $email, 2 );
3857+
38363858
/*
3837-
* Strip soft hyphens and whitespace adjacent to structural separators (dots and @),
3838-
* e.g. copy-paste artifacts like "info@example\u{00AD}.com" or "info@example .com".
3839-
*
3840-
* In some cases, e.g. autocorrect, some older software has been seen to add the
3841-
* space for unrecognized TLDs. This re-joins the parts for proper examination.
3859+
* LOCAL PART
3860+
* Test for invalid characters.
38423861
*/
3843-
$email = preg_replace( '/[\x{00AD}\s]*([.@])[\x{00AD}\s]*/u', '$1', $email ) ?? $email;
3844-
3845-
// Strip a trailing dot from the domain (e.g. if pasted from the end of a sentence).
3846-
if ( str_contains( $email, '@' ) ) {
3847-
list( $local, $domain ) = explode( '@', $email, 2 );
3848-
$domain = rtrim( $domain, '.' );
3849-
$email = $local . '@' . $domain;
3862+
$local = preg_replace( '/[^a-zA-Z0-9!#$%&\'*+\/=?^_`{|}~\.-]/', '', $local );
3863+
if ( '' === $local ) {
3864+
/** This filter is documented in wp-includes/formatting.php */
3865+
return apply_filters( 'sanitize_email', '', $email, 'local_invalid_chars' );
38503866
}
38513867

3852-
/**
3853-
* Filters a sanitized email address.
3854-
*
3855-
* Filters registered on this hook perform the actual validation and return
3856-
* the canonical email string on success or an empty string on failure.
3857-
* The default filter is registered in default-filters.php.
3858-
*
3859-
* @since 2.8.0
3860-
*
3861-
* @param string $sanitized_email The sanitized email address, or empty string.
3862-
* @param string $email The email address as provided to sanitize_email().
3863-
* @param string|null $context Validation context, or null for the initial call.
3868+
/*
3869+
* DOMAIN PART
3870+
* Test for sequences of periods.
38643871
*/
3865-
return apply_filters( 'sanitize_email', '', $email, null );
3866-
}
3872+
$domain = preg_replace( '/\.{2,}/', '', $domain );
3873+
if ( '' === $domain ) {
3874+
/** This filter is documented in wp-includes/formatting.php */
3875+
return apply_filters( 'sanitize_email', '', $email, 'domain_period_sequence' );
3876+
}
38673877

3868-
/**
3869-
* Default sanitize_email filter for databases that support Unicode (db charset is utf8mb4).
3870-
*
3871-
* Returns the canonical address from {@see WP_Email_Address::from_string()} with Unicode
3872-
* enabled, or an empty string if the address is invalid.
3873-
*
3874-
* @since 7.1.0
3875-
*
3876-
* @param string $value The current filter value.
3877-
* @param string $email The email address being sanitized.
3878-
* @param string|null $context Sanitization context, always null.
3879-
* @return string The canonical email address if valid, empty string otherwise.
3880-
*/
3881-
function wp_sanitize_unicode_email( $value, $email, $context ) {
3882-
$result = WP_Email_Address::from_string( $email, 'unicode' );
3883-
return $result ? $result->get_unicode_address() : '';
3884-
}
3878+
// Test for leading and trailing periods and whitespace.
3879+
$domain = trim( $domain, " \t\n\r\0\x0B." );
3880+
if ( '' === $domain ) {
3881+
/** This filter is documented in wp-includes/formatting.php */
3882+
return apply_filters( 'sanitize_email', '', $email, 'domain_period_limits' );
3883+
}
38853884

3886-
/**
3887-
* Default sanitize_email filter for databases that do not support Unicode (db charset is not utf8mb4).
3888-
*
3889-
* Returns the canonical address from {@see WP_Email_Address::from_string()} with Unicode
3890-
* disabled, or an empty string if the address is invalid.
3891-
*
3892-
* @since 7.1.0
3893-
*
3894-
* @param string $value The current filter value.
3895-
* @param string $email The email address being sanitized.
3896-
* @param string|null $context Sanitization context, always null.
3897-
* @return string The canonical email address if valid, empty string otherwise.
3898-
*/
3899-
function wp_sanitize_ascii_email( $value, $email, $context ) {
3900-
$result = WP_Email_Address::from_string( $email, 'ascii' );
3901-
return $result ? $result->get_unicode_address() : '';
3885+
// Split the domain into subs.
3886+
$subs = explode( '.', $domain );
3887+
3888+
// Assume the domain will have at least two subs.
3889+
if ( 2 > count( $subs ) ) {
3890+
/** This filter is documented in wp-includes/formatting.php */
3891+
return apply_filters( 'sanitize_email', '', $email, 'domain_no_periods' );
3892+
}
3893+
3894+
// Create an array that will contain valid subs.
3895+
$new_subs = array();
3896+
3897+
// Loop through each sub.
3898+
foreach ( $subs as $sub ) {
3899+
// Test for leading and trailing hyphens.
3900+
$sub = trim( $sub, " \t\n\r\0\x0B-" );
3901+
3902+
// Test for invalid characters.
3903+
$sub = preg_replace( '/[^a-z0-9-]+/i', '', $sub );
3904+
3905+
// If there's anything left, add it to the valid subs.
3906+
if ( '' !== $sub ) {
3907+
$new_subs[] = $sub;
3908+
}
3909+
}
3910+
3911+
// If there aren't 2 or more valid subs.
3912+
if ( 2 > count( $new_subs ) ) {
3913+
/** This filter is documented in wp-includes/formatting.php */
3914+
return apply_filters( 'sanitize_email', '', $email, 'domain_no_valid_subs' );
3915+
}
3916+
3917+
// Join valid subs into the new domain.
3918+
$domain = implode( '.', $new_subs );
3919+
3920+
// Put the email back together.
3921+
$sanitized_email = $local . '@' . $domain;
3922+
3923+
// Congratulations, your email made it!
3924+
/** This filter is documented in wp-includes/formatting.php */
3925+
return apply_filters( 'sanitize_email', $sanitized_email, $email, null );
39023926
}
39033927

39043928
/**

src/wp-settings.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,6 @@
112112
require ABSPATH . WPINC . '/class-wp-list-util.php';
113113
require ABSPATH . WPINC . '/class-wp-token-map.php';
114114
require ABSPATH . WPINC . '/utf8.php';
115-
require ABSPATH . WPINC . '/class-wp-email-address.php';
116115
require ABSPATH . WPINC . '/formatting.php';
117116
require ABSPATH . WPINC . '/meta.php';
118117
require ABSPATH . WPINC . '/functions.php';

tests/phpunit/tests/auth.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1520,7 +1520,7 @@ public function test_wp_authenticate_cookie_with_invalid_cookie() {
15201520
*/
15211521
public function test_wp_signon_using_email_with_an_apostrophe() {
15221522
$user_args = array(
1523-
'user_email' => "mail'@example.com",
1523+
'user_email' => "mail\'@example.com",
15241524
'user_pass' => 'password',
15251525
);
15261526
self::factory()->user->create( $user_args );
@@ -1833,7 +1833,7 @@ static function ( $available, WP_User $user ) {
18331833
*/
18341834
public function test_reset_password_with_apostrophe_in_email() {
18351835
$user_args = array(
1836-
'user_email' => "jo\'hn@example.com",
1836+
'user_email' => "jo'hn@example.com",
18371837
'user_pass' => 'password',
18381838
);
18391839

tests/phpunit/tests/formatting/antispambot.php

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,6 @@ public function data_returns_valid_utf8() {
3434
'plain with ip' => array( 'ace@204.32.222.14' ),
3535
'deep subdomain' => array( 'kevin@many.subdomains.make.a.happy.man.edu' ),
3636
'short address' => array( 'a@b.co' ),
37-
'ascii@nonascii' => array( 'info@grå.org' ),
38-
'nonascii@nonascii' => array( 'grå@grå.org' ),
39-
'decomposed unicode' => array( "gr\u{0061}\u{030a}blå@grå.org" ),
4037
'weird but legal dots' => array( '..@example.com' ),
4138
'umlauts' => array( 'bücher@gmx.de' ),
4239
'three-byte UTF-8' => array( "\u{FFFD}@who.knows.com" ),

0 commit comments

Comments
 (0)