Source code
Revision control
Copy as Markdown
Other Tools
/* 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
/**
* 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",
});
ChromeUtils.defineLazyGetter(
lazy,
"l10n",
() =>
new Localization(
[
"toolkit/formautofill/formAutofill.ftl",
"branding/brand.ftl",
"browser/preferences/preferences.ftl",
],
true
)
);
const MANAGE_ADDRESSES_URL =
const MANAGE_CREDITCARDS_URL =
const EDIT_CREDIT_CARD_URL =
const {
MANAGE_ADDRESSES_L10N_IDS,
EDIT_ADDRESS_L10N_IDS,
MANAGE_CREDITCARDS_L10N_IDS,
EDIT_CREDITCARD_L10N_IDS,
} = FormAutofillUtils;
const { ENABLED_AUTOFILL_ADDRESSES_PREF, ENABLED_AUTOFILL_CREDITCARDS_PREF } =
FormAutofill;
const FORM_AUTOFILL_CONFIG = {
payments: {
l10nId: "autofill-payment-methods-header",
items: [
{
id: "saveAndFillPayments",
l10nId: "autofill-payment-methods-checkbox-message-2",
supportPage: "credit-card-autofill",
items: [
{
id: "requireOSAuthForPayments",
l10nId: "autofill-reauth-payment-methods-checkbox-2",
supportPage:
"credit-card-autofill#w_require-authentication-for-autofill",
},
],
},
{
id: "savedPaymentsButton",
l10nId: "autofill-payment-methods-manage-payments-button",
control: "moz-box-button",
controlAttrs: {
"search-l10n-ids": MANAGE_CREDITCARDS_L10N_IDS.concat(
EDIT_CREDITCARD_L10N_IDS
).join(","),
},
},
],
},
addresses: {
l10nId: "autofill-addresses-header",
items: [
{
id: "saveAndFillAddresses",
l10nId: "autofill-addresses-checkbox-message",
supportPage: "automatically-fill-your-address-web-forms",
},
{
id: "savedAddressesButton",
l10nId: "autofill-addresses-manage-addresses-button",
control: "moz-box-button",
controlAttrs: {
"search-l10n-ids": MANAGE_ADDRESSES_L10N_IDS.concat(
EDIT_ADDRESS_L10N_IDS
).join(","),
},
},
],
},
};
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.ownerGlobal;
win.Preferences.addAll([
// Credit cards and addresses
{ id: ENABLED_AUTOFILL_ADDRESSES_PREF, type: "bool" },
{ id: ENABLED_AUTOFILL_CREDITCARDS_PREF, type: "bool" },
{
id: "extensions.formautofill.creditCards.os-auth.locked.enabled",
type: "bool",
},
]);
win.Preferences.addSetting({
id: "saveAndFillAddresses",
pref: ENABLED_AUTOFILL_ADDRESSES_PREF,
visible: () => FormAutofill.isAutofillAddressesAvailable,
});
win.Preferences.addSetting({
id: "savedAddressesButton",
pref: null,
visible: () => FormAutofill.isAutofillAddressesAvailable,
onUserClick: e => {
e.preventDefault();
if (Services.prefs.getBoolPref("browser.settings-redesign.enabled")) {
e.target.ownerGlobal.gotoPref("paneManageAddresses");
} else {
e.target.ownerGlobal.gSubDialog.open(MANAGE_ADDRESSES_URL);
}
},
});
win.Preferences.addSetting({
id: "saveAndFillPayments",
pref: ENABLED_AUTOFILL_CREDITCARDS_PREF,
visible: () => FormAutofill.isAutofillCreditCardsAvailable,
});
win.Preferences.addSetting({
id: "savedPaymentsButton",
pref: null,
visible: () => FormAutofill.isAutofillCreditCardsAvailable,
onUserClick: e => {
e.preventDefault();
if (Services.prefs.getBoolPref("browser.settings-redesign.enabled")) {
e.target.ownerGlobal.gotoPref("paneManagePayments");
} else {
e.target.ownerGlobal.gSubDialog.open(MANAGE_CREDITCARDS_URL);
}
},
});
win.Preferences.addSetting({
id: "requireOSAuthForPayments",
visible: () => lazy.OSKeyStore.canReauth(),
get: () => FormAutofillUtils.getOSAuthEnabled(),
async set(checked) {
await FormAutofillPreferences.prototype.trySetOSAuthEnabled(
win,
checked
);
},
setup: emitChange => {
Services.obs.addObserver(emitChange, "OSAuthEnabledChange");
return () =>
Services.obs.removeObserver(emitChange, "OSAuthEnabledChange");
},
});
let paymentsGroup = document.querySelector(
"setting-group[groupid=payments]"
);
paymentsGroup.config = FORM_AUTOFILL_CONFIG.payments;
paymentsGroup.getSetting = win.Preferences.getSetting.bind(win.Preferences);
let addressesGroup = document.querySelector(
"setting-group[groupid=addresses]"
);
addressesGroup.config = FORM_AUTOFILL_CONFIG.addresses;
addressesGroup.getSetting = win.Preferences.getSetting.bind(
win.Preferences
);
}
async initializePaymentsStorage() {
await lazy.formAutofillStorage.initialize();
}
async initializeAddressesStorage() {
await lazy.formAutofillStorage.initialize();
}
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) {
FormAutofillUtils.setOSAuthEnabled(!checked);
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(record => record.timeCreated)
.map(record => {
const config = {
id: "payment-item",
control: "moz-box-item",
l10nId: "payment-moz-box-item",
iconSrc:
l10nArgs: {
cardNumber: record["cc-number"].replace(/^(\*+)(\d+)$/, "$1 $2"),
expDate: record["cc-exp"].replace(/^(\d{4})-\d{2}$/, "XX/$1"),
},
options: [
{
control: "moz-button",
iconSrc: "chrome://global/skin/icons/delete.svg",
type: "icon",
controlAttrs: {
slot: "actions",
action: "remove",
guid: record.guid,
},
},
{
control: "moz-button",
iconSrc: "chrome://global/skin/icons/edit.svg",
type: "icon",
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 config = {
id: "address-item",
control: "moz-box-item",
l10nId: "address-moz-box-item",
iconSrc: "chrome://browser/skin/notification-icons/geo.svg",
l10nArgs: {
name: `${record.name}`,
address: addressFormatted,
},
options: [
{
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,
},
},
{
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,
];
}
/**
* 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
) {
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);
// Confirmed
if (propBag.get("buttonNumClicked") === 0) {
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 = {};
let errorResult = 0;
if (creditCard && creditCard["cc-number-encrypted"]) {
try {
decryptedCCNumObj["cc-number"] = await lazy.OSKeyStore.decrypt(
creditCard["cc-number-encrypted"],
"formautofill_cc"
);
} catch (ex) {
errorResult = ex.result;
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);
} finally {
Glean.creditcard.osKeystoreDecrypt.record({
isDecryptSuccess: errorResult === 0,
errorResult,
trigger: "edit",
});
}
}
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
) {
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);
// Confirmed
if (propBag.get("buttonNumClicked") === 0) {
lazy.formAutofillStorage.addresses.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(),
});
}
}