Chris@0: /** Chris@0: * @file Chris@0: * User behaviors. Chris@0: */ Chris@0: Chris@17: (function($, Drupal, drupalSettings) { Chris@0: /** Chris@0: * Attach handlers to evaluate the strength of any password fields and to Chris@0: * check that its confirmation is correct. Chris@0: * Chris@0: * @type {Drupal~behavior} Chris@0: * Chris@0: * @prop {Drupal~behaviorAttach} attach Chris@0: * Attaches password strength indicator and other relevant validation to Chris@0: * password fields. Chris@0: */ Chris@0: Drupal.behaviors.password = { Chris@0: attach(context, settings) { Chris@17: const $passwordInput = $(context) Chris@17: .find('input.js-password-field') Chris@17: .once('password'); Chris@0: Chris@0: if ($passwordInput.length) { Chris@0: const translate = settings.password; Chris@0: Chris@0: const $passwordInputParent = $passwordInput.parent(); Chris@0: const $passwordInputParentWrapper = $passwordInputParent.parent(); Chris@0: let $passwordSuggestions; Chris@0: Chris@0: // Add identifying class to password element parent. Chris@0: $passwordInputParent.addClass('password-parent'); Chris@0: Chris@0: // Add the password confirmation layer. Chris@0: $passwordInputParentWrapper Chris@0: .find('input.js-password-confirm') Chris@0: .parent() Chris@17: .append( Chris@17: `
${ Chris@17: translate.confirmTitle Chris@17: }
`, Chris@17: ) Chris@0: .addClass('confirm-parent'); Chris@0: Chris@17: const $confirmInput = $passwordInputParentWrapper.find( Chris@17: 'input.js-password-confirm', Chris@17: ); Chris@17: const $confirmResult = $passwordInputParentWrapper.find( Chris@17: 'div.js-password-confirm', Chris@17: ); Chris@0: const $confirmChild = $confirmResult.find('span'); Chris@0: Chris@0: // If the password strength indicator is enabled, add its markup. Chris@0: if (settings.password.showStrengthIndicator) { Chris@17: const passwordMeter = `
${ Chris@17: translate.strengthTitle Chris@17: }
`; Chris@17: $confirmInput Chris@17: .parent() Chris@17: .after('
'); Chris@0: $passwordInputParent.append(passwordMeter); Chris@17: $passwordSuggestions = $passwordInputParentWrapper Chris@17: .find('div.password-suggestions') Chris@17: .hide(); Chris@0: } Chris@0: Chris@0: // Check that password and confirmation inputs match. Chris@17: const passwordCheckMatch = function(confirmInputVal) { Chris@0: const success = $passwordInput.val() === confirmInputVal; Chris@0: const confirmClass = success ? 'ok' : 'error'; Chris@0: Chris@0: // Fill in the success message and set the class accordingly. Chris@17: $confirmChild Chris@17: .html(translate[`confirm${success ? 'Success' : 'Failure'}`]) Chris@17: .removeClass('ok error') Chris@17: .addClass(confirmClass); Chris@0: }; Chris@0: Chris@0: // Check the password strength. Chris@17: const passwordCheck = function() { Chris@0: if (settings.password.showStrengthIndicator) { Chris@0: // Evaluate the password strength. Chris@17: const result = Drupal.evaluatePasswordStrength( Chris@17: $passwordInput.val(), Chris@17: settings.password, Chris@17: ); Chris@0: Chris@0: // Update the suggestions for how to improve the password. Chris@0: if ($passwordSuggestions.html() !== result.message) { Chris@0: $passwordSuggestions.html(result.message); Chris@0: } Chris@0: Chris@0: // Only show the description box if a weakness exists in the Chris@0: // password. Chris@0: $passwordSuggestions.toggle(result.strength !== 100); Chris@0: Chris@0: // Adjust the length of the strength indicator. Chris@17: $passwordInputParent Chris@17: .find('.js-password-strength__indicator') Chris@0: .css('width', `${result.strength}%`) Chris@0: .removeClass('is-weak is-fair is-good is-strong') Chris@0: .addClass(result.indicatorClass); Chris@0: Chris@0: // Update the strength indication text. Chris@17: $passwordInputParent Chris@17: .find('.js-password-strength__text') Chris@17: .html(result.indicatorText); Chris@0: } Chris@0: Chris@0: // Check the value in the confirm input and show results. Chris@0: if ($confirmInput.val()) { Chris@0: passwordCheckMatch($confirmInput.val()); Chris@0: $confirmResult.css({ visibility: 'visible' }); Chris@17: } else { Chris@0: $confirmResult.css({ visibility: 'hidden' }); Chris@0: } Chris@0: }; Chris@0: Chris@0: // Monitor input events. Chris@0: $passwordInput.on('input', passwordCheck); Chris@0: $confirmInput.on('input', passwordCheck); Chris@0: } Chris@0: }, Chris@0: }; Chris@0: Chris@0: /** Chris@0: * Evaluate the strength of a user's password. Chris@0: * Chris@0: * Returns the estimated strength and the relevant output message. Chris@0: * Chris@0: * @param {string} password Chris@0: * The password to evaluate. Chris@0: * @param {object} translate Chris@0: * An object containing the text to display for each strength level. Chris@0: * Chris@0: * @return {object} Chris@0: * An object containing strength, message, indicatorText and indicatorClass. Chris@0: */ Chris@17: Drupal.evaluatePasswordStrength = function(password, translate) { Chris@0: password = password.trim(); Chris@0: let indicatorText; Chris@0: let indicatorClass; Chris@0: let weaknesses = 0; Chris@0: let strength = 100; Chris@0: let msg = []; Chris@0: Chris@0: const hasLowercase = /[a-z]/.test(password); Chris@0: const hasUppercase = /[A-Z]/.test(password); Chris@0: const hasNumbers = /[0-9]/.test(password); Chris@0: const hasPunctuation = /[^a-zA-Z0-9]/.test(password); Chris@0: Chris@0: // If there is a username edit box on the page, compare password to that, Chris@0: // otherwise use value from the database. Chris@0: const $usernameBox = $('input.username'); Chris@17: const username = Chris@17: $usernameBox.length > 0 ? $usernameBox.val() : translate.username; Chris@0: Chris@0: // Lose 5 points for every character less than 12, plus a 30 point penalty. Chris@0: if (password.length < 12) { Chris@0: msg.push(translate.tooShort); Chris@17: strength -= (12 - password.length) * 5 + 30; Chris@0: } Chris@0: Chris@0: // Count weaknesses. Chris@0: if (!hasLowercase) { Chris@0: msg.push(translate.addLowerCase); Chris@0: weaknesses++; Chris@0: } Chris@0: if (!hasUppercase) { Chris@0: msg.push(translate.addUpperCase); Chris@0: weaknesses++; Chris@0: } Chris@0: if (!hasNumbers) { Chris@0: msg.push(translate.addNumbers); Chris@0: weaknesses++; Chris@0: } Chris@0: if (!hasPunctuation) { Chris@0: msg.push(translate.addPunctuation); Chris@0: weaknesses++; Chris@0: } Chris@0: Chris@0: // Apply penalty for each weakness (balanced against length penalty). Chris@0: switch (weaknesses) { Chris@0: case 1: Chris@0: strength -= 12.5; Chris@0: break; Chris@0: Chris@0: case 2: Chris@0: strength -= 25; Chris@0: break; Chris@0: Chris@0: case 3: Chris@0: strength -= 40; Chris@0: break; Chris@0: Chris@0: case 4: Chris@0: strength -= 40; Chris@0: break; Chris@0: } Chris@0: Chris@0: // Check if password is the same as the username. Chris@0: if (password !== '' && password.toLowerCase() === username.toLowerCase()) { Chris@0: msg.push(translate.sameAsUsername); Chris@0: // Passwords the same as username are always very weak. Chris@0: strength = 5; Chris@0: } Chris@0: Chris@0: // Based on the strength, work out what text should be shown by the Chris@0: // password strength meter. Chris@0: if (strength < 60) { Chris@0: indicatorText = translate.weak; Chris@0: indicatorClass = 'is-weak'; Chris@17: } else if (strength < 70) { Chris@0: indicatorText = translate.fair; Chris@0: indicatorClass = 'is-fair'; Chris@17: } else if (strength < 80) { Chris@0: indicatorText = translate.good; Chris@0: indicatorClass = 'is-good'; Chris@17: } else if (strength <= 100) { Chris@0: indicatorText = translate.strong; Chris@0: indicatorClass = 'is-strong'; Chris@0: } Chris@0: Chris@0: // Assemble the final message. Chris@17: msg = `${translate.hasWeaknesses}`; Chris@0: Chris@0: return { Chris@0: strength, Chris@0: message: msg, Chris@0: indicatorText, Chris@0: indicatorClass, Chris@0: }; Chris@0: }; Chris@17: })(jQuery, Drupal, drupalSettings);