Repository navigation
Users: Treat email addresses that differ only in letter case as the s… #14053
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: trunk
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -32,6 +32,15 @@ class WP_Users_List_Table extends WP_List_Table { | |||||
| */ | ||||||
| public $is_site_users; | ||||||
|
|
||||||
| /** | ||||||
| * IDs of users on the current page whose email address is also used by another | ||||||
| * user, ignoring letter case. Keys are user IDs. | ||||||
| * | ||||||
| * @since 7.2.0 | ||||||
| * @var true[] | ||||||
| */ | ||||||
| protected $duplicate_email_user_ids = array(); | ||||||
|
|
||||||
| /** | ||||||
| * Constructor. | ||||||
| * | ||||||
|
|
@@ -411,11 +420,77 @@ public function display_rows() { | |||||
| $post_counts = count_many_users_posts( array_keys( $this->items ) ); | ||||||
| } | ||||||
|
|
||||||
| $this->duplicate_email_user_ids = $this->get_duplicate_email_user_ids( array_keys( $this->items ) ); | ||||||
|
|
||||||
| foreach ( $this->items as $userid => $user_object ) { | ||||||
| echo "\n\t" . $this->single_row( $user_object, '', '', isset( $post_counts ) ? $post_counts[ $userid ] : 0 ); | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| /** | ||||||
| * Finds which of the given users share an email address with another user, | ||||||
| * ignoring letter case. | ||||||
| * | ||||||
| * Many mailbox providers treat `abc@example.com` and `ABc@example.com` as the | ||||||
| * same mailbox, so such accounts are likely duplicates. | ||||||
| * | ||||||
| * @since 7.2.0 | ||||||
| * | ||||||
| * @global wpdb $wpdb WordPress database abstraction object. | ||||||
| * | ||||||
| * @param int[] $user_ids IDs of the users to check. | ||||||
| * @return true[] Array keyed by the IDs of users whose email address is shared. | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But note above, how this could just return |
||||||
| */ | ||||||
| protected function get_duplicate_email_user_ids( $user_ids ) { | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| global $wpdb; | ||||||
|
|
||||||
| $user_ids = array_filter( array_map( 'intval', $user_ids ) ); | ||||||
| if ( empty( $user_ids ) ) { | ||||||
| return array(); | ||||||
| } | ||||||
|
|
||||||
| $emails = array(); | ||||||
| foreach ( $user_ids as $user_id ) { | ||||||
| $user = get_userdata( $user_id ); | ||||||
| if ( $user && '' !== $user->user_email ) { | ||||||
| $emails[ strtolower( $user->user_email ) ] = true; | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| if ( empty( $emails ) ) { | ||||||
| return array(); | ||||||
| } | ||||||
|
|
||||||
| $emails = array_keys( $emails ); | ||||||
| $placeholders = implode( ',', array_fill( 0, count( $emails ), '%s' ) ); | ||||||
|
|
||||||
| /* | ||||||
| * All users whose email matches, ignoring case, the email of a user on this page. | ||||||
| * With the default case-insensitive collation a plain comparison already ignores | ||||||
| * letter case and can use the user_email index; LOWER() is only needed otherwise. | ||||||
| */ | ||||||
| $email_column = _wp_is_user_email_case_sensitive() ? 'LOWER(user_email)' : 'user_email'; | ||||||
|
|
||||||
| $matches = $wpdb->get_results( | ||||||
| // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare | ||||||
| $wpdb->prepare( "SELECT ID, user_email FROM $wpdb->users WHERE $email_column IN ($placeholders)", $emails ) | ||||||
| ); | ||||||
|
|
||||||
| $users_by_email = array(); | ||||||
| foreach ( $matches as $match ) { | ||||||
| $users_by_email[ strtolower( $match->user_email ) ][] = (int) $match->ID; | ||||||
| } | ||||||
|
|
||||||
| $duplicates = array(); | ||||||
| foreach ( $users_by_email as $ids ) { | ||||||
| if ( count( $ids ) > 1 ) { | ||||||
| $duplicates += array_fill_keys( $ids, true ); | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| return array_intersect_key( $duplicates, array_flip( $user_ids ) ); | ||||||
| } | ||||||
|
|
||||||
| /** | ||||||
| * Generates HTML for a single row on the users.php admin panel. | ||||||
| * | ||||||
|
|
@@ -599,6 +674,12 @@ public function single_row( $user_object, $style = '', $role = '', $numposts = 0 | |||||
| break; | ||||||
| case 'email': | ||||||
| $row .= "<a href='" . esc_url( "mailto:$email" ) . "'>$email</a>"; | ||||||
| if ( isset( $this->duplicate_email_user_ids[ $user_object->ID ] ) ) { | ||||||
| $row .= sprintf( | ||||||
| '<p class="duplicate-email"><span class="dashicons dashicons-warning" aria-hidden="true"></span> %s</p>', | ||||||
| __( 'Another user has this email address, possibly with different letter case.' ) | ||||||
| ); | ||||||
| } | ||||||
| break; | ||||||
| case 'role': | ||||||
| $row .= esc_html( $roles_list ); | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2134,16 +2134,42 @@ function username_exists( $username ) { | |
| * Conditional Tags} article in the Theme Developer Handbook. | ||
| * | ||
| * @since 2.1.0 | ||
| * @since 7.2.0 The comparison is case-insensitive regardless of the database collation. | ||
| * | ||
| * @global wpdb $wpdb WordPress database abstraction object. | ||
| * | ||
| * @param string $email The email to check for existence. | ||
| * @return int|false The user ID on success, false on failure. | ||
| */ | ||
| function email_exists( $email ) { | ||
| global $wpdb; | ||
|
|
||
| $user = get_user_by( 'email', $email ); | ||
| if ( $user ) { | ||
| $user_id = $user->ID; | ||
| } else { | ||
| $user_id = false; | ||
|
|
||
| /* | ||
| * Most mailbox providers treat the local part of an address as case-insensitive. | ||
| * The default users table collation already compares emails that way, so the | ||
| * lookup above has covered it. Only when the column compares case-sensitively | ||
| * (or the collation is unknown) fall back to an explicit case-insensitive lookup. | ||
| * That lookup cannot use the user_email index, so it is avoided where possible. | ||
| */ | ||
| $trimmed_email = trim( (string) $email ); | ||
| if ( '' !== $trimmed_email && _wp_is_user_email_case_sensitive() ) { | ||
| $found_id = $wpdb->get_var( | ||
| $wpdb->prepare( | ||
| "SELECT ID FROM $wpdb->users WHERE LOWER(user_email) = LOWER(%s) LIMIT 1", | ||
| $trimmed_email | ||
| ) | ||
| ); | ||
|
|
||
| if ( $found_id ) { | ||
| $user_id = (int) $found_id; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -2158,6 +2184,56 @@ function email_exists( $email ) { | |
| return apply_filters( 'email_exists', $user_id, $email ); | ||
| } | ||
|
|
||
| /** | ||
| * Determines whether the database compares user email addresses case-sensitively. | ||
| * | ||
| * The default collation of the users table is case-insensitive, in which case | ||
| * a plain comparison already matches email addresses that differ only in letter | ||
| * case, and can use the user_email index. Case-sensitive collations such as | ||
| * `*_bin` or `*_cs` need an explicit LOWER() comparison instead. | ||
| * | ||
| * When the collation cannot be determined, for example on a non-MySQL database, | ||
| * the comparison is assumed to be case-sensitive. | ||
| * | ||
| * @since 7.2.0 | ||
| * @access private | ||
| * | ||
| * @global wpdb $wpdb WordPress database abstraction object. | ||
| * | ||
| * @return bool True if user email comparisons are case-sensitive, false otherwise. | ||
| */ | ||
| function _wp_is_user_email_case_sensitive() { | ||
| global $wpdb; | ||
|
|
||
| static $is_case_sensitive = array(); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this an array when it only ever has one item in it? |
||
|
|
||
| if ( ! isset( $is_case_sensitive[ $wpdb->users ] ) ) { | ||
| $result = true; | ||
|
|
||
| if ( $wpdb->is_mysql ) { | ||
| $column = $wpdb->get_row( "SHOW FULL COLUMNS FROM $wpdb->users LIKE 'user_email'" ); | ||
|
|
||
| if ( $column && ! empty( $column->Collation ) ) { | ||
| $result = ! str_ends_with( strtolower( $column->Collation ), '_ci' ); | ||
| } | ||
| } | ||
|
|
||
| $is_case_sensitive[ $wpdb->users ] = $result; | ||
| } | ||
|
|
||
| /** | ||
| * Filters whether user email comparisons are case-sensitive in the database. | ||
| * | ||
| * When true, email lookups that need to ignore letter case use LOWER(), which | ||
| * cannot use the user_email index. | ||
| * | ||
| * @since 7.2.0 | ||
| * | ||
| * @param bool $is_case_sensitive Whether the user_email column compares case-sensitively. | ||
| */ | ||
| return (bool) apply_filters( 'wp_is_user_email_case_sensitive', $is_case_sensitive[ $wpdb->users ] ); | ||
| } | ||
|
|
||
| /** | ||
| * Checks whether a username is valid. | ||
| * | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This being said, I'm not sure an associative array is needed here. It could just be
list<int>, without any mapping.