user.es6.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344
  1. /**
  2. * @file
  3. * Overrides Drupal core user.js that provides password strength indicator.
  4. *
  5. * @todo remove these overrides after
  6. * https://www.drupal.org/project/drupal/issues/3067523 has been resolved.
  7. */
  8. (($, Drupal) => {
  9. /**
  10. * This overrides the default Drupal.behaviors.password functionality.
  11. *
  12. * - Markup has been moved to theme functions so that to enable customizations
  13. * needed for matching Claro's design requirements
  14. * (https://www.drupal.org/project/drupal/issues/3067523).
  15. * - Modified classes so that same class names are not being used for different
  16. * elements (https://www.drupal.org/project/drupal/issues/3061265).
  17. */
  18. Drupal.behaviors.password = {
  19. attach(context, settings) {
  20. const $passwordInput = $(context)
  21. .find('input.js-password-field')
  22. .once('password');
  23. if ($passwordInput.length) {
  24. // Settings and translated messages added by
  25. // user_form_process_password_confirm().
  26. const translate = settings.password;
  27. // The form element object of the password input.
  28. const $passwordInputParent = $passwordInput.parent();
  29. // The password_confirm form element object.
  30. const $passwordWidget = $passwordInput.closest(
  31. '.js-form-type-password-confirm',
  32. );
  33. // The password confirm input.
  34. const $passwordConfirmInput = $passwordWidget.find(
  35. 'input.js-password-confirm',
  36. );
  37. // The strength feedback element for the password input.
  38. const $passwordInputHelp = $(
  39. Drupal.theme.passwordInputHelp(translate.strengthTitle),
  40. );
  41. // The password match feedback for the password confirm input.
  42. const $passwordConfirmHelp = $(
  43. Drupal.theme.passwordConfirmHelp(translate.confirmTitle),
  44. );
  45. const $passwordInputStrengthBar = $passwordInputHelp.find(
  46. '.js-password-strength-bar',
  47. );
  48. const $passwordInputStrengthMessageWrapper = $passwordInputHelp.find(
  49. '.js-password-strength-text',
  50. );
  51. const $passwordConfirmMatch = $passwordConfirmHelp.find(
  52. '.js-password-match-text',
  53. );
  54. let $passwordSuggestionsTips = $(
  55. Drupal.theme.passwordSuggestionsTips('', ''),
  56. ).hide();
  57. // If the password strength indicator is enabled, add its markup.
  58. if (settings.password.showStrengthIndicator) {
  59. $passwordConfirmInput
  60. .after($passwordConfirmHelp)
  61. .parent()
  62. .after($passwordSuggestionsTips);
  63. $passwordInputParent.append($passwordInputHelp);
  64. }
  65. // Check that password and confirmation inputs match.
  66. const passwordCheckMatch = confirmInputVal => {
  67. if (confirmInputVal) {
  68. const success = $passwordInput.val() === confirmInputVal;
  69. const confirmClass = success ? 'ok' : 'error';
  70. const confirmMatchMessage = success
  71. ? translate.confirmSuccess
  72. : translate.confirmFailure;
  73. // Update the success message and set the class accordingly if
  74. // needed.
  75. if (
  76. !$passwordConfirmMatch.hasClass(confirmClass) ||
  77. !$passwordConfirmMatch.html() === confirmMatchMessage
  78. ) {
  79. $passwordConfirmMatch
  80. .html(confirmMatchMessage)
  81. .removeClass('ok error')
  82. .addClass(confirmClass);
  83. }
  84. }
  85. };
  86. // Check the password strength.
  87. const passwordCheck = () => {
  88. if (settings.password.showStrengthIndicator) {
  89. // Evaluate the password strength.
  90. const result = Drupal.evaluatePasswordStrength(
  91. $passwordInput.val(),
  92. settings.password,
  93. );
  94. const $newSuggestions = $(
  95. Drupal.theme.passwordSuggestionsTips(
  96. translate.hasWeaknesses,
  97. result.tips,
  98. ),
  99. );
  100. // Update the suggestions for how to improve the password.
  101. if ($newSuggestions.html() !== $passwordSuggestionsTips.html()) {
  102. $passwordSuggestionsTips.replaceWith($newSuggestions);
  103. $passwordSuggestionsTips = $newSuggestions;
  104. // Only show the description box if a weakness exists in the
  105. // password.
  106. $passwordSuggestionsTips.toggle(result.strength !== 100);
  107. }
  108. // Adjust the length of the strength indicator.
  109. $passwordInputStrengthBar
  110. .css('width', `${result.strength}%`)
  111. .removeClass('is-weak is-fair is-good is-strong')
  112. .addClass(result.indicatorClass);
  113. // Update the strength indication text if needed.
  114. if (
  115. !$passwordInputStrengthMessageWrapper.hasClass(
  116. result.indicatorClass,
  117. ) ||
  118. !$passwordInputStrengthMessageWrapper.html() ===
  119. result.indicatorText
  120. ) {
  121. $passwordInputStrengthMessageWrapper
  122. .html(result.indicatorText)
  123. .removeClass('is-weak is-fair is-good is-strong')
  124. .addClass(result.indicatorClass);
  125. }
  126. }
  127. $passwordWidget
  128. .removeClass('is-initial')
  129. .removeClass('is-password-empty is-password-filled')
  130. .removeClass('is-confirm-empty is-confirm-filled');
  131. // Check the value of the password input and add the proper classes.
  132. $passwordWidget.addClass(
  133. $passwordInput.val() ? 'is-password-filled' : 'is-password-empty',
  134. );
  135. // Check the value in the confirm input and show results.
  136. passwordCheckMatch($passwordConfirmInput.val());
  137. $passwordWidget.addClass(
  138. $passwordConfirmInput.val()
  139. ? 'is-confirm-filled'
  140. : 'is-confirm-empty',
  141. );
  142. };
  143. // Add initial classes.
  144. $passwordWidget
  145. .addClass(
  146. $passwordInput.val() ? 'is-password-filled' : 'is-password-empty',
  147. )
  148. .addClass(
  149. $passwordConfirmInput.val()
  150. ? 'is-confirm-filled'
  151. : 'is-confirm-empty',
  152. );
  153. // Monitor input events.
  154. $passwordInput.on('input', passwordCheck);
  155. $passwordConfirmInput.on('input', passwordCheck);
  156. }
  157. },
  158. };
  159. /**
  160. * Override the default Drupal.evaluatePasswordStrength.
  161. *
  162. * The default implementation of this function hard codes some markup inside
  163. * this function. Rendering markup is now handled by
  164. * Drupal.behaviors.password.
  165. *
  166. * @param {string} password
  167. * Password to evaluate the strength.
  168. *
  169. * @param {Array.<string>} translate
  170. * Settings and translated messages added by user_form_process_password_confirm().
  171. *
  172. * @return {Array.<string>}
  173. * Array containing the strength, tips, indicators text and class.
  174. */
  175. Drupal.evaluatePasswordStrength = (password, translate) => {
  176. password = password.trim();
  177. let indicatorText;
  178. let indicatorClass;
  179. let weaknesses = 0;
  180. let strength = 100;
  181. const tips = [];
  182. const hasLowercase = /[a-z]/.test(password);
  183. const hasUppercase = /[A-Z]/.test(password);
  184. const hasNumbers = /[0-9]/.test(password);
  185. const hasPunctuation = /[^a-zA-Z0-9]/.test(password);
  186. // If there is a username edit box on the page, compare password to that,
  187. // otherwise use value from the database.
  188. const $usernameBox = $('input.username');
  189. const username =
  190. $usernameBox.length > 0 ? $usernameBox.val() : translate.username;
  191. // Lose 5 points for every character less than 12, plus a 30 point penalty.
  192. if (password.length < 12) {
  193. tips.push(translate.tooShort);
  194. strength -= (12 - password.length) * 5 + 30;
  195. }
  196. // Count weaknesses.
  197. if (!hasLowercase) {
  198. tips.push(translate.addLowerCase);
  199. weaknesses += 1;
  200. }
  201. if (!hasUppercase) {
  202. tips.push(translate.addUpperCase);
  203. weaknesses += 1;
  204. }
  205. if (!hasNumbers) {
  206. tips.push(translate.addNumbers);
  207. weaknesses += 1;
  208. }
  209. if (!hasPunctuation) {
  210. tips.push(translate.addPunctuation);
  211. weaknesses += 1;
  212. }
  213. // Apply penalty for each weakness (balanced against length penalty).
  214. switch (weaknesses) {
  215. case 1:
  216. strength -= 12.5;
  217. break;
  218. case 2:
  219. strength -= 25;
  220. break;
  221. case 3:
  222. strength -= 40;
  223. break;
  224. case 4:
  225. strength -= 40;
  226. break;
  227. default:
  228. // Default: 0. Nothing to do.
  229. break;
  230. }
  231. // Check if password is the same as the username.
  232. if (password !== '' && password.toLowerCase() === username.toLowerCase()) {
  233. tips.push(translate.sameAsUsername);
  234. // Passwords the same as username are always very weak.
  235. strength = 5;
  236. }
  237. // Based on the strength, work out what text should be shown by the
  238. // password strength meter.
  239. if (strength < 60) {
  240. indicatorText = translate.weak;
  241. indicatorClass = 'is-weak';
  242. } else if (strength < 70) {
  243. indicatorText = translate.fair;
  244. indicatorClass = 'is-fair';
  245. } else if (strength < 80) {
  246. indicatorText = translate.good;
  247. indicatorClass = 'is-good';
  248. } else if (strength <= 100) {
  249. indicatorText = translate.strong;
  250. indicatorClass = 'is-strong';
  251. }
  252. return {
  253. strength,
  254. tips,
  255. indicatorText,
  256. indicatorClass,
  257. };
  258. };
  259. /**
  260. * Password strength feedback for password confirm's main input.
  261. *
  262. * @param {string} message
  263. * The prefix text for the strength feedback word.
  264. *
  265. * @return {string}
  266. * The string representing the DOM fragment.
  267. */
  268. Drupal.theme.passwordInputHelp = message =>
  269. `<div class="password-strength">
  270. <div class="password-strength__track">
  271. <div class="password-strength__bar js-password-strength-bar"></div>
  272. </div>
  273. <div aria-live="polite" aria-atomic="true" class="password-strength__title">
  274. ${message} <span class="password-strength__text js-password-strength-text"></span>
  275. </div>
  276. </div>`;
  277. /**
  278. * Password match feedback for password confirm input.
  279. *
  280. * @param {string} message
  281. * The message that precedes the yes|no text.
  282. *
  283. * @return {string}
  284. * A string representing the DOM fragment.
  285. */
  286. Drupal.theme.passwordConfirmHelp = message =>
  287. `<div aria-live="polite" aria-atomic="true" class="password-match-message">${message} <span class="password-match-message__text js-password-match-text"></span></div>`;
  288. /**
  289. * Password suggestions tips.
  290. *
  291. * @param {string} title
  292. * The title that precedes tips.
  293. * @param {Array.<string>} tips
  294. * Array containing the tips.
  295. *
  296. * @return {string}
  297. * A string representing the DOM fragment.
  298. */
  299. Drupal.theme.passwordSuggestionsTips = (title, tips) =>
  300. `<div class="password-suggestions">${
  301. tips.length
  302. ? `${title}<ul class="password-suggestions__tips"><li class="password-suggestions__tip">${tips.join(
  303. '</li><li class="password-suggestions__tip">',
  304. )}</li></ul>`
  305. : ''
  306. }</div>`;
  307. })(jQuery, Drupal);