tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

ContentSearchParent.sys.mjs (24368B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
      3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 const lazy = {};
      6 
      7 ChromeUtils.defineESModuleGetters(lazy, {
      8  AboutNewTab: "resource:///modules/AboutNewTab.sys.mjs",
      9  BrowserSearchTelemetry:
     10    "moz-src:///browser/components/search/BrowserSearchTelemetry.sys.mjs",
     11  BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
     12  DEFAULT_FORM_HISTORY_PARAM:
     13    "moz-src:///toolkit/components/search/SearchSuggestionController.sys.mjs",
     14  FormHistory: "resource://gre/modules/FormHistory.sys.mjs",
     15  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
     16  SearchSuggestionController:
     17    "moz-src:///toolkit/components/search/SearchSuggestionController.sys.mjs",
     18  UrlbarPrefs: "moz-src:///browser/components/urlbar/UrlbarPrefs.sys.mjs",
     19 });
     20 
     21 const MAX_LOCAL_SUGGESTIONS = 3;
     22 const MAX_SUGGESTIONS = 6;
     23 const SEARCH_ENGINE_PLACEHOLDER_ICON =
     24  "chrome://browser/skin/search-engine-placeholder.png";
     25 
     26 // Set of all ContentSearch actors, used to broadcast messages to all of them.
     27 let gContentSearchActors = new Set();
     28 
     29 /**
     30 * Inbound messages have the following types:
     31 *
     32 *   AddFormHistoryEntry
     33 *     Adds an entry to the search form history.
     34 *     data: the entry, a string
     35 *   GetSuggestions
     36 *     Retrieves an array of search suggestions given a search string.
     37 *     data: { engineName, searchString }
     38 *   GetState
     39 *     Retrieves the current search engine state.
     40 *     data: null
     41 *   GetStrings
     42 *     Retrieves localized search UI strings.
     43 *     data: null
     44 *   ManageEngines
     45 *     Opens the search engine management window.
     46 *     data: null
     47 *   RemoveFormHistoryEntry
     48 *     Removes an entry from the search form history.
     49 *     data: the entry, a string
     50 *   Search
     51 *     Performs a search.
     52 *     Any GetSuggestions messages in the queue from the same target will be
     53 *     cancelled.
     54 *     data: { engineName, searchString, healthReportKey, searchPurpose }
     55 *   SetCurrentEngine
     56 *     Sets the current engine.
     57 *     data: the name of the engine
     58 *   SpeculativeConnect
     59 *     Speculatively connects to an engine.
     60 *     data: the name of the engine
     61 *
     62 * Outbound messages have the following types:
     63 *
     64 *   CurrentEngine
     65 *     Broadcast when the current engine changes.
     66 *     data: see _currentEngineObj
     67 *   CurrentState
     68 *     Broadcast when the current search state changes.
     69 *     data: see currentStateObj
     70 *   State
     71 *     Sent in reply to GetState.
     72 *     data: see currentStateObj
     73 *   Strings
     74 *     Sent in reply to GetStrings
     75 *     data: Object containing string names and values for the current locale.
     76 *   Suggestions
     77 *     Sent in reply to GetSuggestions.
     78 *     data: see _onMessageGetSuggestions
     79 *   SuggestionsCancelled
     80 *     Sent in reply to GetSuggestions when pending GetSuggestions events are
     81 *     cancelled.
     82 *     data: null
     83 */
     84 
     85 export let ContentSearch = {
     86  initialized: false,
     87 
     88  // Inbound events are queued and processed in FIFO order instead of handling
     89  // them immediately, which would result in non-FIFO responses due to the
     90  // asynchrononicity added by converting image data URIs to ArrayBuffers.
     91  _eventQueue: [],
     92  _currentEventPromise: null,
     93 
     94  // This is used to handle search suggestions.  It maps xul:browsers to objects
     95  // { controller, previousFormHistoryResults }.  See _onMessageGetSuggestions.
     96  _suggestionMap: new WeakMap(),
     97 
     98  // Resolved when we finish shutting down.
     99  _destroyedPromise: null,
    100 
    101  // The current controller and browser in _onMessageGetSuggestions.  Allows
    102  // fetch cancellation from _cancelSuggestions.
    103  _currentSuggestion: null,
    104 
    105  init() {
    106    if (!this.initialized) {
    107      Services.obs.addObserver(this, "browser-search-engine-modified");
    108      Services.obs.addObserver(this, "shutdown-leaks-before-check");
    109      lazy.UrlbarPrefs.addObserver(this);
    110 
    111      this.initialized = true;
    112    }
    113  },
    114 
    115  get searchSuggestionUIStrings() {
    116    if (this._searchSuggestionUIStrings) {
    117      return this._searchSuggestionUIStrings;
    118    }
    119    this._searchSuggestionUIStrings = {};
    120    let searchBundle = Services.strings.createBundle(
    121      "chrome://browser/locale/search.properties"
    122    );
    123    let stringNames = [
    124      "searchHeader",
    125      "searchForSomethingWith2",
    126      "searchWithHeader",
    127      "searchSettings",
    128    ];
    129 
    130    for (let name of stringNames) {
    131      this._searchSuggestionUIStrings[name] =
    132        searchBundle.GetStringFromName(name);
    133    }
    134    return this._searchSuggestionUIStrings;
    135  },
    136 
    137  destroy() {
    138    if (!this.initialized) {
    139      return new Promise();
    140    }
    141 
    142    if (this._destroyedPromise) {
    143      return this._destroyedPromise;
    144    }
    145 
    146    Services.obs.removeObserver(this, "browser-search-engine-modified");
    147    Services.obs.removeObserver(this, "shutdown-leaks-before-check");
    148 
    149    this._eventQueue.length = 0;
    150    this._destroyedPromise = Promise.resolve(this._currentEventPromise);
    151    return this._destroyedPromise;
    152  },
    153 
    154  observe(subj, topic, data) {
    155    switch (topic) {
    156      case "browser-search-engine-modified":
    157        this._eventQueue.push({
    158          type: "Observe",
    159          data,
    160        });
    161        this._processEventQueue();
    162        break;
    163      case "shutdown-leaks-before-check":
    164        subj.wrappedJSObject.client.addBlocker(
    165          "ContentSearch: Wait until the service is destroyed",
    166          () => this.destroy()
    167        );
    168        break;
    169    }
    170  },
    171 
    172  /**
    173   * Observes changes in prefs tracked by UrlbarPrefs.
    174   *
    175   * @param {string} pref
    176   *   The name of the pref, relative to `browser.urlbar.` if the pref is
    177   *   in that branch.
    178   */
    179  onPrefChanged(pref) {
    180    if (lazy.UrlbarPrefs.shouldHandOffToSearchModePrefs.includes(pref)) {
    181      this._eventQueue.push({
    182        type: "Observe",
    183        data: "shouldHandOffToSearchMode",
    184      });
    185      this._processEventQueue();
    186    }
    187  },
    188 
    189  removeFormHistoryEntry(browser, entry) {
    190    let browserData = this._suggestionDataForBrowser(browser);
    191    if (browserData?.previousFormHistoryResults) {
    192      let result = browserData.previousFormHistoryResults.find(
    193        e => e.text == entry
    194      );
    195      lazy.FormHistory.update({
    196        op: "remove",
    197        fieldname: lazy.DEFAULT_FORM_HISTORY_PARAM,
    198        value: entry,
    199        guid: result.guid,
    200      }).catch(err =>
    201        console.error("Error removing form history entry: ", err)
    202      );
    203    }
    204  },
    205 
    206  performSearch(actor, browser, data) {
    207    this._ensureDataHasProperties(data, [
    208      "engineName",
    209      "searchString",
    210      "healthReportKey",
    211    ]);
    212    let engine = Services.search.getEngineByName(data.engineName);
    213    let submission = engine.getSubmission(data.searchString, "");
    214    let win = browser.ownerGlobal;
    215    if (!win) {
    216      // The browser may have been closed between the time its content sent the
    217      // message and the time we handle it.
    218      return;
    219    }
    220    let where = lazy.BrowserUtils.whereToOpenLink(data.originalEvent);
    221 
    222    // There is a chance that by the time we receive the search message, the user
    223    // has switched away from the tab that triggered the search. If, based on the
    224    // event, we need to load the search in the same tab that triggered it (i.e.
    225    // where === "current"), openUILinkIn will not work because that tab is no
    226    // longer the current one. For this case we manually load the URI.
    227    if (where === "current") {
    228      // Since we're going to load the search in the same browser, blur the search
    229      // UI to prevent further interaction before we start loading.
    230      this._reply(actor, "Blur");
    231      browser.loadURI(submission.uri, {
    232        postData: submission.postData,
    233        triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal(
    234          {
    235            userContextId:
    236              win.gBrowser.selectedBrowser.getAttribute("userContextId"),
    237          }
    238        ),
    239      });
    240    } else {
    241      let params = {
    242        postData: submission.postData,
    243        inBackground: Services.prefs.getBoolPref(
    244          "browser.tabs.loadInBackground"
    245        ),
    246      };
    247      win.openTrustedLinkIn(submission.uri.spec, where, params);
    248    }
    249    lazy.BrowserSearchTelemetry.recordSearch(
    250      browser,
    251      engine,
    252      data.healthReportKey,
    253      {
    254        selection: data.selection,
    255      }
    256    );
    257  },
    258 
    259  async getSuggestions(engineName, searchString, browser) {
    260    let engine = Services.search.getEngineByName(engineName);
    261    if (!engine) {
    262      throw new Error("Unknown engine name: " + engineName);
    263    }
    264 
    265    let browserData = this._suggestionDataForBrowser(browser, true);
    266    let { controller } = browserData;
    267    let ok = lazy.SearchSuggestionController.engineOffersSuggestions(engine);
    268    let maxLocalResults = ok ? MAX_LOCAL_SUGGESTIONS : MAX_SUGGESTIONS;
    269    let maxRemoteResults = ok ? MAX_SUGGESTIONS : 0;
    270    // fetch() rejects its promise if there's a pending request, but since we
    271    // process our event queue serially, there's never a pending request.
    272    this._currentSuggestion = { controller, browser };
    273    let suggestions = await controller.fetch({
    274      searchString,
    275      inPrivateBrowsing: lazy.PrivateBrowsingUtils.isBrowserPrivate(browser),
    276      engine,
    277      maxLocalResults,
    278      maxRemoteResults,
    279    });
    280 
    281    // Simplify results since we do not support rich results in this component.
    282    suggestions.local = suggestions.local.map(e => e.value);
    283    // We shouldn't show tail suggestions in their full-text form.
    284    let nonTailEntries = suggestions.remote.filter(
    285      e => !e.matchPrefix && !e.tail
    286    );
    287    suggestions.remote = nonTailEntries.map(e => e.value);
    288 
    289    this._currentSuggestion = null;
    290 
    291    // suggestions will be null if the request was cancelled
    292    let result = {};
    293    if (!suggestions) {
    294      return result;
    295    }
    296 
    297    // Keep the form history results so RemoveFormHistoryEntry can remove entries
    298    // from it.  Keeping only one result isn't foolproof because the client may
    299    // try to remove an entry from one set of suggestions after it has requested
    300    // more but before it's received them.  In that case, the entry may not
    301    // appear in the new suggestions.  But that should happen rarely.
    302    browserData.previousFormHistoryResults = suggestions.formHistoryResults;
    303    result = {
    304      engineName,
    305      term: suggestions.term,
    306      local: suggestions.local,
    307      remote: suggestions.remote,
    308    };
    309    return result;
    310  },
    311 
    312  async addFormHistoryEntry(browser, entry = null) {
    313    let isPrivate = false;
    314    try {
    315      // isBrowserPrivate assumes that the passed-in browser has all the normal
    316      // properties, which won't be true if the browser has been destroyed.
    317      // That may be the case here due to the asynchronous nature of messaging.
    318      isPrivate = lazy.PrivateBrowsingUtils.isBrowserPrivate(browser);
    319    } catch (err) {
    320      return false;
    321    }
    322    if (
    323      isPrivate ||
    324      !entry ||
    325      entry.value.length >
    326        lazy.SearchSuggestionController.SEARCH_HISTORY_MAX_VALUE_LENGTH
    327    ) {
    328      return false;
    329    }
    330    lazy.FormHistory.update({
    331      op: "bump",
    332      fieldname: lazy.DEFAULT_FORM_HISTORY_PARAM,
    333      value: entry.value,
    334      source: entry.engineName,
    335    }).catch(err => console.error("Error adding form history entry: ", err));
    336    return true;
    337  },
    338 
    339  /**
    340   * Construct a state object representing the search engine state.
    341   *
    342   * @returns {object} state
    343   */
    344  async currentStateObj() {
    345    let state = {
    346      engines: [],
    347      currentEngine: await this._currentEngineObj(false),
    348      currentPrivateEngine: await this._currentEngineObj(true),
    349    };
    350 
    351    for (let engine of await Services.search.getVisibleEngines()) {
    352      state.engines.push({
    353        name: engine.name,
    354        iconData: await this._getEngineIconURL(engine),
    355        hidden: engine.hideOneOffButton,
    356        isConfigEngine: engine.isConfigEngine,
    357      });
    358    }
    359 
    360    return state;
    361  },
    362 
    363  _processEventQueue() {
    364    if (this._currentEventPromise || !this._eventQueue.length) {
    365      return;
    366    }
    367 
    368    let event = this._eventQueue.shift();
    369 
    370    this._currentEventPromise = (async () => {
    371      try {
    372        await this["_on" + event.type](event);
    373      } catch (err) {
    374        console.error(err);
    375      } finally {
    376        this._currentEventPromise = null;
    377 
    378        this._processEventQueue();
    379      }
    380    })();
    381  },
    382 
    383  _cancelSuggestions({ actor, browser }) {
    384    let cancelled = false;
    385    // cancel active suggestion request
    386    if (
    387      this._currentSuggestion &&
    388      this._currentSuggestion.browser === browser
    389    ) {
    390      this._currentSuggestion.controller.stop();
    391      cancelled = true;
    392    }
    393    // cancel queued suggestion requests
    394    for (let i = 0; i < this._eventQueue.length; i++) {
    395      let m = this._eventQueue[i];
    396      if (actor === m.actor && m.name === "GetSuggestions") {
    397        this._eventQueue.splice(i, 1);
    398        cancelled = true;
    399        i--;
    400      }
    401    }
    402    if (cancelled) {
    403      this._reply(actor, "SuggestionsCancelled");
    404    }
    405  },
    406 
    407  async _onMessage(eventItem) {
    408    let methodName = "_onMessage" + eventItem.name;
    409    if (methodName in this) {
    410      await this._initService();
    411      await this[methodName](eventItem);
    412      eventItem.browser.removeEventListener("SwapDocShells", eventItem, true);
    413    }
    414  },
    415 
    416  async _onMessageGetState({ actor }) {
    417    let state = await this.currentStateObj();
    418    return this._reply(actor, "State", state);
    419  },
    420 
    421  async _onMessageGetEngine({ actor }) {
    422    let state = await this.currentStateObj();
    423    let { usePrivateBrowsing } = actor.browsingContext;
    424    return this._reply(actor, "Engine", {
    425      isPrivateEngine: usePrivateBrowsing,
    426      engine: usePrivateBrowsing
    427        ? state.currentPrivateEngine
    428        : state.currentEngine,
    429    });
    430  },
    431 
    432  _onMessageGetHandoffSearchModePrefs({ actor }) {
    433    this._reply(
    434      actor,
    435      "HandoffSearchModePrefs",
    436      lazy.UrlbarPrefs.get("shouldHandOffToSearchMode")
    437    );
    438  },
    439 
    440  _onMessageGetStrings({ actor }) {
    441    this._reply(actor, "Strings", this.searchSuggestionUIStrings);
    442  },
    443 
    444  _onMessageSearch({ actor, browser, data }) {
    445    this.performSearch(actor, browser, data);
    446  },
    447 
    448  _onMessageSetCurrentEngine({ data }) {
    449    Services.search.setDefault(
    450      Services.search.getEngineByName(data),
    451      Ci.nsISearchService.CHANGE_REASON_USER_SEARCHBAR
    452    );
    453  },
    454 
    455  _onMessageManageEngines({ browser }) {
    456    browser.ownerGlobal.openPreferences("paneSearch");
    457  },
    458 
    459  async _onMessageGetSuggestions({ actor, browser, data }) {
    460    this._ensureDataHasProperties(data, ["engineName", "searchString"]);
    461    let { engineName, searchString } = data;
    462    let suggestions = await this.getSuggestions(
    463      engineName,
    464      searchString,
    465      browser
    466    );
    467 
    468    this._reply(actor, "Suggestions", {
    469      engineName: data.engineName,
    470      searchString: suggestions.term,
    471      formHistory: suggestions.local,
    472      remote: suggestions.remote,
    473    });
    474  },
    475 
    476  async _onMessageAddFormHistoryEntry({ browser, data: entry }) {
    477    await this.addFormHistoryEntry(browser, entry);
    478  },
    479 
    480  _onMessageRemoveFormHistoryEntry({ browser, data: entry }) {
    481    this.removeFormHistoryEntry(browser, entry);
    482  },
    483 
    484  _onMessageSpeculativeConnect({ browser, data: engineName }) {
    485    let engine = Services.search.getEngineByName(engineName);
    486    if (!engine) {
    487      throw new Error("Unknown engine name: " + engineName);
    488    }
    489    if (browser.contentWindow) {
    490      engine.speculativeConnect({
    491        window: browser.contentWindow,
    492        originAttributes: browser.contentPrincipal.originAttributes,
    493      });
    494    }
    495  },
    496 
    497  _onMessageSearchHandoff({ browser, data, actor }) {
    498    let win = browser.ownerGlobal;
    499    let text = data.text;
    500    let urlBar = win.gURLBar;
    501    let inPrivateBrowsing = lazy.PrivateBrowsingUtils.isBrowserPrivate(browser);
    502    let searchEngine = inPrivateBrowsing
    503      ? Services.search.defaultPrivateEngine
    504      : Services.search.defaultEngine;
    505    let isFirstChange = true;
    506 
    507    // It's possible that this is a handoff from about:home / about:newtab,
    508    // in which case we want to include the newtab_session_id in our call to
    509    // urlBar.handoff. We have to jump through some unfortunate hoops to get
    510    // that.
    511    let newtabSessionId = null;
    512    let newtabActor =
    513      browser.browsingContext?.currentWindowGlobal?.getExistingActor(
    514        "AboutNewTab"
    515      );
    516    if (newtabActor) {
    517      const portID = newtabActor.getTabDetails()?.portID;
    518      if (portID) {
    519        newtabSessionId = lazy.AboutNewTab.activityStream.store.feeds
    520          .get("feeds.telemetry")
    521          ?.sessions.get(portID)?.session_id;
    522      }
    523    }
    524 
    525    if (!text) {
    526      urlBar.setHiddenFocus();
    527    } else {
    528      // Pass the provided text to the awesomebar
    529      urlBar.handoff(text, searchEngine, newtabSessionId);
    530      isFirstChange = false;
    531    }
    532 
    533    let checkFirstChange = () => {
    534      // Check if this is the first change since we hidden focused. If it is,
    535      // remove hidden focus styles, prepend the search alias and hide the
    536      // in-content search.
    537      if (isFirstChange) {
    538        isFirstChange = false;
    539        urlBar.removeHiddenFocus(true);
    540        urlBar.handoff("", searchEngine, newtabSessionId);
    541        actor.sendAsyncMessage("DisableSearch");
    542        urlBar.removeEventListener("compositionstart", checkFirstChange);
    543        urlBar.removeEventListener("paste", checkFirstChange);
    544      }
    545    };
    546 
    547    let onKeydown = ev => {
    548      // Check if the keydown will cause a value change.
    549      if (ev.key.length === 1 && !ev.altKey && !ev.ctrlKey && !ev.metaKey) {
    550        checkFirstChange();
    551      }
    552      // If the Esc button is pressed, we are done. Show in-content search and cleanup.
    553      if (ev.key === "Escape") {
    554        onDone();
    555      }
    556    };
    557 
    558    let onDone = ev => {
    559      // We are done. Show in-content search again and cleanup.
    560      const forceSuppressFocusBorder = ev?.type === "mousedown";
    561      urlBar.removeHiddenFocus(forceSuppressFocusBorder);
    562 
    563      urlBar.removeEventListener("keydown", onKeydown);
    564      urlBar.removeEventListener("mousedown", onDone);
    565      urlBar.removeEventListener("blur", onDone);
    566      urlBar.removeEventListener("compositionstart", checkFirstChange);
    567      urlBar.removeEventListener("paste", checkFirstChange);
    568 
    569      actor.sendAsyncMessage("ShowSearch");
    570    };
    571 
    572    urlBar.addEventListener("keydown", onKeydown);
    573    urlBar.addEventListener("mousedown", onDone);
    574    urlBar.addEventListener("blur", onDone);
    575    urlBar.addEventListener("compositionstart", checkFirstChange);
    576    urlBar.addEventListener("paste", checkFirstChange);
    577  },
    578 
    579  async _onObserve(eventItem) {
    580    let engine;
    581    switch (eventItem.data) {
    582      case "engine-default":
    583        engine = await this._currentEngineObj(false);
    584        this._broadcast("CurrentEngine", engine);
    585        break;
    586      case "engine-default-private":
    587        engine = await this._currentEngineObj(true);
    588        this._broadcast("CurrentPrivateEngine", engine);
    589        break;
    590      case "shouldHandOffToSearchMode":
    591        this._broadcast(
    592          "HandoffSearchModePrefs",
    593          lazy.UrlbarPrefs.get("shouldHandOffToSearchMode")
    594        );
    595        break;
    596      default: {
    597        let state = await this.currentStateObj();
    598        this._broadcast("CurrentState", state);
    599        break;
    600      }
    601    }
    602  },
    603 
    604  _suggestionDataForBrowser(browser, create = false) {
    605    let data = this._suggestionMap.get(browser);
    606    if (!data && create) {
    607      // Since one SearchSuggestionController instance is meant to be used per
    608      // autocomplete widget, this means that we assume each xul:browser has at
    609      // most one such widget.
    610      data = {
    611        controller: new lazy.SearchSuggestionController(),
    612      };
    613      this._suggestionMap.set(browser, data);
    614    }
    615    return data;
    616  },
    617 
    618  _reply(actor, type, data) {
    619    actor.sendAsyncMessage(type, data);
    620  },
    621 
    622  _broadcast(type, data) {
    623    for (let actor of gContentSearchActors) {
    624      actor.sendAsyncMessage(type, data);
    625    }
    626  },
    627 
    628  async _currentEngineObj(usePrivate) {
    629    let engine =
    630      Services.search[usePrivate ? "defaultPrivateEngine" : "defaultEngine"];
    631    let obj = {
    632      name: engine.name,
    633      iconData: await this._getEngineIconURL(engine),
    634      isConfigEngine: engine.isConfigEngine,
    635    };
    636    return obj;
    637  },
    638 
    639  /**
    640   * Used in _getEngineIconURL
    641   *
    642   * @typedef {object} iconData
    643   * @property {ArrayBuffer|string} icon
    644   *   The icon data in an ArrayBuffer or a placeholder icon string.
    645   * @property {string|null} mimeType
    646   *   The MIME type of the icon.
    647   */
    648 
    649  /**
    650   * Converts the engine's icon into a URL or an ArrayBuffer for passing to the
    651   * content process.
    652   *
    653   * @param {nsISearchEngine} engine
    654   *   The engine to get the icon for.
    655   * @returns {string|iconData}
    656   *   The icon's URL or an iconData object containing the icon data.
    657   */
    658  async _getEngineIconURL(engine) {
    659    let url = await engine.getIconURL();
    660    if (!url) {
    661      return SEARCH_ENGINE_PLACEHOLDER_ICON;
    662    }
    663 
    664    // The uri received here can be one of several types:
    665    // 1 - moz-extension://[uuid]/path/to/icon.ico
    666    // 2 - data:image/x-icon;base64,VERY-LONG-STRING
    667    // 3 - blob:
    668    //
    669    // For moz-extension URIs we can pass the URI to the content process and
    670    // use it directly as they can be accessed from there and it is cheaper.
    671    //
    672    // For blob URIs the content process is a different scope and we can't share
    673    // the blob with that scope. Hence we have to create a copy of the data.
    674    //
    675    // For data: URIs we convert to an ArrayBuffer as that is more optimal for
    676    // passing the data across to the content process. This is passed to the
    677    // 'icon' field of the return object. The object also receives the
    678    // content-type of the URI, which is passed to its 'mimeType' field.
    679    if (!url.startsWith("data:") && !url.startsWith("blob:")) {
    680      return url;
    681    }
    682 
    683    try {
    684      const response = await fetch(url);
    685      const mimeType = response.headers.get("Content-Type") || "";
    686      const data = await response.arrayBuffer();
    687      return { icon: data, mimeType };
    688    } catch (err) {
    689      console.error("Fetch error: ", err);
    690      return SEARCH_ENGINE_PLACEHOLDER_ICON;
    691    }
    692  },
    693 
    694  _ensureDataHasProperties(data, requiredProperties) {
    695    for (let prop of requiredProperties) {
    696      if (!(prop in data)) {
    697        throw new Error("Message data missing required property: " + prop);
    698      }
    699    }
    700  },
    701 
    702  _initService() {
    703    if (!this._initServicePromise) {
    704      this._initServicePromise = Services.search.init();
    705    }
    706    return this._initServicePromise;
    707  },
    708 };
    709 
    710 export class ContentSearchParent extends JSWindowActorParent {
    711  constructor() {
    712    super();
    713    ContentSearch.init();
    714    gContentSearchActors.add(this);
    715  }
    716 
    717  didDestroy() {
    718    gContentSearchActors.delete(this);
    719  }
    720 
    721  receiveMessage(msg) {
    722    // Add a temporary event handler that exists only while the message is in
    723    // the event queue.  If the message's source docshell changes browsers in
    724    // the meantime, then we need to update the browser.  event.detail will be
    725    // the docshell's new parent <xul:browser> element.
    726    let browser = this.browsingContext.top.embedderElement;
    727    if (!browser) {
    728      // The associated browser has gone away, so there's nothing more we can
    729      // do here.
    730      return;
    731    }
    732    let eventItem = {
    733      type: "Message",
    734      name: msg.name,
    735      data: msg.data,
    736      browser,
    737      actor: this,
    738      handleEvent: event => {
    739        let browserData = ContentSearch._suggestionMap.get(eventItem.browser);
    740        if (browserData) {
    741          ContentSearch._suggestionMap.delete(eventItem.browser);
    742          ContentSearch._suggestionMap.set(event.detail, browserData);
    743        }
    744        browser.removeEventListener("SwapDocShells", eventItem, true);
    745        eventItem.browser = event.detail;
    746        eventItem.browser.addEventListener("SwapDocShells", eventItem, true);
    747      },
    748    };
    749    browser.addEventListener("SwapDocShells", eventItem, true);
    750 
    751    // Search requests cause cancellation of all Suggestion requests from the
    752    // same browser.
    753    if (msg.name === "Search") {
    754      ContentSearch._cancelSuggestions(eventItem);
    755    }
    756 
    757    ContentSearch._eventQueue.push(eventItem);
    758    ContentSearch._processEventQueue();
    759  }
    760 }