Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 1 addition & 11 deletions src/wp-includes/default-filters.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,17 +87,6 @@
add_filter( $filter, 'wp_filter_kses' );
}

// Email addresses: Allow unicode if and only if as the database can
// store them. This affects all addresses, including those entered
// into contact forms.
if ( 'utf8mb4' === $wpdb->charset ) {
add_filter( 'is_email', 'wp_is_unicode_email', 10, 3 );
add_filter( 'sanitize_email', 'wp_sanitize_unicode_email', 10, 3 );
} else {
add_filter( 'is_email', 'wp_is_ascii_email', 10, 3 );
add_filter( 'sanitize_email', 'wp_sanitize_ascii_email', 10, 3 );
}

// Display URL.
foreach ( array( 'user_url', 'link_url', 'link_image', 'link_rss', 'comment_url', 'post_guid' ) as $filter ) {
if ( is_admin() ) {
Expand Down Expand Up @@ -282,6 +271,7 @@
add_filter( 'the_guid', 'esc_url' );

// Email filters.
add_action( 'plugins_loaded', 'wp_select_email_address_support' );
add_filter( 'wp_mail', 'wp_staticize_emoji_for_email' );

// Robots filters.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,14 @@ final class WP_Email_Address {
*/
const DOMAIN_UNICODE_REGEX = '/^' . self::DOMAIN_LABEL_UNICODE . '(?:\.' . self::DOMAIN_LABEL_UNICODE . ')*$/u';

/**
* The last email address parsing error, if any.
*
* @since 7.1.0
* @var ?string
*/
private static $last_error = null;

/**
* The local part of the email address (the portion before the '@').
*
Expand Down Expand Up @@ -198,16 +206,21 @@ private function __construct( string $localpart, string $ascii_domain, ?string $
* Note! If an address contains punycode encodings but the required {@see idn_to_utf8()}
* function is missing (from the `intl` extension), this will reject that email address.
*
* @see self::get_last_error() for why a provided address was rejected.
*
* @since 7.1.0
*
* @param string $input The email address string to parse.
* @param 'ascii'|'unicode' $character_set Allow only ASCII addresses or all valid Unicode addresses.
* @return WP_Email_Address|null A WP_Email_Address instance, or null if the input fails to validate.
*/
public static function from_string( string $input, string $character_set = 'unicode' ): ?WP_Email_Address {
self::$last_error = null;

// There must be exactly one '@' sign.
$at_pos = strpos( $input, '@' );
if ( false === $at_pos || strrpos( $input, '@' ) !== $at_pos ) {
self::$last_error = 'email_no_at';
return null;
}

Expand All @@ -221,6 +234,7 @@ public static function from_string( string $input, string $character_set = 'unic
foreach ( $domain_labels as $label ) {
// DNS limits each label to 63 octets.
if ( strlen( $label ) > 63 ) {
self::$last_error = 'domain_label_too_long';
return null;
}
}
Expand All @@ -233,6 +247,7 @@ public static function from_string( string $input, string $character_set = 'unic
*/
$needs_decoding = 1 === preg_match( '/(?:^|\.)..--/', $ascii_domain );
if ( $needs_decoding && ! function_exists( 'idn_to_utf8' ) ) {
self::$last_error = 'wp_unsupported';
return null;
}

Expand All @@ -247,10 +262,12 @@ public static function from_string( string $input, string $character_set = 'unic
if ( str_starts_with( $label, 'xn--' ) ) {
$label = idn_to_utf8( $label, IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46 );
if ( false === $label ) {
self::$last_error = 'domain_invalid_idn';
return null;
}
} elseif ( 1 === preg_match( '/^..--/', $label ) ) {
// Reject labels with a reserved ACE-like prefix (two chars followed by '--').
self::$last_error = 'domain_invalid_chars';
return null;
}
$decoded_labels[] = $label;
Expand Down Expand Up @@ -298,9 +315,24 @@ public static function from_string( string $input, string $character_set = 'unic

// Validate the domain against the allowed structure.
if ( 1 !== preg_match( $domain_pattern, $decoded_domain ) ) {
self::$last_error = 'domaind_invalid_sequence';
return null;
}

// Renormalize the ASCII domain when non-ASCII characters exist
if ( 1 === preg_match( '~[^\x00-\x7F]~', $ascii_domain ) ) {
if ( ! function_exists( 'idn_to_ascii' ) ) {
self::$last_error = 'wp_unsupported';
return null;
}

$labels = explode( '.', $ascii_domain );
foreach ( $labels as &$label ) {
$label = idn_to_ascii( $label, IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46 );
}
$ascii_domain = implode( '.', $labels );
}

return new self( $localpart, $ascii_domain, $decoded_domain );
}

Expand Down Expand Up @@ -402,4 +434,18 @@ public function get_ascii_address(): string {
public function get_unicode_address(): string {
return $this->localpart . '@' . $this->decoded_domain;
}

/**
* Returns the error code for the last attempt to parse an email address,
* if any error prevented recognizing the address.
*
* @see self::from_string()
*
* @since 7.1.0
*
* @return string|null
*/
public static function get_last_error(): ?string {
return self::$last_error;
}
}
117 changes: 117 additions & 0 deletions src/wp-includes/email/wp-email-support.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
<?php
/**
* Email Support
*
* Governs how WordPress treats email addresses and what level of support
* it provides for different email addressing standards.
*
* @package WordPress
* @subpackage Email
* @since 7.1.0
*/

/**
* Selects which rules to apply to email addresses based on the intended
* level of support for Unicode, ASCII, and legacy address formats.
*
* @since 7.1.0
*/
function wp_select_email_address_support(): void {
/**
* Filters the supported email address formats: Unicode, ASCII, or legacy.
*
* Note that Internationalized Domain Names (IDN) are supported in every
* email address policy when encoded in their ASCII compatible encoding (ACE),
* also known as “punycode.” Unicode email addresses makes it possible to
* support addresses with non-ASCII characters in the localpart, which has
* no equivalent ACE.
*
* - 'unicode' supports non-US-ASCII characters in the mailbox or localpart of the address;
* e.g. "josé@españa.es" or "josé@xn--espaa-rta.es".
* - 'ascii' applies updated parsing rules for traditional non-Unicode addresses, allowing
* for stronger interoperability with email systems.
* e.g. "jose@espana.com" or "jose@xn--espaa-rta.es".
* - 'legacy' supports the same email addresses as before WordPress 7.1.0.
*
* @see https://datatracker.ietf.org/doc/html/rfc3490
* @see https://datatracker.ietf.org/doc/html/rfc6530
*
* @since 7.1.0
*
* @param 'unicode'|'ascii'|'legacy' $support_level Which kinds of email addresses to allow.
*/
$support_level = apply_filters( 'wp_email_address_support', 'ascii' );

/**
* If Unicode support has been requested, but the site is incapable of storing
* and interacting with Unicode addresses, degrade to ASCII support.
*/
if ( 'unicode' === $support_level && ! wp_can_support_unicode_email_addresses() ) {
$support_level = 'ascii';

wp_trigger_error(
__FUNCTION__,
__( 'Unable to support Unicode email addresses because of missing platform support; supporting ASCII addresses instead.' ),
E_USER_WARNING
);
}

remove_filter( 'is_email', 'wp_is_ascii_email' );
remove_filter( 'is_email', 'wp_is_unicode_email' );
remove_filter( 'sanitize_email', 'wp_sanitize_ascii_email' );
remove_filter( 'sanitize_email', 'wp_sanitize_unicode_email' );

switch ( $support_level ) {
// Revert to pre-7.1 email address support.
case 'legacy':
break;

case 'ascii':
add_filter( 'is_email', 'wp_is_ascii_email', 10, 3 );
add_filter( 'sanitize_email', 'wp_sanitize_ascii_email', 10, 3 );
break;

case 'unicode':
add_filter( 'is_email', 'wp_is_unicode_email', 10, 3 );
add_filter( 'sanitize_email', 'wp_sanitize_unicode_email', 10, 3 );
break;
}
}


/**
* Returns whether Unicode email addresses could be supported.
*
* Handling Unicode email addresses depends on some platform support, namely
* that the emails can be stored in the database and that functionality
* exists to convert to and from punycode-encoded IDN domains.
*
* This function does not indicate what kind of email addresses WordPress
* supports, only whether it is possible to store Unicode email addresses.
*
* @see \wp_select_email_address_support() to change the support level.
* @see 'wp_email_address_support' filter for how to override the default support level.
*
* @since 7.1.0
*
* @return bool
*/
function wp_can_support_unicode_email_addresses(): bool {
global $wpdb;

/*
* While domains can be reliably encoded with punycode and stored in ASCII,
* Unicode usernames have no translation format, therefore need safe support
* for storing any UTF-8 character.
*/
$is_utf8_db = 'utf8mb4' === $wpdb->charset;

/**
* These functions convert into and out of punycode. Should WordPress add a polyfill
* for these then this check could be removed. The {@see \WpOrg\Requests\IdnaEncoder}
* class exists, but provides encoding only, and incompletely as noted in the class.
*/
$has_idn_codec = function_exists( 'idn_to_utf8' ) && function_exists( 'idn_to_ascii' );

return $is_utf8_db && $has_idn_codec;
}
Loading
Loading