Skip to content

Users: Treat email addresses that differ only in letter case as the s… - #14053

Closed
rvpatel wants to merge 12 commits into
WordPress:trunkfrom
rvpatel:trac-66238
Closed

rvpatel wants to merge 12 commits into
WordPress:trunkfrom
rvpatel:trac-66238

Conversation

@rvpatel

@rvpatel rvpatel commented Oct 6, 2026 •

Copy link
Copy Markdown

Trac ticket: https://core.trac.wordpress.org/ticket/66238

What

email_exists() now matches emails case-insensitively regardless of the DB collation, so ABc@example.com can't be registered when abc@example.com exists. The Users list table flags existing case-variant duplicates so they can be cleaned up.

Index tradeoff (from Trac feedback)

The LOWER(user_email) lookup now only runs when the user_email column has a case-sensitive collation (*_bin, *_cs) or the collation can't be determined. With the default *_ci collation, the existing indexed lookup already ignores case, so no extra query is made.

The list table check only queries the email addresses on the current page, using user_email IN (...) on *_ci collations so the index is used.

Collation detection is cached per request in the private _wp_is_user_email_case_sensitive() and can be overridden with the wp_is_user_email_case_sensitive filter.

Testing

  1. npm run test:php -- --group 66238 (covers both the case-insensitive and case-sensitive paths).
  2. Create a user abc@example.com, then try to add ABc@example.com: it should be rejected with "This email address is already registered".

Use of AI Tools

AI assistance: Yes
Tool(s): Claude Code
Model(s): Claude Opus 5.5
Used for: Reworking the patch to address the Trac feedback (limiting the LOWER() lookup to case-sensitive collations), the list table query, PHPUnit tests, and running PHPCS/PHPUnit/PHPStan; also the latest review fixes. I reviewed and tested the final implementation and take responsibility for it.

…ame.

`email_exists()` now matches email addresses case-insensitively regardless of
the database collation, so a second account cannot be created with an address
that only differs in letter case (for example `abc@example.com` and
`ABc@example.com`).

With the default case-insensitive collation of the users table, the existing
lookup already ignores letter case and uses the `user_email` index, so no extra
query is made. The `LOWER()` fallback, which cannot use the index, only runs
when the `user_email` column has a case-sensitive collation or the collation
cannot be determined. This is detected once per request by the new private
`_wp_is_user_email_case_sensitive()` function, and can be overridden with the
`wp_is_user_email_case_sensitive` filter.

The Users list table also flags users on the current page whose email address
is shared with another account, ignoring letter case, so that existing
duplicates can be found and cleaned up. The lookup only uses the addresses on
the current page and is index-friendly on case-insensitive collations.

See #66238.
@github-actions

github-actions Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

Core Committers: Use this line as a base for the props when committing in SVN:

Props ravipatel, westonruter.

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown

Test using WordPress Playground

The changes in this pull request can previewed and tested using a WordPress Playground instance.

WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser.

Some things to be aware of

  • All changes will be lost when closing a tab with a Playground instance.
  • All changes will be lost when refreshing the page.
  • A fresh instance is created each time the link below is clicked.
  • Every time this pull request is updated, a new ZIP file containing all changes is created. If changes are not reflected in the Playground instance,
    it's possible that the most recent build failed, or has not completed. Check the list of workflow runs to be sure.

For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation.

Test this pull request with WordPress Playground.

@westonruter

Copy link
Copy Markdown
Member

Please add the AI disclosure to the PR description:

## Use of AI Tools
<!--
You are free to use artificial intelligence (AI) tooling to contribute, but you must disclose what tooling you are using and to what extent a pull request has been authored by AI. It is your responsibility to review and take responsibility for what AI generates. See the WordPress AI Guidelines: <https://make.wordpress.org/ai/handbook/ai-guidelines/>.
Example disclosure:
AI assistance: Yes
Tool(s): GitHub Copilot, ChatGPT
Model(s): GPT-5.1
Used for: Initial code skeleton and test suggestions; final implementation and tests were reviewed and edited by me.
-->

