summaryrefslogtreecommitdiffstats
path: root/toolkit/components/search/current/SearchSuggestionController.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/search/current/SearchSuggestionController.jsm')
-rw-r--r--toolkit/components/search/current/SearchSuggestionController.jsm398
1 files changed, 0 insertions, 398 deletions
diff --git a/toolkit/components/search/current/SearchSuggestionController.jsm b/toolkit/components/search/current/SearchSuggestionController.jsm
deleted file mode 100644
index 952838c0c..000000000
--- a/toolkit/components/search/current/SearchSuggestionController.jsm
+++ /dev/null
@@ -1,398 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-"use strict";
-
-this.EXPORTED_SYMBOLS = ["SearchSuggestionController"];
-
-const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
-
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/Promise.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "NS_ASSERT", "resource://gre/modules/debug.js");
-
-const SEARCH_RESPONSE_SUGGESTION_JSON = "application/x-suggestions+json";
-const DEFAULT_FORM_HISTORY_PARAM = "searchbar-history";
-const HTTP_OK = 200;
-const REMOTE_TIMEOUT = 500; // maximum time (ms) to wait before giving up on a remote suggestions
-const BROWSER_SUGGEST_PREF = "browser.search.suggest.enabled";
-
-/**
- * Remote search suggestions will be shown if gRemoteSuggestionsEnabled
- * is true. Global because only one pref observer is needed for all instances.
- */
-var gRemoteSuggestionsEnabled = Services.prefs.getBoolPref(BROWSER_SUGGEST_PREF);
-Services.prefs.addObserver(BROWSER_SUGGEST_PREF, function(aSubject, aTopic, aData) {
- gRemoteSuggestionsEnabled = Services.prefs.getBoolPref(BROWSER_SUGGEST_PREF);
-}, false);
-
-/**
- * SearchSuggestionController.jsm exists as a helper module to allow multiple consumers to request and display
- * search suggestions from a given engine, regardless of the base implementation. Much of this
- * code was originally in nsSearchSuggestions.js until it was refactored to separate it from the
- * nsIAutoCompleteSearch dependency.
- * One instance of SearchSuggestionController should be used per field since form history results are cached.
- */
-
-/**
- * @param {function} [callback] - Callback for search suggestion results. You can use the promise
- * returned by the search method instead if you prefer.
- * @constructor
- */
-this.SearchSuggestionController = function SearchSuggestionController(callback = null) {
- this._callback = callback;
-};
-
-this.SearchSuggestionController.prototype = {
- /**
- * The maximum number of local form history results to return. This limit is
- * only enforced if remote results are also returned.
- */
- maxLocalResults: 5,
-
- /**
- * The maximum number of remote search engine results to return.
- * We'll actually only display at most
- * maxRemoteResults - <displayed local results count> remote results.
- */
- maxRemoteResults: 10,
-
- /**
- * The maximum time (ms) to wait before giving up on a remote suggestions.
- */
- remoteTimeout: REMOTE_TIMEOUT,
-
- /**
- * The additional parameter used when searching form history.
- */
- formHistoryParam: DEFAULT_FORM_HISTORY_PARAM,
-
- // Private properties
- /**
- * The last form history result used to improve the performance of subsequent searches.
- * This shouldn't be used for any other purpose as it is never cleared and therefore could be stale.
- */
- _formHistoryResult: null,
-
- /**
- * The remote server timeout timer, if applicable. The timer starts when form history
- * search is completed.
- */
- _remoteResultTimer: null,
-
- /**
- * The deferred for the remote results before its promise is resolved.
- */
- _deferredRemoteResult: null,
-
- /**
- * The optional result callback registered from the constructor.
- */
- _callback: null,
-
- /**
- * The XMLHttpRequest object for remote results.
- */
- _request: null,
-
- // Public methods
-
- /**
- * Fetch search suggestions from all of the providers. Fetches in progress will be stopped and
- * results from them will not be provided.
- *
- * @param {string} searchTerm - the term to provide suggestions for
- * @param {bool} privateMode - whether the request is being made in the context of private browsing
- * @param {nsISearchEngine} engine - search engine for the suggestions.
- * @param {int} userContextId - the userContextId of the selected tab.
- *
- * @return {Promise} resolving to an object containing results or null.
- */
- fetch: function(searchTerm, privateMode, engine, userContextId) {
- // There is no smart filtering from previous results here (as there is when looking through
- // history/form data) because the result set returned by the server is different for every typed
- // value - e.g. "ocean breathes" does not return a subset of the results returned for "ocean".
-
- this.stop();
-
- if (!Services.search.isInitialized) {
- throw new Error("Search not initialized yet (how did you get here?)");
- }
- if (typeof privateMode === "undefined") {
- throw new Error("The privateMode argument is required to avoid unintentional privacy leaks");
- }
- if (!(engine instanceof Ci.nsISearchEngine)) {
- throw new Error("Invalid search engine");
- }
- if (!this.maxLocalResults && !this.maxRemoteResults) {
- throw new Error("Zero results expected, what are you trying to do?");
- }
- if (this.maxLocalResults < 0 || this.maxRemoteResults < 0) {
- throw new Error("Number of requested results must be positive");
- }
-
- // Array of promises to resolve before returning results.
- let promises = [];
- this._searchString = searchTerm;
-
- // Remote results
- if (searchTerm && gRemoteSuggestionsEnabled && this.maxRemoteResults &&
- engine.supportsResponseType(SEARCH_RESPONSE_SUGGESTION_JSON)) {
- this._deferredRemoteResult = this._fetchRemote(searchTerm, engine, privateMode, userContextId);
- promises.push(this._deferredRemoteResult.promise);
- }
-
- // Local results from form history
- if (this.maxLocalResults) {
- let deferredHistoryResult = this._fetchFormHistory(searchTerm);
- promises.push(deferredHistoryResult.promise);
- }
-
- function handleRejection(reason) {
- if (reason == "HTTP request aborted") {
- // Do nothing since this is normal.
- return null;
- }
- Cu.reportError("SearchSuggestionController rejection: " + reason);
- return null;
- }
- return Promise.all(promises).then(this._dedupeAndReturnResults.bind(this), handleRejection);
- },
-
- /**
- * Stop pending fetches so no results are returned from them.
- *
- * Note: If there was no remote results fetched, the fetching cannot be stopped and local results
- * will still be returned because stopping relies on aborting the XMLHTTPRequest to reject the
- * promise for Promise.all.
- */
- stop: function() {
- if (this._request) {
- this._request.abort();
- } else if (!this.maxRemoteResults) {
- Cu.reportError("SearchSuggestionController: Cannot stop fetching if remote results were not "+
- "requested");
- }
- this._reset();
- },
-
- // Private methods
-
- _fetchFormHistory: function(searchTerm) {
- let deferredFormHistory = Promise.defer();
-
- let acSearchObserver = {
- // Implements nsIAutoCompleteSearch
- onSearchResult: (search, result) => {
- this._formHistoryResult = result;
-
- if (this._request) {
- this._remoteResultTimer = Cc["@mozilla.org/timer;1"].
- createInstance(Ci.nsITimer);
- this._remoteResultTimer.initWithCallback(this._onRemoteTimeout.bind(this),
- this.remoteTimeout || REMOTE_TIMEOUT,
- Ci.nsITimer.TYPE_ONE_SHOT);
- }
-
- switch (result.searchResult) {
- case Ci.nsIAutoCompleteResult.RESULT_SUCCESS:
- case Ci.nsIAutoCompleteResult.RESULT_NOMATCH:
- if (result.searchString !== this._searchString) {
- deferredFormHistory.resolve("Unexpected response, this._searchString does not match form history response");
- return;
- }
- let fhEntries = [];
- for (let i = 0; i < result.matchCount; ++i) {
- fhEntries.push(result.getValueAt(i));
- }
- deferredFormHistory.resolve({
- result: fhEntries,
- formHistoryResult: result,
- });
- break;
- case Ci.nsIAutoCompleteResult.RESULT_FAILURE:
- case Ci.nsIAutoCompleteResult.RESULT_IGNORED:
- deferredFormHistory.resolve("Form History returned RESULT_FAILURE or RESULT_IGNORED");
- break;
- }
- },
- };
-
- let formHistory = Cc["@mozilla.org/autocomplete/search;1?name=form-history"].
- createInstance(Ci.nsIAutoCompleteSearch);
- formHistory.startSearch(searchTerm, this.formHistoryParam || DEFAULT_FORM_HISTORY_PARAM,
- this._formHistoryResult,
- acSearchObserver);
- return deferredFormHistory;
- },
-
- /**
- * Fetch suggestions from the search engine over the network.
- */
- _fetchRemote: function(searchTerm, engine, privateMode, userContextId) {
- let deferredResponse = Promise.defer();
- this._request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
- createInstance(Ci.nsIXMLHttpRequest);
- let submission = engine.getSubmission(searchTerm,
- SEARCH_RESPONSE_SUGGESTION_JSON);
- let method = (submission.postData ? "POST" : "GET");
- this._request.open(method, submission.uri.spec, true);
-
- this._request.setOriginAttributes({userContextId,
- privateBrowsingId: privateMode ? 1 : 0});
-
- this._request.mozBackgroundRequest = true; // suppress dialogs and fail silently
-
- this._request.addEventListener("load", this._onRemoteLoaded.bind(this, deferredResponse));
- this._request.addEventListener("error", (evt) => deferredResponse.resolve("HTTP error"));
- // Reject for an abort assuming it's always from .stop() in which case we shouldn't return local
- // or remote results for existing searches.
- this._request.addEventListener("abort", (evt) => deferredResponse.reject("HTTP request aborted"));
-
- this._request.send(submission.postData);
-
- return deferredResponse;
- },
-
- /**
- * Called when the request completed successfully (thought the HTTP status could be anything)
- * so we can handle the response data.
- * @private
- */
- _onRemoteLoaded: function(deferredResponse) {
- if (!this._request) {
- deferredResponse.resolve("Got HTTP response after the request was cancelled");
- return;
- }
-
- let status, serverResults;
- try {
- status = this._request.status;
- } catch (e) {
- // The XMLHttpRequest can throw NS_ERROR_NOT_AVAILABLE.
- deferredResponse.resolve("Unknown HTTP status: " + e);
- return;
- }
-
- if (status != HTTP_OK || this._request.responseText == "") {
- deferredResponse.resolve("Non-200 status or empty HTTP response: " + status);
- return;
- }
-
- try {
- serverResults = JSON.parse(this._request.responseText);
- } catch (ex) {
- deferredResponse.resolve("Failed to parse suggestion JSON: " + ex);
- return;
- }
-
- if (!serverResults[0] ||
- this._searchString.localeCompare(serverResults[0], undefined,
- { sensitivity: "base" })) {
- // something is wrong here so drop remote results
- deferredResponse.resolve("Unexpected response, this._searchString does not match remote response");
- return;
- }
- let results = serverResults[1] || [];
- deferredResponse.resolve({ result: results });
- },
-
- /**
- * Called when this._remoteResultTimer fires indicating the remote request took too long.
- */
- _onRemoteTimeout: function () {
- this._request = null;
-
- // FIXME: bug 387341
- // Need to break the cycle between us and the timer.
- this._remoteResultTimer = null;
-
- // The XMLHTTPRequest for suggest results is taking too long
- // so send out the form history results and cancel the request.
- if (this._deferredRemoteResult) {
- this._deferredRemoteResult.resolve("HTTP Timeout");
- this._deferredRemoteResult = null;
- }
- },
-
- /**
- * @param {Array} suggestResults - an array of result objects from different sources (local or remote)
- * @return {Object}
- */
- _dedupeAndReturnResults: function(suggestResults) {
- if (this._searchString === null) {
- // _searchString can be null if stop() was called and remote suggestions
- // were disabled (stopping if we are fetching remote suggestions will
- // cause a promise rejection before we reach _dedupeAndReturnResults).
- return null;
- }
-
- let results = {
- term: this._searchString,
- remote: [],
- local: [],
- formHistoryResult: null,
- };
-
- for (let result of suggestResults) {
- if (typeof result === "string") { // Failure message
- Cu.reportError("SearchSuggestionController: " + result);
- } else if (result.formHistoryResult) { // Local results have a formHistoryResult property.
- results.formHistoryResult = result.formHistoryResult;
- results.local = result.result || [];
- } else { // Remote result
- results.remote = result.result || [];
- }
- }
-
- // If we have remote results, cap the number of local results
- if (results.remote.length) {
- results.local = results.local.slice(0, this.maxLocalResults);
- }
-
- // We don't want things to appear in both history and suggestions so remove entries from
- // remote results that are already in local.
- if (results.remote.length && results.local.length) {
- for (let i = 0; i < results.local.length; ++i) {
- let term = results.local[i];
- let dupIndex = results.remote.indexOf(term);
- if (dupIndex != -1) {
- results.remote.splice(dupIndex, 1);
- }
- }
- }
-
- // Trim the number of results to the maximum requested (now that we've pruned dupes).
- results.remote =
- results.remote.slice(0, this.maxRemoteResults - results.local.length);
-
- if (this._callback) {
- this._callback(results);
- }
- this._reset();
-
- return results;
- },
-
- _reset: function() {
- this._request = null;
- if (this._remoteResultTimer) {
- this._remoteResultTimer.cancel();
- this._remoteResultTimer = null;
- }
- this._deferredRemoteResult = null;
- this._searchString = null;
- },
-};
-
-/**
- * Determines whether the given engine offers search suggestions.
- *
- * @param {nsISearchEngine} engine - The search engine
- * @return {boolean} True if the engine offers suggestions and false otherwise.
- */
-this.SearchSuggestionController.engineOffersSuggestions = function(engine) {
- return engine.supportsResponseType(SEARCH_RESPONSE_SUGGESTION_JSON);
-};