Bug 452317 - FeedConverter.js: QueryInterface should throw NS_ERROR_NO_INTERFACE...
[wine-gecko.git] / toolkit / components / search / nsSearchSuggestions.js
bloba1cde8c75bf2160682117f2126d4e18fe6d81b22
1 /* ***** BEGIN LICENSE BLOCK *****
2  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
3  *
4  * The contents of this file are subject to the Mozilla Public License Version
5  * 1.1 (the "License"); you may not use this file except in compliance with
6  * the License. You may obtain a copy of the License at
7  * http://www.mozilla.org/MPL/
8  *
9  * Software distributed under the License is distributed on an "AS IS" basis,
10  * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
11  * for the specific language governing rights and limitations under the
12  * License.
13  *
14  * The Original Code is Google Suggest Autocomplete Implementation for Firefox.
15  *
16  * The Initial Developer of the Original Code is Google Inc.
17  * Portions created by the Initial Developer are Copyright (C) 2006
18  * the Initial Developer. All Rights Reserved.
19  *
20  * Contributor(s):
21  *   Ben Goodger <beng@google.com>
22  *   Mike Connor <mconnor@mozilla.com>
23  *   Joe Hughes  <joe@retrovirus.com>
24  *   Pamela Greene <pamg.bugs@gmail.com>
25  *
26  * Alternatively, the contents of this file may be used under the terms of
27  * either the GNU General Public License Version 2 or later (the "GPL"), or
28  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
29  * in which case the provisions of the GPL or the LGPL are applicable instead
30  * of those above. If you wish to allow use of your version of this file only
31  * under the terms of either the GPL or the LGPL, and not to allow others to
32  * use your version of this file under the terms of the MPL, indicate your
33  * decision by deleting the provisions above and replace them with the notice
34  * and other provisions required by the GPL or the LGPL. If you do not delete
35  * the provisions above, a recipient may use your version of this file under
36  * the terms of any one of the MPL, the GPL or the LGPL.
37  *
38  * ***** END LICENSE BLOCK ***** */
40 const SEARCH_RESPONSE_SUGGESTION_JSON = "application/x-suggestions+json";
42 const BROWSER_SUGGEST_PREF = "browser.search.suggest.enabled";
43 const XPCOM_SHUTDOWN_TOPIC              = "xpcom-shutdown";
44 const NS_PREFBRANCH_PREFCHANGE_TOPIC_ID = "nsPref:changed";
45 const SEARCH_BUNDLE = "chrome://global/locale/search/search.properties";
47 const Cc = Components.classes;
48 const Ci = Components.interfaces;
49 const Cr = Components.results;
50 const Cu = Components.utils;
52 const HTTP_OK                    = 200;
53 const HTTP_INTERNAL_SERVER_ERROR = 500;
54 const HTTP_BAD_GATEWAY           = 502;
55 const HTTP_SERVICE_UNAVAILABLE   = 503;
57 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
58 Cu.import("resource://gre/modules/JSON.jsm");
60 /**
61  * SuggestAutoCompleteResult contains the results returned by the Suggest
62  * service - it implements nsIAutoCompleteResult and is used by the auto-
63  * complete controller to populate the front end.
64  * @constructor
65  */
66 function SuggestAutoCompleteResult(searchString,
67                                    searchResult,
68                                    defaultIndex,
69                                    errorDescription,
70                                    results,
71                                    comments,
72                                    formHistoryResult) {
73   this._searchString = searchString;
74   this._searchResult = searchResult;
75   this._defaultIndex = defaultIndex;
76   this._errorDescription = errorDescription;
77   this._results = results;
78   this._comments = comments;
79   this._formHistoryResult = formHistoryResult;
81 SuggestAutoCompleteResult.prototype = {
82   /**
83    * The user's query string
84    * @private
85    */
86   _searchString: "",
88   /**
89    * The result code of this result object, see |get searchResult| for possible
90    * values.
91    * @private
92    */
93   _searchResult: 0,
95   /**
96    * The default item that should be entered if none is selected
97    * @private
98    */
99   _defaultIndex: 0,
101   /**
102    * The reason the search failed
103    * @private
104    */
105   _errorDescription: "",
107   /**
108    * The list of words returned by the Suggest Service
109    * @private
110    */
111   _results: [],
113   /**
114    * The list of Comments (number of results - or page titles) returned by the
115    * Suggest Service.
116    * @private
117    */
118   _comments: [],
120   /**
121    * A reference to the form history nsIAutocompleteResult that we're wrapping.
122    * We use this to forward removeEntryAt calls as needed.
123    */
124   _formHistoryResult: null,
126   /**
127    * @return the user's query string
128    */
129   get searchString() {
130     return this._searchString;
131   },
133   /**
134    * @return the result code of this result object, either:
135    *         RESULT_IGNORED   (invalid searchString)
136    *         RESULT_FAILURE   (failure)
137    *         RESULT_NOMATCH   (no matches found)
138    *         RESULT_SUCCESS   (matches found)
139    */
140   get searchResult() {
141     return this._searchResult;
142   },
144   /**
145    * @return the default item that should be entered if none is selected
146    */
147   get defaultIndex() {
148     return this._defaultIndex;
149   },
151   /**
152    * @return the reason the search failed
153    */
154   get errorDescription() {
155     return this._errorDescription;
156   },
158   /**
159    * @return the number of results
160    */
161   get matchCount() {
162     return this._results.length;
163   },
165   /**
166    * Retrieves a result
167    * @param  index    the index of the result requested
168    * @return          the result at the specified index
169    */
170   getValueAt: function(index) {
171     return this._results[index];
172   },
174   /**
175    * Retrieves a comment (metadata instance)
176    * @param  index    the index of the comment requested
177    * @return          the comment at the specified index
178    */
179   getCommentAt: function(index) {
180     return this._comments[index];
181   },
183   /**
184    * Retrieves a style hint specific to a particular index.
185    * @param  index    the index of the style hint requested
186    * @return          the style hint at the specified index
187    */
188   getStyleAt: function(index) {
189     if (!this._comments[index])
190       return null;  // not a category label, so no special styling
192     if (index == 0)
193       return "suggestfirst";  // category label on first line of results
195     return "suggesthint";   // category label on any other line of results
196   },
198   /**
199    * Retrieves an image url.
200    * @param  index    the index of the image url requested
201    * @return          the image url at the specified index
202    */
203   getImageAt: function(index) {
204     return "";
205   },
207   /**
208    * Removes a result from the resultset
209    * @param  index    the index of the result to remove
210    */
211   removeValueAt: function(index, removeFromDatabase) {
212     // Forward the removeValueAt call to the underlying result if we have one
213     // Note: this assumes that the form history results were added to the top
214     // of our arrays.
215     if (removeFromDatabase && this._formHistoryResult &&
216         index < this._formHistoryResult.matchCount) {
217       // Delete the history result from the DB
218       this._formHistoryResult.removeValueAt(index, true);
219     }
220     this._results.splice(index, 1);
221     this._comments.splice(index, 1);
222   },
224   // nsISupports
225   QueryInterface: XPCOMUtils.generateQI([Ci.nsIAutoCompleteResult])
229  * SuggestAutoComplete is a base class that implements nsIAutoCompleteSearch
230  * and can collect results for a given search by using the search URL supplied
231  * by the subclass. We do it this way since the AutoCompleteController in
232  * Mozilla requires a unique XPCOM Service for every search provider, even if
233  * the logic for two providers is identical.
234  * @constructor
235  */
236 function SuggestAutoComplete() {
237   this._init();
239 SuggestAutoComplete.prototype = {
241   _init: function() {
242     this._addObservers();
243     this._loadSuggestPref();
244   },
246   /**
247    * this._strings is the string bundle for message internationalization.
248    */
249   get _strings() {
250     if (!this.__strings) {
251       var sbs = Cc["@mozilla.org/intl/stringbundle;1"].
252                 getService(Ci.nsIStringBundleService);
254       this.__strings = sbs.createBundle(SEARCH_BUNDLE);
255     }
256     return this.__strings;
257   },
258   __strings: null,
260   /**
261    * Search suggestions will be shown if this._suggestEnabled is true.
262    */
263   _loadSuggestPref: function SAC_loadSuggestPref() {
264     var prefService = Cc["@mozilla.org/preferences-service;1"].
265                       getService(Ci.nsIPrefBranch);
266     this._suggestEnabled = prefService.getBoolPref(BROWSER_SUGGEST_PREF);
267   },
268   _suggestEnabled: null,
270   /*************************************************************************
271    * Server request backoff implementation fields below
272    * These allow us to throttle requests if the server is getting hammered.
273    **************************************************************************/
275   /**
276    * This is an array that contains the timestamps (in unixtime) of
277    * the last few backoff-triggering errors.
278    */
279   _serverErrorLog: [],
281   /**
282    * If we receive this number of backoff errors within the amount of time
283    * specified by _serverErrorPeriod, then we initiate backoff.
284    */
285   _maxErrorsBeforeBackoff: 3,
287   /**
288    * If we receive enough consecutive errors (where "enough" is defined by
289    * _maxErrorsBeforeBackoff above) within this time period,
290    * we trigger the backoff behavior.
291    */
292   _serverErrorPeriod: 600000,  // 10 minutes in milliseconds
294   /**
295    * If we get another backoff error immediately after timeout, we increase the
296    * backoff to (2 x old period) + this value.
297    */
298   _serverErrorTimeoutIncrement: 600000,  // 10 minutes in milliseconds
300   /**
301    * The current amount of time to wait before trying a server request
302    * after receiving a backoff error.
303    */
304   _serverErrorTimeout: 0,
306   /**
307    * Time (in unixtime) after which we're allowed to try requesting again.
308    */
309   _nextRequestTime: 0,
311   /**
312    * The last engine we requested against (so that we can tell if the
313    * user switched engines).
314    */
315   _serverErrorEngine: null,
317   /**
318    * The XMLHttpRequest object.
319    * @private
320    */
321   _request: null,
323   /**
324    * The object implementing nsIAutoCompleteObserver that we notify when
325    * we have found results
326    * @private
327    */
328   _listener: null,
330   /**
331    * If this is true, we'll integrate form history results with the
332    * suggest results.
333    */
334   _includeFormHistory: true,
336   /**
337    * True if a request for remote suggestions was sent. This is used to
338    * differentiate between the "_request is null because the request has
339    * already returned a result" and "_request is null because no request was
340    * sent" cases.
341    */
342   _sentSuggestRequest: false,
344   /**
345    * This is the callback for the suggest timeout timer.
346    */
347   notify: function SAC_notify(timer) {
348     // FIXME: bug 387341
349     // Need to break the cycle between us and the timer.
350     this._formHistoryTimer = null;
352     // If this._listener is null, we've already sent out suggest results, so
353     // nothing left to do here.
354     if (!this._listener)
355       return;
357     // Otherwise, the XMLHTTPRequest for suggest results is taking too long,
358     // so send out the form history results and cancel the request.
359     this._listener.onSearchResult(this, this._formHistoryResult);
360     this._reset();
361   },
363   /**
364    * This determines how long (in ms) we should wait before giving up on
365    * the suggestions and just showing local form history results.
366    */
367   _suggestionTimeout: 500,
369   /**
370    * This is the callback for that the form history service uses to
371    * send us results.
372    */
373   onSearchResult: function SAC_onSearchResult(search, result) {
374     this._formHistoryResult = result;
376     if (this._request) {
377       // We still have a pending request, wait a bit to give it a chance to
378       // finish.
379       this._formHistoryTimer = Cc["@mozilla.org/timer;1"].
380                                createInstance(Ci.nsITimer);
381       this._formHistoryTimer.initWithCallback(this, this._suggestionTimeout,
382                                               Ci.nsITimer.TYPE_ONE_SHOT);
383     } else if (!this._sentSuggestRequest) {
384       // We didn't send a request, so just send back the form history results.
385       this._listener.onSearchResult(this, this._formHistoryResult);
386       this._reset();
387     }
388   },
390   /**
391    * This is the URI that the last suggest request was sent to.
392    */
393   _suggestURI: null,
395   /**
396    * Autocomplete results from the form history service get stored here.
397    */
398   _formHistoryResult: null,
400   /**
401    * This holds the suggest server timeout timer, if applicable.
402    */
403   _formHistoryTimer: null,
405   /**
406    * This clears all the per-request state.
407    */
408   _reset: function SAC_reset() {
409     // Don't let go of our listener and form history result if the timer is
410     // still pending, the timer will call _reset() when it fires.
411     if (!this._formHistoryTimer) {
412       this._listener = null;
413       this._formHistoryResult = null;
414     }
415     this._request = null;
416   },
418   /**
419    * This sends an autocompletion request to the form history service,
420    * which will call onSearchResults with the results of the query.
421    */
422   _startHistorySearch: function SAC_SHSearch(searchString, searchParam, previousResult) {
423     var formHistory =
424       Cc["@mozilla.org/autocomplete/search;1?name=form-history"].
425       createInstance(Ci.nsIAutoCompleteSearch);
426     formHistory.startSearch(searchString, searchParam, previousResult, this);
427   },
429   /**
430    * Makes a note of the fact that we've received a backoff-triggering
431    * response, so that we can adjust the backoff behavior appropriately.
432    */
433   _noteServerError: function SAC__noteServeError() {
434     var currentTime = Date.now();
436     this._serverErrorLog.push(currentTime);
437     if (this._serverErrorLog.length > this._maxErrorsBeforeBackoff)
438       this._serverErrorLog.shift();
440     if ((this._serverErrorLog.length == this._maxErrorsBeforeBackoff) &&
441         ((currentTime - this._serverErrorLog[0]) < this._serverErrorPeriod)) {
442       // increase timeout, and then don't request until timeout is over
443       this._serverErrorTimeout = (this._serverErrorTimeout * 2) +
444                                  this._serverErrorTimeoutIncrement;
445       this._nextRequestTime = currentTime + this._serverErrorTimeout;
446     }
447   },
449   /**
450    * Resets the backoff behavior; called when we get a successful response.
451    */
452   _clearServerErrors: function SAC__clearServerErrors() {
453     this._serverErrorLog = [];
454     this._serverErrorTimeout = 0;
455     this._nextRequestTime = 0;
456   },
458   /**
459    * This checks whether we should send a server request (i.e. we're not
460    * in a error-triggered backoff period.
461    *
462    * @private
463    */
464   _okToRequest: function SAC__okToRequest() {
465     return Date.now() > this._nextRequestTime;
466   },
468   /**
469    * This checks to see if the new search engine is different
470    * from the previous one, and if so clears any error state that might
471    * have accumulated for the old engine.
472    *
473    * @param engine The engine that the suggestion request would be sent to.
474    * @private
475    */
476   _checkForEngineSwitch: function SAC__checkForEngineSwitch(engine) {
477     if (engine == this._serverErrorEngine)
478       return;
480     // must've switched search providers, clear old errors
481     this._serverErrorEngine = engine;
482     this._clearServerErrors();
483   },
485   /**
486    * This returns true if the status code of the HTTP response
487    * represents a backoff-triggering error.
488    *
489    * @param status  The status code from the HTTP response
490    * @private
491    */
492   _isBackoffError: function SAC__isBackoffError(status) {
493     return ((status == HTTP_INTERNAL_SERVER_ERROR) ||
494             (status == HTTP_BAD_GATEWAY) ||
495             (status == HTTP_SERVICE_UNAVAILABLE));
496   },
498   /**
499    * Called when the 'readyState' of the XMLHttpRequest changes. We only care
500    * about state 4 (COMPLETED) - handle the response data.
501    * @private
502    */
503   onReadyStateChange: function() {
504     // xxx use the real const here
505     if (!this._request || this._request.readyState != 4)
506       return;
508     try {
509       var status = this._request.status;
510     } catch (e) {
511       // The XML HttpRequest can throw NS_ERROR_NOT_AVAILABLE.
512       return;
513     }
515     if (this._isBackoffError(status)) {
516       this._noteServerError();
517       return;
518     }
520     var responseText = this._request.responseText;
521     if (status != HTTP_OK || responseText == "")
522       return;
524     this._clearServerErrors();
526     var serverResults = JSON.fromString(responseText);
527     var searchString = serverResults[0] || "";
528     var results = serverResults[1] || [];
530     var comments = [];  // "comments" column values for suggestions
531     var historyResults = [];
532     var historyComments = [];
534     // If form history is enabled and has results, add them to the list.
535     if (this._includeFormHistory && this._formHistoryResult &&
536         (this._formHistoryResult.searchResult ==
537          Ci.nsIAutoCompleteResult.RESULT_SUCCESS)) {
538       for (var i = 0; i < this._formHistoryResult.matchCount; ++i) {
539         var term = this._formHistoryResult.getValueAt(i);
541         // we don't want things to appear in both history and suggestions
542         var dupIndex = results.indexOf(term);
543         if (dupIndex != -1)
544           results.splice(dupIndex, 1);
546         historyResults.push(term);
547         historyComments.push("");
548       }
549     }
551     // fill out the comment column for the suggestions
552     for (var i = 0; i < results.length; ++i)
553       comments.push("");
555     // if we have any suggestions, put a label at the top
556     if (comments.length > 0)
557       comments[0] = this._strings.GetStringFromName("suggestion_label");
559     // now put the history results above the suggestions
560     var finalResults = historyResults.concat(results);
561     var finalComments = historyComments.concat(comments);
563     // Notify the FE of our new results
564     this.onResultsReady(searchString, finalResults, finalComments,
565                         this._formHistoryResult);
567     // Reset our state for next time.
568     this._reset();
569   },
571   /**
572    * Notifies the front end of new results.
573    * @param searchString  the user's query string
574    * @param results       an array of results to the search
575    * @param comments      an array of metadata corresponding to the results
576    * @private
577    */
578   onResultsReady: function(searchString, results, comments,
579                            formHistoryResult) {
580     if (this._listener) {
581       var result = new SuggestAutoCompleteResult(
582           searchString,
583           Ci.nsIAutoCompleteResult.RESULT_SUCCESS,
584           0,
585           "",
586           results,
587           comments,
588           formHistoryResult);
590       this._listener.onSearchResult(this, result);
592       // Null out listener to make sure we don't notify it twice, in case our
593       // timer callback still hasn't run.
594       this._listener = null;
595     }
596   },
598   /**
599    * Initiates the search result gathering process. Part of
600    * nsIAutoCompleteSearch implementation.
601    *
602    * @param searchString    the user's query string
603    * @param searchParam     unused, "an extra parameter"; even though
604    *                        this parameter and the next are unused, pass
605    *                        them through in case the form history
606    *                        service wants them
607    * @param previousResult  unused, a client-cached store of the previous
608    *                        generated resultset for faster searching.
609    * @param listener        object implementing nsIAutoCompleteObserver which
610    *                        we notify when results are ready.
611    */
612   startSearch: function(searchString, searchParam, previousResult, listener) {
613     var searchService = Cc["@mozilla.org/browser/search-service;1"].
614                         getService(Ci.nsIBrowserSearchService);
616     // If there's an existing request, stop it. There is no smart filtering
617     // here as there is when looking through history/form data because the
618     // result set returned by the server is different for every typed value -
619     // "ocean breathes" does not return a subset of the results returned for
620     // "ocean", for example. This does nothing if there is no current request.
621     this.stopSearch();
623     this._listener = listener;
625     var engine = searchService.currentEngine;
627     this._checkForEngineSwitch(engine);
629     if (!searchString ||
630         !this._suggestEnabled ||
631         !engine.supportsResponseType(SEARCH_RESPONSE_SUGGESTION_JSON) ||
632         !this._okToRequest()) {
633       // We have an empty search string (user pressed down arrow to see
634       // history), or search suggestions are disabled, or the current engine
635       // has no suggest functionality, or we're in backoff mode; so just use
636       // local history.
637       this._sentSuggestRequest = false;
638       this._startHistorySearch(searchString, searchParam, previousResult);
639       return;
640     }
642     // Actually do the search
643     this._request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
644                     createInstance(Ci.nsIXMLHttpRequest);
645     var submission = engine.getSubmission(searchString,
646                                           SEARCH_RESPONSE_SUGGESTION_JSON);
647     this._suggestURI = submission.uri;
648     var method = (submission.postData ? "POST" : "GET");
649     this._request.open(method, this._suggestURI.spec, true);
650     this._request.channel.notificationCallbacks = new SearchSuggestLoadListener();
652     var self = this;
653     function onReadyStateChange() {
654       self.onReadyStateChange();
655     }
656     this._request.onreadystatechange = onReadyStateChange;
657     this._request.send(submission.postData);
659     if (this._includeFormHistory) {
660       this._sentSuggestRequest = true;
661       this._startHistorySearch(searchString, searchParam, previousResult);
662     }
663   },
665   /**
666    * Ends the search result gathering process. Part of nsIAutoCompleteSearch
667    * implementation.
668    */
669   stopSearch: function() {
670     if (this._request) {
671       this._request.abort();
672       this._reset();
673     }
674   },
676   /**
677    * nsIObserver
678    */
679   observe: function SAC_observe(aSubject, aTopic, aData) {
680     switch (aTopic) {
681       case NS_PREFBRANCH_PREFCHANGE_TOPIC_ID:
682         this._loadSuggestPref();
683         break;
684       case XPCOM_SHUTDOWN_TOPIC:
685         this._removeObservers();
686         break;
687     }
688   },
690   _addObservers: function SAC_addObservers() {
691     var prefService2 = Cc["@mozilla.org/preferences-service;1"].
692                        getService(Ci.nsIPrefBranch2);
693     prefService2.addObserver(BROWSER_SUGGEST_PREF, this, false);
695     var os = Cc["@mozilla.org/observer-service;1"].
696              getService(Ci.nsIObserverService);
697     os.addObserver(this, XPCOM_SHUTDOWN_TOPIC, false);
698   },
700   _removeObservers: function SAC_removeObservers() {
701     var prefService2 = Cc["@mozilla.org/preferences-service;1"].
702                        getService(Ci.nsIPrefBranch2);
703     prefService2.removeObserver(BROWSER_SUGGEST_PREF, this);
705     var os = Cc["@mozilla.org/observer-service;1"].
706              getService(Ci.nsIObserverService);
707     os.removeObserver(this, XPCOM_SHUTDOWN_TOPIC);
708   },
710   // nsISupports
711   QueryInterface: XPCOMUtils.generateQI([Ci.nsIAutoCompleteSearch,
712                                          Ci.nsIAutoCompleteObserver])
715 function SearchSuggestLoadListener() {
717 SearchSuggestLoadListener.prototype = {
718   // nsIBadCertListener2
719   notifyCertProblem: function SSLL_certProblem(socketInfo, status, targetSite) {
720     return true;
721   },
723   // nsISSLErrorListener
724   notifySSLError: function SSLL_SSLError(socketInfo, error, targetSite) {
725     return true;
726   },
728   // nsIInterfaceRequestor
729   getInterface: function SSLL_getInterface(iid) {
730     return this.QueryInterface(iid);
731   },
733   // nsISupports
734   QueryInterface: XPCOMUtils.generateQI([Ci.nsIBadCertListener2,
735                                          Ci.nsISSLErrorListener,
736                                          Ci.nsIInterfaceRequestor])
740  * SearchSuggestAutoComplete is a service implementation that handles suggest
741  * results specific to web searches.
742  * @constructor
743  */
744 function SearchSuggestAutoComplete() {
745   // This calls _init() in the parent class (SuggestAutoComplete) via the
746   // prototype, below.
747   this._init();
749 SearchSuggestAutoComplete.prototype = {
750   classDescription: "Remote Search Suggestions",
751   contractID: "@mozilla.org/autocomplete/search;1?name=search-autocomplete",
752   classID: Components.ID("{aa892eb4-ffbf-477d-9f9a-06c995ae9f27}"),
753   __proto__: SuggestAutoComplete.prototype,
754   serviceURL: ""
757 var component = [SearchSuggestAutoComplete];
758 function NSGetModule(compMgr, fileSpec) {
759   return XPCOMUtils.generateModule(component);