From c6b4d6c3d359ce9bb3165757a21237de13d08f4d Mon Sep 17 00:00:00 2001
From: Dave Long <dave@longwaveconsulting.com>
Date: Wed, 20 Nov 2024 17:59:41 +0000
Subject: [PATCH] SA-CORE-2024-004 by zengenuity, cilefen,
 kristiaanvandeneynde, mcdruid, larowlan

---
 .../Constraint/UniqueFieldConstraint.php      | 10 ++++++
 .../Constraint/UniqueFieldValueValidator.php  | 13 ++++++-
 .../Validation/Constraint/FileUriUnique.php   | 19 ++++++++---
 core/modules/user/user.install                | 34 ++++++++++++++-----
 4 files changed, 62 insertions(+), 14 deletions(-)

diff --git a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/UniqueFieldConstraint.php b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/UniqueFieldConstraint.php
index 9196e0b86abb..7b121a5bf64c 100644
--- a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/UniqueFieldConstraint.php
+++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/UniqueFieldConstraint.php
@@ -17,6 +17,16 @@ class UniqueFieldConstraint extends SymfonyConstraint {
 
   public $message = 'A @entity_type with @field_name %value already exists.';
 
+  /**
+   * This constraint is case-insensitive by default.
+   *
+   * For example "FOO" and "foo" would be considered as equivalent, and
+   * validation of the constraint would fail.
+   *
+   * @var bool
+   */
+  public $caseSensitive = FALSE;
+
   /**
    * Returns the name of the class that validates this constraint.
    *
diff --git a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/UniqueFieldValueValidator.php b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/UniqueFieldValueValidator.php
index 430b0793cdd7..ec0958002802 100644
--- a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/UniqueFieldValueValidator.php
+++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/UniqueFieldValueValidator.php
@@ -64,12 +64,23 @@ public function validate($items, Constraint $constraint) {
       ->getStorage($entity_type_id)
       ->getAggregateQuery()
       ->accessCheck(FALSE)
-      ->condition($field_name, $item_values, 'IN')
       ->groupBy("$field_name.$property_name");
     if (!$is_new) {
       $entity_id = $entity->id();
       $query->condition($id_key, $entity_id, '<>');
     }
+
+    if ($constraint->caseSensitive) {
+      $query->condition($field_name, $item_values, 'IN');
+    }
+    else {
+      $or_group = $query->orConditionGroup();
+      foreach ($item_values as $item_value) {
+        $or_group->condition($field_name, \Drupal::database()->escapeLike($item_value), 'LIKE');
+      }
+      $query->condition($or_group);
+    }
+
     $results = $query->execute();
 
     if (!empty($results)) {
diff --git a/core/modules/file/src/Plugin/Validation/Constraint/FileUriUnique.php b/core/modules/file/src/Plugin/Validation/Constraint/FileUriUnique.php
index 855766f99de8..fd2300a7ca18 100644
--- a/core/modules/file/src/Plugin/Validation/Constraint/FileUriUnique.php
+++ b/core/modules/file/src/Plugin/Validation/Constraint/FileUriUnique.php
@@ -4,7 +4,8 @@
 
 use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Drupal\Core\Validation\Attribute\Constraint;
-use Symfony\Component\Validator\Constraint as SymfonyConstraint;
+use Drupal\Core\Validation\Plugin\Validation\Constraint\UniqueFieldConstraint;
+use Drupal\Core\Validation\Plugin\Validation\Constraint\UniqueFieldValueValidator;
 
 /**
  * Supports validating file URIs.
@@ -13,15 +14,25 @@
   id: 'FileUriUnique',
   label: new TranslatableMarkup('File URI', [], ['context' => 'Validation'])
 )]
-class FileUriUnique extends SymfonyConstraint {
+class FileUriUnique extends UniqueFieldConstraint {
 
   public $message = 'The file %value already exists. Enter a unique file URI.';
 
+  /**
+   * This constraint is case-sensitive.
+   *
+   * For example "public://foo.txt" and "public://FOO.txt" are treated as
+   * different values, and can co-exist.
+   *
+   * @var bool
+   */
+  public $caseSensitive = TRUE;
+
   /**
    * {@inheritdoc}
    */
-  public function validatedBy() {
-    return '\Drupal\Core\Validation\Plugin\Validation\Constraint\UniqueFieldValueValidator';
+  public function validatedBy(): string {
+    return UniqueFieldValueValidator::class;
   }
 
 }
diff --git a/core/modules/user/user.install b/core/modules/user/user.install
index 73fb2947aa3e..35ff4fe9d0e2 100644
--- a/core/modules/user/user.install
+++ b/core/modules/user/user.install
@@ -100,6 +100,7 @@ function user_requirements($phase): array {
   if ($phase !== 'runtime') {
     return [];
   }
+  $return = [];
 
   $result = (bool) \Drupal::entityQuery('user')
     ->accessCheck(FALSE)
@@ -108,17 +109,32 @@ function user_requirements($phase): array {
     ->execute();
 
   if ($result === FALSE) {
-    return [
-      'anonymous user' => [
-        'title' => t('Anonymous user'),
-        'description' => t('The anonymous user does not exist. See the <a href=":url">restore the anonymous (user ID 0) user record</a> for more information', [
-          ':url' => 'https://www.drupal.org/node/1029506',
-        ]),
-        'severity' => REQUIREMENT_WARNING,
-      ],
+    $return['anonymous user'] = [
+      'title' => t('Anonymous user'),
+      'description' => t('The anonymous user does not exist. See the <a href=":url">restore the anonymous (user ID 0) user record</a> for more information', [
+        ':url' => 'https://www.drupal.org/node/1029506',
+      ]),
+      'severity' => REQUIREMENT_WARNING,
+    ];
+  }
+
+  $query = \Drupal::database()->select('users_field_data');
+  $query->addExpression('LOWER(mail)', 'lower_mail');
+  $query->groupBy('lower_mail');
+  $query->having('COUNT(uid) > :matches', [':matches' => 1]);
+  $conflicts = $query->countQuery()->execute()->fetchField();
+
+  if ($conflicts > 0) {
+    $return['conflicting emails'] = [
+      'title' => t('Conflicting user emails'),
+      'description' => t('Some user accounts have email addresses that differ only by case. For example, one account might have alice@example.com and another might have Alice@Example.com. See <a href=":url">Conflicting User Emails</a> for more information.', [
+        ':url' => 'https://www.drupal.org/node/3486109',
+      ]),
+      'severity' => REQUIREMENT_WARNING,
     ];
   }
-  return [];
+
+  return $return;
 }
 
 /**
-- 
GitLab