gecko/services/sync/modules/engines/forms.js
Gregory Szorc 45d3f90cf3 Bug 785225 - Part 9: Refactor engines to not use singletons; r=rnewman
Engines now maintain a reference to the service they belong to. This
allows them to obtain references to other engine instances belonging to
that service and that service only.

Stores and trackers now maintain a reference to the engine they belong
to.

Engine managers now maintain a reference back to a service.

The clients singleton has been removed. It now exists as an instance
variable on Service. Parts of ClientsEngine do behave as singletons
(e.g. commands). This will be addressed in future refactoring.
2012-08-29 14:43:41 -07:00

377 lines
11 KiB
JavaScript

/* 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/. */
const EXPORTED_SYMBOLS = ['FormEngine', 'FormRec'];
const Cc = Components.classes;
const Ci = Components.interfaces;
const 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-common/log4moz.js");
const FORMS_TTL = 5184000; // 60 days
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"]);
let FormWrapper = {
_log: Log4Moz.repository.getLogger("Sync.Engine.Forms"),
_getEntryCols: ["name", "value"],
_guidCols: ["guid"],
_stmts: {},
_getStmt: function _getStmt(query) {
if (query in this._stmts) {
return this._stmts[query];
}
this._log.trace("Creating SQL statement: " + query);
let db = Svc.Form.DBConnection;
return this._stmts[query] = db.createAsyncStatement(query);
},
_finalize : function () {
for each (let stmt in FormWrapper._stmts) {
stmt.finalize();
}
FormWrapper._stmts = {};
},
get _getAllEntriesStmt() {
const query =
"SELECT fieldname name, value FROM moz_formhistory " +
"ORDER BY 1.0 * (lastUsed - (SELECT lastUsed FROM moz_formhistory ORDER BY lastUsed ASC LIMIT 1)) / " +
"((SELECT lastUsed FROM moz_formhistory ORDER BY lastUsed DESC LIMIT 1) - (SELECT lastUsed FROM moz_formhistory ORDER BY lastUsed ASC LIMIT 1)) * " +
"timesUsed / (SELECT timesUsed FROM moz_formhistory ORDER BY timesUsed DESC LIMIT 1) DESC " +
"LIMIT 500";
return this._getStmt(query);
},
get _getEntryStmt() {
const query = "SELECT fieldname name, value FROM moz_formhistory " +
"WHERE guid = :guid";
return this._getStmt(query);
},
get _getGUIDStmt() {
const query = "SELECT guid FROM moz_formhistory " +
"WHERE fieldname = :name AND value = :value";
return this._getStmt(query);
},
get _setGUIDStmt() {
const query = "UPDATE moz_formhistory SET guid = :guid " +
"WHERE fieldname = :name AND value = :value";
return this._getStmt(query);
},
get _hasGUIDStmt() {
const query = "SELECT guid FROM moz_formhistory WHERE guid = :guid LIMIT 1";
return this._getStmt(query);
},
get _replaceGUIDStmt() {
const query = "UPDATE moz_formhistory SET guid = :newGUID " +
"WHERE guid = :oldGUID";
return this._getStmt(query);
},
getAllEntries: function getAllEntries() {
return Async.querySpinningly(this._getAllEntriesStmt, this._getEntryCols);
},
getEntry: function getEntry(guid) {
let stmt = this._getEntryStmt;
stmt.params.guid = guid;
return Async.querySpinningly(stmt, this._getEntryCols)[0];
},
getGUID: function getGUID(name, value) {
// Query for the provided entry.
let getStmt = this._getGUIDStmt;
getStmt.params.name = name;
getStmt.params.value = value;
// Give the GUID if we found one.
let item = Async.querySpinningly(getStmt, this._guidCols)[0];
if (!item) {
// Shouldn't happen, but Bug 597400...
// Might as well just return.
this._log.warn("GUID query returned " + item + "; turn on Trace logging for details.");
this._log.trace("getGUID(" + JSON.stringify(name) + ", " +
JSON.stringify(value) + ") => " + item);
return null;
}
if (item.guid != null) {
return item.guid;
}
// We need to create a GUID for this entry.
let setStmt = this._setGUIDStmt;
let guid = Utils.makeGUID();
setStmt.params.guid = guid;
setStmt.params.name = name;
setStmt.params.value = value;
Async.querySpinningly(setStmt);
return guid;
},
hasGUID: function hasGUID(guid) {
let stmt = this._hasGUIDStmt;
stmt.params.guid = guid;
return Async.querySpinningly(stmt, this._guidCols).length == 1;
},
replaceGUID: function replaceGUID(oldGUID, newGUID) {
let stmt = this._replaceGUIDStmt;
stmt.params.oldGUID = oldGUID;
stmt.params.newGUID = newGUID;
Async.querySpinningly(stmt);
}
};
function FormEngine(service) {
SyncEngine.call(this, "Forms", service);
}
FormEngine.prototype = {
__proto__: SyncEngine.prototype,
_storeObj: FormStore,
_trackerObj: FormTracker,
_recordObj: FormRec,
applyIncomingBatchSize: FORMS_STORE_BATCH_SIZE,
get prefName() "history",
_findDupe: function _findDupe(item) {
if (Svc.Form.entryExists(item.name, item.value)) {
return FormWrapper.getGUID(item.name, item.value);
}
}
};
function FormStore(name, engine) {
Store.call(this, name, engine);
}
FormStore.prototype = {
__proto__: Store.prototype,
applyIncomingBatch: function applyIncomingBatch(records) {
return Utils.runInTransaction(Svc.Form.DBConnection, function() {
return Store.prototype.applyIncomingBatch.call(this, records);
}, this);
},
applyIncoming: function applyIncoming(record) {
Store.prototype.applyIncoming.call(this, record);
this._sleep(0); // Yield back to main thread after synchronous operation.
},
getAllIDs: function FormStore_getAllIDs() {
let guids = {};
for each (let {name, value} in FormWrapper.getAllEntries()) {
guids[FormWrapper.getGUID(name, value)] = true;
}
return guids;
},
changeItemID: function FormStore_changeItemID(oldID, newID) {
FormWrapper.replaceGUID(oldID, newID);
},
itemExists: function FormStore_itemExists(id) {
return FormWrapper.hasGUID(id);
},
createRecord: function createRecord(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 FormStore_create(record) {
this._log.trace("Adding form record for " + record.name);
Svc.Form.addEntry(record.name, record.value);
},
remove: function FormStore_remove(record) {
this._log.trace("Removing form record: " + record.id);
// Just skip remove requests for things already gone
let entry = FormWrapper.getEntry(record.id);
if (entry == null) {
return;
}
Svc.Form.removeEntry(entry.name, entry.value);
},
update: function FormStore_update(record) {
this._log.trace("Ignoring form record update request!");
},
wipe: function FormStore_wipe() {
Svc.Form.removeAllEntries();
}
};
function FormTracker(name, engine) {
Tracker.call(this, name, engine);
Svc.Obs.add("weave:engine:start-tracking", this);
Svc.Obs.add("weave:engine:stop-tracking", this);
Svc.Obs.add("profile-change-teardown", this);
}
FormTracker.prototype = {
__proto__: Tracker.prototype,
QueryInterface: XPCOMUtils.generateQI([
Ci.nsIFormSubmitObserver,
Ci.nsIObserver,
Ci.nsISupportsWeakReference]),
trackEntry: function trackEntry(name, value) {
this.addChangedID(FormWrapper.getGUID(name, value));
this.score += SCORE_INCREMENT_MEDIUM;
},
_enabled: false,
observe: function observe(subject, topic, data) {
switch (topic) {
case "weave:engine:start-tracking":
if (!this._enabled) {
Svc.Obs.add("form-notifier", this);
Svc.Obs.add("satchel-storage-changed", this);
// nsHTMLFormElement doesn't use the normal observer/observe
// pattern and looks up nsIFormSubmitObservers to .notify()
// them so add manually to observers
Cc["@mozilla.org/observer-service;1"]
.getService(Ci.nsIObserverService)
.addObserver(this, "earlyformsubmit", true);
this._enabled = true;
}
break;
case "weave:engine:stop-tracking":
if (this._enabled) {
Svc.Obs.remove("form-notifier", this);
Svc.Obs.remove("satchel-storage-changed", this);
Cc["@mozilla.org/observer-service;1"]
.getService(Ci.nsIObserverService)
.removeObserver(this, "earlyformsubmit");
this._enabled = false;
}
break;
case "satchel-storage-changed":
if (data == "addEntry" || data == "before-removeEntry") {
subject = subject.QueryInterface(Ci.nsIArray);
let name = subject.queryElementAt(0, Ci.nsISupportsString)
.toString();
let value = subject.queryElementAt(1, Ci.nsISupportsString)
.toString();
this.trackEntry(name, value);
}
break;
case "profile-change-teardown":
FormWrapper._finalize();
break;
}
},
notify: function FormTracker_notify(formElement, aWindow, actionURI) {
if (this.ignoreAll) {
return;
}
this._log.trace("Form submission notification for " + actionURI.spec);
// XXX Bug 487541 Copy the logic from nsFormHistory::Notify to avoid
// divergent logic, which can lead to security issues, until there's a
// better way to get satchel's results like with a notification.
// Determine if a dom node has the autocomplete attribute set to "off"
let completeOff = function(domNode) {
let autocomplete = domNode.getAttribute("autocomplete");
return autocomplete && autocomplete.search(/^off$/i) == 0;
}
if (completeOff(formElement)) {
this._log.trace("Form autocomplete set to off");
return;
}
/* Get number of elements in form, add points and changedIDs */
let len = formElement.length;
let elements = formElement.elements;
for (let i = 0; i < len; i++) {
let el = elements.item(i);
// Grab the name for debugging, but check if empty when satchel would
let name = el.name;
if (name === "") {
name = el.id;
}
if (!(el instanceof Ci.nsIDOMHTMLInputElement)) {
this._log.trace(name + " is not a DOMHTMLInputElement: " + el);
continue;
}
if (el.type.search(/^text$/i) != 0) {
this._log.trace(name + "'s type is not 'text': " + el.type);
continue;
}
if (completeOff(el)) {
this._log.trace(name + "'s autocomplete set to off");
continue;
}
if (el.value === "") {
this._log.trace(name + "'s value is empty");
continue;
}
if (el.value == el.defaultValue) {
this._log.trace(name + "'s value is the default");
continue;
}
if (name === "") {
this._log.trace("Text input element has no name or id");
continue;
}
// Get the GUID on a delay so that it can be added to the DB first...
Utils.nextTick(function() {
this._log.trace("Logging form element: " + [name, el.value]);
this.trackEntry(name, el.value);
}, this);
}
}
};