From d5fdf8611230af19c4bce42fc65ab614fc111539 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Tim=20D=C3=BCsterhus?= Date: Fri, 30 Oct 2020 16:48:52 +0100 Subject: [PATCH] Convert `Ui/User/PasswordStrength` to TypeScript --- global.d.ts | 1 + package-lock.json | 14 ++ package.json | 1 + .../Core/Ui/User/PasswordStrength.js | 156 ++++++++---------- .../Core/Ui/User/PasswordStrength.js | 145 ---------------- .../Core/Ui/User/PasswordStrength.ts | 136 +++++++++++++++ 6 files changed, 217 insertions(+), 236 deletions(-) delete mode 100644 wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/User/PasswordStrength.js create mode 100644 wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/User/PasswordStrength.ts diff --git a/global.d.ts b/global.d.ts index a11ef48d52..20766bfff5 100644 --- a/global.d.ts +++ b/global.d.ts @@ -3,6 +3,7 @@ import Devtools from './wcfsetup/install/files/ts/WoltLabSuite/Core/Devtools'; 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 { diff --git a/package-lock.json b/package-lock.json index b61d84e3cb..c0a4054589 100644 --- a/package-lock.json +++ b/package-lock.json @@ -113,6 +113,12 @@ "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", @@ -196,6 +202,14 @@ "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", diff --git a/package.json b/package.json index f64c55651d..18c8160b50 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "@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", diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/User/PasswordStrength.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/User/PasswordStrength.js index d0268fb835..cabb16a7ec 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/User/PasswordStrength.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/User/PasswordStrength.js @@ -7,14 +7,18 @@ * @license GNU Lesser General Public License * @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); }, []); } @@ -22,98 +26,68 @@ define(['Core', 'Language'], function (Core, Language) { 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; }); diff --git a/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/User/PasswordStrength.js b/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/User/PasswordStrength.js deleted file mode 100644 index 00a4087ea7..0000000000 --- a/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/User/PasswordStrength.js +++ /dev/null @@ -1,145 +0,0 @@ -/** - * 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 - * @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; -}); diff --git a/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/User/PasswordStrength.ts b/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/User/PasswordStrength.ts new file mode 100644 index 0000000000..7e867d6cce --- /dev/null +++ b/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/User/PasswordStrength.ts @@ -0,0 +1,136 @@ +/** + * 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 + * @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(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) { + 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"]; +} -- 2.20.1