tor-browser

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

UrlbarProviderTopSites.sys.mjs (13730B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 /**
      6 * This module exports a provider returning the user's newtab Top Sites.
      7 */
      8 
      9 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
     10 
     11 import {
     12  UrlbarProvider,
     13  UrlbarUtils,
     14 } from "moz-src:///browser/components/urlbar/UrlbarUtils.sys.mjs";
     15 
     16 const lazy = {};
     17 
     18 ChromeUtils.defineESModuleGetters(lazy, {
     19  AboutNewTab: "resource:///modules/AboutNewTab.sys.mjs",
     20  PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
     21  TopSites: "resource:///modules/topsites/TopSites.sys.mjs",
     22  TOP_SITES_DEFAULT_ROWS: "resource:///modules/topsites/constants.mjs",
     23  TOP_SITES_MAX_SITES_PER_ROW: "resource:///modules/topsites/constants.mjs",
     24  UrlbarPrefs: "moz-src:///browser/components/urlbar/UrlbarPrefs.sys.mjs",
     25  UrlbarProviderOpenTabs:
     26    "moz-src:///browser/components/urlbar/UrlbarProviderOpenTabs.sys.mjs",
     27  UrlbarResult: "moz-src:///browser/components/urlbar/UrlbarResult.sys.mjs",
     28  UrlbarSearchUtils:
     29    "moz-src:///browser/components/urlbar/UrlbarSearchUtils.sys.mjs",
     30 });
     31 
     32 // These prefs must be true for the provider to return results. They are assumed
     33 // to be booleans. We check `system.topsites` because if it is disabled we would
     34 // get stale or empty top sites data.
     35 const TOP_SITES_ENABLED_PREFS = [
     36  "browser.urlbar.suggest.topsites",
     37  "browser.newtabpage.activity-stream.feeds.system.topsites",
     38 ];
     39 
     40 // Helper function to compare 2 URLs without refs.
     41 function sameUrlIgnoringRef(url1, url2) {
     42  if (!url1 || !url2) {
     43    return false;
     44  }
     45 
     46  let cleanUrl1 = url1.replace(/#.*$/, "");
     47  let cleanUrl2 = url2.replace(/#.*$/, "");
     48 
     49  return cleanUrl1 == cleanUrl2;
     50 }
     51 
     52 /**
     53 * A provider that returns the Top Sites shown on about:newtab.
     54 */
     55 export class UrlbarProviderTopSites extends UrlbarProvider {
     56  constructor() {
     57    super();
     58  }
     59 
     60  static get PRIORITY() {
     61    // Top sites are prioritized over the UrlbarProviderPlaces provider.
     62    return 1;
     63  }
     64 
     65  /**
     66   * @returns {Values<typeof UrlbarUtils.PROVIDER_TYPE>}
     67   */
     68  get type() {
     69    return UrlbarUtils.PROVIDER_TYPE.PROFILE;
     70  }
     71 
     72  /**
     73   * Whether this provider should be invoked for the given context.
     74   * If this method returns false, the providers manager won't start a query
     75   * with this provider, to save on resources.
     76   *
     77   * @param {UrlbarQueryContext} queryContext The query context object
     78   */
     79  async isActive(queryContext) {
     80    return (
     81      !queryContext.restrictSource &&
     82      !queryContext.searchString &&
     83      !queryContext.searchMode
     84    );
     85  }
     86 
     87  /**
     88   * Gets the provider's priority.
     89   *
     90   * @returns {number} The provider's priority for the given query.
     91   */
     92  getPriority() {
     93    return UrlbarProviderTopSites.PRIORITY;
     94  }
     95 
     96  /**
     97   * Starts querying.
     98   *
     99   * @param {UrlbarQueryContext} queryContext
    100   * @param {(provider: UrlbarProvider, result: UrlbarResult) => void} addCallback
    101   *   Callback invoked by the provider to add a new result.
    102   */
    103  async startQuery(queryContext, addCallback) {
    104    // Bail if Top Sites are not enabled. We check this condition here instead
    105    // of in isActive because we still want this provider to be restricting even
    106    // if this is not true. If it wasn't restricting, we would show the results
    107    // from UrlbarProviderPlaces's empty search behaviour. We aren't interested
    108    // in those since they are very similar to Top Sites and thus might be
    109    // confusing, especially since users can configure Top Sites but cannot
    110    // configure the default empty search results. See bug 1623666.
    111    let enabled = TOP_SITES_ENABLED_PREFS.every(p =>
    112      Services.prefs.getBoolPref(p, false)
    113    );
    114    if (!enabled) {
    115      return;
    116    }
    117 
    118    let sites;
    119    if (Services.prefs.getBoolPref("browser.topsites.component.enabled")) {
    120      sites = await lazy.TopSites.getSites();
    121    } else {
    122      sites = lazy.AboutNewTab.getTopSites();
    123    }
    124 
    125    let instance = this.queryInstance;
    126 
    127    // Filter out empty values. Site is empty when there's a gap between tiles
    128    // on about:newtab.
    129    sites = sites.filter(site => site);
    130 
    131    if (!lazy.UrlbarPrefs.get("sponsoredTopSites")) {
    132      sites = sites.filter(site => !site.sponsored_position);
    133    }
    134 
    135    // This is done here, rather than in the global scope, because
    136    // TOP_SITES_DEFAULT_ROWS causes import of topsites constants.mjs, and we want to
    137    // do that only when actually querying for Top Sites.
    138    if (UrlbarProviderTopSites.topSitesRows === undefined) {
    139      XPCOMUtils.defineLazyPreferenceGetter(
    140        UrlbarProviderTopSites,
    141        "topSitesRows",
    142        "browser.newtabpage.activity-stream.topSitesRows",
    143        lazy.TOP_SITES_DEFAULT_ROWS
    144      );
    145    }
    146 
    147    // We usually respect maxRichResults, though we never show a number of Top
    148    // Sites greater than what is visible in the New Tab Page, because the
    149    // additional ones couldn't be managed from the page.
    150    let numTopSites = Math.min(
    151      lazy.UrlbarPrefs.get("maxRichResults"),
    152      lazy.TOP_SITES_MAX_SITES_PER_ROW * UrlbarProviderTopSites.topSitesRows
    153    );
    154    sites = sites.slice(0, numTopSites);
    155 
    156    sites = sites.map(link => {
    157      let site = {
    158        type: link.searchTopSite ? "search" : "url",
    159        url: link.url_urlbar || link.url,
    160        isPinned: !!link.isPinned,
    161        isSponsored: !!link.sponsored_position,
    162        // The newtab page allows the user to set custom site titles, which
    163        // are stored in `label`, so prefer it.  Search top sites currently
    164        // don't have titles but `hostname` instead.
    165        title: link.label || link.title || link.hostname || "",
    166        favicon: link.smallFavicon || link.favicon || undefined,
    167        sendAttributionRequest: !!link.sendAttributionRequest,
    168      };
    169      if (site.isSponsored) {
    170        let {
    171          sponsored_tile_id,
    172          sponsored_impression_url,
    173          sponsored_click_url,
    174        } = link;
    175        site = {
    176          ...site,
    177          sponsoredTileId: sponsored_tile_id,
    178          sponsoredImpressionUrl: sponsored_impression_url,
    179          sponsoredClickUrl: sponsored_click_url,
    180        };
    181      }
    182      return site;
    183    });
    184 
    185    let tabUrlsToContextIds = new Map();
    186    if (lazy.UrlbarPrefs.get("suggest.openpage")) {
    187      if (lazy.UrlbarPrefs.get("switchTabs.searchAllContainers")) {
    188        lazy.UrlbarProviderOpenTabs.getOpenTabUrls(
    189          queryContext.isPrivate
    190        ).forEach((userContextAndGroupIds, url) => {
    191          let userContextIds = new Set();
    192          for (let [userContextId] of userContextAndGroupIds) {
    193            userContextIds.add(userContextId);
    194          }
    195          tabUrlsToContextIds.set(url, userContextIds);
    196        });
    197      } else {
    198        for (let [
    199          url,
    200          userContextId,
    201        ] of lazy.UrlbarProviderOpenTabs.getOpenTabUrlsForUserContextId(
    202          queryContext.userContextId,
    203          queryContext.isPrivate
    204        )) {
    205          let userContextIds = tabUrlsToContextIds.get(url);
    206          if (!userContextIds) {
    207            userContextIds = new Set();
    208          }
    209          userContextIds.add(userContextId);
    210          tabUrlsToContextIds.set(url, userContextIds);
    211        }
    212      }
    213    }
    214 
    215    for (let site of sites) {
    216      switch (site.type) {
    217        case "url": {
    218          let payload = {
    219            title: site.title,
    220            url: site.url,
    221            icon: site.favicon,
    222            isPinned: site.isPinned,
    223            isSponsored: site.isSponsored,
    224          };
    225 
    226          // Fuzzy match both the URL as-is, and the URL without ref, then
    227          // generate a merged Set.
    228          if (tabUrlsToContextIds) {
    229            let tabUserContextIds = new Set([
    230              ...(tabUrlsToContextIds.get(site.url) ?? []),
    231              ...(tabUrlsToContextIds.get(site.url.replace(/#.*$/, "")) ?? []),
    232            ]);
    233            if (tabUserContextIds.size) {
    234              let switchToTabResultAdded = false;
    235              for (let userContextId of tabUserContextIds) {
    236                // Normally we could skip the whole for loop, but if searchAllContainers
    237                // is set then the current page userContextId may differ, then we should
    238                // allow switching to other ones.
    239                if (
    240                  sameUrlIgnoringRef(queryContext.currentPage, site.url) &&
    241                  (!lazy.UrlbarPrefs.get("switchTabs.searchAllContainers") ||
    242                    queryContext.userContextId == userContextId)
    243                ) {
    244                  // Don't suggest switching to the current tab.
    245                  continue;
    246                }
    247                payload.userContextId = userContextId;
    248                let result = new lazy.UrlbarResult({
    249                  type: UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
    250                  source: UrlbarUtils.RESULT_SOURCE.TABS,
    251                  payload,
    252                });
    253                addCallback(this, result);
    254                switchToTabResultAdded = true;
    255              }
    256              // Avoid adding url result if Switch to Tab result was added.
    257              if (switchToTabResultAdded) {
    258                break;
    259              }
    260            }
    261          }
    262 
    263          if (site.isSponsored) {
    264            payload.sponsoredTileId = site.sponsoredTileId;
    265            payload.sponsoredClickUrl = site.sponsoredClickUrl;
    266          }
    267          payload.sendAttributionRequest = site.sendAttributionRequest;
    268 
    269          /** @type {Values<typeof UrlbarUtils.RESULT_SOURCE>} */
    270          let resultSource = UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL;
    271          if (lazy.UrlbarPrefs.get("suggest.bookmark")) {
    272            let bookmark = await lazy.PlacesUtils.bookmarks.fetch({
    273              url: new URL(payload.url),
    274            });
    275            // Check if query has been cancelled.
    276            if (instance != this.queryInstance) {
    277              break;
    278            }
    279            if (bookmark) {
    280              resultSource = UrlbarUtils.RESULT_SOURCE.BOOKMARKS;
    281            }
    282          }
    283 
    284          let result = new lazy.UrlbarResult({
    285            type: UrlbarUtils.RESULT_TYPE.URL,
    286            source: resultSource,
    287            payload,
    288          });
    289          addCallback(this, result);
    290          break;
    291        }
    292        case "search": {
    293          let engine = await lazy.UrlbarSearchUtils.engineForAlias(site.title);
    294 
    295          if (!engine && site.url) {
    296            // Look up the engine by its domain.
    297            let host = URL.parse(site.url)?.hostname;
    298            if (host) {
    299              engine = (
    300                await lazy.UrlbarSearchUtils.enginesForDomainPrefix(host)
    301              )[0];
    302            }
    303          }
    304 
    305          if (!engine) {
    306            // No engine found. We skip this Top Site.
    307            break;
    308          }
    309 
    310          if (instance != this.queryInstance) {
    311            break;
    312          }
    313 
    314          let result = new lazy.UrlbarResult({
    315            type: UrlbarUtils.RESULT_TYPE.SEARCH,
    316            source: UrlbarUtils.RESULT_SOURCE.SEARCH,
    317            payload: {
    318              keyword: site.title,
    319              providesSearchMode: true,
    320              engine: engine.name,
    321              query: "",
    322              icon: site.favicon,
    323              isPinned: site.isPinned,
    324            },
    325          });
    326          addCallback(this, result);
    327          break;
    328        }
    329        default:
    330          this.logger.error(`Unknown Top Site type: ${site.type}`);
    331          break;
    332      }
    333    }
    334  }
    335 
    336  onImpression(state, queryContext, controller, providerVisibleResults) {
    337    if (queryContext.isPrivate) {
    338      return;
    339    }
    340 
    341    providerVisibleResults.forEach(({ index, result }) => {
    342      if (result?.payload.isSponsored) {
    343        Glean.contextualServicesTopsites.impression[`urlbar_${index}`].add(1);
    344      }
    345    });
    346  }
    347 
    348  /**
    349   * Once initialized, contains an array of weak
    350   * references of top sites listener functions.
    351   *
    352   * @type {?{get: Function}[]}
    353   */
    354  static #topSitesListeners = null;
    355 
    356  /**
    357   * Adds a listener function that will be called when the top sites change or
    358   * they are enabled/disabled. This class will hold a weak reference to the
    359   * listener, so you do not need to unregister it, but you or someone else must
    360   * keep a strong reference to it to keep it from being immediately garbage
    361   * collected.
    362   *
    363   * @param {Function} callback
    364   *   The listener function. This class will hold a weak reference to it.
    365   */
    366  static addTopSitesListener(callback) {
    367    // Lazily init observers.
    368    if (!UrlbarProviderTopSites.#topSitesListeners) {
    369      UrlbarProviderTopSites.#topSitesListeners = [];
    370      let callListeners = UrlbarProviderTopSites.#callTopSitesListeners;
    371      if (Services.prefs.getBoolPref("browser.topsites.component.enabled")) {
    372        Services.obs.addObserver(callListeners, "topsites-refreshed");
    373      } else {
    374        Services.obs.addObserver(callListeners, "newtab-top-sites-changed");
    375      }
    376      for (let pref of TOP_SITES_ENABLED_PREFS) {
    377        Services.prefs.addObserver(pref, callListeners);
    378      }
    379    }
    380    UrlbarProviderTopSites.#topSitesListeners.push(
    381      Cu.getWeakReference(callback)
    382    );
    383  }
    384 
    385  static #callTopSitesListeners() {
    386    for (let i = 0; i < UrlbarProviderTopSites.#topSitesListeners.length; ) {
    387      let listener = UrlbarProviderTopSites.#topSitesListeners[i].get();
    388      if (!listener) {
    389        // The listener has been GC'ed, so remove it from our list.
    390        UrlbarProviderTopSites.#topSitesListeners.splice(i, 1);
    391      } else {
    392        listener();
    393        ++i;
    394      }
    395    }
    396  }
    397 
    398  /**
    399   * The number of top site rows to display by default.
    400   *
    401   * @type {number|undefined}
    402   */
    403  static topSitesRows;
    404 }