From 1aef9bad11b7f876c4b406ff9869c024e8889c40 Mon Sep 17 00:00:00 2001 From: Emanuele Feliziani Date: Mon, 11 Sep 2023 13:53:24 +0200 Subject: [PATCH 01/20] Mock getComputedStyles to speed up tests Signed-off-by: Emanuele Feliziani --- jest.setup.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/jest.setup.js b/jest.setup.js index cb87948f1..391dbd1f3 100644 --- a/jest.setup.js +++ b/jest.setup.js @@ -111,3 +111,18 @@ Object.defineProperty(window.HTMLElement.prototype, 'offsetHeight', { return this._jsdomMockOffsetHeight || 0 } }) + +// getComputedStyle is super slow on jsdom, by providing this mock we speed tests up significantly +const defaultStyle = { + display: 'block', + visibility: 'visible', + opacity: '1', + paddingRight: '10' +} +const mockGetComputedStyle = (el) => { + return { + getPropertyValue: (prop) => el.style?.[prop] || defaultStyle[prop] + } +} +// @ts-ignore +jest.spyOn(window, 'getComputedStyle').mockImplementation(mockGetComputedStyle) From 99b590b0f9d82b8ddbaea916cd942e862f8900ae Mon Sep 17 00:00:00 2001 From: Emanuele Feliziani Date: Mon, 11 Sep 2023 13:53:59 +0200 Subject: [PATCH 02/20] Batch form file reading Signed-off-by: Emanuele Feliziani --- src/Form/input-classifiers.test.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Form/input-classifiers.test.js b/src/Form/input-classifiers.test.js index 2d4c66139..edf174869 100644 --- a/src/Form/input-classifiers.test.js +++ b/src/Form/input-classifiers.test.js @@ -13,6 +13,9 @@ import {createAvailableInputTypes} from '../../integration-test/helpers/utils.js * @type {object[]} */ const testCases = JSON.parse(fs.readFileSync(path.join(__dirname, 'test-cases/index.json')).toString('utf-8')) +testCases.forEach(testCase => { + testCase.testContent = fs.readFileSync(path.resolve(__dirname, './test-cases', testCase.html), 'utf-8') +}) /** * @param {HTMLInputElement} el @@ -147,15 +150,16 @@ describe.each(testCases)('Test $html fields', (testCase) => { expectedSubmitFalsePositives = 0, expectedSubmitFalseNegatives = 0, title = '__test__', - hasExtraWrappers = true + hasExtraWrappers = true, + testContent } = testCase const testTextString = expectedFailures.length > 0 ? `should contain ${expectedFailures.length} known failure(s): ${JSON.stringify(expectedFailures)}` : `should NOT contain failures` - it(testTextString, () => { - const testContent = fs.readFileSync(path.resolve(__dirname, './test-cases', html), 'utf-8') + it.concurrent(testTextString, async () => { + document.body.innerHTML = '' let baseWrapper = document.body From 3253b7ebf464ff1a79df1f9f3b5fd648652d264d Mon Sep 17 00:00:00 2001 From: Emanuele Feliziani Date: Mon, 11 Sep 2023 13:54:45 +0200 Subject: [PATCH 03/20] Memoize isCCForm Signed-off-by: Emanuele Feliziani --- src/Form/Form.js | 4 ++++ src/Form/FormAnalyzer.js | 39 +++++++++++++++++++++++++++++++++++++++ src/Form/matching.js | 29 ++--------------------------- src/Form/matching.test.js | 18 +++++++++++++++--- 4 files changed, 60 insertions(+), 30 deletions(-) diff --git a/src/Form/Form.js b/src/Form/Form.js index 0632ed086..800c8fdd0 100644 --- a/src/Form/Form.js +++ b/src/Form/Form.js @@ -128,6 +128,9 @@ class Form { get isHybrid () { return this.formAnalyzer.isHybrid } + get isCCForm () { + return this.formAnalyzer.isCCForm() + } logFormInfo () { if (!shouldLog()) return @@ -451,6 +454,7 @@ class Form { const opts = { isLogin: this.isLogin, isHybrid: this.isHybrid, + isCCForm: this.isCCForm, hasCredentials: Boolean(this.device.settings.availableInputTypes.credentials?.username), supportsIdentitiesAutofill: this.device.settings.featureToggles.inputType_identities } diff --git a/src/Form/FormAnalyzer.js b/src/Form/FormAnalyzer.js index b87f80f38..5ce2bbe9d 100644 --- a/src/Form/FormAnalyzer.js +++ b/src/Form/FormAnalyzer.js @@ -277,6 +277,45 @@ class FormAnalyzer { } return this } + + /** @type {undefined|boolean} */ + _isCCForm = undefined + /** + * Tries to infer if it's a credit card form + * @returns {boolean} + */ + isCCForm () { + const formEl = this.form + if (this._isCCForm !== undefined) return this._isCCForm + + const ccFieldSelector = this.matching.joinCssSelectors('cc') + if (!ccFieldSelector) { + this._isCCForm = false + return this._isCCForm + } + const hasCCSelectorChild = formEl.matches(ccFieldSelector) || formEl.querySelector(ccFieldSelector) + // If the form contains one of the specific selectors, we have high confidence + if (hasCCSelectorChild) { + this._isCCForm = true + return this._isCCForm + } + + // Read form attributes to find a signal + const hasCCAttribute = [...formEl.attributes].some(({name, value}) => + /(credit|payment).?card/i.test(`${name}=${value}`) + ) + if (hasCCAttribute) { + this._isCCForm = true + return this._isCCForm + } + + // Match form textContent against common cc fields (includes hidden labels) + const textMatches = formEl.textContent?.match(/(credit|payment).?card(.?number)?|ccv|security.?code|cvv|cvc|csc/ig) + + // We check for more than one to minimise false positives + this._isCCForm = Boolean(textMatches && textMatches.length > 1) + return this._isCCForm + } } export default FormAnalyzer diff --git a/src/Form/matching.js b/src/Form/matching.js index 35b6d6600..89c1532e9 100644 --- a/src/Form/matching.js +++ b/src/Form/matching.js @@ -223,7 +223,7 @@ class Matching { // // For CC forms we run aggressive matches, so we want to make sure we only // // run them on actual CC forms to avoid false positives and expensive loops - if (this.isCCForm(formEl)) { + if (opts.isCCForm) { const subtype = this.subtypeFromMatchers('cc', input) if (subtype && isValidCreditCardSubtype(subtype)) { return `creditCards.${subtype}` @@ -278,6 +278,7 @@ class Matching { * @typedef {{ * isLogin?: boolean, * isHybrid?: boolean, + * isCCForm?: boolean, * hasCredentials?: boolean, * supportsIdentitiesAutofill?: boolean * }} SetInputTypeOpts @@ -559,32 +560,6 @@ class Matching { this.setActiveElementStrings(input, form) return this } - /** - * Tries to infer if it's a credit card form - * @param {HTMLElement} formEl - * @returns {boolean} - */ - isCCForm (formEl) { - const ccFieldSelector = this.joinCssSelectors('cc') - if (!ccFieldSelector) { - return false - } - const hasCCSelectorChild = formEl.matches(ccFieldSelector) || formEl.querySelector(ccFieldSelector) - // If the form contains one of the specific selectors, we have high confidence - if (hasCCSelectorChild) return true - - // Read form attributes to find a signal - const hasCCAttribute = [...formEl.attributes].some(({name, value}) => - /(credit|payment).?card/i.test(`${name}=${value}`) - ) - if (hasCCAttribute) return true - - // Match form textContent against common cc fields (includes hidden labels) - const textMatches = formEl.textContent?.match(/(credit|payment).?card(.?number)?|ccv|security.?code|cvv|cvc|csc/ig) - - // We check for more than one to minimise false positives - return Boolean(textMatches && textMatches.length > 1) - } /** * @type {MatchingConfiguration} diff --git a/src/Form/matching.test.js b/src/Form/matching.test.js index 146164ddb..9154a3b8d 100644 --- a/src/Form/matching.test.js +++ b/src/Form/matching.test.js @@ -92,9 +92,21 @@ describe('matching', () => { { html: ``, subtype: 'unknown' }, { html: ``, subtype: 'credentials.username' }, { html: ``, subtype: 'unknown' }, - { html: ``, subtype: 'creditCards.cardName' }, - { html: ``, subtype: 'creditCards.cardName' }, - { html: ``, subtype: 'creditCards.expirationMonth' }, + { + html: ``, + subtype: 'creditCards.cardName', + opts: {isCCForm: true} + }, + { + html: ``, + subtype: 'creditCards.cardName', + opts: {isCCForm: true} + }, + { + html: ``, + subtype: 'creditCards.expirationMonth', + opts: {isCCForm: true} + }, { html: ``, subtype: 'identities.addressPostalCode' }, { html: ``, subtype: 'identities.addressStreet2' }, { html: ``, subtype: 'identities.addressStreet' }, From 51aa77bc19f8044f229d58721b3c585cfc28b01d Mon Sep 17 00:00:00 2001 From: Emanuele Feliziani Date: Mon, 11 Sep 2023 13:56:24 +0200 Subject: [PATCH 04/20] Minimise impact of Array.from Signed-off-by: Emanuele Feliziani --- src/Scanner.js | 5 +++-- src/autofill-utils.js | 12 ++++++++---- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/Scanner.js b/src/Scanner.js index 389c1a889..3f8d5a59c 100644 --- a/src/Scanner.js +++ b/src/Scanner.js @@ -196,9 +196,10 @@ class DefaultScanner { */ addInput (input) { const parentForm = this.getParentForm(input) + const seenFormElements = [...this.forms.keys()] // Note that el.contains returns true for el itself - const previouslyFoundParent = [...this.forms.keys()].find((form) => form.contains(parentForm)) + const previouslyFoundParent = seenFormElements.find((form) => form.contains(parentForm)) if (previouslyFoundParent) { if (parentForm instanceof HTMLFormElement && parentForm !== previouslyFoundParent) { @@ -210,7 +211,7 @@ class DefaultScanner { } } else { // if this form is an ancestor of an existing form, remove that before adding this - const childForm = [...this.forms.keys()].find((form) => parentForm.contains(form)) + const childForm = seenFormElements.find((form) => parentForm.contains(form)) if (childForm) { this.forms.get(childForm)?.destroy() this.forms.delete(childForm) diff --git a/src/autofill-utils.js b/src/autofill-utils.js index 30893f29c..3bcfe525d 100644 --- a/src/autofill-utils.js +++ b/src/autofill-utils.js @@ -349,10 +349,14 @@ const getText = (el) => { return removeExcessWhitespace(el.alt || el.value || el.title || el.name) } - return removeExcessWhitespace( - Array.from(el.childNodes).reduce((text, child) => - child instanceof Text ? text + ' ' + child.textContent : text, '') - ) + let text = '' + for (const childNode of el.childNodes) { + if (childNode instanceof Text) { + text += ' ' + childNode.textContent + } + } + + return removeExcessWhitespace(text) } /** From 2338e41b19e0f151b77d0fbbf1497120153322e9 Mon Sep 17 00:00:00 2001 From: Emanuele Feliziani Date: Mon, 11 Sep 2023 13:57:14 +0200 Subject: [PATCH 05/20] Limit test logs to surface failures Signed-off-by: Emanuele Feliziani --- jest.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jest.config.js b/jest.config.js index 9ad32a7a7..4e9a969d0 100644 --- a/jest.config.js +++ b/jest.config.js @@ -14,7 +14,7 @@ module.exports = { testEnvironment: './jest-test-environment.js', // Indicates whether each individual test should be reported during the run - verbose: true, + verbose: false, // ensure snapshots are in a JSON format snapshotFormat: { From 97d4553004ff9ca9283732cfcd7e33f5ab5b8879 Mon Sep 17 00:00:00 2001 From: Emanuele Feliziani Date: Mon, 11 Sep 2023 13:58:14 +0200 Subject: [PATCH 06/20] Centralize scan stopping and disconnect observer Signed-off-by: Emanuele Feliziani --- src/DeviceInterface/ExtensionInterface.js | 7 +--- src/DeviceInterface/InterfacePrototype.js | 11 +++-- src/Form/Form.js | 1 + src/InContextSignup.js | 2 +- src/Scanner.js | 51 ++++++++++++++--------- 5 files changed, 41 insertions(+), 31 deletions(-) diff --git a/src/DeviceInterface/ExtensionInterface.js b/src/DeviceInterface/ExtensionInterface.js index d864935f6..c7f60a043 100644 --- a/src/DeviceInterface/ExtensionInterface.js +++ b/src/DeviceInterface/ExtensionInterface.js @@ -57,13 +57,8 @@ class ExtensionInterface extends InterfacePrototype { return null } - removeAutofillUIFromPage () { - super.removeAutofillUIFromPage() - this.activeForm?.removeAllDecorations() - } - async resetAutofillUI (callback) { - this.removeAutofillUIFromPage() + this.removeAutofillUIFromPage('Resetting autofill.') await this.setupAutofill() diff --git a/src/DeviceInterface/InterfacePrototype.js b/src/DeviceInterface/InterfacePrototype.js index 92ca56d5d..d3f30dffa 100644 --- a/src/DeviceInterface/InterfacePrototype.js +++ b/src/DeviceInterface/InterfacePrototype.js @@ -73,7 +73,7 @@ class InterfacePrototype { /** @type {boolean} */ isInitializationStarted; - /** @type {(()=>void) | null} */ + /** @type {((reason, ...rest) => void) | null} */ _scannerCleanup = null /** @@ -102,9 +102,12 @@ class InterfacePrototype { return new NativeUIController() } - removeAutofillUIFromPage () { + /** + * @param {string} reason + */ + removeAutofillUIFromPage (reason) { this.uiController?.destroy() - this._scannerCleanup?.() + this._scannerCleanup?.(reason) } get hasLocalAddresses () { @@ -332,7 +335,7 @@ class InterfacePrototype { postInit () { const cleanup = this.scanner.init() this.addLogoutListener(() => { - cleanup() + cleanup('Logged out') if (this.globalConfig.isDDGDomain) { notifyWebApp({ deviceSignedIn: {value: false} }) } diff --git a/src/Form/Form.js b/src/Form/Form.js index 800c8fdd0..4c9b5cb72 100644 --- a/src/Form/Form.js +++ b/src/Form/Form.js @@ -355,6 +355,7 @@ class Form { destroy () { this.removeAllDecorations() this.removeTooltip() + this.forgetAllInputs() this.mutObs.disconnect() this.matching.clear() this.intObs = null diff --git a/src/InContextSignup.js b/src/InContextSignup.js index 4c53c98d9..e776eb31f 100644 --- a/src/InContextSignup.js +++ b/src/InContextSignup.js @@ -110,7 +110,7 @@ export class InContextSignup { onIncontextSignupDismissed (options = { shouldHideTooltip: true }) { if (options.shouldHideTooltip) { - this.device.removeAutofillUIFromPage() + this.device.removeAutofillUIFromPage('Email Protection in-context signup dismissed.') this.device.deviceApi.notify(new CloseAutofillParentCall(null)) } this.permanentlyDismissedAt = new Date().getTime() diff --git a/src/Scanner.js b/src/Scanner.js index 3f8d5a59c..6c4da2640 100644 --- a/src/Scanner.js +++ b/src/Scanner.js @@ -14,7 +14,7 @@ const { /** * @typedef {{ * forms: Map; - * init(): ()=> void; + * init(): (reason, ...rest)=> void; * enqueue(elements: (HTMLElement|Document)[]): void; * findEligibleInputs(context): Scanner; * options: ScannerOptions; @@ -92,7 +92,7 @@ class DefaultScanner { * Call this to scan once and then watch for changes. * * Call the returned function to remove listeners. - * @returns {() => void} + * @returns {(reason: string, ...rest) => void} */ init () { if (this.device.globalConfig.isExtension) { @@ -106,21 +106,8 @@ class DefaultScanner { // otherwise, use the delay time to defer the initial scan setTimeout(() => this.scanAndObserve(), delay) } - return () => { - const activeInput = this.device.activeForm?.activeInput - - // remove Dax, listeners, timers, and observers - clearTimeout(this.debounceTimer) - this.mutObs.disconnect() - - this.forms.forEach(form => { - form.resetAllInputs() - form.removeAllDecorations() - }) - this.forms.clear() - - // Bring the user back to the input they were interacting with - activeInput?.focus() + return (reason, ...rest) => { + this.stopScanner(reason, ...rest) } } @@ -148,6 +135,7 @@ class DefaultScanner { } else { const inputs = context.querySelectorAll(FORM_INPUTS_SELECTOR) if (inputs.length > this.options.maxInputsPerPage) { + this.stopScanner('Too many input fields in the given context, stop scanning', context) return this } inputs.forEach((input) => this.addInput(input)) @@ -155,6 +143,31 @@ class DefaultScanner { return this } + /** + * Stops scanning, switches off the mutation observer and clears all forms + * @param {string} reason + * @param {...any} rest + */ + stopScanner (reason, ...rest) { + if (shouldLog()) { + console.log(reason, ...rest) + } + + const activeInput = this.device.activeForm?.activeInput + + // remove Dax, listeners, timers, and observers + clearTimeout(this.debounceTimer) + this.mutObs.disconnect() + + this.forms.forEach(form => { + form.destroy() + }) + this.forms.clear() + + // Bring the user back to the input they were interacting with + activeInput?.focus() + } + /** * @param {HTMLElement|HTMLInputElement|HTMLSelectElement} input * @returns {HTMLFormElement|HTMLElement} @@ -221,9 +234,7 @@ class DefaultScanner { if (this.forms.size < this.options.maxFormsPerPage) { this.forms.set(parentForm, new Form(parentForm, input, this.device, this.matching, this.shouldAutoprompt)) } else { - if (shouldLog()) { - console.log('The page has too many forms, stop adding them.') - } + this.stopScanner('The page has too many forms, stop adding them.') } } } From 87bca483d14145443aabce2d5509fb95cbdb160e Mon Sep 17 00:00:00 2001 From: Emanuele Feliziani Date: Mon, 11 Sep 2023 13:58:32 +0200 Subject: [PATCH 07/20] Update snapshots and commit compiled files Signed-off-by: Emanuele Feliziani --- dist/autofill-debug.js | 203 +++++++++++------- dist/autofill.js | 203 +++++++++++------- src/__snapshots__/Scanner.test.js.snap | 15 +- .../Resources/assets/autofill-debug.js | 203 +++++++++++------- swift-package/Resources/assets/autofill.js | 203 +++++++++++------- 5 files changed, 509 insertions(+), 318 deletions(-) diff --git a/dist/autofill-debug.js b/dist/autofill-debug.js index b830b634d..9d2b9dcfa 100644 --- a/dist/autofill-debug.js +++ b/dist/autofill-debug.js @@ -161,7 +161,7 @@ class Messaging { } /** * Send a 'fire-and-forget' message. - * @throws + * @throws {Error} * {@link MissingHandler} * * @example @@ -181,7 +181,7 @@ class Messaging { } /** * Send a request, and wait for a response - * @throws + * @throws {Error} * {@link MissingHandler} * * @example @@ -8147,15 +8147,8 @@ class ExtensionInterface extends _InterfacePrototype.default { return null; } - removeAutofillUIFromPage() { - var _this$activeForm2; - - super.removeAutofillUIFromPage(); - (_this$activeForm2 = this.activeForm) === null || _this$activeForm2 === void 0 ? void 0 : _this$activeForm2.removeAllDecorations(); - } - async resetAutofillUI(callback) { - this.removeAutofillUIFromPage(); + this.removeAutofillUIFromPage('Resetting autofill.'); await this.setupAutofill(); if (callback) await callback(); this.uiController = this.createUIController(); @@ -8193,7 +8186,7 @@ class ExtensionInterface extends _InterfacePrototype.default { switch (this.getActiveTooltipType()) { case TOOLTIP_TYPES.EmailProtection: { - var _this$activeForm3; + var _this$activeForm2; this._scannerCleanup = this.scanner.init(); this.addLogoutListener(() => { @@ -8208,12 +8201,12 @@ class ExtensionInterface extends _InterfacePrototype.default { } }); - if ((_this$activeForm3 = this.activeForm) !== null && _this$activeForm3 !== void 0 && _this$activeForm3.activeInput) { - var _this$activeForm4; + if ((_this$activeForm2 = this.activeForm) !== null && _this$activeForm2 !== void 0 && _this$activeForm2.activeInput) { + var _this$activeForm3; this.attachTooltip({ form: this.activeForm, - input: (_this$activeForm4 = this.activeForm) === null || _this$activeForm4 === void 0 ? void 0 : _this$activeForm4.activeInput, + input: (_this$activeForm3 = this.activeForm) === null || _this$activeForm3 === void 0 ? void 0 : _this$activeForm3.activeInput, click: null, trigger: 'postSignup', triggerMetaData: { @@ -8449,7 +8442,7 @@ class InterfacePrototype { /** @type {boolean} */ - /** @type {(()=>void) | null} */ + /** @type {((reason, ...rest) => void) | null} */ /** * @param {GlobalConfig} config @@ -8523,12 +8516,16 @@ class InterfacePrototype { createUIController() { return new _NativeUIController.NativeUIController(); } + /** + * @param {string} reason + */ + - removeAutofillUIFromPage() { + removeAutofillUIFromPage(reason) { var _this$uiController, _this$_scannerCleanup; (_this$uiController = this.uiController) === null || _this$uiController === void 0 ? void 0 : _this$uiController.destroy(); - (_this$_scannerCleanup = this._scannerCleanup) === null || _this$_scannerCleanup === void 0 ? void 0 : _this$_scannerCleanup.call(this); + (_this$_scannerCleanup = this._scannerCleanup) === null || _this$_scannerCleanup === void 0 ? void 0 : _this$_scannerCleanup.call(this, reason); } get hasLocalAddresses() { @@ -8768,7 +8765,7 @@ class InterfacePrototype { postInit() { const cleanup = this.scanner.init(); this.addLogoutListener(() => { - cleanup(); + cleanup('Logged out'); if (this.globalConfig.isDDGDomain) { (0, _autofillUtils.notifyWebApp)({ @@ -10220,6 +10217,10 @@ class Form { return this.formAnalyzer.isHybrid; } + get isCCForm() { + return this.formAnalyzer.isCCForm(); + } + logFormInfo() { if (!(0, _autofillUtils.shouldLog)()) return; console.log("Form type: %c".concat(this.getFormType()), 'font-weight: bold'); @@ -10476,6 +10477,7 @@ class Form { destroy() { this.removeAllDecorations(); this.removeTooltip(); + this.forgetAllInputs(); this.mutObs.disconnect(); this.matching.clear(); this.intObs = null; @@ -10578,6 +10580,7 @@ class Form { const opts = { isLogin: this.isLogin, isHybrid: this.isHybrid, + isCCForm: this.isCCForm, hasCredentials: Boolean((_this$device$settings = this.device.settings.availableInputTypes.credentials) === null || _this$device$settings === void 0 ? void 0 : _this$device$settings.username), supportsIdentitiesAutofill: this.device.settings.featureToggles.inputType_identities }; @@ -11030,6 +11033,8 @@ class FormAnalyzer { _defineProperty(this, "matching", void 0); + _defineProperty(this, "_isCCForm", undefined); + this.form = form; this.matching = matching || new _matching.Matching(_matchingConfiguration.matchingConfiguration); /** @@ -11308,6 +11313,52 @@ class FormAnalyzer { return this; } + /** @type {undefined|boolean} */ + + + /** + * Tries to infer if it's a credit card form + * @returns {boolean} + */ + isCCForm() { + var _formEl$textContent; + + const formEl = this.form; + if (this._isCCForm !== undefined) return this._isCCForm; + const ccFieldSelector = this.matching.joinCssSelectors('cc'); + + if (!ccFieldSelector) { + this._isCCForm = false; + return this._isCCForm; + } + + const hasCCSelectorChild = formEl.matches(ccFieldSelector) || formEl.querySelector(ccFieldSelector); // If the form contains one of the specific selectors, we have high confidence + + if (hasCCSelectorChild) { + this._isCCForm = true; + return this._isCCForm; + } // Read form attributes to find a signal + + + const hasCCAttribute = [...formEl.attributes].some(_ref3 => { + let { + name, + value + } = _ref3; + return /(credit|payment).?card/i.test("".concat(name, "=").concat(value)); + }); + + if (hasCCAttribute) { + this._isCCForm = true; + return this._isCCForm; + } // Match form textContent against common cc fields (includes hidden labels) + + + const textMatches = (_formEl$textContent = formEl.textContent) === null || _formEl$textContent === void 0 ? void 0 : _formEl$textContent.match(/(credit|payment).?card(.?number)?|ccv|security.?code|cvv|cvc|csc/ig); // We check for more than one to minimise false positives + + this._isCCForm = Boolean(textMatches && textMatches.length > 1); + return this._isCCForm; + } } @@ -13755,7 +13806,7 @@ class Matching { this.setActiveElementStrings(input, formEl); // // For CC forms we run aggressive matches, so we want to make sure we only // // run them on actual CC forms to avoid false positives and expensive loops - if (this.isCCForm(formEl)) { + if (opts.isCCForm) { const subtype = this.subtypeFromMatchers('cc', input); if (subtype && isValidCreditCardSubtype(subtype)) { @@ -13806,6 +13857,7 @@ class Matching { * @typedef {{ * isLogin?: boolean, * isHybrid?: boolean, + * isCCForm?: boolean, * hasCredentials?: boolean, * supportsIdentitiesAutofill?: boolean * }} SetInputTypeOpts @@ -14139,39 +14191,6 @@ class Matching { this.setActiveElementStrings(input, form); return this; } - /** - * Tries to infer if it's a credit card form - * @param {HTMLElement} formEl - * @returns {boolean} - */ - - - isCCForm(formEl) { - var _formEl$textContent; - - const ccFieldSelector = this.joinCssSelectors('cc'); - - if (!ccFieldSelector) { - return false; - } - - const hasCCSelectorChild = formEl.matches(ccFieldSelector) || formEl.querySelector(ccFieldSelector); // If the form contains one of the specific selectors, we have high confidence - - if (hasCCSelectorChild) return true; // Read form attributes to find a signal - - const hasCCAttribute = [...formEl.attributes].some(_ref => { - let { - name, - value - } = _ref; - return /(credit|payment).?card/i.test("".concat(name, "=").concat(value)); - }); - if (hasCCAttribute) return true; // Match form textContent against common cc fields (includes hidden labels) - - const textMatches = (_formEl$textContent = formEl.textContent) === null || _formEl$textContent === void 0 ? void 0 : _formEl$textContent.match(/(credit|payment).?card(.?number)?|ccv|security.?code|cvv|cvc|csc/ig); // We check for more than one to minimise false positives - - return Boolean(textMatches && textMatches.length > 1); - } /** * @type {MatchingConfiguration} */ @@ -14780,7 +14799,7 @@ class InContextSignup { }; if (options.shouldHideTooltip) { - this.device.removeAutofillUIFromPage(); + this.device.removeAutofillUIFromPage('Email Protection in-context signup dismissed.'); this.device.deviceApi.notify(new _deviceApiCalls.CloseAutofillParentCall(null)); } @@ -15251,7 +15270,7 @@ const { /** * @typedef {{ * forms: Map; - * init(): ()=> void; + * init(): (reason, ...rest)=> void; * enqueue(elements: (HTMLElement|Document)[]): void; * findEligibleInputs(context): Scanner; * options: ScannerOptions; @@ -15365,11 +15384,13 @@ class DefaultScanner { * Call this to scan once and then watch for changes. * * Call the returned function to remove listeners. - * @returns {() => void} + * @returns {(reason: string, ...rest) => void} */ init() { + var _this = this; + if (this.device.globalConfig.isExtension) { this.device.deviceApi.notify(new _deviceApiCalls.AddDebugFlagCall({ flag: 'autofill' @@ -15385,20 +15406,12 @@ class DefaultScanner { setTimeout(() => this.scanAndObserve(), delay); } - return () => { - var _this$device$activeFo; - - const activeInput = (_this$device$activeFo = this.device.activeForm) === null || _this$device$activeFo === void 0 ? void 0 : _this$device$activeFo.activeInput; // remove Dax, listeners, timers, and observers - - clearTimeout(this.debounceTimer); - this.mutObs.disconnect(); - this.forms.forEach(form => { - form.resetAllInputs(); - form.removeAllDecorations(); - }); - this.forms.clear(); // Bring the user back to the input they were interacting with + return function (reason) { + for (var _len = arguments.length, rest = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { + rest[_key - 1] = arguments[_key]; + } - activeInput === null || activeInput === void 0 ? void 0 : activeInput.focus(); + _this.stopScanner(reason, ...rest); }; } /** @@ -15436,6 +15449,7 @@ class DefaultScanner { const inputs = context.querySelectorAll(_selectorsCss.FORM_INPUTS_SELECTOR); if (inputs.length > this.options.maxInputsPerPage) { + this.stopScanner('Too many input fields in the given context, stop scanning', context); return this; } @@ -15444,6 +15458,35 @@ class DefaultScanner { return this; } + /** + * Stops scanning, switches off the mutation observer and clears all forms + * @param {string} reason + * @param {...any} rest + */ + + + stopScanner(reason) { + var _this$device$activeFo; + + if ((0, _autofillUtils.shouldLog)()) { + for (var _len2 = arguments.length, rest = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) { + rest[_key2 - 1] = arguments[_key2]; + } + + console.log(reason, ...rest); + } + + const activeInput = (_this$device$activeFo = this.device.activeForm) === null || _this$device$activeFo === void 0 ? void 0 : _this$device$activeFo.activeInput; // remove Dax, listeners, timers, and observers + + clearTimeout(this.debounceTimer); + this.mutObs.disconnect(); + this.forms.forEach(form => { + form.destroy(); + }); + this.forms.clear(); // Bring the user back to the input they were interacting with + + activeInput === null || activeInput === void 0 ? void 0 : activeInput.focus(); + } /** * @param {HTMLElement|HTMLInputElement|HTMLSelectElement} input * @returns {HTMLFormElement|HTMLElement} @@ -15489,9 +15532,10 @@ class DefaultScanner { addInput(input) { - const parentForm = this.getParentForm(input); // Note that el.contains returns true for el itself + const parentForm = this.getParentForm(input); + const seenFormElements = [...this.forms.keys()]; // Note that el.contains returns true for el itself - const previouslyFoundParent = [...this.forms.keys()].find(form => form.contains(parentForm)); + const previouslyFoundParent = seenFormElements.find(form => form.contains(parentForm)); if (previouslyFoundParent) { if (parentForm instanceof HTMLFormElement && parentForm !== previouslyFoundParent) { @@ -15505,7 +15549,7 @@ class DefaultScanner { } } else { // if this form is an ancestor of an existing form, remove that before adding this - const childForm = [...this.forms.keys()].find(form => parentForm.contains(form)); + const childForm = seenFormElements.find(form => parentForm.contains(form)); if (childForm) { var _this$forms$get2; @@ -15518,9 +15562,7 @@ class DefaultScanner { if (this.forms.size < this.options.maxFormsPerPage) { this.forms.set(parentForm, new _Form.Form(parentForm, input, this.device, this.matching, this.shouldAutoprompt)); } else { - if ((0, _autofillUtils.shouldLog)()) { - console.log('The page has too many forms, stop adding them.'); - } + this.stopScanner('The page has too many forms, stop adding them.'); } } } @@ -18164,7 +18206,15 @@ const getText = el => { return (0, _matching.removeExcessWhitespace)(el.alt || el.value || el.title || el.name); } - return (0, _matching.removeExcessWhitespace)(Array.from(el.childNodes).reduce((text, child) => child instanceof Text ? text + ' ' + child.textContent : text, '')); + let text = ''; + + for (const childNode of el.childNodes) { + if (childNode instanceof Text) { + text += ' ' + childNode.textContent; + } + } + + return (0, _matching.removeExcessWhitespace)(text); }; /** * Check if hostname is a local address @@ -18299,9 +18349,8 @@ var _autofillUtils = require("./autofill-utils.js"); (() => { if ((0, _autofillUtils.shouldLog)()) { console.log('DuckDuckGo Autofill Active'); - } + } // if (!window.isSecureContext) return false - if (!window.isSecureContext) return false; try { const startupAutofill = () => { diff --git a/dist/autofill.js b/dist/autofill.js index 6923dae24..6e3644788 100644 --- a/dist/autofill.js +++ b/dist/autofill.js @@ -161,7 +161,7 @@ class Messaging { } /** * Send a 'fire-and-forget' message. - * @throws + * @throws {Error} * {@link MissingHandler} * * @example @@ -181,7 +181,7 @@ class Messaging { } /** * Send a request, and wait for a response - * @throws + * @throws {Error} * {@link MissingHandler} * * @example @@ -4471,15 +4471,8 @@ class ExtensionInterface extends _InterfacePrototype.default { return null; } - removeAutofillUIFromPage() { - var _this$activeForm2; - - super.removeAutofillUIFromPage(); - (_this$activeForm2 = this.activeForm) === null || _this$activeForm2 === void 0 ? void 0 : _this$activeForm2.removeAllDecorations(); - } - async resetAutofillUI(callback) { - this.removeAutofillUIFromPage(); + this.removeAutofillUIFromPage('Resetting autofill.'); await this.setupAutofill(); if (callback) await callback(); this.uiController = this.createUIController(); @@ -4517,7 +4510,7 @@ class ExtensionInterface extends _InterfacePrototype.default { switch (this.getActiveTooltipType()) { case TOOLTIP_TYPES.EmailProtection: { - var _this$activeForm3; + var _this$activeForm2; this._scannerCleanup = this.scanner.init(); this.addLogoutListener(() => { @@ -4532,12 +4525,12 @@ class ExtensionInterface extends _InterfacePrototype.default { } }); - if ((_this$activeForm3 = this.activeForm) !== null && _this$activeForm3 !== void 0 && _this$activeForm3.activeInput) { - var _this$activeForm4; + if ((_this$activeForm2 = this.activeForm) !== null && _this$activeForm2 !== void 0 && _this$activeForm2.activeInput) { + var _this$activeForm3; this.attachTooltip({ form: this.activeForm, - input: (_this$activeForm4 = this.activeForm) === null || _this$activeForm4 === void 0 ? void 0 : _this$activeForm4.activeInput, + input: (_this$activeForm3 = this.activeForm) === null || _this$activeForm3 === void 0 ? void 0 : _this$activeForm3.activeInput, click: null, trigger: 'postSignup', triggerMetaData: { @@ -4773,7 +4766,7 @@ class InterfacePrototype { /** @type {boolean} */ - /** @type {(()=>void) | null} */ + /** @type {((reason, ...rest) => void) | null} */ /** * @param {GlobalConfig} config @@ -4847,12 +4840,16 @@ class InterfacePrototype { createUIController() { return new _NativeUIController.NativeUIController(); } + /** + * @param {string} reason + */ + - removeAutofillUIFromPage() { + removeAutofillUIFromPage(reason) { var _this$uiController, _this$_scannerCleanup; (_this$uiController = this.uiController) === null || _this$uiController === void 0 ? void 0 : _this$uiController.destroy(); - (_this$_scannerCleanup = this._scannerCleanup) === null || _this$_scannerCleanup === void 0 ? void 0 : _this$_scannerCleanup.call(this); + (_this$_scannerCleanup = this._scannerCleanup) === null || _this$_scannerCleanup === void 0 ? void 0 : _this$_scannerCleanup.call(this, reason); } get hasLocalAddresses() { @@ -5092,7 +5089,7 @@ class InterfacePrototype { postInit() { const cleanup = this.scanner.init(); this.addLogoutListener(() => { - cleanup(); + cleanup('Logged out'); if (this.globalConfig.isDDGDomain) { (0, _autofillUtils.notifyWebApp)({ @@ -6544,6 +6541,10 @@ class Form { return this.formAnalyzer.isHybrid; } + get isCCForm() { + return this.formAnalyzer.isCCForm(); + } + logFormInfo() { if (!(0, _autofillUtils.shouldLog)()) return; console.log("Form type: %c".concat(this.getFormType()), 'font-weight: bold'); @@ -6800,6 +6801,7 @@ class Form { destroy() { this.removeAllDecorations(); this.removeTooltip(); + this.forgetAllInputs(); this.mutObs.disconnect(); this.matching.clear(); this.intObs = null; @@ -6902,6 +6904,7 @@ class Form { const opts = { isLogin: this.isLogin, isHybrid: this.isHybrid, + isCCForm: this.isCCForm, hasCredentials: Boolean((_this$device$settings = this.device.settings.availableInputTypes.credentials) === null || _this$device$settings === void 0 ? void 0 : _this$device$settings.username), supportsIdentitiesAutofill: this.device.settings.featureToggles.inputType_identities }; @@ -7354,6 +7357,8 @@ class FormAnalyzer { _defineProperty(this, "matching", void 0); + _defineProperty(this, "_isCCForm", undefined); + this.form = form; this.matching = matching || new _matching.Matching(_matchingConfiguration.matchingConfiguration); /** @@ -7632,6 +7637,52 @@ class FormAnalyzer { return this; } + /** @type {undefined|boolean} */ + + + /** + * Tries to infer if it's a credit card form + * @returns {boolean} + */ + isCCForm() { + var _formEl$textContent; + + const formEl = this.form; + if (this._isCCForm !== undefined) return this._isCCForm; + const ccFieldSelector = this.matching.joinCssSelectors('cc'); + + if (!ccFieldSelector) { + this._isCCForm = false; + return this._isCCForm; + } + + const hasCCSelectorChild = formEl.matches(ccFieldSelector) || formEl.querySelector(ccFieldSelector); // If the form contains one of the specific selectors, we have high confidence + + if (hasCCSelectorChild) { + this._isCCForm = true; + return this._isCCForm; + } // Read form attributes to find a signal + + + const hasCCAttribute = [...formEl.attributes].some(_ref3 => { + let { + name, + value + } = _ref3; + return /(credit|payment).?card/i.test("".concat(name, "=").concat(value)); + }); + + if (hasCCAttribute) { + this._isCCForm = true; + return this._isCCForm; + } // Match form textContent against common cc fields (includes hidden labels) + + + const textMatches = (_formEl$textContent = formEl.textContent) === null || _formEl$textContent === void 0 ? void 0 : _formEl$textContent.match(/(credit|payment).?card(.?number)?|ccv|security.?code|cvv|cvc|csc/ig); // We check for more than one to minimise false positives + + this._isCCForm = Boolean(textMatches && textMatches.length > 1); + return this._isCCForm; + } } @@ -10079,7 +10130,7 @@ class Matching { this.setActiveElementStrings(input, formEl); // // For CC forms we run aggressive matches, so we want to make sure we only // // run them on actual CC forms to avoid false positives and expensive loops - if (this.isCCForm(formEl)) { + if (opts.isCCForm) { const subtype = this.subtypeFromMatchers('cc', input); if (subtype && isValidCreditCardSubtype(subtype)) { @@ -10130,6 +10181,7 @@ class Matching { * @typedef {{ * isLogin?: boolean, * isHybrid?: boolean, + * isCCForm?: boolean, * hasCredentials?: boolean, * supportsIdentitiesAutofill?: boolean * }} SetInputTypeOpts @@ -10463,39 +10515,6 @@ class Matching { this.setActiveElementStrings(input, form); return this; } - /** - * Tries to infer if it's a credit card form - * @param {HTMLElement} formEl - * @returns {boolean} - */ - - - isCCForm(formEl) { - var _formEl$textContent; - - const ccFieldSelector = this.joinCssSelectors('cc'); - - if (!ccFieldSelector) { - return false; - } - - const hasCCSelectorChild = formEl.matches(ccFieldSelector) || formEl.querySelector(ccFieldSelector); // If the form contains one of the specific selectors, we have high confidence - - if (hasCCSelectorChild) return true; // Read form attributes to find a signal - - const hasCCAttribute = [...formEl.attributes].some(_ref => { - let { - name, - value - } = _ref; - return /(credit|payment).?card/i.test("".concat(name, "=").concat(value)); - }); - if (hasCCAttribute) return true; // Match form textContent against common cc fields (includes hidden labels) - - const textMatches = (_formEl$textContent = formEl.textContent) === null || _formEl$textContent === void 0 ? void 0 : _formEl$textContent.match(/(credit|payment).?card(.?number)?|ccv|security.?code|cvv|cvc|csc/ig); // We check for more than one to minimise false positives - - return Boolean(textMatches && textMatches.length > 1); - } /** * @type {MatchingConfiguration} */ @@ -11104,7 +11123,7 @@ class InContextSignup { }; if (options.shouldHideTooltip) { - this.device.removeAutofillUIFromPage(); + this.device.removeAutofillUIFromPage('Email Protection in-context signup dismissed.'); this.device.deviceApi.notify(new _deviceApiCalls.CloseAutofillParentCall(null)); } @@ -11575,7 +11594,7 @@ const { /** * @typedef {{ * forms: Map; - * init(): ()=> void; + * init(): (reason, ...rest)=> void; * enqueue(elements: (HTMLElement|Document)[]): void; * findEligibleInputs(context): Scanner; * options: ScannerOptions; @@ -11689,11 +11708,13 @@ class DefaultScanner { * Call this to scan once and then watch for changes. * * Call the returned function to remove listeners. - * @returns {() => void} + * @returns {(reason: string, ...rest) => void} */ init() { + var _this = this; + if (this.device.globalConfig.isExtension) { this.device.deviceApi.notify(new _deviceApiCalls.AddDebugFlagCall({ flag: 'autofill' @@ -11709,20 +11730,12 @@ class DefaultScanner { setTimeout(() => this.scanAndObserve(), delay); } - return () => { - var _this$device$activeFo; - - const activeInput = (_this$device$activeFo = this.device.activeForm) === null || _this$device$activeFo === void 0 ? void 0 : _this$device$activeFo.activeInput; // remove Dax, listeners, timers, and observers - - clearTimeout(this.debounceTimer); - this.mutObs.disconnect(); - this.forms.forEach(form => { - form.resetAllInputs(); - form.removeAllDecorations(); - }); - this.forms.clear(); // Bring the user back to the input they were interacting with + return function (reason) { + for (var _len = arguments.length, rest = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { + rest[_key - 1] = arguments[_key]; + } - activeInput === null || activeInput === void 0 ? void 0 : activeInput.focus(); + _this.stopScanner(reason, ...rest); }; } /** @@ -11760,6 +11773,7 @@ class DefaultScanner { const inputs = context.querySelectorAll(_selectorsCss.FORM_INPUTS_SELECTOR); if (inputs.length > this.options.maxInputsPerPage) { + this.stopScanner('Too many input fields in the given context, stop scanning', context); return this; } @@ -11768,6 +11782,35 @@ class DefaultScanner { return this; } + /** + * Stops scanning, switches off the mutation observer and clears all forms + * @param {string} reason + * @param {...any} rest + */ + + + stopScanner(reason) { + var _this$device$activeFo; + + if ((0, _autofillUtils.shouldLog)()) { + for (var _len2 = arguments.length, rest = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) { + rest[_key2 - 1] = arguments[_key2]; + } + + console.log(reason, ...rest); + } + + const activeInput = (_this$device$activeFo = this.device.activeForm) === null || _this$device$activeFo === void 0 ? void 0 : _this$device$activeFo.activeInput; // remove Dax, listeners, timers, and observers + + clearTimeout(this.debounceTimer); + this.mutObs.disconnect(); + this.forms.forEach(form => { + form.destroy(); + }); + this.forms.clear(); // Bring the user back to the input they were interacting with + + activeInput === null || activeInput === void 0 ? void 0 : activeInput.focus(); + } /** * @param {HTMLElement|HTMLInputElement|HTMLSelectElement} input * @returns {HTMLFormElement|HTMLElement} @@ -11813,9 +11856,10 @@ class DefaultScanner { addInput(input) { - const parentForm = this.getParentForm(input); // Note that el.contains returns true for el itself + const parentForm = this.getParentForm(input); + const seenFormElements = [...this.forms.keys()]; // Note that el.contains returns true for el itself - const previouslyFoundParent = [...this.forms.keys()].find(form => form.contains(parentForm)); + const previouslyFoundParent = seenFormElements.find(form => form.contains(parentForm)); if (previouslyFoundParent) { if (parentForm instanceof HTMLFormElement && parentForm !== previouslyFoundParent) { @@ -11829,7 +11873,7 @@ class DefaultScanner { } } else { // if this form is an ancestor of an existing form, remove that before adding this - const childForm = [...this.forms.keys()].find(form => parentForm.contains(form)); + const childForm = seenFormElements.find(form => parentForm.contains(form)); if (childForm) { var _this$forms$get2; @@ -11842,9 +11886,7 @@ class DefaultScanner { if (this.forms.size < this.options.maxFormsPerPage) { this.forms.set(parentForm, new _Form.Form(parentForm, input, this.device, this.matching, this.shouldAutoprompt)); } else { - if ((0, _autofillUtils.shouldLog)()) { - console.log('The page has too many forms, stop adding them.'); - } + this.stopScanner('The page has too many forms, stop adding them.'); } } } @@ -14488,7 +14530,15 @@ const getText = el => { return (0, _matching.removeExcessWhitespace)(el.alt || el.value || el.title || el.name); } - return (0, _matching.removeExcessWhitespace)(Array.from(el.childNodes).reduce((text, child) => child instanceof Text ? text + ' ' + child.textContent : text, '')); + let text = ''; + + for (const childNode of el.childNodes) { + if (childNode instanceof Text) { + text += ' ' + childNode.textContent; + } + } + + return (0, _matching.removeExcessWhitespace)(text); }; /** * Check if hostname is a local address @@ -14623,9 +14673,8 @@ var _autofillUtils = require("./autofill-utils.js"); (() => { if ((0, _autofillUtils.shouldLog)()) { console.log('DuckDuckGo Autofill Active'); - } + } // if (!window.isSecureContext) return false - if (!window.isSecureContext) return false; try { const startupAutofill = () => { diff --git a/src/__snapshots__/Scanner.test.js.snap b/src/__snapshots__/Scanner.test.js.snap index 100c91d5e..45b263093 100644 --- a/src/__snapshots__/Scanner.test.js.snap +++ b/src/__snapshots__/Scanner.test.js.snap @@ -212,7 +212,6 @@ exports[`performance should stop scanning if page grows above maximum forms 1`] for="input-01" > @@ -223,7 +222,6 @@ exports[`performance should stop scanning if page grows above maximum forms 1`] for="input-02" > @@ -234,7 +232,6 @@ exports[`performance should stop scanning if page grows above maximum forms 1`] for="input-03" > @@ -245,7 +242,6 @@ exports[`performance should stop scanning if page grows above maximum forms 1`] for="input-04" > @@ -256,7 +252,6 @@ exports[`performance should stop scanning if page grows above maximum forms 1`] for="input-05" > @@ -273,6 +268,7 @@ exports[`performance should stop scanning if page grows above maximum forms 1`] for="input-01" > @@ -283,6 +279,7 @@ exports[`performance should stop scanning if page grows above maximum forms 1`] for="input-02" > @@ -293,6 +290,7 @@ exports[`performance should stop scanning if page grows above maximum forms 1`] for="input-03" > @@ -303,6 +301,7 @@ exports[`performance should stop scanning if page grows above maximum forms 1`] for="input-04" > @@ -313,6 +312,7 @@ exports[`performance should stop scanning if page grows above maximum forms 1`] for="input-05" > @@ -336,7 +336,6 @@ exports[`performance should stop scanning if page grows above maximum inputs 1`] for="input-01" > @@ -347,7 +346,6 @@ exports[`performance should stop scanning if page grows above maximum inputs 1`] for="input-02" > @@ -358,7 +356,6 @@ exports[`performance should stop scanning if page grows above maximum inputs 1`] for="input-03" > @@ -369,7 +366,6 @@ exports[`performance should stop scanning if page grows above maximum inputs 1`] for="input-04" > @@ -380,7 +376,6 @@ exports[`performance should stop scanning if page grows above maximum inputs 1`] for="input-05" > diff --git a/swift-package/Resources/assets/autofill-debug.js b/swift-package/Resources/assets/autofill-debug.js index b830b634d..9d2b9dcfa 100644 --- a/swift-package/Resources/assets/autofill-debug.js +++ b/swift-package/Resources/assets/autofill-debug.js @@ -161,7 +161,7 @@ class Messaging { } /** * Send a 'fire-and-forget' message. - * @throws + * @throws {Error} * {@link MissingHandler} * * @example @@ -181,7 +181,7 @@ class Messaging { } /** * Send a request, and wait for a response - * @throws + * @throws {Error} * {@link MissingHandler} * * @example @@ -8147,15 +8147,8 @@ class ExtensionInterface extends _InterfacePrototype.default { return null; } - removeAutofillUIFromPage() { - var _this$activeForm2; - - super.removeAutofillUIFromPage(); - (_this$activeForm2 = this.activeForm) === null || _this$activeForm2 === void 0 ? void 0 : _this$activeForm2.removeAllDecorations(); - } - async resetAutofillUI(callback) { - this.removeAutofillUIFromPage(); + this.removeAutofillUIFromPage('Resetting autofill.'); await this.setupAutofill(); if (callback) await callback(); this.uiController = this.createUIController(); @@ -8193,7 +8186,7 @@ class ExtensionInterface extends _InterfacePrototype.default { switch (this.getActiveTooltipType()) { case TOOLTIP_TYPES.EmailProtection: { - var _this$activeForm3; + var _this$activeForm2; this._scannerCleanup = this.scanner.init(); this.addLogoutListener(() => { @@ -8208,12 +8201,12 @@ class ExtensionInterface extends _InterfacePrototype.default { } }); - if ((_this$activeForm3 = this.activeForm) !== null && _this$activeForm3 !== void 0 && _this$activeForm3.activeInput) { - var _this$activeForm4; + if ((_this$activeForm2 = this.activeForm) !== null && _this$activeForm2 !== void 0 && _this$activeForm2.activeInput) { + var _this$activeForm3; this.attachTooltip({ form: this.activeForm, - input: (_this$activeForm4 = this.activeForm) === null || _this$activeForm4 === void 0 ? void 0 : _this$activeForm4.activeInput, + input: (_this$activeForm3 = this.activeForm) === null || _this$activeForm3 === void 0 ? void 0 : _this$activeForm3.activeInput, click: null, trigger: 'postSignup', triggerMetaData: { @@ -8449,7 +8442,7 @@ class InterfacePrototype { /** @type {boolean} */ - /** @type {(()=>void) | null} */ + /** @type {((reason, ...rest) => void) | null} */ /** * @param {GlobalConfig} config @@ -8523,12 +8516,16 @@ class InterfacePrototype { createUIController() { return new _NativeUIController.NativeUIController(); } + /** + * @param {string} reason + */ + - removeAutofillUIFromPage() { + removeAutofillUIFromPage(reason) { var _this$uiController, _this$_scannerCleanup; (_this$uiController = this.uiController) === null || _this$uiController === void 0 ? void 0 : _this$uiController.destroy(); - (_this$_scannerCleanup = this._scannerCleanup) === null || _this$_scannerCleanup === void 0 ? void 0 : _this$_scannerCleanup.call(this); + (_this$_scannerCleanup = this._scannerCleanup) === null || _this$_scannerCleanup === void 0 ? void 0 : _this$_scannerCleanup.call(this, reason); } get hasLocalAddresses() { @@ -8768,7 +8765,7 @@ class InterfacePrototype { postInit() { const cleanup = this.scanner.init(); this.addLogoutListener(() => { - cleanup(); + cleanup('Logged out'); if (this.globalConfig.isDDGDomain) { (0, _autofillUtils.notifyWebApp)({ @@ -10220,6 +10217,10 @@ class Form { return this.formAnalyzer.isHybrid; } + get isCCForm() { + return this.formAnalyzer.isCCForm(); + } + logFormInfo() { if (!(0, _autofillUtils.shouldLog)()) return; console.log("Form type: %c".concat(this.getFormType()), 'font-weight: bold'); @@ -10476,6 +10477,7 @@ class Form { destroy() { this.removeAllDecorations(); this.removeTooltip(); + this.forgetAllInputs(); this.mutObs.disconnect(); this.matching.clear(); this.intObs = null; @@ -10578,6 +10580,7 @@ class Form { const opts = { isLogin: this.isLogin, isHybrid: this.isHybrid, + isCCForm: this.isCCForm, hasCredentials: Boolean((_this$device$settings = this.device.settings.availableInputTypes.credentials) === null || _this$device$settings === void 0 ? void 0 : _this$device$settings.username), supportsIdentitiesAutofill: this.device.settings.featureToggles.inputType_identities }; @@ -11030,6 +11033,8 @@ class FormAnalyzer { _defineProperty(this, "matching", void 0); + _defineProperty(this, "_isCCForm", undefined); + this.form = form; this.matching = matching || new _matching.Matching(_matchingConfiguration.matchingConfiguration); /** @@ -11308,6 +11313,52 @@ class FormAnalyzer { return this; } + /** @type {undefined|boolean} */ + + + /** + * Tries to infer if it's a credit card form + * @returns {boolean} + */ + isCCForm() { + var _formEl$textContent; + + const formEl = this.form; + if (this._isCCForm !== undefined) return this._isCCForm; + const ccFieldSelector = this.matching.joinCssSelectors('cc'); + + if (!ccFieldSelector) { + this._isCCForm = false; + return this._isCCForm; + } + + const hasCCSelectorChild = formEl.matches(ccFieldSelector) || formEl.querySelector(ccFieldSelector); // If the form contains one of the specific selectors, we have high confidence + + if (hasCCSelectorChild) { + this._isCCForm = true; + return this._isCCForm; + } // Read form attributes to find a signal + + + const hasCCAttribute = [...formEl.attributes].some(_ref3 => { + let { + name, + value + } = _ref3; + return /(credit|payment).?card/i.test("".concat(name, "=").concat(value)); + }); + + if (hasCCAttribute) { + this._isCCForm = true; + return this._isCCForm; + } // Match form textContent against common cc fields (includes hidden labels) + + + const textMatches = (_formEl$textContent = formEl.textContent) === null || _formEl$textContent === void 0 ? void 0 : _formEl$textContent.match(/(credit|payment).?card(.?number)?|ccv|security.?code|cvv|cvc|csc/ig); // We check for more than one to minimise false positives + + this._isCCForm = Boolean(textMatches && textMatches.length > 1); + return this._isCCForm; + } } @@ -13755,7 +13806,7 @@ class Matching { this.setActiveElementStrings(input, formEl); // // For CC forms we run aggressive matches, so we want to make sure we only // // run them on actual CC forms to avoid false positives and expensive loops - if (this.isCCForm(formEl)) { + if (opts.isCCForm) { const subtype = this.subtypeFromMatchers('cc', input); if (subtype && isValidCreditCardSubtype(subtype)) { @@ -13806,6 +13857,7 @@ class Matching { * @typedef {{ * isLogin?: boolean, * isHybrid?: boolean, + * isCCForm?: boolean, * hasCredentials?: boolean, * supportsIdentitiesAutofill?: boolean * }} SetInputTypeOpts @@ -14139,39 +14191,6 @@ class Matching { this.setActiveElementStrings(input, form); return this; } - /** - * Tries to infer if it's a credit card form - * @param {HTMLElement} formEl - * @returns {boolean} - */ - - - isCCForm(formEl) { - var _formEl$textContent; - - const ccFieldSelector = this.joinCssSelectors('cc'); - - if (!ccFieldSelector) { - return false; - } - - const hasCCSelectorChild = formEl.matches(ccFieldSelector) || formEl.querySelector(ccFieldSelector); // If the form contains one of the specific selectors, we have high confidence - - if (hasCCSelectorChild) return true; // Read form attributes to find a signal - - const hasCCAttribute = [...formEl.attributes].some(_ref => { - let { - name, - value - } = _ref; - return /(credit|payment).?card/i.test("".concat(name, "=").concat(value)); - }); - if (hasCCAttribute) return true; // Match form textContent against common cc fields (includes hidden labels) - - const textMatches = (_formEl$textContent = formEl.textContent) === null || _formEl$textContent === void 0 ? void 0 : _formEl$textContent.match(/(credit|payment).?card(.?number)?|ccv|security.?code|cvv|cvc|csc/ig); // We check for more than one to minimise false positives - - return Boolean(textMatches && textMatches.length > 1); - } /** * @type {MatchingConfiguration} */ @@ -14780,7 +14799,7 @@ class InContextSignup { }; if (options.shouldHideTooltip) { - this.device.removeAutofillUIFromPage(); + this.device.removeAutofillUIFromPage('Email Protection in-context signup dismissed.'); this.device.deviceApi.notify(new _deviceApiCalls.CloseAutofillParentCall(null)); } @@ -15251,7 +15270,7 @@ const { /** * @typedef {{ * forms: Map; - * init(): ()=> void; + * init(): (reason, ...rest)=> void; * enqueue(elements: (HTMLElement|Document)[]): void; * findEligibleInputs(context): Scanner; * options: ScannerOptions; @@ -15365,11 +15384,13 @@ class DefaultScanner { * Call this to scan once and then watch for changes. * * Call the returned function to remove listeners. - * @returns {() => void} + * @returns {(reason: string, ...rest) => void} */ init() { + var _this = this; + if (this.device.globalConfig.isExtension) { this.device.deviceApi.notify(new _deviceApiCalls.AddDebugFlagCall({ flag: 'autofill' @@ -15385,20 +15406,12 @@ class DefaultScanner { setTimeout(() => this.scanAndObserve(), delay); } - return () => { - var _this$device$activeFo; - - const activeInput = (_this$device$activeFo = this.device.activeForm) === null || _this$device$activeFo === void 0 ? void 0 : _this$device$activeFo.activeInput; // remove Dax, listeners, timers, and observers - - clearTimeout(this.debounceTimer); - this.mutObs.disconnect(); - this.forms.forEach(form => { - form.resetAllInputs(); - form.removeAllDecorations(); - }); - this.forms.clear(); // Bring the user back to the input they were interacting with + return function (reason) { + for (var _len = arguments.length, rest = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { + rest[_key - 1] = arguments[_key]; + } - activeInput === null || activeInput === void 0 ? void 0 : activeInput.focus(); + _this.stopScanner(reason, ...rest); }; } /** @@ -15436,6 +15449,7 @@ class DefaultScanner { const inputs = context.querySelectorAll(_selectorsCss.FORM_INPUTS_SELECTOR); if (inputs.length > this.options.maxInputsPerPage) { + this.stopScanner('Too many input fields in the given context, stop scanning', context); return this; } @@ -15444,6 +15458,35 @@ class DefaultScanner { return this; } + /** + * Stops scanning, switches off the mutation observer and clears all forms + * @param {string} reason + * @param {...any} rest + */ + + + stopScanner(reason) { + var _this$device$activeFo; + + if ((0, _autofillUtils.shouldLog)()) { + for (var _len2 = arguments.length, rest = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) { + rest[_key2 - 1] = arguments[_key2]; + } + + console.log(reason, ...rest); + } + + const activeInput = (_this$device$activeFo = this.device.activeForm) === null || _this$device$activeFo === void 0 ? void 0 : _this$device$activeFo.activeInput; // remove Dax, listeners, timers, and observers + + clearTimeout(this.debounceTimer); + this.mutObs.disconnect(); + this.forms.forEach(form => { + form.destroy(); + }); + this.forms.clear(); // Bring the user back to the input they were interacting with + + activeInput === null || activeInput === void 0 ? void 0 : activeInput.focus(); + } /** * @param {HTMLElement|HTMLInputElement|HTMLSelectElement} input * @returns {HTMLFormElement|HTMLElement} @@ -15489,9 +15532,10 @@ class DefaultScanner { addInput(input) { - const parentForm = this.getParentForm(input); // Note that el.contains returns true for el itself + const parentForm = this.getParentForm(input); + const seenFormElements = [...this.forms.keys()]; // Note that el.contains returns true for el itself - const previouslyFoundParent = [...this.forms.keys()].find(form => form.contains(parentForm)); + const previouslyFoundParent = seenFormElements.find(form => form.contains(parentForm)); if (previouslyFoundParent) { if (parentForm instanceof HTMLFormElement && parentForm !== previouslyFoundParent) { @@ -15505,7 +15549,7 @@ class DefaultScanner { } } else { // if this form is an ancestor of an existing form, remove that before adding this - const childForm = [...this.forms.keys()].find(form => parentForm.contains(form)); + const childForm = seenFormElements.find(form => parentForm.contains(form)); if (childForm) { var _this$forms$get2; @@ -15518,9 +15562,7 @@ class DefaultScanner { if (this.forms.size < this.options.maxFormsPerPage) { this.forms.set(parentForm, new _Form.Form(parentForm, input, this.device, this.matching, this.shouldAutoprompt)); } else { - if ((0, _autofillUtils.shouldLog)()) { - console.log('The page has too many forms, stop adding them.'); - } + this.stopScanner('The page has too many forms, stop adding them.'); } } } @@ -18164,7 +18206,15 @@ const getText = el => { return (0, _matching.removeExcessWhitespace)(el.alt || el.value || el.title || el.name); } - return (0, _matching.removeExcessWhitespace)(Array.from(el.childNodes).reduce((text, child) => child instanceof Text ? text + ' ' + child.textContent : text, '')); + let text = ''; + + for (const childNode of el.childNodes) { + if (childNode instanceof Text) { + text += ' ' + childNode.textContent; + } + } + + return (0, _matching.removeExcessWhitespace)(text); }; /** * Check if hostname is a local address @@ -18299,9 +18349,8 @@ var _autofillUtils = require("./autofill-utils.js"); (() => { if ((0, _autofillUtils.shouldLog)()) { console.log('DuckDuckGo Autofill Active'); - } + } // if (!window.isSecureContext) return false - if (!window.isSecureContext) return false; try { const startupAutofill = () => { diff --git a/swift-package/Resources/assets/autofill.js b/swift-package/Resources/assets/autofill.js index 6923dae24..6e3644788 100644 --- a/swift-package/Resources/assets/autofill.js +++ b/swift-package/Resources/assets/autofill.js @@ -161,7 +161,7 @@ class Messaging { } /** * Send a 'fire-and-forget' message. - * @throws + * @throws {Error} * {@link MissingHandler} * * @example @@ -181,7 +181,7 @@ class Messaging { } /** * Send a request, and wait for a response - * @throws + * @throws {Error} * {@link MissingHandler} * * @example @@ -4471,15 +4471,8 @@ class ExtensionInterface extends _InterfacePrototype.default { return null; } - removeAutofillUIFromPage() { - var _this$activeForm2; - - super.removeAutofillUIFromPage(); - (_this$activeForm2 = this.activeForm) === null || _this$activeForm2 === void 0 ? void 0 : _this$activeForm2.removeAllDecorations(); - } - async resetAutofillUI(callback) { - this.removeAutofillUIFromPage(); + this.removeAutofillUIFromPage('Resetting autofill.'); await this.setupAutofill(); if (callback) await callback(); this.uiController = this.createUIController(); @@ -4517,7 +4510,7 @@ class ExtensionInterface extends _InterfacePrototype.default { switch (this.getActiveTooltipType()) { case TOOLTIP_TYPES.EmailProtection: { - var _this$activeForm3; + var _this$activeForm2; this._scannerCleanup = this.scanner.init(); this.addLogoutListener(() => { @@ -4532,12 +4525,12 @@ class ExtensionInterface extends _InterfacePrototype.default { } }); - if ((_this$activeForm3 = this.activeForm) !== null && _this$activeForm3 !== void 0 && _this$activeForm3.activeInput) { - var _this$activeForm4; + if ((_this$activeForm2 = this.activeForm) !== null && _this$activeForm2 !== void 0 && _this$activeForm2.activeInput) { + var _this$activeForm3; this.attachTooltip({ form: this.activeForm, - input: (_this$activeForm4 = this.activeForm) === null || _this$activeForm4 === void 0 ? void 0 : _this$activeForm4.activeInput, + input: (_this$activeForm3 = this.activeForm) === null || _this$activeForm3 === void 0 ? void 0 : _this$activeForm3.activeInput, click: null, trigger: 'postSignup', triggerMetaData: { @@ -4773,7 +4766,7 @@ class InterfacePrototype { /** @type {boolean} */ - /** @type {(()=>void) | null} */ + /** @type {((reason, ...rest) => void) | null} */ /** * @param {GlobalConfig} config @@ -4847,12 +4840,16 @@ class InterfacePrototype { createUIController() { return new _NativeUIController.NativeUIController(); } + /** + * @param {string} reason + */ + - removeAutofillUIFromPage() { + removeAutofillUIFromPage(reason) { var _this$uiController, _this$_scannerCleanup; (_this$uiController = this.uiController) === null || _this$uiController === void 0 ? void 0 : _this$uiController.destroy(); - (_this$_scannerCleanup = this._scannerCleanup) === null || _this$_scannerCleanup === void 0 ? void 0 : _this$_scannerCleanup.call(this); + (_this$_scannerCleanup = this._scannerCleanup) === null || _this$_scannerCleanup === void 0 ? void 0 : _this$_scannerCleanup.call(this, reason); } get hasLocalAddresses() { @@ -5092,7 +5089,7 @@ class InterfacePrototype { postInit() { const cleanup = this.scanner.init(); this.addLogoutListener(() => { - cleanup(); + cleanup('Logged out'); if (this.globalConfig.isDDGDomain) { (0, _autofillUtils.notifyWebApp)({ @@ -6544,6 +6541,10 @@ class Form { return this.formAnalyzer.isHybrid; } + get isCCForm() { + return this.formAnalyzer.isCCForm(); + } + logFormInfo() { if (!(0, _autofillUtils.shouldLog)()) return; console.log("Form type: %c".concat(this.getFormType()), 'font-weight: bold'); @@ -6800,6 +6801,7 @@ class Form { destroy() { this.removeAllDecorations(); this.removeTooltip(); + this.forgetAllInputs(); this.mutObs.disconnect(); this.matching.clear(); this.intObs = null; @@ -6902,6 +6904,7 @@ class Form { const opts = { isLogin: this.isLogin, isHybrid: this.isHybrid, + isCCForm: this.isCCForm, hasCredentials: Boolean((_this$device$settings = this.device.settings.availableInputTypes.credentials) === null || _this$device$settings === void 0 ? void 0 : _this$device$settings.username), supportsIdentitiesAutofill: this.device.settings.featureToggles.inputType_identities }; @@ -7354,6 +7357,8 @@ class FormAnalyzer { _defineProperty(this, "matching", void 0); + _defineProperty(this, "_isCCForm", undefined); + this.form = form; this.matching = matching || new _matching.Matching(_matchingConfiguration.matchingConfiguration); /** @@ -7632,6 +7637,52 @@ class FormAnalyzer { return this; } + /** @type {undefined|boolean} */ + + + /** + * Tries to infer if it's a credit card form + * @returns {boolean} + */ + isCCForm() { + var _formEl$textContent; + + const formEl = this.form; + if (this._isCCForm !== undefined) return this._isCCForm; + const ccFieldSelector = this.matching.joinCssSelectors('cc'); + + if (!ccFieldSelector) { + this._isCCForm = false; + return this._isCCForm; + } + + const hasCCSelectorChild = formEl.matches(ccFieldSelector) || formEl.querySelector(ccFieldSelector); // If the form contains one of the specific selectors, we have high confidence + + if (hasCCSelectorChild) { + this._isCCForm = true; + return this._isCCForm; + } // Read form attributes to find a signal + + + const hasCCAttribute = [...formEl.attributes].some(_ref3 => { + let { + name, + value + } = _ref3; + return /(credit|payment).?card/i.test("".concat(name, "=").concat(value)); + }); + + if (hasCCAttribute) { + this._isCCForm = true; + return this._isCCForm; + } // Match form textContent against common cc fields (includes hidden labels) + + + const textMatches = (_formEl$textContent = formEl.textContent) === null || _formEl$textContent === void 0 ? void 0 : _formEl$textContent.match(/(credit|payment).?card(.?number)?|ccv|security.?code|cvv|cvc|csc/ig); // We check for more than one to minimise false positives + + this._isCCForm = Boolean(textMatches && textMatches.length > 1); + return this._isCCForm; + } } @@ -10079,7 +10130,7 @@ class Matching { this.setActiveElementStrings(input, formEl); // // For CC forms we run aggressive matches, so we want to make sure we only // // run them on actual CC forms to avoid false positives and expensive loops - if (this.isCCForm(formEl)) { + if (opts.isCCForm) { const subtype = this.subtypeFromMatchers('cc', input); if (subtype && isValidCreditCardSubtype(subtype)) { @@ -10130,6 +10181,7 @@ class Matching { * @typedef {{ * isLogin?: boolean, * isHybrid?: boolean, + * isCCForm?: boolean, * hasCredentials?: boolean, * supportsIdentitiesAutofill?: boolean * }} SetInputTypeOpts @@ -10463,39 +10515,6 @@ class Matching { this.setActiveElementStrings(input, form); return this; } - /** - * Tries to infer if it's a credit card form - * @param {HTMLElement} formEl - * @returns {boolean} - */ - - - isCCForm(formEl) { - var _formEl$textContent; - - const ccFieldSelector = this.joinCssSelectors('cc'); - - if (!ccFieldSelector) { - return false; - } - - const hasCCSelectorChild = formEl.matches(ccFieldSelector) || formEl.querySelector(ccFieldSelector); // If the form contains one of the specific selectors, we have high confidence - - if (hasCCSelectorChild) return true; // Read form attributes to find a signal - - const hasCCAttribute = [...formEl.attributes].some(_ref => { - let { - name, - value - } = _ref; - return /(credit|payment).?card/i.test("".concat(name, "=").concat(value)); - }); - if (hasCCAttribute) return true; // Match form textContent against common cc fields (includes hidden labels) - - const textMatches = (_formEl$textContent = formEl.textContent) === null || _formEl$textContent === void 0 ? void 0 : _formEl$textContent.match(/(credit|payment).?card(.?number)?|ccv|security.?code|cvv|cvc|csc/ig); // We check for more than one to minimise false positives - - return Boolean(textMatches && textMatches.length > 1); - } /** * @type {MatchingConfiguration} */ @@ -11104,7 +11123,7 @@ class InContextSignup { }; if (options.shouldHideTooltip) { - this.device.removeAutofillUIFromPage(); + this.device.removeAutofillUIFromPage('Email Protection in-context signup dismissed.'); this.device.deviceApi.notify(new _deviceApiCalls.CloseAutofillParentCall(null)); } @@ -11575,7 +11594,7 @@ const { /** * @typedef {{ * forms: Map; - * init(): ()=> void; + * init(): (reason, ...rest)=> void; * enqueue(elements: (HTMLElement|Document)[]): void; * findEligibleInputs(context): Scanner; * options: ScannerOptions; @@ -11689,11 +11708,13 @@ class DefaultScanner { * Call this to scan once and then watch for changes. * * Call the returned function to remove listeners. - * @returns {() => void} + * @returns {(reason: string, ...rest) => void} */ init() { + var _this = this; + if (this.device.globalConfig.isExtension) { this.device.deviceApi.notify(new _deviceApiCalls.AddDebugFlagCall({ flag: 'autofill' @@ -11709,20 +11730,12 @@ class DefaultScanner { setTimeout(() => this.scanAndObserve(), delay); } - return () => { - var _this$device$activeFo; - - const activeInput = (_this$device$activeFo = this.device.activeForm) === null || _this$device$activeFo === void 0 ? void 0 : _this$device$activeFo.activeInput; // remove Dax, listeners, timers, and observers - - clearTimeout(this.debounceTimer); - this.mutObs.disconnect(); - this.forms.forEach(form => { - form.resetAllInputs(); - form.removeAllDecorations(); - }); - this.forms.clear(); // Bring the user back to the input they were interacting with + return function (reason) { + for (var _len = arguments.length, rest = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { + rest[_key - 1] = arguments[_key]; + } - activeInput === null || activeInput === void 0 ? void 0 : activeInput.focus(); + _this.stopScanner(reason, ...rest); }; } /** @@ -11760,6 +11773,7 @@ class DefaultScanner { const inputs = context.querySelectorAll(_selectorsCss.FORM_INPUTS_SELECTOR); if (inputs.length > this.options.maxInputsPerPage) { + this.stopScanner('Too many input fields in the given context, stop scanning', context); return this; } @@ -11768,6 +11782,35 @@ class DefaultScanner { return this; } + /** + * Stops scanning, switches off the mutation observer and clears all forms + * @param {string} reason + * @param {...any} rest + */ + + + stopScanner(reason) { + var _this$device$activeFo; + + if ((0, _autofillUtils.shouldLog)()) { + for (var _len2 = arguments.length, rest = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) { + rest[_key2 - 1] = arguments[_key2]; + } + + console.log(reason, ...rest); + } + + const activeInput = (_this$device$activeFo = this.device.activeForm) === null || _this$device$activeFo === void 0 ? void 0 : _this$device$activeFo.activeInput; // remove Dax, listeners, timers, and observers + + clearTimeout(this.debounceTimer); + this.mutObs.disconnect(); + this.forms.forEach(form => { + form.destroy(); + }); + this.forms.clear(); // Bring the user back to the input they were interacting with + + activeInput === null || activeInput === void 0 ? void 0 : activeInput.focus(); + } /** * @param {HTMLElement|HTMLInputElement|HTMLSelectElement} input * @returns {HTMLFormElement|HTMLElement} @@ -11813,9 +11856,10 @@ class DefaultScanner { addInput(input) { - const parentForm = this.getParentForm(input); // Note that el.contains returns true for el itself + const parentForm = this.getParentForm(input); + const seenFormElements = [...this.forms.keys()]; // Note that el.contains returns true for el itself - const previouslyFoundParent = [...this.forms.keys()].find(form => form.contains(parentForm)); + const previouslyFoundParent = seenFormElements.find(form => form.contains(parentForm)); if (previouslyFoundParent) { if (parentForm instanceof HTMLFormElement && parentForm !== previouslyFoundParent) { @@ -11829,7 +11873,7 @@ class DefaultScanner { } } else { // if this form is an ancestor of an existing form, remove that before adding this - const childForm = [...this.forms.keys()].find(form => parentForm.contains(form)); + const childForm = seenFormElements.find(form => parentForm.contains(form)); if (childForm) { var _this$forms$get2; @@ -11842,9 +11886,7 @@ class DefaultScanner { if (this.forms.size < this.options.maxFormsPerPage) { this.forms.set(parentForm, new _Form.Form(parentForm, input, this.device, this.matching, this.shouldAutoprompt)); } else { - if ((0, _autofillUtils.shouldLog)()) { - console.log('The page has too many forms, stop adding them.'); - } + this.stopScanner('The page has too many forms, stop adding them.'); } } } @@ -14488,7 +14530,15 @@ const getText = el => { return (0, _matching.removeExcessWhitespace)(el.alt || el.value || el.title || el.name); } - return (0, _matching.removeExcessWhitespace)(Array.from(el.childNodes).reduce((text, child) => child instanceof Text ? text + ' ' + child.textContent : text, '')); + let text = ''; + + for (const childNode of el.childNodes) { + if (childNode instanceof Text) { + text += ' ' + childNode.textContent; + } + } + + return (0, _matching.removeExcessWhitespace)(text); }; /** * Check if hostname is a local address @@ -14623,9 +14673,8 @@ var _autofillUtils = require("./autofill-utils.js"); (() => { if ((0, _autofillUtils.shouldLog)()) { console.log('DuckDuckGo Autofill Active'); - } + } // if (!window.isSecureContext) return false - if (!window.isSecureContext) return false; try { const startupAutofill = () => { From 9edae2f95bd0993e0c22b1deccd69ad18d3148ce Mon Sep 17 00:00:00 2001 From: Emanuele Feliziani Date: Mon, 11 Sep 2023 15:12:30 +0200 Subject: [PATCH 08/20] Commit compiled code Signed-off-by: Emanuele Feliziani --- dist/autofill-debug.js | 7 ++++--- dist/autofill.js | 7 ++++--- swift-package/Resources/assets/autofill-debug.js | 7 ++++--- swift-package/Resources/assets/autofill.js | 7 ++++--- 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/dist/autofill-debug.js b/dist/autofill-debug.js index 9d2b9dcfa..1d94454f5 100644 --- a/dist/autofill-debug.js +++ b/dist/autofill-debug.js @@ -161,7 +161,7 @@ class Messaging { } /** * Send a 'fire-and-forget' message. - * @throws {Error} + * @throws * {@link MissingHandler} * * @example @@ -181,7 +181,7 @@ class Messaging { } /** * Send a request, and wait for a response - * @throws {Error} + * @throws * {@link MissingHandler} * * @example @@ -18349,8 +18349,9 @@ var _autofillUtils = require("./autofill-utils.js"); (() => { if ((0, _autofillUtils.shouldLog)()) { console.log('DuckDuckGo Autofill Active'); - } // if (!window.isSecureContext) return false + } + if (!window.isSecureContext) return false; try { const startupAutofill = () => { diff --git a/dist/autofill.js b/dist/autofill.js index 6e3644788..fdc146691 100644 --- a/dist/autofill.js +++ b/dist/autofill.js @@ -161,7 +161,7 @@ class Messaging { } /** * Send a 'fire-and-forget' message. - * @throws {Error} + * @throws * {@link MissingHandler} * * @example @@ -181,7 +181,7 @@ class Messaging { } /** * Send a request, and wait for a response - * @throws {Error} + * @throws * {@link MissingHandler} * * @example @@ -14673,8 +14673,9 @@ var _autofillUtils = require("./autofill-utils.js"); (() => { if ((0, _autofillUtils.shouldLog)()) { console.log('DuckDuckGo Autofill Active'); - } // if (!window.isSecureContext) return false + } + if (!window.isSecureContext) return false; try { const startupAutofill = () => { diff --git a/swift-package/Resources/assets/autofill-debug.js b/swift-package/Resources/assets/autofill-debug.js index 9d2b9dcfa..1d94454f5 100644 --- a/swift-package/Resources/assets/autofill-debug.js +++ b/swift-package/Resources/assets/autofill-debug.js @@ -161,7 +161,7 @@ class Messaging { } /** * Send a 'fire-and-forget' message. - * @throws {Error} + * @throws * {@link MissingHandler} * * @example @@ -181,7 +181,7 @@ class Messaging { } /** * Send a request, and wait for a response - * @throws {Error} + * @throws * {@link MissingHandler} * * @example @@ -18349,8 +18349,9 @@ var _autofillUtils = require("./autofill-utils.js"); (() => { if ((0, _autofillUtils.shouldLog)()) { console.log('DuckDuckGo Autofill Active'); - } // if (!window.isSecureContext) return false + } + if (!window.isSecureContext) return false; try { const startupAutofill = () => { diff --git a/swift-package/Resources/assets/autofill.js b/swift-package/Resources/assets/autofill.js index 6e3644788..fdc146691 100644 --- a/swift-package/Resources/assets/autofill.js +++ b/swift-package/Resources/assets/autofill.js @@ -161,7 +161,7 @@ class Messaging { } /** * Send a 'fire-and-forget' message. - * @throws {Error} + * @throws * {@link MissingHandler} * * @example @@ -181,7 +181,7 @@ class Messaging { } /** * Send a request, and wait for a response - * @throws {Error} + * @throws * {@link MissingHandler} * * @example @@ -14673,8 +14673,9 @@ var _autofillUtils = require("./autofill-utils.js"); (() => { if ((0, _autofillUtils.shouldLog)()) { console.log('DuckDuckGo Autofill Active'); - } // if (!window.isSecureContext) return false + } + if (!window.isSecureContext) return false; try { const startupAutofill = () => { From 15627154345474aafe58dafe03e9e2acf003ad84 Mon Sep 17 00:00:00 2001 From: Emanuele Feliziani Date: Wed, 23 Aug 2023 16:44:12 +0200 Subject: [PATCH 09/20] Add performance logging capability Signed-off-by: Emanuele Feliziani # Conflicts: # src/Scanner.js --- src/Scanner.js | 6 +++++- src/autofill-utils.js | 22 ++++++++++++++++++++-- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/Scanner.js b/src/Scanner.js index 6c4da2640..abc375a57 100644 --- a/src/Scanner.js +++ b/src/Scanner.js @@ -2,7 +2,7 @@ import { Form } from './Form/Form.js' import { SUBMIT_BUTTON_SELECTOR, FORM_INPUTS_SELECTOR } from './Form/selectors-css.js' import { constants } from './constants.js' import { createMatching } from './Form/matching.js' -import {isFormLikelyToBeUsedAsPageWrapper, shouldLog} from './autofill-utils.js' +import {isFormLikelyToBeUsedAsPageWrapper, shouldLog, shouldLogPerformance} from './autofill-utils.js' import { AddDebugFlagCall } from './deviceApiCalls/__generated__/deviceApiCalls.js' const { @@ -118,6 +118,10 @@ class DefaultScanner { window.performance?.mark?.('scanner:init:start') this.findEligibleInputs(document) window.performance?.mark?.('scanner:init:end') + if (shouldLogPerformance()) { + const measurement = window.performance?.measure('scanner:init', 'scanner:init:start', 'scanner:init:end') + console.log(`Initial scan took ${Math.round(measurement?.duration)}ms`) + } this.mutObs.observe(document.documentElement, { childList: true, subtree: true }) } diff --git a/src/autofill-utils.js b/src/autofill-utils.js index 3bcfe525d..ca29a70c0 100644 --- a/src/autofill-utils.js +++ b/src/autofill-utils.js @@ -401,13 +401,30 @@ const wasAutofilledByChrome = (input) => { } /** - * Checks if we should log debug info to the console + * Checks if we should log form analysis debug info to the console * @returns {boolean} */ function shouldLog () { + return readDebugSetting('ddg-autofill-debug') +} + +/** + * Checks if we should log performance info to the console + * @returns {boolean} + */ +function shouldLogPerformance () { + return readDebugSetting('ddg-autofill-perf') +} + +/** + * Check if a sessionStorage item is set to 'true' + * @param setting + * @returns {boolean} + */ +function readDebugSetting (setting) { // sessionStorage throws in invalid schemes like data: and file: try { - return window.sessionStorage?.getItem('ddg-autofill-debug') === 'true' + return window.sessionStorage?.getItem(setting) === 'true' } catch (e) { return false } @@ -490,6 +507,7 @@ export { isValidTLD, wasAutofilledByChrome, shouldLog, + shouldLogPerformance, whenIdle, truncateFromMiddle, isFormLikelyToBeUsedAsPageWrapper From e66a74e4a1283ab942e200bfdb4230beab042689 Mon Sep 17 00:00:00 2001 From: Emanuele Feliziani Date: Mon, 11 Sep 2023 15:22:24 +0200 Subject: [PATCH 10/20] Commit files Signed-off-by: Emanuele Feliziani --- dist/autofill-debug.js | 32 +++++++++++++++++-- dist/autofill.js | 32 +++++++++++++++++-- .../Resources/assets/autofill-debug.js | 32 +++++++++++++++++-- swift-package/Resources/assets/autofill.js | 32 +++++++++++++++++-- 4 files changed, 120 insertions(+), 8 deletions(-) diff --git a/dist/autofill-debug.js b/dist/autofill-debug.js index 1d94454f5..5bb8d998b 100644 --- a/dist/autofill-debug.js +++ b/dist/autofill-debug.js @@ -15425,6 +15425,14 @@ class DefaultScanner { (_window$performance = window.performance) === null || _window$performance === void 0 ? void 0 : (_window$performance$m = _window$performance.mark) === null || _window$performance$m === void 0 ? void 0 : _window$performance$m.call(_window$performance, 'scanner:init:start'); this.findEligibleInputs(document); (_window$performance2 = window.performance) === null || _window$performance2 === void 0 ? void 0 : (_window$performance2$ = _window$performance2.mark) === null || _window$performance2$ === void 0 ? void 0 : _window$performance2$.call(_window$performance2, 'scanner:init:end'); + + if ((0, _autofillUtils.shouldLogPerformance)()) { + var _window$performance3; + + const measurement = (_window$performance3 = window.performance) === null || _window$performance3 === void 0 ? void 0 : _window$performance3.measure('scanner:init', 'scanner:init:start', 'scanner:init:end'); + console.log("Initial scan took ".concat(Math.round(measurement === null || measurement === void 0 ? void 0 : measurement.duration), "ms")); + } + this.mutObs.observe(document.documentElement, { childList: true, subtree: true @@ -17770,6 +17778,7 @@ exports.isPotentiallyViewable = void 0; exports.isValidTLD = isValidTLD; exports.setValue = exports.sendAndWaitForAnswer = exports.safeExecute = exports.removeInlineStyles = exports.notifyWebApp = void 0; exports.shouldLog = shouldLog; +exports.shouldLogPerformance = shouldLogPerformance; exports.truncateFromMiddle = truncateFromMiddle; exports.wasAutofilledByChrome = void 0; exports.whenIdle = whenIdle; @@ -18258,7 +18267,7 @@ const wasAutofilledByChrome = input => { } }; /** - * Checks if we should log debug info to the console + * Checks if we should log form analysis debug info to the console * @returns {boolean} */ @@ -18266,11 +18275,30 @@ const wasAutofilledByChrome = input => { exports.wasAutofilledByChrome = wasAutofilledByChrome; function shouldLog() { + return readDebugSetting('ddg-autofill-debug'); +} +/** + * Checks if we should log performance info to the console + * @returns {boolean} + */ + + +function shouldLogPerformance() { + return readDebugSetting('ddg-autofill-perf'); +} +/** + * Check if a sessionStorage item is set to 'true' + * @param setting + * @returns {boolean} + */ + + +function readDebugSetting(setting) { // sessionStorage throws in invalid schemes like data: and file: try { var _window$sessionStorag; - return ((_window$sessionStorag = window.sessionStorage) === null || _window$sessionStorag === void 0 ? void 0 : _window$sessionStorag.getItem('ddg-autofill-debug')) === 'true'; + return ((_window$sessionStorag = window.sessionStorage) === null || _window$sessionStorag === void 0 ? void 0 : _window$sessionStorag.getItem(setting)) === 'true'; } catch (e) { return false; } diff --git a/dist/autofill.js b/dist/autofill.js index fdc146691..33e549f82 100644 --- a/dist/autofill.js +++ b/dist/autofill.js @@ -11749,6 +11749,14 @@ class DefaultScanner { (_window$performance = window.performance) === null || _window$performance === void 0 ? void 0 : (_window$performance$m = _window$performance.mark) === null || _window$performance$m === void 0 ? void 0 : _window$performance$m.call(_window$performance, 'scanner:init:start'); this.findEligibleInputs(document); (_window$performance2 = window.performance) === null || _window$performance2 === void 0 ? void 0 : (_window$performance2$ = _window$performance2.mark) === null || _window$performance2$ === void 0 ? void 0 : _window$performance2$.call(_window$performance2, 'scanner:init:end'); + + if ((0, _autofillUtils.shouldLogPerformance)()) { + var _window$performance3; + + const measurement = (_window$performance3 = window.performance) === null || _window$performance3 === void 0 ? void 0 : _window$performance3.measure('scanner:init', 'scanner:init:start', 'scanner:init:end'); + console.log("Initial scan took ".concat(Math.round(measurement === null || measurement === void 0 ? void 0 : measurement.duration), "ms")); + } + this.mutObs.observe(document.documentElement, { childList: true, subtree: true @@ -14094,6 +14102,7 @@ exports.isPotentiallyViewable = void 0; exports.isValidTLD = isValidTLD; exports.setValue = exports.sendAndWaitForAnswer = exports.safeExecute = exports.removeInlineStyles = exports.notifyWebApp = void 0; exports.shouldLog = shouldLog; +exports.shouldLogPerformance = shouldLogPerformance; exports.truncateFromMiddle = truncateFromMiddle; exports.wasAutofilledByChrome = void 0; exports.whenIdle = whenIdle; @@ -14582,7 +14591,7 @@ const wasAutofilledByChrome = input => { } }; /** - * Checks if we should log debug info to the console + * Checks if we should log form analysis debug info to the console * @returns {boolean} */ @@ -14590,11 +14599,30 @@ const wasAutofilledByChrome = input => { exports.wasAutofilledByChrome = wasAutofilledByChrome; function shouldLog() { + return readDebugSetting('ddg-autofill-debug'); +} +/** + * Checks if we should log performance info to the console + * @returns {boolean} + */ + + +function shouldLogPerformance() { + return readDebugSetting('ddg-autofill-perf'); +} +/** + * Check if a sessionStorage item is set to 'true' + * @param setting + * @returns {boolean} + */ + + +function readDebugSetting(setting) { // sessionStorage throws in invalid schemes like data: and file: try { var _window$sessionStorag; - return ((_window$sessionStorag = window.sessionStorage) === null || _window$sessionStorag === void 0 ? void 0 : _window$sessionStorag.getItem('ddg-autofill-debug')) === 'true'; + return ((_window$sessionStorag = window.sessionStorage) === null || _window$sessionStorag === void 0 ? void 0 : _window$sessionStorag.getItem(setting)) === 'true'; } catch (e) { return false; } diff --git a/swift-package/Resources/assets/autofill-debug.js b/swift-package/Resources/assets/autofill-debug.js index 1d94454f5..5bb8d998b 100644 --- a/swift-package/Resources/assets/autofill-debug.js +++ b/swift-package/Resources/assets/autofill-debug.js @@ -15425,6 +15425,14 @@ class DefaultScanner { (_window$performance = window.performance) === null || _window$performance === void 0 ? void 0 : (_window$performance$m = _window$performance.mark) === null || _window$performance$m === void 0 ? void 0 : _window$performance$m.call(_window$performance, 'scanner:init:start'); this.findEligibleInputs(document); (_window$performance2 = window.performance) === null || _window$performance2 === void 0 ? void 0 : (_window$performance2$ = _window$performance2.mark) === null || _window$performance2$ === void 0 ? void 0 : _window$performance2$.call(_window$performance2, 'scanner:init:end'); + + if ((0, _autofillUtils.shouldLogPerformance)()) { + var _window$performance3; + + const measurement = (_window$performance3 = window.performance) === null || _window$performance3 === void 0 ? void 0 : _window$performance3.measure('scanner:init', 'scanner:init:start', 'scanner:init:end'); + console.log("Initial scan took ".concat(Math.round(measurement === null || measurement === void 0 ? void 0 : measurement.duration), "ms")); + } + this.mutObs.observe(document.documentElement, { childList: true, subtree: true @@ -17770,6 +17778,7 @@ exports.isPotentiallyViewable = void 0; exports.isValidTLD = isValidTLD; exports.setValue = exports.sendAndWaitForAnswer = exports.safeExecute = exports.removeInlineStyles = exports.notifyWebApp = void 0; exports.shouldLog = shouldLog; +exports.shouldLogPerformance = shouldLogPerformance; exports.truncateFromMiddle = truncateFromMiddle; exports.wasAutofilledByChrome = void 0; exports.whenIdle = whenIdle; @@ -18258,7 +18267,7 @@ const wasAutofilledByChrome = input => { } }; /** - * Checks if we should log debug info to the console + * Checks if we should log form analysis debug info to the console * @returns {boolean} */ @@ -18266,11 +18275,30 @@ const wasAutofilledByChrome = input => { exports.wasAutofilledByChrome = wasAutofilledByChrome; function shouldLog() { + return readDebugSetting('ddg-autofill-debug'); +} +/** + * Checks if we should log performance info to the console + * @returns {boolean} + */ + + +function shouldLogPerformance() { + return readDebugSetting('ddg-autofill-perf'); +} +/** + * Check if a sessionStorage item is set to 'true' + * @param setting + * @returns {boolean} + */ + + +function readDebugSetting(setting) { // sessionStorage throws in invalid schemes like data: and file: try { var _window$sessionStorag; - return ((_window$sessionStorag = window.sessionStorage) === null || _window$sessionStorag === void 0 ? void 0 : _window$sessionStorag.getItem('ddg-autofill-debug')) === 'true'; + return ((_window$sessionStorag = window.sessionStorage) === null || _window$sessionStorag === void 0 ? void 0 : _window$sessionStorag.getItem(setting)) === 'true'; } catch (e) { return false; } diff --git a/swift-package/Resources/assets/autofill.js b/swift-package/Resources/assets/autofill.js index fdc146691..33e549f82 100644 --- a/swift-package/Resources/assets/autofill.js +++ b/swift-package/Resources/assets/autofill.js @@ -11749,6 +11749,14 @@ class DefaultScanner { (_window$performance = window.performance) === null || _window$performance === void 0 ? void 0 : (_window$performance$m = _window$performance.mark) === null || _window$performance$m === void 0 ? void 0 : _window$performance$m.call(_window$performance, 'scanner:init:start'); this.findEligibleInputs(document); (_window$performance2 = window.performance) === null || _window$performance2 === void 0 ? void 0 : (_window$performance2$ = _window$performance2.mark) === null || _window$performance2$ === void 0 ? void 0 : _window$performance2$.call(_window$performance2, 'scanner:init:end'); + + if ((0, _autofillUtils.shouldLogPerformance)()) { + var _window$performance3; + + const measurement = (_window$performance3 = window.performance) === null || _window$performance3 === void 0 ? void 0 : _window$performance3.measure('scanner:init', 'scanner:init:start', 'scanner:init:end'); + console.log("Initial scan took ".concat(Math.round(measurement === null || measurement === void 0 ? void 0 : measurement.duration), "ms")); + } + this.mutObs.observe(document.documentElement, { childList: true, subtree: true @@ -14094,6 +14102,7 @@ exports.isPotentiallyViewable = void 0; exports.isValidTLD = isValidTLD; exports.setValue = exports.sendAndWaitForAnswer = exports.safeExecute = exports.removeInlineStyles = exports.notifyWebApp = void 0; exports.shouldLog = shouldLog; +exports.shouldLogPerformance = shouldLogPerformance; exports.truncateFromMiddle = truncateFromMiddle; exports.wasAutofilledByChrome = void 0; exports.whenIdle = whenIdle; @@ -14582,7 +14591,7 @@ const wasAutofilledByChrome = input => { } }; /** - * Checks if we should log debug info to the console + * Checks if we should log form analysis debug info to the console * @returns {boolean} */ @@ -14590,11 +14599,30 @@ const wasAutofilledByChrome = input => { exports.wasAutofilledByChrome = wasAutofilledByChrome; function shouldLog() { + return readDebugSetting('ddg-autofill-debug'); +} +/** + * Checks if we should log performance info to the console + * @returns {boolean} + */ + + +function shouldLogPerformance() { + return readDebugSetting('ddg-autofill-perf'); +} +/** + * Check if a sessionStorage item is set to 'true' + * @param setting + * @returns {boolean} + */ + + +function readDebugSetting(setting) { // sessionStorage throws in invalid schemes like data: and file: try { var _window$sessionStorag; - return ((_window$sessionStorag = window.sessionStorage) === null || _window$sessionStorag === void 0 ? void 0 : _window$sessionStorag.getItem('ddg-autofill-debug')) === 'true'; + return ((_window$sessionStorag = window.sessionStorage) === null || _window$sessionStorag === void 0 ? void 0 : _window$sessionStorag.getItem(setting)) === 'true'; } catch (e) { return false; } From 2ebe89e6d6027f0b679b271f8200665d037badde Mon Sep 17 00:00:00 2001 From: Emanuele Feliziani Date: Tue, 12 Sep 2023 08:50:53 +0200 Subject: [PATCH 11/20] Add comment Signed-off-by: Emanuele Feliziani --- jest.setup.js | 1 + 1 file changed, 1 insertion(+) diff --git a/jest.setup.js b/jest.setup.js index 391dbd1f3..65a97ade6 100644 --- a/jest.setup.js +++ b/jest.setup.js @@ -121,6 +121,7 @@ const defaultStyle = { } const mockGetComputedStyle = (el) => { return { + // since we don't load stylesheets in the tests, the style prop is all the css applied, so it's a safe fallback getPropertyValue: (prop) => el.style?.[prop] || defaultStyle[prop] } } From 98c7d9492db7eba4f09e77fb15a69b7d9636aa4d Mon Sep 17 00:00:00 2001 From: Emanuele Feliziani Date: Tue, 12 Sep 2023 08:58:47 +0200 Subject: [PATCH 12/20] Minor change Signed-off-by: Emanuele Feliziani --- dist/autofill-debug.js | 2 +- dist/autofill.js | 2 +- src/Form/FormAnalyzer.js | 2 +- swift-package/Resources/assets/autofill-debug.js | 2 +- swift-package/Resources/assets/autofill.js | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/dist/autofill-debug.js b/dist/autofill-debug.js index 5bb8d998b..7a6af2146 100644 --- a/dist/autofill-debug.js +++ b/dist/autofill-debug.js @@ -11323,8 +11323,8 @@ class FormAnalyzer { isCCForm() { var _formEl$textContent; - const formEl = this.form; if (this._isCCForm !== undefined) return this._isCCForm; + const formEl = this.form; const ccFieldSelector = this.matching.joinCssSelectors('cc'); if (!ccFieldSelector) { diff --git a/dist/autofill.js b/dist/autofill.js index 33e549f82..a230cda9a 100644 --- a/dist/autofill.js +++ b/dist/autofill.js @@ -7647,8 +7647,8 @@ class FormAnalyzer { isCCForm() { var _formEl$textContent; - const formEl = this.form; if (this._isCCForm !== undefined) return this._isCCForm; + const formEl = this.form; const ccFieldSelector = this.matching.joinCssSelectors('cc'); if (!ccFieldSelector) { diff --git a/src/Form/FormAnalyzer.js b/src/Form/FormAnalyzer.js index 5ce2bbe9d..79e36061d 100644 --- a/src/Form/FormAnalyzer.js +++ b/src/Form/FormAnalyzer.js @@ -285,9 +285,9 @@ class FormAnalyzer { * @returns {boolean} */ isCCForm () { - const formEl = this.form if (this._isCCForm !== undefined) return this._isCCForm + const formEl = this.form const ccFieldSelector = this.matching.joinCssSelectors('cc') if (!ccFieldSelector) { this._isCCForm = false diff --git a/swift-package/Resources/assets/autofill-debug.js b/swift-package/Resources/assets/autofill-debug.js index 5bb8d998b..7a6af2146 100644 --- a/swift-package/Resources/assets/autofill-debug.js +++ b/swift-package/Resources/assets/autofill-debug.js @@ -11323,8 +11323,8 @@ class FormAnalyzer { isCCForm() { var _formEl$textContent; - const formEl = this.form; if (this._isCCForm !== undefined) return this._isCCForm; + const formEl = this.form; const ccFieldSelector = this.matching.joinCssSelectors('cc'); if (!ccFieldSelector) { diff --git a/swift-package/Resources/assets/autofill.js b/swift-package/Resources/assets/autofill.js index 33e549f82..a230cda9a 100644 --- a/swift-package/Resources/assets/autofill.js +++ b/swift-package/Resources/assets/autofill.js @@ -7647,8 +7647,8 @@ class FormAnalyzer { isCCForm() { var _formEl$textContent; - const formEl = this.form; if (this._isCCForm !== undefined) return this._isCCForm; + const formEl = this.form; const ccFieldSelector = this.matching.joinCssSelectors('cc'); if (!ccFieldSelector) { From 73ac4cd92d45cc6ce3d64c4414c4a3687cdace1a Mon Sep 17 00:00:00 2001 From: Emanuele Feliziani Date: Tue, 12 Sep 2023 10:20:59 +0200 Subject: [PATCH 13/20] Improve stopping the scanner Signed-off-by: Emanuele Feliziani --- dist/autofill-debug.js | 8 ++++++++ dist/autofill.js | 8 ++++++++ src/Scanner.js | 7 +++++++ src/__snapshots__/Scanner.test.js.snap | 5 ----- swift-package/Resources/assets/autofill-debug.js | 8 ++++++++ swift-package/Resources/assets/autofill.js | 8 ++++++++ 6 files changed, 39 insertions(+), 5 deletions(-) diff --git a/dist/autofill-debug.js b/dist/autofill-debug.js index 7a6af2146..5696a9268 100644 --- a/dist/autofill-debug.js +++ b/dist/autofill-debug.js @@ -15323,6 +15323,8 @@ class DefaultScanner { /** @type {boolean} A flag to indicate the whole page will be re-scanned */ + /** @type {boolean} Indicates whether we called stopScanning */ + /** * @param {import("./DeviceInterface/InterfacePrototype").default} device * @param {ScannerOptions} options @@ -15340,6 +15342,8 @@ class DefaultScanner { _defineProperty(this, "rescanAll", false); + _defineProperty(this, "stopped", false); + _defineProperty(this, "mutObs", new MutationObserver(mutationList => { /** @type {HTMLElement[]} */ if (this.rescanAll) { @@ -15476,6 +15480,8 @@ class DefaultScanner { stopScanner(reason) { var _this$device$activeFo; + this.stopped = true; + if ((0, _autofillUtils.shouldLog)()) { for (var _len2 = arguments.length, rest = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) { rest[_key2 - 1] = arguments[_key2]; @@ -15487,6 +15493,7 @@ class DefaultScanner { const activeInput = (_this$device$activeFo = this.device.activeForm) === null || _this$device$activeFo === void 0 ? void 0 : _this$device$activeFo.activeInput; // remove Dax, listeners, timers, and observers clearTimeout(this.debounceTimer); + this.changedElements.clear(); this.mutObs.disconnect(); this.forms.forEach(form => { form.destroy(); @@ -15540,6 +15547,7 @@ class DefaultScanner { addInput(input) { + if (this.stopped) return; const parentForm = this.getParentForm(input); const seenFormElements = [...this.forms.keys()]; // Note that el.contains returns true for el itself diff --git a/dist/autofill.js b/dist/autofill.js index a230cda9a..57884672e 100644 --- a/dist/autofill.js +++ b/dist/autofill.js @@ -11647,6 +11647,8 @@ class DefaultScanner { /** @type {boolean} A flag to indicate the whole page will be re-scanned */ + /** @type {boolean} Indicates whether we called stopScanning */ + /** * @param {import("./DeviceInterface/InterfacePrototype").default} device * @param {ScannerOptions} options @@ -11664,6 +11666,8 @@ class DefaultScanner { _defineProperty(this, "rescanAll", false); + _defineProperty(this, "stopped", false); + _defineProperty(this, "mutObs", new MutationObserver(mutationList => { /** @type {HTMLElement[]} */ if (this.rescanAll) { @@ -11800,6 +11804,8 @@ class DefaultScanner { stopScanner(reason) { var _this$device$activeFo; + this.stopped = true; + if ((0, _autofillUtils.shouldLog)()) { for (var _len2 = arguments.length, rest = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) { rest[_key2 - 1] = arguments[_key2]; @@ -11811,6 +11817,7 @@ class DefaultScanner { const activeInput = (_this$device$activeFo = this.device.activeForm) === null || _this$device$activeFo === void 0 ? void 0 : _this$device$activeFo.activeInput; // remove Dax, listeners, timers, and observers clearTimeout(this.debounceTimer); + this.changedElements.clear(); this.mutObs.disconnect(); this.forms.forEach(form => { form.destroy(); @@ -11864,6 +11871,7 @@ class DefaultScanner { addInput(input) { + if (this.stopped) return; const parentForm = this.getParentForm(input); const seenFormElements = [...this.forms.keys()]; // Note that el.contains returns true for el itself diff --git a/src/Scanner.js b/src/Scanner.js index abc375a57..43f996c71 100644 --- a/src/Scanner.js +++ b/src/Scanner.js @@ -66,6 +66,8 @@ class DefaultScanner { activeInput = null; /** @type {boolean} A flag to indicate the whole page will be re-scanned */ rescanAll = false; + /** @type {boolean} Indicates whether we called stopScanning */ + stopped = false /** * @param {import("./DeviceInterface/InterfacePrototype").default} device @@ -153,6 +155,8 @@ class DefaultScanner { * @param {...any} rest */ stopScanner (reason, ...rest) { + this.stopped = true + if (shouldLog()) { console.log(reason, ...rest) } @@ -161,6 +165,7 @@ class DefaultScanner { // remove Dax, listeners, timers, and observers clearTimeout(this.debounceTimer) + this.changedElements.clear() this.mutObs.disconnect() this.forms.forEach(form => { @@ -212,6 +217,8 @@ class DefaultScanner { * @param {HTMLInputElement|HTMLSelectElement} input */ addInput (input) { + if (this.stopped) return + const parentForm = this.getParentForm(input) const seenFormElements = [...this.forms.keys()] diff --git a/src/__snapshots__/Scanner.test.js.snap b/src/__snapshots__/Scanner.test.js.snap index 45b263093..b5e19867b 100644 --- a/src/__snapshots__/Scanner.test.js.snap +++ b/src/__snapshots__/Scanner.test.js.snap @@ -268,7 +268,6 @@ exports[`performance should stop scanning if page grows above maximum forms 1`] for="input-01" > @@ -279,7 +278,6 @@ exports[`performance should stop scanning if page grows above maximum forms 1`] for="input-02" > @@ -290,7 +288,6 @@ exports[`performance should stop scanning if page grows above maximum forms 1`] for="input-03" > @@ -301,7 +298,6 @@ exports[`performance should stop scanning if page grows above maximum forms 1`] for="input-04" > @@ -312,7 +308,6 @@ exports[`performance should stop scanning if page grows above maximum forms 1`] for="input-05" > diff --git a/swift-package/Resources/assets/autofill-debug.js b/swift-package/Resources/assets/autofill-debug.js index 7a6af2146..5696a9268 100644 --- a/swift-package/Resources/assets/autofill-debug.js +++ b/swift-package/Resources/assets/autofill-debug.js @@ -15323,6 +15323,8 @@ class DefaultScanner { /** @type {boolean} A flag to indicate the whole page will be re-scanned */ + /** @type {boolean} Indicates whether we called stopScanning */ + /** * @param {import("./DeviceInterface/InterfacePrototype").default} device * @param {ScannerOptions} options @@ -15340,6 +15342,8 @@ class DefaultScanner { _defineProperty(this, "rescanAll", false); + _defineProperty(this, "stopped", false); + _defineProperty(this, "mutObs", new MutationObserver(mutationList => { /** @type {HTMLElement[]} */ if (this.rescanAll) { @@ -15476,6 +15480,8 @@ class DefaultScanner { stopScanner(reason) { var _this$device$activeFo; + this.stopped = true; + if ((0, _autofillUtils.shouldLog)()) { for (var _len2 = arguments.length, rest = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) { rest[_key2 - 1] = arguments[_key2]; @@ -15487,6 +15493,7 @@ class DefaultScanner { const activeInput = (_this$device$activeFo = this.device.activeForm) === null || _this$device$activeFo === void 0 ? void 0 : _this$device$activeFo.activeInput; // remove Dax, listeners, timers, and observers clearTimeout(this.debounceTimer); + this.changedElements.clear(); this.mutObs.disconnect(); this.forms.forEach(form => { form.destroy(); @@ -15540,6 +15547,7 @@ class DefaultScanner { addInput(input) { + if (this.stopped) return; const parentForm = this.getParentForm(input); const seenFormElements = [...this.forms.keys()]; // Note that el.contains returns true for el itself diff --git a/swift-package/Resources/assets/autofill.js b/swift-package/Resources/assets/autofill.js index a230cda9a..57884672e 100644 --- a/swift-package/Resources/assets/autofill.js +++ b/swift-package/Resources/assets/autofill.js @@ -11647,6 +11647,8 @@ class DefaultScanner { /** @type {boolean} A flag to indicate the whole page will be re-scanned */ + /** @type {boolean} Indicates whether we called stopScanning */ + /** * @param {import("./DeviceInterface/InterfacePrototype").default} device * @param {ScannerOptions} options @@ -11664,6 +11666,8 @@ class DefaultScanner { _defineProperty(this, "rescanAll", false); + _defineProperty(this, "stopped", false); + _defineProperty(this, "mutObs", new MutationObserver(mutationList => { /** @type {HTMLElement[]} */ if (this.rescanAll) { @@ -11800,6 +11804,8 @@ class DefaultScanner { stopScanner(reason) { var _this$device$activeFo; + this.stopped = true; + if ((0, _autofillUtils.shouldLog)()) { for (var _len2 = arguments.length, rest = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) { rest[_key2 - 1] = arguments[_key2]; @@ -11811,6 +11817,7 @@ class DefaultScanner { const activeInput = (_this$device$activeFo = this.device.activeForm) === null || _this$device$activeFo === void 0 ? void 0 : _this$device$activeFo.activeInput; // remove Dax, listeners, timers, and observers clearTimeout(this.debounceTimer); + this.changedElements.clear(); this.mutObs.disconnect(); this.forms.forEach(form => { form.destroy(); @@ -11864,6 +11871,7 @@ class DefaultScanner { addInput(input) { + if (this.stopped) return; const parentForm = this.getParentForm(input); const seenFormElements = [...this.forms.keys()]; // Note that el.contains returns true for el itself From 107f236727417716912d51b970d0063fee1a1a5b Mon Sep 17 00:00:00 2001 From: Emanuele Feliziani Date: Fri, 15 Sep 2023 10:11:22 +0200 Subject: [PATCH 14/20] Update privacy pages urls Signed-off-by: Emanuele Feliziani --- docs/runtime.windows.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/runtime.windows.md b/docs/runtime.windows.md index 063780df2..68c4a02b4 100644 --- a/docs/runtime.windows.md +++ b/docs/runtime.windows.md @@ -1,6 +1,6 @@ ## Links -- [Privacy Test Pages, Form Submissions](https://privacy-test-pages.glitch.me/autofill/form-submission.html) +- [Privacy Test Pages, Form Submissions](https://privacy-test-pages.site/autofill/form-submission.html) ## `getRuntimeConfiguration()` diff --git a/package.json b/package.json index 04adafffa..44de19911 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "lint": "eslint .", "lint:fix": "npm run lint -- --fix", "copy-assets": "node scripts/copy-assets.js", - "open-test-extension": "npx web-ext run -t chromium -u https://privacy-test-pages.glitch.me/ -s integration-test/extension", + "open-test-extension": "npx web-ext run -t chromium -u https://privacy-test-pages.site/ -s integration-test/extension", "schema:generate": "node scripts/api-call-generator.js", "test": "npm run test:unit && npm run lint && tsc", "test:clean-tree": "npm run build && sh scripts/check-for-changes.sh", From fbc5cc8db03722cee4b1ac47c27975863c12e8be Mon Sep 17 00:00:00 2001 From: Emanuele Feliziani Date: Fri, 15 Sep 2023 10:50:03 +0200 Subject: [PATCH 15/20] Improved performance logging Signed-off-by: Emanuele Feliziani --- src/Scanner.js | 14 +++++++------- src/autofill-utils.js | 9 +++++++++ 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/Scanner.js b/src/Scanner.js index 43f996c71..faa80eb07 100644 --- a/src/Scanner.js +++ b/src/Scanner.js @@ -2,7 +2,7 @@ import { Form } from './Form/Form.js' import { SUBMIT_BUTTON_SELECTOR, FORM_INPUTS_SELECTOR } from './Form/selectors-css.js' import { constants } from './constants.js' import { createMatching } from './Form/matching.js' -import {isFormLikelyToBeUsedAsPageWrapper, shouldLog, shouldLogPerformance} from './autofill-utils.js' +import {logPerformance, isFormLikelyToBeUsedAsPageWrapper, shouldLog} from './autofill-utils.js' import { AddDebugFlagCall } from './deviceApiCalls/__generated__/deviceApiCalls.js' const { @@ -117,13 +117,10 @@ class DefaultScanner { * Scan the page and begin observing changes */ scanAndObserve () { - window.performance?.mark?.('scanner:init:start') + window.performance?.mark?.('initial_scanner:init:start') this.findEligibleInputs(document) - window.performance?.mark?.('scanner:init:end') - if (shouldLogPerformance()) { - const measurement = window.performance?.measure('scanner:init', 'scanner:init:start', 'scanner:init:end') - console.log(`Initial scan took ${Math.round(measurement?.duration)}ms`) - } + window.performance?.mark?.('initial_scanner:init:end') + logPerformance('initial_scanner') this.mutObs.observe(document.documentElement, { childList: true, subtree: true }) } @@ -270,9 +267,12 @@ class DefaultScanner { clearTimeout(this.debounceTimer) this.debounceTimer = setTimeout(() => { + window.performance?.mark?.('scanner:init:start') this.processChangedElements() this.changedElements.clear() this.rescanAll = false + window.performance?.mark?.('scanner:init:end') + logPerformance('scanner') }, this.options.debounceTimePeriod) } diff --git a/src/autofill-utils.js b/src/autofill-utils.js index ca29a70c0..78e32d62d 100644 --- a/src/autofill-utils.js +++ b/src/autofill-utils.js @@ -430,6 +430,14 @@ function readDebugSetting (setting) { } } +function logPerformance (markName) { + if (shouldLogPerformance()) { + const measurement = window.performance?.measure(`${markName}:init`, `${markName}:init:start`, `${markName}:init:end`) + console.log(`${markName} took ${Math.round(measurement?.duration)}ms`) + window.performance?.clearMarks() + } +} + /** * * @param {Function} callback @@ -508,6 +516,7 @@ export { wasAutofilledByChrome, shouldLog, shouldLogPerformance, + logPerformance, whenIdle, truncateFromMiddle, isFormLikelyToBeUsedAsPageWrapper From 19e9e96f65bf07b7ba19e19959c3c2f298787e23 Mon Sep 17 00:00:00 2001 From: Emanuele Feliziani Date: Fri, 15 Sep 2023 13:53:07 +0200 Subject: [PATCH 16/20] Don't trim empty strings Signed-off-by: Emanuele Feliziani --- src/Form/matching.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Form/matching.js b/src/Form/matching.js index 89c1532e9..41bc130db 100644 --- a/src/Form/matching.js +++ b/src/Form/matching.js @@ -734,7 +734,9 @@ function getInputSubtype (input) { * @return {string} */ const removeExcessWhitespace = (string = '') => { - return (string || '') + if (!string) return '' + + return (string) .replace(/\n/g, ' ') .replace(/\s{2,}/g, ' ').trim() } From d7838909cd529ef5df9a09732c205cc9d1e8334a Mon Sep 17 00:00:00 2001 From: Emanuele Feliziani Date: Fri, 15 Sep 2023 13:54:09 +0200 Subject: [PATCH 17/20] Minor improvement to getText Signed-off-by: Emanuele Feliziani --- src/autofill-utils.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/autofill-utils.js b/src/autofill-utils.js index 78e32d62d..9bd15c972 100644 --- a/src/autofill-utils.js +++ b/src/autofill-utils.js @@ -334,6 +334,7 @@ const buttonMatchesFormType = (el, formObj) => { } } +const buttonInputTypes = ['submit', 'button'] /** * Get the text of an element * @param {Element} el @@ -344,9 +345,14 @@ const getText = (el) => { // this is important in order to give proper attribution of the text to the button if (el instanceof HTMLButtonElement) return removeExcessWhitespace(el.textContent) - if (el instanceof HTMLInputElement && ['submit', 'button'].includes(el.type)) return el.value - if (el instanceof HTMLInputElement && el.type === 'image') { - return removeExcessWhitespace(el.alt || el.value || el.title || el.name) + if (el instanceof HTMLInputElement) { + if (buttonInputTypes.includes(el.type)) { + return el.value + } + + if (el.type === 'image') { + return removeExcessWhitespace(el.alt || el.value || el.title || el.name) + } } let text = '' From 4b1222d76fce793e1c382dd2e0780ce79c11005c Mon Sep 17 00:00:00 2001 From: Emanuele Feliziani Date: Fri, 15 Sep 2023 13:57:30 +0200 Subject: [PATCH 18/20] Don't repeat isFormLikelyToBeUsedAsPageWrapper Signed-off-by: Emanuele Feliziani --- src/Scanner.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/Scanner.js b/src/Scanner.js index faa80eb07..68b12c90b 100644 --- a/src/Scanner.js +++ b/src/Scanner.js @@ -180,10 +180,15 @@ class DefaultScanner { */ getParentForm (input) { if (input instanceof HTMLInputElement || input instanceof HTMLSelectElement) { - // Use input.form unless it encloses most of the DOM - // In that case we proceed to identify more precise wrappers - if (input.form && !isFormLikelyToBeUsedAsPageWrapper(input.form)) { - return input.form + if (input.form) { + // Use input.form unless it encloses most of the DOM + // In that case we proceed to identify more precise wrappers + if ( + this.forms.has(input.form) || // If we've added the form we've already checked that it's not a page wrapper + !isFormLikelyToBeUsedAsPageWrapper(input.form) + ) { + return input.form + } } } From 91dcc51492d0b0e0ece2185b3e7d6baf7f055601 Mon Sep 17 00:00:00 2001 From: Emanuele Feliziani Date: Fri, 15 Sep 2023 16:45:30 +0200 Subject: [PATCH 19/20] Improve for searching loop Signed-off-by: Emanuele Feliziani --- src/Scanner.js | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/src/Scanner.js b/src/Scanner.js index 68b12c90b..23bdadf7d 100644 --- a/src/Scanner.js +++ b/src/Scanner.js @@ -222,10 +222,31 @@ class DefaultScanner { if (this.stopped) return const parentForm = this.getParentForm(input) - const seenFormElements = [...this.forms.keys()] - // Note that el.contains returns true for el itself - const previouslyFoundParent = seenFormElements.find((form) => form.contains(parentForm)) + if (parentForm instanceof HTMLFormElement && this.forms.has(parentForm)) { + // We've met the form, add the input + this.forms.get(parentForm)?.addInput(input) + return + } + + // Check if the forms we've seen are either disconnected, + // or are parent/child of the currently-found form + let previouslyFoundParent, childForm + for (const [formEl] of this.forms) { + // Remove disconnected forms to avoid leaks + if (!formEl.isConnected) { + this.forms.delete(formEl) + continue + } + if (formEl.contains(parentForm)) { + previouslyFoundParent = formEl + break + } + if (parentForm.contains(formEl)) { + childForm = formEl + break + } + } if (previouslyFoundParent) { if (parentForm instanceof HTMLFormElement && parentForm !== previouslyFoundParent) { @@ -237,7 +258,6 @@ class DefaultScanner { } } else { // if this form is an ancestor of an existing form, remove that before adding this - const childForm = seenFormElements.find((form) => parentForm.contains(form)) if (childForm) { this.forms.get(childForm)?.destroy() this.forms.delete(childForm) From e30be19f33f0777e8bd9c496e40491bf1dbfb47f Mon Sep 17 00:00:00 2001 From: Emanuele Feliziani Date: Fri, 15 Sep 2023 16:45:41 +0200 Subject: [PATCH 20/20] Commit compiled files Signed-off-by: Emanuele Feliziani --- dist/autofill-debug.js | 101 +++++++++++++----- dist/autofill.js | 101 +++++++++++++----- .../Resources/assets/autofill-debug.js | 101 +++++++++++++----- swift-package/Resources/assets/autofill.js | 101 +++++++++++++----- 4 files changed, 288 insertions(+), 116 deletions(-) diff --git a/dist/autofill-debug.js b/dist/autofill-debug.js index 5696a9268..799be7996 100644 --- a/dist/autofill-debug.js +++ b/dist/autofill-debug.js @@ -14359,7 +14359,8 @@ function getInputSubtype(input) { const removeExcessWhitespace = function () { let string = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; - return (string || '').replace(/\n/g, ' ').replace(/\s{2,}/g, ' ').trim(); + if (!string) return ''; + return string.replace(/\n/g, ' ').replace(/\s{2,}/g, ' ').trim(); }; /** * Get text from all explicit labels @@ -15426,17 +15427,10 @@ class DefaultScanner { scanAndObserve() { var _window$performance, _window$performance$m, _window$performance2, _window$performance2$; - (_window$performance = window.performance) === null || _window$performance === void 0 ? void 0 : (_window$performance$m = _window$performance.mark) === null || _window$performance$m === void 0 ? void 0 : _window$performance$m.call(_window$performance, 'scanner:init:start'); + (_window$performance = window.performance) === null || _window$performance === void 0 ? void 0 : (_window$performance$m = _window$performance.mark) === null || _window$performance$m === void 0 ? void 0 : _window$performance$m.call(_window$performance, 'initial_scanner:init:start'); this.findEligibleInputs(document); - (_window$performance2 = window.performance) === null || _window$performance2 === void 0 ? void 0 : (_window$performance2$ = _window$performance2.mark) === null || _window$performance2$ === void 0 ? void 0 : _window$performance2$.call(_window$performance2, 'scanner:init:end'); - - if ((0, _autofillUtils.shouldLogPerformance)()) { - var _window$performance3; - - const measurement = (_window$performance3 = window.performance) === null || _window$performance3 === void 0 ? void 0 : _window$performance3.measure('scanner:init', 'scanner:init:start', 'scanner:init:end'); - console.log("Initial scan took ".concat(Math.round(measurement === null || measurement === void 0 ? void 0 : measurement.duration), "ms")); - } - + (_window$performance2 = window.performance) === null || _window$performance2 === void 0 ? void 0 : (_window$performance2$ = _window$performance2.mark) === null || _window$performance2$ === void 0 ? void 0 : _window$performance2$.call(_window$performance2, 'initial_scanner:init:end'); + (0, _autofillUtils.logPerformance)('initial_scanner'); this.mutObs.observe(document.documentElement, { childList: true, subtree: true @@ -15510,10 +15504,13 @@ class DefaultScanner { getParentForm(input) { if (input instanceof HTMLInputElement || input instanceof HTMLSelectElement) { - // Use input.form unless it encloses most of the DOM - // In that case we proceed to identify more precise wrappers - if (input.form && !(0, _autofillUtils.isFormLikelyToBeUsedAsPageWrapper)(input.form)) { - return input.form; + if (input.form) { + // Use input.form unless it encloses most of the DOM + // In that case we proceed to identify more precise wrappers + if (this.forms.has(input.form) || // If we've added the form we've already checked that it's not a page wrapper + !(0, _autofillUtils.isFormLikelyToBeUsedAsPageWrapper)(input.form)) { + return input.form; + } } } @@ -15549,28 +15546,53 @@ class DefaultScanner { addInput(input) { if (this.stopped) return; const parentForm = this.getParentForm(input); - const seenFormElements = [...this.forms.keys()]; // Note that el.contains returns true for el itself - const previouslyFoundParent = seenFormElements.find(form => form.contains(parentForm)); + if (parentForm instanceof HTMLFormElement && this.forms.has(parentForm)) { + var _this$forms$get; + + // We've met the form, add the input + (_this$forms$get = this.forms.get(parentForm)) === null || _this$forms$get === void 0 ? void 0 : _this$forms$get.addInput(input); + return; + } // Check if the forms we've seen are either disconnected, + // or are parent/child of the currently-found form + + + let previouslyFoundParent, childForm; + + for (const [formEl] of this.forms) { + // Remove disconnected forms to avoid leaks + if (!formEl.isConnected) { + this.forms.delete(formEl); + continue; + } + + if (formEl.contains(parentForm)) { + previouslyFoundParent = formEl; + break; + } + + if (parentForm.contains(formEl)) { + childForm = formEl; + break; + } + } if (previouslyFoundParent) { if (parentForm instanceof HTMLFormElement && parentForm !== previouslyFoundParent) { // If we had a prior parent but this is an explicit form, the previous was a false positive this.forms.delete(previouslyFoundParent); } else { - var _this$forms$get; + var _this$forms$get2; // If we've already met the form or a descendant, add the input - (_this$forms$get = this.forms.get(previouslyFoundParent)) === null || _this$forms$get === void 0 ? void 0 : _this$forms$get.addInput(input); + (_this$forms$get2 = this.forms.get(previouslyFoundParent)) === null || _this$forms$get2 === void 0 ? void 0 : _this$forms$get2.addInput(input); } } else { // if this form is an ancestor of an existing form, remove that before adding this - const childForm = seenFormElements.find(form => parentForm.contains(form)); - if (childForm) { - var _this$forms$get2; + var _this$forms$get3; - (_this$forms$get2 = this.forms.get(childForm)) === null || _this$forms$get2 === void 0 ? void 0 : _this$forms$get2.destroy(); + (_this$forms$get3 = this.forms.get(childForm)) === null || _this$forms$get3 === void 0 ? void 0 : _this$forms$get3.destroy(); this.forms.delete(childForm); } // Only add the form if below the limit of forms per page @@ -15604,9 +15626,14 @@ class DefaultScanner { clearTimeout(this.debounceTimer); this.debounceTimer = setTimeout(() => { + var _window$performance3, _window$performance3$, _window$performance4, _window$performance4$; + + (_window$performance3 = window.performance) === null || _window$performance3 === void 0 ? void 0 : (_window$performance3$ = _window$performance3.mark) === null || _window$performance3$ === void 0 ? void 0 : _window$performance3$.call(_window$performance3, 'scanner:init:start'); this.processChangedElements(); this.changedElements.clear(); this.rescanAll = false; + (_window$performance4 = window.performance) === null || _window$performance4 === void 0 ? void 0 : (_window$performance4$ = _window$performance4.mark) === null || _window$performance4$ === void 0 ? void 0 : _window$performance4$.call(_window$performance4, 'scanner:init:end'); + (0, _autofillUtils.logPerformance)('scanner'); }, this.options.debounceTimePeriod); } /** @@ -17784,6 +17811,7 @@ exports.isLikelyASubmitButton = exports.isIncontextSignupEnabledFromProcessedCon exports.isLocalNetwork = isLocalNetwork; exports.isPotentiallyViewable = void 0; exports.isValidTLD = isValidTLD; +exports.logPerformance = logPerformance; exports.setValue = exports.sendAndWaitForAnswer = exports.safeExecute = exports.removeInlineStyles = exports.notifyWebApp = void 0; exports.shouldLog = shouldLog; exports.shouldLogPerformance = shouldLogPerformance; @@ -18204,23 +18232,28 @@ const buttonMatchesFormType = (el, formObj) => { return true; } }; + +exports.buttonMatchesFormType = buttonMatchesFormType; +const buttonInputTypes = ['submit', 'button']; /** * Get the text of an element * @param {Element} el * @returns {string} */ - -exports.buttonMatchesFormType = buttonMatchesFormType; - const getText = el => { // for buttons, we don't care about descendants, just get the whole text as is // this is important in order to give proper attribution of the text to the button if (el instanceof HTMLButtonElement) return (0, _matching.removeExcessWhitespace)(el.textContent); - if (el instanceof HTMLInputElement && ['submit', 'button'].includes(el.type)) return el.value; - if (el instanceof HTMLInputElement && el.type === 'image') { - return (0, _matching.removeExcessWhitespace)(el.alt || el.value || el.title || el.name); + if (el instanceof HTMLInputElement) { + if (buttonInputTypes.includes(el.type)) { + return el.value; + } + + if (el.type === 'image') { + return (0, _matching.removeExcessWhitespace)(el.alt || el.value || el.title || el.name); + } } let text = ''; @@ -18311,6 +18344,16 @@ function readDebugSetting(setting) { return false; } } + +function logPerformance(markName) { + if (shouldLogPerformance()) { + var _window$performance, _window$performance2; + + const measurement = (_window$performance = window.performance) === null || _window$performance === void 0 ? void 0 : _window$performance.measure("".concat(markName, ":init"), "".concat(markName, ":init:start"), "".concat(markName, ":init:end")); + console.log("".concat(markName, " took ").concat(Math.round(measurement === null || measurement === void 0 ? void 0 : measurement.duration), "ms")); + (_window$performance2 = window.performance) === null || _window$performance2 === void 0 ? void 0 : _window$performance2.clearMarks(); + } +} /** * * @param {Function} callback diff --git a/dist/autofill.js b/dist/autofill.js index 57884672e..0d68909e1 100644 --- a/dist/autofill.js +++ b/dist/autofill.js @@ -10683,7 +10683,8 @@ function getInputSubtype(input) { const removeExcessWhitespace = function () { let string = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; - return (string || '').replace(/\n/g, ' ').replace(/\s{2,}/g, ' ').trim(); + if (!string) return ''; + return string.replace(/\n/g, ' ').replace(/\s{2,}/g, ' ').trim(); }; /** * Get text from all explicit labels @@ -11750,17 +11751,10 @@ class DefaultScanner { scanAndObserve() { var _window$performance, _window$performance$m, _window$performance2, _window$performance2$; - (_window$performance = window.performance) === null || _window$performance === void 0 ? void 0 : (_window$performance$m = _window$performance.mark) === null || _window$performance$m === void 0 ? void 0 : _window$performance$m.call(_window$performance, 'scanner:init:start'); + (_window$performance = window.performance) === null || _window$performance === void 0 ? void 0 : (_window$performance$m = _window$performance.mark) === null || _window$performance$m === void 0 ? void 0 : _window$performance$m.call(_window$performance, 'initial_scanner:init:start'); this.findEligibleInputs(document); - (_window$performance2 = window.performance) === null || _window$performance2 === void 0 ? void 0 : (_window$performance2$ = _window$performance2.mark) === null || _window$performance2$ === void 0 ? void 0 : _window$performance2$.call(_window$performance2, 'scanner:init:end'); - - if ((0, _autofillUtils.shouldLogPerformance)()) { - var _window$performance3; - - const measurement = (_window$performance3 = window.performance) === null || _window$performance3 === void 0 ? void 0 : _window$performance3.measure('scanner:init', 'scanner:init:start', 'scanner:init:end'); - console.log("Initial scan took ".concat(Math.round(measurement === null || measurement === void 0 ? void 0 : measurement.duration), "ms")); - } - + (_window$performance2 = window.performance) === null || _window$performance2 === void 0 ? void 0 : (_window$performance2$ = _window$performance2.mark) === null || _window$performance2$ === void 0 ? void 0 : _window$performance2$.call(_window$performance2, 'initial_scanner:init:end'); + (0, _autofillUtils.logPerformance)('initial_scanner'); this.mutObs.observe(document.documentElement, { childList: true, subtree: true @@ -11834,10 +11828,13 @@ class DefaultScanner { getParentForm(input) { if (input instanceof HTMLInputElement || input instanceof HTMLSelectElement) { - // Use input.form unless it encloses most of the DOM - // In that case we proceed to identify more precise wrappers - if (input.form && !(0, _autofillUtils.isFormLikelyToBeUsedAsPageWrapper)(input.form)) { - return input.form; + if (input.form) { + // Use input.form unless it encloses most of the DOM + // In that case we proceed to identify more precise wrappers + if (this.forms.has(input.form) || // If we've added the form we've already checked that it's not a page wrapper + !(0, _autofillUtils.isFormLikelyToBeUsedAsPageWrapper)(input.form)) { + return input.form; + } } } @@ -11873,28 +11870,53 @@ class DefaultScanner { addInput(input) { if (this.stopped) return; const parentForm = this.getParentForm(input); - const seenFormElements = [...this.forms.keys()]; // Note that el.contains returns true for el itself - const previouslyFoundParent = seenFormElements.find(form => form.contains(parentForm)); + if (parentForm instanceof HTMLFormElement && this.forms.has(parentForm)) { + var _this$forms$get; + + // We've met the form, add the input + (_this$forms$get = this.forms.get(parentForm)) === null || _this$forms$get === void 0 ? void 0 : _this$forms$get.addInput(input); + return; + } // Check if the forms we've seen are either disconnected, + // or are parent/child of the currently-found form + + + let previouslyFoundParent, childForm; + + for (const [formEl] of this.forms) { + // Remove disconnected forms to avoid leaks + if (!formEl.isConnected) { + this.forms.delete(formEl); + continue; + } + + if (formEl.contains(parentForm)) { + previouslyFoundParent = formEl; + break; + } + + if (parentForm.contains(formEl)) { + childForm = formEl; + break; + } + } if (previouslyFoundParent) { if (parentForm instanceof HTMLFormElement && parentForm !== previouslyFoundParent) { // If we had a prior parent but this is an explicit form, the previous was a false positive this.forms.delete(previouslyFoundParent); } else { - var _this$forms$get; + var _this$forms$get2; // If we've already met the form or a descendant, add the input - (_this$forms$get = this.forms.get(previouslyFoundParent)) === null || _this$forms$get === void 0 ? void 0 : _this$forms$get.addInput(input); + (_this$forms$get2 = this.forms.get(previouslyFoundParent)) === null || _this$forms$get2 === void 0 ? void 0 : _this$forms$get2.addInput(input); } } else { // if this form is an ancestor of an existing form, remove that before adding this - const childForm = seenFormElements.find(form => parentForm.contains(form)); - if (childForm) { - var _this$forms$get2; + var _this$forms$get3; - (_this$forms$get2 = this.forms.get(childForm)) === null || _this$forms$get2 === void 0 ? void 0 : _this$forms$get2.destroy(); + (_this$forms$get3 = this.forms.get(childForm)) === null || _this$forms$get3 === void 0 ? void 0 : _this$forms$get3.destroy(); this.forms.delete(childForm); } // Only add the form if below the limit of forms per page @@ -11928,9 +11950,14 @@ class DefaultScanner { clearTimeout(this.debounceTimer); this.debounceTimer = setTimeout(() => { + var _window$performance3, _window$performance3$, _window$performance4, _window$performance4$; + + (_window$performance3 = window.performance) === null || _window$performance3 === void 0 ? void 0 : (_window$performance3$ = _window$performance3.mark) === null || _window$performance3$ === void 0 ? void 0 : _window$performance3$.call(_window$performance3, 'scanner:init:start'); this.processChangedElements(); this.changedElements.clear(); this.rescanAll = false; + (_window$performance4 = window.performance) === null || _window$performance4 === void 0 ? void 0 : (_window$performance4$ = _window$performance4.mark) === null || _window$performance4$ === void 0 ? void 0 : _window$performance4$.call(_window$performance4, 'scanner:init:end'); + (0, _autofillUtils.logPerformance)('scanner'); }, this.options.debounceTimePeriod); } /** @@ -14108,6 +14135,7 @@ exports.isLikelyASubmitButton = exports.isIncontextSignupEnabledFromProcessedCon exports.isLocalNetwork = isLocalNetwork; exports.isPotentiallyViewable = void 0; exports.isValidTLD = isValidTLD; +exports.logPerformance = logPerformance; exports.setValue = exports.sendAndWaitForAnswer = exports.safeExecute = exports.removeInlineStyles = exports.notifyWebApp = void 0; exports.shouldLog = shouldLog; exports.shouldLogPerformance = shouldLogPerformance; @@ -14528,23 +14556,28 @@ const buttonMatchesFormType = (el, formObj) => { return true; } }; + +exports.buttonMatchesFormType = buttonMatchesFormType; +const buttonInputTypes = ['submit', 'button']; /** * Get the text of an element * @param {Element} el * @returns {string} */ - -exports.buttonMatchesFormType = buttonMatchesFormType; - const getText = el => { // for buttons, we don't care about descendants, just get the whole text as is // this is important in order to give proper attribution of the text to the button if (el instanceof HTMLButtonElement) return (0, _matching.removeExcessWhitespace)(el.textContent); - if (el instanceof HTMLInputElement && ['submit', 'button'].includes(el.type)) return el.value; - if (el instanceof HTMLInputElement && el.type === 'image') { - return (0, _matching.removeExcessWhitespace)(el.alt || el.value || el.title || el.name); + if (el instanceof HTMLInputElement) { + if (buttonInputTypes.includes(el.type)) { + return el.value; + } + + if (el.type === 'image') { + return (0, _matching.removeExcessWhitespace)(el.alt || el.value || el.title || el.name); + } } let text = ''; @@ -14635,6 +14668,16 @@ function readDebugSetting(setting) { return false; } } + +function logPerformance(markName) { + if (shouldLogPerformance()) { + var _window$performance, _window$performance2; + + const measurement = (_window$performance = window.performance) === null || _window$performance === void 0 ? void 0 : _window$performance.measure("".concat(markName, ":init"), "".concat(markName, ":init:start"), "".concat(markName, ":init:end")); + console.log("".concat(markName, " took ").concat(Math.round(measurement === null || measurement === void 0 ? void 0 : measurement.duration), "ms")); + (_window$performance2 = window.performance) === null || _window$performance2 === void 0 ? void 0 : _window$performance2.clearMarks(); + } +} /** * * @param {Function} callback diff --git a/swift-package/Resources/assets/autofill-debug.js b/swift-package/Resources/assets/autofill-debug.js index 5696a9268..799be7996 100644 --- a/swift-package/Resources/assets/autofill-debug.js +++ b/swift-package/Resources/assets/autofill-debug.js @@ -14359,7 +14359,8 @@ function getInputSubtype(input) { const removeExcessWhitespace = function () { let string = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; - return (string || '').replace(/\n/g, ' ').replace(/\s{2,}/g, ' ').trim(); + if (!string) return ''; + return string.replace(/\n/g, ' ').replace(/\s{2,}/g, ' ').trim(); }; /** * Get text from all explicit labels @@ -15426,17 +15427,10 @@ class DefaultScanner { scanAndObserve() { var _window$performance, _window$performance$m, _window$performance2, _window$performance2$; - (_window$performance = window.performance) === null || _window$performance === void 0 ? void 0 : (_window$performance$m = _window$performance.mark) === null || _window$performance$m === void 0 ? void 0 : _window$performance$m.call(_window$performance, 'scanner:init:start'); + (_window$performance = window.performance) === null || _window$performance === void 0 ? void 0 : (_window$performance$m = _window$performance.mark) === null || _window$performance$m === void 0 ? void 0 : _window$performance$m.call(_window$performance, 'initial_scanner:init:start'); this.findEligibleInputs(document); - (_window$performance2 = window.performance) === null || _window$performance2 === void 0 ? void 0 : (_window$performance2$ = _window$performance2.mark) === null || _window$performance2$ === void 0 ? void 0 : _window$performance2$.call(_window$performance2, 'scanner:init:end'); - - if ((0, _autofillUtils.shouldLogPerformance)()) { - var _window$performance3; - - const measurement = (_window$performance3 = window.performance) === null || _window$performance3 === void 0 ? void 0 : _window$performance3.measure('scanner:init', 'scanner:init:start', 'scanner:init:end'); - console.log("Initial scan took ".concat(Math.round(measurement === null || measurement === void 0 ? void 0 : measurement.duration), "ms")); - } - + (_window$performance2 = window.performance) === null || _window$performance2 === void 0 ? void 0 : (_window$performance2$ = _window$performance2.mark) === null || _window$performance2$ === void 0 ? void 0 : _window$performance2$.call(_window$performance2, 'initial_scanner:init:end'); + (0, _autofillUtils.logPerformance)('initial_scanner'); this.mutObs.observe(document.documentElement, { childList: true, subtree: true @@ -15510,10 +15504,13 @@ class DefaultScanner { getParentForm(input) { if (input instanceof HTMLInputElement || input instanceof HTMLSelectElement) { - // Use input.form unless it encloses most of the DOM - // In that case we proceed to identify more precise wrappers - if (input.form && !(0, _autofillUtils.isFormLikelyToBeUsedAsPageWrapper)(input.form)) { - return input.form; + if (input.form) { + // Use input.form unless it encloses most of the DOM + // In that case we proceed to identify more precise wrappers + if (this.forms.has(input.form) || // If we've added the form we've already checked that it's not a page wrapper + !(0, _autofillUtils.isFormLikelyToBeUsedAsPageWrapper)(input.form)) { + return input.form; + } } } @@ -15549,28 +15546,53 @@ class DefaultScanner { addInput(input) { if (this.stopped) return; const parentForm = this.getParentForm(input); - const seenFormElements = [...this.forms.keys()]; // Note that el.contains returns true for el itself - const previouslyFoundParent = seenFormElements.find(form => form.contains(parentForm)); + if (parentForm instanceof HTMLFormElement && this.forms.has(parentForm)) { + var _this$forms$get; + + // We've met the form, add the input + (_this$forms$get = this.forms.get(parentForm)) === null || _this$forms$get === void 0 ? void 0 : _this$forms$get.addInput(input); + return; + } // Check if the forms we've seen are either disconnected, + // or are parent/child of the currently-found form + + + let previouslyFoundParent, childForm; + + for (const [formEl] of this.forms) { + // Remove disconnected forms to avoid leaks + if (!formEl.isConnected) { + this.forms.delete(formEl); + continue; + } + + if (formEl.contains(parentForm)) { + previouslyFoundParent = formEl; + break; + } + + if (parentForm.contains(formEl)) { + childForm = formEl; + break; + } + } if (previouslyFoundParent) { if (parentForm instanceof HTMLFormElement && parentForm !== previouslyFoundParent) { // If we had a prior parent but this is an explicit form, the previous was a false positive this.forms.delete(previouslyFoundParent); } else { - var _this$forms$get; + var _this$forms$get2; // If we've already met the form or a descendant, add the input - (_this$forms$get = this.forms.get(previouslyFoundParent)) === null || _this$forms$get === void 0 ? void 0 : _this$forms$get.addInput(input); + (_this$forms$get2 = this.forms.get(previouslyFoundParent)) === null || _this$forms$get2 === void 0 ? void 0 : _this$forms$get2.addInput(input); } } else { // if this form is an ancestor of an existing form, remove that before adding this - const childForm = seenFormElements.find(form => parentForm.contains(form)); - if (childForm) { - var _this$forms$get2; + var _this$forms$get3; - (_this$forms$get2 = this.forms.get(childForm)) === null || _this$forms$get2 === void 0 ? void 0 : _this$forms$get2.destroy(); + (_this$forms$get3 = this.forms.get(childForm)) === null || _this$forms$get3 === void 0 ? void 0 : _this$forms$get3.destroy(); this.forms.delete(childForm); } // Only add the form if below the limit of forms per page @@ -15604,9 +15626,14 @@ class DefaultScanner { clearTimeout(this.debounceTimer); this.debounceTimer = setTimeout(() => { + var _window$performance3, _window$performance3$, _window$performance4, _window$performance4$; + + (_window$performance3 = window.performance) === null || _window$performance3 === void 0 ? void 0 : (_window$performance3$ = _window$performance3.mark) === null || _window$performance3$ === void 0 ? void 0 : _window$performance3$.call(_window$performance3, 'scanner:init:start'); this.processChangedElements(); this.changedElements.clear(); this.rescanAll = false; + (_window$performance4 = window.performance) === null || _window$performance4 === void 0 ? void 0 : (_window$performance4$ = _window$performance4.mark) === null || _window$performance4$ === void 0 ? void 0 : _window$performance4$.call(_window$performance4, 'scanner:init:end'); + (0, _autofillUtils.logPerformance)('scanner'); }, this.options.debounceTimePeriod); } /** @@ -17784,6 +17811,7 @@ exports.isLikelyASubmitButton = exports.isIncontextSignupEnabledFromProcessedCon exports.isLocalNetwork = isLocalNetwork; exports.isPotentiallyViewable = void 0; exports.isValidTLD = isValidTLD; +exports.logPerformance = logPerformance; exports.setValue = exports.sendAndWaitForAnswer = exports.safeExecute = exports.removeInlineStyles = exports.notifyWebApp = void 0; exports.shouldLog = shouldLog; exports.shouldLogPerformance = shouldLogPerformance; @@ -18204,23 +18232,28 @@ const buttonMatchesFormType = (el, formObj) => { return true; } }; + +exports.buttonMatchesFormType = buttonMatchesFormType; +const buttonInputTypes = ['submit', 'button']; /** * Get the text of an element * @param {Element} el * @returns {string} */ - -exports.buttonMatchesFormType = buttonMatchesFormType; - const getText = el => { // for buttons, we don't care about descendants, just get the whole text as is // this is important in order to give proper attribution of the text to the button if (el instanceof HTMLButtonElement) return (0, _matching.removeExcessWhitespace)(el.textContent); - if (el instanceof HTMLInputElement && ['submit', 'button'].includes(el.type)) return el.value; - if (el instanceof HTMLInputElement && el.type === 'image') { - return (0, _matching.removeExcessWhitespace)(el.alt || el.value || el.title || el.name); + if (el instanceof HTMLInputElement) { + if (buttonInputTypes.includes(el.type)) { + return el.value; + } + + if (el.type === 'image') { + return (0, _matching.removeExcessWhitespace)(el.alt || el.value || el.title || el.name); + } } let text = ''; @@ -18311,6 +18344,16 @@ function readDebugSetting(setting) { return false; } } + +function logPerformance(markName) { + if (shouldLogPerformance()) { + var _window$performance, _window$performance2; + + const measurement = (_window$performance = window.performance) === null || _window$performance === void 0 ? void 0 : _window$performance.measure("".concat(markName, ":init"), "".concat(markName, ":init:start"), "".concat(markName, ":init:end")); + console.log("".concat(markName, " took ").concat(Math.round(measurement === null || measurement === void 0 ? void 0 : measurement.duration), "ms")); + (_window$performance2 = window.performance) === null || _window$performance2 === void 0 ? void 0 : _window$performance2.clearMarks(); + } +} /** * * @param {Function} callback diff --git a/swift-package/Resources/assets/autofill.js b/swift-package/Resources/assets/autofill.js index 57884672e..0d68909e1 100644 --- a/swift-package/Resources/assets/autofill.js +++ b/swift-package/Resources/assets/autofill.js @@ -10683,7 +10683,8 @@ function getInputSubtype(input) { const removeExcessWhitespace = function () { let string = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; - return (string || '').replace(/\n/g, ' ').replace(/\s{2,}/g, ' ').trim(); + if (!string) return ''; + return string.replace(/\n/g, ' ').replace(/\s{2,}/g, ' ').trim(); }; /** * Get text from all explicit labels @@ -11750,17 +11751,10 @@ class DefaultScanner { scanAndObserve() { var _window$performance, _window$performance$m, _window$performance2, _window$performance2$; - (_window$performance = window.performance) === null || _window$performance === void 0 ? void 0 : (_window$performance$m = _window$performance.mark) === null || _window$performance$m === void 0 ? void 0 : _window$performance$m.call(_window$performance, 'scanner:init:start'); + (_window$performance = window.performance) === null || _window$performance === void 0 ? void 0 : (_window$performance$m = _window$performance.mark) === null || _window$performance$m === void 0 ? void 0 : _window$performance$m.call(_window$performance, 'initial_scanner:init:start'); this.findEligibleInputs(document); - (_window$performance2 = window.performance) === null || _window$performance2 === void 0 ? void 0 : (_window$performance2$ = _window$performance2.mark) === null || _window$performance2$ === void 0 ? void 0 : _window$performance2$.call(_window$performance2, 'scanner:init:end'); - - if ((0, _autofillUtils.shouldLogPerformance)()) { - var _window$performance3; - - const measurement = (_window$performance3 = window.performance) === null || _window$performance3 === void 0 ? void 0 : _window$performance3.measure('scanner:init', 'scanner:init:start', 'scanner:init:end'); - console.log("Initial scan took ".concat(Math.round(measurement === null || measurement === void 0 ? void 0 : measurement.duration), "ms")); - } - + (_window$performance2 = window.performance) === null || _window$performance2 === void 0 ? void 0 : (_window$performance2$ = _window$performance2.mark) === null || _window$performance2$ === void 0 ? void 0 : _window$performance2$.call(_window$performance2, 'initial_scanner:init:end'); + (0, _autofillUtils.logPerformance)('initial_scanner'); this.mutObs.observe(document.documentElement, { childList: true, subtree: true @@ -11834,10 +11828,13 @@ class DefaultScanner { getParentForm(input) { if (input instanceof HTMLInputElement || input instanceof HTMLSelectElement) { - // Use input.form unless it encloses most of the DOM - // In that case we proceed to identify more precise wrappers - if (input.form && !(0, _autofillUtils.isFormLikelyToBeUsedAsPageWrapper)(input.form)) { - return input.form; + if (input.form) { + // Use input.form unless it encloses most of the DOM + // In that case we proceed to identify more precise wrappers + if (this.forms.has(input.form) || // If we've added the form we've already checked that it's not a page wrapper + !(0, _autofillUtils.isFormLikelyToBeUsedAsPageWrapper)(input.form)) { + return input.form; + } } } @@ -11873,28 +11870,53 @@ class DefaultScanner { addInput(input) { if (this.stopped) return; const parentForm = this.getParentForm(input); - const seenFormElements = [...this.forms.keys()]; // Note that el.contains returns true for el itself - const previouslyFoundParent = seenFormElements.find(form => form.contains(parentForm)); + if (parentForm instanceof HTMLFormElement && this.forms.has(parentForm)) { + var _this$forms$get; + + // We've met the form, add the input + (_this$forms$get = this.forms.get(parentForm)) === null || _this$forms$get === void 0 ? void 0 : _this$forms$get.addInput(input); + return; + } // Check if the forms we've seen are either disconnected, + // or are parent/child of the currently-found form + + + let previouslyFoundParent, childForm; + + for (const [formEl] of this.forms) { + // Remove disconnected forms to avoid leaks + if (!formEl.isConnected) { + this.forms.delete(formEl); + continue; + } + + if (formEl.contains(parentForm)) { + previouslyFoundParent = formEl; + break; + } + + if (parentForm.contains(formEl)) { + childForm = formEl; + break; + } + } if (previouslyFoundParent) { if (parentForm instanceof HTMLFormElement && parentForm !== previouslyFoundParent) { // If we had a prior parent but this is an explicit form, the previous was a false positive this.forms.delete(previouslyFoundParent); } else { - var _this$forms$get; + var _this$forms$get2; // If we've already met the form or a descendant, add the input - (_this$forms$get = this.forms.get(previouslyFoundParent)) === null || _this$forms$get === void 0 ? void 0 : _this$forms$get.addInput(input); + (_this$forms$get2 = this.forms.get(previouslyFoundParent)) === null || _this$forms$get2 === void 0 ? void 0 : _this$forms$get2.addInput(input); } } else { // if this form is an ancestor of an existing form, remove that before adding this - const childForm = seenFormElements.find(form => parentForm.contains(form)); - if (childForm) { - var _this$forms$get2; + var _this$forms$get3; - (_this$forms$get2 = this.forms.get(childForm)) === null || _this$forms$get2 === void 0 ? void 0 : _this$forms$get2.destroy(); + (_this$forms$get3 = this.forms.get(childForm)) === null || _this$forms$get3 === void 0 ? void 0 : _this$forms$get3.destroy(); this.forms.delete(childForm); } // Only add the form if below the limit of forms per page @@ -11928,9 +11950,14 @@ class DefaultScanner { clearTimeout(this.debounceTimer); this.debounceTimer = setTimeout(() => { + var _window$performance3, _window$performance3$, _window$performance4, _window$performance4$; + + (_window$performance3 = window.performance) === null || _window$performance3 === void 0 ? void 0 : (_window$performance3$ = _window$performance3.mark) === null || _window$performance3$ === void 0 ? void 0 : _window$performance3$.call(_window$performance3, 'scanner:init:start'); this.processChangedElements(); this.changedElements.clear(); this.rescanAll = false; + (_window$performance4 = window.performance) === null || _window$performance4 === void 0 ? void 0 : (_window$performance4$ = _window$performance4.mark) === null || _window$performance4$ === void 0 ? void 0 : _window$performance4$.call(_window$performance4, 'scanner:init:end'); + (0, _autofillUtils.logPerformance)('scanner'); }, this.options.debounceTimePeriod); } /** @@ -14108,6 +14135,7 @@ exports.isLikelyASubmitButton = exports.isIncontextSignupEnabledFromProcessedCon exports.isLocalNetwork = isLocalNetwork; exports.isPotentiallyViewable = void 0; exports.isValidTLD = isValidTLD; +exports.logPerformance = logPerformance; exports.setValue = exports.sendAndWaitForAnswer = exports.safeExecute = exports.removeInlineStyles = exports.notifyWebApp = void 0; exports.shouldLog = shouldLog; exports.shouldLogPerformance = shouldLogPerformance; @@ -14528,23 +14556,28 @@ const buttonMatchesFormType = (el, formObj) => { return true; } }; + +exports.buttonMatchesFormType = buttonMatchesFormType; +const buttonInputTypes = ['submit', 'button']; /** * Get the text of an element * @param {Element} el * @returns {string} */ - -exports.buttonMatchesFormType = buttonMatchesFormType; - const getText = el => { // for buttons, we don't care about descendants, just get the whole text as is // this is important in order to give proper attribution of the text to the button if (el instanceof HTMLButtonElement) return (0, _matching.removeExcessWhitespace)(el.textContent); - if (el instanceof HTMLInputElement && ['submit', 'button'].includes(el.type)) return el.value; - if (el instanceof HTMLInputElement && el.type === 'image') { - return (0, _matching.removeExcessWhitespace)(el.alt || el.value || el.title || el.name); + if (el instanceof HTMLInputElement) { + if (buttonInputTypes.includes(el.type)) { + return el.value; + } + + if (el.type === 'image') { + return (0, _matching.removeExcessWhitespace)(el.alt || el.value || el.title || el.name); + } } let text = ''; @@ -14635,6 +14668,16 @@ function readDebugSetting(setting) { return false; } } + +function logPerformance(markName) { + if (shouldLogPerformance()) { + var _window$performance, _window$performance2; + + const measurement = (_window$performance = window.performance) === null || _window$performance === void 0 ? void 0 : _window$performance.measure("".concat(markName, ":init"), "".concat(markName, ":init:start"), "".concat(markName, ":init:end")); + console.log("".concat(markName, " took ").concat(Math.round(measurement === null || measurement === void 0 ? void 0 : measurement.duration), "ms")); + (_window$performance2 = window.performance) === null || _window$performance2 === void 0 ? void 0 : _window$performance2.clearMarks(); + } +} /** * * @param {Function} callback