first commit

This commit is contained in:
2024-07-15 12:33:27 +02:00
commit ce50ae282b
22084 changed files with 2623791 additions and 0 deletions

View File

@@ -0,0 +1,36 @@
<?php
namespace Drupal\user\Access;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Routing\Access\AccessInterface;
use Drupal\Core\Session\AccountInterface;
use Symfony\Component\Routing\Route;
/**
* Determines access to routes based on login status of current user.
*/
class LoginStatusCheck implements AccessInterface {
/**
* Checks access.
*
* @param \Drupal\Core\Session\AccountInterface $account
* The currently logged in account.
* @param \Symfony\Component\Routing\Route $route
* The route to check against.
*
* @return \Drupal\Core\Access\AccessResultInterface
* The access result.
*/
public function access(AccountInterface $account, Route $route) {
$required_status = filter_var($route->getRequirement('_user_is_logged_in'), FILTER_VALIDATE_BOOLEAN);
$actual_status = $account->isAuthenticated();
$access_result = AccessResult::allowedIf($required_status === $actual_status)->addCacheContexts(['user.roles:authenticated']);
if (!$access_result->isAllowed()) {
$access_result->setReason($required_status === TRUE ? 'This route can only be accessed by authenticated users.' : 'This route can only be accessed by anonymous users.');
}
return $access_result;
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace Drupal\user\Access;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Routing\Access\AccessInterface;
use Drupal\Core\Session\AccountInterface;
use Symfony\Component\Routing\Route;
/**
* Permission access check.
*
* Determines access to routes based on permissions defined via
* $module.permissions.yml files.
*/
class PermissionAccessCheck implements AccessInterface {
/**
* Checks access.
*
* @param \Symfony\Component\Routing\Route $route
* The route to check against.
* @param \Drupal\Core\Session\AccountInterface $account
* The currently logged in account.
*
* @return \Drupal\Core\Access\AccessResultInterface
* The access result.
*/
public function access(Route $route, AccountInterface $account) {
$permission = $route->getRequirement('_permission');
if ($permission === NULL) {
return AccessResult::neutral();
}
// Allow to conjunct the permissions with OR ('+') or AND (',').
$split = explode(',', $permission);
if (count($split) > 1) {
return AccessResult::allowedIfHasPermissions($account, $split, 'AND');
}
else {
$split = explode('+', $permission);
return AccessResult::allowedIfHasPermissions($account, $split, 'OR');
}
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Drupal\user\Access;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Routing\Access\AccessInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\user\UserInterface;
/**
* Access check for user registration routes.
*/
class RegisterAccessCheck implements AccessInterface {
/**
* Checks access.
*
* @param \Drupal\Core\Session\AccountInterface $account
* The currently logged in account.
*
* @return \Drupal\Core\Access\AccessResultInterface
* The access result.
*/
public function access(AccountInterface $account) {
$user_settings = \Drupal::config('user.settings');
return AccessResult::allowedIf($account->isAnonymous() && $user_settings->get('register') != UserInterface::REGISTER_ADMINISTRATORS_ONLY)->addCacheableDependency($user_settings);
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace Drupal\user\Access;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Routing\Access\AccessInterface;
use Drupal\Core\Session\AccountInterface;
use Symfony\Component\Routing\Route;
/**
* Determines access to routes based on roles.
*
* You can specify the '_role' key on route requirements. If you specify a
* single role, users with that role with have access. If you specify multiple
* ones you can conjunct them with AND by using a "," and with OR by using "+".
*/
class RoleAccessCheck implements AccessInterface {
/**
* Checks access.
*
* @param \Symfony\Component\Routing\Route $route
* The route to check against.
* @param \Drupal\Core\Session\AccountInterface $account
* The currently logged in account.
*
* @return \Drupal\Core\Access\AccessResultInterface
* The access result.
*/
public function access(Route $route, AccountInterface $account) {
// Requirements just allow strings, so this might be a comma separated list.
$rid_string = $route->getRequirement('_role');
$explode_and = array_filter(array_map('trim', explode(',', $rid_string)));
if (count($explode_and) > 1) {
$diff = array_diff($explode_and, $account->getRoles());
if (empty($diff)) {
return AccessResult::allowed()->addCacheContexts(['user.roles']);
}
}
else {
$explode_or = array_filter(array_map('trim', explode('+', $rid_string)));
$intersection = array_intersect($explode_or, $account->getRoles());
if (!empty($intersection)) {
return AccessResult::allowed()->addCacheContexts(['user.roles']);
}
}
// If there is no allowed role, give other access checks a chance.
return AccessResult::neutral()->addCacheContexts(['user.roles']);
}
}

View File

@@ -0,0 +1,445 @@
<?php
namespace Drupal\user;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Component\Utility\Html;
use Drupal\Core\Datetime\TimeZoneFormHelper;
use Drupal\Core\Entity\ContentEntityForm;
use Drupal\Core\Entity\EntityConstraintViolationListInterface;
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Security\TrustedCallbackInterface;
use Drupal\Core\Url;
use Drupal\language\ConfigurableLanguageManagerInterface;
use Drupal\user\Entity\Role;
use Drupal\user\Plugin\LanguageNegotiation\LanguageNegotiationUser;
use Drupal\user\Plugin\LanguageNegotiation\LanguageNegotiationUserAdmin;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Form controller for the user account forms.
*/
abstract class AccountForm extends ContentEntityForm implements TrustedCallbackInterface {
/**
* The language manager.
*
* @var \Drupal\Core\Language\LanguageManagerInterface
*/
protected $languageManager;
/**
* Constructs a new EntityForm object.
*
* @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
* The entity repository.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
* @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info
* The entity type bundle service.
* @param \Drupal\Component\Datetime\TimeInterface $time
* The time service.
*/
public function __construct(EntityRepositoryInterface $entity_repository, LanguageManagerInterface $language_manager, ?EntityTypeBundleInfoInterface $entity_type_bundle_info = NULL, ?TimeInterface $time = NULL) {
parent::__construct($entity_repository, $entity_type_bundle_info, $time);
$this->languageManager = $language_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity.repository'),
$container->get('language_manager'),
$container->get('entity_type.bundle.info'),
$container->get('datetime.time')
);
}
/**
* {@inheritdoc}
*/
public function form(array $form, FormStateInterface $form_state) {
/** @var \Drupal\user\UserInterface $account */
$account = $this->entity;
$user = $this->currentUser();
$config = \Drupal::config('user.settings');
$form['#cache']['tags'] = $config->getCacheTags();
$language_interface = \Drupal::languageManager()->getCurrentLanguage();
// Check for new account.
$register = $account->isNew();
// For a new account, there are 2 sub-cases:
// $self_register: A user creates their own, new, account
// (path '/user/register')
// $admin_create: An administrator creates a new account for another user
// (path '/admin/people/create')
// If the current user is logged in and has permission to create users
// then it must be the second case.
$admin_create = $register && $account->access('create');
$self_register = $register && !$admin_create;
// Account information.
$form['account'] = [
'#type' => 'container',
'#weight' => -10,
];
// The mail field is NOT required if account originally had no mail set
// and the user performing the edit has 'administer users' permission.
// This allows users without email address to be edited and deleted.
// Also see \Drupal\user\Plugin\Validation\Constraint\UserMailRequired.
$form['account']['mail'] = [
'#type' => 'email',
'#title' => $this->t('Email address'),
'#description' => $this->t('The email address is not made public. It will only be used if you need to be contacted about your account or for opted-in notifications.'),
'#required' => !(!$account->getEmail() && $user->hasPermission('administer users')),
'#default_value' => (!$register ? $account->getEmail() : ''),
'#access' => $account->mail->access('edit'),
];
// Only show name field on registration form or user can change own username.
$form['account']['name'] = [
'#type' => 'textfield',
'#title' => $this->t('Username'),
'#maxlength' => UserInterface::USERNAME_MAX_LENGTH,
'#description' => $this->t("Several special characters are allowed, including space, period (.), hyphen (-), apostrophe ('), underscore (_), and the @ sign."),
'#required' => TRUE,
'#attributes' => [
'class' => ['username'],
'autocorrect' => 'off',
'autocapitalize' => 'off',
'spellcheck' => 'false',
],
'#default_value' => (!$register ? $account->getAccountName() : ''),
'#access' => $account->name->access('edit'),
];
// Display password field only for existing users or when user is allowed to
// assign a password during registration.
if (!$register) {
$form['account']['pass'] = [
'#type' => 'password_confirm',
'#size' => 25,
'#description' => $this->t('To change the current user password, enter the new password in both fields.'),
];
// To skip the current password field, the user must have logged in via a
// one-time link and have the token in the URL. Store this in $form_state
// so it persists even on subsequent Ajax requests.
$request = $this->getRequest();
if (!$form_state->get('user_pass_reset') && ($token = $request->query->get('pass-reset-token'))) {
$session_key = 'pass_reset_' . $account->id();
$session_value = $request->getSession()->get($session_key);
$user_pass_reset = isset($session_value) && hash_equals($session_value, $token);
$form_state->set('user_pass_reset', $user_pass_reset);
}
// The user must enter their current password to change to a new one.
if ($user->id() == $account->id()) {
$form['account']['current_pass'] = [
'#type' => 'password',
'#title' => $this->t('Current password'),
'#size' => 25,
'#access' => !$form_state->get('user_pass_reset'),
'#weight' => -5,
// Do not let web browsers remember this password, since we are
// trying to confirm that the person submitting the form actually
// knows the current one.
'#attributes' => ['autocomplete' => 'off'],
];
$form_state->set('user', $account);
// The user may only change their own password without their current
// password if they logged in via a one-time login link.
if (!$form_state->get('user_pass_reset')) {
$form['account']['current_pass']['#description'] = $this->t('Required if you want to change the %mail or %pass below. <a href=":request_new_url" title="Send password reset instructions via email.">Reset your password</a>.', [
'%mail' => $form['account']['mail']['#title'],
'%pass' => $this->t('Password'),
':request_new_url' => Url::fromRoute('user.pass')->toString(),
]);
}
}
}
elseif (!$config->get('verify_mail') || $admin_create) {
$form['account']['pass'] = [
'#type' => 'password_confirm',
'#size' => 25,
'#description' => $this->t('Provide a password for the new account in both fields.'),
'#required' => TRUE,
];
}
// When not building the user registration form, prevent web browsers from
// auto-filling/prefilling the email, username, and password fields.
if (!$register) {
foreach (['mail', 'name', 'pass'] as $key) {
if (isset($form['account'][$key])) {
$form['account'][$key]['#attributes']['autocomplete'] = 'off';
}
}
}
if (!$self_register) {
$status = $account->get('status')->value;
}
else {
$status = $config->get('register') == UserInterface::REGISTER_VISITORS ? 1 : 0;
}
$form['account']['status'] = [
'#type' => 'radios',
'#title' => $this->t('Status'),
'#default_value' => $status,
'#options' => [$this->t('Blocked'), $this->t('Active')],
'#access' => $account->status->access('edit'),
];
$roles = Role::loadMultiple();
unset($roles[RoleInterface::ANONYMOUS_ID]);
$roles = array_map(fn(RoleInterface $role) => Html::escape($role->label()), $roles);
$form['account']['roles'] = [
'#type' => 'checkboxes',
'#title' => $this->t('Roles'),
'#default_value' => (!$register ? $account->getRoles() : []),
'#options' => $roles,
'#access' => $roles && $user->hasPermission('administer permissions'),
];
// Special handling for the inevitable "Authenticated user" role.
$form['account']['roles'][RoleInterface::AUTHENTICATED_ID] = [
'#default_value' => TRUE,
'#disabled' => TRUE,
];
$form['account']['notify'] = [
'#type' => 'checkbox',
'#title' => $this->t('Notify user of new account'),
'#access' => $admin_create,
];
$user_preferred_langcode = $register ? $language_interface->getId() : $account->getPreferredLangcode();
$user_preferred_admin_langcode = $register ? $language_interface->getId() : $account->getPreferredAdminLangcode(FALSE);
// Is the user preferred language added?
$user_language_added = FALSE;
if ($this->languageManager instanceof ConfigurableLanguageManagerInterface) {
$negotiator = $this->languageManager->getNegotiator();
$user_language_added = $negotiator && $negotiator->isNegotiationMethodEnabled(LanguageNegotiationUser::METHOD_ID, LanguageInterface::TYPE_INTERFACE);
}
$form['language'] = [
'#type' => $this->languageManager->isMultilingual() ? 'details' : 'container',
'#title' => $this->t('Language settings'),
'#open' => TRUE,
// Display language selector when either creating a user on the admin
// interface or editing a user account.
'#access' => !$self_register,
];
$form['language']['preferred_langcode'] = [
'#type' => 'language_select',
'#title' => $this->t('Site language'),
'#languages' => LanguageInterface::STATE_CONFIGURABLE,
'#default_value' => $user_preferred_langcode,
'#description' => $user_language_added ? $this->t("This account's preferred language for emails and site presentation.") : $this->t("This account's preferred language for emails."),
// This is used to explain that user preferred language and entity
// language are synchronized. It can be removed if a different behavior is
// desired.
'#pre_render' => ['user_langcode' => [$this, 'alterPreferredLangcodeDescription']],
];
// Only show the account setting for Administration pages language to users
// if one of the detection and selection methods uses it.
$show_admin_language = FALSE;
if (($account->hasPermission('access administration pages') || $account->hasPermission('view the administration theme')) && $this->languageManager instanceof ConfigurableLanguageManagerInterface) {
$negotiator = $this->languageManager->getNegotiator();
$show_admin_language = $negotiator && $negotiator->isNegotiationMethodEnabled(LanguageNegotiationUserAdmin::METHOD_ID);
}
$form['language']['preferred_admin_langcode'] = [
'#type' => 'language_select',
'#title' => $this->t('Administration pages language'),
'#languages' => LanguageInterface::STATE_CONFIGURABLE,
'#default_value' => $user_preferred_admin_langcode,
'#access' => $show_admin_language,
'#empty_option' => $this->t('- No preference -'),
'#empty_value' => '',
];
// User entities contain both a langcode property (for identifying the
// language of the entity data) and a preferred_langcode property (see
// above). Rather than provide a UI forcing the user to choose both
// separately, assume that the user profile data is in the user's preferred
// language. This entity builder provides that synchronization. For
// use-cases where this synchronization is not desired, a module can alter
// or remove this item. Sync user langcode only when a user registers and
// not when a user is updated or translated.
if ($register) {
$form['#entity_builders']['sync_user_langcode'] = '::syncUserLangcode';
}
$system_date_config = \Drupal::config('system.date');
$form['timezone'] = [
'#type' => 'details',
'#title' => $this->t('Locale settings'),
'#open' => TRUE,
'#weight' => 6,
'#access' => $system_date_config->get('timezone.user.configurable'),
];
if ($self_register && $system_date_config->get('timezone.user.default') != UserInterface::TIMEZONE_SELECT) {
$form['timezone']['#access'] = FALSE;
}
$form['timezone']['timezone'] = [
'#type' => 'select',
'#title' => $this->t('Time zone'),
'#default_value' => $account->getTimezone() ?: $system_date_config->get('timezone.default'),
'#options' => TimeZoneFormHelper::getOptionsListByRegion($account->id() != $user->id()),
'#description' => $this->t('Select the desired local time and time zone. Dates and times throughout this site will be displayed using this time zone.'),
];
// If not set or selected yet, detect timezone for the current user only.
$user_input = $form_state->getUserInput();
if (!$account->getTimezone() && $account->id() == $user->id() && empty($user_input['timezone'])) {
$form['timezone']['#attached']['library'][] = 'core/drupal.timezone';
$form['timezone']['timezone']['#attributes'] = ['class' => ['timezone-detect']];
}
return parent::form($form, $form_state);
}
/**
* {@inheritdoc}
*/
public static function trustedCallbacks() {
return ['alterPreferredLangcodeDescription'];
}
/**
* Alters the preferred language widget description.
*
* @param array $element
* The preferred language form element.
*
* @return array
* The preferred language form element.
*/
public function alterPreferredLangcodeDescription(array $element) {
// Only add to the description if the form element has a description.
if (isset($element['#description'])) {
$element['#description'] .= ' ' . $this->t("This is also assumed to be the primary language of this account's profile information.");
}
return $element;
}
/**
* Synchronizes preferred language and entity language.
*
* @param string $entity_type_id
* The entity type identifier.
* @param \Drupal\user\UserInterface $user
* The entity updated with the submitted values.
* @param array $form
* The complete form array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*/
public function syncUserLangcode($entity_type_id, UserInterface $user, array &$form, FormStateInterface &$form_state) {
$user->getUntranslated()->langcode = $user->preferred_langcode;
}
/**
* {@inheritdoc}
*/
public function buildEntity(array $form, FormStateInterface $form_state) {
// Change the roles array to a list of enabled roles.
// @todo Alter the form state as the form values are directly extracted and
// set on the field, which throws an exception as the list requires
// numeric keys. Allow to override this per field. As this function is
// called twice, we have to prevent it from getting the array keys twice.
if (is_string(key($form_state->getValue('roles')))) {
$form_state->setValue('roles', array_keys(array_filter($form_state->getValue('roles'))));
}
/** @var \Drupal\user\UserInterface $account */
$account = parent::buildEntity($form, $form_state);
// Translate the empty value '' of language selects to an unset field.
foreach (['preferred_langcode', 'preferred_admin_langcode'] as $field_name) {
if ($form_state->getValue($field_name) === '') {
$account->$field_name = NULL;
}
}
// Set existing password if set in the form state.
$current_pass = trim($form_state->getValue('current_pass', ''));
if (strlen($current_pass) > 0) {
$account->setExistingPassword($current_pass);
}
// Skip the protected user field constraint if the user came from the
// password recovery page.
$account->_skipProtectedUserFieldConstraint = $form_state->get('user_pass_reset');
return $account;
}
/**
* {@inheritdoc}
*/
protected function getEditedFieldNames(FormStateInterface $form_state) {
return array_merge([
'name',
'pass',
'mail',
'timezone',
'langcode',
'preferred_langcode',
'preferred_admin_langcode',
], parent::getEditedFieldNames($form_state));
}
/**
* {@inheritdoc}
*/
protected function flagViolations(EntityConstraintViolationListInterface $violations, array $form, FormStateInterface $form_state) {
// Manually flag violations of fields not handled by the form display. This
// is necessary as entity form displays only flag violations for fields
// contained in the display.
$field_names = [
'name',
'pass',
'mail',
'timezone',
'langcode',
'preferred_langcode',
'preferred_admin_langcode',
];
foreach ($violations->getByFields($field_names) as $violation) {
[$field_name] = explode('.', $violation->getPropertyPath(), 2);
$form_state->setErrorByName($field_name, $violation->getMessage());
}
parent::flagViolations($violations, $form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
parent::submitForm($form, $form_state);
$user = $this->getEntity();
// If there's a session set to the users id, remove the password reset tag
// since a new password was saved.
$this->getRequest()->getSession()->remove('pass_reset_' . $user->id());
}
}

View File

@@ -0,0 +1,421 @@
<?php
namespace Drupal\user;
use Drupal\Core\Config\TypedConfigManagerInterface;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Configure user settings for this site.
*
* @internal
*/
class AccountSettingsForm extends ConfigFormBase {
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The role storage used when changing the admin role.
*
* @var \Drupal\user\RoleStorageInterface
*/
protected $roleStorage;
/**
* Constructs a \Drupal\user\AccountSettingsForm object.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The factory for configuration objects.
* @param \Drupal\Core\Config\TypedConfigManagerInterface $typedConfigManager
* The typed config manager.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\user\RoleStorageInterface $role_storage
* The role storage.
*/
public function __construct(ConfigFactoryInterface $config_factory, TypedConfigManagerInterface $typedConfigManager, ModuleHandlerInterface $module_handler, RoleStorageInterface $role_storage) {
parent::__construct($config_factory, $typedConfigManager);
$this->moduleHandler = $module_handler;
$this->roleStorage = $role_storage;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('config.factory'),
$container->get('config.typed'),
$container->get('module_handler'),
$container->get('entity_type.manager')->getStorage('user_role')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'user_admin_settings';
}
/**
* {@inheritdoc}
*/
protected function getEditableConfigNames() {
return [
'system.site',
'user.mail',
'user.settings',
];
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$form = parent::buildForm($form, $form_state);
$config = $this->config('user.settings');
$site_config = $this->config('system.site');
$form['#attached']['library'][] = 'user/drupal.user.admin';
// Settings for anonymous users.
$form['anonymous_settings'] = [
'#type' => 'details',
'#title' => $this->t('Anonymous users'),
'#open' => TRUE,
];
$form['anonymous_settings']['anonymous'] = [
'#type' => 'textfield',
'#title' => $this->t('Name'),
'#config_target' => 'user.settings:anonymous',
'#description' => $this->t('The name used to indicate anonymous users.'),
'#required' => TRUE,
];
// @todo Remove this check once language settings are generalized.
if ($this->moduleHandler->moduleExists('content_translation')) {
$form['language'] = [
'#type' => 'details',
'#title' => $this->t('Language settings'),
'#open' => TRUE,
'#tree' => TRUE,
];
$form_state->set(['content_translation', 'key'], 'language');
$form['language'] += content_translation_enable_widget('user', 'user', $form, $form_state);
}
// User registration settings.
$form['registration_cancellation'] = [
'#type' => 'details',
'#title' => $this->t('Registration and cancellation'),
'#open' => TRUE,
];
$form['registration_cancellation']['user_register'] = [
'#type' => 'radios',
'#title' => $this->t('Who can register accounts?'),
'#config_target' => 'user.settings:register',
'#options' => [
UserInterface::REGISTER_ADMINISTRATORS_ONLY => $this->t('Administrators only'),
UserInterface::REGISTER_VISITORS => $this->t('Visitors'),
UserInterface::REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL => $this->t('Visitors, but administrator approval is required'),
],
];
$form['registration_cancellation']['user_email_verification'] = [
'#type' => 'checkbox',
'#title' => $this->t('Require email verification when a visitor creates an account'),
'#config_target' => 'user.settings:verify_mail',
'#description' => $this->t('New users will be required to validate their email address prior to logging into the site, and will be assigned a system-generated password. With this setting disabled, users will be logged in immediately upon registering, and may select their own passwords during registration.'),
];
$form['registration_cancellation']['user_password_strength'] = [
'#type' => 'checkbox',
'#title' => $this->t('Enable password strength indicator'),
'#config_target' => 'user.settings:password_strength',
];
$form['registration_cancellation']['user_cancel_method'] = [
'#type' => 'radios',
'#title' => $this->t('When cancelling a user account'),
'#config_target' => 'user.settings:cancel_method',
'#description' => $this->t('Users with the %select-cancel-method or %administer-users <a href=":permissions-url">permissions</a> can override this default method.', ['%select-cancel-method' => $this->t('Select method for cancelling account'), '%administer-users' => $this->t('Administer users'), ':permissions-url' => Url::fromRoute('user.admin_permissions')->toString()]),
];
$form['registration_cancellation']['user_cancel_method'] += user_cancel_methods();
foreach (Element::children($form['registration_cancellation']['user_cancel_method']) as $key) {
// All account cancellation methods that specify #access cannot be
// configured as default method.
// @see hook_user_cancel_methods_alter()
if (isset($form['registration_cancellation']['user_cancel_method'][$key]['#access'])) {
$form['registration_cancellation']['user_cancel_method'][$key]['#access'] = FALSE;
}
}
// Default notifications address.
$form['mail_notification_address'] = [
'#type' => 'email',
'#title' => $this->t('Notification email address'),
'#config_target' => 'system.site:mail_notification',
'#description' => $this->t("The email address to be used as the 'from' address for all account notifications listed below. If <em>'Visitors, but administrator approval is required'</em> is selected above, a notification email will also be sent to this address for any new registrations. Leave empty to use the default system email address <em>(%site-email).</em>", ['%site-email' => $site_config->get('mail')]),
'#maxlength' => 180,
];
$form['email'] = [
'#type' => 'vertical_tabs',
'#title' => $this->t('Emails'),
];
// These email tokens are shared for all settings, so just define
// the list once to help ensure they stay in sync.
$email_token_help = $this->t('Available variables are: [site:name], [site:url], [user:display-name], [user:account-name], [user:mail], [site:login-url], [site:url-brief], [user:edit-url], [user:one-time-login-url], [user:cancel-url].');
$form['email_admin_created'] = [
'#type' => 'details',
'#title' => $this->t('Welcome (new user created by administrator)'),
'#open' => $config->get('register') == UserInterface::REGISTER_ADMINISTRATORS_ONLY,
'#description' => $this->t('Edit the welcome email messages sent to new member accounts created by an administrator.') . ' ' . $email_token_help,
'#group' => 'email',
];
$form['email_admin_created']['user_mail_register_admin_created_subject'] = [
'#type' => 'textfield',
'#title' => $this->t('Subject'),
'#config_target' => 'user.mail:register_admin_created.subject',
'#required' => TRUE,
'#maxlength' => 180,
];
$form['email_admin_created']['user_mail_register_admin_created_body'] = [
'#type' => 'textarea',
'#title' => $this->t('Body'),
'#config_target' => 'user.mail:register_admin_created.body',
'#rows' => 15,
];
$form['email_pending_approval'] = [
'#type' => 'details',
'#title' => $this->t('Welcome (awaiting approval)'),
'#open' => $config->get('register') == UserInterface::REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL,
'#description' => $this->t('Edit the welcome email messages sent to new members upon registering, when administrative approval is required.') . ' ' . $email_token_help,
'#group' => 'email',
];
$form['email_pending_approval']['user_mail_register_pending_approval_subject'] = [
'#type' => 'textfield',
'#title' => $this->t('Subject'),
'#config_target' => 'user.mail:register_pending_approval.subject',
'#required' => TRUE,
'#maxlength' => 180,
];
$form['email_pending_approval']['user_mail_register_pending_approval_body'] = [
'#type' => 'textarea',
'#title' => $this->t('Body'),
'#config_target' => 'user.mail:register_pending_approval.body',
'#rows' => 8,
];
$form['email_pending_approval_admin'] = [
'#type' => 'details',
'#title' => $this->t('Admin (user awaiting approval)'),
'#open' => $config->get('register') == UserInterface::REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL,
'#description' => $this->t('Edit the email notifying the site administrator that there are new members awaiting administrative approval.') . ' ' . $email_token_help,
'#group' => 'email',
];
$form['email_pending_approval_admin']['register_pending_approval_admin_subject'] = [
'#type' => 'textfield',
'#title' => $this->t('Subject'),
'#config_target' => 'user.mail:register_pending_approval_admin.subject',
'#required' => TRUE,
'#maxlength' => 180,
];
$form['email_pending_approval_admin']['register_pending_approval_admin_body'] = [
'#type' => 'textarea',
'#title' => $this->t('Body'),
'#config_target' => 'user.mail:register_pending_approval_admin.body',
'#rows' => 8,
];
$form['email_no_approval_required'] = [
'#type' => 'details',
'#title' => $this->t('Welcome (no approval required)'),
'#open' => $config->get('register') == UserInterface::REGISTER_VISITORS,
'#description' => $this->t('Edit the welcome email messages sent to new members upon registering, when no administrator approval is required.') . ' ' . $email_token_help,
'#group' => 'email',
];
$form['email_no_approval_required']['user_mail_register_no_approval_required_subject'] = [
'#type' => 'textfield',
'#title' => $this->t('Subject'),
'#config_target' => 'user.mail:register_no_approval_required.subject',
'#required' => TRUE,
'#maxlength' => 180,
];
$form['email_no_approval_required']['user_mail_register_no_approval_required_body'] = [
'#type' => 'textarea',
'#title' => $this->t('Body'),
'#config_target' => 'user.mail:register_no_approval_required.body',
'#rows' => 15,
];
$form['email_password_reset'] = [
'#type' => 'details',
'#title' => $this->t('Password recovery'),
'#description' => $this->t('Edit the email messages sent to users who request a new password.') . ' ' . $email_token_help,
'#group' => 'email',
'#weight' => 10,
];
$form['email_password_reset']['user_mail_password_reset_subject'] = [
'#type' => 'textfield',
'#title' => $this->t('Subject'),
'#config_target' => 'user.mail:password_reset.subject',
'#required' => TRUE,
'#maxlength' => 180,
];
$form['email_password_reset']['user_mail_password_reset_body'] = [
'#type' => 'textarea',
'#title' => $this->t('Body'),
'#config_target' => 'user.mail:password_reset.body',
'#rows' => 12,
];
$form['email_activated'] = [
'#type' => 'details',
'#title' => $this->t('Account activation'),
'#description' => $this->t('Enable and edit email messages sent to users upon account activation (when an administrator activates an account of a user who has already registered, on a site where administrative approval is required).') . ' ' . $email_token_help,
'#group' => 'email',
];
$form['email_activated']['user_mail_status_activated_notify'] = [
'#type' => 'checkbox',
'#title' => $this->t('Notify user when account is activated'),
'#config_target' => 'user.settings:notify.status_activated',
];
$form['email_activated']['settings'] = [
'#type' => 'container',
'#states' => [
// Hide the additional settings when this email is disabled.
'invisible' => [
'input[name="user_mail_status_activated_notify"]' => ['checked' => FALSE],
],
],
];
$form['email_activated']['settings']['user_mail_status_activated_subject'] = [
'#type' => 'textfield',
'#title' => $this->t('Subject'),
'#config_target' => 'user.mail:status_activated.subject',
'#states' => [
'required' => [
'input[name="user_mail_status_activated_notify"]' => ['checked' => TRUE],
],
],
'#maxlength' => 180,
];
$form['email_activated']['settings']['user_mail_status_activated_body'] = [
'#type' => 'textarea',
'#title' => $this->t('Body'),
'#config_target' => 'user.mail:status_activated.body',
'#rows' => 15,
];
$form['email_blocked'] = [
'#type' => 'details',
'#title' => $this->t('Account blocked'),
'#description' => $this->t('Enable and edit email messages sent to users when their accounts are blocked.') . ' ' . $email_token_help,
'#group' => 'email',
];
$form['email_blocked']['user_mail_status_blocked_notify'] = [
'#type' => 'checkbox',
'#title' => $this->t('Notify user when account is blocked'),
'#config_target' => 'user.settings:notify.status_blocked',
];
$form['email_blocked']['settings'] = [
'#type' => 'container',
'#states' => [
// Hide the additional settings when the blocked email is disabled.
'invisible' => [
'input[name="user_mail_status_blocked_notify"]' => ['checked' => FALSE],
],
],
];
$form['email_blocked']['settings']['user_mail_status_blocked_subject'] = [
'#type' => 'textfield',
'#title' => $this->t('Subject'),
'#config_target' => 'user.mail:status_blocked.subject',
'#states' => [
'required' => [
'input[name="user_mail_status_blocked_notify"]' => ['checked' => TRUE],
],
],
'#maxlength' => 180,
];
$form['email_blocked']['settings']['user_mail_status_blocked_body'] = [
'#type' => 'textarea',
'#title' => $this->t('Body'),
'#config_target' => 'user.mail:status_blocked.body',
'#rows' => 3,
];
$form['email_cancel_confirm'] = [
'#type' => 'details',
'#title' => $this->t('Account cancellation confirmation'),
'#description' => $this->t('Edit the email messages sent to users when they attempt to cancel their accounts.') . ' ' . $email_token_help,
'#group' => 'email',
];
$form['email_cancel_confirm']['user_mail_cancel_confirm_subject'] = [
'#type' => 'textfield',
'#title' => $this->t('Subject'),
'#config_target' => 'user.mail:cancel_confirm.subject',
'#required' => TRUE,
'#maxlength' => 180,
];
$form['email_cancel_confirm']['user_mail_cancel_confirm_body'] = [
'#type' => 'textarea',
'#title' => $this->t('Body'),
'#config_target' => 'user.mail:cancel_confirm.body',
'#rows' => 3,
];
$form['email_canceled'] = [
'#type' => 'details',
'#title' => $this->t('Account canceled'),
'#description' => $this->t('Enable and edit email messages sent to users when their accounts are canceled.') . ' ' . $email_token_help,
'#group' => 'email',
];
$form['email_canceled']['user_mail_status_canceled_notify'] = [
'#type' => 'checkbox',
'#title' => $this->t('Notify user when account is canceled'),
'#config_target' => 'user.settings:notify.status_canceled',
];
$form['email_canceled']['settings'] = [
'#type' => 'container',
'#states' => [
// Hide the settings when the cancel notify checkbox is disabled.
'invisible' => [
'input[name="user_mail_status_canceled_notify"]' => ['checked' => FALSE],
],
],
];
$form['email_canceled']['settings']['user_mail_status_canceled_subject'] = [
'#type' => 'textfield',
'#title' => $this->t('Subject'),
'#config_target' => 'user.mail:status_canceled.subject',
'#states' => [
'required' => [
'input[name="user_mail_status_canceled_subject"]' => ['checked' => TRUE],
],
],
'#maxlength' => 180,
];
$form['email_canceled']['settings']['user_mail_status_canceled_body'] = [
'#type' => 'textarea',
'#title' => $this->t('Body'),
'#config_target' => 'user.mail:status_canceled.body',
'#rows' => 3,
];
return $form;
}
}

View File

@@ -0,0 +1,158 @@
<?php
namespace Drupal\user\Authentication\Provider;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Authentication\AuthenticationProviderInterface;
use Drupal\Core\Database\Connection;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Routing\TrustedRedirectResponse;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Session\UserSession;
use Drupal\Core\Session\SessionConfigurationInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
/**
* Cookie based authentication provider.
*/
class Cookie implements AuthenticationProviderInterface, EventSubscriberInterface {
use StringTranslationTrait;
/**
* The session configuration.
*
* @var \Drupal\Core\Session\SessionConfigurationInterface
*/
protected $sessionConfiguration;
/**
* The database connection.
*
* @var \Drupal\Core\Database\Connection
*/
protected $connection;
/**
* The messenger.
*
* @var \Drupal\Core\Messenger\MessengerInterface
*/
protected $messenger;
/**
* Constructs a new cookie authentication provider.
*
* @param \Drupal\Core\Session\SessionConfigurationInterface $session_configuration
* The session configuration.
* @param \Drupal\Core\Database\Connection $connection
* The database connection.
* @param \Drupal\Core\Messenger\MessengerInterface $messenger
* The messenger.
*/
public function __construct(SessionConfigurationInterface $session_configuration, Connection $connection, MessengerInterface $messenger) {
$this->sessionConfiguration = $session_configuration;
$this->connection = $connection;
$this->messenger = $messenger;
}
/**
* {@inheritdoc}
*/
public function applies(Request $request) {
$applies = $this->sessionConfiguration->hasSession($request);
if (!$applies && $request->query->has('check_logged_in')) {
$domain = ltrim(ini_get('session.cookie_domain'), '.') ?: $request->getHttpHost();
$this->messenger->addMessage($this->t('To log in to this site, your browser must accept cookies from the domain %domain.', ['%domain' => $domain]), 'error');
}
return $applies;
}
/**
* {@inheritdoc}
*/
public function authenticate(Request $request) {
return $this->getUserFromSession($request->getSession());
}
/**
* Returns the UserSession object for the given session.
*
* @param \Symfony\Component\HttpFoundation\Session\SessionInterface $session
* The session.
*
* @return \Drupal\Core\Session\AccountInterface|null
* The UserSession object for the current user, or NULL if this is an
* anonymous session.
*/
protected function getUserFromSession(SessionInterface $session) {
if ($uid = $session->get('uid')) {
// @todo Load the User entity in SessionHandler so we don't need queries.
// @see https://www.drupal.org/node/2345611
$values = $this->connection
->query('SELECT * FROM {users_field_data} [u] WHERE [u].[uid] = :uid AND [u].[default_langcode] = 1', [':uid' => $uid])
->fetchAssoc();
// Check if the user data was found and the user is active.
if (!empty($values) && $values['status'] == 1) {
// Add the user's roles.
$rids = $this->connection
->query('SELECT [roles_target_id] FROM {user__roles} WHERE [entity_id] = :uid', [':uid' => $values['uid']])
->fetchCol();
$values['roles'] = array_merge([AccountInterface::AUTHENTICATED_ROLE], $rids);
return new UserSession($values);
}
}
// This is an anonymous session.
return NULL;
}
/**
* Adds a query parameter to check successful log in redirect URL.
*
* @param \Symfony\Component\HttpKernel\Event\ResponseEvent $event
* The Event to process.
*/
public function addCheckToUrl(ResponseEvent $event) {
$response = $event->getResponse();
if ($response instanceof RedirectResponse) {
if ($event->getRequest()->getSession()->has('check_logged_in')) {
$event->getRequest()->getSession()->remove('check_logged_in');
$url = $response->getTargetUrl();
$options = UrlHelper::parse($url);
$options['query']['check_logged_in'] = '1';
$url = $options['path'] . '?' . UrlHelper::buildQuery($options['query']);
if (!empty($options['fragment'])) {
$url .= '#' . $options['fragment'];
}
// In the case of trusted redirect, we have to update the list of
// trusted URLs because here we've just modified its target URL
// which is in the list.
if ($response instanceof TrustedRedirectResponse) {
$response->setTrustedTargetUrl($url);
}
$response->setTargetUrl($url);
}
}
}
/**
* Registers the methods in this class that should be listeners.
*
* @return array
* An array of event listener definitions.
*/
public static function getSubscribedEvents(): array {
$events[KernelEvents::RESPONSE][] = ['addCheckToUrl', -1000];
return $events;
}
}

View File

@@ -0,0 +1,82 @@
<?php
namespace Drupal\user\ContextProvider;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Plugin\Context\ContextProviderInterface;
use Drupal\Core\Plugin\Context\EntityContext;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
/**
* Sets the current user as a context.
*/
class CurrentUserContext implements ContextProviderInterface {
use StringTranslationTrait;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $account;
/**
* The user storage.
*
* @var \Drupal\user\UserStorageInterface
*/
protected $userStorage;
/**
* Constructs a new CurrentUserContext.
*
* @param \Drupal\Core\Session\AccountInterface $account
* The current user.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
*/
public function __construct(AccountInterface $account, EntityTypeManagerInterface $entity_type_manager) {
$this->account = $account;
$this->userStorage = $entity_type_manager->getStorage('user');
}
/**
* {@inheritdoc}
*/
public function getRuntimeContexts(array $unqualified_context_ids) {
$current_user = $this->userStorage->load($this->account->id());
if ($current_user) {
// @todo Do not validate protected fields to avoid bug in TypedData,
// remove this in https://www.drupal.org/project/drupal/issues/2934192.
$current_user->_skipProtectedUserFieldConstraint = TRUE;
$context = EntityContext::fromEntity($current_user, $this->t('Current user'));
}
else {
// If not user is available, provide an empty context object.
$context = EntityContext::fromEntityTypeId('user', $this->t('Current user'));
}
$cacheability = new CacheableMetadata();
$cacheability->setCacheContexts(['user']);
$context->addCacheableDependency($cacheability);
$result = [
'current_user' => $context,
];
return $result;
}
/**
* {@inheritdoc}
*/
public function getAvailableContexts() {
return $this->getRuntimeContexts([]);
}
}

View File

@@ -0,0 +1,441 @@
<?php
namespace Drupal\user\Controller;
use Drupal\Core\Access\CsrfTokenGenerator;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Routing\RouteProviderInterface;
use Drupal\user\UserAuthenticationInterface;
use Drupal\user\UserAuthInterface;
use Drupal\user\UserFloodControlInterface;
use Drupal\user\UserInterface;
use Drupal\user\UserStorageInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Serializer;
/**
* Provides controllers for login, login status and logout via HTTP requests.
*/
class UserAuthenticationController extends ControllerBase implements ContainerInjectionInterface {
/**
* String sent in responses, to describe the user as being logged in.
*
* @var string
*/
const LOGGED_IN = '1';
/**
* String sent in responses, to describe the user as being logged out.
*
* @var string
*/
const LOGGED_OUT = '0';
/**
* The user flood control service.
*
* @var \Drupal\user\UserFloodControl
*/
protected $userFloodControl;
/**
* The user storage.
*
* @var \Drupal\user\UserStorageInterface
*/
protected $userStorage;
/**
* The CSRF token generator.
*
* @var \Drupal\Core\Access\CsrfTokenGenerator
*/
protected $csrfToken;
/**
* The user authentication.
* @var \Drupal\user\UserAuthInterface|\Drupal\user\UserAuthenticationInterface
*/
protected $userAuth;
/**
* The route provider.
*
* @var \Drupal\Core\Routing\RouteProviderInterface
*/
protected $routeProvider;
/**
* The serializer.
*
* @var \Symfony\Component\Serializer\Serializer
*/
protected $serializer;
/**
* The available serialization formats.
*
* @var array
*/
protected $serializerFormats = [];
/**
* A logger instance.
*
* @var \Psr\Log\LoggerInterface
*/
protected $logger;
/**
* Constructs a new UserAuthenticationController object.
*
* @param \Drupal\user\UserFloodControlInterface $user_flood_control
* The user flood control service.
* @param \Drupal\user\UserStorageInterface $user_storage
* The user storage.
* @param \Drupal\Core\Access\CsrfTokenGenerator $csrf_token
* The CSRF token generator.
* @param \Drupal\user\UserAuthenticationInterface|\Drupal\user\UserAuthInterface $user_auth
* The user authentication.
* @param \Drupal\Core\Routing\RouteProviderInterface $route_provider
* The route provider.
* @param \Symfony\Component\Serializer\Serializer $serializer
* The serializer.
* @param array $serializer_formats
* The available serialization formats.
* @param \Psr\Log\LoggerInterface $logger
* A logger instance.
*/
public function __construct(UserFloodControlInterface $user_flood_control, UserStorageInterface $user_storage, CsrfTokenGenerator $csrf_token, UserAuthenticationInterface|UserAuthInterface $user_auth, RouteProviderInterface $route_provider, Serializer $serializer, array $serializer_formats, LoggerInterface $logger) {
$this->userFloodControl = $user_flood_control;
$this->userStorage = $user_storage;
$this->csrfToken = $csrf_token;
if (!$user_auth instanceof UserAuthenticationInterface) {
@trigger_error('The $user_auth parameter implementing UserAuthInterface is deprecated in drupal:10.3.0 and will be removed in drupal:12.0.0. Implement UserAuthenticationInterface instead. See https://www.drupal.org/node/3411040');
}
$this->userAuth = $user_auth;
$this->serializer = $serializer;
$this->serializerFormats = $serializer_formats;
$this->routeProvider = $route_provider;
$this->logger = $logger;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
if ($container->hasParameter('serializer.formats') && $container->has('serializer')) {
$serializer = $container->get('serializer');
$formats = $container->getParameter('serializer.formats');
}
else {
$formats = ['json'];
$encoders = [new JsonEncoder()];
$serializer = new Serializer([], $encoders);
}
return new static(
$container->get('user.flood_control'),
$container->get('entity_type.manager')->getStorage('user'),
$container->get('csrf_token'),
$container->get('user.auth'),
$container->get('router.route_provider'),
$serializer,
$formats,
$container->get('logger.factory')->get('user')
);
}
/**
* Logs in a user.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request.
*
* @return \Symfony\Component\HttpFoundation\Response
* A response which contains the ID and CSRF token.
*/
public function login(Request $request) {
$format = $this->getRequestFormat($request);
$content = $request->getContent();
$credentials = $this->serializer->decode($content, $format);
if (!isset($credentials['name']) && !isset($credentials['pass'])) {
throw new BadRequestHttpException('Missing credentials.');
}
if (!isset($credentials['name'])) {
throw new BadRequestHttpException('Missing credentials.name.');
}
if (!isset($credentials['pass'])) {
throw new BadRequestHttpException('Missing credentials.pass.');
}
$this->floodControl($request, $credentials['name']);
$account = FALSE;
if ($this->userAuth instanceof UserAuthenticationInterface) {
$account = $this->userAuth->lookupAccount($credentials['name']);
}
else {
$accounts = $this->userStorage->loadByProperties(['name' => $credentials['name']]);
if ($accounts) {
$account = reset($accounts);
}
}
if ($account) {
if ($account->isBlocked()) {
throw new BadRequestHttpException('The user has not been activated or is blocked.');
}
if ($this->userAuth instanceof UserAuthenticationInterface) {
$authenticated = $this->userAuth->authenticateAccount($account, $credentials['pass']) ? $account->id() : FALSE;
}
else {
$authenticated = $this->userAuth->authenticateAccount($credentials['name'], $credentials['pass']);
}
if ($authenticated) {
$this->userFloodControl->clear('user.http_login', $this->getLoginFloodIdentifier($request, $credentials['name']));
$this->userLoginFinalize($account);
// Send basic metadata about the logged in user.
$response_data = [];
if ($account->get('uid')->access('view', $account)) {
$response_data['current_user']['uid'] = $account->id();
}
if ($account->get('roles')->access('view', $account)) {
$response_data['current_user']['roles'] = $account->getRoles();
}
if ($account->get('name')->access('view', $account)) {
$response_data['current_user']['name'] = $account->getAccountName();
}
$response_data['csrf_token'] = $this->csrfToken->get('rest');
$logout_route = $this->routeProvider->getRouteByName('user.logout.http');
// Trim '/' off path to match \Drupal\Core\Access\CsrfAccessCheck.
$logout_path = ltrim($logout_route->getPath(), '/');
$response_data['logout_token'] = $this->csrfToken->get($logout_path);
$encoded_response_data = $this->serializer->encode($response_data, $format);
return new Response($encoded_response_data);
}
}
$flood_config = $this->config('user.flood');
if ($identifier = $this->getLoginFloodIdentifier($request, $credentials['name'])) {
$this->userFloodControl->register('user.http_login', $flood_config->get('user_window'), $identifier);
}
// Always register an IP-based failed login event.
$this->userFloodControl->register('user.failed_login_ip', $flood_config->get('ip_window'));
throw new BadRequestHttpException('Sorry, unrecognized username or password.');
}
/**
* Resets a user password.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request.
*
* @return \Symfony\Component\HttpFoundation\Response
* The response object.
*/
public function resetPassword(Request $request) {
$format = $this->getRequestFormat($request);
$content = $request->getContent();
$credentials = $this->serializer->decode($content, $format);
// Check if a name or mail is provided.
if (!isset($credentials['name']) && !isset($credentials['mail'])) {
throw new BadRequestHttpException('Missing credentials.name or credentials.mail');
}
// Load by name if provided.
$identifier = '';
if (isset($credentials['name'])) {
$identifier = $credentials['name'];
$users = $this->userStorage->loadByProperties(['name' => trim($identifier)]);
}
elseif (isset($credentials['mail'])) {
$identifier = $credentials['mail'];
$users = $this->userStorage->loadByProperties(['mail' => trim($identifier)]);
}
/** @var \Drupal\user\UserInterface $account */
$account = reset($users);
if ($account && $account->id()) {
if ($account->isBlocked()) {
$this->logger->error('Unable to send password reset email for blocked or not yet activated user %identifier.', [
'%identifier' => $identifier,
]);
return new Response();
}
// Send the password reset email.
$mail = _user_mail_notify('password_reset', $account);
if (empty($mail)) {
throw new BadRequestHttpException('Unable to send email. Contact the site administrator if the problem persists.');
}
else {
$this->logger->info('Password reset instructions mailed to %name at %email.', ['%name' => $account->getAccountName(), '%email' => $account->getEmail()]);
return new Response();
}
}
// Error if no users found with provided name or mail.
$this->logger->error('Unable to send password reset email for unrecognized username or email address %identifier.', [
'%identifier' => $identifier,
]);
return new Response();
}
/**
* Verifies if the user is blocked.
*
* @param string $name
* The username.
*
* @return bool
* TRUE if the user is blocked, otherwise FALSE.
*
* @deprecated in drupal:10.3.0 and is removed from drupal:12.0.0. There
* is no replacement.
* @see https://www.drupal.org/node/3425340
*/
protected function userIsBlocked($name) {
@trigger_error(__METHOD__ . ' is deprecated in drupal:10.3.0 and is removed from drupal:12.0.0. There is no replacement. See https://www.drupal.org/node/3425340', E_USER_DEPRECATED);
return user_is_blocked($name);
}
/**
* Finalizes the user login.
*
* @param \Drupal\user\UserInterface $user
* The user.
*/
protected function userLoginFinalize(UserInterface $user) {
user_login_finalize($user);
}
/**
* Logs out a user.
*
* @return \Symfony\Component\HttpFoundation\Response
* The response object.
*/
public function logout() {
$this->userLogout();
return new Response(NULL, 204);
}
/**
* Logs the user out.
*/
protected function userLogout() {
user_logout();
}
/**
* Checks whether a user is logged in or not.
*
* @return \Symfony\Component\HttpFoundation\Response
* The response.
*/
public function loginStatus() {
if ($this->currentUser()->isAuthenticated()) {
$response = new Response(self::LOGGED_IN);
}
else {
$response = new Response(self::LOGGED_OUT);
}
$response->headers->set('Content-Type', 'text/plain');
return $response;
}
/**
* Gets the format of the current request.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The current request.
*
* @return string
* The format of the request.
*/
protected function getRequestFormat(Request $request) {
$format = $request->getRequestFormat();
if (!in_array($format, $this->serializerFormats)) {
throw new BadRequestHttpException("Unrecognized format: $format.");
}
return $format;
}
/**
* Enforces flood control for the current login request.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The current request.
* @param string $username
* The user name sent for login credentials.
*/
protected function floodControl(Request $request, $username) {
$flood_config = $this->config('user.flood');
if (!$this->userFloodControl->isAllowed('user.failed_login_ip', $flood_config->get('ip_limit'), $flood_config->get('ip_window'))) {
throw new AccessDeniedHttpException('Access is blocked because of IP based flood prevention.', NULL, Response::HTTP_TOO_MANY_REQUESTS);
}
if ($identifier = $this->getLoginFloodIdentifier($request, $username)) {
// Don't allow login if the limit for this user has been reached.
// Default is to allow 5 failed attempts every 6 hours.
if (!$this->userFloodControl->isAllowed('user.http_login', $flood_config->get('user_limit'), $flood_config->get('user_window'), $identifier)) {
if ($flood_config->get('uid_only')) {
$error_message = sprintf('There have been more than %s failed login attempts for this account. It is temporarily blocked. Try again later or request a new password.', $flood_config->get('user_limit'));
}
else {
$error_message = 'Too many failed login attempts from your IP address. This IP address is temporarily blocked.';
}
throw new AccessDeniedHttpException($error_message, NULL, Response::HTTP_TOO_MANY_REQUESTS);
}
}
}
/**
* Gets the login identifier for user login flood control.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The current request.
* @param string $username
* The username supplied in login credentials.
*
* @return string
* The login identifier or if the user does not exist an empty string.
*/
protected function getLoginFloodIdentifier(Request $request, $username) {
$flood_config = $this->config('user.flood');
$accounts = $this->userStorage->loadByProperties(['name' => $username, 'status' => 1]);
if ($account = reset($accounts)) {
if ($flood_config->get('uid_only')) {
// Register flood events based on the uid only, so they apply for any
// IP address. This is the most secure option.
$identifier = $account->id();
}
else {
// The default identifier is a combination of uid and IP address. This
// is less secure but more resistant to denial-of-service attacks that
// could lock out all users with public user names.
$identifier = $account->id() . '-' . $request->getClientIp();
}
return $identifier;
}
return '';
}
}

View File

@@ -0,0 +1,446 @@
<?php
namespace Drupal\user\Controller;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Component\Utility\Crypt;
use Drupal\Component\Utility\Xss;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Datetime\DateFormatterInterface;
use Drupal\Core\Flood\FloodInterface;
use Drupal\Core\Url;
use Drupal\user\Form\UserPasswordResetForm;
use Drupal\user\UserDataInterface;
use Drupal\user\UserInterface;
use Drupal\user\UserStorageInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
/**
* Controller routines for user routes.
*/
class UserController extends ControllerBase {
/**
* The date formatter service.
*
* @var \Drupal\Core\Datetime\DateFormatterInterface
*/
protected $dateFormatter;
/**
* The user storage.
*
* @var \Drupal\user\UserStorageInterface
*/
protected $userStorage;
/**
* The user data service.
*
* @var \Drupal\user\UserDataInterface
*/
protected $userData;
/**
* A logger instance.
*
* @var \Psr\Log\LoggerInterface
*/
protected $logger;
/**
* The flood service.
*
* @var \Drupal\Core\Flood\FloodInterface
*/
protected $flood;
/**
* Constructs a UserController object.
*
* @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter
* The date formatter service.
* @param \Drupal\user\UserStorageInterface $user_storage
* The user storage.
* @param \Drupal\user\UserDataInterface $user_data
* The user data service.
* @param \Psr\Log\LoggerInterface $logger
* A logger instance.
* @param \Drupal\Core\Flood\FloodInterface $flood
* The flood service.
* @param \Drupal\Component\Datetime\TimeInterface|null $time
* The time service.
*/
public function __construct(
DateFormatterInterface $date_formatter,
UserStorageInterface $user_storage,
UserDataInterface $user_data,
LoggerInterface $logger,
FloodInterface $flood,
protected ?TimeInterface $time = NULL,
) {
$this->dateFormatter = $date_formatter;
$this->userStorage = $user_storage;
$this->userData = $user_data;
$this->logger = $logger;
$this->flood = $flood;
if ($this->time === NULL) {
@trigger_error('Calling ' . __METHOD__ . ' without the $time argument is deprecated in drupal:10.3.0 and it will be required in drupal:11.0.0. See https://www.drupal.org/node/3112298', E_USER_DEPRECATED);
$this->time = \Drupal::service('datetime.time');
}
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('date.formatter'),
$container->get('entity_type.manager')->getStorage('user'),
$container->get('user.data'),
$container->get('logger.factory')->get('user'),
$container->get('flood'),
$container->get('datetime.time'),
);
}
/**
* Redirects to the user password reset form.
*
* In order to never disclose a reset link via a referrer header this
* controller must always return a redirect response.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request.
* @param int $uid
* User ID of the user requesting reset.
* @param int $timestamp
* The current timestamp.
* @param string $hash
* Login link hash.
*
* @return \Symfony\Component\HttpFoundation\RedirectResponse
* The redirect response.
*/
public function resetPass(Request $request, $uid, $timestamp, $hash) {
$account = $this->currentUser();
// When processing the one-time login link, we have to make sure that a user
// isn't already logged in.
if ($account->isAuthenticated()) {
// The current user is already logged in.
if ($account->id() == $uid) {
user_logout();
// We need to begin the redirect process again because logging out will
// destroy the session.
return $this->redirect(
'user.reset',
[
'uid' => $uid,
'timestamp' => $timestamp,
'hash' => $hash,
]
);
}
// A different user is already logged in on the computer.
else {
/** @var \Drupal\user\UserInterface $reset_link_user */
$reset_link_user = $this->userStorage->load($uid);
if ($reset_link_user && $this->validatePathParameters($reset_link_user, $timestamp, $hash)) {
$this->messenger()
->addWarning($this->t('Another user (%other_user) is already logged into the site on this computer, but you tried to use a one-time link for user %resetting_user. <a href=":logout">Log out</a> and try using the link again.',
[
'%other_user' => $account->getAccountName(),
'%resetting_user' => $reset_link_user->getAccountName(),
':logout' => Url::fromRoute('user.logout')->toString(),
]));
}
else {
// Invalid one-time link specifies an unknown user.
$this->messenger()->addError($this->t('The one-time login link you clicked is invalid.'));
}
return $this->redirect('<front>');
}
}
/** @var \Drupal\user\UserInterface $reset_link_user */
$reset_link_user = $this->userStorage->load($uid);
if ($redirect = $this->determineErrorRedirect($reset_link_user, $timestamp, $hash)) {
return $redirect;
}
$session = $request->getSession();
$session->set('pass_reset_hash', $hash);
$session->set('pass_reset_timeout', $timestamp);
return $this->redirect(
'user.reset.form',
['uid' => $uid]
);
}
/**
* Returns the user password reset form.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request.
* @param int $uid
* User ID of the user requesting reset.
*
* @return array|\Symfony\Component\HttpFoundation\RedirectResponse
* The form structure or a redirect response.
*
* @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
* If the pass_reset_timeout or pass_reset_hash are not available in the
* session. Or if $uid is for a blocked user or invalid user ID.
*/
public function getResetPassForm(Request $request, $uid) {
$session = $request->getSession();
$timestamp = $session->get('pass_reset_timeout');
$hash = $session->get('pass_reset_hash');
// As soon as the session variables are used they are removed to prevent the
// hash and timestamp from being leaked unexpectedly. This could occur if
// the user does not click on the log in button on the form.
$session->remove('pass_reset_timeout');
$session->remove('pass_reset_hash');
if (!$hash || !$timestamp) {
throw new AccessDeniedHttpException();
}
/** @var \Drupal\user\UserInterface $user */
$user = $this->userStorage->load($uid);
if ($user === NULL || !$user->isActive()) {
// Blocked or invalid user ID, so deny access. The parameters will be in
// the watchdog's URL for the administrator to check.
throw new AccessDeniedHttpException();
}
// Time out, in seconds, until login URL expires.
$timeout = $this->config('user.settings')->get('password_reset_timeout');
$expiration_date = $user->getLastLoginTime() ? $this->dateFormatter->format($timestamp + $timeout) : NULL;
return $this->formBuilder()->getForm(UserPasswordResetForm::class, $user, $expiration_date, $timestamp, $hash);
}
/**
* Validates user, hash, and timestamp; logs the user in if correct.
*
* @param int $uid
* User ID of the user requesting reset.
* @param int $timestamp
* The current timestamp.
* @param string $hash
* Login link hash.
* @param \Symfony\Component\HttpFoundation\Request $request
* The request.
*
* @return \Symfony\Component\HttpFoundation\RedirectResponse
* Returns a redirect to the user edit form if the information is correct.
* If the information is incorrect redirects to 'user.pass' route with a
* message for the user.
*
* @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
* If $uid is for a blocked user or invalid user ID.
*/
public function resetPassLogin($uid, $timestamp, $hash, Request $request) {
/** @var \Drupal\user\UserInterface $user */
$user = $this->userStorage->load($uid);
if ($redirect = $this->determineErrorRedirect($user, $timestamp, $hash)) {
return $redirect;
}
$flood_config = $this->config('user.flood');
if ($flood_config->get('uid_only')) {
$identifier = $user->id();
}
else {
$identifier = $user->id() . '-' . $request->getClientIP();
}
$this->flood->clear('user.failed_login_user', $identifier);
$this->flood->clear('user.http_login', $identifier);
user_login_finalize($user);
$this->logger->info('User %name used one-time login link at time %timestamp.', ['%name' => $user->getDisplayName(), '%timestamp' => $timestamp]);
$this->messenger()->addStatus($this->t('You have just used your one-time login link. It is no longer necessary to use this link to log in. It is recommended that you set your password.'));
// Let the user's password be changed without the current password
// check.
$token = Crypt::randomBytesBase64(55);
$request->getSession()->set('pass_reset_' . $user->id(), $token);
// Clear any flood events for this user.
$this->flood->clear('user.password_request_user', $uid);
return $this->redirect(
'entity.user.edit_form',
['user' => $user->id()],
[
'query' => ['pass-reset-token' => $token],
'absolute' => TRUE,
]
);
}
/**
* Validates user, hash, and timestamp.
*
* This method allows the 'user.reset' and 'user.reset.login' routes to use
* the same logic to check the user, timestamp and hash and redirect to the
* same location with the same messages.
*
* @param \Drupal\user\UserInterface|null $user
* User requesting reset. NULL if the user does not exist.
* @param int $timestamp
* The current timestamp.
* @param string $hash
* Login link hash.
*
* @return \Symfony\Component\HttpFoundation\RedirectResponse|null
* Returns a redirect if the information is incorrect. It redirects to
* 'user.pass' route with a message for the user.
*
* @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
* If $uid is for a blocked user or invalid user ID.
*/
protected function determineErrorRedirect(?UserInterface $user, int $timestamp, string $hash): ?RedirectResponse {
// The current user is not logged in, so check the parameters.
$current = $this->time->getRequestTime();
// Verify that the user exists and is active.
if ($user === NULL || !$user->isActive()) {
// Blocked or invalid user ID, so deny access. The parameters will be in
// the watchdog's URL for the administrator to check.
throw new AccessDeniedHttpException();
}
// Time out, in seconds, until login URL expires.
$timeout = $this->config('user.settings')->get('password_reset_timeout');
// No time out for first time login.
if ($user->getLastLoginTime() && $current - $timestamp > $timeout) {
$this->messenger()->addError($this->t('You have tried to use a one-time login link that has expired. Request a new one using the form below.'));
return $this->redirect('user.pass');
}
elseif ($user->isAuthenticated() && $this->validatePathParameters($user, $timestamp, $hash, $timeout)) {
// The information provided is valid.
return NULL;
}
$this->messenger()->addError($this->t('You have tried to use a one-time login link that has either been used or is no longer valid. Request a new one using the form below.'));
return $this->redirect('user.pass');
}
/**
* Validates hash and timestamp.
*
* @param \Drupal\user\UserInterface $user
* User requesting reset.
* @param int $timestamp
* The timestamp.
* @param string $hash
* Login link hash.
* @param int $timeout
* Link expiration timeout.
*
* @return bool
* Whether the provided data are valid.
*/
protected function validatePathParameters(UserInterface $user, int $timestamp, string $hash, int $timeout = 0): bool {
$current = \Drupal::time()->getRequestTime();
$timeout_valid = ((!empty($timeout) && $current - $timestamp < $timeout) || empty($timeout));
return ($timestamp >= $user->getLastLoginTime()) && $timestamp <= $current && $timeout_valid && hash_equals($hash, user_pass_rehash($user, $timestamp));
}
/**
* Redirects users to their profile page.
*
* This controller assumes that it is only invoked for authenticated users.
* This is enforced for the 'user.page' route with the '_user_is_logged_in'
* requirement.
*
* @return \Symfony\Component\HttpFoundation\RedirectResponse
* Returns a redirect to the profile of the currently logged in user.
*/
public function userPage() {
return $this->redirect('entity.user.canonical', ['user' => $this->currentUser()->id()]);
}
/**
* Redirects users to their profile edit page.
*
* This controller assumes that it is only invoked for authenticated users.
* This is typically enforced with the '_user_is_logged_in' requirement.
*
* @return \Symfony\Component\HttpFoundation\RedirectResponse
* Returns a redirect to the profile edit form of the currently logged in
* user.
*/
public function userEditPage() {
return $this->redirect('entity.user.edit_form', ['user' => $this->currentUser()->id()], [], 302);
}
/**
* Route title callback.
*
* @param \Drupal\user\UserInterface $user
* The user account.
*
* @return string|array
* The user account name as a render array or an empty string if $user is
* NULL.
*/
public function userTitle(?UserInterface $user = NULL) {
return $user ? ['#markup' => $user->getDisplayName(), '#allowed_tags' => Xss::getHtmlTagList()] : '';
}
/**
* Logs the current user out.
*
* @return \Symfony\Component\HttpFoundation\RedirectResponse
* A redirection to home page.
*/
public function logout() {
if ($this->currentUser()->isAuthenticated()) {
user_logout();
}
return $this->redirect('<front>');
}
/**
* Confirms cancelling a user account via an email link.
*
* @param \Drupal\user\UserInterface $user
* The user account.
* @param int $timestamp
* The timestamp.
* @param string $hashed_pass
* The hashed password.
*
* @return \Symfony\Component\HttpFoundation\RedirectResponse
* A redirect response.
*/
public function confirmCancel(UserInterface $user, $timestamp = 0, $hashed_pass = '') {
// Time out in seconds until cancel URL expires; 24 hours = 86400 seconds.
$timeout = 86400;
// Basic validation of arguments.
$account_data = $this->userData->get('user', $user->id());
if (isset($account_data['cancel_method']) && !empty($timestamp) && !empty($hashed_pass)) {
// Validate expiration and hashed password/login.
if ($user->id() && $this->validatePathParameters($user, $timestamp, $hashed_pass, $timeout)) {
$edit = [
'user_cancel_notify' => $account_data['cancel_notify'] ?? $this->config('user.settings')->get('notify.status_canceled'),
];
user_cancel($edit, $user->id(), $account_data['cancel_method']);
// Since user_cancel() is not invoked via Form API, batch processing
// needs to be invoked manually and should redirect to the front page
// after completion.
return batch_process('<front>');
}
else {
$this->messenger()->addError($this->t('You have tried to use an account cancellation link that has expired. Request a new one using the form below.'));
return $this->redirect('entity.user.cancel_form', ['user' => $user->id()], ['absolute' => TRUE]);
}
}
throw new AccessDeniedHttpException();
}
}

View File

@@ -0,0 +1,117 @@
<?php
namespace Drupal\user\Entity;
use Drupal\Core\Entity\EntityHandlerInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\Routing\EntityRouteProviderInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
/**
* Provides routes for the entity permissions form.
*
* Use this class or EntityPermissionsRouteProviderWithCheck as a route
* provider for an entity type such as Vocabulary. Either one will provide
* routes for the entity permissions form. The
* EntityPermissionsRouteProviderWithCheck class provides a custom access check:
* it denies access if there are no entity-specific permissions. If you know
* that each entity has permissions, or if the check is too expensive, then use
* this class.
*/
class EntityPermissionsRouteProvider implements EntityRouteProviderInterface, EntityHandlerInterface {
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Constructs a new EntityPermissionsRouteProvider.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager) {
$this->entityTypeManager = $entity_type_manager;
}
/**
* {@inheritdoc}
*/
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
return new static(
$container->get('entity_type.manager')
);
}
/**
* {@inheritdoc}
*/
public function getRoutes(EntityTypeInterface $entity_type) {
$collection = new RouteCollection();
$entity_type_id = $entity_type->id();
if ($entity_permissions_route = $this->getEntityPermissionsRoute($entity_type)) {
$collection->add("entity.$entity_type_id.entity_permissions_form", $entity_permissions_route);
}
return $collection;
}
/**
* Gets the entity permissions route.
*
* Built only for entity types that are bundles of other entity types and
* define the 'entity-permissions-form' link template.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type.
*
* @return \Symfony\Component\Routing\Route|null
* The generated route, if available.
*/
protected function getEntityPermissionsRoute(EntityTypeInterface $entity_type): ?Route {
if (!$entity_type->hasLinkTemplate('entity-permissions-form')) {
return NULL;
}
if (!$bundle_of_id = $entity_type->getBundleOf()) {
return NULL;
}
$entity_type_id = $entity_type->id();
$route = new Route(
$entity_type->getLinkTemplate('entity-permissions-form'),
[
'_title' => 'Manage permissions',
'_form' => 'Drupal\user\Form\EntityPermissionsForm',
'entity_type_id' => $bundle_of_id,
'bundle_entity_type' => $entity_type_id,
],
[
'_permission' => 'administer permissions',
],
[
// Indicate that Drupal\Core\Entity\Enhancer\EntityBundleRouteEnhancer should
// set the bundle parameter.
'_field_ui' => TRUE,
'parameters' => [
$entity_type_id => [
'type' => "entity:$entity_type_id",
'with_config_overrides' => TRUE,
],
],
'_admin_route' => TRUE,
]
);
return $route;
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace Drupal\user\Entity;
use Drupal\Core\Entity\EntityTypeInterface;
use Symfony\Component\Routing\Route;
/**
* Provides routes for the entity permissions form with a custom access check.
*
* Use this class or EntityPermissionsRouteProvider as a route provider for an
* entity type such as BlockContentType. Either one will provide routes for the
* entity permissions form. This class provides a custom access check: it denies
* access if there are no entity-specific permissions. If you know that each
* entity has permissions, or if the check is too expensive, then use
* EntityPermissionsRouteProvider instead of this class.
*/
class EntityPermissionsRouteProviderWithCheck extends EntityPermissionsRouteProvider {
/**
* {@inheritdoc}
*/
protected function getEntityPermissionsRoute(EntityTypeInterface $entity_type): ?Route {
$route = parent::getEntityPermissionsRoute($entity_type);
if ($route) {
$route->setRequirement('_custom_access', '\Drupal\user\Form\EntityPermissionsForm::access');
}
return $route;
}
}

View File

@@ -0,0 +1,268 @@
<?php
namespace Drupal\user\Entity;
use Drupal\Core\Config\Action\Attribute\ActionMethod;
use Drupal\Core\Config\Entity\ConfigEntityBase;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\user\RoleInterface;
/**
* Defines the user role entity class.
*
* @ConfigEntityType(
* id = "user_role",
* label = @Translation("Role"),
* label_collection = @Translation("Roles"),
* label_singular = @Translation("role"),
* label_plural = @Translation("roles"),
* label_count = @PluralTranslation(
* singular = "@count role",
* plural = "@count roles",
* ),
* handlers = {
* "storage" = "Drupal\user\RoleStorage",
* "access" = "Drupal\user\RoleAccessControlHandler",
* "list_builder" = "Drupal\user\RoleListBuilder",
* "form" = {
* "default" = "Drupal\user\RoleForm",
* "delete" = "Drupal\Core\Entity\EntityDeleteForm"
* }
* },
* admin_permission = "administer permissions",
* config_prefix = "role",
* static_cache = TRUE,
* entity_keys = {
* "id" = "id",
* "weight" = "weight",
* "label" = "label"
* },
* links = {
* "delete-form" = "/admin/people/roles/manage/{user_role}/delete",
* "edit-form" = "/admin/people/roles/manage/{user_role}",
* "edit-permissions-form" = "/admin/people/permissions/{user_role}",
* "collection" = "/admin/people/roles",
* },
* config_export = {
* "id",
* "label",
* "weight",
* "is_admin",
* "permissions",
* }
* )
*/
class Role extends ConfigEntityBase implements RoleInterface {
/**
* The machine name of this role.
*
* @var string
*/
protected $id;
/**
* The human-readable label of this role.
*
* @var string
*/
protected $label;
/**
* The weight of this role in administrative listings.
*
* @var int
*/
protected $weight;
/**
* The permissions belonging to this role.
*
* @var array
*/
protected $permissions = [];
/**
* An indicator whether the role has all permissions.
*
* @var bool
*/
protected $is_admin;
/**
* {@inheritdoc}
*/
public function getPermissions() {
if ($this->isAdmin()) {
return [];
}
return $this->permissions;
}
/**
* {@inheritdoc}
*/
public function getWeight() {
return $this->get('weight');
}
/**
* {@inheritdoc}
*/
public function setWeight($weight) {
$this->set('weight', $weight);
return $this;
}
/**
* {@inheritdoc}
*/
public function hasPermission($permission) {
if ($this->isAdmin()) {
return TRUE;
}
return in_array($permission, $this->permissions);
}
/**
* {@inheritdoc}
*/
#[ActionMethod(adminLabel: new TranslatableMarkup('Add permission to role'))]
public function grantPermission($permission) {
if ($this->isAdmin()) {
return $this;
}
if (!$this->hasPermission($permission)) {
$this->permissions[] = $permission;
}
return $this;
}
/**
* {@inheritdoc}
*/
public function revokePermission($permission) {
if ($this->isAdmin()) {
return $this;
}
$this->permissions = array_diff($this->permissions, [$permission]);
return $this;
}
/**
* {@inheritdoc}
*/
public function isAdmin() {
return (bool) $this->is_admin;
}
/**
* {@inheritdoc}
*/
public function setIsAdmin($is_admin) {
$this->is_admin = $is_admin;
return $this;
}
/**
* {@inheritdoc}
*/
public static function postLoad(EntityStorageInterface $storage, array &$entities) {
parent::postLoad($storage, $entities);
// Sort the queried roles by their weight.
// See \Drupal\Core\Config\Entity\ConfigEntityBase::sort().
uasort($entities, [static::class, 'sort']);
}
/**
* {@inheritdoc}
*/
public function preSave(EntityStorageInterface $storage) {
parent::preSave($storage);
if (!isset($this->weight) && ($roles = $storage->loadMultiple())) {
// Set a role weight to make this new role last.
$max = array_reduce($roles, function ($max, $role) {
return $max > $role->weight ? $max : $role->weight;
});
$this->weight = $max + 1;
}
if (!$this->isSyncing() && $this->hasTrustedData()) {
// Permissions are always ordered alphabetically to avoid conflicts in the
// exported configuration. If the save is not trusted then the
// configuration will be sorted by StorableConfigBase.
sort($this->permissions);
}
}
/**
* {@inheritdoc}
*/
public function calculateDependencies() {
parent::calculateDependencies();
// Load all permission definitions.
$permission_definitions = \Drupal::service('user.permissions')->getPermissions();
$valid_permissions = array_intersect($this->permissions, array_keys($permission_definitions));
$invalid_permissions = array_diff($this->permissions, $valid_permissions);
if (!empty($invalid_permissions)) {
throw new \RuntimeException('Adding non-existent permissions to a role is not allowed. The incorrect permissions are "' . implode('", "', $invalid_permissions) . '".');
}
foreach ($valid_permissions as $permission) {
// Depend on the module that is providing this permissions.
$this->addDependency('module', $permission_definitions[$permission]['provider']);
// Depend on any other dependencies defined by permissions granted to
// this role.
if (!empty($permission_definitions[$permission]['dependencies'])) {
$this->addDependencies($permission_definitions[$permission]['dependencies']);
}
}
return $this;
}
/**
* {@inheritdoc}
*/
public function onDependencyRemoval(array $dependencies) {
$changed = parent::onDependencyRemoval($dependencies);
// Load all permission definitions.
$permission_definitions = \Drupal::service('user.permissions')->getPermissions();
// Convert config and content entity dependencies to a list of names to make
// it easier to check.
foreach (['content', 'config'] as $type) {
$dependencies[$type] = array_keys($dependencies[$type]);
}
// Remove any permissions from the role that are dependent on anything being
// deleted or uninstalled.
foreach ($this->permissions as $key => $permission) {
if (!isset($permission_definitions[$permission])) {
// If the permission is not defined then there's nothing we can do.
continue;
}
if (in_array($permission_definitions[$permission]['provider'], $dependencies['module'], TRUE)) {
unset($this->permissions[$key]);
$changed = TRUE;
// Process the next permission.
continue;
}
if (isset($permission_definitions[$permission]['dependencies'])) {
foreach ($permission_definitions[$permission]['dependencies'] as $type => $list) {
if (array_intersect($list, $dependencies[$type])) {
unset($this->permissions[$key]);
$changed = TRUE;
// Process the next permission.
continue 2;
}
}
}
}
return $changed;
}
}

View File

@@ -0,0 +1,603 @@
<?php
namespace Drupal\user\Entity;
use Drupal\Core\Entity\ContentEntityBase;
use Drupal\Core\Entity\EntityChangedTrait;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Flood\PrefixFloodInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\user\RoleInterface;
use Drupal\user\StatusItem;
use Drupal\user\TimeZoneItem;
use Drupal\user\UserInterface;
/**
* Defines the user entity class.
*
* The base table name here is plural, despite Drupal table naming standards,
* because "user" is a reserved word in many databases.
*
* @ContentEntityType(
* id = "user",
* label = @Translation("User"),
* label_collection = @Translation("Users"),
* label_singular = @Translation("user"),
* label_plural = @Translation("users"),
* label_count = @PluralTranslation(
* singular = "@count user",
* plural = "@count users",
* ),
* handlers = {
* "storage" = "Drupal\user\UserStorage",
* "storage_schema" = "Drupal\user\UserStorageSchema",
* "access" = "Drupal\user\UserAccessControlHandler",
* "list_builder" = "Drupal\user\UserListBuilder",
* "views_data" = "Drupal\user\UserViewsData",
* "route_provider" = {
* "html" = "Drupal\user\Entity\UserRouteProvider",
* },
* "form" = {
* "default" = "Drupal\user\ProfileForm",
* "cancel" = "Drupal\user\Form\UserCancelForm",
* "register" = "Drupal\user\RegisterForm"
* },
* "translation" = "Drupal\user\ProfileTranslationHandler"
* },
* admin_permission = "administer users",
* base_table = "users",
* data_table = "users_field_data",
* translatable = TRUE,
* entity_keys = {
* "id" = "uid",
* "langcode" = "langcode",
* "uuid" = "uuid"
* },
* links = {
* "canonical" = "/user/{user}",
* "edit-form" = "/user/{user}/edit",
* "cancel-form" = "/user/{user}/cancel",
* "collection" = "/admin/people",
* },
* field_ui_base_route = "entity.user.admin_form",
* common_reference_target = TRUE
* )
*/
class User extends ContentEntityBase implements UserInterface {
use EntityChangedTrait;
/**
* Stores a reference for a reusable anonymous user entity.
*
* @var \Drupal\user\UserInterface
*/
protected static $anonymousUser;
/**
* {@inheritdoc}
*/
public function isNew() {
return !empty($this->enforceIsNew) || $this->id() === NULL;
}
/**
* {@inheritdoc}
*/
public function label() {
return $this->getDisplayName();
}
/**
* {@inheritdoc}
*/
public function preSave(EntityStorageInterface $storage) {
parent::preSave($storage);
// Make sure that the authenticated/anonymous roles are not persisted.
foreach ($this->get('roles') as $index => $item) {
if (in_array($item->target_id, [RoleInterface::ANONYMOUS_ID, RoleInterface::AUTHENTICATED_ID])) {
$this->get('roles')->offsetUnset($index);
}
}
// Store account cancellation information.
foreach (['user_cancel_method', 'user_cancel_notify'] as $key) {
if (isset($this->{$key})) {
\Drupal::service('user.data')->set('user', $this->id(), substr($key, 5), $this->{$key});
}
}
}
/**
* {@inheritdoc}
*/
public function postSave(EntityStorageInterface $storage, $update = TRUE) {
parent::postSave($storage, $update);
if ($update) {
$session_manager = \Drupal::service('session_manager');
// If the password has been changed, delete all open sessions for the
// user and recreate the current one.
if ($this->pass->value != $this->original->pass->value) {
$session_manager->delete($this->id());
if ($this->id() == \Drupal::currentUser()->id()) {
\Drupal::service('session')->migrate();
}
$flood_config = \Drupal::config('user.flood');
$flood_service = \Drupal::flood();
$identifier = $this->id();
if ($flood_config->get('uid_only')) {
// Clear flood events based on the uid only if configured.
$flood_service->clear('user.failed_login_user', $identifier);
}
elseif ($flood_service instanceof PrefixFloodInterface) {
$flood_service->clearByPrefix('user.failed_login_user', $identifier);
}
}
// If the user was blocked, delete the user's sessions to force a logout.
if ($this->original->status->value != $this->status->value && $this->status->value == 0) {
$session_manager->delete($this->id());
}
// Send emails after we have the new user object.
if ($this->status->value != $this->original->status->value) {
// The user's status is changing; conditionally send notification email.
$op = $this->status->value == 1 ? 'status_activated' : 'status_blocked';
_user_mail_notify($op, $this);
}
}
}
/**
* {@inheritdoc}
*/
public static function postDelete(EntityStorageInterface $storage, array $entities) {
parent::postDelete($storage, $entities);
$uids = array_keys($entities);
\Drupal::service('user.data')->delete(NULL, $uids);
}
/**
* {@inheritdoc}
*/
public function getRoles($exclude_locked_roles = FALSE) {
$roles = [];
// Users with an ID always have the authenticated user role.
if (!$exclude_locked_roles) {
if ($this->isAuthenticated()) {
$roles[] = RoleInterface::AUTHENTICATED_ID;
}
else {
$roles[] = RoleInterface::ANONYMOUS_ID;
}
}
foreach ($this->get('roles') as $role) {
if ($role->target_id) {
$roles[] = $role->target_id;
}
}
return $roles;
}
/**
* {@inheritdoc}
*/
public function hasRole($rid) {
return in_array($rid, $this->getRoles());
}
/**
* {@inheritdoc}
*/
public function addRole($rid) {
if (in_array($rid, [RoleInterface::AUTHENTICATED_ID, RoleInterface::ANONYMOUS_ID])) {
throw new \InvalidArgumentException('Anonymous or authenticated role ID must not be assigned manually.');
}
$roles = $this->getRoles(TRUE);
$roles[] = $rid;
$this->set('roles', array_unique($roles));
return $this;
}
/**
* {@inheritdoc}
*/
public function removeRole($rid) {
$this->set('roles', array_diff($this->getRoles(TRUE), [$rid]));
return $this;
}
/**
* {@inheritdoc}
*/
public function hasPermission(/* string */$permission) {
if (!is_string($permission)) {
@trigger_error('Calling ' . __METHOD__ . '() with a $permission parameter of type other than string is deprecated in drupal:10.3.0 and will cause an error in drupal:11.0.0. See https://www.drupal.org/node/3411485', E_USER_DEPRECATED);
return FALSE;
}
return \Drupal::service('permission_checker')->hasPermission($permission, $this);
}
/**
* {@inheritdoc}
*/
public function getPassword() {
return $this->get('pass')->value;
}
/**
* {@inheritdoc}
*/
public function setPassword(#[\SensitiveParameter] $password) {
$this->get('pass')->value = $password;
return $this;
}
/**
* {@inheritdoc}
*/
public function getEmail() {
return $this->get('mail')->value;
}
/**
* {@inheritdoc}
*/
public function setEmail($mail) {
$this->get('mail')->value = $mail;
return $this;
}
/**
* {@inheritdoc}
*/
public function getCreatedTime() {
return $this->get('created')->value;
}
/**
* {@inheritdoc}
*/
public function getLastAccessedTime() {
return $this->get('access')->value;
}
/**
* {@inheritdoc}
*/
public function setLastAccessTime($timestamp) {
$this->get('access')->value = $timestamp;
return $this;
}
/**
* {@inheritdoc}
*/
public function getLastLoginTime() {
return $this->get('login')->value;
}
/**
* {@inheritdoc}
*/
public function setLastLoginTime($timestamp) {
$this->get('login')->value = $timestamp;
return $this;
}
/**
* {@inheritdoc}
*/
public function isActive() {
return $this->get('status')->value == 1;
}
/**
* {@inheritdoc}
*/
public function isBlocked() {
return $this->get('status')->value == 0;
}
/**
* {@inheritdoc}
*/
public function activate() {
if ($this->isAnonymous()) {
throw new \LogicException('The anonymous user account should remain blocked at all times.');
}
$this->get('status')->value = 1;
return $this;
}
/**
* {@inheritdoc}
*/
public function block() {
$this->get('status')->value = 0;
return $this;
}
/**
* {@inheritdoc}
*/
public function getTimeZone() {
return $this->get('timezone')->value;
}
/**
* {@inheritdoc}
*/
public function getPreferredLangcode($fallback_to_default = TRUE) {
$language_list = $this->languageManager()->getLanguages();
$preferred_langcode = $this->get('preferred_langcode')->value;
if (!empty($preferred_langcode) && isset($language_list[$preferred_langcode])) {
return $language_list[$preferred_langcode]->getId();
}
else {
return $fallback_to_default ? $this->languageManager()->getDefaultLanguage()->getId() : '';
}
}
/**
* {@inheritdoc}
*/
public function getPreferredAdminLangcode($fallback_to_default = TRUE) {
$language_list = $this->languageManager()->getLanguages();
$preferred_langcode = $this->get('preferred_admin_langcode')->value;
if (!empty($preferred_langcode) && isset($language_list[$preferred_langcode])) {
return $language_list[$preferred_langcode]->getId();
}
else {
return $fallback_to_default ? $this->languageManager()->getDefaultLanguage()->getId() : '';
}
}
/**
* {@inheritdoc}
*/
public function getInitialEmail() {
return $this->get('init')->value;
}
/**
* {@inheritdoc}
*/
public function isAuthenticated() {
return $this->id() > 0;
}
/**
* {@inheritdoc}
*/
public function isAnonymous() {
return $this->id() === 0 || $this->id() === '0';
}
/**
* {@inheritdoc}
*/
public function getAccountName() {
return $this->get('name')->value ?: '';
}
/**
* {@inheritdoc}
*/
public function getDisplayName() {
$name = $this->getAccountName() ?: \Drupal::config('user.settings')->get('anonymous');
\Drupal::moduleHandler()->alter('user_format_name', $name, $this);
return $name;
}
/**
* {@inheritdoc}
*/
public function setUsername($username) {
$this->set('name', $username);
return $this;
}
/**
* {@inheritdoc}
*/
public function setExistingPassword(#[\SensitiveParameter] $password) {
$this->get('pass')->existing = $password;
return $this;
}
/**
* {@inheritdoc}
*/
public function checkExistingPassword(UserInterface $account_unchanged) {
$existing = $this->get('pass')->existing;
return $existing !== NULL && strlen($existing) > 0 &&
\Drupal::service('password')->check(trim($existing), $account_unchanged->getPassword());
}
/**
* Returns an anonymous user entity.
*
* @return \Drupal\user\UserInterface
* An anonymous user entity.
*/
public static function getAnonymousUser() {
if (!isset(static::$anonymousUser)) {
// @todo Use the entity factory once available, see
// https://www.drupal.org/node/1867228.
$entity_type_manager = \Drupal::entityTypeManager();
$entity_type = $entity_type_manager->getDefinition('user');
$class = $entity_type->getClass();
static::$anonymousUser = new $class([
'uid' => [LanguageInterface::LANGCODE_DEFAULT => 0],
'name' => [LanguageInterface::LANGCODE_DEFAULT => ''],
// Explicitly set the langcode to ensure that field definitions do not
// need to be fetched to figure out a default.
'langcode' => [LanguageInterface::LANGCODE_DEFAULT => LanguageInterface::LANGCODE_NOT_SPECIFIED],
], $entity_type->id());
}
return clone static::$anonymousUser;
}
/**
* {@inheritdoc}
*/
public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
/** @var \Drupal\Core\Field\BaseFieldDefinition[] $fields */
$fields = parent::baseFieldDefinitions($entity_type);
$fields['uid']->setLabel(t('User ID'))
->setDescription(t('The user ID.'));
$fields['uuid']->setDescription(t('The user UUID.'));
$fields['langcode']->setLabel(t('Language code'))
->setDescription(t('The user language code.'))
->setDisplayOptions('form', ['region' => 'hidden']);
$fields['preferred_langcode'] = BaseFieldDefinition::create('language')
->setLabel(t('Preferred language code'))
->setDescription(t("The user's preferred language code for receiving emails and viewing the site."))
// @todo Define this via an options provider once
// https://www.drupal.org/node/2329937 is completed.
->addPropertyConstraints('value', [
'AllowedValues' => ['callback' => __CLASS__ . '::getAllowedConfigurableLanguageCodes'],
]);
$fields['preferred_admin_langcode'] = BaseFieldDefinition::create('language')
->setLabel(t('Preferred admin language code'))
->setDescription(t("The user's preferred language code for viewing administration pages."))
// @todo A default value of NULL is ignored, so we have to specify
// an empty field item structure instead. Fix this in
// https://www.drupal.org/node/2318605.
->setDefaultValue([0 => ['value' => NULL]])
// @todo Define this via an options provider once
// https://www.drupal.org/node/2329937 is completed.
->addPropertyConstraints('value', [
'AllowedValues' => ['callback' => __CLASS__ . '::getAllowedConfigurableLanguageCodes'],
]);
// The name should not vary per language. The username is the visual
// identifier for a user and needs to be consistent in all languages.
$fields['name'] = BaseFieldDefinition::create('string')
->setLabel(t('Name'))
->setDescription(t('The name of this user.'))
->setRequired(TRUE)
->setConstraints([
// No Length constraint here because the UserName constraint also covers
// that.
'UserName' => [],
'UserNameUnique' => [],
]);
$fields['name']->getItemDefinition()->setClass('\Drupal\user\UserNameItem');
$fields['pass'] = BaseFieldDefinition::create('password')
->setLabel(t('Password'))
->setDescription(t('The password of this user (hashed).'))
->addConstraint('ProtectedUserField');
$fields['mail'] = BaseFieldDefinition::create('email')
->setLabel(t('Email'))
->setDescription(t('The email of this user.'))
->setDefaultValue('')
->addConstraint('UserMailUnique')
->addConstraint('UserMailRequired')
->addConstraint('ProtectedUserField');
$fields['timezone'] = BaseFieldDefinition::create('string')
->setLabel(t('Timezone'))
->setDescription(t('The timezone of this user.'))
->setSetting('max_length', 32)
// @todo Define this via an options provider once
// https://www.drupal.org/node/2329937 is completed.
->addPropertyConstraints('value', [
'AllowedValues' => ['callback' => __CLASS__ . '::getAllowedTimezones'],
]);
$fields['timezone']->getItemDefinition()->setClass(TimeZoneItem::class);
$fields['status'] = BaseFieldDefinition::create('boolean')
->setLabel(t('User status'))
->setDescription(t('Whether the user is active or blocked.'))
->setDefaultValue(FALSE);
$fields['status']->getItemDefinition()->setClass(StatusItem::class);
$fields['created'] = BaseFieldDefinition::create('created')
->setLabel(t('Created'))
->setDescription(t('The time that the user was created.'));
$fields['changed'] = BaseFieldDefinition::create('changed')
->setLabel(t('Changed'))
->setDescription(t('The time that the user was last edited.'))
->setTranslatable(TRUE);
$fields['access'] = BaseFieldDefinition::create('timestamp')
->setLabel(t('Last access'))
->setDescription(t('The time that the user last accessed the site.'))
->setDefaultValue(0);
$fields['login'] = BaseFieldDefinition::create('timestamp')
->setLabel(t('Last login'))
->setDescription(t('The time that the user last logged in.'))
->setDefaultValue(0);
$fields['init'] = BaseFieldDefinition::create('email')
->setLabel(t('Initial email'))
->setDescription(t('The email address used for initial account creation.'))
->setDefaultValue('');
$fields['roles'] = BaseFieldDefinition::create('entity_reference')
->setLabel(t('Roles'))
->setCardinality(BaseFieldDefinition::CARDINALITY_UNLIMITED)
->setDescription(t('The roles the user has.'))
->setSetting('target_type', 'user_role');
return $fields;
}
/**
* Returns the role storage object.
*
* @return \Drupal\user\RoleStorageInterface
* The role storage object.
*/
protected function getRoleStorage() {
return \Drupal::entityTypeManager()->getStorage('user_role');
}
/**
* Defines allowed timezones for the field's AllowedValues constraint.
*
* @return string[]
* The allowed values.
*/
public static function getAllowedTimezones() {
return \DateTimeZone::listIdentifiers();
}
/**
* Defines allowed configurable language codes for AllowedValues constraints.
*
* @return string[]
* The allowed values.
*/
public static function getAllowedConfigurableLanguageCodes() {
return array_keys(\Drupal::languageManager()->getLanguages(LanguageInterface::STATE_CONFIGURABLE));
}
}

View File

@@ -0,0 +1,52 @@
<?php
namespace Drupal\user\Entity;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\Routing\EntityRouteProviderInterface;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
/**
* Provides routes for the user entity.
*/
class UserRouteProvider implements EntityRouteProviderInterface {
/**
* {@inheritdoc}
*/
public function getRoutes(EntityTypeInterface $entity_type) {
$route_collection = new RouteCollection();
$route = (new Route('/user/{user}'))
->setDefaults([
'_entity_view' => 'user.full',
'_title_callback' => 'Drupal\user\Controller\UserController::userTitle',
])
->setRequirement('user', '\d+')
->setRequirement('_entity_access', 'user.view');
$route_collection->add('entity.user.canonical', $route);
$route = (new Route('/user/{user}/edit'))
->setDefaults([
'_entity_form' => 'user.default',
'_title_callback' => 'Drupal\user\Controller\UserController::userTitle',
])
->setOption('_admin_route', TRUE)
->setRequirement('user', '\d+')
->setRequirement('_entity_access', 'user.update');
$route_collection->add('entity.user.edit_form', $route);
$route = (new Route('/user/{user}/cancel'))
->setDefaults([
'_title' => 'Cancel account',
'_entity_form' => 'user.cancel',
])
->setOption('_admin_route', TRUE)
->setRequirement('user', '\d+')
->setRequirement('_entity_access', 'user.delete');
$route_collection->add('entity.user.cancel_form', $route);
return $route_collection;
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace Drupal\user;
/**
* Defines a common interface for entities that have an owner.
*
* An owner is someone who has primary control over an entity, similar to
* owners in Unix file system access. This may or may not be the entity's
* original author. The owner may also have less permissions than other users,
* such as administrators.
*
* @ingroup entity_type_characteristics
*/
interface EntityOwnerInterface {
/**
* Returns the entity owner's user entity.
*
* @return \Drupal\user\UserInterface
* The owner user entity.
*/
public function getOwner();
/**
* Sets the entity owner's user entity.
*
* @param \Drupal\user\UserInterface $account
* The owner user entity.
*
* @return $this
*/
public function setOwner(UserInterface $account);
/**
* Returns the entity owner's user ID.
*
* @return int|null
* The owner user ID, or NULL in case the user ID field has not been set on
* the entity.
*/
public function getOwnerId();
/**
* Sets the entity owner's user ID.
*
* @param int $uid
* The owner user id.
*
* @return $this
*/
public function setOwnerId($uid);
}

View File

@@ -0,0 +1,90 @@
<?php
namespace Drupal\user;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\Exception\UnsupportedEntityTypeDefinitionException;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Provides a trait for entities that have an owner.
*/
trait EntityOwnerTrait {
/**
* Returns an array of base field definitions for entity owners.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type to add the owner field to.
*
* @return \Drupal\Core\Field\BaseFieldDefinition[]
* An array of base field definitions.
*
* @throws \Drupal\Core\Entity\Exception\UnsupportedEntityTypeDefinitionException
* Thrown when the entity type does not implement EntityOwnerInterface or
* if it does not have an "owner" entity key.
*/
public static function ownerBaseFieldDefinitions(EntityTypeInterface $entity_type) {
if (!is_subclass_of($entity_type->getClass(), EntityOwnerInterface::class)) {
throw new UnsupportedEntityTypeDefinitionException('The entity type ' . $entity_type->id() . ' does not implement \Drupal\user\EntityOwnerInterface.');
}
if (!$entity_type->hasKey('owner')) {
throw new UnsupportedEntityTypeDefinitionException('The entity type ' . $entity_type->id() . ' does not have an "owner" entity key.');
}
return [
$entity_type->getKey('owner') => BaseFieldDefinition::create('entity_reference')
->setLabel(new TranslatableMarkup('User ID'))
->setSetting('target_type', 'user')
->setTranslatable($entity_type->isTranslatable())
->setDefaultValueCallback(static::class . '::getDefaultEntityOwner'),
];
}
/**
* {@inheritdoc}
*/
public function getOwnerId() {
return $this->getEntityKey('owner');
}
/**
* {@inheritdoc}
*/
public function setOwnerId($uid) {
$key = $this->getEntityType()->getKey('owner');
$this->set($key, $uid);
return $this;
}
/**
* {@inheritdoc}
*/
public function getOwner() {
$key = $this->getEntityType()->getKey('owner');
return $this->get($key)->entity;
}
/**
* {@inheritdoc}
*/
public function setOwner(UserInterface $account) {
$key = $this->getEntityType()->getKey('owner');
$this->set($key, $account);
return $this;
}
/**
* Default value callback for 'owner' base field.
*
* @return mixed
* A default value for the owner field.
*/
public static function getDefaultEntityOwner() {
return \Drupal::currentUser()->id();
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace Drupal\user\Event;
/**
* Defines events for the user module.
*/
final class UserEvents {
/**
* The name of the event fired when a login is blocked by flood control.
*
* This event allows modules to perform an action whenever flood control has
* been triggered by excessive login attempts for a particular user account.
* The event listener method receives a \Drupal\user\Event\UserFloodEvent
* instance.
*
* @Event
*
* @see: \Drupal\user\UserFloodControl::isAllowed
* @see: \Drupal\user\EventSubscriber\UserFloodSubscriber
*
* @var string
*/
const FLOOD_BLOCKED_USER = 'user.flood_blocked_user';
/**
* The name of the event fired when a login is blocked by flood control.
*
* This event allows modules to perform an action whenever flood control has
* been triggered by excessive login attempts from a particular IP. The event
* listener method receives a \Drupal\user\Event\UserFloodEvent instance.
*
* @Event
*
* @see: \Drupal\user\UserFloodControl::isAllowed
* @see: \Drupal\user\EventSubscriber\UserFloodSubscriber
*
* @var string
*/
const FLOOD_BLOCKED_IP = 'user.flood_blocked_ip';
}

View File

@@ -0,0 +1,165 @@
<?php
namespace Drupal\user\Event;
use Drupal\Component\EventDispatcher\Event;
/**
* Provides a user flood event for event listeners.
*/
class UserFloodEvent extends Event {
/**
* Flood event name.
*
* @var string
*/
protected $name;
/**
* Flood event threshold.
*
* @var int
*/
protected $threshold;
/**
* Flood event window.
*
* @var int
*/
protected $window;
/**
* Flood event identifier.
*
* @var string
*/
protected $identifier;
/**
* Flood event uid.
*
* @var int
*/
protected $uid = NULL;
/**
* Flood event IP.
*
* @var string
*/
protected $ip = NULL;
/**
* Constructs a user flood event object.
*
* @param string $name
* The name of the flood event.
* @param int $threshold
* The threshold for the flood event.
* @param int $window
* The window for the flood event.
* @param string $identifier
* The identifier of the flood event.
*/
public function __construct($name, $threshold, $window, $identifier) {
$this->name = $name;
$this->threshold = $threshold;
$this->window = $window;
$this->identifier = $identifier;
// The identifier could be a uid or an IP, or a composite of both.
if (is_numeric($identifier)) {
$this->uid = $identifier;
return;
}
if (str_contains($identifier, '-')) {
[$uid, $ip] = explode('-', $identifier);
$this->uid = $uid;
$this->ip = $ip;
return;
}
$this->ip = $identifier;
}
/**
* Gets the name of the user flood event object.
*
* @return string
* The name of the flood event.
*/
public function getName() {
return $this->name;
}
/**
* Gets the threshold for the user flood event object.
*
* @return int
* The threshold for the flood event.
*/
public function getThreshold() {
return $this->threshold;
}
/**
* Gets the window for the user flood event object.
*
* @return int
* The window for the flood event.
*/
public function getWindow() {
return $this->window;
}
/**
* Gets the identifier of the user flood event object.
*
* @return string
* The identifier of the flood event.
*/
public function getIdentifier() {
return $this->identifier;
}
/**
* Gets the IP of the user flood event object.
*
* @return string
* The IP of the flood event.
*/
public function getIp() {
return $this->ip;
}
/**
* Gets the uid of the user flood event object.
*
* @return int
* The uid of the flood event.
*/
public function getUid() {
return $this->uid;
}
/**
* Is the user flood event associated with an IP?
*
* @return bool
* Whether the event has an IP.
*/
public function hasIp() {
return !empty($this->ip);
}
/**
* Is the user flood event associated with a uid?
*
* @return bool
* Whether the event has a uid.
*/
public function hasUid() {
return !empty($this->uid);
}
}

View File

@@ -0,0 +1,89 @@
<?php
namespace Drupal\user\EventSubscriber;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Routing\RouteMatch;
use Drupal\Core\Url;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\KernelEvents;
/**
* Redirects users when access is denied.
*
* Anonymous users are taken to the login page when attempting to access the
* user profile pages. Authenticated users are redirected from the login form to
* their profile page and from the user registration form to their profile edit
* form.
*/
class AccessDeniedSubscriber implements EventSubscriberInterface {
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $account;
/**
* Constructs a new redirect subscriber.
*
* @param \Drupal\Core\Session\AccountInterface $account
* The current user.
*/
public function __construct(AccountInterface $account) {
$this->account = $account;
}
/**
* Redirects users when access is denied.
*
* @param \Symfony\Component\HttpKernel\Event\ExceptionEvent $event
* The event to process.
*/
public function onException(ExceptionEvent $event) {
$exception = $event->getThrowable();
if ($exception instanceof AccessDeniedHttpException) {
$route_name = RouteMatch::createFromRequest($event->getRequest())->getRouteName();
$redirect_url = NULL;
if ($this->account->isAuthenticated()) {
switch ($route_name) {
case 'user.login';
// Redirect an authenticated user to the profile page.
$redirect_url = Url::fromRoute('entity.user.canonical', ['user' => $this->account->id()], ['absolute' => TRUE]);
break;
case 'user.register';
// Redirect an authenticated user to the profile form.
$redirect_url = Url::fromRoute('entity.user.edit_form', ['user' => $this->account->id()], ['absolute' => TRUE]);
break;
}
}
elseif ($route_name === 'user.page') {
$redirect_url = Url::fromRoute('user.login', [], ['absolute' => TRUE]);
}
elseif (in_array($route_name, ['user.logout', 'user.logout.confirm'], TRUE)) {
$redirect_url = Url::fromRoute('<front>', [], ['absolute' => TRUE]);
}
if ($redirect_url) {
$event->setResponse(new RedirectResponse($redirect_url->toString()));
}
}
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents(): array {
// Use a higher priority than
// \Drupal\Core\EventSubscriber\ExceptionLoggingSubscriber, because there's
// no need to log the exception if we can redirect.
$events[KernelEvents::EXCEPTION][] = ['onException', 75];
return $events;
}
}

View File

@@ -0,0 +1,73 @@
<?php
namespace Drupal\user\EventSubscriber;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Site\MaintenanceModeEvents;
use Drupal\Core\Site\MaintenanceModeInterface;
use Drupal\Core\Url;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpKernel\Event\RequestEvent;
/**
* Maintenance mode subscriber to log out users.
*/
class MaintenanceModeSubscriber implements EventSubscriberInterface {
/**
* The maintenance mode.
*
* @var \Drupal\Core\Site\MaintenanceMode
*/
protected $maintenanceMode;
/**
* The current account.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $account;
/**
* Constructs a new MaintenanceModeSubscriber.
*
* @param \Drupal\Core\Site\MaintenanceModeInterface $maintenance_mode
* The maintenance mode.
* @param \Drupal\Core\Session\AccountInterface $account
* The current user.
*/
public function __construct(MaintenanceModeInterface $maintenance_mode, AccountInterface $account) {
$this->maintenanceMode = $maintenance_mode;
$this->account = $account;
}
/**
* Logout users if site is in maintenance mode and user is not exempt.
*
* @param \Symfony\Component\HttpKernel\Event\RequestEvent $event
* The event to process.
*/
public function onMaintenanceModeRequest(RequestEvent $event) {
// If the site is offline, log out unprivileged users.
if ($this->account->isAuthenticated()) {
user_logout();
// Redirect to homepage.
$event->setResponse(
new RedirectResponse(Url::fromRoute('<front>')->toString())
);
}
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents(): array {
$events[MaintenanceModeEvents::MAINTENANCE_MODE_REQUEST][] = [
'onMaintenanceModeRequest',
-900,
];
return $events;
}
}

View File

@@ -0,0 +1,72 @@
<?php
namespace Drupal\user\EventSubscriber;
use Drupal\user\Event\UserEvents;
use Drupal\user\Event\UserFloodEvent;
use Drupal\Core\Site\Settings;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Psr\Log\LoggerInterface;
/**
* Logs details of User Flood Control events.
*/
class UserFloodSubscriber implements EventSubscriberInterface {
/**
* The default logger service.
*
* @var \Psr\Log\LoggerInterface
*/
protected $logger;
/**
* Constructs a UserFloodSubscriber.
*
* @param \Psr\Log\LoggerInterface $logger
* A logger instance.
*/
public function __construct(?LoggerInterface $logger = NULL) {
$this->logger = $logger;
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents(): array {
$events[UserEvents::FLOOD_BLOCKED_USER][] = ['blockedUser'];
$events[UserEvents::FLOOD_BLOCKED_IP][] = ['blockedIp'];
return $events;
}
/**
* An attempt to login has been blocked based on user name.
*
* @param \Drupal\user\Event\UserFloodEvent $floodEvent
* The flood event.
*/
public function blockedUser(UserFloodEvent $floodEvent) {
if (Settings::get('log_user_flood', TRUE)) {
$uid = $floodEvent->getUid();
if ($floodEvent->hasIp()) {
$ip = $floodEvent->getIp();
$this->logger->notice('Flood control blocked login attempt for uid %uid from %ip', ['%uid' => $uid, '%ip' => $ip]);
return;
}
$this->logger->notice('Flood control blocked login attempt for uid %uid', ['%uid' => $uid]);
}
}
/**
* An attempt to login has been blocked based on IP.
*
* @param \Drupal\user\Event\UserFloodEvent $floodEvent
* The flood event.
*/
public function blockedIp(UserFloodEvent $floodEvent) {
if (Settings::get('log_user_flood', TRUE)) {
$this->logger->notice('Flood control blocked login attempt from %ip', ['%ip' => $floodEvent->getIp()]);
}
}
}

View File

@@ -0,0 +1,75 @@
<?php
namespace Drupal\user\EventSubscriber;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Site\Settings;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\TerminateEvent;
use Symfony\Component\HttpKernel\KernelEvents;
/**
* Updates the current user's last access time.
*/
class UserRequestSubscriber implements EventSubscriberInterface {
/**
* The current account.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $account;
/**
* The entity type manager service.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Constructs a new UserRequestSubscriber.
*
* @param \Drupal\Core\Session\AccountInterface $account
* The current user.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager service.
* @param \Drupal\Component\Datetime\TimeInterface|null $time
* The time service.
*/
public function __construct(AccountInterface $account, EntityTypeManagerInterface $entity_type_manager, protected ?TimeInterface $time = NULL) {
$this->account = $account;
$this->entityTypeManager = $entity_type_manager;
if (!$time) {
@trigger_error('Calling ' . __METHOD__ . '() without the $time argument is deprecated in drupal:10.3.0 and it will be required in drupal:11.0.0. See https://www.drupal.org/node/3387233', E_USER_DEPRECATED);
$this->time = \Drupal::service(TimeInterface::class);
}
}
/**
* Updates the current user's last access time.
*
* @param \Symfony\Component\HttpKernel\Event\TerminateEvent $event
* The event to process.
*/
public function onKernelTerminate(TerminateEvent $event) {
if ($this->account->isAuthenticated() && $this->time->getRequestTime() - $this->account->getLastAccessedTime() > Settings::get('session_write_interval', 180)) {
// Do that no more than once per 180 seconds.
/** @var \Drupal\user\UserStorageInterface $storage */
$storage = $this->entityTypeManager->getStorage('user');
$storage->updateLastAccessTimestamp($this->account, $this->time->getRequestTime());
}
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents(): array {
// Should go before other subscribers start to write their caches.
$events[KernelEvents::TERMINATE][] = ['onKernelTerminate', 300];
return $events;
}
}

View File

@@ -0,0 +1,188 @@
<?php
namespace Drupal\user\Form;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Access\AccessResultInterface;
use Drupal\Core\Config\ConfigManagerInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleExtensionList;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\user\PermissionHandlerInterface;
use Drupal\user\RoleStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Routing\Route;
/**
* Provides the permissions administration form for a bundle.
*
* This class handles bundles that are defined by configuration objects.
*
* @internal
*/
class EntityPermissionsForm extends UserPermissionsForm {
/**
* The configuration entity manager.
*
* @var \Drupal\Core\Config\ConfigManagerInterface
*/
protected $configManager;
/**
* The entity type manager service.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The bundle object.
*
* @var \Drupal\Core\Entity\EntityInterface
*/
protected $bundle;
/**
* Constructs a new EntityPermissionsForm.
*
* @param \Drupal\user\PermissionHandlerInterface $permission_handler
* The permission handler.
* @param \Drupal\user\RoleStorageInterface $role_storage
* The role storage.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\Core\Config\ConfigManagerInterface $config_manager
* The configuration entity manager.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager service.
* @param \Drupal\Core\Extension\ModuleExtensionList|null $module_extension_list
* The module extension list.
*/
public function __construct(PermissionHandlerInterface $permission_handler, RoleStorageInterface $role_storage, ModuleHandlerInterface $module_handler, ConfigManagerInterface $config_manager, EntityTypeManagerInterface $entity_type_manager, ?ModuleExtensionList $module_extension_list = NULL) {
if ($module_extension_list === NULL) {
@trigger_error('Calling ' . __METHOD__ . '() without the $module_extension_list argument is deprecated in drupal:10.3.0 and will be required in drupal:12.0.0. See https://www.drupal.org/node/3310017', E_USER_DEPRECATED);
$module_extension_list = \Drupal::service('extension.list.module');
}
parent::__construct($permission_handler, $role_storage, $module_handler, $module_extension_list);
$this->configManager = $config_manager;
$this->entityTypeManager = $entity_type_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('user.permissions'),
$container->get('entity_type.manager')->getStorage('user_role'),
$container->get('module_handler'),
$container->get('config.manager'),
$container->get('entity_type.manager'),
$container->get('extension.list.module'),
);
}
/**
* {@inheritdoc}
*/
protected function permissionsByProvider(): array {
// Get the names of all config entities that depend on $this->bundle.
$config_name = $this->bundle->getConfigDependencyName();
$config_entities = $this->configManager
->getConfigEntitiesToChangeOnDependencyRemoval('config', [$config_name]);
$config_names = array_map(
function ($dependent_config) {
return $dependent_config->getConfigDependencyName();
}, $config_entities['delete'] ?? []
);
$config_names[] = $config_name;
// Find all the permissions that depend on $this->bundle.
$permissions = $this->permissionHandler->getPermissions();
$permissions_by_provider = [];
foreach ($permissions as $permission_name => $permission) {
$required_configs = $permission['dependencies']['config'] ?? [];
if (array_intersect($required_configs, $config_names)) {
$provider = $permission['provider'];
$permissions_by_provider[$provider][$permission_name] = $permission;
}
}
return $permissions_by_provider;
}
/**
* Builds the user permissions administration form for a bundle.
*
* @param array $form
* An associative array containing the structure of the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
* @param string $bundle_entity_type
* (optional) The entity type ID.
* @param string|\Drupal\Core\Entity\EntityInterface $bundle
* (optional) Either the bundle name or the bundle object.
*/
public function buildForm(array $form, FormStateInterface $form_state, ?string $bundle_entity_type = NULL, $bundle = NULL): array {
// Set $this->bundle for use by ::permissionsByProvider().
if ($bundle instanceof EntityInterface) {
$this->bundle = $bundle;
return parent::buildForm($form, $form_state);
}
$this->bundle = $this->entityTypeManager
->getStorage($bundle_entity_type)
->load($bundle);
return parent::buildForm($form, $form_state);
}
/**
* Checks that there are permissions to be managed.
*
* @param \Symfony\Component\Routing\Route $route
* The route to check against.
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The parametrized route.
* @param string|EntityInterface $bundle
* (optional) The bundle. Different entity types can have different names
* for their bundle key, so if not specified on the route via a {bundle}
* parameter, the access checker determines the appropriate key name, and
* gets the value from the corresponding request attribute. For example,
* for nodes, the bundle key is "node_type", so the value would be
* available via the {node_type} parameter rather than a {bundle}
* parameter.
*
* @return \Drupal\Core\Access\AccessResultInterface
* The access result.
*/
public function access(Route $route, RouteMatchInterface $route_match, $bundle = NULL): AccessResultInterface {
$permission = $route->getRequirement('_permission');
if ($permission && !$this->currentUser()->hasPermission($permission)) {
return AccessResult::neutral()->cachePerPermissions();
}
// Set $this->bundle for use by ::permissionsByProvider().
if ($bundle instanceof EntityInterface) {
$this->bundle = $bundle;
}
else {
$bundle_entity_type = $route->getDefault('bundle_entity_type');
$bundle_name = is_string($bundle) ? $bundle : $route_match->getRawParameter($bundle_entity_type);
$this->bundle = $this->entityTypeManager
->getStorage($bundle_entity_type)
->load($bundle_name);
}
if (empty($this->bundle)) {
// A typo in the request path can lead to this case.
return AccessResult::forbidden();
}
return AccessResult::allowedIf((bool) $this->permissionsByProvider());
}
}

View File

@@ -0,0 +1,108 @@
<?php
namespace Drupal\user\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\user\RoleInterface;
use Drupal\user\RoleStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Configure administrator role settings for this site.
*/
class RoleSettingsForm extends FormBase {
/**
* The role storage used when changing the admin role.
*
* @var \Drupal\user\RoleStorageInterface
*/
protected $roleStorage;
/**
* Constructs a \Drupal\user\Form\RoleSettingsForm object.
*
* @param \Drupal\user\RoleStorageInterface $role_storage
* The role storage.
*/
public function __construct(RoleStorageInterface $role_storage) {
$this->roleStorage = $role_storage;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_type.manager')->getStorage('user_role')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'role_settings';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
// Administrative role option.
$form['admin_role'] = [
'#type' => 'details',
'#title' => $this->t('Administrator role'),
'#open' => TRUE,
];
// Do not allow users to set the anonymous or authenticated user roles as
// the administrator role.
$roles = $this->roleStorage->loadMultiple();
unset($roles[RoleInterface::ANONYMOUS_ID]);
unset($roles[RoleInterface::AUTHENTICATED_ID]);
$roles = array_map(fn(RoleInterface $role) => $role->label(), $roles);
$admin_roles = $this->roleStorage->getQuery()
->condition('is_admin', TRUE)
->execute();
$default_value = reset($admin_roles);
$form['admin_role']['user_admin_role'] = [
'#type' => 'select',
'#title' => $this->t('Administrator role'),
'#empty_value' => '',
'#default_value' => $default_value,
'#options' => $roles,
'#description' => $this->t('This role will be automatically granted all permissions.'),
// Don't allow to select a single admin role in case multiple roles got
// marked as admin role already.
'#access' => count($admin_roles) <= 1,
];
$form['actions'] = ['#type' => 'actions'];
$form['actions']['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Save configuration'),
'#button_type' => 'primary',
];
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
if ($form_state->hasValue('user_admin_role')) {
$admin_roles = $this->roleStorage->getQuery()
->condition('is_admin', TRUE)
->execute();
foreach ($admin_roles as $rid) {
$this->roleStorage->load($rid)->setIsAdmin(FALSE)->save();
}
$new_admin_role = $form_state->getValue('user_admin_role');
if ($new_admin_role) {
$this->roleStorage->load($new_admin_role)->setIsAdmin(TRUE)->save();
}
}
}
}

View File

@@ -0,0 +1,162 @@
<?php
namespace Drupal\user\Form;
use Drupal\Core\Entity\ContentEntityConfirmFormBase;
use Drupal\Core\Form\FormStateInterface;
/**
* Provides a confirmation form for cancelling user account.
*
* @internal
*/
class UserCancelForm extends ContentEntityConfirmFormBase {
/**
* Available account cancellation methods.
*
* @var array
*/
protected $cancelMethods;
/**
* Whether it is allowed to select cancellation method.
*
* @var bool
*/
protected $selectCancel;
/**
* The account being cancelled.
*
* @var \Drupal\user\UserInterface
*/
protected $entity;
/**
* {@inheritdoc}
*/
public function getQuestion() {
if ($this->entity->id() == $this->currentUser()->id()) {
return $this->t('Are you sure you want to cancel your account?');
}
return $this->t('Are you sure you want to cancel the account %name?', ['%name' => $this->entity->label()]);
}
/**
* {@inheritdoc}
*/
public function getCancelUrl() {
return $this->entity->toUrl();
}
/**
* {@inheritdoc}
*/
public function getDescription() {
if ($this->selectCancel) {
return '';
}
$default_method = $this->config('user.settings')->get('cancel_method');
$own_account = $this->entity->id() == $this->currentUser()->id();
// Options supplied via user_cancel_methods() can have a custom
// #confirm_description property for the confirmation form description.
// This text refers to "Your account" so only user it if cancelling own account.
if ($own_account && isset($this->cancelMethods[$default_method]['#confirm_description'])) {
return $this->cancelMethods[$default_method]['#confirm_description'];
}
return $this->cancelMethods['#options'][$default_method];
}
/**
* {@inheritdoc}
*/
public function getConfirmText() {
return $this->t('Confirm');
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$user = $this->currentUser();
$this->cancelMethods = user_cancel_methods();
// Display account cancellation method selection, if allowed.
$own_account = $this->entity->id() == $user->id();
$this->selectCancel = $user->hasPermission('administer users') || $user->hasPermission('select account cancellation method');
$form['user_cancel_method'] = [
'#type' => 'radios',
'#title' => $own_account ? $this->t('When cancelling your account') : $this->t('Cancellation method'),
'#access' => $this->selectCancel,
];
$form['user_cancel_method'] += $this->cancelMethods;
// When managing another user, can skip the account cancellation
// confirmation mail (by default).
$override_access = !$own_account;
$form['user_cancel_confirm'] = [
'#type' => 'checkbox',
'#title' => $this->t('Require email confirmation'),
'#default_value' => !$override_access,
'#access' => $override_access,
'#description' => $this->t('When enabled, the user must confirm the account cancellation via email.'),
];
// Also allow to send account canceled notification mail, if enabled.
$default_notify = $this->config('user.settings')->get('notify.status_canceled');
$form['user_cancel_notify'] = [
'#type' => 'checkbox',
'#title' => $this->t('Notify user when account is canceled'),
'#default_value' => ($override_access ? FALSE : $default_notify),
'#access' => $override_access && $default_notify,
'#description' => $this->t('When enabled, the user will receive an email notification after the account has been canceled.'),
];
// Always provide entity id in the same form key as in the entity edit form.
$form['uid'] = ['#type' => 'value', '#value' => $this->entity->id()];
// Store the user permissions so that it can be altered in hook_form_alter()
// if desired.
$form['access'] = [
'#type' => 'value',
'#value' => !$own_account,
];
$form = parent::buildForm($form, $form_state);
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
// Cancel account immediately, if the current user has administrative
// privileges, no confirmation mail shall be sent, and the user does not
// attempt to cancel the own account.
if (!$form_state->isValueEmpty('access') && $form_state->isValueEmpty('user_cancel_confirm') && $this->entity->id() != $this->currentUser()->id()) {
user_cancel($form_state->getValues(), $this->entity->id(), $form_state->getValue('user_cancel_method'));
$form_state->setRedirectUrl($this->entity->toUrl('collection'));
}
else {
// Store cancelling method and whether to notify the user in
// $this->entity for
// \Drupal\user\Controller\UserController::confirmCancel().
$this->entity->user_cancel_method = $form_state->getValue('user_cancel_method');
$this->entity->user_cancel_notify = $form_state->getValue('user_cancel_notify');
$this->entity->save();
_user_mail_notify('cancel_confirm', $this->entity);
$this->messenger()->addStatus($this->t('A confirmation request to cancel your account has been sent to your email address.'));
$this->logger('user')->info('Sent account cancellation request to %name %email.', ['%name' => $this->entity->label(), '%email' => '<' . $this->entity->getEmail() . '>']);
$form_state->setRedirect(
'entity.user.canonical',
['user' => $this->entity->id()]
);
}
}
}

View File

@@ -0,0 +1,304 @@
<?php
namespace Drupal\user\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Render\BareHtmlPageRendererInterface;
use Drupal\Core\Url;
use Drupal\user\UserAuthenticationInterface;
use Drupal\user\UserAuthInterface;
use Drupal\user\UserInterface;
use Drupal\user\UserStorageInterface;
use Drupal\user\UserFloodControlInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a user login form.
*
* @internal
*/
class UserLoginForm extends FormBase {
/**
* The user flood control service.
*
* @var \Drupal\user\UserFloodControl
*/
protected $userFloodControl;
/**
* The user storage.
*
* @var \Drupal\user\UserStorageInterface
*/
protected $userStorage;
/**
* The user authentication object.
*
* @var \Drupal\user\UserAuthInterface|\Drupal\user\UserAuthenticationInterface
*/
protected $userAuth;
/**
* The renderer.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected $renderer;
/**
* The bare HTML renderer.
*
* @var \Drupal\Core\Render\BareHtmlPageRendererInterface
*/
protected $bareHtmlPageRenderer;
/**
* Constructs a new UserLoginForm.
*
* @param \Drupal\user\UserFloodControlInterface $user_flood_control
* The user flood control service.
* @param \Drupal\user\UserStorageInterface $user_storage
* The user storage.
* @param \Drupal\user\UserAuthInterface|\Drupal\user\UserAuthenticationInterface $user_auth
* The user authentication object.
* @param \Drupal\Core\Render\RendererInterface $renderer
* The renderer.
* @param \Drupal\Core\Render\BareHtmlPageRendererInterface $bare_html_renderer
* The renderer.
*/
public function __construct(UserFloodControlInterface $user_flood_control, UserStorageInterface $user_storage, UserAuthInterface|UserAuthenticationInterface $user_auth, RendererInterface $renderer, BareHtmlPageRendererInterface $bare_html_renderer) {
$this->userFloodControl = $user_flood_control;
$this->userStorage = $user_storage;
if (!$user_auth instanceof UserAuthenticationInterface) {
@trigger_error('The $user_auth parameter not implementing UserAuthenticationInterface is deprecated in drupal:10.3.0 and will be removed in drupal:12.0.0. See https://www.drupal.org/node/3411040');
}
$this->userAuth = $user_auth;
$this->renderer = $renderer;
$this->bareHtmlPageRenderer = $bare_html_renderer;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('user.flood_control'),
$container->get('entity_type.manager')->getStorage('user'),
$container->get('user.auth'),
$container->get('renderer'),
$container->get('bare_html_page_renderer')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'user_login_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$config = $this->config('system.site');
// Display login form:
$form['name'] = [
'#type' => 'textfield',
'#title' => $this->t('Username'),
'#size' => 60,
'#maxlength' => UserInterface::USERNAME_MAX_LENGTH,
'#required' => TRUE,
'#attributes' => [
'autocorrect' => 'none',
'autocapitalize' => 'none',
'spellcheck' => 'false',
'autofocus' => 'autofocus',
'autocomplete' => 'username',
],
];
$form['pass'] = [
'#type' => 'password',
'#title' => $this->t('Password'),
'#size' => 60,
'#required' => TRUE,
'#attributes' => [
'autocomplete' => 'current-password',
],
];
$form['actions'] = ['#type' => 'actions'];
$form['actions']['submit'] = ['#type' => 'submit', '#value' => $this->t('Log in')];
$form['#validate'][] = '::validateAuthentication';
$form['#validate'][] = '::validateFinal';
$this->renderer->addCacheableDependency($form, $config);
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
if (empty($uid = $form_state->get('uid'))) {
return;
}
$account = $this->userStorage->load($uid);
// A destination was set, probably on an exception controller.
if (!$this->getRequest()->request->has('destination')) {
$form_state->setRedirect(
'entity.user.canonical',
['user' => $account->id()]
);
}
else {
$this->getRequest()->query->set('destination', $this->getRequest()->request->get('destination'));
}
user_login_finalize($account);
}
/**
* Sets an error if supplied username has been blocked.
*
* @deprecated in drupal:10.3.0 and is removed from drupal:11.0.0. There is no replacement.
* @see https://www.drupal.org/node/3410706
*/
public function validateName(array &$form, FormStateInterface $form_state) {
@trigger_error(__METHOD__ . ' is deprecated in drupal:10.3.0 and is removed from drupal:11.0.0. There is no replacement. See https://www.drupal.org/node/3410706', E_USER_DEPRECATED);
if (!$form_state->isValueEmpty('name') && user_is_blocked($form_state->getValue('name'))) {
// Blocked in user administration.
$form_state->setErrorByName('name', $this->t('The username %name has not been activated or is blocked.', ['%name' => $form_state->getValue('name')]));
}
}
/**
* Checks supplied username/password against local users table.
*
* If successful, $form_state->get('uid') is set to the matching user ID.
*/
public function validateAuthentication(array &$form, FormStateInterface $form_state) {
$password = trim($form_state->getValue('pass'));
$flood_config = $this->config('user.flood');
$account = FALSE;
if (!$form_state->isValueEmpty('name') && strlen($password) > 0) {
// Do not allow any login from the current user's IP if the limit has been
// reached. Default is 50 failed attempts allowed in one hour. This is
// independent of the per-user limit to catch attempts from one IP to log
// in to many different user accounts. We have a reasonably high limit
// since there may be only one apparent IP for all users at an institution.
if (!$this->userFloodControl->isAllowed('user.failed_login_ip', $flood_config->get('ip_limit'), $flood_config->get('ip_window'))) {
$form_state->set('flood_control_triggered', 'ip');
return;
}
if ($this->userAuth instanceof UserAuthenticationInterface) {
$account = $this->userAuth->lookupAccount($form_state->getValue('name'));
}
else {
$accounts = $this->userStorage->loadByProperties(['name' => $form_state->getValue('name')]);
$account = reset($accounts);
}
if ($account && $account->isBlocked()) {
$form_state->setErrorByName('name', $this->t('The username %name has not been activated or is blocked.', ['%name' => $form_state->getValue('name')]));
}
elseif ($account && $account->isActive()) {
if ($flood_config->get('uid_only')) {
// Register flood events based on the uid only, so they apply for any
// IP address. This is the most secure option.
$identifier = $account->id();
}
else {
// The default identifier is a combination of uid and IP address. This
// is less secure but more resistant to denial-of-service attacks that
// could lock out all users with public user names.
$identifier = $account->id() . '-' . $this->getRequest()->getClientIP();
}
$form_state->set('flood_control_user_identifier', $identifier);
// If there are zero flood records for this user, then we don't need to
// clear any failed login attempts after a successful login, so check
// for this case first before checking the actual flood limit and store
// the result in form state.
if (!$this->userFloodControl->isAllowed('user.failed_login_user', 1, $flood_config->get('user_window'), $identifier)) {
// Now check the actual limit for the user. Default is to allow 5
// failed attempts every 6 hours. This means we check the flood table
// twice if flood control has already been triggered by a previous
// login attempt, but this should be the less common case.
if (!$this->userFloodControl->isAllowed('user.failed_login_user', $flood_config->get('user_limit'), $flood_config->get('user_window'), $identifier)) {
$form_state->set('flood_control_triggered', 'user');
return;
}
}
else {
$form_state->set('flood_control_skip_clear', 'user');
}
// We are not limited by flood control, so try to authenticate.
// Store the user ID in form state as a flag for self::validateFinal().
if ($this->userAuth instanceof UserAuthenticationInterface) {
$form_state->set('uid', $this->userAuth->authenticateAccount($account, $password) ? $account->id() : FALSE);
}
}
elseif (!$this->userAuth instanceof UserAuthenticationInterface) {
$uid = $this->userAuth->authenticate($form_state->getValue('name'), $password);
$form_state->set('uid', $uid);
}
}
}
/**
* Checks if user was not authenticated, or if too many logins were attempted.
*
* This validation function should always be the last one.
*/
public function validateFinal(array &$form, FormStateInterface $form_state) {
$flood_config = $this->config('user.flood');
if (!$form_state->get('uid')) {
// Always register an IP-based failed login event.
$this->userFloodControl->register('user.failed_login_ip', $flood_config->get('ip_window'));
// Register a per-user failed login event.
if ($flood_control_user_identifier = $form_state->get('flood_control_user_identifier')) {
$this->userFloodControl->register('user.failed_login_user', $flood_config->get('user_window'), $flood_control_user_identifier);
}
if ($flood_control_triggered = $form_state->get('flood_control_triggered')) {
if ($flood_control_triggered == 'user') {
$message = $this->formatPlural($flood_config->get('user_limit'), 'There has been more than one failed login attempt for this account. It is temporarily blocked. Try again later or <a href=":url">request a new password</a>.', 'There have been more than @count failed login attempts for this account. It is temporarily blocked. Try again later or <a href=":url">request a new password</a>.', [':url' => Url::fromRoute('user.pass')->toString()]);
}
else {
// We did not find a uid, so the limit is IP-based.
$message = $this->t('Too many failed login attempts from your IP address. This IP address is temporarily blocked. Try again later or <a href=":url">request a new password</a>.', [':url' => Url::fromRoute('user.pass')->toString()]);
}
$response = $this->bareHtmlPageRenderer->renderBarePage(['#markup' => $message], $this->t('Login failed'), 'maintenance_page');
$response->setStatusCode(403);
$form_state->setResponse($response);
}
else {
$form_state->setErrorByName('name', $this->t('Unrecognized username or password. <a href=":password">Forgot your password?</a>', [':password' => Url::fromRoute('user.pass')->toString()]));
$accounts = $this->userStorage->loadByProperties(['name' => $form_state->getValue('name')]);
if (!empty($accounts)) {
$this->logger('user')->notice('Login attempt failed for %user.', ['%user' => $form_state->getValue('name')]);
}
else {
// If the username entered is not a valid user,
// only store the IP address.
$this->logger('user')->notice('Login attempt failed from %ip.', ['%ip' => $this->getRequest()->getClientIp()]);
}
}
}
elseif (!$form_state->get('flood_control_skip_clear') && $flood_control_user_identifier = $form_state->get('flood_control_user_identifier')) {
// Clear past failures for this user so as not to block a user who might
// log in and out more than once in an hour.
$this->userFloodControl->clear('user.failed_login_user', $flood_control_user_identifier);
}
}
}

View File

@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace Drupal\user\Form;
use Drupal\Core\Form\ConfirmFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\WorkspaceSafeFormInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Url;
/**
* Provides a confirmation form for user logout.
*/
class UserLogoutConfirm extends ConfirmFormBase implements WorkspaceSafeFormInterface {
/**
* {@inheritdoc}
*/
public function getConfirmText(): TranslatableMarkup {
return $this->t('Log out');
}
/**
* {@inheritdoc}
*/
public function getQuestion(): TranslatableMarkup {
return $this->t('Are you sure you want to log out?');
}
/**
* {@inheritdoc}
*/
public function getCancelUrl(): Url {
return new Url('<front>');
}
/**
* {@inheritdoc}
*/
public function getFormId(): string {
return 'user_logout_confirm';
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state): void {
user_logout();
$form_state->setRedirect('<front>');
}
}

View File

@@ -0,0 +1,231 @@
<?php
namespace Drupal\user\Form;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\ConfirmFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Url;
use Drupal\Core\TempStore\PrivateTempStoreFactory;
use Drupal\user\UserStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a confirmation form for cancelling multiple user accounts.
*
* @internal
*/
class UserMultipleCancelConfirm extends ConfirmFormBase {
/**
* The temp store factory.
*
* @var \Drupal\Core\TempStore\PrivateTempStoreFactory
*/
protected $tempStoreFactory;
/**
* The user storage.
*
* @var \Drupal\user\UserStorageInterface
*/
protected $userStorage;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Constructs a new UserMultipleCancelConfirm.
*
* @param \Drupal\Core\TempStore\PrivateTempStoreFactory $temp_store_factory
* The temp store factory.
* @param \Drupal\user\UserStorageInterface $user_storage
* The user storage.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
*/
public function __construct(PrivateTempStoreFactory $temp_store_factory, UserStorageInterface $user_storage, EntityTypeManagerInterface $entity_type_manager) {
$this->tempStoreFactory = $temp_store_factory;
$this->userStorage = $user_storage;
$this->entityTypeManager = $entity_type_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('tempstore.private'),
$container->get('entity_type.manager')->getStorage('user'),
$container->get('entity_type.manager')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'user_multiple_cancel_confirm';
}
/**
* {@inheritdoc}
*/
public function getQuestion() {
return $this->t('Are you sure you want to cancel these user accounts?');
}
/**
* {@inheritdoc}
*/
public function getCancelUrl() {
return new Url('entity.user.collection');
}
/**
* {@inheritdoc}
*/
public function getConfirmText() {
return $this->t('Confirm');
}
/**
* {@inheritdoc}
*/
public function getDescription() {
return '';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
// Retrieve the accounts to be canceled from the temp store.
/** @var \Drupal\user\Entity\User[] $accounts */
$accounts = $this->tempStoreFactory
->get('user_user_operations_cancel')
->get($this->currentUser()->id());
if (!$accounts) {
return $this->redirect('entity.user.collection');
}
$root = NULL;
$names = [];
$form['accounts'] = ['#tree' => TRUE];
foreach ($accounts as $account) {
$uid = $account->id();
$names[$uid] = $account->label();
// Prevent user 1 from being canceled.
if ($uid <= 1) {
$root = intval($uid) === 1 ? $account : $root;
continue;
}
$form['accounts'][$uid] = [
'#type' => 'hidden',
'#value' => $uid,
];
}
$form['account']['names'] = [
'#theme' => 'item_list',
'#items' => $names,
];
// Output a notice that user 1 cannot be canceled.
if (isset($root)) {
$redirect = (count($accounts) == 1);
$message = $this->t('The user account %name cannot be canceled.', ['%name' => $root->label()]);
$this->messenger()->addMessage($message, $redirect ? MessengerInterface::TYPE_ERROR : MessengerInterface::TYPE_WARNING);
// If only user 1 was selected, redirect to the overview.
if ($redirect) {
return $this->redirect('entity.user.collection');
}
}
$form['operation'] = ['#type' => 'hidden', '#value' => 'cancel'];
// Display account cancellation method selection, if allowed.
$user = $this->currentUser();
$selectCancel = $user->hasPermission('administer users') || $user->hasPermission('select account cancellation method');
$form['user_cancel_method'] = [
'#type' => 'radios',
'#title' => $this->t('Cancellation method'),
'#access' => $selectCancel,
];
$form['user_cancel_method'] += user_cancel_methods();
if (!$selectCancel) {
// Display an item to inform the user of the setting.
$default_method = $form['user_cancel_method']['#default_value'];
$form['user_cancel_method_show'] = [
'#type' => 'item',
'#title' => $this->t('When cancelling these accounts'),
'#plain_text' => $form['user_cancel_method']['#options'][$default_method],
];
}
// Allow to send the account cancellation confirmation mail.
$form['user_cancel_confirm'] = [
'#type' => 'checkbox',
'#title' => $this->t('Require email confirmation'),
'#default_value' => FALSE,
'#description' => $this->t('When enabled, the user must confirm the account cancellation via email.'),
];
// Also allow to send account canceled notification mail, if enabled.
$form['user_cancel_notify'] = [
'#type' => 'checkbox',
'#title' => $this->t('Notify user when account is canceled'),
'#default_value' => FALSE,
'#access' => $this->config('user.settings')->get('notify.status_canceled'),
'#description' => $this->t('When enabled, the user will receive an email notification after the account has been canceled.'),
];
$form = parent::buildForm($form, $form_state);
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$current_user_id = $this->currentUser()->id();
// Clear out the accounts from the temp store.
$this->tempStoreFactory->get('user_user_operations_cancel')->delete($current_user_id);
if ($form_state->getValue('confirm')) {
foreach ($form_state->getValue('accounts') as $uid => $value) {
// Prevent programmatic form submissions from cancelling user 1.
if ($uid <= 1) {
continue;
}
// Prevent user administrators from deleting themselves without confirmation.
if ($uid == $current_user_id) {
$admin_form_mock = [];
$admin_form_state = $form_state;
$admin_form_state->unsetValue('user_cancel_confirm');
// The $user global is not a complete user entity, so load the full
// entity.
$account = $this->userStorage->load($uid);
$admin_form = $this->entityTypeManager->getFormObject('user', 'cancel');
$admin_form->setEntity($account);
// Calling this directly required to init form object with $account.
$admin_form->buildForm($admin_form_mock, $admin_form_state);
$admin_form->submitForm($admin_form_mock, $admin_form_state);
}
else {
user_cancel($form_state->getValues(), $uid, $form_state->getValue('user_cancel_method'));
}
}
}
$form_state->setRedirect('entity.user.collection');
}
}

View File

@@ -0,0 +1,238 @@
<?php
namespace Drupal\user\Form;
use Drupal\Component\Utility\EmailValidatorInterface;
use Drupal\Core\Config\ConfigFactory;
use Drupal\Core\DependencyInjection\DeprecatedServicePropertyTrait;
use Drupal\Core\Flood\FloodInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Render\Element\Email;
use Drupal\Core\TypedData\TypedDataManagerInterface;
use Drupal\user\UserInterface;
use Drupal\user\UserStorageInterface;
use Drupal\user\UserNameValidator;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a user password reset form.
*
* Send the user an email to reset their password.
*
* @internal
*/
class UserPasswordForm extends FormBase {
use DeprecatedServicePropertyTrait;
/**
* The deprecated properties.
*/
protected array $deprecatedProperties = [
'typedDataManager' => 'typed_data_manager',
];
/**
* The user storage.
*
* @var \Drupal\user\UserStorageInterface
*/
protected $userStorage;
/**
* The language manager.
*
* @var \Drupal\Core\Language\LanguageManagerInterface
*/
protected $languageManager;
/**
* The flood service.
*
* @var \Drupal\Core\Flood\FloodInterface
*/
protected $flood;
/**
* The email validator service.
*
* @var \Drupal\Component\Utility\EmailValidatorInterface
*/
protected $emailValidator;
/**
* Constructs a UserPasswordForm object.
*
* @param \Drupal\user\UserStorageInterface $user_storage
* The user storage.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
* @param \Drupal\Core\Config\ConfigFactory $config_factory
* The config factory.
* @param \Drupal\Core\Flood\FloodInterface $flood
* The flood service.
* @param \Drupal\user\UserNameValidator|\Drupal\Core\TypedData\TypedDataManagerInterface $userNameValidator
* The user validator service.
* @param \Drupal\Component\Utility\EmailValidatorInterface $email_validator
* The email validator service.
*/
public function __construct(
UserStorageInterface $user_storage,
LanguageManagerInterface $language_manager,
ConfigFactory $config_factory,
FloodInterface $flood,
protected UserNameValidator|TypedDataManagerInterface $userNameValidator,
EmailValidatorInterface $email_validator,
) {
$this->userStorage = $user_storage;
$this->languageManager = $language_manager;
$this->configFactory = $config_factory;
$this->flood = $flood;
$this->emailValidator = $email_validator;
if (!$userNameValidator instanceof UserNameValidator) {
@\trigger_error('Passing $userNameValidator as \Drupal\Core\TypedData\TypedDataManagerInterface to ' . __METHOD__ . ' () is deprecated in drupal:10.3.0 and is removed in drupal:11.0.0. Pass a Drupal\user\UserValidator instead. See https://www.drupal.org/node/3431205', E_USER_DEPRECATED);
$this->userNameValidator = \Drupal::service('user.name_validator');
}
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_type.manager')->getStorage('user'),
$container->get('language_manager'),
$container->get('config.factory'),
$container->get('flood'),
$container->get('user.name_validator'),
$container->get('email.validator'),
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'user_pass';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$form['name'] = [
'#type' => 'textfield',
'#title' => $this->t('Username or email address'),
'#size' => 60,
'#maxlength' => max(UserInterface::USERNAME_MAX_LENGTH, Email::EMAIL_MAX_LENGTH),
'#required' => TRUE,
'#attributes' => [
'autocorrect' => 'off',
'autocapitalize' => 'off',
'spellcheck' => 'false',
'autofocus' => 'autofocus',
'autocomplete' => 'username',
],
];
// Allow logged in users to request this also.
$user = $this->currentUser();
if ($user->isAuthenticated()) {
$form['name']['#type'] = 'value';
$form['name']['#value'] = $user->getEmail();
$form['mail'] = [
'#prefix' => '<p>',
'#markup' => $this->t('Password reset instructions will be mailed to %email. You must log out to use the password reset link in the email.', ['%email' => $user->getEmail()]),
'#suffix' => '</p>',
];
}
else {
$form['mail'] = [
'#prefix' => '<p>',
'#markup' => $this->t('Password reset instructions will be sent to your registered email address.'),
'#suffix' => '</p>',
];
$form['name']['#default_value'] = $this->getRequest()->query->get('name');
}
$form['actions'] = ['#type' => 'actions'];
$form['actions']['submit'] = ['#type' => 'submit', '#value' => $this->t('Submit')];
$form['#cache']['contexts'][] = 'url.query_args';
return $form;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
$flood_config = $this->configFactory->get('user.flood');
if (!$this->flood->isAllowed('user.password_request_ip', $flood_config->get('ip_limit'), $flood_config->get('ip_window'))) {
$form_state->setErrorByName('name', $this->t('Too many password recovery requests from your IP address. It is temporarily blocked. Try again later or contact the site administrator.'));
return;
}
$this->flood->register('user.password_request_ip', $flood_config->get('ip_window'));
// First, see if the input is possibly valid as a username.
$name = trim($form_state->getValue('name'));
$violations = $this->userNameValidator->validateName($name);
// Usernames have a maximum length shorter than email addresses. Only print
// this error if the input is not valid as a username or email address.
if ($violations->count() > 0 && !$this->emailValidator->isValid($name)) {
$form_state->setErrorByName('name', $this->t("The username or email address is invalid."));
return;
}
// Try to load by email.
$users = $this->userStorage->loadByProperties(['mail' => $name]);
if (empty($users)) {
// No success, try to load by name.
$users = $this->userStorage->loadByProperties(['name' => $name]);
}
$account = reset($users);
// Blocked accounts cannot request a new password.
if ($account && $account->id() && $account->isActive()) {
// Register flood events based on the uid only, so they apply for any
// IP address. This allows them to be cleared on successful reset (from
// any IP).
$identifier = $account->id();
if (!$this->flood->isAllowed('user.password_request_user', $flood_config->get('user_limit'), $flood_config->get('user_window'), $identifier)) {
return;
}
$this->flood->register('user.password_request_user', $flood_config->get('user_window'), $identifier);
$form_state->setValueForElement(['#parents' => ['account']], $account);
}
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$account = $form_state->getValue('account');
if ($account) {
// Mail one time login URL and instructions using current language.
$mail = _user_mail_notify('password_reset', $account);
if (!empty($mail)) {
$this->logger('user')
->info('Password reset instructions mailed to %name at %email.', [
'%name' => $account->getAccountName(),
'%email' => $account->getEmail(),
]);
}
}
else {
$this->logger('user')
->info('Password reset form was submitted with an unknown or inactive account: %name.', [
'%name' => $form_state->getValue('name'),
]);
}
// Make sure the status text is displayed even if no email was sent. This
// message is deliberately the same as the success message for privacy.
$this->messenger()
->addStatus($this->t('If %identifier is a valid account, an email will be sent with instructions to reset your password.', [
'%identifier' => $form_state->getValue('name'),
]));
$form_state->setRedirect('<front>');
}
}

View File

@@ -0,0 +1,76 @@
<?php
namespace Drupal\user\Form;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Url;
/**
* Form controller for the user password forms.
*
* Users followed the link in the email, now they can enter a new password.
*
* @internal
*/
class UserPasswordResetForm extends FormBase {
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'user_pass_reset';
}
/**
* {@inheritdoc}
*
* @param array $form
* An associative array containing the structure of the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
* @param \Drupal\Core\Session\AccountInterface $user
* User requesting reset.
* @param string $expiration_date
* Formatted expiration date for the login link, or NULL if the link does
* not expire.
* @param int $timestamp
* The current timestamp.
* @param string $hash
* Login link hash.
*/
public function buildForm(array $form, FormStateInterface $form_state, ?AccountInterface $user = NULL, $expiration_date = NULL, $timestamp = NULL, $hash = NULL) {
if ($expiration_date) {
$form['message'] = ['#markup' => $this->t('<p>This is a one-time login for %user_name and will expire on %expiration_date.</p><p>Click on this button to log in to the site and change your password.</p>', ['%user_name' => $user->getAccountName(), '%expiration_date' => $expiration_date])];
$form['#title'] = $this->t('Reset password');
}
else {
// No expiration for first time login.
$form['message'] = ['#markup' => $this->t('<p>This is a one-time login for %user_name.</p><p>Click on this button to log in to the site and change your password.</p>', ['%user_name' => $user->getAccountName()])];
$form['#title'] = $this->t('Set password');
}
$form['help'] = ['#markup' => '<p>' . $this->t('This login can be used only once.') . '</p>'];
$form['actions'] = ['#type' => 'actions'];
$form['actions']['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Log in'),
];
$form['#action'] = Url::fromRoute('user.reset.login', [
'uid' => $user->id(),
'timestamp' => $timestamp,
'hash' => $hash,
])->toString();
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
// This form works by submitting the hash and timestamp to the user.reset
// route with a 'login' action.
}
}

View File

@@ -0,0 +1,263 @@
<?php
namespace Drupal\user\Form;
use Drupal\Core\Extension\ModuleExtensionList;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\user\PermissionHandlerInterface;
use Drupal\user\RoleStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides the user permissions administration form.
*
* @internal
*/
class UserPermissionsForm extends FormBase {
/**
* The permission handler.
*
* @var \Drupal\user\PermissionHandlerInterface
*/
protected $permissionHandler;
/**
* The role storage.
*
* @var \Drupal\user\RoleStorageInterface
*/
protected $roleStorage;
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* Constructs a new UserPermissionsForm.
*
* @param \Drupal\user\PermissionHandlerInterface $permission_handler
* The permission handler.
* @param \Drupal\user\RoleStorageInterface $role_storage
* The role storage.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\Core\Extension\ModuleExtensionList|null $moduleExtensionList
* The module extension list.
*/
public function __construct(PermissionHandlerInterface $permission_handler, RoleStorageInterface $role_storage, ModuleHandlerInterface $module_handler, protected ?ModuleExtensionList $moduleExtensionList = NULL) {
$this->permissionHandler = $permission_handler;
$this->roleStorage = $role_storage;
$this->moduleHandler = $module_handler;
if ($this->moduleExtensionList === NULL) {
@trigger_error('Calling ' . __METHOD__ . '() without the $moduleExtensionList argument is deprecated in drupal:10.3.0 and will be required in drupal:12.0.0. See https://www.drupal.org/node/3310017', E_USER_DEPRECATED);
$this->moduleExtensionList = \Drupal::service('extension.list.module');
}
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('user.permissions'),
$container->get('entity_type.manager')->getStorage('user_role'),
$container->get('module_handler'),
$container->get('extension.list.module'),
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'user_admin_permissions';
}
/**
* Gets the roles to display in this form.
*
* @return \Drupal\user\RoleInterface[]
* An array of role objects.
*/
protected function getRoles() {
return $this->roleStorage->loadMultiple();
}
/**
* Group permissions by the modules that provide them.
*
* @return string[][]
* A nested array. The outer keys are modules that provide permissions. The
* inner arrays are permission names keyed by their machine names.
*/
protected function permissionsByProvider(): array {
$permissions = $this->permissionHandler->getPermissions();
$permissions_by_provider = [];
foreach ($permissions as $permission_name => $permission) {
$permissions_by_provider[$permission['provider']][$permission_name] = $permission;
}
// Move the access content permission to the Node module if it is installed.
// @todo Add an alter so that this section can be moved to the Node module.
if ($this->moduleHandler->moduleExists('node')) {
// Insert 'access content' before the 'view own unpublished content' key
// in order to maintain the UI even though the permission is provided by
// the system module.
$keys = array_keys($permissions_by_provider['node']);
$offset = (int) array_search('view own unpublished content', $keys);
$permissions_by_provider['node'] = array_merge(
array_slice($permissions_by_provider['node'], 0, $offset),
['access content' => $permissions_by_provider['system']['access content']],
array_slice($permissions_by_provider['node'], $offset)
);
unset($permissions_by_provider['system']['access content']);
}
return $permissions_by_provider;
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$role_names = [];
$role_permissions = [];
$admin_roles = [];
foreach ($this->getRoles() as $role_name => $role) {
// Retrieve role names for columns.
$role_names[$role_name] = $role->label();
// Fetch permissions for the roles.
$role_permissions[$role_name] = $role->getPermissions();
$admin_roles[$role_name] = $role->isAdmin();
}
// Store $role_names for use when saving the data.
$form['role_names'] = [
'#type' => 'value',
'#value' => $role_names,
];
// Render role/permission overview:
$hide_descriptions = system_admin_compact_mode();
$form['system_compact_link'] = [
'#id' => FALSE,
'#type' => 'system_compact_link',
];
$form['filters'] = [
'#type' => 'container',
'#attributes' => [
'class' => ['table-filter', 'js-show'],
],
];
$form['filters']['text'] = [
'#type' => 'search',
'#title' => $this->t('Filter permissions'),
'#title_display' => 'invisible',
'#size' => 30,
'#placeholder' => $this->t('Filter by permission name'),
'#description' => $this->t('Enter permission name'),
'#attributes' => [
'class' => ['table-filter-text'],
'data-table' => '#permissions',
'autocomplete' => 'off',
],
];
$form['permissions'] = [
'#type' => 'table',
'#header' => [$this->t('Permission')],
'#id' => 'permissions',
'#attributes' => ['class' => ['permissions', 'js-permissions']],
'#sticky' => TRUE,
];
foreach ($role_names as $name) {
$form['permissions']['#header'][] = [
'data' => $name,
'class' => ['checkbox'],
];
}
foreach ($this->permissionsByProvider() as $provider => $permissions) {
// Module name.
$form['permissions'][$provider] = [
[
'#wrapper_attributes' => [
'colspan' => count($role_names) + 1,
'class' => ['module'],
'id' => 'module-' . $provider,
],
'#markup' => $this->moduleExtensionList->getName($provider),
],
];
foreach ($permissions as $perm => $perm_item) {
// Fill in default values for the permission.
$perm_item += [
'description' => '',
'restrict access' => FALSE,
'warning' => !empty($perm_item['restrict access']) ? $this->t('Warning: Give to trusted roles only; this permission has security implications.') : '',
];
$form['permissions'][$perm]['description'] = [
'#type' => 'inline_template',
'#template' => '<div class="permission"><span class="title table-filter-text-source">{{ title }}</span>{% if description or warning %}<div class="description">{% if warning %}<em class="permission-warning">{{ warning }}</em> {% endif %}{{ description }}</div>{% endif %}</div>',
'#context' => [
'title' => $perm_item['title'],
],
];
// Show the permission description.
if (!$hide_descriptions) {
$form['permissions'][$perm]['description']['#context']['description'] = $perm_item['description'];
$form['permissions'][$perm]['description']['#context']['warning'] = $perm_item['warning'];
}
foreach ($role_names as $rid => $name) {
$form['permissions'][$perm][$rid] = [
'#title' => $name . ': ' . $perm_item['title'],
'#title_display' => 'invisible',
'#wrapper_attributes' => [
'class' => ['checkbox'],
],
'#type' => 'checkbox',
'#default_value' => in_array($perm, $role_permissions[$rid]) ? 1 : 0,
'#attributes' => ['class' => ['rid-' . $rid, 'js-rid-' . $rid]],
'#parents' => [$rid, $perm],
];
// Show a column of disabled but checked checkboxes.
if ($admin_roles[$rid]) {
$form['permissions'][$perm][$rid]['#disabled'] = TRUE;
$form['permissions'][$perm][$rid]['#default_value'] = TRUE;
}
}
}
}
$form['actions'] = ['#type' => 'actions'];
$form['actions']['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Save permissions'),
'#button_type' => 'primary',
];
$form['#attached']['library'][] = 'user/drupal.user.permissions';
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
foreach ($form_state->getValue('role_names') as $role_name => $name) {
user_role_change_permissions($role_name, (array) $form_state->getValue($role_name));
}
$this->messenger()->addStatus($this->t('The changes have been saved.'));
}
}

View File

@@ -0,0 +1,68 @@
<?php
namespace Drupal\user\Form;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Access\AccessResultInterface;
use Drupal\Core\Form\FormStateInterface;
/**
* Provides the user permissions administration form for one or more module(s).
*
* @internal
*/
class UserPermissionsModuleSpecificForm extends UserPermissionsForm {
/**
* The module list.
*
* A keyed array of module machine names.
*
* @var string[]
*/
protected $moduleList;
/**
* {@inheritdoc}
*/
protected function permissionsByProvider(): array {
return array_intersect_key(
parent::permissionsByProvider(),
array_flip($this->moduleList)
);
}
/**
* Builds the user permissions administration form for a specific module(s).
*
* @param array $form
* An associative array containing the structure of the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
* @param string $modules
* (optional) One or more module machine names, comma-separated.
*/
public function buildForm(array $form, FormStateInterface $form_state, $modules = ''): array {
$this->moduleList = explode(',', $modules);
return parent::buildForm($form, $form_state);
}
/**
* Checks that at least one module defines permissions.
*
* @param string $modules
* (optional) One or more module machine names, comma-separated.
*
* @return \Drupal\Core\Access\AccessResultInterface
* The access result.
*/
public function access($modules): AccessResultInterface {
foreach (explode(',', $modules) as $module) {
if ($this->permissionHandler->moduleProvidesPermissions($module)) {
return AccessResult::allowed();
}
}
return AccessResult::forbidden();
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace Drupal\user\Form;
use Drupal\Core\Form\FormStateInterface;
use Drupal\user\RoleInterface;
/**
* Provides the user permissions administration form for a specific role.
*
* @internal
*/
class UserPermissionsRoleSpecificForm extends UserPermissionsForm {
/**
* The specific role for this form.
*
* @var \Drupal\user\RoleInterface
*/
protected $userRole;
/**
* {@inheritdoc}
*/
protected function getRoles() {
return [$this->userRole->id() => $this->userRole];
}
/**
* Builds the user permissions administration form for a specific role.
*
* @param array $form
* An associative array containing the structure of the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
* @param \Drupal\user\RoleInterface|null $user_role
* (optional) The user role used for this form. Defaults to NULL.
*/
public function buildForm(array $form, FormStateInterface $form_state, ?RoleInterface $user_role = NULL) {
$this->userRole = $user_role;
return parent::buildForm($form, $form_state);
}
}

View File

@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace Drupal\user;
use Drupal\Core\Access\AccessManagerInterface;
use Drupal\Core\Extension\ModuleExtensionList;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Url;
/**
* Provides a helper for generating module permissions links.
*/
class ModulePermissionsLinkHelper {
use StringTranslationTrait;
/**
* Constructs a new service instance.
*
* @param \Drupal\user\PermissionHandlerInterface $permissionHandler
* The user permissions handler service.
* @param \Drupal\Core\Access\AccessManagerInterface $accessManager
* The access manager service.
* @param \Drupal\Core\Extension\ModuleExtensionList $moduleExtensionList
* The module handler service.
*/
public function __construct(
protected PermissionHandlerInterface $permissionHandler,
protected AccessManagerInterface $accessManager,
protected ModuleExtensionList $moduleExtensionList,
) {}
/**
* Generates a link pointing to a given module's permissions page section.
*
* @param string $module
* The module name.
* @param string $name
* The module display name.
*
* @return array|null
* A module permissions link as a render array or NULL if the module doesn't
* expose any permission or the current user cannot access it.
*/
public function getModulePermissionsLink(string $module, string $name): ?array {
if ($this->permissionHandler->moduleProvidesPermissions($module)) {
if ($this->accessManager->checkNamedRoute('user.admin_permissions.module', ['modules' => $module])) {
$url = new Url('user.admin_permissions.module', ['modules' => $module]);
return [
'title' => t('Configure @module permissions', ['@module' => $name]),
'description' => '',
'url' => $url,
];
}
}
return NULL;
}
}

View File

@@ -0,0 +1,249 @@
<?php
namespace Drupal\user;
use Drupal\Core\Discovery\YamlDiscovery;
use Drupal\Core\Controller\ControllerResolverInterface;
use Drupal\Core\Extension\ModuleExtensionList;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\Core\Utility\CallableResolver;
/**
* Provides the available permissions based on yml files.
*
* To define permissions you can use a $module.permissions.yml file. This file
* defines machine names, human-readable names, restrict access (if required for
* security warning), and optionally descriptions for each permission type. The
* machine names are the canonical way to refer to permissions for access
* checking.
*
* If your module needs to define dynamic permissions you can use the
* permission_callbacks key to declare a callable that will return an array of
* permissions, keyed by machine name. Each item in the array can contain the
* same keys as an entry in $module.permissions.yml.
*
* Here is an example from the core filter module (comments have been added):
* @code
* # The key is the permission machine name, and is required.
* administer filters:
* # (required) Human readable name of the permission used in the UI.
* title: 'Administer text formats and filters'
* # (optional) Additional description fo the permission used in the UI.
* description: 'Define how text is handled by combining filters into text formats.'
* # (optional) Boolean, when set to true a warning about site security will
* # be displayed on the Permissions page. Defaults to false.
* restrict access: false
*
* # An array of callables used to generate dynamic permissions.
* permission_callbacks:
* # The callable should return an associative array with one or more
* # permissions. Each permission array can use the same keys as the example
* # permission defined above. Additionally, a dependencies key is supported.
* # For more information about permission dependencies see
* # PermissionHandlerInterface::getPermissions().
* - Drupal\filter\FilterPermissions::permissions
* @endcode
*
* @see \Drupal\user\PermissionHandlerInterface::getPermissions()
* @see filter.permissions.yml
* @see \Drupal\filter\FilterPermissions
* @see user_api
*/
class PermissionHandler implements PermissionHandlerInterface {
use StringTranslationTrait;
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The YAML discovery class to find all .permissions.yml files.
*
* @var \Drupal\Core\Discovery\YamlDiscovery
*/
protected $yamlDiscovery;
/**
* The callable resolver.
*
* @var \Drupal\Core\Utility\CallableResolver
*/
protected CallableResolver $callableResolver;
/**
* Constructs a new PermissionHandler.
*
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
* The string translation.
* @param \Drupal\Core\Utility\CallableResolver|\Drupal\Core\Controller\ControllerResolverInterface $callable_resolver
* The callable resolver.
* @param \Drupal\Core\Extension\ModuleExtensionList|null $moduleExtensionList
* The module extension list.
*/
public function __construct(ModuleHandlerInterface $module_handler, TranslationInterface $string_translation, ControllerResolverInterface|CallableResolver $callable_resolver, protected ?ModuleExtensionList $moduleExtensionList = NULL) {
if ($callable_resolver instanceof ControllerResolverInterface) {
@trigger_error('Calling ' . __METHOD__ . '() with an argument of ControllerResolverInterface is deprecated in drupal:10.2.0 and is removed in drupal:11.0.0. Use \Drupal\Core\Utility\CallableResolver instead. See https://www.drupal.org/node/3397954', E_USER_DEPRECATED);
$callable_resolver = \Drupal::service('callable_resolver');
}
$this->callableResolver = $callable_resolver;
// @todo It would be nice if you could pull all module directories from the
// container.
$this->moduleHandler = $module_handler;
$this->stringTranslation = $string_translation;
if ($this->moduleExtensionList === NULL) {
@trigger_error('Calling ' . __METHOD__ . '() without the $moduleExtensionList argument is deprecated in drupal:10.3.0 and will be required in drupal:11.0.0. See https://www.drupal.org/node/3310017', E_USER_DEPRECATED);
$this->moduleExtensionList = \Drupal::service('extension.list.module');
}
}
/**
* Gets the YAML discovery.
*
* @return \Drupal\Core\Discovery\YamlDiscovery
* The YAML discovery.
*/
protected function getYamlDiscovery() {
if (!isset($this->yamlDiscovery)) {
$this->yamlDiscovery = new YamlDiscovery('permissions', $this->moduleHandler->getModuleDirectories());
}
return $this->yamlDiscovery;
}
/**
* {@inheritdoc}
*/
public function getPermissions() {
$all_permissions = $this->buildPermissionsYaml();
return $this->sortPermissions($all_permissions);
}
/**
* {@inheritdoc}
*/
public function moduleProvidesPermissions($module_name) {
// @todo Static cache this information.
// https://www.drupal.org/node/2339487
$permissions = $this->getPermissions();
foreach ($permissions as $permission) {
if ($permission['provider'] == $module_name) {
return TRUE;
}
}
return FALSE;
}
/**
* Builds all permissions provided by .permissions.yml files.
*
* @return array[]
* An array with the same structure as
* PermissionHandlerInterface::getPermissions().
*
* @see \Drupal\user\PermissionHandlerInterface::getPermissions()
*/
protected function buildPermissionsYaml() {
$all_permissions = [];
$all_callback_permissions = [];
foreach ($this->getYamlDiscovery()->findAll() as $provider => $permissions) {
// The top-level 'permissions_callback' is a list of methods in callable
// syntax, see \Drupal\Core\Utility\CallableResolver. These methods
// should return an array of permissions in the same structure.
if (isset($permissions['permission_callbacks'])) {
foreach ($permissions['permission_callbacks'] as $permission_callback) {
$callback = $this->callableResolver->getCallableFromDefinition($permission_callback);
if ($callback_permissions = call_user_func($callback)) {
// Add any callback permissions to the array of permissions. Any
// defaults can then get processed below.
foreach ($callback_permissions as $name => $callback_permission) {
if (!is_array($callback_permission)) {
$callback_permission = [
'title' => $callback_permission,
];
}
$callback_permission += [
'description' => NULL,
'provider' => $provider,
];
$all_callback_permissions[$name] = $callback_permission;
}
}
}
unset($permissions['permission_callbacks']);
}
foreach ($permissions as &$permission) {
if (!is_array($permission)) {
$permission = [
'title' => $permission,
];
}
$permission['title'] = $this->t($permission['title']);
$permission['description'] = isset($permission['description']) ? $this->t($permission['description']) : NULL;
$permission['provider'] = !empty($permission['provider']) ? $permission['provider'] : $provider;
}
$all_permissions += $permissions;
}
return $all_permissions + $all_callback_permissions;
}
/**
* Sorts the given permissions by provider name and title.
*
* @param array $all_permissions
* The permissions to be sorted.
*
* @return array[]
* An array with the same structure as
* PermissionHandlerInterface::getPermissions().
*
* @see \Drupal\user\PermissionHandlerInterface::getPermissions()
*/
protected function sortPermissions(array $all_permissions = []) {
// Get a list of all the modules providing permissions and sort by
// display name.
$modules = $this->getModuleNames();
uasort($all_permissions, function (array $permission_a, array $permission_b) use ($modules) {
if ($modules[$permission_a['provider']] == $modules[$permission_b['provider']]) {
return $permission_a['title'] <=> $permission_b['title'];
}
else {
return $modules[$permission_a['provider']] <=> $modules[$permission_b['provider']];
}
});
return $all_permissions;
}
/**
* Returns all module names.
*
* @return string[]
* Returns the human readable names of all modules keyed by machine name.
*/
protected function getModuleNames() {
$modules = [];
foreach (array_keys($this->moduleHandler->getModuleList()) as $module) {
$modules[$module] = $this->moduleExtensionList->getName($module);
}
asort($modules);
return $modules;
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace Drupal\user;
/**
* Defines an interface to list available permissions.
*/
interface PermissionHandlerInterface {
/**
* Gets all available permissions.
*
* @return array
* An array whose keys are permission names and whose corresponding values
* are arrays containing the following key-value pairs:
* - title: The human-readable name of the permission, to be shown on the
* permission administration page. This should be wrapped in the t()
* function so it can be translated.
* - description: (optional) A description of what the permission does. This
* should be wrapped in the t() function so it can be translated.
* - restrict access: (optional) A boolean which can be set to TRUE to
* indicate that site administrators should restrict access to this
* permission to trusted users. This should be used for permissions that
* have inherent security risks across a variety of potential use cases
* (for example, the "administer filters" and "bypass node access"
* permissions provided by Drupal core). When set to TRUE, a standard
* warning message defined in user_admin_permissions() will be displayed
* with the permission on the permission administration page. Defaults
* to FALSE.
* - warning: (optional) A translated warning message to display for this
* permission on the permission administration page. This warning
* overrides the automatic warning generated by 'restrict access' being
* set to TRUE. This should rarely be used, since it is important for all
* permissions to have a clear, consistent security warning that is the
* same across the site. Use the 'description' key instead to provide any
* information that is specific to the permission you are defining.
* - dependencies: (optional) An array of dependency entities used when
* building this permission, structured in the same way as the return
* of ConfigEntityInterface::calculateDependencies(). For example, if this
* permission was generated as effect of the existence of node type
* 'article', then value of the dependency key is:
* @code
* 'dependencies' => ['config' => ['node.type.article']]
* @endcode
* The module providing this permission doesn't have to be added as a
* dependency. It is automatically parsed, stored and retrieved from the
* 'provider' key.
* - provider: The provider name of the permission. This is set
* automatically to the module that provides the permission.yml file.
*
* @see \Drupal\Core\Config\Entity\ConfigDependencyManager
*/
public function getPermissions();
/**
* Determines whether a module provides some permissions.
*
* @param string $module_name
* The module name.
*
* @return bool
* Returns TRUE if the module provides some permissions, otherwise FALSE.
*/
public function moduleProvidesPermissions($module_name);
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Drupal\user\Plugin\Action;
use Drupal\Core\Action\Attribute\Action;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Adds a role to a user.
*/
#[Action(
id: 'user_add_role_action',
label: new TranslatableMarkup('Add a role to the selected users'),
type: 'user'
)]
class AddRoleUser extends ChangeUserRoleBase {
/**
* {@inheritdoc}
*/
public function execute($account = NULL) {
$rid = $this->configuration['rid'];
// Skip adding the role to the user if they already have it.
if ($account !== FALSE && !$account->hasRole($rid)) {
// For efficiency manually save the original account before applying
// any changes.
$account->original = clone $account;
$account->addRole($rid)->save();
}
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace Drupal\user\Plugin\Action;
use Drupal\Core\Action\ActionBase;
use Drupal\Core\Action\Attribute\Action;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Blocks a user.
*/
#[Action(
id: 'user_block_user_action',
label: new TranslatableMarkup('Block the selected users'),
type: 'user'
)]
class BlockUser extends ActionBase {
/**
* {@inheritdoc}
*/
public function execute($account = NULL) {
// Skip blocking user if they are already blocked.
if ($account !== FALSE && $account->isActive()) {
// For efficiency manually save the original account before applying any
// changes.
$account->original = clone $account;
$account->block();
$account->save();
}
}
/**
* {@inheritdoc}
*/
public function access($object, ?AccountInterface $account = NULL, $return_as_object = FALSE) {
/** @var \Drupal\user\UserInterface $object */
$access = $object->status->access('edit', $account, TRUE)
->andIf($object->access('update', $account, TRUE));
return $return_as_object ? $access : $access->isAllowed();
}
}

View File

@@ -0,0 +1,94 @@
<?php
namespace Drupal\user\Plugin\Action;
use Drupal\Core\Action\ActionBase;
use Drupal\Core\Action\Attribute\Action;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\TempStore\PrivateTempStoreFactory;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Cancels a user account.
*/
#[Action(
id: 'user_cancel_user_action',
label: new TranslatableMarkup('Cancel the selected user accounts'),
type: 'user',
confirm_form_route_name: 'user.multiple_cancel_confirm'
)]
class CancelUser extends ActionBase implements ContainerFactoryPluginInterface {
/**
* The tempstore factory.
*
* @var \Drupal\Core\TempStore\PrivateTempStoreFactory
*/
protected $tempStoreFactory;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* Constructs a CancelUser object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin ID for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\TempStore\PrivateTempStoreFactory $temp_store_factory
* The tempstore factory.
* @param \Drupal\Core\Session\AccountInterface $current_user
* Current user.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, PrivateTempStoreFactory $temp_store_factory, AccountInterface $current_user) {
$this->currentUser = $current_user;
$this->tempStoreFactory = $temp_store_factory;
parent::__construct($configuration, $plugin_id, $plugin_definition);
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('tempstore.private'),
$container->get('current_user')
);
}
/**
* {@inheritdoc}
*/
public function executeMultiple(array $entities) {
$this->tempStoreFactory->get('user_user_operations_cancel')->set($this->currentUser->id(), $entities);
}
/**
* {@inheritdoc}
*/
public function execute($object = NULL) {
$this->executeMultiple([$object]);
}
/**
* {@inheritdoc}
*/
public function access($object, ?AccountInterface $account = NULL, $return_as_object = FALSE) {
/** @var \Drupal\user\UserInterface $object */
return $object->access('delete', $account, $return_as_object);
}
}

View File

@@ -0,0 +1,105 @@
<?php
namespace Drupal\user\Plugin\Action;
use Drupal\Core\Action\ConfigurableActionBase;
use Drupal\Core\Entity\DependencyTrait;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\user\Entity\Role;
use Drupal\user\RoleInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a base class for operations to change a user's role.
*/
abstract class ChangeUserRoleBase extends ConfigurableActionBase implements ContainerFactoryPluginInterface {
use DependencyTrait;
/**
* The user role entity type.
*
* @var \Drupal\Core\Entity\EntityTypeInterface
*/
protected $entityType;
/**
* {@inheritdoc}
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeInterface $entity_type) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->entityType = $entity_type;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('entity_type.manager')->getDefinition('user_role')
);
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
'rid' => '',
];
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$roles = Role::loadMultiple();
unset($roles[RoleInterface::ANONYMOUS_ID]);
unset($roles[RoleInterface::AUTHENTICATED_ID]);
$roles = array_map(fn(RoleInterface $role) => $role->label(), $roles);
$form['rid'] = [
'#type' => 'radios',
'#title' => $this->t('Role'),
'#options' => $roles,
'#default_value' => $this->configuration['rid'],
'#required' => TRUE,
];
return $form;
}
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
$this->configuration['rid'] = $form_state->getValue('rid');
}
/**
* {@inheritdoc}
*/
public function calculateDependencies() {
if (!empty($this->configuration['rid'])) {
$prefix = $this->entityType->getConfigPrefix() . '.';
$this->addDependency('config', $prefix . $this->configuration['rid']);
}
return $this->dependencies;
}
/**
* {@inheritdoc}
*/
public function access($object, ?AccountInterface $account = NULL, $return_as_object = FALSE) {
/** @var \Drupal\user\UserInterface $object */
$access = $object->access('update', $account, TRUE)
->andIf($object->roles->access('edit', $account, TRUE));
return $return_as_object ? $access : $access->isAllowed();
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Drupal\user\Plugin\Action;
use Drupal\Core\Action\Attribute\Action;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Removes a role from a user.
*/
#[Action(
id: 'user_remove_role_action',
label: new TranslatableMarkup('Remove a role from the selected users'),
type: 'user'
)]
class RemoveRoleUser extends ChangeUserRoleBase {
/**
* {@inheritdoc}
*/
public function execute($account = NULL) {
$rid = $this->configuration['rid'];
// Skip removing the role from the user if they already don't have it.
if ($account !== FALSE && $account->hasRole($rid)) {
// For efficiency manually save the original account before applying
// any changes.
$account->original = clone $account;
$account->removeRole($rid)->save();
}
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace Drupal\user\Plugin\Action;
use Drupal\Core\Action\ActionBase;
use Drupal\Core\Action\Attribute\Action;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Unblocks a user.
*/
#[Action(
id: 'user_unblock_user_action',
label: new TranslatableMarkup('Unblock the selected users'),
type: 'user'
)]
class UnblockUser extends ActionBase {
/**
* {@inheritdoc}
*/
public function execute($account = NULL) {
// Skip unblocking user if they are already unblocked.
if ($account !== FALSE && $account->isBlocked()) {
$account->activate();
$account->save();
}
}
/**
* {@inheritdoc}
*/
public function access($object, ?AccountInterface $account = NULL, $return_as_object = FALSE) {
/** @var \Drupal\user\UserInterface $object */
$access = $object->status->access('edit', $account, TRUE)
->andIf($object->access('update', $account, TRUE));
return $return_as_object ? $access : $access->isAllowed();
}
}

View File

@@ -0,0 +1,181 @@
<?php
namespace Drupal\user\Plugin\Block;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Block\Attribute\Block;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Security\TrustedCallbackInterface;
use Drupal\Core\Routing\RedirectDestinationTrait;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Url;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Form\FormBuilderInterface;
use Drupal\user\Form\UserLoginForm;
use Drupal\user\UserInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a 'User login' block.
*/
#[Block(
id: "user_login_block",
admin_label: new TranslatableMarkup("User login"),
category: new TranslatableMarkup("Forms")
)]
class UserLoginBlock extends BlockBase implements ContainerFactoryPluginInterface, TrustedCallbackInterface {
use RedirectDestinationTrait;
/**
* The route match.
*
* @var \Drupal\Core\Routing\RouteMatchInterface
*/
protected $routeMatch;
/**
* Constructs a new UserLoginBlock instance.
*
* @param array $configuration
* The plugin configuration, i.e. an array with configuration values keyed
* by configuration option name. The special key 'context' may be used to
* initialize the defined contexts by setting it to an array of context
* values keyed by context names.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The route match.
* @param \Drupal\Core\Form\FormBuilderInterface $formBuilder
* The form builder.
*/
public function __construct(
array $configuration,
$plugin_id,
$plugin_definition,
RouteMatchInterface $route_match,
protected ?FormBuilderInterface $formBuilder = NULL,
) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->routeMatch = $route_match;
if (!$formBuilder) {
@trigger_error('Calling ' . __METHOD__ . ' without the $formBuilder argument is deprecated in drupal:10.3.0 and it will be required in drupal:11.0.0. See https://www.drupal.org/node/3159776', E_USER_DEPRECATED);
$this->formBuilder = \Drupal::service('form_builder');
}
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('current_route_match'),
$container->get('form_builder')
);
}
/**
* {@inheritdoc}
*/
protected function blockAccess(AccountInterface $account) {
$route_name = $this->routeMatch->getRouteName();
if ($account->isAnonymous() && !in_array($route_name, ['user.login', 'user.logout'])) {
return AccessResult::allowed()
->addCacheContexts(['route.name', 'user.roles:anonymous']);
}
return AccessResult::forbidden();
}
/**
* {@inheritdoc}
*/
public function build() {
$form = $this->formBuilder->getForm(UserLoginForm::class);
unset($form['name']['#attributes']['autofocus']);
$form['name']['#size'] = 15;
$form['pass']['#size'] = 15;
// Instead of setting an actual action URL, we set the placeholder, which
// will be replaced at the very last moment. This ensures forms with
// dynamically generated action URLs don't have poor cacheability.
// Use the proper API to generate the placeholder, when we have one. See
// https://www.drupal.org/node/2562341. The placeholder uses a fixed string
// that is
// Crypt::hashBase64('\Drupal\user\Plugin\Block\UserLoginBlock::build');
// This is based on the implementation in
// \Drupal\Core\Form\FormBuilder::prepareForm(), but the user login block
// requires different behavior for the destination query argument.
// cspell:disable-next-line
$placeholder = 'form_action_p_4r8ITd22yaUvXM6SzwrSe9rnQWe48hz9k1Sxto3pBvE';
$form['#attached']['placeholders'][$placeholder] = [
'#lazy_builder' => ['\Drupal\user\Plugin\Block\UserLoginBlock::renderPlaceholderFormAction', []],
];
$form['#action'] = $placeholder;
// Build action links.
$items = [];
if (\Drupal::config('user.settings')->get('register') != UserInterface::REGISTER_ADMINISTRATORS_ONLY) {
$items['create_account'] = [
'#type' => 'link',
'#title' => $this->t('Create new account'),
'#url' => Url::fromRoute('user.register', [], [
'attributes' => [
'title' => $this->t('Create a new user account.'),
'class' => ['create-account-link'],
],
]),
];
}
$items['request_password'] = [
'#type' => 'link',
'#title' => $this->t('Reset your password'),
'#url' => Url::fromRoute('user.pass', [], [
'attributes' => [
'title' => $this->t('Send password reset instructions via email.'),
'class' => ['request-password-link'],
],
]),
];
return [
'user_login_form' => $form,
'user_links' => [
'#theme' => 'item_list',
'#items' => $items,
],
];
}
/**
* #lazy_builder callback; renders a form action URL including destination.
*
* @return array
* A renderable array representing the form action.
*
* @see \Drupal\Core\Form\FormBuilder::renderPlaceholderFormAction()
*/
public static function renderPlaceholderFormAction() {
return [
'#type' => 'markup',
'#markup' => UrlHelper::filterBadProtocol(Url::fromRoute('<current>', [], ['query' => \Drupal::destination()->getAsArray(), 'external' => FALSE])->toString()),
'#cache' => ['contexts' => ['url.path', 'url.query_args']],
];
}
/**
* {@inheritdoc}
*/
public static function trustedCallbacks() {
return ['renderPlaceholderFormAction'];
}
}

View File

@@ -0,0 +1,105 @@
<?php
namespace Drupal\user\Plugin\Condition;
use Drupal\Component\Utility\Html;
use Drupal\Core\Condition\Attribute\Condition;
use Drupal\Core\Condition\ConditionPluginBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\Context\EntityContextDefinition;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\user\Entity\Role;
use Drupal\user\RoleInterface;
/**
* Provides a 'User Role' condition.
*/
#[Condition(
id: "user_role",
label: new TranslatableMarkup("User Role"),
context_definitions: [
"user" => new EntityContextDefinition(
data_type: "entity:user",
label: new TranslatableMarkup("User"),
),
],
)]
class UserRole extends ConditionPluginBase {
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$form['roles'] = [
'#type' => 'checkboxes',
'#title' => $this->t('When the user has the following roles'),
'#default_value' => $this->configuration['roles'],
'#options' => array_map(fn(RoleInterface $role) => Html::escape($role->label()), Role::loadMultiple()),
'#description' => $this->t('If you select no roles, the condition will evaluate to TRUE for all users.'),
];
return parent::buildConfigurationForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
'roles' => [],
] + parent::defaultConfiguration();
}
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
$this->configuration['roles'] = array_filter($form_state->getValue('roles'));
parent::submitConfigurationForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function summary() {
// Use the role labels. They will be sanitized below.
$roles = array_map(fn(RoleInterface $role) => $role->label(), Role::loadMultiple());
$roles = array_intersect_key($roles, $this->configuration['roles']);
if (count($roles) > 1) {
$roles = implode(', ', $roles);
}
else {
$roles = reset($roles);
}
if (!empty($this->configuration['negate'])) {
return $this->t('The user is not a member of @roles', ['@roles' => $roles]);
}
else {
return $this->t('The user is a member of @roles', ['@roles' => $roles]);
}
}
/**
* {@inheritdoc}
*/
public function evaluate() {
if (empty($this->configuration['roles']) && !$this->isNegated()) {
return TRUE;
}
$user = $this->getContextValue('user');
return (bool) array_intersect($this->configuration['roles'], $user->getRoles());
}
/**
* {@inheritdoc}
*/
public function getCacheContexts() {
// Optimize cache context, if a user cache context is provided, only use
// user.roles, since that's the only part this condition cares about.
$contexts = [];
foreach (parent::getCacheContexts() as $context) {
$contexts[] = $context == 'user' ? 'user.roles' : $context;
}
return $contexts;
}
}

