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