username-1.0.x-dev/src/UsernameGenerator.php
src/UsernameGenerator.php
<?php
namespace Drupal\username;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\Unicode;
use Drupal\Component\Uuid\UuidInterface;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Utility\Token;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* The Username Generator service.
*/
final class UsernameGenerator {
/**
* The config factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $config;
/**
* The entity field manager.
*
* @var \Drupal\Core\Entity\EntityFieldManagerInterface
*/
protected $entityFieldManager;
/**
* The language manager.
*
* @var \Drupal\Core\Language\LanguageManagerInterface
*/
protected $languageManager;
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The instantiated Cache backend.
*
* @var \Drupal\Core\Cache\CacheBackendInterface
*/
protected $cache;
/**
* The uuid generator object.
*
* @var \Drupal\Component\Uuid\UuidInterface
*/
protected $uuid;
/**
* Token service.
*
* @var \Drupal\Core\Utility\Token
*/
protected $token;
/**
* Constructs an UsernameGenerator object.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The configuration factory.
* @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
* The entity field manager.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache
* The cache backend.
* @param \Drupal\Component\Uuid\UuidInterface $uuid_service
* The UUID service.
* @param \Drupal\Core\Utility\Token $token
* The token service.
*/
public function __construct(ConfigFactoryInterface $config_factory, EntityFieldManagerInterface $entity_field_manager, LanguageManagerInterface $language_manager, ModuleHandlerInterface $module_handler, CacheBackendInterface $cache, UuidInterface $uuid_service, Token $token) {
$this->config = $config_factory->get('username.settings');
$this->entityFieldManager = $entity_field_manager;
$this->languageManager = $language_manager;
$this->moduleHandler = $module_handler;
$this->cache = $cache;
$this->uuid = $uuid_service;
$this->token = $token;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('config.factory'),
$container->get('entity_type.manager'),
$container->get('language_manager'),
$container->get('module_handler'),
$container->get('cache.default'),
$container->get('uuid'),
$container->get('token'),
);
}
/**
* Generates a random suffixed username.
*
* @return string
* The generated username.
*/
public function generateRandomUsername(): string {
return 'username_' . $this->uuid->generate();
}
/**
* Generates a username value.
*
* Determines what the new username could be, calling API hooks where
* applicable, and adding a number suffix if necessary.
*
* @param object $account
* The user account object.
*
* @return string
* The generated username.
*/
public function generateUsername($account) {
// Other modules may implement hook_username_auto_name($edit, $account) to
// generate a username (return a string to be used as the username, NULL to
// have username generate it).
$names = $this->moduleHandler->invokeAll('username_auto_name', [$account]);
// Remove any empty entries.
$names = array_filter($names);
if (empty($names)) {
// Default implementation of name generation.
$new_name = $this->patternProcessor($account);
}
else {
// One would expect a single implementation of the hook, but if there
// are multiples out there use the last one.
$new_name = array_pop($names);
}
// If no new name was found, return the display name.
if (empty($new_name)) {
return $account->getDisplayName();
}
return $new_name;
}
/**
* Processes account and assigns new username per current pattern.
*
* @param object $account
* The user object to process.
*
* @return string
* The new name for the user object.
*/
public function patternProcessor($account) {
$output = '';
$pattern = $this->config->get('token.pattern');
if (trim($pattern)) {
// Replace any tokens in the pattern.
$pattern_array = explode('\n', trim($pattern));
// Uses callback option to clean replacements. No sanitization.
$output = $this->token->replace($pattern, ['user' => $account], [
'clear' => TRUE,
'callback' => 'username_cleanup_token_values',
]);
// Check if the token replacement has not actually replaced any values.
// If that is the case, then stop because we should not generate a name.
$pattern_tokens_removed = preg_replace('/\[[^\s\]:]*:[^\s\]]*\]/', '', implode('\n', $pattern_array));
if ($output === $pattern_tokens_removed) {
return '';
}
}
return trim($output);
}
/**
* Cleans up a string segment to be used in a username.
*
* Performs various alterations:
* - Remove all HTML tags.
* - Process the string through the transliteration module.
* - Replace or remove punctuation with the separator character.
* - Remove backslashes.
* - Replace non-ascii and non-numeric characters with the separator.
* - Remove common words.
* - Replace whitespace with the separator character.
* - Trim duplicate, leading, and trailing separators.
* - Convert to lower-case.
* - Shorten to a desired length and logical position based on word
* boundaries.
*
* @param string $string
* A string to clean.
*
* @return string
* The cleaned string.
*/
public function cleanUsernameString(string $string): string {
// Since this is often called, use the advanced drupal_static() pattern.
static $drupal_static_fast;
if (!isset($drupal_static_fast)) {
$drupal_static_fast['cache'] = &drupal_static(__FUNCTION__);
}
$cache = &$drupal_static_fast['cache'];
// Generate and cache variables used in this function so that on the second
// call to cleanUsernameString() we focus on processing.
if (!isset($cache)) {
$cache = [
'lowercase' => (bool) $this->config->get('token.lowercase'),
'whitespace' => (bool) $this->config->get('token.whitespace'),
'punctuation' => [],
'separator' => '_',
'ascii' => FALSE,
'maxlength' => min(60, self::getNameMaxLength()),
];
// Generate and cache the punctuation replacements for strtr().
$punctuation = $this->getPunctuationChars();
foreach ($punctuation as $details) {
$cache['punctuation'][$details['value']] = '';
}
}
// Empty strings do not need any processing.
if ($string === '' || $string === NULL) {
return '';
}
// Remove all HTML tags from the string.
$output = strip_tags(Html::decodeEntities($string));
// Replace or drop punctuation based on user settings.
$output = strtr($output, $cache['punctuation']);
// Reduce strings to letters and numbers.
if ($cache['ascii']) {
$output = preg_replace('/[^a-zA-Z0-9\/]+/', $cache['separator'], $output);
}
// Replace whitespace with the separator.
if ($cache['whitespace']) {
$output = preg_replace('/\s+/', $cache['separator'], $output);
}
// Trim duplicates and remove trailing and leading separators.
$output = $this->cleanUsernameSeparator($output, $cache['separator']);
// Optionally convert to lower case.
if ($cache['lowercase']) {
$output = mb_strtolower($output);
}
// Shorten to a logical place based on word boundaries.
return Unicode::truncate($output, $cache['maxlength'], TRUE);
}
/**
* Trims duplicate, leading, and trailing separators from a string.
*
* @param string $string
* The string to clean separators from.
* @param string $separator
* The separator to use when cleaning.
*
* @return string
* The cleaned version of the string.
*/
public function cleanUsernameSeparator(string $string, string $separator = NULL): string {
static $default_separator;
if (!isset($separator)) {
if (!isset($default_separator)) {
$default_separator = '_';
}
$separator = $default_separator;
}
$output = $string;
// Clean duplicate or trailing separators.
if (strlen($separator)) {
// Escape the separator.
$seppattern = preg_quote($separator, '/');
// Trim any leading or trailing separators.
$output = preg_replace("/^$seppattern+|$seppattern+$/", '', $output);
// Replace trailing separators around slashes.
if ($separator !== '/') {
$output = preg_replace("/$seppattern+\/|\/$seppattern+/", "/", $output);
}
// Replace multiple separators with a single one.
$output = preg_replace("/$seppattern+/", $separator, $output);
}
return $output;
}
/**
* Returns an array of arrays for punctuation values.
*
* Returns an array of arrays for punctuation values keyed by a name,
* including the value and a textual description.
*
* @return array
* An array of arrays for punctuation values keyed by a name, including the
* value and a textual description.
*/
public function getPunctuationChars(): array {
$punctuation = &drupal_static(__FUNCTION__);
$language = $this->languageManager->getCurrentLanguage()->getId();
if (!isset($punctuation)) {
$cid = 'username:punctuation:' . $language;
if ($cache = $this->cache->get($cid)) {
$punctuation = $cache->data;
}
else {
$punctuations = [
'double_quotes' => ['"', 'Double quotation marks'],
'quotes' => ["'", "Single quotation marks (apostrophe)"],
'backtick' => ['`', 'Back tick'],
'comma' => [',', 'Comma'],
'period' => ['.', 'Period'],
'hyphen' => ['-', 'Hyphen'],
'underscore' => ['_', 'Underscore'],
'colon' => [':', 'Colon'],
'semicolon' => [';', 'Semicolon'],
'pipe' => ['|', 'Vertical bar (pipe)'],
'left_curly' => ['{', 'Left curly bracket'],
'left_square' => ['[', 'Left square bracket'],
'right_curly' => ['}', 'Right curly bracket'],
'right_square' => [']', 'Right square bracket'],
'plus' => ['+', 'Plus sign'],
'equal' => ['=', 'Equal sign'],
'asterisk' => ['*', 'Asterisk'],
'ampersand' => ['&', 'Ampersand'],
'percent' => ['%', 'Percent sign'],
'caret' => ['^', 'Caret'],
'dollar' => ['$', 'Dollar sign'],
'hash' => ['#', 'Number sign (pound sign, hash)'],
'at' => ['@', 'At sign'],
'exclamation' => ['!', 'Exclamation mark'],
'tilde' => ['~', 'Tilde'],
'left_parenthesis' => ['(', 'Left parenthesis'],
'right_parenthesis' => [')', 'Right parenthesis'],
'question_mark' => ['?', 'Question mark'],
'less_than' => ['<', 'Less-than sign'],
'greater_than' => ['>', 'Greater-than sign'],
'slash' => ['/', 'Slash'],
'back_slash' => ['\\', 'Backslash'],
];
$punctuation = [];
foreach ($punctuations as $name => $mark) {
$punctuation[$name] = [
'value' => $mark[0],
'name' => $this->t('@punctuation_mark', ['@punctuation_mark' => $mark[1]]),
];
}
// Allow modules to alter the punctuation list and cache the result.
$this->moduleHandler->alter('getPunctuationChars', $punctuation);
$this->cache->set($cid, $punctuation);
}
}
return $punctuation;
}
/**
* Fetches the maximum length of the {users}.name field from the schema.
*
* @return int
* The maximum username length allowed by the database.
*/
public function getNameMaxLength(): int {
$maxlength = &drupal_static(__FUNCTION__);
if (!isset($maxlength)) {
$field_definitions = $this->entityFieldManager->getFieldDefinitions('user', 'user');
$name_settings = $field_definitions['name']->getItemDefinition()->getSettings();
$maxlength = $name_settings['max_length'] ?? 255;
}
return $maxlength;
}
}