View File

@@ -0,0 +1,81 @@
<?php
namespace Drupal\user\Plugin\Derivative;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides local task definitions for all entity bundles.
*/
class UserLocalTask extends DeriverBase implements ContainerDeriverInterface {
use StringTranslationTrait;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Creates a UserLocalTask object.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
* The translation manager.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, TranslationInterface $string_translation) {
$this->entityTypeManager = $entity_type_manager;
$this->stringTranslation = $string_translation;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, $base_plugin_id) {
return new static(
$container->get('entity_type.manager'),
$container->get('string_translation')
);
}
/**
* {@inheritdoc}
*/
public function getDerivativeDefinitions($base_plugin_definition) {
$this->derivatives = [];
$entity_definitions = $this->entityTypeManager->getDefinitions();
foreach ($entity_definitions as $bundle_type_id => $bundle_entity_type) {
if (!$bundle_entity_type->hasLinkTemplate('entity-permissions-form')) {
continue;
}
if (!$entity_type_id = $bundle_entity_type->getBundleOf()) {
continue;
}
$entity_type = $entity_definitions[$entity_type_id];
if (!$base_route = $entity_type->get('field_ui_base_route')) {
continue;
}
$this->derivatives["permissions_$bundle_type_id"] = [
'route_name' => "entity.$bundle_type_id.entity_permissions_form",
'weight' => 10,
'title' => $this->t('Manage permissions'),
'base_route' => $base_route,
] + $base_plugin_definition;
}
return parent::getDerivativeDefinitions($base_plugin_definition);
}
}

