1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/toolkit/components/satchel/nsFormAutoComplete.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,506 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 + 1.9 +const Cc = Components.classes; 1.10 +const Ci = Components.interfaces; 1.11 +const Cr = Components.results; 1.12 + 1.13 +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); 1.14 +Components.utils.import("resource://gre/modules/Services.jsm"); 1.15 + 1.16 +XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils", 1.17 + "resource://gre/modules/BrowserUtils.jsm"); 1.18 +XPCOMUtils.defineLazyModuleGetter(this, "Deprecated", 1.19 + "resource://gre/modules/Deprecated.jsm"); 1.20 +XPCOMUtils.defineLazyModuleGetter(this, "FormHistory", 1.21 + "resource://gre/modules/FormHistory.jsm"); 1.22 + 1.23 +function FormAutoComplete() { 1.24 + this.init(); 1.25 +} 1.26 + 1.27 +/** 1.28 + * FormAutoComplete 1.29 + * 1.30 + * Implements the nsIFormAutoComplete interface in the main process. 1.31 + */ 1.32 +FormAutoComplete.prototype = { 1.33 + classID : Components.ID("{c11c21b2-71c9-4f87-a0f8-5e13f50495fd}"), 1.34 + QueryInterface : XPCOMUtils.generateQI([Ci.nsIFormAutoComplete, Ci.nsISupportsWeakReference]), 1.35 + 1.36 + _prefBranch : null, 1.37 + _debug : true, // mirrors browser.formfill.debug 1.38 + _enabled : true, // mirrors browser.formfill.enable preference 1.39 + _agedWeight : 2, 1.40 + _bucketSize : 1, 1.41 + _maxTimeGroupings : 25, 1.42 + _timeGroupingSize : 7 * 24 * 60 * 60 * 1000 * 1000, 1.43 + _expireDays : null, 1.44 + _boundaryWeight : 25, 1.45 + _prefixWeight : 5, 1.46 + 1.47 + // Only one query is performed at a time, which will be stored in _pendingQuery 1.48 + // while the query is being performed. It will be cleared when the query finishes, 1.49 + // is cancelled, or an error occurs. If a new query occurs while one is already 1.50 + // pending, the existing one is cancelled. The pending query will be an 1.51 + // mozIStoragePendingStatement object. 1.52 + _pendingQuery : null, 1.53 + 1.54 + init : function() { 1.55 + // Preferences. Add observer so we get notified of changes. 1.56 + this._prefBranch = Services.prefs.getBranch("browser.formfill."); 1.57 + this._prefBranch.addObserver("", this.observer, true); 1.58 + this.observer._self = this; 1.59 + 1.60 + this._debug = this._prefBranch.getBoolPref("debug"); 1.61 + this._enabled = this._prefBranch.getBoolPref("enable"); 1.62 + this._agedWeight = this._prefBranch.getIntPref("agedWeight"); 1.63 + this._bucketSize = this._prefBranch.getIntPref("bucketSize"); 1.64 + this._maxTimeGroupings = this._prefBranch.getIntPref("maxTimeGroupings"); 1.65 + this._timeGroupingSize = this._prefBranch.getIntPref("timeGroupingSize") * 1000 * 1000; 1.66 + this._expireDays = this._prefBranch.getIntPref("expire_days"); 1.67 + }, 1.68 + 1.69 + observer : { 1.70 + _self : null, 1.71 + 1.72 + QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver, 1.73 + Ci.nsISupportsWeakReference]), 1.74 + 1.75 + observe : function (subject, topic, data) { 1.76 + let self = this._self; 1.77 + if (topic == "nsPref:changed") { 1.78 + let prefName = data; 1.79 + self.log("got change to " + prefName + " preference"); 1.80 + 1.81 + switch (prefName) { 1.82 + case "agedWeight": 1.83 + self._agedWeight = self._prefBranch.getIntPref(prefName); 1.84 + break; 1.85 + case "debug": 1.86 + self._debug = self._prefBranch.getBoolPref(prefName); 1.87 + break; 1.88 + case "enable": 1.89 + self._enabled = self._prefBranch.getBoolPref(prefName); 1.90 + break; 1.91 + case "maxTimeGroupings": 1.92 + self._maxTimeGroupings = self._prefBranch.getIntPref(prefName); 1.93 + break; 1.94 + case "timeGroupingSize": 1.95 + self._timeGroupingSize = self._prefBranch.getIntPref(prefName) * 1000 * 1000; 1.96 + break; 1.97 + case "bucketSize": 1.98 + self._bucketSize = self._prefBranch.getIntPref(prefName); 1.99 + break; 1.100 + case "boundaryWeight": 1.101 + self._boundaryWeight = self._prefBranch.getIntPref(prefName); 1.102 + break; 1.103 + case "prefixWeight": 1.104 + self._prefixWeight = self._prefBranch.getIntPref(prefName); 1.105 + break; 1.106 + default: 1.107 + self.log("Oops! Pref not handled, change ignored."); 1.108 + } 1.109 + } 1.110 + } 1.111 + }, 1.112 + 1.113 + 1.114 + /* 1.115 + * log 1.116 + * 1.117 + * Internal function for logging debug messages to the Error Console 1.118 + * window 1.119 + */ 1.120 + log : function (message) { 1.121 + if (!this._debug) 1.122 + return; 1.123 + dump("FormAutoComplete: " + message + "\n"); 1.124 + Services.console.logStringMessage("FormAutoComplete: " + message); 1.125 + }, 1.126 + 1.127 + 1.128 + autoCompleteSearch : function (aInputName, aUntrimmedSearchString, aField, aPreviousResult) { 1.129 + Deprecated.warning("nsIFormAutoComplete::autoCompleteSearch is deprecated", "https://bugzilla.mozilla.org/show_bug.cgi?id=697377"); 1.130 + 1.131 + let result = null; 1.132 + let listener = { 1.133 + onSearchCompletion: function (r) result = r 1.134 + }; 1.135 + this._autoCompleteSearchShared(aInputName, aUntrimmedSearchString, aField, aPreviousResult, listener); 1.136 + 1.137 + // Just wait for the result to to be available. 1.138 + let thread = Components.classes["@mozilla.org/thread-manager;1"].getService().currentThread; 1.139 + while (!result && this._pendingQuery) { 1.140 + thread.processNextEvent(true); 1.141 + } 1.142 + 1.143 + return result; 1.144 + }, 1.145 + 1.146 + autoCompleteSearchAsync : function (aInputName, aUntrimmedSearchString, aField, aPreviousResult, aListener) { 1.147 + this._autoCompleteSearchShared(aInputName, aUntrimmedSearchString, aField, aPreviousResult, aListener); 1.148 + }, 1.149 + 1.150 + /* 1.151 + * autoCompleteSearchShared 1.152 + * 1.153 + * aInputName -- |name| attribute from the form input being autocompleted. 1.154 + * aUntrimmedSearchString -- current value of the input 1.155 + * aField -- nsIDOMHTMLInputElement being autocompleted (may be null if from chrome) 1.156 + * aPreviousResult -- previous search result, if any. 1.157 + * aListener -- nsIFormAutoCompleteObserver that listens for the nsIAutoCompleteResult 1.158 + * that may be returned asynchronously. 1.159 + */ 1.160 + _autoCompleteSearchShared : function (aInputName, aUntrimmedSearchString, aField, aPreviousResult, aListener) { 1.161 + function sortBytotalScore (a, b) { 1.162 + return b.totalScore - a.totalScore; 1.163 + } 1.164 + 1.165 + let result = null; 1.166 + if (!this._enabled) { 1.167 + result = new FormAutoCompleteResult(FormHistory, [], aInputName, aUntrimmedSearchString); 1.168 + if (aListener) { 1.169 + aListener.onSearchCompletion(result); 1.170 + } 1.171 + return; 1.172 + } 1.173 + 1.174 + // don't allow form inputs (aField != null) to get results from search bar history 1.175 + if (aInputName == 'searchbar-history' && aField) { 1.176 + this.log('autoCompleteSearch for input name "' + aInputName + '" is denied'); 1.177 + result = new FormAutoCompleteResult(FormHistory, [], aInputName, aUntrimmedSearchString); 1.178 + if (aListener) { 1.179 + aListener.onSearchCompletion(result); 1.180 + } 1.181 + return; 1.182 + } 1.183 + 1.184 + this.log("AutoCompleteSearch invoked. Search is: " + aUntrimmedSearchString); 1.185 + let searchString = aUntrimmedSearchString.trim().toLowerCase(); 1.186 + 1.187 + // reuse previous results if: 1.188 + // a) length greater than one character (others searches are special cases) AND 1.189 + // b) the the new results will be a subset of the previous results 1.190 + if (aPreviousResult && aPreviousResult.searchString.trim().length > 1 && 1.191 + searchString.indexOf(aPreviousResult.searchString.trim().toLowerCase()) >= 0) { 1.192 + this.log("Using previous autocomplete result"); 1.193 + result = aPreviousResult; 1.194 + result.wrappedJSObject.searchString = aUntrimmedSearchString; 1.195 + 1.196 + let searchTokens = searchString.split(/\s+/); 1.197 + // We have a list of results for a shorter search string, so just 1.198 + // filter them further based on the new search string and add to a new array. 1.199 + let entries = result.wrappedJSObject.entries; 1.200 + let filteredEntries = []; 1.201 + for (let i = 0; i < entries.length; i++) { 1.202 + let entry = entries[i]; 1.203 + // Remove results that do not contain the token 1.204 + // XXX bug 394604 -- .toLowerCase can be wrong for some intl chars 1.205 + if(searchTokens.some(function (tok) entry.textLowerCase.indexOf(tok) < 0)) 1.206 + continue; 1.207 + this._calculateScore(entry, searchString, searchTokens); 1.208 + this.log("Reusing autocomplete entry '" + entry.text + 1.209 + "' (" + entry.frecency +" / " + entry.totalScore + ")"); 1.210 + filteredEntries.push(entry); 1.211 + } 1.212 + filteredEntries.sort(sortBytotalScore); 1.213 + result.wrappedJSObject.entries = filteredEntries; 1.214 + 1.215 + if (aListener) { 1.216 + aListener.onSearchCompletion(result); 1.217 + } 1.218 + } else { 1.219 + this.log("Creating new autocomplete search result."); 1.220 + 1.221 + // Start with an empty list. 1.222 + result = new FormAutoCompleteResult(FormHistory, [], aInputName, aUntrimmedSearchString); 1.223 + 1.224 + let processEntry = function(aEntries) { 1.225 + if (aField && aField.maxLength > -1) { 1.226 + result.entries = 1.227 + aEntries.filter(function (el) { return el.text.length <= aField.maxLength; }); 1.228 + } else { 1.229 + result.entries = aEntries; 1.230 + } 1.231 + 1.232 + if (aListener) { 1.233 + aListener.onSearchCompletion(result); 1.234 + } 1.235 + } 1.236 + 1.237 + this.getAutoCompleteValues(aInputName, searchString, processEntry); 1.238 + } 1.239 + }, 1.240 + 1.241 + stopAutoCompleteSearch : function () { 1.242 + if (this._pendingQuery) { 1.243 + this._pendingQuery.cancel(); 1.244 + this._pendingQuery = null; 1.245 + } 1.246 + }, 1.247 + 1.248 + /* 1.249 + * Get the values for an autocomplete list given a search string. 1.250 + * 1.251 + * fieldName - fieldname field within form history (the form input name) 1.252 + * searchString - string to search for 1.253 + * callback - called when the values are available. Passed an array of objects, 1.254 + * containing properties for each result. The callback is only called 1.255 + * when successful. 1.256 + */ 1.257 + getAutoCompleteValues : function (fieldName, searchString, callback) { 1.258 + let params = { 1.259 + agedWeight: this._agedWeight, 1.260 + bucketSize: this._bucketSize, 1.261 + expiryDate: 1000 * (Date.now() - this._expireDays * 24 * 60 * 60 * 1000), 1.262 + fieldname: fieldName, 1.263 + maxTimeGroupings: this._maxTimeGroupings, 1.264 + timeGroupingSize: this._timeGroupingSize, 1.265 + prefixWeight: this._prefixWeight, 1.266 + boundaryWeight: this._boundaryWeight 1.267 + } 1.268 + 1.269 + this.stopAutoCompleteSearch(); 1.270 + 1.271 + let results = []; 1.272 + let processResults = { 1.273 + handleResult: aResult => { 1.274 + results.push(aResult); 1.275 + }, 1.276 + handleError: aError => { 1.277 + this.log("getAutocompleteValues failed: " + aError.message); 1.278 + }, 1.279 + handleCompletion: aReason => { 1.280 + this._pendingQuery = null; 1.281 + if (!aReason) { 1.282 + callback(results); 1.283 + } 1.284 + } 1.285 + }; 1.286 + 1.287 + this._pendingQuery = FormHistory.getAutoCompleteResults(searchString, params, processResults); 1.288 + }, 1.289 + 1.290 + /* 1.291 + * _calculateScore 1.292 + * 1.293 + * entry -- an nsIAutoCompleteResult entry 1.294 + * aSearchString -- current value of the input (lowercase) 1.295 + * searchTokens -- array of tokens of the search string 1.296 + * 1.297 + * Returns: an int 1.298 + */ 1.299 + _calculateScore : function (entry, aSearchString, searchTokens) { 1.300 + let boundaryCalc = 0; 1.301 + // for each word, calculate word boundary weights 1.302 + for each (let token in searchTokens) { 1.303 + boundaryCalc += (entry.textLowerCase.indexOf(token) == 0); 1.304 + boundaryCalc += (entry.textLowerCase.indexOf(" " + token) >= 0); 1.305 + } 1.306 + boundaryCalc = boundaryCalc * this._boundaryWeight; 1.307 + // now add more weight if we have a traditional prefix match and 1.308 + // multiply boundary bonuses by boundary weight 1.309 + boundaryCalc += this._prefixWeight * 1.310 + (entry.textLowerCase. 1.311 + indexOf(aSearchString) == 0); 1.312 + entry.totalScore = Math.round(entry.frecency * Math.max(1, boundaryCalc)); 1.313 + } 1.314 + 1.315 +}; // end of FormAutoComplete implementation 1.316 + 1.317 +/** 1.318 + * FormAutoCompleteChild 1.319 + * 1.320 + * Implements the nsIFormAutoComplete interface in a child content process, 1.321 + * and forwards the auto-complete requests to the parent process which 1.322 + * also implements a nsIFormAutoComplete interface and has 1.323 + * direct access to the FormHistory database. 1.324 + */ 1.325 +function FormAutoCompleteChild() { 1.326 + this.init(); 1.327 +} 1.328 + 1.329 +FormAutoCompleteChild.prototype = { 1.330 + classID : Components.ID("{c11c21b2-71c9-4f87-a0f8-5e13f50495fd}"), 1.331 + QueryInterface : XPCOMUtils.generateQI([Ci.nsIFormAutoComplete, Ci.nsISupportsWeakReference]), 1.332 + 1.333 + _debug: false, 1.334 + _enabled: true, 1.335 + 1.336 + /* 1.337 + * init 1.338 + * 1.339 + * Initializes the content-process side of the FormAutoComplete component, 1.340 + * and add a listener for the message that the parent process sends when 1.341 + * a result is produced. 1.342 + */ 1.343 + init: function() { 1.344 + this._debug = Services.prefs.getBoolPref("browser.formfill.debug"); 1.345 + this._enabled = Services.prefs.getBoolPref("browser.formfill.enable"); 1.346 + this.log("init"); 1.347 + }, 1.348 + 1.349 + /* 1.350 + * log 1.351 + * 1.352 + * Internal function for logging debug messages 1.353 + */ 1.354 + log : function (message) { 1.355 + if (!this._debug) 1.356 + return; 1.357 + dump("FormAutoCompleteChild: " + message + "\n"); 1.358 + }, 1.359 + 1.360 + autoCompleteSearch : function (aInputName, aUntrimmedSearchString, aField, aPreviousResult) { 1.361 + // This function is deprecated 1.362 + }, 1.363 + 1.364 + autoCompleteSearchAsync : function (aInputName, aUntrimmedSearchString, aField, aPreviousResult, aListener) { 1.365 + this.log("autoCompleteSearchAsync"); 1.366 + 1.367 + this._pendingListener = aListener; 1.368 + 1.369 + let rect = BrowserUtils.getElementBoundingScreenRect(aField); 1.370 + 1.371 + let window = aField.ownerDocument.defaultView; 1.372 + let topLevelDocshell = window.QueryInterface(Ci.nsIInterfaceRequestor) 1.373 + .getInterface(Ci.nsIDocShell) 1.374 + .sameTypeRootTreeItem 1.375 + .QueryInterface(Ci.nsIDocShell); 1.376 + 1.377 + let mm = topLevelDocshell.QueryInterface(Ci.nsIInterfaceRequestor) 1.378 + .getInterface(Ci.nsIContentFrameMessageManager); 1.379 + 1.380 + mm.sendAsyncMessage("FormHistory:AutoCompleteSearchAsync", { 1.381 + inputName: aInputName, 1.382 + untrimmedSearchString: aUntrimmedSearchString, 1.383 + left: rect.left, 1.384 + top: rect.top, 1.385 + width: rect.width, 1.386 + height: rect.height 1.387 + }); 1.388 + 1.389 + mm.addMessageListener("FormAutoComplete:AutoCompleteSearchAsyncResult", 1.390 + function searchFinished(message) { 1.391 + mm.removeMessageListener("FormAutoComplete:AutoCompleteSearchAsyncResult", searchFinished); 1.392 + let result = new FormAutoCompleteResult( 1.393 + null, 1.394 + [{text: res} for (res of message.data.results)], 1.395 + null, 1.396 + null 1.397 + ); 1.398 + if (aListener) { 1.399 + aListener.onSearchCompletion(result); 1.400 + } 1.401 + } 1.402 + ); 1.403 + 1.404 + this.log("autoCompleteSearchAsync message was sent"); 1.405 + }, 1.406 + 1.407 + stopAutoCompleteSearch : function () { 1.408 + this.log("stopAutoCompleteSearch"); 1.409 + }, 1.410 +}; // end of FormAutoCompleteChild implementation 1.411 + 1.412 +// nsIAutoCompleteResult implementation 1.413 +function FormAutoCompleteResult (formHistory, entries, fieldName, searchString) { 1.414 + this.formHistory = formHistory; 1.415 + this.entries = entries; 1.416 + this.fieldName = fieldName; 1.417 + this.searchString = searchString; 1.418 +} 1.419 + 1.420 +FormAutoCompleteResult.prototype = { 1.421 + QueryInterface : XPCOMUtils.generateQI([Ci.nsIAutoCompleteResult, 1.422 + Ci.nsISupportsWeakReference]), 1.423 + 1.424 + // private 1.425 + formHistory : null, 1.426 + entries : null, 1.427 + fieldName : null, 1.428 + 1.429 + _checkIndexBounds : function (index) { 1.430 + if (index < 0 || index >= this.entries.length) 1.431 + throw Components.Exception("Index out of range.", Cr.NS_ERROR_ILLEGAL_VALUE); 1.432 + }, 1.433 + 1.434 + // Allow autoCompleteSearch to get at the JS object so it can 1.435 + // modify some readonly properties for internal use. 1.436 + get wrappedJSObject() { 1.437 + return this; 1.438 + }, 1.439 + 1.440 + // Interfaces from idl... 1.441 + searchString : null, 1.442 + errorDescription : "", 1.443 + get defaultIndex() { 1.444 + if (entries.length == 0) 1.445 + return -1; 1.446 + else 1.447 + return 0; 1.448 + }, 1.449 + get searchResult() { 1.450 + if (this.entries.length == 0) 1.451 + return Ci.nsIAutoCompleteResult.RESULT_NOMATCH; 1.452 + return Ci.nsIAutoCompleteResult.RESULT_SUCCESS; 1.453 + }, 1.454 + get matchCount() { 1.455 + return this.entries.length; 1.456 + }, 1.457 + 1.458 + getValueAt : function (index) { 1.459 + this._checkIndexBounds(index); 1.460 + return this.entries[index].text; 1.461 + }, 1.462 + 1.463 + getLabelAt: function(index) { 1.464 + return getValueAt(index); 1.465 + }, 1.466 + 1.467 + getCommentAt : function (index) { 1.468 + this._checkIndexBounds(index); 1.469 + return ""; 1.470 + }, 1.471 + 1.472 + getStyleAt : function (index) { 1.473 + this._checkIndexBounds(index); 1.474 + return ""; 1.475 + }, 1.476 + 1.477 + getImageAt : function (index) { 1.478 + this._checkIndexBounds(index); 1.479 + return ""; 1.480 + }, 1.481 + 1.482 + getFinalCompleteValueAt : function (index) { 1.483 + return this.getValueAt(index); 1.484 + }, 1.485 + 1.486 + removeValueAt : function (index, removeFromDB) { 1.487 + this._checkIndexBounds(index); 1.488 + 1.489 + let [removedEntry] = this.entries.splice(index, 1); 1.490 + 1.491 + if (removeFromDB) { 1.492 + this.formHistory.update({ op: "remove", 1.493 + fieldname: this.fieldName, 1.494 + value: removedEntry.text }); 1.495 + } 1.496 + } 1.497 +}; 1.498 + 1.499 + 1.500 +let remote = Services.appinfo.browserTabsRemote; 1.501 +if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT && remote) { 1.502 + // Register the stub FormAutoComplete module in the child which will 1.503 + // forward messages to the parent through the process message manager. 1.504 + let component = [FormAutoCompleteChild]; 1.505 + this.NSGetFactory = XPCOMUtils.generateNSGetFactory(component); 1.506 +} else { 1.507 + let component = [FormAutoComplete]; 1.508 + this.NSGetFactory = XPCOMUtils.generateNSGetFactory(component); 1.509 +}