username-1.0.x-dev/username.module
username.module
<?php
/**
* @file
* Exposes global functionality for username on user form.
*/
use Drupal\Component\Utility\Crypt;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Asset\AttachedAssetsInterface;
use Drupal\Core\Config\ImmutableConfig;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Link;
use Drupal\Core\Render\BubbleableMetadata;
use Drupal\Core\Render\Element\Email;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Url;
use Drupal\user\Entity\User;
use Drupal\user\UserInterface;
/**
* Implements hook_preprocess_HOOK().
*/
function username_preprocess_page_title(array &$variables) {
// Get the current route name.
$route_name = \Drupal::routeMatch()->getRouteName();
$user_routes = [
'user.login',
'user.register',
'user.pass',
];
// Check if the current route is one of the user routes.
if (in_array($route_name, $user_routes)) {
// Load the username settings configuration.
$config = \Drupal::config('username.settings');
$override = $config->get('override');
$title = $config->get('title');
// Override the page title based on the route and configuration settings.
if ($route_name == 'user.login' && $override['login']) {
$variables['title'] = !empty($title['login']) ? $title['login'] : t('Log in');
}
if ($route_name == 'user.register' && $override['register']) {
$variables['title'] = !empty($title['register']) ? $title['register'] : t('Create new account');
}
if ($route_name == 'user.pass' && $override['reset']) {
$variables['title'] = !empty($title['reset']) ? $title['reset'] : t('Reset your password');
}
}
}
/**
* Implements hook_menu_local_tasks_alter().
*
* Override tabs from username settings.
*/
function username_menu_local_tasks_alter(array &$data, $route_name) {
// Load the username settings configuration.
$config = \Drupal::config('username.settings');
$override = $config->get('override');
$title = $config->get('title');
// Override the tabs titles based on the route and configuration settings.
if ($route_name == 'user.login' || $route_name == 'user.register' || $route_name == 'user.pass') {
if ($override['login'] && isset($data['tabs'][0]['user.login']['#link']["title"])) {
$data['tabs'][0]['user.login']['#link']["title"] = !empty($title['login']) ? $title['login'] : t('Log in');
}
if ($override['register'] && isset($data['tabs'][0]['user.register']['#link']["title"])) {
$data['tabs'][0]['user.register']['#link']["title"] = !empty($title['register']) ? $title['register'] : t('Create new account');
}
if ($override['reset'] && isset($data['tabs'][0]['user.pass']['#link']["title"])) {
$data['tabs'][0]['user.pass']['#link']["title"] = !empty($title['reset']) ? $title['reset'] : t('Reset your password');
}
}
}
/**
* Implements hook_js_alter().
*/
function username_js_alter(array &$javascript, AttachedAssetsInterface $assets) {
$module_path = \Drupal::service('extension.list.module')->getPath('username');
$username_js = $module_path . '/js/username.js';
$usermask_js = $module_path . '/js/username.mask.js';
// Move username.js to be loaded after user.js file.
if (isset($javascript[$usermask_js])) {
$javascript[$usermask_js]['group'] = -99;
}
if (isset($javascript[$username_js])) {
$javascript[$username_js]['group'] = -90;
}
}
/**
* Implements hook_theme().
*/
function username_theme($existing, $type, $theme, $path) {
return [
'input__password' => [
'render element' => 'element',
'template' => 'input--password',
'path' => $path . '/templates',
],
];
}
/**
* Implements hook_ENTITY_TYPE_presave().
*/
function username_user_presave(UserInterface $account) {
$config = \Drupal::config('username.settings');
$username = array_filter($config->get('username'));
// Generate a username if the name field is not enabled.
if (!isset($username['name'])) {
$auto = $config->get('auto');
switch ($auto) {
case 'mail':
$new_name = username_strip_mail_and_cleanup($account->getEmail());
break;
case 'hash':
$new_name = Crypt::hashBase64($account->getEmail());
break;
case 'token':
$new_name = \Drupal::service('username.username_generator')->generateUsername($account);
break;
}
// Allow other modules to alter the username.
\Drupal::moduleHandler()->alter('username_name', $new_name, $account);
// Ensure the username is unique.
$new_name = username_unique_username($new_name, (int) $account->id());
$account->setUsername($new_name);
}
}
/**
* Makes the username unique.
*
* Given a starting point for a Drupal username (e.g., the name portion of an
* email address), return a legal, unique Drupal username. This function is
* designed to work on the results of the /user/register or /admin/people/create
* forms which have already called user_validate_name, valid_email_address,
* or a similar function. If your custom code is creating users, you should
* ensure that the email/name is already validated using something like that.
*
* @param string $name
* A name from which to base the final username. May contain illegal
* characters; these will be stripped.
* @param int $uid
* (optional) Uid to ignore when searching for a unique username
* (e.g., if we update the username after the {users} row is inserted).
*
* @return string
* A unique username based on $name.
*
* @see user_validate_name()
*/
function username_unique_username($name, $uid = 0) {
// Iterate until we find a unique name.
$i = 0;
$database = \Drupal::database();
do {
$new_name = empty($i) ? $name : $name . '_' . $i;
$found = $database->queryRange("SELECT uid from {users_field_data} WHERE uid <> :uid AND name = :name", 0, 1, [
':uid' => $uid,
':name' => $new_name,
])->fetchAssoc();
$i++;
} while (!empty($found));
return $new_name;
}
/**
* Cleans up a username.
*
* Run username sanitation, e.g., replace two or more spaces
* with a single underscore, and strip illegal characters.
*
* @param string $name
* The username to be cleaned up.
*
* @return string
* Cleaned up username.
*/
function username_cleanup_username($name) {
// Strip illegal characters.
$name = preg_replace('/[^\x{80}-\x{F7} a-zA-Z0-9@_.\'-]/u', '', $name);
// Strip leading and trailing spaces.
$name = trim($name);
// Convert any other series of spaces to a single underscore.
$name = preg_replace('/\s+/', '_', $name);
// If there's nothing left, use a default.
$name = ('' === $name) ? t('user') : $name;
// Truncate to a reasonable size.
$name = (mb_strlen($name) > (UserInterface::USERNAME_MAX_LENGTH - 10)) ? mb_substr($name, 0, UserInterface::USERNAME_MAX_LENGTH - 11) : $name;
return $name;
}
/**
* Strips down the email address to a username and cleans it up.
*
* Strips everything after and including the '@' and cleans the username (e.g.,
* stripping illegal characters, spaces, etc.).
*
* @param string $mail
* The email address to be stripped and cleaned up.
*
* @return string
* A username converted from an email address.
*/
function username_strip_mail_and_cleanup($mail) {
// Strip the email.
$username = preg_replace('/@.*$/', '', $mail);
// Clean up the resulting username.
return username_cleanup_username($username);
}
/**
* Clean token values.
*
* Callback from token replacement service.
*
* @param array $replacements
* An array of token replacements that need to be cleaned.
*/
function username_cleanup_token_values(array &$replacements) {
foreach ($replacements as $token => $value) {
$replacements[$token] = \Drupal::service('username.username_generator')->cleanUsernameString($value);
}
}
/**
* Detects the input type of the username.
*
* @param string $username
* The input username.
*
* @return string
* The detected type of the input username.
*/
function username_detect_input_type($username) {
$email_pattern = '/^\w{2,}@\w{2,}\.\w{2,4}$/';
$mobile_pattern = "/^[7-9][0-9]{9}$/";
if (preg_match($email_pattern, $username)) {
$detected_type = "email";
}
elseif (preg_match($mobile_pattern, $username)) {
$detected_type = 'phone';
}
else {
$detected_type = 'name';
}
return $detected_type;
}
/**
* Implements hook_form_BASE_FORM_ID_alter().
*/
function username_form_user_form_alter(array &$form, FormStateInterface $form_state) {
// Load configuration from username settings.
$config = \Drupal::config('username.settings');
$username = array_filter($config->get('username'));
$usernames = username_options();
// Load override settings for register button and messages.
$override = $config->get('override');
$message = $config->get('message');
$button = $config->get('button');
// Check if the name and mail fields exist in the form.
if (isset($form['account']['name']) && isset($form['account']['mail'])) {
// Check if 'name' is part of the enabled usernames.
$name_enabled = in_array('name', $username);
// Make the mail field always required with this module.
$form['account']['mail']['#required'] = TRUE;
$currentUser = \Drupal::currentUser();
/** @var \Drupal\user\UserInterface $user */
$user = $form_state->getFormObject()->getEntity();
// Check if the current user has the permission to edit the username.
$hasUsernameEditPermissions = ($currentUser->hasPermission('change own username') && $currentUser->id() === $user->id()) || $currentUser->hasPermission('administer users');
// Disable the username field if it is not active
// and the user doesn't have the right permissions.
if (!$name_enabled && !$hasUsernameEditPermissions) {
$form['account']['name']['#type'] = 'value';
}
// If the user is new and the username field is
// disabled, ensure we are on the user creation page.
if (!$name_enabled && $user->isNew()) {
// Change the form validation to allow an empty username.
array_unshift($form['#validate'], 'username_validate_user_form_name');
$form['account']['name']['#required'] = FALSE;
// Add a warning if the user has
// permission to edit the username field.
if ($hasUsernameEditPermissions) {
$primary = $config->get('selective.primary');
$usernames = array_splice($usernames, 1);
if (isset($usernames[$primary])) {
$primary_name = strtolower($usernames[$primary]);
$form['account']['name']['#description'] = t('Leave empty to generate the username from the @primary_name.', ['@primary_name' => $primary_name]) . '<br>' . $form['account']['name']['#description'];
}
else {
$form['account']['name']['#description'] = t('Leave empty to generate the username automatically by the system.') . '<br>' . $form['account']['name']['#description'];
}
}
}
}
// Check if the current route is an admin route.
$is_admin = \Drupal::service('router.admin_context')->isAdminRoute();
if (!$is_admin && $override['register']) {
// Add a custom message to the form if configured.
if (!empty($message['register'])) {
$form['#markup'] = '<p>' . $message['register'] . '</p>';
}
// Override the default register button label.
$form['actions']['submit']['#value'] = !empty($button['register']) ? $button['register'] : t('Create new account');
}
// Attach username options and admin status to the form.
$form['#attached']['drupalSettings']['username']['options'] = $config->get('options');
$form['#attached']['drupalSettings']['username']['isAdmin'] = $is_admin;
// Add username restriction mask and options if enabled.
if ($config->get('restriction.enabled')) {
// Change the username field restriction description.
username_change_restricts_description($form, $config);
// Attach the username mask library.
$form['#attached']['library'][] = 'username/username.mask';
$form['#attached']['drupalSettings']['username']['masks'] = $config->get('restriction.mask');
}
// Add init class for username JavaScript to the user form page.
$form['#attributes']['class'][] = 'username-init';
// Attach the username form library.
$form['#attached']['library'][] = 'username/username';
}
/**
* The username_form_user_form_alter validation callback.
*
* This is used to set a temporary username if the username is empty, since
* core doesn't allow us to have an empty username.
*
* @param array $form
* The form array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state object.
*/
function username_validate_user_form_name(array &$form, FormStateInterface $form_state) {
$username = $form_state->getValue('name');
if (empty($username)) {
// Create a temporary username.
$form_state->setValue('name', \Drupal::service('username.username_generator')->generateRandomUsername());
}
}
/**
* Implements hook_form_FORM_ID_alter().
*/
function username_form_user_login_form_alter(array &$form, FormStateInterface $form_state) {
// Load configuration from username settings.
$config = \Drupal::config('username.settings');
$username = array_filter($config->get('username'));
// Load selective username settings.
$selective = $config->get('selective.enabled');
$primary = $config->get('selective.primary');
$switch = $config->get('selective.switch');
// Load override settings for login label and description.
$override = $config->get('override');
$message = $config->get('message');
$button = $config->get('button');
// Set custom username validation.
$form['name']['#element_validate'][] = 'username_user_login_validate';
// Remove username autofocus if disabled in the configuration.
if (!$config->get('options.autofocus') && isset($form['name']['#attributes']['autofocus'])) {
unset($form['name']['#attributes']['autofocus']);
}
// Check if selective username is configured and more
// than one username is activated. Show options for
// selecting which username to use for login.
if ($selective && count($username) > 1) {
$usernames = array_intersect_key(username_options(), $username);
$form['username'] = [
'#type' => 'radios',
'#options' => $usernames,
'#default_value' => $primary,
'#weight' => -999,
];
}
// Only email as a username with override label and description.
if (count($username) == 1 && isset($username['mail'])) {
// Change name field parameters to email format.
$form['name']['#type'] = 'email';
$form['name']['#maxlength'] = Email::EMAIL_MAX_LENGTH;
}
// Both username and email as a username with override label and description.
if (count($username) == 2 && isset($username['name']) && isset($username['mail'])) {
// Create email input field separately if selective username is configured.
if ($selective) {
$form['mail'] = [
'#type' => 'email',
'#title' => t('Email address'),
'#weight' => 0,
'#states' => [
'invisible' => [
':input[name="username"]' => ['value' => 'name'],
],
'required' => [
':input[name="username"]' => ['value' => 'mail'],
],
],
];
// Set name field to not required and handle it with states condition.
$form['name']['#required'] = FALSE;
$form['name']['#states'] = [
'invisible' => [
':input[name="username"]' => ['value' => 'mail'],
],
'required' => [
':input[name="username"]' => ['value' => 'name'],
],
];
}
}
// Set username labels.
username_set_label($form);
// Set username placeholders.
username_set_placeholder($form);
// Set username descriptions.
username_set_description($form);
// Add message to login form.
if ($override['login'] && !empty($message['login'])) {
$form['#markup'] = '<p>' . $message['login'] . '</p>';
}
// Override login button text.
$form['actions']['submit']['#value'] = $override['login'] && !empty($button['login']) ? $button['login'] : t('Log in');
// Get username options and attach them to the form.
$form['#attached']['drupalSettings']['username']['options'] = $config->get('options');
// Add username restriction mask and options if enabled.
if ($config->get('restriction.enabled')) {
// Ensure username field is available for restriction.
if ((count($username) == 1 && isset($username['name'])) || ($selective && isset($username['name']))) {
$form['#attached']['library'][] = 'username/username.mask';
$form['#attached']['drupalSettings']['username']['masks'] = $config->get('restriction.mask');
}
}
// Add init class for username JavaScript to login form.
$form['#attributes']['class'][] = 'username-init';
// Attach username form library.
$form['#attached']['library'][] = 'username/username';
// Attach switch username library if selective
// username is enabled and switch is configured.
if ($selective && $switch != '') {
$form['#attributes']['class'][] = 'username-' . strtr($switch, '_', '-');
$form['#attached']['library'][] = 'username/username.style';
}
// Ensure the login form cache is invalidated when the setting changes.
$form['#cache']['tags'][] = 'config:username.settings';
}
/**
* Form element validation handler for the user login form.
*
* Allows users to authenticate by email, which is our preferred method.
*
* @param array $form
* The form array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state object.
*/
function username_user_login_validate(array &$form, FormStateInterface $form_state) {
// Get all form values.
$values = $form_state->getValues();
// Load the username settings configuration.
$config = \Drupal::config('username.settings');
// Filter the username settings.
$usernames = array_filter($config->get('username'));
// Check if email is enabled for login.
if (isset($usernames['mail'])) {
// Determine the field to use for
// email (selective usernames) or default to 'name'.
$field = isset($values['mail']) ? 'mail' : 'name';
$email = $values['mail'] ?? $values['name'];
if (!empty($email)) {
// Attempt to load the user by email address.
$user = user_load_by_mail($email);
// If a user is found, the input is a valid email address.
if ($user) {
// Check if the account is active.
if (!$user->isActive()) {
// Set an error if the account is not activated or blocked.
$form_state->setErrorByName(
$field,
t('The account associated with the email address %email is not activated or has been blocked.', ['%email' => $email])
);
}
else {
// Set the 'name' field with the account name obtained from the email.
$form_state->setValue('name', $user->getAccountName());
}
}
elseif (count($usernames) == 1 || isset($values['mail'])) {
// If the user is not found and email login is enabled, set an error.
$user_input = $form_state->getUserInput();
$query = isset($user_input[$field]) ? ['name' => $user_input[$field]] : [];
$form_state->setErrorByName(
$field,
t('Unrecognized email address or password. <a href=":password">Forgot your password?</a>', [
':password' => Url::fromRoute('user.pass', [], ['query' => $query])->toString(),
])
);
}
}
}
}
/**
* Implements hook_form_FORM_ID_alter().
*
* Alters the user password reset form.
*/
function username_form_user_pass_alter(array &$form, FormStateInterface $form_state) {
// Load the username settings configuration.
$config = \Drupal::config('username.settings');
$usernames = array_filter($config->get('username'));
// Load various configuration options.
$override = $config->get('override');
$label = $config->get('label');
$button = $config->get('button');
$message = $config->get('message');
// Remove username autofocus if disabled in the configuration.
if (!$config->get('options.autofocus') && isset($form['name']['#attributes']['autofocus'])) {
unset($form['name']['#attributes']['autofocus']);
}
// Convert text fields to email fields if only email username is enabled.
if (count($usernames) == 1 && isset($usernames['mail'])) {
// Set the title for the email field based on the configuration.
$form['name']['#title'] = $override['username'] && !empty($label['username']) ? $label['username'] : t('Email address');
// Change the field type to email if it's a text field.
if ($form['name']['#type'] === 'textfield') {
$form['name']['#type'] = 'email';
$form['name']['#maxlength'] = Email::EMAIL_MAX_LENGTH;
}
}
// Change the title for the username or email field if both are enabled.
if (count($usernames) == 2 && isset($usernames['name']) && isset($usernames['mail'])) {
$form['name']['#title'] = $override['username'] && !empty($label['username']) ? $label['username'] : t('Username or email address');
}
// Add a message to the reset password form if configured.
if ($override['reset'] && !empty($message['reset'])) {
$form['#markup'] = '<p>' . $message['reset'] . '</p>';
}
// Set the submit button text based on the configuration.
$form['actions']['submit']['#value'] = $override['reset'] && !empty($button['reset']) ? $button['reset'] : t('Submit');
// Attach username options to the form.
$form['#attached']['drupalSettings']['username']['options'] = $config->get('options');
// Attach restriction masks if enabled in the configuration.
if ($config->get('restriction.enabled')) {
$form['#attached']['library'][] = 'username/username.mask';
$form['#attached']['drupalSettings']['username']['masks'] = $config->get('restriction.mask');
}
// Attach the username form library.
$form['#attached']['library'][] = 'username/username';
// Check if the password reset prevention is enabled in the configuration.
if ($config->get('prevention')) {
// Add custom validation handler for password reset.
$form['#validate'][] = 'username_user_pass_validate';
// Override the core submit handler for password reset.
$key_submit = array_search('::submitForm', $form['#submit']);
if ($key_submit !== FALSE) {
$form['#submit'][$key_submit] = 'username_user_pass_submit';
}
}
}
/**
* Overrides user_pass_validate() found in user.pages.inc.
*
* Validates the username or email for password reset.
*
* @param array $form
* The form array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state object.
*/
function username_user_pass_validate(array &$form, FormStateInterface $form_state) {
// Get the trimmed value of the username or email.
$name = trim($form_state->getValue('name'));
// Try to load the user account by email.
$account = user_load_by_mail($name);
if (empty($account)) {
// If not found by email, try to load by username.
$account = user_load_by_name($name);
}
// Check if the account is active and set it in the form state.
if ($account && $account->id() && $account->isActive()) {
$form_state->setValueForElement(['#parents' => ['account']], $account);
}
else {
// Log a notice if a blocked user is attempting to reset the password.
\Drupal::logger('username')->notice('Blocked user attempting to reset password.');
}
// Set a flag in the form state to indicate if the username is blocked.
$form_state->set('username_blocked', !empty($form_state->getErrors()));
// Clear errors so they are not displayed to the end-user.
$form_state->clearErrors();
}
/**
* Overrides the user_pass_submit() found in user.pages.inc.
*
* Submits the password reset request.
*
* @param array $form
* The form array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state object.
*/
function username_user_pass_submit(array &$form, FormStateInterface $form_state) {
// Get the user account from the form state.
$account = $form_state->getValue('account');
// Check if the account is set and the username is not blocked.
if (isset($account) && !$form_state->get('username_blocked')) {
// Get the current language code.
$langcode = \Drupal::languageManager()->getCurrentLanguage()->getId();
// Mail one-time login URL and instructions using the current language.
$mail = _user_mail_notify('password_reset', $account, $langcode);
if (!empty($mail)) {
// Log a notice when the password reset instructions are mailed.
\Drupal::logger('username')->notice('Password reset instructions mailed to %name at %email.', [
'%name' => $account->getAccountName(),
'%email' => $account->getEmail(),
]);
}
}
// Display a message to the user.
\Drupal::messenger()->addMessage(t('If the username or email address exists and is active, further instructions have been sent to your email address.'));
// Redirect to the user login page.
$form_state->setRedirect('user.page');
}
/**
* Implements hook_form_FORM_ID_alter().
*
* Ensures usernames are not exposed when spoofing a reset password URL.
*/
function username_form_user_pass_reset_alter(array &$form, FormStateInterface $form_state, $form_id) {
// Alter the form to include a message indicating a one-time login.
$form['message'] = [
'#markup' => t('<p>This is a one-time login.</p><p>Click on this button to log in to the site and change your password.</p>'),
];
}
/**
* @defgroup username Display name API
*/
/**
* Implements hook_entity_extra_field_info().
*/
function username_entity_extra_field_info() {
// Define an extra field for the 'user' entity type to display name.
$fields['user']['user']['display']['displayname'] = [
'label' => t('Display name'),
'description' => t('Display name'),
'weight' => -1,
'visible' => FALSE,
];
return $fields;
}
/**
* Implements hook_user_format_name_alter().
*/
function username_user_format_name_alter(&$name, AccountInterface $account) {
static $in_username_alter = FALSE;
$uid = $account->id();
// Don't alter anonymous users or objects that do not have any user ID.
if (empty($uid)) {
return;
}
// Use the already loaded/generated display name if available.
if (isset($account->displayname)) {
if (mb_strlen($account->displayname)) {
// Only alter $name if displayname is a non-empty string.
$name = $account->displayname;
}
return;
}
// If the display name wasn't available, generate it.
// Prevent recursion caused by tokens calling format_username().
if (!$in_username_alter) {
$in_username_alter = TRUE;
// Ensure the user account object is properly loaded.
if ($displayname_account = User::load($uid)) {
username_user_format_name_alter($name, $displayname_account);
}
$in_username_alter = FALSE;
}
}
/**
* Implements hook_ENTITY_TYPE_load() for user entities.
*/
function username_user_load(array $accounts) {
// Load display names for the given user accounts.
$displaynames = username_display_name_load_multiple($accounts);
foreach ($displaynames as $uid => $displayname) {
// Attach the display name to the user account object.
$accounts[$uid]->displayname = $displayname;
}
}
/**
* Implements hook_ENTITY_TYPE_update() for user entities.
*/
function username_user_update(EntityInterface $account) {
// Since user data may have changed, update the display name and its cache.
$displaynames = &drupal_static('username_display_name_load_multiple', []);
$displaynames[$account->id()] = username_display_name_update($account);
}
/**
* Implements hook_ENTITY_TYPE_delete() for user entities.
*/
function username_user_delete(EntityInterface $account) {
// Delete the display name for the given user ID.
username_display_name_delete($account->id());
}
/**
* Implements hook_ENTITY_TYPE_view() for user entities.
*/
function username_user_view(array &$build, EntityInterface $account, EntityViewDisplayInterface $display, $view_mode) {
// Check if the 'displayname' component is enabled in the view display.
if ($display->getComponent('displayname')) {
// Check if the user has access to view the display name.
if ($account->access('view')) {
// Create a URL to the user's canonical page.
$url = Url::fromRoute('entity.user.canonical', ['user' => $account->id()]);
$markup = Link::fromTextAndUrl($account->displayname, $url)->toString();
}
else {
// Escape the display name for safe output.
$markup = Html::escape($account->displayname);
}
// Build the render array for the display name field.
$build['displayname'] = [
'#theme' => 'field',
'#title' => t('Display name'),
'#label_display' => 'inline',
'#view_mode' => '_custom',
'#field_name' => 'displayname',
'#field_type' => 'text',
'#field_translatable' => FALSE,
'#entity_type' => 'custom',
'#bundle' => 'custom',
'#object' => $account,
'#items' => [TRUE],
'#is_multiple' => FALSE,
0 => [
'#markup' => $markup,
],
];
}
}
/**
* @addtogroup username Display name
* @{
*/
/**
* Loads a display name.
*
* @param \Drupal\user\Entity\User $account
* A user account object.
*
* @return mixed
* The user's generated display name.
*/
function username_display_name_load(User $account) {
// Load multiple display names for the provided user account.
$usernames = username_display_name_load_multiple([$account->id() => $account]);
// Return the first display name found.
return reset($usernames);
}
/**
* Loads multiple display names.
*
* @param array $accounts
* An array of user account objects keyed by user ID.
*
* @return array
* An array of display names keyed by user ID.
*/
function username_display_name_load_multiple(array $accounts): array {
// Use static caching for display names.
$displaynames = &drupal_static(__FUNCTION__, []);
// Find accounts that need to be loaded.
if ($new_accounts = array_diff_key($accounts, $displaynames)) {
// Fetch display names from the database for new accounts.
$displaynames += \Drupal::database()->query(
"SELECT uid, name FROM {user__names} WHERE uid IN (:uids[])",
[':uids[]' => array_keys($new_accounts)]
)->fetchAllKeyed();
// Generate display names for accounts not found in the database.
foreach ($new_accounts as $uid => $account) {
if (!isset($displaynames[$uid])) {
$displaynames[$uid] = username_display_name_update($account);
}
}
}
// Return the display names for the provided accounts.
return array_intersect_key($displaynames, $accounts);
}
/**
* Update the display name for a user account.
*
* @param \Drupal\user\Entity\User $account
* A user account object.
*
* @return string
* A string with the display name.
*
* @see hook_username_display_name_pattern_alter()
* @see hook_username_display_name_alter()
* @see hook_username_display_name_update()
*/
function username_display_name_update(User $account): string {
// Load the username settings configuration.
$config = \Drupal::config('username.settings');
$displayname = '';
// Check if display names are enabled in the configuration.
if ($config->get('display')) {
// Get the replacement method from the configuration.
$replace = $config->get('replace');
// Check if the user is not anonymous.
if (!$account->isAnonymous()) {
// Get the default pattern and allow other modules to alter it.
if ($replace === 'mail') {
// Use the email address to generate the display name.
$displayname = username_strip_mail_and_cleanup($account->getEmail());
}
else {
// Get the custom pattern from the configuration.
$pattern = $config->get('custom');
if ($replace !== 'custom') {
// Allow other modules to alter the display name pattern.
\Drupal::moduleHandler()->alter('username_display_name_pattern', $pattern, $account);
}
// Perform token replacement on the display name pattern.
$displayname = \Drupal::token()->replace($pattern, ['user' => $account], [
'clear' => TRUE,
'sanitize' => FALSE,
], new BubbleableMetadata());
// Process the pattern with TWIG.
$twig_service = \Drupal::service('twig');
$displayname = $twig_service->renderInline($displayname);
// Remove any HTML tags.
$displayname = strip_tags(Html::decodeEntities($displayname));
// Remove double spaces (if a token had no value).
$displayname = preg_replace('/ {2,}/', ' ', $displayname);
// Allow other modules to alter the generated username display name.
\Drupal::moduleHandler()->alter('username_display_name', $displayname, $account);
// Trim the display name to 255 characters.
$displayname = Unicode::truncate(trim($displayname), 255);
}
}
else {
// Display name cannot be generated with tokens for anonymous users.
$displayname = $account->label();
}
// Save the display name to the database and the static cache.
\Drupal::database()->merge('user__names')
->key(['uid' => $account->id()])
->fields([
'name' => $displayname,
'created' => \Drupal::time()->getRequestTime(),
])
->execute();
// Allow modules to react to the username display name being updated.
\Drupal::moduleHandler()->invokeAll('username_display_name_update', [
$displayname,
$account,
]);
// Clear the entity cache for the user.
\Drupal::entityTypeManager()->getStorage('user')->resetCache([$account->id()]);
}
// Return the generated display name.
return $displayname;
}
/**
* Delete a display name.
*
* @param int $uid
* A user ID.
*/
function username_display_name_delete(int $uid): void {
// Call the function to delete multiple display names with a single user ID.
username_display_name_delete_multiple([$uid]);
}
/**
* Delete multiple display names.
*
* @param array $uids
* An array of user IDs.
*/
function username_display_name_delete_multiple(array $uids): void {
// Delete the entries from the 'user__names' table for the given user IDs.
\Drupal::database()->delete('user__names')
->condition('uid', $uids, 'IN')
->execute();
// Reset the static cache.
drupal_static_reset('username_display_name_load_multiple');
// Reset the cache for the user entities for the given user IDs.
\Drupal::entityTypeManager()->getStorage('user')->resetCache($uids);
}
/**
* Delete all display names.
*/
function username_display_name_delete_all(): void {
// Truncate the 'user__names' table to delete all entries.
\Drupal::database()->truncate('user__names')->execute();
// Reset the static cache.
drupal_static_reset('username_display_name_load_multiple');
// Reset the cache for all user entities.
\Drupal::entityTypeManager()->getStorage('user')->resetCache();
}
/**
* @} End of "addtogroup username Display name".
*/
/**
* Set the username field labels.
*
* @param array $form
* An associative array containing the structure of the form.
*
* @return array
* The updated entity bundle form array.
*/
function username_set_label(array &$form): array {
// Load the username settings configuration.
$config = \Drupal::config('username.settings');
// Get the username modes from the configuration.
$username_modes = array_filter($config->get('username'));
// Get the username options.
$usernames = username_options();
// Get the override settings from the configuration.
$override = $config->get('override');
// Get the label settings from the configuration.
$label = $config->get('label');
// Check if selective username label is enabled.
$selective = $config->get('selective.enabled');
// Set username labels if selective is enabled.
if ($selective) {
if ($override['username']) {
$username_label = $label['username'];
if (empty($username_label)) {
// Set the title display to 'invisible' if the label is empty.
foreach ($usernames as $field_name => $username) {
if (isset($form[$field_name])) {
$form[$field_name]['#title_display'] = 'invisible';
}
}
}
else {
// Split the label if it contains '%%'.
if (strpos($username_label, '%%') !== FALSE) {
$username_label = explode('%%', $username_label);
}
// Set the username labels for each username field.
$i = 0;
foreach (array_keys($usernames) as $field_name) {
if (isset($form[$field_name])) {
if (is_array($username_label)) {
if (isset($username_label[$i])) {
$form[$field_name]['#title'] = t('@$username_label', ['@$username_label' => trim($username_label[$i])]);
$i++;
}
}
else {
$form[$field_name]['#title'] = t('@$username_label', ['@$username_label' => $username_label]);
}
}
}
}
}
else {
// Set default username labels if not overridden.
foreach ($usernames as $field_name => $username) {
if (isset($form[$field_name])) {
$username_list = strtolower($username->getUntranslatedString());
$form[$field_name]['#title'] = t('@$username_label', ['@$username_label' => ucfirst($username_list)]);
}
}
}
}
else {
// Check if override for username label is enabled.
if ($override['username']) {
if (empty($label['username'])) {
$form['name']['#title_display'] = 'invisible';
}
else {
$form['name']['#title'] = t('@$username_label', ['@$username_label' => $label['username']]);
}
}
else {
// Set default username label if not overridden.
if (isset($form['name']['#title']) && !empty(trim($form['name']['#title']))) {
$username_list = '';
$i = 0;
$modes = count($username_modes);
// Create a list of username modes separated by commas and "or".
foreach ($username_modes as $username) {
$username_list .= strtolower($usernames[$username]->getUntranslatedString());
$username_list .= (++$i === $modes ? '' : ($modes - $i === 1 ? ' or ' : ', '));
}
$form['name']['#title'] = t('@$username_label', ['@$username_label' => ucfirst($username_list)]);
}
}
}
// Set password label if overridden.
if ($override['password']) {
$password_label = $label['password'];
if (empty($password_label)) {
$form['pass']['#title_display'] = 'invisible';
}
else {
$form['pass']['#title'] = t('@password_label', ['@password_label' => $password_label]);
}
}
// Return the updated form array.
return $form;
}
/**
* Set the username field placeholders.
*
* @param array $form
* An associative array containing the structure of the form.
*
* @return array
* The updated entity bundle form array.
*/
function username_set_placeholder(array &$form): array {
// Load the username settings configuration.
$config = \Drupal::config('username.settings');
// Get the username options.
$usernames = username_options();
// Get the override settings from the configuration.
$override = $config->get('override');
// Get the placeholder settings from the configuration.
$placeholder = $config->get('placeholder');
// Check if selective username placeholder is enabled.
$selective = $config->get('selective.enabled');
// Set username placeholders if selective is enabled.
if ($selective && $override['username']) {
$username_placeholder = $placeholder['username'];
if (!empty($username_placeholder)) {
// Split the placeholder if it contains '%%'.
if (strpos($username_placeholder, '%%') !== FALSE) {
$username_placeholder = explode('%%', $username_placeholder);
}
// Set the username placeholders for each username field.
$i = 0;
foreach (array_keys($usernames) as $field_name) {
if (isset($form[$field_name])) {
if (is_array($username_placeholder)) {
if (isset($username_placeholder[$i])) {
$form[$field_name]['#attributes']['placeholder'] = t('@username_placeholder', ['@username_placeholder' => trim($username_placeholder[$i])]);
$i++;
}
}
else {
$form[$field_name]['#attributes']['placeholder'] = t('@username_placeholder', ['@username_placeholder' => $username_placeholder]);
}
}
}
}
}
elseif ($override['username'] && !empty($placeholder['username'])) {
// Set the username placeholder from the configuration if not selective.
$form['name']['#attributes']['placeholder'] = t('@username_placeholder', ['@username_placeholder' => $placeholder['username']]);
}
// Set password placeholder if overridden.
if ($override['password'] && !empty($placeholder['password'])) {
$form['pass']['#attributes']['placeholder'] = t('@password_placeholder', ['@password_placeholder' => $placeholder['password']]);
}
// Return the updated form array.
return $form;
}
/**
* Set the username field descriptions.
*
* @param array $form
* An associative array containing the structure of the form.
*
* @return array
* The updated entity bundle form array.
*/
function username_set_description(array &$form): array {
// Load the username settings configuration.
$config = \Drupal::config('username.settings');
// Get the site name from the site configuration.
$site_name = \Drupal::config('system.site')->get('name');
// Get the username modes from the configuration.
$username_modes = array_filter($config->get('username'));
// Get the username options.
$usernames = username_options();
// Get the override settings from the configuration.
$override = $config->get('override');
// Get the description settings from the configuration.
$description = $config->get('description');
// Check if selective username description is enabled.
$selective = $config->get('selective.enabled');
// Early return if selective username description is not enabled.
if (!$selective) {
// Check if override for username description is enabled.
if ($override['username']) {
if (empty($description['username'])) {
// Unset the existing description if the description is empty.
unset($form['name']['#description']);
}
else {
// Set the username description from the configuration.
$form['name']['#description'] = t('@description', ['@description' => $description['username']]);
}
}
else {
// Set default username description if not overridden.
if (isset($form['name']['#description']) && !empty(trim($form['name']['#description']))) {
$username_list = '';
$modes = count($username_modes);
// Create a list of username modes separated by commas and "or".
foreach ($username_modes as $index => $username) {
$username_list .= strtolower($usernames[$username]->getUntranslatedString());
$username_list .= ($index === $modes - 1 ? '.' : ($index === $modes - 2 ? ' or ' : ', '));
}
// Set the default username description with placeholders.
$username_desc = t('Enter your @s @usernames', [
'@site_name' => $site_name,
'@usernames' => $username_list,
]);
$form['name']['#description'] = $username_desc;
}
}
// Set password description if not overridden.
if ($override['password']) {
$password_description = $description['password'];
if (empty($password_description)) {
// Unset the existing password description if the description is empty.
unset($form['pass']['#description']);
}
else {
// Set the password description from the configuration.
$form['pass']['#description'] = t('@description', ['@description' => $password_description]);
}
}
else {
// Set default password description if not overridden.
if (isset($form['pass']['#description']) && !empty(trim($form['pass']['#description']))) {
$username_list = '';
$modes = count($username_modes);
// Create a list of username modes separated by commas and "or".
foreach ($username_modes as $index => $username) {
$username_list .= strtolower($usernames[$username]->getUntranslatedString());
$username_list .= ($index === $modes - 1 ? '.' : ($index === $modes - 2 ? ' or ' : ', '));
}
// Set the default password description with placeholders.
$form['pass']['#description'] = t('Enter the password that accompanies your @usernames', ['@usernames' => $username_list]);
}
}
// Return the updated form array.
return $form;
}
// Set username description if selective is enabled.
if ($override['username']) {
$username_description = $description['username'];
if (empty($username_description)) {
// Unset existing descriptions if the username description is empty.
foreach ($usernames as $field_name => $username) {
unset($form[$field_name]['#description']);
}
}
else {
// Split the username description if it contains '%%'.
$username_descriptions = strpos($username_description, '%%') !== FALSE ? explode('%%', $username_description) : [$username_description];
// Set the username descriptions for each username field.
foreach (array_keys($usernames) as $index => $field_name) {
if (isset($form[$field_name])) {
$form[$field_name]['#description'] = t('@description', ['@description' => trim($username_descriptions[$index % count($username_descriptions)])]);
}
}
}
}
else {
// Set default username descriptions if not overridden.
if (isset($form['name']['#description']) && !empty(trim($form['name']['#description']))) {
foreach ($usernames as $field_name => $username) {
if (isset($form[$field_name])) {
$username_description = t('Enter your @s registered @username', [
'@sitename' => $site_name,
'@username' => strtolower($form[$field_name]['#title']->getUntranslatedString()),
]);
$form[$field_name]['#description'] = $username_description;
}
}
}
}
// Set password description if not overridden.
if ($override['password']) {
$password_description = $description['password'];
if (empty($password_description)) {
// Unset existing password description if the description is empty.
unset($form['pass']['#description']);
}
else {
// Set the password description from the configuration.
$form['pass']['#description'] = t('@description', ['@description' => $password_description]);
}
}
else {
// Set default password description if not overridden.
if (isset($form['pass']['#description']) && !empty(trim($form['pass']['#description']))) {
$username_list = '';
$modes = count($username_modes);
// Create a list of username modes separated by commas and "or".
foreach ($username_modes as $index => $username) {
$username_list .= strtolower($usernames[$username]->getUntranslatedString());
$username_list .= ($index === $modes - 1 ? '.' : ($index === $modes - 2 ? ' or ' : ', '));
}
// Set the default password description with placeholders.
$form['pass']['#description'] = t('Enter the password that accompanies your @usernames', ['@usernames' => $username_list]);
}
}
// Return the updated form array.
return $form;
}
/**
* Change the username field restricts description in the create user form.
*
* @param array $form
* An associative array containing the structure of the form.
* @param \Drupal\Core\Config\ImmutableConfig $config
* The configuration object.
*
* @return array
* The updated entity bundle form array.
*/
function username_change_restricts_description(array &$form, ImmutableConfig $config): array {
$restrictions = array_filter($config->get('restriction.mask'));
// Set main username strings.
$lowercase = isset($restrictions['lowercase']) ? 'lowercase' : '';
$uppercase = isset($restrictions['uppercase']) ? 'uppercase' : '';
$numeric = isset($restrictions['numeric']) ? 'digits' : '';
// Unset unnecessary keys.
unset($restrictions['lowercase']);
unset($restrictions['uppercase']);
unset($restrictions['numeric']);
unset($restrictions['minlength']);
unset($restrictions['maxlength']);
// Provide new description for username on create new account page.
if (count($restrictions) < 6) {
$only = '';
$allowed_string = ' are allowed';
// Check main username options.
if (!empty($lowercase) && !empty($uppercase) && !empty($numeric)) {
$separator_1 = ', ';
$separator_2 = ' and ';
}
elseif (!empty($lowercase) && !empty($uppercase) && empty($numeric)) {
$separator_1 = ' and ';
$separator_2 = '';
}
elseif (
(empty($lowercase) && !empty($uppercase) && !empty($numeric)) ||
(!empty($lowercase) && empty($uppercase) && !empty($numeric))
) {
$separator_1 = '';
$separator_2 = ' and ';
}
else {
$only = 'Only ';
$allowed_string = ' is allowed';
$separator_1 = '';
$separator_2 = '';
}
$char_string = empty($lowercase) && empty($uppercase) ? '' : ' characters';
$including = count($restrictions) > 0 ? ', including ' : '.';
// Create dynamic descriptions by username restriction options.
$restricts_description = $only . $lowercase . $separator_1 . $uppercase . $char_string . $separator_2 . $numeric . $allowed_string . $including;
$i = 0;
$restricts = count($restrictions);
foreach ($restrictions as $restriction => $allowed) {
// Check special characters.
if ($allowed) {
switch ($restriction) {
case 'space':
$restricts_label = 'space';
break;
case 'period':
$restricts_label = 'period (.)';
break;
case 'hyphen':
$restricts_label = 'hyphen (-)';
break;
case 'apostrophe':
$restricts_label = "apostrophe (')";
break;
case 'underscore':
$restricts_label = 'underscore (_)';
break;
case 'sign':
$restricts_label = '@ sign';
break;
}
$restricts_description .= $restricts_label . (++$i === $restricts ? '.' : ', ');
}
}
// Ensure the first letter of the description is uppercase.
$restricts_description = ucfirst($restricts_description);
// Set new description using translation placeholder.
$form['account']['name']['#description'] = t('@description', ['@description' => $restricts_description]);
}
return $form;
}
/**
* Provides a variety of username mode options.
*
* @return array
* An associative array of username mode options.
*/
function username_options(): array {
// Define the default username mode options.
$usernames = [
'name' => t('Username'),
'mail' => t('Email address'),
];
// Invoke all implementations of hook_username_mode().
$username_modes = \Drupal::moduleHandler()->invokeAll('username_mode', [$usernames]);
// Merge custom username modes with the default username mode options.
return count($username_modes) ? array_merge($usernames, $username_modes) : $usernames;
}
/**
* Provides options for selective username switch styles.
*
* @return array
* An associative array of switch style options.
*/
function username_switch_options(): array {
// Define the default switch style options.
$styles = [
'' => t('None'),
'simple_text' => t('Simple inline text'),
'simple_icon' => t('Simple inline icon'),
'animate_text' => t('Animate inline radio'),
'icon_text' => t('Inline icon and text'),
'tab_text' => t('Tab style text'),
];
// Invoke all implementations of hook_username_switch().
$switch_styles = \Drupal::moduleHandler()->invokeAll('username_switch', [$styles]);
// Merge custom switch styles with the default switch style options.
$switch_styles = count($switch_styles) ? array_merge($styles, $switch_styles) : $styles;
return $switch_styles;
}
/**
* Provides options for automatically generating usernames.
*
* @return array
* An associative array of options for generating usernames.
*/
function username_generate_options(): array {
// Define the default generate options.
$generate = [
'mail' => t('First part of user email'),
'hash' => t('Unique hash by user email'),
'token' => t('Custom value with token'),
];
// Invoke all implementations of hook_username_generate().
$generate_modes = \Drupal::moduleHandler()->invokeAll('username_generate', [$generate]);
// Merge custom generate modes with the default generate options.
$generate_modes = count($generate_modes) ? array_merge($generate, $generate_modes) : $generate;
return $generate_modes;
}
/**
* Provides display name options for usernames.
*
* @return array
* An associative array of display name options.
*/
function username_display_options(): array {
// Define the default display options.
$display = [
'mail' => t('Use the part of the email address before "@" as the username.'),
'custom' => t('Use tokens to generate a custom username from user fields.'),
];
// Invoke all implementations of hook_username_display().
$display_modes = \Drupal::moduleHandler()->invokeAll('username_display', [$display]);
// Merge custom display modes with the default display options.
if (count($display_modes)) {
$display_modes = array_merge($display, $display_modes);
}
else {
$display_modes = $display;
}
// Ensure 'custom' option is the last item in the array.
$display_clone = $display_modes['custom'];
unset($display_modes['custom']);
$display_modes['custom'] = $display_clone;
return $display_modes;
}
