UrlbarUtils.jsm 54.6 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
/* 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/. */

"use strict";

/**
 * This module exports the UrlbarUtils singleton, which contains constants and
 * helper functions that are useful to all components of the urlbar.
 */

12
13
14
var EXPORTED_SYMBOLS = [
  "UrlbarMuxer",
  "UrlbarProvider",
15
  "UrlbarQueryContext",
16
  "UrlbarUtils",
17
  "SkippableTimer",
18
];
19

20
21
22
const { XPCOMUtils } = ChromeUtils.import(
  "resource://gre/modules/XPCOMUtils.jsm"
);
23
24
XPCOMUtils.defineLazyModuleGetters(this, {
  BrowserUtils: "resource://gre/modules/BrowserUtils.jsm",
25
  BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.jsm",
26
  FormHistory: "resource://gre/modules/FormHistory.jsm",
27
  Log: "resource://gre/modules/Log.jsm",
28
29
30
  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
  PlacesUIUtils: "resource:///modules/PlacesUIUtils.jsm",
  PlacesUtils: "resource://gre/modules/PlacesUtils.jsm",
31
32
  SearchSuggestionController:
    "resource://gre/modules/SearchSuggestionController.jsm",
33
  Services: "resource://gre/modules/Services.jsm",
34
  UrlbarPrefs: "resource:///modules/UrlbarPrefs.jsm",
35
  UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.jsm",
36
37
  UrlbarSearchUtils: "resource:///modules/UrlbarSearchUtils.jsm",
  UrlbarTokenizer: "resource:///modules/UrlbarTokenizer.jsm",
38
39
});

