diff options
Diffstat (limited to 'toolkit/components/formautofill/default/FormAutofillStorage.sys.mjs')
-rw-r--r-- | toolkit/components/formautofill/default/FormAutofillStorage.sys.mjs | 274 |
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) +); |