diff --git a/core/modules/user/src/AccountForm.php b/core/modules/user/src/AccountForm.php index 3624a9c..fb82918 100644 --- a/core/modules/user/src/AccountForm.php +++ b/core/modules/user/src/AccountForm.php @@ -68,8 +68,19 @@ public function form(array $form, FormStateInterface $form_state) { $form['#cache']['tags'] = $config->getCacheTags(); $language_interface = \Drupal::languageManager()->getCurrentLanguage(); + + // Check for new account. $register = $account->isAnonymous(); - $admin = $user->hasPermission('administer users'); + + // 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'] = [ @@ -78,14 +89,14 @@ public function form(array $form, FormStateInterface $form_state) { ]; // The mail field is NOT required if account originally had no mail set - // and the user performing the edit has 'administer users' permission. + // and the user performing the edit has appropriate 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('A valid email address. All emails from the system will be sent to this address. The email address is not made public and will only be used if you wish to receive a new password or wish to receive certain news or notifications by email.'), - '#required' => !(!$account->getEmail() && $admin), + '#required' => $account->getEmail() || user_mail_required($user), '#default_value' => (!$register ? $account->getEmail() : ''), ]; @@ -103,7 +114,7 @@ public function form(array $form, FormStateInterface $form_state) { 'spellcheck' => 'false', ], '#default_value' => (!$register ? $account->getAccountName() : ''), - '#access' => ($register || ($user->id() == $account->id() && $user->hasPermission('change own username')) || $admin), + '#access' => $account->name->access('edit'), ]; // Display password field only for existing users or when user is allowed to @@ -150,7 +161,7 @@ public function form(array $form, FormStateInterface $form_state) { } } } - elseif (!$config->get('verify_mail') || $admin) { + elseif (!$config->get('verify_mail') || $admin_create) { $form['account']['pass'] = [ '#type' => 'password_confirm', '#size' => 25, @@ -169,7 +180,7 @@ public function form(array $form, FormStateInterface $form_state) { } } - if ($admin || !$register) { + if (!$self_register) { $status = $account->get('status')->value; } else { @@ -181,7 +192,7 @@ public function form(array $form, FormStateInterface $form_state) { '#title' => $this->t('Status'), '#default_value' => $status, '#options' => [$this->t('Blocked'), $this->t('Active')], - '#access' => $admin, + '#access' => $account->status->access('edit'), ]; $roles = array_map(['\Drupal\Component\Utility\Html', 'escape'], user_role_names(TRUE)); @@ -203,7 +214,7 @@ public function form(array $form, FormStateInterface $form_state) { $form['account']['notify'] = [ '#type' => 'checkbox', '#title' => $this->t('Notify user of new account'), - '#access' => $register && $admin, + '#access' => $admin_create, ]; $user_preferred_langcode = $register ? $language_interface->getId() : $account->getPreferredLangcode(); @@ -222,7 +233,7 @@ public function form(array $form, FormStateInterface $form_state) { '#open' => TRUE, // Display language selector when either creating a user on the admin // interface or editing a user account. - '#access' => !$register || $admin, + '#access' => !$self_register, ]; $form['language']['preferred_langcode'] = [ diff --git a/core/modules/user/src/Form/UserCancelForm.php b/core/modules/user/src/Form/UserCancelForm.php index a1fd5f7..a7d0de1 100644 --- a/core/modules/user/src/Form/UserCancelForm.php +++ b/core/modules/user/src/Form/UserCancelForm.php @@ -20,6 +20,13 @@ class UserCancelForm extends ContentEntityConfirmFormBase { protected $cancelMethods; /** + * Whether allowed to select cancellation method. + * + * @var boolean + */ + protected $selectCancel; + + /** * The user being cancelled. * * @var \Drupal\user\UserInterface @@ -49,14 +56,19 @@ public function getCancelUrl() { public function getDescription() { $description = ''; $default_method = $this->config('user.settings')->get('cancel_method'); - if ($this->currentUser()->hasPermission('administer users') || $this->currentUser()->hasPermission('select account cancellation method')) { + $own_account = $this->entity->id() == $this->currentUser()->id(); + if ($this->selectCancel) { $description = $this->t('Select the method to cancel the account above.'); } // Options supplied via user_cancel_methods() can have a custom // #confirm_description property for the confirmation form description. - elseif (isset($this->cancelMethods[$default_method]['#confirm_description'])) { + // This text refers to "Your account" so only user it if cancelling own account. + elseif ($own_account && isset($this->cancelMethods[$default_method]['#confirm_description'])) { $description = $this->cancelMethods[$default_method]['#confirm_description']; } + else { + $description = $this->cancelMethods['#options'][$default_method]; + } return $description . ' ' . $this->t('This action cannot be undone.'); } @@ -75,18 +87,19 @@ public function buildForm(array $form, FormStateInterface $form_state) { $this->cancelMethods = user_cancel_methods(); // Display account cancellation method selection, if allowed. - $admin_access = $user->hasPermission('administer users'); + $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' => ($this->entity->id() == $user->id() ? $this->t('When cancelling your account') : $this->t('When cancelling the account')), - '#access' => $admin_access || $user->hasPermission('select account cancellation method'), + '#title' => $own_account ? $this->t('When cancelling your account') : $this->t('When cancelling the account'), + '#access' => $this->selectCancel, ]; $form['user_cancel_method'] += $this->cancelMethods; - // Allow user administrators to skip the account cancellation confirmation - // mail (by default), as long as they do not attempt to cancel their own - // account. - $override_access = $admin_access && ($this->entity->id() != $user->id()); + // 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 to cancel account'), @@ -111,7 +124,7 @@ public function buildForm(array $form, FormStateInterface $form_state) { // if desired. $form['access'] = [ '#type' => 'value', - '#value' => $user->hasPermission('administer users'), + '#value' => !$own_account, ]; $form = parent::buildForm($form, $form_state); diff --git a/core/modules/user/src/Form/UserMultipleCancelConfirm.php b/core/modules/user/src/Form/UserMultipleCancelConfirm.php index b858d6a..c0fa97c 100644 --- a/core/modules/user/src/Form/UserMultipleCancelConfirm.php +++ b/core/modules/user/src/Form/UserMultipleCancelConfirm.php @@ -142,13 +142,28 @@ public function buildForm(array $form, FormStateInterface $form_state) { $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('When cancelling these accounts'), + '#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', diff --git a/core/modules/user/src/Plugin/Validation/Constraint/UserMailRequiredValidator.php b/core/modules/user/src/Plugin/Validation/Constraint/UserMailRequiredValidator.php index 7e20b7a..3fc9368 100644 --- a/core/modules/user/src/Plugin/Validation/Constraint/UserMailRequiredValidator.php +++ b/core/modules/user/src/Plugin/Validation/Constraint/UserMailRequiredValidator.php @@ -9,7 +9,7 @@ * 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. + * and the user performing the edit has appropriate permission. * This allows users without email address to be edited and deleted. */ class UserMailRequiredValidator extends ConstraintValidator { @@ -29,7 +29,7 @@ public function validate($items, Constraint $constraint) { $existing_value = $account_unchanged->getEmail(); } - $required = !(!$existing_value && \Drupal::currentUser()->hasPermission('administer users')); + $required = $existing_value || user_mail_required(\Drupal::currentUser()); if ($required && (!isset($items) || $items->isEmpty())) { $this->context->addViolation($constraint->message, ['@name' => $account->getFieldDefinition('mail')->getLabel()]); diff --git a/core/modules/user/src/ProfileForm.php b/core/modules/user/src/ProfileForm.php index 137868c..a1e1550 100644 --- a/core/modules/user/src/ProfileForm.php +++ b/core/modules/user/src/ProfileForm.php @@ -20,12 +20,10 @@ protected function actions(array $form, FormStateInterface $form_state) { // The user account being edited. $account = $this->entity; - // The user doing the editing. - $user = $this->currentUser(); $element['delete']['#type'] = 'submit'; $element['delete']['#value'] = $this->t('Cancel account'); $element['delete']['#submit'] = ['::editCancelSubmit']; - $element['delete']['#access'] = $account->id() > 1 && (($account->id() == $user->id() && $user->hasPermission('cancel account')) || $user->hasPermission('administer users')); + $element['delete']['#access'] = $account->id() > 1 && $account->access('delete'); return $element; } diff --git a/core/modules/user/src/RegisterForm.php b/core/modules/user/src/RegisterForm.php index 18760f4..3ffa88a 100644 --- a/core/modules/user/src/RegisterForm.php +++ b/core/modules/user/src/RegisterForm.php @@ -18,7 +18,14 @@ public function form(array $form, FormStateInterface $form_state) { $user = $this->currentUser(); /** @var \Drupal\user\UserInterface $account */ $account = $this->entity; - $admin = $user->hasPermission('administer users'); + + // This form is used for two cases: + // - Self-register (route = 'user.register'). + // - Admin-create (route = 'user.admin_create'). + // If the current user has permission to create users then it must be the + // second case. + $admin = $account->access('create'); + // Pass access information to the submit handler. Running an access check // inside the submit function interferes with form processing and breaks // hook_form_alter(). diff --git a/core/modules/user/src/Tests/UserSubAdminTest.php b/core/modules/user/src/Tests/UserSubAdminTest.php new file mode 100644 index 0000000..1102a32 --- /dev/null +++ b/core/modules/user/src/Tests/UserSubAdminTest.php @@ -0,0 +1,66 @@ +drupalCreateUser(['sub-admin']); + $this->drupalLogin($user); + + // Test that the create user page has admin fields. + $this->drupalGet('admin/people/create'); + $this->assertField("edit-name", "Name field exists."); + $this->assertField("edit-notify", "Notify field exists."); + + // Not 'status' or 'roles' as they require extra permission. + $this->assertNoField("edit-status-0", "Status field missing."); + $this->assertNoField("edit-role", "Role field missing."); + + // Test that create user gives an admin style message. + $edit = [ + 'name' => $this->randomMachineName(), + 'mail' => $this->randomMachineName() . '@example.com', + 'pass[pass1]' => $pass = $this->randomString(), + 'pass[pass2]' => $pass, + 'notify' => FALSE, + ]; + $this->drupalPostForm('admin/people/create', $edit, t('Create new account')); + $this->assertText(t('Created a new user account for @name. No email has been sent.', ['@name' => $edit['name']]), 'User created'); + + // Test that the cancel user page has admin fields. + $cancel_user = $this->createUser(); + $this->drupalGet('user/' . $cancel_user->id() . '/cancel'); + $this->assertRaw(t('Are you sure you want to cancel the account %name?', ['%name' => $cancel_user->getUsername()]), 'Confirmation form to cancel account displayed.'); + $this->assertRaw(t('Disable the account and keep its content.') . ' ' . t('This action cannot be undone.'), 'Cannot select account cancellation method.'); + + // Test that cancel confirmation gives an admin style message. + $this->drupalPostForm(NULL, NULL, t('Cancel account')); + $this->assertRaw(t('%name has been disabled.', ['%name' => $cancel_user->getUsername()]), "Confirmation message displayed to user."); + + // Repeat with permission to select account cancellation method. + $user->addRole($this->drupalCreateRole(['select account cancellation method'])); + $user->save(); + $cancel_user = $this->createUser(); + $this->drupalGet('user/' . $cancel_user->id() . '/cancel'); + $this->assertText(t('Select the method to cancel the account above.'), 'Allows to select account cancellation method.'); + } + +} diff --git a/core/modules/user/src/UserAccessControlHandler.php b/core/modules/user/src/UserAccessControlHandler.php index 19fe6b6..1a2042e 100644 --- a/core/modules/user/src/UserAccessControlHandler.php +++ b/core/modules/user/src/UserAccessControlHandler.php @@ -93,11 +93,9 @@ protected function checkFieldAccess($operation, FieldDefinitionInterface $field_ $is_own_account = $items ? $items->getEntity()->id() == $account->id() : FALSE; switch ($field_definition->getName()) { case 'name': - // Allow view access to anyone with access to the entity. Anonymous - // users should be able to access the username field during the - // registration process, otherwise the username and email constraints - // are not checked. - if ($operation == 'view' || ($items && $account->isAnonymous() && $items->getEntity()->isAnonymous())) { + // Allow view access to anyone with access to the entity. + // The username field is editable during the registration process. + if ($operation == 'view' || ($items && $items->getEntity()->isAnonymous())) { return AccessResult::allowed()->cachePerPermissions(); } // Allow edit access for the own user name if the permission is diff --git a/core/modules/user/tests/modules/user_access_test/user_access_test.module b/core/modules/user/tests/modules/user_access_test/user_access_test.module index ff9f0b2..bc951ea 100644 --- a/core/modules/user/tests/modules/user_access_test/user_access_test.module +++ b/core/modules/user/tests/modules/user_access_test/user_access_test.module @@ -23,18 +23,41 @@ function user_access_test_user_access(User $entity, $operation, $account) { // Deny delete access. return AccessResult::forbidden(); } + + // Account with role sub-admin can manage users with no roles. + if (count($entity->getRoles()) == 1) { + return AccessResult::allowedIfHasPermission($account, 'sub-admin'); + } + return AccessResult::neutral(); } /** + * Implements hook_entity_create_access(). + */ +function user_access_test_entity_create_access(AccountInterface $account, array $context, $entity_bundle) { + if ($context['entity_type_id'] != 'user') { + return AccessResult::neutral(); + } + + // Account with role sub-admin can create users. + return AccessResult::allowedIfHasPermission($account, 'sub-admin'); +} + +/** * Implements hook_entity_field_access(). */ function user_access_test_entity_field_access($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, FieldItemListInterface $items = NULL) { + if ($field_definition->getTargetEntityTypeId() != 'user') { + return AccessResult::neutral(); + } + // Account with role sub-admin can view the status, init and mail fields for user with no roles. if ($operation === 'view' && in_array($field_definition->getName(), ['status', 'init', 'mail'])) { if (($items == NULL) || (count($items->getEntity()->getRoles()) == 1)) { return AccessResult::allowedIfHasPermission($account, 'sub-admin'); } } + return AccessResult::neutral(); } diff --git a/core/modules/user/user.api.php b/core/modules/user/user.api.php index 823c08d..8980e60 100644 --- a/core/modules/user/user.api.php +++ b/core/modules/user/user.api.php @@ -128,6 +128,28 @@ function hook_user_format_name_alter(&$name, $account) { } /** + * Alter whether the user mail field is required. + * + * By default a user with permission 'administer users' may create and edit + * a user account without setting the mail field, but a user without that + * permission must set the mail field. This hook can be user to allow + * other users to omit the mail field, or to require the mail field for + * all users. + * + * @param boolean $required + * True if the mail field is required. + * + * @param \Drupal\user\UserInterface $user + * User editing the account. + */ +function hook_user_mail_required_alter(&$required, $user) { + // Check for a new permission that is defined in custom code. + if ($user->hasPermission('allow empty user mail')) { + $required = FALSE; + } +} + +/** * The user just logged in. * * @param object $account diff --git a/core/modules/user/user.module b/core/modules/user/user.module index a2b6464..2316559 100644 --- a/core/modules/user/user.module +++ b/core/modules/user/user.module @@ -1422,3 +1422,18 @@ function template_preprocess_user(&$variables) { $variables['content'][$key] = $variables['elements'][$key]; } } + +/** + * Determine if the user account 'mail' field is required. + * + * @param \Drupal\user\UserInterface $user + * User editing the account. + * + * @return boolean True if the mail field is required. + */ +function user_mail_required(AccountInterface $user) { + $required = !$user->hasPermission('administer users'); + \Drupal::moduleHandler()->alter('user_mail_required', $required, $user); + + return $required; +} diff --git a/core/modules/user/user.permissions.yml b/core/modules/user/user.permissions.yml index 810583c..a295b1f 100644 --- a/core/modules/user/user.permissions.yml +++ b/core/modules/user/user.permissions.yml @@ -14,7 +14,7 @@ access user profiles: change own username: title: 'Change own username' select account cancellation method: - title: 'Select method for cancelling own account' + title: 'Select method for cancelling account' restrict access: true cancel account: title: 'Cancel own user account' diff --git a/core/modules/user/user.routing.yml b/core/modules/user/user.routing.yml index e38bb7a..bfacec2 100644 --- a/core/modules/user/user.routing.yml +++ b/core/modules/user/user.routing.yml @@ -43,7 +43,7 @@ user.admin_create: _entity_form: 'user.register' _title: 'Add user' requirements: - _permission: 'administer users' + _entity_create_access: 'user' user.admin_permissions: path: '/admin/people/permissions'