diff --git a/core/lib/Drupal/Component/Utility/Random.php b/core/lib/Drupal/Component/Utility/Random.php index 356eefd..ef8c242 100644 --- a/core/lib/Drupal/Component/Utility/Random.php +++ b/core/lib/Drupal/Component/Utility/Random.php @@ -7,6 +7,8 @@ namespace Drupal\Component\Utility; +use Drupal\user\Plugin\Validation\Constraint\UserNameConstraintValidator; + /** * Defines a utility class for creating random data. * @@ -70,7 +72,20 @@ public function string($length = 8, $unique = FALSE, $validator = NULL) { } $str = ''; for ($i = 0; $i < $length; $i++) { - $str .= chr(mt_rand(32, 126)); + $ch_counter = 0; + do { + if ($ch_counter == static::MAXIMUM_TRIES) { + throw new \RuntimeException('Unable to generate a unique character for a random name'); + } + $ch = '' . chr(mt_rand(32, 126)); + $get_next_ch = FALSE; + if (UserNameConstraintValidator::hasIllegalCharacters($ch)) { + $get_next_ch = TRUE; + } + $ch_counter++; + } while ($get_next_ch); + + $str .= $ch; } $counter++; diff --git a/core/modules/simpletest/src/UserCreationTrait.php b/core/modules/simpletest/src/UserCreationTrait.php index 92262cf..13212f9 100644 --- a/core/modules/simpletest/src/UserCreationTrait.php +++ b/core/modules/simpletest/src/UserCreationTrait.php @@ -60,8 +60,14 @@ protected function createUser(array $permissions = array(), $name = NULL, $admin // Create a user assigned to that role. $edit = array(); - $edit['name'] = !empty($name) ? $name : $this->randomMachineName(); + $edit['name'] = !empty($name) ? $name : $this->getRandomGenerator()->string(8, TRUE, array($this, 'randomUsernameValidate')); $edit['mail'] = $edit['name'] . '@example.com'; + // It is possible that name + @example.com is not a valid email address + // since user names can contain whitespace and @ characters and start with a + // dot. + if (!valid_email_address($edit['mail'])) { + $edit['mail'] = $this->getRandomGenerator()->name() . '@example.com'; + } $edit['pass'] = user_password(); $edit['status'] = 1; if ($rid) { diff --git a/core/modules/simpletest/src/WebTestBase.php b/core/modules/simpletest/src/WebTestBase.php index 99cb227..6297de5 100644 --- a/core/modules/simpletest/src/WebTestBase.php +++ b/core/modules/simpletest/src/WebTestBase.php @@ -14,6 +14,7 @@ use Drupal\Component\Utility\Html; use Drupal\Component\Utility\NestedArray; use Drupal\Component\Utility\UrlHelper; +use Drupal\Component\Utility\Unicode; use Drupal\Core\Cache\Cache; use Drupal\Component\Utility\SafeMarkup; use Drupal\Core\Database\Database; @@ -28,6 +29,7 @@ use Drupal\Core\StreamWrapper\PublicStream; use Drupal\Core\Url; use Drupal\node\Entity\NodeType; +use Drupal\user\Plugin\Validation\Constraint\UserNameConstraintValidator; use Symfony\Component\HttpFoundation\Request; /** @@ -2481,6 +2483,10 @@ protected function assertNoResponse($code, $message = '', $group = 'Browser') { * TRUE on pass, FALSE on fail. */ protected function assertMail($name, $value = '', $message = '', $group = 'Email') { + // The mail subject is mime encoded. + if ($name == 'subject') { + $value = Unicode::mimeHeaderEncode($value); + } $captured_emails = \Drupal::state()->get('system.test_mail_collector') ?: array(); $email = end($captured_emails); return $this->assertTrue($email && isset($email[$name]) && $email[$name] == $value, $message, $group); @@ -2655,9 +2661,25 @@ protected function buildUrl($path, array $options = array()) { * @param string $expected_cache_context * The expected cache context. */ - protected function assertCacheContext($expected_cache_context) { - $cache_contexts = explode(' ', $this->drupalGetHeader('X-Drupal-Cache-Contexts')); - $this->assertTrue(in_array($expected_cache_context, $cache_contexts), "'" . $expected_cache_context . "' is present in the X-Drupal-Cache-Contexts header."); + protected function assertCacheContext($expected_cache_context) + { + $cache_contexts = explode(' ', $this->drupalGetHeader('X-Drupal-Cache-Contexts')); + $this->assertTrue(in_array($expected_cache_context, $cache_contexts), "'" . $expected_cache_context . "' is present in the X-Drupal-Cache-Contexts header."); + } + + /** + * Callback for random username validation. + * + * @see \Drupal\Component\Utility\Random::string() + * + * @param string $string + * The random string to validate. + * + * @return bool + * TRUE if the random string is valid, FALSE if not. + */ + public function randomUsernameValidate($string) { + return $this->randomStringValidate($string) && !UserNameConstraintValidator::hasIllegalCharacters($string); } /** diff --git a/core/modules/system/src/Tests/Path/UrlAlterFunctionalTest.php b/core/modules/system/src/Tests/Path/UrlAlterFunctionalTest.php index 1988708..4c5a12a 100644 --- a/core/modules/system/src/Tests/Path/UrlAlterFunctionalTest.php +++ b/core/modules/system/src/Tests/Path/UrlAlterFunctionalTest.php @@ -37,7 +37,7 @@ function testUrlAlter() { // Test a single altered path. $this->drupalGet("user/$name"); $this->assertResponse('200', 'The user/username path gets resolved correctly'); - $this->assertUrlOutboundAlter("user/$uid", "user/$name"); + $this->assertUrlOutboundAlter("user/$uid", "user/" . urlencode($name)); // Test that a path always uses its alias. $path = array('source' => "user/$uid/test1", 'alias' => 'alias/test1'); @@ -88,7 +88,7 @@ function testUrlAlter() { protected function assertUrlOutboundAlter($original, $final) { // Test outbound altering. $result = $this->container->get('url_generator')->generateFromPath($original); - $final = Url::fromUri('internal:/' . $final)->toString(); + $final = $this->container->get('url_generator')->generateFromPath(urldecode($final)); $this->assertIdentical($result, $final, format_string('Altered outbound URL %original, expected %final, and got %result.', array('%original' => $original, '%final' => $final, '%result' => $result))); } diff --git a/core/modules/user/src/Plugin/Validation/Constraint/UserNameConstraintValidator.php b/core/modules/user/src/Plugin/Validation/Constraint/UserNameConstraintValidator.php index 12d4fa3..63503a5 100644 --- a/core/modules/user/src/Plugin/Validation/Constraint/UserNameConstraintValidator.php +++ b/core/modules/user/src/Plugin/Validation/Constraint/UserNameConstraintValidator.php @@ -34,23 +34,36 @@ public function validate($items, Constraint $constraint) { 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) - ) { + if (static::hasIllegalCharacters($name)) { $this->context->addViolation($constraint->illegalMessage); } if (Unicode::strlen($name) > USERNAME_MAX_LENGTH) { $this->context->addViolation($constraint->tooLongMessage, array('%name' => $name, '%max' => USERNAME_MAX_LENGTH)); } } + + /** + * Checks if the username has illegal characters. + * + * @param string $name + * The username to check. + * + * @return bool + * TRUE if the username has illegal characters, FALSE if not. + */ + public static function hasIllegalCharacters($name) { + return 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); + } + } diff --git a/core/modules/user/src/Tests/UserPasswordResetTest.php b/core/modules/user/src/Tests/UserPasswordResetTest.php index 90bdee8..47288c9 100644 --- a/core/modules/user/src/Tests/UserPasswordResetTest.php +++ b/core/modules/user/src/Tests/UserPasswordResetTest.php @@ -87,7 +87,8 @@ function testUserPasswordReset() { // Verify that the user was sent an email. $this->assertMail('to', $this->account->getEmail(), 'Password email sent to user.'); - $subject = t('Replacement login information for @username at @site', array('@username' => $this->account->getUsername(), '@site' => $this->config('system.site')->get('name'))); + //email subject is not sanitized - same as in user_mail + $subject = t('Replacement login information for !username at !site', array('!username' => $this->account->getUsername(), '!site' => $this->config('system.site')->get('name'))); $this->assertMail('subject', $subject, 'Password reset email subject is correct.'); $resetURL = $this->getResetURL(); diff --git a/core/modules/user/src/Tests/UserValidationTest.php b/core/modules/user/src/Tests/UserValidationTest.php index ec44a79..448e168 100644 --- a/core/modules/user/src/Tests/UserValidationTest.php +++ b/core/modules/user/src/Tests/UserValidationTest.php @@ -55,6 +55,7 @@ function testUsernames() { 'foo@example.com' => array('Valid username', 'assertNull'), 'foo@-example.com' => array('Valid username', 'assertNull'), // invalid domains are allowed in usernames 'þòøÇߪř€' => array('Valid username', 'assertNull'), + 'foo+bar' => array('Valid username', 'assertNull'), // '+' symbol is allowed 'ᚠᛇᚻ᛫ᛒᛦᚦ' => array('Valid UTF8 username', 'assertNull'), // runes ' foo' => array('Invalid username that starts with a space', 'assertNotNull'), 'foo ' => array('Invalid username that ends with a space', 'assertNotNull'),