summaryrefslogtreecommitdiffstats
path: root/toolkit/components/formautofill/default/FormAutofillStorage.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/formautofill/default/FormAutofillStorage.sys.mjs')
-rw-r--r--toolkit/components/formautofill/default/FormAutofillStorage.sys.mjs274
1 files changed, 274 insertions, 0 deletions
diff --git a/toolkit/components/formautofill/default/FormAutofillStorage.sys.mjs b/toolkit/components/formautofill/default/FormAutofillStorage.sys.mjs
new file mode 100644
index 0000000000..c45186453d
--- /dev/null
+++ b/toolkit/components/formautofill/default/FormAutofillStorage.sys.mjs
@@ -0,0 +1,274 @@
+/* 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.
+ */
+
+// We expose a singleton from this module. Some tests may import the
+// constructor via a backstage pass.
+import {
+ AddressesBase,
+ CreditCardsBase,
+ FormAutofillStorageBase,
+} from "resource://autofill/FormAutofillStorageBase.sys.mjs";
+import { FormAutofill } from "resource://autofill/FormAutofill.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ CreditCard: "resource://gre/modules/CreditCard.sys.mjs",
+ FormAutofillUtils: "resource://gre/modules/shared/FormAutofillUtils.sys.mjs",
+ JSONFile: "resource://gre/modules/JSONFile.sys.mjs",
+ OSKeyStore: "resource://gre/modules/OSKeyStore.sys.mjs",
+});
+
+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;
+ }
+}
+
+export 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.
+export const formAutofillStorage = new FormAutofillStorage(
+ PathUtils.join(PathUtils.profileDir, PROFILE_JSON_FILE_NAME)
+);