summaryrefslogtreecommitdiffstats
path: root/toolkit/components/formautofill
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-06-12 05:43:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-06-12 05:43:14 +0000
commit8dd16259287f58f9273002717ec4d27e97127719 (patch)
tree3863e62a53829a84037444beab3abd4ed9dfc7d0 /toolkit/components/formautofill
parentReleasing progress-linux version 126.0.1-1~progress7.99u1. (diff)
downloadfirefox-8dd16259287f58f9273002717ec4d27e97127719.tar.xz
firefox-8dd16259287f58f9273002717ec4d27e97127719.zip
Merging upstream version 127.0.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/formautofill')
-rw-r--r--toolkit/components/formautofill/AutofillProfileAutoComplete.sys.mjs334
-rw-r--r--toolkit/components/formautofill/FormAutofill.sys.mjs6
-rw-r--r--toolkit/components/formautofill/FormAutofillChild.sys.mjs217
-rw-r--r--toolkit/components/formautofill/FormAutofillParent.sys.mjs86
-rw-r--r--toolkit/components/formautofill/FormAutofillPreferences.sys.mjs65
-rw-r--r--toolkit/components/formautofill/Helpers.ios.mjs5
-rw-r--r--toolkit/components/formautofill/default/FormAutofillPrompter.sys.mjs30
-rw-r--r--toolkit/components/formautofill/shared/FormAutofillSection.sys.mjs1
-rw-r--r--toolkit/components/formautofill/shared/FormAutofillUtils.sys.mjs234
9 files changed, 539 insertions, 439 deletions
diff --git a/toolkit/components/formautofill/AutofillProfileAutoComplete.sys.mjs b/toolkit/components/formautofill/AutofillProfileAutoComplete.sys.mjs
index 7594fc8fcf..21282ff936 100644
--- a/toolkit/components/formautofill/AutofillProfileAutoComplete.sys.mjs
+++ b/toolkit/components/formautofill/AutofillProfileAutoComplete.sys.mjs
@@ -6,38 +6,21 @@
* Form Autofill content process module.
*/
-import {
- GenericAutocompleteItem,
- sendFillRequestToParent,
-} from "resource://gre/modules/FillHelpers.sys.mjs";
+import { sendFillRequestToParent } from "resource://gre/modules/FillHelpers.sys.mjs";
/* eslint-disable no-use-before-define */
-const Cm = Components.manager;
-
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
- AddressResult: "resource://autofill/ProfileAutoCompleteResult.sys.mjs",
- ComponentUtils: "resource://gre/modules/ComponentUtils.sys.mjs",
- CreditCardResult: "resource://autofill/ProfileAutoCompleteResult.sys.mjs",
FormAutofill: "resource://autofill/FormAutofill.sys.mjs",
- FormAutofillUtils: "resource://gre/modules/shared/FormAutofillUtils.sys.mjs",
FormAutofillContent: "resource://autofill/FormAutofillContent.sys.mjs",
- FormScenarios: "resource://gre/modules/FormScenarios.sys.mjs",
- InsecurePasswordUtils: "resource://gre/modules/InsecurePasswordUtils.sys.mjs",
});
const autocompleteController = Cc[
"@mozilla.org/autocomplete/controller;1"
].getService(Ci.nsIAutoCompleteController);
-ChromeUtils.defineLazyGetter(
- lazy,
- "FIELD_STATES",
- () => lazy.FormAutofillUtils.FIELD_STATES
-);
-
function getActorFromWindow(contentWindow, name = "FormAutofill") {
// In unit tests, contentWindow isn't a real window.
if (!contentWindow) {
@@ -49,257 +32,10 @@ function getActorFromWindow(contentWindow, name = "FormAutofill") {
: null;
}
-// Register/unregister a constructor as a factory.
-function AutocompleteFactory() {}
-AutocompleteFactory.prototype = {
- register(targetConstructor) {
- let proto = targetConstructor.prototype;
- this._classID = proto.classID;
-
- let factory =
- lazy.ComponentUtils.generateSingletonFactory(targetConstructor);
- this._factory = factory;
-
- let registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar);
- registrar.registerFactory(
- proto.classID,
- proto.classDescription,
- proto.contractID,
- factory
- );
-
- if (proto.classID2) {
- this._classID2 = proto.classID2;
- registrar.registerFactory(
- proto.classID2,
- proto.classDescription,
- proto.contractID2,
- factory
- );
- }
- },
-
- unregister() {
- let registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar);
- registrar.unregisterFactory(this._classID, this._factory);
- if (this._classID2) {
- registrar.unregisterFactory(this._classID2, this._factory);
- }
- this._factory = null;
- },
-};
-
-/**
- * @class
- *
- * @implements {nsIAutoCompleteSearch}
- */
-function AutofillProfileAutoCompleteSearch() {
- this.log = lazy.FormAutofill.defineLogGetter(
- this,
- "AutofillProfileAutoCompleteSearch"
- );
-}
-AutofillProfileAutoCompleteSearch.prototype = {
- classID: Components.ID("4f9f1e4c-7f2c-439e-9c9e-566b68bc187d"),
- contractID: "@mozilla.org/autocomplete/search;1?name=autofill-profiles",
- classDescription: "AutofillProfileAutoCompleteSearch",
- QueryInterface: ChromeUtils.generateQI(["nsIAutoCompleteSearch"]),
-
- // Begin nsIAutoCompleteSearch implementation
-
- /**
- * Searches for a given string and notifies a listener (either synchronously
- * or asynchronously) of the result
- *
- * @param {string} searchString the string to search for
- * @param {string} searchParam
- * @param {object} previousResult a previous result to use for faster searchinig
- * @param {object} listener the listener to notify when the search is complete
- */
- startSearch(searchString, searchParam, previousResult, listener) {
- let {
- activeInput,
- activeSection,
- activeFieldDetail,
- activeHandler,
- savedFieldNames,
- } = lazy.FormAutofillContent;
- this.forceStop = false;
-
- let isAddressField = lazy.FormAutofillUtils.isAddressField(
- activeFieldDetail.fieldName
- );
- const isCreditCardField = lazy.FormAutofillUtils.isCreditCardField(
- activeFieldDetail.fieldName
- );
- let isInputAutofilled =
- activeHandler.getFilledStateByElement(activeInput) ==
- lazy.FIELD_STATES.AUTO_FILLED;
- let allFieldNames = activeSection.allFieldNames;
- let filledRecordGUID = activeSection.filledRecordGUID;
-
- let searchPermitted = isAddressField
- ? lazy.FormAutofill.isAutofillAddressesEnabled
- : lazy.FormAutofill.isAutofillCreditCardsEnabled;
- let AutocompleteResult = isAddressField
- ? lazy.AddressResult
- : lazy.CreditCardResult;
- let isFormAutofillSearch = true;
- let pendingSearchResult = null;
-
- ProfileAutocomplete.lastProfileAutoCompleteFocusedInput = activeInput;
- // Fallback to form-history if ...
- // - specified autofill feature is pref off.
- // - no profile can fill the currently-focused input.
- // - the current form has already been populated and the field is not
- // an empty credit card field.
- // - (address only) less than 3 inputs are covered by all saved fields in the storage.
- if (
- !searchPermitted ||
- !savedFieldNames.has(activeFieldDetail.fieldName) ||
- (!isInputAutofilled &&
- filledRecordGUID &&
- !(isCreditCardField && activeInput.value === "")) ||
- (isAddressField &&
- allFieldNames.filter(field => savedFieldNames.has(field)).length <
- lazy.FormAutofillUtils.AUTOFILL_FIELDS_THRESHOLD)
- ) {
- isFormAutofillSearch = false;
- if (activeInput.autocomplete == "off") {
- // Create a dummy result as an empty search result.
- pendingSearchResult = new AutocompleteResult("", "", [], [], {});
- } else {
- pendingSearchResult = new Promise(resolve => {
- let formHistory = Cc[
- "@mozilla.org/autocomplete/search;1?name=form-history"
- ].createInstance(Ci.nsIAutoCompleteSearch);
- formHistory.startSearch(searchString, searchParam, previousResult, {
- onSearchResult: (_, result) => resolve(result),
- });
- });
- }
- } else if (isInputAutofilled) {
- pendingSearchResult = new AutocompleteResult(searchString, "", [], [], {
- isInputAutofilled,
- });
- } else {
- const data = {
- fieldName: activeFieldDetail.fieldName,
- searchString,
- };
-
- pendingSearchResult = this._getRecords(activeInput, data).then(
- ({ records, externalEntries }) => {
- if (this.forceStop) {
- return null;
- }
- // Sort addresses by timeLastUsed for showing the lastest used address at top.
- records.sort((a, b) => b.timeLastUsed - a.timeLastUsed);
-
- let adaptedRecords = activeSection.getAdaptedProfiles(records);
- let handler = lazy.FormAutofillContent.activeHandler;
- let isSecure = lazy.InsecurePasswordUtils.isFormSecure(handler.form);
-
- const result = new AutocompleteResult(
- searchString,
- activeFieldDetail.fieldName,
- allFieldNames,
- adaptedRecords,
- { isSecure, isInputAutofilled }
- );
-
- result.externalEntries.push(
- ...externalEntries.map(
- entry =>
- new GenericAutocompleteItem(
- entry.image,
- entry.title,
- entry.subtitle,
- entry.fillMessageName,
- entry.fillMessageData
- )
- )
- );
-
- return result;
- }
- );
- }
-
- Promise.resolve(pendingSearchResult).then(result => {
- if (this.forceStop) {
- // If we notify the listener the search result when the search is already
- // cancelled, it corrupts the internal state of the listener. So we only
- // reset the controller's state in this case.
- if (isFormAutofillSearch) {
- autocompleteController.resetInternalState();
- }
- return;
- }
-
- listener.onSearchResult(this, result);
- // Don't save cache results or reset state when returning non-autofill results such as the
- // form history fallback above.
- if (isFormAutofillSearch) {
- ProfileAutocomplete.lastProfileAutoCompleteResult = result;
- // Reset AutoCompleteController's state at the end of startSearch to ensure that
- // none of form autofill result will be cached in other places and make the
- // result out of sync.
- autocompleteController.resetInternalState();
- } else {
- // Clear the cache so that we don't try to autofill from it after falling
- // back to form history.
- ProfileAutocomplete.lastProfileAutoCompleteResult = null;
- }
- });
- },
-
- /**
- * Stops an asynchronous search that is in progress
- */
- stopSearch() {
- ProfileAutocomplete.lastProfileAutoCompleteResult = null;
- this.forceStop = true;
- },
-
- /**
- * Get the records from parent process for AutoComplete result.
- *
- * @private
- * @param {object} input
- * Input element for autocomplete.
- * @param {object} data
- * Parameters for querying the corresponding result.
- * @param {string} data.searchString
- * The typed string for filtering out the matched records.
- * @param {string} data.fieldName
- * The identified field name for the input
- * @returns {Promise}
- * Promise that resolves when addresses returned from parent process.
- */
- _getRecords(input, data) {
- if (!input) {
- return [];
- }
-
- let actor = getActorFromWindow(input.ownerGlobal);
- return actor.sendQuery("FormAutofill:GetRecords", {
- scenarioName: lazy.FormScenarios.detect({ input }).signUpForm
- ? "SignUpFormScenario"
- : "",
- ...data,
- });
- },
-};
-
export const ProfileAutocomplete = {
QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),
- lastProfileAutoCompleteResult: null,
- lastProfileAutoCompleteFocusedInput: null,
_registered: false,
- _factory: null,
ensureRegistered() {
if (this._registered) {
@@ -308,8 +44,6 @@ export const ProfileAutocomplete = {
this.log = lazy.FormAutofill.defineLogGetter(this, "ProfileAutocomplete");
this.debug("ensureRegistered");
- this._factory = new AutocompleteFactory();
- this._factory.register(AutofillProfileAutoCompleteSearch);
this._registered = true;
Services.obs.addObserver(this, "autocomplete-will-enter-text");
@@ -326,10 +60,7 @@ export const ProfileAutocomplete = {
}
this.debug("ensureUnregistered");
- this._factory.unregister();
- this._factory = null;
this._registered = false;
- this._lastAutoCompleteResult = null;
Services.obs.removeObserver(this, "autocomplete-will-enter-text");
},
@@ -341,27 +72,14 @@ export const ProfileAutocomplete = {
// The observer notification is for autocomplete in a different process.
break;
}
- lazy.FormAutofillContent.autofillPending = true;
- Services.obs.notifyObservers(null, "autofill-fill-starting");
await this._fillFromAutocompleteRow(
lazy.FormAutofillContent.activeInput
);
- Services.obs.notifyObservers(null, "autofill-fill-complete");
- lazy.FormAutofillContent.autofillPending = false;
break;
}
}
},
- _getSelectedIndex(contentWindow) {
- let actor = getActorFromWindow(contentWindow, "AutoComplete");
- if (!actor) {
- throw new Error("Invalid autocomplete selectedIndex");
- }
-
- return actor.selectedIndex;
- },
-
async _fillFromAutocompleteRow(focusedInput) {
this.debug("_fillFromAutocompleteRow:", focusedInput);
let formDetails = lazy.FormAutofillContent.activeFormDetails;
@@ -369,20 +87,24 @@ export const ProfileAutocomplete = {
// The observer notification is for a different frame.
return;
}
+ const actor = getActorFromWindow(focusedInput.ownerGlobal, "AutoComplete");
+ if (!actor) {
+ throw new Error("Invalid autocomplete selectedIndex");
+ }
- let selectedIndex = this._getSelectedIndex(focusedInput.ownerGlobal);
+ const selectedIndex = actor.selectedIndex;
+ const lastProfileAutoCompleteResult = actor.lastProfileAutoCompleteResult;
const validIndex =
selectedIndex >= 0 &&
- selectedIndex < this.lastProfileAutoCompleteResult?.matchCount;
+ selectedIndex < lastProfileAutoCompleteResult?.matchCount;
const comment = validIndex
- ? this.lastProfileAutoCompleteResult.getCommentAt(selectedIndex)
+ ? lastProfileAutoCompleteResult.getCommentAt(selectedIndex)
: null;
let profile = JSON.parse(comment);
if (
selectedIndex == -1 ||
- !this.lastProfileAutoCompleteResult ||
- this.lastProfileAutoCompleteResult.getStyleAt(selectedIndex) != "autofill"
+ lastProfileAutoCompleteResult?.getStyleAt(selectedIndex) != "autofill"
) {
if (
focusedInput &&
@@ -400,42 +122,6 @@ export const ProfileAutocomplete = {
);
}
}
- return;
- }
-
- await lazy.FormAutofillContent.activeHandler.autofillFormFields(profile);
- },
-
- _clearProfilePreview() {
- if (
- !this.lastProfileAutoCompleteFocusedInput ||
- !lazy.FormAutofillContent.activeSection
- ) {
- return;
- }
-
- lazy.FormAutofillContent.activeSection.clearPreviewedFormFields();
- },
-
- _previewSelectedProfile(selectedIndex) {
- if (
- !lazy.FormAutofillContent.activeInput ||
- !lazy.FormAutofillContent.activeFormDetails
- ) {
- // The observer notification is for a different process/frame.
- return;
- }
-
- if (
- !this.lastProfileAutoCompleteResult ||
- this.lastProfileAutoCompleteResult.getStyleAt(selectedIndex) != "autofill"
- ) {
- return;
}
-
- let profile = JSON.parse(
- this.lastProfileAutoCompleteResult.getCommentAt(selectedIndex)
- );
- lazy.FormAutofillContent.activeSection.previewFormFields(profile);
},
};
diff --git a/toolkit/components/formautofill/FormAutofill.sys.mjs b/toolkit/components/formautofill/FormAutofill.sys.mjs
index 8f50aad7bd..08b6acaee1 100644
--- a/toolkit/components/formautofill/FormAutofill.sys.mjs
+++ b/toolkit/components/formautofill/FormAutofill.sys.mjs
@@ -24,8 +24,8 @@ const ENABLED_AUTOFILL_ADDRESSES_SUPPORTED_COUNTRIES_PREF =
"extensions.formautofill.addresses.supportedCountries";
const ENABLED_AUTOFILL_CREDITCARDS_PREF =
"extensions.formautofill.creditCards.enabled";
-const ENABLED_AUTOFILL_CREDITCARDS_REAUTH_PREF =
- "extensions.formautofill.reauth.enabled";
+const AUTOFILL_CREDITCARDS_REAUTH_PREF =
+ "extensions.formautofill.creditCards.reauth.optout";
const AUTOFILL_CREDITCARDS_HIDE_UI_PREF =
"extensions.formautofill.creditCards.hideui";
const FORM_AUTOFILL_SUPPORT_RTL_PREF = "extensions.formautofill.supportRTL";
@@ -44,7 +44,7 @@ export const FormAutofill = {
ENABLED_AUTOFILL_CAPTURE_ON_FORM_REMOVAL_PREF,
ENABLED_AUTOFILL_CAPTURE_ON_PAGE_NAVIGATION_PREF,
ENABLED_AUTOFILL_CREDITCARDS_PREF,
- ENABLED_AUTOFILL_CREDITCARDS_REAUTH_PREF,
+ AUTOFILL_CREDITCARDS_REAUTH_PREF,
AUTOFILL_CREDITCARDS_AUTOCOMPLETE_OFF_PREF,
AUTOFILL_ADDRESSES_AUTOCOMPLETE_OFF_PREF,
diff --git a/toolkit/components/formautofill/FormAutofillChild.sys.mjs b/toolkit/components/formautofill/FormAutofillChild.sys.mjs
index af84459432..f45c962d2b 100644
--- a/toolkit/components/formautofill/FormAutofillChild.sys.mjs
+++ b/toolkit/components/formautofill/FormAutofillChild.sys.mjs
@@ -8,15 +8,18 @@ import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
+ AddressResult: "resource://autofill/ProfileAutoCompleteResult.sys.mjs",
AutoCompleteChild: "resource://gre/actors/AutoCompleteChild.sys.mjs",
AutofillTelemetry: "resource://gre/modules/shared/AutofillTelemetry.sys.mjs",
+ CreditCardResult: "resource://autofill/ProfileAutoCompleteResult.sys.mjs",
+ GenericAutocompleteItem: "resource://gre/modules/FillHelpers.sys.mjs",
+ InsecurePasswordUtils: "resource://gre/modules/InsecurePasswordUtils.sys.mjs",
FormAutofill: "resource://autofill/FormAutofill.sys.mjs",
FormAutofillContent: "resource://autofill/FormAutofillContent.sys.mjs",
FormAutofillUtils: "resource://gre/modules/shared/FormAutofillUtils.sys.mjs",
+ FormScenarios: "resource://gre/modules/FormScenarios.sys.mjs",
FormStateManager: "resource://gre/modules/shared/FormStateManager.sys.mjs",
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
- ProfileAutocomplete:
- "resource://autofill/AutofillProfileAutoComplete.sys.mjs",
setTimeout: "resource://gre/modules/Timer.sys.mjs",
FORM_SUBMISSION_REASON: "resource://gre/actors/FormHandlerChild.sys.mjs",
});
@@ -94,6 +97,9 @@ const observer = {
* Handles content's interactions for the frame.
*/
export class FormAutofillChild extends JSWindowActorChild {
+ // Flag indicating whether the form is waiting to be filled by Autofill.
+ #autofillPending = false;
+
constructor() {
super();
@@ -104,9 +110,6 @@ export class FormAutofillChild extends JSWindowActorChild {
this._hasDOMContentLoadedHandler = false;
this._hasPendingTask = false;
- // Flag indicating whether the form is waiting to be filled by Autofill.
- this._autofillPending = false;
-
/**
* @type {FormAutofillFieldDetailsManager} handling state management of current forms and handlers.
*/
@@ -473,14 +476,14 @@ export class FormAutofillChild extends JSWindowActorChild {
this.unregisterProgressListener();
}
- receiveMessage(message) {
+ async receiveMessage(message) {
if (!lazy.FormAutofill.isAutofillEnabled) {
return;
}
switch (message.name) {
case "FormAutofill:PreviewProfile": {
- this.previewProfile(message.data.selectedIndex);
+ this.previewProfile(message.data);
break;
}
case "FormAutofill:ClearForm": {
@@ -488,7 +491,7 @@ export class FormAutofillChild extends JSWindowActorChild {
break;
}
case "FormAutofill:FillForm": {
- this.activeHandler.autofillFormFields(message.data);
+ await this.autofillFields(message.data);
break;
}
}
@@ -610,7 +613,7 @@ export class FormAutofillChild extends JSWindowActorChild {
this.debug("updateActiveElement: checking for popup-on-focus");
// We know this element just received focus. If it's a credit card field,
// open its popup.
- if (this._autofillPending) {
+ if (this.#autofillPending) {
this.debug("updateActiveElement: skipping check; autofill is imminent");
} else if (element.value?.length !== 0) {
this.debug(
@@ -631,16 +634,8 @@ export class FormAutofillChild extends JSWindowActorChild {
}
}
- set autofillPending(flag) {
- this.debug("Setting autofillPending to", flag);
- this._autofillPending = flag;
- }
-
clearForm() {
- let focusedInput =
- this.activeInput ||
- lazy.ProfileAutocomplete._lastAutoCompleteFocusedInput;
- if (!focusedInput) {
+ if (!this.activeSection) {
return;
}
@@ -656,26 +651,41 @@ export class FormAutofillChild extends JSWindowActorChild {
}
}
- previewProfile(selectedIndex) {
- let lastAutoCompleteResult =
- lazy.ProfileAutocomplete.lastProfileAutoCompleteResult;
- let focusedInput = this.activeInput;
+ get lastProfileAutoCompleteResult() {
+ return this.manager.getActor("AutoComplete")?.lastProfileAutoCompleteResult;
+ }
- if (
- selectedIndex === -1 ||
- !focusedInput ||
- !lastAutoCompleteResult ||
- lastAutoCompleteResult.getStyleAt(selectedIndex) != "autofill"
- ) {
- lazy.ProfileAutocomplete._clearProfilePreview();
+ get lastProfileAutoCompleteFocusedInput() {
+ return this.manager.getActor("AutoComplete")
+ ?.lastProfileAutoCompleteFocusedInput;
+ }
+
+ previewProfile(profile) {
+ if (profile && this.activeSection) {
+ const adaptedProfile = this.activeSection.getAdaptedProfiles([
+ profile,
+ ])[0];
+ this.activeSection.previewFormFields(adaptedProfile);
} else {
- lazy.ProfileAutocomplete._previewSelectedProfile(selectedIndex);
+ this.activeSection.clearPreviewedFormFields();
+ }
+ }
+
+ async autofillFields(profile) {
+ this.#autofillPending = true;
+ Services.obs.notifyObservers(null, "autofill-fill-starting");
+ try {
+ Services.obs.notifyObservers(null, "autofill-fill-starting");
+ await this.activeHandler.autofillFormFields(profile);
+ Services.obs.notifyObservers(null, "autofill-fill-complete");
+ } finally {
+ this.#autofillPending = false;
}
}
onPopupClosed() {
this.debug("Popup has closed.");
- lazy.ProfileAutocomplete._clearProfilePreview();
+ this.activeSection?.clearPreviewedFormFields();
}
onPopupOpened() {
@@ -701,6 +711,149 @@ export class FormAutofillChild extends JSWindowActorChild {
return;
}
- formFillController.markAsAutofillField(field);
+ this.manager
+ .getActor("AutoComplete")
+ ?.markAsAutoCompletableField(field, this);
+ }
+
+ get actorName() {
+ return "FormAutofill";
+ }
+
+ /**
+ * Get the search options when searching for autocomplete entries in the parent
+ *
+ * @param {HTMLInputElement} input - The input element to search for autocompelte entries
+ * @returns {object} the search options for the input
+ */
+ getAutoCompleteSearchOption(input) {
+ const fieldDetail = this._fieldDetailsManager
+ ._getFormHandler(input)
+ ?.getFieldDetailByElement(input);
+
+ const scenarioName = lazy.FormScenarios.detect({ input }).signUpForm
+ ? "SignUpFormScenario"
+ : "";
+ return { fieldName: fieldDetail?.fieldName, scenarioName };
+ }
+
+ /**
+ * Ask the provider whether it might have autocomplete entry to show
+ * for the given input.
+ *
+ * @param {HTMLInputElement} input - The input element to search for autocompelte entries
+ * @returns {boolean} true if we shold search for autocomplete entries
+ */
+ shouldSearchForAutoComplete(input) {
+ const fieldDetail = this._fieldDetailsManager
+ ._getFormHandler(input)
+ ?.getFieldDetailByElement(input);
+ if (!fieldDetail) {
+ return false;
+ }
+ const fieldName = fieldDetail.fieldName;
+ const isAddressField = lazy.FormAutofillUtils.isAddressField(fieldName);
+ const searchPermitted = isAddressField
+ ? lazy.FormAutofill.isAutofillAddressesEnabled
+ : lazy.FormAutofill.isAutofillCreditCardsEnabled;
+ // If the specified autofill feature is pref off, do not search
+ if (!searchPermitted) {
+ return false;
+ }
+
+ // No profile can fill the currently-focused input.
+ if (!lazy.FormAutofillContent.savedFieldNames.has(fieldName)) {
+ return false;
+ }
+
+ // The current form has already been populated and the field is not
+ // an empty credit card field.
+ const isCreditCardField =
+ lazy.FormAutofillUtils.isCreditCardField(fieldName);
+ const isInputAutofilled =
+ this.activeHandler.getFilledStateByElement(input) ==
+ lazy.FormAutofillUtils.FIELD_STATES.AUTO_FILLED;
+ const filledRecordGUID = this.activeSection.filledRecordGUID;
+ if (
+ !isInputAutofilled &&
+ filledRecordGUID &&
+ !(isCreditCardField && this.activeInput.value === "")
+ ) {
+ return false;
+ }
+
+ // (address only) less than 3 inputs are covered by all saved fields in the storage.
+ if (
+ isAddressField &&
+ this.activeSection.allFieldNames.filter(field =>
+ lazy.FormAutofillContent.savedFieldNames.has(field)
+ ).length < lazy.FormAutofillUtils.AUTOFILL_FIELDS_THRESHOLD
+ ) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Convert the search result to autocomplete results
+ *
+ * @param {string} searchString - The string to search for
+ * @param {HTMLInputElement} input - The input element to search for autocompelte entries
+ * @param {Array<object>} records - autocomplete records
+ * @returns {AutocompleteResult}
+ */
+ searchResultToAutoCompleteResult(searchString, input, records) {
+ if (!records) {
+ return null;
+ }
+
+ const entries = records.records;
+ const externalEntries = records.externalEntries;
+
+ const fieldDetail = this._fieldDetailsManager
+ ._getFormHandler(input)
+ ?.getFieldDetailByElement(input);
+ if (!fieldDetail) {
+ return null;
+ }
+
+ const adaptedRecords = this.activeSection.getAdaptedProfiles(entries);
+ const isSecure = lazy.InsecurePasswordUtils.isFormSecure(
+ this.activeHandler.form
+ );
+ const isInputAutofilled =
+ this.activeHandler.getFilledStateByElement(input) ==
+ lazy.FormAutofillUtils.FIELD_STATES.AUTO_FILLED;
+ const allFieldNames = this.activeSection.allFieldNames;
+
+ const AutocompleteResult = lazy.FormAutofillUtils.isAddressField(
+ fieldDetail.fieldName
+ )
+ ? lazy.AddressResult
+ : lazy.CreditCardResult;
+
+ const acResult = new AutocompleteResult(
+ searchString,
+ fieldDetail.fieldName,
+ allFieldNames,
+ adaptedRecords,
+ { isSecure, isInputAutofilled }
+ );
+
+ acResult.externalEntries.push(
+ ...externalEntries.map(
+ entry =>
+ new lazy.GenericAutocompleteItem(
+ entry.image,
+ entry.title,
+ entry.subtitle,
+ entry.fillMessageName,
+ entry.fillMessageData
+ )
+ )
+ );
+
+ return acResult;
}
}
diff --git a/toolkit/components/formautofill/FormAutofillParent.sys.mjs b/toolkit/components/formautofill/FormAutofillParent.sys.mjs
index 34dac8ce15..026801c83f 100644
--- a/toolkit/components/formautofill/FormAutofillParent.sys.mjs
+++ b/toolkit/components/formautofill/FormAutofillParent.sys.mjs
@@ -48,8 +48,11 @@ ChromeUtils.defineLazyGetter(lazy, "log", () =>
FormAutofill.defineLogGetter(lazy, "FormAutofillParent")
);
-const { ENABLED_AUTOFILL_ADDRESSES_PREF, ENABLED_AUTOFILL_CREDITCARDS_PREF } =
- FormAutofill;
+const {
+ ENABLED_AUTOFILL_ADDRESSES_PREF,
+ ENABLED_AUTOFILL_CREDITCARDS_PREF,
+ AUTOFILL_CREDITCARDS_REAUTH_PREF,
+} = FormAutofill;
const { ADDRESSES_COLLECTION_NAME, CREDITCARDS_COLLECTION_NAME } =
FormAutofillUtils;
@@ -267,17 +270,8 @@ export class FormAutofillParent extends JSWindowActorParent {
break;
}
case "FormAutofill:GetRecords": {
- const relayPromise = lazy.FirefoxRelay.autocompleteItemsAsync({
- formOrigin: this.formOrigin,
- scenarioName: data.scenarioName,
- hasInput: !!data.searchString?.length,
- });
- const recordsPromise = FormAutofillParent.getRecords(data);
- const [records, externalEntries] = await Promise.all([
- recordsPromise,
- relayPromise,
- ]);
- return { records, externalEntries };
+ const records = await FormAutofillParent.getRecords(data);
+ return { records };
}
case "FormAutofill:OnFormSubmit": {
this.notifyMessageObservers("onFormSubmitted", data);
@@ -291,7 +285,9 @@ export class FormAutofillParent extends JSWindowActorParent {
}
case "FormAutofill:GetDecryptedString": {
let { cipherText, reauth } = data;
- if (!FormAutofillUtils._reauthEnabledByUser) {
+ if (
+ !FormAutofillUtils.getOSAuthEnabled(AUTOFILL_CREDITCARDS_REAUTH_PREF)
+ ) {
lazy.log.debug("Reauth is disabled");
reauth = false;
}
@@ -327,7 +323,9 @@ export class FormAutofillParent extends JSWindowActorParent {
break;
}
case "FormAutofill:SaveCreditCard": {
- if (!(await FormAutofillUtils.ensureLoggedIn()).authenticated) {
+ // 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;
}
@@ -402,6 +400,43 @@ export class FormAutofillParent extends JSWindowActorParent {
}
/**
+ * Retrieves autocomplete entries for a given search string and data context.
+ *
+ * @param {string} searchString
+ * The search string used to filter autocomplete entries.
+ * @param {object} options
+ * @param {string} options.fieldName
+ * The name of the field for which autocomplete entries are being fetched.
+ * @param {string} options.scenarioName
+ * The scenario name used in the autocomplete operation to fetch external entries.
+ * @returns {Promise<object>} A promise that resolves to an object containing two properties: `records` and `externalEntries`.
+ * `records` is an array of autofill records from the form's internal data, sorted by `timeLastUsed`.
+ * `externalEntries` is an array of external autocomplete items fetched based on the scenario.
+ */
+ async searchAutoCompleteEntries(searchString, options) {
+ const { fieldName, scenarioName } = options;
+ const relayPromise = lazy.FirefoxRelay.autocompleteItemsAsync({
+ formOrigin: this.formOrigin,
+ scenarioName,
+ hasInput: !!searchString?.length,
+ });
+
+ const recordsPromise = FormAutofillParent.getRecords({
+ searchString,
+ fieldName,
+ });
+ const [records, externalEntries] = await Promise.all([
+ recordsPromise,
+ relayPromise,
+ ]);
+
+ // Sort addresses by timeLastUsed for showing the lastest used address at top.
+ records.sort((a, b) => b.timeLastUsed - a.timeLastUsed);
+
+ return { records, externalEntries };
+ }
+
+ /**
* Get the records from profile store and return results back to content
* process. It will decrypt the credit card number and append
* "cc-number-decrypted" to each record if OSKeyStore isn't set.
@@ -668,4 +703,25 @@ export class FormAutofillParent extends JSWindowActorParent {
return true;
}
+
+ previewFields(result) {
+ try {
+ const profile =
+ result.style == "autofill" ? JSON.parse(result.comment) : null;
+ this.sendAsyncMessage("FormAutofill:PreviewProfile", profile);
+ } catch (e) {
+ lazy.log.debug("Fail to get preview profile: ", e.message);
+ }
+ }
+
+ autofillFields(result) {
+ if (result.style == "autofill") {
+ try {
+ const profile = JSON.parse(result.comment);
+ this.sendAsyncMessage("FormAutofill:FillForm", profile);
+ } catch (e) {
+ lazy.log.debug("Fail to get autofill profile.");
+ }
+ }
+ }
}
diff --git a/toolkit/components/formautofill/FormAutofillPreferences.sys.mjs b/toolkit/components/formautofill/FormAutofillPreferences.sys.mjs
index 18937371b9..40438a128e 100644
--- a/toolkit/components/formautofill/FormAutofillPreferences.sys.mjs
+++ b/toolkit/components/formautofill/FormAutofillPreferences.sys.mjs
@@ -16,7 +16,6 @@ const MANAGE_CREDITCARDS_URL =
import { FormAutofill } from "resource://autofill/FormAutofill.sys.mjs";
import { FormAutofillUtils } from "resource://gre/modules/shared/FormAutofillUtils.sys.mjs";
-import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
@@ -25,13 +24,17 @@ ChromeUtils.defineESModuleGetters(lazy, {
ChromeUtils.defineLazyGetter(
lazy,
"l10n",
- () => new Localization(["browser/preferences/preferences.ftl"], true)
+ () =>
+ new Localization(
+ ["branding/brand.ftl", "browser/preferences/preferences.ftl"],
+ true
+ )
);
const {
ENABLED_AUTOFILL_ADDRESSES_PREF,
ENABLED_AUTOFILL_CREDITCARDS_PREF,
- ENABLED_AUTOFILL_CREDITCARDS_REAUTH_PREF,
+ AUTOFILL_CREDITCARDS_REAUTH_PREF,
} = FormAutofill;
const {
MANAGE_ADDRESSES_L10N_IDS,
@@ -267,7 +270,13 @@ FormAutofillPreferences.prototype = {
reauthCheckbox.setAttribute(
"label",
- lazy.l10n.formatValueSync("autofill-reauth-checkbox")
+ lazy.l10n.formatValueSync("autofill-reauth-payment-methods-checkbox")
+ );
+
+ // If target.checked is checked, enable OSAuth. Otherwise, reset the pref value.
+ reauthCheckbox.setAttribute(
+ "checked",
+ FormAutofillUtils.getOSAuthEnabled(AUTOFILL_CREDITCARDS_REAUTH_PREF)
);
reauthLearnMore.setAttribute(
@@ -275,11 +284,6 @@ FormAutofillPreferences.prototype = {
"credit-card-autofill#w_require-authentication-for-autofill"
);
- // Manually set the checked state
- if (FormAutofillUtils._reauthEnabledByUser) {
- reauthCheckbox.setAttribute("checked", true);
- }
-
reauthCheckboxGroup.setAttribute("align", "center");
reauthCheckboxGroup.setAttribute("flex", "1");
@@ -321,36 +325,31 @@ FormAutofillPreferences.prototype = {
break;
}
- let messageTextId = "autofillReauthOSDialog";
- // We reuse the if/else order from wizard markup to increase
- // odds of consistent behavior.
- if (AppConstants.platform == "macosx") {
- messageTextId += "Mac";
- } else if (AppConstants.platform == "linux") {
- messageTextId += "Lin";
- } else {
- messageTextId += "Win";
- }
-
- let messageText = this.bundle.GetStringFromName(messageTextId);
-
- const brandBundle = Services.strings.createBundle(
- "chrome://branding/locale/brand.properties"
+ let messageText = await lazy.l10n.formatValueSync(
+ "autofill-creditcard-os-dialog-message"
);
- let win = target.ownerGlobal.docShell.chromeEventHandler.ownerGlobal;
- let loggedIn = await lazy.OSKeyStore.ensureLoggedIn(
- messageText,
- brandBundle.GetStringFromName("brandFullName"),
- win,
- false
+ let captionText = await lazy.l10n.formatValueSync(
+ "autofill-creditcard-os-auth-dialog-caption"
);
- if (!loggedIn.authenticated) {
+ let win = target.ownerGlobal.docShell.chromeEventHandler.ownerGlobal;
+ // Calling OSKeyStore.ensureLoggedIn() instead of FormAutofillUtils.verifyOSAuth()
+ // since we want to authenticate user each time this stting is changed.
+ let isAuthorized = (
+ await lazy.OSKeyStore.ensureLoggedIn(
+ messageText,
+ captionText,
+ win,
+ false
+ )
+ ).authenticated;
+ if (!isAuthorized) {
target.checked = !target.checked;
break;
}
- Services.prefs.setBoolPref(
- ENABLED_AUTOFILL_CREDITCARDS_REAUTH_PREF,
+ // If target.checked is checked, enable OSAuth. Otherwise, reset the pref value.
+ FormAutofillUtils.setOSAuthEnabled(
+ AUTOFILL_CREDITCARDS_REAUTH_PREF,
target.checked
);
} else if (target == this.refs.savedAddressesBtn) {
diff --git a/toolkit/components/formautofill/Helpers.ios.mjs b/toolkit/components/formautofill/Helpers.ios.mjs
index 83137331f1..0b83f84c2e 100644
--- a/toolkit/components/formautofill/Helpers.ios.mjs
+++ b/toolkit/components/formautofill/Helpers.ios.mjs
@@ -111,6 +111,11 @@ export const XPCOMUtils = withNotImplementedError({
defineLazyModuleGetters(obj, modules) {
internalModuleResolvers.resolveModules(obj, modules);
},
+ defineLazyServiceGetter() {
+ // Don't do anything
+ // We need this for OS Auth fixes for formautofill.
+ // TODO(issam, Bug 1894967): Move os auth to separate module and remove this.
+ },
});
// eslint-disable-next-line no-shadow
diff --git a/toolkit/components/formautofill/default/FormAutofillPrompter.sys.mjs b/toolkit/components/formautofill/default/FormAutofillPrompter.sys.mjs
index 05dcf5bace..2a72f302fe 100644
--- a/toolkit/components/formautofill/default/FormAutofillPrompter.sys.mjs
+++ b/toolkit/components/formautofill/default/FormAutofillPrompter.sys.mjs
@@ -19,6 +19,7 @@ const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
CreditCard: "resource://gre/modules/CreditCard.sys.mjs",
formAutofillStorage: "resource://autofill/FormAutofillStorage.sys.mjs",
+ OSKeyStore: "resource://gre/modules/OSKeyStore.sys.mjs",
});
ChromeUtils.defineLazyGetter(lazy, "log", () =>
@@ -475,9 +476,29 @@ export class AddressSaveDoorhanger extends AutofillDoorhanger {
];
break;
case "address":
- data = ["address-level2", "address-level1", "postal-code"].map(
- field => [field, this.oldRecord[field], this.newRecord[field]]
- );
+ data = [
+ [
+ "address-level2",
+ this.oldRecord["address-level2"],
+ this.newRecord["address-level2"],
+ ],
+ [
+ "address-level1",
+ FormAutofillUtils.getAbbreviatedSubregionName(
+ this.oldRecord["address-level1"],
+ this.oldRecord.country
+ ) || this.oldRecord["address-level1"],
+ FormAutofillUtils.getAbbreviatedSubregionName(
+ this.newRecord["address-level1"],
+ this.newRecord.country
+ ) || this.newRecord["address-level1"],
+ ],
+ [
+ "postal-code",
+ this.oldRecord["postal-code"],
+ this.newRecord["postal-code"],
+ ],
+ ];
break;
case "name":
case "country":
@@ -1301,7 +1322,7 @@ export let FormAutofillPrompter = {
return;
}
- if (!(await FormAutofillUtils.ensureLoggedIn()).authenticated) {
+ if (!(await lazy.OSKeyStore.ensureLoggedIn(false)).authenticated) {
lazy.log.warn("User canceled encryption login");
return;
}
@@ -1338,7 +1359,6 @@ export let FormAutofillPrompter = {
);
const { ownerGlobal: win } = browser;
- await win.ensureCustomElements("moz-support-link");
win.MozXULElement.insertFTLIfNeeded(
"toolkit/formautofill/formAutofill.ftl"
);
diff --git a/toolkit/components/formautofill/shared/FormAutofillSection.sys.mjs b/toolkit/components/formautofill/shared/FormAutofillSection.sys.mjs
index 1a5b3014c9..aa4f795521 100644
--- a/toolkit/components/formautofill/shared/FormAutofillSection.sys.mjs
+++ b/toolkit/components/formautofill/shared/FormAutofillSection.sys.mjs
@@ -413,6 +413,7 @@ export class FormAutofillSection {
profile[`${fieldDetail.fieldName}-formatted`] ||
profile[fieldDetail.fieldName] ||
"";
+
if (HTMLSelectElement.isInstance(element)) {
// Unlike text input, select element is always previewed even if
// the option is already selected.
diff --git a/toolkit/components/formautofill/shared/FormAutofillUtils.sys.mjs b/toolkit/components/formautofill/shared/FormAutofillUtils.sys.mjs
index c2b48a53a3..b09f611db1 100644
--- a/toolkit/components/formautofill/shared/FormAutofillUtils.sys.mjs
+++ b/toolkit/components/formautofill/shared/FormAutofillUtils.sys.mjs
@@ -26,10 +26,19 @@ ChromeUtils.defineLazyGetter(
)
);
+XPCOMUtils.defineLazyServiceGetter(
+ lazy,
+ "Crypto",
+ "@mozilla.org/login-manager/crypto/SDR;1",
+ "nsILoginManagerCrypto"
+);
+
export let FormAutofillUtils;
const ADDRESSES_COLLECTION_NAME = "addresses";
const CREDITCARDS_COLLECTION_NAME = "creditCards";
+const AUTOFILL_CREDITCARDS_REAUTH_PREF =
+ FormAutofill.AUTOFILL_CREDITCARDS_REAUTH_PREF;
const MANAGE_ADDRESSES_L10N_IDS = [
"autofill-add-address-title",
"autofill-manage-addresses-title",
@@ -38,6 +47,7 @@ const EDIT_ADDRESS_L10N_IDS = [
"autofill-address-given-name",
"autofill-address-additional-name",
"autofill-address-family-name",
+ "autofill-address-name",
"autofill-address-organization",
"autofill-address-street",
"autofill-address-state",
@@ -48,6 +58,27 @@ const EDIT_ADDRESS_L10N_IDS = [
"autofill-address-postal-code",
"autofill-address-email",
"autofill-address-tel",
+ "autofill-edit-address-title",
+ "autofill-address-neighborhood",
+ "autofill-address-village-township",
+ "autofill-address-island",
+ "autofill-address-townland",
+ "autofill-address-district",
+ "autofill-address-county",
+ "autofill-address-post-town",
+ "autofill-address-suburb",
+ "autofill-address-parish",
+ "autofill-address-prefecture",
+ "autofill-address-area",
+ "autofill-address-do-si",
+ "autofill-address-department",
+ "autofill-address-emirate",
+ "autofill-address-oblast",
+ "autofill-address-pin",
+ "autofill-address-eircode",
+ "autofill-address-country-only",
+ "autofill-cancel-button",
+ "autofill-save-button",
];
const MANAGE_CREDITCARDS_L10N_IDS = [
"autofill-add-card-title",
@@ -85,6 +116,7 @@ FormAutofillUtils = {
ADDRESSES_COLLECTION_NAME,
CREDITCARDS_COLLECTION_NAME,
+ AUTOFILL_CREDITCARDS_REAUTH_PREF,
MANAGE_ADDRESSES_L10N_IDS,
EDIT_ADDRESS_L10N_IDS,
MANAGE_CREDITCARDS_L10N_IDS,
@@ -146,13 +178,91 @@ FormAutofillUtils = {
return ccNumber && lazy.CreditCard.isValidNumber(ccNumber);
},
- ensureLoggedIn(promptMessage) {
- return lazy.OSKeyStore.ensureLoggedIn(
- this._reauthEnabledByUser && promptMessage ? promptMessage : false
+ /**
+ * Get the decrypted value for a string pref.
+ *
+ * @param {string} prefName -> The pref whose value is needed.
+ * @param {string} safeDefaultValue -> Value to be returned incase the pref is not yet set.
+ * @returns {string}
+ */
+ getSecurePref(prefName, safeDefaultValue) {
+ try {
+ const encryptedValue = Services.prefs.getStringPref(prefName, "");
+ return encryptedValue === ""
+ ? safeDefaultValue
+ : lazy.Crypto.decrypt(encryptedValue);
+ } catch {
+ return safeDefaultValue;
+ }
+ },
+
+ /**
+ * Set the pref to the encrypted form of the value.
+ *
+ * @param {string} prefName -> The pref whose value is to be set.
+ * @param {string} value -> The value to be set in its encrypted form.
+ */
+ setSecurePref(prefName, value) {
+ if (value) {
+ const encryptedValue = lazy.Crypto.encrypt(value);
+ Services.prefs.setStringPref(prefName, encryptedValue);
+ } else {
+ Services.prefs.clearUserPref(prefName);
+ }
+ },
+
+ /**
+ * Get whether the OSAuth is enabled or not.
+ *
+ * @param {string} prefName -> The name of the pref (creditcards or addresses)
+ * @returns {boolean}
+ */
+ getOSAuthEnabled(prefName) {
+ return (
+ lazy.OSKeyStore.canReauth() &&
+ this.getSecurePref(prefName, "") !== "opt out"
);
},
/**
+ * Set whether the OSAuth is enabled or not.
+ *
+ * @param {string} prefName -> The pref to encrypt.
+ * @param {boolean} enable -> Whether the pref is to be enabled.
+ */
+ setOSAuthEnabled(prefName, enable) {
+ this.setSecurePref(prefName, enable ? null : "opt out");
+ },
+
+ async verifyUserOSAuth(
+ prefName,
+ promptMessage,
+ captionDialog = "",
+ parentWindow = null,
+ generateKeyIfNotAvailable = true
+ ) {
+ if (!this.getOSAuthEnabled(prefName)) {
+ promptMessage = false;
+ }
+ try {
+ return (
+ await lazy.OSKeyStore.ensureLoggedIn(
+ promptMessage,
+ captionDialog,
+ parentWindow,
+ generateKeyIfNotAvailable
+ )
+ ).authenticated;
+ } catch (ex) {
+ // Since Win throws an exception whereas Mac resolves to false upon cancelling.
+ if (ex.result !== Cr.NS_ERROR_FAILURE) {
+ throw ex;
+ }
+ }
+ return false;
+ },
+
+ /**
* Get the array of credit card network ids ("types") we expect and offer as valid choices
*
* @returns {Array}
@@ -636,7 +746,7 @@ FormAutofillUtils = {
findSelectOption(selectEl, record, fieldName) {
if (this.isAddressField(fieldName)) {
- return this.findAddressSelectOption(selectEl, record, fieldName);
+ return this.findAddressSelectOption(selectEl.options, record, fieldName);
}
if (this.isCreditCardField(fieldName)) {
return this.findCreditCardSelectOption(selectEl, record, fieldName);
@@ -710,13 +820,13 @@ FormAutofillUtils = {
* 3. Second pass try to identify values from address value and options,
* and look for a match.
*
- * @param {DOMElement} selectEl
+ * @param {Array<{text: string, value: string}>} options
* @param {object} address
* @param {string} fieldName
* @returns {DOMElement}
*/
- findAddressSelectOption(selectEl, address, fieldName) {
- if (selectEl.options.length > 512) {
+ findAddressSelectOption(options, address, fieldName) {
+ if (options.length > 512) {
// Allow enough space for all countries (roughly 300 distinct values) and all
// timezones (roughly 400 distinct values), plus some extra wiggle room.
return null;
@@ -728,7 +838,7 @@ FormAutofillUtils = {
let collators = this.getSearchCollators(address.country);
- for (let option of selectEl.options) {
+ for (const option of options) {
if (
this.strCompare(value, option.value, collators) ||
this.strCompare(value, option.text, collators)
@@ -763,7 +873,7 @@ FormAutofillUtils = {
"\\b" + this.escapeRegExp(identifiedValue) + "\\b",
"i"
);
- for (let option of selectEl.options) {
+ for (const option of options) {
let optionValue = this.identifyValue(
keys,
names,
@@ -789,7 +899,7 @@ FormAutofillUtils = {
}
case "country": {
if (this.getCountryAddressData(value)) {
- for (let option of selectEl.options) {
+ for (const option of options) {
if (
this.identifyCountryCode(option.text, value) ||
this.identifyCountryCode(option.value, value)
@@ -822,23 +932,13 @@ FormAutofillUtils = {
* @returns {XULElement}
*/
findAddressSelectOptionWithMenuPopup(menupopup, address, fieldName) {
- class MenuitemProxy {
- constructor(menuitem) {
- this.menuitem = menuitem;
- }
- get text() {
- return this.menuitem.label;
- }
- get value() {
- return this.menuitem.value;
- }
- }
- const selectEl = {
- options: Array.from(menupopup.childNodes).map(
- menuitem => new MenuitemProxy(menuitem)
- ),
- };
- return this.findAddressSelectOption(selectEl, address, fieldName)?.menuitem;
+ const options = Array.from(menupopup.childNodes).map(menuitem => ({
+ text: menuitem.label,
+ value: menuitem.value,
+ menuitem,
+ }));
+
+ return this.findAddressSelectOption(options, address, fieldName)?.menuitem;
},
findCreditCardSelectOption(selectEl, creditCard, fieldName) {
@@ -1055,6 +1155,86 @@ FormAutofillUtils = {
postalCodePattern: dataset.zip,
};
},
+ /**
+ * Converts a Map to an array of objects with `value` and `text` properties ( option like).
+ *
+ * @param {Map} optionsMap
+ * @returns {Array<{ value: string, text: string }>|null}
+ */
+ optionsMapToArray(optionsMap) {
+ return optionsMap?.size
+ ? [...optionsMap].map(([value, text]) => ({ value, text }))
+ : null;
+ },
+
+ /**
+ * Get flattened form layout information of a given country
+ * TODO(Bug 1891730): Remove getFormFormat and use this instead.
+ *
+ * @param {object} record - An object containing at least the 'country' property.
+ * @returns {Array} Flattened array with the address fiels in order.
+ */
+ getFormLayout(record) {
+ const formFormat = this.getFormFormat(record.country);
+ let fieldsInOrder = formFormat.fieldsOrder;
+
+ // Add missing fields that are always present but not in the .fmt of addresses
+ // TODO: extend libaddress later to support this if possible
+ fieldsInOrder = [
+ ...fieldsInOrder,
+ {
+ fieldId: "country",
+ options: this.optionsMapToArray(FormAutofill.countries),
+ required: true,
+ },
+ { fieldId: "tel", type: "tel" },
+ { fieldId: "email", type: "email" },
+ ];
+
+ const addressLevel1Options = this.optionsMapToArray(
+ formFormat.addressLevel1Options
+ );
+
+ const addressLevel1SelectedValue = addressLevel1Options
+ ? this.findAddressSelectOption(
+ addressLevel1Options,
+ record,
+ "address-level1"
+ )?.value
+ : record["address-level1"];
+
+ for (const field of fieldsInOrder) {
+ const flattenedObject = {
+ fieldId: field.fieldId,
+ newLine: field.newLine,
+ l10nId: this.getAddressFieldL10nId(field.fieldId),
+ required: formFormat.countryRequiredFields.includes(field.fieldId),
+ value: record[field.fieldId] ?? "",
+ ...(field.fieldId === "street-address" && {
+ l10nId: "autofill-address-street",
+ multiline: true,
+ }),
+ ...(field.fieldId === "address-level1" && {
+ l10nId: formFormat.addressLevel1L10nId,
+ options: addressLevel1Options,
+ value: addressLevel1SelectedValue,
+ }),
+ ...(field.fieldId === "address-level2" && {
+ l10nId: formFormat.addressLevel2L10nId,
+ }),
+ ...(field.fieldId === "address-level3" && {
+ l10nId: formFormat.addressLevel3L10nId,
+ }),
+ ...(field.fieldId === "postal-code" && {
+ pattern: formFormat.postalCodePattern,
+ l10nId: formFormat.postalCodeL10nId,
+ }),
+ };
+ Object.assign(field, flattenedObject);
+ }
+
+ return fieldsInOrder;
+ },
getAddressFieldL10nId(type) {
return "autofill-address-" + type.replace(/_/g, "-");