/* 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", FieldDetail: "resource://gre/modules/shared/FieldScanner.sys.mjs", FormAutofill: "resource://autofill/FormAutofill.sys.mjs", FormAutofillContent: "resource://autofill/FormAutofillContent.sys.mjs", FormAutofillHandler: "resource://gre/modules/shared/FormAutofillHandler.sys.mjs", FormAutofillUtils: "resource://gre/modules/shared/FormAutofillUtils.sys.mjs", FormLikeFactory: "resource://gre/modules/FormLikeFactory.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 { // Flag to indicate whethere there is an ongoing autofilling process. #autofillInProgress = false; /** * Keep track of autofill handlers that are waiting for the parent process * to send back the identified result. */ #handlerWaitingForDetectedComplete = new Set(); constructor() { super(); this.log = lazy.FormAutofill.defineLogGetter(this, "FormAutofillChild"); this.debug("init"); 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; } /** * After the parent process finishes classifying the fields, the parent process * informs all the child process of the classified field result. The child process * then sets the updated result to the corresponding AutofillHandler * * @param {Array} fieldDetails * An array of the identified fields. */ onFieldsDetectedComplete(fieldDetails) { if (!fieldDetails.length) { return; } const handler = this.#getHandlerByElementId(fieldDetails[0].elementId); this.#handlerWaitingForDetectedComplete.delete(handler); handler.setIdentifiedFieldDetails(fieldDetails); // 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 addressFields = new Set( handler.fieldDetails .map(f => f.fieldName) .filter(fieldName => lazy.FormAutofillUtils.isAddressField(fieldName)) ); const validAddressSection = addressFields.size >= lazy.FormAutofillUtils.AUTOFILL_FIELDS_THRESHOLD; let hasInterestedField = false; for (const fieldDetail of handler.fieldDetails) { if ( !fieldDetail.fieldName || (!validAddressSection && lazy.FormAutofillUtils.isAddressField(fieldDetail.fieldName)) ) { continue; } // Inform the autocomplete controller these fields are autofillable hasInterestedField = true; this.#markAsAutofillField(fieldDetail); } // If we are not interested in any of the detected fields, return to not // mark fields autofillable and register form submission event. if (hasInterestedField) { 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); } this.showCreditCardPopupIfEmpty(lazy.FormAutofillContent.focusedInput); } } /** * Identifies elements that are in the associated form of the passed element. * * @param {Element} element * The element to be identified. * * @returns {FormAutofillHandler} * The autofill handler instance for the form that is associated with the * passed element. */ identifyFieldsWhenFocused(element) { this.debug( `identifyFieldsWhenFocused: ${element.ownerDocument.location?.hostname}` ); const handler = this._fieldDetailsManager.getOrCreateFormHandler(element); // If the child process is still waiting for the parent to send to // `onFieldsDetectedComplete` message, bail out. if (this.#handlerWaitingForDetectedComplete.has(handler)) { return; } // Bail out if there is nothing changed since last time we identified this element // or there is no interested fields. if (handler.hasIdentifiedFields() && !handler.updateFormIfNeeded(element)) { // This is for testing purposes only. It sends a notification to indicate that the // form has been identified and is ready to open the popup. // If new fields are detected, the message will be sent to the parent // once the parent finishes collecting information from sub-frames if they exist. this.sendAsyncMessage("FormAutofill:FieldsIdentified"); } else { const detectedFields = lazy.FormAutofillHandler.collectFormFields( handler.form ); // If none of the detected fields are credit card or address fields, // there's no need to notify the parent because nothing will change. if ( !detectedFields.some( fd => lazy.FormAutofillUtils.isCreditCardField(fd.fieldName) || lazy.FormAutofillUtils.isAddressField(fd.fieldName) ) ) { handler.setIdentifiedFieldDetails(detectedFields); return; } this.sendAsyncMessage( "FormAutofill:OnFieldsDetected", detectedFields.map(field => field.toVanillaObject()) ); // Notify the parent about the newly identified fields because // the autofill section information is maintained on the parent side. this.#handlerWaitingForDetectedComplete.add(handler); } } /** * This function is called by the parent when a field is detected in another * frame. The parent uses this function to collect field information from frames * that are part of the same form as the detected field. * * @param {string} focusedBCId * The browsing context ID of the top-level iframe * that contains the detected field. * Note that this value is set only when the current frame is the top-level. * * @returns {Array} * Array of FieldDetail objects of identified fields (including iframes). */ identifyFields(focusedBCId) { const isTop = this.browsingContext == this.browsingContext.top; let element; if (isTop) { // Find the focused iframe element = BrowsingContext.get(focusedBCId).embedderElement; } else { // Ignore form as long as the frame is not the top-level, which means // we can just pick any of the eligible elements to identify. element = this.document.querySelector("input, select, iframe"); } if (!element) { return []; } const handler = this._fieldDetailsManager.getOrCreateFormHandler(element); // We don't have to call 'updateFormIfNeeded' like we do in // 'identifyFieldsWhenFocused' because 'collectFormFields' doesn't use cached // result. const detectedFields = lazy.FormAutofillHandler.collectFormFields( handler.form ); if (detectedFields.length) { // This actor should receive `onFieldsDetectedComplete`message after // `idenitfyFields` is called this.#handlerWaitingForDetectedComplete.add(handler); } return detectedFields; } showCreditCardPopupIfEmpty(element) { if (!element || element != lazy.FormAutofillContent.focusedInput) { return; } if (element.value?.length !== 0) { this.debug(`Not opening popup because field is not empty.`); return; } const handler = this._fieldDetailsManager.getFormHandler(element); if (!handler?.hasIdentifiedFields()) { this.debug( `Not opening popup because we have not yet identified the field` ); return; } 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