1 /* ***** BEGIN LICENSE BLOCK *****
2 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
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/
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
14 * The Original Code is Google Suggest Autocomplete Implementation for Firefox.
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.
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>
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.
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://browser/locale/search.properties";
47 const Cc
= Components
.classes
;
48 const Ci
= Components
.interfaces
;
49 const Cr
= Components
.results
;
50 const Cu
= Components
.utils
;
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");
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.
66 function SuggestAutoCompleteResult(searchString
,
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 = {
83 * The user's query string
89 * The result code of this result object, see |get searchResult| for possible
96 * The default item that should be entered if none is selected
102 * The reason the search failed
105 _errorDescription
: "",
108 * The list of words returned by the Suggest Service
114 * The list of Comments (number of results - or page titles) returned by the
121 * A reference to the form history nsIAutocompleteResult that we're wrapping.
122 * We use this to forward removeEntryAt calls as needed.
124 _formHistoryResult
: null,
127 * @return the user's query string
130 return this._searchString
;
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)
141 return this._searchResult
;
145 * @return the default item that should be entered if none is selected
148 return this._defaultIndex
;
152 * @return the reason the search failed
154 get errorDescription() {
155 return this._errorDescription
;
159 * @return the number of results
162 return this._results
.length
;
167 * @param index the index of the result requested
168 * @return the result at the specified index
170 getValueAt: function(index
) {
171 return this._results
[index
];
175 * Retrieves a comment (metadata instance)
176 * @param index the index of the comment requested
177 * @return the comment at the specified index
179 getCommentAt: function(index
) {
180 return this._comments
[index
];
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
188 getStyleAt: function(index
) {
189 if (!this._comments
[index
])
190 return null; // not a category label, so no special styling
193 return "suggestfirst"; // category label on first line of results
195 return "suggesthint"; // category label on any other line of results
199 * Retrieves an image url.
200 * @param index the index of the image url requested
201 * @return the image url at the specified index
203 getImageAt: function(index
) {
208 * Removes a result from the resultset
209 * @param index the index of the result to remove
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
215 if (removeFromDatabase
&& this._formHistoryResult
&&
216 index
< this._formHistoryResult
.matchCount
) {
217 // Delete the history result from the DB
218 this._formHistoryResult
.removeValueAt(index
, true);
220 this._results
.splice(index
, 1);
221 this._comments
.splice(index
, 1);
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.
236 function SuggestAutoComplete() {
239 SuggestAutoComplete
.prototype = {
242 this._addObservers();
243 this._loadSuggestPref();
247 * this._strings is the string bundle for message internationalization.
250 if (!this.__strings
) {
251 var sbs
= Cc
["@mozilla.org/intl/stringbundle;1"].
252 getService(Ci
.nsIStringBundleService
);
254 this.__strings
= sbs
.createBundle(SEARCH_BUNDLE
);
256 return this.__strings
;
261 * Search suggestions will be shown if this._suggestEnabled is true.
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
);
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 **************************************************************************/
276 * This is an array that contains the timestamps (in unixtime) of
277 * the last few backoff-triggering errors.
282 * If we receive this number of backoff errors within the amount of time
283 * specified by _serverErrorPeriod, then we initiate backoff.
285 _maxErrorsBeforeBackoff
: 3,
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.
292 _serverErrorPeriod
: 600000, // 10 minutes in milliseconds
295 * If we get another backoff error immediately after timeout, we increase the
296 * backoff to (2 x old period) + this value.
298 _serverErrorTimeoutIncrement
: 600000, // 10 minutes in milliseconds
301 * The current amount of time to wait before trying a server request
302 * after receiving a backoff error.
304 _serverErrorTimeout
: 0,
307 * Time (in unixtime) after which we're allowed to try requesting again.
312 * The last engine we requested against (so that we can tell if the
313 * user switched engines).
315 _serverErrorEngine
: null,
318 * The XMLHttpRequest object.
324 * The object implementing nsIAutoCompleteObserver that we notify when
325 * we have found results
331 * If this is true, we'll integrate form history results with the
334 _includeFormHistory
: true,
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
342 _sentSuggestRequest
: false,
345 * This is the callback for the suggest timeout timer.
347 notify
: function SAC_notify(timer
) {
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.
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
);
364 * This determines how long (in ms) we should wait before giving up on
365 * the suggestions and just showing local form history results.
367 _suggestionTimeout
: 500,
370 * This is the callback for that the form history service uses to
373 onSearchResult
: function SAC_onSearchResult(search
, result
) {
374 this._formHistoryResult
= result
;
377 // We still have a pending request, wait a bit to give it a chance to
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
);
391 * This is the URI that the last suggest request was sent to.
396 * Autocomplete results from the form history service get stored here.
398 _formHistoryResult
: null,
401 * This holds the suggest server timeout timer, if applicable.
403 _formHistoryTimer
: null,
406 * This clears all the per-request state.
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;
415 this._request
= null;
419 * This sends an autocompletion request to the form history service,
420 * which will call onSearchResults with the results of the query.
422 _startHistorySearch
: function SAC_SHSearch(searchString
, searchParam
, previousResult
) {
424 Cc
["@mozilla.org/autocomplete/search;1?name=form-history"].
425 createInstance(Ci
.nsIAutoCompleteSearch
);
426 formHistory
.startSearch(searchString
, searchParam
, previousResult
, this);
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.
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
;
450 * Resets the backoff behavior; called when we get a successful response.
452 _clearServerErrors
: function SAC__clearServerErrors() {
453 this._serverErrorLog
= [];
454 this._serverErrorTimeout
= 0;
455 this._nextRequestTime
= 0;
459 * This checks whether we should send a server request (i.e. we're not
460 * in a error-triggered backoff period.
464 _okToRequest
: function SAC__okToRequest() {
465 return Date
.now() > this._nextRequestTime
;
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.
473 * @param engine The engine that the suggestion request would be sent to.
476 _checkForEngineSwitch
: function SAC__checkForEngineSwitch(engine
) {
477 if (engine
== this._serverErrorEngine
)
480 // must've switched search providers, clear old errors
481 this._serverErrorEngine
= engine
;
482 this._clearServerErrors();
486 * This returns true if the status code of the HTTP response
487 * represents a backoff-triggering error.
489 * @param status The status code from the HTTP response
492 _isBackoffError
: function SAC__isBackoffError(status
) {
493 return ((status
== HTTP_INTERNAL_SERVER_ERROR
) ||
494 (status
== HTTP_BAD_GATEWAY
) ||
495 (status
== HTTP_SERVICE_UNAVAILABLE
));
499 * Called when the 'readyState' of the XMLHttpRequest changes. We only care
500 * about state 4 (COMPLETED) - handle the response data.
503 onReadyStateChange: function() {
504 // xxx use the real const here
505 if (!this._request
|| this._request
.readyState
!= 4)
509 var status
= this._request
.status
;
511 // The XML HttpRequest can throw NS_ERROR_NOT_AVAILABLE.
515 if (this._isBackoffError(status
)) {
516 this._noteServerError();
520 var responseText
= this._request
.responseText
;
521 if (status
!= HTTP_OK
|| responseText
== "")
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
);
544 results
.splice(dupIndex
, 1);
546 historyResults
.push(term
);
547 historyComments
.push("");
551 // fill out the comment column for the suggestions
552 for (var i
= 0; i
< results
.length
; ++i
)
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.
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
578 onResultsReady: function(searchString
, results
, comments
,
580 if (this._listener
) {
581 var result
= new SuggestAutoCompleteResult(
583 Ci
.nsIAutoCompleteResult
.RESULT_SUCCESS
,
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;
599 * Initiates the search result gathering process. Part of
600 * nsIAutoCompleteSearch implementation.
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
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.
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.
623 this._listener
= listener
;
625 var engine
= searchService
.currentEngine
;
627 this._checkForEngineSwitch(engine
);
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
637 this._sentSuggestRequest
= false;
638 this._startHistorySearch(searchString
, searchParam
, previousResult
);
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();
653 function onReadyStateChange() {
654 self
.onReadyStateChange();
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
);
666 * Ends the search result gathering process. Part of nsIAutoCompleteSearch
669 stopSearch: function() {
671 this._request
.abort();
679 observe
: function SAC_observe(aSubject
, aTopic
, aData
) {
681 case NS_PREFBRANCH_PREFCHANGE_TOPIC_ID
:
682 this._loadSuggestPref();
684 case XPCOM_SHUTDOWN_TOPIC
:
685 this._removeObservers();
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);
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
);
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
) {
723 // nsISSLErrorListener
724 notifySSLError
: function SSLL_SSLError(socketInfo
, error
, targetSite
) {
728 // nsIInterfaceRequestor
729 getInterface
: function SSLL_getInterface(iid
) {
730 return this.QueryInterface(iid
);
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.
744 function SearchSuggestAutoComplete() {
745 // This calls _init() in the parent class (SuggestAutoComplete) via the
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,
757 var component
= [SearchSuggestAutoComplete
];
758 function NSGetModule(compMgr
, fileSpec
) {
759 return XPCOMUtils
.generateModule(component
);