View File

@@ -0,0 +1,266 @@
<?php
namespace Drupal\user\Plugin\EntityReferenceSelection;
use Drupal\Core\Database\Connection;
use Drupal\Core\Database\Query\SelectInterface;
use Drupal\Core\Entity\Attribute\EntityReferenceSelection;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\Plugin\EntityReferenceSelection\DefaultSelection;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\user\Entity\Role;
use Drupal\user\RoleInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides specific access control for the user entity type.
*/
#[EntityReferenceSelection(
id: "default:user",
label: new TranslatableMarkup("User selection"),
entity_types: ["user"],
group: "default",
weight: 1
)]
class UserSelection extends DefaultSelection {
/**
* The database connection.
*
* @var \Drupal\Core\Database\Connection
*/
protected $connection;
/**
* Constructs a new UserSelection object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager service.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler service.
* @param \Drupal\Core\Session\AccountInterface $current_user
* The current user.
* @param \Drupal\Core\Database\Connection $connection
* The database connection.
* @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
* The entity field manager.
* @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info
* The entity type bundle info service.
* @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
* The entity repository.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, ModuleHandlerInterface $module_handler, AccountInterface $current_user, Connection $connection, ?EntityFieldManagerInterface $entity_field_manager = NULL, ?EntityTypeBundleInfoInterface $entity_type_bundle_info = NULL, ?EntityRepositoryInterface $entity_repository = NULL) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $entity_type_manager, $module_handler, $current_user, $entity_field_manager, $entity_type_bundle_info, $entity_repository);
$this->connection = $connection;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('entity_type.manager'),
$container->get('module_handler'),
$container->get('current_user'),
$container->get('database'),
$container->get('entity_field.manager'),
$container->get('entity_type.bundle.info'),
$container->get('entity.repository')
);
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
'filter' => [
'type' => '_none',
'role' => NULL,
],
'include_anonymous' => TRUE,
] + parent::defaultConfiguration();
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$configuration = $this->getConfiguration();
$form['include_anonymous'] = [
'#type' => 'checkbox',
'#title' => $this->t('Include the anonymous user.'),
'#default_value' => $configuration['include_anonymous'],
];
// Add user specific filter options.
$form['filter']['type'] = [
'#type' => 'select',
'#title' => $this->t('Filter by'),
'#options' => [
'_none' => $this->t('- None -'),
'role' => $this->t('User role'),
],
// Use a form process callback to build #ajax property properly and also
// to avoid code duplication.
// @see \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem::fieldSettingsAjaxProcess()
'#ajax' => TRUE,
'#limit_validation_errors' => [],
'#default_value' => $configuration['filter']['type'],
];
$form['filter']['settings'] = [
'#type' => 'container',
'#attributes' => ['class' => ['entity_reference-settings']],
'#process' => [['\Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem', 'formProcessMergeParent']],
];
if ($configuration['filter']['type'] == 'role') {
$roles = Role::loadMultiple();
unset($roles[RoleInterface::ANONYMOUS_ID]);
unset($roles[RoleInterface::AUTHENTICATED_ID]);
$roles = array_map(fn(RoleInterface $role) => $role->label(), $roles);
$form['filter']['settings']['role'] = [
'#type' => 'checkboxes',
'#title' => $this->t('Restrict to the selected roles'),
'#required' => TRUE,
'#options' => $roles,
'#default_value' => $configuration['filter']['role'],
];
}
$form += parent::buildConfigurationForm($form, $form_state);
return $form;
}
/**
* {@inheritdoc}
*/
protected function buildEntityQuery($match = NULL, $match_operator = 'CONTAINS') {
$query = parent::buildEntityQuery($match, $match_operator);
$configuration = $this->getConfiguration();
// Filter out the Anonymous user if the selection handler is configured to
// exclude it.
if (!$configuration['include_anonymous']) {
$query->condition('uid', 0, '<>');
}
// The user entity doesn't have a label column.
if (isset($match)) {
$query->condition('name', $match, $match_operator);
}
// Filter by role.
if (!empty($configuration['filter']['role'])) {
$query->condition('roles', $configuration['filter']['role'], 'IN');
}
// Adding the permission check is sadly insufficient for users: core
// requires us to also know about the concept of 'blocked' and 'active'.
if (!$this->currentUser->hasPermission('administer users')) {
$query->condition('status', 1);
}
return $query;
}
/**
* {@inheritdoc}
*/
public function createNewEntity($entity_type_id, $bundle, $label, $uid) {
$user = parent::createNewEntity($entity_type_id, $bundle, $label, $uid);
// In order to create a referenceable user, it needs to be active.
if (!$this->currentUser->hasPermission('administer users')) {
/** @var \Drupal\user\UserInterface $user */
$user->activate();
}
return $user;
}
/**
* {@inheritdoc}
*/
public function validateReferenceableNewEntities(array $entities) {
$entities = parent::validateReferenceableNewEntities($entities);
// Mirror the conditions checked in buildEntityQuery().
if ($role = $this->getConfiguration()['filter']['role']) {
$entities = array_filter($entities, function ($user) use ($role) {
/** @var \Drupal\user\UserInterface $user */
return !empty(array_intersect($user->getRoles(), $role));
});
}
if (!$this->currentUser->hasPermission('administer users')) {
$entities = array_filter($entities, function ($user) {
/** @var \Drupal\user\UserInterface $user */
return $user->isActive();
});
}
return $entities;
}
/**
* {@inheritdoc}
*/
public function entityQueryAlter(SelectInterface $query) {
parent::entityQueryAlter($query);
// Bail out early if we do not need to match the Anonymous user.
if (!$this->getConfiguration()['include_anonymous']) {
return;
}
if ($this->currentUser->hasPermission('administer users')) {
// In addition, if the user is administrator, we need to make sure to
// match the anonymous user, that doesn't actually have a name in the
// database.
$conditions = &$query->conditions();
foreach ($conditions as $key => $condition) {
if ($key !== '#conjunction' && is_string($condition['field']) && $condition['field'] === 'users_field_data.name') {
// Remove the condition.
unset($conditions[$key]);
// Re-add the condition and a condition on uid = 0 so that we end up
// with a query in the form:
// WHERE (name LIKE :name) OR (:anonymous_name LIKE :name AND uid = 0)
$or = $this->connection->condition('OR');
$or->condition($condition['field'], $condition['value'], $condition['operator']);
// Sadly, the Database layer doesn't allow us to build a condition
// in the form ':placeholder = :placeholder2', because the 'field'
// part of a condition is always escaped.
// As a (cheap) workaround, we separately build a condition with no
// field, and concatenate the field and the condition separately.
$value_part = $this->connection->condition('AND');
$value_part->condition('anonymous_name', $condition['value'], $condition['operator']);
$value_part->compile($this->connection, $query);
$or->condition(($this->connection->condition('AND'))
->where(str_replace($query->escapeField('anonymous_name'), ':anonymous_name', (string) $value_part), $value_part->arguments() + [':anonymous_name' => \Drupal::config('user.settings')->get('anonymous')])
->condition('base_table.uid', 0)
);
$query->condition($or);
}
}
}
}
}

