diff --git a/core/lib/Drupal/Core/Entity/EntityStorageControllerBase.php b/core/lib/Drupal/Core/Entity/EntityStorageControllerBase.php index 22eb4724ca060f8090c7151a536b5342aad29843..6069cecd1fa4f583ecebfc58e8b5cceb5d61d09c 100644 --- a/core/lib/Drupal/Core/Entity/EntityStorageControllerBase.php +++ b/core/lib/Drupal/Core/Entity/EntityStorageControllerBase.php @@ -190,7 +190,8 @@ public function invokeFieldItemPrepareCache(EntityInterface $entity) { // of making LegacyConfigFieldItem implement PrepareCacheInterface. // @todo Remove once all core field types have been converted (see // http://drupal.org/node/2014671). - || (is_subclass_of($type_definition['class'], '\Drupal\field\Plugin\field\field_type\LegacyConfigFieldItem') && function_exists($type_definition['provider'] . '_field_load'))) { + || (is_subclass_of($type_definition['class'], '\Drupal\field\Plugin\field\field_type\LegacyConfigFieldItem') + && isset($type_definition['provider']) && function_exists($type_definition['provider'] . '_field_load'))) { // Call the prepareCache() method directly on each item // individually. diff --git a/core/modules/user/lib/Drupal/user/Entity/User.php b/core/modules/user/lib/Drupal/user/Entity/User.php index 3f1a54fe1910f4398e9b0fc95f7c8322a12c502f..21accca8bb8d1a4e1b4db3a0f8064be0ba4d6add 100644 --- a/core/modules/user/lib/Drupal/user/Entity/User.php +++ b/core/modules/user/lib/Drupal/user/Entity/User.php @@ -446,37 +446,59 @@ public static function baseFieldDefinitions($entity_type) { 'description' => t('The name of this user'), 'type' => 'string_field', 'settings' => array('default_value' => ''), + 'property_constraints' => array( + // No Length contraint here because the UserName constraint also covers + // that. + 'value' => array( + 'UserName' => array(), + 'UserNameUnique' => array(), + ), + ), ); $properties['pass'] = array( - 'label' => t('Name'), + 'label' => t('Password'), 'description' => t('The password of this user (hashed)'), 'type' => 'string_field', ); $properties['mail'] = array( - 'label' => t('Name'), + 'label' => t('E-mail'), 'description' => t('The e-mail of this user'), - 'type' => 'string_field', + 'type' => 'email_field', 'settings' => array('default_value' => ''), + 'property_constraints' => array( + 'value' => array('UserMailUnique' => array()), + ), ); $properties['signature'] = array( - 'label' => t('Name'), + 'label' => t('Signature'), 'description' => t('The signature of this user'), 'type' => 'string_field', + 'property_constraints' => array( + 'value' => array('Length' => array('max' => 255)), + ), ); $properties['signature_format'] = array( - 'label' => t('Name'), + 'label' => t('Signature format'), 'description' => t('The signature format of this user'), + // @todo Convert the type to filter_format once + // https://drupal.org/node/1758622 is comitted 'type' => 'string_field', ); $properties['theme'] = array( 'label' => t('Theme'), 'description' => t('The default theme of this user'), 'type' => 'string_field', + 'property_constraints' => array( + 'value' => array('Length' => array('max' => DRUPAL_EXTENSION_NAME_MAX_LENGTH)), + ), ); $properties['timezone'] = array( 'label' => t('Timezone'), 'description' => t('The timezone of this user'), 'type' => 'string_field', + 'property_constraints' => array( + 'value' => array('Length' => array('max' => 32)), + ), ); $properties['status'] = array( 'label' => t('User status'), @@ -504,12 +526,14 @@ public static function baseFieldDefinitions($entity_type) { $properties['init'] = array( 'label' => t('Init'), 'description' => t('The email address used for initial account creation.'), - 'type' => 'string_field', + 'type' => 'email_field', 'settings' => array('default_value' => ''), ); $properties['roles'] = array( 'label' => t('Roles'), 'description' => t('The roles the user has.'), + // @todo Convert this to entity_reference_field, see + // https://drupal.org/node/2044859 'type' => 'string_field', ); return $properties; diff --git a/core/modules/user/lib/Drupal/user/Plugin/Validation/Constraint/UserMailUnique.php b/core/modules/user/lib/Drupal/user/Plugin/Validation/Constraint/UserMailUnique.php new file mode 100644 index 0000000000000000000000000000000000000000..652abaaee7e557f707cb1d64702b19a5c2351cfe --- /dev/null +++ b/core/modules/user/lib/Drupal/user/Plugin/Validation/Constraint/UserMailUnique.php @@ -0,0 +1,33 @@ +<?php + +/** + * @file + * Contains \Drupal\user\Plugin\Validation\Constraint\UserMailUnique. + */ + +namespace Drupal\user\Plugin\Validation\Constraint; + +use Symfony\Component\Validator\Constraint; +use Drupal\Component\Annotation\Plugin; +use Drupal\Core\Annotation\Translation; + + +/** + * Checks if a user's e-mail address is unique on the site. + * + * @Plugin( + * id = "UserMailUnique", + * label = @Translation("User e-mail unique", context = "Validation") + * ) + */ +class UserMailUnique extends Constraint { + + public $message = 'The e-mail address %value is already taken.'; + + /** + * {@inheritdoc} + */ + public function validatedBy() { + return '\Drupal\user\Plugin\Validation\Constraint\UserUniqueValidator'; + } +} diff --git a/core/modules/user/lib/Drupal/user/Plugin/Validation/Constraint/UserNameConstraint.php b/core/modules/user/lib/Drupal/user/Plugin/Validation/Constraint/UserNameConstraint.php new file mode 100644 index 0000000000000000000000000000000000000000..a70908758e0d3162374c055f15260abd89e54633 --- /dev/null +++ b/core/modules/user/lib/Drupal/user/Plugin/Validation/Constraint/UserNameConstraint.php @@ -0,0 +1,31 @@ +<?php + +/** + * @file + * Contains \Drupal\user\Plugin\Validation\Constraint\UserNameConstraint. + */ + +namespace Drupal\user\Plugin\Validation\Constraint; + +use Symfony\Component\Validator\Constraint; +use Drupal\Component\Annotation\Plugin; +use Drupal\Core\Annotation\Translation; + + +/** + * Checks if a value is a valid user name. + * + * @Plugin( + * id = "UserName", + * label = @Translation("User name", context = "Validation") + * ) + */ +class UserNameConstraint extends Constraint { + + public $emptyMessage = 'You must enter a username.'; + public $spaceBeginMessage = 'The username cannot begin with a space.'; + public $spaceEndMessage = 'The username cannot end with a space.'; + public $multipleSpacesMessage = 'The username cannot contain multiple spaces in a row.'; + public $illegalMessage = 'The username contains an illegal character.'; + public $tooLongMessage = 'The username %name is too long: it must be %max characters or less.'; +} diff --git a/core/modules/user/lib/Drupal/user/Plugin/Validation/Constraint/UserNameConstraintValidator.php b/core/modules/user/lib/Drupal/user/Plugin/Validation/Constraint/UserNameConstraintValidator.php new file mode 100644 index 0000000000000000000000000000000000000000..ffd4d82017af85da77eb8eeeabd7b88cff094bc3 --- /dev/null +++ b/core/modules/user/lib/Drupal/user/Plugin/Validation/Constraint/UserNameConstraintValidator.php @@ -0,0 +1,54 @@ +<?php + +/** + * @file + * Contains \Drupal\user\Plugin\Validation\Constraint\UserNameConstraintValidator. + */ + +namespace Drupal\user\Plugin\Validation\Constraint; + +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\ConstraintValidator; + +/** + * Validates the UserName constraint. + */ +class UserNameConstraintValidator extends ConstraintValidator { + + /** + * {@inheritdoc} + */ + public function validate($name, Constraint $constraint) { + if (!$name) { + $this->context->addViolation($constraint->emptyMessage); + return; + } + if (substr($name, 0, 1) == ' ') { + $this->context->addViolation($constraint->spaceBeginMessage); + } + if (substr($name, -1) == ' ') { + $this->context->addViolation($constraint->spaceEndMessage); + } + if (strpos($name, ' ') !== FALSE) { + $this->context->addViolation($constraint->multipleSpacesMessage); + } + if (preg_match('/[^\x{80}-\x{F7} a-z0-9@_.\'-]/i', $name) + || preg_match( + '/[\x{80}-\x{A0}' . // Non-printable ISO-8859-1 + NBSP + '\x{AD}' . // Soft-hyphen + '\x{2000}-\x{200F}' . // Various space characters + '\x{2028}-\x{202F}' . // Bidirectional text overrides + '\x{205F}-\x{206F}' . // Various text hinting characters + '\x{FEFF}' . // Byte order mark + '\x{FF01}-\x{FF60}' . // Full-width latin + '\x{FFF9}-\x{FFFD}' . // Replacement characters + '\x{0}-\x{1F}]/u', // NULL byte and control characters + $name) + ) { + $this->context->addViolation($constraint->illegalMessage); + } + if (drupal_strlen($name) > USERNAME_MAX_LENGTH) { + $this->context->addViolation($constraint->tooLongMessage, array('%name' => $name, '%max' => USERNAME_MAX_LENGTH)); + } + } +} diff --git a/core/modules/user/lib/Drupal/user/Plugin/Validation/Constraint/UserNameUnique.php b/core/modules/user/lib/Drupal/user/Plugin/Validation/Constraint/UserNameUnique.php new file mode 100644 index 0000000000000000000000000000000000000000..72bfcb8bb8bbc7da3ddd9f560c6503bfeedd4971 --- /dev/null +++ b/core/modules/user/lib/Drupal/user/Plugin/Validation/Constraint/UserNameUnique.php @@ -0,0 +1,33 @@ +<?php + +/** + * @file + * Contains \Drupal\user\Plugin\Validation\Constraint\UserNameUnique. + */ + +namespace Drupal\user\Plugin\Validation\Constraint; + +use Symfony\Component\Validator\Constraint; +use Drupal\Component\Annotation\Plugin; +use Drupal\Core\Annotation\Translation; + + +/** + * Checks if a user name is unique on the site. + * + * @Plugin( + * id = "UserNameUnique", + * label = @Translation("User name unique", context = "Validation") + * ) + */ +class UserNameUnique extends Constraint { + + public $message = 'The name %value is already taken.'; + + /** + * {@inheritdoc} + */ + public function validatedBy() { + return '\Drupal\user\Plugin\Validation\Constraint\UserUniqueValidator'; + } +} diff --git a/core/modules/user/lib/Drupal/user/Plugin/Validation/Constraint/UserUniqueValidator.php b/core/modules/user/lib/Drupal/user/Plugin/Validation/Constraint/UserUniqueValidator.php new file mode 100644 index 0000000000000000000000000000000000000000..945223f445057d5ec57821ab738ac6c7f407e4c0 --- /dev/null +++ b/core/modules/user/lib/Drupal/user/Plugin/Validation/Constraint/UserUniqueValidator.php @@ -0,0 +1,38 @@ +<?php + +/** + * @file + * Contains \Drupal\user\Plugin\Validation\Constraint\UserUniqueValidator. + */ + +namespace Drupal\user\Plugin\Validation\Constraint; + +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\ConstraintValidator; + +/** + * Validates the unique user property constraint, such as name and e-mail. + */ +class UserUniqueValidator extends ConstraintValidator { + + /** + * {@inheritdoc} + */ + public function validate($value, Constraint $constraint) { + $field = $this->context->getMetadata()->getTypedData()->getParent(); + $uid = $field->getParent()->id(); + + $value_taken = (bool) db_select('users') + ->fields('users', array('uid')) + // The UID could be NULL, so we cast it to 0 in that case. + ->condition('uid', (int) $uid, '<>') + ->condition($field->getName(), db_like($value), 'LIKE') + ->range(0, 1) + ->execute() + ->fetchField(); + + if ($value_taken) { + $this->context->addViolation($constraint->message, array("%value" => $value)); + } + } +} diff --git a/core/modules/user/lib/Drupal/user/Tests/UserValidationTest.php b/core/modules/user/lib/Drupal/user/Tests/UserValidationTest.php index 8801b1d8aabf6d859a28da2a35e5f453eb50ff24..14cf2af41da6561543b4bcc6cd157e3af4161e12 100644 --- a/core/modules/user/lib/Drupal/user/Tests/UserValidationTest.php +++ b/core/modules/user/lib/Drupal/user/Tests/UserValidationTest.php @@ -7,18 +7,41 @@ namespace Drupal\user\Tests; -use Drupal\simpletest\WebTestBase; +use Drupal\Core\Entity\EntityInterface; +use Drupal\simpletest\DrupalUnitTestBase; + +/** + * Performs validation tests on user fields. + */ +class UserValidationTest extends DrupalUnitTestBase { + + /** + * Modules to enable. + * + * @var array + */ + public static $modules = array('field', 'user', 'system'); -class UserValidationTest extends WebTestBase { public static function getInfo() { return array( - 'name' => 'Username/e-mail validation', - 'description' => 'Verify that username/email validity checks behave as designed.', + 'name' => 'User validation', + 'description' => 'Verify that user validity checks behave as designed.', 'group' => 'User' ); } - // Username validation. + /** + * {@inheritdoc} + */ + public function setUp() { + parent::setUp(); + $this->installSchema('user', array('users')); + $this->installSchema('system', array('sequences')); + } + + /** + * Tests user name validation. + */ function testUsernames() { $test_cases = array( // '<username>' => array('<description>', 'assert<testName>'), 'foo' => array('Valid username', 'assertNull'), @@ -44,4 +67,96 @@ function testUsernames() { $this->$test($result, $description . ' (' . $name . ')'); } } + + /** + * Runs entity validation checks. + */ + function testValidation() { + $user = entity_create('user', array('name' => 'test')); + $violations = $user->validate(); + $this->assertEqual(count($violations), 0, 'No violations when validating a default user.'); + + // Only test one example invalid name here, the rest is already covered in + // the testUsernames() method in this class. + $name = $this->randomName(61); + $user->set('name', $name); + $violations = $user->validate(); + $this->assertEqual(count($violations), 1, 'Violation found when name is too long.'); + $this->assertEqual($violations[0]->getPropertyPath(), 'name.0.value'); + $this->assertEqual($violations[0]->getMessage(), t('The username %name is too long: it must be %max characters or less.', array('%name' => $name, '%max' => 60))); + + // Create a second test user to provoke a name collision. + $user2 = entity_create('user', array( + 'name' => 'existing', + 'mail' => 'existing@exmaple.com', + )); + $user2->save(); + $user->set('name', 'existing'); + $violations = $user->validate(); + $this->assertEqual(count($violations), 1, 'Violation found on name collision.'); + $this->assertEqual($violations[0]->getPropertyPath(), 'name.0.value'); + $this->assertEqual($violations[0]->getMessage(), t('The name %name is already taken.', array('%name' => 'existing'))); + + // Make the name valid. + $user->set('name', $this->randomName()); + + $user->set('mail', 'invalid'); + $violations = $user->validate(); + $this->assertEqual(count($violations), 1, 'Violation found when email is invalid'); + $this->assertEqual($violations[0]->getPropertyPath(), 'mail.0.value'); + $this->assertEqual($violations[0]->getMessage(), t('This value is not a valid email address.')); + + $mail = $this->randomName(EMAIL_MAX_LENGTH - 11) . '@example.com'; + $user->set('mail', $mail); + $violations = $user->validate(); + $this->assertEqual(count($violations), 1, 'Violation found when email is too long'); + $this->assertEqual($violations[0]->getPropertyPath(), 'mail.0.value'); + $this->assertEqual($violations[0]->getMessage(), t('This value is not a valid email address.')); + + // Provoke a e-mail collision with an exsiting user. + $user->set('mail', 'existing@exmaple.com'); + $violations = $user->validate(); + $this->assertEqual(count($violations), 1, 'Violation found when e-mail already exists.'); + $this->assertEqual($violations[0]->getPropertyPath(), 'mail.0.value'); + $this->assertEqual($violations[0]->getMessage(), t('The e-mail address %mail is already taken.', array('%mail' => 'existing@exmaple.com'))); + $user->set('mail', NULL); + + $user->set('signature', $this->randomString(256)); + $this->assertLengthViolation($user, 'signature', 255); + $user->set('signature', NULL); + + $user->set('theme', $this->randomString(DRUPAL_EXTENSION_NAME_MAX_LENGTH + 1)); + $this->assertLengthViolation($user, 'theme', DRUPAL_EXTENSION_NAME_MAX_LENGTH); + $user->set('theme', NULL); + + $user->set('timezone', $this->randomString(33)); + $this->assertLengthViolation($user, 'timezone', 32); + $user->set('timezone', NULL); + + $user->set('init', 'invalid'); + $violations = $user->validate(); + $this->assertEqual(count($violations), 1, 'Violation found when init email is invalid'); + $this->assertEqual($violations[0]->getPropertyPath(), 'init.0.value'); + $this->assertEqual($violations[0]->getMessage(), t('This value is not a valid email address.')); + + // @todo Test user role validation once https://drupal.org/node/2015701 got + // committed. + } + + /** + * Verifies that a length violation exists for the given field. + * + * @param \Drupal\core\Entity\EntityInterface $entity + * The entity object to validate. + * @param string $field_name + * The field that violates the maximum length. + * @param int $length + * Number of characters that was exceeded. + */ + protected function assertLengthViolation(EntityInterface $entity, $field_name, $length) { + $violations = $entity->validate(); + $this->assertEqual(count($violations), 1, "Violation found when $field_name is too long."); + $this->assertEqual($violations[0]->getPropertyPath(), "$field_name.0.value"); + $this->assertEqual($violations[0]->getMessage(), t('This value is too long. It should have %limit characters or less.', array('%limit' => $length))); + } } diff --git a/core/modules/user/user.module b/core/modules/user/user.module index 91e7ee174b9fcfec4574abc599136c61ef573445..4768f937a264133f515df138d9192c3a2ff46900 100644 --- a/core/modules/user/user.module +++ b/core/modules/user/user.module @@ -27,7 +27,7 @@ /** * Maximum length of user e-mail text field. */ -const EMAIL_MAX_LENGTH = 254; +const EMAIL_MAX_LENGTH = 255; /** * Only administrators can create user accounts. @@ -321,37 +321,24 @@ function user_load_by_name($name) { /** * Verify the syntax of the given name. + * + * @param string $name + * The user name to validate. + * + * @return string|null + * A translated violation message if the name is invalid or NULL if the name + * is valid. + * */ function user_validate_name($name) { - if (!$name) { - return t('You must enter a username.'); - } - if (substr($name, 0, 1) == ' ') { - return t('The username cannot begin with a space.'); - } - if (substr($name, -1) == ' ') { - return t('The username cannot end with a space.'); - } - if (strpos($name, ' ') !== FALSE) { - return t('The username cannot contain multiple spaces in a row.'); - } - if (preg_match('/[^\x{80}-\x{F7} a-z0-9@_.\'-]/i', $name)) { - return t('The username contains an illegal character.'); - } - if (preg_match('/[\x{80}-\x{A0}' . // Non-printable ISO-8859-1 + NBSP - '\x{AD}' . // Soft-hyphen - '\x{2000}-\x{200F}' . // Various space characters - '\x{2028}-\x{202F}' . // Bidirectional text overrides - '\x{205F}-\x{206F}' . // Various text hinting characters - '\x{FEFF}' . // Byte order mark - '\x{FF01}-\x{FF60}' . // Full-width latin - '\x{FFF9}-\x{FFFD}' . // Replacement characters - '\x{0}-\x{1F}]/u', // NULL byte and control characters - $name)) { - return t('The username contains an illegal character.'); - } - if (drupal_strlen($name) > USERNAME_MAX_LENGTH) { - return t('The username %name is too long: it must be %max characters or less.', array('%name' => $name, '%max' => USERNAME_MAX_LENGTH)); + $data = \Drupal::typedData()->create(array( + 'type' => 'string', + 'constraints' => array('UserName' => array()), + )); + $data->setValue($name); + $violations = $data->validate(); + if (count($violations) > 0) { + return $violations[0]->getMessage(); } }