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.
*/
const MANAGE_ADDRESSES_URL =
const MANAGE_CREDITCARDS_URL =
const EDIT_CREDIT_CARD_URL =
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 { 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",
},
],
},
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",
},
],
},
};
export class FormAutofillPreferences {
/**
* Create the Form Autofill preference group.
*
* @param {HTMLDocument} document
* @returns {XULElement}
*/
init(document) {
this.createPreferenceGroup(document);
return this.refs.formAutofillFragment;
}
/**
* Remove event listeners and the preference group.
*/
uninit() {
this.refs.formAutofillGroup.remove();
}
/**
* Create Form Autofill preference group
*
* @param {HTMLDocument} document
*/
createPreferenceGroup(document) {
const win = document.ownerGlobal;
this.refs = {};
this.refs.formAutofillGroup = document.querySelector(
"#formAutofillGroupBox"
);
let showAddressUI = FormAutofill.isAutofillAddressesAvailable;
let showCreditCardUI = FormAutofill.isAutofillCreditCardsAvailable;
if (!showAddressUI && !showCreditCardUI) {
return;
}
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: ({ target }) => {
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 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();
if (!records.length) {
return [];
}
const items = records
.sort(record => record.timeCreated)
.map(record => {
const config = {
id: "payment-item",
control: "moz-box-item",
l10nId: "payment-moz-box-item",
l10nArgs: {
cardNumber: record["cc-number"].replace(/^(\*+)(\d+)$/, "$2$1"),
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-item-label",
},
...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,
}
);
}
}