diff --git a/core/lib/Drupal/Core/Render/Element/PasswordConfirm.php b/core/lib/Drupal/Core/Render/Element/PasswordConfirm.php index 0d5e0e2..df2fd2a 100644 --- a/core/lib/Drupal/Core/Render/Element/PasswordConfirm.php +++ b/core/lib/Drupal/Core/Render/Element/PasswordConfirm.php @@ -18,6 +18,7 @@ * $form['pass'] = array( * '#type' => 'password_confirm', * '#title' => $this->t('Password'), + * '#title_confirm' => $this->t('Confirm password'), * '#size' => 25, * ); * @endcode @@ -70,7 +71,7 @@ public static function valueCallback(&$element, $input, FormStateInterface $form public static function processPasswordConfirm(&$element, FormStateInterface $form_state, &$complete_form) { $element['pass1'] = array( '#type' => 'password', - '#title' => t('Password'), + '#title' => !empty($element['#title']) ? $element['#title'] : t('Password'), '#value' => empty($element['#value']) ? NULL : $element['#value']['pass1'], '#required' => $element['#required'], '#attributes' => array('class' => array('password-field', 'js-password-field')), @@ -78,12 +79,21 @@ public static function processPasswordConfirm(&$element, FormStateInterface $for ); $element['pass2'] = array( '#type' => 'password', - '#title' => t('Confirm password'), + '#title' => !empty($element['#title_confirm']) ? $element['#title_confirm'] : t('Confirm password'), '#value' => empty($element['#value']) ? NULL : $element['#value']['pass2'], '#required' => $element['#required'], '#attributes' => array('class' => array('password-confirm', 'js-password-confirm')), '#error_no_message' => TRUE, + '#states' => array( + 'visible' => array( + ':input[name="pass[pass1]"]' => ['filled' => TRUE], + ), + 'required' => array( + ':input[name="pass[pass1]"]' => ['filled' => TRUE], + ), + ), ); + $element['#title'] = FALSE; $element['#element_validate'] = array(array(get_called_class(), 'validatePasswordConfirm')); $element['#tree'] = TRUE; diff --git a/core/modules/user/src/AccountForm.php b/core/modules/user/src/AccountForm.php index 05257fd..79abc74 100644 --- a/core/modules/user/src/AccountForm.php +++ b/core/modules/user/src/AccountForm.php @@ -89,18 +89,6 @@ public function form(array $form, FormStateInterface $form_state) { '#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'] = array( - '#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() && $user->hasPermission('administer users')), - '#default_value' => (!$register ? $account->getEmail() : ''), - ); - // Only show name field on registration form or user can change own username. $form['account']['name'] = array( '#type' => 'textfield', @@ -116,25 +104,25 @@ public function form(array $form, FormStateInterface $form_state) { ), '#default_value' => (!$register ? $account->getAccountName() : ''), '#access' => ($register || ($user->id() == $account->id() && $user->hasPermission('change own username')) || $admin), + '#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'] = array( + '#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. In case you want to change this email address you also need to provide your current password.'), + '#required' => !(!$account->getEmail() && $user->hasPermission('administer users')), + '#default_value' => (!$register ? $account->getEmail() : ''), + '#weight' => -9, ); // Display password field only for existing users or when user is allowed to // assign a password during registration. if (!$register) { - $form['account']['pass'] = array( - '#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. - if (!$form_state->get('user_pass_reset') && ($token = $this->getRequest()->get('pass-reset-token'))) { - $session_key = 'pass_reset_' . $account->id(); - $user_pass_reset = isset($_SESSION[$session_key]) && Crypt::hashEquals($_SESSION[$session_key], $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()) { @@ -143,7 +131,7 @@ public function form(array $form, FormStateInterface $form_state) { '#title' => $this->t('Current password'), '#size' => 25, '#access' => !$form_state->get('user_pass_reset'), - '#weight' => -5, + '#weight' => -8, // 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. @@ -154,13 +142,44 @@ public function form(array $form, FormStateInterface $form_state) { // 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. Reset your password.', array( + $form['account']['current_pass']['#description'] = $this->t('Confirm your current password to change the %mail or to set a %pass. Reset your password.', array( '%mail' => $form['account']['mail']['#title'], - '%pass' => $this->t('Password'), + '%pass' => $this->t('New password'), ':request_new_url' => $this->url('user.pass'), )); + $form['account']['current_pass']['#states'] = [ + // Only show this field when mail or new password has changed. + 'visible' => [ + [':input[name="mail"]' => ['!value' => $account->getEmail()]], + [':input[name="pass[pass1]"]' => ['filled' => TRUE]], + ], + // Mark the field as required if mail or new password has changed. + 'required' => [ + [':input[name="mail"]' => ['!value' => $account->getEmail()]], + [':input[name="pass[pass1]"]' => ['filled' => TRUE]], + ], + ]; } } + + $form['account']['pass'] = array( + '#type' => 'password_confirm', + '#prefix' => '

' . $this->t('Change password') . '

', + '#title' => $this->t('New password'), + '#title_confirm' => $this->t('Confirm new password'), + '#size' => 25, + '#description' => $this->t('To change the current user password, enter the new password here. In that case you also need to provide your current password.'), + '#weight' => -7, + ); + + // 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. + if (!$form_state->get('user_pass_reset') && ($token = $this->getRequest()->get('pass-reset-token'))) { + $session_key = 'pass_reset_' . $account->id(); + $user_pass_reset = isset($_SESSION[$session_key]) && Crypt::hashEquals($_SESSION[$session_key], $token); + $form_state->set('user_pass_reset', $user_pass_reset); + } } elseif (!$config->get('verify_mail') || $admin) { $form['account']['pass'] = array( diff --git a/core/modules/user/src/Tests/UserEditTest.php b/core/modules/user/src/Tests/UserEditTest.php index 5b34d2e..19a38a2 100644 --- a/core/modules/user/src/Tests/UserEditTest.php +++ b/core/modules/user/src/Tests/UserEditTest.php @@ -49,7 +49,7 @@ function testUserEdit() { $edit = array(); $edit['mail'] = $this->randomMachineName() . '@new.example.com'; $this->drupalPostForm("user/" . $user1->id() . "/edit", $edit, t('Save')); - $this->assertRaw(t("Your current password is missing or incorrect; it's required to change the %name.", array('%name' => t('Email')))); + $this->assertRaw(t("Your current password is missing or incorrect; it's required to change the %name.", array('%name' => 'Email'))); $edit['current_pass'] = $user1->pass_raw; $this->drupalPostForm("user/" . $user1->id() . "/edit", $edit, t('Save')); @@ -60,7 +60,7 @@ function testUserEdit() { $edit['pass[pass1]'] = $new_pass = $this->randomMachineName(); $edit['pass[pass2]'] = $new_pass; $this->drupalPostForm("user/" . $user1->id() . "/edit", $edit, t('Save')); - $this->assertRaw(t("Your current password is missing or incorrect; it's required to change the %name.", array('%name' => t('Password')))); + $this->assertRaw(t("Your current password is missing or incorrect; it's required to change the %name.", array('%name' => 'Password'))); // Try again with the current password. $edit['current_pass'] = $user1->pass_raw; diff --git a/core/modules/user/src/Tests/UserPasswordResetTest.php b/core/modules/user/src/Tests/UserPasswordResetTest.php index f3463bf..df285e0 100644 --- a/core/modules/user/src/Tests/UserPasswordResetTest.php +++ b/core/modules/user/src/Tests/UserPasswordResetTest.php @@ -130,7 +130,7 @@ function testUserPasswordReset() { // Verify that the password reset session has been destroyed. $this->drupalPostForm(NULL, $edit, t('Save')); - $this->assertText(t('Your current password is missing or incorrect; it\'s required to change the Password.'), 'Password needed to make profile changes.'); + $this->assertRaw(t("Your current password is missing or incorrect; it's required to change the %name.", array('%name' => 'Password')), 'Password needed to make profile changes.'); // Log out, and try to log in again using the same one-time link. $this->drupalLogout(); diff --git a/core/modules/user/tests/src/Kernel/UserAccountFormFieldsTest.php b/core/modules/user/tests/src/Kernel/UserAccountFormFieldsTest.php index 4022972..9523660 100644 --- a/core/modules/user/tests/src/Kernel/UserAccountFormFieldsTest.php +++ b/core/modules/user/tests/src/Kernel/UserAccountFormFieldsTest.php @@ -33,7 +33,7 @@ function testInstallConfigureForm() { ->buildForm('Drupal\Core\Installer\Form\SiteConfigureForm', $form_state); // Verify name and pass field order. - $this->assertFieldOrder($form['admin_account']['account']); + $this->assertSiteConfigureFormFieldOrder($form['admin_account']['account']); // Verify that web browsers may autocomplete the email value and // autofill/prefill the name and pass values. @@ -56,8 +56,8 @@ function testUserRegistrationForm() { $form = $this->buildAccountForm('register'); - // Verify name and pass field order. - $this->assertFieldOrder($form['account']); + // Verify name, email and pass field order. + $this->assertAccountFieldOrder($form['account']); // Verify that web browsers may autocomplete the email value and // autofill/prefill the name and pass values. @@ -78,8 +78,8 @@ function testUserEditForm() { $form = $this->buildAccountForm('default'); - // Verify name and pass field order. - $this->assertFieldOrder($form['account']); + // Verify name, mail and pass field order. + $this->assertAccountFieldOrder($form['account']); // Verify that autocomplete is off on all account fields. foreach (array('mail', 'name', 'pass') as $key) { @@ -93,7 +93,7 @@ function testUserEditForm() { * @param array $elements * A form array section that contains the user account form elements. */ - protected function assertFieldOrder(array $elements) { + protected function assertSiteConfigureFormFieldOrder(array $elements) { $name_index = 0; $name_weight = 0; $pass_index = 0; @@ -112,11 +112,56 @@ protected function assertFieldOrder(array $elements) { } $index++; } - $this->assertEqual($name_index, $pass_index - 1, "'name' field ($name_index) appears before 'pass' field ($pass_index)."); + $this->assertEquals($name_index, $pass_index - 1, "'name' field ($name_index) appears before 'pass' field ($pass_index)."); $this->assertTrue($name_weight < $pass_weight, "'name' field weight ($name_weight) is smaller than 'pass' field weight ($pass_weight)."); } /** + * Asserts the correct order of name, mail and pass fields. + * + * @param array $elements + * A form array section that contains the user account form elements. + */ + protected function assertAccountFieldOrder(array $elements) { + $name_index = 0; + $name_weight = 0; + $mail_index = 0; + $mail_weight = 0; + $pass_index = 0; + $pass_weight = 0; + $index = 0; + + foreach ($elements as $key => $element) { + switch ($key) { + case 'name': + $name_index = $index; + $name_weight = $element['#weight']; + $this->assertTrue($element['#sorted'], "'name' field is #sorted."); + break; + + case 'mail': + $mail_index = $index; + $mail_weight = $element['#weight']; + $this->assertTrue($element['#sorted'], "'mail' field is #sorted."); + break; + + case 'pass': + $pass_index = $index; + $pass_weight = $element['#weight']; + $this->assertTrue($element['#sorted'], "'pass' field is #sorted."); + break; + } + $index++; + } + + $this->assertEquals($name_index, $mail_index - 1, "'name' field ($name_index) appears before 'mail' field ($mail_index)."); + $this->assertEquals($mail_index, $pass_index - 1, "'mail' field ($mail_index) appears before 'pass' field ($pass_index)."); + $this->assertTrue($name_weight < $mail_weight, "'name' field weight ($name_weight) is smaller than 'mail' field weight ($mail_weight)."); + $this->assertTrue($mail_weight < $pass_weight, "'mail' field weight ($mail_weight) is smaller than 'pass' field weight ($pass_weight)."); + } + + + /** * Builds the user account form for a given operation. * * @param string $operation