import DomUtil from './wcfsetup/install/files/ts/WoltLabSuite/Core/Dom/Util';
import * as ColorUtil from './wcfsetup/install/files/ts/WoltLabSuite/Core/ColorUtil';
import UiDropdownSimple from './wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Dropdown/Simple';
+import "@woltlab/zxcvbn";
declare global {
interface Window {
"integrity": "sha512-zSLdgIcZXxqamFwIuogGVM22UwStYhWhHgzXXczW3GwNAv1LZdgL0XYGaufipf/FgB2Jj5jTT0ov0v5TVYnUjA==",
"dev": true
},
+ "@types/zxcvbn": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/@types/zxcvbn/-/zxcvbn-4.4.0.tgz",
+ "integrity": "sha512-GQLOT+SN20a+AI51y3fAimhyTF4Y0RG+YP3gf91OibIZ7CJmPFgoZi+ZR5a+vRbS01LbQosITWum4ATmJ1Z6Pg==",
+ "dev": true
+ },
"@typescript-eslint/eslint-plugin": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.6.0.tgz",
"eslint-visitor-keys": "^2.0.0"
}
},
+ "@woltlab/zxcvbn": {
+ "version": "git+https://github.com/WoltLab/zxcvbn.git#6868f54f9d66073c83c1789e736c6c7f358c2da7",
+ "from": "git+https://github.com/WoltLab/zxcvbn.git#master",
+ "dev": true,
+ "requires": {
+ "@types/zxcvbn": "^4.4.0"
+ }
+ },
"acorn": {
"version": "7.4.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
"@types/pica": "^5.1.1",
"@typescript-eslint/eslint-plugin": "^4.6.0",
"@typescript-eslint/parser": "^4.6.0",
+ "@woltlab/zxcvbn": "git+https://github.com/WoltLab/zxcvbn.git#master",
"eslint": "^7.12.1",
"eslint-config-prettier": "^6.15.0",
"prettier": "^2.1.2",
* @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
* @module WoltLabSuite/Core/Ui/User/PasswordStrength
*/
-define(['Core', 'Language'], function (Core, Language) {
- 'use strict';
- var STATIC_DICTIONARY = [];
- if (elBySel('meta[property="og:site_name"]')) {
- STATIC_DICTIONARY.push(elBySel('meta[property="og:site_name"]').getAttribute('content'));
+define(["require", "exports", "tslib", "../../Language", "../../Dom/Util"], function (require, exports, tslib_1, Language, Util_1) {
+ "use strict";
+ var _a;
+ Language = tslib_1.__importStar(Language);
+ Util_1 = tslib_1.__importDefault(Util_1);
+ const STATIC_DICTIONARY = [];
+ const siteName = (_a = document.querySelector('meta[property="og:site_name"]')) === null || _a === void 0 ? void 0 : _a.getAttribute("content");
+ if (siteName) {
+ STATIC_DICTIONARY.push(siteName);
}
function flatMap(array, callback) {
- return array.map(callback).reduce(function (carry, item) {
+ return array.map(callback).reduce((carry, item) => {
return carry.concat(item);
}, []);
}
return [].concat(value, value.split(/\W+/));
}
function initializeFeedbacker(Feedback) {
- var phrases = Core.extend({}, Feedback.default_phrases);
- for (var type in phrases) {
- if (phrases.hasOwnProperty(type)) {
- for (var phrase in phrases[type]) {
- if (phrases[type].hasOwnProperty(phrase)) {
- var languageItem = 'wcf.user.password.zxcvbn.' + type + '.' + phrase;
- var value = Language.get(languageItem);
- if (value !== languageItem) {
- phrases[type][phrase] = value;
- }
- }
- }
- }
- }
- return new Feedback(phrases);
- }
- /**
- * @constructor
- */
- function PasswordStrength(input, options) {
- require(['zxcvbn']).then(function (modules) {
- var zxcvbn = modules[0];
- this.init(zxcvbn, input, options);
- }.bind(this));
- }
- PasswordStrength.prototype = {
- /**
- * @param {*} zxcvbn
- * @param {Element} input
- * @param {object} options
- */
- init: function (zxcvbn, input, options) {
- this._zxcvbn = zxcvbn;
- this._input = input;
- this._options = Core.extend({
- relatedInputs: [],
- staticDictionary: []
- }, options);
- if (!this._options.feedbacker) {
- this._options.feedbacker = initializeFeedbacker(zxcvbn.Feedback);
- }
- this._wrapper = elCreate('div');
- this._wrapper.className = 'inputAddon inputAddonPasswordStrength';
- this._input.parentNode.insertBefore(this._wrapper, this._input);
- this._wrapper.appendChild(this._input);
- var rating = elCreate('div');
- rating.className = 'passwordStrengthRating';
- var ratingLabel = elCreate('small');
- ratingLabel.textContent = Language.get('wcf.user.password.strength');
- rating.appendChild(ratingLabel);
- this._score = elCreate('span');
- this._score.className = 'passwordStrengthScore';
- elData(this._score, 'score', '-1');
- rating.appendChild(this._score);
- this._wrapper.appendChild(rating);
- this._feedback = elCreate('div');
- this._feedback.className = 'passwordStrengthFeedback';
- this._wrapper.appendChild(this._feedback);
- this._verdictResult = elCreate('input');
- this._verdictResult.type = 'hidden';
- this._verdictResult.name = this._input.name + '_passwordStrengthVerdict';
- this._wrapper.parentNode.insertBefore(this._verdictResult, this._wrapper);
- var callback = this._evaluate.bind(this);
- this._input.addEventListener('input', callback);
- this._options.relatedInputs.forEach(function (input) {
- input.addEventListener('input', callback);
+ const localizedPhrases = {};
+ Object.entries(Feedback.default_phrases).forEach(([type, phrases]) => {
+ localizedPhrases[type] = {};
+ Object.entries(phrases).forEach(([identifier, phrase]) => {
+ const languageItem = `wcf.user.password.zxcvbn.${type}.${identifier}`;
+ const localizedValue = Language.get(languageItem);
+ localizedPhrases[type][identifier] = localizedValue !== languageItem ? localizedValue : phrase;
});
- if (this._input.value.trim() !== '') {
- this._evaluate();
- }
- },
- /**
- * @param {Event=} event
- */
- _evaluate: function (event) {
- var dictionary = flatMap(STATIC_DICTIONARY.concat(this._options.staticDictionary, this._options.relatedInputs.map(function (input) {
- return input.value.trim();
- })), splitIntoWords).filter(function (value) {
- return value.length > 0;
+ });
+ return new Feedback(localizedPhrases);
+ }
+ class PasswordStrength {
+ constructor(input, options) {
+ this.input = input;
+ this.wrapper = document.createElement("div");
+ this.score = document.createElement("span");
+ this.verdictResult = document.createElement("input");
+ void new Promise((resolve_1, reject_1) => { require(["zxcvbn"], resolve_1, reject_1); }).then(tslib_1.__importStar).then(({ default: zxcvbn }) => {
+ this.zxcvbn = zxcvbn;
+ if (options.relatedInputs) {
+ this.relatedInputs = options.relatedInputs;
+ }
+ if (options.staticDictionary) {
+ this.staticDictionary = options.staticDictionary;
+ }
+ this.feedbacker = initializeFeedbacker(zxcvbn.Feedback);
+ this.wrapper.className = "inputAddon inputAddonPasswordStrength";
+ this.input.parentNode.insertBefore(this.wrapper, this.input);
+ this.wrapper.appendChild(this.input);
+ const rating = document.createElement("div");
+ rating.className = "passwordStrengthRating";
+ const ratingLabel = document.createElement("small");
+ ratingLabel.textContent = Language.get("wcf.user.password.strength");
+ rating.appendChild(ratingLabel);
+ this.score.className = "passwordStrengthScore";
+ this.score.dataset.score = "-1";
+ rating.appendChild(this.score);
+ this.wrapper.appendChild(rating);
+ this.verdictResult.type = "hidden";
+ this.verdictResult.name = `${this.input.name}_passwordStrengthVerdict`;
+ this.wrapper.parentNode.insertBefore(this.verdictResult, this.wrapper);
+ this.input.addEventListener("input", (ev) => this.evaluate(ev));
+ this.relatedInputs.forEach((input) => input.addEventListener("input", (ev) => this.evaluate(ev)));
+ if (this.input.value.trim() !== "") {
+ this.evaluate();
+ }
});
- var value = this._input.value.trim();
+ }
+ evaluate(event) {
+ const dictionary = flatMap(STATIC_DICTIONARY.concat(this.staticDictionary, this.relatedInputs.map((input) => input.value.trim())), splitIntoWords).filter((value) => value.length > 0);
+ const value = this.input.value.trim();
// To bound runtime latency for really long passwords, consider sending zxcvbn() only
// the first 100 characters or so of user input.
- var verdict = this._zxcvbn(value.substr(0, 100), dictionary);
- verdict.feedback = this._options.feedbacker.from_result(verdict);
- elData(this._score, 'score', value.length === 0 ? '-1' : verdict.score);
+ const verdict = this.zxcvbn(value.substr(0, 100), dictionary);
+ verdict.feedback = this.feedbacker.from_result(verdict);
+ this.score.dataset.score = value.length === 0 ? "-1" : verdict.score.toString();
if (event !== undefined) {
// Do not overwrite the value on page load.
- elInnerError(this._wrapper, verdict.feedback.warning);
+ Util_1.default.innerError(this.wrapper, verdict.feedback.warning);
}
- this._verdictResult.value = JSON.stringify(verdict);
+ this.verdictResult.value = JSON.stringify(verdict);
}
- };
+ }
return PasswordStrength;
});
+++ /dev/null
-/**
- * Adds a password strength meter to a password input and exposes
- * zxcbn's verdict as sibling input.
- *
- * @author Tim Duesterhus
- * @copyright 2001-2020 WoltLab GmbH
- * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module WoltLabSuite/Core/Ui/User/PasswordStrength
- */
-define(['Core', 'Language'], function (Core, Language) {
- 'use strict';
-
- var STATIC_DICTIONARY = [];
- if (elBySel('meta[property="og:site_name"]')) {
- STATIC_DICTIONARY.push(elBySel('meta[property="og:site_name"]').getAttribute('content'));
- }
-
- function flatMap(array, callback) {
- return array.map(callback).reduce(function (carry, item) {
- return carry.concat(item);
- }, []);
- }
-
- function splitIntoWords(value) {
- return [].concat(value, value.split(/\W+/));
- }
-
- function initializeFeedbacker(Feedback) {
- var phrases = Core.extend({}, Feedback.default_phrases);
- for (var type in phrases) {
- if (phrases.hasOwnProperty(type)) {
- for (var phrase in phrases[type]) {
- if (phrases[type].hasOwnProperty(phrase)) {
- var languageItem = 'wcf.user.password.zxcvbn.' + type + '.' + phrase;
- var value = Language.get(languageItem);
- if (value !== languageItem) {
- phrases[type][phrase] = value;
- }
- }
- }
- }
- }
- return new Feedback(phrases);
- }
-
- /**
- * @constructor
- */
- function PasswordStrength(input, options) {
- require(['zxcvbn']).then(function (modules) {
- var zxcvbn = modules[0];
- this.init(zxcvbn, input, options);
- }.bind(this));
- }
-
- PasswordStrength.prototype = {
- /**
- * @param {*} zxcvbn
- * @param {Element} input
- * @param {object} options
- */
- init: function (zxcvbn, input, options) {
- this._zxcvbn = zxcvbn;
- this._input = input;
-
- this._options = Core.extend({
- relatedInputs: [],
- staticDictionary: []
- }, options);
-
- if (!this._options.feedbacker) {
- this._options.feedbacker = initializeFeedbacker(zxcvbn.Feedback);
- }
-
- this._wrapper = elCreate('div');
- this._wrapper.className = 'inputAddon inputAddonPasswordStrength';
- this._input.parentNode.insertBefore(this._wrapper, this._input);
- this._wrapper.appendChild(this._input);
-
- var rating = elCreate('div');
- rating.className = 'passwordStrengthRating';
-
- var ratingLabel = elCreate('small');
- ratingLabel.textContent = Language.get('wcf.user.password.strength');
- rating.appendChild(ratingLabel);
-
- this._score = elCreate('span');
- this._score.className = 'passwordStrengthScore';
- elData(this._score, 'score', '-1');
- rating.appendChild(this._score);
-
- this._wrapper.appendChild(rating);
-
- this._feedback = elCreate('div');
- this._feedback.className = 'passwordStrengthFeedback';
- this._wrapper.appendChild(this._feedback);
-
- this._verdictResult = elCreate('input');
- this._verdictResult.type = 'hidden';
- this._verdictResult.name = this._input.name + '_passwordStrengthVerdict';
- this._wrapper.parentNode.insertBefore(this._verdictResult, this._wrapper);
-
- var callback = this._evaluate.bind(this);
- this._input.addEventListener('input', callback);
- this._options.relatedInputs.forEach(function (input) {
- input.addEventListener('input', callback);
- });
-
- if (this._input.value.trim() !== '') {
- this._evaluate();
- }
- },
-
- /**
- * @param {Event=} event
- */
- _evaluate: function (event) {
- var dictionary = flatMap(STATIC_DICTIONARY.concat(this._options.staticDictionary,
- this._options.relatedInputs.map(function (input) {
- return input.value.trim();
- })
- ), splitIntoWords).filter(function (value) {
- return value.length > 0;
- });
-
- var value = this._input.value.trim();
-
- // To bound runtime latency for really long passwords, consider sending zxcvbn() only
- // the first 100 characters or so of user input.
- var verdict = this._zxcvbn(value.substr(0, 100), dictionary);
- verdict.feedback = this._options.feedbacker.from_result(verdict);
-
- elData(this._score, 'score', value.length === 0 ? '-1' : verdict.score);
-
- if (event !== undefined) {
- // Do not overwrite the value on page load.
- elInnerError(this._wrapper, verdict.feedback.warning);
- }
-
- this._verdictResult.value = JSON.stringify(verdict);
- }
- };
-
- return PasswordStrength;
-});
--- /dev/null
+/**
+ * Adds a password strength meter to a password input and exposes
+ * zxcbn's verdict as sibling input.
+ *
+ * @author Tim Duesterhus
+ * @copyright 2001-2020 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module WoltLabSuite/Core/Ui/User/PasswordStrength
+ */
+
+import * as Language from "../../Language";
+import DomUtil from "../../Dom/Util";
+
+// zxcvbn is imported for the types only. It is loaded on demand, due to its size.
+import zxcvbn from "zxcvbn";
+
+type StaticDictionary = string[];
+
+const STATIC_DICTIONARY: StaticDictionary = [];
+
+const siteName = document.querySelector('meta[property="og:site_name"]')?.getAttribute("content");
+if (siteName) {
+ STATIC_DICTIONARY.push(siteName);
+}
+
+function flatMap<T, U>(array: T[], callback: (x: T) => U[]): U[] {
+ return array.map(callback).reduce((carry, item) => {
+ return carry.concat(item);
+ }, [] as U[]);
+}
+
+function splitIntoWords(value: string): string[] {
+ return ([] as string[]).concat(value, value.split(/\W+/));
+}
+
+function initializeFeedbacker(Feedback: typeof zxcvbn.Feedback): zxcvbn.Feedback {
+ const localizedPhrases: typeof Feedback.default_phrases = {} as typeof Feedback.default_phrases;
+
+ Object.entries(Feedback.default_phrases).forEach(([type, phrases]) => {
+ localizedPhrases[type] = {};
+ Object.entries(phrases).forEach(([identifier, phrase]) => {
+ const languageItem = `wcf.user.password.zxcvbn.${type}.${identifier}`;
+ const localizedValue = Language.get(languageItem);
+ localizedPhrases[type][identifier] = localizedValue !== languageItem ? localizedValue : phrase;
+ });
+ });
+
+ return new Feedback(localizedPhrases);
+}
+
+class PasswordStrength {
+ private zxcvbn: typeof zxcvbn;
+ private relatedInputs: HTMLInputElement[];
+ private staticDictionary: StaticDictionary;
+ private feedbacker: zxcvbn.Feedback;
+
+ private readonly wrapper = document.createElement("div");
+ private readonly score = document.createElement("span");
+ private readonly verdictResult = document.createElement("input");
+
+ constructor(private readonly input: HTMLInputElement, options: Partial<Options>) {
+ void import("zxcvbn").then(({ default: zxcvbn }) => {
+ this.zxcvbn = zxcvbn;
+
+ if (options.relatedInputs) {
+ this.relatedInputs = options.relatedInputs;
+ }
+ if (options.staticDictionary) {
+ this.staticDictionary = options.staticDictionary;
+ }
+
+ this.feedbacker = initializeFeedbacker(zxcvbn.Feedback);
+
+ this.wrapper.className = "inputAddon inputAddonPasswordStrength";
+ this.input.parentNode!.insertBefore(this.wrapper, this.input);
+ this.wrapper.appendChild(this.input);
+
+ const rating = document.createElement("div");
+ rating.className = "passwordStrengthRating";
+
+ const ratingLabel = document.createElement("small");
+ ratingLabel.textContent = Language.get("wcf.user.password.strength");
+ rating.appendChild(ratingLabel);
+
+ this.score.className = "passwordStrengthScore";
+ this.score.dataset.score = "-1";
+ rating.appendChild(this.score);
+
+ this.wrapper.appendChild(rating);
+
+ this.verdictResult.type = "hidden";
+ this.verdictResult.name = `${this.input.name}_passwordStrengthVerdict`;
+ this.wrapper.parentNode!.insertBefore(this.verdictResult, this.wrapper);
+
+ this.input.addEventListener("input", (ev) => this.evaluate(ev));
+ this.relatedInputs.forEach((input) => input.addEventListener("input", (ev) => this.evaluate(ev)));
+ if (this.input.value.trim() !== "") {
+ this.evaluate();
+ }
+ });
+ }
+
+ private evaluate(event?: Event) {
+ const dictionary = flatMap(
+ STATIC_DICTIONARY.concat(
+ this.staticDictionary,
+ this.relatedInputs.map((input) => input.value.trim())
+ ),
+ splitIntoWords
+ ).filter((value) => value.length > 0);
+
+ const value = this.input.value.trim();
+
+ // To bound runtime latency for really long passwords, consider sending zxcvbn() only
+ // the first 100 characters or so of user input.
+ const verdict = this.zxcvbn(value.substr(0, 100), dictionary);
+ verdict.feedback = this.feedbacker.from_result(verdict);
+
+ this.score.dataset.score = value.length === 0 ? "-1" : verdict.score.toString();
+
+ if (event !== undefined) {
+ // Do not overwrite the value on page load.
+ DomUtil.innerError(this.wrapper, verdict.feedback.warning);
+ }
+
+ this.verdictResult.value = JSON.stringify(verdict);
+ }
+}
+
+export = PasswordStrength;
+
+interface Options {
+ relatedInputs: PasswordStrength["relatedInputs"];
+ staticDictionary: PasswordStrength["staticDictionary"];
+ feedbacker: PasswordStrength["feedbacker"];
+}