1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
10 } from "resource://services-sync/engines.sys.mjs";
11 import { CryptoWrapper } from "resource://services-sync/record.sys.mjs";
12 import { Utils } from "resource://services-sync/util.sys.mjs";
14 import { SCORE_INCREMENT_XLARGE } from "resource://services-sync/constants.sys.mjs";
18 ChromeUtils.defineESModuleGetters(lazy, {
19 Log: "resource://gre/modules/Log.sys.mjs",
20 formAutofillStorage: "resource://autofill/FormAutofillStorage.sys.mjs",
23 // A helper to sanitize address and creditcard records suitable for logging.
24 export function sanitizeStorageObject(ob) {
28 const allowList = ["timeCreated", "timeLastUsed", "timeLastModified"];
30 for (let key of Object.keys(ob)) {
31 let origVal = ob[key];
32 if (allowList.includes(key)) {
33 result[key] = origVal;
34 } else if (typeof origVal == "string") {
35 result[key] = "X".repeat(origVal.length);
37 result[key] = typeof origVal; // *shrug*
43 export function AutofillRecord(collection, id) {
44 CryptoWrapper.call(this, collection, id);
47 AutofillRecord.prototype = {
60 // The GUID is already stored in record.id, so we nuke it from the entry
61 // itself to save a tiny bit of space. The formAutofillStorage clones profiles,
62 // so nuking in-place is OK.
63 delete this.entry.guid;
67 // And a helper so logging a *Sync* record auto sanitizes.
68 let record = this.cleartext;
69 return JSON.stringify({ entry: sanitizeStorageObject(record.entry) });
72 Object.setPrototypeOf(AutofillRecord.prototype, CryptoWrapper.prototype);
74 // Profile data is stored in the "entry" object of the record.
75 Utils.deferGetSet(AutofillRecord, "cleartext", ["entry"]);
77 function FormAutofillStore(name, engine) {
78 Store.call(this, name, engine);
81 FormAutofillStore.prototype = {
82 _subStorageName: null, // overridden below.
87 this._storage = lazy.formAutofillStorage[this._subStorageName];
94 for (let { guid } of await this.storage.getAll({ includeDeleted: true })) {
100 async changeItemID(oldID, newID) {
101 this.storage.changeGUID(oldID, newID);
104 // Note: this function intentionally returns false in cases where we only have
105 // a (local) tombstone - and formAutofillStorage.get() filters them for us.
106 async itemExists(id) {
107 return Boolean(await this.storage.get(id));
110 async applyIncoming(remoteRecord) {
111 if (remoteRecord.deleted) {
112 this._log.trace("Deleting record", remoteRecord);
113 this.storage.remove(remoteRecord.id, { sourceSync: true });
117 // Records from the remote might come from an older device. To ensure that
118 // remote records from older devices can still sync with the local records,
119 // we migrate the remote records. This enables the merging of older records
120 // with newer records.
122 // Currently, this migration is only used for converting `*-name` fields to `name` fields.
123 // The migration process involves:
124 // 1. Generating a `name` field so we don't assume the `name` field is empty, thereby
125 // avoiding erasing its value.
126 // 2. Removing deprecated *-name fields from the remote record because the autofill storage
127 // does not expect to see those fields.
128 this.storage.migrateRemoteRecord(remoteRecord.entry);
130 if (await this.itemExists(remoteRecord.id)) {
131 // We will never get a tombstone here, so we are updating a real record.
132 await this._doUpdateRecord(remoteRecord);
136 // No matching local record. Try to dedupe a NEW local record.
137 let localDupeID = await this.storage.findDuplicateGUID(
138 remoteRecord.toEntry()
142 `Deduping local record ${localDupeID} to remote`,
145 // Change the local GUID to match the incoming record, then apply the
147 await this.changeItemID(localDupeID, remoteRecord.id);
148 await this._doUpdateRecord(remoteRecord);
152 // We didn't find a dupe, either, so must be a new record (or possibly
153 // a non-deleted version of an item we have a tombstone for, which add()
155 this._log.trace("Add record", remoteRecord);
156 let entry = remoteRecord.toEntry();
157 await this.storage.add(entry, { sourceSync: true });
160 async createRecord(id, collection) {
161 this._log.trace("Create record", id);
162 let record = new AutofillRecord(collection, id);
163 let entry = await this.storage.get(id, {
167 record.fromEntry(entry);
169 // We should consider getting a more authortative indication it's actually deleted.
171 `Failed to get autofill record with id "${id}", assuming deleted`
173 record.deleted = true;
178 async _doUpdateRecord(record) {
179 this._log.trace("Updating record", record);
181 let entry = record.toEntry();
182 let { forkedGUID } = await this.storage.reconcile(entry);
183 if (this._log.level <= lazy.Log.Level.Debug) {
184 let forkedRecord = forkedGUID ? await this.storage.get(forkedGUID) : null;
185 let reconciledRecord = await this.storage.get(record.id);
186 this._log.debug("Updated local record", {
187 forked: sanitizeStorageObject(forkedRecord),
188 updated: sanitizeStorageObject(reconciledRecord),
193 // NOTE: Because we re-implement the incoming/reconcilliation logic we leave
194 // the |create|, |remove| and |update| methods undefined - the base
195 // implementation throws, which is what we want to happen so we can identify
196 // any places they are "accidentally" called.
198 Object.setPrototypeOf(FormAutofillStore.prototype, Store.prototype);
200 function FormAutofillTracker(name, engine) {
201 Tracker.call(this, name, engine);
204 FormAutofillTracker.prototype = {
205 async observe(subject, topic, data) {
206 if (topic != "formautofill-storage-changed") {
211 subject.wrappedJSObject &&
212 subject.wrappedJSObject.sourceSync
220 this.score += SCORE_INCREMENT_XLARGE;
223 this._log.debug("unrecognized autofill notification", data);
229 Services.obs.addObserver(this, "formautofill-storage-changed");
233 Services.obs.removeObserver(this, "formautofill-storage-changed");
236 Object.setPrototypeOf(FormAutofillTracker.prototype, Tracker.prototype);
238 // This uses the same conventions as BookmarkChangeset in
239 // services/sync/modules/engines/bookmarks.js. Specifically,
240 // - "synced" means the item has already been synced (or we have another reason
241 // to ignore it), and should be ignored in most methods.
242 class AutofillChangeset extends Changeset {
247 getModifiedTimestamp(_id) {
248 throw new Error("Don't use timestamps to resolve autofill merge conflicts");
252 let change = this.changes[id];
254 return !change.synced;
260 let change = this.changes[id];
262 // Mark the change as synced without removing it from the set. We do this
263 // so that we can update FormAutofillStorage in `trackRemainingChanges`.
264 change.synced = true;
269 function FormAutofillEngine(service, name) {
270 SyncEngine.call(this, name, service);
273 FormAutofillEngine.prototype = {
274 // the priority for this engine is == addons, so will happen after bookmarks
275 // prefs and tabs, but before forms, history, etc.
278 // We don't use SyncEngine.initialize() for this, as we initialize even if
279 // the engine is disabled, and we don't want to be the loader of
280 // FormAutofillStorage in this case.
281 async _syncStartup() {
282 await lazy.formAutofillStorage.initialize();
283 await SyncEngine.prototype._syncStartup.call(this);
286 // We handle reconciliation in the store, not the engine.
292 return new AutofillChangeset();
295 async _uploadOutgoing() {
296 this._modified.replace(this._store.storage.pullSyncChanges());
297 await SyncEngine.prototype._uploadOutgoing.call(this);
300 // Typically, engines populate the changeset before downloading records.
301 // However, we handle conflict resolution in the store, so we can wait
302 // to pull changes until we're ready to upload.
303 async pullAllChanges() {
307 async pullNewChanges() {
311 async trackRemainingChanges() {
312 this._store.storage.pushSyncChanges(this._modified.changes);
316 this._noteDeletedId(id);
319 async _resetClient() {
320 await lazy.formAutofillStorage.initialize();
321 this._store.storage.resetSync();
322 await this.resetLastSync(0);
325 async _wipeClient() {
326 await lazy.formAutofillStorage.initialize();
327 this._store.storage.removeAll({ sourceSync: true });
330 Object.setPrototypeOf(FormAutofillEngine.prototype, SyncEngine.prototype);
332 // The concrete engines
334 function AddressesRecord(collection, id) {
335 AutofillRecord.call(this, collection, id);
338 AddressesRecord.prototype = {
339 _logName: "Sync.Record.Addresses",
341 Object.setPrototypeOf(AddressesRecord.prototype, AutofillRecord.prototype);
343 function AddressesStore(name, engine) {
344 FormAutofillStore.call(this, name, engine);
347 AddressesStore.prototype = {
348 _subStorageName: "addresses",
350 Object.setPrototypeOf(AddressesStore.prototype, FormAutofillStore.prototype);
352 export function AddressesEngine(service) {
353 FormAutofillEngine.call(this, service, "Addresses");
356 AddressesEngine.prototype = {
357 _trackerObj: FormAutofillTracker,
358 _storeObj: AddressesStore,
359 _recordObj: AddressesRecord,
365 Object.setPrototypeOf(AddressesEngine.prototype, FormAutofillEngine.prototype);
367 function CreditCardsRecord(collection, id) {
368 AutofillRecord.call(this, collection, id);
371 CreditCardsRecord.prototype = {
372 _logName: "Sync.Record.CreditCards",
374 Object.setPrototypeOf(CreditCardsRecord.prototype, AutofillRecord.prototype);
376 function CreditCardsStore(name, engine) {
377 FormAutofillStore.call(this, name, engine);
380 CreditCardsStore.prototype = {
381 _subStorageName: "creditCards",
383 Object.setPrototypeOf(CreditCardsStore.prototype, FormAutofillStore.prototype);
385 export function CreditCardsEngine(service) {
386 FormAutofillEngine.call(this, service, "CreditCards");
389 CreditCardsEngine.prototype = {
390 _trackerObj: FormAutofillTracker,
391 _storeObj: CreditCardsStore,
392 _recordObj: CreditCardsRecord,
394 return "creditcards";
397 Object.setPrototypeOf(
398 CreditCardsEngine.prototype,
399 FormAutofillEngine.prototype