View File

@@ -0,0 +1,59 @@
<?php
namespace Drupal\user\Plugin\Field\FieldFormatter;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Field\Attribute\FieldFormatter;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\Plugin\Field\FieldFormatter\EntityReferenceFormatterBase;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Plugin implementation of the 'author' formatter.
*/
#[FieldFormatter(
id: 'author',
label: new TranslatableMarkup('Author'),
description: new TranslatableMarkup('Display the referenced author user entity.'),
field_types: [
'entity_reference',
],
)]
class AuthorFormatter extends EntityReferenceFormatterBase {
/**
* {@inheritdoc}
*/
public function viewElements(FieldItemListInterface $items, $langcode) {
$elements = [];
foreach ($this->getEntitiesToView($items, $langcode) as $delta => $entity) {
$elements[$delta] = [
'#theme' => 'username',
'#account' => $entity,
'#link_options' => ['attributes' => ['rel' => 'author']],
'#cache' => [
'tags' => $entity->getCacheTags(),
],
];
}
return $elements;
}
/**
* {@inheritdoc}
*/
public static function isApplicable(FieldDefinitionInterface $field_definition) {
return $field_definition->getFieldStorageDefinition()->getSetting('target_type') == 'user';
}
/**
* {@inheritdoc}
*/
protected function checkAccess(EntityInterface $entity) {
return $entity->access('view label', NULL, TRUE);
}
}

