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