summaryrefslogtreecommitdiffstats
path: root/toolkit/components/formautofill/default/FormAutofillPrompter.sys.mjs
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
commit36d22d82aa202bb199967e9512281e9a53db42c9 (patch)
tree105e8c98ddea1c1e4784a60a5a6410fa416be2de /toolkit/components/formautofill/default/FormAutofillPrompter.sys.mjs
parentInitial commit. (diff)
downloadfirefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz
firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip
Adding upstream version 115.7.0esr.upstream/115.7.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/formautofill/default/FormAutofillPrompter.sys.mjs')
-rw-r--r--toolkit/components/formautofill/default/FormAutofillPrompter.sys.mjs677
1 files changed, 677 insertions, 0 deletions
diff --git a/toolkit/components/formautofill/default/FormAutofillPrompter.sys.mjs b/toolkit/components/formautofill/default/FormAutofillPrompter.sys.mjs
new file mode 100644
index 0000000000..31975cd968
--- /dev/null
+++ b/toolkit/components/formautofill/default/FormAutofillPrompter.sys.mjs
@@ -0,0 +1,677 @@
+/* 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 doorhanger singleton that wraps up the PopupNotifications and handles
+ * the doorhager UI for formautofill related features.
+ */
+
+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 { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+const { AutofillTelemetry } = ChromeUtils.import(
+ "resource://autofill/AutofillTelemetry.jsm"
+);
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ CreditCard: "resource://gre/modules/CreditCard.sys.mjs",
+});
+
+XPCOMUtils.defineLazyGetter(lazy, "log", () =>
+ FormAutofill.defineLogGetter(lazy, "FormAutofillPrompter")
+);
+
+const { ENABLED_AUTOFILL_CREDITCARDS_PREF } = FormAutofill;
+
+const GetStringFromName = FormAutofillUtils.stringBundle.GetStringFromName;
+const formatStringFromName =
+ FormAutofillUtils.stringBundle.formatStringFromName;
+const brandShortName =
+ FormAutofillUtils.brandBundle.GetStringFromName("brandShortName");
+let changeAutofillOptsKey = "changeAutofillOptions";
+let autofillOptsKey = "autofillOptionsLink";
+if (AppConstants.platform == "macosx") {
+ changeAutofillOptsKey += "OSX";
+ autofillOptsKey += "OSX";
+}
+
+const CONTENT = {
+ addFirstTimeUse: {
+ notificationId: "autofill-address",
+ message: formatStringFromName("saveAddressesMessage", [brandShortName]),
+ anchor: {
+ id: "autofill-address-notification-icon",
+ URL: "chrome://formautofill/content/formfill-anchor.svg",
+ tooltiptext: GetStringFromName("openAutofillMessagePanel"),
+ },
+ mainAction: {
+ label: GetStringFromName(changeAutofillOptsKey),
+ accessKey: GetStringFromName("changeAutofillOptionsAccessKey"),
+ callbackState: "open-pref",
+ },
+ options: {
+ persistWhileVisible: true,
+ popupIconURL: "chrome://formautofill/content/icon-address-save.svg",
+ checkbox: {
+ get checked() {
+ return Services.prefs.getBoolPref("services.sync.engine.addresses");
+ },
+ get label() {
+ // If sync account is not set, return null label to hide checkbox
+ return Services.prefs.prefHasUserValue("services.sync.username")
+ ? GetStringFromName("addressesSyncCheckbox")
+ : null;
+ },
+ callback(event) {
+ let checked = event.target.checked;
+ Services.prefs.setBoolPref("services.sync.engine.addresses", checked);
+ lazy.log.debug("Set addresses sync to", checked);
+ },
+ },
+ hideClose: true,
+ },
+ },
+ addAddress: {
+ notificationId: "autofill-address",
+ message: formatStringFromName("saveAddressesMessage", [brandShortName]),
+ descriptionLabel: GetStringFromName("saveAddressDescriptionLabel"),
+ descriptionIcon: true,
+ linkMessage: GetStringFromName(autofillOptsKey),
+ spotlightURL: "about:preferences#privacy-address-autofill",
+ anchor: {
+ id: "autofill-address-notification-icon",
+ URL: "chrome://formautofill/content/formfill-anchor.svg",
+ tooltiptext: GetStringFromName("openAutofillMessagePanel"),
+ },
+ mainAction: {
+ label: GetStringFromName("saveAddressLabel"),
+ accessKey: GetStringFromName("saveAddressAccessKey"),
+ callbackState: "create",
+ },
+ secondaryActions: [
+ {
+ label: GetStringFromName("cancelAddressLabel"),
+ accessKey: GetStringFromName("cancelAddressAccessKey"),
+ callbackState: "cancel",
+ },
+ ],
+ options: {
+ persistWhileVisible: true,
+ popupIconURL: "chrome://formautofill/content/icon-address-update.svg",
+ hideClose: true,
+ },
+ },
+ updateAddress: {
+ notificationId: "autofill-address",
+ message: GetStringFromName("updateAddressMessage"),
+ descriptionLabel: GetStringFromName("updateAddressNewDescriptionLabel"),
+ additionalDescriptionLabel: GetStringFromName(
+ "updateAddressOldDescriptionLabel"
+ ),
+ descriptionIcon: false,
+ linkMessage: GetStringFromName(autofillOptsKey),
+ spotlightURL: "about:preferences#privacy-address-autofill",
+ anchor: {
+ id: "autofill-address-notification-icon",
+ URL: "chrome://formautofill/content/formfill-anchor.svg",
+ tooltiptext: GetStringFromName("openAutofillMessagePanel"),
+ },
+ mainAction: {
+ label: GetStringFromName("updateAddressLabel"),
+ accessKey: GetStringFromName("updateAddressAccessKey"),
+ callbackState: "update",
+ },
+ secondaryActions: [
+ {
+ label: GetStringFromName("createAddressLabel"),
+ accessKey: GetStringFromName("createAddressAccessKey"),
+ callbackState: "create",
+ },
+ ],
+ options: {
+ persistWhileVisible: true,
+ popupIconURL: "chrome://formautofill/content/icon-address-update.svg",
+ hideClose: true,
+ },
+ },
+ addCreditCard: {
+ notificationId: "autofill-credit-card",
+ message: formatStringFromName("saveCreditCardMessage", [brandShortName]),
+ descriptionLabel: GetStringFromName("saveCreditCardDescriptionLabel"),
+ descriptionIcon: true,
+ linkMessage: GetStringFromName(autofillOptsKey),
+ spotlightURL: "about:preferences#privacy-credit-card-autofill",
+ anchor: {
+ id: "autofill-credit-card-notification-icon",
+ URL: "chrome://formautofill/content/formfill-anchor.svg",
+ tooltiptext: GetStringFromName("openAutofillMessagePanel"),
+ },
+ mainAction: {
+ label: GetStringFromName("saveCreditCardLabel"),
+ accessKey: GetStringFromName("saveCreditCardAccessKey"),
+ callbackState: "save",
+ },
+ secondaryActions: [
+ {
+ label: GetStringFromName("cancelCreditCardLabel"),
+ accessKey: GetStringFromName("cancelCreditCardAccessKey"),
+ callbackState: "cancel",
+ },
+ {
+ label: GetStringFromName("neverSaveCreditCardLabel"),
+ accessKey: GetStringFromName("neverSaveCreditCardAccessKey"),
+ callbackState: "disable",
+ },
+ ],
+ options: {
+ persistWhileVisible: true,
+ popupIconURL: "chrome://formautofill/content/icon-credit-card.svg",
+ hideClose: true,
+ checkbox: {
+ get checked() {
+ return Services.prefs.getBoolPref("services.sync.engine.creditcards");
+ },
+ get label() {
+ // Only set the label when the fallowing conditions existed:
+ // - sync account is set
+ // - credit card sync is disabled
+ // - credit card sync is available
+ // otherwise return null label to hide checkbox.
+ return Services.prefs.prefHasUserValue("services.sync.username") &&
+ !Services.prefs.getBoolPref("services.sync.engine.creditcards") &&
+ Services.prefs.getBoolPref(
+ "services.sync.engine.creditcards.available"
+ )
+ ? GetStringFromName("creditCardsSyncCheckbox")
+ : null;
+ },
+ callback(event) {
+ let { secondaryButton, menubutton } =
+ event.target.closest("popupnotification");
+ let checked = event.target.checked;
+ Services.prefs.setBoolPref(
+ "services.sync.engine.creditcards",
+ checked
+ );
+ secondaryButton.disabled = checked;
+ menubutton.disabled = checked;
+ lazy.log.debug("Set creditCard sync to", checked);
+ },
+ },
+ },
+ },
+ updateCreditCard: {
+ notificationId: "autofill-credit-card",
+ message: GetStringFromName("updateCreditCardMessage"),
+ descriptionLabel: GetStringFromName("updateCreditCardDescriptionLabel"),
+ descriptionIcon: true,
+ linkMessage: GetStringFromName(autofillOptsKey),
+ spotlightURL: "about:preferences#privacy-credit-card-autofill",
+ anchor: {
+ id: "autofill-credit-card-notification-icon",
+ URL: "chrome://formautofill/content/formfill-anchor.svg",
+ tooltiptext: GetStringFromName("openAutofillMessagePanel"),
+ },
+ mainAction: {
+ label: GetStringFromName("updateCreditCardLabel"),
+ accessKey: GetStringFromName("updateCreditCardAccessKey"),
+ callbackState: "update",
+ },
+ secondaryActions: [
+ {
+ label: GetStringFromName("createCreditCardLabel"),
+ accessKey: GetStringFromName("createCreditCardAccessKey"),
+ callbackState: "create",
+ },
+ ],
+ options: {
+ persistWhileVisible: true,
+ popupIconURL: "chrome://formautofill/content/icon-credit-card.svg",
+ hideClose: true,
+ },
+ },
+};
+
+export let FormAutofillPrompter = {
+ /**
+ * Generate the main action and secondary actions from content parameters and
+ * promise resolve.
+ *
+ * @private
+ * @param {object} mainActionParams
+ * Parameters for main action.
+ * @param {Array<object>} secondaryActionParams
+ * Array of the parameters for secondary actions.
+ * @param {Function} resolve Should be called in action callback.
+ * @returns {Array<object>}
+ Return the mainAction and secondary actions in an array for showing doorhanger
+ */
+ _createActions(mainActionParams, secondaryActionParams, resolve) {
+ if (!mainActionParams) {
+ return [null, null];
+ }
+
+ let { label, accessKey, callbackState } = mainActionParams;
+ let callback = resolve.bind(null, callbackState);
+ let mainAction = { label, accessKey, callback };
+
+ if (!secondaryActionParams) {
+ return [mainAction, null];
+ }
+
+ let secondaryActions = [];
+ for (let params of secondaryActionParams) {
+ let cb = resolve.bind(null, params.callbackState);
+ secondaryActions.push({
+ label: params.label,
+ accessKey: params.accessKey,
+ callback: cb,
+ });
+ }
+
+ return [mainAction, secondaryActions];
+ },
+ _getNotificationElm(browser, id) {
+ let notificationId = id + "-notification";
+ let chromeDoc = browser.ownerDocument;
+ return chromeDoc.getElementById(notificationId);
+ },
+ /**
+ * Append the link label element to the popupnotificationcontent.
+ *
+ * @param {XULElement} content
+ * popupnotificationcontent
+ * @param {string} message
+ * The localized string for link title.
+ * @param {string} link
+ * Makes it possible to open and highlight a section in preferences
+ */
+ _appendPrivacyPanelLink(content, message, link) {
+ let chromeDoc = content.ownerDocument;
+ let privacyLinkElement = chromeDoc.createXULElement("label", {
+ is: "text-link",
+ });
+ privacyLinkElement.setAttribute("useoriginprincipal", true);
+ privacyLinkElement.setAttribute(
+ "href",
+ link || "about:preferences#privacy-form-autofill"
+ );
+ privacyLinkElement.setAttribute("value", message);
+ content.appendChild(privacyLinkElement);
+ },
+
+ /**
+ * Append the description section to the popupnotificationcontent.
+ *
+ * @param {XULElement} content
+ * popupnotificationcontent
+ * @param {string} descriptionLabel
+ * The label showing above description.
+ * @param {string} descriptionIcon
+ * The src of description icon.
+ * @param {string} descriptionId
+ * The id of description
+ */
+ _appendDescription(
+ content,
+ descriptionLabel,
+ descriptionIcon,
+ descriptionId
+ ) {
+ let chromeDoc = content.ownerDocument;
+ let docFragment = chromeDoc.createDocumentFragment();
+
+ let descriptionLabelElement = chromeDoc.createXULElement("label");
+ descriptionLabelElement.setAttribute("value", descriptionLabel);
+ docFragment.appendChild(descriptionLabelElement);
+
+ let descriptionWrapper = chromeDoc.createXULElement("hbox");
+ descriptionWrapper.className = "desc-message-box";
+
+ if (descriptionIcon) {
+ let descriptionIconElement = chromeDoc.createXULElement("image");
+ if (
+ typeof descriptionIcon == "string" &&
+ (descriptionIcon.includes("cc-logo") ||
+ descriptionIcon.includes("icon-credit"))
+ ) {
+ descriptionIconElement.setAttribute("src", descriptionIcon);
+ }
+ descriptionWrapper.appendChild(descriptionIconElement);
+ }
+
+ let descriptionElement = chromeDoc.createXULElement(descriptionId);
+ descriptionWrapper.appendChild(descriptionElement);
+ docFragment.appendChild(descriptionWrapper);
+
+ content.appendChild(docFragment);
+ },
+
+ _updateDescription(content, descriptionId, description) {
+ let element = content.querySelector(descriptionId);
+ element.textContent = description;
+ },
+
+ /**
+ * Create an image element for notification anchor if it doesn't already exist.
+ *
+ * @param {XULElement} browser
+ * Target browser element for showing doorhanger.
+ * @param {object} anchor
+ * Anchor options for setting the anchor element.
+ * @param {string} anchor.id
+ * ID of the anchor element.
+ * @param {string} anchor.URL
+ * Path of the icon asset.
+ * @param {string} anchor.tooltiptext
+ * Tooltip string for the anchor.
+ */
+ _setAnchor(browser, anchor) {
+ let chromeDoc = browser.ownerDocument;
+ let { id, URL, tooltiptext } = anchor;
+ let anchorEt = chromeDoc.getElementById(id);
+ if (!anchorEt) {
+ let notificationPopupBox = chromeDoc.getElementById(
+ "notification-popup-box"
+ );
+ // Icon shown on URL bar
+ let anchorElement = chromeDoc.createXULElement("image");
+ anchorElement.id = id;
+ anchorElement.setAttribute("src", URL);
+ anchorElement.classList.add("notification-anchor-icon");
+ anchorElement.setAttribute("role", "button");
+ anchorElement.setAttribute("tooltiptext", tooltiptext);
+ notificationPopupBox.appendChild(anchorElement);
+ }
+ },
+ _addCheckboxListener(browser, { notificationId, options }) {
+ if (!options.checkbox) {
+ return;
+ }
+ let { checkbox } = this._getNotificationElm(browser, notificationId);
+
+ if (checkbox && !checkbox.hidden) {
+ checkbox.addEventListener("command", options.checkbox.callback);
+ }
+ },
+
+ _removeCheckboxListener(browser, { notificationId, options }) {
+ if (!options.checkbox) {
+ return;
+ }
+ let { checkbox } = this._getNotificationElm(browser, notificationId);
+
+ if (checkbox && !checkbox.hidden) {
+ checkbox.removeEventListener("command", options.checkbox.callback);
+ }
+ },
+
+ /**
+ * Show save or update address doorhanger
+ *
+ * @param {Element<browser>} browser Browser to show the save/update address prompt
+ * @param {object} storage Address storage
+ * @param {object} newRecord Address record to save
+ * @param {string} flowId Unique GUID to record a series of the same user action
+ * @param {object} options
+ * @param {object} [options.mergeableRecord] Record to be merged
+ * @param {Array} [options.mergeableFields] List of field name that can be merged
+ */
+ async promptToSaveAddress(
+ browser,
+ storage,
+ newRecord,
+ flowId,
+ { mergeableRecord, mergeableFields }
+ ) {
+ // Overwrite the guid if there is a duplicate
+ let doorhangerType;
+ if (mergeableRecord) {
+ doorhangerType = "updateAddress";
+ } else if (FormAutofill.isAutofillAddressesCaptureV2Enabled) {
+ doorhangerType = "addAddress";
+ } else {
+ doorhangerType = "addFirstTimeUse";
+ this._updateStorageAfterInteractWithPrompt("save", storage, newRecord);
+
+ // Show first time use doorhanger
+ if (FormAutofill.isAutofillAddressesFirstTimeUse) {
+ Services.prefs.setBoolPref(
+ FormAutofill.ADDRESSES_FIRST_TIME_USE_PREF,
+ false
+ );
+ } else {
+ return;
+ }
+ }
+
+ const description = FormAutofillUtils.getAddressLabel(newRecord);
+ const additionalDescription = mergeableRecord
+ ? FormAutofillUtils.getAddressLabel(mergeableRecord)
+ : null;
+
+ const state = await FormAutofillPrompter._showCCorAddressCaptureDoorhanger(
+ browser,
+ doorhangerType,
+ description,
+ flowId,
+ { additionalDescription }
+ );
+
+ if (state == "cancel") {
+ return;
+ } else if (state == "open-pref") {
+ browser.ownerGlobal.openPreferences("privacy-address-autofill");
+ return;
+ }
+
+ this._updateStorageAfterInteractWithPrompt(
+ state,
+ storage,
+ newRecord,
+ mergeableRecord?.guid
+ );
+ },
+
+ async promptToSaveCreditCard(browser, storage, record, flowId) {
+ // Overwrite the guid if there is a duplicate
+ let doorhangerType;
+ const duplicateRecord = (await storage.getDuplicateRecords(record).next())
+ .value;
+ if (duplicateRecord) {
+ doorhangerType = "updateCreditCard";
+ } else {
+ doorhangerType = "addCreditCard";
+ }
+
+ const number = record["cc-number"] || record["cc-number-decrypted"];
+ const name = record["cc-name"];
+ const network = lazy.CreditCard.getType(number);
+ const maskedNumber = lazy.CreditCard.getMaskedNumber(number);
+ const description = `${maskedNumber}` + (name ? `, ${name}` : ``);
+ const descriptionIcon = lazy.CreditCard.getCreditCardLogo(network);
+
+ const state = await FormAutofillPrompter._showCCorAddressCaptureDoorhanger(
+ browser,
+ doorhangerType,
+ description,
+ flowId,
+ { descriptionIcon }
+ );
+
+ if (state == "cancel") {
+ return;
+ } else if (state == "disable") {
+ Services.prefs.setBoolPref(ENABLED_AUTOFILL_CREDITCARDS_PREF, false);
+ return;
+ }
+
+ if (!(await FormAutofillUtils.ensureLoggedIn()).authenticated) {
+ lazy.log.warn("User canceled encryption login");
+ return;
+ }
+
+ this._updateStorageAfterInteractWithPrompt(
+ state,
+ storage,
+ record,
+ duplicateRecord?.guid
+ );
+ },
+
+ async _updateStorageAfterInteractWithPrompt(
+ state,
+ storage,
+ record,
+ guid = null
+ ) {
+ let changedGUID = null;
+ if (state == "create" || state == "save") {
+ changedGUID = await storage.add(record);
+ } else if (state == "update") {
+ await storage.update(guid, record, true);
+ changedGUID = guid;
+ }
+ storage.notifyUsed(changedGUID);
+ },
+
+ _getUpdatedCCIcon(network) {
+ return FormAutofillUtils.getCreditCardLogo(network);
+ },
+
+ /**
+ * Show different types of doorhanger by leveraging PopupNotifications.
+ *
+ * @param {XULElement} browser Target browser element for showing doorhanger.
+ * @param {string} type The type of the doorhanger. There will have first time use/update/credit card.
+ * @param {string} description The message that provides more information on doorhanger.
+ * @param {string} flowId guid used to correlate events relating to the same form
+ * @param {object} [options = {}] a list of options for this method
+ * @param {string} options.descriptionIcon The icon for descriotion
+ * @param {string} options.additionalDescription The message that provides more information on doorhanger.
+ * @returns {Promise} Resolved with action type when action callback is triggered.
+ */
+ async _showCCorAddressCaptureDoorhanger(
+ browser,
+ type,
+ description,
+ flowId,
+ { descriptionIcon = null, additionalDescription = null }
+ ) {
+ const telemetryType = type.endsWith("CreditCard")
+ ? AutofillTelemetry.CREDIT_CARD
+ : AutofillTelemetry.ADDRESS;
+ const isCapture = type.startsWith("add");
+
+ AutofillTelemetry.recordDoorhangerShown(telemetryType, flowId, isCapture);
+
+ lazy.log.debug("show doorhanger with type:", type);
+ return new Promise(resolve => {
+ let {
+ notificationId,
+ message,
+ descriptionLabel,
+ additionalDescriptionLabel,
+ linkMessage,
+ spotlightURL,
+ anchor,
+ mainAction,
+ secondaryActions,
+ options,
+ } = CONTENT[type];
+ descriptionIcon = descriptionIcon ?? CONTENT[type].descriptionIcon;
+
+ const { ownerGlobal: chromeWin, ownerDocument: chromeDoc } = browser;
+ options.eventCallback = topic => {
+ lazy.log.debug("eventCallback:", topic);
+
+ if (topic == "removed" || topic == "dismissed") {
+ this._removeCheckboxListener(browser, { notificationId, options });
+ return;
+ }
+
+ // The doorhanger is customizable only when notification box is shown
+ if (topic != "shown") {
+ return;
+ }
+ this._addCheckboxListener(browser, { notificationId, options });
+
+ // There's no preferences link or other customization in first time use doorhanger.
+ if (type == "addFirstTimeUse") {
+ return;
+ }
+
+ const DESCRIPTION_ID = "description";
+ const ADDITIONAL_DESCRIPTION_ID = "additional-description";
+ const NOTIFICATION_ID = notificationId + "-notification";
+
+ const notification = chromeDoc.getElementById(NOTIFICATION_ID);
+ const notificationContent =
+ notification.querySelector("popupnotificationcontent") ||
+ chromeDoc.createXULElement("popupnotificationcontent");
+ if (!notification.contains(notificationContent)) {
+ notificationContent.setAttribute("orient", "vertical");
+
+ this._appendDescription(
+ notificationContent,
+ descriptionLabel,
+ descriptionIcon,
+ DESCRIPTION_ID
+ );
+
+ if (additionalDescription) {
+ this._appendDescription(
+ notificationContent,
+ additionalDescriptionLabel,
+ descriptionIcon,
+ ADDITIONAL_DESCRIPTION_ID
+ );
+ }
+
+ this._appendPrivacyPanelLink(
+ notificationContent,
+ linkMessage,
+ spotlightURL
+ );
+
+ notification.appendNotificationContent(notificationContent);
+ }
+
+ this._updateDescription(
+ notificationContent,
+ DESCRIPTION_ID,
+ description
+ );
+ if (additionalDescription) {
+ this._updateDescription(
+ notificationContent,
+ ADDITIONAL_DESCRIPTION_ID,
+ additionalDescription
+ );
+ }
+ };
+ this._setAnchor(browser, anchor);
+ chromeWin.PopupNotifications.show(
+ browser,
+ notificationId,
+ message,
+ anchor.id,
+ ...this._createActions(mainAction, secondaryActions, resolve),
+ options
+ );
+ }).then(state => {
+ AutofillTelemetry.recordDoorhangerClicked(
+ telemetryType,
+ state,
+ flowId,
+ isCapture
+ );
+ return state;
+ });
+ },
+};