toolkit/components/satchel/nsFormAutoComplete.js

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

     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/. */
     6 const Cc = Components.classes;
     7 const Ci = Components.interfaces;
     8 const Cr = Components.results;
    10 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
    11 Components.utils.import("resource://gre/modules/Services.jsm");
    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");
    20 function FormAutoComplete() {
    21     this.init();
    22 }
    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]),
    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,
    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,
    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;
    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     },
    66     observer : {
    67         _self : null,
    69         QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver,
    70                                                 Ci.nsISupportsWeakReference]),
    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");
    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     },
   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     },
   125     autoCompleteSearch : function (aInputName, aUntrimmedSearchString, aField, aPreviousResult) {
   126       Deprecated.warning("nsIFormAutoComplete::autoCompleteSearch is deprecated", "https://bugzilla.mozilla.org/show_bug.cgi?id=697377");
   128       let result = null;
   129       let listener = {
   130         onSearchCompletion: function (r) result = r
   131       };
   132       this._autoCompleteSearchShared(aInputName, aUntrimmedSearchString, aField, aPreviousResult, listener);
   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       }
   140       return result;
   141     },
   143     autoCompleteSearchAsync : function (aInputName, aUntrimmedSearchString, aField, aPreviousResult, aListener) {
   144       this._autoCompleteSearchShared(aInputName, aUntrimmedSearchString, aField, aPreviousResult, aListener);
   145     },
   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         }
   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         }
   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         }
   181         this.log("AutoCompleteSearch invoked. Search is: " + aUntrimmedSearchString);
   182         let searchString = aUntrimmedSearchString.trim().toLowerCase();
   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;
   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;
   212             if (aListener) {
   213               aListener.onSearchCompletion(result);
   214             }
   215         } else {
   216             this.log("Creating new autocomplete search result.");
   218             // Start with an empty list.
   219             result = new FormAutoCompleteResult(FormHistory, [], aInputName, aUntrimmedSearchString);
   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               }
   229               if (aListener) {
   230                 aListener.onSearchCompletion(result);
   231               }
   232             }
   234             this.getAutoCompleteValues(aInputName, searchString, processEntry);
   235         }
   236     },
   238     stopAutoCompleteSearch : function () {
   239         if (this._pendingQuery) {
   240             this._pendingQuery.cancel();
   241             this._pendingQuery = null;
   242         }
   243     },
   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         }
   266         this.stopAutoCompleteSearch();
   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         };
   284         this._pendingQuery = FormHistory.getAutoCompleteResults(searchString, params, processResults);
   285     },
   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     }
   312 }; // end of FormAutoComplete implementation
   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 }
   326 FormAutoCompleteChild.prototype = {
   327     classID          : Components.ID("{c11c21b2-71c9-4f87-a0f8-5e13f50495fd}"),
   328     QueryInterface   : XPCOMUtils.generateQI([Ci.nsIFormAutoComplete, Ci.nsISupportsWeakReference]),
   330     _debug: false,
   331     _enabled: true,
   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     },
   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     },
   357     autoCompleteSearch : function (aInputName, aUntrimmedSearchString, aField, aPreviousResult) {
   358       // This function is deprecated
   359     },
   361     autoCompleteSearchAsync : function (aInputName, aUntrimmedSearchString, aField, aPreviousResult, aListener) {
   362       this.log("autoCompleteSearchAsync");
   364       this._pendingListener = aListener;
   366       let rect = BrowserUtils.getElementBoundingScreenRect(aField);
   368       let window = aField.ownerDocument.defaultView;
   369       let topLevelDocshell = window.QueryInterface(Ci.nsIInterfaceRequestor)
   370                                    .getInterface(Ci.nsIDocShell)
   371                                    .sameTypeRootTreeItem
   372                                    .QueryInterface(Ci.nsIDocShell);
   374       let mm = topLevelDocshell.QueryInterface(Ci.nsIInterfaceRequestor)
   375                                .getInterface(Ci.nsIContentFrameMessageManager);
   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       });
   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       );
   401       this.log("autoCompleteSearchAsync message was sent");
   402     },
   404     stopAutoCompleteSearch : function () {
   405        this.log("stopAutoCompleteSearch");
   406     },
   407 }; // end of FormAutoCompleteChild implementation
   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 }
   417 FormAutoCompleteResult.prototype = {
   418     QueryInterface : XPCOMUtils.generateQI([Ci.nsIAutoCompleteResult,
   419                                             Ci.nsISupportsWeakReference]),
   421     // private
   422     formHistory : null,
   423     entries : null,
   424     fieldName : null,
   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     },
   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     },
   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     },
   455     getValueAt : function (index) {
   456         this._checkIndexBounds(index);
   457         return this.entries[index].text;
   458     },
   460     getLabelAt: function(index) {
   461         return getValueAt(index);
   462     },
   464     getCommentAt : function (index) {
   465         this._checkIndexBounds(index);
   466         return "";
   467     },
   469     getStyleAt : function (index) {
   470         this._checkIndexBounds(index);
   471         return "";
   472     },
   474     getImageAt : function (index) {
   475         this._checkIndexBounds(index);
   476         return "";
   477     },
   479     getFinalCompleteValueAt : function (index) {
   480         return this.getValueAt(index);
   481     },
   483     removeValueAt : function (index, removeFromDB) {
   484         this._checkIndexBounds(index);
   486         let [removedEntry] = this.entries.splice(index, 1);
   488         if (removeFromDB) {
   489           this.formHistory.update({ op: "remove",
   490                                     fieldname: this.fieldName,
   491                                     value: removedEntry.text });
   492         }
   493     }
   494 };
   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