diff --git a/firefox-ios/Client/Assets/CC_Script/Constants.ios.mjs b/firefox-ios/Client/Assets/CC_Script/Constants.ios.mjs index 76f99e0bb90fa..3b85ae11f8ee6 100644 --- a/firefox-ios/Client/Assets/CC_Script/Constants.ios.mjs +++ b/firefox-ios/Client/Assets/CC_Script/Constants.ios.mjs @@ -9,14 +9,16 @@ const IOS_DEFAULT_PREFERENCES = { "extensions.formautofill.creditCards.heuristics.fathom.testConfidence": 0, "extensions.formautofill.creditCards.heuristics.fathom.types": "cc-number,cc-name", + "extensions.formautofill.addresses.capture.requiredFields": + "street-address,postal-code,address-level1,address-level2", "extensions.formautofill.loglevel": "Warn", "extensions.formautofill.addresses.supported": "off", "extensions.formautofill.creditCards.supported": "detect", "browser.search.region": "US", "extensions.formautofill.creditCards.supportedCountries": "US,CA,GB,FR,DE", - "extensions.formautofill.addresses.enabled": false, + "extensions.formautofill.addresses.enabled": true, + "extensions.formautofill.addresses.experiments.enabled": false, // TODO(FXCM-765): fetch this value from swift "extensions.formautofill.addresses.capture.enabled": false, - "extensions.formautofill.addresses.capture.v2.enabled": false, "extensions.formautofill.addresses.supportedCountries": "", "extensions.formautofill.creditCards.enabled": true, "extensions.formautofill.reauth.enabled": true, @@ -26,13 +28,10 @@ const IOS_DEFAULT_PREFERENCES = { "extensions.formautofill.addresses.ignoreAutocompleteOff": true, "extensions.formautofill.heuristics.enabled": true, "extensions.formautofill.section.enabled": true, - // WebKit doesn't support the checkVisibility API, setting the threshold value to 0 to ensure - // `IsFieldVisible` function doesn't use it - "extensions.formautofill.heuristics.visibilityCheckThreshold": 0, - "extensions.formautofill.heuristics.interactivityCheckMode": "focusability", "extensions.formautofill.heuristics.captureOnFormRemoval": false, "extensions.formautofill.heuristics.captureOnPageNavigation": false, "extensions.formautofill.focusOnAutofill": false, + "extensions.formautofill.test.ignoreVisibilityCheck": false, }; // Used Mimic the behavior of .getAutocompleteInfo() diff --git a/firefox-ios/Client/Assets/CC_Script/FieldScanner.sys.mjs b/firefox-ios/Client/Assets/CC_Script/FieldScanner.sys.mjs index 22adfdabe818c..2118de3de890d 100644 --- a/firefox-ios/Client/Assets/CC_Script/FieldScanner.sys.mjs +++ b/firefox-ios/Client/Assets/CC_Script/FieldScanner.sys.mjs @@ -2,6 +2,11 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +const lazy = {}; +ChromeUtils.defineESModuleGetters(lazy, { + FormAutofillUtils: "resource://gre/modules/shared/FormAutofillUtils.sys.mjs", +}); + /** * Represents the detailed information about a form field, including * the inferred field name, the approach used for inferring, and additional metadata. @@ -73,6 +78,14 @@ export class FieldDetail { get sectionName() { return this.section || this.addressType; } + + #isVisible = null; + get isVisible() { + if (this.#isVisible == null) { + this.#isVisible = lazy.FormAutofillUtils.isFieldVisible(this.element); + } + return this.#isVisible; + } } /** diff --git a/firefox-ios/Client/Assets/CC_Script/FormAutofill.sys.mjs b/firefox-ios/Client/Assets/CC_Script/FormAutofill.sys.mjs index aac8fa70a7575..77502afbbe968 100644 --- a/firefox-ios/Client/Assets/CC_Script/FormAutofill.sys.mjs +++ b/firefox-ios/Client/Assets/CC_Script/FormAutofill.sys.mjs @@ -4,6 +4,7 @@ import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; import { Region } from "resource://gre/modules/Region.sys.mjs"; +import { AddressMetaDataLoader } from "resource://gre/modules/shared/AddressMetaDataLoader.sys.mjs"; const AUTOFILL_ADDRESSES_AVAILABLE_PREF = "extensions.formautofill.addresses.supported"; @@ -17,8 +18,8 @@ const ENABLED_AUTOFILL_ADDRESSES_PREF = "extensions.formautofill.addresses.enabled"; const ENABLED_AUTOFILL_ADDRESSES_CAPTURE_PREF = "extensions.formautofill.addresses.capture.enabled"; -const ENABLED_AUTOFILL_ADDRESSES_CAPTURE_V2_PREF = - "extensions.formautofill.addresses.capture.v2.enabled"; +const ENABLED_AUTOFILL_ADDRESSES_CAPTURE_REQUIRED_FIELDS_PREF = + "extensions.formautofill.addresses.capture.requiredFields"; const ENABLED_AUTOFILL_ADDRESSES_SUPPORTED_COUNTRIES_PREF = "extensions.formautofill.addresses.supportedCountries"; const ENABLED_AUTOFILL_CREDITCARDS_PREF = @@ -32,17 +33,16 @@ const AUTOFILL_CREDITCARDS_AUTOCOMPLETE_OFF_PREF = "extensions.formautofill.creditCards.ignoreAutocompleteOff"; const AUTOFILL_ADDRESSES_AUTOCOMPLETE_OFF_PREF = "extensions.formautofill.addresses.ignoreAutocompleteOff"; -const ENABLED_AUTOFILL_CAPTURE_ON_FORM_REMOVAL = +const ENABLED_AUTOFILL_CAPTURE_ON_FORM_REMOVAL_PREF = "extensions.formautofill.heuristics.captureOnFormRemoval"; -const ENABLED_AUTOFILL_CAPTURE_ON_PAGE_NAVIGATION = +const ENABLED_AUTOFILL_CAPTURE_ON_PAGE_NAVIGATION_PREF = "extensions.formautofill.heuristics.captureOnPageNavigation"; export const FormAutofill = { ENABLED_AUTOFILL_ADDRESSES_PREF, ENABLED_AUTOFILL_ADDRESSES_CAPTURE_PREF, - ENABLED_AUTOFILL_ADDRESSES_CAPTURE_V2_PREF, - ENABLED_AUTOFILL_CAPTURE_ON_FORM_REMOVAL, - ENABLED_AUTOFILL_CAPTURE_ON_PAGE_NAVIGATION, + 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_AUTOCOMPLETE_OFF_PREF, @@ -101,14 +101,25 @@ export const FormAutofill = { /** * Determines if the address autofill feature is available to use in the browser. * If the feature is not available, then there are no user facing ways to enable it. + * Two conditions must be met for the autofill feature to be considered available: + * 1. Address autofill support is confirmed when: + * - `extensions.formautofill.addresses.supported` is set to `on`. + * - The user is located in a region supported by the feature + * (`extensions.formautofill.creditCards.supportedCountries`). + * 2. Address autofill is enabled through a Nimbus experiment: + * - The experiment pref `extensions.formautofill.addresses.experiments.enabled` is set to true. * * @returns {boolean} `true` if address autofill is available */ get isAutofillAddressesAvailable() { - return this._isSupportedRegion( + const isUserInSupportedRegion = this._isSupportedRegion( FormAutofill._isAutofillAddressesAvailable, FormAutofill._addressAutofillSupportedCountries ); + return ( + isUserInSupportedRegion || + FormAutofill._isAutofillAddressesAvailableInExperiment + ); }, /** * Determines if the user has enabled or disabled credit card autofill. @@ -208,11 +219,6 @@ XPCOMUtils.defineLazyPreferenceGetter( "isAutofillAddressesCaptureEnabled", ENABLED_AUTOFILL_ADDRESSES_CAPTURE_PREF ); -XPCOMUtils.defineLazyPreferenceGetter( - FormAutofill, - "isAutofillAddressesCaptureV2Enabled", - ENABLED_AUTOFILL_ADDRESSES_CAPTURE_V2_PREF -); XPCOMUtils.defineLazyPreferenceGetter( FormAutofill, "_isAutofillCreditCardsAvailable", @@ -261,25 +267,28 @@ XPCOMUtils.defineLazyPreferenceGetter( XPCOMUtils.defineLazyPreferenceGetter( FormAutofill, "captureOnFormRemoval", - ENABLED_AUTOFILL_CAPTURE_ON_FORM_REMOVAL + ENABLED_AUTOFILL_CAPTURE_ON_FORM_REMOVAL_PREF ); XPCOMUtils.defineLazyPreferenceGetter( FormAutofill, "captureOnPageNavigation", - ENABLED_AUTOFILL_CAPTURE_ON_PAGE_NAVIGATION + ENABLED_AUTOFILL_CAPTURE_ON_PAGE_NAVIGATION_PREF +); +XPCOMUtils.defineLazyPreferenceGetter( + FormAutofill, + "addressCaptureRequiredFields", + ENABLED_AUTOFILL_ADDRESSES_CAPTURE_REQUIRED_FIELDS_PREF, + null, + null, + val => val?.split(",").filter(v => !!v) +); + +XPCOMUtils.defineLazyPreferenceGetter( + FormAutofill, + "_isAutofillAddressesAvailableInExperiment", + "extensions.formautofill.addresses.experiments.enabled" ); -// XXX: This should be invalidated on intl:app-locales-changed. -ChromeUtils.defineLazyGetter(FormAutofill, "countries", () => { - let availableRegionCodes = - Services.intl.getAvailableLocaleDisplayNames("region"); - let displayNames = Services.intl.getRegionDisplayNames( - undefined, - availableRegionCodes - ); - let result = new Map(); - for (let i = 0; i < availableRegionCodes.length; i++) { - result.set(availableRegionCodes[i].toUpperCase(), displayNames[i]); - } - return result; -}); +ChromeUtils.defineLazyGetter(FormAutofill, "countries", () => + AddressMetaDataLoader.getCountries() +); diff --git a/firefox-ios/Client/Assets/CC_Script/FormAutofillChild.ios.sys.mjs b/firefox-ios/Client/Assets/CC_Script/FormAutofillChild.ios.sys.mjs index d2500d9ec4121..1aa713b5b7ed0 100644 --- a/firefox-ios/Client/Assets/CC_Script/FormAutofillChild.ios.sys.mjs +++ b/firefox-ios/Client/Assets/CC_Script/FormAutofillChild.ios.sys.mjs @@ -33,20 +33,21 @@ export class FormAutofillChild { _doIdentifyAutofillFields(element) { this.fieldDetailsManager.updateActiveInput(element); - const validDetails = - this.fieldDetailsManager.identifyAutofillFields(element); + this.fieldDetailsManager.identifyAutofillFields(element); const activeFieldName = this.fieldDetailsManager.activeFieldDetail?.fieldName; + const activeFieldDetails = + this.fieldDetailsManager.activeSection?.fieldDetails; + // Only ping swift if current field is either a cc or address field - if (!validDetails?.find(field => field.element === element)) { + if (!activeFieldDetails?.find(field => field.element === element)) { return; } const fieldNamesWithValues = - this.transformToFieldNamesWithValues(validDetails); - + this.transformToFieldNamesWithValues(activeFieldDetails); if (FormAutofillUtils.isAddressField(activeFieldName)) { this.callbacks.address.autofill(fieldNamesWithValues); } else if (FormAutofillUtils.isCreditCardField(activeFieldName)) { @@ -77,9 +78,14 @@ export class FormAutofillChild { } onSubmit(evt) { + if (!this.fieldDetailsManager.activeHandler) { + return; + } + this.fieldDetailsManager.activeHandler.onFormSubmitted(); const records = this.fieldDetailsManager.activeHandler.createRecords(); - if (records.creditCard) { + + if (records.creditCard.length) { // Normalize record format so we always get a consistent // credit card record format: {cc-number, cc-name, cc-exp-month, cc-exp-year} const creditCardRecords = records.creditCard.map(entry => { diff --git a/firefox-ios/Client/Assets/CC_Script/FormAutofillHeuristics.sys.mjs b/firefox-ios/Client/Assets/CC_Script/FormAutofillHeuristics.sys.mjs index c3d0477411212..ccf9a886d565c 100644 --- a/firefox-ios/Client/Assets/CC_Script/FormAutofillHeuristics.sys.mjs +++ b/firefox-ios/Client/Assets/CC_Script/FormAutofillHeuristics.sys.mjs @@ -14,10 +14,6 @@ ChromeUtils.defineESModuleGetters(lazy, { LabelUtils: "resource://gre/modules/shared/LabelUtils.sys.mjs", }); -ChromeUtils.defineLazyGetter(lazy, "log", () => - FormAutofill.defineLogGetter(lazy, "FormAutofillHeuristics") -); - /** * To help us classify sections, we want to know what fields can appear * multiple times in a row. @@ -551,7 +547,9 @@ export const FormAutofillHeuristics = { * all sections within its field details in the form. */ getFormInfo(form) { - let elements = this.getFormElements(form); + const elements = Array.from(form.elements).filter(element => + lazy.FormAutofillUtils.isCreditCardOrAddressFieldType(element) + ); const scanner = new lazy.FieldScanner(elements, element => this.inferFieldInfo(element, elements) @@ -600,44 +598,6 @@ export const FormAutofillHeuristics = { ); }, - /** - * Get form elements that are of credit card or address type and filtered by either - * visibility or focusability - depending on the interactivity mode (default = focusability) - * This distinction is only temporary as we want to test switching from visibility mode - * to focusability mode. The visibility mode is then removed. - * - * @param {HTMLElement} form - * @returns {Array} elements filtered by interactivity mode (visibility or focusability) - */ - getFormElements(form) { - let elements = Array.from(form.elements).filter(element => - lazy.FormAutofillUtils.isCreditCardOrAddressFieldType(element) - ); - const interactivityMode = lazy.FormAutofillUtils.interactivityCheckMode; - - if (interactivityMode == "focusability") { - elements = elements.filter(element => - lazy.FormAutofillUtils.isFieldFocusable(element) - ); - } else if (interactivityMode == "visibility") { - // Due to potential performance impact while running visibility check on - // a large amount of elements, a comprehensive visibility check - // (considering opacity and CSS visibility) is only applied when the number - // of eligible elements is below a certain threshold. - const runVisiblityCheck = - elements.length < lazy.FormAutofillUtils.visibilityCheckThreshold; - if (!runVisiblityCheck) { - lazy.log.debug( - `Skip running visibility check, because of too many elements (${elements.length})` - ); - } - elements = elements.filter(element => - lazy.FormAutofillUtils.isFieldVisible(element, runVisiblityCheck) - ); - } - return elements; - }, - /** * The result is an array contains the sections with its belonging field details. * @@ -647,46 +607,54 @@ export const FormAutofillHeuristics = { _classifySections(fieldDetails) { let sections = []; for (let i = 0; i < fieldDetails.length; i++) { - const fieldName = fieldDetails[i].fieldName; - const sectionName = fieldDetails[i].sectionName; - + const cur = fieldDetails[i]; const [currentSection] = sections.slice(-1); - // The section this field might belong to + // The section this field might be placed into. let candidateSection = null; - // If the field doesn't have a section name, MAYBE put it to the previous - // section if exists. If the field has a section name, maybe put it to the - // nearest section that either has the same name or it doesn't has a name. - // Otherwise, create a new section. - if (!currentSection || !sectionName) { + // Use name group from autocomplete attribute (ex, section-xxx) to look for the section + // we might place this field into. + // If the field doesn't have a section name, the candidate section is the previous section. + if (!currentSection || !cur.sectionName) { candidateSection = currentSection; - } else if (sectionName) { + } else if (cur.sectionName) { + // If the field has a section name, the candidate section is the nearest section that + // either shares the same name or lacks a name. for (let idx = sections.length - 1; idx >= 0; idx--) { - if (!sections[idx].name || sections[idx].name == sectionName) { + if (!sections[idx].name || sections[idx].name == cur.sectionName) { candidateSection = sections[idx]; break; } } } - // We got an candidate section to put the field to, check whether the section - // already has a field with the same field name. If yes, only add the field to when - // the type of the field might appear multiple times in a row. if (candidateSection) { let createNewSection = true; - if (candidateSection.fieldDetails.find(f => f.fieldName == fieldName)) { + + // We might create a new section instead of placing the field in the candiate section if + // the section already has a field with the same field name. + // We also check visibility for both the fields with the same field name because we don't + // wanht to create a new section for an invisible field. + if ( + candidateSection.fieldDetails.find( + f => f.fieldName == cur.fieldName && f.isVisible && cur.isVisible + ) + ) { + // For some field type, it is common to have multiple fields in one section, for example, + // email. In that case, we will not create a new section even when the candidate section + // already has a field with the same field name. const [lastFieldDetail] = candidateSection.fieldDetails.slice(-1); - if (lastFieldDetail.fieldName == fieldName) { - if (MULTI_FIELD_NAMES.includes(fieldName)) { + if (lastFieldDetail.fieldName == cur.fieldName) { + if (MULTI_FIELD_NAMES.includes(cur.fieldName)) { createNewSection = false; - } else if (fieldName in MULTI_N_FIELD_NAMES) { + } else if (cur.fieldName in MULTI_N_FIELD_NAMES) { // This is the heuristic to handle special cases where we can have multiple // fields in one section, but only if the field has appeared N times in a row. // For example, websites can use 4 consecutive 4-digit `cc-number` fields // instead of one 16-digit `cc-number` field. - const N = MULTI_N_FIELD_NAMES[fieldName]; + const N = MULTI_N_FIELD_NAMES[cur.fieldName]; if (lastFieldDetail.part) { // If `part` is set, we have already identified this field can be // merged previously @@ -699,7 +667,7 @@ export const FormAutofillHeuristics = { N == 2 || fieldDetails .slice(i + 1, i + N - 1) - .every(f => f.fieldName == fieldName) + .every(f => f.fieldName == cur.fieldName) ) { lastFieldDetail.part = 1; fieldDetails[i].part = 2; diff --git a/firefox-ios/Client/Assets/CC_Script/FormAutofillSection.sys.mjs b/firefox-ios/Client/Assets/CC_Script/FormAutofillSection.sys.mjs index 1147006578076..f848b79385984 100644 --- a/firefox-ios/Client/Assets/CC_Script/FormAutofillSection.sys.mjs +++ b/firefox-ios/Client/Assets/CC_Script/FormAutofillSection.sys.mjs @@ -7,7 +7,7 @@ import { FormAutofill } from "resource://autofill/FormAutofill.sys.mjs"; const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { - AutofillTelemetry: "resource://autofill/AutofillTelemetry.sys.mjs", + AutofillTelemetry: "resource://gre/modules/shared/AutofillTelemetry.sys.mjs", CreditCard: "resource://gre/modules/CreditCard.sys.mjs", FormAutofillNameUtils: "resource://gre/modules/shared/FormAutofillNameUtils.sys.mjs", @@ -174,28 +174,26 @@ export class FormAutofillSection { this._cacheValue.matchingSelectOption = new WeakMap(); } - for (let fieldName in profile) { - let fieldDetail = this.getFieldDetailByName(fieldName); - if (!fieldDetail) { - continue; - } + for (const fieldName in profile) { + const fieldDetail = this.getFieldDetailByName(fieldName); + const element = fieldDetail?.element; - let element = fieldDetail.element; if (!HTMLSelectElement.isInstance(element)) { continue; } - let cache = this._cacheValue.matchingSelectOption.get(element) || {}; - let value = profile[fieldName]; + const cache = this._cacheValue.matchingSelectOption.get(element) || {}; + const value = profile[fieldName]; if (cache[value] && cache[value].deref()) { continue; } - let option = FormAutofillUtils.findSelectOption( + const option = FormAutofillUtils.findSelectOption( element, profile, fieldName ); + if (option) { cache[value] = new WeakRef(option); this._cacheValue.matchingSelectOption.set(element, cache); @@ -204,9 +202,14 @@ export class FormAutofillSection { delete cache[value]; this._cacheValue.matchingSelectOption.set(element, cache); } - // Delete the field so the phishing hint won't treat it as a "also fill" - // field. - delete profile[fieldName]; + // Skip removing cc-type since this is needed for displaying the icon for credit card network + // TODO(Bug 1874339): Cleanup transformation and normalization of data to not remove any + // fields and be more consistent + if (!["cc-type"].includes(fieldName)) { + // Delete the field so the phishing hint won't treat it as a "also fill" + // field. + delete profile[fieldName]; + } } } } @@ -323,6 +326,7 @@ export class FormAutofillSection { throw new Error("No fieldDetail for the focused input."); } + this.getAdaptedProfiles([profile]); if (!(await this.prepareFillingProfile(profile))) { this.log.debug("profile cannot be filled"); return false; @@ -1285,13 +1289,4 @@ export class FormAutofillCreditCardSection extends FormAutofillSection { } return true; } - - async autofillFields(profile) { - this.getAdaptedProfiles([profile]); - if (!(await super.autofillFields(profile))) { - return false; - } - - return true; - } } diff --git a/firefox-ios/Client/Assets/CC_Script/FormAutofillUtils.sys.mjs b/firefox-ios/Client/Assets/CC_Script/FormAutofillUtils.sys.mjs index cdd4dd1fb1f7d..c9e570bf4a33d 100644 --- a/firefox-ios/Client/Assets/CC_Script/FormAutofillUtils.sys.mjs +++ b/firefox-ios/Client/Assets/CC_Script/FormAutofillUtils.sys.mjs @@ -12,7 +12,10 @@ ChromeUtils.defineESModuleGetters(lazy, { FormAutofillNameUtils: "resource://gre/modules/shared/FormAutofillNameUtils.sys.mjs", OSKeyStore: "resource://gre/modules/OSKeyStore.sys.mjs", + AddressMetaDataLoader: + "resource://gre/modules/shared/AddressMetaDataLoader.sys.mjs", }); + ChromeUtils.defineLazyGetter( lazy, "l10n", @@ -25,10 +28,6 @@ ChromeUtils.defineLazyGetter( export let FormAutofillUtils; -const ADDRESS_METADATA_PATH = "resource://autofill/addressmetadata/"; -const ADDRESS_REFERENCES = "addressReferences.js"; -const ADDRESS_REFERENCES_EXT = "addressReferencesExt.js"; - const ADDRESSES_COLLECTION_NAME = "addresses"; const CREDITCARDS_COLLECTION_NAME = "creditCards"; const MANAGE_ADDRESSES_L10N_IDS = [ @@ -79,149 +78,6 @@ const ELIGIBLE_INPUT_TYPES = ["text", "email", "tel", "number", "month"]; // attacks that fill the user's hard drive(s). const MAX_FIELD_VALUE_LENGTH = 200; -export let AddressDataLoader = { - // Status of address data loading. We'll load all the countries with basic level 1 - // information while requesting conutry information, and set country to true. - // Level 1 Set is for recording which country's level 1/level 2 data is loaded, - // since we only load this when getCountryAddressData called with level 1 parameter. - _dataLoaded: { - country: false, - level1: new Set(), - }, - - /** - * Load address data and extension script into a sandbox from different paths. - * - * @param {string} path - * The path for address data and extension script. It could be root of the address - * metadata folder(addressmetadata/) or under specific country(addressmetadata/TW/). - * @returns {object} - * A sandbox that contains address data object with properties from extension. - */ - _loadScripts(path) { - let sandbox = {}; - let extSandbox = {}; - - try { - sandbox = FormAutofillUtils.loadDataFromScript(path + ADDRESS_REFERENCES); - extSandbox = FormAutofillUtils.loadDataFromScript( - path + ADDRESS_REFERENCES_EXT - ); - } catch (e) { - // Will return only address references if extension loading failed or empty sandbox if - // address references loading failed. - return sandbox; - } - - if (extSandbox.addressDataExt) { - for (let key in extSandbox.addressDataExt) { - let addressDataForKey = sandbox.addressData[key]; - if (!addressDataForKey) { - addressDataForKey = sandbox.addressData[key] = {}; - } - - Object.assign(addressDataForKey, extSandbox.addressDataExt[key]); - } - } - return sandbox; - }, - - /** - * Convert certain properties' string value into array. We should make sure - * the cached data is parsed. - * - * @param {object} data Original metadata from addressReferences. - * @returns {object} parsed metadata with property value that converts to array. - */ - _parse(data) { - if (!data) { - return null; - } - - const properties = [ - "languages", - "sub_keys", - "sub_isoids", - "sub_names", - "sub_lnames", - ]; - for (let key of properties) { - if (!data[key]) { - continue; - } - // No need to normalize data if the value is array already. - if (Array.isArray(data[key])) { - return data; - } - - data[key] = data[key].split("~"); - } - return data; - }, - - /** - * We'll cache addressData in the loader once the data loaded from scripts. - * It'll become the example below after loading addressReferences with extension: - * addressData: { - * "data/US": {"lang": ["en"], ...// Data defined in libaddressinput metadata - * "alternative_names": ... // Data defined in extension } - * "data/CA": {} // Other supported country metadata - * "data/TW": {} // Other supported country metadata - * "data/TW/台北市": {} // Other supported country level 1 metadata - * } - * - * @param {string} country - * @param {string?} level1 - * @returns {object} Default locale metadata - */ - _loadData(country, level1 = null) { - // Load the addressData if needed - if (!this._dataLoaded.country) { - this._addressData = this._loadScripts(ADDRESS_METADATA_PATH).addressData; - this._dataLoaded.country = true; - } - if (!level1) { - return this._parse(this._addressData[`data/${country}`]); - } - // If level1 is set, load addressReferences under country folder with specific - // country/level 1 for level 2 information. - if (!this._dataLoaded.level1.has(country)) { - Object.assign( - this._addressData, - this._loadScripts(`${ADDRESS_METADATA_PATH}${country}/`).addressData - ); - this._dataLoaded.level1.add(country); - } - return this._parse(this._addressData[`data/${country}/${level1}`]); - }, - - /** - * Return the region metadata with default locale and other locales (if exists). - * - * @param {string} country - * @param {string?} level1 - * @returns {object} Return default locale and other locales metadata. - */ - getData(country, level1 = null) { - let defaultLocale = this._loadData(country, level1); - if (!defaultLocale) { - return null; - } - - let countryData = this._parse(this._addressData[`data/${country}`]); - let locales = []; - // TODO: Should be able to support multi-locale level 1/ level 2 metadata query - // in Bug 1421886 - if (countryData.languages) { - let list = countryData.languages.filter(key => key !== countryData.lang); - locales = list.map(key => - this._parse(this._addressData[`${defaultLocale.id}--${key}`]) - ); - } - return { defaultLocale, locales }; - }, -}; - FormAutofillUtils = { get AUTOFILL_FIELDS_THRESHOLD() { return 3; @@ -455,7 +311,11 @@ FormAutofillUtils = { * @returns {boolean} true if the element is visible */ isFieldVisible(element, visibilityCheck = true) { - if (visibilityCheck && element.checkVisibility) { + if ( + visibilityCheck && + element.checkVisibility && + !FormAutofillUtils.ignoreVisibilityCheck + ) { return element.checkVisibility({ checkOpacity: true, checkVisibilityCSS: true, @@ -465,23 +325,6 @@ FormAutofillUtils = { return !element.hidden && element.style.display != "none"; }, - /** - * Determines if an element is focusable - * and accessible via keyboard navigation or not. - * - * @param {HTMLElement} element - * - * @returns {bool} true if the element is focusable and accessible - */ - isFieldFocusable(element) { - return ( - // The Services.focus.elementIsFocusable API considers elements with - // tabIndex="-1" set as focusable. But since they are not accessible - // via keyboard navigation we treat them as non-interactive - Services.focus.elementIsFocusable(element, 0) && element.tabIndex != "-1" - ); - }, - /** * Determines if an element is eligible to be used by credit card or address autofill. * @@ -508,7 +351,7 @@ FormAutofillUtils = { /** * Get country address data and fallback to US if not found. - * See AddressDataLoader._loadData for more details of addressData structure. + * See AddressMetaDataLoader.#loadData for more details of addressData structure. * * @param {string} [country=FormAutofill.DEFAULT_REGION] * The country code for requesting specific country's metadata. It'll be @@ -524,21 +367,23 @@ FormAutofillUtils = { country = FormAutofill.DEFAULT_REGION, level1 = null ) { - let metadata = AddressDataLoader.getData(country, level1); + let metadata = lazy.AddressMetaDataLoader.getData(country, level1); if (!metadata) { if (level1) { return null; } // Fallback to default region if we couldn't get data from given country. if (country != FormAutofill.DEFAULT_REGION) { - metadata = AddressDataLoader.getData(FormAutofill.DEFAULT_REGION); + metadata = lazy.AddressMetaDataLoader.getData( + FormAutofill.DEFAULT_REGION + ); } } // TODO: Now we fallback to US if we couldn't get data from default region, // but it could be removed in bug 1423464 if it's not necessary. if (!metadata) { - metadata = AddressDataLoader.getData("US"); + metadata = lazy.AddressMetaDataLoader.getData("US"); } return metadata; }, @@ -726,7 +571,7 @@ FormAutofillUtils = { return null; } - if (AddressDataLoader.getData(countryName)) { + if (lazy.AddressMetaDataLoader.getData(countryName)) { return countryName; } @@ -1282,20 +1127,6 @@ XPCOMUtils.defineLazyPreferenceGetter( pref => parseFloat(pref) ); -XPCOMUtils.defineLazyPreferenceGetter( - FormAutofillUtils, - "visibilityCheckThreshold", - "extensions.formautofill.heuristics.visibilityCheckThreshold", - 200 -); - -XPCOMUtils.defineLazyPreferenceGetter( - FormAutofillUtils, - "interactivityCheckMode", - "extensions.formautofill.heuristics.interactivityCheckMode", - "focusability" -); - // This is only used in iOS XPCOMUtils.defineLazyPreferenceGetter( FormAutofillUtils, @@ -1303,3 +1134,11 @@ XPCOMUtils.defineLazyPreferenceGetter( "extensions.formautofill.focusOnAutofill", true ); + +// This is only used for testing +XPCOMUtils.defineLazyPreferenceGetter( + FormAutofillUtils, + "ignoreVisibilityCheck", + "extensions.formautofill.test.ignoreVisibilityCheck", + false +); diff --git a/firefox-ios/Client/Assets/CC_Script/Helpers.ios.mjs b/firefox-ios/Client/Assets/CC_Script/Helpers.ios.mjs index 28429b0efe487..097fb1cdd2ef6 100644 --- a/firefox-ios/Client/Assets/CC_Script/Helpers.ios.mjs +++ b/firefox-ios/Client/Assets/CC_Script/Helpers.ios.mjs @@ -12,9 +12,23 @@ HTMLFormElement.isInstance = element => element instanceof HTMLFormElement; ShadowRoot.isInstance = element => element instanceof ShadowRoot; HTMLElement.prototype.ownerGlobal = window; + HTMLInputElement.prototype.setUserInput = function (value) { this.value = value; - this.dispatchEvent(new Event("input", { bubbles: true })); + + // In React apps, setting .value may not always work reliably. + // We dispatch change, input as a workaround. + // There are other more "robust" solutions: + // - Dispatching keyboard events and comparing the value after setting it + // (https://github.com/fmeum/browserpass-extension/blob/5efb1f9de6078b509904a83847d370c8e92fc097/src/inject.js#L412-L440) + // - Using the native setter + // (https://github.com/facebook/react/issues/10135#issuecomment-401496776) + // These are a bit more bloated. We can consider using these later if we encounter any further issues. + ["input", "change"].forEach(eventName => { + this.dispatchEvent(new Event(eventName, { bubbles: true })); + }); + + this.dispatchEvent(new Event("blur", { bubbles: true })); }; // Mimic the behavior of .getAutocompleteInfo() @@ -50,6 +64,15 @@ const withNotImplementedError = obj => }, }); +// This function will create a proxy for each undefined property +// This is useful when the accessed property name is unkonwn beforehand +const undefinedProxy = () => + new Proxy(() => {}, { + get() { + return undefinedProxy(); + }, + }); + // Webpack needs to be able to statically analyze require statements in order to build the dependency graph // In order to require modules dynamically at runtime, we use require.context() to create a dynamic require // that is still able to be parsed by Webpack at compile time. The "./" and ".mjs" tells webpack that files @@ -78,9 +101,6 @@ const internalModuleResolvers = { // Define mock for XPCOMUtils export const XPCOMUtils = withNotImplementedError({ - defineLazyGetter: (obj, prop, getFn) => { - obj[prop] = getFn?.call(obj); - }, defineLazyPreferenceGetter: ( obj, prop, @@ -123,27 +143,10 @@ export const OSKeyStore = withNotImplementedError({ ensureLoggedIn: () => true, }); -// Checks an element's focusability and accessibility via keyboard navigation -const checkFocusability = element => { - return ( - !element.disabled && - !element.hidden && - element.style.display != "none" && - element.tabIndex != "-1" - ); -}; - // Define mock for Services // NOTE: Services is a global so we need to attach it to the window // eslint-disable-next-line no-shadow export const Services = withNotImplementedError({ - focus: withNotImplementedError({ - elementIsFocusable: checkFocusability, - }), - intl: withNotImplementedError({ - getAvailableLocaleDisplayNames: () => [], - getRegionDisplayNames: () => [], - }), locale: withNotImplementedError({ isAppLocaleRTL: false }), prefs: withNotImplementedError({ prefIsLocked: () => false }), strings: withNotImplementedError({ @@ -153,7 +156,60 @@ export const Services = withNotImplementedError({ formatStringFromName: () => "", }), }), - uuid: withNotImplementedError({ generateUUID: () => "" }), + telemetry: withNotImplementedError({ + scalarAdd: (scalarName, scalarValue) => { + // For now, we only care about the address form telemetry + // TODO(FXCM-935): move address telemetry to Glean so we can remove this + // Data format of the sent message is: + // { + // type: "scalar", + // name: "formautofill.addresses.detected_sections_count", + // value: Number, + // } + if (scalarName !== "formautofill.addresses.detected_sections_count") { + return; + } + + // eslint-disable-next-line no-undef + webkit.messageHandlers.addressFormTelemetryMessageHandler.postMessage({ + type: "scalar", + name: scalarName, + value: scalarValue, + }); + }, + recordEvent: (category, method, object, value, extra) => { + // For now, we only care about the address form telemetry + // TODO(FXCM-935): move address telemetry to Glean so we can remove this + // Data format of the sent message is: + // { + // type: "event", + // category: "address", + // method: "detected" | "filled" | "filled_modified", + // object: "address_form" | "address_form_ext", + // value: String, + // extra: Any, + // } + if (category !== "address") { + return; + } + + // eslint-disable-next-line no-undef + webkit.messageHandlers.addressFormTelemetryMessageHandler.postMessage({ + type: "event", + category, + method, + object, + value, + extra, + }); + }, + }), + // TODO(FXCM-936): we should use crypto.randomUUID() instead of Services.uuid.generateUUID() in our codebase + // Underneath crypto.randomUUID() uses the same implementation as generateUUID() + // https://searchfox.org/mozilla-central/rev/d405168c4d3c0fb900a7354ae17bb34e939af996/dom/base/Crypto.cpp#96 + // The only limitation is that it's not available in insecure contexts, which should be fine for both iOS and Desktop + // since we only autofill in secure contexts + uuid: withNotImplementedError({ generateUUID: () => crypto.randomUUID() }), }); window.Services = Services; @@ -162,15 +218,18 @@ window.Localization = function () { return { formatValueSync: () => "" }; }; +// For now, we ignore all calls to glean. +// TODO(FXCM-935): move address telemetry to Glean so we can create a universal mock for glean that +// dispatches telemetry messages to the iOS. +window.Glean = { + formautofillCreditcards: undefinedProxy(), + formautofill: undefinedProxy(), +}; + export const windowUtils = withNotImplementedError({ removeManuallyManagedState: () => {}, addManuallyManagedState: () => {}, }); window.windowUtils = windowUtils; -export const AutofillTelemetry = withNotImplementedError({ - recordFormInteractionEvent: () => {}, - recordDetectedSectionCount: () => {}, -}); - export { IOSAppConstants as AppConstants } from "resource://gre/modules/shared/Constants.ios.mjs"; diff --git a/firefox-ios/Client/Assets/CC_Script/Overrides.ios.js b/firefox-ios/Client/Assets/CC_Script/Overrides.ios.js index a0023a267ccdc..ae5998992bf3f 100644 --- a/firefox-ios/Client/Assets/CC_Script/Overrides.ios.js +++ b/firefox-ios/Client/Assets/CC_Script/Overrides.ios.js @@ -7,7 +7,6 @@ // This array defines overrides that webpack will use when bundling the JS on iOS // in order to load the right modules const ModuleOverrides = { - "AutofillTelemetry.sys.mjs": "Helpers.ios.mjs", "AppConstants.sys.mjs": "Helpers.ios.mjs", "XPCOMUtils.sys.mjs": "Helpers.ios.mjs", "Region.sys.mjs": "Helpers.ios.mjs",