diff options
Diffstat (limited to 'toolkit/components/formautofill/FormAutofillContent.jsm')
-rw-r--r-- | toolkit/components/formautofill/FormAutofillContent.jsm | 890 |
1 files changed, 890 insertions, 0 deletions
diff --git a/toolkit/components/formautofill/FormAutofillContent.jsm b/toolkit/components/formautofill/FormAutofillContent.jsm new file mode 100644 index 0000000000..646c30c3d6 --- /dev/null +++ b/toolkit/components/formautofill/FormAutofillContent.jsm @@ -0,0 +1,890 @@ +/* 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/. */ + +/** + * Form Autofill content process module. + */ + +/* eslint-disable no-use-before-define */ + +"use strict"; + +var EXPORTED_SYMBOLS = ["FormAutofillContent"]; + +const Cm = Components.manager; + +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + FormLikeFactory: "resource://gre/modules/FormLikeFactory.sys.mjs", + PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", +}); + +XPCOMUtils.defineLazyModuleGetters(lazy, { + AddressResult: "resource://autofill/ProfileAutoCompleteResult.jsm", + ComponentUtils: "resource://gre/modules/ComponentUtils.jsm", + CreditCardResult: "resource://autofill/ProfileAutoCompleteResult.jsm", + AutofillTelemetry: "resource://autofill/AutofillTelemetry.jsm", + FormAutofill: "resource://autofill/FormAutofill.jsm", + FormAutofillHandler: "resource://autofill/FormAutofillHandler.jsm", + FormAutofillUtils: "resource://autofill/FormAutofillUtils.jsm", + InsecurePasswordUtils: "resource://gre/modules/InsecurePasswordUtils.jsm", +}); + +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "DELEGATE_AUTOCOMPLETE", + "toolkit.autocomplete.delegate", + false +); + +const formFillController = Cc[ + "@mozilla.org/satchel/form-fill-controller;1" +].getService(Ci.nsIFormFillController); +const autocompleteController = Cc[ + "@mozilla.org/autocomplete/controller;1" +].getService(Ci.nsIAutoCompleteController); + +XPCOMUtils.defineLazyGetter( + lazy, + "ADDRESSES_COLLECTION_NAME", + () => lazy.FormAutofillUtils.ADDRESSES_COLLECTION_NAME +); +XPCOMUtils.defineLazyGetter( + lazy, + "CREDITCARDS_COLLECTION_NAME", + () => lazy.FormAutofillUtils.CREDITCARDS_COLLECTION_NAME +); +XPCOMUtils.defineLazyGetter( + lazy, + "FIELD_STATES", + () => lazy.FormAutofillUtils.FIELD_STATES +); + +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; +} + +// Register/unregister a constructor as a factory. +function AutocompleteFactory() {} +AutocompleteFactory.prototype = { + register(targetConstructor) { + let proto = targetConstructor.prototype; + this._classID = proto.classID; + + let factory = lazy.ComponentUtils.generateSingletonFactory( + targetConstructor + ); + this._factory = factory; + + let registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar); + registrar.registerFactory( + proto.classID, + proto.classDescription, + proto.contractID, + factory + ); + + if (proto.classID2) { + this._classID2 = proto.classID2; + registrar.registerFactory( + proto.classID2, + proto.classDescription, + proto.contractID2, + factory + ); + } + }, + + unregister() { + let registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar); + registrar.unregisterFactory(this._classID, this._factory); + if (this._classID2) { + registrar.unregisterFactory(this._classID2, this._factory); + } + this._factory = null; + }, +}; + +/** + * @class + * + * @implements {nsIAutoCompleteSearch} + */ +function AutofillProfileAutoCompleteSearch() { + this.log = lazy.FormAutofill.defineLogGetter( + this, + "AutofillProfileAutoCompleteSearch" + ); +} +AutofillProfileAutoCompleteSearch.prototype = { + classID: Components.ID("4f9f1e4c-7f2c-439e-9c9e-566b68bc187d"), + contractID: "@mozilla.org/autocomplete/search;1?name=autofill-profiles", + classDescription: "AutofillProfileAutoCompleteSearch", + QueryInterface: ChromeUtils.generateQI(["nsIAutoCompleteSearch"]), + + // Begin nsIAutoCompleteSearch implementation + + /** + * Searches for a given string and notifies a listener (either synchronously + * or asynchronously) of the result + * + * @param {string} searchString the string to search for + * @param {string} searchParam + * @param {object} previousResult a previous result to use for faster searchinig + * @param {object} listener the listener to notify when the search is complete + */ + startSearch(searchString, searchParam, previousResult, listener) { + let { + activeInput, + activeSection, + activeFieldDetail, + savedFieldNames, + } = FormAutofillContent; + this.forceStop = false; + + let isAddressField = lazy.FormAutofillUtils.isAddressField( + activeFieldDetail.fieldName + ); + const isCreditCardField = lazy.FormAutofillUtils.isCreditCardField( + activeFieldDetail.fieldName + ); + let isInputAutofilled = + activeFieldDetail.state == lazy.FIELD_STATES.AUTO_FILLED; + let allFieldNames = activeSection.allFieldNames; + let filledRecordGUID = activeSection.filledRecordGUID; + + let searchPermitted = isAddressField + ? lazy.FormAutofill.isAutofillAddressesEnabled + : lazy.FormAutofill.isAutofillCreditCardsEnabled; + let AutocompleteResult = isAddressField + ? lazy.AddressResult + : lazy.CreditCardResult; + let isFormAutofillSearch = true; + let pendingSearchResult = null; + + ProfileAutocomplete.lastProfileAutoCompleteFocusedInput = activeInput; + // Fallback to form-history if ... + // - specified autofill feature is pref off. + // - no profile can fill the currently-focused input. + // - the current form has already been populated and the field is not + // an empty credit card field. + // - (address only) less than 3 inputs are covered by all saved fields in the storage. + if ( + !searchPermitted || + !savedFieldNames.has(activeFieldDetail.fieldName) || + (!isInputAutofilled && + filledRecordGUID && + !(isCreditCardField && activeInput.value === "")) || + (isAddressField && + allFieldNames.filter(field => savedFieldNames.has(field)).length < + lazy.FormAutofillUtils.AUTOFILL_FIELDS_THRESHOLD) + ) { + isFormAutofillSearch = false; + if (activeInput.autocomplete == "off") { + // Create a dummy result as an empty search result. + pendingSearchResult = new AutocompleteResult("", "", [], [], {}); + } else { + pendingSearchResult = new Promise(resolve => { + let formHistory = Cc[ + "@mozilla.org/autocomplete/search;1?name=form-history" + ].createInstance(Ci.nsIAutoCompleteSearch); + formHistory.startSearch(searchString, searchParam, previousResult, { + onSearchResult: (_, result) => resolve(result), + }); + }); + } + } else if (isInputAutofilled) { + pendingSearchResult = new AutocompleteResult(searchString, "", [], [], { + isInputAutofilled, + }); + } else { + let infoWithoutElement = { ...activeFieldDetail }; + delete infoWithoutElement.elementWeakRef; + + let data = { + collectionName: isAddressField + ? lazy.ADDRESSES_COLLECTION_NAME + : lazy.CREDITCARDS_COLLECTION_NAME, + info: infoWithoutElement, + searchString, + }; + + pendingSearchResult = this._getRecords(activeInput, data).then( + records => { + if (this.forceStop) { + return null; + } + // Sort addresses by timeLastUsed for showing the lastest used address at top. + records.sort((a, b) => b.timeLastUsed - a.timeLastUsed); + + let adaptedRecords = activeSection.getAdaptedProfiles(records); + let handler = FormAutofillContent.activeHandler; + let isSecure = lazy.InsecurePasswordUtils.isFormSecure(handler.form); + + return new AutocompleteResult( + searchString, + activeFieldDetail.fieldName, + allFieldNames, + adaptedRecords, + { isSecure, isInputAutofilled } + ); + } + ); + } + + Promise.resolve(pendingSearchResult).then(result => { + if (this.forceStop) { + // If we notify the listener the search result when the search is already + // cancelled, it corrupts the internal state of the listener. So we only + // reset the controller's state in this case. + if (isFormAutofillSearch) { + autocompleteController.resetInternalState(); + } + return; + } + + listener.onSearchResult(this, result); + // Don't save cache results or reset state when returning non-autofill results such as the + // form history fallback above. + if (isFormAutofillSearch) { + ProfileAutocomplete.lastProfileAutoCompleteResult = result; + // Reset AutoCompleteController's state at the end of startSearch to ensure that + // none of form autofill result will be cached in other places and make the + // result out of sync. + autocompleteController.resetInternalState(); + } else { + // Clear the cache so that we don't try to autofill from it after falling + // back to form history. + ProfileAutocomplete.lastProfileAutoCompleteResult = null; + } + }); + }, + + /** + * Stops an asynchronous search that is in progress + */ + stopSearch() { + ProfileAutocomplete.lastProfileAutoCompleteResult = null; + this.forceStop = true; + }, + + /** + * Get the records from parent process for AutoComplete result. + * + * @private + * @param {object} input + * Input element for autocomplete. + * @param {object} data + * Parameters for querying the corresponding result. + * @param {string} data.collectionName + * The name used to specify which collection to retrieve records. + * @param {string} data.searchString + * The typed string for filtering out the matched records. + * @param {string} data.info + * The input autocomplete property's information. + * @returns {Promise} + * Promise that resolves when addresses returned from parent process. + */ + _getRecords(input, data) { + if (!input) { + return []; + } + + let actor = getActorFromWindow(input.ownerGlobal); + return actor.sendQuery("FormAutofill:GetRecords", data); + }, +}; + +let ProfileAutocomplete = { + QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), + + lastProfileAutoCompleteResult: null, + lastProfileAutoCompleteFocusedInput: null, + _registered: false, + _factory: null, + + ensureRegistered() { + if (this._registered) { + return; + } + + this.log = lazy.FormAutofill.defineLogGetter(this, "ProfileAutocomplete"); + this.debug("ensureRegistered"); + this._factory = new AutocompleteFactory(); + this._factory.register(AutofillProfileAutoCompleteSearch); + this._registered = true; + + Services.obs.addObserver(this, "autocomplete-will-enter-text"); + + this.debug( + "ensureRegistered. Finished with _registered:", + this._registered + ); + }, + + ensureUnregistered() { + if (!this._registered) { + return; + } + + this.debug("ensureUnregistered"); + this._factory.unregister(); + this._factory = null; + this._registered = false; + this._lastAutoCompleteResult = null; + + Services.obs.removeObserver(this, "autocomplete-will-enter-text"); + }, + + async observe(subject, topic, data) { + switch (topic) { + case "autocomplete-will-enter-text": { + if (!FormAutofillContent.activeInput) { + // The observer notification is for autocomplete in a different process. + break; + } + FormAutofillContent.autofillPending = true; + Services.obs.notifyObservers(null, "autofill-fill-starting"); + await this._fillFromAutocompleteRow(FormAutofillContent.activeInput); + Services.obs.notifyObservers(null, "autofill-fill-complete"); + FormAutofillContent.autofillPending = false; + break; + } + } + }, + + _getSelectedIndex(contentWindow) { + let actor = getActorFromWindow(contentWindow, "AutoComplete"); + if (!actor) { + throw new Error("Invalid autocomplete selectedIndex"); + } + + return actor.selectedIndex; + }, + + async _fillFromAutocompleteRow(focusedInput) { + this.debug("_fillFromAutocompleteRow:", focusedInput); + let formDetails = FormAutofillContent.activeFormDetails; + if (!formDetails) { + // The observer notification is for a different frame. + return; + } + + let selectedIndex = this._getSelectedIndex(focusedInput.ownerGlobal); + if ( + selectedIndex == -1 || + !this.lastProfileAutoCompleteResult || + this.lastProfileAutoCompleteResult.getStyleAt(selectedIndex) != + "autofill-profile" + ) { + return; + } + + let profile = JSON.parse( + this.lastProfileAutoCompleteResult.getCommentAt(selectedIndex) + ); + + await FormAutofillContent.activeHandler.autofillFormFields(profile); + }, + + _clearProfilePreview() { + if ( + !this.lastProfileAutoCompleteFocusedInput || + !FormAutofillContent.activeSection + ) { + return; + } + + FormAutofillContent.activeSection.clearPreviewedFormFields(); + }, + + _previewSelectedProfile(selectedIndex) { + if ( + !FormAutofillContent.activeInput || + !FormAutofillContent.activeFormDetails + ) { + // The observer notification is for a different process/frame. + return; + } + + if ( + !this.lastProfileAutoCompleteResult || + this.lastProfileAutoCompleteResult.getStyleAt(selectedIndex) != + "autofill-profile" + ) { + return; + } + + let profile = JSON.parse( + this.lastProfileAutoCompleteResult.getCommentAt(selectedIndex) + ); + FormAutofillContent.activeSection.previewFormFields(profile); + }, +}; + +/** + * Handles content's interactions for the process. + * + * NOTE: Declares it by "var" to make it accessible in unit tests. + */ +var FormAutofillContent = { + /** + * @type {WeakMap} mapping FormLike root HTML elements to FormAutofillHandler objects. + */ + _formsDetails: new WeakMap(), + + /** + * @type {Set} Set of the fields with usable values in any saved profile. + */ + get savedFieldNames() { + return Services.cpmm.sharedData.get("FormAutofill:savedFieldNames"); + }, + + /** + * @type {object} The object where to store the active items, e.g. element, + * handler, section, and field detail. + */ + _activeItems: {}, + + /** + * @type {boolean} Flag indicating whether a focus action requiring + * the popup to be active is pending. + */ + _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"); + + // eslint-disable-next-line mozilla/balanced-listeners + Services.cpmm.sharedData.addEventListener("change", this); + + let 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 = + autofillEnabled === undefined && + (lazy.FormAutofill.isAutofillAddressesEnabled || + lazy.FormAutofill.isAutofillCreditCardsEnabled); + if (autofillEnabled || shouldEnableAutofill) { + ProfileAutocomplete.ensureRegistered(); + } + }, + + /** + * 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); + }, + + /** + * 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 {Window} domWin Content window; passed for unit tests and when + * invoked by the FormAutofillSection + * @param {object} handler FormAutofillHander, if known by caller + */ + formSubmitted( + formElement, + domWin = formElement.ownerGlobal, + handler = undefined + ) { + this.debug("Handling form submission"); + + 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._formsDetails.get(formElement); + if (!handler) { + this.debug("Form element could not map to an existing handler"); + return; + } + + let records = handler.createRecords(); + if (!Object.values(records).some(typeRecords => typeRecords.length)) { + return; + } + + [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); + }, + + handleEvent(evt) { + switch (evt.type) { + case "change": { + if (!evt.changedKeys.includes("FormAutofill:enabled")) { + return; + } + if (Services.cpmm.sharedData.get("FormAutofill:enabled")) { + ProfileAutocomplete.ensureRegistered(); + if (this._popupPending) { + this._popupPending = false; + this.debug("handleEvent: Opening deferred popup"); + formFillController.showPopup(); + } + } else { + ProfileAutocomplete.ensureUnregistered(); + } + break; + } + } + }, + + /** + * Get the form's handler from cache which is created after page identified. + * + * @param {HTMLInputElement} element Focused input which triggered profile searching + * @returns {Array<object> | null} + * Return target form's handler from content cache + * (or return null if the information is not found in the cache). + * + */ + _getFormHandler(element) { + if (!element) { + return null; + } + let rootElement = lazy.FormLikeFactory.findRootForField(element); + return this._formsDetails.get(rootElement); + }, + + /** + * Get the active form's information from cache which is created after page + * identified. + * + * @returns {Array<object> | null} + * Return target form's information from content cache + * (or return null if the information is not found in the cache). + * + */ + get activeFormDetails() { + let formHandler = this.activeHandler; + return formHandler ? formHandler.fieldDetails : null; + }, + + /** + * 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"); + this._activeItems = {}; + return; + } + this._activeItems = { + elementWeakRef: Cu.getWeakReference(element), + fieldDetail: null, + }; + + 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"); + formFillController.showPopup(); + } else { + this.debug( + "updateActiveElement: Deferring pop-up until Autofill is ready" + ); + this._popupPending = true; + } + } + } + }, + + get activeInput() { + let elementWeakRef = this._activeItems.elementWeakRef; + return elementWeakRef ? elementWeakRef.get() : null; + }, + + get activeHandler() { + const activeInput = this.activeInput; + if (!activeInput) { + return null; + } + + // XXX: We are recomputing the activeHandler every time to avoid keeping a + // reference on the active element. This might be called quite frequently + // so if _getFormHandler/findRootForField become more costly, we should + // look into caching this result (eg by adding a weakmap). + let handler = this._getFormHandler(activeInput); + if (handler) { + handler.focusedInput = activeInput; + } + return handler; + }, + + get activeSection() { + let formHandler = this.activeHandler; + return formHandler ? formHandler.activeSection : null; + }, + + /** + * Get the active input's information from cache which is created after page + * identified. + * + * @returns {object | null} + * Return the active input's information that cloned from content cache + * (or return null if the information is not found in the cache). + */ + get activeFieldDetail() { + if (!this._activeItems.fieldDetail) { + let formDetails = this.activeFormDetails; + if (!formDetails) { + return null; + } + for (let detail of formDetails) { + let detailElement = detail.elementWeakRef.get(); + if (detailElement && this.activeInput == detailElement) { + this._activeItems.fieldDetail = detail; + break; + } + } + } + return this._activeItems.fieldDetail; + }, + + set autofillPending(flag) { + this.debug("Setting autofillPending to", flag); + this._autofillPending = flag; + }, + + 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"); + } + } + + let formHandler = this._getFormHandler(element); + if (!formHandler) { + let formLike = lazy.FormLikeFactory.createFromField(element); + formHandler = new lazy.FormAutofillHandler( + formLike, + this.formSubmitted.bind(this) + ); + } else if (!formHandler.updateFormIfNeeded(element)) { + this.debug("No control is removed or inserted since last collection."); + return; + } + let validDetails = formHandler.collectFormFields(); + + this._formsDetails.set(formHandler.form.rootElement, formHandler); + + validDetails.forEach(detail => + this._markAsAutofillField(detail.elementWeakRef.get()) + ); + }, + + clearForm() { + let focusedInput = + this.activeInput || 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 = ProfileAutocomplete._getSelectedIndex(docWin); + let lastAutoCompleteResult = + ProfileAutocomplete.lastProfileAutoCompleteResult; + let focusedInput = this.activeInput; + let actor = getActorFromWindow(docWin); + + if ( + selectedIndex === -1 || + !focusedInput || + !lastAutoCompleteResult || + lastAutoCompleteResult.getStyleAt(selectedIndex) != "autofill-profile" + ) { + actor.sendAsyncMessage("FormAutofill:UpdateWarningMessage", {}); + + 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, + }); + + ProfileAutocomplete._previewSelectedProfile(selectedIndex); + } + }, + + onPopupClosed(selectedRowStyle) { + this.debug("Popup has closed."); + ProfileAutocomplete._clearProfilePreview(); + + let lastAutoCompleteResult = + ProfileAutocomplete.lastProfileAutoCompleteResult; + let focusedInput = FormAutofillContent.activeInput; + if ( + lastAutoCompleteResult && + FormAutofillContent._keyDownEnterForInput && + focusedInput === FormAutofillContent._keyDownEnterForInput && + focusedInput === 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; + 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 = + ProfileAutocomplete.lastProfileAutoCompleteResult; + let focusedInput = FormAutofillContent.activeInput; + if ( + e.keyCode != e.DOM_VK_RETURN || + !lastAutoCompleteResult || + !focusedInput || + focusedInput != ProfileAutocomplete.lastProfileAutoCompleteFocusedInput + ) { + return; + } + FormAutofillContent._keyDownEnterForInput = focusedInput; + }, +}; + +FormAutofillContent.init(); |