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