From 9e0da3dc7c39786bf6d972e07819fe2880cdeaa3 Mon Sep 17 00:00:00 2001 From: Dries Buytaert <dries@buytaert.net> Date: Fri, 8 Jun 2007 06:04:15 +0000 Subject: [PATCH] - Patch #143026 by ChrisKennedy and Steven: dynamically check password strength and confirmation. --- CHANGELOG.txt | 1 + includes/form.inc | 2 + install.php | 2 + modules/system/system-rtl.css | 8 ++ modules/system/system.css | 56 ++++++++++- modules/user/user.js | 169 ++++++++++++++++++++++++++++++++++ modules/user/user.module | 35 +++++++ 7 files changed, 272 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 565ad9e15549..e7fc4102670d 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -33,6 +33,7 @@ Drupal 6.0, xxxx-xx-xx (development version) * Made it possible to configure your own date formats. * Remember anonymous comment posters. * Only allow modules and themes to be enabled that have explicitly been ported to the right core API version. + * Dynamically check password strength and confirmation. - Theme system: * Added .info files to themes and made it easier to specify regions and features. * Added theme registry: modules can directly provide .tpl.php files for their themes without having to create theme_ functions. diff --git a/includes/form.inc b/includes/form.inc index 1bd9542b6f30..6ad249cc142b 100644 --- a/includes/form.inc +++ b/includes/form.inc @@ -1214,12 +1214,14 @@ function expand_password_confirm($element) { '#title' => t('Password'), '#value' => empty($element['#value']) ? NULL : $element['#value']['pass1'], '#required' => $element['#required'], + '#attributes' => array('class' => 'password-field'), ); $element['pass2'] = array( '#type' => 'password', '#title' => t('Confirm password'), '#value' => empty($element['#value']) ? NULL : $element['#value']['pass2'], '#required' => $element['#required'], + '#attributes' => array('class' => 'password-confirm'), ); $element['#element_validate'] = array('password_confirm_validate'); $element['#tree'] = TRUE; diff --git a/install.php b/install.php index b8b44a765233..ff7a263675cb 100644 --- a/install.php +++ b/install.php @@ -822,6 +822,8 @@ function install_configure_form() { // This is necessary to add the task to the $_GET args so the install // system will know that it is done and we've taken over. + _user_password_dynamic_validation(); + $form['intro'] = array( '#value' => st('To configure your web site, please provide the following information.'), '#weight' => -10, diff --git a/modules/system/system-rtl.css b/modules/system/system-rtl.css index cf93c728db66..0ccf0f4c66f8 100644 --- a/modules/system/system-rtl.css +++ b/modules/system/system-rtl.css @@ -83,3 +83,11 @@ div.teaser-button-wrapper { .progress .percentage { float: left; } +input.password-field { + margin-left: 10px; + margin-right: inherit; +} +input.password-confirm { + margin-left: 10px; + margin-right: inherit; +} diff --git a/modules/system/system.css b/modules/system/system.css index 67f92d89d9e1..e5bc14500a3c 100644 --- a/modules/system/system.css +++ b/modules/system/system.css @@ -33,7 +33,7 @@ thead th { padding-bottom: .5em } .error { - color: #f00; + color: #e55; } div.error { border: 1px solid #d77; @@ -41,12 +41,29 @@ div.error { div.error, tr.error { background: #fcc; color: #200; + padding: 2px; +} +.warning { + color: #e09010; +} +div.warning { + border: 1px solid #f0c020; } div.warning, tr.warning { background: #ffd; + color: #220; + padding: 2px; +} +.ok { + color: #008000; +} +div.ok { + border: 1px solid #00aa00; } div.ok, tr.ok { background: #dfd; + color: #020; + padding: 2px; } .item-list .icon { color: #555; @@ -452,3 +469,40 @@ html.js .js-hide { #system-modules div.incompatible { font-weight: bold; } + +/* +** Password strength indicator +*/ +span.password-strength { + visibility: hidden; +} +input.password-field { + margin-right: 10px; /* LTR */ +} +div.password-description { + padding: 0 2px; + margin: 4px 0 0 0; + font-size: 0.85em; + max-width: 500px; +} +div.password-description ul { + margin-bottom: 0; +} +.password-parent { + margin: 0 0 0 0; +} +/* +** Password confirmation checker +*/ +input.password-confirm { + margin-right: 10px; /* LTR */ +} +.confirm-parent { + margin: 5px 0 0 0; +} +span.password-confirm { + visibility: hidden; +} +span.password-confirm span { + font-weight: normal; +} diff --git a/modules/user/user.js b/modules/user/user.js index caffaf270218..cbd46c992bec 100644 --- a/modules/user/user.js +++ b/modules/user/user.js @@ -1,5 +1,173 @@ /* $Id$ */ +/** + * Attach handlers to evaluate the strength of any password fields and to check + * that its confirmation is correct. + */ +Drupal.passwordAttach = function(context) { + var context = context || $(document); + var translate = Drupal.settings.password; + $("input.password-field", context).each(function() { + var passwordInput = $(this); + var parent = $(this).parent(); + // Wait this number of milliseconds before checking password. + var monitorDelay = 700; + + // Add the password strength layers. + $(this).after('<span class="password-strength"><span class="password-title">'+ translate.strengthTitle +'</span> <span class="password-result"></span></span>').parent(); + var passwordStrength = $("span.password-strength", parent); + var passwordResult = $("span.password-result", passwordStrength); + parent.addClass("password-parent"); + + // Add the password confirmation layer. + var outerItem = $(this).parent().parent(); + $("input.password-confirm", outerItem).after('<span class="password-confirm">'+ translate["confirmTitle"] +' <span></span></span>').parent().addClass("confirm-parent"); + var confirmInput = $("input.password-confirm", outerItem); + var confirmResult = $("span.password-confirm", outerItem); + var confirmChild = $("span", confirmResult); + + // Add the description box at the end. + $(confirmInput).parent().after('<div class="password-description"></div>'); + var passwordDescription = $("div.password-description", $(this).parent().parent()).hide(); + + // Check the password fields. + var passwordCheck = function () { + // Remove timers for a delayed check if they exist. + if (this.timer) { + clearTimeout(this.timer); + } + + // Verify that there is a password to check. + if (!passwordInput.val()) { + passwordStrength.css({ visibility: "hidden" }); + passwordDescription.hide(); + return; + } + + // Evaluate password strength. + + var result = Drupal.evaluatePasswordStrength(passwordInput.val()); + passwordResult.html(result.strength == "" ? "" : translate[result.strength +"Strength"]); + + // Map the password strength to the relevant drupal CSS class. + var classMap = { low: "error", medium: "warning", high: "ok" }; + var newClass = classMap[result.strength] || ""; + + // Remove the previous styling if any exists; add the new class. + if (this.passwordClass) { + passwordResult.removeClass(this.passwordClass); + passwordDescription.removeClass(this.passwordClass); + } + passwordDescription.html(result.message); + passwordResult.addClass(newClass); + if (result.strength == "high") { + passwordDescription.hide(); + } + else { + passwordDescription.addClass(newClass); + } + this.passwordClass = newClass; + + // Check that password and confirmation match. + + // Hide the result layer if confirmation is empty, otherwise show the layer. + confirmResult.css({ visibility: (confirmInput.val() == "" ? "hidden" : "visible") }); + + var success = passwordInput.val() == confirmInput.val(); + + // Remove the previous styling if any exists. + if (this.confirmClass) { + confirmChild.removeClass(this.confirmClass); + } + + // Fill in the correct message and set the class accordingly. + var confirmClass = success ? "ok" : "error"; + confirmChild.html(translate["confirm"+ (success ? "Success" : "Failure")]).addClass(confirmClass); + this.confirmClass = confirmClass; + + // Show the indicator and tips. + passwordStrength.css({ visibility: "visible" }); + passwordDescription.show(); + }; + + // Do a delayed check on the password fields. + var passwordDelayedCheck = function() { + // Postpone the check since the user is most likely still typing. + if (this.timer) { + clearTimeout(this.timer); + } + + // When the user clears the field, hide the tips immediately. + if (!passwordInput.val()) { + passwordStrength.css({ visibility: "hidden" }); + passwordDescription.hide(); + return; + } + + // Schedule the actual check. + this.timer = setTimeout(passwordCheck, monitorDelay); + }; + // Monitor keyup and blur events. + // Blur must be used because a mouse paste does not trigger keyup. + passwordInput.keyup(passwordDelayedCheck).blur(passwordCheck); + confirmInput.keyup(passwordDelayedCheck).blur(passwordCheck); + }); +}; + +/** + * Evaluate the strength of a user's password. + * + * Returns the estimated strength and the relevant output message. + */ +Drupal.evaluatePasswordStrength = function(value) { + var strength = "", msg = "", translate = Drupal.settings.password; + + var hasLetters = value.match(/[a-zA-Z]+/); + var hasNumbers = value.match(/[0-9]+/); + var hasPunctuation = value.match(/[^a-zA-Z0-9]+/); + var hasCasing = value.match(/[a-z]+.*[A-Z]+|[A-Z]+.*[a-z]+/); + + // Check if the password is blank. + if (!value.length) { + strength = ""; + msg = ""; + } + // Check if length is less than 6 characters. + else if (value.length < 6) { + strength = "low"; + msg = translate.tooShort; + } + // Check if password is the same as the username (convert both to lowercase). + else if (value.toLowerCase() == translate.username.toLowerCase()) { + strength = "low"; + msg = translate.sameAsUsername; + } + // Check if it contains letters, numbers, punctuation, and upper/lower case. + else if (hasLetters && hasNumbers && hasPunctuation && hasCasing) { + strength = "high"; + } + // Password is not secure enough so construct the medium-strength message. + else { + // Extremely bad passwords still count as low. + var count = (hasLetters ? 1 : 0) + (hasNumbers ? 1 : 0) + (hasPunctuation ? 1 : 0) + (hasCasing ? 1 : 0); + strength = count > 1 ? "medium" : "low"; + + msg = []; + if (!hasLetters || !hasCasing) { + msg.push(translate.addLetters); + } + if (!hasNumbers) { + msg.push(translate.addNumbers); + } + if (!hasPunctuation) { + msg.push(translate.addPunctuation); + } + msg = translate.needsMoreVariation +"<ul><li>"+ msg.join("</li><li>") +"</li></ul>"; + } + + return { strength: strength, message: msg }; +}; + /** * On the admin/user/settings page, conditionally show all of the * picture-related form elements depending on the current value of the @@ -10,5 +178,6 @@ if (Drupal.jsEnabled) { $('div.user-admin-picture-radios input[@type=radio]').click(function () { $('div.user-admin-picture-settings')[['hide', 'show'][this.value]](); }); + Drupal.passwordAttach(); }); } diff --git a/modules/user/user.module b/modules/user/user.module index 741af7e0efc9..1ba1c772c389 100644 --- a/modules/user/user.module +++ b/modules/user/user.module @@ -1414,6 +1414,7 @@ function user_register_submit($form, &$form_state) { } function user_edit_form(&$form_state, $uid, $edit, $register = FALSE) { + _user_password_dynamic_validation(); $admin = user_access('administer users'); // Account information: @@ -3151,3 +3152,37 @@ function _user_mail_notify($op, $account, $password = NULL) { } return $result; } + +/** + * Add javascript and string translations for dynamic password validation (strength and confirmation checking). + * + * This is an internal function that makes it easier to manage the translation + * strings that need to be passed to the javascript code. + */ +function _user_password_dynamic_validation() { + static $complete = FALSE; + global $user; + // Only need to do once per page. + if (!$complete) { + drupal_add_js(drupal_get_path('module', 'user') .'/user.js', 'module'); + + drupal_add_js(array( + 'password' => array( + 'strengthTitle' => t('Password strength:'), + 'lowStrength' => t('Low'), + 'mediumStrength' => t('Medium'), + 'highStrength' => t('High'), + 'tooShort' => t('It is recommended to choose a password that contains at least six characters. It should include numbers, punctuation, and both upper and lowercase letters.'), + 'needsMoreVariation' => t('The password does not include enough variation to be secure. Try:'), + 'addLetters' => t('Adding both upper and lowercase letters.'), + 'addNumbers' => t('Adding numbers.'), + 'addPunctuation' => t('Adding punctuation.'), + 'sameAsUsername' => t('It is recommended to choose a password different from the username.'), + 'confirmSuccess' => t('Yes'), + 'confirmFailure' => t('No'), + 'confirmTitle' => t('Passwords match:'), + 'username' => (isset($user->name) ? $user->name : ''))), + 'setting'); + $complete = TRUE; + } +} -- GitLab