Problem/Motivation
When registering a duplicate user name, the registration should be gracefully blocked when user name is the same case insensitively
This is similar to https://www.drupal.org/project/drupal/issues/2502021
Steps to reproduce
- Go to user/register and register an account named 'example'. (note the lowercase e as the first character)
- Go back to user/register and register an account named 'Example' (note the uppercase E as the first character).
This results in a WSD with the message "The website encountered an unexpected error. Try again later."
The following 'user' type error is logged:
Drupal\Core\Database\IntegrityConstraintViolationException: SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry 'Example-en' for key 'user__name': INSERT INTO "users_field_data" ("uid", "langcode", "preferred_langcode", "preferred_admin_langcode", "name", "pass", "mail", "timezone", "status", "created", "changed", "access", "login", "init", "default_langcode") VALUES (:db_insert_placeholder_0, :db_insert_placeholder_1, :db_insert_placeholder_2, :db_insert_placeholder_3, :db_insert_placeholder_4, :db_insert_placeholder_5, :db_insert_placeholder_6, :db_insert_placeholder_7, :db_insert_placeholder_8, :db_insert_placeholder_9, :db_insert_placeholder_10, :db_insert_placeholder_11, :db_insert_placeholder_12, :db_insert_placeholder_13, :db_insert_placeholder_14); Array ( [:db_insert_placeholder_0] => 3319 [:db_insert_placeholder_1] => en [:db_insert_placeholder_2] => en [:db_insert_placeholder_3] => en [:db_insert_placeholder_4] => Example [:db_insert_placeholder_5] => [REMOVED] [:db_insert_placeholder_6] => example2@example.com [:db_insert_placeholder_7] => Europe/London [:db_insert_placeholder_8] => 1 [:db_insert_placeholder_9] => 1705591756 [:db_insert_placeholder_10] => 1705591756 [:db_insert_placeholder_11] => 0 [:db_insert_placeholder_12] => 0 [:db_insert_placeholder_13] => example2@example.com [:db_insert_placeholder_14] => 1 ) in Drupal\mysql\Driver\Database\mysql\ExceptionHandler->handleExecutionException() (line 45 of /code/web/core/modules/mysql/src/Driver/Database/mysql/ExceptionHandler.php).
And a similar 'php' type error is also logged:
Drupal\Core\Entity\EntityStorageException: SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry 'Example-en' for key 'user__name': INSERT INTO .........
Proposed resolution
Drupal displays an error message on the registration form, which should be "The username Example is already taken."
Drupal v10.2.1
Issue fork drupal-3415582
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
- 11.x
compare
- 3415582-unhandled-exception-when
changes, plain diff MR !6474
Comments
Comment #2
cilefen commentedThere is no evident data corruption or other signifiers of critical priority so this is probably major. I am adjusting that.
Comment #3
matt bFair enough, but for me a WSD when someone tries to register renders a site unusable
I wanted to rule out any contrib module on my site causing this issue, so I stood up a fresh v10.2.2 standard install on Pantheon and am able to replicated the issue.
Comment #4
cilefen commentedThe critical priority guideline means most or all of the site would be unusable (not the case) and have no workaround (another username is a workaround). Also, #2502021: Unhandled exception when trying to register a duplicate user was major priority and is essentially the same.
Comment #5
cilefen commentedLOL, I just noticed I was the reporter of #2502021: Unhandled exception when trying to register a duplicate user and raised it from normal to major.
Comment #6
matt bThanks, and understood on the priorities. 9 years and it comes back to get you!
I have also checked I can log in as the original user as 'Example' or 'example' (didn't check any other variations) so the username is treated in a case insensitive manner on login, but not on registration.
Comment #7
cilefen commentedDatabase string type fields are usually case-insensitive for lookups, which is why.
Comment #8
nico.b commentedI believe this is heavily related to the changes introduced by https://www.drupal.org/node/3372085. It seems like the UniqueFieldValueValidator is no longer case-insensitive.
Furthermore, this also introduces another problem: You can now create multiple user accounts with the same e-mail address if you use a different case since the validator does no longer detect that there is an account with the same e-mail address, for example you can create one account with user@example.com and one with User@example.com.
Comment #9
caesius commentedWhat may need to happen is that this condition get updated to build out a
'LIKE'condition for each item rather than simply an'IN'condition.Building a 'LIKE' condition in Drupal
Note that you can't combine 'LIKE' and 'IN', but a loop should make short work of it.
The tests added in #2478663: UniqueFieldValueValidator works only with single value fields should be easy to expand to check for case insensitivity.
ETA: The PHP logic will probably also need to be updated to account for the 'LIKE' condition, e.g. at
$duplicate_values = array_intersect($item_values, $other_entity_values);-- I wouldn't expectarray_intersect()to be case-insensitive.Comment #10
nico.b commented@caesius I did some debugging on the code and, actually, it seems like the condition you mentioned already produces correct results. Only this statement seems to get rid of duplicates. For example, when already having an account "example" and then trying to create an account "Example",
$other_entity_valuescontains "example", which is what we want, right? Only #L85 then gets rid of "example" and thus, the code ends up in the conclusion that there are no duplicate values.A really straightforward fix that worked for me was converting all values to lowercase before making the intersection
$duplicate_values = array_intersect(array_map('strtolower', $item_values), array_map('strtolower', $other_entity_values));Not sure if this might have any unintended side effects, though.
EDIT: Is it possible that the statement mentioned by @caesius behaves differently on different entities? As I stated before, just adjusting the line above was sufficient for the user registration, but in the test cases, it seems like that's not sufficient since$other_entity_valuesis not filled correctly, as also stated by @caesius.Comment #12
nico.b commentedI added a test case that outlines the issue. This is my first ever test contribution (or Core contribution in general), so I hope everything is fine.
Comment #13
nico.b commentedI've also added a potential fix. As stated before, not sure about potential side effects though.
Comment #14
larowlanLeft a comment on the MR, we can't assume all unique field constraints are case-insensitive.
#3419816: Add support for case sensitive validation to UniqueFieldValuesConstraint has a different approach.
I think we should combine the two issues.
Comment #15
caesius commentedAre there other instances where two values may be considered the same by a database query but not in a PHP function like array_intersect?
Comment #16
larowlan#2068655: Entity fields do not support case sensitive queries leads me to believe that entity queries/values are case sensitive at the DB layer but I'm happy to be wrong about that
Comment #17
larowlanhttps://git.drupalcode.org/project/drupal/-/blob/11.x/core/tests/Drupal/... seems it depends on the field
Comment #18
caesius commentedI was actually asking about any theoretical "similarities" that can occur in the data comparisons besides case sensitivity. For example,
0 == FALSE(a stretch, I know) or encoding issues.It's probably fine to only check for case sensitivity for emails and usernames, and it's also probably unlikely that there's anything else to consider here, but still something that should be pondered for a minute before moving on IMO.
Comment #19
larowlanYes, agree @caesius
Comment #20
nico.b commentedIn general, I agree with this statement.
However, there are still some things not clear to me: If I understand the code of the
UniqueFieldValueValidatorand the changes made correctly, wasn't the behavior exactly the same in the past since the case-sensitivity was solely depend on the DB layer (since there were no additional PHP checks)? Do the changes I suggested really change this behavior since if the DB layer is case-sensitive, the code I suggested doesn't change that because the PHP code doesn't even see the potential case-insensitive duplicate because it's not even returned by the DB layer as a potential candidate (=> the results are still case-sensitive)? On the other hand, if the DB layer is case-insensitive, we now preserve this behavior and don't do an additional check on the PHP-side to filter out case-insensitive duplicates.In contrast, in the code changes you suggested in https://www.drupal.org/project/drupal/issues/3419816, I believe the behavior is now quite confusing:
- If shouldIgnoreCase == FALSE, case-sensitivity is enforced, so this is fine.
- However, if shouldIgnoreCase == TRUE, case-insensitivity is not enforced if the DB layer performs a case-sensitive comparison, right? Therefore, even if shouldIgnoreCase == TRUE, it can happen that the uniqueness is still checked in a case-sensitive manner.
This behavior doesn't align with the expectations I would have towards a shouldIgnoreCase toggle. There might be an oversight on my end, so obviously the above only applies if my understandings are correct.
Comment #22
gerzenstl commentedI able to reproduce this issue on Drupal v10.2.1.
Comment #23
nico.b commentedComment #24
smustgrave commentedAppears to have open thread, also MR target branch needs to be updated
Comment #25
aasarava commentedI have to agree with @nico.b. The MR gets the UniqueFieldValueValidator back to where it was before the breaking change.
Given that the breaking change was inadvertent (as it was not documented and it broke major core functionality), there's no reason to assume that any core and contrib modules that use the UniqueField constraint now expect there to be case sensitivity when comparing values. So there's no strong reason to separate out a properly working constraint only for username and email while leaving any other uses potentially broken.
If there's a need for case sensitivity in UniqueField, that should be handled as its own constraint & validator (CaseSensitiveUniqueField?) that contributors can use if needed.
For context, I have a client site that relies on dozens of user self-registrations per day. With 50K users in the system, we were seeing a lot of username clashes showing up as SQL integrity violations in the logs. This means lots of potential users were getting WSODs without explanation. We've deployed the MR to production due to urgently needing a fix, and it has been working without problems so far.
Comment #26
nico.b commentedThanks for the confirmation @aasarava. We've also already applied the patch to our production system due to the same reasons. For large pages with a high number of user registrations per day, this issue is really critical.
@smustgrave I've updated the MR target branch. For the open thread, there's not much I can do except for waiting for a response. I've also added a response directly in the MR again (not only here).
Comment #27
aasarava commentedDoes changing the target branch to 11.x mean there's no plan to try to get this included as a bug fix in a 10.2.5 or 6 release?
Comment #28
smustgrave commented@nico.b Will let @larowlan respond to the thread
Comment #29
larowlanYes, we'd want this in a 10.2 bugfix.
Replied to the comment, sorry this one dropped off my radar. Left a couple of minor nitpicks on the MR.
Comment #30
smustgrave commentedThanks @larowlan!
Comment #31
nico.b commentedThanks a lot, @smustgrave and @larowlan! The suggestions in the MR seem perfectly valid and I've applied them.
Pipeline runs are still successful (and test-only changes still fail) and all threads are resolved, so I hope the MR is good to go now :)
@aasarava: Based on what I've also only learned just a few days ago (from here), patches usually go to the main dev branch (currently 11.x). The patch is then cherry-picked back to stable branches, so e.g. 10.2. So as @larowlan also said, the change of the target is rather a "technical detail"/"process adherence"-thing instead of a decision to not bring it to 10.2.
Comment #32
smustgrave commentedFeedback appears to be addressed
Thanks!
Comment #36
larowlanCommitted to 11.x and backported to 10.2.x and 10.3.x
Thanks folks
I'll repurpose the other issue to add a case-insenstive option (which as you rightly point out, we never supported before)
Comment #38
larowlanComment #39
matt bThank you for this - I'm very time poor at the moment, so haven't had an opportunity to test!
Comment #40
tonytheferg commentedJust found this. Thanks for the fix!
How should accounts that are created programatically handle this sort when there is an existing username? Like is there a way to put an
_1etc after the username?Comment #42
mxr576It seems the fix that was merged caused an unexpected side-effect, referencing the new issue in case anybody from the old crew would like to jump on the new one as well: #3507777: User names uniqueness is no longer accent-insensitive.