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/. */
4
5"use strict";
6
7var EXPORTED_SYMBOLS = ["AddressesEngine", "CreditCardsEngine"];
8
9const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
10const { Changeset, Store, SyncEngine, Tracker } = ChromeUtils.import(
11  "resource://services-sync/engines.js"
12);
13const { CryptoWrapper } = ChromeUtils.import(
14  "resource://services-sync/record.js"
15);
16const { Utils } = ChromeUtils.import("resource://services-sync/util.js");
17const { SCORE_INCREMENT_XLARGE } = ChromeUtils.import(
18  "resource://services-sync/constants.js"
19);
20
21ChromeUtils.defineModuleGetter(this, "Log", "resource://gre/modules/Log.jsm");
22ChromeUtils.defineModuleGetter(
23  this,
24  "formAutofillStorage",
25  "resource://autofill/FormAutofillStorage.jsm"
26);
27
28// A helper to sanitize address and creditcard records suitable for logging.
29function sanitizeStorageObject(ob) {
30  if (!ob) {
31    return null;
32  }
33  const allowList = ["timeCreated", "timeLastUsed", "timeLastModified"];
34  let result = {};
35  for (let key of Object.keys(ob)) {
36    let origVal = ob[key];
37    if (allowList.includes(key)) {
38      result[key] = origVal;
39    } else if (typeof origVal == "string") {
40      result[key] = "X".repeat(origVal.length);
41    } else {
42      result[key] = typeof origVal; // *shrug*
43    }
44  }
45  return result;
46}
47
48function AutofillRecord(collection, id) {
49  CryptoWrapper.call(this, collection, id);
50}
51
52AutofillRecord.prototype = {
53  __proto__: CryptoWrapper.prototype,
54
55  toEntry() {
56    return Object.assign(
57      {
58        guid: this.id,
59      },
60      this.entry
61    );
62  },
63
64  fromEntry(entry) {
65    this.id = entry.guid;
66    this.entry = entry;
67    // The GUID is already stored in record.id, so we nuke it from the entry
68    // itself to save a tiny bit of space. The formAutofillStorage clones profiles,
69    // so nuking in-place is OK.
70    delete this.entry.guid;
71  },
72
73  cleartextToString() {
74    // And a helper so logging a *Sync* record auto sanitizes.
75    let record = this.cleartext;
76    return JSON.stringify({ entry: sanitizeStorageObject(record.entry) });
77  },
78};
79
80// Profile data is stored in the "entry" object of the record.
81Utils.deferGetSet(AutofillRecord, "cleartext", ["entry"]);
82
83function FormAutofillStore(name, engine) {
84  Store.call(this, name, engine);
85}
86
87FormAutofillStore.prototype = {
88  __proto__: Store.prototype,
89
90  _subStorageName: null, // overridden below.
91  _storage: null,
92
93  get storage() {
94    if (!this._storage) {
95      this._storage = formAutofillStorage[this._subStorageName];
96    }
97    return this._storage;
98  },
99
100  async getAllIDs() {
101    let result = {};
102    for (let { guid } of await this.storage.getAll({ includeDeleted: true })) {
103      result[guid] = true;
104    }
105    return result;
106  },
107
108  async changeItemID(oldID, newID) {
109    this.storage.changeGUID(oldID, newID);
110  },
111
112  // Note: this function intentionally returns false in cases where we only have
113  // a (local) tombstone - and formAutofillStorage.get() filters them for us.
114  async itemExists(id) {
115    return Boolean(await this.storage.get(id));
116  },
117
118  async applyIncoming(remoteRecord) {
119    if (remoteRecord.deleted) {
120      this._log.trace("Deleting record", remoteRecord);
121      this.storage.remove(remoteRecord.id, { sourceSync: true });
122      return;
123    }
124
125    if (await this.itemExists(remoteRecord.id)) {
126      // We will never get a tombstone here, so we are updating a real record.
127      await this._doUpdateRecord(remoteRecord);
128      return;
129    }
130
131    // No matching local record. Try to dedupe a NEW local record.
132    let localDupeID = await this.storage.findDuplicateGUID(
133      remoteRecord.toEntry()
134    );
135    if (localDupeID) {
136      this._log.trace(
137        `Deduping local record ${localDupeID} to remote`,
138        remoteRecord
139      );
140      // Change the local GUID to match the incoming record, then apply the
141      // incoming record.
142      await this.changeItemID(localDupeID, remoteRecord.id);
143      await this._doUpdateRecord(remoteRecord);
144      return;
145    }
146
147    // We didn't find a dupe, either, so must be a new record (or possibly
148    // a non-deleted version of an item we have a tombstone for, which add()
149    // handles for us.)
150    this._log.trace("Add record", remoteRecord);
151    let entry = remoteRecord.toEntry();
152    await this.storage.add(entry, { sourceSync: true });
153  },
154
155  async createRecord(id, collection) {
156    this._log.trace("Create record", id);
157    let record = new AutofillRecord(collection, id);
158    let entry = await this.storage.get(id, {
159      rawData: true,
160    });
161    if (entry) {
162      record.fromEntry(entry);
163    } else {
164      // We should consider getting a more authortative indication it's actually deleted.
165      this._log.debug(
166        `Failed to get autofill record with id "${id}", assuming deleted`
167      );
168      record.deleted = true;
169    }
170    return record;
171  },
172
173  async _doUpdateRecord(record) {
174    this._log.trace("Updating record", record);
175
176    let entry = record.toEntry();
177    let { forkedGUID } = await this.storage.reconcile(entry);
178    if (this._log.level <= Log.Level.Debug) {
179      let forkedRecord = forkedGUID ? await this.storage.get(forkedGUID) : null;
180      let reconciledRecord = await this.storage.get(record.id);
181      this._log.debug("Updated local record", {
182        forked: sanitizeStorageObject(forkedRecord),
183        updated: sanitizeStorageObject(reconciledRecord),
184      });
185    }
186  },
187
188  // NOTE: Because we re-implement the incoming/reconcilliation logic we leave
189  // the |create|, |remove| and |update| methods undefined - the base
190  // implementation throws, which is what we want to happen so we can identify
191  // any places they are "accidentally" called.
192};
193
194function FormAutofillTracker(name, engine) {
195  Tracker.call(this, name, engine);
196}
197
198FormAutofillTracker.prototype = {
199  __proto__: Tracker.prototype,
200  async observe(subject, topic, data) {
201    if (topic != "formautofill-storage-changed") {
202      return;
203    }
204    if (
205      subject &&
206      subject.wrappedJSObject &&
207      subject.wrappedJSObject.sourceSync
208    ) {
209      return;
210    }
211    switch (data) {
212      case "add":
213      case "update":
214      case "remove":
215        this.score += SCORE_INCREMENT_XLARGE;
216        break;
217      default:
218        this._log.debug("unrecognized autofill notification", data);
219        break;
220    }
221  },
222
223  onStart() {
224    Services.obs.addObserver(this, "formautofill-storage-changed");
225  },
226
227  onStop() {
228    Services.obs.removeObserver(this, "formautofill-storage-changed");
229  },
230};
231
232// This uses the same conventions as BookmarkChangeset in
233// services/sync/modules/engines/bookmarks.js. Specifically,
234// - "synced" means the item has already been synced (or we have another reason
235//   to ignore it), and should be ignored in most methods.
236class AutofillChangeset extends Changeset {
237  constructor() {
238    super();
239  }
240
241  getModifiedTimestamp(id) {
242    throw new Error("Don't use timestamps to resolve autofill merge conflicts");
243  }
244
245  has(id) {
246    let change = this.changes[id];
247    if (change) {
248      return !change.synced;
249    }
250    return false;
251  }
252
253  delete(id) {
254    let change = this.changes[id];
255    if (change) {
256      // Mark the change as synced without removing it from the set. We do this
257      // so that we can update FormAutofillStorage in `trackRemainingChanges`.
258      change.synced = true;
259    }
260  }
261}
262
263function FormAutofillEngine(service, name) {
264  SyncEngine.call(this, name, service);
265}
266
267FormAutofillEngine.prototype = {
268  __proto__: SyncEngine.prototype,
269
270  // the priority for this engine is == addons, so will happen after bookmarks
271  // prefs and tabs, but before forms, history, etc.
272  syncPriority: 5,
273
274  // We don't use SyncEngine.initialize() for this, as we initialize even if
275  // the engine is disabled, and we don't want to be the loader of
276  // FormAutofillStorage in this case.
277  async _syncStartup() {
278    await formAutofillStorage.initialize();
279    await SyncEngine.prototype._syncStartup.call(this);
280  },
281
282  // We handle reconciliation in the store, not the engine.
283  async _reconcile() {
284    return true;
285  },
286
287  emptyChangeset() {
288    return new AutofillChangeset();
289  },
290
291  async _uploadOutgoing() {
292    this._modified.replace(this._store.storage.pullSyncChanges());
293    await SyncEngine.prototype._uploadOutgoing.call(this);
294  },
295
296  // Typically, engines populate the changeset before downloading records.
297  // However, we handle conflict resolution in the store, so we can wait
298  // to pull changes until we're ready to upload.
299  async pullAllChanges() {
300    return {};
301  },
302
303  async pullNewChanges() {
304    return {};
305  },
306
307  async trackRemainingChanges() {
308    this._store.storage.pushSyncChanges(this._modified.changes);
309  },
310
311  _deleteId(id) {
312    this._noteDeletedId(id);
313  },
314
315  async _resetClient() {
316    await formAutofillStorage.initialize();
317    this._store.storage.resetSync();
318  },
319
320  async _wipeClient() {
321    await formAutofillStorage.initialize();
322    this._store.storage.removeAll({ sourceSync: true });
323  },
324};
325
326// The concrete engines
327
328function AddressesRecord(collection, id) {
329  AutofillRecord.call(this, collection, id);
330}
331
332AddressesRecord.prototype = {
333  __proto__: AutofillRecord.prototype,
334  _logName: "Sync.Record.Addresses",
335};
336
337function AddressesStore(name, engine) {
338  FormAutofillStore.call(this, name, engine);
339}
340
341AddressesStore.prototype = {
342  __proto__: FormAutofillStore.prototype,
343  _subStorageName: "addresses",
344};
345
346function AddressesEngine(service) {
347  FormAutofillEngine.call(this, service, "Addresses");
348}
349
350AddressesEngine.prototype = {
351  __proto__: FormAutofillEngine.prototype,
352  _trackerObj: FormAutofillTracker,
353  _storeObj: AddressesStore,
354  _recordObj: AddressesRecord,
355
356  get prefName() {
357    return "addresses";
358  },
359};
360
361function CreditCardsRecord(collection, id) {
362  AutofillRecord.call(this, collection, id);
363}
364
365CreditCardsRecord.prototype = {
366  __proto__: AutofillRecord.prototype,
367  _logName: "Sync.Record.CreditCards",
368};
369
370function CreditCardsStore(name, engine) {
371  FormAutofillStore.call(this, name, engine);
372}
373
374CreditCardsStore.prototype = {
375  __proto__: FormAutofillStore.prototype,
376  _subStorageName: "creditCards",
377};
378
379function CreditCardsEngine(service) {
380  FormAutofillEngine.call(this, service, "CreditCards");
381}
382
383CreditCardsEngine.prototype = {
384  __proto__: FormAutofillEngine.prototype,
385  _trackerObj: FormAutofillTracker,
386  _storeObj: CreditCardsStore,
387  _recordObj: CreditCardsRecord,
388  get prefName() {
389    return "creditcards";
390  },
391};
392