View File

@@ -0,0 +1,90 @@
<?php
namespace Drupal\user\Plugin\Field\FieldFormatter;
use Drupal\Core\Field\Attribute\FieldFormatter;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\FormatterBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Plugin implementation of the 'user_name' formatter.
*/
#[FieldFormatter(
id: 'user_name',
label: new TranslatableMarkup('User name'),
description: new TranslatableMarkup('Display the user or author name.'),
field_types: [
'string',
],
)]
class UserNameFormatter extends FormatterBase {
/**
* {@inheritdoc}
*/
public static function defaultSettings() {
$options = parent::defaultSettings();
$options['link_to_entity'] = TRUE;
return $options;
}
/**
* {@inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $form_state) {
$form = parent::settingsForm($form, $form_state);
$form['link_to_entity'] = [
'#type' => 'checkbox',
'#title' => $this->t('Link to the user'),
'#default_value' => $this->getSetting('link_to_entity'),
];
return $form;
}
/**
* {@inheritdoc}
*/
public function viewElements(FieldItemListInterface $items, $langcode) {
$elements = [];
foreach ($items as $delta => $item) {
/** @var \Drupal\user\UserInterface $user */
if ($user = $item->getEntity()) {
if ($this->getSetting('link_to_entity')) {
$elements[$delta] = [
'#theme' => 'username',
'#account' => $user,
'#link_options' => ['attributes' => ['rel' => 'user']],
'#cache' => [
'tags' => $user->getCacheTags(),
],
];
}
else {
$elements[$delta] = [
'#markup' => $user->getDisplayName(),
'#cache' => [
'tags' => $user->getCacheTags(),
],
];
}
}
}
return $elements;
}
/**
* {@inheritdoc}
*/
public static function isApplicable(FieldDefinitionInterface $field_definition) {
return $field_definition->getTargetEntityTypeId() === 'user' && $field_definition->getName() === 'name';
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace Drupal\user\Plugin\LanguageNegotiation;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\language\Attribute\LanguageNegotiation;
use Drupal\language\LanguageNegotiationMethodBase;
use Symfony\Component\HttpFoundation\Request;
/**
* Class for identifying language from the user preferences.
*/
#[LanguageNegotiation(
id: LanguageNegotiationUser::METHOD_ID,
name: new TranslatableMarkup('User'),
weight: -4,
description: new TranslatableMarkup("Follow the user's language preference.")
)]
class LanguageNegotiationUser extends LanguageNegotiationMethodBase {
/**
* The language negotiation method id.
*/
const METHOD_ID = 'language-user';
/**
* {@inheritdoc}
*/
public function getLangcode(?Request $request = NULL) {
$langcode = NULL;
// User preference (only for authenticated users).
if ($this->languageManager && $this->currentUser->isAuthenticated()) {
$preferred_langcode = $this->currentUser->getPreferredLangcode(FALSE);
$languages = $this->languageManager->getLanguages();
if (!empty($preferred_langcode) && isset($languages[$preferred_langcode])) {
$langcode = $preferred_langcode;
}
}
// No language preference from the user.
return $langcode;
}
}

