michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: michael@0: const Cc = Components.classes; michael@0: const Ci = Components.interfaces; michael@0: const Cr = Components.results; michael@0: michael@0: Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: Components.utils.import("resource://gre/modules/Services.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils", michael@0: "resource://gre/modules/BrowserUtils.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "Deprecated", michael@0: "resource://gre/modules/Deprecated.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "FormHistory", michael@0: "resource://gre/modules/FormHistory.jsm"); michael@0: michael@0: function FormAutoComplete() { michael@0: this.init(); michael@0: } michael@0: michael@0: /** michael@0: * FormAutoComplete michael@0: * michael@0: * Implements the nsIFormAutoComplete interface in the main process. michael@0: */ michael@0: FormAutoComplete.prototype = { michael@0: classID : Components.ID("{c11c21b2-71c9-4f87-a0f8-5e13f50495fd}"), michael@0: QueryInterface : XPCOMUtils.generateQI([Ci.nsIFormAutoComplete, Ci.nsISupportsWeakReference]), michael@0: michael@0: _prefBranch : null, michael@0: _debug : true, // mirrors browser.formfill.debug michael@0: _enabled : true, // mirrors browser.formfill.enable preference michael@0: _agedWeight : 2, michael@0: _bucketSize : 1, michael@0: _maxTimeGroupings : 25, michael@0: _timeGroupingSize : 7 * 24 * 60 * 60 * 1000 * 1000, michael@0: _expireDays : null, michael@0: _boundaryWeight : 25, michael@0: _prefixWeight : 5, michael@0: michael@0: // Only one query is performed at a time, which will be stored in _pendingQuery michael@0: // while the query is being performed. It will be cleared when the query finishes, michael@0: // is cancelled, or an error occurs. If a new query occurs while one is already michael@0: // pending, the existing one is cancelled. The pending query will be an michael@0: // mozIStoragePendingStatement object. michael@0: _pendingQuery : null, michael@0: michael@0: init : function() { michael@0: // Preferences. Add observer so we get notified of changes. michael@0: this._prefBranch = Services.prefs.getBranch("browser.formfill."); michael@0: this._prefBranch.addObserver("", this.observer, true); michael@0: this.observer._self = this; michael@0: michael@0: this._debug = this._prefBranch.getBoolPref("debug"); michael@0: this._enabled = this._prefBranch.getBoolPref("enable"); michael@0: this._agedWeight = this._prefBranch.getIntPref("agedWeight"); michael@0: this._bucketSize = this._prefBranch.getIntPref("bucketSize"); michael@0: this._maxTimeGroupings = this._prefBranch.getIntPref("maxTimeGroupings"); michael@0: this._timeGroupingSize = this._prefBranch.getIntPref("timeGroupingSize") * 1000 * 1000; michael@0: this._expireDays = this._prefBranch.getIntPref("expire_days"); michael@0: }, michael@0: michael@0: observer : { michael@0: _self : null, michael@0: michael@0: QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver, michael@0: Ci.nsISupportsWeakReference]), michael@0: michael@0: observe : function (subject, topic, data) { michael@0: let self = this._self; michael@0: if (topic == "nsPref:changed") { michael@0: let prefName = data; michael@0: self.log("got change to " + prefName + " preference"); michael@0: michael@0: switch (prefName) { michael@0: case "agedWeight": michael@0: self._agedWeight = self._prefBranch.getIntPref(prefName); michael@0: break; michael@0: case "debug": michael@0: self._debug = self._prefBranch.getBoolPref(prefName); michael@0: break; michael@0: case "enable": michael@0: self._enabled = self._prefBranch.getBoolPref(prefName); michael@0: break; michael@0: case "maxTimeGroupings": michael@0: self._maxTimeGroupings = self._prefBranch.getIntPref(prefName); michael@0: break; michael@0: case "timeGroupingSize": michael@0: self._timeGroupingSize = self._prefBranch.getIntPref(prefName) * 1000 * 1000; michael@0: break; michael@0: case "bucketSize": michael@0: self._bucketSize = self._prefBranch.getIntPref(prefName); michael@0: break; michael@0: case "boundaryWeight": michael@0: self._boundaryWeight = self._prefBranch.getIntPref(prefName); michael@0: break; michael@0: case "prefixWeight": michael@0: self._prefixWeight = self._prefBranch.getIntPref(prefName); michael@0: break; michael@0: default: michael@0: self.log("Oops! Pref not handled, change ignored."); michael@0: } michael@0: } michael@0: } michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * log michael@0: * michael@0: * Internal function for logging debug messages to the Error Console michael@0: * window michael@0: */ michael@0: log : function (message) { michael@0: if (!this._debug) michael@0: return; michael@0: dump("FormAutoComplete: " + message + "\n"); michael@0: Services.console.logStringMessage("FormAutoComplete: " + message); michael@0: }, michael@0: michael@0: michael@0: autoCompleteSearch : function (aInputName, aUntrimmedSearchString, aField, aPreviousResult) { michael@0: Deprecated.warning("nsIFormAutoComplete::autoCompleteSearch is deprecated", "https://bugzilla.mozilla.org/show_bug.cgi?id=697377"); michael@0: michael@0: let result = null; michael@0: let listener = { michael@0: onSearchCompletion: function (r) result = r michael@0: }; michael@0: this._autoCompleteSearchShared(aInputName, aUntrimmedSearchString, aField, aPreviousResult, listener); michael@0: michael@0: // Just wait for the result to to be available. michael@0: let thread = Components.classes["@mozilla.org/thread-manager;1"].getService().currentThread; michael@0: while (!result && this._pendingQuery) { michael@0: thread.processNextEvent(true); michael@0: } michael@0: michael@0: return result; michael@0: }, michael@0: michael@0: autoCompleteSearchAsync : function (aInputName, aUntrimmedSearchString, aField, aPreviousResult, aListener) { michael@0: this._autoCompleteSearchShared(aInputName, aUntrimmedSearchString, aField, aPreviousResult, aListener); michael@0: }, michael@0: michael@0: /* michael@0: * autoCompleteSearchShared michael@0: * michael@0: * aInputName -- |name| attribute from the form input being autocompleted. michael@0: * aUntrimmedSearchString -- current value of the input michael@0: * aField -- nsIDOMHTMLInputElement being autocompleted (may be null if from chrome) michael@0: * aPreviousResult -- previous search result, if any. michael@0: * aListener -- nsIFormAutoCompleteObserver that listens for the nsIAutoCompleteResult michael@0: * that may be returned asynchronously. michael@0: */ michael@0: _autoCompleteSearchShared : function (aInputName, aUntrimmedSearchString, aField, aPreviousResult, aListener) { michael@0: function sortBytotalScore (a, b) { michael@0: return b.totalScore - a.totalScore; michael@0: } michael@0: michael@0: let result = null; michael@0: if (!this._enabled) { michael@0: result = new FormAutoCompleteResult(FormHistory, [], aInputName, aUntrimmedSearchString); michael@0: if (aListener) { michael@0: aListener.onSearchCompletion(result); michael@0: } michael@0: return; michael@0: } michael@0: michael@0: // don't allow form inputs (aField != null) to get results from search bar history michael@0: if (aInputName == 'searchbar-history' && aField) { michael@0: this.log('autoCompleteSearch for input name "' + aInputName + '" is denied'); michael@0: result = new FormAutoCompleteResult(FormHistory, [], aInputName, aUntrimmedSearchString); michael@0: if (aListener) { michael@0: aListener.onSearchCompletion(result); michael@0: } michael@0: return; michael@0: } michael@0: michael@0: this.log("AutoCompleteSearch invoked. Search is: " + aUntrimmedSearchString); michael@0: let searchString = aUntrimmedSearchString.trim().toLowerCase(); michael@0: michael@0: // reuse previous results if: michael@0: // a) length greater than one character (others searches are special cases) AND michael@0: // b) the the new results will be a subset of the previous results michael@0: if (aPreviousResult && aPreviousResult.searchString.trim().length > 1 && michael@0: searchString.indexOf(aPreviousResult.searchString.trim().toLowerCase()) >= 0) { michael@0: this.log("Using previous autocomplete result"); michael@0: result = aPreviousResult; michael@0: result.wrappedJSObject.searchString = aUntrimmedSearchString; michael@0: michael@0: let searchTokens = searchString.split(/\s+/); michael@0: // We have a list of results for a shorter search string, so just michael@0: // filter them further based on the new search string and add to a new array. michael@0: let entries = result.wrappedJSObject.entries; michael@0: let filteredEntries = []; michael@0: for (let i = 0; i < entries.length; i++) { michael@0: let entry = entries[i]; michael@0: // Remove results that do not contain the token michael@0: // XXX bug 394604 -- .toLowerCase can be wrong for some intl chars michael@0: if(searchTokens.some(function (tok) entry.textLowerCase.indexOf(tok) < 0)) michael@0: continue; michael@0: this._calculateScore(entry, searchString, searchTokens); michael@0: this.log("Reusing autocomplete entry '" + entry.text + michael@0: "' (" + entry.frecency +" / " + entry.totalScore + ")"); michael@0: filteredEntries.push(entry); michael@0: } michael@0: filteredEntries.sort(sortBytotalScore); michael@0: result.wrappedJSObject.entries = filteredEntries; michael@0: michael@0: if (aListener) { michael@0: aListener.onSearchCompletion(result); michael@0: } michael@0: } else { michael@0: this.log("Creating new autocomplete search result."); michael@0: michael@0: // Start with an empty list. michael@0: result = new FormAutoCompleteResult(FormHistory, [], aInputName, aUntrimmedSearchString); michael@0: michael@0: let processEntry = function(aEntries) { michael@0: if (aField && aField.maxLength > -1) { michael@0: result.entries = michael@0: aEntries.filter(function (el) { return el.text.length <= aField.maxLength; }); michael@0: } else { michael@0: result.entries = aEntries; michael@0: } michael@0: michael@0: if (aListener) { michael@0: aListener.onSearchCompletion(result); michael@0: } michael@0: } michael@0: michael@0: this.getAutoCompleteValues(aInputName, searchString, processEntry); michael@0: } michael@0: }, michael@0: michael@0: stopAutoCompleteSearch : function () { michael@0: if (this._pendingQuery) { michael@0: this._pendingQuery.cancel(); michael@0: this._pendingQuery = null; michael@0: } michael@0: }, michael@0: michael@0: /* michael@0: * Get the values for an autocomplete list given a search string. michael@0: * michael@0: * fieldName - fieldname field within form history (the form input name) michael@0: * searchString - string to search for michael@0: * callback - called when the values are available. Passed an array of objects, michael@0: * containing properties for each result. The callback is only called michael@0: * when successful. michael@0: */ michael@0: getAutoCompleteValues : function (fieldName, searchString, callback) { michael@0: let params = { michael@0: agedWeight: this._agedWeight, michael@0: bucketSize: this._bucketSize, michael@0: expiryDate: 1000 * (Date.now() - this._expireDays * 24 * 60 * 60 * 1000), michael@0: fieldname: fieldName, michael@0: maxTimeGroupings: this._maxTimeGroupings, michael@0: timeGroupingSize: this._timeGroupingSize, michael@0: prefixWeight: this._prefixWeight, michael@0: boundaryWeight: this._boundaryWeight michael@0: } michael@0: michael@0: this.stopAutoCompleteSearch(); michael@0: michael@0: let results = []; michael@0: let processResults = { michael@0: handleResult: aResult => { michael@0: results.push(aResult); michael@0: }, michael@0: handleError: aError => { michael@0: this.log("getAutocompleteValues failed: " + aError.message); michael@0: }, michael@0: handleCompletion: aReason => { michael@0: this._pendingQuery = null; michael@0: if (!aReason) { michael@0: callback(results); michael@0: } michael@0: } michael@0: }; michael@0: michael@0: this._pendingQuery = FormHistory.getAutoCompleteResults(searchString, params, processResults); michael@0: }, michael@0: michael@0: /* michael@0: * _calculateScore michael@0: * michael@0: * entry -- an nsIAutoCompleteResult entry michael@0: * aSearchString -- current value of the input (lowercase) michael@0: * searchTokens -- array of tokens of the search string michael@0: * michael@0: * Returns: an int michael@0: */ michael@0: _calculateScore : function (entry, aSearchString, searchTokens) { michael@0: let boundaryCalc = 0; michael@0: // for each word, calculate word boundary weights michael@0: for each (let token in searchTokens) { michael@0: boundaryCalc += (entry.textLowerCase.indexOf(token) == 0); michael@0: boundaryCalc += (entry.textLowerCase.indexOf(" " + token) >= 0); michael@0: } michael@0: boundaryCalc = boundaryCalc * this._boundaryWeight; michael@0: // now add more weight if we have a traditional prefix match and michael@0: // multiply boundary bonuses by boundary weight michael@0: boundaryCalc += this._prefixWeight * michael@0: (entry.textLowerCase. michael@0: indexOf(aSearchString) == 0); michael@0: entry.totalScore = Math.round(entry.frecency * Math.max(1, boundaryCalc)); michael@0: } michael@0: michael@0: }; // end of FormAutoComplete implementation michael@0: michael@0: /** michael@0: * FormAutoCompleteChild michael@0: * michael@0: * Implements the nsIFormAutoComplete interface in a child content process, michael@0: * and forwards the auto-complete requests to the parent process which michael@0: * also implements a nsIFormAutoComplete interface and has michael@0: * direct access to the FormHistory database. michael@0: */ michael@0: function FormAutoCompleteChild() { michael@0: this.init(); michael@0: } michael@0: michael@0: FormAutoCompleteChild.prototype = { michael@0: classID : Components.ID("{c11c21b2-71c9-4f87-a0f8-5e13f50495fd}"), michael@0: QueryInterface : XPCOMUtils.generateQI([Ci.nsIFormAutoComplete, Ci.nsISupportsWeakReference]), michael@0: michael@0: _debug: false, michael@0: _enabled: true, michael@0: michael@0: /* michael@0: * init michael@0: * michael@0: * Initializes the content-process side of the FormAutoComplete component, michael@0: * and add a listener for the message that the parent process sends when michael@0: * a result is produced. michael@0: */ michael@0: init: function() { michael@0: this._debug = Services.prefs.getBoolPref("browser.formfill.debug"); michael@0: this._enabled = Services.prefs.getBoolPref("browser.formfill.enable"); michael@0: this.log("init"); michael@0: }, michael@0: michael@0: /* michael@0: * log michael@0: * michael@0: * Internal function for logging debug messages michael@0: */ michael@0: log : function (message) { michael@0: if (!this._debug) michael@0: return; michael@0: dump("FormAutoCompleteChild: " + message + "\n"); michael@0: }, michael@0: michael@0: autoCompleteSearch : function (aInputName, aUntrimmedSearchString, aField, aPreviousResult) { michael@0: // This function is deprecated michael@0: }, michael@0: michael@0: autoCompleteSearchAsync : function (aInputName, aUntrimmedSearchString, aField, aPreviousResult, aListener) { michael@0: this.log("autoCompleteSearchAsync"); michael@0: michael@0: this._pendingListener = aListener; michael@0: michael@0: let rect = BrowserUtils.getElementBoundingScreenRect(aField); michael@0: michael@0: let window = aField.ownerDocument.defaultView; michael@0: let topLevelDocshell = window.QueryInterface(Ci.nsIInterfaceRequestor) michael@0: .getInterface(Ci.nsIDocShell) michael@0: .sameTypeRootTreeItem michael@0: .QueryInterface(Ci.nsIDocShell); michael@0: michael@0: let mm = topLevelDocshell.QueryInterface(Ci.nsIInterfaceRequestor) michael@0: .getInterface(Ci.nsIContentFrameMessageManager); michael@0: michael@0: mm.sendAsyncMessage("FormHistory:AutoCompleteSearchAsync", { michael@0: inputName: aInputName, michael@0: untrimmedSearchString: aUntrimmedSearchString, michael@0: left: rect.left, michael@0: top: rect.top, michael@0: width: rect.width, michael@0: height: rect.height michael@0: }); michael@0: michael@0: mm.addMessageListener("FormAutoComplete:AutoCompleteSearchAsyncResult", michael@0: function searchFinished(message) { michael@0: mm.removeMessageListener("FormAutoComplete:AutoCompleteSearchAsyncResult", searchFinished); michael@0: let result = new FormAutoCompleteResult( michael@0: null, michael@0: [{text: res} for (res of message.data.results)], michael@0: null, michael@0: null michael@0: ); michael@0: if (aListener) { michael@0: aListener.onSearchCompletion(result); michael@0: } michael@0: } michael@0: ); michael@0: michael@0: this.log("autoCompleteSearchAsync message was sent"); michael@0: }, michael@0: michael@0: stopAutoCompleteSearch : function () { michael@0: this.log("stopAutoCompleteSearch"); michael@0: }, michael@0: }; // end of FormAutoCompleteChild implementation michael@0: michael@0: // nsIAutoCompleteResult implementation michael@0: function FormAutoCompleteResult (formHistory, entries, fieldName, searchString) { michael@0: this.formHistory = formHistory; michael@0: this.entries = entries; michael@0: this.fieldName = fieldName; michael@0: this.searchString = searchString; michael@0: } michael@0: michael@0: FormAutoCompleteResult.prototype = { michael@0: QueryInterface : XPCOMUtils.generateQI([Ci.nsIAutoCompleteResult, michael@0: Ci.nsISupportsWeakReference]), michael@0: michael@0: // private michael@0: formHistory : null, michael@0: entries : null, michael@0: fieldName : null, michael@0: michael@0: _checkIndexBounds : function (index) { michael@0: if (index < 0 || index >= this.entries.length) michael@0: throw Components.Exception("Index out of range.", Cr.NS_ERROR_ILLEGAL_VALUE); michael@0: }, michael@0: michael@0: // Allow autoCompleteSearch to get at the JS object so it can michael@0: // modify some readonly properties for internal use. michael@0: get wrappedJSObject() { michael@0: return this; michael@0: }, michael@0: michael@0: // Interfaces from idl... michael@0: searchString : null, michael@0: errorDescription : "", michael@0: get defaultIndex() { michael@0: if (entries.length == 0) michael@0: return -1; michael@0: else michael@0: return 0; michael@0: }, michael@0: get searchResult() { michael@0: if (this.entries.length == 0) michael@0: return Ci.nsIAutoCompleteResult.RESULT_NOMATCH; michael@0: return Ci.nsIAutoCompleteResult.RESULT_SUCCESS; michael@0: }, michael@0: get matchCount() { michael@0: return this.entries.length; michael@0: }, michael@0: michael@0: getValueAt : function (index) { michael@0: this._checkIndexBounds(index); michael@0: return this.entries[index].text; michael@0: }, michael@0: michael@0: getLabelAt: function(index) { michael@0: return getValueAt(index); michael@0: }, michael@0: michael@0: getCommentAt : function (index) { michael@0: this._checkIndexBounds(index); michael@0: return ""; michael@0: }, michael@0: michael@0: getStyleAt : function (index) { michael@0: this._checkIndexBounds(index); michael@0: return ""; michael@0: }, michael@0: michael@0: getImageAt : function (index) { michael@0: this._checkIndexBounds(index); michael@0: return ""; michael@0: }, michael@0: michael@0: getFinalCompleteValueAt : function (index) { michael@0: return this.getValueAt(index); michael@0: }, michael@0: michael@0: removeValueAt : function (index, removeFromDB) { michael@0: this._checkIndexBounds(index); michael@0: michael@0: let [removedEntry] = this.entries.splice(index, 1); michael@0: michael@0: if (removeFromDB) { michael@0: this.formHistory.update({ op: "remove", michael@0: fieldname: this.fieldName, michael@0: value: removedEntry.text }); michael@0: } michael@0: } michael@0: }; michael@0: michael@0: michael@0: let remote = Services.appinfo.browserTabsRemote; michael@0: if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT && remote) { michael@0: // Register the stub FormAutoComplete module in the child which will michael@0: // forward messages to the parent through the process message manager. michael@0: let component = [FormAutoCompleteChild]; michael@0: this.NSGetFactory = XPCOMUtils.generateNSGetFactory(component); michael@0: } else { michael@0: let component = [FormAutoComplete]; michael@0: this.NSGetFactory = XPCOMUtils.generateNSGetFactory(component); michael@0: }