toolkit/components/satchel/nsFormAutoComplete.js

branch
TOR_BUG_9701
changeset 14
925c144e1f1f
equal deleted inserted replaced
-1:000000000000 0:00b670b11b32
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5
6 const Cc = Components.classes;
7 const Ci = Components.interfaces;
8 const Cr = Components.results;
9
10 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
11 Components.utils.import("resource://gre/modules/Services.jsm");
12
13 XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils",
14 "resource://gre/modules/BrowserUtils.jsm");
15 XPCOMUtils.defineLazyModuleGetter(this, "Deprecated",
16 "resource://gre/modules/Deprecated.jsm");
17 XPCOMUtils.defineLazyModuleGetter(this, "FormHistory",
18 "resource://gre/modules/FormHistory.jsm");
19
20 function FormAutoComplete() {
21 this.init();
22 }
23
24 /**
25 * FormAutoComplete
26 *
27 * Implements the nsIFormAutoComplete interface in the main process.
28 */
29 FormAutoComplete.prototype = {
30 classID : Components.ID("{c11c21b2-71c9-4f87-a0f8-5e13f50495fd}"),
31 QueryInterface : XPCOMUtils.generateQI([Ci.nsIFormAutoComplete, Ci.nsISupportsWeakReference]),
32
33 _prefBranch : null,
34 _debug : true, // mirrors browser.formfill.debug
35 _enabled : true, // mirrors browser.formfill.enable preference
36 _agedWeight : 2,
37 _bucketSize : 1,
38 _maxTimeGroupings : 25,
39 _timeGroupingSize : 7 * 24 * 60 * 60 * 1000 * 1000,
40 _expireDays : null,
41 _boundaryWeight : 25,
42 _prefixWeight : 5,
43
44 // Only one query is performed at a time, which will be stored in _pendingQuery
45 // while the query is being performed. It will be cleared when the query finishes,
46 // is cancelled, or an error occurs. If a new query occurs while one is already
47 // pending, the existing one is cancelled. The pending query will be an
48 // mozIStoragePendingStatement object.
49 _pendingQuery : null,
50
51 init : function() {
52 // Preferences. Add observer so we get notified of changes.
53 this._prefBranch = Services.prefs.getBranch("browser.formfill.");
54 this._prefBranch.addObserver("", this.observer, true);
55 this.observer._self = this;
56
57 this._debug = this._prefBranch.getBoolPref("debug");
58 this._enabled = this._prefBranch.getBoolPref("enable");
59 this._agedWeight = this._prefBranch.getIntPref("agedWeight");
60 this._bucketSize = this._prefBranch.getIntPref("bucketSize");
61 this._maxTimeGroupings = this._prefBranch.getIntPref("maxTimeGroupings");
62 this._timeGroupingSize = this._prefBranch.getIntPref("timeGroupingSize") * 1000 * 1000;
63 this._expireDays = this._prefBranch.getIntPref("expire_days");
64 },
65
66 observer : {
67 _self : null,
68
69 QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver,
70 Ci.nsISupportsWeakReference]),
71
72 observe : function (subject, topic, data) {
73 let self = this._self;
74 if (topic == "nsPref:changed") {
75 let prefName = data;
76 self.log("got change to " + prefName + " preference");
77
78 switch (prefName) {
79 case "agedWeight":
80 self._agedWeight = self._prefBranch.getIntPref(prefName);
81 break;
82 case "debug":
83 self._debug = self._prefBranch.getBoolPref(prefName);
84 break;
85 case "enable":
86 self._enabled = self._prefBranch.getBoolPref(prefName);
87 break;
88 case "maxTimeGroupings":
89 self._maxTimeGroupings = self._prefBranch.getIntPref(prefName);
90 break;
91 case "timeGroupingSize":
92 self._timeGroupingSize = self._prefBranch.getIntPref(prefName) * 1000 * 1000;
93 break;
94 case "bucketSize":
95 self._bucketSize = self._prefBranch.getIntPref(prefName);
96 break;
97 case "boundaryWeight":
98 self._boundaryWeight = self._prefBranch.getIntPref(prefName);
99 break;
100 case "prefixWeight":
101 self._prefixWeight = self._prefBranch.getIntPref(prefName);
102 break;
103 default:
104 self.log("Oops! Pref not handled, change ignored.");
105 }
106 }
107 }
108 },
109
110
111 /*
112 * log
113 *
114 * Internal function for logging debug messages to the Error Console
115 * window
116 */
117 log : function (message) {
118 if (!this._debug)
119 return;
120 dump("FormAutoComplete: " + message + "\n");
121 Services.console.logStringMessage("FormAutoComplete: " + message);
122 },
123
124
125 autoCompleteSearch : function (aInputName, aUntrimmedSearchString, aField, aPreviousResult) {
126 Deprecated.warning("nsIFormAutoComplete::autoCompleteSearch is deprecated", "https://bugzilla.mozilla.org/show_bug.cgi?id=697377");
127
128 let result = null;
129 let listener = {
130 onSearchCompletion: function (r) result = r
131 };
132 this._autoCompleteSearchShared(aInputName, aUntrimmedSearchString, aField, aPreviousResult, listener);
133
134 // Just wait for the result to to be available.
135 let thread = Components.classes["@mozilla.org/thread-manager;1"].getService().currentThread;
136 while (!result && this._pendingQuery) {
137 thread.processNextEvent(true);
138 }
139
140 return result;
141 },
142
143 autoCompleteSearchAsync : function (aInputName, aUntrimmedSearchString, aField, aPreviousResult, aListener) {
144 this._autoCompleteSearchShared(aInputName, aUntrimmedSearchString, aField, aPreviousResult, aListener);
145 },
146
147 /*
148 * autoCompleteSearchShared
149 *
150 * aInputName -- |name| attribute from the form input being autocompleted.
151 * aUntrimmedSearchString -- current value of the input
152 * aField -- nsIDOMHTMLInputElement being autocompleted (may be null if from chrome)
153 * aPreviousResult -- previous search result, if any.
154 * aListener -- nsIFormAutoCompleteObserver that listens for the nsIAutoCompleteResult
155 * that may be returned asynchronously.
156 */
157 _autoCompleteSearchShared : function (aInputName, aUntrimmedSearchString, aField, aPreviousResult, aListener) {
158 function sortBytotalScore (a, b) {
159 return b.totalScore - a.totalScore;
160 }
161
162 let result = null;
163 if (!this._enabled) {
164 result = new FormAutoCompleteResult(FormHistory, [], aInputName, aUntrimmedSearchString);
165 if (aListener) {
166 aListener.onSearchCompletion(result);
167 }
168 return;
169 }
170
171 // don't allow form inputs (aField != null) to get results from search bar history
172 if (aInputName == 'searchbar-history' && aField) {
173 this.log('autoCompleteSearch for input name "' + aInputName + '" is denied');
174 result = new FormAutoCompleteResult(FormHistory, [], aInputName, aUntrimmedSearchString);
175 if (aListener) {
176 aListener.onSearchCompletion(result);
177 }
178 return;
179 }
180
181 this.log("AutoCompleteSearch invoked. Search is: " + aUntrimmedSearchString);
182 let searchString = aUntrimmedSearchString.trim().toLowerCase();
183
184 // reuse previous results if:
185 // a) length greater than one character (others searches are special cases) AND
186 // b) the the new results will be a subset of the previous results
187 if (aPreviousResult && aPreviousResult.searchString.trim().length > 1 &&
188 searchString.indexOf(aPreviousResult.searchString.trim().toLowerCase()) >= 0) {
189 this.log("Using previous autocomplete result");
190 result = aPreviousResult;
191 result.wrappedJSObject.searchString = aUntrimmedSearchString;
192
193 let searchTokens = searchString.split(/\s+/);
194 // We have a list of results for a shorter search string, so just
195 // filter them further based on the new search string and add to a new array.
196 let entries = result.wrappedJSObject.entries;
197 let filteredEntries = [];
198 for (let i = 0; i < entries.length; i++) {
199 let entry = entries[i];
200 // Remove results that do not contain the token
201 // XXX bug 394604 -- .toLowerCase can be wrong for some intl chars
202 if(searchTokens.some(function (tok) entry.textLowerCase.indexOf(tok) < 0))
203 continue;
204 this._calculateScore(entry, searchString, searchTokens);
205 this.log("Reusing autocomplete entry '" + entry.text +
206 "' (" + entry.frecency +" / " + entry.totalScore + ")");
207 filteredEntries.push(entry);
208 }
209 filteredEntries.sort(sortBytotalScore);
210 result.wrappedJSObject.entries = filteredEntries;
211
212 if (aListener) {
213 aListener.onSearchCompletion(result);
214 }
215 } else {
216 this.log("Creating new autocomplete search result.");
217
218 // Start with an empty list.
219 result = new FormAutoCompleteResult(FormHistory, [], aInputName, aUntrimmedSearchString);
220
221 let processEntry = function(aEntries) {
222 if (aField && aField.maxLength > -1) {
223 result.entries =
224 aEntries.filter(function (el) { return el.text.length <= aField.maxLength; });
225 } else {
226 result.entries = aEntries;
227 }
228
229 if (aListener) {
230 aListener.onSearchCompletion(result);
231 }
232 }
233
234 this.getAutoCompleteValues(aInputName, searchString, processEntry);
235 }
236 },
237
238 stopAutoCompleteSearch : function () {
239 if (this._pendingQuery) {
240 this._pendingQuery.cancel();
241 this._pendingQuery = null;
242 }
243 },
244
245 /*
246 * Get the values for an autocomplete list given a search string.
247 *
248 * fieldName - fieldname field within form history (the form input name)
249 * searchString - string to search for
250 * callback - called when the values are available. Passed an array of objects,
251 * containing properties for each result. The callback is only called
252 * when successful.
253 */
254 getAutoCompleteValues : function (fieldName, searchString, callback) {
255 let params = {
256 agedWeight: this._agedWeight,
257 bucketSize: this._bucketSize,
258 expiryDate: 1000 * (Date.now() - this._expireDays * 24 * 60 * 60 * 1000),
259 fieldname: fieldName,
260 maxTimeGroupings: this._maxTimeGroupings,
261 timeGroupingSize: this._timeGroupingSize,
262 prefixWeight: this._prefixWeight,
263 boundaryWeight: this._boundaryWeight
264 }
265
266 this.stopAutoCompleteSearch();
267
268 let results = [];
269 let processResults = {
270 handleResult: aResult => {
271 results.push(aResult);
272 },
273 handleError: aError => {
274 this.log("getAutocompleteValues failed: " + aError.message);
275 },
276 handleCompletion: aReason => {
277 this._pendingQuery = null;
278 if (!aReason) {
279 callback(results);
280 }
281 }
282 };
283
284 this._pendingQuery = FormHistory.getAutoCompleteResults(searchString, params, processResults);
285 },
286
287 /*
288 * _calculateScore
289 *
290 * entry -- an nsIAutoCompleteResult entry
291 * aSearchString -- current value of the input (lowercase)
292 * searchTokens -- array of tokens of the search string
293 *
294 * Returns: an int
295 */
296 _calculateScore : function (entry, aSearchString, searchTokens) {
297 let boundaryCalc = 0;
298 // for each word, calculate word boundary weights
299 for each (let token in searchTokens) {
300 boundaryCalc += (entry.textLowerCase.indexOf(token) == 0);
301 boundaryCalc += (entry.textLowerCase.indexOf(" " + token) >= 0);
302 }
303 boundaryCalc = boundaryCalc * this._boundaryWeight;
304 // now add more weight if we have a traditional prefix match and
305 // multiply boundary bonuses by boundary weight
306 boundaryCalc += this._prefixWeight *
307 (entry.textLowerCase.
308 indexOf(aSearchString) == 0);
309 entry.totalScore = Math.round(entry.frecency * Math.max(1, boundaryCalc));
310 }
311
312 }; // end of FormAutoComplete implementation
313
314 /**
315 * FormAutoCompleteChild
316 *
317 * Implements the nsIFormAutoComplete interface in a child content process,
318 * and forwards the auto-complete requests to the parent process which
319 * also implements a nsIFormAutoComplete interface and has
320 * direct access to the FormHistory database.
321 */
322 function FormAutoCompleteChild() {
323 this.init();
324 }
325
326 FormAutoCompleteChild.prototype = {
327 classID : Components.ID("{c11c21b2-71c9-4f87-a0f8-5e13f50495fd}"),
328 QueryInterface : XPCOMUtils.generateQI([Ci.nsIFormAutoComplete, Ci.nsISupportsWeakReference]),
329
330 _debug: false,
331 _enabled: true,
332
333 /*
334 * init
335 *
336 * Initializes the content-process side of the FormAutoComplete component,
337 * and add a listener for the message that the parent process sends when
338 * a result is produced.
339 */
340 init: function() {
341 this._debug = Services.prefs.getBoolPref("browser.formfill.debug");
342 this._enabled = Services.prefs.getBoolPref("browser.formfill.enable");
343 this.log("init");
344 },
345
346 /*
347 * log
348 *
349 * Internal function for logging debug messages
350 */
351 log : function (message) {
352 if (!this._debug)
353 return;
354 dump("FormAutoCompleteChild: " + message + "\n");
355 },
356
357 autoCompleteSearch : function (aInputName, aUntrimmedSearchString, aField, aPreviousResult) {
358 // This function is deprecated
359 },
360
361 autoCompleteSearchAsync : function (aInputName, aUntrimmedSearchString, aField, aPreviousResult, aListener) {
362 this.log("autoCompleteSearchAsync");
363
364 this._pendingListener = aListener;
365
366 let rect = BrowserUtils.getElementBoundingScreenRect(aField);
367
368 let window = aField.ownerDocument.defaultView;
369 let topLevelDocshell = window.QueryInterface(Ci.nsIInterfaceRequestor)
370 .getInterface(Ci.nsIDocShell)
371 .sameTypeRootTreeItem
372 .QueryInterface(Ci.nsIDocShell);
373
374 let mm = topLevelDocshell.QueryInterface(Ci.nsIInterfaceRequestor)
375 .getInterface(Ci.nsIContentFrameMessageManager);
376
377 mm.sendAsyncMessage("FormHistory:AutoCompleteSearchAsync", {
378 inputName: aInputName,
379 untrimmedSearchString: aUntrimmedSearchString,
380 left: rect.left,
381 top: rect.top,
382 width: rect.width,
383 height: rect.height
384 });
385
386 mm.addMessageListener("FormAutoComplete:AutoCompleteSearchAsyncResult",
387 function searchFinished(message) {
388 mm.removeMessageListener("FormAutoComplete:AutoCompleteSearchAsyncResult", searchFinished);
389 let result = new FormAutoCompleteResult(
390 null,
391 [{text: res} for (res of message.data.results)],
392 null,
393 null
394 );
395 if (aListener) {
396 aListener.onSearchCompletion(result);
397 }
398 }
399 );
400
401 this.log("autoCompleteSearchAsync message was sent");
402 },
403
404 stopAutoCompleteSearch : function () {
405 this.log("stopAutoCompleteSearch");
406 },
407 }; // end of FormAutoCompleteChild implementation
408
409 // nsIAutoCompleteResult implementation
410 function FormAutoCompleteResult (formHistory, entries, fieldName, searchString) {
411 this.formHistory = formHistory;
412 this.entries = entries;
413 this.fieldName = fieldName;
414 this.searchString = searchString;
415 }
416
417 FormAutoCompleteResult.prototype = {
418 QueryInterface : XPCOMUtils.generateQI([Ci.nsIAutoCompleteResult,
419 Ci.nsISupportsWeakReference]),
420
421 // private
422 formHistory : null,
423 entries : null,
424 fieldName : null,
425
426 _checkIndexBounds : function (index) {
427 if (index < 0 || index >= this.entries.length)
428 throw Components.Exception("Index out of range.", Cr.NS_ERROR_ILLEGAL_VALUE);
429 },
430
431 // Allow autoCompleteSearch to get at the JS object so it can
432 // modify some readonly properties for internal use.
433 get wrappedJSObject() {
434 return this;
435 },
436
437 // Interfaces from idl...
438 searchString : null,
439 errorDescription : "",
440 get defaultIndex() {
441 if (entries.length == 0)
442 return -1;
443 else
444 return 0;
445 },
446 get searchResult() {
447 if (this.entries.length == 0)
448 return Ci.nsIAutoCompleteResult.RESULT_NOMATCH;
449 return Ci.nsIAutoCompleteResult.RESULT_SUCCESS;
450 },
451 get matchCount() {
452 return this.entries.length;
453 },
454
455 getValueAt : function (index) {
456 this._checkIndexBounds(index);
457 return this.entries[index].text;
458 },
459
460 getLabelAt: function(index) {
461 return getValueAt(index);
462 },
463
464 getCommentAt : function (index) {
465 this._checkIndexBounds(index);
466 return "";
467 },
468
469 getStyleAt : function (index) {
470 this._checkIndexBounds(index);
471 return "";
472 },
473
474 getImageAt : function (index) {
475 this._checkIndexBounds(index);
476 return "";
477 },
478
479 getFinalCompleteValueAt : function (index) {
480 return this.getValueAt(index);
481 },
482
483 removeValueAt : function (index, removeFromDB) {
484 this._checkIndexBounds(index);
485
486 let [removedEntry] = this.entries.splice(index, 1);
487
488 if (removeFromDB) {
489 this.formHistory.update({ op: "remove",
490 fieldname: this.fieldName,
491 value: removedEntry.text });
492 }
493 }
494 };
495
496
497 let remote = Services.appinfo.browserTabsRemote;
498 if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT && remote) {
499 // Register the stub FormAutoComplete module in the child which will
500 // forward messages to the parent through the process message manager.
501 let component = [FormAutoCompleteChild];
502 this.NSGetFactory = XPCOMUtils.generateNSGetFactory(component);
503 } else {
504 let component = [FormAutoComplete];
505 this.NSGetFactory = XPCOMUtils.generateNSGetFactory(component);
506 }

mercurial