40
var UrlbarUtils = {
41
42
43
44
45
46
47
  // Extensions are allowed to add suggestions if they have registered a keyword
  // with the omnibox API. This is the maximum number of suggestions an extension
  // is allowed to add for a given search string.
  // This value includes the heuristic result.
  MAXIMUM_ALLOWED_EXTENSION_MATCHES: 6,

  // This is used by UnifiedComplete, the new implementation will use
48
  // PROVIDER_TYPE and RESULT_TYPE
49
  RESULT_GROUP: {
50
51
52
53
54
55
    HEURISTIC: "heuristic",
    GENERAL: "general",
    SUGGESTION: "suggestion",
    EXTENSION: "extension",
  },

56
57
58
  // Defines provider types.
  PROVIDER_TYPE: {
    // Should be executed immediately, because it returns heuristic results
59
60
    // that must be handed to the user asap.
    HEURISTIC: 1,
61
62
63
64
65
66
67
68
    // Can be delayed, contains results coming from the session or the profile.
    PROFILE: 2,
    // Can be delayed, contains results coming from the network.
    NETWORK: 3,
    // Can be delayed, contains results coming from unknown sources.
    EXTENSION: 4,
  },

69
  // Defines UrlbarResult types.
70
  RESULT_TYPE: {
71
    // An open tab.
72
    TAB_SWITCH: 1,
73
74
75
76
77
78
    // A search suggestion or engine.
    SEARCH: 2,
    // A common url/title tuple, may be a bookmark with tags.
    URL: 3,
    // A bookmark keyword.
    KEYWORD: 4,
79
    // A WebExtension Omnibox result.
80
81
82
    OMNIBOX: 5,
    // A tab from another synced device.
    REMOTE_TAB: 6,
83
84
    // An actionable message to help the user with their query.
    TIP: 7,
85
86
    // A type of result created at runtime, for example by an extension.
    DYNAMIC: 8,
87
88
89
90

    // When you add a new type, also add its schema to
    // UrlbarUtils.RESULT_PAYLOAD_SCHEMA below.  Also consider checking if
    // consumers of "urlbar-user-start-navigation" need updating.
91
  },
92

93
94
  // This defines the source of results returned by a provider. Each provider
  // can return results from more than one source. This is used by the
95
  // ProvidersManager to decide which providers must be queried and which
96
  // results can be returned.
97
98
  // If you add new source types, consider checking if consumers of
  // "urlbar-user-start-navigation" need update as well.
99
  RESULT_SOURCE: {
100
101
    BOOKMARKS: 1,
    HISTORY: 2,
102
103
104
105
    SEARCH: 3,
    TABS: 4,
    OTHER_LOCAL: 5,
    OTHER_NETWORK: 6,
106
107
  },

108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
  /**
   * Buckets used for logging telemetry to the FX_URLBAR_SELECTED_RESULT_TYPE_2
   * histogram.
   */
  SELECTED_RESULT_TYPES: {
    autofill: 0,
    bookmark: 1,
    history: 2,
    keyword: 3,
    searchengine: 4,
    searchsuggestion: 5,
    switchtab: 6,
    tag: 7,
    visiturl: 8,
    remotetab: 9,
    extension: 10,
124
    "preloaded-top-site": 11, // This is currently unused.
125
126
127
128
    tip: 12,
    topsite: 13,
    formhistory: 14,
    dynamic: 15,
129
    tabtosearch: 16,
130
131
132
    // n_values = 32, so you'll need to create a new histogram if you need more.
  },

133
  // This defines icon locations that are commonly used in the UI.
134
  ICON: {
135
    // DEFAULT is defined lazily so it doesn't eagerly initialize PlacesUtils.
136
    EXTENSION: "chrome://browser/content/extension.svg",
137
    HISTORY: "chrome://browser/skin/history.svg",
138
    SEARCH_GLASS: "chrome://browser/skin/search-glass.svg",
139
    SEARCH_GLASS_INVERTED: "chrome://browser/skin/search-glass-inverted.svg",
140
    TIP: "chrome://browser/skin/tip.svg",
141
142
  },

143
144
145
  // The number of results by which Page Up/Down move the selection.
  PAGE_UP_DOWN_DELTA: 5,

146
147
148
149
150
  // IME composition states.
  COMPOSITION: {
    NONE: 1,
    COMPOSING: 2,
    COMMIT: 3,
151
    CANCELED: 4,
152
153
  },

154
155
156
157
  // Limit the length of titles and URLs we display so layout doesn't spend too
  // much time building text runs.
  MAX_TEXT_LENGTH: 255,

158
159
160
161
162
163
164
  // Whether a result should be highlighted up to the point the user has typed
  // or after that point.
  HIGHLIGHT: {
    TYPED: 1,
    SUGGESTED: 2,
  },

165
166
167
168
  // "Keyword offers" are search results with keywords that enter search mode
  // when the user picks them.  Depending on the use case, a keyword offer can
  // visually show or hide the keyword itself in its result.  For example,
  // typing "@" by itself will show keyword offers for all engines with @
169
170
171
  // aliases, and those results will preview their search modes. When a keyword
  // offer is a heuristic -- like an autofilled @  alias -- usually it hides
  // its keyword since the user is already typing it.
172
173
174
175
176
  KEYWORD_OFFER: {
    SHOW: 1,
    HIDE: 2,
  },

177
178
179
180
181
182
183
184
  // UnifiedComplete's autocomplete results store their titles and tags together
  // in their comments.  This separator is used to separate them.  When we
  // rewrite UnifiedComplete for quantumbar, we should stop using this old hack
  // and store titles and tags separately.  It's important that this be a
  // character that no title would ever have.  We use \x1F, the non-printable
  // unit separator.
  TITLE_TAGS_SEPARATOR: "\x1F",

185
186
187
  // Regex matching single word hosts with an optional port; no spaces, auth or
  // path-like chars are admitted.
  REGEXP_SINGLE_WORD: /^[^\s@:/?#]+(:\d+)?$/,
188

189
190
191
192
193
  // Names of engines shipped in Firefox that search the web in general.  These
  // are used to update the input placeholder when entering search mode.
  // TODO (Bug 1658661): Don't hardcode this list; store search engine category
  // information someplace better.
  WEB_ENGINE_NAMES: new Set([
194
    "百度", // Baidu
195
    "百度搜索", // "Baidu Search", the name of Baidu's OpenSearch engine.
196
197
198
199
200
201
    "Bing",
    "DuckDuckGo",
    "Ecosia",
    "Google",
    "Qwant",
    "Yandex",
202
    "Яндекс", // Yandex, non-EN
203
204
  ]),

205
  // Valid entry points for search mode. If adding a value here, please update
206
  // telemetry documentation and Scalars.yaml.
207
  SEARCH_MODE_ENTRY: new Set([
208
    "bookmarkmenu",
209
210
211
212
213
    "handoff",
    "keywordoffer",
    "oneoff",
    "other",
    "shortcut",
214
    "tabmenu",
215
    "tabtosearch",
216
    "tabtosearch_onboard",
217
218
    "topsites_newtab",
    "topsites_urlbar",
219
    "touchbar",
220
221
222
    "typed",
  ]),

223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
  // Search mode objects corresponding to the local shortcuts in the view, in
  // order they appear.  Pref names are relative to the `browser.urlbar` branch.
  get LOCAL_SEARCH_MODES() {
    return [
      {
        source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
        restrict: UrlbarTokenizer.RESTRICT.BOOKMARK,
        icon: "chrome://browser/skin/bookmark.svg",
        pref: "shortcuts.bookmarks",
      },
      {
        source: UrlbarUtils.RESULT_SOURCE.TABS,
        restrict: UrlbarTokenizer.RESTRICT.OPENPAGE,
        icon: "chrome://browser/skin/tab.svg",
        pref: "shortcuts.tabs",
      },
      {
        source: UrlbarUtils.RESULT_SOURCE.HISTORY,
        restrict: UrlbarTokenizer.RESTRICT.HISTORY,
        icon: "chrome://browser/skin/history.svg",
        pref: "shortcuts.history",
      },
    ];
  },

248
249
250
251
252
253
254
255
256
257
  /**
   * Returns the payload schema for the given type of result.
   *
   * @param {number} type One of the UrlbarUtils.RESULT_TYPE values.
   * @returns {object} The schema for the given type.
   */
  getPayloadSchema(type) {
    return UrlbarUtils.RESULT_PAYLOAD_SCHEMA[type];
  },

258
259
260
261
262
263
264
265
  /**
   * Adds a url to history as long as it isn't in a private browsing window,
   * and it is valid.
   *
   * @param {string} url The url to add to history.
   * @param {nsIDomWindow} window The window from where the url is being added.
   */
  addToUrlbarHistory(url, window) {
266
267
268
269
    if (
      !PrivateBrowsingUtils.isWindowPrivate(window) &&
      url &&
      !url.includes(" ") &&
270
      // eslint-disable-next-line no-control-regex
271
272
      !/[\x00-\x1F]/.test(url)
    ) {
273
      PlacesUIUtils.markPageAsTyped(url);
274
    }
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
  },

  /**
   * Given a string, will generate a more appropriate urlbar value if a Places
   * keyword or a search alias is found at the beginning of it.
   *
   * @param {string} url
   *        A string that may begin with a keyword or an alias.
   *
   * @returns {Promise}
   * @resolves { url, postData, mayInheritPrincipal }. If it's not possible
   *           to discern a keyword or an alias, url will be the input string.
   */
  async getShortcutOrURIAndPostData(url) {
    let mayInheritPrincipal = false;
    let postData = null;
    // Split on the first whitespace.
    let [keyword, param = ""] = url.trim().split(/\s(.+)/, 2);

    if (!keyword) {
      return { url, postData, mayInheritPrincipal };
    }

298
    let engine = await Services.search.getEngineByAlias(keyword);
299
300
    if (engine) {
      let submission = engine.getSubmission(param, null, "keyword");
301
302
303
304
305
      return {
        url: submission.uri.spec,
        postData: submission.postData,
        mayInheritPrincipal,
      };
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
    }

    // A corrupt Places database could make this throw, breaking navigation
    // from the location bar.
    let entry = null;
    try {
      entry = await PlacesUtils.keywords.fetch(keyword);
    } catch (ex) {
      Cu.reportError(`Unable to fetch Places keyword "${keyword}": ${ex}`);
    }
    if (!entry || !entry.url) {
      // This is not a Places keyword.
      return { url, postData, mayInheritPrincipal };
    }

    try {
322
323
324
325
326
      [url, postData] = await BrowserUtils.parseUrlAndPostData(
        entry.url.href,
        entry.postData,
        param
      );
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
      if (postData) {
        postData = this.getPostDataStream(postData);
      }

      // Since this URL came from a bookmark, it's safe to let it inherit the
      // current document's principal.
      mayInheritPrincipal = true;
    } catch (ex) {
      // It was not possible to bind the param, just use the original url value.
    }

    return { url, postData, mayInheritPrincipal };
  },

  /**
   * Returns an input stream wrapper for the given post data.
   *
   * @param {string} postDataString The string to wrap.
   * @param {string} [type] The encoding type.
   * @returns {nsIInputStream} An input stream of the wrapped post data.
   */
348
349
350
351
352
353
354
  getPostDataStream(
    postDataString,
    type = "application/x-www-form-urlencoded"
  ) {
    let dataStream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
      Ci.nsIStringInputStream
    );
355
356
    dataStream.data = postDataString;

357
358
359
    let mimeStream = Cc[
      "@mozilla.org/network/mime-input-stream;1"
    ].createInstance(Ci.nsIMIMEInputStream);
360
361
362
363
    mimeStream.addHeader("Content-Type", type);
    mimeStream.setData(dataStream);
    return mimeStream.QueryInterface(Ci.nsIInputStream);
  },
364

365
366
  _compareIgnoringDiacritics: null,

367
  /**
368
369
370
371
   * Returns a list of all the token substring matches in a string.  Matching is
   * case insensitive.  Each match in the returned list is a tuple: [matchIndex,
   * matchLength].  matchIndex is the index in the string of the match, and
   * matchLength is the length of the match.
372
373
374
   *
   * @param {array} tokens The tokens to search for.
   * @param {string} str The string to match against.
375
376
377
378
379
   * @param {boolean} highlightType
   *   One of the HIGHLIGHT values:
   *     TYPED: match ranges matching the tokens; or
   *     SUGGESTED: match ranges for words not matching the tokens and the
   *                endings of words that start with a token.
380
381
382
383
384
385
386
387
   * @returns {array} An array: [
   *            [matchIndex_0, matchLength_0],
   *            [matchIndex_1, matchLength_1],
   *            ...
   *            [matchIndex_n, matchLength_n]
   *          ].
   *          The array is sorted by match indexes ascending.
   */
388
  getTokenMatches(tokens, str, highlightType) {
389
390
391
392
    // Only search a portion of the string, because not more than a certain
    // amount of characters are visible in the UI, matching over what is visible
    // would be expensive and pointless.
    str = str.substring(0, UrlbarUtils.MAX_TEXT_LENGTH).toLocaleLowerCase();
393
394
    // To generate non-overlapping ranges, we start from a 0-filled array with
    // the same length of the string, and use it as a collision marker, setting
395
    // 1 where the text should be highlighted.
396
397
398
    let hits = new Array(str.length).fill(
      highlightType == this.HIGHLIGHT.SUGGESTED ? 1 : 0
    );
399
    let compareIgnoringDiacritics;
400
    for (let { lowerCaseValue: needle } of tokens) {
401
      // Ideally we should never hit the empty token case, but just in case
402
      // the `needle` check protects us from an infinite loop.
403
404
405
406
407
408
409
      if (!needle) {
        continue;
      }
      let index = 0;
      let found = false;
      // First try a diacritic-sensitive search.
      for (;;) {
410
        index = str.indexOf(needle, index);
411
412
413
        if (index < 0) {
          break;
        }
414
415
416
417
418
419
420
421
422
423
424
425
426
427

        if (highlightType == UrlbarUtils.HIGHLIGHT.SUGGESTED) {
          // We de-emphasize the match only if it's preceded by a space, thus
          // it's a perfect match or the beginning of a longer word.
          let previousSpaceIndex = str.lastIndexOf(" ", index) + 1;
          if (index != previousSpaceIndex) {
            index += needle.length;
            // We found the token but we won't de-emphasize it, because it's not
            // after a word boundary.
            found = true;
            continue;
          }
        }

428
429
430
431
432
433
434
435
436
437
438
        hits.fill(
          highlightType == this.HIGHLIGHT.SUGGESTED ? 0 : 1,
          index,
          index + needle.length
        );
        index += needle.length;
        found = true;
      }
      // If that fails to match anything, try a (computationally intensive)
      // diacritic-insensitive search.
      if (!found) {
439
440
        if (!compareIgnoringDiacritics) {
          if (!this._compareIgnoringDiacritics) {
441
442
443
444
445
            // Diacritic insensitivity in the search engine follows a set of
            // general rules that are not locale-dependent, so use a generic
            // English collator for highlighting matching words instead of a
            // collator for the user's particular locale.
            this._compareIgnoringDiacritics = new Intl.Collator("en", {
446
447
448
449
450
              sensitivity: "base",
            }).compare;
          }
          compareIgnoringDiacritics = this._compareIgnoringDiacritics;
        }
451
452
453
        index = 0;
        while (index < str.length) {
          let hay = str.substr(index, needle.length);
454
          if (compareIgnoringDiacritics(needle, hay) === 0) {
455
456
457
458
459
460
461
            if (highlightType == UrlbarUtils.HIGHLIGHT.SUGGESTED) {
              let previousSpaceIndex = str.lastIndexOf(" ", index) + 1;
              if (index != previousSpaceIndex) {
                index += needle.length;
                continue;
              }
            }
462
463
464
465
466
467
468
469
470
            hits.fill(
              highlightType == this.HIGHLIGHT.SUGGESTED ? 0 : 1,
              index,
              index + needle.length
            );
            index += needle.length;
          } else {
            index++;
          }
471
472
        }
      }
473
474
475
476
    }
    // Starting from the collision array, generate [start, len] tuples
    // representing the ranges to be highlighted.
    let ranges = [];
477
    for (let index = hits.indexOf(1); index >= 0 && index < hits.length; ) {
478
      let len = 0;
479
      // eslint-disable-next-line no-empty
480
      for (let j = index; j < hits.length && hits[j]; ++j, ++len) {}
481
482
483
484
485
      ranges.push([index, len]);
      // Move to the next 1.
      index = hits.indexOf(1, index + len);
    }
    return ranges;
486
  },
487
488
489
490
491
492
493
494
495
496
497
498

  /**
   * Extracts an url from a result, if possible.
   * @param {UrlbarResult} result The result to extract from.
   * @returns {object} a {url, postData} object, or null if a url can't be built
   *          from this result.
   */
  getUrlFromResult(result) {
    switch (result.type) {
      case UrlbarUtils.RESULT_TYPE.URL:
      case UrlbarUtils.RESULT_TYPE.REMOTE_TAB:
      case UrlbarUtils.RESULT_TYPE.TAB_SWITCH:
499
        return { url: result.payload.url, postData: null };
500
501
502
      case UrlbarUtils.RESULT_TYPE.KEYWORD:
        return {
          url: result.payload.url,
503
504
505
          postData: result.payload.postData
            ? this.getPostDataStream(result.payload.postData)
            : null,
506
        };
507
      case UrlbarUtils.RESULT_TYPE.SEARCH: {
508
509
510
511
512
513
514
515
516
        if (result.payload.engine) {
          const engine = Services.search.getEngineByName(result.payload.engine);
          let [url, postData] = this.getSearchQueryUrl(
            engine,
            result.payload.suggestion || result.payload.query
          );
          return { url, postData };
        }
        break;
517
      }
518
519
520
521
522
      case UrlbarUtils.RESULT_TYPE.TIP: {
        // Return the button URL. Consumers must check payload.helpUrl
        // themselves if they need the tip's help link.
        return { url: result.payload.buttonUrl, postData: null };
      }
523
    }
524
    return { url: null, postData: null };
525
526
  },

527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
  /**
   * Get the url to load for the search query.
   *
   * @param {nsISearchEngine} engine
   *   The engine to generate the query for.
   * @param {string} query
   *   The query string to search for.
   * @returns {array}
   *   Returns an array containing the query url (string) and the
   *    post data (object).
   */
  getSearchQueryUrl(engine, query) {
    let submission = engine.getSubmission(query, null, "keyword");
    return [submission.uri.spec, submission.postData];
  },

543
544
545
546
547
548
549
550
551
552
  // Ranks a URL prefix from 3 - 0 with the following preferences:
  // https:// > https://www. > http:// > http://www.
  // Higher is better for the purposes of deduping URLs.
  // Returns -1 if the prefix does not match any of the above.
  getPrefixRank(prefix) {
    return ["http://www.", "http://", "https://www.", "https://"].indexOf(
      prefix
    );
  },

553
554
555
556
557
558
559
560
561
  /**
   * Get the number of rows a result should span in the autocomplete dropdown.
   *
   * @param {UrlbarResult} result The result being created.
   * @returns {number}
   *          The number of rows the result should span in the autocomplete
   *          dropdown.
   */
  getSpanForResult(result) {
562
563
564
    if (result.resultSpan) {
      return result.resultSpan;
    }
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
    switch (result.type) {
      case UrlbarUtils.RESULT_TYPE.URL:
      case UrlbarUtils.RESULT_TYPE.BOOKMARKS:
      case UrlbarUtils.RESULT_TYPE.REMOTE_TAB:
      case UrlbarUtils.RESULT_TYPE.TAB_SWITCH:
      case UrlbarUtils.RESULT_TYPE.KEYWORD:
      case UrlbarUtils.RESULT_TYPE.SEARCH:
      case UrlbarUtils.RESULT_TYPE.OMNIBOX:
        return 1;
      case UrlbarUtils.RESULT_TYPE.TIP:
        return 3;
    }
    return 1;
  },

580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
  /**
   * Returns a search mode object if a token should enter search mode when
   * typed. This does not handle engine aliases.
   *
   * @param {UrlbarUtils.RESTRICT} token
   *   A restriction token to convert to search mode.
   * @returns {object}
   *   A search mode object. Null if search mode should not be entered. See
   *   setSearchMode documentation for details.
   */
  searchModeForToken(token) {
    if (!UrlbarPrefs.get("update2")) {
      return null;
    }

595
596
597
598
599
600
601
602
603
    if (token == UrlbarTokenizer.RESTRICT.SEARCH) {
      return {
        engineName: UrlbarSearchUtils.getDefaultEngine(this.isPrivate).name,
      };
    }

    let mode = UrlbarUtils.LOCAL_SEARCH_MODES.find(m => m.restrict == token);
    if (!mode) {
      return null;
604
605
    }

606
607
    // Return a copy so callers don't modify the object in LOCAL_SEARCH_MODES.
    return { ...mode };
608
609
  },

610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
  /**
   * Tries to initiate a speculative connection to a given url.
   * @param {nsISearchEngine|nsIURI|URL|string} urlOrEngine entity to initiate
   *        a speculative connection for.
   * @param {window} window the window from where the connection is initialized.
   * @note This is not infallible, if a speculative connection cannot be
   *       initialized, it will be a no-op.
   */
  setupSpeculativeConnection(urlOrEngine, window) {
    if (!UrlbarPrefs.get("speculativeConnect.enabled")) {
      return;
    }
    if (urlOrEngine instanceof Ci.nsISearchEngine) {
      try {
        urlOrEngine.speculativeConnect({
          window,
          originAttributes: window.gBrowser.contentPrincipal.originAttributes,
        });
      } catch (ex) {
        // Can't setup speculative connection for this url, just ignore it.
      }
      return;
    }

    if (urlOrEngine instanceof URL) {
      urlOrEngine = urlOrEngine.href;
    }

    try {
639
640
641
642
643
644
645
646
647
      let uri =
        urlOrEngine instanceof Ci.nsIURI
          ? urlOrEngine
          : Services.io.newURI(urlOrEngine);
      Services.io.speculativeConnect(
        uri,
        window.gBrowser.contentPrincipal,
        null
      );
648
649
650
651
    } catch (ex) {
      // Can't setup speculative connection for this url, just ignore it.
    }
  },
652

653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
  /**
   * Strips parts of a URL defined in `options`.
   *
   * @param {string} spec
   *        The text to modify.
   * @param {object} options
   * @param {boolean} options.stripHttp
   *        Whether to strip http.
   * @param {boolean} options.stripHttps
   *        Whether to strip https.
   * @param {boolean} options.stripWww
   *        Whether to strip `www.`.
   * @param {boolean} options.trimSlash
   *        Whether to trim the trailing slash.
   * @param {boolean} options.trimEmptyQuery
   *        Whether to trim a trailing `?`.
   * @param {boolean} options.trimEmptyHash
   *        Whether to trim a trailing `#`.
   * @returns {array} [modified, prefix, suffix]
   *          modified: {string} The modified spec.
   *          prefix: {string} The parts stripped from the prefix, if any.
   *          suffix: {string} The parts trimmed from the suffix, if any.
   */
  stripPrefixAndTrim(spec, options = {}) {
    let prefix = "";
    let suffix = "";
    if (options.stripHttp && spec.startsWith("http://")) {
      spec = spec.slice(7);
      prefix = "http://";
    } else if (options.stripHttps && spec.startsWith("https://")) {
      spec = spec.slice(8);
      prefix = "https://";
    }
    if (options.stripWww && spec.startsWith("www.")) {
      spec = spec.slice(4);
      prefix += "www.";
    }
    if (options.trimEmptyHash && spec.endsWith("#")) {
      spec = spec.slice(0, -1);
      suffix = "#" + suffix;
    }
    if (options.trimEmptyQuery && spec.endsWith("?")) {
      spec = spec.slice(0, -1);
      suffix = "?" + suffix;
    }
    if (options.trimSlash && spec.endsWith("/")) {
      spec = spec.slice(0, -1);
      suffix = "/" + suffix;
    }
    return [spec, prefix, suffix];
  },

705
706
707
708
709
710
711
712
713
  /**
   * Strips a PSL verified public suffix from an hostname.
   * @param {string} host A host name.
   * @returns {string} Host name without the public suffix.
   * @note Because stripping the full suffix requires to verify it against the
   *   Public Suffix List, this call is not the cheapest, and thus it should
   *   not be used in hot paths.
   */
  stripPublicSuffixFromHost(host) {
714
715
716
717
718
719
720
721
722
723
724
    try {
      return host.substring(
        0,
        host.length - Services.eTLD.getKnownPublicSuffixFromHost(host).length
      );
    } catch (ex) {
      if (ex.result != Cr.NS_ERROR_HOST_IS_IP_ADDRESS) {
        throw ex;
      }
    }
    return host;
725
726
  },

727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
  /**
   * Used to filter out the javascript protocol from URIs, since we don't
   * support LOAD_FLAGS_DISALLOW_INHERIT_PRINCIPAL for those.
   * @param {string} pasteData The data to check for javacript protocol.
   * @returns {string} The modified paste data.
   */
  stripUnsafeProtocolOnPaste(pasteData) {
    while (true) {
      let scheme = "";
      try {
        scheme = Services.io.extractScheme(pasteData);
      } catch (ex) {
        // If it throws, this is not a javascript scheme.
      }
      if (scheme != "javascript") {
        break;
      }

      pasteData = pasteData.substring(pasteData.indexOf(":") + 1);
    }
    return pasteData;
  },
749
750
751
752

  async addToInputHistory(url, input) {
    await PlacesUtils.withConnectionWrapper("addToInputHistory", db => {
      // use_count will asymptotically approach the max of 10.
753
754
      return db.executeCached(
        `
755
756
757
758
759
        INSERT OR REPLACE INTO moz_inputhistory
        SELECT h.id, IFNULL(i.input, :input), IFNULL(i.use_count, 0) * .9 + 1
        FROM moz_places h
        LEFT JOIN moz_inputhistory i ON i.place_id = h.id AND i.input = :input
        WHERE url_hash = hash(:url) AND url = :url
760
761
762
      `,
        { url, input }
      );
763
764
    });
  },
765
766
767
768
769
770
771
772
773
774
775
776
777

  /**
   * Whether the passed-in input event is paste event.
   * @param {DOMEvent} event an input DOM event.
   * @returns {boolean} Whether the event is a paste event.
   */
  isPasteEvent(event) {
    return (
      event.inputType &&
      (event.inputType.startsWith("insertFromPaste") ||
        event.inputType == "insertFromYank")
    );
  },
778
779
780
781
782

  /**
   * Given a string, checks if it looks like a single word host, not containing
   * spaces nor dots (apart from a possible trailing one).
   * @note This matching should stay in sync with the related code in
783
   * URIFixup::KeywordURIFixup
784
785
786
787
788
789
790
   * @param {string} value
   * @returns {boolean} Whether the value looks like a single word host.
   */
  looksLikeSingleWordHost(value) {
    let str = value.trim();
    return this.REGEXP_SINGLE_WORD.test(str);
  },
791

792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
  /**
   * Returns the portion of a string starting at the index where another string
   * begins.
   *
   * @param   {string} sourceStr
   *          The string to search within.
   * @param   {string} targetStr
   *          The string to search for.
   * @returns {string} The substring within sourceStr starting at targetStr, or
   *          the empty string if targetStr does not occur in sourceStr.
   */
  substringAt(sourceStr, targetStr) {
    let index = sourceStr.indexOf(targetStr);
    return index < 0 ? "" : sourceStr.substr(index);
  },

  /**
   * Returns the portion of a string starting at the index where another string
   * ends.
   *
   * @param   {string} sourceStr
   *          The string to search within.
   * @param   {string} targetStr
   *          The string to search for.
   * @returns {string} The substring within sourceStr where targetStr ends, or
   *          the empty string if targetStr does not occur in sourceStr.
   */
  substringAfter(sourceStr, targetStr) {
    let index = sourceStr.indexOf(targetStr);
    return index < 0 ? "" : sourceStr.substr(index + targetStr.length);
  },

824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
  /**
   * Strips the prefix from a URL and returns the prefix and the remainder of the
   * URL.  "Prefix" is defined to be the scheme and colon, plus, if present, two
   * slashes.  If the given string is not actually a URL, then an empty prefix and
   * the string itself is returned.
   *
   * @param {string} str The possible URL to strip.
   * @returns {array} If `str` is a URL, then [prefix, remainder].  Otherwise, ["", str].
   */
  stripURLPrefix(str) {
    const REGEXP_STRIP_PREFIX = /^[a-z]+:(?:\/){0,2}/i;
    let match = REGEXP_STRIP_PREFIX.exec(str);
    if (!match) {
      return ["", str];
    }
    let prefix = match[0];
    if (prefix.length < str.length && str[prefix.length] == " ") {
      return ["", str];
    }
    return [prefix, str.substr(prefix.length)];
  },

846
847
848
849
850
851
852
853
854
855
856
857
858
  /**
   * Runs a search for the given string, and returns the heuristic result.
   * @param {string} searchString The string to search for.
   * @param {nsIDOMWindow} window The window requesting it.
   * @returns {UrlbarResult} an heuristic result.
   */
  async getHeuristicResultFor(
    searchString,
    window = BrowserWindowTracker.getTopWindow()
  ) {
    if (!searchString) {
      throw new Error("Must pass a non-null search string");
    }
859
860

    let options = {
861
862
863
864
865
866
867
868
      allowAutofill: false,
      isPrivate: PrivateBrowsingUtils.isWindowPrivate(window),
      maxResults: 1,
      searchString,
      userContextId: window.gBrowser.selectedBrowser.getAttribute(
        "usercontextid"
      ),
      allowSearchSuggestions: false,
869
      providers: ["UnifiedComplete", "HeuristicFallback"],
870
871
872
873
874
875
876
877
878
    };
    if (window.gURLBar.searchMode) {
      let searchMode = window.gURLBar.searchMode;
      options.searchMode = searchMode;
      if (searchMode.source) {
        options.sources = [searchMode.source];
      }
    }
    let context = new UrlbarQueryContext(options);
879
880
881
882
883
884
    await UrlbarProvidersManager.startQuery(context);
    if (!context.heuristicResult) {
      throw new Error("There should always be an heuristic result");
    }
    return context.heuristicResult;
  },
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908

  /**
   * Creates a logger.
   * Logging level can be controlled through browser.urlbar.loglevel.
   * @param {string} [prefix] Prefix to use for the logged messages, "::" will
   *                 be appended automatically to the prefix.
   * @returns {object} The logger.
   */
  getLogger({ prefix = "" } = {}) {
    if (!this._logger) {
      this._logger = Log.repository.getLogger("urlbar");
      this._logger.manageLevelFromPref("browser.urlbar.loglevel");
      this._logger.addAppender(
        new Log.ConsoleAppender(new Log.BasicFormatter())
      );
    }
    if (prefix) {
      // This is not an early return because it is necessary to invoke getLogger
      // at least once before getLoggerWithMessagePrefix; it replaces a
      // method of the original logger, rather than using an actual Proxy.
      return Log.repository.getLoggerWithMessagePrefix("urlbar", prefix + "::");
    }
    return this._logger;
  },
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926

  /**
   * Returns the name of a result source.  The name is the lowercase name of the
   * corresponding property in the RESULT_SOURCE object.
   *
   * @param {string} source A UrlbarUtils.RESULT_SOURCE value.
   * @returns {string} The token's name, a lowercased name in the RESULT_SOURCE
   *   object.
   */
  getResultSourceName(source) {
    if (!this._resultSourceNamesBySource) {
      this._resultSourceNamesBySource = new Map();
      for (let [name, src] of Object.entries(this.RESULT_SOURCE)) {
        this._resultSourceNamesBySource.set(src, name.toLowerCase());
      }
    }
    return this._resultSourceNamesBySource.get(source);
  },
927
928
929
930
931
932

  /**
   * Add the search to form history.  This also updates any existing form
   * history for the search.
   * @param {UrlbarInput} input The UrlbarInput object requesting the addition.
   * @param {string} value The value to add.
933
934
   * @param {string} [source] The source of the addition, usually
   *        the name of the engine the search was made with.
935
936
   * @returns {Promise} resolved once the operation is complete
   */
937
  addToFormHistory(input, value, source) {
938
939
940
    // If the user types a search engine alias without a search string,
    // we have an empty search string and we can't bump it.
    // We also don't want to add history in private browsing mode.
941
942
943
944
945
946
947
    // Finally we don't want to store extremely long strings that would not be
    // particularly useful to the user.
    if (
      !value ||
      input.isPrivate ||
      value.length > SearchSuggestionController.SEARCH_HISTORY_MAX_VALUE_LENGTH
    ) {
948
949
950
951
952
953
954
955
      return Promise.resolve();
    }
    return new Promise((resolve, reject) => {
      FormHistory.update(
        {
          op: "bump",
          fieldname: input.formHistoryName,
          value,
956
          source,
957
958
959
960
961
962
963
964
        },
        {
          handleError: reject,
          handleCompletion: resolve,
        }
      );
    });
  },
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000

  /**
   * Extracts a telemetry type from a result, used by scalars and event
   * telemetry.
   *
   * @param {UrlbarResult} result The result to analyze.
   * @returns {string} A string type for telemetry.
   * @note New types should be added to Scalars.yaml under the urlbar.picked
   *       category and documented in the in-tree documentation. A data-review
   *       is always necessary.
   */
  telemetryTypeFromResult(result) {
    if (!result) {
      return "unknown";
    }
    switch (result.type) {
      case UrlbarUtils.RESULT_TYPE.TAB_SWITCH:
        return "switchtab";
      case UrlbarUtils.RESULT_TYPE.SEARCH:
        if (result.source == UrlbarUtils.RESULT_SOURCE.HISTORY) {
          return "formhistory";
        }
        if (result.providerName == "TabToSearch") {
          return "tabtosearch";
        }
        return result.payload.suggestion ? "searchsuggestion" : "searchengine";
      case UrlbarUtils.RESULT_TYPE.URL:
        if (result.autofill) {
          return "autofill";
        }
        if (
          result.source == UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL &&
          result.heuristic
        ) {
          return "visiturl";
        }