From 3194f768b36f994e9e5955d419c2ff3c7ba3b7fc Mon Sep 17 00:00:00 2001
From: Alex Pott <alex.a.pott@googlemail.com>
Date: Fri, 5 Apr 2024 10:12:07 +0100
Subject: [PATCH] Issue #3431203 by kim.pepper, alexpott, smustgrave, longwave:
 Deprecate user_validate_name() and replace with service

(cherry picked from commit 8e7d3e0f52be31dda66c21fa6dfd248d3646a2d0)
---
 .../Core/Installer/Form/SiteConfigureForm.php | 26 ++++-
 .../user/src/Form/UserPasswordForm.php        | 47 +++++----
 .../UserNameConstraintValidator.php           |  5 +-
 core/modules/user/src/UserNameValidator.php   | 36 +++++++
 .../src/Kernel/UserNameValidatorTest.php      | 96 +++++++++++++++++++
 .../tests/src/Kernel/UserValidationTest.php   |  3 +
 core/modules/user/user.module                 | 13 +--
 core/modules/user/user.services.yml           |  4 +
 8 files changed, 198 insertions(+), 32 deletions(-)
 create mode 100644 core/modules/user/src/UserNameValidator.php
 create mode 100644 core/modules/user/tests/src/Kernel/UserNameValidatorTest.php

diff --git a/core/lib/Drupal/Core/Installer/Form/SiteConfigureForm.php b/core/lib/Drupal/Core/Installer/Form/SiteConfigureForm.php
index d5221dacfc69..ab2e7a0de965 100644
--- a/core/lib/Drupal/Core/Installer/Form/SiteConfigureForm.php
+++ b/core/lib/Drupal/Core/Installer/Form/SiteConfigureForm.php
@@ -8,8 +8,9 @@
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Locale\CountryManagerInterface;
 use Drupal\Core\Site\Settings;
-use Drupal\user\UserStorageInterface;
 use Drupal\user\UserInterface;
