|
1 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
2 * License, v. 2.0. If a copy of the MPL was not distributed with this file, |
|
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 "use strict"; |
|
6 |
|
7 this.EXPORTED_SYMBOLS = [ "PriorityUrlProvider" ]; |
|
8 |
|
9 const Ci = Components.interfaces; |
|
10 const Cc = Components.classes; |
|
11 const Cu = Components.utils; |
|
12 const Cr = Components.results; |
|
13 |
|
14 Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
15 Cu.import("resource://gre/modules/Services.jsm"); |
|
16 Cu.import("resource://gre/modules/Promise.jsm"); |
|
17 Cu.import("resource://gre/modules/Task.jsm"); |
|
18 |
|
19 |
|
20 /** |
|
21 * Provides search engines matches to the PriorityUrlProvider through the |
|
22 * search engines definitions handled by the Search Service. |
|
23 */ |
|
24 const SEARCH_ENGINE_TOPIC = "browser-search-engine-modified"; |
|
25 |
|
26 let SearchEnginesProvider = { |
|
27 init: function () { |
|
28 this._engines = new Map(); |
|
29 let deferred = Promise.defer(); |
|
30 Services.search.init(rv => { |
|
31 if (Components.isSuccessCode(rv)) { |
|
32 Services.search.getVisibleEngines().forEach(this._addEngine, this); |
|
33 deferred.resolve(); |
|
34 } else { |
|
35 deferred.reject(new Error("Unable to initialize search service.")); |
|
36 } |
|
37 }); |
|
38 Services.obs.addObserver(this, SEARCH_ENGINE_TOPIC, true); |
|
39 return deferred.promise; |
|
40 }, |
|
41 |
|
42 observe: function (engine, topic, verb) { |
|
43 let engine = engine.QueryInterface(Ci.nsISearchEngine); |
|
44 switch (verb) { |
|
45 case "engine-added": |
|
46 this._addEngine(engine); |
|
47 break; |
|
48 case "engine-changed": |
|
49 if (engine.hidden) { |
|
50 this._removeEngine(engine); |
|
51 } else { |
|
52 this._addEngine(engine); |
|
53 } |
|
54 break; |
|
55 case "engine-removed": |
|
56 this._removeEngine(engine); |
|
57 break; |
|
58 } |
|
59 }, |
|
60 |
|
61 _addEngine: function (engine) { |
|
62 if (this._engines.has(engine.name)) { |
|
63 return; |
|
64 } |
|
65 let token = engine.getResultDomain(); |
|
66 if (!token) { |
|
67 return; |
|
68 } |
|
69 let match = { token: token, |
|
70 // TODO (bug 557665): searchForm should provide an usable |
|
71 // url with affiliate code, if available. |
|
72 url: engine.searchForm, |
|
73 title: engine.name, |
|
74 iconUrl: engine.iconURI ? engine.iconURI.spec : null, |
|
75 reason: "search" } |
|
76 this._engines.set(engine.name, match); |
|
77 PriorityUrlProvider.addMatch(match); |
|
78 }, |
|
79 |
|
80 _removeEngine: function (engine) { |
|
81 if (!this._engines.has(engine.name)) { |
|
82 return; |
|
83 } |
|
84 this._engines.delete(engine.name); |
|
85 PriorityUrlProvider.removeMatchByToken(engine.getResultDomain()); |
|
86 }, |
|
87 |
|
88 QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, |
|
89 Ci.nsISupportsWeakReference]) |
|
90 } |
|
91 |
|
92 /** |
|
93 * The PriorityUrlProvider allows to match a given string to a list of |
|
94 * urls that should have priority in url search components, like autocomplete. |
|
95 * Each returned match is an object with the following properties: |
|
96 * - token: string used to match the search term to the url |
|
97 * - url: url string represented by the match |
|
98 * - title: title describing the match, or an empty string if not available |
|
99 * - iconUrl: url of the icon associated to the match, or null if not available |
|
100 * - reason: a string describing the origin of the match, for example if it |
|
101 * represents a search engine, it will be "search". |
|
102 */ |
|
103 let matches = new Map(); |
|
104 |
|
105 let initialized = false; |
|
106 function promiseInitialized() { |
|
107 if (initialized) { |
|
108 return Promise.resolve(); |
|
109 } |
|
110 return Task.spawn(function* () { |
|
111 try { |
|
112 yield SearchEnginesProvider.init(); |
|
113 } catch (ex) { |
|
114 Cu.reportError(ex); |
|
115 } |
|
116 initialized = true; |
|
117 }); |
|
118 } |
|
119 |
|
120 this.PriorityUrlProvider = Object.freeze({ |
|
121 addMatch: function (match) { |
|
122 matches.set(match.token, match); |
|
123 }, |
|
124 |
|
125 removeMatchByToken: function (token) { |
|
126 matches.delete(token); |
|
127 }, |
|
128 |
|
129 getMatch: function (searchToken) { |
|
130 return Task.spawn(function* () { |
|
131 yield promiseInitialized(); |
|
132 for (let [token, match] of matches.entries()) { |
|
133 // Match at the beginning for now. In future an aOptions argument may |
|
134 // allow to control the matching behavior. |
|
135 if (token.startsWith(searchToken)) { |
|
136 return match; |
|
137 } |
|
138 } |
|
139 return null; |
|
140 }.bind(this)); |
|
141 } |
|
142 }); |