tor-browser

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

TopSites.sys.mjs (43868B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 // eslint-disable-next-line mozilla/use-static-import
      5 const { AppConstants } = ChromeUtils.importESModule(
      6  "resource://gre/modules/AppConstants.sys.mjs"
      7 );
      8 
      9 import {
     10  getDomain,
     11  TippyTopProvider,
     12 } from "resource:///modules/topsites/TippyTopProvider.sys.mjs";
     13 import { Dedupe } from "resource:///modules/Dedupe.sys.mjs";
     14 import { TOP_SITES_MAX_SITES_PER_ROW } from "resource:///modules/topsites/constants.mjs";
     15 import {
     16  CUSTOM_SEARCH_SHORTCUTS,
     17  checkHasSearchEngine,
     18  getSearchProvider,
     19 } from "moz-src:///toolkit/components/search/SearchShortcuts.sys.mjs";
     20 
     21 const lazy = {};
     22 
     23 ChromeUtils.defineESModuleGetters(lazy, {
     24  FilterAdult: "resource:///modules/FilterAdult.sys.mjs",
     25  LinksCache: "resource:///modules/LinksCache.sys.mjs",
     26  NetUtil: "resource://gre/modules/NetUtil.sys.mjs",
     27  NewTabUtils: "resource://gre/modules/NewTabUtils.sys.mjs",
     28  PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
     29  Region: "resource://gre/modules/Region.sys.mjs",
     30  RemoteSettings: "resource://services-settings/remote-settings.sys.mjs",
     31 });
     32 
     33 ChromeUtils.defineLazyGetter(lazy, "log", () => {
     34  const { Logger } = ChromeUtils.importESModule(
     35    "resource://messaging-system/lib/Logger.sys.mjs"
     36  );
     37  return new Logger("TopSites");
     38 });
     39 
     40 ChromeUtils.defineLazyGetter(lazy, "pageFrecencyThreshold", () => {
     41  // @backward-compat { version 147 }
     42  // Frecency was changed in 147 Nightly. This is a pre-cautionary measure
     43  // for train-hopping.
     44  if (Services.vc.compare(AppConstants.MOZ_APP_VERSION, "147.0a1") >= 0) {
     45    // 30 days ago, 5 visits. The threshold avoids one non-typed visit from
     46    // immediately being included in recent history to mimic the original
     47    // threshold which aimed to prevent first-run visits from being included in
     48    // Top Sites.
     49    return lazy.PlacesUtils.history.pageFrecencyThreshold(30, 5, false);
     50  }
     51  // The old threshold used for classic frecency: Slightly over one visit.
     52  return 101;
     53 });
     54 
     55 export const DEFAULT_TOP_SITES = [];
     56 
     57 const MIN_FAVICON_SIZE = 96;
     58 const PINNED_FAVICON_PROPS_TO_MIGRATE = [
     59  "favicon",
     60  "faviconRef",
     61  "faviconSize",
     62 ];
     63 
     64 // Preferences
     65 const NO_DEFAULT_SEARCH_TILE_PREF =
     66  "browser.newtabpage.activity-stream.improvesearch.noDefaultSearchTile";
     67 const SEARCH_SHORTCUTS_HAVE_PINNED_PREF =
     68  "browser.newtabpage.activity-stream.improvesearch.topSiteSearchShortcuts.havePinned";
     69 // TODO: Rename this when re-subscribing to the search engines pref.
     70 const SEARCH_SHORTCUTS_ENGINES =
     71  "browser.newtabpage.activity-stream.improvesearch.topSiteSearchShortcuts.searchEngines";
     72 const TOP_SITE_SEARCH_SHORTCUTS_PREF =
     73  "browser.newtabpage.activity-stream.improvesearch.topSiteSearchShortcuts";
     74 const TOP_SITES_ROWS_PREF = "browser.newtabpage.activity-stream.topSitesRows";
     75 
     76 // Search experiment stuff
     77 const SEARCH_FILTERS = [
     78  "google",
     79  "search.yahoo",
     80  "yahoo",
     81  "bing",
     82  "ask",
     83  "duckduckgo",
     84 ];
     85 
     86 const REMOTE_SETTING_DEFAULTS_PREF = "browser.topsites.useRemoteSetting";
     87 const DEFAULT_SITES_OVERRIDE_PREF =
     88  "browser.newtabpage.activity-stream.default.sites";
     89 const DEFAULT_SITES_EXPERIMENTS_PREF_BRANCH = "browser.topsites.experiment.";
     90 
     91 function getShortHostnameForCurrentSearch() {
     92  const url = lazy.NewTabUtils.shortHostname(
     93    Services.search.defaultEngine.searchUrlDomain
     94  );
     95  return url;
     96 }
     97 
     98 class _TopSites {
     99  #hasObservers = false;
    100  /**
    101   * A Promise used to determine if initialization is complete.
    102   *
    103   * @type {Promise}
    104   */
    105  #initPromise = null;
    106  #searchShortcuts = [];
    107  #sites = [];
    108 
    109  constructor() {
    110    this._tippyTopProvider = new TippyTopProvider();
    111    ChromeUtils.defineLazyGetter(
    112      this,
    113      "_currentSearchHostname",
    114      getShortHostnameForCurrentSearch
    115    );
    116    this.dedupe = new Dedupe(this._dedupeKey);
    117    this.frecentCache = new lazy.LinksCache(
    118      lazy.NewTabUtils.activityStreamLinks,
    119      "getTopSites",
    120      [],
    121      (oldOptions, newOptions) =>
    122        // Refresh if no old options or requesting more items
    123        !(oldOptions.numItems >= newOptions.numItems)
    124    );
    125    this.pinnedCache = new lazy.LinksCache(
    126      lazy.NewTabUtils.pinnedLinks,
    127      "links",
    128      [...PINNED_FAVICON_PROPS_TO_MIGRATE]
    129    );
    130    this._faviconProvider = new FaviconProvider();
    131    this.handlePlacesEvents = this.handlePlacesEvents.bind(this);
    132  }
    133 
    134  /**
    135   * Initializes the TopSites module.
    136   *
    137   * @returns {Promise}
    138   */
    139  async init() {
    140    if (this.#initPromise) {
    141      return this.#initPromise;
    142    }
    143    this.#initPromise = (async () => {
    144      lazy.log.debug("Initializing TopSites.");
    145      this.#addObservers();
    146      await this._readDefaults({ isStartup: true });
    147      // TopSites was initialized by the store calling the initialization
    148      // function and then updating custom search shortcuts. Since
    149      // initialization now happens upon the first retrieval of sites, we move
    150      // the update custom search shortcuts here.
    151      await this.updateCustomSearchShortcuts(true);
    152    })();
    153    return this.#initPromise;
    154  }
    155 
    156  uninit() {
    157    lazy.log.debug("Un-initializing TopSites.");
    158    this.#removeObservers();
    159    this.#searchShortcuts = [];
    160    this.#sites = [];
    161    this.#initPromise = null;
    162    this.frecentCache.expire();
    163    this.pinnedCache.expire();
    164  }
    165 
    166  #addObservers() {
    167    if (this.#hasObservers) {
    168      return;
    169    }
    170    // If the feed was previously disabled PREFS_INITIAL_VALUES was never received
    171    Services.obs.addObserver(this, "browser-search-engine-modified");
    172    Services.obs.addObserver(this, "browser-region-updated");
    173    Services.obs.addObserver(this, "newtab-linkBlocked");
    174    Services.prefs.addObserver(REMOTE_SETTING_DEFAULTS_PREF, this);
    175    Services.prefs.addObserver(DEFAULT_SITES_OVERRIDE_PREF, this);
    176    Services.prefs.addObserver(DEFAULT_SITES_EXPERIMENTS_PREF_BRANCH, this);
    177    Services.prefs.addObserver(NO_DEFAULT_SEARCH_TILE_PREF, this);
    178    Services.prefs.addObserver(SEARCH_SHORTCUTS_ENGINES, this);
    179    Services.prefs.addObserver(TOP_SITES_ROWS_PREF, this);
    180    Services.prefs.addObserver(TOP_SITE_SEARCH_SHORTCUTS_PREF, this);
    181    lazy.PlacesUtils.observers.addListener(
    182      ["bookmark-added", "bookmark-removed", "history-cleared", "page-removed"],
    183      this.handlePlacesEvents
    184    );
    185    this.#hasObservers = true;
    186  }
    187 
    188  #removeObservers() {
    189    if (!this.#hasObservers) {
    190      return;
    191    }
    192    Services.obs.removeObserver(this, "browser-search-engine-modified");
    193    Services.obs.removeObserver(this, "browser-region-updated");
    194    Services.obs.removeObserver(this, "newtab-linkBlocked");
    195    Services.prefs.removeObserver(REMOTE_SETTING_DEFAULTS_PREF, this);
    196    Services.prefs.removeObserver(DEFAULT_SITES_OVERRIDE_PREF, this);
    197    Services.prefs.removeObserver(DEFAULT_SITES_EXPERIMENTS_PREF_BRANCH, this);
    198    Services.prefs.removeObserver(NO_DEFAULT_SEARCH_TILE_PREF, this);
    199    Services.prefs.removeObserver(SEARCH_SHORTCUTS_ENGINES, this);
    200    Services.prefs.removeObserver(TOP_SITES_ROWS_PREF, this);
    201    Services.prefs.removeObserver(TOP_SITE_SEARCH_SHORTCUTS_PREF, this);
    202    lazy.PlacesUtils.observers.removeListener(
    203      ["bookmark-added", "bookmark-removed", "history-cleared", "page-removed"],
    204      this.handlePlacesEvents
    205    );
    206    this.#hasObservers = false;
    207  }
    208 
    209  _reset() {
    210    // Allow automated tests to reset the internal state of the component.
    211    if (Cu.isInAutomation) {
    212      this.#searchShortcuts = [];
    213      this.#sites = [];
    214    }
    215  }
    216 
    217  observe(subj, topic, data) {
    218    switch (topic) {
    219      case "browser-search-engine-modified":
    220        // We should update the current top sites if the search engine has been changed since
    221        // the search engine that gets filtered out of top sites has changed.
    222        // We also need to drop search shortcuts when their engine gets removed / hidden.
    223        if (
    224          data === "engine-default" &&
    225          Services.prefs.getBoolPref(NO_DEFAULT_SEARCH_TILE_PREF, true)
    226        ) {
    227          delete this._currentSearchHostname;
    228          this._currentSearchHostname = getShortHostnameForCurrentSearch();
    229        }
    230        this.refresh({ broadcast: true });
    231        break;
    232      case "browser-region-updated":
    233        this._readDefaults();
    234        break;
    235      case "newtab-linkBlocked":
    236        this.frecentCache.expire();
    237        this.pinnedCache.expire();
    238        this.refresh();
    239        break;
    240      case "nsPref:changed":
    241        switch (data) {
    242          case DEFAULT_SITES_OVERRIDE_PREF:
    243          case REMOTE_SETTING_DEFAULTS_PREF:
    244            this._readDefaults();
    245            break;
    246          case NO_DEFAULT_SEARCH_TILE_PREF:
    247            this.refresh();
    248            break;
    249          case TOP_SITES_ROWS_PREF:
    250          case SEARCH_SHORTCUTS_ENGINES:
    251            this.refresh();
    252            break;
    253          case TOP_SITE_SEARCH_SHORTCUTS_PREF:
    254            if (Services.prefs.getBoolPref(TOP_SITE_SEARCH_SHORTCUTS_PREF)) {
    255              this.updateCustomSearchShortcuts();
    256            } else {
    257              this.unpinAllSearchShortcuts();
    258            }
    259            this.refresh();
    260            break;
    261          default:
    262            if (data.startsWith(DEFAULT_SITES_EXPERIMENTS_PREF_BRANCH)) {
    263              this._readDefaults();
    264            }
    265            break;
    266        }
    267        break;
    268    }
    269  }
    270 
    271  handlePlacesEvents(events) {
    272    for (const {
    273      itemType,
    274      source,
    275      url,
    276      isRemovedFromStore,
    277      isTagging,
    278      type,
    279    } of events) {
    280      switch (type) {
    281        case "history-cleared":
    282          this.frecentCache.expire();
    283          this.refresh();
    284          break;
    285        case "page-removed":
    286          if (isRemovedFromStore) {
    287            this.frecentCache.expire();
    288            this.refresh();
    289          }
    290          break;
    291        case "bookmark-added":
    292          // Skips items that are not bookmarks (like folders), about:* pages or
    293          // default bookmarks, added when the profile is created.
    294          if (
    295            isTagging ||
    296            itemType !== lazy.PlacesUtils.bookmarks.TYPE_BOOKMARK ||
    297            source === lazy.PlacesUtils.bookmarks.SOURCES.IMPORT ||
    298            source === lazy.PlacesUtils.bookmarks.SOURCES.RESTORE ||
    299            source === lazy.PlacesUtils.bookmarks.SOURCES.RESTORE_ON_STARTUP ||
    300            source === lazy.PlacesUtils.bookmarks.SOURCES.SYNC ||
    301            (!url.startsWith("http://") && !url.startsWith("https://"))
    302          ) {
    303            return;
    304          }
    305 
    306          // TODO: Add a timed delay in case many links are changed.
    307          this.frecentCache.expire();
    308          this.refresh();
    309          break;
    310        case "bookmark-removed":
    311          if (
    312            isTagging ||
    313            (itemType === lazy.PlacesUtils.bookmarks.TYPE_BOOKMARK &&
    314              source !== lazy.PlacesUtils.bookmarks.SOURCES.IMPORT &&
    315              source !== lazy.PlacesUtils.bookmarks.SOURCES.RESTORE &&
    316              source !==
    317                lazy.PlacesUtils.bookmarks.SOURCES.RESTORE_ON_STARTUP &&
    318              source !== lazy.PlacesUtils.bookmarks.SOURCES.SYNC)
    319          ) {
    320            // TODO: Add a timed delay in case many links are changed.
    321            this.frecentCache.expire();
    322            this.refresh();
    323          }
    324          break;
    325      }
    326    }
    327  }
    328 
    329  /**
    330   * Returns a copied version of non-sponsored Top Sites. It will initialize
    331   * the component if it hasn't been already in order to set up and cache the
    332   * list, which will include pinned sites and search shortcuts. The number of
    333   * Top Sites returned is based on the number shown on New Tab due to the fact
    334   * it is the interface in which sites can be pinned/removed.
    335   *
    336   * @returns {Array<object>}
    337   *   A list of Top Sites.
    338   */
    339  async getSites() {
    340    await this.init();
    341    return structuredClone(this.#sites);
    342  }
    343 
    344  async getSearchShortcuts() {
    345    await this.init();
    346    return structuredClone(this.#searchShortcuts);
    347  }
    348 
    349  _dedupeKey(site) {
    350    return site && site.hostname;
    351  }
    352 
    353  /**
    354   * _readDefaults - sets DEFAULT_TOP_SITES
    355   */
    356  async _readDefaults({ isStartup = false } = {}) {
    357    this._useRemoteSetting = false;
    358 
    359    if (!Services.prefs.getBoolPref(REMOTE_SETTING_DEFAULTS_PREF)) {
    360      let sites = Services.prefs.getStringPref(DEFAULT_SITES_OVERRIDE_PREF, "");
    361      await this.refreshDefaults(sites, { isStartup });
    362      return;
    363    }
    364 
    365    // Try using default top sites from enterprise policies or tests. The pref
    366    // is locked when set via enterprise policy. Tests have no default sites
    367    // unless they set them via this pref.
    368    if (
    369      Services.prefs.prefIsLocked(DEFAULT_SITES_OVERRIDE_PREF) ||
    370      Cu.isInAutomation
    371    ) {
    372      let sites = Services.prefs.getStringPref(DEFAULT_SITES_OVERRIDE_PREF, "");
    373      await this.refreshDefaults(sites, { isStartup });
    374      return;
    375    }
    376 
    377    // Clear out the array of any previous defaults.
    378    DEFAULT_TOP_SITES.length = 0;
    379 
    380    // Read defaults from remote settings.
    381    this._useRemoteSetting = true;
    382    let remoteSettingData = await this._getRemoteConfig();
    383 
    384    for (let siteData of remoteSettingData) {
    385      let hostname = lazy.NewTabUtils.shortURL(siteData);
    386      let link = {
    387        isDefault: true,
    388        url: siteData.url,
    389        hostname,
    390        sendAttributionRequest: !!siteData.send_attribution_request,
    391      };
    392      if (siteData.url_urlbar_override) {
    393        link.url_urlbar = siteData.url_urlbar_override;
    394      }
    395      if (siteData.title) {
    396        link.label = siteData.title;
    397      }
    398      if (siteData.search_shortcut) {
    399        link = await this.topSiteToSearchTopSite(link);
    400      }
    401      DEFAULT_TOP_SITES.push(link);
    402    }
    403 
    404    await this.refresh({ isStartup });
    405  }
    406 
    407  async refreshDefaults(sites, { isStartup = false } = {}) {
    408    // Clear out the array of any previous defaults
    409    DEFAULT_TOP_SITES.length = 0;
    410 
    411    // Add default sites if any based on the pref
    412    if (sites) {
    413      for (const url of sites.split(",")) {
    414        const site = {
    415          isDefault: true,
    416          url,
    417        };
    418        site.hostname = lazy.NewTabUtils.shortURL(site);
    419        DEFAULT_TOP_SITES.push(site);
    420      }
    421    }
    422 
    423    await this.refresh({ isStartup });
    424  }
    425 
    426  async _getRemoteConfig(firstTime = true) {
    427    if (!this._remoteConfig) {
    428      this._remoteConfig = await lazy.RemoteSettings("top-sites");
    429      this._remoteConfig.on("sync", () => {
    430        this._readDefaults();
    431      });
    432    }
    433 
    434    let result = [];
    435    let failed = false;
    436    try {
    437      result = await this._remoteConfig.get();
    438    } catch (ex) {
    439      console.error(ex);
    440      failed = true;
    441    }
    442    if (!result.length) {
    443      console.error("Received empty top sites configuration!");
    444      failed = true;
    445    }
    446    // If we failed, or the result is empty, try loading from the local dump.
    447    if (firstTime && failed) {
    448      await this._remoteConfig.db.clear();
    449      // Now call this again.
    450      return this._getRemoteConfig(false);
    451    }
    452 
    453    // Sort sites based on the "order" attribute.
    454    result.sort((a, b) => a.order - b.order);
    455 
    456    result = result.filter(topsite => {
    457      // Filter by region.
    458      if (topsite.exclude_regions?.includes(lazy.Region.home)) {
    459        return false;
    460      }
    461      if (
    462        topsite.include_regions?.length &&
    463        !topsite.include_regions.includes(lazy.Region.home)
    464      ) {
    465        return false;
    466      }
    467 
    468      // Filter by locale.
    469      if (topsite.exclude_locales?.includes(Services.locale.appLocaleAsBCP47)) {
    470        return false;
    471      }
    472      if (
    473        topsite.include_locales?.length &&
    474        !topsite.include_locales.includes(Services.locale.appLocaleAsBCP47)
    475      ) {
    476        return false;
    477      }
    478 
    479      // Filter by experiment.
    480      // Exclude this top site if any of the specified experiments are running.
    481      if (
    482        topsite.exclude_experiments?.some(experimentID =>
    483          Services.prefs.getBoolPref(
    484            DEFAULT_SITES_EXPERIMENTS_PREF_BRANCH + experimentID,
    485            false
    486          )
    487        )
    488      ) {
    489        return false;
    490      }
    491      // Exclude this top site if none of the specified experiments are running.
    492      if (
    493        topsite.include_experiments?.length &&
    494        topsite.include_experiments.every(
    495          experimentID =>
    496            !Services.prefs.getBoolPref(
    497              DEFAULT_SITES_EXPERIMENTS_PREF_BRANCH + experimentID,
    498              false
    499            )
    500        )
    501      ) {
    502        return false;
    503      }
    504 
    505      return true;
    506    });
    507 
    508    return result;
    509  }
    510 
    511  /**
    512   * shouldFilterSearchTile - is default filtering enabled and does a given hostname match the user's default search engine?
    513   *
    514   * @param {string} hostname a top site hostname, such as "amazon" or "foo"
    515   * @returns {bool}
    516   */
    517  shouldFilterSearchTile(hostname) {
    518    if (
    519      Services.prefs.getBoolPref(NO_DEFAULT_SEARCH_TILE_PREF, true) &&
    520      (SEARCH_FILTERS.includes(hostname) ||
    521        hostname === this._currentSearchHostname)
    522    ) {
    523      return true;
    524    }
    525    return false;
    526  }
    527 
    528  /**
    529   * _maybeInsertSearchShortcuts - if the search shortcuts experiment is running,
    530   *                               insert search shortcuts if needed
    531   *
    532   * @param {Array} plainPinnedSites (from the pinnedSitesCache)
    533   * @returns {boolean} Did we insert any search shortcuts?
    534   */
    535  async _maybeInsertSearchShortcuts(plainPinnedSites) {
    536    // Only insert shortcuts if the experiment is running
    537    if (Services.prefs.getBoolPref(TOP_SITE_SEARCH_SHORTCUTS_PREF, true)) {
    538      // We don't want to insert shortcuts we've previously inserted
    539      const prevInsertedShortcuts = Services.prefs
    540        .getStringPref(SEARCH_SHORTCUTS_HAVE_PINNED_PREF, "")
    541        .split(",")
    542        .filter(s => s); // Filter out empty strings
    543      const newInsertedShortcuts = [];
    544 
    545      let shouldPin = this._useRemoteSetting
    546        ? DEFAULT_TOP_SITES.filter(s => s.searchTopSite).map(s => s.hostname)
    547        : Services.prefs.getStringPref(SEARCH_SHORTCUTS_ENGINES, "").split(",");
    548      shouldPin = shouldPin
    549        .map(getSearchProvider)
    550        .filter(s => s && s.shortURL !== this._currentSearchHostname);
    551 
    552      // If we've previously inserted all search shortcuts return early
    553      if (
    554        shouldPin.every(shortcut =>
    555          prevInsertedShortcuts.includes(shortcut.shortURL)
    556        )
    557      ) {
    558        return false;
    559      }
    560 
    561      const numberOfSlots =
    562        Services.prefs.getIntPref(TOP_SITES_ROWS_PREF, 1) *
    563        TOP_SITES_MAX_SITES_PER_ROW;
    564 
    565      // The plainPinnedSites array is populated with pinned sites at their
    566      // respective indices, and null everywhere else, but is not always the
    567      // right length
    568      const emptySlots = Math.max(numberOfSlots - plainPinnedSites.length, 0);
    569      const pinnedSites = [...plainPinnedSites].concat(
    570        Array(emptySlots).fill(null)
    571      );
    572 
    573      const tryToInsertSearchShortcut = async shortcut => {
    574        const nextAvailable = pinnedSites.indexOf(null);
    575        // Only add a search shortcut if the site isn't already pinned, we
    576        // haven't previously inserted it, there's space to pin it, and the
    577        // search engine is available in Firefox
    578        if (
    579          !pinnedSites.find(
    580            s => s && lazy.NewTabUtils.shortURL(s) === shortcut.shortURL
    581          ) &&
    582          !prevInsertedShortcuts.includes(shortcut.shortURL) &&
    583          nextAvailable > -1 &&
    584          (await checkHasSearchEngine(shortcut.keyword))
    585        ) {
    586          const site = await this.topSiteToSearchTopSite({ url: shortcut.url });
    587          this._pinSiteAt(site, nextAvailable);
    588          pinnedSites[nextAvailable] = site;
    589          newInsertedShortcuts.push(shortcut.shortURL);
    590        }
    591      };
    592 
    593      for (let shortcut of shouldPin) {
    594        await tryToInsertSearchShortcut(shortcut);
    595      }
    596 
    597      if (newInsertedShortcuts.length) {
    598        Services.prefs.setStringPref(
    599          SEARCH_SHORTCUTS_HAVE_PINNED_PREF,
    600          prevInsertedShortcuts.concat(newInsertedShortcuts).join(",")
    601        );
    602        return true;
    603      }
    604    }
    605 
    606    return false;
    607  }
    608 
    609  // eslint-disable-next-line max-statements
    610  async getLinksWithDefaults() {
    611    // Clear the previous sites.
    612    this.#sites = [];
    613 
    614    const numItems =
    615      Services.prefs.getIntPref(TOP_SITES_ROWS_PREF, 1) *
    616      TOP_SITES_MAX_SITES_PER_ROW;
    617    const searchShortcutsExperiment = Services.prefs.getBoolPref(
    618      TOP_SITE_SEARCH_SHORTCUTS_PREF,
    619      true
    620    );
    621    // We must wait for search services to initialize in order to access default
    622    // search engine properties without triggering a synchronous initialization
    623    try {
    624      await Services.search.init();
    625    } catch {
    626      // We continue anyway because we want the user to see their sponsored,
    627      // saved, or visited shortcut tiles even if search engines are not
    628      // available.
    629    }
    630 
    631    // Get all frecent sites from history.
    632    let frecent = [];
    633    let cache;
    634    try {
    635      // Request can throw if executing the linkGetter inside LinksCache returns
    636      // a null object.
    637      cache = await this.frecentCache.request({
    638        // We need to overquery due to the top 5 alexa search + default search possibly being removed
    639        numItems: numItems + SEARCH_FILTERS.length + 1,
    640        topsiteFrecency: lazy.pageFrecencyThreshold,
    641      });
    642    } catch (ex) {
    643      cache = [];
    644    }
    645 
    646    for (let link of cache) {
    647      // The cache can contain null values.
    648      if (!link) {
    649        continue;
    650      }
    651      const hostname = lazy.NewTabUtils.shortURL(link);
    652      if (!this.shouldFilterSearchTile(hostname)) {
    653        frecent.push({
    654          ...(searchShortcutsExperiment
    655            ? await this.topSiteToSearchTopSite(link)
    656            : link),
    657          hostname,
    658        });
    659      }
    660    }
    661 
    662    // Get defaults.
    663    let notBlockedDefaultSites = [];
    664    for (let link of DEFAULT_TOP_SITES) {
    665      if (this.shouldFilterSearchTile(link.hostname)) {
    666        continue;
    667      }
    668      // Drop blocked default sites.
    669      if (
    670        lazy.NewTabUtils.blockedLinks.isBlocked({
    671          url: link.url,
    672        })
    673      ) {
    674        continue;
    675      }
    676      // If we've previously blocked a search shortcut, remove the default top site
    677      // that matches the hostname
    678      const searchProvider = getSearchProvider(lazy.NewTabUtils.shortURL(link));
    679      if (
    680        searchProvider &&
    681        lazy.NewTabUtils.blockedLinks.isBlocked({ url: searchProvider.url })
    682      ) {
    683        continue;
    684      }
    685      notBlockedDefaultSites.push(
    686        searchShortcutsExperiment
    687          ? await this.topSiteToSearchTopSite(link)
    688          : link
    689      );
    690    }
    691 
    692    // Get pinned links augmented with desired properties
    693    let plainPinned = await this.pinnedCache.request();
    694 
    695    // Insert search shortcuts if we need to.
    696    // _maybeInsertSearchShortcuts returns true if any search shortcuts are
    697    // inserted, meaning we need to expire and refresh the pinnedCache
    698    if (await this._maybeInsertSearchShortcuts(plainPinned)) {
    699      this.pinnedCache.expire();
    700      plainPinned = await this.pinnedCache.request();
    701    }
    702 
    703    const pinned = await Promise.all(
    704      plainPinned.map(async link => {
    705        if (!link) {
    706          return link;
    707        }
    708 
    709        // Drop pinned search shortcuts when their engine has been removed / hidden.
    710        if (link.searchTopSite) {
    711          const searchProvider = getSearchProvider(
    712            lazy.NewTabUtils.shortURL(link)
    713          );
    714          if (
    715            !searchProvider ||
    716            !(await checkHasSearchEngine(searchProvider.keyword))
    717          ) {
    718            return null;
    719          }
    720        }
    721 
    722        // Copy all properties from a frecent link and add more
    723        const finder = other => other.url === link.url;
    724 
    725        const frecentSite = frecent.find(finder);
    726        // If the link is a frecent site, do not copy over 'isDefault', else check
    727        // if the site is a default site
    728        const copy = Object.assign(
    729          {},
    730          frecentSite || { isDefault: !!notBlockedDefaultSites.find(finder) },
    731          link,
    732          { hostname: lazy.NewTabUtils.shortURL(link) },
    733          { searchTopSite: !!link.searchTopSite }
    734        );
    735 
    736        // Add in favicons if we don't already have it
    737        if (!copy.favicon) {
    738          try {
    739            lazy.NewTabUtils.activityStreamProvider._faviconBytesToDataURI(
    740              await lazy.NewTabUtils.activityStreamProvider._addFavicons([copy])
    741            );
    742 
    743            for (const prop of PINNED_FAVICON_PROPS_TO_MIGRATE) {
    744              copy.__sharedCache.updateLink(prop, copy[prop]);
    745            }
    746          } catch (e) {
    747            // Some issue with favicon, so just continue without one
    748          }
    749        }
    750 
    751        return copy;
    752      })
    753    );
    754 
    755    // Remove any duplicates from frecent and default sites
    756    const [, dedupedFrecent, dedupedDefaults] = this.dedupe.group(
    757      pinned,
    758      frecent,
    759      notBlockedDefaultSites
    760    );
    761    const dedupedUnpinned = [...dedupedFrecent, ...dedupedDefaults];
    762 
    763    // Remove adult sites if we need to
    764    const checkedAdult = lazy.FilterAdult.filter(dedupedUnpinned);
    765 
    766    // Insert the original pinned sites into the deduped frecent and defaults.
    767    let withPinned = insertPinned(checkedAdult, pinned);
    768    // Remove excess items.
    769    withPinned = withPinned.slice(0, numItems);
    770 
    771    // Now, get a tippy top icon or a rich icon for every item.
    772    for (const link of withPinned) {
    773      if (link) {
    774        if (link.searchTopSite && !link.isDefault) {
    775          this._tippyTopProvider.processSite(link);
    776        } else {
    777          this._fetchIcon(link);
    778        }
    779 
    780        // Remove internal properties that might be updated after dispatch
    781        delete link.__sharedCache;
    782 
    783        // Indicate that these links should get a frecency bonus when clicked
    784        link.typedBonus = true;
    785      }
    786    }
    787 
    788    this.#sites = withPinned;
    789 
    790    return withPinned;
    791  }
    792 
    793  /**
    794   * Refresh the top sites data for content.
    795   *
    796   * @param {object} options
    797   * @param {bool} options.isStartup Being called while TopSitesFeed is initting.
    798   */
    799  async refresh(options = {}) {
    800    // Avoiding refreshing if it's already happening.
    801    if (this._refreshing) {
    802      return;
    803    }
    804    if (!this._startedUp && !options.isStartup) {
    805      // Initial refresh still pending.
    806      return;
    807    }
    808    this._refreshing = true;
    809    this._startedUp = true;
    810 
    811    if (!this._tippyTopProvider.initialized) {
    812      await this._tippyTopProvider.init();
    813    }
    814 
    815    await this.getLinksWithDefaults();
    816    this._refreshing = false;
    817    Services.obs.notifyObservers(null, "topsites-refreshed", options.isStartup);
    818  }
    819 
    820  async updateCustomSearchShortcuts(isStartup = false) {
    821    if (
    822      !Services.prefs.getBoolPref(
    823        "browser.newtabpage.activity-stream.improvesearch.noDefaultSearchTile",
    824        true
    825      )
    826    ) {
    827      return;
    828    }
    829 
    830    if (!this._tippyTopProvider.initialized) {
    831      await this._tippyTopProvider.init();
    832    }
    833 
    834    // Populate the state with available search shortcuts
    835    let searchShortcuts = [];
    836    for (const engine of await Services.search.getAppProvidedEngines()) {
    837      const shortcut = CUSTOM_SEARCH_SHORTCUTS.find(s =>
    838        engine.aliases.includes(s.keyword)
    839      );
    840      if (shortcut) {
    841        let clone = { ...shortcut };
    842        this._tippyTopProvider.processSite(clone);
    843        searchShortcuts.push(clone);
    844      }
    845    }
    846 
    847    // TODO: Determine what the purpose of this is.
    848    this.#searchShortcuts = searchShortcuts;
    849 
    850    Services.obs.notifyObservers(
    851      null,
    852      "topsites-updated-custom-search-shortcuts",
    853      isStartup
    854    );
    855  }
    856 
    857  async topSiteToSearchTopSite(site) {
    858    const searchProvider = getSearchProvider(lazy.NewTabUtils.shortURL(site));
    859    if (
    860      !searchProvider ||
    861      !(await checkHasSearchEngine(searchProvider.keyword))
    862    ) {
    863      return site;
    864    }
    865    return {
    866      ...site,
    867      searchTopSite: true,
    868      label: searchProvider.keyword,
    869    };
    870  }
    871 
    872  /**
    873   * Get an image for the link preferring tippy top, or rich favicon.
    874   */
    875  async _fetchIcon(link) {
    876    // Nothing to do if we already have a rich icon from the page
    877    if (link.favicon && link.faviconSize >= MIN_FAVICON_SIZE) {
    878      return;
    879    }
    880 
    881    // Nothing more to do if we can use a default tippy top icon
    882    this._tippyTopProvider.processSite(link);
    883    if (link.tippyTopIcon) {
    884      return;
    885    }
    886 
    887    // Make a request for a better icon
    888    this._requestRichIcon(link.url);
    889  }
    890 
    891  _requestRichIcon(url) {
    892    this._faviconProvider.fetchIcon(url);
    893  }
    894 
    895  /**
    896   * Inform others that top sites data has been updated due to pinned changes.
    897   */
    898  _broadcastPinnedSitesUpdated() {
    899    // Pinned data changed, so make sure we get latest
    900    this.pinnedCache.expire();
    901 
    902    // Refresh to trigger deduping, etc.
    903    this.refresh();
    904  }
    905 
    906  /**
    907   * Pin a site at a specific position saving only the desired keys.
    908   *
    909   * @param label {string} User set string of custom site name
    910   */
    911  // To refactor in Bug 1891997
    912  /* eslint-enable jsdoc/check-param-names */
    913  async _pinSiteAt({ label, url, searchTopSite }, index) {
    914    const toPin = { url };
    915    if (label) {
    916      toPin.label = label;
    917    }
    918    if (searchTopSite) {
    919      toPin.searchTopSite = searchTopSite;
    920    }
    921    lazy.NewTabUtils.pinnedLinks.pin(toPin, index);
    922  }
    923 
    924  /**
    925   * Handle a pin action of a site to a position.
    926   */
    927  async pin(action) {
    928    let { site, index } = action.data;
    929    index = this._adjustPinIndexForSponsoredLinks(site, index);
    930    // If valid index provided, pin at that position
    931    if (index >= 0) {
    932      await this._pinSiteAt(site, index);
    933      this._broadcastPinnedSitesUpdated();
    934    } else {
    935      // Bug 1458658. If the top site is being pinned from an 'Add a Top Site' option,
    936      // then we want to make sure to unblock that link if it has previously been
    937      // blocked. We know if the site has been added because the index will be -1.
    938      if (index === -1) {
    939        lazy.NewTabUtils.blockedLinks.unblock({ url: site.url });
    940        this.frecentCache.expire();
    941      }
    942      this.insert(action);
    943    }
    944  }
    945 
    946  /**
    947   * Handle an unpin action of a site.
    948   */
    949  unpin(action) {
    950    const { site } = action.data;
    951    lazy.NewTabUtils.pinnedLinks.unpin(site);
    952    this._broadcastPinnedSitesUpdated();
    953  }
    954 
    955  unpinAllSearchShortcuts() {
    956    Services.prefs.clearUserPref(SEARCH_SHORTCUTS_HAVE_PINNED_PREF);
    957    for (let pinnedLink of lazy.NewTabUtils.pinnedLinks.links) {
    958      if (pinnedLink && pinnedLink.searchTopSite) {
    959        lazy.NewTabUtils.pinnedLinks.unpin(pinnedLink);
    960      }
    961    }
    962    this.pinnedCache.expire();
    963  }
    964 
    965  _unpinSearchShortcut(vendor) {
    966    for (let pinnedLink of lazy.NewTabUtils.pinnedLinks.links) {
    967      if (
    968        pinnedLink &&
    969        pinnedLink.searchTopSite &&
    970        lazy.NewTabUtils.shortURL(pinnedLink) === vendor
    971      ) {
    972        lazy.NewTabUtils.pinnedLinks.unpin(pinnedLink);
    973        this.pinnedCache.expire();
    974 
    975        const prevInsertedShortcuts = Services.prefs.getStringPref(
    976          SEARCH_SHORTCUTS_HAVE_PINNED_PREF,
    977          ""
    978        );
    979        Services.prefs.setStringPref(
    980          SEARCH_SHORTCUTS_HAVE_PINNED_PREF,
    981          prevInsertedShortcuts.filter(s => s !== vendor).join(",")
    982        );
    983        break;
    984      }
    985    }
    986  }
    987 
    988  /**
    989   * Reduces the given pinning index by the number of preceding sponsored
    990   * sites, to accomodate for sponsored sites pushing pinned ones to the side,
    991   * effectively increasing their index again.
    992   */
    993  _adjustPinIndexForSponsoredLinks(site, index) {
    994    if (!this.#sites) {
    995      return index;
    996    }
    997    // Adjust insertion index for sponsored sites since their position is
    998    // fixed.
    999    let adjustedIndex = index;
   1000    for (let i = 0; i < index; i++) {
   1001      const link = this.#sites[i];
   1002      if (link && link.sponsored_position && this.#sites[i]?.url !== site.url) {
   1003        adjustedIndex--;
   1004      }
   1005    }
   1006    return adjustedIndex;
   1007  }
   1008 
   1009  /**
   1010   * Insert a site to pin at a position shifting over any other pinned sites.
   1011   */
   1012  _insertPin(site, originalIndex, draggedFromIndex) {
   1013    let index = this._adjustPinIndexForSponsoredLinks(site, originalIndex);
   1014 
   1015    // Don't insert any pins past the end of the visible top sites. Otherwise,
   1016    // we can end up with a bunch of pinned sites that can never be unpinned again
   1017    // from the UI.
   1018    const topSitesCount =
   1019      Services.prefs.getIntPref(TOP_SITES_ROWS_PREF, 1) *
   1020      TOP_SITES_MAX_SITES_PER_ROW;
   1021    if (index >= topSitesCount) {
   1022      return;
   1023    }
   1024 
   1025    let pinned = lazy.NewTabUtils.pinnedLinks.links;
   1026    if (!pinned[index]) {
   1027      this._pinSiteAt(site, index);
   1028    } else {
   1029      pinned[draggedFromIndex] = null;
   1030      // Find the hole to shift the pinned site(s) towards. We shift towards the
   1031      // hole left by the site being dragged.
   1032      let holeIndex = index;
   1033      const indexStep = index > draggedFromIndex ? -1 : 1;
   1034      while (pinned[holeIndex]) {
   1035        holeIndex += indexStep;
   1036      }
   1037      if (holeIndex >= topSitesCount || holeIndex < 0) {
   1038        // There are no holes, so we will effectively unpin the last slot and shifting
   1039        // towards it. This only happens when adding a new top site to an already
   1040        // fully pinned grid.
   1041        holeIndex = topSitesCount - 1;
   1042      }
   1043 
   1044      // Shift towards the hole.
   1045      const shiftingStep = holeIndex > index ? -1 : 1;
   1046      while (holeIndex !== index) {
   1047        const nextIndex = holeIndex + shiftingStep;
   1048        this._pinSiteAt(pinned[nextIndex], holeIndex);
   1049        holeIndex = nextIndex;
   1050      }
   1051      this._pinSiteAt(site, index);
   1052    }
   1053  }
   1054 
   1055  /**
   1056   * Handle an insert (drop/add) action of a site.
   1057   */
   1058  async insert(action) {
   1059    let { index } = action.data;
   1060    // Treat invalid pin index values (e.g., -1, undefined) as insert in the first position
   1061    if (!(index > 0)) {
   1062      index = 0;
   1063    }
   1064 
   1065    // Inserting a top site pins it in the specified slot, pushing over any link already
   1066    // pinned in the slot (unless it's the last slot, then it replaces).
   1067    this._insertPin(
   1068      action.data.site,
   1069      index,
   1070      action.data.draggedFromIndex !== undefined
   1071        ? action.data.draggedFromIndex
   1072        : Services.prefs.getIntPref(TOP_SITES_ROWS_PREF, 1) *
   1073            TOP_SITES_MAX_SITES_PER_ROW
   1074    );
   1075 
   1076    this._broadcastPinnedSitesUpdated();
   1077  }
   1078 
   1079  updatePinnedSearchShortcuts({ addedShortcuts, deletedShortcuts }) {
   1080    // Unpin the deletedShortcuts.
   1081    deletedShortcuts.forEach(({ url }) => {
   1082      lazy.NewTabUtils.pinnedLinks.unpin({ url });
   1083    });
   1084 
   1085    // Pin the addedShortcuts.
   1086    const numberOfSlots =
   1087      Services.prefs.getIntPref(TOP_SITES_ROWS_PREF, 1) *
   1088      TOP_SITES_MAX_SITES_PER_ROW;
   1089    addedShortcuts.forEach(shortcut => {
   1090      // Find first hole in pinnedLinks.
   1091      let index = lazy.NewTabUtils.pinnedLinks.links.findIndex(link => !link);
   1092      if (
   1093        index < 0 &&
   1094        lazy.NewTabUtils.pinnedLinks.links.length + 1 < numberOfSlots
   1095      ) {
   1096        // pinnedLinks can have less slots than the total available.
   1097        index = lazy.NewTabUtils.pinnedLinks.links.length;
   1098      }
   1099      if (index >= 0) {
   1100        lazy.NewTabUtils.pinnedLinks.pin(shortcut, index);
   1101      } else {
   1102        // No slots available, we need to do an insert in first slot and push over other pinned links.
   1103        this._insertPin(shortcut, 0, numberOfSlots);
   1104      }
   1105    });
   1106 
   1107    this._broadcastPinnedSitesUpdated();
   1108  }
   1109 }
   1110 
   1111 /**
   1112 * insertPinned - Inserts pinned links in their specified slots
   1113 *
   1114 * @param {Array} links list of links
   1115 * @param {Array} pinned list of pinned links
   1116 * @returns {Array} resulting list of links with pinned links inserted
   1117 */
   1118 export function insertPinned(links, pinned) {
   1119  // Remove any pinned links
   1120  const pinnedUrls = pinned.map(link => link && link.url);
   1121  let newLinks = links.filter(link =>
   1122    link ? !pinnedUrls.includes(link.url) : false
   1123  );
   1124  newLinks = newLinks.map(link => {
   1125    if (link && link.isPinned) {
   1126      delete link.isPinned;
   1127      delete link.pinIndex;
   1128    }
   1129    return link;
   1130  });
   1131 
   1132  // Then insert them in their specified location
   1133  pinned.forEach((val, index) => {
   1134    if (!val) {
   1135      return;
   1136    }
   1137    let link = Object.assign({}, val, { isPinned: true, pinIndex: index });
   1138    if (index > newLinks.length) {
   1139      newLinks[index] = link;
   1140    } else {
   1141      newLinks.splice(index, 0, link);
   1142    }
   1143  });
   1144 
   1145  return newLinks;
   1146 }
   1147 
   1148 /**
   1149 * FaviconProvider class handles the retrieval and management of favicons
   1150 * for TopSites.
   1151 */
   1152 export class FaviconProvider {
   1153  constructor() {
   1154    this._queryForRedirects = new Set();
   1155  }
   1156 
   1157  /**
   1158   * fetchIcon attempts to fetch a rich icon for the given url from two sources.
   1159   * First, it looks up the tippy top feed, if it's still missing, then it queries
   1160   * the places for rich icon with its most recent visit in order to deal with
   1161   * the redirected visit. See Bug 1421428 for more details.
   1162   */
   1163  async fetchIcon(url) {
   1164    // Avoid initializing and fetching icons if prefs are turned off
   1165    if (!this.shouldFetchIcons) {
   1166      return;
   1167    }
   1168 
   1169    const site = await this.getSite(getDomain(url));
   1170    if (!site) {
   1171      if (!this._queryForRedirects.has(url)) {
   1172        this._queryForRedirects.add(url);
   1173        Services.tm.idleDispatchToMainThread(() =>
   1174          this.fetchIconFromRedirects(url)
   1175        );
   1176      }
   1177      return;
   1178    }
   1179 
   1180    let iconUri = Services.io.newURI(site.image_url);
   1181    // The #tippytop is to be able to identify them for telemetry.
   1182    iconUri = iconUri.mutate().setRef("tippytop").finalize();
   1183    await this.#setFaviconForPage(Services.io.newURI(url), iconUri);
   1184  }
   1185 
   1186  /**
   1187   * Get the site tippy top data from Remote Settings.
   1188   */
   1189  async getSite(domain) {
   1190    const sites = await this.tippyTop.get({
   1191      filters: { domain },
   1192      syncIfEmpty: false,
   1193    });
   1194    return sites.length ? sites[0] : null;
   1195  }
   1196 
   1197  /**
   1198   * Get the tippy top collection from Remote Settings.
   1199   */
   1200  get tippyTop() {
   1201    if (!this._tippyTop) {
   1202      this._tippyTop = lazy.RemoteSettings("tippytop");
   1203    }
   1204    return this._tippyTop;
   1205  }
   1206 
   1207  /**
   1208   * Determine if we should be fetching and saving icons.
   1209   */
   1210  get shouldFetchIcons() {
   1211    return Services.prefs.getBoolPref("browser.chrome.site_icons");
   1212  }
   1213 
   1214  /**
   1215   * Get favicon info (uri and size) for a uri from Places.
   1216   *
   1217   * @param {nsIURI} uri
   1218   *        Page to check for favicon data
   1219   * @returns {object}
   1220   *        Favicon info object. If there is no data in DB, return null.
   1221   */
   1222  async getFaviconInfo(uri) {
   1223    let favicon = await lazy.PlacesUtils.favicons.getFaviconForPage(
   1224      uri,
   1225      lazy.NewTabUtils.activityStreamProvider.THUMB_FAVICON_SIZE
   1226    );
   1227    return favicon
   1228      ? { iconUri: favicon.uri, faviconSize: favicon.width }
   1229      : null;
   1230  }
   1231 
   1232  /**
   1233   * Fetch favicon for a url by following its redirects in Places.
   1234   *
   1235   * This can improve the rich icon coverage for Top Sites since Places only
   1236   * associates the favicon to the final url if the original one gets redirected.
   1237   * Note this is not an urgent request, hence it is dispatched to the main
   1238   * thread idle handler to avoid any possible performance impact.
   1239   */
   1240  async fetchIconFromRedirects(url) {
   1241    const visitPaths = await this.#fetchVisitPaths(url);
   1242    if (visitPaths.length > 1) {
   1243      const lastVisit = visitPaths.pop();
   1244      const redirectedUri = Services.io.newURI(lastVisit.url);
   1245      const iconInfo = await this.getFaviconInfo(redirectedUri);
   1246      if (iconInfo?.faviconSize >= MIN_FAVICON_SIZE) {
   1247        try {
   1248          await lazy.PlacesUtils.favicons.tryCopyFavicons(
   1249            redirectedUri,
   1250            Services.io.newURI(url),
   1251            lazy.PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE
   1252          );
   1253        } catch (ex) {
   1254          console.error(`Failed to copy favicon [${ex}]`);
   1255        }
   1256      }
   1257    }
   1258  }
   1259 
   1260  /**
   1261   * Get favicon data for given URL from network.
   1262   *
   1263   * @param {nsIURI} faviconURI
   1264   *        nsIURI for the favicon.
   1265   * @returns {nsIURI} data URL
   1266   */
   1267  async getFaviconDataURLFromNetwork(faviconURI) {
   1268    let channel = lazy.NetUtil.newChannel({
   1269      uri: faviconURI,
   1270      loadingPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
   1271      securityFlags:
   1272        Ci.nsILoadInfo.SEC_REQUIRE_CORS_INHERITS_SEC_CONTEXT |
   1273        Ci.nsILoadInfo.SEC_ALLOW_CHROME |
   1274        Ci.nsILoadInfo.SEC_DISALLOW_SCRIPT,
   1275      contentPolicyType: Ci.nsIContentPolicy.TYPE_INTERNAL_IMAGE_FAVICON,
   1276    });
   1277 
   1278    let resolver = Promise.withResolvers();
   1279 
   1280    lazy.NetUtil.asyncFetch(channel, async (input, status, request) => {
   1281      if (!Components.isSuccessCode(status)) {
   1282        resolver.resolve();
   1283        return;
   1284      }
   1285 
   1286      try {
   1287        let data = lazy.NetUtil.readInputStream(input, input.available());
   1288        let { contentType } = request.QueryInterface(Ci.nsIChannel);
   1289        input.close();
   1290 
   1291        let buffer = new Uint8ClampedArray(data);
   1292        let blob = new Blob([buffer], { type: contentType });
   1293        let dataURL = await new Promise((resolve, reject) => {
   1294          let reader = new FileReader();
   1295          reader.addEventListener("load", () => resolve(reader.result));
   1296          reader.addEventListener("error", reject);
   1297          reader.readAsDataURL(blob);
   1298        });
   1299        resolver.resolve(Services.io.newURI(dataURL));
   1300      } catch (e) {
   1301        resolver.reject(e);
   1302      }
   1303    });
   1304 
   1305    return resolver.promise;
   1306  }
   1307 
   1308  /**
   1309   * Set favicon for page.
   1310   *
   1311   * @param {nsIURI} pageURI
   1312   * @param {nsIURI} faviconURI
   1313   */
   1314  async #setFaviconForPage(pageURI, faviconURI) {
   1315    try {
   1316      // If the given faviconURI is data URL, set it as is.
   1317      if (faviconURI.schemeIs("data")) {
   1318        lazy.PlacesUtils.favicons
   1319          .setFaviconForPage(pageURI, faviconURI, faviconURI)
   1320          .catch(console.error);
   1321        return;
   1322      }
   1323 
   1324      // Try to find the favicon data from DB.
   1325      const faviconInfo = await this.getFaviconInfo(pageURI);
   1326      if (faviconInfo?.faviconSize) {
   1327        // As valid favicon data is already stored for the page,
   1328        // we don't have to update.
   1329        return;
   1330      }
   1331 
   1332      // Otherwise, fetch from network.
   1333      lazy.PlacesUtils.favicons
   1334        .setFaviconForPage(
   1335          pageURI,
   1336          faviconURI,
   1337          await this.getFaviconDataURLFromNetwork(faviconURI)
   1338        )
   1339        .catch(console.error);
   1340    } catch (ex) {
   1341      console.error(`Failed to set favicon for page:${ex}`);
   1342    }
   1343  }
   1344 
   1345  /**
   1346   * Fetches visit paths for a given URL from its most recent visit in Places.
   1347   *
   1348   * Note that this includes the URL itself as well as all the following
   1349   * permenent&temporary redirected URLs if any.
   1350   *
   1351   * @param {string} url
   1352   *        a URL string
   1353   *
   1354   * @returns {Array} Returns an array containing objects as
   1355   *   {int}    visit_id: ID of the visit in moz_historyvisits.
   1356   *   {String} url: URL of the redirected URL.
   1357   */
   1358  async #fetchVisitPaths(url) {
   1359    const query = `
   1360    WITH RECURSIVE path(visit_id)
   1361    AS (
   1362      SELECT v.id
   1363      FROM moz_places h
   1364      JOIN moz_historyvisits v
   1365        ON v.place_id = h.id
   1366      WHERE h.url_hash = hash(:url) AND h.url = :url
   1367        AND v.visit_date = h.last_visit_date
   1368 
   1369      UNION
   1370 
   1371      SELECT id
   1372      FROM moz_historyvisits
   1373      JOIN path
   1374        ON visit_id = from_visit
   1375      WHERE visit_type IN
   1376        (${lazy.PlacesUtils.history.TRANSITIONS.REDIRECT_PERMANENT},
   1377         ${lazy.PlacesUtils.history.TRANSITIONS.REDIRECT_TEMPORARY})
   1378    )
   1379    SELECT visit_id, (
   1380      SELECT (
   1381        SELECT url
   1382        FROM moz_places
   1383        WHERE id = place_id)
   1384      FROM moz_historyvisits
   1385      WHERE id = visit_id) AS url
   1386    FROM path
   1387  `;
   1388 
   1389    const visits =
   1390      await lazy.NewTabUtils.activityStreamProvider.executePlacesQuery(query, {
   1391        columns: ["visit_id", "url"],
   1392        params: { url },
   1393      });
   1394    return visits;
   1395  }
   1396 }
   1397 
   1398 export const TopSites = new _TopSites();