2 * Adds a password strength meter to a password input and exposes
3 * zxcbn's verdict as sibling input.
5 * @author Tim Duesterhus
6 * @copyright 2001-2020 WoltLab GmbH
7 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
8 * @module WoltLabSuite/Core/Ui/User/PasswordStrength
10 define(["require", "exports", "tslib", "../../Language", "../../Dom/Util"], function (require
, exports
, tslib_1
, Language
, Util_1
) {
13 Language
= tslib_1
.__importStar(Language
);
14 Util_1
= tslib_1
.__importDefault(Util_1
);
15 const STATIC_DICTIONARY
= [];
16 const siteName
= (_a
= document
.querySelector('meta[property="og:site_name"]')) === null || _a
=== void 0 ? void 0 : _a
.getAttribute("content");
18 STATIC_DICTIONARY
.push(siteName
);
20 function flatMap(array
, callback
) {
21 return array
.map(callback
).reduce((carry
, item
) => {
22 return carry
.concat(item
);
25 function splitIntoWords(value
) {
26 return [].concat(value
, value
.split(/\W+/));
28 function initializeFeedbacker(Feedback
) {
29 const localizedPhrases
= {};
30 Object
.entries(Feedback
.default_phrases
).forEach(([type
, phrases
]) => {
31 localizedPhrases
[type
] = {};
32 Object
.entries(phrases
).forEach(([identifier
, phrase
]) => {
33 const languageItem
= `wcf.user.password.zxcvbn.${type}.${identifier}`;
34 const localizedValue
= Language
.get(languageItem
);
35 localizedPhrases
[type
][identifier
] = localizedValue
!== languageItem
? localizedValue
: phrase
;
38 return new Feedback(localizedPhrases
);
40 class PasswordStrength
{
41 constructor(input
, options
) {
43 this.relatedInputs
= [];
44 this.staticDictionary
= [];
45 this.score
= document
.createElement("span");
46 this.verdictResult
= document
.createElement("input");
47 void new Promise((resolve_1
, reject_1
) => { require(["zxcvbn"], resolve_1
, reject_1
); }).then(tslib_1
.__importStar
).then(({ default: zxcvbn
}) => {
49 if (options
.relatedInputs
) {
50 this.relatedInputs
= options
.relatedInputs
;
52 if (options
.staticDictionary
) {
53 this.staticDictionary
= options
.staticDictionary
;
55 this.feedbacker
= initializeFeedbacker(zxcvbn
.Feedback
);
56 const wrapper
= this.input
.closest(".inputAddon");
57 if (wrapper
=== null) {
58 throw new Error("Expected a parent with `.inputAddon`.");
60 this.wrapper
= wrapper
;
61 this.wrapper
.classList
.add("inputAddonPasswordStrength");
62 const rating
= document
.createElement("div");
63 rating
.className
= "passwordStrengthRating";
64 const ratingLabel
= document
.createElement("small");
65 ratingLabel
.textContent
= Language
.get("wcf.user.password.strength");
66 rating
.appendChild(ratingLabel
);
67 this.score
.className
= "passwordStrengthScore";
68 this.score
.dataset
.score
= "-1";
69 rating
.appendChild(this.score
);
70 this.wrapper
.appendChild(rating
);
71 this.verdictResult
.type
= "hidden";
72 this.verdictResult
.name
= `${this.input.name}_passwordStrengthVerdict`;
73 this.wrapper
.parentNode
.insertBefore(this.verdictResult
, this.wrapper
);
74 this.input
.addEventListener("input", (ev
) => this.evaluate(ev
));
75 this.relatedInputs
.forEach((input
) => input
.addEventListener("input", (ev
) => this.evaluate(ev
)));
76 if (this.input
.value
.trim() !== "") {
82 const dictionary
= flatMap(STATIC_DICTIONARY
.concat(this.staticDictionary
, this.relatedInputs
.map((input
) => input
.value
.trim())), splitIntoWords
).filter((value
) => value
.length
> 0);
83 const value
= this.input
.value
.trim();
84 // To bound runtime latency for really long passwords, consider sending zxcvbn() only
85 // the first 100 characters or so of user input.
86 const verdict
= this.zxcvbn(value
.substr(0, 100), dictionary
);
87 verdict
.feedback
= this.feedbacker
.from_result(verdict
);
88 this.score
.dataset
.score
= value
.length
=== 0 ? "-1" : verdict
.score
.toString();
89 if (event
!== undefined) {
90 // Do not overwrite the value on page load.
91 Util_1
.default.innerError(this.wrapper
, verdict
.feedback
.warning
);
93 this.verdictResult
.value
= JSON
.stringify(verdict
);
96 return PasswordStrength
;