Source code

Revision control

Other Tools

1
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
2
* vim: sw=2 ts=2 sts=2 expandtab
3
* This Source Code Form is subject to the terms of the Mozilla Public
4
* License, v. 2.0. If a copy of the MPL was not distributed with this
5
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6
/* eslint complexity: ["error", 50] */
7
8
"use strict";
9
10
// Constants
11
12
const MS_PER_DAY = 86400000; // 24 * 60 * 60 * 1000
13
14
// AutoComplete query type constants.
15
// Describes the various types of queries that we can process rows for.
16
const QUERYTYPE_FILTERED = 0;
17
const QUERYTYPE_AUTOFILL_ORIGIN = 1;
18
const QUERYTYPE_AUTOFILL_URL = 2;
19
const QUERYTYPE_ADAPTIVE = 3;
20
21
// The default frecency value used when inserting matches with unknown frecency.
22
const FRECENCY_DEFAULT = 1000;
23
24
// After this time, we'll give up waiting for the extension to return matches.
25
const MAXIMUM_ALLOWED_EXTENSION_TIME_MS = 3000;
26
27
// By default we add remote tabs that have been used less than this time ago.
28
// Any remaining remote tabs are added in queue if no other results are found.
29
const RECENT_REMOTE_TAB_THRESHOLD_MS = 259200000; // 72 hours.
30
31
// Regex used to match userContextId.
32
const REGEXP_USER_CONTEXT_ID = /(?:^| )user-context-id:(\d+)/;
33
34
// Regex used to match maxResults.
35
const REGEXP_MAX_RESULTS = /(?:^| )max-results:(\d+)/;
36
37
// Regex used to match insertMethod.
38
const REGEXP_INSERT_METHOD = /(?:^| )insert-method:(\d+)/;
39
40
// Regex used to match one or more whitespace.
41
const REGEXP_SPACES = /\s+/;
42
43
// Regex used to strip prefixes from URLs. See stripPrefix().
44
const REGEXP_STRIP_PREFIX = /^[a-z]+:(?:\/){0,2}/i;
45
46
// The result is notified on a delay, to avoid rebuilding the panel at every match.
47
const NOTIFYRESULT_DELAY_MS = 16;
48
49
// Sqlite result row index constants.
50
const QUERYINDEX_QUERYTYPE = 0;
51
const QUERYINDEX_URL = 1;
52
const QUERYINDEX_TITLE = 2;
53
const QUERYINDEX_BOOKMARKED = 3;
54
const QUERYINDEX_BOOKMARKTITLE = 4;
55
const QUERYINDEX_TAGS = 5;
56
// QUERYINDEX_VISITCOUNT = 6;
57
// QUERYINDEX_TYPED = 7;
58
const QUERYINDEX_PLACEID = 8;
59
const QUERYINDEX_SWITCHTAB = 9;
60
const QUERYINDEX_FRECENCY = 10;
61
62
// If a URL starts with one of these prefixes, then we don't provide search
63
// suggestions for it.
64
const DISALLOWED_URLLIKE_PREFIXES = ["http", "https", "ftp"];
65
66
// This SQL query fragment provides the following:
67
// - whether the entry is bookmarked (QUERYINDEX_BOOKMARKED)
68
// - the bookmark title, if it is a bookmark (QUERYINDEX_BOOKMARKTITLE)
69
// - the tags associated with a bookmarked entry (QUERYINDEX_TAGS)
70
const SQL_BOOKMARK_TAGS_FRAGMENT = `EXISTS(SELECT 1 FROM moz_bookmarks WHERE fk = h.id) AS bookmarked,
71
( SELECT title FROM moz_bookmarks WHERE fk = h.id AND title NOTNULL
72
ORDER BY lastModified DESC LIMIT 1
73
) AS btitle,
74
( SELECT GROUP_CONCAT(t.title, ', ')
75
FROM moz_bookmarks b
76
JOIN moz_bookmarks t ON t.id = +b.parent AND t.parent = :parent
77
WHERE b.fk = h.id
78
) AS tags`;
79
80
// TODO bug 412736: in case of a frecency tie, we might break it with h.typed
81
// and h.visit_count. That is slower though, so not doing it yet...
82
// NB: as a slight performance optimization, we only evaluate the "bookmarked"
83
// condition once, and avoid evaluating "btitle" and "tags" when it is false.
84
function defaultQuery(conditions = "") {
85
let query = `SELECT :query_type, h.url, h.title, ${SQL_BOOKMARK_TAGS_FRAGMENT},
86
h.visit_count, h.typed, h.id, t.open_count, h.frecency
87
FROM moz_places h
88
LEFT JOIN moz_openpages_temp t
89
ON t.url = h.url
90
AND t.userContextId = :userContextId
91
WHERE h.frecency <> 0
92
AND CASE WHEN bookmarked
93
THEN
94
AUTOCOMPLETE_MATCH(:searchString, h.url,
95
IFNULL(btitle, h.title), tags,
96
h.visit_count, h.typed,
97
1, t.open_count,
98
:matchBehavior, :searchBehavior)
99
ELSE
100
AUTOCOMPLETE_MATCH(:searchString, h.url,
101
h.title, '',
102
h.visit_count, h.typed,
103
0, t.open_count,
104
:matchBehavior, :searchBehavior)
105
END
106
${conditions}
107
ORDER BY h.frecency DESC, h.id DESC
108
LIMIT :maxResults`;
109
return query;
110
}
111
112
const SQL_SWITCHTAB_QUERY = `SELECT :query_type, t.url, t.url, NULL, NULL, NULL, NULL, NULL, NULL,
113
t.open_count, NULL
114
FROM moz_openpages_temp t
115
LEFT JOIN moz_places h ON h.url_hash = hash(t.url) AND h.url = t.url
116
WHERE h.id IS NULL
117
AND t.userContextId = :userContextId
118
AND AUTOCOMPLETE_MATCH(:searchString, t.url, t.url, NULL,
119
NULL, NULL, NULL, t.open_count,
120
:matchBehavior, :searchBehavior)
121
ORDER BY t.ROWID DESC
122
LIMIT :maxResults`;
123
124
const SQL_ADAPTIVE_QUERY = `/* do not warn (bug 487789) */
125
SELECT :query_type, h.url, h.title, ${SQL_BOOKMARK_TAGS_FRAGMENT},
126
h.visit_count, h.typed, h.id, t.open_count, h.frecency
127
FROM (
128
SELECT ROUND(MAX(use_count) * (1 + (input = :search_string)), 1) AS rank,
129
place_id
130
FROM moz_inputhistory
131
WHERE input BETWEEN :search_string AND :search_string || X'FFFF'
132
GROUP BY place_id
133
) AS i
134
JOIN moz_places h ON h.id = i.place_id
135
LEFT JOIN moz_openpages_temp t
136
ON t.url = h.url
137
AND t.userContextId = :userContextId
138
WHERE AUTOCOMPLETE_MATCH(NULL, h.url,
139
IFNULL(btitle, h.title), tags,
140
h.visit_count, h.typed, bookmarked,
141
t.open_count,
142
:matchBehavior, :searchBehavior)
143
ORDER BY rank DESC, h.frecency DESC
144
LIMIT :maxResults`;
145
146
// Result row indexes for originQuery()
147
const QUERYINDEX_ORIGIN_AUTOFILLED_VALUE = 1;
148
const QUERYINDEX_ORIGIN_URL = 2;
149
const QUERYINDEX_ORIGIN_FRECENCY = 3;
150
151
// `WITH` clause for the autofill queries. autofill_frecency_threshold.value is
152
// the mean of all moz_origins.frecency values + stddevMultiplier * one standard
153
// deviation. This is inlined directly in the SQL (as opposed to being a custom
154
// Sqlite function for example) in order to be as efficient as possible.
155
const SQL_AUTOFILL_WITH = `
156
WITH
157
frecency_stats(count, sum, squares) AS (
158
SELECT
159
CAST((SELECT IFNULL(value, 0.0) FROM moz_meta WHERE key = 'origin_frecency_count') AS REAL),
160
CAST((SELECT IFNULL(value, 0.0) FROM moz_meta WHERE key = 'origin_frecency_sum') AS REAL),
161
CAST((SELECT IFNULL(value, 0.0) FROM moz_meta WHERE key = 'origin_frecency_sum_of_squares') AS REAL)
162
),
163
autofill_frecency_threshold(value) AS (
164
SELECT
165
CASE count
166
WHEN 0 THEN 0.0
167
WHEN 1 THEN sum
168
ELSE (sum / count) + (:stddevMultiplier * sqrt((squares - ((sum * sum) / count)) / count))
169
END
170
FROM frecency_stats
171
)
172
`;
173
174
const SQL_AUTOFILL_FRECENCY_THRESHOLD = `(
175
SELECT value FROM autofill_frecency_threshold
176
)`;
177
178
function originQuery({ select = "", where = "", having = "" }) {
179
return `${SQL_AUTOFILL_WITH}
180
SELECT :query_type,
181
fixed_up_host || '/',
182
IFNULL(:prefix, prefix) || moz_origins.host || '/',
183
frecency,
184
bookmarked,
185
id
186
FROM (
187
SELECT host,
188
host AS fixed_up_host,
189
TOTAL(frecency) AS host_frecency,
190
(
191
SELECT TOTAL(foreign_count) > 0
192
FROM moz_places
193
WHERE moz_places.origin_id = moz_origins.id
194
) AS bookmarked
195
${select}
196
FROM moz_origins
197
WHERE host BETWEEN :searchString AND :searchString || X'FFFF'
198
${where}
199
GROUP BY host
200
HAVING host_frecency >= ${SQL_AUTOFILL_FRECENCY_THRESHOLD}
201
${having}
202
UNION ALL
203
SELECT host,
204
fixup_url(host) AS fixed_up_host,
205
TOTAL(frecency) AS host_frecency,
206
(
207
SELECT TOTAL(foreign_count) > 0
208
FROM moz_places
209
WHERE moz_places.origin_id = moz_origins.id
210
) AS bookmarked
211
${select}
212
FROM moz_origins
213
WHERE host BETWEEN 'www.' || :searchString AND 'www.' || :searchString || X'FFFF'
214
${where}
215
GROUP BY host
216
HAVING host_frecency >= ${SQL_AUTOFILL_FRECENCY_THRESHOLD}
217
${having}
218
) AS grouped_hosts
219
JOIN moz_origins ON moz_origins.host = grouped_hosts.host
220
ORDER BY frecency DESC, id DESC
221
LIMIT 1 `;
222
}
223
224
const QUERY_ORIGIN_HISTORY_BOOKMARK = originQuery({
225
having: `OR bookmarked`,
226
});
227
228
const QUERY_ORIGIN_PREFIX_HISTORY_BOOKMARK = originQuery({
229
where: `AND prefix BETWEEN :prefix AND :prefix || X'FFFF'`,
230
having: `OR bookmarked`,
231
});
232
233
const QUERY_ORIGIN_HISTORY = originQuery({
234
select: `, (
235
SELECT TOTAL(visit_count) > 0
236
FROM moz_places
237
WHERE moz_places.origin_id = moz_origins.id
238
) AS visited`,
239
having: `AND (visited OR NOT bookmarked)`,
240
});
241
242
const QUERY_ORIGIN_PREFIX_HISTORY = originQuery({
243
select: `, (
244
SELECT TOTAL(visit_count) > 0
245
FROM moz_places
246
WHERE moz_places.origin_id = moz_origins.id
247
) AS visited`,
248
where: `AND prefix BETWEEN :prefix AND :prefix || X'FFFF'`,
249
having: `AND (visited OR NOT bookmarked)`,
250
});
251
252
const QUERY_ORIGIN_BOOKMARK = originQuery({
253
having: `AND bookmarked`,
254
});
255
256
const QUERY_ORIGIN_PREFIX_BOOKMARK = originQuery({
257
where: `AND prefix BETWEEN :prefix AND :prefix || X'FFFF'`,
258
having: `AND bookmarked`,
259
});
260
261
// Result row indexes for urlQuery()
262
const QUERYINDEX_URL_URL = 1;
263
const QUERYINDEX_URL_STRIPPED_URL = 2;
264
const QUERYINDEX_URL_FRECENCY = 3;
265
266
function urlQuery(where1, where2) {
267
// We limit the search to places that are either bookmarked or have a frecency
268
// over some small, arbitrary threshold (20) in order to avoid scanning as few
269
// rows as possible. Keep in mind that we run this query every time the user
270
// types a key when the urlbar value looks like a URL with a path.
271
return `/* do not warn (bug no): cannot use an index to sort */
272
SELECT :query_type,
273
url,
274
:strippedURL,
275
frecency,
276
foreign_count > 0 AS bookmarked,
277
visit_count > 0 AS visited,
278
id
279
FROM moz_places
280
WHERE rev_host = :revHost
281
${where1}
282
UNION ALL
283
SELECT :query_type,
284
url,
285
:strippedURL,
286
frecency,
287
foreign_count > 0 AS bookmarked,
288
visit_count > 0 AS visited,
289
id
290
FROM moz_places
291
WHERE rev_host = :revHost || 'www.'
292
${where2}
293
ORDER BY frecency DESC, id DESC
294
LIMIT 1 `;
295
}
296
297
const QUERY_URL_HISTORY_BOOKMARK = urlQuery(
298
`AND (bookmarked OR frecency > 20)
299
AND strip_prefix_and_userinfo(url) BETWEEN :strippedURL AND :strippedURL || X'FFFF'`,
300
`AND (bookmarked OR frecency > 20)
301
AND strip_prefix_and_userinfo(url) BETWEEN 'www.' || :strippedURL AND 'www.' || :strippedURL || X'FFFF'`
302
);
303
304
const QUERY_URL_PREFIX_HISTORY_BOOKMARK = urlQuery(
305
`AND (bookmarked OR frecency > 20)
306
AND url BETWEEN :prefix || :strippedURL AND :prefix || :strippedURL || X'FFFF'`,
307
`AND (bookmarked OR frecency > 20)
308
AND url BETWEEN :prefix || 'www.' || :strippedURL AND :prefix || 'www.' || :strippedURL || X'FFFF'`
309
);
310
311
const QUERY_URL_HISTORY = urlQuery(
312
`AND (visited OR NOT bookmarked)
313
AND frecency > 20
314
AND strip_prefix_and_userinfo(url) BETWEEN :strippedURL AND :strippedURL || X'FFFF'`,
315
`AND (visited OR NOT bookmarked)
316
AND frecency > 20
317
AND strip_prefix_and_userinfo(url) BETWEEN 'www.' || :strippedURL AND 'www.' || :strippedURL || X'FFFF'`
318
);
319
320
const QUERY_URL_PREFIX_HISTORY = urlQuery(
321
`AND (visited OR NOT bookmarked)
322
AND frecency > 20
323
AND url BETWEEN :prefix || :strippedURL AND :prefix || :strippedURL || X'FFFF'`,
324
`AND (visited OR NOT bookmarked)
325
AND frecency > 20
326
AND url BETWEEN :prefix || 'www.' || :strippedURL AND :prefix || 'www.' || :strippedURL || X'FFFF'`
327
);
328
329
const QUERY_URL_BOOKMARK = urlQuery(
330
`AND bookmarked
331
AND strip_prefix_and_userinfo(url) BETWEEN :strippedURL AND :strippedURL || X'FFFF'`,
332
`AND bookmarked
333
AND strip_prefix_and_userinfo(url) BETWEEN 'www.' || :strippedURL AND 'www.' || :strippedURL || X'FFFF'`
334
);
335
336
const QUERY_URL_PREFIX_BOOKMARK = urlQuery(
337
`AND bookmarked
338
AND url BETWEEN :prefix || :strippedURL AND :prefix || :strippedURL || X'FFFF'`,
339
`AND bookmarked
340
AND url BETWEEN :prefix || 'www.' || :strippedURL AND :prefix || 'www.' || :strippedURL || X'FFFF'`
341
);
342
343
// Getters
344
345
const { XPCOMUtils } = ChromeUtils.import(
347
);
348
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
349
350
XPCOMUtils.defineLazyGlobalGetters(this, ["fetch"]);
351
352
XPCOMUtils.defineLazyModuleGetters(this, {
355
ExtensionSearchHandler: "resource://gre/modules/ExtensionSearchHandler.jsm",
357
PlacesRemoteTabsAutocompleteProvider:
359
PlacesSearchAutocompleteProvider:
365
UrlbarProviderOpenTabs: "resource:///modules/UrlbarProviderOpenTabs.jsm",
366
UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.jsm",
367
UrlbarTokenizer: "resource:///modules/UrlbarTokenizer.jsm",
369
});
370
371
XPCOMUtils.defineLazyPreferenceGetter(
372
this,
373
"syncUsernamePref",
374
"services.sync.username"
375
);
376
377
function setTimeout(callback, ms) {
378
let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
379
timer.initWithCallback(callback, ms, timer.TYPE_ONE_SHOT);
380
return timer;
381
}
382
383
const kProtocolsWithIcons = [
384
"chrome:",
385
"moz-extension:",
386
"about:",
387
"http:",
388
"https:",
389
"ftp:",
390
];
391
function iconHelper(url) {
392
if (typeof url == "string") {
393
return kProtocolsWithIcons.some(p => url.startsWith(p))
394
? "page-icon:" + url
395
: PlacesUtils.favicons.defaultFavicon.spec;
396
}
397
if (url && url instanceof URL && kProtocolsWithIcons.includes(url.protocol)) {
398
return "page-icon:" + url.href;
399
}
400
return PlacesUtils.favicons.defaultFavicon.spec;
401
}
402
403
// Preloaded Sites related
404
405
function PreloadedSite(url, title) {
406
this.uri = Services.io.newURI(url);
407
this.title = title;
408
this._matchTitle = title.toLowerCase();
409
this._hasWWW = this.uri.host.startsWith("www.");
410
this._hostWithoutWWW = this._hasWWW ? this.uri.host.slice(4) : this.uri.host;
411
}
412
413
/**
414
* Storage object for Preloaded Sites.
415
* add(url, title): adds a site to storage
416
* populate(sites) : populates the storage with array of [url,title]
417
* sites[]: resulting array of sites (PreloadedSite objects)
418
*/
419
XPCOMUtils.defineLazyGetter(this, "PreloadedSiteStorage", () =>
420
Object.seal({
421
sites: [],
422
423
add(url, title) {
424
let site = new PreloadedSite(url, title);
425
this.sites.push(site);
426
},
427
428
populate(sites) {
429
this.sites = [];
430
for (let site of sites) {
431
this.add(site[0], site[1]);
432
}
433
},
434
})
435
);
436
437
XPCOMUtils.defineLazyGetter(this, "ProfileAgeCreatedPromise", async () => {
438
let times = await ProfileAge();
439
return times.created;
440
});
441
442
// Maps restriction character types to textual behaviors.
443
XPCOMUtils.defineLazyGetter(this, "typeToBehaviorMap", () => {
444
return new Map([
445
[UrlbarTokenizer.TYPE.RESTRICT_HISTORY, "history"],
446
[UrlbarTokenizer.TYPE.RESTRICT_BOOKMARK, "bookmark"],
447
[UrlbarTokenizer.TYPE.RESTRICT_TAG, "tag"],
448
[UrlbarTokenizer.TYPE.RESTRICT_OPENPAGE, "openpage"],
449
[UrlbarTokenizer.TYPE.RESTRICT_SEARCH, "search"],
450
[UrlbarTokenizer.TYPE.RESTRICT_TITLE, "title"],
451
[UrlbarTokenizer.TYPE.RESTRICT_URL, "url"],
452
]);
453
});
454
455
// Helper functions
456
457
/**
458
* Strips the prefix from a URL and returns the prefix and the remainder of the
459
* URL. "Prefix" is defined to be the scheme and colon, plus, if present, two
460
* slashes. If the given string is not actually a URL, then an empty prefix and
461
* the string itself is returned.
462
*
463
* @param str
464
* The possible URL to strip.
465
* @return If `str` is a URL, then [prefix, remainder]. Otherwise, ["", str].
466
*/
467
function stripPrefix(str) {
468
let match = REGEXP_STRIP_PREFIX.exec(str);
469
if (!match) {
470
return ["", str];
471
}
472
let prefix = match[0];
473
if (prefix.length < str.length && str[prefix.length] == " ") {
474
return ["", str];
475
}
476
return [prefix, str.substr(prefix.length)];
477
}
478
479
/**
480
* Strip http and trailing separators from a spec.
481
*
482
* @param spec
483
* The text to modify.
484
* @param trimSlash
485
* Whether to trim the trailing slash.
486
* @return the modified spec.
487
*/
488
function stripHttpAndTrim(spec, trimSlash = true) {
489
if (spec.startsWith("http://")) {
490
spec = spec.slice(7);
491
}
492
if (spec.endsWith("?")) {
493
spec = spec.slice(0, -1);
494
}
495
if (trimSlash && spec.endsWith("/")) {
496
spec = spec.slice(0, -1);
497
}
498
return spec;
499
}
500
501
/**
502
* Returns the key to be used for a match in a map for the purposes of removing
503
* duplicate entries - any 2 matches that should be considered the same should
504
* return the same key. The type of the returned key depends on the type of the
505
* match, so don't assume you can compare keys using ==. Instead, use
506
* ObjectUtils.deepEqual().
507
*
508
* @param {object} match
509
* The match object.
510
* @returns {value} Some opaque key object. Use ObjectUtils.deepEqual() to
511
* compare keys.
512
*/
513
function makeKeyForMatch(match) {
514
// For autofill entries, we need to have a key based on the comment rather
515
// than the value field, because the latter may have been trimmed.
516
if (match.style && match.style.includes("autofill")) {
517
return [stripHttpAndTrim(match.comment), null];
518
}
519
520
let action = PlacesUtils.parseActionUrl(match.value);
521
if (!action) {
522
return [stripHttpAndTrim(match.value), null];
523
}
524
525
let key;
526
switch (action.type) {
527
case "searchengine":
528
// We want to exclude search suggestion matches that simply echo back the
529
// query string in the heuristic result. For example, if the user types
530
// "@engine test", we want to exclude a "test" suggestion match.
531
key = [
532
action.type,
533
action.params.engineName,
534
(
535
action.params.searchSuggestion || action.params.searchQuery
536
).toLocaleLowerCase(),
537
];
538
break;
539
default:
540
key = stripHttpAndTrim(action.params.url || match.value);
541
break;
542
}
543
544
return [key, action];
545
}
546
547
/**
548
* Returns whether the passed in string looks like a url.
549
*/
550
function looksLikeUrl(str, ignoreAlphanumericHosts = false) {
551
// Single word including special chars.
552
return (
553
!REGEXP_SPACES.test(str) &&
554
(["/", "@", ":", "["].some(c => str.includes(c)) ||
555
(ignoreAlphanumericHosts
556
? /^([\[\]A-Z0-9.-]+\.){3,}[^.]+$/i.test(str)
557
: str.includes(".")))
558
);
559
}
560
561
/**
562
* Returns the portion of a string starting at the index where another string
563
* begins.
564
*
565
* @param {string} sourceStr
566
* The string to search within.
567
* @param {string} targetStr
568
* The string to search for.
569
* @returns {string} The substring within sourceStr starting at targetStr, or
570
* the empty string if targetStr does not occur in sourceStr.
571
*/
572
function substringAt(sourceStr, targetStr) {
573
let index = sourceStr.indexOf(targetStr);
574
return index < 0 ? "" : sourceStr.substr(index);
575
}
576
577
/**
578
* Returns the portion of a string starting at the index where another string
579
* ends.
580
*
581
* @param {string} sourceStr
582
* The string to search within.
583
* @param {string} targetStr
584
* The string to search for.
585
* @returns {string} The substring within sourceStr where targetStr ends, or the
586
* empty string if targetStr does not occur in sourceStr.
587
*/
588
function substringAfter(sourceStr, targetStr) {
589
let index = sourceStr.indexOf(targetStr);
590
return index < 0 ? "" : sourceStr.substr(index + targetStr.length);
591
}
592
593
/**
594
* Makes a moz-action url for the given action and set of parameters.
595
*
596
* @param type
597
* The action type.
598
* @param params
599
* A JS object of action params.
600
* @returns A moz-action url as a string.
601
*/
602
function makeActionUrl(type, params) {
603
let encodedParams = {};
604
for (let key in params) {
605
// Strip null or undefined.
606
// Regardless, don't encode them or they would be converted to a string.
607
if (params[key] === null || params[key] === undefined) {
608
continue;
609
}
610
encodedParams[key] = encodeURIComponent(params[key]);
611
}
612
return `moz-action:${type},${JSON.stringify(encodedParams)}`;
613
}
614
615
/**
616
* Manages a single instance of an autocomplete search.
617
*
618
* The first three parameters all originate from the similarly named parameters
619
* of nsIAutoCompleteSearch.startSearch().
620
*
621
* @param searchString
622
* The search string.
623
* @param searchParam
624
* A space-delimited string of search parameters. The following
625
* parameters are supported:
626
* * enable-actions: Include "actions", such as switch-to-tab and search
627
* engine aliases, in the results.
628
* * disable-private-actions: The search is taking place in a private
629
* window outside of permanent private-browsing mode. The search
630
* should exclude privacy-sensitive results as appropriate.
631
* * private-window: The search is taking place in a private window,
632
* possibly in permanent private-browsing mode. The search
633
* should exclude privacy-sensitive results as appropriate.
634
* * user-context-id: The userContextId of the selected tab.
635
* @param autocompleteListener
636
* An nsIAutoCompleteObserver.
637
* @param autocompleteSearch
638
* An nsIAutoCompleteSearch.
639
* @param prohibitSearchSuggestions
640
* Whether search suggestions are allowed for this search.
641
* @param [optional] previousResult
642
* The result object from the previous search. if available.
643
*/
644
function Search(
645
searchString,
646
searchParam,
647
autocompleteListener,
648
autocompleteSearch,
649
prohibitSearchSuggestions,
650
previousResult
651
) {
652
// We want to store the original string for case sensitive searches.
653
this._originalSearchString = searchString;
654
this._trimmedOriginalSearchString = searchString.trim();
655
let unescapedSearchString = Services.textToSubURI.unEscapeURIForUI(
656
"UTF-8",
657
this._trimmedOriginalSearchString
658
);
659
let [prefix, suffix] = stripPrefix(unescapedSearchString);
660
this._searchString = suffix;
661
this._strippedPrefix = prefix.toLowerCase();
662
663
this._matchBehavior = Ci.mozIPlacesAutoComplete.MATCH_BOUNDARY;
664
// Set the default behavior for this search.
665
this._behavior = this._searchString
666
? UrlbarPrefs.get("defaultBehavior")
667
: UrlbarPrefs.get("emptySearchDefaultBehavior");
668
669
let params = new Set(searchParam.split(" "));
670
this._enableActions = params.has("enable-actions");
671
this._disablePrivateActions = params.has("disable-private-actions");
672
this._inPrivateWindow = params.has("private-window");
673
this._prohibitAutoFill = params.has("prohibit-autofill");
674
675
// Extract the max-results param.
676
let maxResults = searchParam.match(REGEXP_MAX_RESULTS);
677
this._maxResults = maxResults
678
? parseInt(maxResults[1])
679
: UrlbarPrefs.get("maxRichResults");
680
681
// Extract the user-context-id param.
682
let userContextId = searchParam.match(REGEXP_USER_CONTEXT_ID);
683
this._userContextId = userContextId
684
? parseInt(userContextId[1], 10)
685
: Ci.nsIScriptSecurityManager.DEFAULT_USER_CONTEXT_ID;
686
687
// Use the original string here, not the stripped one, so the tokenizer can
688
// properly recognize token types.
689
let { tokens } = UrlbarTokenizer.tokenize({
690
searchString: unescapedSearchString,
691
});
692
693
// This allows to handle leading or trailing restriction characters specially.
694
this._leadingRestrictionToken = null;
695
if (tokens.length) {
696
if (
697
UrlbarTokenizer.isRestrictionToken(tokens[0]) &&
698
(tokens.length > 1 ||
699
tokens[0].type == UrlbarTokenizer.TYPE.RESTRICT_SEARCH)
700
) {
701
this._leadingRestrictionToken = tokens[0].value;
702
}
703
704
// Check if the first token has a strippable prefix and remove it, but don't
705
// create an empty token.
706
if (prefix && tokens[0].value.length > prefix.length) {
707
tokens[0].value = tokens[0].value.substring(prefix.length);
708
}
709
}
710
711
this._searchTokens = this.filterTokens(tokens);
712
713
// The heuristic token is the first filtered search token, but only when it's
714
// actually the first thing in the search string. If a prefix or restriction
715
// character occurs first, then the heurstic token is null. We use the
716
// heuristic token to help determine the heuristic result. It may be a Places
717
// keyword, a search engine alias, an extension keyword, or simply a URL or
718
// part of the search string the user has typed. We won't know until we
719
// create the heuristic result.
720
let firstToken = !!this._searchTokens.length && this._searchTokens[0].value;
721
this._heuristicToken =
722
firstToken && this._trimmedOriginalSearchString.startsWith(firstToken)
723
? firstToken
724
: null;
725
726
this._keywordSubstitute = null;
727
728
this._prohibitSearchSuggestions = prohibitSearchSuggestions;
729
730
this._listener = autocompleteListener;
731
this._autocompleteSearch = autocompleteSearch;
732
733
// Create a new result to add eventual matches. Note we need a result
734
// regardless having matches.
735
let result =
736
previousResult ||
737
Cc["@mozilla.org/autocomplete/simple-result;1"].createInstance(
738
Ci.nsIAutoCompleteSimpleResult
739
);
740
result.setSearchString(searchString);
741
result.setListener({
742
onValueRemoved(result, spec, removeFromDB) {
743
if (removeFromDB) {
744
PlacesUtils.history.remove(spec).catch(Cu.reportError);
745
}
746
},
747
QueryInterface: ChromeUtils.generateQI([
748
Ci.nsIAutoCompleteSimpleResultListener,
749
]),
750
});
751
// Will be set later, if needed.
752
result.setDefaultIndex(-1);
753
this._result = result;
754
755
this._previousSearchMatchTypes = [];
756
for (let i = 0; previousResult && i < previousResult.matchCount; ++i) {
757
let style = previousResult.getStyleAt(i);
758
if (style.includes("heuristic")) {
759
this._previousSearchMatchTypes.push(UrlbarUtils.RESULT_GROUP.HEURISTIC);
760
} else if (style.includes("suggestion")) {
761
this._previousSearchMatchTypes.push(UrlbarUtils.RESULT_GROUP.SUGGESTION);
762
} else if (style.includes("extension")) {
763
this._previousSearchMatchTypes.push(UrlbarUtils.RESULT_GROUP.EXTENSION);
764
} else {
765
this._previousSearchMatchTypes.push(UrlbarUtils.RESULT_GROUP.GENERAL);
766
}
767
}
768
769
// Used to limit the number of adaptive results.
770
this._adaptiveCount = 0;
771
this._extraAdaptiveRows = [];
772
773
// Used to limit the number of remote tab results.
774
this._extraRemoteTabRows = [];
775
776
// This is a replacement for this._result.matchCount, to be used when you need
777
// to check how many "current" matches have been inserted.
778
// Indeed this._result.matchCount may include matches from the previous search.
779
this._currentMatchCount = 0;
780
781
// These are used to avoid adding duplicate entries to the results.
782
this._usedURLs = [];
783
this._usedPlaceIds = new Set();
784
785
// Counters for the number of results per RESULT_GROUP.
786
this._counts = Object.values(UrlbarUtils.RESULT_GROUP).reduce((o, p) => {
787
o[p] = 0;
788
return o;
789
}, {});
790
}
791
792
Search.prototype = {
793
/**
794
* Enables the desired AutoComplete behavior.
795
*
796
* @param type
797
* The behavior type to set.
798
*/
799
setBehavior(type) {
800
type = type.toUpperCase();
801
this._behavior |= Ci.mozIPlacesAutoComplete["BEHAVIOR_" + type];
802
},
803
804
/**
805
* Determines if the specified AutoComplete behavior is set.
806
*
807
* @param aType
808
* The behavior type to test for.
809
* @return true if the behavior is set, false otherwise.
810
*/
811
hasBehavior(type) {
812
let behavior = Ci.mozIPlacesAutoComplete["BEHAVIOR_" + type.toUpperCase()];
813
814
if (
815
this._disablePrivateActions &&
816
behavior == Ci.mozIPlacesAutoComplete.BEHAVIOR_OPENPAGE
817
) {
818
return false;
819
}
820
821
return this._behavior & behavior;
822
},
823
824
/**
825
* Used to delay the most complex queries, to save IO while the user is
826
* typing.
827
*/
828
_sleepResolve: null,
829
_sleep(aTimeMs) {
830
// Reuse a single instance to try shaving off some usless work before
831
// the first query.
832
if (!this._sleepTimer) {
833
this._sleepTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
834
}
835
return new Promise(resolve => {
836
this._sleepResolve = resolve;
837
this._sleepTimer.initWithCallback(
838
resolve,
839
aTimeMs,
840
Ci.nsITimer.TYPE_ONE_SHOT
841
);
842
});
843
},
844
845
/**
846
* Given an array of tokens, this function determines which query should be
847
* ran. It also removes any special search tokens.
848
*
849
* @param tokens
850
* An array of search tokens.
851
* @return A new, filtered array of tokens.
852
*/
853
filterTokens(tokens) {
854
let foundToken = false;
855
// Set the proper behavior while filtering tokens.
856
let filtered = [];
857
for (let token of tokens) {
858
if (!UrlbarTokenizer.isRestrictionToken(token)) {
859
filtered.push(token);
860
continue;
861
}
862
let behavior = typeToBehaviorMap.get(token.type);
863
if (!behavior) {
864
throw new Error(`Unknown token type ${token.type}`);
865
}
866
// Don't remove the token if it didn't match, or if it's an action but
867
// actions are not enabled.
868
if (behavior != "openpage" || this._enableActions) {
869
// Don't use the suggest preferences if it is a token search and
870
// set the restrict bit to 1 (to intersect the search results).
871
if (!foundToken) {
872
foundToken = true;
873
// Do not take into account previous behavior (e.g.: history, bookmark)
874
this._behavior = 0;
875
this.setBehavior("restrict");
876
}
877
this.setBehavior(behavior);
878
// We return tags only for bookmarks, thus when tags are enforced, we
879
// must also set the bookmark behavior.
880
if (behavior == "tag") {
881
this.setBehavior("bookmark");
882
}
883
}
884
}
885
// Set the right JavaScript behavior based on our preference. Note that the
886
// preference is whether or not we should filter JavaScript, and the
887
// behavior is if we should search it or not.
888
if (!UrlbarPrefs.get("filter.javascript")) {
889
this.setBehavior("javascript");
890
}
891
return filtered;
892
},
893
894
/**
895
* Stop this search.
896
* After invoking this method, we won't run any more searches or heuristics,
897
* and no new matches may be added to the current result.
898
*/
899
stop() {
900
// Avoid multiple calls or re-entrance.
901
if (!this.pending) {
902
return;
903
}
904
if (this._notifyTimer) {
905
this._notifyTimer.cancel();
906
}
907
this._notifyDelaysCount = 0;
908
if (this._sleepTimer) {
909
this._sleepTimer.cancel();
910
}
911
if (this._sleepResolve) {
912
this._sleepResolve();
913
this._sleepResolve = null;
914
}
915
if (this._suggestionsFetch) {
916
this._suggestionsFetch.stop();
917
this._suggestionsFetch = null;
918
}
919
if (typeof this.interrupt == "function") {
920
this.interrupt();
921
}
922
this.pending = false;
923
},
924
925
/**
926
* Whether this search is active.
927
*/
928
pending: true,
929
930
/**
931
* Execute the search and populate results.
932
* @param conn
933
* The Sqlite connection.
934
*/
935
async execute(conn) {
936
// A search might be canceled before it starts.
937
if (!this.pending) {
938
return;
939
}
940
941
// Used by stop() to interrupt an eventual running statement.
942
this.interrupt = () => {
943
// Interrupt any ongoing statement to run the search sooner.
944
if (!UrlbarProvidersManager.interruptLevel) {
945
conn.interrupt();
946
}
947
};
948
949
// Since we call the synchronous parseSubmissionURL function later, we must
950
// wait for the initialization of PlacesSearchAutocompleteProvider first.
951
await PlacesSearchAutocompleteProvider.ensureInitialized();
952
if (!this.pending) {
953
return;
954
}
955
956
// For any given search, we run many queries/heuristics:
957
// 1) by alias (as defined in SearchService)
958
// 2) inline completion from search engine resultDomains
959
// 3) inline completion for origins (this._originQuery) or urls (this._urlQuery)
960
// 4) directly typed in url (ie, can be navigated to as-is)
961
// 5) submission for the current search engine
962
// 6) Places keywords
963
// 7) adaptive learning (this._adaptiveQuery)
964
// 8) open pages not supported by history (this._switchToTabQuery)
965
// 9) query based on match behavior
966
//
967
// (6) only gets run if we get any filtered tokens, since if there are no
968
// tokens, there is nothing to match.
969
//
970
// (1), (4), (5) only get run if actions are enabled. When actions are
971
// enabled, the first result is always a special result (resulting from one
972
// of the queries between (1) and (6) inclusive). As such, the UI is
973
// expected to auto-select the first result when actions are enabled. If the
974
// first result is an inline completion result, that will also be the
975
// default result and therefore be autofilled (this also happens if actions
976
// are not enabled).
977
978
// Check for Preloaded Sites Expiry before Autofill
979
await this._checkPreloadedSitesExpiry();
980
981
// If the query is simply "@", then the results should be a list of all the
982
// search engines with "@" aliases, without a hueristic result.
983
if (this._trimmedOriginalSearchString == "@") {
984
let added = await this._addSearchEngineTokenAliasMatches();
985
if (added) {
986
this._cleanUpNonCurrentMatches(null);
987
this._autocompleteSearch.finishSearch(true);
988
return;
989
}
990
}
991
992
// Add the first heuristic result, if any. Set _addingHeuristicFirstMatch
993
// to true so that when the result is added, "heuristic" can be included in
994
// its style.
995
this._addingHeuristicFirstMatch = true;
996
let hasHeuristic = await this._matchFirstHeuristicResult(conn);
997
this._addingHeuristicFirstMatch = false;
998
this._cleanUpNonCurrentMatches(UrlbarUtils.RESULT_GROUP.HEURISTIC);
999
if (!this.pending) {
1000
return;
1001
}
1002
1003
// We sleep a little between adding the heuristicFirstMatch and matching
1004
// any other searches so we aren't kicking off potentially expensive
1005
// searches on every keystroke.
1006
// Though, if there's no heuristic result, we start searching immediately,
1007
// since autocomplete may be waiting for us.
1008
if (hasHeuristic) {
1009
await this._sleep(UrlbarPrefs.get("delay"));
1010
if (!this.pending) {
1011
return;
1012
}
1013
1014
// If the heuristic result is a search engine result with an empty query
1015
// and we have either a token alias or the search restriction char, then
1016
// we're done. We want to show only that single result as a clear hint
1017
// that the user can continue typing to search.
1018
// For the restriction character case, also consider a single char query
1019
// or just the char itself, anyway we don't return search suggestions
1020
// unless at least 2 chars have been typed. Thus "?__" and "? a" should
1021
// finish here, while "?aa" should continue.
1022
let emptyQueryTokenAlias =
1023
this._searchEngineAliasMatch &&
1024
this._searchEngineAliasMatch.isTokenAlias &&
1025
!this._searchEngineAliasMatch.query;
1026
let emptySearchRestriction =
1027
this._trimmedOriginalSearchString.length <= 3 &&
1028
this._leadingRestrictionToken == UrlbarTokenizer.RESTRICT.SEARCH &&
1029
/\s*\S?$/.test(this._trimmedOriginalSearchString);
1030
if (emptySearchRestriction || emptyQueryTokenAlias) {
1031
this._cleanUpNonCurrentMatches(null, false);
1032
this._autocompleteSearch.finishSearch(true);
1033
return;
1034
}
1035
}
1036
1037
// Only add extension suggestions if the first token is a registered keyword
1038
// and the search string has characters after the first token.
1039
let extensionsCompletePromise = Promise.resolve();
1040
if (
1041
this._heuristicToken &&
1042
ExtensionSearchHandler.isKeywordRegistered(this._heuristicToken) &&
1043
substringAfter(this._originalSearchString, this._heuristicToken) &&
1044
!this._searchEngineAliasMatch
1045
) {
1046
// Do not await on this, since extensions cannot notify when they are done
1047
// adding results, it may take too long.
1048
extensionsCompletePromise = this._matchExtensionSuggestions();
1049
} else if (ExtensionSearchHandler.hasActiveInputSession()) {
1050
ExtensionSearchHandler.handleInputCancelled();
1051
}
1052
1053
// Start adding search suggestions, unless they aren't required or the
1054
// window is private.
1055
let searchSuggestionsCompletePromise = Promise.resolve();
1056
if (
1057
this._enableActions &&
1058
this.hasBehavior("search") &&
1059
(!this._inPrivateWindow ||
1060
UrlbarPrefs.get("browser.search.suggest.enabled.private"))
1061
) {
1062
let query = this._searchEngineAliasMatch
1063
? this._searchEngineAliasMatch.query
1064
: substringAt(this._originalSearchString, this._searchTokens[0].value);
1065
if (query) {
1066
// Limit the string sent for search suggestions to a maximum length.
1067
query = query.substr(
1068
0,
1069
UrlbarPrefs.get("maxCharsForSearchSuggestions")
1070
);
1071
// Don't add suggestions if the query may expose sensitive information.
1072
if (!this._prohibitSearchSuggestionsFor(query)) {
1073
let engine;
1074
if (this._searchEngineAliasMatch) {
1075
engine = this._searchEngineAliasMatch.engine;
1076
} else {
1077
engine = await PlacesSearchAutocompleteProvider.currentEngine(
1078
this._inPrivateWindow
1079
);
1080
if (!this.pending) {
1081
return;
1082
}
1083
}
1084
let alias =
1085
(this._searchEngineAliasMatch &&
1086
this._searchEngineAliasMatch.alias) ||
1087
"";
1088
searchSuggestionsCompletePromise = this._matchSearchSuggestions(
1089
engine,
1090
query,
1091
alias
1092
);
1093
}
1094
}
1095
}
1096
1097
// If the user used a search engine token alias, then the only results we
1098
// want to show are suggestions from that engine, so we're done. We're also
1099
// done if we're restricting results to suggestions.
1100
if (
1101
(this._searchEngineAliasMatch &&
1102
this._searchEngineAliasMatch.isTokenAlias) ||
1103
(this._enableActions &&
1104
this.hasBehavior("search") &&
1105
this.hasBehavior("restrict"))
1106
) {
1107
// Wait for the suggestions to be added.
1108
await searchSuggestionsCompletePromise;
1109
this._cleanUpNonCurrentMatches(null);
1110
this._autocompleteSearch.finishSearch(true);
1111
return;
1112
}
1113
1114
// Clear previous search suggestions.
1115
searchSuggestionsCompletePromise.then(() => {
1116
this._cleanUpNonCurrentMatches(UrlbarUtils.RESULT_GROUP.SUGGESTION);
1117
});
1118
1119
// Run the adaptive query first.
1120
await conn.executeCached(
1121
this._adaptiveQuery[0],
1122
this._adaptiveQuery[1],
1123
this._onResultRow.bind(this)
1124
);
1125
if (!this.pending) {
1126
return;
1127
}
1128
1129
// Then fetch remote tabs.
1130
if (this._enableActions && this.hasBehavior("openpage")) {
1131
await this._matchRemoteTabs();
1132
if (!this.pending) {
1133
return;
1134
}
1135
}
1136
1137
// Get the final query, based on the tokens found in the search string and
1138
// the keyword substitution, if any.
1139
let queries = [];
1140
// "openpage" behavior is supported by the default query.
1141
// _switchToTabQuery instead returns only pages not supported by history.
1142
if (this.hasBehavior("openpage")) {
1143
queries.push(this._switchToTabQuery);
1144
}
1145
queries.push(this._searchQuery);
1146
1147
// Finally run all the other queries.
1148
for (let [query, params] of queries) {
1149
await conn.executeCached(query, params, this._onResultRow.bind(this));
1150
if (!this.pending) {
1151
return;
1152
}
1153
}
1154
1155
// If we have some unused adaptive matches, add them now.
1156
while (
1157
this._extraAdaptiveRows.length &&
1158
this._currentMatchCount < this._maxResults
1159
) {
1160
this._addFilteredQueryMatch(this._extraAdaptiveRows.shift());
1161
}
1162
1163
// If we have some unused remote tab matches, add them now.
1164
while (
1165
this._extraRemoteTabRows.length &&
1166
this._currentMatchCount < this._maxResults
1167
) {
1168
this._addMatch(this._extraRemoteTabRows.shift());
1169
}
1170
1171
// Ideally we should wait until MATCH_BOUNDARY_ANYWHERE, but that query
1172
// may be really slow and we may end up showing old results for too long.
1173
this._cleanUpNonCurrentMatches(UrlbarUtils.RESULT_GROUP.GENERAL);
1174
1175
this._matchAboutPages();
1176
1177
// If we do not have enough matches search again with MATCH_ANYWHERE, to
1178
// get more matches.
1179
let count =
1180
this._counts[UrlbarUtils.RESULT_GROUP.GENERAL] +
1181
this._counts[UrlbarUtils.RESULT_GROUP.HEURISTIC];
1182
if (count < this._maxResults) {
1183
this._matchBehavior = Ci.mozIPlacesAutoComplete.MATCH_ANYWHERE;
1184
for (let [query, params] of [this._adaptiveQuery, this._searchQuery]) {
1185
await conn.executeCached(query, params, this._onResultRow.bind(this));
1186
if (!this.pending) {
1187
return;
1188
}
1189
}
1190
}
1191
1192
this._matchPreloadedSites();
1193
1194
// Ensure to fill any remaining space.
1195
await searchSuggestionsCompletePromise;
1196
await extensionsCompletePromise;
1197
},
1198
1199
_shouldMatchAboutPages() {
1200
// Only autocomplete input that starts with 'about:' and has at least 1 more
1201
// character.
1202
return this._strippedPrefix == "about:" && this._searchString;
1203
},
1204
1205
_matchAboutPages() {
1206
if (!this._shouldMatchAboutPages()) {
1207
return;
1208
}
1209
for (const url of AboutPagesUtils.visibleAboutUrls) {
1210
if (url.startsWith(`about:${this._searchString}`)) {
1211
this._addMatch({
1212
value: url,
1213
comment: url,
1214
frecency: FRECENCY_DEFAULT,
1215
});
1216
}
1217
}
1218
},
1219
1220
_matchAboutPageForAutofill() {
1221
if (!this._shouldMatchAboutPages()) {
1222
return false;
1223
}
1224
for (const url of AboutPagesUtils.visibleAboutUrls) {
1225
if (url.startsWith(`about:${this._searchString.toLowerCase()}`)) {
1226
this._result.setDefaultIndex(0);
1227
this._addAutofillMatch(url.substr(6), url);
1228
return true;
1229
}
1230
}
1231
return false;
1232
},
1233
1234
async _checkPreloadedSitesExpiry() {
1235
if (!UrlbarPrefs.get("usepreloadedtopurls.enabled")) {
1236
return;
1237
}
1238
let profileCreationDate = await ProfileAgeCreatedPromise;
1239
let daysSinceProfileCreation =
1240
(Date.now() - profileCreationDate) / MS_PER_DAY;
1241
if (
1242
daysSinceProfileCreation >
1243
UrlbarPrefs.get("usepreloadedtopurls.expire_days")
1244
) {
1245
Services.prefs.setBoolPref(
1246
"browser.urlbar.usepreloadedtopurls.enabled",
1247
false
1248
);
1249
}
1250
},
1251
1252
_matchPreloadedSites() {
1253
if (!UrlbarPrefs.get("usepreloadedtopurls.enabled")) {
1254
return;
1255
}
1256
1257
if (!this._searchString) {
1258
// The user hasn't typed anything, or they've only typed a scheme.
1259
return;
1260
}
1261
1262
for (let site of PreloadedSiteStorage.sites) {
1263
let url = site.uri.spec;
1264
if (
1265
(!this._strippedPrefix || url.startsWith(this._strippedPrefix)) &&
1266
(site.uri.host.includes(this._searchString) ||
1267
site._matchTitle.includes(this._searchString))
1268
) {
1269
this._addMatch({
1270
value: url,
1271
comment: site.title,
1272
style: "preloaded-top-site",
1273
frecency: FRECENCY_DEFAULT - 1,
1274
});
1275
}
1276
}
1277
},
1278
1279
_matchPreloadedSiteForAutofill() {
1280
if (!UrlbarPrefs.get("usepreloadedtopurls.enabled")) {
1281
return false;
1282
}
1283
1284
let matchedSite = PreloadedSiteStorage.sites.find(site => {
1285
return (
1286
(!this._strippedPrefix ||
1287
site.uri.spec.startsWith(this._strippedPrefix)) &&
1288
(site.uri.host.startsWith(this._searchString) ||
1289
site.uri.host.startsWith("www." + this._searchString))
1290
);
1291
});
1292
if (!matchedSite) {
1293
return false;
1294
}
1295
1296
this._result.setDefaultIndex(0);
1297
1298
let url = matchedSite.uri.spec;
1299
let value = stripPrefix(url)[1];
1300
value = value.substr(value.indexOf(this._searchString));
1301
1302
this._addAutofillMatch(value, url, Infinity, ["preloaded-top-site"]);
1303
return true;
1304
},
1305
1306
/**
1307
* Adds matches for all the engines with "@" aliases, if any.
1308
*
1309
* @returns {bool} True if any results were added, false if not.
1310
*/
1311
async _addSearchEngineTokenAliasMatches() {
1312
let engines = await PlacesSearchAutocompleteProvider.tokenAliasEngines();
1313
if (!engines || !engines.length) {
1314
return false;
1315
}
1316
for (let { engine, tokenAliases } of engines) {
1317
this._addSearchEngineMatch({
1318
engine,
1319
alias: tokenAliases[0],
1320
});
1321
}
1322
return true;
1323
},
1324
1325
async _matchSearchEngineTokenAliasForAutofill() {
1326
// We need an "@engine" heuristic token.
1327
let token = this._heuristicToken;
1328
if (!token || token.length == 1 || !token.startsWith("@")) {
1329
return false;
1330
}
1331
1332
// See if any engine has a token alias that starts with the heuristic token.
1333
let engines = await PlacesSearchAutocompleteProvider.tokenAliasEngines();
1334
for (let { engine, tokenAliases } of engines) {
1335
for (let alias of tokenAliases) {
1336
if (alias.startsWith(token.toLocaleLowerCase())) {
1337
// We found one. The match we add here is a little special compared
1338
// to others. It needs to be an autofill match and its `value` must
1339
// be the string that will be autofilled so that the controller will
1340
// autofill it. But it also must be a searchengine action so that the
1341
// front end will style it as a search engine result. The front end
1342
// uses `finalCompleteValue` as the URL for autofill results, so set
1343
// that to the moz-action URL.
1344
let aliasPreservingUserCase = token + alias.substr(token.length);
1345
let value = aliasPreservingUserCase + " ";
1346
this._result.setDefaultIndex(0);
1347
this._addMatch({
1348
value,
1349
finalCompleteValue: makeActionUrl("searchengine", {
1350
engineName: engine.name,
1351
alias: aliasPreservingUserCase,
1352
input: value,
1353
searchQuery: "",
1354
}),
1355
comment: engine.name,
1356
frecency: FRECENCY_DEFAULT,
1357
style: "autofill action searchengine",
1358
icon: engine.iconURI ? engine.iconURI.spec : null,
1359
});
1360
1361
// Set _searchEngineAliasMatch with an empty query so that we don't
1362
// attempt to add any more matches. When a token alias is autofilled,
1363
// the only match should be the one we just added.
1364
this._searchEngineAliasMatch = {
1365
engine,
1366
alias: aliasPreservingUserCase,
1367
query: "",
1368
isTokenAlias: true,
1369
};
1370
1371
return true;
1372
}
1373
}
1374
}
1375
1376
return false;
1377
},
1378
1379
async _matchFirstHeuristicResult(conn) {
1380
// We always try to make the first result a special "heuristic" result. The
1381
// heuristics below determine what type of result it will be, if any.
1382
1383
let hasSearchTerms = !!this._searchTokens.length;
1384
1385
if (hasSearchTerms) {
1386
// It may be a keyword registered by an extension.
1387
let matched = await this._matchExtensionHeuristicResult();
1388
if (matched) {
1389
return true;
1390
}
1391
}
1392
1393
if (this._enableActions && hasSearchTerms) {
1394
// It may be a search engine with an alias - which works like a keyword.
1395
let matched = await this._matchSearchEngineAlias();
1396
if (matched) {
1397
return true;
1398
}
1399
}
1400
1401
if (this.pending && hasSearchTerms) {
1402
// It may be a Places keyword.
1403
let matched = await this._matchPlacesKeyword();
1404
if (matched) {
1405
return true;
1406
}
1407
}
1408
1409
let shouldAutofill = this._shouldAutofill;
1410
1411
if (this.pending && shouldAutofill) {
1412
// It may also look like an about: link.
1413
let matched = await this._matchAboutPageForAutofill();
1414
if (matched) {
1415
return true;
1416
}
1417
}
1418
1419
if (this.pending && shouldAutofill) {
1420
// It may also look like a URL we know from the database.
1421
let matched = await this._matchKnownUrl(conn);
1422
if (matched) {
1423
return true;
1424
}
1425
}
1426
1427
if (this.pending && shouldAutofill) {
1428
// Or it may look like a search engine domain.
1429
let matched = await this._matchSearchEngineDomain();
1430
if (matched) {
1431
return true;
1432
}
1433
}
1434
1435
if (this.pending && shouldAutofill) {
1436
let matched = this._matchPreloadedSiteForAutofill();
1437
if (matched) {
1438
return true;
1439
}
1440
}
1441
1442
if (this.pending && shouldAutofill) {
1443
let matched = await this._matchSearchEngineTokenAliasForAutofill();
1444
if (matched) {
1445
return true;
1446
}
1447
}
1448
1449
if (this.pending && hasSearchTerms && this._enableActions) {
1450
// If we don't have a result that matches what we know about, then
1451
// we use a fallback for things we don't know about.
1452
1453
// We may not have auto-filled, but this may still look like a URL.
1454
// However, even if the input is a valid URL, we may not want to use
1455
// it as such. This can happen if the host would require whitelisting,
1456
// but isn't in the whitelist.
1457
let matched = await this._matchUnknownUrl();
1458
if (matched) {
1459
// Because we think this may be a URL, we won't be fetching search
1460
// suggestions for it.
1461
this._prohibitSearchSuggestions = true;
1462
// Since we can't tell if this is a real URL and
1463
// whether the user wants to visit or search for it,
1464
// we always provide an alternative searchengine match.
1465
try {
1466
new URL(this._originalSearchString);
1467
} catch (ex) {
1468
if (
1469
UrlbarPrefs.get("keyword.enabled") &&
1470
!looksLikeUrl(this._originalSearchString, true)
1471
) {
1472
this._addingHeuristicFirstMatch = false;
1473
await this._matchCurrentSearchEngine();
1474
this._addingHeuristicFirstMatch = true;
1475
}
1476
}
1477
return true;
1478
}
1479
}
1480
1481
if (this.pending && this._enableActions && this._originalSearchString) {
1482
// When all else fails, and the search string is non-empty, we search
1483
// using the current search engine.
1484
let matched = await this._matchCurrentSearchEngine();
1485
if (matched) {
1486
return true;
1487
}
1488
}
1489
1490
return false;
1491
},
1492
1493
_matchSearchSuggestions(engine, searchString, alias) {
1494
this._suggestionsFetch = PlacesSearchAutocompleteProvider.newSuggestionsFetch(
1495
engine,
1496
searchString,
1497
this._inPrivateWindow,
1498
UrlbarPrefs.get("maxHistoricalSearchSuggestions"),
1499
this._maxResults - UrlbarPrefs.get("maxHistoricalSearchSuggestions"),
1500
this._userContextId
1501
);
1502
return this._suggestionsFetch.fetchCompletePromise
1503
.then(() => {
1504
// The fetch has been canceled already.
1505
if (!this._suggestionsFetch) {
1506
return;
1507
}
1508
if (
1509
this._suggestionsFetch.resultsCount >= 0 &&
1510
this._suggestionsFetch.resultsCount < 2
1511
) {
1512
// The original string is used to properly compare with the next fetch.
1513
this._lastLowResultsSearchSuggestion = this._originalSearchString;
1514
}
1515
while (this.pending) {
1516
let result = this._suggestionsFetch.consume();
1517
if (!result) {
1518
break;
1519
}
1520
let { suggestion, historical } = result;
1521
if (!looksLikeUrl(suggestion)) {
1522
this._addSearchEngineMatch({
1523
engine,
1524
alias,
1525
query: searchString,
1526
suggestion,
1527
historical,
1528
});
1529
}
1530
}
1531
})
1532
.catch(Cu.reportError);
1533
},
1534
1535
_prohibitSearchSuggestionsFor(searchString) {
1536
if (this._prohibitSearchSuggestions) {
1537
return true;
1538
}
1539
1540
// Never prohibit suggestions when the user has used a search engine token
1541
// alias. We want "@engine query" to return suggestions from the engine.
1542
if (
1543
this._searchEngineAliasMatch &&
1544
this._searchEngineAliasMatch.isTokenAlias
1545
) {
1546
return false;
1547
}
1548
1549
// Suggestions for a single letter are unlikely to be useful.
1550
if (searchString.length < 2) {
1551
return true;
1552
}
1553
1554
// The first token may be a whitelisted host.
1555
if (
1556
this._searchTokens.length == 1 &&
1557
this._searchTokens[0].type == UrlbarTokenizer.TYPE.POSSIBLE_ORIGIN &&
1558
Services.uriFixup.isDomainWhitelisted(this._searchTokens[0].value, -1)
1559
) {
1560
return true;
1561
}
1562
1563
// Disallow fetching search suggestions for strings that start off looking
1564
// like urls.
1565
if (
1566
DISALLOWED_URLLIKE_PREFIXES.some(
1567
prefix => this._trimmedOriginalSearchString == prefix
1568
) ||
1569
DISALLOWED_URLLIKE_PREFIXES.some(prefix =>
1570
this._trimmedOriginalSearchString.startsWith(prefix + ":")
1571
)
1572
) {
1573
return true;
1574
}
1575
1576
// Disallow fetching search suggestions for strings looking like URLs, or
1577
// non-alphanumeric origins, to avoid disclosing information about networks
1578
// or passwords.
1579
return this._searchTokens.some(t => {
1580
return (
1581
t.type == UrlbarTokenizer.TYPE.POSSIBLE_URL ||
1582
(t.type == UrlbarTokenizer.TYPE.POSSIBLE_ORIGIN &&
1583
!/^[a-z0-9-]+$/i.test(t.value))
1584
);
1585
});
1586
},
1587
1588
async _matchKnownUrl(conn) {
1589
let gotResult = false;
1590
1591
// If search string looks like an origin, try to autofill against origins.
1592
// Otherwise treat it as a possible URL. When the string has only one slash
1593
// at the end, we still treat it as an URL.
1594
let query, params;
1595
if (UrlbarTokenizer.looksLikeOrigin(this._searchString)) {
1596
[query, params] = this._originQuery;
1597
} else {
1598
[query, params] = this._urlQuery;
1599
}
1600
1601
// _urlQuery doesn't always return a query.
1602
if (query) {
1603
await conn.executeCached(query, params, (row, cancel) => {
1604
gotResult = true;
1605
this._onResultRow(row, cancel);
1606
});
1607
}
1608
return gotResult;
1609
},
1610
1611
_matchExtensionHeuristicResult() {
1612
if (
1613
this._heuristicToken &&
1614
ExtensionSearchHandler.isKeywordRegistered(this._heuristicToken) &&
1615
substringAfter(this._originalSearchString, this._heuristicToken)
1616
) {
1617
let description = ExtensionSearchHandler.getDescription(
1618
this._heuristicToken
1619
);
1620
this._addExtensionMatch(this._originalSearchString, description);
1621
return true;
1622
}
1623
return false;
1624
},
1625
1626
async _matchPlacesKeyword() {
1627
if (!this._heuristicToken) {
1628
return false;
1629
}
1630
let keyword = this._heuristicToken;
1631
let entry = await PlacesUtils.keywords.fetch(keyword);
1632
if (!entry) {
1633
return false;
1634
}
1635
1636
let searchString = substringAfter(
1637
this._originalSearchString,
1638
keyword
1639
).trim();
1640
1641
let url = null;
1642
let postData = null;
1643
try {
1644
[url, postData] = await BrowserUtils.parseUrlAndPostData(
1645
entry.url.href,
1646
entry.postData,
1647
searchString
1648
);
1649
} catch (ex) {
1650
// It's not possible to bind a param to this keyword.
1651
return false;
1652
}
1653
1654
let style = "keyword";
1655
let value = url;
1656
if (this._enableActions) {
1657
style = "action " + style;
1658
value = makeActionUrl("keyword", {
1659
url,
1660
keyword,
1661
input: this._originalSearchString,
1662
postData,
1663
});
1664
}
1665
1666
let match = {
1667
value,
1668
// Don't use the url with replaced strings, since the icon doesn't change
1669
// but the string does, it may cause pointless icon flicker on typing.
1670
icon: iconHelper(entry.url),
1671
style,
1672
frecency: Infinity,
1673
};
1674
// If there is a query string, the title will be "host: queryString".
1675
if (this._searchTokens.length > 1) {
1676
match.comment = entry.url.host;
1677
}
1678
1679
this._addMatch(match);
1680
if (!this._keywordSubstitute) {
1681
this._keywordSubstitute = entry.url.host;
1682
}
1683
return true;
1684
},
1685
1686
async _matchSearchEngineDomain() {
1687
if (!UrlbarPrefs.get("autoFill.searchEngines")) {
1688
return false;
1689
}
1690
if (!this._searchString) {
1691
return false;
1692
}
1693
1694
// PlacesSearchAutocompleteProvider only matches against engine domains.
1695
// Remove an eventual trailing slash from the search string (without the
1696
// prefix) and check if the resulting string is worth matching.
1697
// Later, we'll verify that the found result matches the original
1698
// searchString and eventually discard it.
1699
let searchStr = this._searchString;
1700
if (searchStr.indexOf("/") == searchStr.length - 1) {
1701
searchStr = searchStr.slice(0, -1);
1702
}
1703
// If the search string looks more like a url than a domain, bail out.
1704
if (!UrlbarTokenizer.looksLikeOrigin(searchStr)) {
1705
return false;
1706
}
1707
1708
let engine = await PlacesSearchAutocompleteProvider.engineForDomainPrefix(
1709
searchStr
1710
);
1711
if (!engine) {
1712
return false;
1713
}
1714
let url = engine.searchForm;
1715
let domain = engine.getResultDomain();
1716
// Verify that the match we got is acceptable. Autofilling "example/" to
1717
// "example.com/" would not be good.
1718
if (
1719
(this._strippedPrefix && !url.startsWith(this._strippedPrefix)) ||
1720
!(domain + "/").includes(this._searchString)
1721
) {
1722
return false;
1723
}
1724
1725
// The value that's autofilled in the input is the prefix the user typed, if
1726
// any, plus the portion of the engine domain that the user typed. Append a
1727
// trailing slash too, as is usual with autofill.
1728
let value =
1729
this._strippedPrefix + domain.substr(domain.indexOf(searchStr)) + "/";
1730
1731
let finalCompleteValue = url;
1732
try {
1733
let fixupInfo = Services.uriFixup.getFixupURIInfo(url, 0);
1734
if (fixupInfo.fixedURI) {
1735
finalCompleteValue = fixupInfo.fixedURI.spec;
1736
}
1737
} catch (ex) {}
1738
1739
this._result.setDefaultIndex(0);
1740
this._addMatch({
1741
value,
1742
finalCompleteValue,
1743
comment: engine.name,
1744
icon: engine.iconURI ? engine.iconURI.spec : null,
1745
style: "priority-search",
1746
frecency: Infinity,
1747
});
1748
return true;
1749
},
1750
1751
async _matchSearchEngineAlias() {
1752
if (!this._heuristicToken) {
1753
return false;
1754
}
1755
1756
let alias = this._heuristicToken;
1757
let engine = await PlacesSearchAutocompleteProvider.engineForAlias(alias);
1758
if (!engine) {
1759
return false;
1760
}
1761
1762
this._searchEngineAliasMatch = {
1763
engine,
1764
alias,
1765
query: substringAfter(this._originalSearchString, alias).trim(),
1766
isTokenAlias: alias.startsWith("@"),
1767
};
1768
this._addSearchEngineMatch(this._searchEngineAliasMatch);
1769
if (!this._keywordSubstitute) {
1770
this._keywordSubstitute = engine.getResultDomain();
1771
}
1772
return true;
1773
},
1774
1775
async _matchCurrentSearchEngine() {
1776
let engine = await PlacesSearchAutocompleteProvider.currentEngine(
1777
this._inPrivateWindow
1778
);
1779
if (!engine || !this.pending) {
1780
return false;
1781
}
1782
// Strip a leading search restriction char, because we prepend it to text
1783
// when the search shortcut is used and it's not user typed. Don't strip
1784
// other restriction chars, so that it's possible to search for things
1785
// including one of those (e.g. "c#").
1786
let query = this._trimmedOriginalSearchString;
1787
if (this._leadingRestrictionToken === UrlbarTokenizer.RESTRICT.SEARCH) {
1788
query = substringAfter(query, this._leadingRestrictionToken).trim();
1789
}
1790
this._addSearchEngineMatch({ engine, query });
1791
return true;
1792
},
1793
1794
_addExtensionMatch(content, comment) {
1795
let count =
1796
this._counts[UrlbarUtils.RESULT_GROUP.EXTENSION] +
1797
this._counts[UrlbarUtils.RESULT_GROUP.HEURISTIC];
1798
if (count >= UrlbarUtils.MAXIMUM_ALLOWED_EXTENSION_MATCHES) {
1799
return;
1800
}
1801
1802
this._addMatch({
1803
value: makeActionUrl("extension", {
1804
content,
1805
keyword: this._heuristicToken,
1806
}),
1807
comment,
1809
style: "action extension",
1810
frecency: Infinity,
1811
type: UrlbarUtils.RESULT_GROUP.EXTENSION,
1812
});
1813
},
1814
1815
/**
1816
* Adds a search engine match.
1817
*
1818
* @param {nsISearchEngine} engine
1819
* The search engine associated with the match.
1820
* @param {string} [query]
1821
* The search query string.
1822
* @param {string} [alias]
1823
* The search engine alias associated with the match, if any.
1824
* @param {string} [suggestion]
1825
* The suggestion from the search engine, if you're adding a suggestion
1826
* match.
1827
* @param {bool} [historical]
1828
* True if you're adding a suggestion match and the suggestion is from
1829
* the user's local history (and not the search engine).
1830
*/
1831
_addSearchEngineMatch({
1832
engine,
1833
query = "",
1834
alias = undefined,
1835
suggestion = undefined,
1836
historical = false,
1837
}) {
1838
let actionURLParams = {
1839
engineName: engine.name,
1840
searchQuery: query,
1841
};
1842
1843
if (suggestion) {
1844
// `input` should include the alias.
1845
actionURLParams.input = (alias ? `${alias} ` : "") + suggestion;
1846
} else if (alias && !query) {
1847
// `input` should have a trailing space so that when the user selects the
1848
// result, they can start typing their query without first having to enter
1849
// a space between the alias and query.
1850
actionURLParams.input = `${alias} `;
1851
} else {
1852
actionURLParams.input = this._originalSearchString;
1853
}
1854
1855
let match = {
1856
comment: engine.name,
1857
icon: engine.iconURI && !suggestion ? engine.iconURI.spec : null,
1858
style: "action searchengine",
1859
frecency: FRECENCY_DEFAULT,
1860
};
1861
1862
if (alias) {
1863
actionURLParams.alias = alias;
1864
match.style += " alias";
1865
}
1866
if (suggestion) {
1867
actionURLParams.searchSuggestion = suggestion;
1868
match.style += " suggestion";
1869
match.type = UrlbarUtils.RESULT_GROUP.SUGGESTION;
1870
}
1871
1872
match.value = makeActionUrl("searchengine", actionURLParams);
1873
this._addMatch(match);
1874
},
1875
1876
_matchExtensionSuggestions() {
1877
let data = {
1878
keyword: this._heuristicToken,
1879
text: this._originalSearchString,
1880
inPrivateWindow: this._inPrivateWindow,
1881
};
1882
let promise = ExtensionSearchHandler.handleSearch(data, suggestions => {
1883
for (let suggestion of suggestions) {
1884
let content = `${this._heuristicToken} ${suggestion.content}`;
1885
this._addExtensionMatch(content, suggestion.description);
1886
}
1887
});
1888
// Remove previous search matches sooner than the maximum timeout, otherwise
1889
// matches may appear stale for a long time.
1890
// This is necessary because WebExtensions don't have a method to notify
1891
// that they are done providing results, so they could be pending forever.
1892
setTimeout(
1893
() => this._cleanUpNonCurrentMatches(UrlbarUtils.RESULT_GROUP.EXTENSION),
1894
100
1895
);
1896
1897
// Since the extension has no way to signale when it's done pushing
1898
// results, we add a timeout racing with the addition.
1899
let timeoutPromise = new Promise(resolve => {
1900
let timer = setTimeout(resolve, MAXIMUM_ALLOWED_EXTENSION_TIME_MS);
1901
// TODO Bug 1531268: Figure out why this cancel helps makes the tests
1902
// stable.
1903
promise.then(timer.cancel);
1904
});
1905
return Promise.race([timeoutPromise, promise]).catch(Cu.reportError);
1906
},
1907
1908
async _matchRemoteTabs() {
1909
// Bail out early for non-sync users.
1910
if (!syncUsernamePref) {
1911
return;
1912
}
1913
let matches = await PlacesRemoteTabsAutocompleteProvider.getMatches(
1914
this._originalSearchString,
1915
this._maxResults
1916
);
1917
for (let { url, title, icon, deviceName, lastUsed } of matches) {
1918
// It's rare that Sync supplies the icon for the page (but if it does, it
1919
// is a string URL)
1920
if (!icon) {
1921
icon = iconHelper(url);
1922
} else {
1923
icon = PlacesUtils.favicons.getFaviconLinkForIcon(
1924
Services.io.newURI(icon)