diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
commit | 43a97878ce14b72f0981164f87f2e35e14151312 (patch) | |
tree | 620249daf56c0258faa40cbdcf9cfba06de2a846 /toolkit/components/formautofill/default | |
parent | Initial commit. (diff) | |
download | firefox-upstream.tar.xz firefox-upstream.zip |
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/formautofill/default')
-rw-r--r-- | toolkit/components/formautofill/default/FormAutofillPrompter.jsm | 540 | ||||
-rw-r--r-- | toolkit/components/formautofill/default/FormAutofillStorage.jsm | 289 |
2 files changed, 829 insertions, 0 deletions
diff --git a/toolkit/components/formautofill/default/FormAutofillPrompter.jsm b/toolkit/components/formautofill/default/FormAutofillPrompter.jsm new file mode 100644 index 0000000000..6656c0a062 --- /dev/null +++ b/toolkit/components/formautofill/default/FormAutofillPrompter.jsm @@ -0,0 +1,540 @@ +/* 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. + */ + +"use strict"; + +var EXPORTED_SYMBOLS = ["FormAutofillPrompter"]; + +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); +const { AutofillTelemetry } = ChromeUtils.import( + "resource://autofill/AutofillTelemetry.jsm" +); +const { FormAutofill } = ChromeUtils.import( + "resource://autofill/FormAutofill.jsm" +); +const { FormAutofillUtils } = ChromeUtils.import( + "resource://autofill/FormAutofillUtils.jsm" +); +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + CreditCard: "resource://gre/modules/CreditCard.sys.mjs", +}); + +XPCOMUtils.defineLazyGetter(lazy, "log", () => + FormAutofill.defineLogGetter(lazy, EXPORTED_SYMBOLS[0]) +); + +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 = { + firstTimeUse: { + 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, + }, + }, + updateAddress: { + notificationId: "autofill-address", + message: GetStringFromName("updateAddressMessage"), + descriptionLabel: GetStringFromName("updateAddressDescriptionLabel"), + 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, + }, + }, +}; + +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. + */ + _appendDescription(content, descriptionLabel, descriptionIcon) { + 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("description"); + descriptionWrapper.appendChild(descriptionElement); + docFragment.appendChild(descriptionWrapper); + + content.appendChild(docFragment); + }, + + _updateDescription(content, description) { + content.querySelector("description").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); + } + }, + + async promptToSaveAddress(browser, address, description) { + const state = this._showCCorAddressCaptureDoorhanger( + browser, + address, + address.guid ? "updateAddress" : "firstTimeUse", + description + ); + + return state; + }, + + async promptToSaveCreditCard(browser, creditCard, storage) { + let number = + creditCard.record["cc-number"] || + creditCard.record["cc-number-decrypted"]; + let name = creditCard.record["cc-name"]; + let type = lazy.CreditCard.getType(number); + let maskedNumber = lazy.CreditCard.getMaskedNumber(number); + let description = `${maskedNumber}, ${name}`; + + const state = await FormAutofillPrompter._showCCorAddressCaptureDoorhanger( + browser, + creditCard, + creditCard.guid ? "updateCreditCard" : "addCreditCard", + description, + type + ); + + 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; + } + + let changedGUID = null; + if (state == "create" || state == "save") { + changedGUID = await storage.creditCards.add(creditCard.record); + } else if (state == "update") { + await storage.creditCards.update( + creditCard.guid, + creditCard.record, + true + ); + changedGUID = creditCard.guid; + } + storage.creditCards.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 {object} record The record being saved + * @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} network The network type for credit card doorhangers. + * @returns {Promise} Resolved with action type when action callback is triggered. + */ + async _showCCorAddressCaptureDoorhanger( + browser, + record, + type, + description, + network + ) { + const telemetryType = ["updateCreditCard", "addCreditCard"].includes(type) + ? AutofillTelemetry.CREDIT_CARD + : AutofillTelemetry.ADDRESS; + + AutofillTelemetry.recordDoorhangerShown(telemetryType, record); + + lazy.log.debug("show doorhanger with type:", type); + return new Promise(resolve => { + let { + notificationId, + message, + descriptionLabel, + descriptionIcon, + linkMessage, + spotlightURL, + anchor, + mainAction, + secondaryActions, + options, + } = CONTENT[type]; + // Follow up in Bug 1737329 to make doorhanger types more explicit + if (type == "updateCreditCard" || type == "addCreditCard") { + descriptionIcon = lazy.CreditCard.getCreditCardLogo(network); + } + + 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 == "firstTimeUse") { + return; + } + + const notificationElementId = notificationId + "-notification"; + const notification = chromeDoc.getElementById(notificationElementId); + const notificationContent = + notification.querySelector("popupnotificationcontent") || + chromeDoc.createXULElement("popupnotificationcontent"); + if (!notification.contains(notificationContent)) { + notificationContent.setAttribute("orient", "vertical"); + this._appendDescription( + notificationContent, + descriptionLabel, + descriptionIcon + ); + this._appendPrivacyPanelLink( + notificationContent, + linkMessage, + spotlightURL + ); + notification.appendNotificationContent(notificationContent); + } + this._updateDescription(notificationContent, description); + }; + this._setAnchor(browser, anchor); + chromeWin.PopupNotifications.show( + browser, + notificationId, + message, + anchor.id, + ...this._createActions(mainAction, secondaryActions, resolve), + options + ); + }).then(state => { + AutofillTelemetry.recordDoorhangerClicked(telemetryType, state, record); + return state; + }); + }, +}; diff --git a/toolkit/components/formautofill/default/FormAutofillStorage.jsm b/toolkit/components/formautofill/default/FormAutofillStorage.jsm new file mode 100644 index 0000000000..6fff055b8b --- /dev/null +++ b/toolkit/components/formautofill/default/FormAutofillStorage.jsm @@ -0,0 +1,289 @@ +/* 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 an interface of the storage of Form Autofill. + */ + +"use strict"; + +// We expose a singleton from this module. Some tests may import the +// constructor via a backstage pass. +const EXPORTED_SYMBOLS = ["formAutofillStorage", "FormAutofillStorage"]; + +const { FormAutofill } = ChromeUtils.import( + "resource://autofill/FormAutofill.jsm" +); + +const { + FormAutofillStorageBase, + CreditCardsBase, + AddressesBase, +} = ChromeUtils.import("resource://autofill/FormAutofillStorageBase.jsm"); + +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + CreditCard: "resource://gre/modules/CreditCard.sys.mjs", + JSONFile: "resource://gre/modules/JSONFile.sys.mjs", + OSKeyStore: "resource://gre/modules/OSKeyStore.sys.mjs", +}); + +XPCOMUtils.defineLazyModuleGetters(lazy, { + FormAutofillUtils: "resource://autofill/FormAutofillUtils.jsm", +}); + +const PROFILE_JSON_FILE_NAME = "autofill-profiles.json"; + +class Addresses extends AddressesBase { + /** + * Merge new address into the specified address if mergeable. + * + * @param {string} guid + * Indicates which address to merge. + * @param {object} address + * The new address used to merge into the old one. + * @param {boolean} strict + * In strict merge mode, we'll treat the subset record with empty field + * as unable to be merged, but mergeable if in non-strict mode. + * @returns {Promise<boolean>} + * Return true if address is merged into target with specific guid or false if not. + */ + async mergeIfPossible(guid, address, strict) { + this.log.debug(`mergeIfPossible: ${guid}`); + + let addressFound = this._findByGUID(guid); + if (!addressFound) { + throw new Error("No matching address."); + } + + let addressToMerge = this._clone(address); + this._normalizeRecord(addressToMerge, strict); + let hasMatchingField = false; + + let country = + addressFound.country || + addressToMerge.country || + FormAutofill.DEFAULT_REGION; + let collators = lazy.FormAutofillUtils.getSearchCollators(country); + for (let field of this.VALID_FIELDS) { + let existingField = addressFound[field]; + let incomingField = addressToMerge[field]; + if (incomingField !== undefined && existingField !== undefined) { + if (incomingField != existingField) { + // Treat "street-address" as mergeable if their single-line versions + // match each other. + if ( + field == "street-address" && + lazy.FormAutofillUtils.compareStreetAddress( + existingField, + incomingField, + collators + ) + ) { + // Keep the street-address in storage if its amount of lines is greater than + // or equal to the incoming one. + if ( + existingField.split("\n").length >= + incomingField.split("\n").length + ) { + // Replace the incoming field with the one in storage so it will + // be further merged back to storage. + addressToMerge[field] = existingField; + } + } else if ( + field != "street-address" && + lazy.FormAutofillUtils.strCompare( + existingField, + incomingField, + collators + ) + ) { + addressToMerge[field] = existingField; + } else { + this.log.debug("Conflicts: field", field, "has different value."); + return false; + } + } + hasMatchingField = true; + } + } + + // We merge the address only when at least one field has the same value. + if (!hasMatchingField) { + this.log.debug("Unable to merge because no field has the same value"); + return false; + } + + // Early return if the data is the same or subset. + let noNeedToUpdate = this.VALID_FIELDS.every(field => { + // When addressFound doesn't contain a field, it's unnecessary to update + // if the same field in addressToMerge is omitted or an empty string. + if (addressFound[field] === undefined) { + return !addressToMerge[field]; + } + + // When addressFound contains a field, it's unnecessary to update if + // the same field in addressToMerge is omitted or a duplicate. + return ( + addressToMerge[field] === undefined || + addressFound[field] === addressToMerge[field] + ); + }); + if (noNeedToUpdate) { + return true; + } + + await this.update(guid, addressToMerge, true); + return true; + } +} + +class CreditCards extends CreditCardsBase { + constructor(store) { + super(store); + } + + async _encryptNumber(creditCard) { + if (!("cc-number-encrypted" in creditCard)) { + if ("cc-number" in creditCard) { + let ccNumber = creditCard["cc-number"]; + if (lazy.CreditCard.isValidNumber(ccNumber)) { + creditCard["cc-number"] = lazy.CreditCard.getLongMaskedNumber( + ccNumber + ); + } else { + // Credit card numbers can be entered on versions of Firefox that don't validate + // the number and then synced to this version of Firefox. Therefore, mask the + // full number if the number is invalid on this version. + creditCard["cc-number"] = "*".repeat(ccNumber.length); + } + creditCard["cc-number-encrypted"] = await lazy.OSKeyStore.encrypt( + ccNumber + ); + } else { + creditCard["cc-number-encrypted"] = ""; + } + } + } + + /** + * Merge new credit card into the specified record if cc-number is identical. + * (Note that credit card records always do non-strict merge.) + * + * @param {string} guid + * Indicates which credit card to merge. + * @param {object} creditCard + * The new credit card used to merge into the old one. + * @returns {boolean} + * Return true if credit card is merged into target with specific guid or false if not. + */ + async mergeIfPossible(guid, creditCard) { + this.log.debug(`mergeIfPossible: ${guid}`); + + // Credit card number is required since it also must match. + if (!creditCard["cc-number"]) { + return false; + } + + // Query raw data for comparing the decrypted credit card number + let creditCardFound = await this.get(guid, { rawData: true }); + if (!creditCardFound) { + throw new Error("No matching credit card."); + } + + let creditCardToMerge = this._clone(creditCard); + this._normalizeRecord(creditCardToMerge); + + for (let field of this.VALID_FIELDS) { + let existingField = creditCardFound[field]; + + // Make sure credit card field is existed and have value + if ( + field == "cc-number" && + (!existingField || !creditCardToMerge[field]) + ) { + return false; + } + + if (!creditCardToMerge[field] && typeof existingField != "undefined") { + creditCardToMerge[field] = existingField; + } + + let incomingField = creditCardToMerge[field]; + if (incomingField && existingField) { + if (incomingField != existingField) { + this.log.debug("Conflicts: field", field, "has different value."); + return false; + } + } + } + + // Early return if the data is the same. + let exactlyMatch = this.VALID_FIELDS.every( + field => creditCardFound[field] === creditCardToMerge[field] + ); + if (exactlyMatch) { + return true; + } + + await this.update(guid, creditCardToMerge, true); + return true; + } +} + +class FormAutofillStorage extends FormAutofillStorageBase { + constructor(path) { + super(path); + } + + getAddresses() { + if (!this._addresses) { + this._store.ensureDataReady(); + this._addresses = new Addresses(this._store); + } + return this._addresses; + } + + getCreditCards() { + if (!this._creditCards) { + this._store.ensureDataReady(); + this._creditCards = new CreditCards(this._store); + } + return this._creditCards; + } + + /** + * Loads the profile data from file to memory. + * + * @returns {JSONFile} + * The JSONFile store. + */ + _initializeStore() { + return new lazy.JSONFile({ + path: this._path, + dataPostProcessor: this._dataPostProcessor.bind(this), + }); + } + + _dataPostProcessor(data) { + data.version = this.version; + if (!data.addresses) { + data.addresses = []; + } + if (!data.creditCards) { + data.creditCards = []; + } + return data; + } +} + +// The singleton exposed by this module. +const formAutofillStorage = new FormAutofillStorage( + PathUtils.join(PathUtils.profileDir, PROFILE_JSON_FILE_NAME) +); |