diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /toolkit/components/formautofill/default/FormAutofillPrompter.sys.mjs | |
parent | Initial commit. (diff) | |
download | firefox-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.mjs | 677 |
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; + }); + }, +}; |