user.es6.js 7.4 KB

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