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