Source code

Revision control

Other Tools

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
3
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5
/*
6
* Provides functions to handle search engine URLs in the browser history.
7
*/
8
9
"use strict";
10
11
var EXPORTED_SYMBOLS = ["PlacesSearchAutocompleteProvider"];
12
13
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
14
15
ChromeUtils.defineModuleGetter(
16
this,
17
"SearchSuggestionController",
19
);
20
21
const SEARCH_ENGINE_TOPIC = "browser-search-engine-modified";
22
23
const SearchAutocompleteProviderInternal = {
24
/**
25
* {Map<string: nsISearchEngine>} Maps from each domain to the engine with
26
* that domain. If more than one engine has the same domain, the last one
27
* passed to _addEngine will be the one in this map.
28
*/
29
enginesByDomain: new Map(),
30
31
/**
32
* {Map<string: nsISearchEngine>} Maps from each lowercased alias to the
33
* engine with that alias. If more than one engine has the same alias, the
34
* last one passed to _addEngine will be the one in this map.
35
*/
36
enginesByAlias: new Map(),
37
38
/**
39
* {array<{ {nsISearchEngine} engine, {array<string>} tokenAliases }>} Array
40
* of engines that have "@" aliases.
41
*/
42
tokenAliasEngines: [],
43
44
async initialize() {
45
try {
46
await Services.search.init();
47
} catch (errorCode) {
48
throw new Error("Unable to initialize search service.");
49
}
50
51
// The initial loading of the search engines must succeed.
52
await this._refresh();
53
54
Services.obs.addObserver(this, SEARCH_ENGINE_TOPIC, true);
55
56
this.initialized = true;
57
},
58
59
initialized: false,
60
61
observe(subject, topic, data) {
62
switch (data) {
63
case "engine-added":
64
case "engine-changed":
65
case "engine-removed":
66
case "engine-default":
67
this._refresh();
68
}
69
},
70
71
async _refresh() {
72
this.enginesByDomain.clear();
73
this.enginesByAlias.clear();
74
this.tokenAliasEngines = [];
75
76
// The search engines will always be processed in the order returned by the
77
// search service, which can be defined by the user.
78
(await Services.search.getEngines()).forEach(e => this._addEngine(e));
79
},
80
81
_addEngine(engine) {
82
if (engine.hidden) {
83
return;
84
}
85
86
let domain = engine.getResultDomain();
87
if (domain) {
88
this.enginesByDomain.set(domain, engine);
89
}
90
91
let aliases = [];
92
if (engine.alias) {
93
aliases.push(engine.alias);
94
}
95
aliases.push(...engine.wrappedJSObject._internalAliases);
96
for (let alias of aliases) {
97
this.enginesByAlias.set(alias.toLocaleLowerCase(), engine);
98
}
99
100
let tokenAliases = aliases.filter(a => a.startsWith("@"));
101
if (tokenAliases.length) {
102
this.tokenAliasEngines.push({ engine, tokenAliases });
103
}
104
},
105
106
QueryInterface: ChromeUtils.generateQI([
107
Ci.nsIObserver,
108
Ci.nsISupportsWeakReference,
109
]),
110
};
111
112
class SuggestionsFetch {
113
/**
114
* Create a new instance of this class for each new suggestions fetch.
115
*
116
* @param {nsISearchEngine} engine
117
* The engine from which suggestions will be fetched.
118
* @param {string} searchString
119
* The search query string.
120
* @param {bool} inPrivateContext
121
* Pass true if the fetch is being done in a private window.
122
* @param {int} maxLocalResults
123
* The maximum number of results to fetch from the user's local
124
* history.
125
* @param {int} maxRemoteResults
126
* The maximum number of results to fetch from the search engine.
127
* @param {int} userContextId
128
* The user context ID in which the fetch is being performed.
129
*/
130
constructor(
131
engine,
132
searchString,
133
inPrivateContext,
134
maxLocalResults,
135
maxRemoteResults,
136
userContextId
137
) {
138
this._controller = new SearchSuggestionController();
139
this._controller.maxLocalResults = maxLocalResults;
140
this._controller.maxRemoteResults = maxRemoteResults;
141
this._engine = engine;
142
this._suggestions = [];
143
this._success = false;
144
this._promise = this._controller
145
.fetch(searchString, inPrivateContext, engine, userContextId)
146
.then(results => {
147
this._success = true;
148
if (results) {
149
this._suggestions.push(
150
...results.local.map(r => ({ suggestion: r, historical: true })),
151
...results.remote.map(r => ({ suggestion: r, historical: false }))
152
);
153
}
154
})
155
.catch(err => {
156
// fetch() rejects its promise if there's a pending request.
157
});
158
}
159
160
/**
161
* {nsISearchEngine} The engine from which suggestions are being fetched.
162
*/
163
get engine() {
164
return this._engine;
165
}
166
167
/**
168
* {promise} Resolved when all suggestions have been fetched.
169
*/
170
get fetchCompletePromise() {
171
return this._promise;
172
}
173
174
/**
175
* Returns one suggestion, if any are available, otherwise returns null.
176
* Note that may be multiple reasons why suggestions are not available:
177
* - all suggestions have already been consumed
178
* - the fetch failed
179
* - the fetch didn't complete yet (should have awaited the promise)
180
*
181
* @returns {object} An object { suggestion, historical } or null if no
182
* suggestions are available.
183
* - suggestion {string} The suggestion.
184
* - historical {bool} True if the suggestion comes from the user's
185
* local history (instead of the search engine).
186
*/
187
consume() {
188
return this._suggestions.shift() || null;
189
}
190
191
/**
192
* Returns the number of fetched suggestions, or -1 if the fetching was
193
* incomplete or failed.
194
*/
195
get resultsCount() {
196
return this._success ? this._suggestions.length : -1;
197
}
198
199
/**
200
* Stops the fetch.
201
*/
202
stop() {
203
this._controller.stop();
204
}
205
}
206
207
var gInitializationPromise = null;
208
209
var PlacesSearchAutocompleteProvider = Object.freeze({
210
/**
211
* Starts initializing the component and returns a promise that is resolved or
212
* rejected when initialization finished. The same promise is returned if
213
* this function is called multiple times.
214
*/
215
ensureInitialized() {
216
if (!gInitializationPromise) {
217
gInitializationPromise = SearchAutocompleteProviderInternal.initialize();
218
}
219
return gInitializationPromise;
220
},
221
222
/**
223
* Gets the engine whose domain matches a given prefix.
224
*
225
* @param {string} prefix
226
* String containing the first part of the matching domain name.
227
* @returns {nsISearchEngine} The matching engine or null if there isn't one.
228
*/
229
async engineForDomainPrefix(prefix) {
230
await this.ensureInitialized();
231
232
// Match at the beginning for now. In the future, an "options" argument may
233
// allow the matching behavior to be tuned.
234
let tuples = SearchAutocompleteProviderInternal.enginesByDomain.entries();
235
for (let [domain, engine] of tuples) {
236
if (domain.startsWith(prefix) || domain.startsWith("www." + prefix)) {
237
return engine;
238
}
239
}
240
return null;
241
},
242
243
/**
244
* Gets the engine with a given alias.
245
*
246
* @param {string} alias
247
* A search engine alias.
248
* @returns {nsISearchEngine} The matching engine or null if there isn't one.
249
*/
250
async engineForAlias(alias) {
251
await this.ensureInitialized();
252
253
return (
254
SearchAutocompleteProviderInternal.enginesByAlias.get(
255
alias.toLocaleLowerCase()
256
) || null
257
);
258
},
259
260
/**
261
* Gets the list of engines with token ("@") aliases.
262
*
263
* @returns {array<{ {nsISearchEngine} engine, {array<string>} tokenAliases }>}
264
* Array of objects { engine, tokenAliases } for token alias engines.
265
*/
266
async tokenAliasEngines() {
267
await this.ensureInitialized();
268
269
return SearchAutocompleteProviderInternal.tokenAliasEngines.slice();
270
},
271
272
/**
273
* Use this to get the current engine rather than Services.search.defaultEngine
274
* directly. This method makes sure that the service is first initialized.
275
*
276
* @param {boolean} inPrivateWindow
277
* Set to true if this search is being run in a private window.
278
* @returns {nsISearchEngine} The current search engine.
279
*/
280
async currentEngine(inPrivateWindow) {
281
await this.ensureInitialized();
282
283
return inPrivateWindow
284
? Services.search.defaultPrivateEngine
285
: Services.search.defaultEngine;
286
},
287
288
/**
289
* Synchronously determines if the provided URL represents results from a
290
* search engine, and provides details about the match.
291
*
292
* @param url
293
* String containing the URL to parse.
294
*
295
* @return An object with the following properties, or null if the URL does
296
* not represent a search result:
297
* {
298
* engineName: The display name of the search engine.
299
* terms: The originally sought terms extracted from the URI.
300
* }
301
*
302
* @remarks The asynchronous ensureInitialized function must be called before
303
* this synchronous method can be used.
304
*
305
* @note This API function needs to be synchronous because it is called inside
306
* a row processing callback of Sqlite.jsm, in UnifiedComplete.js.
307
*/
308
parseSubmissionURL(url) {
309
if (!SearchAutocompleteProviderInternal.initialized) {
310
throw new Error("The component has not been initialized.");
311
}
312
313
let parseUrlResult = Services.search.parseSubmissionURL(url);
314
return (
315
parseUrlResult.engine && {
316
engineName: parseUrlResult.engine.name,
317
terms: parseUrlResult.terms,
318
}
319
);
320
},
321
322
/**
323
* Starts a new suggestions fetch.
324
*
325
* @param {nsISearchEngine} engine
326
* The engine from which suggestions will be fetched.
327
* @param {string} searchString
328
* The search query string.
329
* @param {bool} inPrivateContext
330
* Pass true if the fetch is being done in a private window.
331
* @param {int} maxLocalResults
332
* The maximum number of results to fetch from the user's local
333
* history.
334
* @param {int} maxRemoteResults
335
* The maximum number of results to fetch from the search engine.
336
* @param {int} userContextId
337
* The user context ID in which the fetch is being performed.
338
* @returns {SuggestionsFetch} A new suggestions fetch object you should use
339
* to track the fetch.
340
*/
341
newSuggestionsFetch(
342
engine,
343
searchString,
344
inPrivateContext,
345
maxLocalResults,
346
maxRemoteResults,
347
userContextId
348
) {
349
if (!SearchAutocompleteProviderInternal.initialized) {
350
throw new Error("The component has not been initialized.");
351
}
352
if (!engine) {
353
throw new Error("`engine` is null");
354
}
355
return new SuggestionsFetch(
356
engine,
357
searchString,
358
inPrivateContext,
359
maxLocalResults,
360
maxRemoteResults,
361
userContextId
362
);
363
},
364
});