diff --git a/core/core.libraries.yml b/core/core.libraries.yml index e21acf2..6189d55 100644 --- a/core/core.libraries.yml +++ b/core/core.libraries.yml @@ -234,6 +234,15 @@ drupal.progress: - core/jquery - core/drupalSettings +drupal.revealpass: + version: VERSION + js: + misc/revealpass.js: {} + dependencies: + - core/drupal + - core/jquery + - core/jquery.once + drupal.states: version: VERSION js: diff --git a/core/lib/Drupal/Core/Render/Element/PasswordConfirm.php b/core/lib/Drupal/Core/Render/Element/PasswordConfirm.php index 98b65ed..8334f52 100644 --- a/core/lib/Drupal/Core/Render/Element/PasswordConfirm.php +++ b/core/lib/Drupal/Core/Render/Element/PasswordConfirm.php @@ -78,7 +78,10 @@ public static function processPasswordConfirm(&$element, FormStateInterface $for '#title' => t('Confirm password'), '#value' => empty($element['#value']) ? NULL : $element['#value']['pass2'], '#required' => $element['#required'], - '#attributes' => array('class' => array('password-confirm', 'js-password-confirm')), + '#attributes' => array( + 'class' => array('password-confirm', 'js-password-confirm'), + 'data-drupal-password-strength' => TRUE, + ), '#error_no_message' => TRUE, ); $element['#element_validate'] = array(array(get_called_class(), 'validatePasswordConfirm')); diff --git a/core/lib/Drupal/Core/Render/Element/PasswordReveal.php b/core/lib/Drupal/Core/Render/Element/PasswordReveal.php index e69de29..5124194 100644 --- a/core/lib/Drupal/Core/Render/Element/PasswordReveal.php +++ b/core/lib/Drupal/Core/Render/Element/PasswordReveal.php @@ -0,0 +1,55 @@ + 'password_confirm', + * '#title' => t('Password'), + * '#size' => 25, + * ); + * @endcode + * + * @see \Drupal\Core\Render\Element\Password + * + * @FormElement("password_reveal") + */ +class PasswordReveal extends Password { + + /** + * {@inheritdoc} + */ + public function getInfo() { + $info = parent::getInfo(); + $info['#process'][] = [static::class, 'processPasswordReveal']; + + return $info; + } + + /** + * Adds the attributes needed to add password strength and the reveal button. + */ + public static function processPasswordReveal(&$element, FormStateInterface $form_state, &$complete_form) { + $element['#attached']['library'][] = 'core/drupal.revealpass'; + $element['#attributes']['class'][] = 'password-field'; + $element['#attributes']['class'][] = 'js-password-field'; + $element['#attributes']['data-drupal-revealpass'] = TRUE; + $element['#attributes']['data-drupal-password-strength'] = TRUE; + + return $element; + } + +} diff --git a/core/misc/revealpass.js b/core/misc/revealpass.js index e69de29..7d9a27c 100644 --- a/core/misc/revealpass.js +++ b/core/misc/revealpass.js @@ -0,0 +1,59 @@ +/** + * @file + * Creates a reveal password link on password inputs. + */ + +(function ($, Drupal) { + + 'use strict'; + + var showPass = Drupal.t('Show password'); + var hidePass = Drupal.t('Hide password'); + + /** + * Reveal click handle that either shows or hides the password. + * + * @param {jQuery.Event} event + * The jQuery event. + */ + function revealClickHandle(event) { + var $pass = $(event.data.password); + var $button = $(event.target); + + if ($pass.attr('type') === 'password') { + $pass.attr('type', 'text'); + $button.text(hidePass); + } + else { + $pass.attr('type', 'password'); + $button.text(showPass); + } + } + + /** + * Create reveal button and bind event. + * + * @param {number} index + * @param {HTMLElement} element + */ + function revealLink(index, element) { + var $trigger = $(''); + + $trigger.on('click', {password: element}, revealClickHandle); + $trigger.insertAfter(element); + } + + /** + * Initialize reveal links. + * + * @type {Drupal~behavior} + */ + Drupal.behaviors.revealPass = { + attach: function (context) { + $(context).find('input[type=password][data-drupal-revealpass]') + .once('revealpass') + .each(revealLink); + } + }; + +})(jQuery, Drupal); diff --git a/core/modules/user/config/install/user.settings.yml b/core/modules/user/config/install/user.settings.yml index 8372ccd..7e12609 100644 --- a/core/modules/user/config/install/user.settings.yml +++ b/core/modules/user/config/install/user.settings.yml @@ -13,4 +13,5 @@ register: visitors cancel_method: user_cancel_block password_reset_timeout: 86400 password_strength: true +password_type_reveal: false langcode: en diff --git a/core/modules/user/config/schema/user.schema.yml b/core/modules/user/config/schema/user.schema.yml index 627d8a6..81f224b 100644 --- a/core/modules/user/config/schema/user.schema.yml +++ b/core/modules/user/config/schema/user.schema.yml @@ -50,6 +50,9 @@ user.settings: password_strength: type: boolean label: 'Enable password strength indicator' + password_type_reveal: + type: boolean + label: 'Use password reveal on account edit page' user.mail: type: config_object diff --git a/core/modules/user/src/AccountForm.php b/core/modules/user/src/AccountForm.php index dbdb08b..7f25d9f 100644 --- a/core/modules/user/src/AccountForm.php +++ b/core/modules/user/src/AccountForm.php @@ -113,11 +113,21 @@ public function form(array $form, FormStateInterface $form_state) { // 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.'), - ); + if ($config->get('password_type_reveal')) { + $form['account']['pass'] = array( + '#title' => $this->t('Password'), + '#type' => 'password_reveal', + '#size' => 25, + '#description' => $this->t('To change the current user password, enter the new password.'), + ); + } + else { + $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 @@ -155,12 +165,23 @@ public function form(array $form, FormStateInterface $form_state) { } } elseif (!$config->get('verify_mail') || $admin) { - $form['account']['pass'] = array( - '#type' => 'password_confirm', - '#size' => 25, - '#description' => $this->t('Provide a password for the new account in both fields.'), - '#required' => TRUE, - ); + if ($config->get('password_type_reveal')) { + $form['account']['pass'] = array( + '#title' => $this->t('Password'), + '#type' => 'password_reveal', + '#size' => 25, + '#description' => $this->t('Provide a password for the new account.'), + '#required' => TRUE, + ); + } + else { + $form['account']['pass'] = array( + '#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 diff --git a/core/modules/user/src/AccountSettingsForm.php b/core/modules/user/src/AccountSettingsForm.php index 1e5cd49..07b71db 100644 --- a/core/modules/user/src/AccountSettingsForm.php +++ b/core/modules/user/src/AccountSettingsForm.php @@ -165,6 +165,11 @@ public function buildForm(array $form, FormStateInterface $form_state) { '#title' => $this->t('Enable password strength indicator'), '#default_value' => $config->get('password_strength'), ); + $form['registration_cancellation']['user_password_type_reveal'] = array( + '#type' => 'checkbox', + '#title' => $this->t('Use password reveal, not password confirm, on account edit pages'), + '#default_value' => $config->get('password_type_reveal'), + ); $form['registration_cancellation']['user_cancel_method'] = array( '#type' => 'radios', '#title' => $this->t('When cancelling a user account'), @@ -429,6 +434,7 @@ public function submitForm(array &$form, FormStateInterface $form_state) { ->set('anonymous', $form_state->getValue('anonymous')) ->set('register', $form_state->getValue('user_register')) ->set('password_strength', $form_state->getValue('user_password_strength')) + ->set('password_type_reveal', $form_state->getValue('user_password_type_reveal')) ->set('verify_mail', $form_state->getValue('user_email_verification')) ->set('cancel_method', $form_state->getValue('user_cancel_method')) ->set('notify.status_activated', $form_state->getValue('user_mail_status_activated_notify')) diff --git a/core/modules/user/src/Tests/UserEditTest.php b/core/modules/user/src/Tests/UserEditTest.php index 225b194..d624c59 100644 --- a/core/modules/user/src/Tests/UserEditTest.php +++ b/core/modules/user/src/Tests/UserEditTest.php @@ -101,6 +101,19 @@ function testUserEdit() { $this->assertText(t('The changes have been saved.')); $this->assertNoFieldChecked('edit-status-0'); $this->assertFieldChecked('edit-status-1'); + + // Test editing the user with a password_reveal field. + $config->set('password_type_reveal', TRUE)->save(); + $config->set('password_strength', TRUE)->save(); + + $this->drupalGet("user/" . $admin_user->id() . "/edit"); + $this->assertRaw('Password strength:', 'The password strength indicator is displayed.'); + + $edit = array(); + $edit['pass'] = $this->randomMachineName(); + $edit['current_pass'] = $admin_user->pass_raw; + $this->drupalPostForm("user/" . $admin_user->id() . "/edit", $edit, t('Save')); + $this->assertRaw(t("The changes have been saved.")); } /** diff --git a/core/modules/user/user.install b/core/modules/user/user.install index 7cc46ef..6cb2b7c 100644 --- a/core/modules/user/user.install +++ b/core/modules/user/user.install @@ -105,5 +105,12 @@ function user_update_8100() { } /** + * Add the configuration setting for password_type_reveal. + */ +function user_update_8101() { + \Drupal::configFactory()->getEditable('user.settings')->set('password_type_reveal', FALSE)->save(); +} + +/** * @} End of "addtogroup updates-8.1.0-beta". */ diff --git a/core/modules/user/user.js b/core/modules/user/user.js index f4602c6..582848b 100644 --- a/core/modules/user/user.js +++ b/core/modules/user/user.js @@ -37,7 +37,6 @@ .parent() .append('
' + translate.confirmTitle + '
') .addClass('confirm-parent'); - var $confirmInput = $passwordInputParentWrapper.find('input.js-password-confirm'); var $confirmResult = $passwordInputParentWrapper.find('div.js-password-confirm'); var $confirmChild = $confirmResult.find('span'); diff --git a/core/modules/user/user.module b/core/modules/user/user.module index 1e9f539..43ffbb5 100644 --- a/core/modules/user/user.module +++ b/core/modules/user/user.module @@ -1228,6 +1228,9 @@ function user_element_info_alter(array &$types) { if (isset($types['password_confirm'])) { $types['password_confirm']['#process'][] = 'user_form_process_password_confirm'; } + if (isset($types['password_reveal'])) { + $types['password_reveal']['#process'][] = 'user_form_process_password_reveal'; + } } /** @@ -1271,6 +1274,44 @@ function user_form_process_password_confirm($element) { } /** + * Form element process handler for client-side password validation. + * + * This #process handler is automatically invoked for 'password_reveal' form + * elements to add the JavaScript and string translations for dynamic password + * validation. + */ +function user_form_process_password_reveal($element) { + $password_settings = array( + 'confirmTitle' => '', + 'showStrengthIndicator' => FALSE, + ); + + if (\Drupal::config('user.settings')->get('password_strength')) { + $password_settings['showStrengthIndicator'] = TRUE; + $password_settings += array( + 'strengthTitle' => t('Password strength:'), + 'hasWeaknesses' => t('To make your password stronger:'), + 'tooShort' => t('Make it at least 12 characters'), + 'addLowerCase' => t('Add lowercase letters'), + 'addUpperCase' => t('Add uppercase letters'), + 'addNumbers' => t('Add numbers'), + 'addPunctuation' => t('Add punctuation'), + 'sameAsUsername' => t('Make it different from your username'), + 'weak' => t('Weak'), + 'fair' => t('Fair'), + 'good' => t('Good'), + 'strong' => t('Strong'), + 'username' => \Drupal::currentUser()->getUsername(), + ); + } + + $element['#attached']['library'][] = 'user/drupal.user'; + $element['#attached']['drupalSettings']['password'] = $password_settings; + + return $element; +} + +/** * Implements hook_modules_uninstalled(). */ function user_modules_uninstalled($modules) { diff --git a/core/tests/Drupal/Tests/Core/Render/Element/PasswordRevealTest.php b/core/tests/Drupal/Tests/Core/Render/Element/PasswordRevealTest.php index e69de29..9fbffdb 100644 --- a/core/tests/Drupal/Tests/Core/Render/Element/PasswordRevealTest.php +++ b/core/tests/Drupal/Tests/Core/Render/Element/PasswordRevealTest.php @@ -0,0 +1,45 @@ +prophesize(FormStateInterface::class)->reveal(); + $this->assertSame($expected, PasswordReveal::valueCallback($element, $input, $form_state)); + } + + /** + * Data provider for testValueCallback(). + */ + public function providerTestValueCallback() { + $data = []; + $data[] = [NULL, FALSE]; + $data[] = [NULL, NULL]; + $data[] = ['', ['test']]; + $data[] = ['test', 'test']; + $data[] = ['123', 123]; + + return $data; + } + +} diff --git a/core/themes/classy/css/components/user.css b/core/themes/classy/css/components/user.css index d62ff25..5905e85 100644 --- a/core/themes/classy/css/components/user.css +++ b/core/themes/classy/css/components/user.css @@ -54,6 +54,11 @@ [dir="rtl"] .password-parent { clear: right; } +@media all and (min-width: 600px) { + .password-parent .toggle-password { + margin: 0 0.75em; + } +} /* Styling for the status indicator of the passwords match test. */ .password-confirm .ok { diff --git a/sites/default/default.services.yml b/sites/default/default.services.yml index e1bbbc7..23f6483 100644 --- a/sites/default/default.services.yml +++ b/sites/default/default.services.yml @@ -153,22 +153,3 @@ parameters: - sftp - webcal - rtsp - - # Configure Cross-Site HTTP requests (CORS). - # Read https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS - # for more information about the topic in general. - # Note: By default the configuration is disabled. - cors.config: - enabled: false - # Specify allowed headers, like 'x-allowed-header'. - allowedHeaders: [] - # Specify allowed request methods, specify ['*'] to allow all possible ones. - allowedMethods: [] - # Configure requests allowed from specific origins. - allowedOrigins: ['*'] - # Sets the Access-Control-Expose-Headers header. - exposedHeaders: false - # Sets the Access-Control-Max-Age header. - maxAge: false - # Sets the Access-Control-Allow-Credentials header. - supportsCredentials: false diff --git a/sites/default/default.settings.php b/sites/default/default.settings.php index 94a1e04..d42962d 100644 --- a/sites/default/default.settings.php +++ b/sites/default/default.settings.php @@ -144,11 +144,6 @@ * @code * 'prefix' => 'main_', * @endcode - * - * Per-table prefixes are deprecated as of Drupal 8.2, and will be removed in - * Drupal 9.0. After that, only a single prefix for all tables will be - * supported. - * * To provide prefixes for specific tables, set 'prefix' as an array. * The array's keys are the table names and the values are the prefixes. * The 'default' element is mandatory and holds the prefix for any tables @@ -422,20 +417,6 @@ */ # $settings['omit_vary_cookie'] = TRUE; - -/** - * Cache TTL for client error (4xx) responses. - * - * Items cached per-URL tend to result in a large number of cache items, and - * this can be problematic on 404 pages which by their nature are unbounded. A - * fixed TTL can be set for these items, defaulting to one hour, so that cache - * backends which do not support LRU can purge older entries. To disable caching - * of client error responses set the value to 0. Currently applies only to - * page_cache module. - */ -# $settings['cache_ttl_4xx'] = 3600; - - /** * Class Loader. * @@ -684,15 +665,6 @@ # $settings['container_base_class'] = '\Drupal\Core\DependencyInjection\Container'; /** - * Override the default yaml parser class. - * - * Provide a fully qualified class name here if you would like to provide an - * alternate implementation YAML parser. The class must implement the - * \Drupal\Component\Serialization\SerializationInterface interface. - */ -# $settings['yaml_parser_class'] = NULL; - -/** * Trusted host configuration. * * Drupal core can use the Symfony trusted host mechanism to prevent HTTP Host @@ -730,21 +702,6 @@ */ /** - * The default list of directories that will be ignored by Drupal's file API. - * - * By default ignore node_modules and bower_components folders to avoid issues - * with common frontend tools and recursive scanning of directories looking for - * extensions. - * - * @see file_scan_directory() - * @see \Drupal\Core\Extension\ExtensionDiscovery::scanDirectory() - */ -$settings['file_scan_ignore_directories'] = [ - 'node_modules', - 'bower_components', -]; - -/** * Load local development override configuration, if available. * * Use settings.local.php to override variables on secondary (staging,