Bug 1933479 - Add tab close button on hover to vertical tabs when sidebar is collapse...
[gecko.git] / toolkit / components / formautofill / FormAutofillParent.sys.mjs
blob1fff05e914f81bf9d7535b4b95cbbcafb980c630
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this
3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 /*
6  * Implements a service used to access storage and communicate with content.
7  *
8  * A "fields" array is used to communicate with FormAutofillChild. Each item
9  * represents a single input field in the content page as well as its
10  * @autocomplete properties. The schema is as below. Please refer to
11  * FormAutofillChild.js for more details.
12  *
13  * [
14  *   {
15  *     section,
16  *     addressType,
17  *     contactType,
18  *     fieldName,
19  *     value,
20  *     index
21  *   },
22  *   {
23  *     // ...
24  *   }
25  * ]
26  */
28 // We expose a singleton from this module. Some tests may import the
29 // constructor via the system global.
30 import { FormAutofill } from "resource://autofill/FormAutofill.sys.mjs";
31 import { FormAutofillUtils } from "resource://gre/modules/shared/FormAutofillUtils.sys.mjs";
33 const { FIELD_STATES } = FormAutofillUtils;
35 const lazy = {};
37 ChromeUtils.defineESModuleGetters(lazy, {
38   AddressComponent: "resource://gre/modules/shared/AddressComponent.sys.mjs",
39   // eslint-disable-next-line mozilla/no-browser-refs-in-toolkit
40   BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs",
41   FormAutofillAddressSection:
42     "resource://gre/modules/shared/FormAutofillSection.sys.mjs",
43   FormAutofillCreditCardSection:
44     "resource://gre/modules/shared/FormAutofillSection.sys.mjs",
45   FormAutofillHeuristics:
46     "resource://gre/modules/shared/FormAutofillHeuristics.sys.mjs",
47   FormAutofillSection:
48     "resource://gre/modules/shared/FormAutofillSection.sys.mjs",
49   FormAutofillPreferences:
50     "resource://autofill/FormAutofillPreferences.sys.mjs",
51   FormAutofillPrompter: "resource://autofill/FormAutofillPrompter.sys.mjs",
52   FirefoxRelay: "resource://gre/modules/FirefoxRelay.sys.mjs",
53   LoginHelper: "resource://gre/modules/LoginHelper.sys.mjs",
54   MLAutofill: "resource://autofill/MLAutofill.sys.mjs",
55   OSKeyStore: "resource://gre/modules/OSKeyStore.sys.mjs",
56 });
58 ChromeUtils.defineLazyGetter(lazy, "log", () =>
59   FormAutofill.defineLogGetter(lazy, "FormAutofillParent")
62 const { ENABLED_AUTOFILL_ADDRESSES_PREF, ENABLED_AUTOFILL_CREDITCARDS_PREF } =
63   FormAutofill;
65 const { ADDRESSES_COLLECTION_NAME, CREDITCARDS_COLLECTION_NAME } =
66   FormAutofillUtils;
68 let gMessageObservers = new Set();
70 export let FormAutofillStatus = {
71   _initialized: false,
73   /**
74    * Cache of the Form Autofill status (considering preferences and storage).
75    */
76   _active: null,
78   /**
79    * Initializes observers and registers the message handler.
80    */
81   init() {
82     if (this._initialized) {
83       return;
84     }
85     this._initialized = true;
87     Services.obs.addObserver(this, "privacy-pane-loaded");
89     // Observing the pref and storage changes
90     Services.prefs.addObserver(ENABLED_AUTOFILL_ADDRESSES_PREF, this);
91     Services.obs.addObserver(this, "formautofill-storage-changed");
93     // Only listen to credit card related preference if it is available
94     if (FormAutofill.isAutofillCreditCardsAvailable) {
95       Services.prefs.addObserver(ENABLED_AUTOFILL_CREDITCARDS_PREF, this);
96     }
97   },
99   /**
100    * Uninitializes FormAutofillStatus. This is for testing only.
101    *
102    * @private
103    */
104   uninit() {
105     lazy.gFormAutofillStorage._saveImmediately();
107     if (!this._initialized) {
108       return;
109     }
110     this._initialized = false;
112     this._active = null;
114     Services.obs.removeObserver(this, "privacy-pane-loaded");
115     Services.prefs.removeObserver(ENABLED_AUTOFILL_ADDRESSES_PREF, this);
116     Services.wm.removeListener(this);
118     if (FormAutofill.isAutofillCreditCardsAvailable) {
119       Services.prefs.removeObserver(ENABLED_AUTOFILL_CREDITCARDS_PREF, this);
120     }
121   },
123   get formAutofillStorage() {
124     return lazy.gFormAutofillStorage;
125   },
127   /**
128    * Broadcast the status to frames when the form autofill status changes.
129    */
130   onStatusChanged() {
131     lazy.log.debug("onStatusChanged: Status changed to", this._active);
132     Services.ppmm.sharedData.set("FormAutofill:enabled", this._active);
133     // Sync autofill enabled to make sure the value is up-to-date
134     // no matter when the new content process is initialized.
135     Services.ppmm.sharedData.flush();
136   },
138   /**
139    * Query preference and storage status to determine the overall status of the
140    * form autofill feature.
141    *
142    * @returns {boolean} whether form autofill is active (enabled and has data)
143    */
144   computeStatus() {
145     const savedFieldNames = Services.ppmm.sharedData.get(
146       "FormAutofill:savedFieldNames"
147     );
149     return (
150       (Services.prefs.getBoolPref(ENABLED_AUTOFILL_ADDRESSES_PREF) ||
151         Services.prefs.getBoolPref(ENABLED_AUTOFILL_CREDITCARDS_PREF)) &&
152       savedFieldNames &&
153       savedFieldNames.size > 0
154     );
155   },
157   /**
158    * Update the status and trigger onStatusChanged, if necessary.
159    */
160   updateStatus() {
161     lazy.log.debug("updateStatus");
162     let wasActive = this._active;
163     this._active = this.computeStatus();
164     if (this._active !== wasActive) {
165       this.onStatusChanged();
166     }
167   },
169   async updateSavedFieldNames() {
170     lazy.log.debug("updateSavedFieldNames");
172     let savedFieldNames;
173     const addressNames =
174       await lazy.gFormAutofillStorage.addresses.getSavedFieldNames();
176     // Don't access the credit cards store unless it is enabled.
177     if (FormAutofill.isAutofillCreditCardsAvailable) {
178       const creditCardNames =
179         await lazy.gFormAutofillStorage.creditCards.getSavedFieldNames();
180       savedFieldNames = new Set([...addressNames, ...creditCardNames]);
181     } else {
182       savedFieldNames = addressNames;
183     }
185     Services.ppmm.sharedData.set(
186       "FormAutofill:savedFieldNames",
187       savedFieldNames
188     );
189     Services.ppmm.sharedData.flush();
191     this.updateStatus();
192   },
194   async observe(subject, topic, data) {
195     lazy.log.debug("observe:", topic, "with data:", data);
197     if (
198       !FormAutofill.isAutofillCreditCardsAvailable &&
199       !FormAutofill.isAutofillAddressesAvailable
200     ) {
201       return;
202     }
204     switch (topic) {
205       case "privacy-pane-loaded": {
206         let formAutofillPreferences = new lazy.FormAutofillPreferences();
207         let document = subject.document;
208         let prefFragment = formAutofillPreferences.init(document);
209         let formAutofillGroupBox = document.getElementById(
210           "formAutofillGroupBox"
211         );
212         formAutofillGroupBox.appendChild(prefFragment);
213         break;
214       }
216       case "nsPref:changed": {
217         // Observe pref changes and update _active cache if status is changed.
218         this.updateStatus();
219         break;
220       }
222       case "formautofill-storage-changed": {
223         // Early exit if only metadata is changed
224         if (data == "notifyUsed") {
225           break;
226         }
228         await this.updateSavedFieldNames();
229         break;
230       }
232       default: {
233         throw new Error(
234           `FormAutofillStatus: Unexpected topic observed: ${topic}`
235         );
236       }
237     }
238   },
241 // Lazily load the storage JSM to avoid disk I/O until absolutely needed.
242 // Once storage is loaded we need to update saved field names and inform content processes.
243 ChromeUtils.defineLazyGetter(lazy, "gFormAutofillStorage", () => {
244   let { formAutofillStorage } = ChromeUtils.importESModule(
245     "resource://autofill/FormAutofillStorage.sys.mjs"
246   );
247   lazy.log.debug("Loading formAutofillStorage");
249   formAutofillStorage.initialize().then(() => {
250     // Update the saved field names to compute the status and update child processes.
251     FormAutofillStatus.updateSavedFieldNames();
252   });
254   return formAutofillStorage;
257 export class FormAutofillParent extends JSWindowActorParent {
258   constructor() {
259     super();
260     FormAutofillStatus.init();
262     // This object maintains data that should be shared among all
263     // FormAutofillParent actors in the same DOM tree.
264     this._topLevelCache = {
265       sectionsByRootId: new Map(),
266       filledResult: new Map(),
267       submittedData: new Map(),
268     };
269   }
271   get topLevelCache() {
272     let actor;
273     try {
274       actor =
275         this.browsingContext.top == this.browsingContext
276           ? this
277           : FormAutofillParent.getActor(this.browsingContext.top);
278     } catch {}
279     actor ||= this;
280     return actor._topLevelCache;
281   }
283   get sectionsByRootId() {
284     return this.topLevelCache.sectionsByRootId;
285   }
287   get filledResult() {
288     return this.topLevelCache.filledResult;
289   }
291   get submittedData() {
292     return this.topLevelCache.submittedData;
293   }
294   /**
295    * Handles the message coming from FormAutofillChild.
296    *
297    * @param   {object} message
298    * @param   {string} message.name The name of the message.
299    * @param   {object} message.data The data of the message.
300    */
301   async receiveMessage({ name, data }) {
302     if (
303       !FormAutofill.isAutofillCreditCardsAvailable &&
304       !FormAutofill.isAutofillAddressesAvailable
305     ) {
306       return undefined;
307     }
309     switch (name) {
310       case "FormAutofill:InitStorage": {
311         await lazy.gFormAutofillStorage.initialize();
312         await FormAutofillStatus.updateSavedFieldNames();
313         break;
314       }
315       case "FormAutofill:GetRecords": {
316         const records = await this.getRecords(data);
317         return { records };
318       }
319       case "FormAutofill:OnFormSubmit": {
320         const { rootElementId, formFilledData } = data;
321         this.notifyMessageObservers("onFormSubmitted", data);
322         this.onFormSubmit(rootElementId, formFilledData);
323         break;
324       }
326       case "FormAutofill:FieldsIdentified":
327         this.notifyMessageObservers("fieldsIdentified", data);
328         break;
330       case "FormAutofill:OnFieldsDetected":
331         await this.onFieldsDetected(data);
332         break;
333       case "FormAutofill:FieldFilledModified": {
334         this.onFieldFilledModified(data);
335         break;
336       }
338       // The remaining Save and Remove messages are invoked only by tests.
339       case "FormAutofill:SaveAddress": {
340         if (data.guid) {
341           await lazy.gFormAutofillStorage.addresses.update(
342             data.guid,
343             data.address
344           );
345         } else {
346           await lazy.gFormAutofillStorage.addresses.add(data.address);
347         }
348         break;
349       }
350       case "FormAutofill:SaveCreditCard": {
351         // Setting the first parameter of OSKeyStore.ensurLoggedIn as false
352         // since this case only called in tests. Also the reason why we're not calling FormAutofill.verifyUserOSAuth.
353         if (!(await lazy.OSKeyStore.ensureLoggedIn(false)).authenticated) {
354           lazy.log.warn("User canceled encryption login");
355           return undefined;
356         }
357         await lazy.gFormAutofillStorage.creditCards.add(data.creditcard);
358         break;
359       }
360       case "FormAutofill:RemoveAddresses": {
361         data.guids.forEach(guid =>
362           lazy.gFormAutofillStorage.addresses.remove(guid)
363         );
364         break;
365       }
366       case "FormAutofill:RemoveCreditCards": {
367         data.guids.forEach(guid =>
368           lazy.gFormAutofillStorage.creditCards.remove(guid)
369         );
370         break;
371       }
372     }
374     return undefined;
375   }
377   // For a third-party frame, we only autofill when the frame is same origin
378   // with the frame that triggers autofill.
379   isBCSameOrigin(browsingContext) {
380     return this.manager.documentPrincipal.equals(
381       browsingContext.currentWindowGlobal.documentPrincipal
382     );
383   }
385   static getActor(browsingContext) {
386     return browsingContext?.currentWindowGlobal?.getActor("FormAutofill");
387   }
389   get formOrigin() {
390     return lazy.LoginHelper.getLoginOrigin(
391       this.manager.documentPrincipal?.originNoSuffix
392     );
393   }
395   /**
396    * Recursively identifies autofillable fields within each sub-frame of the
397    * given browsing context.
398    *
399    * This function iterates through all sub-frames and uses the provided
400    * browsing context to locate and identify fields that are eligible for
401    * autofill. It handles both the top-level context and any nested
402    * iframes, aggregating all identified fields into a single array.
403    *
404    * @param {BrowsingContext} browsingContext
405    *        The browsing context where autofill fields are to be identified.
406    * @param {string} focusedBCId
407    *        The browsing context ID of the <iframe> within the top-level context
408    *        that contains the currently focused field. Null if this call is
409    *        triggered from the top-level.
410    * @param {Array} alreadyIdentifiedFields
411    *        An array of previously identified fields for the current actor.
412    *        This serves as a cache to avoid redundant field identification.
413    *
414    * @returns {Promise<Array>}
415    *        A promise that resolves to an array containing two elements:
416    *        1. An array of FieldDetail objects representing detected fields.
417    *        2. The root element ID.
418    */
419   async identifyAllSubTreeFields(
420     browsingContext,
421     focusedBCId,
422     alreadyIdentifiedFields,
423     msg
424   ) {
425     let identifiedFieldsIncludeIframe = [];
426     try {
427       const actor = FormAutofillParent.getActor(browsingContext);
428       if (actor == this) {
429         identifiedFieldsIncludeIframe = alreadyIdentifiedFields;
430       } else {
431         msg ||= "FormAutofill:IdentifyFields";
432         identifiedFieldsIncludeIframe = await actor.sendQuery(msg, {
433           focusedBCId,
434         });
435       }
436     } catch (e) {
437       console.error("There was an error identifying fields: ", e.message);
438     }
440     if (!identifiedFieldsIncludeIframe.length) {
441       return [[], null];
442     }
444     const rootElementId = identifiedFieldsIncludeIframe[0].rootElementId;
446     const subTreeDetails = [];
447     for (const field of identifiedFieldsIncludeIframe) {
448       if (field.localName != "iframe") {
449         subTreeDetails.push(field);
450         continue;
451       }
453       const iframeBC = BrowsingContext.get(field.browsingContextId);
454       const [fields] = await this.identifyAllSubTreeFields(
455         iframeBC,
456         focusedBCId,
457         alreadyIdentifiedFields,
458         msg
459       );
460       subTreeDetails.push(...fields);
461     }
462     return [subTreeDetails, rootElementId];
463   }
465   /**
466    * After collecting all the fields, we apply heuristics to:
467    * 1. Update field names based on the context of surrounding fields.
468    *    For instance, a field named 'name' might be renamed to 'cc-name' if
469    *    it follows a field named 'cc-number'.
470    * 2. Identify and classify address and credit card sections. Sections
471    *    are used to group fields that should be autofilled together.
472    *
473    * @param {Array<FieldDetail>} fieldDetails
474    *        An array of the identified fields.
475    * @param {object} options
476    *        options to parse to 'classifySections'
477    */
478   static parseAndClassifyFields(fieldDetails, options = {}) {
479     lazy.FormAutofillHeuristics.parseAndUpdateFieldNamesParent(fieldDetails);
481     // At this point we have identified all the fields that are under the same
482     // root element. We can run section classification heuristic now.
483     return lazy.FormAutofillSection.classifySections(fieldDetails, options);
484   }
486   /**
487    * When a field is detected, identify fields in other frames, if they exist.
488    * To ensure that the identified fields across frames still follow the document
489    * order, we traverse from the top-level window and recursively identify fields
490    * in subframes.
491    *
492    * @param {Array} fieldsIncludeIframe
493    *        Array of FieldDetail objects of detected fields (include iframes).
494    */
495   async onFieldsDetected(fieldsIncludeIframe) {
496     // If the detected fields are not in the top-level, identify the <iframe> in
497     // the top-level that contains the detected fields. This is necessary to determine
498     // the root element of this form. For non-top-level frames, the focused <iframe>
499     // is not needed because, in the case of iframes, the root element is always
500     // the frame itself (we disregard <form> elements within <iframes>).
501     let focusedBCId;
502     const topBC = this.browsingContext.top;
503     if (this.browsingContext != topBC) {
504       let bc = this.browsingContext;
505       while (bc.parent != topBC) {
506         bc = bc.parent;
507       }
508       focusedBCId = bc.id;
509     }
511     const [fieldDetails, rootElementId] = await this.identifyAllSubTreeFields(
512       topBC,
513       focusedBCId,
514       fieldsIncludeIframe
515     );
517     // Now we have collected all the fields for the form, run parsing heuristics
518     // to update the field name based on surrounding fields.
519     const sections = FormAutofillParent.parseAndClassifyFields(fieldDetails);
521     this.sectionsByRootId.set(rootElementId, sections);
523     // Note that 'onFieldsDetected' is not only called when a form is detected,
524     // but also called when the elements in a form are changed. When the elements
525     // in a form are changed, we treat the "updated" section as a new detected section.
526     sections.forEach(section => section.onDetected());
528     if (FormAutofill.isMLExperimentEnabled) {
529       const allFieldDetails = sections.flatMap(section => section.fieldDetails);
530       lazy.MLAutofill.runInference(allFieldDetails);
531     }
533     // Inform all the child actors of the updated 'fieldDetails'
534     const detailsByBC =
535       lazy.FormAutofillSection.groupFieldDetailsByBrowsingContext(fieldDetails);
536     for (const [bcId, fds] of Object.entries(detailsByBC)) {
537       try {
538         const actor = FormAutofillParent.getActor(BrowsingContext.get(bcId));
539         await actor.sendQuery("FormAutofill:onFieldsDetectedComplete", {
540           fds,
541         });
542       } catch (e) {
543         console.error(
544           "There was an error sending 'onFieldsDetectedComplete' msg",
545           e.message
546         );
547       }
548     }
550     // This is for testing purpose only which sends a notification to indicate that the
551     // form has been identified, and ready to open popup.
552     this.notifyMessageObservers("fieldsIdentified");
553   }
555   /**
556    * Called when a form is submitted
557    *
558    * @param {string} rootElementId
559    *        The id of the root element. If the form
560    * @param {object} formFilledData
561    *        An object keyed by element id, and the value is an object that
562    *        includes the following properties:
563    *          - filledState: The autofill state of the element.
564    *          - filledValue: The value of the element.
565    *        See `collectFormFilledData` in FormAutofillHandler.
566    */
567   async onFormSubmit(rootElementId, formFilledData) {
568     const submittedSections = this.sectionsByRootId.values().find(sections => {
569       const details = sections.flatMap(s => s.fieldDetails).flat();
570       return details.some(detail => detail.rootElementId == rootElementId);
571     });
573     if (!submittedSections) {
574       return;
575     }
577     const address = [];
578     const creditCard = [];
580     // Caching the submitted data as actors may be destroyed immediately after
581     // submission.
582     this.submittedData.set(rootElementId, formFilledData);
584     for (const section of submittedSections) {
585       const submittedResult = new Map();
586       const autofillFields = section.getAutofillFields();
587       const detailsByBC =
588         lazy.FormAutofillSection.groupFieldDetailsByBrowsingContext(
589           autofillFields
590         );
591       for (const [bcId, fieldDetails] of Object.entries(detailsByBC)) {
592         try {
593           // Fields within the same section that share the same browsingContextId
594           // should also share the same rootElementId.
595           const rootEId = fieldDetails[0].rootElementId;
597           let result = this.submittedData.get(rootEId);
598           if (!result) {
599             const actor = FormAutofillParent.getActor(
600               BrowsingContext.get(bcId)
601             );
602             result = await actor.sendQuery("FormAutofill:GetFilledInfo", {
603               rootElementId: rootEId,
604             });
605           }
606           result.forEach((value, key) => submittedResult.set(key, value));
607         } catch (e) {
608           console.error("There was an error submitting: ", e.message);
609           return;
610         }
611       }
613       // At this point, it's possible to discover that this section has already
614       // been submitted since submission events may be triggered concurrently by
615       // multiple actors.
616       if (section.submitted) {
617         continue;
618       }
619       section.onSubmitted(submittedResult);
621       const secRecord = section.createRecord(submittedResult);
622       if (!secRecord) {
623         continue;
624       }
626       if (section instanceof lazy.FormAutofillAddressSection) {
627         address.push(secRecord);
628       } else if (section instanceof lazy.FormAutofillCreditCardSection) {
629         creditCard.push(secRecord);
630       } else {
631         throw new Error("Unknown section type");
632       }
633     }
635     const browser = this.manager?.browsingContext.top.embedderElement;
636     if (!browser) {
637       return;
638     }
640     // Transmit the telemetry immediately in the meantime form submitted, and handle
641     // these pending doorhangers later.
642     await Promise.all(
643       [
644         await Promise.all(
645           address.map(addrRecord => this._onAddressSubmit(addrRecord, browser))
646         ),
647         await Promise.all(
648           creditCard.map(ccRecord =>
649             this._onCreditCardSubmit(ccRecord, browser)
650           )
651         ),
652       ]
653         .map(pendingDoorhangers => {
654           return pendingDoorhangers.filter(
655             pendingDoorhanger =>
656               !!pendingDoorhanger && typeof pendingDoorhanger == "function"
657           );
658         })
659         .map(pendingDoorhangers =>
660           (async () => {
661             for (const showDoorhanger of pendingDoorhangers) {
662               await showDoorhanger();
663             }
664           })()
665         )
666     );
667   }
669   /**
670    * Get the records from profile store and return results back to content
671    * process. It will decrypt the credit card number and append
672    * "cc-number-decrypted" to each record if OSKeyStore isn't set.
673    *
674    * This is static as a unit test calls this.
675    *
676    * @param  {object} data
677    * @param  {string} data.searchString
678    *         The typed string for filtering out the matched records.
679    * @param  {string} data.collectionName
680    *         The name used to specify which collection to retrieve records.
681    * @param  {string} data.fieldName
682    *         The field name to search.
683    */
684   async getRecords({ searchString, collectionName, fieldName }) {
685     // Derive the collection name from field name if it doesn't exist
686     collectionName ||=
687       FormAutofillUtils.getCollectionNameFromFieldName(fieldName);
689     const collection = lazy.gFormAutofillStorage[collectionName];
690     if (!collection) {
691       return [];
692     }
694     const records = await collection.getAll();
696     // Add testing records if exists
697     records.push(...this.#getTemporaryRecordForTab(collectionName));
699     if (!fieldName || !records.length) {
700       return records;
701     }
703     // We don't filter "cc-number"
704     if (collectionName == CREDITCARDS_COLLECTION_NAME) {
705       if (fieldName == "cc-number") {
706         return records.filter(record => !!record["cc-number"]);
707       }
708     }
710     const lcSearchString = searchString.toLowerCase();
711     return records.filter(record => {
712       const fieldValue = record[fieldName];
713       if (!fieldValue) {
714         return false;
715       }
717       if (
718         collectionName == ADDRESSES_COLLECTION_NAME &&
719         !FormAutofill.isAutofillAddressesAvailableInCountry(record.country)
720       ) {
721         // Address autofill isn't supported for the record's country so we don't
722         // want to attempt to potentially incorrectly fill the address fields.
723         return false;
724       }
726       return (
727         !lcSearchString ||
728         String(fieldValue).toLowerCase().startsWith(lcSearchString)
729       );
730     });
731   }
733   /*
734    * Capture-related functions
735    */
737   async _onAddressSubmit(address, browser) {
738     if (!FormAutofill.isAutofillAddressesEnabled) {
739       return false;
740     }
742     const storage = lazy.gFormAutofillStorage.addresses;
744     // Make sure record is normalized before comparing with records in the storage
745     try {
746       storage._normalizeRecord(address.record);
747     } catch (_e) {
748       return false;
749     }
751     const newAddress = new lazy.AddressComponent(
752       address.record,
753       // Invalid address fields in the address form will not be captured.
754       { ignoreInvalid: true }
755     );
757     // Exams all stored record to determine whether to show the prompt or not.
758     let mergeableFields = [];
759     let preserveFields = [];
760     let oldRecord = {};
762     for (const record of await storage.getAll()) {
763       const savedAddress = new lazy.AddressComponent(record);
764       // filter invalid field
765       const result = newAddress.compare(savedAddress);
767       // If any of the fields in the new address are different from the corresponding fields
768       // in the saved address, the two addresses are considered different. For example, if
769       // the name, email, country are the same but the street address is different, the two
770       // addresses are not considered the same.
771       if (Object.values(result).includes("different")) {
772         continue;
773       }
775       // If none of the fields in the new address are mergeable, the new address is considered
776       // a duplicate of a local address. Therefore, we don't need to capture this address.
777       const fields = Object.entries(result)
778         .filter(v => ["superset", "similar"].includes(v[1]))
779         .map(v => v[0]);
780       if (!fields.length) {
781         lazy.log.debug(
782           "A duplicated address record is found, do not show the prompt"
783         );
784         storage.notifyUsed(record.guid);
785         return false;
786       }
788       // If the new address is neither a duplicate of the saved address nor a different address.
789       // There must be at least one field we can merge, show the update doorhanger
790       lazy.log.debug(
791         "A mergeable address record is found, show the update prompt"
792       );
794       // If one record has fewer mergeable fields compared to another, it suggests greater similarity
795       // to the merged record. In such cases, we opt for the record with the fewest mergeable fields.
796       // TODO: Bug 1830841. Add a testcase
797       if (!mergeableFields.length || mergeableFields > fields.length) {
798         mergeableFields = fields;
799         preserveFields = Object.entries(result)
800           .filter(v => ["same", "subset"].includes(v[1]))
801           .map(v => v[0]);
802         oldRecord = record;
803       }
804     }
806     // Find a mergeable old record, construct the new record by only copying mergeable fields
807     // from the new address.
808     let newRecord = {};
809     if (mergeableFields.length) {
810       // TODO: This is only temporarily, should be removed after Bug 1836438 is fixed
811       if (mergeableFields.includes("name")) {
812         mergeableFields.push("given-name", "additional-name", "family-name");
813       }
814       mergeableFields.forEach(f => {
815         if (f in newAddress.record) {
816           newRecord[f] = newAddress.record[f];
817         }
818       });
820       if (preserveFields.includes("name")) {
821         preserveFields.push("given-name", "additional-name", "family-name");
822       }
823       preserveFields.forEach(f => {
824         if (f in oldRecord) {
825           newRecord[f] = oldRecord[f];
826         }
827       });
828     } else {
829       newRecord = newAddress.record;
830     }
832     if (!this._shouldShowSaveAddressPrompt(newAddress.record)) {
833       return false;
834     }
836     return async () => {
837       await lazy.FormAutofillPrompter.promptToSaveAddress(
838         browser,
839         storage,
840         address.flowId,
841         { oldRecord, newRecord }
842       );
843     };
844   }
846   async _onCreditCardSubmit(creditCard, browser) {
847     const storage = lazy.gFormAutofillStorage.creditCards;
849     // Make sure record is normalized before comparing with records in the storage
850     try {
851       storage._normalizeRecord(creditCard.record);
852     } catch (_e) {
853       return false;
854     }
856     // If the record alreay exists in the storage, don't bother showing the prompt
857     const matchRecord = (
858       await storage.getMatchRecords(creditCard.record).next()
859     ).value;
860     if (matchRecord) {
861       storage.notifyUsed(matchRecord.guid);
862       return false;
863     }
865     // Suppress the pending doorhanger from showing up if user disabled credit card in previous doorhanger.
866     if (!FormAutofill.isAutofillCreditCardsEnabled) {
867       return false;
868     }
870     // Overwrite the guid if there is a duplicate
871     const duplicateRecord =
872       (await storage.getDuplicateRecords(creditCard.record).next()).value ?? {};
874     return async () => {
875       await lazy.FormAutofillPrompter.promptToSaveCreditCard(
876         browser,
877         storage,
878         creditCard.flowId,
879         { oldRecord: duplicateRecord, newRecord: creditCard.record }
880       );
881     };
882   }
884   _shouldShowSaveAddressPrompt(record) {
885     if (!FormAutofill.isAutofillAddressesCaptureEnabled) {
886       return false;
887     }
889     // Do not save address for regions that we don't support
890     if (!FormAutofill.isAutofillAddressesAvailableInCountry(record.country)) {
891       lazy.log.debug(
892         `Do not show the address capture prompt for unsupported regions - ${record.country}`
893       );
894       return false;
895     }
897     // Display the address capture doorhanger only when the submitted form contains all
898     // the required fields. This approach is implemented to prevent excessive prompting.
899     let requiredFields = FormAutofill.addressCaptureRequiredFields;
900     requiredFields ??=
901       FormAutofillUtils.getFormFormat(record.country).countryRequiredFields ??
902       [];
904     if (!requiredFields.every(field => field in record)) {
905       lazy.log.debug(
906         "Do not show the address capture prompt when the submitted form doesn't contain all the required fields"
907       );
908       return false;
909     }
911     return true;
912   }
914   /*
915    * AutoComplete-related functions
916    */
918   /**
919    * Retrieves autocomplete entries for a given search string and data context.
920    *
921    * @param {string} searchString
922    *                 The search string used to filter autocomplete entries.
923    * @param {object} options
924    * @param {string} options.fieldName
925    *                 The name of the field for which autocomplete entries are being fetched.
926    * @param {string} options.elementId
927    *                 The id of the element for which we are searching for an autocomplete entry.
928    * @param {string} options.scenarioName
929    *                 The scenario name used in the autocomplete operation to fetch external entries.
930    * @returns {Promise<object>} A promise that resolves to an object containing two properties: `records` and `externalEntries`.
931    *         `records` is an array of autofill records from the form's internal data, sorted by `timeLastUsed`.
932    *         `externalEntries` is an array of external autocomplete items fetched based on the scenario.
933    *         `allFieldNames` is an array containing all the matched field name found in this section.
934    */
935   async searchAutoCompleteEntries(searchString, options) {
936     const { fieldName, elementId, scenarioName } = options;
938     const section = this.getSectionByElementId(elementId);
939     if (!section.isValidSection() || !section.isEnabled()) {
940       return null;
941     }
943     const relayPromise = lazy.FirefoxRelay.autocompleteItemsAsync({
944       origin: this.formOrigin,
945       scenarioName,
946       hasInput: !!searchString?.length,
947     });
949     // Retrieve information for the autocomplete entry
950     const recordsPromise = this.getRecords({
951       searchString,
952       fieldName,
953     });
955     const [records, externalEntries] = await Promise.all([
956       recordsPromise,
957       relayPromise,
958     ]);
960     // Sort addresses by timeLastUsed for showing the lastest used address at top.
961     records.sort((a, b) => b.timeLastUsed - a.timeLastUsed);
962     return { records, externalEntries, allFieldNames: section.allFieldNames };
963   }
965   /**
966    * This function is called when an autocomplete entry that is provided by
967    * formautofill is selected by the user.
968    */
969   async onAutoCompleteEntrySelected(message, data) {
970     switch (message) {
971       case "FormAutofill:OpenPreferences": {
972         const win = lazy.BrowserWindowTracker.getTopWindow();
973         win.openPreferences("privacy-form-autofill");
974         break;
975       }
977       case "FormAutofill:ClearForm": {
978         this.clearForm(data.focusElementId);
979         break;
980       }
982       case "FormAutofill:FillForm": {
983         this.autofillFields(data.focusElementId, data.profile);
984         break;
985       }
987       default: {
988         lazy.log.debug("Unsupported autocomplete message:", message);
989         break;
990       }
991     }
992   }
994   onAutoCompletePopupOpened(elementId) {
995     const section = this.getSectionByElementId(elementId);
996     section?.onPopupOpened(elementId);
997   }
999   onAutoCompleteEntryClearPreview(message, data) {
1000     this.previewFields(data.focusElementId, null);
1001   }
1003   onAutoCompleteEntryHovered(message, data) {
1004     if (message == "FormAutofill:FillForm") {
1005       this.previewFields(data.focusElementId, data.profile);
1006     } else {
1007       // Make sure the preview is cleared when users select an entry
1008       // that doesn't support preview.
1009       this.previewFields(data.focusElementId, null);
1010     }
1011   }
1013   // Credit card number will only be filled when it is same-origin with the frame that
1014   // triggers the autofilling.
1015   #FIELDS_FILLED_WHEN_SAME_ORIGIN = ["cc-number"];
1017   /**
1018    * Determines if the field should be autofilled based on its origin.
1019    *
1020    * @param {BorwsingContext} bc
1021    *        The browsing context the field is in.
1022    * @param {object} fieldDetail
1023    *        The Field detail of the field to be autofilled.
1024    *
1025    * @returns {boolean}
1026    *        Returns true if the field should be autofilled, false otherwise.
1027    */
1028   shouldAutofill(bc, fieldDetail) {
1029     const isSameOrigin = this.isBCSameOrigin(bc);
1031     // Autofill always applies to frames that are the same origin as the triggered frame.
1032     if (isSameOrigin) {
1033       return true;
1034     }
1036     // Relaxed autofill rule is controlled by a preference.
1037     if (!FormAutofill.autofillSameOriginWithTop) {
1038       return false;
1039     }
1041     // Relaxed autofill restrictions: for fields other than the credit card number,
1042     // if the field is in a top-level frame or in a first-party origin iframe,
1043     // autofill is allowed.
1044     if (this.#FIELDS_FILLED_WHEN_SAME_ORIGIN.includes(fieldDetail.fieldName)) {
1045       return false;
1046     }
1048     return FormAutofillUtils.isBCSameOriginWithTop(bc);
1049   }
1051   /**
1052    * Trigger the autofill-related action in child processes that are within
1053    * this section.
1054    *
1055    * @param {string} message
1056    *        The message to be sent to the child processes to trigger the corresponding
1057    *        action.
1058    * @param {string} focusedId
1059    *        The ID of the element that initially triggers the autofill action.
1060    * @param {object} section
1061    *        The section that contains fields to be autofilled.
1062    * @param {object} profile
1063    *        The profile data used for autofilling the fields.
1064    */
1065   async #triggerAutofillActionInChildren(message, focusedId, section, profile) {
1066     const autofillFields = section.getAutofillFields();
1067     const detailsByBC =
1068       lazy.FormAutofillSection.groupFieldDetailsByBrowsingContext(
1069         autofillFields
1070       );
1072     const result = new Map();
1073     const entries = Object.entries(detailsByBC);
1075     // Since we focus on the element when setting its autofill value, we need to ensure
1076     // the frame that contains the focused input is the last one that runs autofill. Doing
1077     // this guarantees the focused element remains the focused one after autofilling.
1078     const index = entries.findIndex(e =>
1079       e[1].some(f => f.elementId == focusedId)
1080     );
1081     if (index != -1) {
1082       const entry = entries.splice(index, 1)[0];
1083       entries.push(entry);
1084     }
1086     for (const [bcId, fieldDetails] of entries) {
1087       const bc = BrowsingContext.get(bcId);
1089       // For sensitive fields, we ONLY fill them when they are same-origin with
1090       // the triggered frame.
1091       const ids = fieldDetails
1092         .filter(detail => this.shouldAutofill(bc, detail))
1093         .map(detail => detail.elementId);
1095       try {
1096         const actor = FormAutofillParent.getActor(bc);
1097         const ret = await actor.sendQuery(message, {
1098           focusedId: bc == this.manager.browsingContext ? focusedId : null,
1099           ids,
1100           profile,
1101         });
1102         if (ret instanceof Map) {
1103           ret.forEach((value, key) => result.set(key, value));
1104         }
1105       } catch (e) {
1106         console.error("There was an error autofilling: ", e.message);
1107       }
1108     }
1110     return result;
1111   }
1113   /**
1114    * Previews autofill results for the section containing the triggered element
1115    * using the selected user profile.
1116    *
1117    * @param {string} elementId
1118    *        The id of the element that triggers the autofill preview
1119    * @param {object} profile
1120    *        The user-selected profile data to be used for the autofill preview
1121    */
1122   async previewFields(elementId, profile) {
1123     const section = this.getSectionByElementId(elementId);
1125     if (!(await section.preparePreviewProfile(profile))) {
1126       lazy.log.debug("profile cannot be previewed");
1127       return;
1128     }
1130     const msg = "FormAutofill:PreviewFields";
1131     await this.#triggerAutofillActionInChildren(
1132       msg,
1133       elementId,
1134       section,
1135       profile
1136     );
1138     // For testing only
1139     Services.obs.notifyObservers(null, "formautofill-preview-complete");
1140   }
1142   /**
1143    * Autofill results for the section containing the triggered element.
1144    * using the selected user profile.
1145    *
1146    * @param {string} elementId
1147    *        The id of the element that triggers the autofill.
1148    * @param {object} profile
1149    *        The user-selected profile data to be used for the autofill
1150    */
1151   async autofillFields(elementId, profile) {
1152     const section = this.getSectionByElementId(elementId);
1153     if (!(await section.prepareFillingProfile(profile))) {
1154       lazy.log.debug("profile cannot be filled");
1155       return;
1156     }
1158     const msg = "FormAutofill:FillFields";
1159     const result = await this.#triggerAutofillActionInChildren(
1160       msg,
1161       elementId,
1162       section,
1163       profile
1164     );
1166     result.forEach((value, key) => this.filledResult.set(key, value));
1167     section.onFilled(result);
1169     // For testing only
1170     Services.obs.notifyObservers(null, "formautofill-autofill-complete");
1171   }
1173   /**
1174    * Clears autofill results for the section containing the triggered element.
1175    *
1176    * @param {string} elementId
1177    *        The id of the element that triggers the clear action.
1178    */
1179   async clearForm(elementId) {
1180     const section = this.getSectionByElementId(elementId);
1182     section.onCleared(elementId);
1184     const msg = "FormAutofill:ClearFilledFields";
1185     await this.#triggerAutofillActionInChildren(msg, elementId, section);
1187     // For testing only
1188     Services.obs.notifyObservers(null, "formautofill-clear-form-complete");
1189   }
1191   /**
1192    * Called when a autofilled fields is modified by the user.
1193    *
1194    * @param {string} elementId
1195    *        The id of the element that users modify its value after autofilling.
1196    */
1197   onFieldFilledModified(elementId) {
1198     if (!this.filledResult?.get(elementId)) {
1199       return;
1200     }
1202     this.filledResult.get(elementId).filledState = FIELD_STATES.NORMAL;
1204     const section = this.getSectionByElementId(elementId);
1206     // For telemetry
1207     section?.onFilledModified(elementId);
1209     // Restore <select> fields to their initial state once we know
1210     // that the user intends to manually clear the filled form.
1211     const fieldDetails = section.fieldDetails;
1212     const selects = fieldDetails.filter(field => field.localName == "select");
1213     if (selects.length) {
1214       const inputs = fieldDetails.filter(
1215         field =>
1216           this.filledResult.has(field.elementId) && field.localName == "input"
1217       );
1218       if (
1219         inputs.every(
1220           field =>
1221             this.filledResult.get(field.elementId).filledState ==
1222             FIELD_STATES.NORMAL
1223         )
1224       ) {
1225         const ids = selects.map(field => field.elementId);
1226         this.sendAsyncMessage("FormAutofill:ClearFilledFields", { ids });
1227       }
1228     }
1229   }
1231   getSectionByElementId(elementId) {
1232     for (const sections of this.sectionsByRootId.values()) {
1233       const section = sections.find(s =>
1234         s.getFieldDetailByElementId(elementId)
1235       );
1236       if (section) {
1237         return section;
1238       }
1239     }
1240     return null;
1241   }
1243   static addMessageObserver(observer) {
1244     gMessageObservers.add(observer);
1245   }
1247   static removeMessageObserver(observer) {
1248     gMessageObservers.delete(observer);
1249   }
1251   notifyMessageObservers(callbackName, data) {
1252     for (let observer of gMessageObservers) {
1253       try {
1254         if (callbackName in observer) {
1255           observer[callbackName](
1256             data,
1257             this.manager.browsingContext.topChromeWindow
1258           );
1259         }
1260       } catch (ex) {
1261         console.error(ex);
1262       }
1263     }
1264   }
1266   /**
1267    * Autofill Developer Tools Related API.
1268    * API Below are used by autofill developer tools.
1269    * Do not change the function name or argument unless we are going to update
1270    * the autofill developer tool as well.
1271    */
1273   /**
1274    * Autofill Developer Tool API to inspect the autofill fields in this
1275    * tab.
1276    *
1277    * @param {Array<object>} overwriteFieldDetails
1278    *        A list of FieldDetail object to overwrite the detected result.
1279    *        This is used by the developer tool to correct the inspected
1280    *        result.
1281    * @returns {Array<object>}
1282    *        A list of sections representing the inspected result for this page.
1283    */
1284   async inspectFields(overwriteFieldDetails = []) {
1285     // Start with inspecting the fields in the top-level
1286     const topBC = this.browsingContext.top;
1287     const actor = FormAutofillParent.getActor(topBC);
1288     const fields = await actor.sendQuery("FormAutofill:InspectFields");
1290     if (!fields.length) {
1291       return [];
1292     }
1294     // Group fields that belong to the same form.
1295     const fieldsByForm = [];
1296     const rootElementIdByFormIndex = {};
1297     for (const field of fields) {
1298       let index = rootElementIdByFormIndex[field.rootElementId];
1299       if (index == undefined) {
1300         index = fieldsByForm.length;
1301         rootElementIdByFormIndex[field.rootElementId] = index;
1302         fieldsByForm.push([]);
1303       }
1304       fieldsByForm[index].push(field);
1305     }
1307     // Use `onFieldsDetected` function to simulate the behavior that when
1308     // users click on a field, we will also identify fields that are in an <iframe>
1309     const allSections = [];
1310     for (const formFields of fieldsByForm) {
1311       const msg = "FormAutofill:InspectFields";
1312       const [fieldDetails] = await this.identifyAllSubTreeFields(
1313         topBC,
1314         null,
1315         formFields,
1316         msg
1317       );
1319       fieldDetails.forEach(field => {
1320         const overwriteField = overwriteFieldDetails.find(
1321           ow => ow.inspectId == field.inspectId
1322         );
1323         if (overwriteField) {
1324           Object.assign(field, overwriteField);
1325         }
1326       });
1328       const formSections = FormAutofillParent.parseAndClassifyFields(
1329         fieldDetails,
1330         { ignoreUnknownField: false }
1331       );
1332       if (formSections.length) {
1333         allSections.push(formSections);
1334       }
1335     }
1336     return allSections;
1337   }
1339   #getTemporaryRecordForTab(collectionName) {
1340     // The temporary record is stored in the top-level actor.
1341     const topBC = this.browsingContext.top;
1342     const actor = FormAutofillParent.getActor(topBC);
1343     return actor?.temporaryRecords?.[collectionName] ?? [];
1344   }
1346   /**
1347    * Autofill Developer Tools Related API:
1348    * Add test records for this tab.
1349    *
1350    * @param {Array<object>} records
1351    *        A list of address or credit card records
1352    */
1353   async setTemporaryRecordsForTab(records) {
1354     const topBC = this.browsingContext.top;
1355     const actor = FormAutofillParent.getActor(topBC);
1356     actor.temporaryRecords = {
1357       [ADDRESSES_COLLECTION_NAME]: [],
1358       [CREDITCARDS_COLLECTION_NAME]: [],
1359     };
1361     for (const record of records) {
1362       const fields = Object.keys(record);
1363       if (!fields.length) {
1364         continue;
1365       }
1366       const collection = FormAutofillUtils.getCollectionNameFromFieldName(
1367         fields[0]
1368       );
1369       const storage =
1370         collection == ADDRESSES_COLLECTION_NAME
1371           ? lazy.gFormAutofillStorage.addresses
1372           : lazy.gFormAutofillStorage.creditCards;
1373       // Since we don't define the pattern for the passed 'record',
1374       // we need to normalize it first.
1375       storage._normalizeRecord(record);
1376       await storage.computeFields(record);
1378       actor.temporaryRecords[collection]?.push(record);
1379     }
1380   }