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}- ${msg.join(
Chris@17: '
- ',
Chris@17: )}
`;
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);