Index: install.php =================================================================== RCS file: /cvs/drupal/drupal/install.php,v retrieving revision 1.53 diff -u -p -r1.53 install.php --- install.php 22 May 2007 07:42:36 -0000 1.53 +++ install.php 24 May 2007 10:22:40 -0000 @@ -818,6 +818,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, Index: includes/form.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/form.inc,v retrieving revision 1.196 diff -u -p -r1.196 form.inc --- includes/form.inc 16 May 2007 07:56:19 -0000 1.196 +++ includes/form.inc 24 May 2007 10:22:40 -0000 @@ -1198,11 +1198,13 @@ function expand_password_confirm($elemen '#type' => 'password', '#title' => t('Password'), '#value' => empty($element['#value']) ? NULL : $element['#value']['pass1'], + '#attributes' => array('class' => 'password-field'), ); $element['pass2'] = array( '#type' => 'password', '#title' => t('Confirm password'), '#value' => empty($element['#value']) ? NULL : $element['#value']['pass2'], + '#attributes' => array('class' => 'password-confirm'), ); $element['#element_validate'] = array('password_confirm_validate'); $element['#tree'] = TRUE; Index: modules/system/system.css =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.css,v retrieving revision 1.27 diff -u -p -r1.27 system.css --- modules/system/system.css 20 May 2007 16:38:19 -0000 1.27 +++ modules/system/system.css 24 May 2007 10:22:40 -0000 @@ -33,7 +33,7 @@ thead th { padding-bottom: .5em } .error { - color: #f00; + color: #e55; } div.error { border: 1px solid #d77; @@ -42,9 +42,21 @@ div.error, tr.error { background: #fcc; color: #200; } +.warning { + color: #e0b010; +} +div.warning { + border: 1px solid #f0c020; +} div.warning, tr.warning { background: #ffd; } +.ok { + color: #0a0; +} +div.ok { + border: 1px solid #0a0; +} div.ok, tr.ok { background: #dfd; } @@ -446,3 +458,37 @@ thead div.sticky-header { html.js .js-hide { display: none; } + +/* +** Password strength indicator +*/ +span.password-strength { + visibility: hidden; +} +span.password-title { + font-weight: bold; +} +input.password-field { + margin-right: 10px; +} +.password-description { + padding: 0 0 0 2px; + margin: 4px 0 0 0; + font-size: 0.85em; + max-width: 500px; +} +.password-parent { + margin: 0 0 0 0; +} +/* +** Password confirmation checker +*/ +input.password-confirm { + margin-right: 10px; +} +.confirm-parent { + margin: 5px 0 0 0; +} +span.password-confirm { + visibility: hidden; +} Index: modules/user/user.js =================================================================== RCS file: /cvs/drupal/drupal/modules/user/user.js,v retrieving revision 1.1 diff -u -p -r1.1 user.js --- modules/user/user.js 20 May 2007 16:38:19 -0000 1.1 +++ modules/user/user.js 24 May 2007 10:22:40 -0000 @@ -1,6 +1,132 @@ /* $Id: user.js,v 1.1 2007/05/20 16:38:19 dries Exp $ */ /** + * 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 = 300; + + // Add the password strength layers. + $(this).after(''+ translate.title +' ').parent().after('
'); + var passwordStrength = $(".password-strength", parent); + var passwordDescription = $(".password-description", $(this).parent().parent()); + var passwordResult = $(".password-result", passwordStrength); + parent.addClass("password-parent"); + + // Add the password confirmation layer. + var outerItem = $(this).parent().parent(); + $("input.password-confirm", outerItem).after('').parent().addClass("confirm-parent"); + var confirmInput = $("input.password-confirm", outerItem); + var confirmResult = $("span.password-confirm", outerItem); + + var passwordMonitor = function() { + if (this.timer) { + clearTimeout(this.timer); + } + + this.timer = setTimeout(function() { + // Evaluate password strength. + + if (!passwordInput.val()) { + // Password is empty so hide the password strength UI. + passwordStrength.css({ visibility: "hidden" }); + passwordDescription.hide(); + } + else { + passwordStrength.css({ visibility: "visible" }); + passwordDescription.show(); + } + + var result = Drupal.evaluatePasswordStrength(passwordInput.val()); + passwordResult.html(result.strength == "" ? "" : translate[result.strength +"Strength"]); + passwordDescription.html(result.message); + + // Map the password strength to the relevant drupal CSS class. + var classMap = { low: "error", medium: "warning", high: "ok" }; + var class = 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); + } + passwordResult.addClass(class); + passwordDescription.addClass(class); + this.passwordClass = class; + + // 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) { + confirmResult.removeClass(this.confirmClass); + } + + // Display the correct message and set the class accordingly. + var class = success ? "ok" : "error"; + confirmResult.html(translate["confirm"+ (success ? "Success" : "Failure")]).addClass(class); + this.confirmClass = class; + }, monitorDelay); + }; + // Monitor keyup and blur events. + // Blur must be used because a mouse paste does not trigger keyup. + passwordInput.keyup(passwordMonitor).blur(passwordMonitor); + confirmInput.keyup(passwordMonitor).blur(passwordMonitor); + }); +} + +/** + * 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; + + // 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 only contains letters. + else if (value.match(/^[a-zA-Z]*$/)) { + strength = "medium"; + msg = translate.onlyLetters; + } + // Check if it contains punctuation or other special characters. + else if (value.match(/^[a-zA-Z0-9]*$/)) { + strength = "medium"; + msg = translate.addPunctuation; + } + else { + strength = "high"; + msg = translate.goodPassword; + } + 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 * "Picture support" radio buttons. @@ -10,5 +136,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(); }); } Index: modules/user/user.module =================================================================== RCS file: /cvs/drupal/drupal/modules/user/user.module,v retrieving revision 1.785 diff -u -p -r1.785 user.module --- modules/user/user.module 22 May 2007 05:52:17 -0000 1.785 +++ modules/user/user.module 24 May 2007 10:22:41 -0000 @@ -1424,6 +1424,7 @@ function user_register_submit($form_valu } function user_edit_form($uid, $edit, $register = FALSE) { + _user_password_dynamic_validation(); $admin = user_access('administer users'); // Account information: @@ -3161,3 +3162,35 @@ function _user_mail_notify($op, $account } 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( + 'title' => t('Password strength:'), + 'lowStrength' => t('Low'), + 'mediumStrength' => t('Medium'), + 'highStrength' => t('High'), + 'tooShort' => t('Your password is recommended to be at least six characters in length for a minimum level of security.'), + 'onlyLetters' => t('Your password currently only contains letters. Please add numbers and punctuation to strengthen it.'), + 'addPunctuation' => t('To increase the strength of your password please add punctuation characters.'), + 'sameAsUsername' => t('Your password should not be the same as your username.'), + 'goodPassword' => t('Your password is complex enough to be reasonably secure.'), + 'confirmSuccess' => t('Confirmation matches password.'), + 'confirmFailure' => t('Password and confirmation do not match.'), + 'username' => (isset($user->name) ? $user->name : ''))), + 'setting'); + $complete = TRUE; + } +}