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://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;
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);