diff --git a/CHANGELOG.txt b/CHANGELOG.txt index c4243c04c134766ee5b33fc4b14b84d9b39f7e7f..e419c23c6ffccd4b6a03f189f428cf11d4a699d2 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -28,6 +28,7 @@ Drupal 7.0, xxxx-xx-xx (development version) * Image toolkits are now provided by modules (rather than requiring a manual file copy to the includes directory). * Added an edit tab to taxonomy term pages. + * Redesigned password strength validator. - News aggregator: * Added OPML import functionality for RSS feeds. * Optionally, RSS feeds may be configured to not automatically generate feed blocks. diff --git a/install.php b/install.php index c0a362a3e908a39b20649669dd53ae6fab7f3aa4..21ad8f755902723c4a925236470814c5e1db7ed9 100644 --- a/install.php +++ b/install.php @@ -1052,6 +1052,7 @@ function install_configure_form(&$form_state, $url) { '#description' => st('Spaces are allowed; punctuation is not allowed except for periods, hyphens, and underscores.'), '#required' => TRUE, '#weight' => -10, + '#attributes' => array('class' => 'username'), ); $form['admin_account']['account']['mail'] = array('#type' => 'textfield', diff --git a/modules/system/system-rtl.css b/modules/system/system-rtl.css index 8e4ad2165a933cd9b794a786ddae6f71d9a4deda..fb5146527463f31d0a219aec09ae256f301ca135 100644 --- a/modules/system/system-rtl.css +++ b/modules/system/system-rtl.css @@ -95,6 +95,12 @@ input.password-confirm { margin-left: 10px; margin-right: 0; } +.password-strength-title { + float: right; +} +.password-parent { + float: right; +} .draggable a.tabledrag-handle { float: right; diff --git a/modules/system/system.css b/modules/system/system.css index 919417ec93b9c16fe2a4ccc3249e518bcbd0dc02..72c0816ba8da0a6d149b7f208ca8f9e35e8a88bb 100644 --- a/modules/system/system.css +++ b/modules/system/system.css @@ -516,36 +516,46 @@ html.js .js-hide { /* ** Password strength indicator */ -span.password-strength { - visibility: hidden; +.password-strength-title { + float: left; /* LTR */ } -input.password-field { - margin-right: 10px; /* LTR */ +#password-indicator { + border: 1px solid #B4B4B4; + float: right; + height: 0.9em; + margin: 0.3em 0.80em 0 0.3em; + width: 5em; } -div.password-description { - padding: 0 2px; - margin: 4px 0 0 0; - font-size: 0.85em; - max-width: 500px; +#password-indicator div { + height: 100%; + width: 0%; + background-color: #47C965; +} +input.password-confirm, input.password-field { + width: 16em; + margin-bottom: 0.4em; +} +div.password-suggestions { + padding: 0.2em 0.5em; + margin: 0.7em 0; + width: 38.5em; + border: 1px solid #B4B4B4; } -div.password-description ul { +div.password-suggestions ul { margin-bottom: 0; } .password-parent { - margin: 0 0 0 0; + margin: 0; + float: left; /* LTR */ + width: 17.3em; } + /* ** Password confirmation checker */ -input.password-confirm { - margin-right: 10px; /* LTR */ -} .confirm-parent { - margin: 5px 0 0 0; + margin: 0; } -span.password-confirm { +div.password-confirm { visibility: hidden; } -span.password-confirm span { - font-weight: normal; -} diff --git a/modules/user/user.js b/modules/user/user.js index 371474d7ce52c446bfbc1ed546060c99ca2768c5..504db3246fd9dcd3afc4d1e523f44600c17d365d 100644 --- a/modules/user/user.js +++ b/modules/user/user.js @@ -5,111 +5,83 @@ * that its confirmation is correct. */ Drupal.behaviors.password = function(context) { + var translate = Drupal.settings.password; $("input.password-field:not(.password-processed)", context).each(function() { var passwordInput = $(this).addClass('password-processed'); - var parent = $(this).parent(); - // Wait this number of milliseconds before checking password. - var monitorDelay = 700; + var innerWrapper = $(this).parent(); + var outerWrapper = $(this).parent().parent(); // 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 passwordStrength = $("span.password-strength", innerWrapper); var passwordResult = $("span.password-result", passwordStrength); - parent.addClass("password-parent"); + innerWrapper.addClass("password-parent"); + + // Add the description box at the end. + var passwordMeter = '<div id="password-strength"><div class="password-strength-title">' + translate.strengthTitle + '</div><div id="password-indicator"><div id="indicator"></div></div></div>'; + $("div.description", outerWrapper).prepend('<div class="password-suggestions"></div>'); + $(innerWrapper).append(passwordMeter); + var passwordDescription = $("div.password-suggestions", outerWrapper).hide(); // 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); + $("input.password-confirm", outerWrapper).after('<div class="password-confirm">' + translate["confirmTitle"] + ' <span></span></div>').parent().addClass("confirm-parent"); + var confirmInput = $("input.password-confirm", outerWrapper); + var confirmResult = $("div.password-confirm", outerWrapper); 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. + // Check the password strength. 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. + // Evaluate the 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); + // Update the suggestions for how to improve the password. + if (passwordDescription.html() != result.message) { + passwordDescription.html(result.message); } - passwordDescription.html(result.message); - passwordResult.addClass(newClass); - if (result.strength == "high") { + + // Only show the description box if there is a weakness in the password. + if (result.strength == 100) { passwordDescription.hide(); } else { - passwordDescription.addClass(newClass); + passwordDescription.show(); } - this.passwordClass = newClass; - // Check that password and confirmation match. + // Adjust the length of the strength indicator. + $("#indicator").css('width', result.strength + '%'); - // Hide the result layer if confirmation is empty, otherwise show the layer. - confirmResult.css({ visibility: (confirmInput.val() == "" ? "hidden" : "visible") }); + passwordCheckMatch(); + }; - var success = passwordInput.val() == confirmInput.val(); + // Check that password and confirmation inputs match. + var passwordCheckMatch = function () { - // Remove the previous styling if any exists. - if (this.confirmClass) { - confirmChild.removeClass(this.confirmClass); - } + if (confirmInput.val()) { + var success = passwordInput.val() === confirmInput.val(); - // 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 confirm result. + confirmResult.css({ visibility: "visible" }); - // Show the indicator and tips. - passwordStrength.css({ visibility: "visible" }); - passwordDescription.show(); - }; + // Remove the previous styling if any exists. + if (this.confirmClass) { + confirmChild.removeClass(this.confirmClass); + } - // 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); + // Fill in the success message and set the class accordingly. + var confirmClass = success ? "ok" : 'error'; + confirmChild.html(translate["confirm" + (success ? "Success" : "Failure")]).addClass(confirmClass); + this.confirmClass = confirmClass; } - - // When the user clears the field, hide the tips immediately. - if (!passwordInput.val()) { - passwordStrength.css({ visibility: "hidden" }); - passwordDescription.hide(); - return; + else { + confirmResult.css({ visibility: "hidden" }); } + } - // 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); + passwordInput.keyup(passwordCheck).focus(passwordCheck).blur(passwordCheck); + confirmInput.keyup(passwordCheckMatch).blur(passwordCheckMatch); }); }; @@ -118,52 +90,71 @@ Drupal.behaviors.password = function(context) { * * 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 = ""; +Drupal.evaluatePasswordStrength = function (password) { + var weaknesses = 0, strength = 100, msg = [], translate = Drupal.settings.password; + + var hasLowercase = password.match(/[a-z]+/); + var hasUppercase = password.match(/[A-Z]+/); + var hasNumbers = password.match(/[0-9]+/); + var hasPunctuation = password.match(/[^a-zA-Z0-9]+/); + + // If there is a username edit box on the page, compare password to that, otherwise + // use value from the database. + var usernameBox = $("input.username"); + var username = (usernameBox.length > 0) ? usernameBox.val() : translate.username; + + // Lose 10 points for every character less than 6. + if (password.length < 6) { + msg.push(translate.tooShort); + strength -= (6 - password.length) * 10; } - // Check if length is less than 6 characters. - else if (value.length < 6) { - strength = "low"; - msg = translate.tooShort; + + // Count weaknesses. + if (!hasLowercase) { + msg.push(translate.addLowerCase); + weaknesses++; } - // 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; + if (!hasUppercase) { + msg.push(translate.addUpperCase); + weaknesses++; } - // Check if it contains letters, numbers, punctuation, and upper/lower case. - else if (hasLetters && hasNumbers && hasPunctuation && hasCasing) { - strength = "high"; + if (!hasNumbers) { + msg.push(translate.addNumbers); + weaknesses++; } - // 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>"; + if (!hasPunctuation) { + msg.push(translate.addPunctuation); + weaknesses++; + } + + // Apply penalty for each weakness (balanced against length penalty). + switch (weaknesses) { + case 1: + strength -= 12.5; + break; + + case 2: + strength -= 25; + break; + + case 3: + strength -= 40; + break; + + case 4: + strength -= 40; + break; + } + + // Check if password is the same as the username. + if ((password !== '') && (password.toLowerCase() === username.toLowerCase())){ + msg.push(translate.sameAsUsername); + // Passwords the same as username are always very weak. + strength = 5; } + // Assemble the final message. + msg = translate.hasWeaknesses + "<ul><li>" + msg.join("</li><li>") + "</li></ul>"; return { strength: strength, message: msg }; }; diff --git a/modules/user/user.module b/modules/user/user.module index 8b666cf1b0ce11a64a0b44afdf5fb343d5c15184..a8c9de8be437f72a862056e6947babcd3f84f800 100644 --- a/modules/user/user.module +++ b/modules/user/user.module @@ -1451,6 +1451,7 @@ function user_edit_form(&$form_state, $uid, $edit, $register = FALSE) { '#maxlength' => USERNAME_MAX_LENGTH, '#description' => t('Spaces are allowed; punctuation is not allowed except for periods, hyphens, apostrophes, and underscores.'), '#required' => TRUE, + '#attributes' => array('class' => 'username'), ); } $form['account']['mail'] = array('#type' => 'textfield', @@ -2164,17 +2165,15 @@ function _user_password_dynamic_validation() { 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'), + 'hasWeaknesses' => t('To make your password stronger:'), + 'tooShort' => t('Make it at least 6 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'), + 'confirmSuccess' => t('yes'), + 'confirmFailure' => t('no'), 'confirmTitle' => t('Passwords match:'), 'username' => (isset($user->name) ? $user->name : ''))), 'setting');