diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:13:33 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:13:33 +0000 |
commit | 086c044dc34dfc0f74fbe41f4ecb402b2cd34884 (patch) | |
tree | a4f824bd33cb075dd5aa3eb5a0a94af221bbe83a /toolkit/components/formautofill/FormAutofillChild.sys.mjs | |
parent | Adding debian version 124.0.1-1. (diff) | |
download | firefox-086c044dc34dfc0f74fbe41f4ecb402b2cd34884.tar.xz firefox-086c044dc34dfc0f74fbe41f4ecb402b2cd34884.zip |
Merging upstream version 125.0.1.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/formautofill/FormAutofillChild.sys.mjs')
-rw-r--r-- | toolkit/components/formautofill/FormAutofillChild.sys.mjs | 524 |
1 files changed, 418 insertions, 106 deletions
diff --git a/toolkit/components/formautofill/FormAutofillChild.sys.mjs b/toolkit/components/formautofill/FormAutofillChild.sys.mjs index c40bfddbce..8678a7bd45 100644 --- a/toolkit/components/formautofill/FormAutofillChild.sys.mjs +++ b/toolkit/components/formautofill/FormAutofillChild.sys.mjs @@ -2,16 +2,36 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; +import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; + const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { AutoCompleteChild: "resource://gre/actors/AutoCompleteChild.sys.mjs", + AutofillTelemetry: "resource://gre/modules/shared/AutofillTelemetry.sys.mjs", FormAutofill: "resource://autofill/FormAutofill.sys.mjs", FormAutofillContent: "resource://autofill/FormAutofillContent.sys.mjs", FormAutofillUtils: "resource://gre/modules/shared/FormAutofillUtils.sys.mjs", + FormStateManager: "resource://gre/modules/shared/FormStateManager.sys.mjs", + PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", + ProfileAutocomplete: + "resource://autofill/AutofillProfileAutoComplete.sys.mjs", setTimeout: "resource://gre/modules/Timer.sys.mjs", + FORM_SUBMISSION_REASON: "resource://gre/actors/FormHandlerChild.sys.mjs", }); +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "DELEGATE_AUTOCOMPLETE", + "toolkit.autocomplete.delegate", + false +); + +const formFillController = Cc[ + "@mozilla.org/satchel/form-fill-controller;1" +].getService(Ci.nsIFormFillController); + const observer = { QueryInterface: ChromeUtils.generateQI([ "nsIWebProgressListener", @@ -31,7 +51,7 @@ const observer = { formAutofillChild.onPageNavigation(); }, - onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) { + onStateChange(aWebProgress, aRequest, aStateFlags, _aStatus) { if ( // if restoring a previously-rendered presentation (bfcache) aStateFlags & Ci.nsIWebProgressListener.STATE_RESTORING && @@ -77,21 +97,34 @@ export class FormAutofillChild extends JSWindowActorChild { constructor() { super(); + this.log = lazy.FormAutofill.defineLogGetter(this, "FormAutofillChild"); + this.debug("init"); + this._nextHandleElement = null; - this._alreadyDOMContentLoaded = false; this._hasDOMContentLoadedHandler = false; this._hasPendingTask = false; - this.testListener = null; + + // Flag indicating whether the form is waiting to be filled by Autofill. + this._autofillPending = false; + + /** + * @type {FormAutofillFieldDetailsManager} handling state management of current forms and handlers. + */ + this._fieldDetailsManager = new lazy.FormStateManager( + this.formSubmitted.bind(this), + this.formAutofilled.bind(this) + ); lazy.AutoCompleteChild.addPopupStateListener(this); } didDestroy() { + this._fieldDetailsManager.didDestroy(); + lazy.AutoCompleteChild.removePopupStateListener(this); - lazy.FormAutofillContent.didDestroy(); } - popupStateChanged(messageName, data, target) { + popupStateChanged(messageName, data, _target) { let docShell; try { docShell = this.docShell; @@ -108,50 +141,56 @@ export class FormAutofillChild extends JSWindowActorChild { switch (messageName) { case "FormAutoComplete:PopupClosed": { - lazy.FormAutofillContent.onPopupClosed(data.selectedRowStyle); + this.onPopupClosed(data.selectedRowStyle); Services.tm.dispatchToMainThread(() => { - chromeEventHandler.removeEventListener( - "keydown", - lazy.FormAutofillContent._onKeyDown, - true - ); + chromeEventHandler.removeEventListener("keydown", this, true); }); break; } case "FormAutoComplete:PopupOpened": { - lazy.FormAutofillContent.onPopupOpened(); - chromeEventHandler.addEventListener( - "keydown", - lazy.FormAutofillContent._onKeyDown, - true - ); + this.onPopupOpened(); + chromeEventHandler.addEventListener("keydown", this, true); break; } } } /** - * Invokes the FormAutofillContent to identify the autofill fields - * and consider opening the dropdown menu for the focused field - * + * Identifies and marks each autofill field */ - _doIdentifyAutofillFields() { + identifyAutofillFields() { if (this._hasPendingTask) { return; } this._hasPendingTask = true; lazy.setTimeout(() => { - const isAnyFieldIdentified = - lazy.FormAutofillContent.identifyAutofillFields( - this._nextHandleElement - ); - if (isAnyFieldIdentified) { + const element = this._nextHandleElement; + this.debug( + `identifyAutofillFields: ${element.ownerDocument.location?.hostname}` + ); + + if ( + lazy.DELEGATE_AUTOCOMPLETE || + !lazy.FormAutofillContent.savedFieldNames + ) { + this.debug("identifyAutofillFields: savedFieldNames are not known yet"); + + // Init can be asynchronous because we don't need anything from the parent + // at this point. + this.sendAsyncMessage("FormAutofill:InitStorage"); + } + + const validDetails = + this._fieldDetailsManager.identifyAutofillFields(element); + + validDetails?.forEach(detail => + this._markAsAutofillField(detail.element) + ); + if (validDetails.length) { if (lazy.FormAutofill.captureOnFormRemoval) { - this.registerDOMDocFetchSuccessEventListener( - this._nextHandleElement.ownerDocument - ); + this.registerDOMDocFetchSuccessEventListener(); } if (lazy.FormAutofill.captureOnPageNavigation) { this.registerProgressListener(); @@ -163,7 +202,7 @@ export class FormAutofillChild extends JSWindowActorChild { // This is for testing purpose only which sends a notification to indicate that the // form has been identified, and ready to open popup. this.sendAsyncMessage("FormAutofill:FieldsIdentified"); - lazy.FormAutofillContent.updateActiveInput(); + this.updateActiveInput(); }); } @@ -192,13 +231,18 @@ export class FormAutofillChild extends JSWindowActorChild { /** * After being notified of a page navigation, we check whether * the navigated window is the active window or one of its parents - * (active window = FormAutofillContent.activeHandler.window) + * (active window = activeHandler.window) * * @returns {boolean} whether the navigation affects the active window */ isActiveWindowNavigation() { - const activeWindow = lazy.FormAutofillContent.activeHandler.window; + const activeWindow = lazy.FormAutofillContent.activeHandler?.window; const navigatedWindow = this.document.defaultView; + + if (!activeWindow || !navigatedWindow) { + return false; + } + const navigatedBrowsingContext = BrowsingContext.getFromWindow(navigatedWindow); @@ -218,19 +262,23 @@ export class FormAutofillChild extends JSWindowActorChild { * Infer a form submission after document is navigated */ onPageNavigation() { - const activeElement = - lazy.FormAutofillContent.activeFieldDetail?.elementWeakRef.deref(); - if (!this.isActiveWindowNavigation()) { return; } - const formSubmissionReason = - lazy.FormAutofillUtils.FORM_SUBMISSION_REASON.PAGE_NAVIGATION; + // TODO: We should not use FormAutofillContent and let the + // parent decides which child to notify + const activeChild = lazy.FormAutofillContent.activeAutofillChild; + const activeElement = activeChild.activeFieldDetail?.elementWeakRef.deref(); + if (!activeElement) { + return; + } + + const formSubmissionReason = lazy.FORM_SUBMISSION_REASON.PAGE_NAVIGATION; // We only capture the form of the active field right now, // this means that we might miss some fields (see bug 1871356) - lazy.FormAutofillContent.formSubmitted(activeElement, formSubmissionReason); + activeChild.formSubmitted(activeElement, formSubmissionReason); } /** @@ -267,11 +315,9 @@ export class FormAutofillChild extends JSWindowActorChild { /** * After a focusin event and after we identify formautofill fields, * we set up an event listener for the DOMDocFetchSuccess event - * - * @param {Document} document The document we want to be notified by of a DOMDocFetchSuccess event */ - registerDOMDocFetchSuccessEventListener(document) { - document.setNotifyFetchSuccess(true); + registerDOMDocFetchSuccessEventListener() { + this.document.setNotifyFetchSuccess(true); // Is removed after a DOMDocFetchSuccess event (bug 1864855) /* eslint-disable mozilla/balanced-listeners */ @@ -284,11 +330,9 @@ export class FormAutofillChild extends JSWindowActorChild { /** * After a DOMDocFetchSuccess event, we register an event listener for the DOMFormRemoved event - * - * @param {Document} document The document we want to be notified by of a DOMFormRemoved event */ - registerDOMFormRemovedEventListener(document) { - document.setNotifyFormOrPasswordRemoved(true); + registerDOMFormRemovedEventListener() { + this.document.setNotifyFormOrPasswordRemoved(true); // Is removed after a DOMFormRemoved event (bug 1864855) /* eslint-disable mozilla/balanced-listeners */ @@ -301,11 +345,9 @@ export class FormAutofillChild extends JSWindowActorChild { /** * After a DOMDocFetchSuccess event we remove the DOMDocFetchSuccess event listener - * - * @param {Document} document The document we are notified by of a DOMDocFetchSuccess event */ - unregisterDOMDocFetchSuccessEventListener(document) { - document.setNotifyFetchSuccess(false); + unregisterDOMDocFetchSuccessEventListener() { + this.document.setNotifyFetchSuccess(false); this.docShell.chromeEventHandler.removeEventListener( "DOMDocFetchSuccess", this @@ -314,11 +356,9 @@ export class FormAutofillChild extends JSWindowActorChild { /** * After a DOMFormRemoved event we remove the DOMFormRemoved event listener - * - * @param {Document} document The document we are notified by of a DOMFormRemoved event */ - unregisterDOMFormRemovedEventListener(document) { - document.setNotifyFormOrPasswordRemoved(false); + unregisterDOMFormRemovedEventListener() { + this.document.setNotifyFormOrPasswordRemoved(false); this.docShell.chromeEventHandler.removeEventListener( "DOMFormRemoved", this @@ -327,11 +367,7 @@ export class FormAutofillChild extends JSWindowActorChild { shouldIgnoreFormAutofillEvent(event) { let nodePrincipal = event.target.nodePrincipal; - return ( - nodePrincipal.isSystemPrincipal || - nodePrincipal.isNullPrincipal || - nodePrincipal.schemeIs("about") - ); + return nodePrincipal.isSystemPrincipal || nodePrincipal.schemeIs("about"); } handleEvent(evt) { @@ -342,16 +378,20 @@ export class FormAutofillChild extends JSWindowActorChild { return; } + if (!this.windowContext) { + // !this.windowContext must not be null, because we need the + // windowContext and/or docShell to (un)register form submission listeners + return; + } + switch (evt.type) { - case "focusin": { - if (lazy.FormAutofill.isAutofillEnabled) { - this.onFocusIn(evt); - } + case "keydown": { + this._onKeyDown(evt); break; } - case "DOMFormBeforeSubmit": { + case "focusin": { if (lazy.FormAutofill.isAutofillEnabled) { - this.onDOMFormBeforeSubmit(evt); + this.onFocusIn(evt); } break; } @@ -360,7 +400,13 @@ export class FormAutofillChild extends JSWindowActorChild { break; } case "DOMDocFetchSuccess": { - this.onDOMDocFetchSuccess(evt); + this.onDOMDocFetchSuccess(); + break; + } + case "form-submission-detected": { + if (lazy.FormAutofill.isAutofillEnabled) { + this.onFormSubmission(evt); + } break; } @@ -371,45 +417,42 @@ export class FormAutofillChild extends JSWindowActorChild { } onFocusIn(evt) { - lazy.FormAutofillContent.updateActiveInput(); + this.updateActiveInput(); - let element = evt.target; + const element = evt.target; if (!lazy.FormAutofillUtils.isCreditCardOrAddressFieldType(element)) { return; } - this._nextHandleElement = element; - if (!this._alreadyDOMContentLoaded) { - let doc = element.ownerDocument; - if (doc.readyState === "loading") { - if (!this._hasDOMContentLoadedHandler) { - this._hasDOMContentLoadedHandler = true; - doc.addEventListener( - "DOMContentLoaded", - () => this._doIdentifyAutofillFields(), - { once: true } - ); - } - return; + this._nextHandleElement = element; + const doc = element.ownerDocument; + if (doc.readyState === "loading") { + // For auto-focused input, we might receive focus event before document becomes ready. + // When this happens, run field identification after receiving `DOMContentLoaded` event + if (!this._hasDOMContentLoadedHandler) { + this._hasDOMContentLoadedHandler = true; + doc.addEventListener( + "DOMContentLoaded", + () => this.identifyAutofillFields(), + { once: true } + ); } - this._alreadyDOMContentLoaded = true; + return; } - this._doIdentifyAutofillFields(); + this.identifyAutofillFields(); } /** - * Handle the DOMFormBeforeSubmit event. + * Handle form-submission-detected event (dispatched by FormHandlerChild) * - * @param {Event} evt + * @param {CustomEvent} evt form-submission-detected event */ - onDOMFormBeforeSubmit(evt) { - const formElement = evt.target; - - const formSubmissionReason = - lazy.FormAutofillUtils.FORM_SUBMISSION_REASON.FORM_SUBMIT_EVENT; + onFormSubmission(evt) { + const formElement = evt.detail.form; + const formSubmissionReason = evt.detail.reason; - lazy.FormAutofillContent.formSubmitted(formElement, formSubmissionReason); + this.formSubmitted(formElement, formSubmissionReason); } /** @@ -421,14 +464,10 @@ export class FormAutofillChild extends JSWindowActorChild { * @param {Event} evt DOMFormRemoved */ onDOMFormRemoved(evt) { - const document = evt.composedTarget.ownerDocument; - const formSubmissionReason = - lazy.FormAutofillUtils.FORM_SUBMISSION_REASON.FORM_REMOVAL_AFTER_FETCH; - - lazy.FormAutofillContent.formSubmitted(evt.target, formSubmissionReason); + lazy.FORM_SUBMISSION_REASON.FORM_REMOVAL_AFTER_FETCH; - this.unregisterDOMFormRemovedEventListener(document); + this.formSubmitted(evt.target, formSubmissionReason); } /** @@ -436,15 +475,21 @@ export class FormAutofillChild extends JSWindowActorChild { * * Sets up an event listener for the DOMFormRemoved event * and unregisters the event listener for DOMDocFetchSuccess event. - * - * @param {Event} evt DOMDocFetchSuccess */ - onDOMDocFetchSuccess(evt) { - const document = evt.target; + onDOMDocFetchSuccess() { + this.registerDOMFormRemovedEventListener(); - this.registerDOMFormRemovedEventListener(document); + this.unregisterDOMDocFetchSuccessEventListener(); + } - this.unregisterDOMDocFetchSuccessEventListener(document); + /** + * Unregister all listeners that notify of a form submission, + * because we just detected and acted on a form submission + */ + unregisterFormSubmissionListeners() { + this.unregisterDOMDocFetchSuccessEventListener(); + this.unregisterDOMFormRemovedEventListener(); + this.unregisterProgressListener(); } receiveMessage(message) { @@ -456,17 +501,284 @@ export class FormAutofillChild extends JSWindowActorChild { switch (message.name) { case "FormAutofill:PreviewProfile": { - lazy.FormAutofillContent.previewProfile(doc); + this.previewProfile(doc); break; } case "FormAutofill:ClearForm": { - lazy.FormAutofillContent.clearForm(); + this.clearForm(); break; } case "FormAutofill:FillForm": { - lazy.FormAutofillContent.activeHandler.autofillFormFields(message.data); + this.activeHandler.autofillFormFields(message.data); break; } } } + + get activeFieldDetail() { + return this._fieldDetailsManager.activeFieldDetail; + } + + get activeFormDetails() { + return this._fieldDetailsManager.activeFormDetails; + } + + get activeInput() { + return this._fieldDetailsManager.activeInput; + } + + get activeHandler() { + return this._fieldDetailsManager.activeHandler; + } + + get activeSection() { + return this._fieldDetailsManager.activeSection; + } + + /** + * Handle a form submission and early return when: + * 1. In private browsing mode. + * 2. Could not map any autofill handler by form element. + * 3. Number of filled fields is less than autofill threshold + * + * @param {HTMLElement} formElement Root element which receives submit event. + * @param {string} formSubmissionReason Reason for invoking the form submission + * (see options for FORM_SUBMISSION_REASON in FormAutofillUtils)) + * @param {Window} domWin Content window; passed for unit tests and when + * invoked by the FormAutofillSection + * @param {object} handler FormAutofillHander, if known by caller + */ + formSubmitted( + formElement, + formSubmissionReason, + domWin = formElement.ownerGlobal, + handler = undefined + ) { + this.debug(`Handling form submission - infered by ${formSubmissionReason}`); + + lazy.AutofillTelemetry.recordFormSubmissionHeuristicCount( + formSubmissionReason + ); + + if (!lazy.FormAutofill.isAutofillEnabled) { + this.debug("Form Autofill is disabled"); + return; + } + + // The `domWin` truthiness test is used by unit tests to bypass this check. + if (domWin && lazy.PrivateBrowsingUtils.isContentWindowPrivate(domWin)) { + this.debug("Ignoring submission in a private window"); + return; + } + + handler = handler || this._fieldDetailsManager._getFormHandler(formElement); + const records = this._fieldDetailsManager.getRecords(formElement, handler); + + if (!records || !handler) { + this.debug("Form element could not map to an existing handler"); + return; + } + + // Unregister the form submission listeners after handling a form submission + this.debug("Unregistering form submission listeners"); + this.unregisterFormSubmissionListeners(); + + [records.address, records.creditCard].forEach((rs, idx) => { + lazy.AutofillTelemetry.recordSubmittedSectionCount( + idx == 0 + ? lazy.AutofillTelemetry.ADDRESS + : lazy.AutofillTelemetry.CREDIT_CARD, + rs?.length + ); + + rs?.forEach(r => { + lazy.AutofillTelemetry.recordFormInteractionEvent( + "submitted", + r.section, + { + record: r, + form: handler.form, + } + ); + delete r.section; + }); + }); + + this.sendAsyncMessage("FormAutofill:OnFormSubmit", records); + } + + formAutofilled() { + lazy.FormAutofillContent.showPopup(); + } + + /** + * All active items should be updated according the active element of + * `formFillController.focusedInput`. All of them including element, + * handler, section, and field detail, can be retrieved by their own getters. + * + * @param {HTMLElement|null} element The active item should be updated based + * on this or `formFillController.focusedInput` will be taken. + */ + updateActiveInput(element) { + element = element || formFillController.focusedInput; + if (!element) { + this.debug("updateActiveElement: no element selected"); + return; + } + lazy.FormAutofillContent.updateActiveAutofillChild(this); + + this._fieldDetailsManager.updateActiveInput(element); + this.debug("updateActiveElement: checking for popup-on-focus"); + // We know this element just received focus. If it's a credit card field, + // open its popup. + if (this._autofillPending) { + this.debug("updateActiveElement: skipping check; autofill is imminent"); + } else if (element.value?.length !== 0) { + this.debug( + `updateActiveElement: Not opening popup because field is not empty.` + ); + } else { + this.debug( + "updateActiveElement: checking if empty field is cc-*: ", + this.activeFieldDetail?.fieldName + ); + + if ( + this.activeFieldDetail?.fieldName?.startsWith("cc-") || + AppConstants.platform === "android" + ) { + lazy.FormAutofillContent.showPopup(); + } + } + } + + set autofillPending(flag) { + this.debug("Setting autofillPending to", flag); + this._autofillPending = flag; + } + + clearForm() { + let focusedInput = + this.activeInput || + lazy.ProfileAutocomplete._lastAutoCompleteFocusedInput; + if (!focusedInput) { + return; + } + + this.activeSection.clearPopulatedForm(); + + let fieldName = this.activeFieldDetail?.fieldName; + if (lazy.FormAutofillUtils.isCreditCardField(fieldName)) { + lazy.AutofillTelemetry.recordFormInteractionEvent( + "cleared", + this.activeSection, + { fieldName } + ); + } + } + + previewProfile(doc) { + let docWin = doc.ownerGlobal; + let selectedIndex = lazy.ProfileAutocomplete._getSelectedIndex(docWin); + let lastAutoCompleteResult = + lazy.ProfileAutocomplete.lastProfileAutoCompleteResult; + let focusedInput = this.activeInput; + + if ( + selectedIndex === -1 || + !focusedInput || + !lastAutoCompleteResult || + lastAutoCompleteResult.getStyleAt(selectedIndex) != "autofill-profile" + ) { + this.sendAsyncMessage("FormAutofill:UpdateWarningMessage", {}); + + lazy.ProfileAutocomplete._clearProfilePreview(); + } else { + let focusedInputDetails = this.activeFieldDetail; + let profile = JSON.parse( + lastAutoCompleteResult.getCommentAt(selectedIndex) + ); + let allFieldNames = this.activeSection.allFieldNames; + let profileFields = allFieldNames.filter( + fieldName => !!profile[fieldName] + ); + + let focusedCategory = lazy.FormAutofillUtils.getCategoryFromFieldName( + focusedInputDetails.fieldName + ); + let categories = + lazy.FormAutofillUtils.getCategoriesFromFieldNames(profileFields); + this.sendAsyncMessage("FormAutofill:UpdateWarningMessage", { + focusedCategory, + categories, + }); + + lazy.ProfileAutocomplete._previewSelectedProfile(selectedIndex); + } + } + + onPopupClosed(selectedRowStyle) { + this.debug("Popup has closed."); + lazy.ProfileAutocomplete._clearProfilePreview(); + + let lastAutoCompleteResult = + lazy.ProfileAutocomplete.lastProfileAutoCompleteResult; + let focusedInput = this.activeInput; + if ( + lastAutoCompleteResult && + this._keyDownEnterForInput && + focusedInput === this._keyDownEnterForInput && + focusedInput === + lazy.ProfileAutocomplete.lastProfileAutoCompleteFocusedInput + ) { + if (selectedRowStyle == "autofill-footer") { + this.sendAsyncMessage("FormAutofill:OpenPreferences"); + } else if (selectedRowStyle == "autofill-clear-button") { + this.clearForm(); + } + } + } + + onPopupOpened() { + this.debug( + "Popup has opened, automatic =", + formFillController.passwordPopupAutomaticallyOpened + ); + + let fieldName = this.activeFieldDetail?.fieldName; + if (fieldName && this.activeSection) { + lazy.AutofillTelemetry.recordFormInteractionEvent( + "popup_shown", + this.activeSection, + { fieldName } + ); + } + } + + _markAsAutofillField(field) { + // Since Form Autofill popup is only for input element, any non-Input + // element should be excluded here. + if (!HTMLInputElement.isInstance(field)) { + return; + } + + formFillController.markAsAutofillField(field); + } + + _onKeyDown(e) { + delete this._keyDownEnterForInput; + let lastAutoCompleteResult = + lazy.ProfileAutocomplete.lastProfileAutoCompleteResult; + let focusedInput = this.activeInput; + if ( + e.keyCode != e.DOM_VK_RETURN || + !lastAutoCompleteResult || + !focusedInput || + focusedInput != + lazy.ProfileAutocomplete.lastProfileAutoCompleteFocusedInput + ) { + return; + } + this._keyDownEnterForInput = focusedInput; + } } |