View File

@@ -0,0 +1,152 @@
<?php
namespace Drupal\user\Plugin\LanguageNegotiation;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\PathProcessor\PathProcessorManager;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Routing\AdminContext;
use Drupal\Core\Routing\StackedRouteMatchInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\language\Attribute\LanguageNegotiation;
use Drupal\language\LanguageNegotiationMethodBase;
use Drupal\Core\Routing\RouteObjectInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\Routing\Exception\ExceptionInterface;
use Symfony\Component\Routing\Matcher\UrlMatcherInterface;
/**
* Identifies admin language from the user preferences.
*/
#[LanguageNegotiation(
id: LanguageNegotiationUserAdmin::METHOD_ID,
name: new TranslatableMarkup('Account administration pages'),
types: [LanguageInterface::TYPE_INTERFACE],
weight: -10,
description: new TranslatableMarkup('Account administration pages language setting.')
)]
class LanguageNegotiationUserAdmin extends LanguageNegotiationMethodBase implements ContainerFactoryPluginInterface {
/**
* The language negotiation method id.
*/
const METHOD_ID = 'language-user-admin';
/**
* The admin context.
*
* @var \Drupal\Core\Routing\AdminContext
*/
protected $adminContext;
/**
* The router.
*
* This is only used when called from an event subscriber, before the request
* has been populated with the route info.
*
* @var \Symfony\Component\Routing\Matcher\UrlMatcherInterface
*/
protected $router;
/**
* The path processor manager.
*
* @var \Drupal\Core\PathProcessor\PathProcessorManager
*/
protected $pathProcessorManager;
/**
* The stacked route match.
*
* @var \Drupal\Core\Routing\StackedRouteMatchInterface
*/
protected $stackedRouteMatch;
/**
* Constructs a new LanguageNegotiationUserAdmin instance.
*
* @param \Drupal\Core\Routing\AdminContext $admin_context
* The admin context.
* @param \Symfony\Component\Routing\Matcher\UrlMatcherInterface $router
* The router.
* @param \Drupal\Core\PathProcessor\PathProcessorManager $path_processor_manager
* The path processor manager.
* @param \Drupal\Core\Routing\StackedRouteMatchInterface $stacked_route_match
* The stacked route match.
*/
public function __construct(AdminContext $admin_context, UrlMatcherInterface $router, PathProcessorManager $path_processor_manager, StackedRouteMatchInterface $stacked_route_match) {
$this->adminContext = $admin_context;
$this->router = $router;
$this->pathProcessorManager = $path_processor_manager;
$this->stackedRouteMatch = $stacked_route_match;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$container->get('router.admin_context'),
$container->get('router'),
$container->get('path_processor_manager'),
$container->get('current_route_match')
);
}
/**
* {@inheritdoc}
*/
public function getLangcode(?Request $request = NULL) {
$langcode = NULL;
// User preference (only for administrators).
if (($this->currentUser->hasPermission('access administration pages') || $this->currentUser->hasPermission('view the administration theme')) && ($preferred_admin_langcode = $this->currentUser->getPreferredAdminLangcode(FALSE)) && $this->isAdminPath($request)) {
$langcode = $preferred_admin_langcode;
}
// Not an admin, no admin language preference or not on an admin path.
return $langcode;
}
/**
* Checks whether the given path is an administrative one.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request object.
*
* @return bool
* TRUE if the path is administrative, FALSE otherwise.
*/
protected function isAdminPath(Request $request) {
$result = FALSE;
if ($request && $this->adminContext) {
// If called from an event subscriber, the request may not have the route
// object yet (it is still being built), so use the router to look up
// based on the path.
$route_match = $this->stackedRouteMatch->getRouteMatchFromRequest($request);
if ($route_match && !$route_object = $route_match->getRouteObject()) {
try {
// Some inbound path processors make changes to the request. Make a
// copy as we're not actually routing the request so we do not want to
// make changes.
$cloned_request = clone $request;
// Process the path as an inbound path. This will remove any language
// prefixes and other path components that inbound processing would
// clear out, so we can attempt to load the route clearly.
$path = $this->pathProcessorManager->processInbound(urldecode(rtrim($cloned_request->getPathInfo(), '/')), $cloned_request);
$attributes = $this->router->match($path);
}
catch (ExceptionInterface | HttpException) {
return FALSE;
}
$route_object = $attributes[RouteObjectInterface::ROUTE_OBJECT];
}
$result = $this->adminContext->isAdminRoute($route_object);
}
return $result;
}
}

View File

@@ -0,0 +1,86 @@
<?php
namespace Drupal\user\Plugin\Menu;
use Drupal\Core\Menu\MenuLinkDefault;
use Drupal\Core\Menu\StaticMenuLinkOverridesInterface;
use Drupal\Core\Session\AccountInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* A menu link that shows "Log in" or "Log out" as appropriate.
*/
class LoginLogoutMenuLink extends MenuLinkDefault {
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* Constructs a new LoginLogoutMenuLink.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Menu\StaticMenuLinkOverridesInterface $static_override
* The static override storage.
* @param \Drupal\Core\Session\AccountInterface $current_user
* The current user.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, StaticMenuLinkOverridesInterface $static_override, AccountInterface $current_user) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $static_override);
$this->currentUser = $current_user;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('menu_link.static.overrides'),
$container->get('current_user')
);
}
/**
* {@inheritdoc}
*/
public function getTitle() {
if ($this->currentUser->isAuthenticated()) {
return $this->t('Log out');
}
else {
return $this->t('Log in');
}
}
/**
* {@inheritdoc}
*/
public function getRouteName() {
if ($this->currentUser->isAuthenticated()) {
return 'user.logout';
}
else {
return 'user.login';
}
}
/**
* {@inheritdoc}
*/
public function getCacheContexts() {
return ['user.roles:authenticated'];
}
}

View File

@@ -0,0 +1,180 @@
<?php
namespace Drupal\user\Plugin\Search;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Database\Connection;
use Drupal\Core\Database\Query\PagerSelectExtender;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Access\AccessibleInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\search\Attribute\Search;
use Drupal\search\Plugin\SearchPluginBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Executes a keyword search for users against the {users} database table.
*/
#[Search(
id: 'user_search',
title: new TranslatableMarkup('Users'),
)]
class UserSearch extends SearchPluginBase implements AccessibleInterface {
/**
* The database connection.
*
* @var \Drupal\Core\Database\Connection
*/
protected $database;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$container->get('database'),
$container->get('entity_type.manager'),
$container->get('module_handler'),
$container->get('current_user'),
$configuration,
$plugin_id,
$plugin_definition
);
}
/**
* Creates a UserSearch object.
*
* @param \Drupal\Core\Database\Connection $database
* The database connection.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\Core\Session\AccountInterface $current_user
* The current user.
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
*/
public function __construct(Connection $database, EntityTypeManagerInterface $entity_type_manager, ModuleHandlerInterface $module_handler, AccountInterface $current_user, array $configuration, $plugin_id, $plugin_definition) {
$this->database = $database;
$this->entityTypeManager = $entity_type_manager;
$this->moduleHandler = $module_handler;
$this->currentUser = $current_user;
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->addCacheTags(['user_list']);
}
/**
* {@inheritdoc}
*/
public function access($operation = 'view', ?AccountInterface $account = NULL, $return_as_object = FALSE) {
$result = AccessResult::allowedIf(!empty($account) && $account->hasPermission('access user profiles'))->cachePerPermissions();
return $return_as_object ? $result : $result->isAllowed();
}
/**
* {@inheritdoc}
*/
public function execute() {
$results = [];
if (!$this->isSearchExecutable()) {
return $results;
}
// Process the keywords.
$keys = $this->keywords;
// Escape for LIKE matching.
$keys = $this->database->escapeLike($keys);
// Replace wildcards with MySQL/PostgreSQL wildcards.
$keys = preg_replace('!\*+!', '%', $keys);
// Run the query to find matching users.
$query = $this->database
->select('users_field_data', 'users')
->extend(PagerSelectExtender::class);
$query->fields('users', ['uid']);
$query->condition('default_langcode', 1);
if ($this->currentUser->hasPermission('administer users')) {
// Administrators can also search in the otherwise private email field,
// and they don't need to be restricted to only active users.
$query->fields('users', ['mail']);
$query->condition($query->orConditionGroup()
->condition('name', '%' . $keys . '%', 'LIKE')
->condition('mail', '%' . $keys . '%', 'LIKE')
);
}
else {
// Regular users can only search via usernames, and we do not show them
// blocked accounts.
$query->condition('name', '%' . $keys . '%', 'LIKE')
->condition('status', 1);
}
$uids = $query
->limit(15)
->execute()
->fetchCol();
$accounts = $this->entityTypeManager->getStorage('user')->loadMultiple($uids);
foreach ($accounts as $account) {
$result = [
'title' => $account->getDisplayName(),
'link' => $account->toUrl('canonical', ['absolute' => TRUE])->toString(),
];
if ($this->currentUser->hasPermission('administer users')) {
$result['title'] .= ' (' . $account->getEmail() . ')';
}
$this->addCacheableDependency($account);
$results[] = $result;
}
return $results;
}
/**
* {@inheritdoc}
*/
public function getHelp() {
$help = [
'list' => [
'#theme' => 'item_list',
'#items' => [
$this->t('User search looks for user names and partial user names. Example: mar would match usernames mar, delmar, and maryjane.'),
$this->t('You can use * as a wildcard within your keyword. Example: m*r would match user names mar, delmar, and elementary.'),
],
],
];
return $help;
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace Drupal\user\Plugin\Validation\Constraint;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Validation\Attribute\Constraint;
use Symfony\Component\Validator\Constraint as SymfonyConstraint;
/**
* Checks if the plain text password is provided for editing a protected field.
*/
#[Constraint(
id: 'ProtectedUserField',
label: new TranslatableMarkup('Password required for protected field change', [], ['context' => 'Validation'])
)]
class ProtectedUserFieldConstraint extends SymfonyConstraint {
/**
* Violation message.
*
* @var string
*/
public $message = "Your current password is missing or incorrect; it's required to change the %name.";
}

View File

@@ -0,0 +1,97 @@
<?php
namespace Drupal\user\Plugin\Validation\Constraint;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\user\UserStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
/**
* Validates the ProtectedUserFieldConstraint constraint.
*/
class ProtectedUserFieldConstraintValidator extends ConstraintValidator implements ContainerInjectionInterface {
/**
* User storage handler.
*
* @var \Drupal\user\UserStorageInterface
*/
protected $userStorage;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountProxyInterface
*/
protected $currentUser;
/**
* Constructs the object.
*
* @param \Drupal\user\UserStorageInterface $user_storage
* The user storage handler.
* @param \Drupal\Core\Session\AccountProxyInterface $current_user
* The current user.
*/
public function __construct(UserStorageInterface $user_storage, AccountProxyInterface $current_user) {
$this->userStorage = $user_storage;
$this->currentUser = $current_user;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_type.manager')->getStorage('user'),
$container->get('current_user')
);
}
/**
* {@inheritdoc}
*
* phpcs:ignore Drupal.Commenting.FunctionComment.VoidReturn
* @return void
*/
public function validate($items, Constraint $constraint) {
if (!isset($items)) {
return;
}
/** @var \Drupal\Core\Field\FieldItemListInterface $items */
$field = $items->getFieldDefinition();
/** @var \Drupal\user\UserInterface $account */
$account = $items->getEntity();
if (!isset($account) || !empty($account->_skipProtectedUserFieldConstraint)) {
// Looks like we are validating a field not being part of a user, or the
// constraint should be skipped, so do nothing.
return;
}
// Only validate for existing entities and if this is the current user.
if (!$account->isNew() && $account->id() == $this->currentUser->id()) {
/** @var \Drupal\user\UserInterface $account_unchanged */
$account_unchanged = $this->userStorage
->loadUnchanged($account->id());
$changed = FALSE;
// Special case for the password, it being empty means that the existing
// password should not be changed, ignore empty password fields.
$value = $items->value;
if ($field->getName() != 'pass' || !empty($value)) {
// Compare the values of the field this is being validated on.
$changed = $items->getValue() != $account_unchanged->get($field->getName())->getValue();
}
if ($changed && (!$account->checkExistingPassword($account_unchanged))) {
$this->context->addViolation($constraint->message, ['%name' => $field->getLabel()]);
}
}
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Drupal\user\Plugin\Validation\Constraint;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Validation\Attribute\Constraint;
use Symfony\Component\Validator\Constraint as SymfonyConstraint;
/**
* Checks if the user's email address is provided if required.
*
* The user mail field is NOT required if account originally had no mail set
* and the user performing the edit has 'administer users' permission.
* This allows users without email address to be edited and deleted.
*/
#[Constraint(
id: 'UserMailRequired',
label: new TranslatableMarkup('User email required', [], ['context' => 'Validation'])
)]
class UserMailRequired extends SymfonyConstraint {
/**
* Violation message. Use the same message as FormValidator.
*
* Note that the name argument is not sanitized so that translators only have
* one string to translate. The name is sanitized in self::validate().
*
* @var string
*/
public $message = '@name field is required.';
}

View File

@@ -0,0 +1,48 @@
<?php
namespace Drupal\user\Plugin\Validation\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Constraint;
/**
* Checks if the user's email address is provided if required.
*
* The user mail field is NOT required if account originally had no mail set
* and the user performing the edit has 'administer users' permission.
* This allows users without email address to be edited and deleted.
*/
class UserMailRequiredValidator extends ConstraintValidator {
/**
* {@inheritdoc}
*
* phpcs:ignore Drupal.Commenting.FunctionComment.VoidReturn
* @return void
*/
public function validate($items, Constraint $constraint) {
/** @var \Drupal\Core\Field\FieldItemListInterface $items */
/** @var \Drupal\user\UserInterface $account */
$account = $items->getEntity();
if (!isset($account)) {
return;
}
$existing_value = NULL;
// Only validate for existing user.
if (!$account->isNew()) {
$account_unchanged = \Drupal::entityTypeManager()
->getStorage('user')
->loadUnchanged($account->id());
$existing_value = $account_unchanged->getEmail();
}
$required = !(!$existing_value && \Drupal::currentUser()->hasPermission('administer users'));
if ($required && (!isset($items) || $items->isEmpty())) {
$this->context->addViolation($constraint->message, ['@name' => $account->getFieldDefinition('mail')->getLabel()]);
}
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace Drupal\user\Plugin\Validation\Constraint;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Validation\Attribute\Constraint;
use Drupal\Core\Validation\Plugin\Validation\Constraint\UniqueFieldConstraint;
/**
* Checks if a user's email address is unique on the site.
*/
#[Constraint(
id: 'UserMailUnique',
label: new TranslatableMarkup('User email unique', [], ['context' => 'Validation'])
)]
class UserMailUnique extends UniqueFieldConstraint {
public $message = 'The email address %value is already taken.';
}

View File

