summaryrefslogtreecommitdiffstats
path: root/toolkit/components/places/PlacesSearchAutocompleteProvider.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/places/PlacesSearchAutocompleteProvider.jsm')
-rw-r--r--toolkit/components/places/PlacesSearchAutocompleteProvider.jsm295
1 files changed, 295 insertions, 0 deletions
diff --git a/toolkit/components/places/PlacesSearchAutocompleteProvider.jsm b/toolkit/components/places/PlacesSearchAutocompleteProvider.jsm
new file mode 100644
index 000000000..f4d8f3973
--- /dev/null
+++ b/toolkit/components/places/PlacesSearchAutocompleteProvider.jsm
@@ -0,0 +1,295 @@
+/* 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/. */
+
+/*
+ * Provides functions to handle search engine URLs in the browser history.
+ */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = [ "PlacesSearchAutocompleteProvider" ];
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "SearchSuggestionController",
+ "resource://gre/modules/SearchSuggestionController.jsm");
+
+const SEARCH_ENGINE_TOPIC = "browser-search-engine-modified";
+
+const SearchAutocompleteProviderInternal = {
+ /**
+ * Array of objects in the format returned by findMatchByToken.
+ */
+ priorityMatches: null,
+
+ /**
+ * Array of objects in the format returned by findMatchByAlias.
+ */
+ aliasMatches: null,
+
+ /**
+ * Object for the default search match.
+ **/
+ defaultMatch: null,
+
+ initialize: function () {
+ return new Promise((resolve, reject) => {
+ Services.search.init(status => {
+ if (!Components.isSuccessCode(status)) {
+ reject(new Error("Unable to initialize search service."));
+ }
+
+ try {
+ // The initial loading of the search engines must succeed.
+ this._refresh();
+
+ Services.obs.addObserver(this, SEARCH_ENGINE_TOPIC, true);
+
+ this.initialized = true;
+ resolve();
+ } catch (ex) {
+ reject(ex);
+ }
+ });
+ });
+ },
+
+ initialized: false,
+
+ observe: function (subject, topic, data) {
+ switch (data) {
+ case "engine-added":
+ case "engine-changed":
+ case "engine-removed":
+ case "engine-current":
+ this._refresh();
+ }
+ },
+
+ _refresh: function () {
+ this.priorityMatches = [];
+ this.aliasMatches = [];
+ this.defaultMatch = null;
+
+ let currentEngine = Services.search.currentEngine;
+ // This can be null in XCPShell.
+ if (currentEngine) {
+ this.defaultMatch = {
+ engineName: currentEngine.name,
+ iconUrl: currentEngine.iconURI ? currentEngine.iconURI.spec : null,
+ }
+ }
+
+ // The search engines will always be processed in the order returned by the
+ // search service, which can be defined by the user.
+ Services.search.getVisibleEngines().forEach(e => this._addEngine(e));
+ },
+
+ _addEngine: function (engine) {
+ if (engine.alias) {
+ this.aliasMatches.push({
+ alias: engine.alias,
+ engineName: engine.name,
+ iconUrl: engine.iconURI ? engine.iconURI.spec : null,
+ });
+ }
+
+ let domain = engine.getResultDomain();
+ if (domain) {
+ this.priorityMatches.push({
+ token: domain,
+ // The searchForm property returns a simple URL for the search engine, but
+ // we may need an URL which includes an affiliate code (bug 990799).
+ url: engine.searchForm,
+ engineName: engine.name,
+ iconUrl: engine.iconURI ? engine.iconURI.spec : null,
+ });
+ }
+ },
+
+ getSuggestionController(searchToken, inPrivateContext, maxResults, userContextId) {
+ let engine = Services.search.currentEngine;
+ if (!engine) {
+ return null;
+ }
+ return new SearchSuggestionControllerWrapper(engine, searchToken,
+ inPrivateContext, maxResults,
+ userContextId);
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
+ Ci.nsISupportsWeakReference]),
+}
+
+function SearchSuggestionControllerWrapper(engine, searchToken,
+ inPrivateContext, maxResults,
+ userContextId) {
+ this._controller = new SearchSuggestionController();
+ this._controller.maxLocalResults = 0;
+ this._controller.maxRemoteResults = maxResults;
+ let promise = this._controller.fetch(searchToken, inPrivateContext, engine, userContextId);
+ this._suggestions = [];
+ this._success = false;
+ this._promise = promise.then(results => {
+ this._success = true;
+ this._suggestions = (results ? results.remote : null) || [];
+ }).catch(err => {
+ // fetch() rejects its promise if there's a pending request.
+ });
+}
+
+SearchSuggestionControllerWrapper.prototype = {
+
+ /**
+ * Resolved when all suggestions have been fetched.
+ */
+ get fetchCompletePromise() {
+ return this._promise;
+ },
+
+ /**
+ * Returns one suggestion, if any are available. The returned value is an
+ * array [match, suggestion]. If none are available, returns [null, null].
+ * Note that there are two reasons that suggestions might not be available:
+ * all suggestions may have been fetched and consumed, or the fetch may not
+ * have completed yet.
+ *
+ * @return An array [match, suggestion].
+ */
+ consume() {
+ return !this._suggestions.length ? [null, null] :
+ [SearchAutocompleteProviderInternal.defaultMatch,
+ this._suggestions.shift()];
+ },
+
+ /**
+ * Returns the number of fetched suggestions, or -1 if the fetching was
+ * incomplete or failed.
+ */
+ get resultsCount() {
+ return this._success ? this._suggestions.length : -1;
+ },
+
+ /**
+ * Stops the fetch.
+ */
+ stop() {
+ this._controller.stop();
+ },
+};
+
+var gInitializationPromise = null;
+
+this.PlacesSearchAutocompleteProvider = Object.freeze({
+ /**
+ * Starts initializing the component and returns a promise that is resolved or
+ * rejected when initialization finished. The same promise is returned if
+ * this function is called multiple times.
+ */
+ ensureInitialized: function () {
+ if (!gInitializationPromise) {
+ gInitializationPromise = SearchAutocompleteProviderInternal.initialize();
+ }
+ return gInitializationPromise;
+ },
+
+ /**
+ * Matches a given string to an item that should be included by URL search
+ * components, like autocomplete in the address bar.
+ *
+ * @param searchToken
+ * String containing the first part of the matching domain name.
+ *
+ * @return An object with the following properties, or undefined if the token
+ * does not match any relevant URL:
+ * {
+ * token: The full string used to match the search term to the URL.
+ * url: The URL to navigate to if the match is selected.
+ * engineName: The display name of the search engine.
+ * iconUrl: Icon associated to the match, or null if not available.
+ * }
+ */
+ findMatchByToken: Task.async(function* (searchToken) {
+ yield this.ensureInitialized();
+
+ // Match at the beginning for now. In the future, an "options" argument may
+ // allow the matching behavior to be tuned.
+ return SearchAutocompleteProviderInternal.priorityMatches
+ .find(m => m.token.startsWith(searchToken));
+ }),
+
+ /**
+ * Matches a given search string to an item that should be included by
+ * components wishing to search using search engine aliases, like
+ * autocomple.
+ *
+ * @param searchToken
+ * Search string to match exactly a search engine alias.
+ *
+ * @return An object with the following properties, or undefined if the token
+ * does not match any relevant URL:
+ * {
+ * alias: The matched search engine's alias.
+ * engineName: The display name of the search engine.
+ * iconUrl: Icon associated to the match, or null if not available.
+ * }
+ */
+ findMatchByAlias: Task.async(function* (searchToken) {
+ yield this.ensureInitialized();
+
+ return SearchAutocompleteProviderInternal.aliasMatches
+ .find(m => m.alias.toLocaleLowerCase() == searchToken.toLocaleLowerCase());
+ }),
+
+ getDefaultMatch: Task.async(function* () {
+ yield this.ensureInitialized();
+
+ return SearchAutocompleteProviderInternal.defaultMatch;
+ }),
+
+ /**
+ * Synchronously determines if the provided URL represents results from a
+ * search engine, and provides details about the match.
+ *
+ * @param url
+ * String containing the URL to parse.
+ *
+ * @return An object with the following properties, or null if the URL does
+ * not represent a search result:
+ * {
+ * engineName: The display name of the search engine.
+ * terms: The originally sought terms extracted from the URI.
+ * }
+ *
+ * @remarks The asynchronous ensureInitialized function must be called before
+ * this synchronous method can be used.
+ *
+ * @note This API function needs to be synchronous because it is called inside
+ * a row processing callback of Sqlite.jsm, in UnifiedComplete.js.
+ */
+ parseSubmissionURL: function (url) {
+ if (!SearchAutocompleteProviderInternal.initialized) {
+ throw new Error("The component has not been initialized.");
+ }
+
+ let parseUrlResult = Services.search.parseSubmissionURL(url);
+ return parseUrlResult.engine && {
+ engineName: parseUrlResult.engine.name,
+ terms: parseUrlResult.terms,
+ };
+ },
+
+ getSuggestionController(searchToken, inPrivateContext, maxResults, userContextId) {
+ if (!SearchAutocompleteProviderInternal.initialized) {
+ throw new Error("The component has not been initialized.");
+ }
+ return SearchAutocompleteProviderInternal.getSuggestionController(
+ searchToken, inPrivateContext, maxResults, userContextId);
+ },
+});