From 8dd16259287f58f9273002717ec4d27e97127719 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Wed, 12 Jun 2024 07:43:14 +0200 Subject: Merging upstream version 127.0. Signed-off-by: Daniel Baumann --- .../AutofillProfileAutoComplete.sys.mjs | 334 +-------------------- .../components/formautofill/FormAutofill.sys.mjs | 6 +- .../formautofill/FormAutofillChild.sys.mjs | 217 +++++++++++-- .../formautofill/FormAutofillParent.sys.mjs | 86 +++++- .../formautofill/FormAutofillPreferences.sys.mjs | 65 ++-- toolkit/components/formautofill/Helpers.ios.mjs | 5 + .../default/FormAutofillPrompter.sys.mjs | 30 +- .../shared/FormAutofillSection.sys.mjs | 1 + .../formautofill/shared/FormAutofillUtils.sys.mjs | 234 +++++++++++++-- 9 files changed, 539 insertions(+), 439 deletions(-) (limited to 'toolkit/components/formautofill') 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} 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; } @@ -401,6 +399,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} 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 @@ -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,12 +178,90 @@ 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 * @@ -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, "-"); -- cgit v1.2.3