|
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 } |