@@ -0,0 +1,25 @@
<?php
namespace Drupal\user\Plugin\Validation\Constraint;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Validation\Attribute\Constraint;
use Symfony\Component\Validator\Constraint as SymfonyConstraint;
/**
* Checks if a value is a valid user name.
*/
#[Constraint(
id: 'UserName',
label: new TranslatableMarkup('User name', [], ['context' => 'Validation'])
)]
class UserNameConstraint extends SymfonyConstraint {
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,62 @@
<?php
namespace Drupal\user\Plugin\Validation\Constraint;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\user\UserInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
/**
* Validates the UserName constraint.
*/
class UserNameConstraintValidator extends ConstraintValidator {
/**
* {@inheritdoc}
*/
public function validate($items, Constraint $constraint) {
if (empty($items) || ($items instanceof FieldItemListInterface && $items->isEmpty())) {
$this->context->addViolation($constraint->emptyMessage);
return;
}
$name = $items instanceof FieldItemListInterface ? $items->first()->value : $items;
if (str_starts_with($name, ' ')) {
$this->context->addViolation($constraint->spaceBeginMessage);
}
if (str_ends_with($name, ' ')) {
$this->context->addViolation($constraint->spaceEndMessage);
}
if (str_contains($name, ' ')) {
$this->context->addViolation($constraint->multipleSpacesMessage);
}
if (preg_match('/[^\x{80}-\x{F7} a-z0-9@+_.\'-]/i', $name)
|| preg_match(
// Non-printable ISO-8859-1 + NBSP
'/[\x{80}-\x{A0}' .
// Soft-hyphen
'\x{AD}' .
// Various space characters
'\x{2000}-\x{200F}' .
// Bidirectional text overrides
'\x{2028}-\x{202F}' .
// Various text hinting characters
'\x{205F}-\x{206F}' .
// Byte order mark
'\x{FEFF}' .
// Full-width latin
'\x{FF01}-\x{FF60}' .
// Replacement characters
'\x{FFF9}-\x{FFFD}' .
// NULL byte and control characters
'\x{0}-\x{1F}]/u',
$name)
) {
$this->context->addViolation($constraint->illegalMessage);
}
if (mb_strlen($name) > UserInterface::USERNAME_MAX_LENGTH) {
$this->context->addViolation($constraint->tooLongMessage, ['%name' => $name, '%max' => UserInterface::USERNAME_MAX_LENGTH]);
}
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace Drupal\user\Plugin\Validation\Constraint;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Validation\Attribute\Constraint;
use Drupal\Core\Validation\Plugin\Validation\Constraint\UniqueFieldConstraint;
/**
* Checks if a user name is unique on the site.
*/
#[Constraint(
id: 'UserNameUnique',
label: new TranslatableMarkup('User name unique', [], ['context' => 'Validation'])
)]
class UserNameUnique extends UniqueFieldConstraint {
public $message = 'The username %value is already taken.';
}

View File

@@ -0,0 +1,67 @@
<?php
namespace Drupal\user\Plugin\migrate;
use Drupal\migrate\Exception\RequirementsException;
use Drupal\migrate\MigrateExecutable;
use Drupal\migrate\Plugin\Migration;
/**
* Plugin class for user migrations dealing with profile values.
*/
class ProfileValues extends Migration {
/**
* Flag determining whether the process plugin has been initialized.
*
* @var bool
*/
protected $init = FALSE;
/**
* {@inheritdoc}
*/
public function getProcess() {
if (!$this->init) {
$this->init = TRUE;
$definition['source'] = [
'plugin' => 'profile_field',
'ignore_map' => TRUE,
] + $this->source;
$definition['destination']['plugin'] = 'null';
$definition['idMap']['plugin'] = 'null';
try {
$this->checkRequirements();
$profile_field_migration = $this->migrationPluginManager->createStubMigration($definition);
$migrate_executable = new MigrateExecutable($profile_field_migration);
$source_plugin = $profile_field_migration->getSourcePlugin();
$source_plugin->checkRequirements();
foreach ($source_plugin as $row) {
$name = $row->getSourceProperty('name');
$fid = $row->getSourceProperty('fid');
// The user profile field name can be greater than 32 characters. Use
// the migrated profile field name in the process pipeline.
$configuration =
[
'migration' => 'user_profile_field',
'source_ids' => $fid,
'no_stub' => TRUE,
];
$plugin = $this->processPluginManager->createInstance('migration_lookup', $configuration, $profile_field_migration);
$new_value = $plugin->transform($fid, $migrate_executable, $row, 'tmp');
if (isset($new_value[1])) {
// Set the destination to the migrated profile field name.
$this->process[$new_value[1]] = $name;
}
}
}
catch (RequirementsException $e) {
// The checkRequirements() call will fail when the profile module does
// not exist on the source site, or if the required migrations have not
// yet run.
}
}
return parent::getProcess();
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace Drupal\user\Plugin\migrate;
use Drupal\migrate\Exception\RequirementsException;
use Drupal\migrate_drupal\Plugin\migrate\FieldMigration;
/**
* Plugin class for Drupal 7 user migrations dealing with fields and profiles.
*/
class User extends FieldMigration {
/**
* {@inheritdoc}
*/
public function getProcess() {
if (!$this->init) {
$this->init = TRUE;
$this->fieldDiscovery->addEntityFieldProcesses($this, 'user');
$definition = [
'source' => [
'plugin' => 'profile_field',
'ignore_map' => TRUE,
],
'idMap' => [
'plugin' => 'null',
],
'destination' => [
'plugin' => 'null',
],
];
try {
$profile_migration = $this->migrationPluginManager->createStubMigration($definition);
// Ensure that Profile is enabled in the source DB.
$profile_migration->checkRequirements();
foreach ($profile_migration->getSourcePlugin() as $row) {
$name = $row->getSourceProperty('name');
$this->process[$name] = $name;
}
}
catch (RequirementsException $e) {
// The checkRequirements() call will fail when the profile module does
// not exist on the source site.
}
}
return parent::getProcess();
}
}

View File

@@ -0,0 +1,191 @@
<?php
namespace Drupal\user\Plugin\migrate\destination;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Field\FieldTypePluginManagerInterface;
use Drupal\Core\Field\Plugin\Field\FieldType\EmailItem;
use Drupal\Core\Password\PasswordInterface;
use Drupal\Core\Session\AccountSwitcherInterface;
use Drupal\migrate\Attribute\MigrateDestination;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Plugin\migrate\destination\EntityContentBase;
use Drupal\migrate\Row;
use Drupal\user\UserNameItem;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a destination plugin for migrating user entities.
*
* Example:
*
* The example below migrates users and preserves original passwords from a
* source that has passwords as MD5 hashes without salt. The passwords will be
* salted and re-hashed before they are saved to the destination Drupal
* database. The MD5 hash used in the example is a hash of 'password'.
*
* The example uses the EmbeddedDataSource source plugin for the sake of
* simplicity. The mapping between old user_ids and new Drupal uids is saved in
* the migration map table.
* @code
* id: custom_user_migration
* label: Custom user migration
* source:
* plugin: embedded_data
* data_rows:
* -
* user_id: 1
* name: JohnSmith
* mail: johnsmith@example.com
* hash: '5f4dcc3b5aa765d61d8327deb882cf99'
* ids:
* user_id:
* type: integer
* process:
* name: name
* mail: mail
* pass: hash
* status:
* plugin: default_value
* default_value: 1
* destination:
* plugin: entity:user
* md5_passwords: true
* @endcode
*
* For configuration options inherited from the parent class, refer to
* \Drupal\migrate\Plugin\migrate\destination\EntityContentBase.
*
* The example above is about migrating an MD5 password hash. For more examples
* on different password hash types and a list of other user properties, refer
* to the handbook documentation:
* @see https://www.drupal.org/docs/8/api/migrate-api/migrate-destination-plugins-examples/migrating-users
*/
#[MigrateDestination('entity:user')]
class EntityUser extends EntityContentBase {
/**
* The password service class.
*
* @var \Drupal\Core\Password\PasswordInterface
*/
protected $password;
/**
* Builds a user entity destination.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\migrate\Plugin\MigrationInterface $migration
* The migration.
* @param \Drupal\Core\Entity\EntityStorageInterface $storage
* The storage for this entity type.
* @param array $bundles
* The list of bundles this entity type has.
* @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
* The entity field manager.
* @param \Drupal\Core\Field\FieldTypePluginManagerInterface $field_type_manager
* The field type plugin manager service.
* @param \Drupal\Core\Password\PasswordInterface $password
* The password service.
* @param \Drupal\Core\Session\AccountSwitcherInterface|null $account_switcher
* The account switcher service.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, EntityStorageInterface $storage, array $bundles, EntityFieldManagerInterface $entity_field_manager, FieldTypePluginManagerInterface $field_type_manager, PasswordInterface $password, ?AccountSwitcherInterface $account_switcher = NULL) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $migration, $storage, $bundles, $entity_field_manager, $field_type_manager, $account_switcher);
$this->password = $password;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, ?MigrationInterface $migration = NULL) {
$entity_type = static::getEntityTypeId($plugin_id);
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$migration,
$container->get('entity_type.manager')->getStorage($entity_type),
array_keys($container->get('entity_type.bundle.info')->getBundleInfo($entity_type)),
$container->get('entity_field.manager'),
$container->get('plugin.manager.field.field_type'),
$container->get('password'),
$container->get('account_switcher')
);
}
/**
* {@inheritdoc}
* @throws \Drupal\migrate\MigrateException
*/
public function import(Row $row, array $old_destination_id_values = []) {
// Do not overwrite the root account password.
if ($row->getDestinationProperty('uid') == 1) {
$row->removeDestinationProperty('pass');
}
return parent::import($row, $old_destination_id_values);
}
/**
* {@inheritdoc}
*/
protected function save(ContentEntityInterface $entity, array $old_destination_id_values = []) {
// Do not overwrite the root account password.
if ($entity->id() != 1) {
// Set the pre_hashed password so that the PasswordItem field does not hash
// already hashed passwords. If the md5_passwords configuration option is
// set we need to rehash the password and prefix with a U.
// @see \Drupal\Core\Field\Plugin\Field\FieldType\PasswordItem::preSave()
$entity->pass->pre_hashed = TRUE;
if (isset($this->configuration['md5_passwords'])) {
$entity->pass->value = 'U' . $this->password->hash($entity->pass->value);
}
}
return parent::save($entity, $old_destination_id_values);
}
/**
* {@inheritdoc}
*/
protected function processStubRow(Row $row) {
parent::processStubRow($row);
$field_definitions = $this->entityFieldManager
->getFieldDefinitions($this->storage->getEntityTypeId(),
$this->getKey('bundle'));
// Name is generated using a dedicated sample value generator to ensure
// uniqueness and a valid length.
// @todo Remove this as part of https://www.drupal.org/node/3352288.
$name = UserNameItem::generateSampleValue($field_definitions['name']);
$row->setDestinationProperty('name', reset($name));
// Email address is not defined as required in the base field definition but
// is effectively required by the UserMailRequired constraint. This means
// that Entity::processStubRow() did not populate it - we do it here.
$mail = EmailItem::generateSampleValue($field_definitions['mail']);
$row->setDestinationProperty('mail', reset($mail));
}
/**
* {@inheritdoc}
*/
public function getHighestId() {
$highest_id = parent::getHighestId();
// Every Drupal site must have a user with UID of 1 and it's normal for
// migrations to overwrite this user.
if ($highest_id === 1) {
return 0;
}
return $highest_id;
}
}

View File

@@ -0,0 +1,93 @@
<?php
namespace Drupal\user\Plugin\migrate\destination;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\migrate\Attribute\MigrateDestination;
use Drupal\migrate\Plugin\migrate\destination\EntityConfigBase;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Row;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a destination plugin for migrating user role entities.
*/
#[MigrateDestination('entity:user_role')]
class EntityUserRole extends EntityConfigBase {
/**
* All permissions on the destination site.
*
* @var string[]
*/
protected $destinationPermissions = [];
/**
* Builds a user role entity destination.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\migrate\Plugin\MigrationInterface $migration
* The migration.
* @param \Drupal\Core\Entity\EntityStorageInterface $storage
* The storage for this entity type.
* @param array $bundles
* The list of bundles this entity type has.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The configuration factory.
* @param array $destination_permissions
* All available permissions.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, EntityStorageInterface $storage, array $bundles, LanguageManagerInterface $language_manager, ConfigFactoryInterface $config_factory, array $destination_permissions) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $migration, $storage, $bundles, $language_manager, $config_factory);
$this->destinationPermissions = $destination_permissions;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, ?MigrationInterface $migration = NULL) {
$entity_type_id = static::getEntityTypeId($plugin_id);
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$migration,
$container->get('entity_type.manager')->getStorage($entity_type_id),
array_keys($container->get('entity_type.bundle.info')->getBundleInfo($entity_type_id)),
$container->get('language_manager'),
$container->get('config.factory'),
array_keys($container->get('user.permissions')->getPermissions()),
);
}
/**
* {@inheritdoc}
*/
public function import(Row $row, array $old_destination_id_values = []): array|bool {
$permissions = $row->getDestinationProperty('permissions') ?? [];
// Get permissions that do not exist on the destination.
$invalid_permissions = array_diff($permissions, $this->destinationPermissions);
if ($invalid_permissions) {
sort($invalid_permissions);
// Log the message in the migration message table.
$message = "Permission(s) '" . implode("', '", $invalid_permissions) . "' not found.";
$this->migration->getIdMap()
->saveMessage($row->getSourceIdValues(), $message, MigrationInterface::MESSAGE_WARNING);
}
$valid_permissions = array_intersect($permissions, $this->destinationPermissions);
$row->setDestinationProperty('permissions', $valid_permissions);
return parent::import($row, $old_destination_id_values);
}
}

View File

@@ -0,0 +1,90 @@
<?php
namespace Drupal\user\Plugin\migrate\destination;
use Drupal\migrate\Attribute\MigrateDestination;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\user\UserData as UserDataStorage;
use Drupal\migrate\Row;
use Drupal\migrate\Plugin\migrate\destination\DestinationBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
/**
* Migration destination for user data.
*/
#[MigrateDestination('user_data')]
class UserData extends DestinationBase implements ContainerFactoryPluginInterface {
/**
* @var \Drupal\user\UserData
*/
protected $userData;
/**
* Builds a user data entity destination.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\migrate\Plugin\MigrationInterface $migration
* The migration.
* @param \Drupal\user\UserData $user_data
* The user data service.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, UserDataStorage $user_data) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $migration);
$this->userData = $user_data;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, ?MigrationInterface $migration = NULL) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$migration,
$container->get('user.data')
);
}
/**
* {@inheritdoc}
*/
public function import(Row $row, array $old_destination_id_values = []) {
$uid = $row->getDestinationProperty('uid');
$module = $row->getDestinationProperty('module');
$key = $row->getDestinationProperty('key');
$this->userData->set($module, $uid, $key, $row->getDestinationProperty('settings'));
return [$uid, $module, $key];
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['uid']['type'] = 'integer';
$ids['module']['type'] = 'string';
$ids['key']['type'] = 'string';
return $ids;
}
/**
* {@inheritdoc}
*/
public function fields() {
return [
'uid' => 'The user id.',
'module' => 'The module name responsible for the settings.',
'key' => 'The setting key to save under.',
'settings' => 'The settings to save.',
];
}
}

View File

@@ -0,0 +1,52 @@
<?php
namespace Drupal\user\Plugin\migrate\process;
use Drupal\migrate\Attribute\MigrateProcess;
use Drupal\migrate\MigrateException;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\Row;
/**
* Plugin to replace !tokens with [tokens].
*/
#[MigrateProcess(
id: "convert_tokens",
handle_multiples: TRUE,
)]
class ConvertTokens extends ProcessPluginBase {
/**
* {@inheritdoc}
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
$tokens = [
'!site' => '[site:name]',
'!username' => '[user:name]',
'!mailto' => '[user:mail]',
'!login_uri' => '[site:login-url]',
'!uri_brief' => '[site:url-brief]',
'!edit_uri' => '[user:edit-url]',
'!login_url' => '[user:one-time-login-url]',
'!uri' => '[site:url]',
'!date' => '[date:medium]',
'!password' => '',
];
// Given that our source is a database column that could hold a NULL
// value, sometimes that filters down to here. str_replace() cannot
// handle NULLs as the subject, so we reset to an empty string.
if (is_null($value)) {
$value = '';
}
if (is_string($value)) {
return str_replace(array_keys($tokens), $tokens, $value);
}
else {
throw new MigrateException('Value must be a string.');
}
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace Drupal\user\Plugin\migrate\process;
use Drupal\migrate\Attribute\MigrateProcess;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\Row;
#[MigrateProcess('profile_field_settings')]
class ProfileFieldSettings extends ProcessPluginBase {
/**
* {@inheritdoc}
*/
public function transform($type, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
$settings = [];
switch ($type) {
case 'date':
$settings['datetime_type'] = 'date';
break;
}
return $settings;
}
}

View File

@@ -0,0 +1,84 @@
<?php
namespace Drupal\user\Plugin\migrate\process;
use Drupal\Core\Language\LanguageManager;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\migrate\Attribute\MigrateProcess;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\Row;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a process plugin for the user langcode.
*/
#[MigrateProcess('user_langcode')]
class UserLangcode extends ProcessPluginBase implements ContainerFactoryPluginInterface {
/**
* The language manager.
*
* @var \Drupal\Core\Language\LanguageManager
*/
protected $languageManager;
/**
* Constructs a UserLangcode object.
*
* @param array $configuration
* Plugin configuration.
* @param string $plugin_id
* The plugin ID.
* @param mixed $plugin_definition
* The plugin definition.
* @param \Drupal\Core\Language\LanguageManager $language_manager
* The language manager service.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, LanguageManager $language_manager) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->languageManager = $language_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('language_manager')
);
}
/**
* {@inheritdoc}
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
if (!isset($this->configuration['fallback_to_site_default'])) {
$this->configuration['fallback_to_site_default'] = TRUE;
}
// If the user's language is empty, it means the locale module was not
// installed, so the user's langcode should be English and the user's
// preferred_langcode and preferred_admin_langcode should fallback to the
// default language.
if (empty($value)) {
if ($this->configuration['fallback_to_site_default']) {
return $this->languageManager->getDefaultLanguage()->getId();
}
else {
return 'en';
}
}
// If the user's language does not exist, use the default language.
elseif ($this->languageManager->getLanguage($value) === NULL) {
return $this->languageManager->getDefaultLanguage()->getId();
}
// If the langcode is a valid one, just return it.
return $value;
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace Drupal\user\Plugin\migrate\process;
use Drupal\migrate\Attribute\MigrateProcess;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\Row;
/**
* Keep the predefined roles for rid 1 and 2.
*/
#[MigrateProcess('user_update_8002')]
class UserUpdate8002 extends ProcessPluginBase {
/**
* {@inheritdoc}
*
* Keep the predefined roles for rid 1 and 2.
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
$rid = $row->getSourceProperty('rid');
$map = [
1 => 'anonymous',
2 => 'authenticated',
];
return $map[$rid] ?? $value;
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace Drupal\user\Plugin\migrate\process\d6;
use Drupal\migrate\Attribute\MigrateProcess;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\Row;
/**
* Determines the settings property and translation.
*/
#[MigrateProcess(
id: "d6_profile_field_option_translation",
handle_multiples: TRUE,
)]
class ProfileFieldOptionTranslation extends ProcessPluginBase {
/**
* {@inheritdoc}
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
[$field_type, $translation] = $value;
$new_value = NULL;
if (isset($translation)) {
$allowed_values = [];
$list = explode("\n", $translation);
$list = array_map('trim', $list);
$list = array_filter($list, 'strlen');
if ($field_type === 'list_string') {
foreach ($list as $value) {
$allowed_values[] = ['label' => $value];
}
}
$new_value = ['settings.allowed_values', $allowed_values];
}
return $new_value;
}
}

View File

@@ -0,0 +1,80 @@
<?php
namespace Drupal\user\Plugin\migrate\process\d6;
use Drupal\Core\Datetime\TimeZoneFormHelper;
use Drupal\migrate\Attribute\MigrateProcess;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\Row;
use Drupal\Core\Config\Config;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Converts user time zones from time zone offsets to time zone names.
*/
#[MigrateProcess('user_update_7002')]
class UserUpdate7002 extends ProcessPluginBase implements ContainerFactoryPluginInterface {
/**
* System timezones.
*
* @var array
*/
protected static $timezones;
/**
* Contains the system.theme configuration object.
*
* @var \Drupal\Core\Config\Config
*/
protected $dateConfig;
/**
* {@inheritdoc}
*/
public function __construct(array $configuration, $plugin_id, array $plugin_definition, Config $date_config) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->dateConfig = $date_config;
if (!isset(static::$timezones)) {
static::$timezones = TimeZoneFormHelper::getOptionsList();
}
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('config.factory')->get('system.date')
);
}
/**
* {@inheritdoc}
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
$timezone = NULL;
if ($row->hasSourceProperty('timezone_name')) {
if (isset(static::$timezones[$row->getSourceProperty('timezone_name')])) {
$timezone = $row->getSourceProperty('timezone_name');
}
}
if (!$timezone && $row->hasSourceProperty('event_timezone')) {
if (isset(static::$timezones[$row->getSourceProperty('event_timezone')])) {
$timezone = $row->getSourceProperty('event_timezone');
}
}
if ($timezone === NULL) {
$timezone = $this->dateConfig->get('timezone.default');
}
return $timezone;
}
}

View File

@@ -0,0 +1,133 @@
<?php
namespace Drupal\user\Plugin\migrate\source;
use Drupal\migrate\Exception\RequirementsException;
use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
use Drupal\migrate\Row;
/**
* Drupal 6/7 profile field source from database.
*
* For available configuration keys, refer to the parent classes.
*
* @see \Drupal\migrate\Plugin\migrate\source\SqlBase
* @see \Drupal\migrate\Plugin\migrate\source\SourcePluginBase
*
* @MigrateSource(
* id = "profile_field",
* source_module = "profile"
* )
*/
class ProfileField extends DrupalSqlBase {
/**
* The source table containing profile field info.
*
* @var string
*/
protected $fieldTable;
/**
* The source table containing the profile values.
*
* @var string
*/
protected $valueTable;
/**
* {@inheritdoc}
*/
public function query() {
$this->setTableNames();
return $this->select($this->fieldTable, 'pf')->fields('pf');
}
/**
* {@inheritdoc}
*/
public function prepareRow(Row $row) {
if ($row->getSourceProperty('type') == 'selection') {
// Get the current options.
$current_options = preg_split("/[\r\n]+/", $row->getSourceProperty('options'));
// Select the list values from the profile_values table to ensure we get
// them all since they can get out of sync with profile_fields.
$options = $this->select($this->valueTable, 'pv')
->distinct()
->fields('pv', ['value'])
->condition('fid', $row->getSourceProperty('fid'))
->execute()
->fetchCol();
$options = array_merge($current_options, $options);
// array_combine() takes care of any duplicates options.
$row->setSourceProperty('options', array_combine($options, $options));
}
if ($row->getSourceProperty('type') == 'checkbox') {
// D6 profile checkboxes values are always 0 or 1 (with no labels), so we
// need to create two label-less options that will get 0 and 1 for their
// keys.
$row->setSourceProperty('options', [NULL, NULL]);
}
return parent::prepareRow($row);
}
/**
* {@inheritdoc}
*/
public function fields() {
return [
'fid' => $this->t('Primary Key: Unique profile field ID.'),
'title' => $this->t('Title of the field shown to the end user.'),
'name' => $this->t('Internal name of the field used in the form HTML and URLs.'),
'explanation' => $this->t('Explanation of the field to end users.'),
'category' => $this->t('Profile category that the field will be grouped under.'),
'page' => $this->t("Title of page used for browsing by the field's value"),
'type' => $this->t('Type of form field.'),
'weight' => $this->t('Weight of field in relation to other profile fields.'),
'required' => $this->t('Whether the user is required to enter a value. (0 = no, 1 = yes)'),
'register' => $this->t('Whether the field is visible in the user registration form. (1 = yes, 0 = no)'),
'visibility' => $this->t('The level of visibility for the field. (0 = hidden, 1 = private, 2 = public on profile but not member list pages, 3 = public on profile and list pages)'),
'autocomplete' => $this->t('Whether form auto-completion is enabled. (0 = disabled, 1 = enabled)'),
'options' => $this->t('List of options to be used in a list selection field.'),
];
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['fid']['type'] = 'integer';
return $ids;
}
/**
* {@inheritdoc}
*/
public function checkRequirements() {
$this->setTableNames();
if (!$this->getDatabase()->schema()->tableExists($this->fieldTable)) {
// If we make it to here, the profile module isn't installed.
throw new RequirementsException('Profile module not enabled on source site');
}
parent::checkRequirements();
}
/**
* Helper to set the profile field table names.
*/
protected function setTableNames() {
if (empty($this->fieldTable) || empty($this->valueTable)) {
if ($this->getModuleSchemaVersion('system') >= 7000) {
$this->fieldTable = 'profile_field';
$this->valueTable = 'profile_value';
}
else {
$this->fieldTable = 'profile_fields';
$this->valueTable = 'profile_values';
}
}
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace Drupal\user\Plugin\migrate\source;
use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
use Drupal\migrate\Plugin\migrate\source\DummyQueryTrait;
/**
* Drupal 6/7 user picture field instance source from database.
*
* For available configuration keys, refer to the parent classes.
*
* @see \Drupal\migrate\Plugin\migrate\source\SqlBase
* @see \Drupal\migrate\Plugin\migrate\source\SourcePluginBase
*
* @todo Support default picture?
*
* @MigrateSource(
* id = "user_picture_instance",
* source_module = "user"
* )
*/
class UserPictureInstance extends DrupalSqlBase {
use DummyQueryTrait;
/**
* {@inheritdoc}
*/
public function initializeIterator() {
return new \ArrayIterator([
[
'id' => '',
'file_directory' => $this->variableGet('user_picture_path', 'pictures'),
'max_filesize' => $this->variableGet('user_picture_file_size', '30') . 'KB',
'max_resolution' => $this->variableGet('user_picture_dimensions', '85x85'),
],
]);
}
/**
* {@inheritdoc}
*/
public function fields() {
return [
'file_directory' => 'The directory to store images..',
'max_filesize' => 'The maximum allowed file size in KBs.',
'max_resolution' => "The maximum dimensions.",
];
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['id']['type'] = 'string';
return $ids;
}
}

View File

@@ -0,0 +1,64 @@
<?php
namespace Drupal\user\Plugin\migrate\source\d6;
use Drupal\user\Plugin\migrate\source\ProfileField;
// cspell:ignore nprofile objectid
/**
* Drupal 6 i18n profile field option labels source from database.
*
* For available configuration keys, refer to the parent classes.
*
* @see \Drupal\migrate\Plugin\migrate\source\SqlBase
* @see \Drupal\migrate\Plugin\migrate\source\SourcePluginBase
*
* @MigrateSource(
* id = "d6_profile_field_option_translation",
* source_module = "i18nprofile"
* )
*/
class ProfileFieldOptionTranslation extends ProfileField {
/**
* {@inheritdoc}
*/
public function query() {
$query = parent::query();
$query
->fields('i18n', ['property', 'objectid'])
->fields('lt', ['translation', 'language'])
->condition('i18n.type', 'field')
->condition('property', 'options');
$query->leftJoin('i18n_strings', 'i18n', '[pf].[name] = [i18n].[objectid]');
$query->innerJoin('locales_target', 'lt', '[lt].[lid] = [i18n].[lid]');
return $query;
}
/**
* {@inheritdoc}
*/
public function fields() {
return parent::fields() +
[
'property' => $this->t('Option ID.'),
'objectid' => $this->t('Field name'),
'language' => $this->t('Language for this field.'),
'translation' => $this->t('Translation of either the title or explanation.'),
];
}
/**
* {@inheritdoc}
*/
public function getIds() {
return parent::getIds() +
[
'language' => ['type' => 'string'],
'property' => ['type' => 'string'],
];
}
}

View File

@@ -0,0 +1,99 @@
<?php
namespace Drupal\user\Plugin\migrate\source\d6;
use Drupal\migrate\Row;
use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
/**
* Drupal 6 profile fields values source from database.
*
* For available configuration keys, refer to the parent classes.
*
* @see \Drupal\migrate\Plugin\migrate\source\SqlBase
* @see \Drupal\migrate\Plugin\migrate\source\SourcePluginBase
*
* @MigrateSource(
* id = "d6_profile_field_values",
* source_module = "profile"
* )
*/
class ProfileFieldValues extends DrupalSqlBase {
/**
* {@inheritdoc}
*/
public function query() {
$query = $this->select('profile_values', 'pv')
->distinct()
->fields('pv', ['uid']);
return $query;
}
/**
* {@inheritdoc}
*/
public function prepareRow(Row $row) {
// Find profile values for this row.
$query = $this->select('profile_values', 'pv')
->fields('pv', ['fid', 'value']);
$query->leftJoin('profile_fields', 'pf', '[pf].[fid] = [pv].[fid]');
$query->fields('pf', ['name', 'type']);
$query->condition('uid', $row->getSourceProperty('uid'));
$results = $query->execute();
foreach ($results as $profile_value) {
// Check special case for date. We need to unserialize.
if ($profile_value['type'] == 'date') {
$date = unserialize($profile_value['value']);
$date = date('Y-m-d', mktime(0, 0, 0, $date['month'], $date['day'], $date['year']));
$row->setSourceProperty($profile_value['name'], ['value' => $date]);
}
elseif ($profile_value['type'] == 'list') {
// Explode by newline and comma.
$row->setSourceProperty($profile_value['name'], preg_split("/[\r\n,]+/", $profile_value['value']));
}
else {
$row->setSourceProperty($profile_value['name'], [$profile_value['value']]);
}
}
return parent::prepareRow($row);
}
/**
* {@inheritdoc}
*/
public function fields() {
$fields = [
'fid' => $this->t('Unique profile field ID.'),
'uid' => $this->t('The user Id.'),
'value' => $this->t('The value for this field.'),
];
$query = $this->select('profile_values', 'pv')
->fields('pv', ['fid', 'value']);
$query->leftJoin('profile_fields', 'pf', '[pf].[fid] = [pv].[fid]');
$query->fields('pf', ['name', 'title']);
$results = $query->execute();
foreach ($results as $profile) {
$fields[$profile['name']] = $this->t($profile['title']);
}
return $fields;
}
/**
* {@inheritdoc}
*/
public function getIds() {
return [
'uid' => [
'type' => 'integer',
'alias' => 'pv',
],
];
}
}

View File

@@ -0,0 +1,101 @@
<?php
namespace Drupal\user\Plugin\migrate\source\d6;
use Drupal\migrate\Row;
use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
/**
* Drupal 6 role source from database.
*
* For available configuration keys, refer to the parent classes.
*
* @see \Drupal\migrate\Plugin\migrate\source\SqlBase
* @see \Drupal\migrate\Plugin\migrate\source\SourcePluginBase
*
* @MigrateSource(
* id = "d6_user_role",
* source_module = "user"
* )
*/
class Role extends DrupalSqlBase {
/**
* List of filter IDs per role IDs.
*
* @var array
*/
protected $filterPermissions = [];
/**
* {@inheritdoc}
*/
public function query() {
$query = $this->select('role', 'r')
->fields('r', ['rid', 'name'])
->orderBy('r.rid');
return $query;
}
/**
* {@inheritdoc}
*/
public function fields() {
return [
'rid' => $this->t('Role ID.'),
'name' => $this->t('The name of the user role.'),
];
}
/**
* {@inheritdoc}
*/
protected function initializeIterator() {
$filter_roles = $this->select('filter_formats', 'f')
->fields('f', ['format', 'roles'])
->execute()
->fetchAllKeyed();
foreach ($filter_roles as $format => $roles) {
// Drupal 6 code: $roles = ','. implode(',', $roles) .',';
// Remove the beginning and ending comma.
foreach (explode(',', trim($roles, ',')) as $rid) {
$this->filterPermissions[$rid][] = $format;
}
}
return parent::initializeIterator();
}
/**
* {@inheritdoc}
*/
public function prepareRow(Row $row) {
$rid = $row->getSourceProperty('rid');
$permissions = $this->select('permission', 'p')
->fields('p', ['perm'])
->condition('rid', $rid)
->execute()
->fetchField();
// If a role has no permissions then set to an empty array. The role will
// be migrated and given the default D8 permissions.
if ($permissions) {
$row->setSourceProperty('permissions', explode(', ', $permissions));
}
else {
$row->setSourceProperty('permissions', []);
}
if (isset($this->filterPermissions[$rid])) {
$row->setSourceProperty("filter_permissions:$rid", $this->filterPermissions[$rid]);
}
return parent::prepareRow($row);
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['rid']['type'] = 'integer';
return $ids;
}
}

View File

@@ -0,0 +1,137 @@
<?php
namespace Drupal\user\Plugin\migrate\source\d6;
use Drupal\migrate\Row;
use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
/**
* Drupal 6 user source from database.
*
* For available configuration keys, refer to the parent classes.
*
* @see \Drupal\migrate\Plugin\migrate\source\SqlBase
* @see \Drupal\migrate\Plugin\migrate\source\SourcePluginBase
*
* @MigrateSource(
* id = "d6_user",
* source_module = "user"
* )
*/
class User extends DrupalSqlBase {
/**
* {@inheritdoc}
*/
public function query() {
return $this->select('users', 'u')
->fields('u', array_keys($this->baseFields()))
->condition('u.uid', 0, '>');
}
/**
* {@inheritdoc}
*/
public function fields() {
$fields = $this->baseFields();
// Add roles field.
$fields['roles'] = $this->t('Roles');
return $fields;
}
/**
* {@inheritdoc}
*/
public function prepareRow(Row $row) {
// User roles.
$roles = $this->select('users_roles', 'ur')
->fields('ur', ['rid'])
->condition('ur.uid', $row->getSourceProperty('uid'))
->execute()
->fetchCol();
$row->setSourceProperty('roles', $roles);
// We are adding here the Event contributed module column.
// @see https://api.drupal.org/api/drupal/modules%21user%21user.install/function/user_update_7002/7
if ($row->hasSourceProperty('timezone_id') && $row->getSourceProperty('timezone_id')) {
if ($this->getDatabase()->schema()->tableExists('event_timezones')) {
$event_timezone = $this->select('event_timezones', 'e')
->fields('e', ['name'])
->condition('e.timezone', $row->getSourceProperty('timezone_id'))
->execute()
->fetchField();
if ($event_timezone) {
$row->setSourceProperty('event_timezone', $event_timezone);
}
}
}
// Unserialize Data.
$data = $row->getSourceProperty('data');
if ($data !== NULL) {
$row->setSourceProperty('data', unserialize($row->getSourceProperty('data')));
}
return parent::prepareRow($row);
}
/**
* {@inheritdoc}
*/
public function getIds() {
return [
'uid' => [
'type' => 'integer',
'alias' => 'u',
],
];
}
/**
* Returns the user base fields to be migrated.
*
* @return array
* Associative array having field name as key and description as value.
*/
protected function baseFields() {
$fields = [
'uid' => $this->t('User ID'),
'name' => $this->t('Username'),
'pass' => $this->t('Password'),
'mail' => $this->t('Email address'),
'theme' => $this->t('Theme'),
'signature' => $this->t('Signature'),
'signature_format' => $this->t('Signature format'),
'created' => $this->t('Registered timestamp'),
'access' => $this->t('Last access timestamp'),
'login' => $this->t('Last login timestamp'),
'status' => $this->t('Status'),
'timezone' => $this->t('Timezone'),
'language' => $this->t('Language'),
'picture' => $this->t('Picture'),
'init' => $this->t('Init'),
'data' => $this->t('User data'),
];
// The database connection may not exist, for example, when building
// the Migrate Message form.
if ($source_database = $this->database) {
// Possible field added by Date contributed module.
// @see https://api.drupal.org/api/drupal/modules%21user%21user.install/function/user_update_7002/7
if ($source_database->schema()->fieldExists('users', 'timezone_name')) {
$fields['timezone_name'] = $this->t('Timezone (Date)');
}
// Possible field added by Event contributed module.
// @see https://api.drupal.org/api/drupal/modules%21user%21user.install/function/user_update_7002/7
if ($source_database->schema()->fieldExists('users', 'timezone_id')) {
$fields['timezone_id'] = $this->t('Timezone (Event)');
}
}
return $fields;
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace Drupal\user\Plugin\migrate\source\d6;
use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
/**
* Drupal 6 user picture source from database.
*
* For available configuration keys, refer to the parent classes.
*
* @see \Drupal\migrate\Plugin\migrate\source\SqlBase
* @see \Drupal\migrate\Plugin\migrate\source\SourcePluginBase
*
* @todo Support default picture?
*
* @MigrateSource(
* id = "d6_user_picture",
* source_module = "user"
* )
*/
class UserPicture extends DrupalSqlBase {
/**
* {@inheritdoc}
*/
public function query() {
$query = $this->select('users', 'u')
->condition('picture', '', '<>')
->fields('u', ['uid', 'access', 'picture'])
->orderBy('u.access');
return $query;
}
/**
* {@inheritdoc}
*/
public function fields() {
return [
'uid' => 'Primary Key: Unique user ID.',
'access' => 'Timestamp for previous time user accessed the site.',
'picture' => "Path to the user's uploaded picture.",
];
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['uid']['type'] = 'integer';
return $ids;
}
}

View File

@@ -0,0 +1,89 @@
<?php
namespace Drupal\user\Plugin\migrate\source\d6;
use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
use Drupal\migrate\Row;
/**
* Drupal 6 user picture source from database.
*
* Available configuration keys:
* - site_path: (optional) The path to the site directory relative to Drupal
* root. Defaults to 'sites/default'.
*
* For additional configuration keys, refer to the parent classes.
*
* @see \Drupal\migrate\Plugin\migrate\source\SqlBase
* @see \Drupal\migrate\Plugin\migrate\source\SourcePluginBase
*
* @MigrateSource(
* id = "d6_user_picture_file",
* source_module = "user"
* )
*/
class UserPictureFile extends DrupalSqlBase {
/**
* The file directory path.
*
* @var string
*/
protected $filePath;
/**
* The temporary file path.
*
* @var string
*/
protected $tempFilePath;
/**
* {@inheritdoc}
*/
public function query() {
$query = $this->select('users', 'u')
->condition('u.picture', '', '<>')
->fields('u', ['uid', 'picture']);
return $query;
}
/**
* {@inheritdoc}
*/
public function initializeIterator() {
$site_path = $this->configuration['site_path'] ?? 'sites/default';
$this->filePath = $this->variableGet('file_directory_path', $site_path . '/files') . '/';
$this->tempFilePath = $this->variableGet('file_directory_temp', '/tmp') . '/';
return parent::initializeIterator();
}
/**
* {@inheritdoc}
*/
public function prepareRow(Row $row) {
$row->setSourceProperty('filename', basename($row->getSourceProperty('picture')));
$row->setSourceProperty('file_directory_path', $this->filePath);
$row->setSourceProperty('temp_directory_path', $this->tempFilePath);
return parent::prepareRow($row);
}
/**
* {@inheritdoc}
*/
public function fields() {
return [
'picture' => "Path to the user's uploaded picture.",
'filename' => 'The picture filename.',
];
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['uid']['type'] = 'integer';
return $ids;
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace Drupal\user\Plugin\migrate\source\d7;
use Drupal\migrate\Row;
use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
/**
* Drupal 7 role source from database.
*
* For available configuration keys, refer to the parent classes.
*
* @see \Drupal\migrate\Plugin\migrate\source\SqlBase
* @see \Drupal\migrate\Plugin\migrate\source\SourcePluginBase
*
* @MigrateSource(
* id = "d7_user_role",
* source_module = "user"
* )
*/
class Role extends DrupalSqlBase {
/**
* {@inheritdoc}
*/
public function query() {
return $this->select('role', 'r')->fields('r');
}
/**
* {@inheritdoc}
*/
public function fields() {
return [
'rid' => $this->t('Role ID.'),
'name' => $this->t('The name of the user role.'),
'weight' => $this->t('The weight of the role.'),
];
}
/**
* {@inheritdoc}
*/
public function prepareRow(Row $row) {
$permissions = $this->select('role_permission', 'rp')
->fields('rp', ['permission'])
->condition('rid', $row->getSourceProperty('rid'))
->execute()
->fetchCol();
$row->setSourceProperty('permissions', $permissions);
return parent::prepareRow($row);
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['rid']['type'] = 'integer';
return $ids;
}
}

View File

@@ -0,0 +1,139 @@
<?php
namespace Drupal\user\Plugin\migrate\source\d7;
use Drupal\migrate\Row;
use Drupal\migrate_drupal\Plugin\migrate\source\d7\FieldableEntity;
/**
* Drupal 7 user source from database.
*
* For available configuration keys, refer to the parent classes.
*
* @see \Drupal\migrate\Plugin\migrate\source\SqlBase
* @see \Drupal\migrate\Plugin\migrate\source\SourcePluginBase
*
* @MigrateSource(
* id = "d7_user",
* source_module = "user"
* )
*/
class User extends FieldableEntity {
/**
* {@inheritdoc}
*/
public function query() {
return $this->select('users', 'u')
->fields('u')
->condition('u.uid', 0, '>');
}
/**
* {@inheritdoc}
*/
public function fields() {
$fields = [
'uid' => $this->t('User ID'),
'name' => $this->t('Username'),
'pass' => $this->t('Password'),
'mail' => $this->t('Email address'),
'signature' => $this->t('Signature'),
'signature_format' => $this->t('Signature format'),
'created' => $this->t('Registered timestamp'),
'access' => $this->t('Last access timestamp'),
'login' => $this->t('Last login timestamp'),
'status' => $this->t('Status'),
'timezone' => $this->t('Timezone'),
'language' => $this->t('Language'),
'picture' => $this->t('Picture'),
'init' => $this->t('Init'),
'data' => $this->t('User data'),
'roles' => $this->t('Roles'),
];
// Profile fields.
if ($this->moduleExists('profile')) {
$fields += $this->select('profile_fields', 'pf')
->fields('pf', ['name', 'title'])
->execute()
->fetchAllKeyed();
}
return $fields;
}
/**
* {@inheritdoc}
*/
public function prepareRow(Row $row) {
$uid = $row->getSourceProperty('uid');
$roles = $this->select('users_roles', 'ur')
->fields('ur', ['rid'])
->condition('ur.uid', $uid)
->execute()
->fetchCol();
$row->setSourceProperty('roles', $roles);
$row->setSourceProperty('data', unserialize($row->getSourceProperty('data') ?? ''));
// If this entity was translated using Entity Translation, we need to get
// its source language to get the field values in the right language.
// The translations will be migrated by the d7_user_entity_translation
// migration.
$entity_translatable = $this->isEntityTranslatable('user');
$source_language = $this->getEntityTranslationSourceLanguage('user', $uid);
$language = $entity_translatable && $source_language ? $source_language : $row->getSourceProperty('language');
$row->setSourceProperty('entity_language', $language);
// Get Field API field values.
foreach ($this->getFields('user') as $field_name => $field) {
// Ensure we're using the right language if the entity and the field are
// translatable.
$field_language = $entity_translatable && $field['translatable'] ? $language : NULL;
$row->setSourceProperty($field_name, $this->getFieldValues('user', $field_name, $uid, NULL, $field_language));
}
// Get profile field values. This code is lifted directly from the D6
// ProfileFieldValues plugin.
if ($this->getDatabase()->schema()->tableExists('profile_value')) {
$query = $this->select('profile_value', 'pv')
->fields('pv', ['fid', 'value']);
$query->leftJoin('profile_field', 'pf', '[pf].[fid] = [pv].[fid]');
$query->fields('pf', ['name', 'type']);
$query->condition('uid', $row->getSourceProperty('uid'));
$results = $query->execute();
foreach ($results as $profile_value) {
if ($profile_value['type'] == 'date') {
$date = unserialize($profile_value['value']);
$date = date('Y-m-d', mktime(0, 0, 0, $date['month'], $date['day'], $date['year']));
$row->setSourceProperty($profile_value['name'], ['value' => $date]);
}
elseif ($profile_value['type'] == 'list') {
// Explode by newline and comma.
$row->setSourceProperty($profile_value['name'], preg_split("/[\r\n,]+/", $profile_value['value']));
}
else {
$row->setSourceProperty($profile_value['name'], [$profile_value['value']]);
}
}
}
return parent::prepareRow($row);
}
/**
* {@inheritdoc}
*/
public function getIds() {
return [
'uid' => [
'type' => 'integer',
'alias' => 'u',
],
];
}
}

View File

@@ -0,0 +1,84 @@
<?php
namespace Drupal\user\Plugin\migrate\source\d7;
use Drupal\migrate\Row;
use Drupal\migrate_drupal\Plugin\migrate\source\d7\FieldableEntity;
/**
* Drupal 7 user entity translations source from database.
*
* For available configuration keys, refer to the parent classes.
*
* @see \Drupal\migrate\Plugin\migrate\source\SqlBase
* @see \Drupal\migrate\Plugin\migrate\source\SourcePluginBase
*
* @MigrateSource(
* id = "d7_user_entity_translation",
* source_module = "entity_translation"
* )
*/
class UserEntityTranslation extends FieldableEntity {
/**
* {@inheritdoc}
*/
public function query() {
$query = $this->select('entity_translation', 'et')
->fields('et')
->condition('et.entity_type', 'user')
->condition('et.source', '', '<>');
return $query;
}
/**
* {@inheritdoc}
*/
public function prepareRow(Row $row) {
$uid = $row->getSourceProperty('entity_id');
$language = $row->getSourceProperty('language');
// Get Field API field values.
foreach ($this->getFields('user') as $field_name => $field) {
// Ensure we're using the right language if the entity is translatable.
$field_language = $field['translatable'] ? $language : NULL;
$row->setSourceProperty($field_name, $this->getFieldValues('user', $field_name, $uid, NULL, $field_language));
}
return parent::prepareRow($row);
}
/**
* {@inheritdoc}
*/
public function fields() {
return [
'entity_type' => $this->t('The entity type this translation relates to'),
'entity_id' => $this->t('The entity id this translation relates to'),
'revision_id' => $this->t('The entity revision id this translation relates to'),
'language' => $this->t('The target language for this translation.'),
'source' => $this->t('The source language from which this translation was created.'),
'uid' => $this->t('The author of this translation.'),
'status' => $this->t('Boolean indicating whether the translation is published (visible to non-administrators).'),
'translate' => $this->t('A boolean indicating whether this translation needs to be updated.'),
'created' => $this->t('The Unix timestamp when the translation was created.'),
'changed' => $this->t('The Unix timestamp when the translation was most recently saved.'),
];
}
/**
* {@inheritdoc}
*/
public function getIds() {
return [
'entity_id' => [
'type' => 'integer',
],
'language' => [
'type' => 'string',
],
];
}
}

View File

@@ -0,0 +1,213 @@
<?php
namespace Drupal\user\Plugin\rest\resource;
use Drupal\Core\Config\ImmutableConfig;
use Drupal\Core\Password\PasswordGeneratorInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\rest\Attribute\RestResource;
use Drupal\rest\ModifiedResourceResponse;
use Drupal\rest\Plugin\ResourceBase;
use Drupal\rest\Plugin\rest\resource\EntityResourceAccessTrait;
use Drupal\rest\Plugin\rest\resource\EntityResourceValidationTrait;
use Drupal\user\Entity\User;
use Drupal\user\UserInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
/**
* Represents user registration as a resource.
*/
#[RestResource(
id: "user_registration",
label: new TranslatableMarkup("User registration"),
serialization_class: User::class,
uri_paths: [
"create" => "/user/register",
],
)]
class UserRegistrationResource extends ResourceBase {
use EntityResourceValidationTrait;
use EntityResourceAccessTrait;
/**
* User settings config instance.
*
* @var \Drupal\Core\Config\ImmutableConfig
*/
protected $userSettings;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* The password generator.
*
* @var \Drupal\Core\Password\PasswordGeneratorInterface
*/
protected $passwordGenerator;
/**
* Constructs a new UserRegistrationResource instance.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param array $serializer_formats
* The available serialization formats.
* @param \Psr\Log\LoggerInterface $logger
* A logger instance.
* @param \Drupal\Core\Config\ImmutableConfig $user_settings
* A user settings config instance.
* @param \Drupal\Core\Session\AccountInterface $current_user
* The current user.
* @param \Drupal\Core\Password\PasswordGeneratorInterface|null $password_generator
* The password generator.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, array $serializer_formats, LoggerInterface $logger, ImmutableConfig $user_settings, AccountInterface $current_user, ?PasswordGeneratorInterface $password_generator = NULL) {
if (is_null($password_generator)) {
@trigger_error('Calling ' . __METHOD__ . '() without the $password_generator argument is deprecated in drupal:10.3.0 and will be required in drupal:11.0.0. See https://www.drupal.org/node/3405799', E_USER_DEPRECATED);
$password_generator = \Drupal::service('password_generator');
}
parent::__construct($configuration, $plugin_id, $plugin_definition, $serializer_formats, $logger);
$this->userSettings = $user_settings;
$this->currentUser = $current_user;
$this->passwordGenerator = $password_generator;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->getParameter('serializer.formats'),
$container->get('logger.factory')->get('rest'),
$container->get('config.factory')->get('user.settings'),
$container->get('current_user'),
$container->get('password_generator')
);
}
/**
* Responds to user registration POST request.
*
* @param \Drupal\user\UserInterface $account
* The user account entity.
*
* @return \Drupal\rest\ModifiedResourceResponse
* The HTTP response object.
*
* @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
* @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
*/
public function post(?UserInterface $account = NULL) {
$this->ensureAccountCanRegister($account);
// Only activate new users if visitors are allowed to register.
if ($this->userSettings->get('register') == UserInterface::REGISTER_VISITORS) {
$account->activate();
}
else {
$account->block();
}
// Generate password if email verification required.
if ($this->userSettings->get('verify_mail')) {
$account->setPassword($this->passwordGenerator->generate());
}
$this->checkEditFieldAccess($account);
// Make sure that the user entity is valid (email and name are valid).
$this->validate($account);
// Create the account.
$account->save();
$this->sendEmailNotifications($account);
return new ModifiedResourceResponse($account, 200);
}
/**
* Ensure the account can be registered in this request.
*
* @param \Drupal\user\UserInterface $account
* The user account to register.
*/
protected function ensureAccountCanRegister(?UserInterface $account = NULL) {
if ($account === NULL) {
throw new BadRequestHttpException('No user account data for registration received.');
}
// POSTed user accounts must not have an ID set, because we always want to
// create new entities here.
if (!$account->isNew()) {
throw new BadRequestHttpException('An ID has been set and only new user accounts can be registered.');
}
// Only allow anonymous users to register, authenticated users with the
// necessary permissions can POST a new user to the "user" REST resource.
// @see \Drupal\rest\Plugin\rest\resource\EntityResource
if (!$this->currentUser->isAnonymous()) {
throw new AccessDeniedHttpException('Only anonymous users can register a user.');
}
// Verify that the current user can register a user account.
if ($this->userSettings->get('register') == UserInterface::REGISTER_ADMINISTRATORS_ONLY) {
throw new AccessDeniedHttpException('You cannot register a new user account.');
}
if (!$this->userSettings->get('verify_mail')) {
if (empty($account->getPassword())) {
// If no email verification then the user must provide a password.
throw new UnprocessableEntityHttpException('No password provided.');
}
}
else {
if (!empty($account->getPassword())) {
// If email verification required then a password cannot provided.
// The password will be set when the user logs in.
throw new UnprocessableEntityHttpException('A Password cannot be specified. It will be generated on login.');
}
}
}
/**
* Sends email notifications if necessary for user that was registered.
*
* @param \Drupal\user\UserInterface $account
* The user account.
*/
protected function sendEmailNotifications(UserInterface $account) {
$approval_settings = $this->userSettings->get('register');
// No email verification is required. Activating the user.
if ($approval_settings == UserInterface::REGISTER_VISITORS) {
if ($this->userSettings->get('verify_mail')) {
// No administrator approval required.
_user_mail_notify('register_no_approval_required', $account);
}
}
// Administrator approval required.
elseif ($approval_settings == UserInterface::REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL) {
_user_mail_notify('register_pending_approval', $account);
}
}
}

View File

@@ -0,0 +1,162 @@
<?php
namespace Drupal\user\Plugin\views\access;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\DependencyInjection\DeprecatedServicePropertyTrait;
use Drupal\Core\Extension\ModuleExtensionList;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\user\PermissionHandlerInterface;
use Drupal\views\Attribute\ViewsAccess;
use Drupal\views\Plugin\views\access\AccessPluginBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Routing\Route;
/**
* Access plugin that provides permission-based access control.
*
* @ingroup views_access_plugins
*/
#[ViewsAccess(
id: 'perm',
title: new TranslatableMarkup('Permission'),
help: new TranslatableMarkup('Access will be granted to users with the specified permission string.'),
)]
class Permission extends AccessPluginBase implements CacheableDependencyInterface {
use DeprecatedServicePropertyTrait;
/**
* The service properties that should raise a deprecation error.
*/
private array $deprecatedProperties = ['moduleHandler' => 'module_handler'];
/**
* {@inheritdoc}
*/
protected $usesOptions = TRUE;
/**
* The permission handler.
*
* @var \Drupal\user\PermissionHandlerInterface
*/
protected $permissionHandler;
/**
* Module extension list.
*/
protected ModuleExtensionList $moduleExtensionList;
/**
* Constructs a Permission object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\user\PermissionHandlerInterface $permission_handler
* The permission handler.
* @param \Drupal\Core\Extension\ModuleExtensionList|\Drupal\Core\Extension\ModuleHandlerInterface $module_extension_list
* The module extension list.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, PermissionHandlerInterface $permission_handler, ModuleExtensionList|ModuleHandlerInterface $module_extension_list) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->permissionHandler = $permission_handler;
if ($module_extension_list instanceof ModuleHandlerInterface) {
@trigger_error('Calling ' . __METHOD__ . '() with the $module_extension_list argument as ModuleHandlerInterface is deprecated in drupal:10.3.0 and will be required in drupal:12.0.0. See https://www.drupal.org/node/3310017', E_USER_DEPRECATED);
$module_extension_list = \Drupal::service('extension.list.module');
}
$this->moduleExtensionList = $module_extension_list;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('user.permissions'),
$container->get('extension.list.module'),
);
}
/**
* {@inheritdoc}
*/
public function access(AccountInterface $account) {
return $account->hasPermission($this->options['perm']);
}
/**
* {@inheritdoc}
*/
public function alterRouteDefinition(Route $route) {
$route->setRequirement('_permission', $this->options['perm']);
}
public function summaryTitle() {
$permissions = $this->permissionHandler->getPermissions();
if (isset($permissions[$this->options['perm']])) {
return $permissions[$this->options['perm']]['title'];
}
return $this->t($this->options['perm']);
}
protected function defineOptions() {
$options = parent::defineOptions();
$options['perm'] = ['default' => 'access content'];
return $options;
}
public function buildOptionsForm(&$form, FormStateInterface $form_state) {
parent::buildOptionsForm($form, $form_state);
// Get list of permissions
$perms = [];
$permissions = $this->permissionHandler->getPermissions();
foreach ($permissions as $perm => $perm_item) {
$provider = $perm_item['provider'];
$display_name = $this->moduleExtensionList->getName($provider);
$perms[$display_name][$perm] = strip_tags($perm_item['title']);
}
$form['perm'] = [
'#type' => 'select',
'#options' => $perms,
'#title' => $this->t('Permission'),
'#default_value' => $this->options['perm'],
'#description' => $this->t('Only users with the selected permission flag will be able to access this display.'),
];
}
/**
* {@inheritdoc}
*/
public function getCacheMaxAge() {
return Cache::PERMANENT;
}
/**
* {@inheritdoc}
*/
public function getCacheContexts() {
return ['user.permissions'];
}
/**
* {@inheritdoc}
*/
public function getCacheTags() {
return [];
}
}

View File

@@ -0,0 +1,166 @@
<?php
namespace Drupal\user\Plugin\views\access;
use Drupal\Component\Utility\Html;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\user\RoleInterface;
use Drupal\user\RoleStorageInterface;
use Drupal\views\Attribute\ViewsAccess;
use Drupal\views\Plugin\views\access\AccessPluginBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Routing\Route;
use Drupal\Core\Session\AccountInterface;
/**
* Access plugin that provides role-based access control.
*
* @ingroup views_access_plugins
*/
#[ViewsAccess(
id: 'role',
title: new TranslatableMarkup('Role'),
help: new TranslatableMarkup('Access will be granted to users with any of the specified roles.'),
)]
class Role extends AccessPluginBase implements CacheableDependencyInterface {
/**
* {@inheritdoc}
*/
protected $usesOptions = TRUE;
/**
* The role storage.
*
* @var \Drupal\user\RoleStorageInterface
*/
protected $roleStorage;
/**
* Constructs a Role object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\user\RoleStorageInterface $role_storage
* The role storage.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, RoleStorageInterface $role_storage) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->roleStorage = $role_storage;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('entity_type.manager')->getStorage('user_role')
);
}
/**
* {@inheritdoc}
*/
public function access(AccountInterface $account) {
return !empty(array_intersect(array_filter($this->options['role']), $account->getRoles()));
}
/**
* {@inheritdoc}
*/
public function alterRouteDefinition(Route $route) {
if ($this->options['role']) {
$route->setRequirement('_role', (string) implode('+', $this->options['role']));
}
}
public function summaryTitle() {
$count = count($this->options['role']);
if ($count < 1) {
return $this->t('No role(s) selected');
}
elseif ($count > 1) {
return $this->t('Multiple roles');
}
else {
$rid = reset($this->options['role']);
return $this->roleStorage->load($rid)->label();
}
}
protected function defineOptions() {
$options = parent::defineOptions();
$options['role'] = ['default' => []];
return $options;
}
public function buildOptionsForm(&$form, FormStateInterface $form_state) {
parent::buildOptionsForm($form, $form_state);
$form['role'] = [
'#type' => 'checkboxes',
'#title' => $this->t('Role'),
'#default_value' => $this->options['role'],
'#options' => array_map(fn(RoleInterface $role) => Html::escape($role->label()), $this->roleStorage->loadMultiple()),
'#description' => $this->t('Only the checked roles will be able to access this display.'),
];
}
public function validateOptionsForm(&$form, FormStateInterface $form_state) {
$role = $form_state->getValue(['access_options', 'role']);
$role = array_filter($role);
if (!$role) {
$form_state->setError($form['role'], $this->t('You must select at least one role if type is "by role"'));
}
$form_state->setValue(['access_options', 'role'], $role);
}
/**
* {@inheritdoc}
*/
public function calculateDependencies() {
$dependencies = parent::calculateDependencies();
foreach (array_keys($this->options['role']) as $rid) {
if ($role = $this->roleStorage->load($rid)) {
$dependencies[$role->getConfigDependencyKey()][] = $role->getConfigDependencyName();
}
}
return $dependencies;
}
/**
* {@inheritdoc}
*/
public function getCacheMaxAge() {
return Cache::PERMANENT;
}
/**
* {@inheritdoc}
*/
public function getCacheContexts() {
return ['user.roles'];
}
/**
* {@inheritdoc}
*/
public function getCacheTags() {
return [];
}
}

View File

@@ -0,0 +1,69 @@
<?php
namespace Drupal\user\Plugin\views\argument;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\views\Attribute\ViewsArgument;
use Drupal\views\Plugin\views\argument\ManyToOne;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Allow role ID(s) as argument.
*
* @ingroup views_argument_handlers
*/
#[ViewsArgument(
id: 'user__roles_rid'
)]
class RolesRid extends ManyToOne {
/**
* The role entity storage.
*
* @var \Drupal\user\RoleStorage
*/
protected $roleStorage;
/**
* Constructs a \Drupal\user\Plugin\views\argument\RolesRid object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->roleStorage = $entity_type_manager->getStorage('user_role');
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('entity_type.manager')
);
}
/**
* {@inheritdoc}
*/
public function titleQuery() {
$entities = $this->roleStorage->loadMultiple($this->value);
$titles = [];
foreach ($entities as $entity) {
$titles[] = $entity->label();
}
return $titles;
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Drupal\user\Plugin\views\argument;
use Drupal\views\Attribute\ViewsArgument;
use Drupal\views\Plugin\views\argument\EntityArgument;
/**
* Argument handler to accept a user id.
*
* @ingroup views_argument_handlers
*/
#[ViewsArgument(
id: 'user_uid'
)]
class Uid extends EntityArgument {}

