diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:13:27 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:13:27 +0000 |
commit | 40a355a42d4a9444dc753c04c6608dade2f06a23 (patch) | |
tree | 871fc667d2de662f171103ce5ec067014ef85e61 /toolkit/components/formautofill | |
parent | Adding upstream version 124.0.1. (diff) | |
download | firefox-adbda400be353e676059e335c3c0aaf99e719475.tar.xz firefox-adbda400be353e676059e335c3c0aaf99e719475.zip |
Adding upstream version 125.0.1.upstream/125.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/formautofill')
30 files changed, 889 insertions, 733 deletions
diff --git a/toolkit/components/formautofill/AutofillProfileAutoComplete.sys.mjs b/toolkit/components/formautofill/AutofillProfileAutoComplete.sys.mjs index 2d87f7931d..fc3f0454b0 100644 --- a/toolkit/components/formautofill/AutofillProfileAutoComplete.sys.mjs +++ b/toolkit/components/formautofill/AutofillProfileAutoComplete.sys.mjs @@ -349,7 +349,7 @@ export const ProfileAutocomplete = { Services.obs.removeObserver(this, "autocomplete-will-enter-text"); }, - async observe(subject, topic, data) { + async observe(_subject, topic, _data) { switch (topic) { case "autocomplete-will-enter-text": { if (!lazy.FormAutofillContent.activeInput) { diff --git a/toolkit/components/formautofill/Constants.ios.mjs b/toolkit/components/formautofill/Constants.ios.mjs index b78e47198d..290e690ea6 100644 --- a/toolkit/components/formautofill/Constants.ios.mjs +++ b/toolkit/components/formautofill/Constants.ios.mjs @@ -17,7 +17,7 @@ const IOS_DEFAULT_PREFERENCES = { "browser.search.region": "US", "extensions.formautofill.creditCards.supportedCountries": "US,CA,GB,FR,DE", "extensions.formautofill.addresses.enabled": true, - "extensions.formautofill.addresses.experiments.enabled": false, // TODO(FXCM-765): fetch this value from swift + "extensions.formautofill.addresses.experiments.enabled": true, "extensions.formautofill.addresses.capture.enabled": false, "extensions.formautofill.addresses.supportedCountries": "", "extensions.formautofill.creditCards.enabled": true, @@ -31,6 +31,7 @@ const IOS_DEFAULT_PREFERENCES = { "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/toolkit/components/formautofill/FormAutofill.ios.sys.mjs b/toolkit/components/formautofill/FormAutofill.ios.sys.mjs index 8e205c16c6..0b87fee30a 100644 --- a/toolkit/components/formautofill/FormAutofill.ios.sys.mjs +++ b/toolkit/components/formautofill/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/toolkit/components/formautofill/FormAutofill.sys.mjs b/toolkit/components/formautofill/FormAutofill.sys.mjs index 77502afbbe..8f50aad7bd 100644 --- a/toolkit/components/formautofill/FormAutofill.sys.mjs +++ b/toolkit/components/formautofill/FormAutofill.sys.mjs @@ -81,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; diff --git a/toolkit/components/formautofill/FormAutofillChild.ios.sys.mjs b/toolkit/components/formautofill/FormAutofillChild.ios.sys.mjs index 1aa713b5b7..3183319fd9 100644 --- a/toolkit/components/formautofill/FormAutofillChild.ios.sys.mjs +++ b/toolkit/components/formautofill/FormAutofillChild.ios.sys.mjs @@ -6,6 +6,7 @@ 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 { /** @@ -77,7 +78,7 @@ export class FormAutofillChild { this._doIdentifyAutofillFields(element); } - onSubmit(evt) { + onSubmit(_event) { if (!this.fieldDetailsManager.activeHandler) { return; } @@ -100,6 +101,17 @@ export class FormAutofillChild { } 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/toolkit/components/formautofill/FormAutofillChild.sys.mjs b/toolkit/components/formautofill/FormAutofillChild.sys.mjs index c40bfddbce..8678a7bd45 100644 --- a/toolkit/components/formautofill/FormAutofillChild.sys.mjs +++ b/toolkit/components/formautofill/FormAutofillChild.sys.mjs @@ -2,16 +2,36 @@ * 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 { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; +import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; + const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { AutoCompleteChild: "resource://gre/actors/AutoCompleteChild.sys.mjs", + AutofillTelemetry: "resource://gre/modules/shared/AutofillTelemetry.sys.mjs", FormAutofill: "resource://autofill/FormAutofill.sys.mjs", FormAutofillContent: "resource://autofill/FormAutofillContent.sys.mjs", FormAutofillUtils: "resource://gre/modules/shared/FormAutofillUtils.sys.mjs", + FormStateManager: "resource://gre/modules/shared/FormStateManager.sys.mjs", + PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", + ProfileAutocomplete: + "resource://autofill/AutofillProfileAutoComplete.sys.mjs", setTimeout: "resource://gre/modules/Timer.sys.mjs", + FORM_SUBMISSION_REASON: "resource://gre/actors/FormHandlerChild.sys.mjs", }); +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "DELEGATE_AUTOCOMPLETE", + "toolkit.autocomplete.delegate", + false +); + +const formFillController = Cc[ + "@mozilla.org/satchel/form-fill-controller;1" +].getService(Ci.nsIFormFillController); + const observer = { QueryInterface: ChromeUtils.generateQI([ "nsIWebProgressListener", @@ -31,7 +51,7 @@ const observer = { formAutofillChild.onPageNavigation(); }, - onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) { + onStateChange(aWebProgress, aRequest, aStateFlags, _aStatus) { if ( // if restoring a previously-rendered presentation (bfcache) aStateFlags & Ci.nsIWebProgressListener.STATE_RESTORING && @@ -77,21 +97,34 @@ export class FormAutofillChild extends JSWindowActorChild { constructor() { super(); + this.log = lazy.FormAutofill.defineLogGetter(this, "FormAutofillChild"); + this.debug("init"); + this._nextHandleElement = null; - this._alreadyDOMContentLoaded = false; this._hasDOMContentLoadedHandler = false; this._hasPendingTask = false; - this.testListener = null; + + // Flag indicating whether the form is waiting to be filled by Autofill. + this._autofillPending = false; + + /** + * @type {FormAutofillFieldDetailsManager} handling state management of current forms and handlers. + */ + this._fieldDetailsManager = new lazy.FormStateManager( + this.formSubmitted.bind(this), + this.formAutofilled.bind(this) + ); lazy.AutoCompleteChild.addPopupStateListener(this); } didDestroy() { + this._fieldDetailsManager.didDestroy(); + lazy.AutoCompleteChild.removePopupStateListener(this); - lazy.FormAutofillContent.didDestroy(); } - popupStateChanged(messageName, data, target) { + popupStateChanged(messageName, data, _target) { let docShell; try { docShell = this.docShell; @@ -108,50 +141,56 @@ export class FormAutofillChild extends JSWindowActorChild { switch (messageName) { case "FormAutoComplete:PopupClosed": { - lazy.FormAutofillContent.onPopupClosed(data.selectedRowStyle); + this.onPopupClosed(data.selectedRowStyle); Services.tm.dispatchToMainThread(() => { - chromeEventHandler.removeEventListener( - "keydown", - lazy.FormAutofillContent._onKeyDown, - true - ); + chromeEventHandler.removeEventListener("keydown", this, true); }); break; } case "FormAutoComplete:PopupOpened": { - lazy.FormAutofillContent.onPopupOpened(); - chromeEventHandler.addEventListener( - "keydown", - lazy.FormAutofillContent._onKeyDown, - true - ); + this.onPopupOpened(); + chromeEventHandler.addEventListener("keydown", this, true); break; } } } /** - * Invokes the FormAutofillContent to identify the autofill fields - * and consider opening the dropdown menu for the focused field - * + * Identifies and marks each autofill field */ - _doIdentifyAutofillFields() { + identifyAutofillFields() { if (this._hasPendingTask) { return; } this._hasPendingTask = true; lazy.setTimeout(() => { - const isAnyFieldIdentified = - lazy.FormAutofillContent.identifyAutofillFields( - this._nextHandleElement - ); - if (isAnyFieldIdentified) { + const element = this._nextHandleElement; + this.debug( + `identifyAutofillFields: ${element.ownerDocument.location?.hostname}` + ); + + if ( + lazy.DELEGATE_AUTOCOMPLETE || + !lazy.FormAutofillContent.savedFieldNames + ) { + this.debug("identifyAutofillFields: savedFieldNames are not known yet"); + + // Init can be asynchronous because we don't need anything from the parent + // at this point. + this.sendAsyncMessage("FormAutofill:InitStorage"); + } + + const validDetails = + this._fieldDetailsManager.identifyAutofillFields(element); + + validDetails?.forEach(detail => + this._markAsAutofillField(detail.element) + ); + if (validDetails.length) { if (lazy.FormAutofill.captureOnFormRemoval) { - this.registerDOMDocFetchSuccessEventListener( - this._nextHandleElement.ownerDocument - ); + this.registerDOMDocFetchSuccessEventListener(); } if (lazy.FormAutofill.captureOnPageNavigation) { this.registerProgressListener(); @@ -163,7 +202,7 @@ export class FormAutofillChild extends JSWindowActorChild { // This is for testing purpose only which sends a notification to indicate that the // form has been identified, and ready to open popup. this.sendAsyncMessage("FormAutofill:FieldsIdentified"); - lazy.FormAutofillContent.updateActiveInput(); + this.updateActiveInput(); }); } @@ -192,13 +231,18 @@ export class FormAutofillChild extends JSWindowActorChild { /** * After being notified of a page navigation, we check whether * the navigated window is the active window or one of its parents - * (active window = FormAutofillContent.activeHandler.window) + * (active window = activeHandler.window) * * @returns {boolean} whether the navigation affects the active window */ isActiveWindowNavigation() { - const activeWindow = lazy.FormAutofillContent.activeHandler.window; + const activeWindow = lazy.FormAutofillContent.activeHandler?.window; const navigatedWindow = this.document.defaultView; + + if (!activeWindow || !navigatedWindow) { + return false; + } + const navigatedBrowsingContext = BrowsingContext.getFromWindow(navigatedWindow); @@ -218,19 +262,23 @@ export class FormAutofillChild extends JSWindowActorChild { * Infer a form submission after document is navigated */ onPageNavigation() { - const activeElement = - lazy.FormAutofillContent.activeFieldDetail?.elementWeakRef.deref(); - if (!this.isActiveWindowNavigation()) { return; } - const formSubmissionReason = - lazy.FormAutofillUtils.FORM_SUBMISSION_REASON.PAGE_NAVIGATION; + // TODO: We should not use FormAutofillContent and let the + // parent decides which child to notify + const activeChild = lazy.FormAutofillContent.activeAutofillChild; + const activeElement = activeChild.activeFieldDetail?.elementWeakRef.deref(); + if (!activeElement) { + return; + } + + const formSubmissionReason = lazy.FORM_SUBMISSION_REASON.PAGE_NAVIGATION; // We only capture the form of the active field right now, // this means that we might miss some fields (see bug 1871356) - lazy.FormAutofillContent.formSubmitted(activeElement, formSubmissionReason); + activeChild.formSubmitted(activeElement, formSubmissionReason); } /** @@ -267,11 +315,9 @@ export class FormAutofillChild extends JSWindowActorChild { /** * After a focusin event and after we identify formautofill fields, * we set up an event listener for the DOMDocFetchSuccess event - * - * @param {Document} document The document we want to be notified by of a DOMDocFetchSuccess event */ - registerDOMDocFetchSuccessEventListener(document) { - document.setNotifyFetchSuccess(true); + registerDOMDocFetchSuccessEventListener() { + this.document.setNotifyFetchSuccess(true); // Is removed after a DOMDocFetchSuccess event (bug 1864855) /* eslint-disable mozilla/balanced-listeners */ @@ -284,11 +330,9 @@ export class FormAutofillChild extends JSWindowActorChild { /** * After a DOMDocFetchSuccess event, we register an event listener for the DOMFormRemoved event - * - * @param {Document} document The document we want to be notified by of a DOMFormRemoved event */ - registerDOMFormRemovedEventListener(document) { - document.setNotifyFormOrPasswordRemoved(true); + registerDOMFormRemovedEventListener() { + this.document.setNotifyFormOrPasswordRemoved(true); // Is removed after a DOMFormRemoved event (bug 1864855) /* eslint-disable mozilla/balanced-listeners */ @@ -301,11 +345,9 @@ export class FormAutofillChild extends JSWindowActorChild { /** * After a DOMDocFetchSuccess event we remove the DOMDocFetchSuccess event listener - * - * @param {Document} document The document we are notified by of a DOMDocFetchSuccess event */ - unregisterDOMDocFetchSuccessEventListener(document) { - document.setNotifyFetchSuccess(false); + unregisterDOMDocFetchSuccessEventListener() { + this.document.setNotifyFetchSuccess(false); this.docShell.chromeEventHandler.removeEventListener( "DOMDocFetchSuccess", this @@ -314,11 +356,9 @@ export class FormAutofillChild extends JSWindowActorChild { /** * After a DOMFormRemoved event we remove the DOMFormRemoved event listener - * - * @param {Document} document The document we are notified by of a DOMFormRemoved event */ - unregisterDOMFormRemovedEventListener(document) { - document.setNotifyFormOrPasswordRemoved(false); + unregisterDOMFormRemovedEventListener() { + this.document.setNotifyFormOrPasswordRemoved(false); this.docShell.chromeEventHandler.removeEventListener( "DOMFormRemoved", this @@ -327,11 +367,7 @@ export class FormAutofillChild extends JSWindowActorChild { shouldIgnoreFormAutofillEvent(event) { let nodePrincipal = event.target.nodePrincipal; - return ( - nodePrincipal.isSystemPrincipal || - nodePrincipal.isNullPrincipal || - nodePrincipal.schemeIs("about") - ); + return nodePrincipal.isSystemPrincipal || nodePrincipal.schemeIs("about"); } handleEvent(evt) { @@ -342,16 +378,20 @@ export class FormAutofillChild extends JSWindowActorChild { return; } + if (!this.windowContext) { + // !this.windowContext must not be null, because we need the + // windowContext and/or docShell to (un)register form submission listeners + return; + } + switch (evt.type) { - case "focusin": { - if (lazy.FormAutofill.isAutofillEnabled) { - this.onFocusIn(evt); - } + case "keydown": { + this._onKeyDown(evt); break; } - case "DOMFormBeforeSubmit": { + case "focusin": { if (lazy.FormAutofill.isAutofillEnabled) { - this.onDOMFormBeforeSubmit(evt); + this.onFocusIn(evt); } break; } @@ -360,7 +400,13 @@ export class FormAutofillChild extends JSWindowActorChild { break; } case "DOMDocFetchSuccess": { - this.onDOMDocFetchSuccess(evt); + this.onDOMDocFetchSuccess(); + break; + } + case "form-submission-detected": { + if (lazy.FormAutofill.isAutofillEnabled) { + this.onFormSubmission(evt); + } break; } @@ -371,45 +417,42 @@ export class FormAutofillChild extends JSWindowActorChild { } onFocusIn(evt) { - lazy.FormAutofillContent.updateActiveInput(); + this.updateActiveInput(); - let element = evt.target; + const element = evt.target; if (!lazy.FormAutofillUtils.isCreditCardOrAddressFieldType(element)) { return; } - this._nextHandleElement = element; - if (!this._alreadyDOMContentLoaded) { - let doc = element.ownerDocument; - if (doc.readyState === "loading") { - if (!this._hasDOMContentLoadedHandler) { - this._hasDOMContentLoadedHandler = true; - doc.addEventListener( - "DOMContentLoaded", - () => this._doIdentifyAutofillFields(), - { once: true } - ); - } - return; + this._nextHandleElement = element; + const doc = element.ownerDocument; + if (doc.readyState === "loading") { + // For auto-focused input, we might receive focus event before document becomes ready. + // When this happens, run field identification after receiving `DOMContentLoaded` event + if (!this._hasDOMContentLoadedHandler) { + this._hasDOMContentLoadedHandler = true; + doc.addEventListener( + "DOMContentLoaded", + () => this.identifyAutofillFields(), + { once: true } + ); } - this._alreadyDOMContentLoaded = true; + return; } - this._doIdentifyAutofillFields(); + this.identifyAutofillFields(); } /** - * Handle the DOMFormBeforeSubmit event. + * Handle form-submission-detected event (dispatched by FormHandlerChild) * - * @param {Event} evt + * @param {CustomEvent} evt form-submission-detected event */ - onDOMFormBeforeSubmit(evt) { - const formElement = evt.target; - - const formSubmissionReason = - lazy.FormAutofillUtils.FORM_SUBMISSION_REASON.FORM_SUBMIT_EVENT; + onFormSubmission(evt) { + const formElement = evt.detail.form; + const formSubmissionReason = evt.detail.reason; - lazy.FormAutofillContent.formSubmitted(formElement, formSubmissionReason); + this.formSubmitted(formElement, formSubmissionReason); } /** @@ -421,14 +464,10 @@ export class FormAutofillChild extends JSWindowActorChild { * @param {Event} evt DOMFormRemoved */ onDOMFormRemoved(evt) { - const document = evt.composedTarget.ownerDocument; - const formSubmissionReason = - lazy.FormAutofillUtils.FORM_SUBMISSION_REASON.FORM_REMOVAL_AFTER_FETCH; - - lazy.FormAutofillContent.formSubmitted(evt.target, formSubmissionReason); + lazy.FORM_SUBMISSION_REASON.FORM_REMOVAL_AFTER_FETCH; - this.unregisterDOMFormRemovedEventListener(document); + this.formSubmitted(evt.target, formSubmissionReason); } /** @@ -436,15 +475,21 @@ export class FormAutofillChild extends JSWindowActorChild { * * Sets up an event listener for the DOMFormRemoved event * and unregisters the event listener for DOMDocFetchSuccess event. - * - * @param {Event} evt DOMDocFetchSuccess */ - onDOMDocFetchSuccess(evt) { - const document = evt.target; + onDOMDocFetchSuccess() { + this.registerDOMFormRemovedEventListener(); - this.registerDOMFormRemovedEventListener(document); + this.unregisterDOMDocFetchSuccessEventListener(); + } - this.unregisterDOMDocFetchSuccessEventListener(document); + /** + * Unregister all listeners that notify of a form submission, + * because we just detected and acted on a form submission + */ + unregisterFormSubmissionListeners() { + this.unregisterDOMDocFetchSuccessEventListener(); + this.unregisterDOMFormRemovedEventListener(); + this.unregisterProgressListener(); } receiveMessage(message) { @@ -456,17 +501,284 @@ export class FormAutofillChild extends JSWindowActorChild { switch (message.name) { case "FormAutofill:PreviewProfile": { - lazy.FormAutofillContent.previewProfile(doc); + this.previewProfile(doc); break; } case "FormAutofill:ClearForm": { - lazy.FormAutofillContent.clearForm(); + this.clearForm(); break; } case "FormAutofill:FillForm": { - lazy.FormAutofillContent.activeHandler.autofillFormFields(message.data); + this.activeHandler.autofillFormFields(message.data); break; } } } + + get activeFieldDetail() { + return this._fieldDetailsManager.activeFieldDetail; + } + + get activeFormDetails() { + return this._fieldDetailsManager.activeFormDetails; + } + + get activeInput() { + return this._fieldDetailsManager.activeInput; + } + + get activeHandler() { + return this._fieldDetailsManager.activeHandler; + } + + get activeSection() { + return this._fieldDetailsManager.activeSection; + } + + /** + * Handle a form submission and early return when: + * 1. In private browsing mode. + * 2. Could not map any autofill handler by form element. + * 3. Number of filled fields is less than autofill threshold + * + * @param {HTMLElement} formElement Root element which receives submit event. + * @param {string} formSubmissionReason Reason for invoking the form submission + * (see options for FORM_SUBMISSION_REASON in FormAutofillUtils)) + * @param {Window} domWin Content window; passed for unit tests and when + * invoked by the FormAutofillSection + * @param {object} handler FormAutofillHander, if known by caller + */ + formSubmitted( + formElement, + formSubmissionReason, + domWin = formElement.ownerGlobal, + handler = undefined + ) { + this.debug(`Handling form submission - infered by ${formSubmissionReason}`); + + lazy.AutofillTelemetry.recordFormSubmissionHeuristicCount( + formSubmissionReason + ); + + if (!lazy.FormAutofill.isAutofillEnabled) { + this.debug("Form Autofill is disabled"); + return; + } + + // The `domWin` truthiness test is used by unit tests to bypass this check. + if (domWin && lazy.PrivateBrowsingUtils.isContentWindowPrivate(domWin)) { + this.debug("Ignoring submission in a private window"); + return; + } + + handler = handler || this._fieldDetailsManager._getFormHandler(formElement); + const records = this._fieldDetailsManager.getRecords(formElement, handler); + + if (!records || !handler) { + this.debug("Form element could not map to an existing handler"); + return; + } + + // Unregister the form submission listeners after handling a form submission + this.debug("Unregistering form submission listeners"); + this.unregisterFormSubmissionListeners(); + + [records.address, records.creditCard].forEach((rs, idx) => { + lazy.AutofillTelemetry.recordSubmittedSectionCount( + idx == 0 + ? lazy.AutofillTelemetry.ADDRESS + : lazy.AutofillTelemetry.CREDIT_CARD, + rs?.length + ); + + rs?.forEach(r => { + lazy.AutofillTelemetry.recordFormInteractionEvent( + "submitted", + r.section, + { + record: r, + form: handler.form, + } + ); + delete r.section; + }); + }); + + this.sendAsyncMessage("FormAutofill:OnFormSubmit", records); + } + + formAutofilled() { + lazy.FormAutofillContent.showPopup(); + } + + /** + * All active items should be updated according the active element of + * `formFillController.focusedInput`. All of them including element, + * handler, section, and field detail, can be retrieved by their own getters. + * + * @param {HTMLElement|null} element The active item should be updated based + * on this or `formFillController.focusedInput` will be taken. + */ + updateActiveInput(element) { + element = element || formFillController.focusedInput; + if (!element) { + this.debug("updateActiveElement: no element selected"); + return; + } + lazy.FormAutofillContent.updateActiveAutofillChild(this); + + this._fieldDetailsManager.updateActiveInput(element); + this.debug("updateActiveElement: checking for popup-on-focus"); + // We know this element just received focus. If it's a credit card field, + // open its popup. + if (this._autofillPending) { + this.debug("updateActiveElement: skipping check; autofill is imminent"); + } else if (element.value?.length !== 0) { + this.debug( + `updateActiveElement: Not opening popup because field is not empty.` + ); + } else { + this.debug( + "updateActiveElement: checking if empty field is cc-*: ", + this.activeFieldDetail?.fieldName + ); + + if ( + this.activeFieldDetail?.fieldName?.startsWith("cc-") || + AppConstants.platform === "android" + ) { + lazy.FormAutofillContent.showPopup(); + } + } + } + + set autofillPending(flag) { + this.debug("Setting autofillPending to", flag); + this._autofillPending = flag; + } + + clearForm() { + let focusedInput = + this.activeInput || + lazy.ProfileAutocomplete._lastAutoCompleteFocusedInput; + if (!focusedInput) { + return; + } + + this.activeSection.clearPopulatedForm(); + + let fieldName = this.activeFieldDetail?.fieldName; + if (lazy.FormAutofillUtils.isCreditCardField(fieldName)) { + lazy.AutofillTelemetry.recordFormInteractionEvent( + "cleared", + this.activeSection, + { fieldName } + ); + } + } + + previewProfile(doc) { + let docWin = doc.ownerGlobal; + let selectedIndex = lazy.ProfileAutocomplete._getSelectedIndex(docWin); + let lastAutoCompleteResult = + lazy.ProfileAutocomplete.lastProfileAutoCompleteResult; + let focusedInput = this.activeInput; + + if ( + selectedIndex === -1 || + !focusedInput || + !lastAutoCompleteResult || + lastAutoCompleteResult.getStyleAt(selectedIndex) != "autofill-profile" + ) { + this.sendAsyncMessage("FormAutofill:UpdateWarningMessage", {}); + + lazy.ProfileAutocomplete._clearProfilePreview(); + } else { + let focusedInputDetails = this.activeFieldDetail; + let profile = JSON.parse( + lastAutoCompleteResult.getCommentAt(selectedIndex) + ); + let allFieldNames = this.activeSection.allFieldNames; + let profileFields = allFieldNames.filter( + fieldName => !!profile[fieldName] + ); + + let focusedCategory = lazy.FormAutofillUtils.getCategoryFromFieldName( + focusedInputDetails.fieldName + ); + let categories = + lazy.FormAutofillUtils.getCategoriesFromFieldNames(profileFields); + this.sendAsyncMessage("FormAutofill:UpdateWarningMessage", { + focusedCategory, + categories, + }); + + lazy.ProfileAutocomplete._previewSelectedProfile(selectedIndex); + } + } + + onPopupClosed(selectedRowStyle) { + this.debug("Popup has closed."); + lazy.ProfileAutocomplete._clearProfilePreview(); + + let lastAutoCompleteResult = + lazy.ProfileAutocomplete.lastProfileAutoCompleteResult; + let focusedInput = this.activeInput; + if ( + lastAutoCompleteResult && + this._keyDownEnterForInput && + focusedInput === this._keyDownEnterForInput && + focusedInput === + lazy.ProfileAutocomplete.lastProfileAutoCompleteFocusedInput + ) { + if (selectedRowStyle == "autofill-footer") { + this.sendAsyncMessage("FormAutofill:OpenPreferences"); + } else if (selectedRowStyle == "autofill-clear-button") { + this.clearForm(); + } + } + } + + onPopupOpened() { + this.debug( + "Popup has opened, automatic =", + formFillController.passwordPopupAutomaticallyOpened + ); + + let fieldName = this.activeFieldDetail?.fieldName; + if (fieldName && this.activeSection) { + lazy.AutofillTelemetry.recordFormInteractionEvent( + "popup_shown", + this.activeSection, + { fieldName } + ); + } + } + + _markAsAutofillField(field) { + // Since Form Autofill popup is only for input element, any non-Input + // element should be excluded here. + if (!HTMLInputElement.isInstance(field)) { + return; + } + + formFillController.markAsAutofillField(field); + } + + _onKeyDown(e) { + delete this._keyDownEnterForInput; + let lastAutoCompleteResult = + lazy.ProfileAutocomplete.lastProfileAutoCompleteResult; + let focusedInput = this.activeInput; + if ( + e.keyCode != e.DOM_VK_RETURN || + !lastAutoCompleteResult || + !focusedInput || + focusedInput != + lazy.ProfileAutocomplete.lastProfileAutoCompleteFocusedInput + ) { + return; + } + this._keyDownEnterForInput = focusedInput; + } } diff --git a/toolkit/components/formautofill/FormAutofillContent.sys.mjs b/toolkit/components/formautofill/FormAutofillContent.sys.mjs index 133e5e1d0a..c07e0d67b3 100644 --- a/toolkit/components/formautofill/FormAutofillContent.sys.mjs +++ b/toolkit/components/formautofill/FormAutofillContent.sys.mjs @@ -8,43 +8,18 @@ /* eslint-disable no-use-before-define */ -import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; -import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; - const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { FormAutofill: "resource://autofill/FormAutofill.sys.mjs", - FormAutofillUtils: "resource://gre/modules/shared/FormAutofillUtils.sys.mjs", - PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", - FormStateManager: "resource://gre/modules/shared/FormStateManager.sys.mjs", ProfileAutocomplete: "resource://autofill/AutofillProfileAutoComplete.sys.mjs", - AutofillTelemetry: "resource://autofill/AutofillTelemetry.sys.mjs", }); -XPCOMUtils.defineLazyPreferenceGetter( - lazy, - "DELEGATE_AUTOCOMPLETE", - "toolkit.autocomplete.delegate", - false -); - const formFillController = Cc[ "@mozilla.org/satchel/form-fill-controller;1" ].getService(Ci.nsIFormFillController); -function getActorFromWindow(contentWindow, name = "FormAutofill") { - // In unit tests, contentWindow isn't a real window. - if (!contentWindow) { - return null; - } - - return contentWindow.windowGlobalChild - ? contentWindow.windowGlobalChild.getActor(name) - : null; -} - /** * Handles content's interactions for the process. * @@ -63,12 +38,6 @@ export var FormAutofillContent = { */ _popupPending: false, - /** - * @type {boolean} Flag indicating whether the form is waiting to be - * filled by Autofill. - */ - _autofillPending: false, - init() { this.log = lazy.FormAutofill.defineLogGetter(this, "FormAutofillContent"); this.debug("init"); @@ -76,11 +45,13 @@ export var FormAutofillContent = { // eslint-disable-next-line mozilla/balanced-listeners Services.cpmm.sharedData.addEventListener("change", this); - let autofillEnabled = Services.cpmm.sharedData.get("FormAutofill:enabled"); + const autofillEnabled = Services.cpmm.sharedData.get( + "FormAutofill:enabled" + ); // If storage hasn't be initialized yet autofillEnabled is undefined but we need to ensure // autocomplete is registered before the focusin so register it in this case as long as the // pref is true. - let shouldEnableAutofill = + const shouldEnableAutofill = autofillEnabled === undefined && (lazy.FormAutofill.isAutofillAddressesEnabled || lazy.FormAutofill.isAutofillCreditCardsEnabled); @@ -88,120 +59,49 @@ export var FormAutofillContent = { lazy.ProfileAutocomplete.ensureRegistered(); } - /** - * @type {FormAutofillFieldDetailsManager} handling state management of current forms and handlers. - */ - this._fieldDetailsManager = new lazy.FormStateManager( - this.formSubmitted.bind(this), - this._showPopup.bind(this) - ); + this.activeAutofillChild = null; }, get activeFieldDetail() { - return this._fieldDetailsManager.activeFieldDetail; + return this.activeAutofillChild?.activeFieldDetail; }, get activeFormDetails() { - return this._fieldDetailsManager.activeFormDetails; + return this.activeAutofillChild?.activeFormDetails; }, get activeInput() { - return this._fieldDetailsManager.activeInput; + return this.activeAutofillChild?.activeInput; }, get activeHandler() { - return this._fieldDetailsManager.activeHandler; + return this.activeAutofillChild?.activeHandler; }, get activeSection() { - return this._fieldDetailsManager.activeSection; - }, - - /** - * Send the profile to parent for doorhanger and storage saving/updating. - * - * @param {object} profile Submitted form's address/creditcard guid and record. - * @param {object} domWin Current content window. - */ - _onFormSubmit(profile, domWin) { - let actor = getActorFromWindow(domWin); - actor.sendAsyncMessage("FormAutofill:OnFormSubmit", profile); + return this.activeAutofillChild?.activeSection; }, - /** - * Handle a form submission and early return when: - * 1. In private browsing mode. - * 2. Could not map any autofill handler by form element. - * 3. Number of filled fields is less than autofill threshold - * - * @param {HTMLElement} formElement Root element which receives submit event. - * @param {string} formSubmissionReason Reason for invoking the form submission - * (see options for FORM_SUBMISSION_REASON in FormAutofillUtils)) - * @param {Window} domWin Content window; passed for unit tests and when - * invoked by the FormAutofillSection - * @param {object} handler FormAutofillHander, if known by caller - */ - formSubmitted( - formElement, - formSubmissionReason, - domWin = formElement.ownerGlobal, - handler = undefined - ) { - this.debug(`Handling form submission - infered by ${formSubmissionReason}`); - - // Unregister the progress listener since we detected a form submission - // (domWin is null in unit tests) - getActorFromWindow(domWin)?.unregisterProgressListener(); - - lazy.AutofillTelemetry.recordFormSubmissionHeuristicCount( - formSubmissionReason - ); - - if (!lazy.FormAutofill.isAutofillEnabled) { - this.debug("Form Autofill is disabled"); - return; - } - - // The `domWin` truthiness test is used by unit tests to bypass this check. - if (domWin && lazy.PrivateBrowsingUtils.isContentWindowPrivate(domWin)) { - this.debug("Ignoring submission in a private window"); - return; - } - - handler = handler || this._fieldDetailsManager._getFormHandler(formElement); - const records = this._fieldDetailsManager.getRecords(formElement, handler); - - if (!records || !handler) { - this.debug("Form element could not map to an existing handler"); - return; + set autofillPending(flag) { + if (this.activeAutofillChild) { + this.activeAutofillChild.autofillPending = flag; } + }, - [records.address, records.creditCard].forEach((rs, idx) => { - lazy.AutofillTelemetry.recordSubmittedSectionCount( - idx == 0 - ? lazy.AutofillTelemetry.ADDRESS - : lazy.AutofillTelemetry.CREDIT_CARD, - rs?.length - ); - - rs?.forEach(r => { - lazy.AutofillTelemetry.recordFormInteractionEvent( - "submitted", - r.section, - { - record: r, - form: handler.form, - } - ); - delete r.section; - }); - }); - - this._onFormSubmit(records, domWin); + updateActiveAutofillChild(autofillChild) { + this.activeAutofillChild = autofillChild; }, - _showPopup() { - formFillController.showPopup(); + showPopup() { + if (Services.cpmm.sharedData.get("FormAutofill:enabled")) { + this.debug("updateActiveElement: opening pop up"); + formFillController.showPopup(); + } else { + this.debug( + "updateActiveElement: Deferring pop-up until Autofill is ready" + ); + this._popupPending = true; + } }, handleEvent(evt) { @@ -215,7 +115,7 @@ export var FormAutofillContent = { if (this._popupPending) { this._popupPending = false; this.debug("handleEvent: Opening deferred popup"); - this._showPopup(); + formFillController.showPopup(); } } else { lazy.ProfileAutocomplete.ensureUnregistered(); @@ -224,219 +124,6 @@ export var FormAutofillContent = { } } }, - - /** - * All active items should be updated according the active element of - * `formFillController.focusedInput`. All of them including element, - * handler, section, and field detail, can be retrieved by their own getters. - * - * @param {HTMLElement|null} element The active item should be updated based - * on this or `formFillController.focusedInput` will be taken. - */ - updateActiveInput(element) { - element = element || formFillController.focusedInput; - if (!element) { - this.debug("updateActiveElement: no element selected"); - return; - } - this._fieldDetailsManager.updateActiveInput(element); - this.debug("updateActiveElement: checking for popup-on-focus"); - // We know this element just received focus. If it's a credit card field, - // open its popup. - if (this._autofillPending) { - this.debug("updateActiveElement: skipping check; autofill is imminent"); - } else if (element.value?.length !== 0) { - this.debug( - `updateActiveElement: Not opening popup because field is not empty.` - ); - } else { - this.debug( - "updateActiveElement: checking if empty field is cc-*: ", - this.activeFieldDetail?.fieldName - ); - - if ( - this.activeFieldDetail?.fieldName?.startsWith("cc-") || - AppConstants.platform === "android" - ) { - if (Services.cpmm.sharedData.get("FormAutofill:enabled")) { - this.debug("updateActiveElement: opening pop up"); - this._showPopup(); - } else { - this.debug( - "updateActiveElement: Deferring pop-up until Autofill is ready" - ); - this._popupPending = true; - } - } - } - }, - - set autofillPending(flag) { - this.debug("Setting autofillPending to", flag); - this._autofillPending = flag; - }, - - /** - * Identifies and marks each autofill field - * - * @param {HTMLElement} element - * Element that serves as an anchor for the formautofill heuristics to retrieve - * the root form and run the formautofill heuristics on the form elements - * @returns {boolean} - * whether any autofill fields were identified - */ - identifyAutofillFields(element) { - this.debug( - `identifyAutofillFields: ${element.ownerDocument.location?.hostname}` - ); - - if (lazy.DELEGATE_AUTOCOMPLETE || !this.savedFieldNames) { - this.debug("identifyAutofillFields: savedFieldNames are not known yet"); - let actor = getActorFromWindow(element.ownerGlobal); - if (actor) { - actor.sendAsyncMessage("FormAutofill:InitStorage"); - } - } - - const validDetails = - this._fieldDetailsManager.identifyAutofillFields(element); - - validDetails?.forEach(detail => this._markAsAutofillField(detail.element)); - - return !!validDetails.length; - }, - - clearForm() { - let focusedInput = - this.activeInput || - lazy.ProfileAutocomplete._lastAutoCompleteFocusedInput; - if (!focusedInput) { - return; - } - - this.activeSection.clearPopulatedForm(); - - let fieldName = FormAutofillContent.activeFieldDetail?.fieldName; - if (lazy.FormAutofillUtils.isCreditCardField(fieldName)) { - lazy.AutofillTelemetry.recordFormInteractionEvent( - "cleared", - this.activeSection, - { fieldName } - ); - } - }, - - previewProfile(doc) { - let docWin = doc.ownerGlobal; - let selectedIndex = lazy.ProfileAutocomplete._getSelectedIndex(docWin); - let lastAutoCompleteResult = - lazy.ProfileAutocomplete.lastProfileAutoCompleteResult; - let focusedInput = this.activeInput; - let actor = getActorFromWindow(docWin); - - if ( - selectedIndex === -1 || - !focusedInput || - !lastAutoCompleteResult || - lastAutoCompleteResult.getStyleAt(selectedIndex) != "autofill-profile" - ) { - actor.sendAsyncMessage("FormAutofill:UpdateWarningMessage", {}); - - lazy.ProfileAutocomplete._clearProfilePreview(); - } else { - let focusedInputDetails = this.activeFieldDetail; - let profile = JSON.parse( - lastAutoCompleteResult.getCommentAt(selectedIndex) - ); - let allFieldNames = FormAutofillContent.activeSection.allFieldNames; - let profileFields = allFieldNames.filter( - fieldName => !!profile[fieldName] - ); - - let focusedCategory = lazy.FormAutofillUtils.getCategoryFromFieldName( - focusedInputDetails.fieldName - ); - let categories = - lazy.FormAutofillUtils.getCategoriesFromFieldNames(profileFields); - actor.sendAsyncMessage("FormAutofill:UpdateWarningMessage", { - focusedCategory, - categories, - }); - - lazy.ProfileAutocomplete._previewSelectedProfile(selectedIndex); - } - }, - - onPopupClosed(selectedRowStyle) { - this.debug("Popup has closed."); - lazy.ProfileAutocomplete._clearProfilePreview(); - - let lastAutoCompleteResult = - lazy.ProfileAutocomplete.lastProfileAutoCompleteResult; - let focusedInput = FormAutofillContent.activeInput; - if ( - lastAutoCompleteResult && - FormAutofillContent._keyDownEnterForInput && - focusedInput === FormAutofillContent._keyDownEnterForInput && - focusedInput === - lazy.ProfileAutocomplete.lastProfileAutoCompleteFocusedInput - ) { - if (selectedRowStyle == "autofill-footer") { - let actor = getActorFromWindow(focusedInput.ownerGlobal); - actor.sendAsyncMessage("FormAutofill:OpenPreferences"); - } else if (selectedRowStyle == "autofill-clear-button") { - FormAutofillContent.clearForm(); - } - } - }, - - onPopupOpened() { - this.debug( - "Popup has opened, automatic =", - formFillController.passwordPopupAutomaticallyOpened - ); - - let fieldName = FormAutofillContent.activeFieldDetail?.fieldName; - if (fieldName && this.activeSection) { - lazy.AutofillTelemetry.recordFormInteractionEvent( - "popup_shown", - this.activeSection, - { fieldName } - ); - } - }, - - _markAsAutofillField(field) { - // Since Form Autofill popup is only for input element, any non-Input - // element should be excluded here. - if (!HTMLInputElement.isInstance(field)) { - return; - } - - formFillController.markAsAutofillField(field); - }, - - _onKeyDown(e) { - delete FormAutofillContent._keyDownEnterForInput; - let lastAutoCompleteResult = - lazy.ProfileAutocomplete.lastProfileAutoCompleteResult; - let focusedInput = FormAutofillContent.activeInput; - if ( - e.keyCode != e.DOM_VK_RETURN || - !lastAutoCompleteResult || - !focusedInput || - focusedInput != - lazy.ProfileAutocomplete.lastProfileAutoCompleteFocusedInput - ) { - return; - } - FormAutofillContent._keyDownEnterForInput = focusedInput; - }, - - didDestroy() { - this._fieldDetailsManager.didDestroy(); - }, }; FormAutofillContent.init(); diff --git a/toolkit/components/formautofill/FormAutofillNative.cpp b/toolkit/components/formautofill/FormAutofillNative.cpp index 57af789861..08c462aa44 100644 --- a/toolkit/components/formautofill/FormAutofillNative.cpp +++ b/toolkit/components/formautofill/FormAutofillNative.cpp @@ -200,14 +200,16 @@ enum class CCExpYearParams : uint8_t { }; struct AutofillParams { - EnumeratedArray<CCNumberParams, CCNumberParams::Count, double> + EnumeratedArray<CCNumberParams, double, size_t(CCNumberParams::Count)> mCCNumberParams; - EnumeratedArray<CCNameParams, CCNameParams::Count, double> mCCNameParams; - EnumeratedArray<CCTypeParams, CCTypeParams::Count, double> mCCTypeParams; - EnumeratedArray<CCExpParams, CCExpParams::Count, double> mCCExpParams; - EnumeratedArray<CCExpMonthParams, CCExpMonthParams::Count, double> + EnumeratedArray<CCNameParams, double, size_t(CCNameParams::Count)> + mCCNameParams; + EnumeratedArray<CCTypeParams, double, size_t(CCTypeParams::Count)> + mCCTypeParams; + EnumeratedArray<CCExpParams, double, size_t(CCExpParams::Count)> mCCExpParams; + EnumeratedArray<CCExpMonthParams, double, size_t(CCExpMonthParams::Count)> mCCExpMonthParams; - EnumeratedArray<CCExpYearParams, CCExpYearParams::Count, double> + EnumeratedArray<CCExpYearParams, double, size_t(CCExpYearParams::Count)> mCCExpYearParams; }; @@ -667,13 +669,11 @@ class FormAutofillImpl { // Array contains regular expressions to match the corresponding // field. Ex, CC number, CC type, etc. using RegexStringArray = - EnumeratedArray<RegexKey, RegexKey::Count, nsCString>; + EnumeratedArray<RegexKey, nsCString, size_t(RegexKey::Count)>; RegexStringArray mRuleMap; // Array that holds RegexWrapper that created by regex::ffi::regex_new - using RegexWrapperArray = - EnumeratedArray<RegexKey, RegexKey::Count, - RustRegex>; + using RegexWrapperArray = EnumeratedArray<RegexKey, RustRegex, size_t(RegexKey::Count)>; RegexWrapperArray mRegexes; }; diff --git a/toolkit/components/formautofill/FormAutofillParent.sys.mjs b/toolkit/components/formautofill/FormAutofillParent.sys.mjs index ba0d769906..61c4bd2943 100644 --- a/toolkit/components/formautofill/FormAutofillParent.sys.mjs +++ b/toolkit/components/formautofill/FormAutofillParent.sys.mjs @@ -5,10 +5,10 @@ /* * Implements a service used to access storage and communicate with content. * - * A "fields" array is used to communicate with FormAutofillContent. Each item + * A "fields" array is used to communicate with FormAutofillChild. Each item * represents a single input field in the content page as well as its * @autocomplete properties. The schema is as below. Please refer to - * FormAutofillContent.js for more details. + * FormAutofillChild.js for more details. * * [ * { @@ -293,7 +293,7 @@ export class FormAutofillParent extends JSWindowActorParent { } /** - * Handles the message coming from FormAutofillContent. + * Handles the message coming from FormAutofillChild. * * @param {object} message * @param {string} message.name The name of the message. diff --git a/toolkit/components/formautofill/FormAutofillStorageBase.sys.mjs b/toolkit/components/formautofill/FormAutofillStorageBase.sys.mjs index 591bfc1578..f360be4fa6 100644 --- a/toolkit/components/formautofill/FormAutofillStorageBase.sys.mjs +++ b/toolkit/components/formautofill/FormAutofillStorageBase.sys.mjs @@ -130,18 +130,19 @@ */ import { FormAutofill } from "resource://autofill/FormAutofill.sys.mjs"; +import { AddressRecord } from "resource://gre/modules/shared/AddressRecord.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", CreditCardRecord: "resource://gre/modules/shared/CreditCardRecord.sys.mjs", FormAutofillNameUtils: "resource://gre/modules/shared/FormAutofillNameUtils.sys.mjs", FormAutofillUtils: "resource://gre/modules/shared/FormAutofillUtils.sys.mjs", OSKeyStore: "resource://gre/modules/OSKeyStore.sys.mjs", - PhoneNumber: "resource://autofill/phonenumberutils/PhoneNumber.sys.mjs", + PhoneNumber: "resource://gre/modules/shared/PhoneNumber.sys.mjs", }); const CryptoHash = Components.Constructor( @@ -166,23 +167,6 @@ export const ADDRESS_SCHEMA_VERSION = 1; // Please talk to the sync team before changing this! export const CREDIT_CARD_SCHEMA_VERSION = 3; -const NAME_COMPONENTS = ["given-name", "additional-name", "family-name"]; - -const STREET_ADDRESS_COMPONENTS = [ - "address-line1", - "address-line2", - "address-line3", -]; - -const TEL_COMPONENTS = [ - "tel-country-code", - "tel-national", - "tel-area-code", - "tel-local", - "tel-local-prefix", - "tel-local-suffix", -]; - const VALID_ADDRESS_FIELDS = [ "name", "organization", @@ -198,9 +182,9 @@ const VALID_ADDRESS_FIELDS = [ const VALID_ADDRESS_COMPUTED_FIELDS = [ "country-name", - ...NAME_COMPONENTS, - ...STREET_ADDRESS_COMPONENTS, - ...TEL_COMPONENTS, + ...AddressRecord.NAME_COMPONENTS, + ...AddressRecord.STREET_ADDRESS_COMPONENTS, + ...AddressRecord.TEL_COMPONENTS, ]; const VALID_CREDIT_CARD_FIELDS = [ @@ -299,20 +283,18 @@ class AutofillRecords { }); } - observe(subject, topic, data) { - switch (topic) { - case "formautofill-storage-changed": - let collectionName = subject.wrappedJSObject.collectionName; - if (collectionName != this._collectionName) { - return; - } - const telemetryType = - subject.wrappedJSObject.collectionName == "creditCards" - ? lazy.AutofillTelemetry.CREDIT_CARD - : lazy.AutofillTelemetry.ADDRESS; - const count = this._data.filter(entry => !entry.deleted).length; - lazy.AutofillTelemetry.recordAutofillProfileCount(telemetryType, count); - break; + observe(subject, topic, _data) { + if (topic == "formautofill-storage-changed") { + let collectionName = subject.wrappedJSObject.collectionName; + if (collectionName != this._collectionName) { + return; + } + const telemetryType = + subject.wrappedJSObject.collectionName == "creditCards" + ? lazy.AutofillTelemetry.CREDIT_CARD + : lazy.AutofillTelemetry.ADDRESS; + const count = this._data.filter(entry => !entry.deleted).length; + lazy.AutofillTelemetry.recordAutofillProfileCount(telemetryType, count); } } @@ -675,7 +657,7 @@ class AutofillRecords { // Excluding *-name fields from the sync payload would prevent older devices from // synchronizing with newer devices. To maintain backward compatibility, keep those deprecated // ields in the payload, ensuring that older devices can still sync with newer devices. - const fieldsToKeep = NAME_COMPONENTS; + const fieldsToKeep = AddressRecord.NAME_COMPONENTS; await this._stripComputedFields(clonedRecord, fieldsToKeep); } else { this._recordReadProcessor(clonedRecord); @@ -703,7 +685,7 @@ class AutofillRecords { await Promise.all( clonedRecords.map(async record => { if (rawData) { - const fieldsToKeep = NAME_COMPONENTS; + const fieldsToKeep = AddressRecord.NAME_COMPONENTS; await this._stripComputedFields(record, fieldsToKeep); } else { this._recordReadProcessor(record); @@ -1398,7 +1380,12 @@ class AutofillRecords { return hasChanges; } - hasChanges |= await this.computeFields(record); + const originalNumFields = Object.keys(record).length; + await this.computeFields(record); + const hasNewComputedFields = + Object.keys(record).length != originalNumFields; + + hasChanges |= hasNewComputedFields; return hasChanges; } @@ -1486,36 +1473,36 @@ class AutofillRecords { } // An interface to be inherited. - _recordReadProcessor(record) {} + _recordReadProcessor(_record) {} // An interface to be inherited. - async computeFields(record) {} + async computeFields(_record) {} /** * An interface to be inherited to mutate the argument to normalize it. * - * @param {object} partialRecord containing the record passed by the consumer of + * @param {object} _partialRecord containing the record passed by the consumer of * storage and in the case of `update` with * `preserveOldProperties` will only include the * properties that the user is changing so the * lack of a field doesn't mean that the record * won't have that field. */ - _normalizeFields(partialRecord) {} + _normalizeFields(_partialRecord) {} /** * An interface to be inherited to validate that the complete record is * consistent and isn't missing required fields. Overrides should throw for * invalid records. * - * @param {object} record containing the complete record that would be stored + * @param {object} _record containing the complete record that would be stored * if this doesn't throw due to an error. * @throws */ - _validateFields(record) {} + _validateFields(_record) {} // An interface to be inherited. - migrateRemoteRecord(remoteRecord) {} + migrateRemoteRecord(_remoteRecord) {} } export class AddressesBase extends AutofillRecords { @@ -1578,99 +1565,9 @@ export class AddressesBase extends AutofillRecords { // NOTE: Computed fields should be always present in the storage no matter // it's empty or not. - let hasNewComputedFields = false; - - if (address.deleted) { - return hasNewComputedFields; - } - - // Compute split names - if (!("given-name" in address)) { - const nameParts = lazy.FormAutofillNameUtils.splitName(address.name); - address["given-name"] = nameParts.given; - address["additional-name"] = nameParts.middle; - address["family-name"] = nameParts.family; - hasNewComputedFields = true; - } - - // Compute address lines - if (!("address-line1" in address)) { - let streetAddress = []; - if (address["street-address"]) { - streetAddress = address["street-address"] - .split("\n") - .map(s => s.trim()); - } - for (let i = 0; i < 3; i++) { - address[`address-line${i + 1}`] = streetAddress[i] || ""; - } - if (streetAddress.length > 3) { - address["address-line3"] = lazy.FormAutofillUtils.toOneLineAddress( - streetAddress.slice(2) - ); - } - hasNewComputedFields = true; - } - - // Compute country name - if (!("country-name" in address)) { - if (address.country) { - try { - address["country-name"] = Services.intl.getRegionDisplayNames( - undefined, - [address.country] - ); - } catch (e) { - address["country-name"] = ""; - } - } else { - address["country-name"] = ""; - } - hasNewComputedFields = true; + if (!address.deleted) { + AddressRecord.computeFields(address); } - - // Compute tel - if (!("tel-national" in address)) { - if (address.tel) { - let tel = lazy.PhoneNumber.Parse( - address.tel, - address.country || FormAutofill.DEFAULT_REGION - ); - if (tel) { - if (tel.countryCode) { - address["tel-country-code"] = tel.countryCode; - } - if (tel.nationalNumber) { - address["tel-national"] = tel.nationalNumber; - } - - // PhoneNumberUtils doesn't support parsing the components of a telephone - // number so we hard coded the parser for US numbers only. We will need - // to figure out how to parse numbers from other regions when we support - // new countries in the future. - if (tel.nationalNumber && tel.countryCode == "+1") { - let telComponents = tel.nationalNumber.match( - /(\d{3})((\d{3})(\d{4}))$/ - ); - if (telComponents) { - address["tel-area-code"] = telComponents[1]; - address["tel-local"] = telComponents[2]; - address["tel-local-prefix"] = telComponents[3]; - address["tel-local-suffix"] = telComponents[4]; - } - } - } else { - // Treat "tel" as "tel-national" directly if it can't be parsed. - address["tel-national"] = address.tel; - } - } - - TEL_COMPONENTS.forEach(c => { - address[c] = address[c] || ""; - }); - } - - return hasNewComputedFields; } _normalizeFields(address) { @@ -1700,7 +1597,7 @@ export class AddressesBase extends AutofillRecords { } _normalizeAddressFields(address) { - if (STREET_ADDRESS_COMPONENTS.some(c => !!address[c])) { + if (AddressRecord.STREET_ADDRESS_COMPONENTS.some(c => !!address[c])) { // Treat "street-address" as "address-line1" if it contains only one line // and "address-line1" is omitted. if ( @@ -1714,14 +1611,14 @@ export class AddressesBase extends AutofillRecords { // Concatenate "address-line*" if "street-address" is omitted. if (!address["street-address"]) { - address["street-address"] = STREET_ADDRESS_COMPONENTS.map( + address["street-address"] = AddressRecord.STREET_ADDRESS_COMPONENTS.map( c => address[c] ) .join("\n") .replace(/\n+$/, ""); } } - STREET_ADDRESS_COMPONENTS.forEach(c => delete address[c]); + AddressRecord.STREET_ADDRESS_COMPONENTS.forEach(c => delete address[c]); } _normalizeCountryFields(address) { @@ -1753,7 +1650,7 @@ export class AddressesBase extends AutofillRecords { } _normalizeTelFields(address) { - if (address.tel || TEL_COMPONENTS.some(c => !!address[c])) { + if (address.tel || AddressRecord.TEL_COMPONENTS.some(c => !!address[c])) { lazy.FormAutofillUtils.compressTel(address); let possibleRegion = address.country || FormAutofill.DEFAULT_REGION; @@ -1764,7 +1661,7 @@ export class AddressesBase extends AutofillRecords { address.tel = tel.internationalNumber; } } - TEL_COMPONENTS.forEach(c => delete address[c]); + AddressRecord.TEL_COMPONENTS.forEach(c => delete address[c]); } /** @@ -1793,12 +1690,14 @@ export class AddressesBase extends AutofillRecords { // we will rebuild it and replace the local `name` field with "Jane Poe". if ( !("name" in remoteRecord) && - NAME_COMPONENTS.some(c => c in remoteRecord) + AddressRecord.NAME_COMPONENTS.some(c => c in remoteRecord) ) { const localRecord = this._findByGUID(remoteRecord.guid); if ( localRecord && - NAME_COMPONENTS.every(c => remoteRecord[c] == localRecord[c]) + AddressRecord.NAME_COMPONENTS.every( + c => remoteRecord[c] == localRecord[c] + ) ) { remoteRecord.name = localRecord.name; } else { @@ -1815,7 +1714,7 @@ export class AddressesBase extends AutofillRecords { // This also means that the incoming remote record will also contain *-name fields. // However, since the autofill storage does not expect remote records to contain // computed fields while merging, we remove them from the remote record. - NAME_COMPONENTS.forEach(f => delete remoteRecord[f]); + AddressRecord.NAME_COMPONENTS.forEach(f => delete remoteRecord[f]); } } @@ -1879,7 +1778,7 @@ export class CreditCardsBase extends AutofillRecords { return hasNewComputedFields; } - async _encryptNumber(creditCard) { + async _encryptNumber(_creditCard) { throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); } diff --git a/toolkit/components/formautofill/FormAutofillSync.sys.mjs b/toolkit/components/formautofill/FormAutofillSync.sys.mjs index 4540737e38..15ae9b60b5 100644 --- a/toolkit/components/formautofill/FormAutofillSync.sys.mjs +++ b/toolkit/components/formautofill/FormAutofillSync.sys.mjs @@ -244,7 +244,7 @@ class AutofillChangeset extends Changeset { super(); } - getModifiedTimestamp(id) { + getModifiedTimestamp(_id) { throw new Error("Don't use timestamps to resolve autofill merge conflicts"); } diff --git a/toolkit/components/formautofill/Helpers.ios.mjs b/toolkit/components/formautofill/Helpers.ios.mjs index 4144d3e98c..56bb49f0e9 100644 --- a/toolkit/components/formautofill/Helpers.ios.mjs +++ b/toolkit/components/formautofill/Helpers.ios.mjs @@ -45,6 +45,12 @@ 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, { @@ -58,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 @@ -128,23 +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, - }), locale: withNotImplementedError({ isAppLocaleRTL: false }), prefs: withNotImplementedError({ prefIsLocked: () => false }), strings: withNotImplementedError({ @@ -154,7 +156,64 @@ 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; @@ -163,15 +222,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/toolkit/components/formautofill/Overrides.ios.js b/toolkit/components/formautofill/Overrides.ios.js index a0023a267c..ae5998992b 100644 --- a/toolkit/components/formautofill/Overrides.ios.js +++ b/toolkit/components/formautofill/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/toolkit/components/formautofill/ProfileAutoCompleteResult.sys.mjs b/toolkit/components/formautofill/ProfileAutoCompleteResult.sys.mjs index 15fc1a520c..52ed8bed03 100644 --- a/toolkit/components/formautofill/ProfileAutoCompleteResult.sys.mjs +++ b/toolkit/components/formautofill/ProfileAutoCompleteResult.sys.mjs @@ -12,7 +12,7 @@ ChromeUtils.defineESModuleGetters(lazy, { ChromeUtils.defineLazyGetter( lazy, "l10n", - () => new Localization(["browser/preferences/formAutofill.ftl"], true) + () => new Localization(["toolkit/formautofill/formAutofill.ftl"], true) ); class ProfileAutoCompleteResult { @@ -100,16 +100,16 @@ class ProfileAutoCompleteResult { * Get the secondary label based on the focused field name and related field names * in the same form. * - * @param {string} focusedFieldName The field name of the focused input - * @param {Array<object>} allFieldNames The field names in the same section - * @param {object} profile The profile providing the labels to show. + * @param {string} _focusedFieldName The field name of the focused input + * @param {Array<object>} _allFieldNames The field names in the same section + * @param {object} _profile The profile providing the labels to show. * @returns {string} The secondary label */ - _getSecondaryLabel(focusedFieldName, allFieldNames, profile) { + _getSecondaryLabel(_focusedFieldName, _allFieldNames, _profile) { return ""; } - _generateLabels(focusedFieldName, allFieldNames, profiles) {} + _generateLabels(_focusedFieldName, _allFieldNames, _profiles) {} /** * Get the value of the result at the given index. @@ -190,19 +190,19 @@ class ProfileAutoCompleteResult { /** * Returns true if the value at the given index is removable * - * @param {number} index The index of the result to remove + * @param {number} _index The index of the result to remove * @returns {boolean} True if the value is removable */ - isRemovableAt(index) { + isRemovableAt(_index) { return false; } /** * Removes a result from the resultset * - * @param {number} index The index of the result to remove + * @param {number} _index The index of the result to remove */ - removeValueAt(index) { + removeValueAt(_index) { // There is no plan to support removing profiles via autocomplete. } } @@ -277,10 +277,19 @@ export class AddressResult extends ProfileAutoCompleteResult { } _generateLabels(focusedFieldName, allFieldNames, profiles) { + const manageLabel = lazy.l10n.formatValueSync( + "autofill-manage-addresses-label" + ); + if (this._isInputAutofilled) { return [ { primary: "", secondary: "" }, // Clear button - { primary: "", secondary: "" }, // Footer + // Footer + { + primary: "", + secondary: "", + manageLabel, + }, ]; } @@ -306,6 +315,10 @@ export class AddressResult extends ProfileAutoCompleteResult { ), }; }); + + const focusedCategory = + lazy.FormAutofillUtils.getCategoryFromFieldName(focusedFieldName); + // Add an empty result entry for footer. Its content will come from // the footer binding, so don't assign any value to it. // The additional properties: categories and focusedCategory are required of @@ -313,12 +326,11 @@ export class AddressResult extends ProfileAutoCompleteResult { labels.push({ primary: "", secondary: "", + manageLabel, categories: lazy.FormAutofillUtils.getCategoriesFromFieldNames( this._allFieldNames ), - focusedCategory: lazy.FormAutofillUtils.getCategoryFromFieldName( - this._focusedFieldName - ), + focusedCategory, }); return labels; @@ -385,10 +397,19 @@ export class CreditCardResult extends ProfileAutoCompleteResult { ]; } + const manageLabel = lazy.l10n.formatValueSync( + "autofill-manage-payment-methods-label" + ); + if (this._isInputAutofilled) { return [ { primary: "", secondary: "" }, // Clear button - { primary: "", secondary: "" }, // Footer + // Footer + { + primary: "", + secondary: "", + manageLabel, + }, ]; } @@ -431,8 +452,17 @@ export class CreditCardResult extends ProfileAutoCompleteResult { image, }; }); + + const focusedCategory = + lazy.FormAutofillUtils.getCategoryFromFieldName(focusedFieldName); + // Add an empty result entry for footer. - labels.push({ primary: "", secondary: "" }); + labels.push({ + primary: "", + secondary: "", + manageLabel, + focusedCategory, + }); return labels; } diff --git a/toolkit/components/formautofill/android/FormAutofillPrompter.sys.mjs b/toolkit/components/formautofill/android/FormAutofillPrompter.sys.mjs index 6bb0e991b1..5e737a018b 100644 --- a/toolkit/components/formautofill/android/FormAutofillPrompter.sys.mjs +++ b/toolkit/components/formautofill/android/FormAutofillPrompter.sys.mjs @@ -34,10 +34,10 @@ export let FormAutofillPrompter = { }, async promptToSaveAddress( - browser, - storage, - flowId, - { oldRecord, newRecord } + _browser, + _storage, + _flowId, + { _oldRecord, _newRecord } ) { throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); }, diff --git a/toolkit/components/formautofill/android/FormAutofillStorage.sys.mjs b/toolkit/components/formautofill/android/FormAutofillStorage.sys.mjs index 0d11880ff5..964be31d06 100644 --- a/toolkit/components/formautofill/android/FormAutofillStorage.sys.mjs +++ b/toolkit/components/formautofill/android/FormAutofillStorage.sys.mjs @@ -115,17 +115,17 @@ class Addresses extends AddressesBase { return super.getSavedFieldNames(); } - async reconcile(remoteRecord) { + async reconcile(_remoteRecord) { throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); } - async findDuplicateGUID(remoteRecord) { + async findDuplicateGUID(_remoteRecord) { throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); } } class CreditCards extends CreditCardsBase { - async _encryptNumber(creditCard) { + async _encryptNumber(_creditCard) { // Don't encrypt or obfuscate for GV, since we don't store or show // the number. The API has to always provide the original number. } @@ -220,11 +220,11 @@ class CreditCards extends CreditCardsBase { return null; } - async reconcile(remoteRecord) { + async reconcile(_remoteRecord) { throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); } - async findDuplicateGUID(remoteRecord) { + async findDuplicateGUID(_remoteRecord) { throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); } } diff --git a/toolkit/components/formautofill/default/FormAutofillPrompter.sys.mjs b/toolkit/components/formautofill/default/FormAutofillPrompter.sys.mjs index ecf787137e..f166716de5 100644 --- a/toolkit/components/formautofill/default/FormAutofillPrompter.sys.mjs +++ b/toolkit/components/formautofill/default/FormAutofillPrompter.sys.mjs @@ -11,7 +11,7 @@ import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; import { FormAutofill } from "resource://autofill/FormAutofill.sys.mjs"; import { FormAutofillUtils } from "resource://gre/modules/shared/FormAutofillUtils.sys.mjs"; -import { AutofillTelemetry } from "resource://autofill/AutofillTelemetry.sys.mjs"; +import { AutofillTelemetry } from "resource://gre/modules/shared/AutofillTelemetry.sys.mjs"; import { showConfirmation } from "resource://gre/modules/FillHelpers.sys.mjs"; const lazy = {}; @@ -187,7 +187,7 @@ export class AutofillDoorhanger { renderHeader() { // Render the header text - const text = this.header.querySelector(`p`); + const text = this.header.querySelector(`h1`); this.doc.l10n.setAttributes(text, this.ui.header.l10nId); // Render the menu button @@ -529,6 +529,8 @@ export class AddressSaveDoorhanger extends AutofillDoorhanger { //const img = this.doc.createElement("img"); const img = this.doc.createXULElement("image"); img.setAttribute("class", imgClass); + // ToDo: provide meaningful alt values (bug 1870155): + img.setAttribute("alt", ""); section.appendChild(img); // Each line is consisted of multiple <span> to form diff style texts diff --git a/toolkit/components/formautofill/moz.build b/toolkit/components/formautofill/moz.build index 542fc595e0..d2091dc850 100644 --- a/toolkit/components/formautofill/moz.build +++ b/toolkit/components/formautofill/moz.build @@ -15,6 +15,8 @@ EXTRA_JS_MODULES.shared += [ "shared/AddressMetaDataExtension.sys.mjs", "shared/AddressMetaDataLoader.sys.mjs", "shared/AddressParser.sys.mjs", + "shared/AddressRecord.sys.mjs", + "shared/AutofillTelemetry.sys.mjs", "shared/CreditCardRecord.sys.mjs", "shared/CreditCardRuleset.sys.mjs", "shared/FieldScanner.sys.mjs", @@ -26,6 +28,9 @@ EXTRA_JS_MODULES.shared += [ "shared/FormStateManager.sys.mjs", "shared/HeuristicsRegExp.sys.mjs", "shared/LabelUtils.sys.mjs", + "shared/PhoneNumber.sys.mjs", + "shared/PhoneNumberMetaData.sys.mjs", + "shared/PhoneNumberNormalizer.sys.mjs", ] EXPORTS.mozilla += ["FormAutofillNative.h"] diff --git a/toolkit/components/formautofill/shared/AddressComponent.sys.mjs b/toolkit/components/formautofill/shared/AddressComponent.sys.mjs index a849e889b2..40e00b66a0 100644 --- a/toolkit/components/formautofill/shared/AddressComponent.sys.mjs +++ b/toolkit/components/formautofill/shared/AddressComponent.sys.mjs @@ -11,9 +11,9 @@ ChromeUtils.defineESModuleGetters(lazy, { FormAutofillNameUtils: "resource://gre/modules/shared/FormAutofillNameUtils.sys.mjs", FormAutofillUtils: "resource://gre/modules/shared/FormAutofillUtils.sys.mjs", - PhoneNumber: "resource://autofill/phonenumberutils/PhoneNumber.sys.mjs", + PhoneNumber: "resource://gre/modules/shared/PhoneNumber.sys.mjs", PhoneNumberNormalizer: - "resource://autofill/phonenumberutils/PhoneNumberNormalizer.sys.mjs", + "resource://gre/modules/shared/PhoneNumberNormalizer.sys.mjs", }); /** @@ -201,7 +201,7 @@ class StreetAddress extends AddressField { super(value, region); this.#structuredStreetAddress = lazy.AddressParser.parseStreetAddress( - lazy.AddressParser.replaceControlCharacters(this.userValue, " ") + lazy.AddressParser.replaceControlCharacters(this.userValue) ); } @@ -491,7 +491,7 @@ class Country extends AddressField { return this.country_code == other.country_code; } - contains(other) { + contains(_other) { return false; } @@ -841,7 +841,7 @@ class Email extends AddressField { ); } - contains(other) { + contains(_other) { return false; } diff --git a/toolkit/components/formautofill/shared/AddressParser.sys.mjs b/toolkit/components/formautofill/shared/AddressParser.sys.mjs index 5cb76934c1..1d36b71bba 100644 --- a/toolkit/components/formautofill/shared/AddressParser.sys.mjs +++ b/toolkit/components/formautofill/shared/AddressParser.sys.mjs @@ -271,7 +271,7 @@ export class AddressParser { return s?.replace(/[.,\/#!$%\^&\*;:{}=\-_~()]/g, ""); } - static replaceControlCharacters(s, replace) { + static replaceControlCharacters(s) { return s?.replace(/[\t\n\r]/g, " "); } diff --git a/toolkit/components/formautofill/shared/AddressRecord.sys.mjs b/toolkit/components/formautofill/shared/AddressRecord.sys.mjs new file mode 100644 index 0000000000..599a802dcd --- /dev/null +++ b/toolkit/components/formautofill/shared/AddressRecord.sys.mjs @@ -0,0 +1,119 @@ +/* eslint-disable no-useless-concat */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * 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 { FormAutofillNameUtils } from "resource://gre/modules/shared/FormAutofillNameUtils.sys.mjs"; +import { FormAutofillUtils } from "resource://gre/modules/shared/FormAutofillUtils.sys.mjs"; +import { PhoneNumber } from "resource://gre/modules/shared/PhoneNumber.sys.mjs"; +import { FormAutofill } from "resource://autofill/FormAutofill.sys.mjs"; + +/** + * The AddressRecord class serves to handle and normalize internal address records. + * AddressRecord is used for processing and consistent data representation. + */ +export class AddressRecord { + static NAME_COMPONENTS = ["given-name", "additional-name", "family-name"]; + + static STREET_ADDRESS_COMPONENTS = [ + "address-line1", + "address-line2", + "address-line3", + ]; + static TEL_COMPONENTS = [ + "tel-country-code", + "tel-national", + "tel-area-code", + "tel-local", + "tel-local-prefix", + "tel-local-suffix", + ]; + + static computeFields(address) { + this.#computeNameFields(address); + this.#computeAddressLineFields(address); + this.#computeCountryFields(address); + this.#computeTelFields(address); + } + + static #computeNameFields(address) { + // Compute split names + if (!("given-name" in address)) { + const nameParts = FormAutofillNameUtils.splitName(address.name); + address["given-name"] = nameParts.given; + address["additional-name"] = nameParts.middle; + address["family-name"] = nameParts.family; + } + } + + static #computeAddressLineFields(address) { + // Compute address lines + if (!("address-line1" in address)) { + let streetAddress = []; + if (address["street-address"]) { + streetAddress = address["street-address"] + .split("\n") + .map(s => s.trim()); + } + for (let i = 0; i < 3; i++) { + address[`address-line${i + 1}`] = streetAddress[i] || ""; + } + if (streetAddress.length > 3) { + address["address-line3"] = FormAutofillUtils.toOneLineAddress( + streetAddress.slice(2) + ); + } + } + } + + static #computeCountryFields(address) { + // Compute country name + if (!("country-name" in address)) { + address["country-name"] = + FormAutofill.countries.get(address.country) ?? ""; + } + } + + static #computeTelFields(address) { + // Compute tel + if (!("tel-national" in address)) { + if (address.tel) { + let tel = PhoneNumber.Parse( + address.tel, + address.country || FormAutofill.DEFAULT_REGION + ); + if (tel) { + if (tel.countryCode) { + address["tel-country-code"] = tel.countryCode; + } + if (tel.nationalNumber) { + address["tel-national"] = tel.nationalNumber; + } + + // PhoneNumberUtils doesn't support parsing the components of a telephone + // number so we hard coded the parser for US numbers only. We will need + // to figure out how to parse numbers from other regions when we support + // new countries in the future. + if (tel.nationalNumber && tel.countryCode == "+1") { + let telComponents = tel.nationalNumber.match( + /(\d{3})((\d{3})(\d{4}))$/ + ); + if (telComponents) { + address["tel-area-code"] = telComponents[1]; + address["tel-local"] = telComponents[2]; + address["tel-local-prefix"] = telComponents[3]; + address["tel-local-suffix"] = telComponents[4]; + } + } + } else { + // Treat "tel" as "tel-national" directly if it can't be parsed. + address["tel-national"] = address.tel; + } + } + + this.TEL_COMPONENTS.forEach(c => { + address[c] = address[c] || ""; + }); + } + } +} diff --git a/toolkit/components/formautofill/AutofillTelemetry.sys.mjs b/toolkit/components/formautofill/shared/AutofillTelemetry.sys.mjs index 93aa99a4b8..6a1fa974cc 100644 --- a/toolkit/components/formautofill/AutofillTelemetry.sys.mjs +++ b/toolkit/components/formautofill/shared/AutofillTelemetry.sys.mjs @@ -140,7 +140,7 @@ class AutofillTelemetryBase { this.recordGleanFormEvent("formFilledModified", section.flowId, extra); } - recordFormSubmitted(section, record, form) { + recordFormSubmitted(section, record, _form) { let extra = this.#initFormEventExtra("unavailable"); if (record.guid !== null) { @@ -185,7 +185,7 @@ class AutofillTelemetryBase { ); } - recordGleanFormEvent(eventName, flowId, extra) { + recordGleanFormEvent(_eventName, _flowId, _extra) { throw new Error("Not implemented."); } @@ -222,7 +222,7 @@ class AutofillTelemetryBase { Services.telemetry.recordEvent(this.EVENT_CATEGORY, method, "manage"); } - recordAutofillProfileCount(count) { + recordAutofillProfileCount(_count) { throw new Error("Not implemented."); } @@ -311,7 +311,7 @@ export class AddressTelemetry extends AutofillTelemetryBase { "tel", ]; - recordGleanFormEvent(eventName, flowId, extra) { + recordGleanFormEvent(_eventName, _flowId, _extra) { // To be implemented when migrating the legacy event address.address_form to Glean } diff --git a/toolkit/components/formautofill/shared/FieldScanner.sys.mjs b/toolkit/components/formautofill/shared/FieldScanner.sys.mjs index 22adfdabe8..2118de3de8 100644 --- a/toolkit/components/formautofill/shared/FieldScanner.sys.mjs +++ b/toolkit/components/formautofill/shared/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/toolkit/components/formautofill/shared/FormAutofillHeuristics.sys.mjs b/toolkit/components/formautofill/shared/FormAutofillHeuristics.sys.mjs index 4ee1fc1fe1..fb96e47cae 100644 --- a/toolkit/components/formautofill/shared/FormAutofillHeuristics.sys.mjs +++ b/toolkit/components/formautofill/shared/FormAutofillHeuristics.sys.mjs @@ -182,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; @@ -277,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", @@ -547,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) @@ -597,22 +599,6 @@ export const FormAutofillHeuristics = { }, /** - * Get focusable form elements that are of credit card or address type - * - * @param {HTMLElement} form - * @returns {Array<HTMLElement>} focusable elements - */ - getFormElements(form) { - let elements = Array.from(form.elements).filter( - element => - lazy.FormAutofillUtils.isCreditCardOrAddressFieldType(element) && - lazy.FormAutofillUtils.isFieldFocusable(element) - ); - - return elements; - }, - - /** * The result is an array contains the sections with its belonging field details. * * @param {Array<FieldDetails>} fieldDetails field detail array to be classified @@ -621,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 @@ -673,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/toolkit/components/formautofill/shared/FormAutofillSection.sys.mjs b/toolkit/components/formautofill/shared/FormAutofillSection.sys.mjs index 1c7696432a..7bda4c167b 100644 --- a/toolkit/components/formautofill/shared/FormAutofillSection.sys.mjs +++ b/toolkit/components/formautofill/shared/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", @@ -79,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; } @@ -846,6 +846,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; } @@ -884,7 +888,7 @@ export class FormAutofillCreditCardSection extends FormAutofillSection { } } - _handlePageHide(event) { + _handlePageHide(_event) { this.handler.window.removeEventListener( "pagehide", this._handlePageHide.bind(this) diff --git a/toolkit/components/formautofill/shared/FormAutofillUtils.sys.mjs b/toolkit/components/formautofill/shared/FormAutofillUtils.sys.mjs index ce10c71ce1..e86f14975c 100644 --- a/toolkit/components/formautofill/shared/FormAutofillUtils.sys.mjs +++ b/toolkit/components/formautofill/shared/FormAutofillUtils.sys.mjs @@ -192,7 +192,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 @@ -302,20 +302,27 @@ FormAutofillUtils = { }, /** - * Determines if an element is focusable - * and accessible via keyboard navigation or not. + * Determines if an element is visually hidden or not. * * @param {HTMLElement} element - * - * @returns {bool} true if the element is focusable and accessible + * @param {boolean} visibilityCheck true to run visiblity check against + * element.checkVisibility API. Otherwise, test by only checking + * `hidden` and `display` attributes + * @returns {boolean} true if the element is visible */ - 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" - ); + isFieldVisible(element, visibilityCheck = true) { + if ( + visibilityCheck && + element.checkVisibility && + !FormAutofillUtils.ignoreVisibilityCheck + ) { + return element.checkVisibility({ + checkOpacity: true, + checkVisibilityCSS: true, + }); + } + + return !element.hidden && element.style.display != "none"; }, /** @@ -1127,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/toolkit/components/formautofill/shared/FormStateManager.sys.mjs b/toolkit/components/formautofill/shared/FormStateManager.sys.mjs index 064b4e5356..7481a5981c 100644 --- a/toolkit/components/formautofill/shared/FormStateManager.sys.mjs +++ b/toolkit/components/formautofill/shared/FormStateManager.sys.mjs @@ -150,7 +150,7 @@ export class FormStateManager { } didDestroy() { - this._activeItems = null; + this._activeItems = {}; } } diff --git a/toolkit/components/formautofill/phonenumberutils/PhoneNumber.sys.mjs b/toolkit/components/formautofill/shared/PhoneNumber.sys.mjs index 80b5e43acb..5288765181 100644 --- a/toolkit/components/formautofill/phonenumberutils/PhoneNumber.sys.mjs +++ b/toolkit/components/formautofill/shared/PhoneNumber.sys.mjs @@ -5,13 +5,13 @@ // This library came from https://github.com/andreasgal/PhoneNumber.js but will // be further maintained by our own in Form Autofill codebase. -import { PHONE_NUMBER_META_DATA } from "resource://autofill/phonenumberutils/PhoneNumberMetaData.sys.mjs"; +import { PHONE_NUMBER_META_DATA } from "resource://gre/modules/shared/PhoneNumberMetaData.sys.mjs"; const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { PhoneNumberNormalizer: - "resource://autofill/phonenumberutils/PhoneNumberNormalizer.sys.mjs", + "resource://gre/modules/shared/PhoneNumberNormalizer.sys.mjs", }); export var PhoneNumber = (function (dataBase) { diff --git a/toolkit/components/formautofill/phonenumberutils/PhoneNumberMetaData.sys.mjs b/toolkit/components/formautofill/shared/PhoneNumberMetaData.sys.mjs index 3338ce7c16..3338ce7c16 100644 --- a/toolkit/components/formautofill/phonenumberutils/PhoneNumberMetaData.sys.mjs +++ b/toolkit/components/formautofill/shared/PhoneNumberMetaData.sys.mjs diff --git a/toolkit/components/formautofill/phonenumberutils/PhoneNumberNormalizer.sys.mjs b/toolkit/components/formautofill/shared/PhoneNumberNormalizer.sys.mjs index 604eefe314..604eefe314 100644 --- a/toolkit/components/formautofill/phonenumberutils/PhoneNumberNormalizer.sys.mjs +++ b/toolkit/components/formautofill/shared/PhoneNumberNormalizer.sys.mjs |