Comment thread src/wp-admin/includes/class-wp-users-list-table.php Outdated
Comment thread src/wp-admin/includes/class-wp-users-list-table.php Outdated
Comment thread src/wp-admin/includes/class-wp-users-list-table.php Outdated
Comment thread src/wp-includes/user.php Outdated
function _wp_is_user_email_case_sensitive() {
global $wpdb;

static $is_case_sensitive = array();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. The users table is global, so it's now a single cached value. Fixed in e0f0583.

rvpatel and others added 5 commits October 7, 2026 14:42
Co-authored-by: Weston Ruter <westonruter@gmail.com>
Co-authored-by: Weston Ruter <westonruter@gmail.com>
Co-authored-by: Weston Ruter <westonruter@gmail.com>
* Return a list of user IDs from get_duplicate_email_user_ids() instead of an ID-keyed map.
* Cache the collation check in _wp_is_user_email_case_sensitive() as a single value, since the users table is global.

See #66238.
@rvpatel
rvpatel requested a review from westonruter October 7, 2026 18:31
@rvpatel

rvpatel commented Oct 8, 2026

Copy link
Copy Markdown
Author

Thanks @westonruter! Just a heads-up: my fork was created from another fork (ravikumarup/wordpress-develop) rather than directly from WordPress/wordpress-develop, so "Allow edits by maintainers" may not let you push to this branch. To avoid that, I've invited you as a collaborator on rvpatel/wordpress-develop. Once you accept the invitation, you should be able to push changes directly to the trac-66238 branch. I'll re-fork from the official repository once my open PRs are wrapped up.

@rvpatel

rvpatel commented Oct 9, 2026

Copy link
Copy Markdown
Author

@westonruter The PR has been updated to address the review feedback: the LOWER() lookup now runs only when the user_email column uses a case-sensitive collation, so the default *_ci setup keeps using the index. It's also up to date with trunk and all checks pass. Ready for another review when you have time.

@westonruter westonruter left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changes look good to me. But I don't have a lot of experience with users, so best if I let someone else pick this up the review from here.

Comment thread src/wp-includes/user.php Outdated
*
* @return bool True if user email comparisons are case-sensitive, false otherwise.
*/
function _wp_is_user_email_case_sensitive() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
function _wp_is_user_email_case_sensitive() {
function _wp_is_user_email_case_sensitive(): bool {

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied in 77c1059, thanks!

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

A list-table test passes IDs where user objects are required, and successful fallback lookup remains untested.

2 open findings
What changed in this PR

Makes email uniqueness checks case-insensitive across database collations and identifies legacy duplicate addresses.

Changes:

  • Adds collation-aware email lookup fallback.
  • Highlights duplicate emails in the Users table.
  • Adds related PHPUnit coverage and styling.
File Description
src/​wp-includes/​user.php Adds collation detection and case-insensitive fallback.
src/​wp-admin/​includes/​class-wp-users-list-table.php Detects and displays duplicate-email warnings.
src/​wp-admin/​css/​list-tables.css Styles duplicate-email warnings.
tests/​phpunit/​tests/​user.php Tests email lookup and insertion behavior.
tests/​phpunit/​tests/​admin/​wpUsersListTable.php Tests duplicate detection and display.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

};
add_filter( 'query', $collect );

$flagged = $method->invoke( $this->table, array( $original, $unique ) );

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied in 77c1059, thanks!

Comment on lines +963 to +964
$this->assertSame( $user_id, email_exists( 'mixedcase@example.com' ), 'Lowercase variant should match.' );
$this->assertSame( $user_id, email_exists( 'MIXEDCASE@EXAMPLE.COM' ), 'Uppercase variant should match.' );
- Pass `WP_User` objects to `get_duplicate_email_user_ids()` in the test, matching the updated signature.
- Add a `bool` return type to `_wp_is_user_email_case_sensitive()`.
- Add a test covering a successful `LOWER()` fallback when the exact lookup misses.
@rvpatel

rvpatel commented Oct 12, 2026

Copy link
Copy Markdown
Author

Comment on old #14053. It should already be closed automatically because you deleted the fork, but it still accepts comments

@rvpatel

rvpatel commented Oct 12, 2026

Copy link
Copy Markdown
Author

#14209

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants