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/. */
6 * Implements a service used to access storage and communicate with content.
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.
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;
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",
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",
58 ChromeUtils.defineLazyGetter(lazy, "log", () =>
59 FormAutofill.defineLogGetter(lazy, "FormAutofillParent")
62 const { ENABLED_AUTOFILL_ADDRESSES_PREF, ENABLED_AUTOFILL_CREDITCARDS_PREF } =
65 const { ADDRESSES_COLLECTION_NAME, CREDITCARDS_COLLECTION_NAME } =
68 let gMessageObservers = new Set();
70 export let FormAutofillStatus = {
74 * Cache of the Form Autofill status (considering preferences and storage).
79 * Initializes observers and registers the message handler.
82 if (this._initialized) {
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);
100 * Uninitializes FormAutofillStatus. This is for testing only.
105 lazy.gFormAutofillStorage._saveImmediately();
107 if (!this._initialized) {
110 this._initialized = false;
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);
123 get formAutofillStorage() {
124 return lazy.gFormAutofillStorage;
128 * Broadcast the status to frames when the form autofill status changes.
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();
139 * Query preference and storage status to determine the overall status of the
140 * form autofill feature.
142 * @returns {boolean} whether form autofill is active (enabled and has data)
145 const savedFieldNames = Services.ppmm.sharedData.get(
146 "FormAutofill:savedFieldNames"
150 (Services.prefs.getBoolPref(ENABLED_AUTOFILL_ADDRESSES_PREF) ||
151 Services.prefs.getBoolPref(ENABLED_AUTOFILL_CREDITCARDS_PREF)) &&
153 savedFieldNames.size > 0
158 * Update the status and trigger onStatusChanged, if necessary.
161 lazy.log.debug("updateStatus");
162 let wasActive = this._active;
163 this._active = this.computeStatus();
164 if (this._active !== wasActive) {
165 this.onStatusChanged();
169 async updateSavedFieldNames() {
170 lazy.log.debug("updateSavedFieldNames");
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]);
182 savedFieldNames = addressNames;
185 Services.ppmm.sharedData.set(
186 "FormAutofill:savedFieldNames",
189 Services.ppmm.sharedData.flush();
194 async observe(subject, topic, data) {
195 lazy.log.debug("observe:", topic, "with data:", data);
198 !FormAutofill.isAutofillCreditCardsAvailable &&
199 !FormAutofill.isAutofillAddressesAvailable
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"
212 formAutofillGroupBox.appendChild(prefFragment);
216 case "nsPref:changed": {
217 // Observe pref changes and update _active cache if status is changed.
222 case "formautofill-storage-changed": {
223 // Early exit if only metadata is changed
224 if (data == "notifyUsed") {
228 await this.updateSavedFieldNames();
234 `FormAutofillStatus: Unexpected topic observed: ${topic}`
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"
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();
254 return formAutofillStorage;
257 export class FormAutofillParent extends JSWindowActorParent {
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(),
271 get topLevelCache() {
275 this.browsingContext.top == this.browsingContext
277 : FormAutofillParent.getActor(this.browsingContext.top);
280 return actor._topLevelCache;
283 get sectionsByRootId() {
284 return this.topLevelCache.sectionsByRootId;
288 return this.topLevelCache.filledResult;
291 get submittedData() {
292 return this.topLevelCache.submittedData;
295 * Handles the message coming from FormAutofillChild.
297 * @param {object} message
298 * @param {string} message.name The name of the message.
299 * @param {object} message.data The data of the message.
301 async receiveMessage({ name, data }) {
303 !FormAutofill.isAutofillCreditCardsAvailable &&
304 !FormAutofill.isAutofillAddressesAvailable
310 case "FormAutofill:InitStorage": {
311 await lazy.gFormAutofillStorage.initialize();
312 await FormAutofillStatus.updateSavedFieldNames();
315 case "FormAutofill:GetRecords": {
316 const records = await this.getRecords(data);
319 case "FormAutofill:OnFormSubmit": {
320 const { rootElementId, formFilledData } = data;
321 this.notifyMessageObservers("onFormSubmitted", data);
322 this.onFormSubmit(rootElementId, formFilledData);
326 case "FormAutofill:FieldsIdentified":
327 this.notifyMessageObservers("fieldsIdentified", data);
330 case "FormAutofill:OnFieldsDetected":
331 await this.onFieldsDetected(data);
333 case "FormAutofill:FieldFilledModified": {
334 this.onFieldFilledModified(data);
338 // The remaining Save and Remove messages are invoked only by tests.
339 case "FormAutofill:SaveAddress": {
341 await lazy.gFormAutofillStorage.addresses.update(
346 await lazy.gFormAutofillStorage.addresses.add(data.address);
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");
357 await lazy.gFormAutofillStorage.creditCards.add(data.creditcard);
360 case "FormAutofill:RemoveAddresses": {
361 data.guids.forEach(guid =>
362 lazy.gFormAutofillStorage.addresses.remove(guid)
366 case "FormAutofill:RemoveCreditCards": {
367 data.guids.forEach(guid =>
368 lazy.gFormAutofillStorage.creditCards.remove(guid)
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
385 static getActor(browsingContext) {
386 return browsingContext?.currentWindowGlobal?.getActor("FormAutofill");
390 return lazy.LoginHelper.getLoginOrigin(
391 this.manager.documentPrincipal?.originNoSuffix
396 * Recursively identifies autofillable fields within each sub-frame of the
397 * given browsing context.
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.
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.
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.
419 async identifyAllSubTreeFields(
422 alreadyIdentifiedFields,
425 let identifiedFieldsIncludeIframe = [];
427 const actor = FormAutofillParent.getActor(browsingContext);
429 identifiedFieldsIncludeIframe = alreadyIdentifiedFields;
431 msg ||= "FormAutofill:IdentifyFields";
432 identifiedFieldsIncludeIframe = await actor.sendQuery(msg, {
437 console.error("There was an error identifying fields: ", e.message);
440 if (!identifiedFieldsIncludeIframe.length) {
444 const rootElementId = identifiedFieldsIncludeIframe[0].rootElementId;
446 const subTreeDetails = [];
447 for (const field of identifiedFieldsIncludeIframe) {
448 if (field.localName != "iframe") {
449 subTreeDetails.push(field);
453 const iframeBC = BrowsingContext.get(field.browsingContextId);
454 const [fields] = await this.identifyAllSubTreeFields(
457 alreadyIdentifiedFields,
460 subTreeDetails.push(...fields);
462 return [subTreeDetails, rootElementId];
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.
473 * @param {Array<FieldDetail>} fieldDetails
474 * An array of the identified fields.
475 * @param {object} options
476 * options to parse to 'classifySections'
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);
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
492 * @param {Array} fieldsIncludeIframe
493 * Array of FieldDetail objects of detected fields (include iframes).
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>).
502 const topBC = this.browsingContext.top;
503 if (this.browsingContext != topBC) {
504 let bc = this.browsingContext;
505 while (bc.parent != topBC) {
511 const [fieldDetails, rootElementId] = await this.identifyAllSubTreeFields(
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);
533 // Inform all the child actors of the updated 'fieldDetails'
535 lazy.FormAutofillSection.groupFieldDetailsByBrowsingContext(fieldDetails);
536 for (const [bcId, fds] of Object.entries(detailsByBC)) {
538 const actor = FormAutofillParent.getActor(BrowsingContext.get(bcId));
539 await actor.sendQuery("FormAutofill:onFieldsDetectedComplete", {
544 "There was an error sending 'onFieldsDetectedComplete' msg",
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");
556 * Called when a form is submitted
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.
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);
573 if (!submittedSections) {
578 const creditCard = [];
580 // Caching the submitted data as actors may be destroyed immediately after
582 this.submittedData.set(rootElementId, formFilledData);
584 for (const section of submittedSections) {
585 const submittedResult = new Map();
586 const autofillFields = section.getAutofillFields();
588 lazy.FormAutofillSection.groupFieldDetailsByBrowsingContext(
591 for (const [bcId, fieldDetails] of Object.entries(detailsByBC)) {
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);
599 const actor = FormAutofillParent.getActor(
600 BrowsingContext.get(bcId)
602 result = await actor.sendQuery("FormAutofill:GetFilledInfo", {
603 rootElementId: rootEId,
606 result.forEach((value, key) => submittedResult.set(key, value));
608 console.error("There was an error submitting: ", e.message);
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
616 if (section.submitted) {
619 section.onSubmitted(submittedResult);
621 const secRecord = section.createRecord(submittedResult);
626 if (section instanceof lazy.FormAutofillAddressSection) {
627 address.push(secRecord);
628 } else if (section instanceof lazy.FormAutofillCreditCardSection) {
629 creditCard.push(secRecord);
631 throw new Error("Unknown section type");
635 const browser = this.manager?.browsingContext.top.embedderElement;
640 // Transmit the telemetry immediately in the meantime form submitted, and handle
641 // these pending doorhangers later.
645 address.map(addrRecord => this._onAddressSubmit(addrRecord, browser))
648 creditCard.map(ccRecord =>
649 this._onCreditCardSubmit(ccRecord, browser)
653 .map(pendingDoorhangers => {
654 return pendingDoorhangers.filter(
656 !!pendingDoorhanger && typeof pendingDoorhanger == "function"
659 .map(pendingDoorhangers =>
661 for (const showDoorhanger of pendingDoorhangers) {
662 await showDoorhanger();
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.
674 * This is static as a unit test calls this.
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.
684 async getRecords({ searchString, collectionName, fieldName }) {
685 // Derive the collection name from field name if it doesn't exist
687 FormAutofillUtils.getCollectionNameFromFieldName(fieldName);
689 const collection = lazy.gFormAutofillStorage[collectionName];
694 const records = await collection.getAll();
696 // Add testing records if exists
697 records.push(...this.#getTemporaryRecordForTab(collectionName));
699 if (!fieldName || !records.length) {
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"]);
710 const lcSearchString = searchString.toLowerCase();
711 return records.filter(record => {
712 const fieldValue = record[fieldName];
718 collectionName == ADDRESSES_COLLECTION_NAME &&
719 !FormAutofill.isAutofillAddressesAvailableInCountry(record.country)
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.
728 String(fieldValue).toLowerCase().startsWith(lcSearchString)
734 * Capture-related functions
737 async _onAddressSubmit(address, browser) {
738 if (!FormAutofill.isAutofillAddressesEnabled) {
742 const storage = lazy.gFormAutofillStorage.addresses;
744 // Make sure record is normalized before comparing with records in the storage
746 storage._normalizeRecord(address.record);
751 const newAddress = new lazy.AddressComponent(
753 // Invalid address fields in the address form will not be captured.
754 { ignoreInvalid: true }
757 // Exams all stored record to determine whether to show the prompt or not.
758 let mergeableFields = [];
759 let preserveFields = [];
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")) {
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]))
780 if (!fields.length) {
782 "A duplicated address record is found, do not show the prompt"
784 storage.notifyUsed(record.guid);
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
791 "A mergeable address record is found, show the update prompt"
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]))
806 // Find a mergeable old record, construct the new record by only copying mergeable fields
807 // from the new address.
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");
814 mergeableFields.forEach(f => {
815 if (f in newAddress.record) {
816 newRecord[f] = newAddress.record[f];
820 if (preserveFields.includes("name")) {
821 preserveFields.push("given-name", "additional-name", "family-name");
823 preserveFields.forEach(f => {
824 if (f in oldRecord) {
825 newRecord[f] = oldRecord[f];
829 newRecord = newAddress.record;
832 if (!this._shouldShowSaveAddressPrompt(newAddress.record)) {
837 await lazy.FormAutofillPrompter.promptToSaveAddress(
841 { oldRecord, newRecord }
846 async _onCreditCardSubmit(creditCard, browser) {
847 const storage = lazy.gFormAutofillStorage.creditCards;
849 // Make sure record is normalized before comparing with records in the storage
851 storage._normalizeRecord(creditCard.record);
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()
861 storage.notifyUsed(matchRecord.guid);
865 // Suppress the pending doorhanger from showing up if user disabled credit card in previous doorhanger.
866 if (!FormAutofill.isAutofillCreditCardsEnabled) {
870 // Overwrite the guid if there is a duplicate
871 const duplicateRecord =
872 (await storage.getDuplicateRecords(creditCard.record).next()).value ?? {};
875 await lazy.FormAutofillPrompter.promptToSaveCreditCard(
879 { oldRecord: duplicateRecord, newRecord: creditCard.record }
884 _shouldShowSaveAddressPrompt(record) {
885 if (!FormAutofill.isAutofillAddressesCaptureEnabled) {
889 // Do not save address for regions that we don't support
890 if (!FormAutofill.isAutofillAddressesAvailableInCountry(record.country)) {
892 `Do not show the address capture prompt for unsupported regions - ${record.country}`
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;
901 FormAutofillUtils.getFormFormat(record.country).countryRequiredFields ??
904 if (!requiredFields.every(field => field in record)) {
906 "Do not show the address capture prompt when the submitted form doesn't contain all the required fields"
915 * AutoComplete-related functions
919 * Retrieves autocomplete entries for a given search string and data context.
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.
935 async searchAutoCompleteEntries(searchString, options) {
936 const { fieldName, elementId, scenarioName } = options;
938 const section = this.getSectionByElementId(elementId);
939 if (!section.isValidSection() || !section.isEnabled()) {
943 const relayPromise = lazy.FirefoxRelay.autocompleteItemsAsync({
944 origin: this.formOrigin,
946 hasInput: !!searchString?.length,
949 // Retrieve information for the autocomplete entry
950 const recordsPromise = this.getRecords({
955 const [records, externalEntries] = await Promise.all([
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 };
966 * This function is called when an autocomplete entry that is provided by
967 * formautofill is selected by the user.
969 async onAutoCompleteEntrySelected(message, data) {
971 case "FormAutofill:OpenPreferences": {
972 const win = lazy.BrowserWindowTracker.getTopWindow();
973 win.openPreferences("privacy-form-autofill");
977 case "FormAutofill:ClearForm": {
978 this.clearForm(data.focusElementId);
982 case "FormAutofill:FillForm": {
983 this.autofillFields(data.focusElementId, data.profile);
988 lazy.log.debug("Unsupported autocomplete message:", message);
994 onAutoCompletePopupOpened(elementId) {
995 const section = this.getSectionByElementId(elementId);
996 section?.onPopupOpened(elementId);
999 onAutoCompleteEntryClearPreview(message, data) {
1000 this.previewFields(data.focusElementId, null);
1003 onAutoCompleteEntryHovered(message, data) {
1004 if (message == "FormAutofill:FillForm") {
1005 this.previewFields(data.focusElementId, data.profile);
1007 // Make sure the preview is cleared when users select an entry
1008 // that doesn't support preview.
1009 this.previewFields(data.focusElementId, null);
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"];
1018 * Determines if the field should be autofilled based on its origin.
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.
1025 * @returns {boolean}
1026 * Returns true if the field should be autofilled, false otherwise.
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.
1036 // Relaxed autofill rule is controlled by a preference.
1037 if (!FormAutofill.autofillSameOriginWithTop) {
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)) {
1048 return FormAutofillUtils.isBCSameOriginWithTop(bc);
1052 * Trigger the autofill-related action in child processes that are within
1055 * @param {string} message
1056 * The message to be sent to the child processes to trigger the corresponding
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.
1065 async #triggerAutofillActionInChildren(message, focusedId, section, profile) {
1066 const autofillFields = section.getAutofillFields();
1068 lazy.FormAutofillSection.groupFieldDetailsByBrowsingContext(
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)
1082 const entry = entries.splice(index, 1)[0];
1083 entries.push(entry);
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);
1096 const actor = FormAutofillParent.getActor(bc);
1097 const ret = await actor.sendQuery(message, {
1098 focusedId: bc == this.manager.browsingContext ? focusedId : null,
1102 if (ret instanceof Map) {
1103 ret.forEach((value, key) => result.set(key, value));
1106 console.error("There was an error autofilling: ", e.message);
1114 * Previews autofill results for the section containing the triggered element
1115 * using the selected user profile.
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
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");
1130 const msg = "FormAutofill:PreviewFields";
1131 await this.#triggerAutofillActionInChildren(
1139 Services.obs.notifyObservers(null, "formautofill-preview-complete");
1143 * Autofill results for the section containing the triggered element.
1144 * using the selected user profile.
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
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");
1158 const msg = "FormAutofill:FillFields";
1159 const result = await this.#triggerAutofillActionInChildren(
1166 result.forEach((value, key) => this.filledResult.set(key, value));
1167 section.onFilled(result);
1170 Services.obs.notifyObservers(null, "formautofill-autofill-complete");
1174 * Clears autofill results for the section containing the triggered element.
1176 * @param {string} elementId
1177 * The id of the element that triggers the clear action.
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);
1188 Services.obs.notifyObservers(null, "formautofill-clear-form-complete");
1192 * Called when a autofilled fields is modified by the user.
1194 * @param {string} elementId
1195 * The id of the element that users modify its value after autofilling.
1197 onFieldFilledModified(elementId) {
1198 if (!this.filledResult?.get(elementId)) {
1202 this.filledResult.get(elementId).filledState = FIELD_STATES.NORMAL;
1204 const section = this.getSectionByElementId(elementId);
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(
1216 this.filledResult.has(field.elementId) && field.localName == "input"
1221 this.filledResult.get(field.elementId).filledState ==
1225 const ids = selects.map(field => field.elementId);
1226 this.sendAsyncMessage("FormAutofill:ClearFilledFields", { ids });
1231 getSectionByElementId(elementId) {
1232 for (const sections of this.sectionsByRootId.values()) {
1233 const section = sections.find(s =>
1234 s.getFieldDetailByElementId(elementId)
1243 static addMessageObserver(observer) {
1244 gMessageObservers.add(observer);
1247 static removeMessageObserver(observer) {
1248 gMessageObservers.delete(observer);
1251 notifyMessageObservers(callbackName, data) {
1252 for (let observer of gMessageObservers) {
1254 if (callbackName in observer) {
1255 observer[callbackName](
1257 this.manager.browsingContext.topChromeWindow
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.
1274 * Autofill Developer Tool API to inspect the autofill fields in this
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
1281 * @returns {Array<object>}
1282 * A list of sections representing the inspected result for this page.
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) {
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([]);
1304 fieldsByForm[index].push(field);
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(
1319 fieldDetails.forEach(field => {
1320 const overwriteField = overwriteFieldDetails.find(
1321 ow => ow.inspectId == field.inspectId
1323 if (overwriteField) {
1324 Object.assign(field, overwriteField);
1328 const formSections = FormAutofillParent.parseAndClassifyFields(
1330 { ignoreUnknownField: false }
1332 if (formSections.length) {
1333 allSections.push(formSections);
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] ?? [];
1347 * Autofill Developer Tools Related API:
1348 * Add test records for this tab.
1350 * @param {Array<object>} records
1351 * A list of address or credit card records
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]: [],
1361 for (const record of records) {
1362 const fields = Object.keys(record);
1363 if (!fields.length) {
1366 const collection = FormAutofillUtils.getCollectionNameFromFieldName(
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);