/* This Source Code Form is subject to the terms of the Mozilla Public * 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, { AddressResult: "resource://autofill/ProfileAutoCompleteResult.sys.mjs", AutofillTelemetry: "resource://gre/modules/shared/AutofillTelemetry.sys.mjs", CreditCardResult: "resource://autofill/ProfileAutoCompleteResult.sys.mjs", GenericAutocompleteItem: "resource://gre/modules/FillHelpers.sys.mjs", InsecurePasswordUtils: "resource://gre/modules/InsecurePasswordUtils.sys.mjs", FormAutofill: "resource://autofill/FormAutofill.sys.mjs", FormAutofillContent: "resource://autofill/FormAutofillContent.sys.mjs", FormAutofillUtils: "resource://gre/modules/shared/FormAutofillUtils.sys.mjs", FormScenarios: "resource://gre/modules/FormScenarios.sys.mjs", FormStateManager: "resource://gre/modules/shared/FormStateManager.sys.mjs", PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", FORM_SUBMISSION_REASON: "resource://gre/actors/FormHandlerChild.sys.mjs", }); XPCOMUtils.defineLazyPreferenceGetter( lazy, "DELEGATE_AUTOCOMPLETE", "toolkit.autocomplete.delegate", false ); /** * Handles content's interactions for the frame. */ export class FormAutofillChild extends JSWindowActorChild { constructor() { super(); this.log = lazy.FormAutofill.defineLogGetter(this, "FormAutofillChild"); this.debug("init"); this._nextHandleElements = []; this._hasDOMContentLoadedHandler = false; this._hasRegisteredPageHide = new Set(); /** * @type {FormAutofillFieldDetailsManager} handling state management of current forms and handlers. */ this._fieldDetailsManager = new lazy.FormStateManager( this.onFilledModified.bind(this) ); /** * Tracks whether the last form submission was triggered by a form submit event, * if so we'll ignore the page navigation that follows */ this.isFollowingSubmitEvent = false; } /** * Identifies and marks each autofill field */ identifyAutofillFields() { 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"); } for (const element of this._nextHandleElements) { this.debug( `identifyAutofillFields: ${element.ownerDocument.location?.hostname}` ); const { handler, newFieldsIdentified } = this._fieldDetailsManager.identifyAutofillFields(element); // Bail out if there is nothing changed since last time we identified this element // or there is no interested fields. if (!newFieldsIdentified || !handler.fieldDetails.length) { continue; } const fieldDetails = handler.fieldDetails; // Inform the autocomplete controller these fields are autofillable. // Bug 1905040. This is only a temporarily workaround for now to skip marking address fields // autocompletable whenever we detect an address field. We only mark address field when // it is a valid address section (This is done in the parent) const fields = new Set( fieldDetails .map(f => f.fieldName) .filter(fieldName => lazy.FormAutofillUtils.isAddressField(fieldName)) ); const validAddressSection = fields.size >= lazy.FormAutofillUtils.AUTOFILL_FIELDS_THRESHOLD; for (const fieldDetail of fieldDetails) { if ( !validAddressSection && lazy.FormAutofillUtils.isAddressField(fieldDetail.fieldName) ) { continue; } this.#markAsAutofillField(fieldDetail.element); } // Notify the parent when we detect autofillable fields. this.sendAsyncMessage( "FormAutofill:FieldsDetected", fieldDetails.map(detail => detail.toVanillaObject()) ); this.manager .getActor("FormHandler") .registerFormSubmissionInterest(this, { includesFormRemoval: lazy.FormAutofill.captureOnFormRemoval, includesPageNavigation: lazy.FormAutofill.captureOnPageNavigation, }); // TODO (Bug 1901486): Integrate pagehide to FormHandler. if (!this._hasRegisteredPageHide.has(handler)) { this.registerPageHide(handler); this._hasRegisteredPageHide.add(true); } } if ( this._nextHandleElements.includes(lazy.FormAutofillContent.focusedInput) ) { this.#showCreditCardPopupIfEmpty(lazy.FormAutofillContent.focusedInput); } this._nextHandleElements = []; // 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"); } #showCreditCardPopupIfEmpty(element) { if (element.value?.length !== 0) { this.debug(`Not opening popup because field is not empty.`); return; } const handler = this._fieldDetailsManager.getFormHandler(element); const fieldName = handler?.getFieldDetailByElement(element)?.fieldName ?? ""; if (fieldName.startsWith("cc-") || AppConstants.platform === "android") { lazy.FormAutofillContent.showPopup(); } } /** * We received a form-submission-detected event because * the page was navigated. */ onPageNavigation() { if (!lazy.FormAutofill.captureOnPageNavigation) { return; } if (this.isFollowingSubmitEvent) { // The next page navigation should be handled as form submission again this.isFollowingSubmitEvent = false; return; } let weakIdentifiedForms = ChromeUtils.nondeterministicGetWeakMapKeys( this._fieldDetailsManager._formsDetails ); const formSubmissionReason = lazy.FORM_SUBMISSION_REASON.PAGE_NAVIGATION; for (const form of weakIdentifiedForms) { // Disconnected forms are captured by the form removal heuristic if (!form.isConnected) { continue; } this.formSubmitted(form, formSubmissionReason); } } /** * We received a form-submission-detected event because * a form was removed from the DOM after a successful * xhr/fetch request * * @param {Event} form form to be submitted */ onFormRemoval(form) { if (!lazy.FormAutofill.captureOnFormRemoval) { return; } const formSubmissionReason = lazy.FORM_SUBMISSION_REASON.FORM_REMOVAL_AFTER_FETCH; this.formSubmitted(form, formSubmissionReason); this.manager.getActor("FormHandler").unregisterFormRemovalInterest(this); } registerPageHide(handler) { // Check whether the section is in an