View File

@@ -0,0 +1,43 @@
<?php
namespace Drupal\user\Plugin\views\argument_default;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\views\Attribute\ViewsArgumentDefault;
use Drupal\views\Plugin\views\argument_default\ArgumentDefaultPluginBase;
/**
* Default argument plugin to extract the current user.
*
* This plugin actually has no options so it does not need to do a great deal.
*/
#[ViewsArgumentDefault(
id: 'current_user',
title: new TranslatableMarkup('User ID from logged in user'),
)]
class CurrentUser extends ArgumentDefaultPluginBase implements CacheableDependencyInterface {
/**
* {@inheritdoc}
*/
public function getArgument() {
return \Drupal::currentUser()->id();
}
/**
* {@inheritdoc}
*/
public function getCacheMaxAge() {
return Cache::PERMANENT;
}
/**
* {@inheritdoc}
*/
public function getCacheContexts() {
return ['user'];
}
}

View File

@@ -0,0 +1,117 @@
<?php
namespace Drupal\user\Plugin\views\argument_default;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\views\Attribute\ViewsArgumentDefault;
use Drupal\views\Plugin\views\argument_default\ArgumentDefaultPluginBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\user\UserInterface;
use Drupal\node\NodeInterface;
/**
* Default argument plugin to extract a user from request.
*/
#[ViewsArgumentDefault(
id: 'user',
title: new TranslatableMarkup('User ID from route context"'),
)]
class User extends ArgumentDefaultPluginBase implements CacheableDependencyInterface {
/**
* The route match.
*
* @var \Drupal\Core\Routing\RouteMatchInterface
*/
protected $routeMatch;
/**
* Constructs a new User instance.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The route match.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, RouteMatchInterface $route_match) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->routeMatch = $route_match;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('current_route_match')
);
}
/**
* {@inheritdoc}
*/
protected function defineOptions() {
$options = parent::defineOptions();
$options['user'] = ['default' => ''];
return $options;
}
/**
* {@inheritdoc}
*/
public function buildOptionsForm(&$form, FormStateInterface $form_state) {
$form['user'] = [
'#type' => 'checkbox',
'#title' => $this->t('Also look for a node and use the node author'),
'#default_value' => $this->options['user'],
];
}
/**
* {@inheritdoc}
*/
public function getArgument() {
// If there is a user object in the current route.
if ($user = $this->routeMatch->getParameter('user')) {
if ($user instanceof UserInterface) {
return $user->id();
}
}
// If option to use node author; and node in current route.
if (!empty($this->options['user']) && $node = $this->routeMatch->getParameter('node')) {
if ($node instanceof NodeInterface) {
return $node->getOwnerId();
}
}
}
/**
* {@inheritdoc}
*/
public function getCacheMaxAge() {
return Cache::PERMANENT;
}
/**
* {@inheritdoc}
*/
public function getCacheContexts() {
return ['url'];
}
}

View File

@@ -0,0 +1,119 @@
<?php
namespace Drupal\user\Plugin\views\argument_validator;
use Drupal\Component\Utility\Html;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\user\Entity\Role;
use Drupal\user\RoleInterface;
use Drupal\views\Plugin\views\argument\ArgumentPluginBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\views\Plugin\views\argument_validator\Entity;
/**
* Validate whether an argument is a valid user.
*
* This supports either numeric arguments (UID) or strings (username) and
* converts either one into the user's UID. This validator also sets the
* argument's title to the username.
*/
class User extends Entity {
/**
* The user storage.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $userStorage;
/**
* {@inheritdoc}
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, ?EntityTypeBundleInfoInterface $entity_type_bundle_info = NULL) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $entity_type_manager, $entity_type_bundle_info);
$this->userStorage = $entity_type_manager->getStorage('user');
}
/**
* {@inheritdoc}
*/
protected function defineOptions() {
$options = parent::defineOptions();
$options['restrict_roles'] = ['default' => FALSE];
$options['roles'] = ['default' => []];
return $options;
}
/**
* {@inheritdoc}
*/
public function buildOptionsForm(&$form, FormStateInterface $form_state) {
parent::buildOptionsForm($form, $form_state);
$sanitized_id = ArgumentPluginBase::encodeValidatorId($this->definition['id']);
$form['restrict_roles'] = [
'#type' => 'checkbox',
'#title' => $this->t('Restrict user based on role'),
'#default_value' => $this->options['restrict_roles'],
];
$roles = Role::loadMultiple();
unset($roles[RoleInterface::ANONYMOUS_ID]);
$roles = array_map(fn(RoleInterface $role) => Html::escape($role->label()), $roles);
$form['roles'] = [
'#type' => 'checkboxes',
'#title' => $this->t('Restrict to the selected roles'),
'#options' => $roles,
'#default_value' => $this->options['roles'],
'#description' => $this->t('If no roles are selected, users from any role will be allowed.'),
'#states' => [
'visible' => [
':input[name="options[validate][options][' . $sanitized_id . '][restrict_roles]"]' => ['checked' => TRUE],
],
],
];
}
/**
* {@inheritdoc}
*/
public function submitOptionsForm(&$form, FormStateInterface $form_state, &$options = []) {
// filter trash out of the options so we don't store giant unnecessary arrays
$options['roles'] = array_filter($options['roles']);
}
/**
* {@inheritdoc}
*/
protected function validateEntity(EntityInterface $entity) {
/** @var \Drupal\user\UserInterface $entity */
$role_check_success = TRUE;
// See if we're filtering users based on roles.
if (!empty($this->options['restrict_roles']) && !empty($this->options['roles'])) {
$roles = $this->options['roles'];
if (!(bool) array_intersect($entity->getRoles(), $roles)) {
$role_check_success = FALSE;
}
}
return $role_check_success && parent::validateEntity($entity);
}
/**
* {@inheritdoc}
*/
public function calculateDependencies() {
$dependencies = parent::calculateDependencies();
foreach ($this->entityTypeManager->getStorage('user_role')->loadMultiple(array_keys($this->options['roles'])) as $role) {
$dependencies[$role->getConfigDependencyKey()][] = $role->getConfigDependencyName();
}
return $dependencies;
}
}

View File

@@ -0,0 +1,81 @@
<?php
namespace Drupal\user\Plugin\views\argument_validator;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\views\Attribute\ViewsArgumentValidator;
/**
* Validates whether a user name is valid.
*/
#[ViewsArgumentValidator(
id: 'user_name',
title: new TranslatableMarkup('User name'),
entity_type: 'user'
)]
class UserName extends User {
/**
* {@inheritdoc}
*/
public function buildOptionsForm(&$form, FormStateInterface $form_state) {
parent::buildOptionsForm($form, $form_state);
$entity_type = $this->entityTypeManager->getDefinition('user');
$form['multiple']['#options'] = [
0 => $this->t('Single name', ['%type' => $entity_type->getLabel()]),
1 => $this->t('One or more names separated by , or +', ['%type' => $entity_type->getLabel()]),
];
}
/**
* {@inheritdoc}
*/
public function validateArgument($argument) {
if ($this->multipleCapable && $this->options['multiple']) {
// At this point only interested in individual IDs no matter what type,
// just splitting by the allowed delimiters.
$names = array_filter(preg_split('/[,+ ]/', $argument));
}
elseif ($argument) {
$names = [$argument];
}
// No specified argument should be invalid.
else {
return FALSE;
}
$accounts = $this->userStorage->loadByProperties(['name' => $names]);
// If there are no accounts, return FALSE now. As we will not enter the
// loop below otherwise.
if (empty($accounts)) {
return FALSE;
}
// Validate each account. If any fails break out and return false.
foreach ($accounts as $account) {
if (!in_array($account->getAccountName(), $names) || !$this->validateEntity($account)) {
return FALSE;
}
}
return TRUE;
}
/**
* {@inheritdoc}
*/
public function processSummaryArguments(&$args) {
// If the validation says the input is a username, we should reverse the
// argument so it works for example for generation summary URLs.
$uids_arg_keys = array_flip($args);
foreach ($this->userStorage->loadMultiple($args) as $uid => $account) {
$args[$uids_arg_keys[$uid]] = $account->label();
}
}
}

View File

@@ -0,0 +1,121 @@
<?php
namespace Drupal\user\Plugin\views\field;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\views\Attribute\ViewsField;
use Drupal\views\Plugin\views\display\DisplayPluginBase;
use Drupal\views\ViewExecutable;
use Drupal\views\Plugin\views\field\PrerenderList;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Field handler to provide a list of permissions.
*
* @ingroup views_field_handlers
*/
#[ViewsField("user_permissions")]
class Permissions extends PrerenderList {
/**
* The role storage.
*
* @var \Drupal\user\RoleStorageInterface
*/
protected $roleStorage;
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* Constructs a \Drupal\user\Plugin\views\field\Permissions object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, ModuleHandlerInterface $module_handler, EntityTypeManagerInterface $entity_type_manager) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->roleStorage = $entity_type_manager->getStorage('user_role');
$this->moduleHandler = $module_handler;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('module_handler'),
$container->get('entity_type.manager')
);
}
/**
* {@inheritdoc}
*/
public function init(ViewExecutable $view, DisplayPluginBase $display, ?array &$options = NULL) {
parent::init($view, $display, $options);
$this->additional_fields['uid'] = ['table' => 'users_field_data', 'field' => 'uid'];
}
public function query() {
$this->addAdditionalFields();
$this->field_alias = $this->aliases['uid'];
}
public function preRender(&$values) {
$this->items = [];
$permission_names = \Drupal::service('user.permissions')->getPermissions();
$rids = [];
foreach ($values as $result) {
$user = $this->getEntity($result);
if ($user) {
$user_rids = $user->getRoles();
$uid = $this->getValue($result);
foreach ($user_rids as $rid) {
$rids[$rid][] = $uid;
}
}
}
if ($rids) {
$roles = $this->roleStorage->loadMultiple(array_keys($rids));
foreach ($rids as $rid => $role_uids) {
foreach ($roles[$rid]->getPermissions() as $permission) {
foreach ($role_uids as $uid) {
$this->items[$uid][$permission]['permission'] = $permission_names[$permission]['title'];
}
}
}
foreach ($this->items as &$permission) {
ksort($permission);
}
}
}
public function render_item($count, $item) {
return $item['permission'];
}
}

View File

@@ -0,0 +1,110 @@
<?php
namespace Drupal\user\Plugin\views\field;
use Drupal\Core\Database\Connection;
use Drupal\views\Attribute\ViewsField;
use Drupal\views\Plugin\views\display\DisplayPluginBase;
use Drupal\views\ViewExecutable;
use Drupal\views\Plugin\views\field\PrerenderList;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\user\Entity\Role;
/**
* Field handler to provide a list of roles.
*
* @ingroup views_field_handlers
*/
#[ViewsField("user_roles")]
class Roles extends PrerenderList {
/**
* Database Service Object.
*
* @var \Drupal\Core\Database\Connection
*/
protected $database;
/**
* Constructs a \Drupal\user\Plugin\views\field\Roles object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Database\Connection $database
* Database Service Object.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, Connection $database) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->database = $database;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static($configuration, $plugin_id, $plugin_definition, $container->get('database'));
}
/**
* {@inheritdoc}
*/
public function init(ViewExecutable $view, DisplayPluginBase $display, ?array &$options = NULL) {
parent::init($view, $display, $options);
$this->additional_fields['uid'] = ['table' => 'users_field_data', 'field' => 'uid'];
}
public function query() {
$this->addAdditionalFields();
$this->field_alias = $this->aliases['uid'];
}
public function preRender(&$values) {
$uids = [];
$this->items = [];
foreach ($values as $result) {
$uids[] = $this->getValue($result);
}
if ($uids) {
$roles = Role::loadMultiple();
$result = $this->database->query('SELECT [u].[entity_id] AS [uid], [u].[roles_target_id] AS [rid] FROM {user__roles} [u] WHERE [u].[entity_id] IN ( :uids[] ) AND [u].[roles_target_id] IN ( :rids[] )', [':uids[]' => $uids, ':rids[]' => array_keys($roles)]);
foreach ($result as $role) {
$this->items[$role->uid][$role->rid]['role'] = $roles[$role->rid]->label();
$this->items[$role->uid][$role->rid]['rid'] = $role->rid;
}
// Sort the roles for each user by role weight.
$ordered_roles = array_flip(array_keys($roles));
foreach ($this->items as &$user_roles) {
// Create an array of rids that the user has in the role weight order.
$sorted_keys = array_intersect_key($ordered_roles, $user_roles);
// Merge with the unsorted array of role information which has the
// effect of sorting it.
$user_roles = array_replace($sorted_keys, $user_roles);
}
}
}
public function render_item($count, $item) {
return $item['role'];
}
protected function documentSelfTokens(&$tokens) {
$tokens['{{ ' . $this->options['id'] . '__role' . ' }}'] = $this->t('The name of the role.');
$tokens['{{ ' . $this->options['id'] . '__rid' . ' }}'] = $this->t('The role machine-name of the role.');
}
protected function addSelfTokens(&$tokens, $item) {
if (!empty($item['role'])) {
$tokens['{{ ' . $this->options['id'] . '__role }}'] = $item['role'];
$tokens['{{ ' . $this->options['id'] . '__rid }}'] = $item['rid'];
}
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace Drupal\user\Plugin\views\field;
use Drupal\Core\Form\FormStateInterface;
use Drupal\user\UserInterface;
use Drupal\views\Attribute\ViewsField;
use Drupal\views\Plugin\views\field\BulkForm;
/**
* Defines a user operations bulk form element.
*/
#[ViewsField("user_bulk_form")]
class UserBulkForm extends BulkForm {
/**
* {@inheritdoc}
*
* Provide a more useful title to improve the accessibility.
*/
public function viewsForm(&$form, FormStateInterface $form_state) {
parent::viewsForm($form, $form_state);
if (!empty($this->view->result)) {
foreach ($this->view->result as $row_index => $result) {
$account = $result->_entity;
if ($account instanceof UserInterface) {
$form[$this->options['id']][$row_index]['#title'] = $this->t('Update the user %name', ['%name' => $account->label()]);
}
}
}
}
/**
* {@inheritdoc}
*/
protected function emptySelectedMessage() {
return $this->t('No users selected.');
}
}

View File

@@ -0,0 +1,119 @@
<?php
namespace Drupal\user\Plugin\views\field;
use Drupal\Core\Extension\ModuleExtensionList;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\views\Attribute\ViewsField;
use Drupal\views\Plugin\views\field\FieldPluginBase;
use Drupal\views\ResultRow;
use Drupal\user\UserDataInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides access to the user data service.
*
* @ingroup views_field_handlers
*
* @see \Drupal\user\UserDataInterface
*/
#[ViewsField("user_data")]
class UserData extends FieldPluginBase {
/**
* Provides the user data service object.
*
* @var \Drupal\user\UserDataInterface
*/
protected $userData;
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('user.data'),
$container->get('module_handler'),
$container->get('extension.list.module')
);
}
/**
* Constructs a UserData object.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, UserDataInterface $user_data, ModuleHandlerInterface $module_handler, protected ?ModuleExtensionList $moduleExtensionList = NULL) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->userData = $user_data;
$this->moduleHandler = $module_handler;
if ($this->moduleExtensionList === NULL) {
@trigger_error('Calling ' . __METHOD__ . '() without the $moduleExtensionList argument is deprecated in drupal:10.3.0 and will be required in drupal:12.0.0. See https://www.drupal.org/node/3310017', E_USER_DEPRECATED);
$this->moduleExtensionList = \Drupal::service('extension.list.module');
}
}
/**
* {@inheritdoc}
*/
protected function defineOptions() {
$options = parent::defineOptions();
$options['data_module'] = ['default' => ''];
$options['data_name'] = ['default' => ''];
return $options;
}
/**
* {@inheritdoc}
*/
public function buildOptionsForm(&$form, FormStateInterface $form_state) {
parent::buildOptionsForm($form, $form_state);
$modules = $this->moduleHandler->getModuleList();
$names = [];
foreach (array_keys($modules) as $name) {
$names[$name] = $this->moduleExtensionList->getName($name);
}
$form['data_module'] = [
'#title' => $this->t('Module name'),
'#type' => 'select',
'#description' => $this->t('The module which sets this user data.'),
'#default_value' => $this->options['data_module'],
'#options' => $names,
];
$form['data_name'] = [
'#title' => $this->t('Name'),
'#type' => 'textfield',
'#description' => $this->t('The name of the data key.'),
'#default_value' => $this->options['data_name'],
];
}
/**
* {@inheritdoc}
*/
public function render(ResultRow $values) {
$uid = $this->getValue($values);
$data = $this->userData->get($this->options['data_module'], $uid, $this->options['data_name']);
// Don't sanitize if no value was found.
if (isset($data)) {
return $this->sanitizeValue($data);
}
}
}

View File

@@ -0,0 +1,57 @@
<?php
namespace Drupal\user\Plugin\views\filter;
use Drupal\views\Attribute\ViewsFilter;
use Drupal\views\Plugin\views\display\DisplayPluginBase;
use Drupal\views\ViewExecutable;
use Drupal\views\Plugin\views\filter\BooleanOperator;
/**
* Filter handler for the current user.
*
* @ingroup views_filter_handlers
*/
#[ViewsFilter("user_current")]
class Current extends BooleanOperator {
/**
* {@inheritdoc}
*/
public function init(ViewExecutable $view, DisplayPluginBase $display, ?array &$options = NULL) {
parent::init($view, $display, $options);
$this->value_value = $this->t('Is the logged in user');
}
public function query() {
$this->ensureMyTable();
$field = $this->tableAlias . '.' . $this->realField . ' ';
$or = $this->view->query->getConnection()->condition('OR');
if (empty($this->value)) {
$or->condition($field, '***CURRENT_USER***', '<>');
if ($this->accept_null) {
$or->isNull($field);
}
}
else {
$or->condition($field, '***CURRENT_USER***', '=');
}
$this->query->addWhere($this->options['group'], $or);
}
/**
* {@inheritdoc}
*/
public function getCacheContexts() {
$contexts = parent::getCacheContexts();
// This filter depends on the current user.
$contexts[] = 'user';
return $contexts;
}
}

View File

@@ -0,0 +1,133 @@
<?php
namespace Drupal\user\Plugin\views\filter;
use Drupal\Core\Entity\Element\EntityAutocomplete;
use Drupal\Core\Form\FormStateInterface;
use Drupal\user\Entity\User;
use Drupal\views\Attribute\ViewsFilter;
use Drupal\views\Plugin\views\filter\InOperator;
/**
* Filter handler for usernames.
*
* @ingroup views_filter_handlers
*/
#[ViewsFilter("user_name")]
class Name extends InOperator {
protected $alwaysMultiple = TRUE;
/**
* The validated exposed input.
*/
// phpcs:ignore Drupal.NamingConventions.ValidVariableName.LowerCamelName
protected array $validated_exposed_input;
protected function valueForm(&$form, FormStateInterface $form_state) {
$users = $this->value ? User::loadMultiple($this->value) : [];
$default_value = EntityAutocomplete::getEntityLabels($users);
$form['value'] = [
'#type' => 'entity_autocomplete',
'#title' => $this->t('Usernames'),
'#description' => $this->t('Enter a comma separated list of user names.'),
'#target_type' => 'user',
'#tags' => TRUE,
'#default_value' => $default_value,
'#process_default_value' => $this->isExposed(),
];
$user_input = $form_state->getUserInput();
if ($form_state->get('exposed') && !isset($user_input[$this->options['expose']['identifier']])) {
$user_input[$this->options['expose']['identifier']] = $default_value;
$form_state->setUserInput($user_input);
}
}
protected function valueValidate($form, FormStateInterface $form_state) {
$uids = [];
if ($values = $form_state->getValue(['options', 'value'])) {
foreach ($values as $value) {
$uids[] = $value['target_id'];
}
sort($uids);
}
$form_state->setValue(['options', 'value'], $uids);
}
public function acceptExposedInput($input) {
$rc = parent::acceptExposedInput($input);
if ($rc) {
// If we have previously validated input, override.
if (isset($this->validated_exposed_input)) {
$this->value = $this->validated_exposed_input;
}
}
return $rc;
}
public function validateExposed(&$form, FormStateInterface $form_state) {
if (empty($this->options['exposed'])) {
return;
}
if (empty($this->options['expose']['identifier'])) {
return;
}
$identifier = $this->options['expose']['identifier'];
$input = $form_state->getValue($identifier);
if ($this->options['is_grouped'] && isset($this->options['group_info']['group_items'][$input])) {
$this->operator = $this->options['group_info']['group_items'][$input]['operator'];
$input = $this->options['group_info']['group_items'][$input]['value'];
}
$uids = [];
$values = $form_state->getValue($identifier);
if ($values && (!$this->options['is_grouped'] || ($this->options['is_grouped'] && ($input != 'All')))) {
foreach ($values as $value) {
$uids[] = $value['target_id'];
}
}
if ($uids) {
$this->validated_exposed_input = $uids;
}
}
protected function valueSubmit($form, FormStateInterface $form_state) {
// prevent array filter from removing our anonymous user.
}
/**
* {@inheritdoc}
*/
public function getValueOptions() {
return $this->valueOptions;
}
public function adminSummary() {
// set up $this->valueOptions for the parent summary
$this->valueOptions = [];
if ($this->value) {
$result = \Drupal::entityTypeManager()->getStorage('user')
->loadByProperties(['uid' => $this->value]);
foreach ($result as $account) {
if ($account->id()) {
$this->valueOptions[$account->id()] = $account->label();
}
else {
// Intentionally NOT translated.
$this->valueOptions[$account->id()] = 'Anonymous';
}
}
}
return parent::adminSummary();
}
}

View File

@@ -0,0 +1,119 @@
<?php
namespace Drupal\user\Plugin\views\filter;
use Drupal\Component\Utility\Html;
use Drupal\Core\DependencyInjection\DeprecatedServicePropertyTrait;
use Drupal\Core\Extension\ModuleExtensionList;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\user\Entity\Role;
use Drupal\user\PermissionHandlerInterface;
use Drupal\user\RoleInterface;
use Drupal\views\Attribute\ViewsFilter;
use Drupal\views\Plugin\views\filter\ManyToOne;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Filter handler for user roles.
*
* @ingroup views_filter_handlers
*/
#[ViewsFilter("user_permissions")]
class Permissions extends ManyToOne {
use DeprecatedServicePropertyTrait;
/**
* The service properties that should raise a deprecation error.
*/
private array $deprecatedProperties = ['moduleHandler' => 'module_handler'];
/**
* The permission handler.
*
* @var \Drupal\user\PermissionHandlerInterface
*/
protected $permissionHandler;
/**
* Module extension list.
*/
protected ModuleExtensionList $moduleExtensionList;
/**
* Constructs a Permissions object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\user\PermissionHandlerInterface $permission_handler
* The permission handler.
* @param \Drupal\Core\Extension\ModuleExtensionList|\Drupal\Core\Extension\ModuleHandlerInterface $module_extension_list
* The module extension list.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, PermissionHandlerInterface $permission_handler, ModuleExtensionList|ModuleHandlerInterface $module_extension_list) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->permissionHandler = $permission_handler;
if ($module_extension_list instanceof ModuleHandlerInterface) {
@trigger_error('Calling ' . __METHOD__ . '() with the $module_extension_list argument as ModuleHandlerInterface is deprecated in drupal:10.3.0 and will be required in drupal:12.0.0. See https://www.drupal.org/node/3310017', E_USER_DEPRECATED);
$module_extension_list = \Drupal::service('extension.list.module');
}
$this->moduleExtensionList = $module_extension_list;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('user.permissions'),
$container->get('extension.list.module'),
);
}
public function getValueOptions() {
if (!isset($this->valueOptions)) {
$permissions = $this->permissionHandler->getPermissions();
foreach ($permissions as $perm => $perm_item) {
$provider = $perm_item['provider'];
$display_name = $this->moduleExtensionList->getName($provider);
$this->valueOptions[$display_name][$perm] = Html::escape(strip_tags($perm_item['title']));
}
return $this->valueOptions;
}
else {
return $this->valueOptions;
}
}
/**
* {@inheritdoc}
*
* Replace the configured permission with a filter by all roles that have this
* permission.
*/
public function query() {
$rids = [];
$all_roles = Role::loadMultiple();
// Get all role IDs that have the configured permissions.
foreach ($this->value as $permission) {
$roles = array_filter($all_roles, fn(RoleInterface $role) => $role->hasPermission($permission));
// Method Role::loadMultiple() returns an array with the role IDs as keys,
// so take the array keys and merge them with previously found role IDs.
$rids = array_merge($rids, array_keys($roles));
}
// Remove any duplicate role IDs.
$rids = array_unique($rids);
$this->value = $rids;
// $this->value contains the role IDs that have the configured permission.
parent::query();
}
}

View File

@@ -0,0 +1,119 @@
<?php
namespace Drupal\user\Plugin\views\filter;
use Drupal\user\RoleInterface;
use Drupal\user\RoleStorageInterface;
use Drupal\views\Attribute\ViewsFilter;
use Drupal\views\Plugin\views\filter\ManyToOne;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Filter handler for user roles.
*
* @ingroup views_filter_handlers
*/
#[ViewsFilter("user_roles")]
class Roles extends ManyToOne {
/**
* Constructs a Roles object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\user\RoleStorageInterface $roleStorage
* The role storage.
* @param \Psr\Log\LoggerInterface|null $logger
* The logger service.
*/
public function __construct(
array $configuration,
$plugin_id,
$plugin_definition,
protected readonly RoleStorageInterface $roleStorage,
protected ?LoggerInterface $logger,
) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
if (!$logger) {
@trigger_error('Calling ' . __METHOD__ . '() without the $logger argument is deprecated in drupal:10.3.0 and it will be required in drupal:11.0.0. See https://www.drupal.org/node/3427368', E_USER_DEPRECATED);
$this->logger = \Drupal::service('logger.channel.default');
}
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('entity_type.manager')->getStorage('user_role'),
$container->get('logger.channel.default'),
);
}
/**
* {@inheritdoc}
*/
public function getValueOptions() {
if (!isset($this->valueOptions)) {
$roles = $this->roleStorage->loadMultiple();
unset($roles[RoleInterface::ANONYMOUS_ID]);
unset($roles[RoleInterface::AUTHENTICATED_ID]);
$this->valueOptions = array_map(fn(RoleInterface $role) => $role->label(), $roles);
}
return $this->valueOptions;
}
/**
* Override empty and not empty operator labels to be clearer for user roles.
*
* @return array[]
*/
public function operators() {
$operators = parent::operators();
$operators['empty']['title'] = $this->t("Only has the 'authenticated user' role");
$operators['not empty']['title'] = $this->t("Has roles in addition to 'authenticated user'");
return $operators;
}
/**
* {@inheritdoc}
*/
public function calculateDependencies() {
$dependencies = [];
if (in_array($this->operator, ['empty', 'not empty'])) {
return $dependencies;
}
// The value might be a string due to the wrong plugin being used for role
// field data, and subsequently the incorrect config schema object and
// value. In the empty case stop early. Otherwise we cast it to an array
// later.
if (is_string($this->value) && $this->value === '') {
return [];
}
foreach ((array) $this->value as $role_id) {
if ($role = $this->roleStorage->load($role_id)) {
$dependencies[$role->getConfigDependencyKey()][] = $role->getConfigDependencyName();
}
else {
$this->logger->warning("View %view depends on role %role, but the role does not exist.", [
'%view' => $this->view->id(),
'%role' => $role_id,
]);
}
}
return $dependencies;
}
}

Some files were not shown because too many files have changed in this diff Show More