summaryrefslogtreecommitdiffstats
path: root/browser/extensions/formautofill/FormAutofillParent.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'browser/extensions/formautofill/FormAutofillParent.jsm')
-rw-r--r--browser/extensions/formautofill/FormAutofillParent.jsm878
1 files changed, 878 insertions, 0 deletions
diff --git a/browser/extensions/formautofill/FormAutofillParent.jsm b/browser/extensions/formautofill/FormAutofillParent.jsm
new file mode 100644
index 0000000000..47a965e7f5
--- /dev/null
+++ b/browser/extensions/formautofill/FormAutofillParent.jsm
@@ -0,0 +1,878 @@
+/* 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/. */
+
+/*
+ * Implements a service used to access storage and communicate with content.
+ *
+ * A "fields" array is used to communicate with FormAutofillContent. 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.
+ *
+ * [
+ * {
+ * section,
+ * addressType,
+ * contactType,
+ * fieldName,
+ * value,
+ * index
+ * },
+ * {
+ * // ...
+ * }
+ * ]
+ */
+
+"use strict";
+
+// We expose a singleton from this module. Some tests may import the
+// constructor via a backstage pass.
+var EXPORTED_SYMBOLS = ["FormAutofillParent", "FormAutofillStatus"];
+
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+
+const { FormAutofill } = ChromeUtils.import(
+ "resource://formautofill/FormAutofill.jsm"
+);
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.jsm",
+ CreditCard: "resource://gre/modules/CreditCard.jsm",
+ FormAutofillPreferences:
+ "resource://formautofill/FormAutofillPreferences.jsm",
+ FormAutofillDoorhanger: "resource://formautofill/FormAutofillDoorhanger.jsm",
+ FormAutofillUtils: "resource://formautofill/FormAutofillUtils.jsm",
+ OSKeyStore: "resource://gre/modules/OSKeyStore.jsm",
+});
+
+this.log = null;
+FormAutofill.defineLazyLogGetter(this, EXPORTED_SYMBOLS[0]);
+
+const {
+ ENABLED_AUTOFILL_ADDRESSES_PREF,
+ ENABLED_AUTOFILL_CREDITCARDS_PREF,
+} = FormAutofill;
+
+const {
+ ADDRESSES_COLLECTION_NAME,
+ CREDITCARDS_COLLECTION_NAME,
+} = FormAutofillUtils;
+
+let gMessageObservers = new Set();
+
+let FormAutofillStatus = {
+ _initialized: false,
+
+ /**
+ * Cache of the Form Autofill status (considering preferences and storage).
+ */
+ _active: null,
+
+ /**
+ * Initializes observers and registers the message handler.
+ */
+ init() {
+ if (this._initialized) {
+ return;
+ }
+ this._initialized = true;
+
+ Services.obs.addObserver(this, "privacy-pane-loaded");
+
+ // Observing the pref and storage changes
+ Services.prefs.addObserver(ENABLED_AUTOFILL_ADDRESSES_PREF, this);
+ Services.obs.addObserver(this, "formautofill-storage-changed");
+
+ // Only listen to credit card related preference if it is available
+ if (FormAutofill.isAutofillCreditCardsAvailable) {
+ Services.prefs.addObserver(ENABLED_AUTOFILL_CREDITCARDS_PREF, this);
+ }
+
+ for (let win of Services.wm.getEnumerator("navigator:browser")) {
+ this.injectElements(win.document);
+ }
+ Services.wm.addListener(this);
+
+ Services.telemetry.setEventRecordingEnabled("creditcard", true);
+ },
+
+ /**
+ * Uninitializes FormAutofillStatus. This is for testing only.
+ *
+ * @private
+ */
+ uninit() {
+ gFormAutofillStorage._saveImmediately();
+
+ if (!this._initialized) {
+ return;
+ }
+ this._initialized = false;
+
+ this._active = null;
+
+ Services.obs.removeObserver(this, "privacy-pane-loaded");
+ Services.prefs.removeObserver(ENABLED_AUTOFILL_ADDRESSES_PREF, this);
+ Services.wm.removeListener(this);
+
+ if (FormAutofill.isAutofillCreditCardsAvailable) {
+ Services.prefs.removeObserver(ENABLED_AUTOFILL_CREDITCARDS_PREF, this);
+ }
+ },
+
+ get formAutofillStorage() {
+ return gFormAutofillStorage;
+ },
+
+ /**
+ * Broadcast the status to frames when the form autofill status changes.
+ */
+ onStatusChanged() {
+ log.debug("onStatusChanged: Status changed to", this._active);
+ Services.ppmm.sharedData.set("FormAutofill:enabled", this._active);
+ // Sync autofill enabled to make sure the value is up-to-date
+ // no matter when the new content process is initialized.
+ Services.ppmm.sharedData.flush();
+ },
+
+ /**
+ * Query preference and storage status to determine the overall status of the
+ * form autofill feature.
+ *
+ * @returns {boolean} whether form autofill is active (enabled and has data)
+ */
+ computeStatus() {
+ const savedFieldNames = Services.ppmm.sharedData.get(
+ "FormAutofill:savedFieldNames"
+ );
+
+ return (
+ (Services.prefs.getBoolPref(ENABLED_AUTOFILL_ADDRESSES_PREF) ||
+ Services.prefs.getBoolPref(ENABLED_AUTOFILL_CREDITCARDS_PREF)) &&
+ savedFieldNames &&
+ savedFieldNames.size > 0
+ );
+ },
+
+ /**
+ * Update the status and trigger onStatusChanged, if necessary.
+ */
+ updateStatus() {
+ log.debug("updateStatus");
+ let wasActive = this._active;
+ this._active = this.computeStatus();
+ if (this._active !== wasActive) {
+ this.onStatusChanged();
+ }
+ },
+
+ updateSavedFieldNames() {
+ log.debug("updateSavedFieldNames");
+
+ let savedFieldNames;
+ // Don't access the credit cards store unless it is enabled.
+ if (FormAutofill.isAutofillCreditCardsAvailable) {
+ savedFieldNames = new Set([
+ ...gFormAutofillStorage.addresses.getSavedFieldNames(),
+ ...gFormAutofillStorage.creditCards.getSavedFieldNames(),
+ ]);
+ } else {
+ savedFieldNames = gFormAutofillStorage.addresses.getSavedFieldNames();
+ }
+
+ Services.ppmm.sharedData.set(
+ "FormAutofill:savedFieldNames",
+ savedFieldNames
+ );
+ Services.ppmm.sharedData.flush();
+
+ this.updateStatus();
+ },
+
+ injectElements(doc) {
+ Services.scriptloader.loadSubScript(
+ "chrome://formautofill/content/customElements.js",
+ doc.ownerGlobal
+ );
+ },
+
+ onOpenWindow(xulWindow) {
+ const win = xulWindow.docShell.domWindow;
+ win.addEventListener(
+ "load",
+ () => {
+ if (
+ win.document.documentElement.getAttribute("windowtype") ==
+ "navigator:browser"
+ ) {
+ this.injectElements(win.document);
+ }
+ },
+ { once: true }
+ );
+ },
+
+ onCloseWindow() {},
+
+ observe(subject, topic, data) {
+ log.debug("observe:", topic, "with data:", data);
+ switch (topic) {
+ case "privacy-pane-loaded": {
+ let formAutofillPreferences = new FormAutofillPreferences();
+ let document = subject.document;
+ let prefFragment = formAutofillPreferences.init(document);
+ let formAutofillGroupBox = document.getElementById(
+ "formAutofillGroupBox"
+ );
+ formAutofillGroupBox.appendChild(prefFragment);
+ break;
+ }
+
+ case "nsPref:changed": {
+ // Observe pref changes and update _active cache if status is changed.
+ this.updateStatus();
+ break;
+ }
+
+ case "formautofill-storage-changed": {
+ // Early exit if only metadata is changed
+ if (data == "notifyUsed") {
+ break;
+ }
+
+ this.updateSavedFieldNames();
+ break;
+ }
+
+ default: {
+ throw new Error(
+ `FormAutofillStatus: Unexpected topic observed: ${topic}`
+ );
+ }
+ }
+ },
+};
+
+// Lazily load the storage JSM to avoid disk I/O until absolutely needed.
+// Once storage is loaded we need to update saved field names and inform content processes.
+XPCOMUtils.defineLazyGetter(this, "gFormAutofillStorage", () => {
+ let { formAutofillStorage } = ChromeUtils.import(
+ "resource://formautofill/FormAutofillStorage.jsm"
+ );
+ log.debug("Loading formAutofillStorage");
+
+ formAutofillStorage.initialize().then(() => {
+ // Update the saved field names to compute the status and update child processes.
+ FormAutofillStatus.updateSavedFieldNames();
+ });
+
+ return formAutofillStorage;
+});
+
+class FormAutofillParent extends JSWindowActorParent {
+ constructor() {
+ super();
+ FormAutofillStatus.init();
+ }
+
+ static addMessageObserver(observer) {
+ gMessageObservers.add(observer);
+ }
+
+ static removeMessageObserver(observer) {
+ gMessageObservers.delete(observer);
+ }
+
+ /**
+ * Handles the message coming from FormAutofillContent.
+ *
+ * @param {string} message.name The name of the message.
+ * @param {object} message.data The data of the message.
+ */
+ async receiveMessage({ name, data }) {
+ switch (name) {
+ case "FormAutofill:InitStorage": {
+ await gFormAutofillStorage.initialize();
+ break;
+ }
+ case "FormAutofill:GetRecords": {
+ return FormAutofillParent._getRecords(data);
+ }
+ case "FormAutofill:OnFormSubmit": {
+ this.notifyMessageObservers("onFormSubmitted", data);
+ await this._onFormSubmit(data);
+ break;
+ }
+ case "FormAutofill:OpenPreferences": {
+ const win = BrowserWindowTracker.getTopWindow();
+ win.openPreferences("privacy-form-autofill");
+ break;
+ }
+ case "FormAutofill:GetDecryptedString": {
+ let { cipherText, reauth } = data;
+ if (!FormAutofillUtils._reauthEnabledByUser) {
+ log.debug("Reauth is disabled");
+ reauth = false;
+ }
+ let string;
+ try {
+ string = await OSKeyStore.decrypt(cipherText, reauth);
+ } catch (e) {
+ if (e.result != Cr.NS_ERROR_ABORT) {
+ throw e;
+ }
+ log.warn("User canceled encryption login");
+ }
+ return string;
+ }
+ case "FormAutofill:UpdateWarningMessage":
+ this.notifyMessageObservers("updateWarningNote", data);
+ break;
+
+ case "FormAutofill:FieldsIdentified":
+ this.notifyMessageObservers("fieldsIdentified", data);
+ break;
+
+ // The remaining Save and Remove messages are invoked only by tests.
+ case "FormAutofill:SaveAddress": {
+ if (data.guid) {
+ await gFormAutofillStorage.addresses.update(data.guid, data.address);
+ } else {
+ await gFormAutofillStorage.addresses.add(data.address);
+ }
+ break;
+ }
+ case "FormAutofill:SaveCreditCard": {
+ if (!(await FormAutofillUtils.ensureLoggedIn()).authenticated) {
+ log.warn("User canceled encryption login");
+ return undefined;
+ }
+ await gFormAutofillStorage.creditCards.add(data.creditcard);
+ break;
+ }
+ case "FormAutofill:RemoveAddresses": {
+ data.guids.forEach(guid => gFormAutofillStorage.addresses.remove(guid));
+ break;
+ }
+ case "FormAutofill:RemoveCreditCards": {
+ data.guids.forEach(guid =>
+ gFormAutofillStorage.creditCards.remove(guid)
+ );
+ break;
+ }
+ }
+
+ return undefined;
+ }
+
+ notifyMessageObservers(callbackName, data) {
+ for (let observer of gMessageObservers) {
+ try {
+ if (callbackName in observer) {
+ observer[callbackName](
+ data,
+ this.manager.browsingContext.topChromeWindow
+ );
+ }
+ } catch (ex) {
+ Cu.reportError(ex);
+ }
+ }
+ }
+
+ /**
+ * Get the records from profile store and return results back to content
+ * process. It will decrypt the credit card number and append
+ * "cc-number-decrypted" to each record if OSKeyStore isn't set.
+ *
+ * This is static as a unit test calls this.
+ *
+ * @private
+ * @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.
+ */
+ static async _getRecords({ collectionName, searchString, info }) {
+ let collection = gFormAutofillStorage[collectionName];
+ if (!collection) {
+ return [];
+ }
+
+ let recordsInCollection = await collection.getAll();
+ if (!info || !info.fieldName || !recordsInCollection.length) {
+ return recordsInCollection;
+ }
+
+ let isCC = collectionName == CREDITCARDS_COLLECTION_NAME;
+ // We don't filter "cc-number"
+ if (isCC && info.fieldName == "cc-number") {
+ recordsInCollection = recordsInCollection.filter(
+ record => !!record["cc-number"]
+ );
+ return recordsInCollection;
+ }
+
+ let records = [];
+ let lcSearchString = searchString.toLowerCase();
+
+ for (let record of recordsInCollection) {
+ let fieldValue = record[info.fieldName];
+ if (!fieldValue) {
+ continue;
+ }
+
+ if (
+ collectionName == ADDRESSES_COLLECTION_NAME &&
+ record.country &&
+ !FormAutofill.supportedCountries.includes(record.country)
+ ) {
+ // Address autofill isn't supported for the record's country so we don't
+ // want to attempt to potentially incorrectly fill the address fields.
+ continue;
+ }
+
+ if (
+ lcSearchString &&
+ !String(fieldValue)
+ .toLowerCase()
+ .startsWith(lcSearchString)
+ ) {
+ continue;
+ }
+ records.push(record);
+ }
+
+ return records;
+ }
+
+ async _onAddressSubmit(address, browser, timeStartedFillingMS) {
+ let showDoorhanger = null;
+ if (!FormAutofill.isAutofillAddressesCaptureEnabled) {
+ return showDoorhanger;
+ }
+ if (address.guid) {
+ // Avoid updating the fields that users don't modify.
+ let originalAddress = await gFormAutofillStorage.addresses.get(
+ address.guid
+ );
+ for (let field in address.record) {
+ if (address.untouchedFields.includes(field) && originalAddress[field]) {
+ address.record[field] = originalAddress[field];
+ }
+ }
+
+ if (
+ !(await gFormAutofillStorage.addresses.mergeIfPossible(
+ address.guid,
+ address.record,
+ true
+ ))
+ ) {
+ this._recordFormFillingTime(
+ "address",
+ "autofill-update",
+ timeStartedFillingMS
+ );
+
+ showDoorhanger = async () => {
+ const description = FormAutofillUtils.getAddressLabel(address.record);
+ const state = await FormAutofillDoorhanger.show(
+ browser,
+ "updateAddress",
+ description
+ );
+ let changedGUIDs = await gFormAutofillStorage.addresses.mergeToStorage(
+ address.record,
+ true
+ );
+ switch (state) {
+ case "create":
+ if (!changedGUIDs.length) {
+ changedGUIDs.push(
+ await gFormAutofillStorage.addresses.add(address.record)
+ );
+ }
+ break;
+ case "update":
+ if (!changedGUIDs.length) {
+ await gFormAutofillStorage.addresses.update(
+ address.guid,
+ address.record,
+ true
+ );
+ changedGUIDs.push(address.guid);
+ } else {
+ gFormAutofillStorage.addresses.remove(address.guid);
+ }
+ break;
+ }
+ changedGUIDs.forEach(guid =>
+ gFormAutofillStorage.addresses.notifyUsed(guid)
+ );
+ };
+ // Address should be updated
+ Services.telemetry.scalarAdd(
+ "formautofill.addresses.fill_type_autofill_update",
+ 1
+ );
+ } else {
+ this._recordFormFillingTime(
+ "address",
+ "autofill",
+ timeStartedFillingMS
+ );
+ gFormAutofillStorage.addresses.notifyUsed(address.guid);
+ // Address is merged successfully
+ Services.telemetry.scalarAdd(
+ "formautofill.addresses.fill_type_autofill",
+ 1
+ );
+ }
+ } else {
+ let changedGUIDs = await gFormAutofillStorage.addresses.mergeToStorage(
+ address.record
+ );
+ if (!changedGUIDs.length) {
+ changedGUIDs.push(
+ await gFormAutofillStorage.addresses.add(address.record)
+ );
+ }
+ changedGUIDs.forEach(guid =>
+ gFormAutofillStorage.addresses.notifyUsed(guid)
+ );
+ this._recordFormFillingTime("address", "manual", timeStartedFillingMS);
+
+ // Show first time use doorhanger
+ if (FormAutofill.isAutofillAddressesFirstTimeUse) {
+ Services.prefs.setBoolPref(
+ FormAutofill.ADDRESSES_FIRST_TIME_USE_PREF,
+ false
+ );
+ showDoorhanger = async () => {
+ const description = FormAutofillUtils.getAddressLabel(address.record);
+ const state = await FormAutofillDoorhanger.show(
+ browser,
+ "firstTimeUse",
+ description
+ );
+ if (state !== "open-pref") {
+ return;
+ }
+
+ browser.ownerGlobal.openPreferences("privacy-address-autofill");
+ };
+ } else {
+ // We want to exclude the first time form filling.
+ Services.telemetry.scalarAdd(
+ "formautofill.addresses.fill_type_manual",
+ 1
+ );
+ }
+ }
+ return showDoorhanger;
+ }
+
+ async _onCreditCardSubmit(creditCard, browser, timeStartedFillingMS) {
+ if (FormAutofill.isAutofillCreditCardsHideUI) {
+ return false;
+ }
+
+ // Updates the used status for shield/heartbeat to recognize users who have
+ // used Credit Card Autofill.
+ let setUsedStatus = status => {
+ if (FormAutofill.AutofillCreditCardsUsedStatus < status) {
+ Services.prefs.setIntPref(
+ FormAutofill.CREDITCARDS_USED_STATUS_PREF,
+ status
+ );
+ }
+ };
+
+ // Remove invalid cc-type values
+ if (
+ creditCard.record["cc-type"] &&
+ !CreditCard.isValidNetwork(creditCard.record["cc-type"])
+ ) {
+ // Let's reset the credit card to empty, and then network auto-detect will
+ // pick it up.
+ creditCard.record["cc-type"] = "";
+ }
+
+ // If `guid` is present, the form has been autofilled.
+ if (creditCard.guid) {
+ // Indicate that the user has used Credit Card Autofill to fill in a form.
+ setUsedStatus(3);
+
+ let originalCCData = await gFormAutofillStorage.creditCards.get(
+ creditCard.guid
+ );
+ let recordUnchanged = true;
+ for (let field in creditCard.record) {
+ if (creditCard.record[field] === "" && !originalCCData[field]) {
+ continue;
+ }
+ // Avoid updating the fields that users don't modify, but skip number field
+ // because we don't want to trigger decryption here.
+ let untouched = creditCard.untouchedFields.includes(field);
+ if (untouched && field !== "cc-number") {
+ creditCard.record[field] = originalCCData[field];
+ }
+ // recordUnchanged will be false if one of the field is changed.
+ recordUnchanged &= untouched;
+ }
+
+ if (recordUnchanged) {
+ gFormAutofillStorage.creditCards.notifyUsed(creditCard.guid);
+ // Add probe to record credit card autofill(without modification).
+ Services.telemetry.scalarAdd(
+ "formautofill.creditCards.fill_type_autofill",
+ 1
+ );
+ this._recordFormFillingTime(
+ "creditCard",
+ "autofill",
+ timeStartedFillingMS
+ );
+ return false;
+ }
+ // Add the probe to record credit card autofill with modification.
+ Services.telemetry.scalarAdd(
+ "formautofill.creditCards.fill_type_autofill_modified",
+ 1
+ );
+ this._recordFormFillingTime(
+ "creditCard",
+ "autofill-update",
+ timeStartedFillingMS
+ );
+ } else {
+ // Add the probe to record credit card manual filling.
+ Services.telemetry.scalarAdd(
+ "formautofill.creditCards.fill_type_manual",
+ 1
+ );
+ this._recordFormFillingTime("creditCard", "manual", timeStartedFillingMS);
+
+ let existingGuid = await gFormAutofillStorage.creditCards.getDuplicateGuid(
+ creditCard.record
+ );
+
+ if (existingGuid) {
+ creditCard.guid = existingGuid;
+
+ let originalCCData = await gFormAutofillStorage.creditCards.get(
+ creditCard.guid
+ );
+
+ gFormAutofillStorage.creditCards._normalizeRecord(creditCard.record);
+
+ // If the credit card record is a duplicate, check if the fields match the
+ // record.
+ let recordUnchanged = true;
+ for (let field in creditCard.record) {
+ if (field == "cc-number") {
+ continue;
+ }
+ if (creditCard.record[field] != originalCCData[field]) {
+ recordUnchanged = false;
+ break;
+ }
+ }
+
+ if (recordUnchanged) {
+ // Indicate that the user neither sees the doorhanger nor uses Autofill
+ // but somehow has a duplicate record in the storage. Will be reset to 2
+ // if the doorhanger actually shows below.
+ setUsedStatus(1);
+ gFormAutofillStorage.creditCards.notifyUsed(creditCard.guid);
+ return false;
+ }
+ }
+ }
+
+ // Indicate that the user has seen the doorhanger.
+ setUsedStatus(2);
+
+ return async () => {
+ // Suppress the pending doorhanger from showing up if user disabled credit card in previous doorhanger.
+ if (!FormAutofill.isAutofillCreditCardsEnabled) {
+ return;
+ }
+
+ let number =
+ creditCard.record["cc-number"] ||
+ creditCard.record["cc-number-decrypted"];
+ let name = creditCard.record["cc-name"];
+ const description = await CreditCard.getLabel({ name, number });
+
+ const telemetryObject = creditCard.guid
+ ? "update_doorhanger"
+ : "capture_doorhanger";
+ Services.telemetry.recordEvent(
+ "creditcard",
+ "show",
+ telemetryObject,
+ creditCard.flowId
+ );
+
+ const state = await FormAutofillDoorhanger.show(
+ browser,
+ creditCard.guid ? "updateCreditCard" : "addCreditCard",
+ description
+ );
+ if (state == "cancel") {
+ Services.telemetry.recordEvent(
+ "creditcard",
+ "cancel",
+ telemetryObject,
+ creditCard.flowId
+ );
+ return;
+ }
+
+ if (state == "disable") {
+ Services.prefs.setBoolPref(
+ "extensions.formautofill.creditCards.enabled",
+ false
+ );
+ Services.telemetry.recordEvent(
+ "creditcard",
+ "disable",
+ telemetryObject,
+ creditCard.flowId
+ );
+ return;
+ }
+
+ if (!(await FormAutofillUtils.ensureLoggedIn()).authenticated) {
+ log.warn("User canceled encryption login");
+ return;
+ }
+
+ let changedGUIDs = [];
+ if (creditCard.guid) {
+ if (state == "update") {
+ Services.telemetry.recordEvent(
+ "creditcard",
+ "update",
+ telemetryObject,
+ creditCard.flowId
+ );
+ await gFormAutofillStorage.creditCards.update(
+ creditCard.guid,
+ creditCard.record,
+ true
+ );
+ changedGUIDs.push(creditCard.guid);
+ } else if ("create") {
+ Services.telemetry.recordEvent(
+ "creditcard",
+ "save",
+ telemetryObject,
+ creditCard.flowId
+ );
+ changedGUIDs.push(
+ await gFormAutofillStorage.creditCards.add(creditCard.record)
+ );
+ }
+ } else {
+ changedGUIDs.push(
+ ...(await gFormAutofillStorage.creditCards.mergeToStorage(
+ creditCard.record
+ ))
+ );
+ if (!changedGUIDs.length) {
+ Services.telemetry.recordEvent(
+ "creditcard",
+ "save",
+ telemetryObject,
+ creditCard.flowId
+ );
+ changedGUIDs.push(
+ await gFormAutofillStorage.creditCards.add(creditCard.record)
+ );
+ }
+ }
+ changedGUIDs.forEach(guid =>
+ gFormAutofillStorage.creditCards.notifyUsed(guid)
+ );
+ };
+ }
+
+ async _onFormSubmit(data) {
+ let {
+ profile: { address, creditCard },
+ timeStartedFillingMS,
+ } = data;
+
+ // Don't record filling time if any type of records has more than one section being
+ // populated. We've been recording the filling time, so the other cases that aren't
+ // recorded on the same basis should be out of the data samples. E.g. Filling time of
+ // populating one profile is different from populating two sections, therefore, we
+ // shouldn't record the later to regress the representation of existing statistics.
+ if (address.length > 1 || creditCard.length > 1) {
+ timeStartedFillingMS = null;
+ }
+
+ let browser = this.manager.browsingContext.top.embedderElement;
+
+ // Transmit the telemetry immediately in the meantime form submitted, and handle these pending
+ // doorhangers at a later.
+ await Promise.all(
+ [
+ await Promise.all(
+ address.map(addrRecord =>
+ this._onAddressSubmit(addrRecord, browser, timeStartedFillingMS)
+ )
+ ),
+ await Promise.all(
+ creditCard.map(ccRecord =>
+ this._onCreditCardSubmit(ccRecord, browser, timeStartedFillingMS)
+ )
+ ),
+ ]
+ .map(pendingDoorhangers => {
+ return pendingDoorhangers.filter(
+ pendingDoorhanger =>
+ !!pendingDoorhanger && typeof pendingDoorhanger == "function"
+ );
+ })
+ .map(pendingDoorhangers =>
+ (async () => {
+ for (const showDoorhanger of pendingDoorhangers) {
+ await showDoorhanger();
+ }
+ })()
+ )
+ );
+ }
+
+ /**
+ * Set the probes for the filling time with specific filling type and form type.
+ *
+ * @private
+ * @param {string} formType
+ * 3 type of form (address/creditcard/address-creditcard).
+ * @param {string} fillingType
+ * 3 filling type (manual/autofill/autofill-update).
+ * @param {int|null} startedFillingMS
+ * Time that form started to filling in ms. Early return if start time is null.
+ */
+ _recordFormFillingTime(formType, fillingType, startedFillingMS) {
+ if (!startedFillingMS) {
+ return;
+ }
+ let histogram = Services.telemetry.getKeyedHistogramById(
+ "FORM_FILLING_REQUIRED_TIME_MS"
+ );
+ histogram.add(`${formType}-${fillingType}`, Date.now() - startedFillingMS);
+ }
+}