--- a/toolkit/components/satchel/FormHistoryStartup.js
+++ b/toolkit/components/satchel/FormHistoryStartup.js
@@ -40,16 +40,17 @@ FormHistoryStartup.prototype = {
case "profile-after-change":
this.init();
default:
break;
}
},
inited: false,
+ pendingQuery: null,
init: function()
{
if (this.inited)
return;
this.inited = true;
Services.prefs.addObserver("browser.formfill.", this, true);
@@ -57,17 +58,24 @@ FormHistoryStartup.prototype = {
// triggers needed service cleanup and db shutdown
Services.obs.addObserver(this, "profile-before-change", true);
Services.obs.addObserver(this, "formhistory-expire-now", true);
let messageManager = Cc["@mozilla.org/globalmessagemanager;1"].
getService(Ci.nsIMessageListenerManager);
messageManager.loadFrameScript("chrome://satchel/content/formSubmitListener.js", true);
messageManager.addMessageListener("FormHistory:FormSubmitEntries", this);
- messageManager.addMessageListener("FormHistory:AutoCompleteSearchAsync", this);
+
+ // For each of these messages, we could receive them from content,
+ // or we might receive them from the ppmm if the searchbar is
+ // having its history queried.
+ for (let manager of [messageManager, Services.ppmm]) {
+ manager.addMessageListener("FormHistory:AutoCompleteSearchAsync", this);
+ manager.addMessageListener("FormHistory:RemoveEntry", this);
+ }
},
receiveMessage: function(message) {
switch (message.name) {
case "FormHistory:FormSubmitEntries": {
let entries = message.data;
let changes = entries.map(function(entry) {
return {
@@ -77,16 +85,65 @@ FormHistoryStartup.prototype = {
}
});
FormHistory.update(changes);
break;
}
case "FormHistory:AutoCompleteSearchAsync": {
- AutoCompleteE10S.search(message);
+ let { id, searchString, params } = message.data;
+
+ if (this.pendingQuery) {
+ this.pendingQuery.cancel();
+ this.pendingQuery = null;
+ }
+
+ let mm;
+ if (message.target instanceof Ci.nsIMessageListenerManager) {
+ // The target is the PPMM, meaning that the parent process
+ // is requesting FormHistory data on the searchbar.
+ mm = message.target;
+ } else {
+ // Otherwise, the target is a <xul:browser>.
+ mm = message.target.messageManager;
+ }
+
+ let results = [];
+ let processResults = {
+ handleResult: aResult => {
+ results.push(aResult);
+ },
+ handleCompletion: aReason => {
+ // Check that the current query is still the one we created. Our
+ // query might have been canceled shortly before completing, in
+ // that case we don't want to call the callback anymore.
+ if (query == this.pendingQuery) {
+ this.pendingQuery = null;
+ if (!aReason) {
+ mm.sendAsyncMessage("FormHistory:AutoCompleteSearchResults",
+ { id, results });
+ }
+ }
+ }
+ };
+
+ let query = FormHistory.getAutoCompleteResults(searchString, params,
+ processResults);
+ this.pendingQuery = query;
break;
}
+
+ case "FormHistory:RemoveEntry": {
+ let { inputName, value } = message.data;
+ FormHistory.update({
+ op: "remove",
+ fieldname: inputName,
+ value,
+ });
+ break;
+ }
+
}
}
};
this.NSGetFactory = XPCOMUtils.generateNSGetFactory([FormHistoryStartup]);
--- a/toolkit/components/satchel/nsFormAutoComplete.js
+++ b/toolkit/components/satchel/nsFormAutoComplete.js
@@ -9,27 +9,160 @@ const { classes: Cc, interfaces: Ci, res
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils",
"resource://gre/modules/BrowserUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Deprecated",
"resource://gre/modules/Deprecated.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "FormHistory",
- "resource://gre/modules/FormHistory.jsm");
function isAutocompleteDisabled(aField) {
if (aField.autocomplete !== "") {
return aField.autocomplete === "off";
}
return aField.form && aField.form.autocomplete === "off";
}
+/**
+ * An abstraction to talk with the FormHistory database over
+ * the message layer. FormHistoryClient will take care of
+ * figuring out the most appropriate message manager to use,
+ * and what things to send.
+ *
+ * It is assumed that nsFormAutoComplete will only ever use
+ * one instance at a time, and will not attempt to perform more
+ * than one search request with the same instance at a time.
+ * However, nsFormAutoComplete might call remove() any number of
+ * times with the same instance of the client.
+ *
+ * @param Object with the following properties:
+ *
+ * formField (DOM node):
+ * A DOM node that we're requesting form history for.
+ *
+ * inputName (string):
+ * The name of the input to do the FormHistory look-up
+ * with. If this is searchbar-history, then formField
+ * needs to be null, otherwise constructing will throw.
+ */
+function FormHistoryClient({ formField, inputName }) {
+ if (formField && inputName != this.SEARCHBAR_ID) {
+ let window = formField.ownerDocument.defaultView;
+ let topDocShell = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDocShell)
+ .sameTypeRootTreeItem
+ .QueryInterface(Ci.nsIDocShell);
+ this.mm = topDocShell.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIContentFrameMessageManager);
+ } else {
+ if (inputName == this.SEARCHBAR_ID) {
+ if (formField) {
+ throw new Error("FormHistoryClient constructed with both a " +
+ "formField and an inputName. This is not " +
+ "supported, and only empty results will be " +
+ "returned.");
+ }
+ }
+ this.mm = Services.cpmm;
+ }
+
+ this.inputName = inputName;
+ this.id = FormHistoryClient.nextRequestID++;
+}
+
+FormHistoryClient.prototype = {
+ SEARCHBAR_ID: "searchbar-history",
+
+ // It is assumed that nsFormAutoComplete only uses / cares about
+ // one FormHistoryClient at a time, and won't attempt to have
+ // multiple in-flight searches occurring with the same FormHistoryClient.
+ // We use an ID number per instantiated FormHistoryClient to make
+ // sure we only respond to messages that were meant for us.
+ id: 0,
+ callback: null,
+ inputName: "",
+ mm: null,
+
+ /**
+ * Query FormHistory for some results.
+ *
+ * @param searchString (string)
+ * The string to search FormHistory for. See
+ * FormHistory.getAutoCompleteResults.
+ * @param params (object)
+ * An Object with search properties. See
+ * FormHistory.getAutoCompleteResults.
+ * @param callback
+ * A callback function that will take a single
+ * argument (the found entries).
+ */
+ requestAutoCompleteResults(searchString, params, callback) {
+ this.mm.sendAsyncMessage("FormHistory:AutoCompleteSearchAsync", {
+ id: this.id,
+ searchString,
+ params,
+ });
+
+ this.mm.addMessageListener("FormHistory:AutoCompleteSearchResults",
+ this);
+ this.callback = callback;
+ },
+
+ /**
+ * Cancel an in-flight results request. This ensures that the
+ * callback that requestAutoCompleteResults was passed is never
+ * called from this FormHistoryClient.
+ */
+ cancel() {
+ this.clearListeners();
+ },
+
+ /**
+ * Remove an item from FormHistory.
+ *
+ * @param value (string)
+ *
+ * The value to remove for this particular
+ * field.
+ */
+ remove(value) {
+ this.mm.sendAsyncMessage("FormHistory:RemoveEntry", {
+ inputName: this.inputName,
+ value,
+ });
+ },
+
+ // Private methods
+
+ receiveMessage(msg) {
+ let { id, results } = msg.data;
+ if (id != this.id) {
+ return;
+ }
+ if (!this.callback) {
+ Cu.reportError("FormHistoryClient received message with no " +
+ "callback");
+ return;
+ }
+ this.callback(results);
+ this.clearListeners();
+ },
+
+ clearListeners() {
+ this.mm.removeMessageListener("FormHistory:AutoCompleteSearchResults",
+ this);
+ this.callback = null;
+ },
+};
+
+FormHistoryClient.nextRequestID = 1;
+
+
function FormAutoComplete() {
this.init();
}
/**
* FormAutoComplete
*
* Implements the nsIFormAutoComplete interface in the main process.
@@ -44,22 +177,22 @@ FormAutoComplete.prototype = {
_agedWeight : 2,
_bucketSize : 1,
_maxTimeGroupings : 25,
_timeGroupingSize : 7 * 24 * 60 * 60 * 1000 * 1000,
_expireDays : null,
_boundaryWeight : 25,
_prefixWeight : 5,
- // Only one query is performed at a time, which will be stored in _pendingQuery
- // while the query is being performed. It will be cleared when the query finishes,
- // is cancelled, or an error occurs. If a new query occurs while one is already
- // pending, the existing one is cancelled. The pending query will be an
- // mozIStoragePendingStatement object.
- _pendingQuery : null,
+ // Only one query via FormHistoryClient is performed at a time, and the
+ // most recent FormHistoryClient which will be stored in _pendingClient
+ // while the query is being performed. It will be cleared when the query
+ // finishes, is cancelled, or an error occurs. If a new query occurs while
+ // one is already pending, the existing one is cancelled.
+ _pendingClient : null,
init : function() {
// Preferences. Add observer so we get notified of changes.
this._prefBranch = Services.prefs.getBranch("browser.formfill.");
this._prefBranch.addObserver("", this.observer, true);
this.observer._self = this;
this._debug = this._prefBranch.getBoolPref("debug");
@@ -158,19 +291,21 @@ FormAutoComplete.prototype = {
// Guard against void DOM strings filtering into this code.
if (typeof aInputName === "object") {
aInputName = "";
}
if (typeof aUntrimmedSearchString === "object") {
aUntrimmedSearchString = "";
}
+ let client = new FormHistoryClient({ formField: aField, inputName: aInputName });
+
// If we have datalist results, they become our "empty" result.
let emptyResult = aDatalistResult ||
- new FormAutoCompleteResult(FormHistory, [],
+ new FormAutoCompleteResult(client, [],
aInputName,
aUntrimmedSearchString,
null);
if (!this._enabled) {
if (aListener) {
aListener.onSearchCompletion(emptyResult);
}
return;
@@ -275,17 +410,17 @@ FormAutoComplete.prototype = {
if (aListener) {
aListener.onSearchCompletion(result);
}
} else {
this.log("Creating new autocomplete search result.");
// Start with an empty list.
let result = aDatalistResult ?
- new FormAutoCompleteResult(FormHistory, [], aInputName, aUntrimmedSearchString, null) :
+ new FormAutoCompleteResult(client, [], aInputName, aUntrimmedSearchString, null) :
emptyResult;
let processEntry = (aEntries) => {
if (aField && aField.maxLength > -1) {
result.entries =
aEntries.filter(function (el) { return el.text.length <= aField.maxLength; });
} else {
result.entries = aEntries;
@@ -295,17 +430,17 @@ FormAutoComplete.prototype = {
result = this.mergeResults(result, aDatalistResult);
}
if (aListener) {
aListener.onSearchCompletion(result);
}
}
- this.getAutoCompleteValues(aInputName, searchString, processEntry);
+ this.getAutoCompleteValues(client, aInputName, searchString, processEntry);
}
},
mergeResults(historyResult, datalistResult) {
let values = datalistResult.wrappedJSObject._values;
let labels = datalistResult.wrappedJSObject._labels;
let comments = new Array(values.length).fill("");
@@ -335,68 +470,50 @@ FormAutoComplete.prototype = {
let {FormAutoCompleteResult} = Cu.import("resource://gre/modules/nsFormAutoCompleteResult.jsm", {});
return new FormAutoCompleteResult(datalistResult.searchString,
Ci.nsIAutoCompleteResult.RESULT_SUCCESS,
0, "", finalValues, finalLabels,
finalComments, historyResult);
},
stopAutoCompleteSearch : function () {
- if (this._pendingQuery) {
- this._pendingQuery.cancel();
- this._pendingQuery = null;
+ if (this._pendingClient) {
+ this._pendingClient.cancel();
+ this._pendingClient = null;
}
},
/*
* Get the values for an autocomplete list given a search string.
*
+ * client - a FormHistoryClient instance to perform the search with
* fieldName - fieldname field within form history (the form input name)
* searchString - string to search for
* callback - called when the values are available. Passed an array of objects,
* containing properties for each result. The callback is only called
* when successful.
*/
- getAutoCompleteValues : function (fieldName, searchString, callback) {
+ getAutoCompleteValues : function (client, fieldName, searchString, callback) {
let params = {
agedWeight: this._agedWeight,
bucketSize: this._bucketSize,
expiryDate: 1000 * (Date.now() - this._expireDays * 24 * 60 * 60 * 1000),
fieldname: fieldName,
maxTimeGroupings: this._maxTimeGroupings,
timeGroupingSize: this._timeGroupingSize,
prefixWeight: this._prefixWeight,
boundaryWeight: this._boundaryWeight
}
this.stopAutoCompleteSearch();
-
- let results = [];
- let processResults = {
- handleResult: aResult => {
- results.push(aResult);
- },
- handleError: aError => {
- this.log("getAutocompleteValues failed: " + aError.message);
- },
- handleCompletion: aReason => {
- // Check that the current query is still the one we created. Our
- // query might have been canceled shortly before completing, in
- // that case we don't want to call the callback anymore.
- if (query == this._pendingQuery) {
- this._pendingQuery = null;
- if (!aReason) {
- callback(results);
- }
- }
- }
- };
-
- let query = FormHistory.getAutoCompleteResults(searchString, params, processResults);
- this._pendingQuery = query;
+ client.requestAutoCompleteResults(searchString, params, (entries) => {
+ this._pendingClient = null;
+ callback(entries);
+ });
+ this._pendingClient = client;
},
/*
* _calculateScore
*
* entry -- an nsIAutoCompleteResult entry
* aSearchString -- current value of the input (lowercase)
* searchTokens -- array of tokens of the search string
@@ -416,168 +533,35 @@ FormAutoComplete.prototype = {
boundaryCalc += this._prefixWeight *
(entry.textLowerCase.
indexOf(aSearchString) == 0);
entry.totalScore = Math.round(entry.frecency * Math.max(1, boundaryCalc));
}
}; // end of FormAutoComplete implementation
-/**
- * FormAutoCompleteChild
- *
- * Implements the nsIFormAutoComplete interface in a child content process,
- * and forwards the auto-complete requests to the parent process which
- * also implements a nsIFormAutoComplete interface and has
- * direct access to the FormHistory database.
- */
-function FormAutoCompleteChild() {
- this.init();
-}
-
-FormAutoCompleteChild.prototype = {
- classID : Components.ID("{c11c21b2-71c9-4f87-a0f8-5e13f50495fd}"),
- QueryInterface : XPCOMUtils.generateQI([Ci.nsIFormAutoComplete, Ci.nsISupportsWeakReference]),
-
- _debug: false,
- _enabled: true,
- _pendingSearch: null,
-
- /*
- * init
- *
- * Initializes the content-process side of the FormAutoComplete component,
- * and add a listener for the message that the parent process sends when
- * a result is produced.
- */
- init: function() {
- this._debug = Services.prefs.getBoolPref("browser.formfill.debug");
- this._enabled = Services.prefs.getBoolPref("browser.formfill.enable");
- this.log("init");
- },
-
- /*
- * log
- *
- * Internal function for logging debug messages
- */
- log : function (message) {
- if (!this._debug)
- return;
- dump("FormAutoCompleteChild: " + message + "\n");
- },
-
- autoCompleteSearchAsync : function (aInputName, aUntrimmedSearchString,
- aField, aPreviousResult, aDatalistResult,
- aListener) {
- this.log("autoCompleteSearchAsync");
-
- if (this._pendingSearch) {
- this.stopAutoCompleteSearch();
- }
-
- let window = aField.ownerDocument.defaultView;
-
- let rect = BrowserUtils.getElementBoundingScreenRect(aField);
- let direction = window.getComputedStyle(aField).direction;
- let mockField = {};
- if (isAutocompleteDisabled(aField))
- mockField.autocomplete = "off";
- if (aField.maxLength > -1)
- mockField.maxLength = aField.maxLength;
-
- let datalistResult = aDatalistResult ?
- { values: aDatalistResult.wrappedJSObject._values,
- labels: aDatalistResult.wrappedJSObject._labels} :
- null;
-
- let topLevelDocshell = window.QueryInterface(Ci.nsIInterfaceRequestor)
- .getInterface(Ci.nsIDocShell)
- .sameTypeRootTreeItem
- .QueryInterface(Ci.nsIDocShell);
-
- let mm = topLevelDocshell.QueryInterface(Ci.nsIInterfaceRequestor)
- .getInterface(Ci.nsIContentFrameMessageManager);
-
- mm.sendAsyncMessage("FormHistory:AutoCompleteSearchAsync", {
- inputName: aInputName,
- untrimmedSearchString: aUntrimmedSearchString,
- mockField: mockField,
- datalistResult: datalistResult,
- previousSearchString: aPreviousResult && aPreviousResult.searchString.trim().toLowerCase(),
- left: rect.left,
- top: rect.top,
- width: rect.width,
- height: rect.height,
- direction: direction,
- });
-
- let search = this._pendingSearch = {};
- let searchFinished = message => {
- mm.removeMessageListener("FormAutoComplete:AutoCompleteSearchAsyncResult", searchFinished);
-
- // Check whether stopAutoCompleteSearch() was called, i.e. the search
- // was cancelled, while waiting for a result.
- if (search != this._pendingSearch) {
- return;
- }
- this._pendingSearch = null;
-
- let result = new FormAutoCompleteResult(
- null,
- Array.from(message.data.results, res => ({ text: res })),
- null,
- aUntrimmedSearchString,
- mm
- );
- if (aListener) {
- aListener.onSearchCompletion(result);
- }
- }
-
- mm.addMessageListener("FormAutoComplete:AutoCompleteSearchAsyncResult", searchFinished);
- this.log("autoCompleteSearchAsync message was sent");
- },
-
- stopAutoCompleteSearch : function () {
- this.log("stopAutoCompleteSearch");
- this._pendingSearch = null;
- },
-
- stopControllingInput(aField) {
- let window = aField.ownerDocument.defaultView;
- let topLevelDocshell = window.QueryInterface(Ci.nsIInterfaceRequestor)
- .getInterface(Ci.nsIDocShell)
- .sameTypeRootTreeItem
- .QueryInterface(Ci.nsIDocShell);
- let mm = topLevelDocshell.QueryInterface(Ci.nsIInterfaceRequestor)
- .getInterface(Ci.nsIContentFrameMessageManager);
- mm.sendAsyncMessage("FormAutoComplete:Disconnect");
- }
-}; // end of FormAutoCompleteChild implementation
-
// nsIAutoCompleteResult implementation
-function FormAutoCompleteResult(formHistory,
+function FormAutoCompleteResult(client,
entries,
fieldName,
searchString,
messageManager) {
- this.formHistory = formHistory;
+ this.client = client;
this.entries = entries;
this.fieldName = fieldName;
this.searchString = searchString;
this.messageManager = messageManager;
}
FormAutoCompleteResult.prototype = {
QueryInterface : XPCOMUtils.generateQI([Ci.nsIAutoCompleteResult,
Ci.nsISupportsWeakReference]),
// private
- formHistory : null,
+ client : null,
entries : null,
fieldName : null,
_checkIndexBounds : function (index) {
if (index < 0 || index >= this.entries.length)
throw Components.Exception("Index out of range.", Cr.NS_ERROR_ILLEGAL_VALUE);
},
@@ -633,31 +617,14 @@ FormAutoCompleteResult.prototype = {
},
removeValueAt : function (index, removeFromDB) {
this._checkIndexBounds(index);
let [removedEntry] = this.entries.splice(index, 1);
if (removeFromDB) {
- if (this.formHistory) {
- this.formHistory.update({ op: "remove",
- fieldname: this.fieldName,
- value: removedEntry.text });
- } else {
- this.messageManager.sendAsyncMessage("FormAutoComplete:RemoveEntry",
- { index });
- }
+ this.client.remove(removedEntry.text);
}
}
};
-
-if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT &&
- Services.prefs.getBoolPref("browser.tabs.remote.desktopbehavior", false)) {
- // Register the stub FormAutoComplete module in the child which will
- // forward messages to the parent through the process message manager.
- let component = [FormAutoCompleteChild];
- this.NSGetFactory = XPCOMUtils.generateNSGetFactory(component);
-} else {
- let component = [FormAutoComplete];
- this.NSGetFactory = XPCOMUtils.generateNSGetFactory(component);
-}
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([FormAutoComplete]);