зеркало из https://github.com/mozilla/gecko-dev.git
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:
Родитель
f2cc9ae2ca
Коммит
ea394ebcbc
|
@ -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
|
||||
|
|
Загрузка…
Ссылка в новой задаче