Issue #2002162 by klausi, eugene.ilyin: Convert form validation of users to entity validation.

8.0.x
Nathaniel Catchpole 2013-09-01 14:10:10 +01:00
parent b00eeb7df8
commit fe0fd73a2f
9 changed files with 358 additions and 42 deletions

View File

@ -190,7 +190,8 @@ abstract class EntityStorageControllerBase implements EntityStorageControllerInt
// 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.

View File

@ -446,37 +446,59 @@ class User extends EntityNG implements UserInterface {
'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 @@ class User extends EntityNG implements UserInterface {
$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;

View File

@ -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';
}
}

View File

@ -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.';
}

View File

@ -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));
}
}
}

View File

@ -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';
}
}

View File

@ -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));
}
}
}

View File

@ -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 @@ class UserValidationTest extends WebTestBase {
$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)));
}
}

View File

@ -27,7 +27,7 @@ const USERNAME_MAX_LENGTH = 60;
/**
* 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();
}
}