394 lines
13 KiB
JavaScript
394 lines
13 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
/**
|
|
* This module exports a provider returning the user's newtab Top Sites.
|
|
*/
|
|
|
|
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
|
|
|
|
import {
|
|
UrlbarProvider,
|
|
UrlbarUtils,
|
|
} from "resource:///modules/UrlbarUtils.sys.mjs";
|
|
|
|
const lazy = {};
|
|
|
|
ChromeUtils.defineESModuleGetters(lazy, {
|
|
AboutNewTab: "resource:///modules/AboutNewTab.sys.mjs",
|
|
PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
|
|
TopSites: "resource:///modules/topsites/TopSites.sys.mjs",
|
|
TOP_SITES_DEFAULT_ROWS: "resource:///modules/topsites/constants.mjs",
|
|
TOP_SITES_MAX_SITES_PER_ROW: "resource:///modules/topsites/constants.mjs",
|
|
UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs",
|
|
UrlbarProviderOpenTabs: "resource:///modules/UrlbarProviderOpenTabs.sys.mjs",
|
|
UrlbarResult: "resource:///modules/UrlbarResult.sys.mjs",
|
|
UrlbarSearchUtils: "resource:///modules/UrlbarSearchUtils.sys.mjs",
|
|
});
|
|
|
|
// These prefs must be true for the provider to return results. They are assumed
|
|
// to be booleans. We check `system.topsites` because if it is disabled we would
|
|
// get stale or empty top sites data.
|
|
const TOP_SITES_ENABLED_PREFS = [
|
|
"browser.urlbar.suggest.topsites",
|
|
"browser.newtabpage.activity-stream.feeds.system.topsites",
|
|
];
|
|
|
|
// Helper function to compare 2 URLs without refs.
|
|
function sameUrlIgnoringRef(url1, url2) {
|
|
if (!url1 || !url2) {
|
|
return false;
|
|
}
|
|
|
|
let cleanUrl1 = url1.replace(/#.*$/, "");
|
|
let cleanUrl2 = url2.replace(/#.*$/, "");
|
|
|
|
return cleanUrl1 == cleanUrl2;
|
|
}
|
|
|
|
/**
|
|
* A provider that returns the Top Sites shown on about:newtab.
|
|
*/
|
|
class ProviderTopSites extends UrlbarProvider {
|
|
constructor() {
|
|
super();
|
|
|
|
this._topSitesListeners = [];
|
|
let callListeners = () => this._callTopSitesListeners();
|
|
if (Services.prefs.getBoolPref("browser.topsites.component.enabled")) {
|
|
Services.obs.addObserver(callListeners, "topsites-refreshed");
|
|
} else {
|
|
Services.obs.addObserver(callListeners, "newtab-top-sites-changed");
|
|
}
|
|
for (let pref of TOP_SITES_ENABLED_PREFS) {
|
|
Services.prefs.addObserver(pref, callListeners);
|
|
}
|
|
}
|
|
|
|
get PRIORITY() {
|
|
// Top sites are prioritized over the UrlbarProviderPlaces provider.
|
|
return 1;
|
|
}
|
|
|
|
/**
|
|
* Unique name for the provider, used by the context to filter on providers.
|
|
* Not using a unique name will cause the newest registration to win.
|
|
*
|
|
* @returns {string}
|
|
*/
|
|
get name() {
|
|
return "UrlbarProviderTopSites";
|
|
}
|
|
|
|
/**
|
|
* @returns {Values<typeof UrlbarUtils.PROVIDER_TYPE>}
|
|
*/
|
|
get type() {
|
|
return UrlbarUtils.PROVIDER_TYPE.PROFILE;
|
|
}
|
|
|
|
/**
|
|
* Whether this provider should be invoked for the given context.
|
|
* If this method returns false, the providers manager won't start a query
|
|
* with this provider, to save on resources.
|
|
*
|
|
* @param {UrlbarQueryContext} queryContext The query context object
|
|
*/
|
|
async isActive(queryContext) {
|
|
return (
|
|
!queryContext.restrictSource &&
|
|
!queryContext.searchString &&
|
|
!queryContext.searchMode
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Gets the provider's priority.
|
|
*
|
|
* @returns {number} The provider's priority for the given query.
|
|
*/
|
|
getPriority() {
|
|
return this.PRIORITY;
|
|
}
|
|
|
|
/**
|
|
* Starts querying. Extended classes should return a Promise resolved when the
|
|
* provider is done searching AND returning results.
|
|
*
|
|
* @param {UrlbarQueryContext} queryContext The query context object
|
|
* @param {Function} addCallback Callback invoked by the provider to add a new
|
|
* result. A UrlbarResult should be passed to it.
|
|
* @returns {Promise}
|
|
*/
|
|
async startQuery(queryContext, addCallback) {
|
|
// Bail if Top Sites are not enabled. We check this condition here instead
|
|
// of in isActive because we still want this provider to be restricting even
|
|
// if this is not true. If it wasn't restricting, we would show the results
|
|
// from UrlbarProviderPlaces's empty search behaviour. We aren't interested
|
|
// in those since they are very similar to Top Sites and thus might be
|
|
// confusing, especially since users can configure Top Sites but cannot
|
|
// configure the default empty search results. See bug 1623666.
|
|
let enabled = TOP_SITES_ENABLED_PREFS.every(p =>
|
|
Services.prefs.getBoolPref(p, false)
|
|
);
|
|
if (!enabled) {
|
|
return;
|
|
}
|
|
|
|
let sites;
|
|
if (Services.prefs.getBoolPref("browser.topsites.component.enabled")) {
|
|
sites = await lazy.TopSites.getSites();
|
|
} else {
|
|
sites = lazy.AboutNewTab.getTopSites();
|
|
}
|
|
|
|
let instance = this.queryInstance;
|
|
|
|
// Filter out empty values. Site is empty when there's a gap between tiles
|
|
// on about:newtab.
|
|
sites = sites.filter(site => site);
|
|
|
|
if (!lazy.UrlbarPrefs.get("sponsoredTopSites")) {
|
|
sites = sites.filter(site => !site.sponsored_position);
|
|
}
|
|
|
|
// This is done here, rather than in the global scope, because
|
|
// TOP_SITES_DEFAULT_ROWS causes import of topsites constants.mjs, and we want to
|
|
// do that only when actually querying for Top Sites.
|
|
if (this.topSitesRows === undefined) {
|
|
XPCOMUtils.defineLazyPreferenceGetter(
|
|
this,
|
|
"topSitesRows",
|
|
"browser.newtabpage.activity-stream.topSitesRows",
|
|
lazy.TOP_SITES_DEFAULT_ROWS
|
|
);
|
|
}
|
|
|
|
// We usually respect maxRichResults, though we never show a number of Top
|
|
// Sites greater than what is visible in the New Tab Page, because the
|
|
// additional ones couldn't be managed from the page.
|
|
let numTopSites = Math.min(
|
|
lazy.UrlbarPrefs.get("maxRichResults"),
|
|
lazy.TOP_SITES_MAX_SITES_PER_ROW * this.topSitesRows
|
|
);
|
|
sites = sites.slice(0, numTopSites);
|
|
|
|
sites = sites.map(link => {
|
|
let site = {
|
|
type: link.searchTopSite ? "search" : "url",
|
|
url: link.url_urlbar || link.url,
|
|
isPinned: !!link.isPinned,
|
|
isSponsored: !!link.sponsored_position,
|
|
// The newtab page allows the user to set custom site titles, which
|
|
// are stored in `label`, so prefer it. Search top sites currently
|
|
// don't have titles but `hostname` instead.
|
|
title: link.label || link.title || link.hostname || "",
|
|
favicon: link.smallFavicon || link.favicon || undefined,
|
|
sendAttributionRequest: !!link.sendAttributionRequest,
|
|
};
|
|
if (site.isSponsored) {
|
|
let {
|
|
sponsored_tile_id,
|
|
sponsored_impression_url,
|
|
sponsored_click_url,
|
|
} = link;
|
|
site = {
|
|
...site,
|
|
sponsoredTileId: sponsored_tile_id,
|
|
sponsoredImpressionUrl: sponsored_impression_url,
|
|
sponsoredClickUrl: sponsored_click_url,
|
|
};
|
|
}
|
|
return site;
|
|
});
|
|
|
|
let tabUrlsToContextIds;
|
|
if (lazy.UrlbarPrefs.get("suggest.openpage")) {
|
|
if (lazy.UrlbarPrefs.get("switchTabs.searchAllContainers")) {
|
|
tabUrlsToContextIds = lazy.UrlbarProviderOpenTabs.getOpenTabUrls(
|
|
queryContext.isPrivate
|
|
);
|
|
} else {
|
|
// Build an object compatible with the output of getOpenTabs.
|
|
tabUrlsToContextIds = new Map();
|
|
for (let url of lazy.UrlbarProviderOpenTabs.getOpenTabUrlsForUserContextId(
|
|
queryContext.userContextId,
|
|
queryContext.isPrivate
|
|
)) {
|
|
tabUrlsToContextIds.set(url, new Set([queryContext.userContextId]));
|
|
}
|
|
}
|
|
}
|
|
|
|
for (let site of sites) {
|
|
switch (site.type) {
|
|
case "url": {
|
|
let payload = {
|
|
title: site.title,
|
|
url: site.url,
|
|
icon: site.favicon,
|
|
isPinned: site.isPinned,
|
|
isSponsored: site.isSponsored,
|
|
};
|
|
|
|
// Fuzzy match both the URL as-is, and the URL without ref, then
|
|
// generate a merged Set.
|
|
if (tabUrlsToContextIds) {
|
|
let tabUserContextIds = new Set([
|
|
...(tabUrlsToContextIds.get(site.url) ?? []),
|
|
...(tabUrlsToContextIds.get(site.url.replace(/#.*$/, "")) ?? []),
|
|
]);
|
|
if (tabUserContextIds.size) {
|
|
let switchToTabResultAdded = false;
|
|
for (let userContextId of tabUserContextIds) {
|
|
// Normally we could skip the whole for loop, but if searchAllContainers
|
|
// is set then the current page userContextId may differ, then we should
|
|
// allow switching to other ones.
|
|
if (
|
|
sameUrlIgnoringRef(queryContext.currentPage, site.url) &&
|
|
(!lazy.UrlbarPrefs.get("switchTabs.searchAllContainers") ||
|
|
queryContext.userContextId == userContextId)
|
|
) {
|
|
// Don't suggest switching to the current tab.
|
|
continue;
|
|
}
|
|
payload.userContextId = userContextId;
|
|
let result = new lazy.UrlbarResult(
|
|
UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
|
|
UrlbarUtils.RESULT_SOURCE.TABS,
|
|
...lazy.UrlbarResult.payloadAndSimpleHighlights(
|
|
queryContext.tokens,
|
|
payload
|
|
)
|
|
);
|
|
addCallback(this, result);
|
|
switchToTabResultAdded = true;
|
|
}
|
|
// Avoid adding url result if Switch to Tab result was added.
|
|
if (switchToTabResultAdded) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (site.isSponsored) {
|
|
payload.sponsoredTileId = site.sponsoredTileId;
|
|
payload.sponsoredClickUrl = site.sponsoredClickUrl;
|
|
}
|
|
payload.sendAttributionRequest = site.sendAttributionRequest;
|
|
|
|
let resultSource = UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL;
|
|
if (lazy.UrlbarPrefs.get("suggest.bookmark")) {
|
|
let bookmark = await lazy.PlacesUtils.bookmarks.fetch({
|
|
url: new URL(payload.url),
|
|
});
|
|
// Check if query has been cancelled.
|
|
if (instance != this.queryInstance) {
|
|
break;
|
|
}
|
|
if (bookmark) {
|
|
resultSource = UrlbarUtils.RESULT_SOURCE.BOOKMARKS;
|
|
}
|
|
}
|
|
|
|
let result = new lazy.UrlbarResult(
|
|
UrlbarUtils.RESULT_TYPE.URL,
|
|
resultSource,
|
|
...lazy.UrlbarResult.payloadAndSimpleHighlights(
|
|
queryContext.tokens,
|
|
payload
|
|
)
|
|
);
|
|
addCallback(this, result);
|
|
break;
|
|
}
|
|
case "search": {
|
|
let engine = await lazy.UrlbarSearchUtils.engineForAlias(site.title);
|
|
|
|
if (!engine && site.url) {
|
|
// Look up the engine by its domain.
|
|
let host = URL.parse(site.url)?.hostname;
|
|
if (host) {
|
|
engine = (
|
|
await lazy.UrlbarSearchUtils.enginesForDomainPrefix(host)
|
|
)[0];
|
|
}
|
|
}
|
|
|
|
if (!engine) {
|
|
// No engine found. We skip this Top Site.
|
|
break;
|
|
}
|
|
|
|
if (instance != this.queryInstance) {
|
|
break;
|
|
}
|
|
|
|
let result = new lazy.UrlbarResult(
|
|
UrlbarUtils.RESULT_TYPE.SEARCH,
|
|
UrlbarUtils.RESULT_SOURCE.SEARCH,
|
|
...lazy.UrlbarResult.payloadAndSimpleHighlights(
|
|
queryContext.tokens,
|
|
{
|
|
title: site.title,
|
|
keyword: site.title,
|
|
providesSearchMode: true,
|
|
engine: engine.name,
|
|
query: "",
|
|
icon: site.favicon,
|
|
isPinned: site.isPinned,
|
|
}
|
|
)
|
|
);
|
|
addCallback(this, result);
|
|
break;
|
|
}
|
|
default:
|
|
this.logger.error(`Unknown Top Site type: ${site.type}`);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
onImpression(state, queryContext, controller, providerVisibleResults) {
|
|
if (queryContext.isPrivate) {
|
|
return;
|
|
}
|
|
|
|
providerVisibleResults.forEach(({ index, result }) => {
|
|
if (result?.payload.isSponsored) {
|
|
Glean.contextualServicesTopsites.impression[`urlbar_${index}`].add(1);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Adds a listener function that will be called when the top sites change or
|
|
* they are enabled/disabled. This class will hold a weak reference to the
|
|
* listener, so you do not need to unregister it, but you or someone else must
|
|
* keep a strong reference to it to keep it from being immediately garbage
|
|
* collected.
|
|
*
|
|
* @param {Function} callback
|
|
* The listener function. This class will hold a weak reference to it.
|
|
*/
|
|
addTopSitesListener(callback) {
|
|
this._topSitesListeners.push(Cu.getWeakReference(callback));
|
|
}
|
|
|
|
_callTopSitesListeners() {
|
|
for (let i = 0; i < this._topSitesListeners.length; ) {
|
|
let listener = this._topSitesListeners[i].get();
|
|
if (!listener) {
|
|
// The listener has been GC'ed, so remove it from our list.
|
|
this._topSitesListeners.splice(i, 1);
|
|
} else {
|
|
listener();
|
|
++i;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export var UrlbarProviderTopSites = new ProviderTopSites();
|