/* 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/. */ /** * Injects the form autofill section into about:preferences. */ import { FormAutofill } from "resource://autofill/FormAutofill.sys.mjs"; import { FormAutofillUtils } from "resource://gre/modules/shared/FormAutofillUtils.sys.mjs"; const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { OSKeyStore: "resource://gre/modules/OSKeyStore.sys.mjs", formAutofillStorage: "resource://autofill/FormAutofillStorage.sys.mjs", ManageAddresses: "chrome://formautofill/content/manageDialog.mjs", }); ChromeUtils.defineLazyGetter( lazy, "l10n", () => new Localization( [ "toolkit/formautofill/formAutofill.ftl", "branding/brand.ftl", "browser/preferences/preferences.ftl", ], true ) ); const EDIT_ADDRESS_URL = "chrome://formautofill/content/editAddress.xhtml"; const EDIT_CREDIT_CARD_URL = "chrome://formautofill/content/editCreditCard.xhtml"; const EDIT_PASSPORT_URL = "chrome://formautofill/content/editPassport.xhtml"; export class FormAutofillPreferences { /** * Create the Form Autofill preference group. * * @param {HTMLDocument} document * @returns {XULElement} */ init(document) { this.createPreferenceGroup(document); } /** * Create Form Autofill preference group * * @param {HTMLDocument} document */ createPreferenceGroup(document) { const win = document.documentGlobal; Services.obs.notifyObservers(win, "formautofill-preferences-initialized"); } async initializePaymentsStorage() { await lazy.formAutofillStorage.initialize(); } async initializeAddressesStorage() { await lazy.formAutofillStorage.initialize(); } async initializePassportsStorage() { await lazy.formAutofillStorage.initialize(); } /** * Helper that sets OS Auth from the about:preferences, if authorized. * * @param {object} win * The browser window. * @param {boolean} checked * The new state to set OS auth for payments, which determines if its * enabled or not. If not authorized, set to the current checked state. */ static async trySetOSAuthEnabled(win, checked) { let messageText = await lazy.l10n.formatValueSync( "autofill-creditcard-os-dialog-message" ); let captionText = await lazy.l10n.formatValueSync( "autofill-creditcard-os-auth-dialog-caption" ); // Calling OSKeyStore.ensureLoggedIn() instead of FormAutofillUtils.verifyOSAuth() // since we want to authenticate user each time this setting is changed. // Note on Glean collection: because OSKeyStore.ensureLoggedIn() is not wrapped in // verifyOSAuth(), it will be documenting "success" for unsupported platforms // and won't record "fail_error", only "fail_user_canceled" let isAuthorized = ( await lazy.OSKeyStore.ensureLoggedIn(messageText, captionText, win, false) ).authenticated; Glean.formautofill.promptShownOsReauth.record({ trigger: "toggle_pref_os_auth", result: isAuthorized ? "success" : "fail_user_canceled", }); if (!isAuthorized) { return; } // If target.checked is checked, enable OSAuth. Otherwise, reset the pref value. FormAutofillUtils.setOSAuthEnabled(checked); Glean.formautofill.requireOsReauthToggle.record({ toggle_state: checked, }); } async makePaymentsListItems() { const records = await lazy.formAutofillStorage.creditCards.getAll(); let items = []; if (!records.length) { items = [ { id: "no-payments-stored", l10nId: "payments-no-payments-stored-message", control: "moz-box-item", l10nArgs: {}, }, ]; } else { items = records .sort((a, b) => (a.timeLastUsed || a.timeLastModified) < (b.timeLastUsed || b.timeLastModified) ? 1 : -1 ) .map(record => { const expDate = (record["cc-exp"] ?? "").replace( /^(\d{4})-(\d{2})$/, "$2/$1" ); const hasSecurityCode = FormAutofill.isAutofillCreditCardCVVEnabled && !!record["cc-csc"]; let l10nId = "payment-moz-box-item"; if (hasSecurityCode) { l10nId = expDate ? "payment-moz-box-item-with-security-code" : "payment-moz-box-item-security-code-only"; } const config = { id: "payment-item", control: "moz-box-item", l10nId, iconSrc: "chrome://browser/skin/payment-methods-16.svg", l10nArgs: { cardNumber: record["cc-number"].replace(/^(\*+)(\d+)$/, "$1 $2"), expDate, }, options: [ { control: "moz-button", iconSrc: "chrome://global/skin/icons/delete.svg", type: "icon", l10nId: "payments-delete-payment-button-label", controlAttrs: { slot: "actions", action: "remove", guid: record.guid, }, }, { control: "moz-button", iconSrc: "chrome://global/skin/icons/edit.svg", type: "icon", l10nId: "payments-edit-payment-button-label", controlAttrs: { slot: "actions", action: "edit", guid: record.guid, }, }, ], }; return config; }); } return [ { id: "payments-list-header", control: "moz-box-item", l10nId: "payments-list-header", slot: "header", }, ...items, ]; } async makeAddressesListItems() { const addresses = await lazy.formAutofillStorage.addresses.getAll(); const records = addresses.slice().reverse(); let items = []; if (!records.length) { items = [ { id: "no-addresses-stored", l10nId: "addresses-no-addresses-stored-message", l10nArgs: {}, control: "moz-box-item", }, ]; } else { items = records.map(record => { const addressFormatted = [ record["street-address"], record["address-level2"], record["address-level1"], record.country, record["postal-code"], ] .filter(Boolean) .join(", "); const label = record.name || record.organization || record.email; const config = { id: "address-item", control: "moz-box-item", iconSrc: "chrome://browser/skin/notification-icons/geo.svg", controlAttrs: { label: label || addressFormatted, description: label ? addressFormatted : "", }, options: [ { id: "delete-address-button", control: "moz-button", iconSrc: "chrome://global/skin/icons/delete.svg", type: "icon", l10nId: "addreses-delete-address-button-label", controlAttrs: { slot: "actions", action: "remove", guid: record.guid, }, }, { id: "edit-address-button", control: "moz-button", iconSrc: "chrome://global/skin/icons/edit.svg", type: "icon", l10nId: "addreses-edit-address-button-label", controlAttrs: { slot: "actions", action: "edit", guid: record.guid, }, }, ], }; return config; }); } return [ { id: "addresses-list-header", control: "moz-box-item", l10nId: "addresses-list-header", slot: "header", }, ...items, ]; } async makePassportsListItems() { const passports = await lazy.formAutofillStorage.passports.getAll(); const records = passports.slice().reverse(); let items = []; if (!records.length) { items = [ { id: "no-passports-stored", l10nId: "passports-no-passports-stored-message", l10nArgs: {}, control: "moz-box-item", }, ]; } else { items = records.map(record => { const maskedNumber = record["passport-number"] ? "****" + record["passport-number"].slice(-4) : ""; const pad = value => String(value).padStart(2, "0"); const expiry = [ record["passport-expiry-date-month"] && pad(record["passport-expiry-date-month"]), record["passport-expiry-date-day"] && pad(record["passport-expiry-date-day"]), record["passport-expiry-date-year"], ] .filter(Boolean) .join("/"); const description = [maskedNumber, expiry].filter(Boolean).join(", "); const config = { id: "passport-item", control: "moz-box-item", iconSrc: "chrome://browser/skin/personal-info-16.svg", controlAttrs: { label: record["passport-name"] || maskedNumber, description, }, options: [ { id: "delete-passport-button", control: "moz-button", iconSrc: "chrome://global/skin/icons/delete.svg", type: "icon", l10nId: "passports-delete-passport-button-label", controlAttrs: { slot: "actions", action: "remove", guid: record.guid, }, }, { id: "edit-passport-button", control: "moz-button", iconSrc: "chrome://global/skin/icons/edit.svg", type: "icon", l10nId: "passports-edit-passport-button-label", controlAttrs: { slot: "actions", action: "edit", guid: record.guid, }, }, ], }; return config; }); } return [ { id: "passports-list-header", control: "moz-box-item", l10nId: "passports-list-header", slot: "header", }, ...items, ]; } /** * Show a modal prompt asking the user to confirm removing a stored record. * * @param {object} browsingContext * Browsing context to open the prompt in * @param {string} title * The title text displayed in the modal to prompt the user with * @param {string} confirmBtn * The text for the confirm button * @param {string} cancelBtn * The text for the cancel button * @returns {Promise} * Whether the user confirmed the removal. */ static async #confirmRemoveRecordDialog( browsingContext, title, confirmBtn, cancelBtn ) { const flags = Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_0 + Services.prompt.BUTTON_TITLE_CANCEL * Services.prompt.BUTTON_POS_1; const result = await Services.prompt.asyncConfirmEx( browsingContext, Services.prompt.MODAL_TYPE_INTERNAL_WINDOW, title, null, flags, confirmBtn, cancelBtn, null, null, false ); const propBag = result.QueryInterface(Ci.nsIPropertyBag2); return propBag.get("buttonNumClicked") === 0; } /** * Open the browser window modal to prompt the user whether * or they want to remove their payment. * * @param {string} guid * The guid of the payment item we are prompting to remove. * @param {object} browsingContext * Browsing context to open the prompt in * @param {string} title * The title text displayed in the modal to prompt the user with * @param {string} confirmBtn * The text for confirming removing a payment method * @param {string} cancelBtn * The text for cancelling removing a payment method */ async openRemovePaymentDialog( guid, browsingContext, title, confirmBtn, cancelBtn ) { if ( await FormAutofillPreferences.#confirmRemoveRecordDialog( browsingContext, title, confirmBtn, cancelBtn ) ) { lazy.formAutofillStorage.creditCards.remove(guid); } } async openEditCreditCardDialog(guid, window) { const creditCard = await lazy.formAutofillStorage.creditCards.get(guid); return FormAutofillPreferences.openEditCreditCardDialog(creditCard, window); } /** * Open the edit credit card dialog to create/edit a credit card. * * @param {object} creditCard * The credit card we want to edit. */ static async openEditCreditCardDialog(creditCard, window) { // Ask for reauth if user is trying to edit an existing credit card. if (creditCard) { const promptMessage = FormAutofillUtils.reauthOSPromptMessage( "autofill-edit-payment-method-os-prompt-macos", "autofill-edit-payment-method-os-prompt-windows", "autofill-edit-payment-method-os-prompt-other" ); let verified; let result; try { verified = await FormAutofillUtils.verifyUserOSAuth( FormAutofill.AUTOFILL_CREDITCARDS_OS_AUTH_LOCKED_PREF, promptMessage ); result = verified ? "success" : "fail_user_canceled"; } catch (ex) { result = "fail_error"; throw ex; } finally { Glean.formautofill.promptShownOsReauth.record({ trigger: "edit", result, }); } if (!verified) { return; } } let decryptedCCNumObj = {}; if (creditCard) { try { // The re-authentication above has already happened, so this only // reads. The trigger labels the telemetry of whichever store holds the // key, and that store is what records it: reporting here as well would // have a profile served by Rust also report an OS key store call it // never made. const number = await lazy.formAutofillStorage.creditCards.decryptField( creditCard, "cc-number", { trigger: "edit" } ); // Null means the record holds nothing encrypted under that field, so // there is nothing to put in place of what it already carries. if (number !== null) { decryptedCCNumObj["cc-number"] = number; } } catch (ex) { if (ex.result == Cr.NS_ERROR_ABORT) { // User shouldn't be ask to reauth here, but it could happen. // Return here and skip opening the dialog. return; } // We've got ourselves a real error. // Recover from encryption error so the user gets a chance to re-enter // unencrypted credit card number. decryptedCCNumObj["cc-number"] = ""; console.error(ex); } } let decryptedCreditCard = Object.assign({}, creditCard, decryptedCCNumObj); window.gSubDialog.open( EDIT_CREDIT_CARD_URL, { features: "resizable=no" }, { record: decryptedCreditCard, } ); } /** * Open the browser window modal to prompt the user whether * or they want to remove their address. * * @param {string} guid * The guid of the address item we are prompting to remove. * @param {object} browsingContext * Browsing context to open the prompt in * @param {string} title * The title text displayed in the modal to prompt the user with * @param {string} confirmBtn * The text for confirming the removal of an address * @param {string} cancelBtn * The text for cancelling removal of an address */ async openRemoveAddressDialog( guid, browsingContext, title, confirmBtn, cancelBtn ) { if ( await FormAutofillPreferences.#confirmRemoveRecordDialog( browsingContext, title, confirmBtn, cancelBtn ) ) { await lazy.formAutofillStorage.addresses.remove(guid); } } /** * Open the browser window modal to prompt the user whether * or they want to remove their passport. * * @param {string} guid * The guid of the passport item we are prompting to remove. * @param {object} browsingContext * Browsing context to open the prompt in * @param {string} title * The title text displayed in the modal to prompt the user with * @param {string} confirmBtn * The text for confirming the removal of a passport * @param {string} cancelBtn * The text for cancelling removal of a passport */ async openRemovePassportDialog( guid, browsingContext, title, confirmBtn, cancelBtn ) { if ( await FormAutofillPreferences.#confirmRemoveRecordDialog( browsingContext, title, confirmBtn, cancelBtn ) ) { lazy.formAutofillStorage.passports.remove(guid); } } async openEditAddressDialog(guid, window) { const address = await lazy.formAutofillStorage.addresses.get(guid); return FormAutofillPreferences.openEditAddressDialog(address, window); } /** * Open the edit address dialog to create/edit an address. * * @param {object} address * The address we want to edit. */ static async openEditAddressDialog(address, window) { window.gSubDialog.open(EDIT_ADDRESS_URL, undefined, { record: address ?? undefined, // Don't validate in preferences since it's fine for fields to be missing // for autofill purposes. For PaymentRequest addresses get more validation. noValidate: true, l10nStrings: lazy.ManageAddresses.getAddressL10nStrings(), }); } async openEditPassportDialog(guid, window) { const passport = guid ? await lazy.formAutofillStorage.passports.get(guid) : undefined; return FormAutofillPreferences.openEditPassportDialog(passport, window); } /** * Open the edit passport dialog to create/edit a passport. * * @param {object} passport * The passport we want to edit. */ static async openEditPassportDialog(passport, window) { window.gSubDialog.open(EDIT_PASSPORT_URL, undefined, { record: passport ?? undefined, }); } static openPaymentPreference() { const win = Services.wm.getMostRecentBrowserWindow(); win.openPreferences("privacy-payment-methods-autofill"); } static openAddressPreference() { const win = Services.wm.getMostRecentBrowserWindow(); win.openPreferences("privacy-address-autofill"); } }