2017-06-23 22:43:37 +03:00
|
|
|
/* 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/. */
|
|
|
|
|
|
|
|
"use strict";
|
|
|
|
|
|
|
|
var EXPORTED_SYMBOLS = ["AddressesEngine", "CreditCardsEngine"];
|
|
|
|
|
2019-01-17 21:18:31 +03:00
|
|
|
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
|
|
|
const { Changeset, Store, SyncEngine, Tracker } = ChromeUtils.import(
|
|
|
|
"resource://services-sync/engines.js"
|
|
|
|
);
|
|
|
|
const { CryptoWrapper } = ChromeUtils.import(
|
|
|
|
"resource://services-sync/record.js"
|
|
|
|
);
|
|
|
|
const { Utils } = ChromeUtils.import("resource://services-sync/util.js");
|
|
|
|
const { SCORE_INCREMENT_XLARGE } = ChromeUtils.import(
|
|
|
|
"resource://services-sync/constants.js"
|
|
|
|
);
|
2019-07-05 10:54:47 +03:00
|
|
|
|
2018-01-30 08:17:48 +03:00
|
|
|
ChromeUtils.defineModuleGetter(this, "Log", "resource://gre/modules/Log.jsm");
|
2018-02-11 00:23:19 +03:00
|
|
|
ChromeUtils.defineModuleGetter(
|
|
|
|
this,
|
|
|
|
"formAutofillStorage",
|
2018-01-31 01:44:59 +03:00
|
|
|
"resource://formautofill/FormAutofillStorage.jsm"
|
|
|
|
);
|
2017-06-23 22:43:37 +03:00
|
|
|
|
2017-07-12 02:34:06 +03:00
|
|
|
// A helper to sanitize address and creditcard records suitable for logging.
|
2017-06-23 22:43:37 +03:00
|
|
|
function sanitizeStorageObject(ob) {
|
2017-07-12 02:34:06 +03:00
|
|
|
if (!ob) {
|
|
|
|
return null;
|
|
|
|
}
|
2020-06-04 04:44:14 +03:00
|
|
|
const allowList = ["timeCreated", "timeLastUsed", "timeLastModified"];
|
2017-06-23 22:43:37 +03:00
|
|
|
let result = {};
|
|
|
|
for (let key of Object.keys(ob)) {
|
|
|
|
let origVal = ob[key];
|
2020-06-04 04:44:14 +03:00
|
|
|
if (allowList.includes(key)) {
|
2017-06-23 22:43:37 +03:00
|
|
|
result[key] = origVal;
|
|
|
|
} else if (typeof origVal == "string") {
|
|
|
|
result[key] = "X".repeat(origVal.length);
|
|
|
|
} else {
|
|
|
|
result[key] = typeof origVal; // *shrug*
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
function AutofillRecord(collection, id) {
|
|
|
|
CryptoWrapper.call(this, collection, id);
|
|
|
|
}
|
|
|
|
|
|
|
|
AutofillRecord.prototype = {
|
|
|
|
__proto__: CryptoWrapper.prototype,
|
|
|
|
|
2017-07-12 02:34:06 +03:00
|
|
|
toEntry() {
|
|
|
|
return Object.assign(
|
|
|
|
{
|
|
|
|
guid: this.id,
|
|
|
|
},
|
|
|
|
this.entry
|
|
|
|
);
|
|
|
|
},
|
|
|
|
|
|
|
|
fromEntry(entry) {
|
|
|
|
this.id = entry.guid;
|
|
|
|
this.entry = entry;
|
|
|
|
// The GUID is already stored in record.id, so we nuke it from the entry
|
2018-02-11 00:23:19 +03:00
|
|
|
// itself to save a tiny bit of space. The formAutofillStorage clones profiles,
|
2017-07-12 02:34:06 +03:00
|
|
|
// so nuking in-place is OK.
|
|
|
|
delete this.entry.guid;
|
|
|
|
},
|
|
|
|
|
2017-06-23 22:43:37 +03:00
|
|
|
cleartextToString() {
|
|
|
|
// And a helper so logging a *Sync* record auto sanitizes.
|
|
|
|
let record = this.cleartext;
|
2017-07-12 02:34:06 +03:00
|
|
|
return JSON.stringify({ entry: sanitizeStorageObject(record.entry) });
|
2017-06-23 22:43:37 +03:00
|
|
|
},
|
|
|
|
};
|
|
|
|
|
|
|
|
// Profile data is stored in the "entry" object of the record.
|
|
|
|
Utils.deferGetSet(AutofillRecord, "cleartext", ["entry"]);
|
|
|
|
|
|
|
|
function FormAutofillStore(name, engine) {
|
|
|
|
Store.call(this, name, engine);
|
|
|
|
}
|
|
|
|
|
|
|
|
FormAutofillStore.prototype = {
|
|
|
|
__proto__: Store.prototype,
|
|
|
|
|
|
|
|
_subStorageName: null, // overridden below.
|
|
|
|
_storage: null,
|
|
|
|
|
|
|
|
get storage() {
|
|
|
|
if (!this._storage) {
|
2018-02-11 00:23:19 +03:00
|
|
|
this._storage = formAutofillStorage[this._subStorageName];
|
2017-06-23 22:43:37 +03:00
|
|
|
}
|
|
|
|
return this._storage;
|
|
|
|
},
|
|
|
|
|
|
|
|
async getAllIDs() {
|
|
|
|
let result = {};
|
2018-09-05 20:54:48 +03:00
|
|
|
for (let { guid } of await this.storage.getAll({ includeDeleted: true })) {
|
2017-06-23 22:43:37 +03:00
|
|
|
result[guid] = true;
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
},
|
|
|
|
|
|
|
|
async changeItemID(oldID, newID) {
|
|
|
|
this.storage.changeGUID(oldID, newID);
|
|
|
|
},
|
|
|
|
|
|
|
|
// Note: this function intentionally returns false in cases where we only have
|
2018-02-11 00:23:19 +03:00
|
|
|
// a (local) tombstone - and formAutofillStorage.get() filters them for us.
|
2017-06-23 22:43:37 +03:00
|
|
|
async itemExists(id) {
|
2018-09-05 20:54:48 +03:00
|
|
|
return Boolean(await this.storage.get(id));
|
2017-06-23 22:43:37 +03:00
|
|
|
},
|
|
|
|
|
|
|
|
async applyIncoming(remoteRecord) {
|
|
|
|
if (remoteRecord.deleted) {
|
|
|
|
this._log.trace("Deleting record", remoteRecord);
|
|
|
|
this.storage.remove(remoteRecord.id, { sourceSync: true });
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (await this.itemExists(remoteRecord.id)) {
|
|
|
|
// We will never get a tombstone here, so we are updating a real record.
|
|
|
|
await this._doUpdateRecord(remoteRecord);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// No matching local record. Try to dedupe a NEW local record.
|
2018-09-05 20:54:48 +03:00
|
|
|
let localDupeID = await this.storage.findDuplicateGUID(
|
|
|
|
remoteRecord.toEntry()
|
|
|
|
);
|
2017-06-23 22:43:37 +03:00
|
|
|
if (localDupeID) {
|
|
|
|
this._log.trace(
|
|
|
|
`Deduping local record ${localDupeID} to remote`,
|
|
|
|
remoteRecord
|
|
|
|
);
|
|
|
|
// Change the local GUID to match the incoming record, then apply the
|
|
|
|
// incoming record.
|
|
|
|
await this.changeItemID(localDupeID, remoteRecord.id);
|
|
|
|
await this._doUpdateRecord(remoteRecord);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// We didn't find a dupe, either, so must be a new record (or possibly
|
2017-07-12 02:34:06 +03:00
|
|
|
// a non-deleted version of an item we have a tombstone for, which add()
|
2017-06-23 22:43:37 +03:00
|
|
|
// handles for us.)
|
|
|
|
this._log.trace("Add record", remoteRecord);
|
2017-07-12 02:34:06 +03:00
|
|
|
let entry = remoteRecord.toEntry();
|
2018-09-05 20:54:48 +03:00
|
|
|
await this.storage.add(entry, { sourceSync: true });
|
2017-06-23 22:43:37 +03:00
|
|
|
},
|
|
|
|
|
|
|
|
async createRecord(id, collection) {
|
|
|
|
this._log.trace("Create record", id);
|
|
|
|
let record = new AutofillRecord(collection, id);
|
2018-09-05 20:54:48 +03:00
|
|
|
let entry = await this.storage.get(id, {
|
2017-06-30 00:35:04 +03:00
|
|
|
rawData: true,
|
2017-06-23 22:43:37 +03:00
|
|
|
});
|
2017-07-12 02:34:06 +03:00
|
|
|
if (entry) {
|
|
|
|
record.fromEntry(entry);
|
|
|
|
} else {
|
2017-06-23 22:43:37 +03:00
|
|
|
// We should consider getting a more authortative indication it's actually deleted.
|
|
|
|
this._log.debug(
|
|
|
|
`Failed to get autofill record with id "${id}", assuming deleted`
|
|
|
|
);
|
|
|
|
record.deleted = true;
|
|
|
|
}
|
|
|
|
return record;
|
|
|
|
},
|
|
|
|
|
|
|
|
async _doUpdateRecord(record) {
|
|
|
|
this._log.trace("Updating record", record);
|
|
|
|
|
2017-06-30 00:35:04 +03:00
|
|
|
let entry = record.toEntry();
|
2018-09-05 20:54:48 +03:00
|
|
|
let { forkedGUID } = await this.storage.reconcile(entry);
|
2017-06-30 00:35:04 +03:00
|
|
|
if (this._log.level <= Log.Level.Debug) {
|
2018-09-05 20:54:48 +03:00
|
|
|
let forkedRecord = forkedGUID ? await this.storage.get(forkedGUID) : null;
|
|
|
|
let reconciledRecord = await this.storage.get(record.id);
|
2017-06-30 00:35:04 +03:00
|
|
|
this._log.debug("Updated local record", {
|
|
|
|
forked: sanitizeStorageObject(forkedRecord),
|
|
|
|
updated: sanitizeStorageObject(reconciledRecord),
|
|
|
|
});
|
|
|
|
}
|
2017-06-23 22:43:37 +03:00
|
|
|
},
|
|
|
|
|
|
|
|
// NOTE: Because we re-implement the incoming/reconcilliation logic we leave
|
|
|
|
// the |create|, |remove| and |update| methods undefined - the base
|
|
|
|
// implementation throws, which is what we want to happen so we can identify
|
|
|
|
// any places they are "accidentally" called.
|
|
|
|
};
|
|
|
|
|
|
|
|
function FormAutofillTracker(name, engine) {
|
|
|
|
Tracker.call(this, name, engine);
|
|
|
|
}
|
|
|
|
|
|
|
|
FormAutofillTracker.prototype = {
|
|
|
|
__proto__: Tracker.prototype,
|
2018-01-05 02:07:10 +03:00
|
|
|
async observe(subject, topic, data) {
|
2017-06-23 22:43:37 +03:00
|
|
|
if (topic != "formautofill-storage-changed") {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (
|
|
|
|
subject &&
|
|
|
|
subject.wrappedJSObject &&
|
|
|
|
subject.wrappedJSObject.sourceSync
|
|
|
|
) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
switch (data) {
|
|
|
|
case "add":
|
|
|
|
case "update":
|
|
|
|
case "remove":
|
|
|
|
this.score += SCORE_INCREMENT_XLARGE;
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
this._log.debug("unrecognized autofill notification", data);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2018-01-05 02:07:10 +03:00
|
|
|
onStart() {
|
2017-06-23 22:43:37 +03:00
|
|
|
Services.obs.addObserver(this, "formautofill-storage-changed");
|
|
|
|
},
|
|
|
|
|
2018-01-05 02:07:10 +03:00
|
|
|
onStop() {
|
2017-06-23 22:43:37 +03:00
|
|
|
Services.obs.removeObserver(this, "formautofill-storage-changed");
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
|
|
|
// This uses the same conventions as BookmarkChangeset in
|
|
|
|
// services/sync/modules/engines/bookmarks.js. Specifically,
|
|
|
|
// - "synced" means the item has already been synced (or we have another reason
|
|
|
|
// to ignore it), and should be ignored in most methods.
|
|
|
|
class AutofillChangeset extends Changeset {
|
|
|
|
constructor() {
|
|
|
|
super();
|
|
|
|
}
|
|
|
|
|
|
|
|
getModifiedTimestamp(id) {
|
|
|
|
throw new Error("Don't use timestamps to resolve autofill merge conflicts");
|
|
|
|
}
|
|
|
|
|
|
|
|
has(id) {
|
|
|
|
let change = this.changes[id];
|
|
|
|
if (change) {
|
|
|
|
return !change.synced;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
delete(id) {
|
|
|
|
let change = this.changes[id];
|
|
|
|
if (change) {
|
|
|
|
// Mark the change as synced without removing it from the set. We do this
|
2018-01-31 01:44:59 +03:00
|
|
|
// so that we can update FormAutofillStorage in `trackRemainingChanges`.
|
2017-06-23 22:43:37 +03:00
|
|
|
change.synced = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function FormAutofillEngine(service, name) {
|
|
|
|
SyncEngine.call(this, name, service);
|
|
|
|
}
|
|
|
|
|
|
|
|
FormAutofillEngine.prototype = {
|
|
|
|
__proto__: SyncEngine.prototype,
|
|
|
|
|
|
|
|
// the priority for this engine is == addons, so will happen after bookmarks
|
|
|
|
// prefs and tabs, but before forms, history, etc.
|
|
|
|
syncPriority: 5,
|
|
|
|
|
|
|
|
// We don't use SyncEngine.initialize() for this, as we initialize even if
|
|
|
|
// the engine is disabled, and we don't want to be the loader of
|
2018-01-31 01:44:59 +03:00
|
|
|
// FormAutofillStorage in this case.
|
2017-06-23 22:43:37 +03:00
|
|
|
async _syncStartup() {
|
2018-02-11 00:23:19 +03:00
|
|
|
await formAutofillStorage.initialize();
|
2017-06-23 22:43:37 +03:00
|
|
|
await SyncEngine.prototype._syncStartup.call(this);
|
|
|
|
},
|
|
|
|
|
|
|
|
// We handle reconciliation in the store, not the engine.
|
|
|
|
async _reconcile() {
|
|
|
|
return true;
|
|
|
|
},
|
|
|
|
|
|
|
|
emptyChangeset() {
|
|
|
|
return new AutofillChangeset();
|
|
|
|
},
|
|
|
|
|
|
|
|
async _uploadOutgoing() {
|
|
|
|
this._modified.replace(this._store.storage.pullSyncChanges());
|
|
|
|
await SyncEngine.prototype._uploadOutgoing.call(this);
|
|
|
|
},
|
|
|
|
|
|
|
|
// Typically, engines populate the changeset before downloading records.
|
|
|
|
// However, we handle conflict resolution in the store, so we can wait
|
|
|
|
// to pull changes until we're ready to upload.
|
|
|
|
async pullAllChanges() {
|
|
|
|
return {};
|
|
|
|
},
|
|
|
|
|
|
|
|
async pullNewChanges() {
|
|
|
|
return {};
|
|
|
|
},
|
|
|
|
|
|
|
|
async trackRemainingChanges() {
|
|
|
|
this._store.storage.pushSyncChanges(this._modified.changes);
|
|
|
|
},
|
|
|
|
|
|
|
|
_deleteId(id) {
|
|
|
|
this._noteDeletedId(id);
|
|
|
|
},
|
|
|
|
|
|
|
|
async _resetClient() {
|
2018-02-11 00:23:19 +03:00
|
|
|
await formAutofillStorage.initialize();
|
2017-06-23 22:43:37 +03:00
|
|
|
this._store.storage.resetSync();
|
|
|
|
},
|
2018-05-24 05:36:37 +03:00
|
|
|
|
|
|
|
async _wipeClient() {
|
|
|
|
await formAutofillStorage.initialize();
|
|
|
|
this._store.storage.removeAll({ sourceSync: true });
|
|
|
|
},
|
2017-06-23 22:43:37 +03:00
|
|
|
};
|
|
|
|
|
|
|
|
// The concrete engines
|
|
|
|
|
|
|
|
function AddressesRecord(collection, id) {
|
|
|
|
AutofillRecord.call(this, collection, id);
|
|
|
|
}
|
|
|
|
|
|
|
|
AddressesRecord.prototype = {
|
|
|
|
__proto__: AutofillRecord.prototype,
|
|
|
|
_logName: "Sync.Record.Addresses",
|
|
|
|
};
|
|
|
|
|
|
|
|
function AddressesStore(name, engine) {
|
|
|
|
FormAutofillStore.call(this, name, engine);
|
|
|
|
}
|
|
|
|
|
|
|
|
AddressesStore.prototype = {
|
|
|
|
__proto__: FormAutofillStore.prototype,
|
|
|
|
_subStorageName: "addresses",
|
|
|
|
};
|
|
|
|
|
|
|
|
function AddressesEngine(service) {
|
|
|
|
FormAutofillEngine.call(this, service, "Addresses");
|
|
|
|
}
|
|
|
|
|
|
|
|
AddressesEngine.prototype = {
|
|
|
|
__proto__: FormAutofillEngine.prototype,
|
|
|
|
_trackerObj: FormAutofillTracker,
|
|
|
|
_storeObj: AddressesStore,
|
|
|
|
_recordObj: AddressesRecord,
|
|
|
|
|
|
|
|
get prefName() {
|
|
|
|
return "addresses";
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
|
|
|
function CreditCardsRecord(collection, id) {
|
|
|
|
AutofillRecord.call(this, collection, id);
|
|
|
|
}
|
|
|
|
|
|
|
|
CreditCardsRecord.prototype = {
|
|
|
|
__proto__: AutofillRecord.prototype,
|
|
|
|
_logName: "Sync.Record.CreditCards",
|
|
|
|
};
|
|
|
|
|
|
|
|
function CreditCardsStore(name, engine) {
|
|
|
|
FormAutofillStore.call(this, name, engine);
|
|
|
|
}
|
|
|
|
|
|
|
|
CreditCardsStore.prototype = {
|
|
|
|
__proto__: FormAutofillStore.prototype,
|
|
|
|
_subStorageName: "creditCards",
|
|
|
|
};
|
|
|
|
|
|
|
|
function CreditCardsEngine(service) {
|
|
|
|
FormAutofillEngine.call(this, service, "CreditCards");
|
|
|
|
}
|
|
|
|
|
|
|
|
CreditCardsEngine.prototype = {
|
|
|
|
__proto__: FormAutofillEngine.prototype,
|
|
|
|
_trackerObj: FormAutofillTracker,
|
|
|
|
_storeObj: CreditCardsStore,
|
|
|
|
_recordObj: CreditCardsRecord,
|
|
|
|
get prefName() {
|
|
|
|
return "creditcards";
|
|
|
|
},
|
|
|
|
};
|