michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: this.EXPORTED_SYMBOLS = [ "PriorityUrlProvider" ]; michael@0: michael@0: const Ci = Components.interfaces; michael@0: const Cc = Components.classes; michael@0: const Cu = Components.utils; michael@0: const Cr = Components.results; michael@0: michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: Cu.import("resource://gre/modules/Promise.jsm"); michael@0: Cu.import("resource://gre/modules/Task.jsm"); michael@0: michael@0: michael@0: /** michael@0: * Provides search engines matches to the PriorityUrlProvider through the michael@0: * search engines definitions handled by the Search Service. michael@0: */ michael@0: const SEARCH_ENGINE_TOPIC = "browser-search-engine-modified"; michael@0: michael@0: let SearchEnginesProvider = { michael@0: init: function () { michael@0: this._engines = new Map(); michael@0: let deferred = Promise.defer(); michael@0: Services.search.init(rv => { michael@0: if (Components.isSuccessCode(rv)) { michael@0: Services.search.getVisibleEngines().forEach(this._addEngine, this); michael@0: deferred.resolve(); michael@0: } else { michael@0: deferred.reject(new Error("Unable to initialize search service.")); michael@0: } michael@0: }); michael@0: Services.obs.addObserver(this, SEARCH_ENGINE_TOPIC, true); michael@0: return deferred.promise; michael@0: }, michael@0: michael@0: observe: function (engine, topic, verb) { michael@0: let engine = engine.QueryInterface(Ci.nsISearchEngine); michael@0: switch (verb) { michael@0: case "engine-added": michael@0: this._addEngine(engine); michael@0: break; michael@0: case "engine-changed": michael@0: if (engine.hidden) { michael@0: this._removeEngine(engine); michael@0: } else { michael@0: this._addEngine(engine); michael@0: } michael@0: break; michael@0: case "engine-removed": michael@0: this._removeEngine(engine); michael@0: break; michael@0: } michael@0: }, michael@0: michael@0: _addEngine: function (engine) { michael@0: if (this._engines.has(engine.name)) { michael@0: return; michael@0: } michael@0: let token = engine.getResultDomain(); michael@0: if (!token) { michael@0: return; michael@0: } michael@0: let match = { token: token, michael@0: // TODO (bug 557665): searchForm should provide an usable michael@0: // url with affiliate code, if available. michael@0: url: engine.searchForm, michael@0: title: engine.name, michael@0: iconUrl: engine.iconURI ? engine.iconURI.spec : null, michael@0: reason: "search" } michael@0: this._engines.set(engine.name, match); michael@0: PriorityUrlProvider.addMatch(match); michael@0: }, michael@0: michael@0: _removeEngine: function (engine) { michael@0: if (!this._engines.has(engine.name)) { michael@0: return; michael@0: } michael@0: this._engines.delete(engine.name); michael@0: PriorityUrlProvider.removeMatchByToken(engine.getResultDomain()); michael@0: }, michael@0: michael@0: QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, michael@0: Ci.nsISupportsWeakReference]) michael@0: } michael@0: michael@0: /** michael@0: * The PriorityUrlProvider allows to match a given string to a list of michael@0: * urls that should have priority in url search components, like autocomplete. michael@0: * Each returned match is an object with the following properties: michael@0: * - token: string used to match the search term to the url michael@0: * - url: url string represented by the match michael@0: * - title: title describing the match, or an empty string if not available michael@0: * - iconUrl: url of the icon associated to the match, or null if not available michael@0: * - reason: a string describing the origin of the match, for example if it michael@0: * represents a search engine, it will be "search". michael@0: */ michael@0: let matches = new Map(); michael@0: michael@0: let initialized = false; michael@0: function promiseInitialized() { michael@0: if (initialized) { michael@0: return Promise.resolve(); michael@0: } michael@0: return Task.spawn(function* () { michael@0: try { michael@0: yield SearchEnginesProvider.init(); michael@0: } catch (ex) { michael@0: Cu.reportError(ex); michael@0: } michael@0: initialized = true; michael@0: }); michael@0: } michael@0: michael@0: this.PriorityUrlProvider = Object.freeze({ michael@0: addMatch: function (match) { michael@0: matches.set(match.token, match); michael@0: }, michael@0: michael@0: removeMatchByToken: function (token) { michael@0: matches.delete(token); michael@0: }, michael@0: michael@0: getMatch: function (searchToken) { michael@0: return Task.spawn(function* () { michael@0: yield promiseInitialized(); michael@0: for (let [token, match] of matches.entries()) { michael@0: // Match at the beginning for now. In future an aOptions argument may michael@0: // allow to control the matching behavior. michael@0: if (token.startsWith(searchToken)) { michael@0: return match; michael@0: } michael@0: } michael@0: return null; michael@0: }.bind(this)); michael@0: } michael@0: });