/* 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/. */ this.EXPORTED_SYMBOLS = ['FormEngine', 'FormRec', 'FormValidator']; var Cc = Components.classes; var Ci = Components.interfaces; var Cu = Components.utils; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://services-sync/engines.js"); Cu.import("resource://services-sync/record.js"); Cu.import("resource://services-common/async.js"); Cu.import("resource://services-sync/util.js"); Cu.import("resource://services-sync/constants.js"); Cu.import("resource://services-sync/collection_validator.js"); Cu.import("resource://gre/modules/Log.jsm"); const FORMS_TTL = 3 * 365 * 24 * 60 * 60; // Three years in seconds. this.FormRec = function FormRec(collection, id) { CryptoWrapper.call(this, collection, id); } FormRec.prototype = { __proto__: CryptoWrapper.prototype, _logName: "Sync.Record.Form", ttl: FORMS_TTL }; Utils.deferGetSet(FormRec, "cleartext", ["name", "value"]); var FormWrapper = { _log: Log.repository.getLogger("Sync.Engine.Forms"), _getEntryCols: ["fieldname", "value"], _guidCols: ["guid"], _promiseSearch: function(terms, searchData) { return new Promise(resolve => { let results = []; let callbacks = { handleResult(result) { results.push(result); }, handleCompletion(reason) { resolve(results); } }; Svc.FormHistory.search(terms, searchData, callbacks); }) }, // Do a "sync" search by spinning the event loop until it completes. _searchSpinningly: function(terms, searchData) { return Async.promiseSpinningly(this._promiseSearch(terms, searchData)); }, _updateSpinningly: function(changes) { if (!Svc.FormHistory.enabled) { return; // update isn't going to do anything. } let cb = Async.makeSpinningCallback(); let callbacks = { handleCompletion: function(reason) { cb(); } }; Svc.FormHistory.update(changes, callbacks); return cb.wait(); }, getEntry: function (guid) { let results = this._searchSpinningly(this._getEntryCols, {guid: guid}); if (!results.length) { return null; } return {name: results[0].fieldname, value: results[0].value}; }, getGUID: function (name, value) { // Query for the provided entry. let query = { fieldname: name, value: value }; let results = this._searchSpinningly(this._guidCols, query); return results.length ? results[0].guid : null; }, hasGUID: function (guid) { // We could probably use a count function here, but searchSpinningly exists... return this._searchSpinningly(this._guidCols, {guid: guid}).length != 0; }, replaceGUID: function (oldGUID, newGUID) { let changes = { op: "update", guid: oldGUID, newGuid: newGUID, } this._updateSpinningly(changes); } }; this.FormEngine = function FormEngine(service) { SyncEngine.call(this, "Forms", service); } FormEngine.prototype = { __proto__: SyncEngine.prototype, _storeObj: FormStore, _trackerObj: FormTracker, _recordObj: FormRec, applyIncomingBatchSize: FORMS_STORE_BATCH_SIZE, syncPriority: 6, get prefName() { return "history"; }, _findDupe: function _findDupe(item) { return FormWrapper.getGUID(item.name, item.value); } }; function FormStore(name, engine) { Store.call(this, name, engine); } FormStore.prototype = { __proto__: Store.prototype, _processChange: function (change) { // If this._changes is defined, then we are applying a batch, so we // can defer it. if (this._changes) { this._changes.push(change); return; } // Otherwise we must handle the change synchronously, right now. FormWrapper._updateSpinningly(change); }, applyIncomingBatch: function (records) { // We collect all the changes to be made then apply them all at once. this._changes = []; let failures = Store.prototype.applyIncomingBatch.call(this, records); if (this._changes.length) { FormWrapper._updateSpinningly(this._changes); } delete this._changes; return failures; }, getAllIDs: function () { let results = FormWrapper._searchSpinningly(["guid"], []) let guids = {}; for (let result of results) { guids[result.guid] = true; } return guids; }, changeItemID: function (oldID, newID) { FormWrapper.replaceGUID(oldID, newID); }, itemExists: function (id) { return FormWrapper.hasGUID(id); }, createRecord: function (id, collection) { let record = new FormRec(collection, id); let entry = FormWrapper.getEntry(id); if (entry != null) { record.name = entry.name; record.value = entry.value; } else { record.deleted = true; } return record; }, create: function (record) { this._log.trace("Adding form record for " + record.name); let change = { op: "add", fieldname: record.name, value: record.value }; this._processChange(change); }, remove: function (record) { this._log.trace("Removing form record: " + record.id); let change = { op: "remove", guid: record.id }; this._processChange(change); }, update: function (record) { this._log.trace("Ignoring form record update request!"); }, wipe: function () { let change = { op: "remove" }; FormWrapper._updateSpinningly(change); } }; function FormTracker(name, engine) { Tracker.call(this, name, engine); } FormTracker.prototype = { __proto__: Tracker.prototype, QueryInterface: XPCOMUtils.generateQI([ Ci.nsIObserver, Ci.nsISupportsWeakReference]), startTracking: function() { Svc.Obs.add("satchel-storage-changed", this); }, stopTracking: function() { Svc.Obs.remove("satchel-storage-changed", this); }, observe: function (subject, topic, data) { Tracker.prototype.observe.call(this, subject, topic, data); if (this.ignoreAll) { return; } switch (topic) { case "satchel-storage-changed": if (data == "formhistory-add" || data == "formhistory-remove") { let guid = subject.QueryInterface(Ci.nsISupportsString).toString(); this.trackEntry(guid); } break; } }, trackEntry: function (guid) { this.addChangedID(guid); this.score += SCORE_INCREMENT_MEDIUM; }, }; class FormsProblemData extends CollectionProblemData { getSummary() { // We don't support syncing deleted form data, so "clientMissing" isn't a problem return super.getSummary().filter(entry => entry.name !== "clientMissing"); } } class FormValidator extends CollectionValidator { constructor() { super("forms", "id", ["name", "value"]); } emptyProblemData() { return new FormsProblemData(); } getClientItems() { return FormWrapper._promiseSearch(["guid", "fieldname", "value"], {}); } normalizeClientItem(item) { return { id: item.guid, guid: item.guid, name: item.fieldname, fieldname: item.fieldname, value: item.value, original: item, }; } normalizeServerItem(item) { let res = Object.assign({ guid: item.id, fieldname: item.name, original: item, }, item); // Missing `name` or `value` causes the getGUID call to throw if (item.name !== undefined && item.value !== undefined) { let guid = FormWrapper.getGUID(item.name, item.value); if (guid) { res.guid = guid; res.id = guid; res.duped = true; } } return res; } }