user.es6.js 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. /**
  2. * @file
  3. * User behaviors.
  4. */
  5. (function ($, Drupal, drupalSettings) {
  6. /**
  7. * Attach handlers to evaluate the strength of any password fields and to
  8. * check that its confirmation is correct.
  9. *
  10. * @type {Drupal~behavior}
  11. *
  12. * @prop {Drupal~behaviorAttach} attach
  13. * Attaches password strength indicator and other relevant validation to
  14. * password fields.
  15. */
  16. Drupal.behaviors.password = {
  17. attach(context, settings) {
  18. const $passwordInput = $(context).find('input.js-password-field').once('password');
  19. if ($passwordInput.length) {
  20. const translate = settings.password;
  21. const $passwordInputParent = $passwordInput.parent();
  22. const $passwordInputParentWrapper = $passwordInputParent.parent();
  23. let $passwordSuggestions;
  24. // Add identifying class to password element parent.
  25. $passwordInputParent.addClass('password-parent');
  26. // Add the password confirmation layer.
  27. $passwordInputParentWrapper
  28. .find('input.js-password-confirm')
  29. .parent()
  30. .append(`<div aria-live="polite" aria-atomic="true" class="password-confirm js-password-confirm">${translate.confirmTitle} <span></span></div>`)
  31. .addClass('confirm-parent');
  32. const $confirmInput = $passwordInputParentWrapper.find('input.js-password-confirm');
  33. const $confirmResult = $passwordInputParentWrapper.find('div.js-password-confirm');
  34. const $confirmChild = $confirmResult.find('span');
  35. // If the password strength indicator is enabled, add its markup.
  36. if (settings.password.showStrengthIndicator) {
  37. const passwordMeter = `<div class="password-strength"><div class="password-strength__meter"><div class="password-strength__indicator js-password-strength__indicator"></div></div><div aria-live="polite" aria-atomic="true" class="password-strength__title">${translate.strengthTitle} <span class="password-strength__text js-password-strength__text"></span></div></div>`;
  38. $confirmInput.parent().after('<div class="password-suggestions description"></div>');
  39. $passwordInputParent.append(passwordMeter);
  40. $passwordSuggestions = $passwordInputParentWrapper.find('div.password-suggestions').hide();
  41. }
  42. // Check that password and confirmation inputs match.
  43. const passwordCheckMatch = function (confirmInputVal) {
  44. const success = $passwordInput.val() === confirmInputVal;
  45. const confirmClass = success ? 'ok' : 'error';
  46. // Fill in the success message and set the class accordingly.
  47. $confirmChild.html(translate[`confirm${success ? 'Success' : 'Failure'}`])
  48. .removeClass('ok error').addClass(confirmClass);
  49. };
  50. // Check the password strength.
  51. const passwordCheck = function () {
  52. if (settings.password.showStrengthIndicator) {
  53. // Evaluate the password strength.
  54. const result = Drupal.evaluatePasswordStrength($passwordInput.val(), settings.password);
  55. // Update the suggestions for how to improve the password.
  56. if ($passwordSuggestions.html() !== result.message) {
  57. $passwordSuggestions.html(result.message);
  58. }
  59. // Only show the description box if a weakness exists in the
  60. // password.
  61. $passwordSuggestions.toggle(result.strength !== 100);
  62. // Adjust the length of the strength indicator.
  63. $passwordInputParent.find('.js-password-strength__indicator')
  64. .css('width', `${result.strength}%`)
  65. .removeClass('is-weak is-fair is-good is-strong')
  66. .addClass(result.indicatorClass);
  67. // Update the strength indication text.
  68. $passwordInputParent.find('.js-password-strength__text').html(result.indicatorText);
  69. }
  70. // Check the value in the confirm input and show results.
  71. if ($confirmInput.val()) {
  72. passwordCheckMatch($confirmInput.val());
  73. $confirmResult.css({ visibility: 'visible' });
  74. }
  75. else {
  76. $confirmResult.css({ visibility: 'hidden' });
  77. }
  78. };
  79. // Monitor input events.
  80. $passwordInput.on('input', passwordCheck);
  81. $confirmInput.on('input', passwordCheck);
  82. }
  83. },
  84. };
  85. /**
  86. * Evaluate the strength of a user's password.
  87. *
  88. * Returns the estimated strength and the relevant output message.
  89. *
  90. * @param {string} password
  91. * The password to evaluate.
  92. * @param {object} translate
  93. * An object containing the text to display for each strength level.
  94. *
  95. * @return {object}
  96. * An object containing strength, message, indicatorText and indicatorClass.
  97. */
  98. Drupal.evaluatePasswordStrength = function (password, translate) {
  99. password = password.trim();
  100. let indicatorText;
  101. let indicatorClass;
  102. let weaknesses = 0;
  103. let strength = 100;
  104. let msg = [];
  105. const hasLowercase = /[a-z]/.test(password);
  106. const hasUppercase = /[A-Z]/.test(password);
  107. const hasNumbers = /[0-9]/.test(password);
  108. const hasPunctuation = /[^a-zA-Z0-9]/.test(password);
  109. // If there is a username edit box on the page, compare password to that,
  110. // otherwise use value from the database.
  111. const $usernameBox = $('input.username');
  112. const username = ($usernameBox.length > 0) ? $usernameBox.val() : translate.username;
  113. // Lose 5 points for every character less than 12, plus a 30 point penalty.
  114. if (password.length < 12) {
  115. msg.push(translate.tooShort);
  116. strength -= ((12 - password.length) * 5) + 30;
  117. }
  118. // Count weaknesses.
  119. if (!hasLowercase) {
  120. msg.push(translate.addLowerCase);
  121. weaknesses++;
  122. }
  123. if (!hasUppercase) {
  124. msg.push(translate.addUpperCase);
  125. weaknesses++;
  126. }
  127. if (!hasNumbers) {
  128. msg.push(translate.addNumbers);
  129. weaknesses++;
  130. }
  131. if (!hasPunctuation) {
  132. msg.push(translate.addPunctuation);
  133. weaknesses++;
  134. }
  135. // Apply penalty for each weakness (balanced against length penalty).
  136. switch (weaknesses) {
  137. case 1:
  138. strength -= 12.5;
  139. break;
  140. case 2:
  141. strength -= 25;
  142. break;
  143. case 3:
  144. strength -= 40;
  145. break;
  146. case 4:
  147. strength -= 40;
  148. break;
  149. }
  150. // Check if password is the same as the username.
  151. if (password !== '' && password.toLowerCase() === username.toLowerCase()) {
  152. msg.push(translate.sameAsUsername);
  153. // Passwords the same as username are always very weak.
  154. strength = 5;
  155. }
  156. // Based on the strength, work out what text should be shown by the
  157. // password strength meter.
  158. if (strength < 60) {
  159. indicatorText = translate.weak;
  160. indicatorClass = 'is-weak';
  161. }
  162. else if (strength < 70) {
  163. indicatorText = translate.fair;
  164. indicatorClass = 'is-fair';
  165. }
  166. else if (strength < 80) {
  167. indicatorText = translate.good;
  168. indicatorClass = 'is-good';
  169. }
  170. else if (strength <= 100) {
  171. indicatorText = translate.strong;
  172. indicatorClass = 'is-strong';
  173. }
  174. // Assemble the final message.
  175. msg = `${translate.hasWeaknesses}<ul><li>${msg.join('</li><li>')}</li></ul>`;
  176. return {
  177. strength,
  178. message: msg,
  179. indicatorText,
  180. indicatorClass,
  181. };
  182. };
  183. }(jQuery, Drupal, drupalSettings));