From 152a034cd7431a4b6b38fc0dd41d4ffbcfeaed44 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 1 May 2024 00:03:34 +0000 Subject: [PATCH] Refactor [vXXX] auto update credential provider script --- Client/Assets/CC_Script/Constants.ios.mjs | 14 +- Client/Assets/CC_Script/CreditCard.sys.mjs | 21 +- .../Assets/CC_Script/CreditCardRecord.sys.mjs | 19 +- .../CC_Script/CreditCardRuleset.sys.mjs | 1 + Client/Assets/CC_Script/FieldScanner.sys.mjs | 15 + .../Assets/CC_Script/FormAutofill.ios.sys.mjs | 2 +- Client/Assets/CC_Script/FormAutofill.sys.mjs | 82 ++-- .../CC_Script/FormAutofillChild.ios.sys.mjs | 84 +++- .../CC_Script/FormAutofillHandler.sys.mjs | 11 +- .../CC_Script/FormAutofillHeuristics.sys.mjs | 112 ++--- .../CC_Script/FormAutofillSection.sys.mjs | 106 +++-- .../CC_Script/FormAutofillUtils.sys.mjs | 409 +++++++++--------- .../Assets/CC_Script/FormStateManager.sys.mjs | 2 +- Client/Assets/CC_Script/Helpers.ios.mjs | 130 ++++-- .../Assets/CC_Script/HeuristicsRegExp.sys.mjs | 2 +- .../Assets/CC_Script/LoginManager.shared.mjs | 2 +- Client/Assets/CC_Script/Overrides.ios.js | 1 - Client/Assets/CC_Script/fathom.mjs | 2 +- 18 files changed, 568 insertions(+), 447 deletions(-) diff --git a/Client/Assets/CC_Script/Constants.ios.mjs b/Client/Assets/CC_Script/Constants.ios.mjs index be92dade608db..290e690ea6497 100644 --- a/Client/Assets/CC_Script/Constants.ios.mjs +++ b/Client/Assets/CC_Script/Constants.ios.mjs @@ -9,15 +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.firstTimeUse": true, "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": true, "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, @@ -27,11 +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/Client/Assets/CC_Script/CreditCard.sys.mjs b/Client/Assets/CC_Script/CreditCard.sys.mjs index 3561d5862042c..622c371d76ad8 100644 --- a/Client/Assets/CC_Script/CreditCard.sys.mjs +++ b/Client/Assets/CC_Script/CreditCard.sys.mjs @@ -454,10 +454,15 @@ export class CreditCard { } static formatMaskedNumber(maskedNumber) { - return { - affix: "****", - label: maskedNumber.replace(/^\**/, ""), - }; + return "*".repeat(4) + maskedNumber.substr(-4); + } + + static getMaskedNumber(number) { + return "*".repeat(4) + " " + number.substr(-4); + } + + static getLongMaskedNumber(number) { + return "*".repeat(number.length - 4) + number.substr(-4); } static getCreditCardLogo(network) { @@ -487,14 +492,6 @@ export class CreditCard { } } - static getMaskedNumber(number) { - return "*".repeat(4) + " " + number.substr(-4); - } - - static getLongMaskedNumber(number) { - return "*".repeat(number.length - 4) + number.substr(-4); - } - /* * Validates the number according to the Luhn algorithm. This * method does not throw an exception if the number is invalid. diff --git a/Client/Assets/CC_Script/CreditCardRecord.sys.mjs b/Client/Assets/CC_Script/CreditCardRecord.sys.mjs index 381dbc4bb4d3d..97235e8cddad7 100644 --- a/Client/Assets/CC_Script/CreditCardRecord.sys.mjs +++ b/Client/Assets/CC_Script/CreditCardRecord.sys.mjs @@ -20,19 +20,14 @@ export class CreditCardRecord { } static #normalizeCCNameFields(creditCard) { - if ( - creditCard["cc-given-name"] || - creditCard["cc-additional-name"] || - creditCard["cc-family-name"] - ) { - if (!creditCard["cc-name"]) { - creditCard["cc-name"] = FormAutofillNameUtils.joinNameParts({ - given: creditCard["cc-given-name"], - middle: creditCard["cc-additional-name"], - family: creditCard["cc-family-name"], - }); - } + if (!creditCard["cc-name"]) { + creditCard["cc-name"] = FormAutofillNameUtils.joinNameParts({ + given: creditCard["cc-given-name"] ?? "", + middle: creditCard["cc-additional-name"] ?? "", + family: creditCard["cc-family-name"] ?? "", + }); } + delete creditCard["cc-given-name"]; delete creditCard["cc-additional-name"]; delete creditCard["cc-family-name"]; diff --git a/Client/Assets/CC_Script/CreditCardRuleset.sys.mjs b/Client/Assets/CC_Script/CreditCardRuleset.sys.mjs index ac7a1445a08af..26651fe65ab59 100644 --- a/Client/Assets/CC_Script/CreditCardRuleset.sys.mjs +++ b/Client/Assets/CC_Script/CreditCardRuleset.sys.mjs @@ -78,6 +78,7 @@ var FathomHeuristicsRegExp = { "cc-name": // Firefox-specific rules "account.*holder.*name" + + "|^(credit[-\\s]?card|card).*name" + // de-DE "|^(kredit)?(karten|konto)inhaber" + "|^(name).*karte" + diff --git a/Client/Assets/CC_Script/FieldScanner.sys.mjs b/Client/Assets/CC_Script/FieldScanner.sys.mjs index 8f95d3949e9eb..2118de3de890d 100644 --- a/Client/Assets/CC_Script/FieldScanner.sys.mjs +++ b/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. @@ -32,6 +37,7 @@ export class FieldDetail { section = ""; addressType = ""; contactType = ""; + credentialType = ""; // When a field is split into N fields, we use part to record which field it is // For example, a credit card number field is split into 4 fields, the value of @@ -56,6 +62,7 @@ export class FieldDetail { this.section = autocompleteInfo.section; this.addressType = autocompleteInfo.addressType; this.contactType = autocompleteInfo.contactType; + this.credentialType = autocompleteInfo.credentialType; } else if (confidence) { this.reason = "fathom"; this.confidence = confidence; @@ -71,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/Client/Assets/CC_Script/FormAutofill.ios.sys.mjs b/Client/Assets/CC_Script/FormAutofill.ios.sys.mjs index 8e205c16c63cd..0b87fee30aae6 100644 --- a/Client/Assets/CC_Script/FormAutofill.ios.sys.mjs +++ b/Client/Assets/CC_Script/FormAutofill.ios.sys.mjs @@ -4,7 +4,7 @@ import { FormAutofill } from "resource://autofill/FormAutofill.sys.mjs"; -FormAutofill.defineLogGetter = (scope, logPrefix) => ({ +FormAutofill.defineLogGetter = (_scope, _logPrefix) => ({ // TODO: Bug 1828405. Explore how logging should be handled. // Maybe it makes more sense to do it on swift side and have JS just send messages. info: () => {}, diff --git a/Client/Assets/CC_Script/FormAutofill.sys.mjs b/Client/Assets/CC_Script/FormAutofill.sys.mjs index 368883a88e48a..8f50aad7bda26 100644 --- a/Client/Assets/CC_Script/FormAutofill.sys.mjs +++ b/Client/Assets/CC_Script/FormAutofill.sys.mjs @@ -4,8 +4,8 @@ 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 ADDRESSES_FIRST_TIME_USE_PREF = "extensions.formautofill.firstTimeUse"; const AUTOFILL_ADDRESSES_AVAILABLE_PREF = "extensions.formautofill.addresses.supported"; // This pref should be refactored after the migration of the old bool pref @@ -18,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 = @@ -33,14 +33,18 @@ 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_PREF = + "extensions.formautofill.heuristics.captureOnFormRemoval"; +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_PREF, + ENABLED_AUTOFILL_CAPTURE_ON_PAGE_NAVIGATION_PREF, ENABLED_AUTOFILL_CREDITCARDS_PREF, ENABLED_AUTOFILL_CREDITCARDS_REAUTH_PREF, - ADDRESSES_FIRST_TIME_USE_PREF, AUTOFILL_CREDITCARDS_AUTOCOMPLETE_OFF_PREF, AUTOFILL_ADDRESSES_AUTOCOMPLETE_OFF_PREF, @@ -77,7 +81,9 @@ export const FormAutofill = { return false; }, isAutofillAddressesAvailableInCountry(country) { - return FormAutofill._addressAutofillSupportedCountries.includes(country); + return FormAutofill._addressAutofillSupportedCountries.includes( + country.toUpperCase() + ); }, get isAutofillEnabled() { return this.isAutofillAddressesEnabled || this.isAutofillCreditCardsEnabled; @@ -97,14 +103,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. @@ -204,11 +221,6 @@ XPCOMUtils.defineLazyPreferenceGetter( "isAutofillAddressesCaptureEnabled", ENABLED_AUTOFILL_ADDRESSES_CAPTURE_PREF ); -XPCOMUtils.defineLazyPreferenceGetter( - FormAutofill, - "isAutofillAddressesCaptureV2Enabled", - ENABLED_AUTOFILL_ADDRESSES_CAPTURE_V2_PREF -); XPCOMUtils.defineLazyPreferenceGetter( FormAutofill, "_isAutofillCreditCardsAvailable", @@ -224,11 +236,6 @@ XPCOMUtils.defineLazyPreferenceGetter( "isAutofillCreditCardsHideUI", AUTOFILL_CREDITCARDS_HIDE_UI_PREF ); -XPCOMUtils.defineLazyPreferenceGetter( - FormAutofill, - "isAutofillAddressesFirstTimeUse", - ADDRESSES_FIRST_TIME_USE_PREF -); XPCOMUtils.defineLazyPreferenceGetter( FormAutofill, "_addressAutofillSupportedCountries", @@ -259,18 +266,31 @@ XPCOMUtils.defineLazyPreferenceGetter( "addressesAutocompleteOff", AUTOFILL_ADDRESSES_AUTOCOMPLETE_OFF_PREF ); +XPCOMUtils.defineLazyPreferenceGetter( + FormAutofill, + "captureOnFormRemoval", + ENABLED_AUTOFILL_CAPTURE_ON_FORM_REMOVAL_PREF +); +XPCOMUtils.defineLazyPreferenceGetter( + FormAutofill, + "captureOnPageNavigation", + 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/Client/Assets/CC_Script/FormAutofillChild.ios.sys.mjs b/Client/Assets/CC_Script/FormAutofillChild.ios.sys.mjs index 9f86742c4b37a..3183319fd9376 100644 --- a/Client/Assets/CC_Script/FormAutofillChild.ios.sys.mjs +++ b/Client/Assets/CC_Script/FormAutofillChild.ios.sys.mjs @@ -6,14 +6,25 @@ import { FormAutofillUtils } from "resource://gre/modules/shared/FormAutofillUtils.sys.mjs"; import { FormStateManager } from "resource://gre/modules/shared/FormStateManager.sys.mjs"; import { CreditCardRecord } from "resource://gre/modules/shared/CreditCardRecord.sys.mjs"; +import { AddressRecord } from "resource://gre/modules/shared/AddressRecord.sys.mjs"; export class FormAutofillChild { - constructor(onSubmitCallback, onAutofillCallback) { + /** + * Creates an instance of FormAutofillChild. + * + * @param {object} callbacks - An object containing callback functions. + * @param {object} callbacks.address - Callbacks related to addresses. + * @param {Function} callbacks.address.autofill - Function called to autofill address fields. + * @param {Function} callbacks.address.submit - Function called on address form submission. + * @param {object} callbacks.creditCard - Callbacks related to credit cards. + * @param {Function} callbacks.creditCard.autofill - Function called to autofill credit card fields. + * @param {Function} callbacks.creditCard.submit - Function called on credit card form submission. + */ + constructor(callbacks) { this.onFocusIn = this.onFocusIn.bind(this); this.onSubmit = this.onSubmit.bind(this); - this.onSubmitCallback = onSubmitCallback; - this.onAutofillCallback = onAutofillCallback; + this.callbacks = callbacks; this.fieldDetailsManager = new FormStateManager(); @@ -23,25 +34,41 @@ export class FormAutofillChild { _doIdentifyAutofillFields(element) { this.fieldDetailsManager.updateActiveInput(element); - const validDetails = - this.fieldDetailsManager.identifyAutofillFields(element); - - // Only ping swift if current field is a cc field - if (validDetails?.find(field => field.element === element)) { - const fieldNamesWithValues = validDetails?.reduce( - (acc, field) => ({ - ...acc, - [field.fieldName]: field.element.value, - }), - {} - ); + 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 (!activeFieldDetails?.find(field => field.element === element)) { + return; + } + + const fieldNamesWithValues = + this.transformToFieldNamesWithValues(activeFieldDetails); + if (FormAutofillUtils.isAddressField(activeFieldName)) { + this.callbacks.address.autofill(fieldNamesWithValues); + } else if (FormAutofillUtils.isCreditCardField(activeFieldName)) { // Normalize record format so we always get a consistent // credit card record format: {cc-number, cc-name, cc-exp-month, cc-exp-year} CreditCardRecord.normalizeFields(fieldNamesWithValues); - this.onAutofillCallback(fieldNamesWithValues); + this.callbacks.creditCard.autofill(fieldNamesWithValues); } } + transformToFieldNamesWithValues(details) { + return details?.reduce( + (acc, field) => ({ + ...acc, + [field.fieldName]: field.element.value, + }), + {} + ); + } + onFocusIn(evt) { const element = evt.target; this.fieldDetailsManager.updateActiveInput(element); @@ -51,21 +78,40 @@ export class FormAutofillChild { this._doIdentifyAutofillFields(element); } - onSubmit(evt) { + onSubmit(_event) { + 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 => { CreditCardRecord.normalizeFields(entry.record); return entry.record; }); - this.onSubmitCallback(creditCardRecords); + this.callbacks.creditCard.submit(creditCardRecords); } + + // TODO(FXSP-133 Phase 3): Support address capture + // this.callbacks.address.submit(); } fillFormFields(payload) { + // In iOS, we have access only to valid fields (https://github.com/mozilla/application-services/blob/9054db4bb5031881550ceab3448665ef6499a706/components/autofill/src/autofill.udl#L59-L76) for an address; + // all additional data must be computed. On Desktop, computed fields are handled in FormAutofillStorageBase.sys.mjs at the time of saving. Ideally, we should centralize + // all transformations, computations, and normalization processes within AddressRecord.sys.mjs to maintain a unified implementation across both platforms. + // This will be addressed in FXCM-810, aiming to simplify our data representation for both credit cards and addresses. + if ( + FormAutofillUtils.isAddressField( + this.fieldDetailsManager.activeFieldDetail?.fieldName + ) + ) { + AddressRecord.computeFields(payload); + } this.fieldDetailsManager.activeHandler.autofillFormFields(payload); } } diff --git a/Client/Assets/CC_Script/FormAutofillHandler.sys.mjs b/Client/Assets/CC_Script/FormAutofillHandler.sys.mjs index f81e69d8e05ec..49f79be77a937 100644 --- a/Client/Assets/CC_Script/FormAutofillHandler.sys.mjs +++ b/Client/Assets/CC_Script/FormAutofillHandler.sys.mjs @@ -65,9 +65,10 @@ export class FormAutofillHandler { * @param {FormLike} form Form that need to be auto filled * @param {Function} onFormSubmitted Function that can be invoked * to simulate form submission. Function is passed - * three arguments: (1) a FormLike for the form being - * submitted, (2) the corresponding Window, and (3) the - * responsible FormAutofillHandler. + * four arguments: (1) a FormLike for the form being + * submitted, (2) the reason for infering the form + * submission (3) the corresponding Window, and (4) + * the responsible FormAutofillHandler. * @param {Function} onAutofillCallback Function that can be invoked * when we want to suggest autofill on a form. */ @@ -91,8 +92,8 @@ export class FormAutofillHandler { * This function is used if the form handler (or one of its sections) * determines that it needs to act as if the form had been submitted. */ - this.onFormSubmitted = () => { - onFormSubmitted(this.form, this.window, this); + this.onFormSubmitted = formSubmissionReason => { + onFormSubmitted(this.form, formSubmissionReason, this.window, this); }; this.onAutofillCallback = onAutofillCallback; diff --git a/Client/Assets/CC_Script/FormAutofillHeuristics.sys.mjs b/Client/Assets/CC_Script/FormAutofillHeuristics.sys.mjs index da6671efc6e3a..fb96e47caefdf 100644 --- a/Client/Assets/CC_Script/FormAutofillHeuristics.sys.mjs +++ b/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. @@ -186,7 +182,7 @@ export const FormAutofillHeuristics = { * Return true if there is any field can be recognized in the parser, * otherwise false. */ - _parsePhoneFields(scanner, detail) { + _parsePhoneFields(scanner, _fieldDetail) { let matchingResult; const GRAMMARS = this.PHONE_FIELD_GRAMMARS; @@ -281,7 +277,7 @@ export const FormAutofillHeuristics = { * Return true if there is any field can be recognized in the parser, * otherwise false. */ - _parseStreetAddressFields(scanner, fieldDetail) { + _parseStreetAddressFields(scanner, _fieldDetail) { const INTERESTED_FIELDS = [ "street-address", "address-line1", @@ -510,12 +506,13 @@ export const FormAutofillHeuristics = { // We update the "name" fields to "cc-name" fields when the following // conditions are met: - // 1. The previous elements are identified as credit card fields and - // cc-number is in it - // 2. There is no "cc-name-*" fields in the previous credit card elements + // 1. The preceding fields are identified as credit card fields and + // contain the "cc-number" field. + // 2. No "cc-name-*" field is found among the preceding credit card fields. + // 3. The "cc-csc" field is not present among the preceding credit card fields. if ( ["cc-number"].some(f => prevCCFields.has(f)) && - !["cc-name", "cc-given-name", "cc-family-name"].some(f => + !["cc-name", "cc-given-name", "cc-family-name", "cc-csc"].some(f => prevCCFields.has(f) ) ) { @@ -550,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) @@ -599,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. * @@ -646,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 @@ -698,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; @@ -830,12 +799,13 @@ export const FormAutofillHeuristics = { } // At least two options match the country name, otherwise some state name might - // also match a country name, ex, Georgia + // also match a country name, ex, Georgia. We check the last two + // options rather than the first, as selects often start with a non-country display option. const countryDisplayNames = Array.from(FormAutofill.countries.values()); if ( options.length >= 2 && options - .slice(0, 2) + .slice(-2) .every( option => countryDisplayNames.includes(option.value) || diff --git a/Client/Assets/CC_Script/FormAutofillSection.sys.mjs b/Client/Assets/CC_Script/FormAutofillSection.sys.mjs index ab4ece49e2d83..aa4f79552188c 100644 --- a/Client/Assets/CC_Script/FormAutofillSection.sys.mjs +++ b/Client/Assets/CC_Script/FormAutofillSection.sys.mjs @@ -2,13 +2,12 @@ * 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/. */ -import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; import { FormAutofillUtils } from "resource://gre/modules/shared/FormAutofillUtils.sys.mjs"; 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", @@ -33,17 +32,6 @@ export class FormAutofillSection { this.handler = handler; this.filledRecordGUID = null; - ChromeUtils.defineLazyGetter(this, "reauthPasswordPromptMessage", () => { - const brandShortName = - FormAutofillUtils.brandBundle.GetStringFromName("brandShortName"); - // The string name for Mac is changed because the value needed updating. - const platform = AppConstants.platform.replace("macosx", "macos"); - return FormAutofillUtils.stringBundle.formatStringFromName( - `useCreditCardPasswordPrompt.${platform}`, - [brandShortName] - ); - }); - ChromeUtils.defineLazyGetter(this, "log", () => FormAutofill.defineLogGetter(this, "FormAutofillHandler") ); @@ -91,40 +79,40 @@ export class FormAutofillSection { * Examine the section is createable for storing the profile. This method * must be overrided. * - * @param {Object} record The record for examining createable + * @param {Object} _record The record for examining createable * @returns {boolean} True for the record is createable, otherwise false * */ - isRecordCreatable(record) { + isRecordCreatable(_record) { throw new TypeError("isRecordCreatable method must be overridden"); } /** * Override this method if the profile is needed to apply some transformers. * - * @param {object} profile + * @param {object} _profile * A profile should be converted based on the specific requirement. */ - applyTransformers(profile) {} + applyTransformers(_profile) {} /** * Override this method if the profile is needed to be customized for * previewing values. * - * @param {object} profile + * @param {object} _profile * A profile for pre-processing before previewing values. */ - preparePreviewProfile(profile) {} + preparePreviewProfile(_profile) {} /** * Override this method if the profile is needed to be customized for filling * values. * - * @param {object} profile + * @param {object} _profile * A profile for pre-processing before filling values. * @returns {boolean} Whether the profile should be filled. */ - async prepareFillingProfile(profile) { + async prepareFillingProfile(_profile) { return true; } @@ -148,15 +136,15 @@ export class FormAutofillSection { * specific case. Return the original value in the default case. * @param {String} value * The original field value. - * @param {Object} fieldDetail + * @param {Object} _fieldName * A fieldDetail of the related element. - * @param {HTMLElement} element + * @param {HTMLElement} _element * A element for checking converting value. * * @returns {String} * A string of the converted value. */ - computeFillingValue(value, fieldName, element) { + computeFillingValue(value, _fieldName, _element) { return value; } @@ -186,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); @@ -216,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]; + } } } } @@ -250,15 +241,16 @@ export class FormAutofillSection { // If this is an expiration field and our previous // adaptations haven't resulted in a string that is // short enough to satisfy the field length, and the - // field is constrained to a length of 5, then we + // field is constrained to a length of 4 or 5, then we // assume it is intended to hold an expiration of the - // form "MM/YY". - if (key == "cc-exp" && maxLength == 5) { + // form "MMYY" or "MM/YY". + if (key == "cc-exp" && (maxLength == 4 || maxLength == 5)) { const month2Digits = ( "0" + profile["cc-exp-month"].toString() ).slice(-2); const year2Digits = profile["cc-exp-year"].toString().slice(-2); - profile[key] = `${month2Digits}/${year2Digits}`; + const separator = maxLength == 5 ? "/" : ""; + profile[key] = `${month2Digits}${separator}${year2Digits}`; } else if (key == "cc-number") { // We want to show the last four digits of credit card so that // the masked credit card previews correctly and appears correctly @@ -334,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; @@ -420,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. @@ -433,7 +427,7 @@ export class FormAutofillSection { // Skip the field if the user has already entered text and that text is not the site prefilled value. continue; } - element.previewValue = value; + element.previewValue = value?.toString().replaceAll("*", "•"); this.handler.changeFieldState( fieldDetail, value ? FIELD_STATES.PREVIEW : FIELD_STATES.NORMAL @@ -853,6 +847,10 @@ export class FormAutofillAddressSection extends FormAutofillSection { value = FormAutofillUtils.getAbbreviatedSubregionName([value, text]) || text; } + } else if (fieldDetail.fieldName == "country") { + // This is a temporary fix. Ideally we should have either case-insensitive comparaison of country codes + // or handle this elsewhere see Bug 1889234 for more context. + value = value.toUpperCase(); } return value; } @@ -891,13 +889,16 @@ export class FormAutofillCreditCardSection extends FormAutofillSection { } } - _handlePageHide(event) { + _handlePageHide(_event) { this.handler.window.removeEventListener( "pagehide", this._handlePageHide.bind(this) ); this.log.debug("Credit card subframe is pagehideing", this.handler.form); - this.handler.onFormSubmitted(); + + const formSubmissionReason = + FormAutofillUtils.FORM_SUBMISSION_REASON.IFRAME_PAGEHIDE; + this.handler.onFormSubmitted(formSubmissionReason); } /** @@ -1272,12 +1273,16 @@ export class FormAutofillCreditCardSection extends FormAutofillSection { * @override */ async prepareFillingProfile(profile) { - // Prompt the OS login dialog to get the decrypted credit - // card number. + // Prompt the OS login dialog to get the decrypted credit card number. if (profile["cc-number-encrypted"]) { + const promptMessage = FormAutofillUtils.reauthOSPromptMessage( + "autofill-use-payment-method-os-prompt-macos", + "autofill-use-payment-method-os-prompt-windows", + "autofill-use-payment-method-os-prompt-other" + ); let decrypted = await this._decrypt( profile["cc-number-encrypted"], - this.reauthPasswordPromptMessage + promptMessage ); if (!decrypted) { @@ -1289,13 +1294,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/Client/Assets/CC_Script/FormAutofillUtils.sys.mjs b/Client/Assets/CC_Script/FormAutofillUtils.sys.mjs index 54756221c351b..b3979b1082092 100644 --- a/Client/Assets/CC_Script/FormAutofillUtils.sys.mjs +++ b/Client/Assets/CC_Script/FormAutofillUtils.sys.mjs @@ -4,6 +4,7 @@ import { FormAutofill } from "resource://autofill/FormAutofill.sys.mjs"; import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; +import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { @@ -11,24 +12,33 @@ 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", }); -export let FormAutofillUtils; +ChromeUtils.defineLazyGetter( + lazy, + "l10n", + () => + new Localization( + ["toolkit/formautofill/formAutofill.ftl", "branding/brand.ftl"], + true + ) +); -const ADDRESS_METADATA_PATH = "resource://autofill/addressmetadata/"; -const ADDRESS_REFERENCES = "addressReferences.js"; -const ADDRESS_REFERENCES_EXT = "addressReferencesExt.js"; +export let FormAutofillUtils; const ADDRESSES_COLLECTION_NAME = "addresses"; const CREDITCARDS_COLLECTION_NAME = "creditCards"; const MANAGE_ADDRESSES_L10N_IDS = [ - "autofill-add-new-address-title", + "autofill-add-address-title", "autofill-manage-addresses-title", ]; 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", @@ -39,10 +49,31 @@ 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-new-card-title", - "autofill-manage-credit-cards-title", + "autofill-add-card-title", + "autofill-manage-payment-methods-title", ]; const EDIT_CREDITCARD_L10N_IDS = [ "autofill-card-number", @@ -56,6 +87,12 @@ const FIELD_STATES = { AUTO_FILLED: "AUTO_FILLED", PREVIEW: "PREVIEW", }; +const FORM_SUBMISSION_REASON = { + FORM_SUBMIT_EVENT: "form-submit-event", + FORM_REMOVAL_AFTER_FETCH: "form-removal-after-fetch", + IFRAME_PAGEHIDE: "iframe-pagehide", + PAGE_NAVIGATION: "page-navigation", +}; const ELIGIBLE_INPUT_TYPES = ["text", "email", "tel", "number", "month"]; @@ -63,149 +100,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; @@ -219,6 +113,7 @@ FormAutofillUtils = { EDIT_CREDITCARD_L10N_IDS, MAX_FIELD_VALUE_LENGTH, FIELD_STATES, + FORM_SUBMISSION_REASON, _fieldNameInfo: { name: "name", @@ -303,6 +198,12 @@ FormAutofillUtils = { return Array.from(categories); }, + getCollectionNameFromFieldName(fieldName) { + return this.isCreditCardField(fieldName) + ? CREDITCARDS_COLLECTION_NAME + : ADDRESSES_COLLECTION_NAME; + }, + getAddressSeparator() { // The separator should be based on the L10N address format, and using a // white space is a temporary solution. @@ -319,7 +220,7 @@ FormAutofillUtils = { getAddressLabel(address) { // TODO: Implement a smarter way for deciding what to display // as option text. Possibly improve the algorithm in - // ProfileAutoCompleteResult.jsm and reuse it here. + // ProfileAutoCompleteResult.sys.mjs and reuse it here. let fieldOrder = [ "name", "-moz-street-address-one-line", // Street address @@ -438,7 +339,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, @@ -448,23 +353,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. * @@ -491,7 +379,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 @@ -507,21 +395,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; }, @@ -709,7 +599,7 @@ FormAutofillUtils = { return null; } - if (AddressDataLoader.getData(countryName)) { + if (lazy.AddressMetaDataLoader.getData(countryName)) { return countryName; } @@ -768,7 +658,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); @@ -842,13 +732,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; @@ -860,7 +750,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) @@ -895,7 +785,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, @@ -921,7 +811,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) @@ -937,6 +827,32 @@ FormAutofillUtils = { return null; }, + /** + * Find the option element from xul menu popups, as used in address capture + * doorhanger. + * + * This is a proxy to `findAddressSelectOption`, which expects HTML select + * DOM nodes and operates on options instead of xul menuitems. + * + * NOTE: This is a temporary solution until Bug 1886949 is landed. This + * method will then be removed `findAddressSelectOption` will be used + * directly. + * + * @param {XULPopupElement} menupopup + * @param {object} address + * @param {string} fieldName + * @returns {XULElement} + */ + findAddressSelectOptionWithMenuPopup(menupopup, address, fieldName) { + 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) { let oneDigitMonth = creditCard["cc-exp-month"] ? creditCard["cc-exp-month"].toString() @@ -1151,6 +1067,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, "-"); @@ -1181,6 +1177,35 @@ FormAutofillUtils = { }; return MAP[key]; }, + /** + * Generates the localized os dialog message that + * prompts the user to reauthenticate + * + * @param {string} msgMac fluent message id for macos clients + * @param {string} msgWin fluent message id for windows clients + * @param {string} msgOther fluent message id for other clients + * @param {string} msgLin (optional) fluent message id for linux clients + * @returns {string} localized os prompt message + */ + reauthOSPromptMessage(msgMac, msgWin, msgOther, msgLin = null) { + const platform = AppConstants.platform; + let messageID; + + switch (platform) { + case "win": + messageID = msgWin; + break; + case "macosx": + messageID = msgMac; + break; + case "linux": + messageID = msgLin ?? msgOther; + break; + default: + messageID = msgOther; + } + return lazy.l10n.formatValueSync(messageID); + }, }; ChromeUtils.defineLazyGetter(FormAutofillUtils, "stringBundle", function () { @@ -1236,20 +1261,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, @@ -1257,3 +1268,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/Client/Assets/CC_Script/FormStateManager.sys.mjs b/Client/Assets/CC_Script/FormStateManager.sys.mjs index 064b4e535625b..7481a5981c60c 100644 --- a/Client/Assets/CC_Script/FormStateManager.sys.mjs +++ b/Client/Assets/CC_Script/FormStateManager.sys.mjs @@ -150,7 +150,7 @@ export class FormStateManager { } didDestroy() { - this._activeItems = null; + this._activeItems = {}; } } diff --git a/Client/Assets/CC_Script/Helpers.ios.mjs b/Client/Assets/CC_Script/Helpers.ios.mjs index 9214a2767a3b4..83137331f1604 100644 --- a/Client/Assets/CC_Script/Helpers.ios.mjs +++ b/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() @@ -31,12 +45,6 @@ HTMLElement.prototype.getAutocompleteInfo = function () { }; }; -// Bug 1835024. Webkit doesn't support `checkVisibility` API -// https://drafts.csswg.org/cssom-view-1/#dom-element-checkvisibility -HTMLElement.prototype.checkVisibility = function (options) { - throw new Error(`Not implemented: WebKit doesn't support checkVisibility `); -}; - // This function helps us debug better when an error occurs because a certain mock is missing const withNotImplementedError = obj => new Proxy(obj, { @@ -50,6 +58,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,15 +95,12 @@ const internalModuleResolvers = { // Define mock for XPCOMUtils export const XPCOMUtils = withNotImplementedError({ - defineLazyGetter: (obj, prop, getFn) => { - obj[prop] = getFn?.call(obj); - }, defineLazyPreferenceGetter: ( obj, prop, pref, defaultValue = null, - onUpdate = null, + onUpdate, transform = val => val ) => { if (!Object.keys(IOSAppConstants.prefs).includes(pref)) { @@ -123,27 +137,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,19 +150,84 @@ 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( + JSON.stringify({ + type: "scalar", + object: 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( + JSON.stringify({ + 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; +// Define mock for Localization +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/Client/Assets/CC_Script/HeuristicsRegExp.sys.mjs b/Client/Assets/CC_Script/HeuristicsRegExp.sys.mjs index 536354ace9838..c4141628f8fe7 100644 --- a/Client/Assets/CC_Script/HeuristicsRegExp.sys.mjs +++ b/Client/Assets/CC_Script/HeuristicsRegExp.sys.mjs @@ -497,7 +497,7 @@ export const HeuristicsRegExp = { // ==== Name Fields ==== "cc-name": "card.?(?:holder|owner)|name.*(\\b)?on(\\b)?.*card" + - "|(?:card|cc).?name|cc.?full.?name" + + "|^(credit[-\\s]?card|card).*name|cc.?full.?name" + "|karteninhaber" + // de-DE "|nombre.*tarjeta" + // es "|nom.*carte" + // fr-FR diff --git a/Client/Assets/CC_Script/LoginManager.shared.mjs b/Client/Assets/CC_Script/LoginManager.shared.mjs index d50c53cbad74d..b0122f71265eb 100644 --- a/Client/Assets/CC_Script/LoginManager.shared.mjs +++ b/Client/Assets/CC_Script/LoginManager.shared.mjs @@ -34,7 +34,7 @@ class Logic { /** * Test whether associated labels of the element have the keyword. - * This is a simplified rule of hasLabelMatchingRegex in NewPasswordModel.jsm + * This is a simplified rule of hasLabelMatchingRegex in NewPasswordModel.sys.mjs */ static hasLabelMatchingRegex(element, regex) { return regex.test(element.labels?.[0]?.textContent); diff --git a/Client/Assets/CC_Script/Overrides.ios.js b/Client/Assets/CC_Script/Overrides.ios.js index a0023a267ccdc..ae5998992bf3f 100644 --- a/Client/Assets/CC_Script/Overrides.ios.js +++ b/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", diff --git a/Client/Assets/CC_Script/fathom.mjs b/Client/Assets/CC_Script/fathom.mjs index c1d984a9e3e2c..be60013261216 100644 --- a/Client/Assets/CC_Script/fathom.mjs +++ b/Client/Assets/CC_Script/fathom.mjs @@ -1,5 +1,5 @@ /* -DO NOT TOUCH fathom.jsm DIRECTLY. See the README for instructions. +DO NOT TOUCH fathom.mjs DIRECTLY. See the README for instructions. */ /* This Source Code Form is subject to the terms of the Mozilla Public