Source code

Revision control

Other Tools

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
7
var EXPORTED_SYMBOLS = ["AddressesEngine", "CreditCardsEngine"];
8
9
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
10
const { Changeset, Store, SyncEngine, Tracker } = ChromeUtils.import(
12
);
13
const { CryptoWrapper } = ChromeUtils.import(
15
);
16
const { Utils } = ChromeUtils.import("resource://services-sync/util.js");
17
const { SCORE_INCREMENT_XLARGE } = ChromeUtils.import(
19
);
20
21
ChromeUtils.defineModuleGetter(this, "Log", "resource://gre/modules/Log.jsm");
22
ChromeUtils.defineModuleGetter(
23
this,
24
"formAutofillStorage",
26
);
27
28
// A helper to sanitize address and creditcard records suitable for logging.
29
function sanitizeStorageObject(ob) {
30
if (!ob) {
31
return null;
32
}
33
const whitelist = ["timeCreated", "timeLastUsed", "timeLastModified"];
34
let result = {};
35
for (let key of Object.keys(ob)) {
36
let origVal = ob[key];
37
if (whitelist.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
48
function AutofillRecord(collection, id) {
49
CryptoWrapper.call(this, collection, id);
50
}
51
52
AutofillRecord.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.
81
Utils.deferGetSet(AutofillRecord, "cleartext", ["entry"]);
82
83
function FormAutofillStore(name, engine) {
84
Store.call(this, name, engine);
85
}
86
87
FormAutofillStore.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
194
function FormAutofillTracker(name, engine) {
195
Tracker.call(this, name, engine);
196
}
197
198
FormAutofillTracker.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
// `_ignore` checks the change source for each observer notification, so we
224
// don't want to let the engine ignore all changes during a sync.
225
get ignoreAll() {
226
return false;
227
},
228
229
// Define an empty setter so that the engine doesn't throw a `TypeError`
230
// setting a read-only property.
231
set ignoreAll(value) {},
232
233
onStart() {
234
Services.obs.addObserver(this, "formautofill-storage-changed");
235
},
236
237
onStop() {
238
Services.obs.removeObserver(this, "formautofill-storage-changed");
239
},
240
241
// We never want to persist changed IDs, as the changes are already stored
242
// in FormAutofillStorage
243
persistChangedIDs: false,
244
245
// Ensure we aren't accidentally using the base persistence.
246
get changedIDs() {
247
throw new Error("changedIDs isn't meaningful for this engine");
248
},
249
250
set changedIDs(obj) {
251
throw new Error("changedIDs isn't meaningful for this engine");
252
},
253
254
addChangedID(id, when) {
255
throw new Error("Don't add IDs to the autofill tracker");
256
},
257
258
removeChangedID(id) {
259
throw new Error("Don't remove IDs from the autofill tracker");
260
},
261
262
// This method is called at various times, so we override with a no-op
263
// instead of throwing.
264
clearChangedIDs() {},
265
};
266
267
// This uses the same conventions as BookmarkChangeset in
268
// services/sync/modules/engines/bookmarks.js. Specifically,
269
// - "synced" means the item has already been synced (or we have another reason
270
// to ignore it), and should be ignored in most methods.
271
class AutofillChangeset extends Changeset {
272
constructor() {
273
super();
274
}
275
276
getModifiedTimestamp(id) {
277
throw new Error("Don't use timestamps to resolve autofill merge conflicts");
278
}
279
280
has(id) {
281
let change = this.changes[id];
282
if (change) {
283
return !change.synced;
284
}
285
return false;
286
}
287
288
delete(id) {
289
let change = this.changes[id];
290
if (change) {
291
// Mark the change as synced without removing it from the set. We do this
292
// so that we can update FormAutofillStorage in `trackRemainingChanges`.
293
change.synced = true;
294
}
295
}
296
}
297
298
function FormAutofillEngine(service, name) {
299
SyncEngine.call(this, name, service);
300
}
301
302
FormAutofillEngine.prototype = {
303
__proto__: SyncEngine.prototype,
304
305
// the priority for this engine is == addons, so will happen after bookmarks
306
// prefs and tabs, but before forms, history, etc.
307
syncPriority: 5,
308
309
// We don't use SyncEngine.initialize() for this, as we initialize even if
310
// the engine is disabled, and we don't want to be the loader of
311
// FormAutofillStorage in this case.
312
async _syncStartup() {
313
await formAutofillStorage.initialize();
314
await SyncEngine.prototype._syncStartup.call(this);
315
},
316
317
// We handle reconciliation in the store, not the engine.
318
async _reconcile() {
319
return true;
320
},
321
322
emptyChangeset() {
323
return new AutofillChangeset();
324
},
325
326
async _uploadOutgoing() {
327
this._modified.replace(this._store.storage.pullSyncChanges());
328
await SyncEngine.prototype._uploadOutgoing.call(this);
329
},
330
331
// Typically, engines populate the changeset before downloading records.
332
// However, we handle conflict resolution in the store, so we can wait
333
// to pull changes until we're ready to upload.
334
async pullAllChanges() {
335
return {};
336
},
337
338
async pullNewChanges() {
339
return {};
340
},
341
342
async trackRemainingChanges() {
343
this._store.storage.pushSyncChanges(this._modified.changes);
344
},
345
346
_deleteId(id) {
347
this._noteDeletedId(id);
348
},
349
350
async _resetClient() {
351
await formAutofillStorage.initialize();
352
this._store.storage.resetSync();
353
},
354
355
async _wipeClient() {
356
await formAutofillStorage.initialize();
357
this._store.storage.removeAll({ sourceSync: true });
358
},
359
};
360
361
// The concrete engines
362
363
function AddressesRecord(collection, id) {
364
AutofillRecord.call(this, collection, id);
365
}
366
367
AddressesRecord.prototype = {
368
__proto__: AutofillRecord.prototype,
369
_logName: "Sync.Record.Addresses",
370
};
371
372
function AddressesStore(name, engine) {
373
FormAutofillStore.call(this, name, engine);
374
}
375
376
AddressesStore.prototype = {
377
__proto__: FormAutofillStore.prototype,
378
_subStorageName: "addresses",
379
};
380
381
function AddressesEngine(service) {
382
FormAutofillEngine.call(this, service, "Addresses");
383
}
384
385
AddressesEngine.prototype = {
386
__proto__: FormAutofillEngine.prototype,
387
_trackerObj: FormAutofillTracker,
388
_storeObj: AddressesStore,
389
_recordObj: AddressesRecord,
390
391
get prefName() {
392
return "addresses";
393
},
394
};
395
396
function CreditCardsRecord(collection, id) {
397
AutofillRecord.call(this, collection, id);
398
}
399
400
CreditCardsRecord.prototype = {
401
__proto__: AutofillRecord.prototype,
402
_logName: "Sync.Record.CreditCards",
403
};
404
405
function CreditCardsStore(name, engine) {
406
FormAutofillStore.call(this, name, engine);
407
}
408
409
CreditCardsStore.prototype = {
410
__proto__: FormAutofillStore.prototype,
411
_subStorageName: "creditCards",
412
};
413
414
function CreditCardsEngine(service) {
415
FormAutofillEngine.call(this, service, "CreditCards");
416
}
417
418
CreditCardsEngine.prototype = {
419
__proto__: FormAutofillEngine.prototype,
420
_trackerObj: FormAutofillTracker,
421
_storeObj: CreditCardsStore,
422
_recordObj: CreditCardsRecord,
423
get prefName() {
424
return "creditcards";
425
},
426
};