diff options
Diffstat (limited to 'services/sync/modules/engines/forms.js')
-rw-r--r-- | services/sync/modules/engines/forms.js | 305 |
1 files changed, 305 insertions, 0 deletions
diff --git a/services/sync/modules/engines/forms.js b/services/sync/modules/engines/forms.js new file mode 100644 index 000000000..43f79d4f7 --- /dev/null +++ b/services/sync/modules/engines/forms.js @@ -0,0 +1,305 @@ +/* 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; + } +}
\ No newline at end of file |