/** * @file * User behaviors. */ (($, Drupal) => { /** * An object containing CSS classes used for password widget. * * @type {object} * @prop {string} passwordParent - A CSS class for the parent element. * @prop {string} passwordsMatch - A CSS class indicating password match. * @prop {string} passwordsNotMatch - A CSS class indicating passwords * doesn't match. * @prop {string} passwordWeak - A CSS class indicating weak password * strength. * @prop {string} passwordFair - A CSS class indicating fair password * strength. * @prop {string} passwordGood - A CSS class indicating good password * strength. * @prop {string} passwordStrong - A CSS class indicating strong password * strength. * @prop {string} widgetInitial - Initial CSS class that should be removed * on a state change. * @prop {string} passwordEmpty - A CSS class indicating password has not * been filled. * @prop {string} passwordFilled - A CSS class indicating password has * been filled. * @prop {string} confirmEmpty - A CSS class indicating password * confirmation has not been filled. * @prop {string} confirmFilled - A CSS class indicating password * confirmation has been filled. */ Drupal.user = { password: { css: { passwordParent: 'password-parent', passwordsMatch: 'ok', passwordsNotMatch: 'error', passwordWeak: 'is-weak', passwordFair: 'is-fair', passwordGood: 'is-good', passwordStrong: 'is-strong', widgetInitial: '', passwordEmpty: '', passwordFilled: '', confirmEmpty: '', confirmFilled: '', }, }, }; /** * Attach handlers to evaluate the strength of any password fields and to * check that its confirmation is correct. * * @type {Drupal~behavior} * * @prop {Drupal~behaviorAttach} attach * Attaches password strength indicator and other relevant validation to * password fields. */ Drupal.behaviors.password = { attach(context, settings) { const cssClasses = Drupal.user.password.css; once('password', 'input.js-password-field', context).forEach((value) => { const $mainInput = $(value); const $mainInputParent = $mainInput .parent() .addClass(cssClasses.passwordParent); const $passwordWidget = $mainInput.closest( '.js-form-type-password-confirm', ); const $confirmInput = $passwordWidget.find('input.js-password-confirm'); const $passwordConfirmMessage = $( Drupal.theme('passwordConfirmMessage', settings.password), ); const $passwordMatchStatus = $passwordConfirmMessage .find('[data-drupal-selector="password-match-status-text"]') .first(); const $confirmInputParent = $confirmInput .parent() .addClass('confirm-parent') .append($passwordConfirmMessage); // List of classes to be removed from the strength bar on a state // change. const passwordStrengthBarClassesToRemove = [ cssClasses.passwordWeak || '', cssClasses.passwordFair || '', cssClasses.passwordGood || '', cssClasses.passwordStrong || '', ] .join(' ') .trim(); // List of classes to be removed from the text wrapper on a state // change. const confirmTextWrapperClassesToRemove = [ cssClasses.passwordsMatch || '', cssClasses.passwordsNotMatch || '', ] .join(' ') .trim(); // List of classes to be removed from the widget on a state change. const widgetClassesToRemove = [ cssClasses.widgetInitial || '', cssClasses.passwordEmpty || '', cssClasses.passwordFilled || '', cssClasses.confirmEmpty || '', cssClasses.confirmFilled || '', ] .join(' ') .trim(); const password = {}; // If the password strength indicator is enabled, add its markup. if (settings.password.showStrengthIndicator) { const $passwordStrength = $( Drupal.theme('passwordStrength', settings.password), ); password.$strengthBar = $passwordStrength .find('[data-drupal-selector="password-strength-indicator"]') .first(); password.$strengthTextWrapper = $passwordStrength .find('[data-drupal-selector="password-strength-text"]') .first(); password.$suggestions = $( Drupal.theme('passwordSuggestions', settings.password, []), ); password.$suggestions.hide(); $mainInputParent.append($passwordStrength); $confirmInputParent.after(password.$suggestions); } /** * Adds classes to the widget indicating if the elements are filled. */ const addWidgetClasses = () => { $passwordWidget .addClass( $mainInput[0].value ? cssClasses.passwordFilled : cssClasses.passwordEmpty, ) .addClass( $confirmInput[0].value ? cssClasses.confirmFilled : cssClasses.confirmEmpty, ); }; /** * Check that password and confirmation inputs match. * * @param {string} confirmInputVal * The value of the confirm input. */ const passwordCheckMatch = (confirmInputVal) => { const passwordsAreMatching = $mainInput[0].value === confirmInputVal; const confirmClass = passwordsAreMatching ? cssClasses.passwordsMatch : cssClasses.passwordsNotMatch; const confirmMessage = passwordsAreMatching ? settings.password.confirmSuccess : settings.password.confirmFailure; // Update the success message and set the class if needed. if ( !$passwordMatchStatus.hasClass(confirmClass) || !$passwordMatchStatus.html() === confirmMessage ) { if (confirmTextWrapperClassesToRemove) { $passwordMatchStatus.removeClass( confirmTextWrapperClassesToRemove, ); } $passwordMatchStatus.html(confirmMessage).addClass(confirmClass); } }; /** * Checks the password strength. */ const passwordCheck = () => { if (settings.password.showStrengthIndicator) { // Evaluate the password strength. const result = Drupal.evaluatePasswordStrength( $mainInput[0].value, settings.password, ); const $currentPasswordSuggestions = $( Drupal.theme( 'passwordSuggestions', settings.password, result.messageTips, ), ); // Update the suggestions for how to improve the password if needed. if ( password.$suggestions.html() !== $currentPasswordSuggestions.html() ) { password.$suggestions.replaceWith($currentPasswordSuggestions); password.$suggestions = $currentPasswordSuggestions.toggle( // Only show the description box if a weakness exists in the // password. result.strength !== 100, ); } if (passwordStrengthBarClassesToRemove) { password.$strengthBar.removeClass( passwordStrengthBarClassesToRemove, ); } // Adjust the length of the strength indicator. password.$strengthBar[0].style.width = `${result.strength}%`; password.$strengthBar.addClass(result.indicatorClass); // Update the strength indication text. password.$strengthTextWrapper.html(result.indicatorText); } // Check the value in the confirm input and show results. if ($confirmInput[0].value) { passwordCheckMatch($confirmInput[0].value); $passwordConfirmMessage[0].style.visibility = 'visible'; } else { $passwordConfirmMessage[0].style.visibility = 'hidden'; } if (widgetClassesToRemove) { $passwordWidget.removeClass(widgetClassesToRemove); addWidgetClasses(); } }; if (widgetClassesToRemove) { addWidgetClasses(); } // Monitor input events. $mainInput.on('input', passwordCheck); $confirmInput.on('input', passwordCheck); }); }, }; /** * Evaluate the strength of a user's password. * * Returns the estimated strength and the relevant output message. * * @param {string} password * The password to evaluate. * @param {object} passwordSettings * A password settings object containing the text to display and the CSS * classes for each strength level. * * @return {object} * An object containing strength, message, indicatorText and indicatorClass. */ Drupal.evaluatePasswordStrength = (password, passwordSettings) => { password = password.trim(); let indicatorText; let indicatorClass; let weaknesses = 0; let strength = 100; let msg = []; const hasLowercase = /[a-z]/.test(password); const hasUppercase = /[A-Z]/.test(password); const hasNumbers = /[0-9]/.test(password); const hasPunctuation = /[^a-zA-Z0-9]/.test(password); // If there is a username edit box on the page, compare password to that, // otherwise use value from the database. const $usernameBox = $('input.username'); const username = $usernameBox.length > 0 ? $usernameBox[0].value : passwordSettings.username; // Lose 5 points for every character less than 12, plus a 30 point penalty. if (password.length < 12) { msg.push(passwordSettings.tooShort); strength -= (12 - password.length) * 5 + 30; } // Count weaknesses. if (!hasLowercase) { msg.push(passwordSettings.addLowerCase); weaknesses += 1; } if (!hasUppercase) { msg.push(passwordSettings.addUpperCase); weaknesses += 1; } if (!hasNumbers) { msg.push(passwordSettings.addNumbers); weaknesses += 1; } if (!hasPunctuation) { msg.push(passwordSettings.addPunctuation); weaknesses += 1; } // Apply penalty for each weakness (balanced against length penalty). switch (weaknesses) { case 1: strength -= 12.5; break; case 2: strength -= 25; break; case 3: case 4: strength -= 40; break; } // Check if password is the same as the username. if (password !== '' && password.toLowerCase() === username.toLowerCase()) { msg.push(passwordSettings.sameAsUsername); // Passwords the same as username are always very weak. strength = 5; } const cssClasses = Drupal.user.password.css; // Based on the strength, work out what text should be shown by the // password strength meter. if (strength < 60) { indicatorText = passwordSettings.weak; indicatorClass = cssClasses.passwordWeak; } else if (strength < 70) { indicatorText = passwordSettings.fair; indicatorClass = cssClasses.passwordFair; } else if (strength < 80) { indicatorText = passwordSettings.good; indicatorClass = cssClasses.passwordGood; } else if (strength <= 100) { indicatorText = passwordSettings.strong; indicatorClass = cssClasses.passwordStrong; } // Assemble the final message while keeping the original message array. const messageTips = msg; msg = `${passwordSettings.hasWeaknesses}`; return { strength, indicatorText, indicatorClass, messageTips, }; }; })(jQuery, Drupal);