/* 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/. */ /* * Implements a service used to access storage and communicate with content. * * A "fields" array is used to communicate with FormAutofillChild. Each item * represents a single input field in the content page as well as its * @autocomplete properties. The schema is as below. Please refer to * FormAutofillChild.js for more details. * * [ * { * section, * addressType, * contactType, * fieldName, * value, * index * }, * { * // ... * } * ] */ // We expose a singleton from this module. Some tests may import the // constructor via the system global. import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; import { FormAutofill } from "resource://autofill/FormAutofill.sys.mjs"; import { FormAutofillUtils } from "resource://gre/modules/shared/FormAutofillUtils.sys.mjs"; const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { AddressComponent: "resource://gre/modules/shared/AddressComponent.sys.mjs", // eslint-disable-next-line mozilla/no-browser-refs-in-toolkit BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs", FormAutofillAddressSection: "resource://gre/modules/shared/FormAutofillSection.sys.mjs", FormAutofillCreditCardSection: "resource://gre/modules/shared/FormAutofillSection.sys.mjs", FormAutofillHeuristics: "resource://gre/modules/shared/FormAutofillHeuristics.sys.mjs", FormAutofillSection: "resource://gre/modules/shared/FormAutofillSection.sys.mjs", FormAutofillPreferences: "resource://autofill/FormAutofillPreferences.sys.mjs", FormAutofillPrompter: "resource://autofill/FormAutofillPrompter.sys.mjs", FirefoxRelay: "resource://gre/modules/FirefoxRelay.sys.mjs", LoginHelper: "resource://gre/modules/LoginHelper.sys.mjs", MLAutofill: "resource://autofill/MLAutofill.sys.mjs", NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs", OSKeyStore: "resource://gre/modules/OSKeyStore.sys.mjs", }); ChromeUtils.defineLazyGetter(lazy, "log", () => FormAutofill.defineLogGetter(lazy, "FormAutofillParent") ); const { ENABLED_AUTOFILL_ADDRESSES_PREF, ENABLED_AUTOFILL_CREDITCARDS_PREF } = FormAutofill; const { ADDRESSES_COLLECTION_NAME, CREDITCARDS_COLLECTION_NAME, FIELD_STATES } = FormAutofillUtils; let gMessageObservers = new Set(); export let FormAutofillStatus = { _initialized: false, /** * Cache of the Form Autofill status (considering preferences and storage). */ _active: null, /** * Initializes observers and registers the message handler. */ init() { if (this._initialized) { return; } this._initialized = true; Services.obs.addObserver(this, "privacy-pane-loaded"); // Observing the pref and storage changes Services.prefs.addObserver(ENABLED_AUTOFILL_ADDRESSES_PREF, this); Services.obs.addObserver(this, "formautofill-storage-changed"); // Only listen to credit card related preference if it is available if (FormAutofill.isAutofillCreditCardsAvailable) { Services.prefs.addObserver(ENABLED_AUTOFILL_CREDITCARDS_PREF, this); } }, /** * Uninitializes FormAutofillStatus. This is for testing only. * * @private */ uninit() { lazy.gFormAutofillStorage._saveImmediately(); if (!this._initialized) { return; } this._initialized = false; this._active = null; Services.obs.removeObserver(this, "privacy-pane-loaded"); Services.prefs.removeObserver(ENABLED_AUTOFILL_ADDRESSES_PREF, this); Services.wm.removeListener(this); if (FormAutofill.isAutofillCreditCardsAvailable) { Services.prefs.removeObserver(ENABLED_AUTOFILL_CREDITCARDS_PREF, this); } }, get formAutofillStorage() { return lazy.gFormAutofillStorage; }, /** * Broadcast the status to frames when the form autofill status changes. */ onStatusChanged() { lazy.log.debug("onStatusChanged: Status changed to", this._active); Services.ppmm.sharedData.set("FormAutofill:enabled", this._active); // Sync autofill enabled to make sure the value is up-to-date // no matter when the new content process is initialized. Services.ppmm.sharedData.flush(); }, /** * Query preference and storage status to determine the overall status of the * form autofill feature. * * @returns {boolean} whether form autofill is active (enabled and has data) */ computeStatus() { const savedFieldNames = Services.ppmm.sharedData.get( "FormAutofill:savedFieldNames" ); return ( (Services.prefs.getBoolPref(ENABLED_AUTOFILL_ADDRESSES_PREF) || Services.prefs.getBoolPref(ENABLED_AUTOFILL_CREDITCARDS_PREF)) && savedFieldNames && savedFieldNames.size > 0 ); }, /** * Update the status and trigger onStatusChanged, if necessary. */ updateStatus() { lazy.log.debug("updateStatus"); let wasActive = this._active; this._active = this.computeStatus(); if (this._active !== wasActive) { this.onStatusChanged(); } }, async updateSavedFieldNames() { lazy.log.debug("updateSavedFieldNames"); let savedFieldNames; const addressNames = await lazy.gFormAutofillStorage.addresses.getSavedFieldNames(); // Don't access the credit cards store unless it is enabled. if (FormAutofill.isAutofillCreditCardsAvailable) { const creditCardNames = await lazy.gFormAutofillStorage.creditCards.getSavedFieldNames(); savedFieldNames = new Set([...addressNames, ...creditCardNames]); } else { savedFieldNames = addressNames; } Services.ppmm.sharedData.set( "FormAutofill:savedFieldNames", savedFieldNames ); Services.ppmm.sharedData.flush(); this.updateStatus(); }, async observe(subject, topic, data) { lazy.log.debug("observe:", topic, "with data:", data); if ( !FormAutofill.isAutofillCreditCardsAvailable && !FormAutofill.isAutofillAddressesAvailable ) { return; } switch (topic) { case "privacy-pane-loaded": { let formAutofillPreferences = new lazy.FormAutofillPreferences(); let document = subject.document; let prefFragment = formAutofillPreferences.init(document); let formAutofillGroupBox = document.getElementById( "formAutofillGroupBox" ); formAutofillGroupBox.appendChild(prefFragment); break; } case "nsPref:changed": { // Observe pref changes and update _active cache if status is changed. this.updateStatus(); break; } case "formautofill-storage-changed": { // Early exit if only metadata is changed if (data == "notifyUsed") { break; } await this.updateSavedFieldNames(); break; } default: { throw new Error( `FormAutofillStatus: Unexpected topic observed: ${topic}` ); } } }, }; // Lazily load the storage JSM to avoid disk I/O until absolutely needed. // Once storage is loaded we need to update saved field names and inform content processes. ChromeUtils.defineLazyGetter(lazy, "gFormAutofillStorage", () => { let { formAutofillStorage } = ChromeUtils.importESModule( "resource://autofill/FormAutofillStorage.sys.mjs" ); lazy.log.debug("Loading formAutofillStorage"); formAutofillStorage.initialize().then(() => { // Update the saved field names to compute the status and update child processes. FormAutofillStatus.updateSavedFieldNames(); }); return formAutofillStorage; }); export class FormAutofillParent extends JSWindowActorParent { constructor() { super(); FormAutofillStatus.init(); // This object maintains data that should be shared among all // FormAutofillParent actors in the same DOM tree. this._topLevelCache = { sectionsByRootId: new Map(), filledResult: new Map(), submittedData: new Map(), }; } get topLevelCache() { let actor; try { actor = this.browsingContext.top == this.browsingContext ? this : FormAutofillParent.getActor(this.browsingContext.top); } catch {} actor ||= this; return actor._topLevelCache; } get sectionsByRootId() { return this.topLevelCache.sectionsByRootId; } get filledResult() { return this.topLevelCache.filledResult; } get submittedData() { return this.topLevelCache.submittedData; } /** * Handles the message coming from FormAutofillChild. * * @param {object} message * @param {string} message.name The name of the message. * @param {object} message.data The data of the message. */ async receiveMessage({ name, data }) { if ( !FormAutofill.isAutofillCreditCardsAvailable && !FormAutofill.isAutofillAddressesAvailable ) { return undefined; } switch (name) { case "FormAutofill:InitStorage": { await lazy.gFormAutofillStorage.initialize(); await FormAutofillStatus.updateSavedFieldNames(); break; } case "FormAutofill:GetRecords": { const records = await this.getRecords(data); return { records }; } case "FormAutofill:OnFormSubmit": { const { rootElementId, formFilledData } = data; this.notifyMessageObservers("onFormSubmitted", data); this.onFormSubmit(rootElementId, formFilledData); break; } case "FormAutofill:FieldsIdentified": this.notifyMessageObservers("fieldsIdentified", data); break; case "FormAutofill:OnFieldsDetected": await this.onFieldsDetected( data, "FormAutofill:onFieldsDetectedComplete" ); break; case "FormAutofill:OnFieldsUpdated": await this.onFieldsDetected( data, "FormAutofill:onFieldsUpdatedComplete" ); break; case "FormAutofill:FieldFilledModified": { this.onFieldFilledModified(data); break; } case "FormAutofill:FieldsUpdatedDuringAutofill": { // TODO bug 1953231: The parent should introduce profile ids, so that // the child can simply send a profile id instead of the whole profile data const { elementId, profile } = data; this.onFieldsUpdatedDuringAutofill(elementId, profile); break; } // The remaining Save and Remove messages are invoked only by tests. case "FormAutofill:SaveAddress": { if (data.guid) { await lazy.gFormAutofillStorage.addresses.update( data.guid, data.address ); } else { await lazy.gFormAutofillStorage.addresses.add(data.address); } break; } case "FormAutofill:SaveCreditCard": { // Setting the first parameter of OSKeyStore.ensurLoggedIn as false // since this case only called in tests. Also the reason why we're not calling FormAutofill.verifyUserOSAuth. if (!(await lazy.OSKeyStore.ensureLoggedIn(false)).authenticated) { lazy.log.warn("User canceled encryption login"); return undefined; } await lazy.gFormAutofillStorage.creditCards.add(data.creditcard); break; } case "FormAutofill:RemoveAddresses": { data.guids.forEach(guid => lazy.gFormAutofillStorage.addresses.remove(guid) ); break; } case "FormAutofill:RemoveCreditCards": { data.guids.forEach(guid => lazy.gFormAutofillStorage.creditCards.remove(guid) ); break; } } return undefined; } // For a third-party frame, we only autofill when the frame is same origin // with the frame that triggers autofill. isBCSameOrigin(browsingContext) { return this.manager.documentPrincipal.equals( browsingContext.currentWindowGlobal.documentPrincipal ); } static getActor(browsingContext) { return browsingContext?.currentWindowGlobal?.getActor("FormAutofill"); } get formOrigin() { return lazy.LoginHelper.getLoginOrigin( this.manager.documentPrincipal?.originNoSuffix ); } /** * Recursively identifies autofillable fields within each sub-frame of the * given browsing context. * * This function iterates through all sub-frames and uses the provided * browsing context to locate and identify fields that are eligible for * autofill. It handles both the top-level context and any nested * iframes, aggregating all identified fields into a single array. * * @param {BrowsingContext} browsingContext * The browsing context where autofill fields are to be identified. * @param {string} focusedBCId * The browsing context ID of the