+use Drupal\user\UserStorageInterface;
+use Drupal\user\UserNameValidator;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
@@ -67,13 +68,26 @@ class SiteConfigureForm extends ConfigFormBase {
    *   The module installer.
    * @param \Drupal\Core\Locale\CountryManagerInterface $country_manager
    *   The country manager.
+   * @param \Drupal\user\UserNameValidator|null $userNameValidator
+   *   The user validator.
    */
-  public function __construct($root, $site_path, UserStorageInterface $user_storage, ModuleInstallerInterface $module_installer, CountryManagerInterface $country_manager) {
+  public function __construct(
+    $root,
+    $site_path,
+    UserStorageInterface $user_storage,
+    ModuleInstallerInterface $module_installer,
+    CountryManagerInterface $country_manager,
+    protected ?UserNameValidator $userNameValidator = NULL,
+  ) {
     $this->root = $root;
     $this->sitePath = $site_path;
     $this->userStorage = $user_storage;
     $this->moduleInstaller = $module_installer;
     $this->countryManager = $country_manager;
+    if (!$userNameValidator) {
+      @\trigger_error('Calling ' . __METHOD__ . ' without the $userNameValidator argument is deprecated in drupal:10.3.0 and will be required in drupal:11.0.0. See https://www.drupal.org/node/3431205', E_USER_DEPRECATED);
+      $this->userNameValidator = \Drupal::service('user.name_validator');
+    }
   }
 
   /**
@@ -85,7 +99,8 @@ public static function create(ContainerInterface $container) {
       $container->getParameter('site.path'),
       $container->get('entity_type.manager')->getStorage('user'),
       $container->get('module_installer'),
-      $container->get('country_manager')
+      $container->get('country_manager'),
+      $container->get('user.name_validator'),
     );
   }
 
@@ -253,8 +268,9 @@ public function buildForm(array $form, FormStateInterface $form_state) {
    * {@inheritdoc}
    */
   public function validateForm(array &$form, FormStateInterface $form_state) {
-    if ($error = user_validate_name($form_state->getValue(['account', 'name']))) {
-      $form_state->setErrorByName('account][name', $error);
+    $violations = $this->userNameValidator->validateName($form_state->getValue(['account', 'name']));
+    if ($violations->count() > 0) {
+      $form_state->setErrorByName('account][name', $violations[0]->getMessage());
     }
   }
 
diff --git a/core/modules/user/src/Form/UserPasswordForm.php b/core/modules/user/src/Form/UserPasswordForm.php
index db5efc6180bf..5b28f7d9f243 100644
--- a/core/modules/user/src/Form/UserPasswordForm.php
+++ b/core/modules/user/src/Form/UserPasswordForm.php
@@ -4,7 +4,7 @@
 
 use Drupal\Component\Utility\EmailValidatorInterface;
 use Drupal\Core\Config\ConfigFactory;
-use Drupal\Core\Field\BaseFieldDefinition;
+use Drupal\Core\DependencyInjection\DeprecatedServicePropertyTrait;
 use Drupal\Core\Flood\FloodInterface;
 use Drupal\Core\Form\FormBase;
 use Drupal\Core\Form\FormStateInterface;
@@ -13,6 +13,7 @@
 use Drupal\Core\TypedData\TypedDataManagerInterface;
 use Drupal\user\UserInterface;
 use Drupal\user\UserStorageInterface;
+use Drupal\user\UserNameValidator;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
@@ -24,6 +25,15 @@
  */
 class UserPasswordForm extends FormBase {
 
+  use DeprecatedServicePropertyTrait;
+
+  /**
+   * The deprecated properties.
+   */
+  protected array $deprecatedProperties = [
+    'typedDataManager' => 'typed_data_manager',
+  ];
+
   /**
    * The user storage.
    *
@@ -45,13 +55,6 @@ class UserPasswordForm extends FormBase {
    */
   protected $flood;
 
-  /**
-   * The typed data manager.
-   *
-   * @var \Drupal\Core\TypedData\TypedDataManagerInterface
-   */
-  protected $typedDataManager;
-
   /**
    * The email validator service.
    *
@@ -70,18 +73,28 @@ class UserPasswordForm extends FormBase {
    *   The config factory.
    * @param \Drupal\Core\Flood\FloodInterface $flood
    *   The flood service.
-   * @param \Drupal\Core\TypedData\TypedDataManagerInterface $typed_data_manager
-   *   The typed data manager.
+   * @param \Drupal\user\UserNameValidator|\Drupal\Core\TypedData\TypedDataManagerInterface $userNameValidator
+   *   The user validator service.
    * @param \Drupal\Component\Utility\EmailValidatorInterface $email_validator
    *   The email validator service.
    */
-  public function __construct(UserStorageInterface $user_storage, LanguageManagerInterface $language_manager, ConfigFactory $config_factory, FloodInterface $flood, TypedDataManagerInterface $typed_data_manager, EmailValidatorInterface $email_validator) {
+  public function __construct(
+    UserStorageInterface $user_storage,
+    LanguageManagerInterface $language_manager,
+    ConfigFactory $config_factory,
+    FloodInterface $flood,
+    protected UserNameValidator|TypedDataManagerInterface $userNameValidator,
+    EmailValidatorInterface $email_validator,
+  ) {
     $this->userStorage = $user_storage;
     $this->languageManager = $language_manager;
     $this->configFactory = $config_factory;
     $this->flood = $flood;
-    $this->typedDataManager = $typed_data_manager;
     $this->emailValidator = $email_validator;
+    if (!$userNameValidator instanceof UserNameValidator) {
+      @\trigger_error('Passing $userNameValidator as \Drupal\Core\TypedData\TypedDataManagerInterface to ' . __METHOD__ . ' () is deprecated in drupal:10.3.0 and is removed in drupal:10.0.0. Pass a Drupal\user\UserValidator instead. See https://www.drupal.org/node/3431205', E_USER_DEPRECATED);
+      $this->userNameValidator = \Drupal::service('user.name_validator');
+    }
   }
 
   /**
@@ -93,8 +106,8 @@ public static function create(ContainerInterface $container) {
       $container->get('language_manager'),
       $container->get('config.factory'),
       $container->get('flood'),
-      $container->get('typed_data_manager'),
-      $container->get('email.validator')
+      $container->get('user.name_validator'),
+      $container->get('email.validator'),
     );
   }
 
@@ -161,11 +174,7 @@ public function validateForm(array &$form, FormStateInterface $form_state) {
     $this->flood->register('user.password_request_ip', $flood_config->get('ip_window'));
     // First, see if the input is possibly valid as a username.
     $name = trim($form_state->getValue('name'));
-    $definition = BaseFieldDefinition::create('string')
-      ->addConstraint('UserName', []);
-    $data = $this->typedDataManager->create($definition);
-    $data->setValue($name);
-    $violations = $data->validate();
+    $violations = $this->userNameValidator->validateName($name);
     // Usernames have a maximum length shorter than email addresses. Only print
     // this error if the input is not valid as a username or email address.
     if ($violations->count() > 0 && !$this->emailValidator->isValid($name)) {
diff --git a/core/modules/user/src/Plugin/Validation/Constraint/UserNameConstraintValidator.php b/core/modules/user/src/Plugin/Validation/Constraint/UserNameConstraintValidator.php
index bbe7ea07809c..bce5f6e110e5 100644
--- a/core/modules/user/src/Plugin/Validation/Constraint/UserNameConstraintValidator.php
+++ b/core/modules/user/src/Plugin/Validation/Constraint/UserNameConstraintValidator.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\user\Plugin\Validation\Constraint;
 
+use Drupal\Core\Field\FieldItemListInterface;
 use Drupal\user\UserInterface;
 use Symfony\Component\Validator\Constraint;
 use Symfony\Component\Validator\ConstraintValidator;
@@ -15,11 +16,11 @@ class UserNameConstraintValidator extends ConstraintValidator {
    * {@inheritdoc}
    */
   public function validate($items, Constraint $constraint) {
-    if (!isset($items) || !$items->value) {
+    if (empty($items) || ($items instanceof FieldItemListInterface && $items->isEmpty())) {
       $this->context->addViolation($constraint->emptyMessage);
       return;
     }
-    $name = $items->first()->value;
+    $name = $items instanceof FieldItemListInterface ? $items->first()->value : $items;
     if (str_starts_with($name, ' ')) {
       $this->context->addViolation($constraint->spaceBeginMessage);
     }
diff --git a/core/modules/user/src/UserNameValidator.php b/core/modules/user/src/UserNameValidator.php
new file mode 100644
index 000000000000..03a6cbcd4fdf
--- /dev/null
+++ b/core/modules/user/src/UserNameValidator.php
@@ -0,0 +1,36 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\user;
+
+use Drupal\Core\Validation\BasicRecursiveValidatorFactory;
+use Drupal\Core\Validation\ConstraintManager;
+use Symfony\Component\Validator\ConstraintViolationListInterface;
+
+/**
+ * Provides a username validator.
+ *
+ * This validator re-uses the UserName constraint plugin but does not require a
+ * User entity.
+ */
+class UserNameValidator {
+
+  public function __construct(
+    protected readonly BasicRecursiveValidatorFactory $validatorFactory,
+    protected readonly ConstraintManager $constraintManager,
+  ) {}
+
+  /**
+   * Validates a user name.
+   *
+   * @return \Symfony\Component\Validator\ConstraintViolationListInterface
+   *   The list of constraint violations.
+   */
+  public function validateName(string $name): ConstraintViolationListInterface {
+    $validator = $this->validatorFactory->createValidator();
+    $constraint = $this->constraintManager->create('UserName', []);
+    return $validator->validate($name, $constraint);
+  }
+
+}
diff --git a/core/modules/user/tests/src/Kernel/UserNameValidatorTest.php b/core/modules/user/tests/src/Kernel/UserNameValidatorTest.php
new file mode 100644
index 000000000000..14959d2139f5
--- /dev/null
+++ b/core/modules/user/tests/src/Kernel/UserNameValidatorTest.php
@@ -0,0 +1,96 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\user\Kernel;
+
+use Drupal\KernelTests\KernelTestBase;
+use Drupal\user\UserInterface;
+use Drupal\user\UserNameValidator;
+
+/**
+ * Verify that user validity checks behave as designed.
+ *
+ * @group user
+ */
+class UserNameValidatorTest extends KernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = ['user'];
+
+  /**
+   * The user validator under test.
+   */
+  protected UserNameValidator $userValidator;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+    $this->userValidator = $this->container->get('user.name_validator');
+  }
+
+  /**
+   * Tests valid user name validation.
+   *
+   * @dataProvider validUsernameProvider
+   */
+  public function testValidUsernames($name): void {
+    $violations = $this->userValidator->validateName($name);
+    $this->assertEmpty($violations);
+  }
+
+  /**
+   * Tests invalid user name validation.
+   *
+   * @dataProvider invalidUserNameProvider
+   */
+  public function testInvalidUsernames($name, $expectedMessage): void {
+    $violations = $this->userValidator->validateName($name);
+    $this->assertNotEmpty($violations);
+    $this->assertEquals($expectedMessage, $violations[0]->getMessage());
+  }
+
+  /**
+   * Provides valid user names.
+   */
+  public static function validUsernameProvider(): array {
+    // cSpell:disable
+    return [
+      'lowercase' => ['foo'],
+      'uppercase' => ['FOO'],
+      'contains space' => ['Foo O\'Bar'],
+      'contains @' => ['foo@bar'],
+      'allow email' => ['foo@example.com'],
+      'allow invalid domain' => ['foo@-example.com'],
+      'allow special chars' => ['þòøÇߪř€'],
+      'allow plus' => ['foo+bar'],
+      'utf8 runes' => ['ᚠᛇᚻ᛫ᛒᛦᚦ'],
+    ];
+    // cSpell:enable
+  }
+
+  /**
+   * Provides invalid user names.
+   */
+  public static function invalidUserNameProvider(): array {
+    return [
+      'starts with space' => [' foo', 'The username cannot begin with a space.'],
+      'ends with space' => ['foo ', 'The username cannot end with a space.'],
+      'contains 2 spaces' => ['foo  bar', 'The username cannot contain multiple spaces in a row.'],
+      'empty string' => ['', 'You must enter a username.'],
+      'invalid chars' => ['foo/', 'The username contains an illegal character.'],
+      // NULL.
+      'contains chr(0)' => ['foo' . chr(0) . 'bar', 'The username contains an illegal character.'],
+      // CR.
+      'contains chr(13)' => ['foo' . chr(13) . 'bar', 'The username contains an illegal character.'],
+      'excessively long' => [str_repeat('x', UserInterface::USERNAME_MAX_LENGTH + 1),
+        'The username xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx is too long: it must be 60 characters or less.',
+      ],
+    ];
+  }
+
+}
diff --git a/core/modules/user/tests/src/Kernel/UserValidationTest.php b/core/modules/user/tests/src/Kernel/UserValidationTest.php
index f2fc70bca60a..94f44c3ce37e 100644
--- a/core/modules/user/tests/src/Kernel/UserValidationTest.php
+++ b/core/modules/user/tests/src/Kernel/UserValidationTest.php
@@ -38,6 +38,8 @@ protected function setUp(): void {
 
   /**
    * Tests user name validation.
+   *
+   * @group legacy
    */
   public function testUsernames() {
     // cSpell:disable
@@ -66,6 +68,7 @@ public function testUsernames() {
       'foo' . chr(13) . 'bar'  => ['Invalid username containing chr(13)', 'assertNotNull'],
       str_repeat('x', UserInterface::USERNAME_MAX_LENGTH + 1) => ['Invalid excessively long username', 'assertNotNull'],
     ];
+    $this->expectDeprecation('user_validate_name() is deprecated in drupal:10.3.0 and is removed from drupal:12.0.0. Use \Drupal\user\UserValidator::validateName() instead. See https://www.drupal.org/node/3431205');
     // cSpell:enable
     foreach ($test_cases as $name => $test_case) {
       [$description, $test] = $test_case;
diff --git a/core/modules/user/user.module b/core/modules/user/user.module
index 3f3d21fee0e3..60244c932b33 100644
--- a/core/modules/user/user.module
+++ b/core/modules/user/user.module
@@ -14,7 +14,6 @@
 use Drupal\Core\Batch\BatchBuilder;
 use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
 use Drupal\Core\Entity\EntityInterface;
-use Drupal\Core\Field\BaseFieldDefinition;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Render\Element;
 use Drupal\Core\Routing\RouteMatchInterface;
@@ -206,13 +205,15 @@ function user_load_by_name($name) {
  * @return string|null
  *   A translated violation message if the name is invalid or NULL if the name
  *   is valid.
+ *
+ * @deprecated in drupal:10.3.0 and is removed from drupal:12.0.0. Use
+ *   \Drupal\user\UserValidator::validateName() instead.
+ *
+ * @see https://www.drupal.org/node/3431205
  */
 function user_validate_name($name) {
-  $definition = BaseFieldDefinition::create('string')
-    ->addConstraint('UserName', []);
-  $data = \Drupal::typedDataManager()->create($definition);
-  $data->setValue($name);
-  $violations = $data->validate();
+  @trigger_error(__METHOD__ . '() is deprecated in drupal:10.3.0 and is removed from drupal:12.0.0. Use \Drupal\user\UserValidator::validateName() instead. See https://www.drupal.org/node/3431205', E_USER_DEPRECATED);
+  $violations = \Drupal::service('user.name_validator')->validateName($name);
   if (count($violations) > 0) {
     return $violations[0]->getMessage();
   }
diff --git a/core/modules/user/user.services.yml b/core/modules/user/user.services.yml
index fb0389dd973e..e2ec9cc3a067 100644
--- a/core/modules/user/user.services.yml
+++ b/core/modules/user/user.services.yml
@@ -73,3 +73,7 @@ services:
   user.module_permissions_link_helper:
     class: Drupal\user\ModulePermissionsLinkHelper
     arguments: ['@user.permissions', '@access_manager', '@extension.list.module']
+  user.name_validator:
+    class: Drupal\user\UserNameValidator
+    arguments: ['@validation.basic_recursive_validator_factory', '@validation.constraint']
+  Drupal\user\UserNameValidator: '@user.name_validator'
-- 
GitLab