Bug 1691821 - [2.7] Extend the form autofill component to support GeckoView. r=erik,zbraniecki,dimi,geckoview-reviewers,owlish

Differential Revision: https://phabricator.services.mozilla.com/D108059
This commit is contained in:
Eugen Sawin 2021-04-12 17:33:18 +00:00
Родитель f2cc9ae2ca
Коммит ea394ebcbc
9 изменённых файлов: 667 добавлений и 224 удалений

Просмотреть файл

@ -56,7 +56,7 @@ add_task(async function test_profileSavedFieldNames_update() {
FormAutofillStatus.formAutofillStorage.addresses._data = [];
// The set is empty if there's no profile in the store.
FormAutofillStatus.updateSavedFieldNames();
await FormAutofillStatus.updateSavedFieldNames();
Assert.equal(
Services.ppmm.sharedData.get("FormAutofill:savedFieldNames").size,
0
@ -88,7 +88,7 @@ add_task(async function test_profileSavedFieldNames_update() {
},
];
FormAutofillStatus.updateSavedFieldNames();
await FormAutofillStatus.updateSavedFieldNames();
let autofillSavedFieldNames = Services.ppmm.sharedData.get(
"FormAutofill:savedFieldNames"

Просмотреть файл

@ -147,6 +147,60 @@ const GeckoViewAutocomplete = {
});
},
/**
* Delegates credit card entry fetching to the attached LoginStorage
* GeckoView delegate.
*
* @return {Promise}
* Resolves with an array of credit card objects or null.
* Rejected if no delegate is attached.
* Login object string properties:
* { guid, name, number, expMonth, expYear, type }
*/
fetchCreditCards() {
debug`fetchCreditCards`;
return Promise.resolve(null);
},
/**
* Delegates address entry fetching to the attached LoginStorage
* GeckoView delegate.
*
* @return {Promise}
* Resolves with an array of address objects or null.
* Rejected if no delegate is attached.
* Login object string properties:
* { guid, name, givenName, additionalName, familyName,
* organization, streetAddress, addressLevel1, addressLevel2,
* addressLevel3, postalCode, country, tel, email }
*/
fetchAddresses() {
debug`fetchAddresses`;
return Promise.resolve(null);
},
/**
* Delegates credit card entry saving to the attached LoginStorage GeckoView delegate.
* Call this when a new or modified credit card entry has been submitted.
*
* @param aCreditCard The {CreditCard} to be saved.
*/
onCreditCardSave(aCreditCard) {
debug`onLoginSave ${aCreditCard}`;
},
/**
* Delegates address entry saving to the attached LoginStorage GeckoView delegate.
* Call this when a new or modified address entry has been submitted.
*
* @param aAddress The {Address} to be saved.
*/
onAddressSave(aAddress) {
debug`onLoginSave ${aAddress}`;
},
/**
* Delegates login entry saving to the attached LoginStorage GeckoView delegate.
* Call this when a new login entry or a new password for an existing login

Просмотреть файл

@ -193,6 +193,10 @@ class FormAutofillChild extends JSWindowActorChild {
FormAutofillContent.clearForm();
break;
}
case "FormAutofill:FillForm": {
FormAutofillContent.activeHandler.autofillFormFields(message.data);
break;
}
}
}
}

Просмотреть файл

@ -64,6 +64,12 @@ ChromeUtils.defineModuleGetter(
"PrivateBrowsingUtils",
"resource://gre/modules/PrivateBrowsingUtils.jsm"
);
XPCOMUtils.defineLazyPreferenceGetter(
this,
"DELEGATE_AUTOCOMPLETE",
"toolkit.autocomplete.delegate",
false
);
const formFillController = Cc[
"@mozilla.org/satchel/form-fill-controller;1"
@ -690,6 +696,8 @@ var FormAutofillContent = {
"updateActiveElement: checking if empty field is cc-*: ",
this.activeFieldDetail?.fieldName
);
// This restricts popups to credit card fields and may need adjustment
// when enabling address support for the GeckoView backend.
if (this.activeFieldDetail?.fieldName?.startsWith("cc-")) {
if (Services.cpmm.sharedData.get("FormAutofill:enabled")) {
this.debug("updateActiveElement: opening pop up");
@ -767,7 +775,7 @@ var FormAutofillContent = {
String(element.ownerDocument.location)
);
if (!this.savedFieldNames) {
if (DELEGATE_AUTOCOMPLETE || !this.savedFieldNames) {
this.debug("identifyAutofillFields: savedFieldNames are not known yet");
let actor = getActorFromWindow(element.ownerGlobal);
if (actor) {

Просмотреть файл

@ -35,7 +35,6 @@ const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm"
);
const { FormAutofill } = ChromeUtils.import(
"resource://autofill/FormAutofill.jsm"
);
@ -170,18 +169,18 @@ let FormAutofillStatus = {
}
},
updateSavedFieldNames() {
async updateSavedFieldNames() {
log.debug("updateSavedFieldNames");
let savedFieldNames;
const addressNames = await gFormAutofillStorage.addresses.getSavedFieldNames();
// Don't access the credit cards store unless it is enabled.
if (FormAutofill.isAutofillCreditCardsAvailable) {
savedFieldNames = new Set([
...gFormAutofillStorage.addresses.getSavedFieldNames(),
...gFormAutofillStorage.creditCards.getSavedFieldNames(),
]);
const creditCardNames = await gFormAutofillStorage.creditCards.getSavedFieldNames();
savedFieldNames = new Set([...addressNames, ...creditCardNames]);
} else {
savedFieldNames = gFormAutofillStorage.addresses.getSavedFieldNames();
savedFieldNames = addressNames;
}
Services.ppmm.sharedData.set(
@ -297,6 +296,7 @@ class FormAutofillParent extends JSWindowActorParent {
switch (name) {
case "FormAutofill:InitStorage": {
await gFormAutofillStorage.initialize();
await FormAutofillStatus.updateSavedFieldNames();
break;
}
case "FormAutofill:GetRecords": {

Просмотреть файл

@ -3,7 +3,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/*
* Implements an interface of the storage of Form Autofill.
* Interface for the storage of Form Autofill.
*
* The data is stored in JSON format, without indentation and the computed
* fields, using UTF-8 encoding. With indentation and computed fields applied,
@ -124,15 +124,16 @@
"use strict";
// We expose a singleton from this module. Some tests may import the
// constructor via a backstage pass.
this.EXPORTED_SYMBOLS = ["formAutofillStorage"];
this.EXPORTED_SYMBOLS = [
"FormAutofillStorageBase",
"CreditCardsBase",
"AddressesBase",
];
const { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm"
);
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm");
const { FormAutofill } = ChromeUtils.import(
"resource://autofill/FormAutofill.jsm"
@ -143,11 +144,6 @@ ChromeUtils.defineModuleGetter(
"CreditCard",
"resource://gre/modules/CreditCard.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"JSONFile",
"resource://gre/modules/JSONFile.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"FormAutofillNameUtils",
@ -182,8 +178,6 @@ const CryptoHash = Components.Constructor(
"initWithString"
);
const PROFILE_JSON_FILE_NAME = "autofill-profiles.json";
const STORAGE_SCHEMA_VERSION = 1;
const ADDRESS_SCHEMA_VERSION = 1;
const CREDIT_CARD_SCHEMA_VERSION = 3;
@ -298,6 +292,10 @@ class AutofillRecords {
this._collectionName = collectionName;
this._schemaVersion = schemaVersion;
this._initialize();
}
_initialize() {
this._initializePromise = Promise.all(
this._data.map(async (record, index) =>
this._migrateRecord(record, index)
@ -327,6 +325,10 @@ class AutofillRecords {
* The data object.
*/
get _data() {
return this._getData();
}
_getData() {
return this._store.data[this._collectionName];
}
@ -687,13 +689,11 @@ class AutofillRecords {
}
/**
* Return all saved field names in the collection. This method
* has to be sync because its caller _updateSavedFieldNames() needs
* to dispatch content message synchronously.
* Return all saved field names in the collection.
*
* @returns {Set} Set containing saved field names.
* @returns {Promise<Set>} Set containing saved field names.
*/
getSavedFieldNames() {
async getSavedFieldNames() {
this.log.debug("getSavedFieldNames");
let records = this._data.filter(r => !r.deleted);
@ -1441,7 +1441,7 @@ class AutofillRecords {
async mergeIfPossible(guid, record, strict) {}
}
class Addresses extends AutofillRecords {
class AddressesBase extends AutofillRecords {
constructor(store) {
super(
store,
@ -1669,96 +1669,11 @@ class Addresses extends AutofillRecords {
* 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, address);
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 = 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" &&
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" &&
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;
throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
}
}
class CreditCards extends AutofillRecords {
class CreditCardsBase extends AutofillRecords {
constructor(store) {
super(
store,
@ -1826,26 +1741,15 @@ class CreditCards extends AutofillRecords {
}
// Encrypt credit card number
if (!("cc-number-encrypted" in creditCard)) {
if ("cc-number" in creditCard) {
let ccNumber = creditCard["cc-number"];
if (CreditCard.isValidNumber(ccNumber)) {
creditCard["cc-number"] = 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 OSKeyStore.encrypt(ccNumber);
} else {
creditCard["cc-number-encrypted"] = "";
}
}
await this._encryptNumber(creditCard);
return hasNewComputedFields;
}
async _encryptNumber(creditCard) {
throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
}
async _computeMigratedRecord(creditCard) {
if (creditCard["cc-number-encrypted"]) {
switch (creditCard.version) {
@ -2050,56 +1954,7 @@ class CreditCards extends AutofillRecords {
* 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, creditCard);
// 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;
throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
}
updateUseCountTelemetry() {
@ -2114,35 +1969,35 @@ class CreditCards extends AutofillRecords {
}
}
function FormAutofillStorage(path) {
this._path = path;
this._initializePromise = null;
this.INTERNAL_FIELDS = INTERNAL_FIELDS;
}
class FormAutofillStorageBase {
constructor(path) {
this._path = path;
this._initializePromise = null;
this.INTERNAL_FIELDS = INTERNAL_FIELDS;
}
FormAutofillStorage.prototype = {
get version() {
return STORAGE_SCHEMA_VERSION;
},
}
get addresses() {
if (!this._addresses) {
this._store.ensureDataReady();
this._addresses = new Addresses(this._store);
}
return this._addresses;
},
return this.getAddresses();
}
get creditCards() {
if (!this._creditCards) {
this._store.ensureDataReady();
this._creditCards = new CreditCards(this._store);
}
return this._creditCards;
},
return this.getCreditCards();
}
getAddresses() {
throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
}
getCreditCards() {
throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
}
/**
* Loads the profile data from file to memory.
* Initialize storage to memory.
*
* @returns {Promise}
* @resolves When the operation finished successfully.
@ -2150,10 +2005,7 @@ FormAutofillStorage.prototype = {
*/
initialize() {
if (!this._initializePromise) {
this._store = new JSONFile({
path: this._path,
dataPostProcessor: this._dataPostProcessor.bind(this),
});
this._store = this._initializeStore();
this._initializePromise = this._store.load().then(() => {
let initializeAutofillRecords = [this.addresses.initialize()];
if (FormAutofill.isAutofillCreditCardsAvailable) {
@ -2174,30 +2026,18 @@ FormAutofillStorage.prototype = {
});
}
return this._initializePromise;
},
}
_dataPostProcessor(data) {
data.version = this.version;
if (!data.addresses) {
data.addresses = [];
}
if (!data.creditCards) {
data.creditCards = [];
}
return data;
},
_initializeStore() {
throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
}
// For test only.
_saveImmediately() {
return this._store._save();
},
}
_finalize() {
return this._store.finalize();
},
};
// The singleton exposed by this module.
this.formAutofillStorage = new FormAutofillStorage(
OS.Path.join(OS.Constants.Path.profileDir, PROFILE_JSON_FILE_NAME)
);
}
}

Просмотреть файл

@ -0,0 +1,238 @@
/* 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 for GeckoView.
*/
"use strict";
// We expose a singleton from this module. Some tests may import the
// constructor via a backstage pass.
this.EXPORTED_SYMBOLS = ["formAutofillStorage", "FormAutofillStorage"];
const { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm"
);
const {
FormAutofillStorageBase,
CreditCardsBase,
AddressesBase,
} = ChromeUtils.import("resource://autofill/FormAutofillStorageBase.jsm");
XPCOMUtils.defineLazyModuleGetters(this, {
GeckoViewAutocomplete: "resource://gre/modules/GeckoViewAutocomplete.jsm",
CreditCard: "resource://gre/modules/GeckoViewAutocomplete.jsm",
Address: "resource://gre/modules/GeckoViewAutocomplete.jsm",
JSONFile: "resource://gre/modules/JSONFile.jsm",
});
class GeckoViewStorage extends JSONFile {
constructor() {
super({ path: null });
}
async updateCreditCards() {
const creditCards = await GeckoViewAutocomplete.fetchCreditCards().then(
results => results?.map(r => CreditCard.parse(r).toGecko()) ?? [],
_ => []
);
super.data.creditCards = creditCards;
}
async updateAddresses() {
const addresses = await GeckoViewAutocomplete.fetchAddresses().then(
results => results?.map(r => Address.parse(r).toGecko()) ?? [],
_ => []
);
super.data.addresses = addresses;
}
async load() {
super.data = { creditCards: {}, addresses: {} };
await this.updateCreditCards();
await this.updateAddresses();
}
ensureDataReady() {
if (this.dataReady) {
return;
}
throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
}
async _save() {
// TODO: Implement saving support in bug 1703977.
}
}
class Addresses extends AddressesBase {
// Override AutofillRecords methods.
_initialize() {
this._initializePromise = Promise.resolve();
}
async _saveRecord(record, { sourceSync = false } = {}) {
GeckoViewAutocomplete.onAddressSave(Address.fromGecko(record));
}
/**
* Returns the record with the specified GUID.
*
* @param {string} guid
* Indicates which record to retrieve.
* @param {boolean} [options.rawData = false]
* Returns a raw record without modifications and the computed fields
* (this includes private fields)
* @returns {Promise<Object>}
* A clone of the record.
*/
async get(guid, { rawData = false } = {}) {
await this._store.updateAddresses();
return super.get(guid, { rawData });
}
/**
* Returns all records.
*
* @param {boolean} [options.rawData = false]
* Returns raw records without modifications and the computed fields.
* @param {boolean} [options.includeDeleted = false]
* Also return any tombstone records.
* @returns {Promise<Array.<Object>>}
* An array containing clones of all records.
*/
async getAll({ rawData = false, includeDeleted = false } = {}) {
await this._store.updateAddresses();
return super.getAll({ rawData, includeDeleted });
}
/**
* Return all saved field names in the collection.
*
* @returns {Set} Set containing saved field names.
*/
async getSavedFieldNames() {
await this._store.updateAddresses();
return super.getSavedFieldNames();
}
async reconcile(remoteRecord) {
throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
}
async findDuplicateGUID(remoteRecord) {
throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
}
async mergeToStorage(targetRecord, strict = false) {
throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
}
}
class CreditCards extends CreditCardsBase {
async _encryptNumber(creditCard) {
// Don't encrypt or obfuscate for GV, since we don't store or show
// the number. The API has to always provide the original number.
}
// Override AutofillRecords methods.
_initialize() {
this._initializePromise = Promise.resolve();
}
async _saveRecord(record, { sourceSync = false } = {}) {
GeckoViewAutocomplete.onCreditCardSave(CreditCard.fromGecko(record));
}
/**
* Returns the record with the specified GUID.
*
* @param {string} guid
* Indicates which record to retrieve.
* @param {boolean} [options.rawData = false]
* Returns a raw record without modifications and the computed fields
* (this includes private fields)
* @returns {Promise<Object>}
* A clone of the record.
*/
async get(guid, { rawData = false } = {}) {
await this._store.updateCreditCards();
return super.get(guid, { rawData });
}
/**
* Returns all records.
*
* @param {boolean} [options.rawData = false]
* Returns raw records without modifications and the computed fields.
* @param {boolean} [options.includeDeleted = false]
* Also return any tombstone records.
* @returns {Promise<Array.<Object>>}
* An array containing clones of all records.
*/
async getAll({ rawData = false, includeDeleted = false } = {}) {
await this._store.updateCreditCards();
return super.getAll({ rawData, includeDeleted });
}
/**
* Return all saved field names in the collection.
*
* @returns {Set} Set containing saved field names.
*/
async getSavedFieldNames() {
await this._store.updateCreditCards();
return super.getSavedFieldNames();
}
async reconcile(remoteRecord) {
throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
}
async findDuplicateGUID(remoteRecord) {
throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
}
async mergeToStorage(targetRecord, strict = false) {
throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
}
}
class FormAutofillStorage extends FormAutofillStorageBase {
constructor() {
super(null);
}
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;
}
/**
* Initializes the in-memory async store API.
* @returns {JSONFile}
* The JSONFile store.
*/
_initializeStore() {
return new GeckoViewStorage();
}
}
// The singleton exposed by this module.
this.formAutofillStorage = new FormAutofillStorage();

Просмотреть файл

@ -0,0 +1,294 @@
/* 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.
this.EXPORTED_SYMBOLS = ["formAutofillStorage", "FormAutofillStorage"];
const { FormAutofill } = ChromeUtils.import(
"resource://autofill/FormAutofill.jsm"
);
const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm");
const {
FormAutofillStorageBase,
CreditCardsBase,
AddressesBase,
} = ChromeUtils.import("resource://autofill/FormAutofillStorageBase.jsm");
ChromeUtils.defineModuleGetter(
this,
"JSONFile",
"resource://gre/modules/JSONFile.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"OSKeyStore",
"resource://gre/modules/OSKeyStore.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"CreditCard",
"resource://gre/modules/CreditCard.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"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, address);
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 = 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" &&
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" &&
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 (CreditCard.isValidNumber(ccNumber)) {
creditCard["cc-number"] = 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 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, creditCard);
// 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 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.
this.formAutofillStorage = new FormAutofillStorage(
OS.Path.join(OS.Constants.Path.profileDir, PROFILE_JSON_FILE_NAME)
);

Просмотреть файл

@ -8,3 +8,8 @@ toolkit.jar:
res/autofill/phonenumberutils/ (./phonenumberutils/*.jsm)
res/autofill/addressmetadata/ (./addressmetadata/*)
res/autofill/content/ (./content/*)
#ifdef ANDROID
res/autofill/FormAutofillStorage.jsm (./android/FormAutofillStorage.jsm)
#else
res/autofill/FormAutofillStorage.jsm (./default/FormAutofillStorage.jsm)
#endif