user.es6.js 7.6 KB

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