toolkit/components/search/nsSearchSuggestions.js

Fri, 16 Jan 2015 18:13:44 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Fri, 16 Jan 2015 18:13:44 +0100
branch
TOR_BUG_9701
changeset 14
925c144e1f1f
permissions
-rw-r--r--

Integrate suggestion from review to improve consistency with existing code.

     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/. */
     5 const SEARCH_RESPONSE_SUGGESTION_JSON = "application/x-suggestions+json";
     7 const BROWSER_SUGGEST_PREF = "browser.search.suggest.enabled";
     8 const XPCOM_SHUTDOWN_TOPIC              = "xpcom-shutdown";
     9 const NS_PREFBRANCH_PREFCHANGE_TOPIC_ID = "nsPref:changed";
    11 const Cc = Components.classes;
    12 const Ci = Components.interfaces;
    13 const Cr = Components.results;
    14 const Cu = Components.utils;
    16 const HTTP_OK                    = 200;
    17 const HTTP_INTERNAL_SERVER_ERROR = 500;
    18 const HTTP_BAD_GATEWAY           = 502;
    19 const HTTP_SERVICE_UNAVAILABLE   = 503;
    21 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    22 Cu.import("resource://gre/modules/nsFormAutoCompleteResult.jsm");
    23 Cu.import("resource://gre/modules/Services.jsm");
    25 /**
    26  * SuggestAutoComplete is a base class that implements nsIAutoCompleteSearch
    27  * and can collect results for a given search by using the search URL supplied
    28  * by the subclass. We do it this way since the AutoCompleteController in
    29  * Mozilla requires a unique XPCOM Service for every search provider, even if
    30  * the logic for two providers is identical.
    31  * @constructor
    32  */
    33 function SuggestAutoComplete() {
    34   this._init();
    35 }
    36 SuggestAutoComplete.prototype = {
    38   _init: function() {
    39     this._addObservers();
    40     this._suggestEnabled = Services.prefs.getBoolPref(BROWSER_SUGGEST_PREF);
    41   },
    43   get _suggestionLabel() {
    44     delete this._suggestionLabel;
    45     let bundle = Services.strings.createBundle("chrome://global/locale/search/search.properties");
    46     return this._suggestionLabel = bundle.GetStringFromName("suggestion_label");
    47   },
    49   /**
    50    * Search suggestions will be shown if this._suggestEnabled is true.
    51    */
    52   _suggestEnabled: null,
    54   /*************************************************************************
    55    * Server request backoff implementation fields below
    56    * These allow us to throttle requests if the server is getting hammered.
    57    **************************************************************************/
    59   /**
    60    * This is an array that contains the timestamps (in unixtime) of
    61    * the last few backoff-triggering errors.
    62    */
    63   _serverErrorLog: [],
    65   /**
    66    * If we receive this number of backoff errors within the amount of time
    67    * specified by _serverErrorPeriod, then we initiate backoff.
    68    */
    69   _maxErrorsBeforeBackoff: 3,
    71   /**
    72    * If we receive enough consecutive errors (where "enough" is defined by
    73    * _maxErrorsBeforeBackoff above) within this time period,
    74    * we trigger the backoff behavior.
    75    */
    76   _serverErrorPeriod: 600000,  // 10 minutes in milliseconds
    78   /**
    79    * If we get another backoff error immediately after timeout, we increase the
    80    * backoff to (2 x old period) + this value.
    81    */
    82   _serverErrorTimeoutIncrement: 600000,  // 10 minutes in milliseconds
    84   /**
    85    * The current amount of time to wait before trying a server request
    86    * after receiving a backoff error.
    87    */
    88   _serverErrorTimeout: 0,
    90   /**
    91    * Time (in unixtime) after which we're allowed to try requesting again.
    92    */
    93   _nextRequestTime: 0,
    95   /**
    96    * The last engine we requested against (so that we can tell if the
    97    * user switched engines).
    98    */
    99   _serverErrorEngine: null,
   101   /**
   102    * The XMLHttpRequest object.
   103    * @private
   104    */
   105   _request: null,
   107   /**
   108    * The object implementing nsIAutoCompleteObserver that we notify when
   109    * we have found results
   110    * @private
   111    */
   112   _listener: null,
   114   /**
   115    * If this is true, we'll integrate form history results with the
   116    * suggest results.
   117    */
   118   _includeFormHistory: true,
   120   /**
   121    * True if a request for remote suggestions was sent. This is used to
   122    * differentiate between the "_request is null because the request has
   123    * already returned a result" and "_request is null because no request was
   124    * sent" cases.
   125    */
   126   _sentSuggestRequest: false,
   128   /**
   129    * This is the callback for the suggest timeout timer.
   130    */
   131   notify: function SAC_notify(timer) {
   132     // FIXME: bug 387341
   133     // Need to break the cycle between us and the timer.
   134     this._formHistoryTimer = null;
   136     // If this._listener is null, we've already sent out suggest results, so
   137     // nothing left to do here.
   138     if (!this._listener)
   139       return;
   141     // Otherwise, the XMLHTTPRequest for suggest results is taking too long,
   142     // so send out the form history results and cancel the request.
   143     this._listener.onSearchResult(this, this._formHistoryResult);
   144     this._reset();
   145   },
   147   /**
   148    * This determines how long (in ms) we should wait before giving up on
   149    * the suggestions and just showing local form history results.
   150    */
   151   _suggestionTimeout: 500,
   153   /**
   154    * This is the callback for that the form history service uses to
   155    * send us results.
   156    */
   157   onSearchResult: function SAC_onSearchResult(search, result) {
   158     this._formHistoryResult = result;
   160     if (this._request) {
   161       // We still have a pending request, wait a bit to give it a chance to
   162       // finish.
   163       this._formHistoryTimer = Cc["@mozilla.org/timer;1"].
   164                                createInstance(Ci.nsITimer);
   165       this._formHistoryTimer.initWithCallback(this, this._suggestionTimeout,
   166                                               Ci.nsITimer.TYPE_ONE_SHOT);
   167     } else if (!this._sentSuggestRequest) {
   168       // We didn't send a request, so just send back the form history results.
   169       this._listener.onSearchResult(this, this._formHistoryResult);
   170       this._reset();
   171     }
   172   },
   174   /**
   175    * This is the URI that the last suggest request was sent to.
   176    */
   177   _suggestURI: null,
   179   /**
   180    * Autocomplete results from the form history service get stored here.
   181    */
   182   _formHistoryResult: null,
   184   /**
   185    * This holds the suggest server timeout timer, if applicable.
   186    */
   187   _formHistoryTimer: null,
   189   /**
   190    * Maximum number of history items displayed. This is capped at 7
   191    * because the primary consumer (Firefox search bar) displays 10 rows
   192    * by default, and so we want to leave some space for suggestions
   193    * to be visible.
   194    */
   195   _historyLimit: 7,
   197   /**
   198    * This clears all the per-request state.
   199    */
   200   _reset: function SAC_reset() {
   201     // Don't let go of our listener and form history result if the timer is
   202     // still pending, the timer will call _reset() when it fires.
   203     if (!this._formHistoryTimer) {
   204       this._listener = null;
   205       this._formHistoryResult = null;
   206     }
   207     this._request = null;
   208   },
   210   /**
   211    * This sends an autocompletion request to the form history service,
   212    * which will call onSearchResults with the results of the query.
   213    */
   214   _startHistorySearch: function SAC_SHSearch(searchString, searchParam) {
   215     var formHistory =
   216       Cc["@mozilla.org/autocomplete/search;1?name=form-history"].
   217       createInstance(Ci.nsIAutoCompleteSearch);
   218     formHistory.startSearch(searchString, searchParam, this._formHistoryResult, this);
   219   },
   221   /**
   222    * Makes a note of the fact that we've received a backoff-triggering
   223    * response, so that we can adjust the backoff behavior appropriately.
   224    */
   225   _noteServerError: function SAC__noteServeError() {
   226     var currentTime = Date.now();
   228     this._serverErrorLog.push(currentTime);
   229     if (this._serverErrorLog.length > this._maxErrorsBeforeBackoff)
   230       this._serverErrorLog.shift();
   232     if ((this._serverErrorLog.length == this._maxErrorsBeforeBackoff) &&
   233         ((currentTime - this._serverErrorLog[0]) < this._serverErrorPeriod)) {
   234       // increase timeout, and then don't request until timeout is over
   235       this._serverErrorTimeout = (this._serverErrorTimeout * 2) +
   236                                  this._serverErrorTimeoutIncrement;
   237       this._nextRequestTime = currentTime + this._serverErrorTimeout;
   238     }
   239   },
   241   /**
   242    * Resets the backoff behavior; called when we get a successful response.
   243    */
   244   _clearServerErrors: function SAC__clearServerErrors() {
   245     this._serverErrorLog = [];
   246     this._serverErrorTimeout = 0;
   247     this._nextRequestTime = 0;
   248   },
   250   /**
   251    * This checks whether we should send a server request (i.e. we're not
   252    * in a error-triggered backoff period.
   253    *
   254    * @private
   255    */
   256   _okToRequest: function SAC__okToRequest() {
   257     return Date.now() > this._nextRequestTime;
   258   },
   260   /**
   261    * This checks to see if the new search engine is different
   262    * from the previous one, and if so clears any error state that might
   263    * have accumulated for the old engine.
   264    *
   265    * @param engine The engine that the suggestion request would be sent to.
   266    * @private
   267    */
   268   _checkForEngineSwitch: function SAC__checkForEngineSwitch(engine) {
   269     if (engine == this._serverErrorEngine)
   270       return;
   272     // must've switched search providers, clear old errors
   273     this._serverErrorEngine = engine;
   274     this._clearServerErrors();
   275   },
   277   /**
   278    * This returns true if the status code of the HTTP response
   279    * represents a backoff-triggering error.
   280    *
   281    * @param status  The status code from the HTTP response
   282    * @private
   283    */
   284   _isBackoffError: function SAC__isBackoffError(status) {
   285     return ((status == HTTP_INTERNAL_SERVER_ERROR) ||
   286             (status == HTTP_BAD_GATEWAY) ||
   287             (status == HTTP_SERVICE_UNAVAILABLE));
   288   },
   290   /**
   291    * Called when the 'readyState' of the XMLHttpRequest changes. We only care
   292    * about state 4 (COMPLETED) - handle the response data.
   293    * @private
   294    */
   295   onReadyStateChange: function() {
   296     // xxx use the real const here
   297     if (!this._request || this._request.readyState != 4)
   298       return;
   300     try {
   301       var status = this._request.status;
   302     } catch (e) {
   303       // The XML HttpRequest can throw NS_ERROR_NOT_AVAILABLE.
   304       return;
   305     }
   307     if (this._isBackoffError(status)) {
   308       this._noteServerError();
   309       return;
   310     }
   312     var responseText = this._request.responseText;
   313     if (status != HTTP_OK || responseText == "")
   314       return;
   316     this._clearServerErrors();
   318     try {
   319       var serverResults = JSON.parse(responseText);
   320     } catch(ex) {
   321       Components.utils.reportError("Failed to parse JSON from " + this._suggestURI.spec + ": " + ex);
   322       return;
   323     }
   325     var searchString = serverResults[0] || "";
   326     var results = serverResults[1] || [];
   328     var comments = [];  // "comments" column values for suggestions
   329     var historyResults = [];
   330     var historyComments = [];
   332     // If form history is enabled and has results, add them to the list.
   333     if (this._includeFormHistory && this._formHistoryResult &&
   334         (this._formHistoryResult.searchResult ==
   335          Ci.nsIAutoCompleteResult.RESULT_SUCCESS)) {
   336       var maxHistoryItems = Math.min(this._formHistoryResult.matchCount, this._historyLimit);
   337       for (var i = 0; i < maxHistoryItems; ++i) {
   338         var term = this._formHistoryResult.getValueAt(i);
   340         // we don't want things to appear in both history and suggestions
   341         var dupIndex = results.indexOf(term);
   342         if (dupIndex != -1)
   343           results.splice(dupIndex, 1);
   345         historyResults.push(term);
   346         historyComments.push("");
   347       }
   348     }
   350     // fill out the comment column for the suggestions
   351     for (var i = 0; i < results.length; ++i)
   352       comments.push("");
   354     // if we have any suggestions, put a label at the top
   355     if (comments.length > 0)
   356       comments[0] = this._suggestionLabel;
   358     // now put the history results above the suggestions
   359     var finalResults = historyResults.concat(results);
   360     var finalComments = historyComments.concat(comments);
   362     // Notify the FE of our new results
   363     this.onResultsReady(searchString, finalResults, finalComments,
   364                         this._formHistoryResult);
   366     // Reset our state for next time.
   367     this._reset();
   368   },
   370   /**
   371    * Notifies the front end of new results.
   372    * @param searchString  the user's query string
   373    * @param results       an array of results to the search
   374    * @param comments      an array of metadata corresponding to the results
   375    * @private
   376    */
   377   onResultsReady: function(searchString, results, comments,
   378                            formHistoryResult) {
   379     if (this._listener) {
   380       var result = new FormAutoCompleteResult(
   381           searchString,
   382           Ci.nsIAutoCompleteResult.RESULT_SUCCESS,
   383           0,
   384           "",
   385           results,
   386           results,
   387           comments,
   388           formHistoryResult);
   390       this._listener.onSearchResult(this, result);
   392       // Null out listener to make sure we don't notify it twice, in case our
   393       // timer callback still hasn't run.
   394       this._listener = null;
   395     }
   396   },
   398   /**
   399    * Initiates the search result gathering process. Part of
   400    * nsIAutoCompleteSearch implementation.
   401    *
   402    * @param searchString    the user's query string
   403    * @param searchParam     unused, "an extra parameter"; even though
   404    *                        this parameter and the next are unused, pass
   405    *                        them through in case the form history
   406    *                        service wants them
   407    * @param previousResult  unused, a client-cached store of the previous
   408    *                        generated resultset for faster searching.
   409    * @param listener        object implementing nsIAutoCompleteObserver which
   410    *                        we notify when results are ready.
   411    */
   412   startSearch: function(searchString, searchParam, previousResult, listener) {
   413     // Don't reuse a previous form history result when it no longer applies.
   414     if (!previousResult)
   415       this._formHistoryResult = null;
   417     var formHistorySearchParam = searchParam.split("|")[0];
   419     // Receive the information about the privacy mode of the window to which
   420     // this search box belongs.  The front-end's search.xml bindings passes this
   421     // information in the searchParam parameter.  The alternative would have
   422     // been to modify nsIAutoCompleteSearch to add an argument to startSearch
   423     // and patch all of autocomplete to be aware of this, but the searchParam
   424     // argument is already an opaque argument, so this solution is hopefully
   425     // less hackish (although still gross.)
   426     var privacyMode = (searchParam.split("|")[1] == "private");
   428     // Start search immediately if possible, otherwise once the search
   429     // service is initialized
   430     if (Services.search.isInitialized) {
   431       this._triggerSearch(searchString, formHistorySearchParam, listener, privacyMode);
   432       return;
   433     }
   435     Services.search.init((function startSearch_cb(aResult) {
   436       if (!Components.isSuccessCode(aResult)) {
   437         Cu.reportError("Could not initialize search service, bailing out: " + aResult);
   438         return;
   439       }
   440       this._triggerSearch(searchString, formHistorySearchParam, listener, privacyMode);
   441     }).bind(this));
   442   },
   444   /**
   445    * Actual implementation of search.
   446    */
   447   _triggerSearch: function(searchString, searchParam, listener, privacyMode) {
   448     // If there's an existing request, stop it. There is no smart filtering
   449     // here as there is when looking through history/form data because the
   450     // result set returned by the server is different for every typed value -
   451     // "ocean breathes" does not return a subset of the results returned for
   452     // "ocean", for example. This does nothing if there is no current request.
   453     this.stopSearch();
   455     this._listener = listener;
   457     var engine = Services.search.currentEngine;
   459     this._checkForEngineSwitch(engine);
   461     if (!searchString ||
   462         !this._suggestEnabled ||
   463         !engine.supportsResponseType(SEARCH_RESPONSE_SUGGESTION_JSON) ||
   464         !this._okToRequest()) {
   465       // We have an empty search string (user pressed down arrow to see
   466       // history), or search suggestions are disabled, or the current engine
   467       // has no suggest functionality, or we're in backoff mode; so just use
   468       // local history.
   469       this._sentSuggestRequest = false;
   470       this._startHistorySearch(searchString, searchParam);
   471       return;
   472     }
   474     // Actually do the search
   475     this._request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
   476                     createInstance(Ci.nsIXMLHttpRequest);
   477     var submission = engine.getSubmission(searchString,
   478                                           SEARCH_RESPONSE_SUGGESTION_JSON);
   479     this._suggestURI = submission.uri;
   480     var method = (submission.postData ? "POST" : "GET");
   481     this._request.open(method, this._suggestURI.spec, true);
   482     this._request.channel.notificationCallbacks = new AuthPromptOverride();
   483     if (this._request.channel instanceof Ci.nsIPrivateBrowsingChannel) {
   484       this._request.channel.setPrivate(privacyMode);
   485     }
   487     var self = this;
   488     function onReadyStateChange() {
   489       self.onReadyStateChange();
   490     }
   491     this._request.onreadystatechange = onReadyStateChange;
   492     this._request.send(submission.postData);
   494     if (this._includeFormHistory) {
   495       this._sentSuggestRequest = true;
   496       this._startHistorySearch(searchString, searchParam);
   497     }
   498   },
   500   /**
   501    * Ends the search result gathering process. Part of nsIAutoCompleteSearch
   502    * implementation.
   503    */
   504   stopSearch: function() {
   505     if (this._request) {
   506       this._request.abort();
   507       this._reset();
   508     }
   509   },
   511   /**
   512    * nsIObserver
   513    */
   514   observe: function SAC_observe(aSubject, aTopic, aData) {
   515     switch (aTopic) {
   516       case NS_PREFBRANCH_PREFCHANGE_TOPIC_ID:
   517         this._suggestEnabled = Services.prefs.getBoolPref(BROWSER_SUGGEST_PREF);
   518         break;
   519       case XPCOM_SHUTDOWN_TOPIC:
   520         this._removeObservers();
   521         break;
   522     }
   523   },
   525   _addObservers: function SAC_addObservers() {
   526     Services.prefs.addObserver(BROWSER_SUGGEST_PREF, this, false);
   528     Services.obs.addObserver(this, XPCOM_SHUTDOWN_TOPIC, false);
   529   },
   531   _removeObservers: function SAC_removeObservers() {
   532     Services.prefs.removeObserver(BROWSER_SUGGEST_PREF, this);
   534     Services.obs.removeObserver(this, XPCOM_SHUTDOWN_TOPIC);
   535   },
   537   // nsISupports
   538   QueryInterface: XPCOMUtils.generateQI([Ci.nsIAutoCompleteSearch,
   539                                          Ci.nsIAutoCompleteObserver])
   540 };
   542 function AuthPromptOverride() {
   543 }
   544 AuthPromptOverride.prototype = {
   545   // nsIAuthPromptProvider
   546   getAuthPrompt: function (reason, iid) {
   547     // Return a no-op nsIAuthPrompt2 implementation.
   548     return {
   549       promptAuth: function () {
   550         throw Cr.NS_ERROR_NOT_IMPLEMENTED;
   551       },
   552       asyncPromptAuth: function () {
   553         throw Cr.NS_ERROR_NOT_IMPLEMENTED;
   554       }
   555     };
   556   },
   558   // nsIInterfaceRequestor
   559   getInterface: function SSLL_getInterface(iid) {
   560     return this.QueryInterface(iid);
   561   },
   563   // nsISupports
   564   QueryInterface: XPCOMUtils.generateQI([Ci.nsIAuthPromptProvider,
   565                                          Ci.nsIInterfaceRequestor])
   566 };
   567 /**
   568  * SearchSuggestAutoComplete is a service implementation that handles suggest
   569  * results specific to web searches.
   570  * @constructor
   571  */
   572 function SearchSuggestAutoComplete() {
   573   // This calls _init() in the parent class (SuggestAutoComplete) via the
   574   // prototype, below.
   575   this._init();
   576 }
   577 SearchSuggestAutoComplete.prototype = {
   578   classID: Components.ID("{aa892eb4-ffbf-477d-9f9a-06c995ae9f27}"),
   579   __proto__: SuggestAutoComplete.prototype,
   580   serviceURL: ""
   581 };
   583 var component = [SearchSuggestAutoComplete];
   584 this.NSGetFactory = XPCOMUtils.generateNSGetFactory(component);

mercurial