/* 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"]; 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" ); ChromeUtils.defineModuleGetter(this, "Log", "resource://gre/modules/Log.jsm"); ChromeUtils.defineModuleGetter( this, "formAutofillStorage", "resource://formautofill/FormAutofillStorage.jsm" ); // A helper to sanitize address and creditcard records suitable for logging. function sanitizeStorageObject(ob) { if (!ob) { return null; } const whitelist = ["timeCreated", "timeLastUsed", "timeLastModified"]; let result = {}; for (let key of Object.keys(ob)) { let origVal = ob[key]; if (whitelist.includes(key)) { 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, 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 // itself to save a tiny bit of space. The formAutofillStorage clones profiles, // so nuking in-place is OK. delete this.entry.guid; }, cleartextToString() { // And a helper so logging a *Sync* record auto sanitizes. let record = this.cleartext; return JSON.stringify({ entry: sanitizeStorageObject(record.entry) }); }, }; // 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) { this._storage = formAutofillStorage[this._subStorageName]; } return this._storage; }, async getAllIDs() { let result = {}; for (let { guid } of await this.storage.getAll({ includeDeleted: true })) { 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 // a (local) tombstone - and formAutofillStorage.get() filters them for us. async itemExists(id) { return Boolean(await this.storage.get(id)); }, 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. let localDupeID = await this.storage.findDuplicateGUID( remoteRecord.toEntry() ); 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 // a non-deleted version of an item we have a tombstone for, which add() // handles for us.) this._log.trace("Add record", remoteRecord); let entry = remoteRecord.toEntry(); await this.storage.add(entry, { sourceSync: true }); }, async createRecord(id, collection) { this._log.trace("Create record", id); let record = new AutofillRecord(collection, id); let entry = await this.storage.get(id, { rawData: true, }); if (entry) { record.fromEntry(entry); } else { // 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); let entry = record.toEntry(); let { forkedGUID } = await this.storage.reconcile(entry); if (this._log.level <= Log.Level.Debug) { let forkedRecord = forkedGUID ? await this.storage.get(forkedGUID) : null; let reconciledRecord = await this.storage.get(record.id); this._log.debug("Updated local record", { forked: sanitizeStorageObject(forkedRecord), updated: sanitizeStorageObject(reconciledRecord), }); } }, // 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, async observe(subject, topic, data) { 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; } }, // `_ignore` checks the change source for each observer notification, so we // don't want to let the engine ignore all changes during a sync. get ignoreAll() { return false; }, // Define an empty setter so that the engine doesn't throw a `TypeError` // setting a read-only property. set ignoreAll(value) {}, onStart() { Services.obs.addObserver(this, "formautofill-storage-changed"); }, onStop() { Services.obs.removeObserver(this, "formautofill-storage-changed"); }, // We never want to persist changed IDs, as the changes are already stored // in FormAutofillStorage persistChangedIDs: false, // Ensure we aren't accidentally using the base persistence. get changedIDs() { throw new Error("changedIDs isn't meaningful for this engine"); }, set changedIDs(obj) { throw new Error("changedIDs isn't meaningful for this engine"); }, addChangedID(id, when) { throw new Error("Don't add IDs to the autofill tracker"); }, removeChangedID(id) { throw new Error("Don't remove IDs from the autofill tracker"); }, // This method is called at various times, so we override with a no-op // instead of throwing. clearChangedIDs() {}, }; // 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 // so that we can update FormAutofillStorage in `trackRemainingChanges`. 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 // FormAutofillStorage in this case. async _syncStartup() { await formAutofillStorage.initialize(); 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() { await formAutofillStorage.initialize(); this._store.storage.resetSync(); }, async _wipeClient() { await formAutofillStorage.initialize(); this._store.storage.removeAll({ sourceSync: true }); }, }; // 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"; }, };