зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1713596 - Block all sponsored tiles from a sponsor upon the dismissal r=dao
This patch implements the sponsor level blocking for the sponsored Top Sites. When a sponsored top site gets dismissed, Firefox will extract the sponsor (hostname) out of the URL, and persist it to the pref `browser.topsites.blockedSponsors` as a JSON array. When Firefox fetches the sponsored tiles from Contile again, it will filter all the tiles whose sponsor has been blocked before. Differential Revision: https://phabricator.services.mozilla.com/D116413
This commit is contained in:
Родитель
597ec6c1d5
Коммит
f1bf8c386b
|
@ -86,6 +86,8 @@ export const LinkMenuOptions = {
|
||||||
url: site.original_url || site.open_url || site.url,
|
url: site.original_url || site.open_url || site.url,
|
||||||
// pocket_id is only for pocket stories being in highlights, and then dismissed.
|
// pocket_id is only for pocket stories being in highlights, and then dismissed.
|
||||||
pocket_id: site.pocket_id,
|
pocket_id: site.pocket_id,
|
||||||
|
// used by PlacesFeed and TopSitesFeed for sponsored top sites blocking.
|
||||||
|
isSponsoredTopSite: site.sponsored_position,
|
||||||
...(site.flight_id ? { flight_id: site.flight_id } : {}),
|
...(site.flight_id ? { flight_id: site.flight_id } : {}),
|
||||||
})),
|
})),
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -4521,6 +4521,8 @@ const LinkMenuOptions = {
|
||||||
url: site.original_url || site.open_url || site.url,
|
url: site.original_url || site.open_url || site.url,
|
||||||
// pocket_id is only for pocket stories being in highlights, and then dismissed.
|
// pocket_id is only for pocket stories being in highlights, and then dismissed.
|
||||||
pocket_id: site.pocket_id,
|
pocket_id: site.pocket_id,
|
||||||
|
// used by PlacesFeed and TopSitesFeed for sponsored top sites blocking.
|
||||||
|
isSponsoredTopSite: site.sponsored_position,
|
||||||
...(site.flight_id ? {
|
...(site.flight_id ? {
|
||||||
flight_id: site.flight_id
|
flight_id: site.flight_id
|
||||||
} : {})
|
} : {})
|
||||||
|
|
|
@ -8,6 +8,9 @@ const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||||
const { actionCreators: ac, actionTypes: at } = ChromeUtils.import(
|
const { actionCreators: ac, actionTypes: at } = ChromeUtils.import(
|
||||||
"resource://activity-stream/common/Actions.jsm"
|
"resource://activity-stream/common/Actions.jsm"
|
||||||
);
|
);
|
||||||
|
const { shortURL } = ChromeUtils.import(
|
||||||
|
"resource://activity-stream/lib/ShortURL.jsm"
|
||||||
|
);
|
||||||
|
|
||||||
ChromeUtils.defineModuleGetter(
|
ChromeUtils.defineModuleGetter(
|
||||||
this,
|
this,
|
||||||
|
@ -28,6 +31,11 @@ ChromeUtils.defineModuleGetter(
|
||||||
const LINK_BLOCKED_EVENT = "newtab-linkBlocked";
|
const LINK_BLOCKED_EVENT = "newtab-linkBlocked";
|
||||||
const PLACES_LINKS_CHANGED_DELAY_TIME = 1000; // time in ms to delay timer for places links changed events
|
const PLACES_LINKS_CHANGED_DELAY_TIME = 1000; // time in ms to delay timer for places links changed events
|
||||||
|
|
||||||
|
// The pref to store the blocked sponsors of the sponsored Top Sites.
|
||||||
|
// The value of this pref is an array (JSON serialized) of hostnames of the
|
||||||
|
// blocked sponsors.
|
||||||
|
const TOP_SITES_BLOCKED_SPONSORS_PREF = "browser.topsites.blockedSponsors";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Observer - a wrapper around history/bookmark observers to add the QueryInterface.
|
* Observer - a wrapper around history/bookmark observers to add the QueryInterface.
|
||||||
*/
|
*/
|
||||||
|
@ -438,6 +446,24 @@ class PlacesFeed {
|
||||||
urlBar.addEventListener("paste", checkFirstChange);
|
urlBar.addEventListener("paste", checkFirstChange);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the hostnames of the given urls to the Top Sites sponsor blocklist.
|
||||||
|
*
|
||||||
|
* @param {array} urls
|
||||||
|
* An array of the objects structured as `{ url }`
|
||||||
|
*/
|
||||||
|
addToBlockedTopSitesSponsors(urls) {
|
||||||
|
const blockedPref = JSON.parse(
|
||||||
|
Services.prefs.getStringPref(TOP_SITES_BLOCKED_SPONSORS_PREF, "[]")
|
||||||
|
);
|
||||||
|
const merged = new Set([...blockedPref, ...urls.map(url => shortURL(url))]);
|
||||||
|
|
||||||
|
Services.prefs.setStringPref(
|
||||||
|
TOP_SITES_BLOCKED_SPONSORS_PREF,
|
||||||
|
JSON.stringify([...merged])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
onAction(action) {
|
onAction(action) {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case at.INIT:
|
case at.INIT:
|
||||||
|
@ -457,10 +483,17 @@ class PlacesFeed {
|
||||||
}
|
}
|
||||||
case at.BLOCK_URL: {
|
case at.BLOCK_URL: {
|
||||||
if (action.data) {
|
if (action.data) {
|
||||||
|
let sponsoredTopSites = [];
|
||||||
action.data.forEach(site => {
|
action.data.forEach(site => {
|
||||||
const { url, pocket_id } = site;
|
const { url, pocket_id, isSponsoredTopSite } = site;
|
||||||
NewTabUtils.activityStreamLinks.blockURL({ url, pocket_id });
|
NewTabUtils.activityStreamLinks.blockURL({ url, pocket_id });
|
||||||
|
if (isSponsoredTopSite) {
|
||||||
|
sponsoredTopSites.push({ url });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
if (sponsoredTopSites.length) {
|
||||||
|
this.addToBlockedTopSitesSponsors(sponsoredTopSites);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
@ -110,6 +110,7 @@ const DEFAULT_SITES_EXPERIMENTS_PREF_BRANCH = "browser.topsites.experiment.";
|
||||||
const CONTILE_ENABLED_PREF = "browser.topsites.contile.enabled";
|
const CONTILE_ENABLED_PREF = "browser.topsites.contile.enabled";
|
||||||
const CONTILE_ENDPOINT_PREF = "browser.topsites.contile.endpoint";
|
const CONTILE_ENDPOINT_PREF = "browser.topsites.contile.endpoint";
|
||||||
const CONTILE_UPDATE_INTERVAL = 15 * 60 * 1000; // 15 minutes
|
const CONTILE_UPDATE_INTERVAL = 15 * 60 * 1000; // 15 minutes
|
||||||
|
const TOP_SITES_BLOCKED_SPONSORS_PREF = "browser.topsites.blockedSponsors";
|
||||||
|
|
||||||
function getShortURLForCurrentSearch() {
|
function getShortURLForCurrentSearch() {
|
||||||
const url = shortURL({ url: Services.search.defaultEngine.searchForm });
|
const url = shortURL({ url: Services.search.defaultEngine.searchForm });
|
||||||
|
@ -142,6 +143,19 @@ class ContileIntegration {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter the tiles whose sponsor is on the Top Sites sponsor blocklist.
|
||||||
|
*
|
||||||
|
* @param {array} tiles
|
||||||
|
* An array of the tile objects
|
||||||
|
*/
|
||||||
|
_filterBlockedSponsors(tiles) {
|
||||||
|
const blocklist = JSON.parse(
|
||||||
|
Services.prefs.getStringPref(TOP_SITES_BLOCKED_SPONSORS_PREF, "[]")
|
||||||
|
);
|
||||||
|
return tiles.filter(tile => !blocklist.includes(shortURL(tile)));
|
||||||
|
}
|
||||||
|
|
||||||
async _fetchSites() {
|
async _fetchSites() {
|
||||||
if (
|
if (
|
||||||
!Services.prefs.getBoolPref(CONTILE_ENABLED_PREF) ||
|
!Services.prefs.getBoolPref(CONTILE_ENABLED_PREF) ||
|
||||||
|
@ -172,6 +186,7 @@ class ContileIntegration {
|
||||||
const body = await response.json();
|
const body = await response.json();
|
||||||
if (body?.tiles && Array.isArray(body.tiles)) {
|
if (body?.tiles && Array.isArray(body.tiles)) {
|
||||||
let { tiles } = body;
|
let { tiles } = body;
|
||||||
|
tiles = this._filterBlockedSponsors(tiles);
|
||||||
if (tiles.length > MAX_NUM_SPONSORED) {
|
if (tiles.length > MAX_NUM_SPONSORED) {
|
||||||
Cu.reportError(
|
Cu.reportError(
|
||||||
`Contile provided more links than permitted. (${tiles.length} received, limit is ${MAX_NUM_SPONSORED})`
|
`Contile provided more links than permitted. (${tiles.length} received, limit is ${MAX_NUM_SPONSORED})`
|
||||||
|
|
|
@ -114,7 +114,7 @@ describe("<CollectionCardGrid>", () => {
|
||||||
assert.ok(!wrapper.exists(".ds-collection-card-grid"));
|
assert.ok(!wrapper.exists(".ds-collection-card-grid"));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should dispath telemety events on dismiss", () => {
|
it("should dispatch telemety events on dismiss", () => {
|
||||||
wrapper.instance().onDismissClick();
|
wrapper.instance().onDismissClick();
|
||||||
|
|
||||||
const firstCall = dispatchStub.getCall(0);
|
const firstCall = dispatchStub.getCall(0);
|
||||||
|
@ -123,9 +123,9 @@ describe("<CollectionCardGrid>", () => {
|
||||||
|
|
||||||
assert.equal(firstCall.args[0].type, "BLOCK_URL");
|
assert.equal(firstCall.args[0].type, "BLOCK_URL");
|
||||||
assert.deepEqual(firstCall.args[0].data, [
|
assert.deepEqual(firstCall.args[0].data, [
|
||||||
{ url: "123", pocket_id: undefined },
|
{ url: "123", pocket_id: undefined, isSponsoredTopSite: undefined },
|
||||||
{ url: "456", pocket_id: undefined },
|
{ url: "456", pocket_id: undefined, isSponsoredTopSite: undefined },
|
||||||
{ url: "789", pocket_id: undefined },
|
{ url: "789", pocket_id: undefined, isSponsoredTopSite: undefined },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
assert.equal(secondCall.args[0].type, "TELEMETRY_USER_EVENT");
|
assert.equal(secondCall.args[0].type, "TELEMETRY_USER_EVENT");
|
||||||
|
|
|
@ -73,6 +73,7 @@ describe("<DSTextPromo>", () => {
|
||||||
{
|
{
|
||||||
url: undefined,
|
url: undefined,
|
||||||
pocket_id: undefined,
|
pocket_id: undefined,
|
||||||
|
isSponsoredTopSite: undefined,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
|
@ -357,6 +357,7 @@ describe("<LinkMenu>", () => {
|
||||||
{
|
{
|
||||||
url: FAKE_SITE.url,
|
url: FAKE_SITE.url,
|
||||||
pocket_id: FAKE_SITE.pocket_id,
|
pocket_id: FAKE_SITE.pocket_id,
|
||||||
|
isSponsoredTopSite: undefined,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
menu_action_webext_dismiss: {
|
menu_action_webext_dismiss: {
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { actionCreators as ac, actionTypes as at } from "common/Actions.jsm";
|
import { actionCreators as ac, actionTypes as at } from "common/Actions.jsm";
|
||||||
import { GlobalOverrider } from "test/unit/utils";
|
import { GlobalOverrider } from "test/unit/utils";
|
||||||
import { PlacesFeed } from "lib/PlacesFeed.jsm";
|
import injector from "inject!lib/PlacesFeed.jsm";
|
||||||
const { BookmarksObserver, PlacesObserver } = PlacesFeed;
|
|
||||||
|
|
||||||
const FAKE_BOOKMARK = {
|
const FAKE_BOOKMARK = {
|
||||||
bookmarkGuid: "xi31",
|
bookmarkGuid: "xi31",
|
||||||
|
@ -20,10 +19,16 @@ const SOURCES = {
|
||||||
|
|
||||||
const BLOCKED_EVENT = "newtab-linkBlocked"; // The event dispatched in NewTabUtils when a link is blocked;
|
const BLOCKED_EVENT = "newtab-linkBlocked"; // The event dispatched in NewTabUtils when a link is blocked;
|
||||||
|
|
||||||
|
const TOP_SITES_BLOCKED_SPONSORS_PREF = "browser.topsites.blockedSponsors";
|
||||||
|
|
||||||
describe("PlacesFeed", () => {
|
describe("PlacesFeed", () => {
|
||||||
|
let PlacesFeed;
|
||||||
|
let BookmarksObserver;
|
||||||
|
let PlacesObserver;
|
||||||
let globals;
|
let globals;
|
||||||
let sandbox;
|
let sandbox;
|
||||||
let feed;
|
let feed;
|
||||||
|
let shortURLStub;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
globals = new GlobalOverrider();
|
globals = new GlobalOverrider();
|
||||||
sandbox = globals.sandbox;
|
sandbox = globals.sandbox;
|
||||||
|
@ -55,6 +60,11 @@ describe("PlacesFeed", () => {
|
||||||
sandbox.spy(global.Services.obs, "addObserver");
|
sandbox.spy(global.Services.obs, "addObserver");
|
||||||
sandbox.spy(global.Services.obs, "removeObserver");
|
sandbox.spy(global.Services.obs, "removeObserver");
|
||||||
sandbox.spy(global.Cu, "reportError");
|
sandbox.spy(global.Cu, "reportError");
|
||||||
|
shortURLStub = sandbox
|
||||||
|
.stub()
|
||||||
|
.callsFake(site =>
|
||||||
|
site.url.replace(/(.com|.ca)/, "").replace("https://", "")
|
||||||
|
);
|
||||||
|
|
||||||
global.Services.io.newURI = spec => ({
|
global.Services.io.newURI = spec => ({
|
||||||
mutate: () => ({
|
mutate: () => ({
|
||||||
|
@ -77,6 +87,11 @@ describe("PlacesFeed", () => {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
({ PlacesFeed } = injector({
|
||||||
|
"lib/ShortURL.jsm": { shortURL: shortURLStub },
|
||||||
|
}));
|
||||||
|
BookmarksObserver = PlacesFeed.BookmarksObserver;
|
||||||
|
PlacesObserver = PlacesFeed.PlacesObserver;
|
||||||
feed = new PlacesFeed();
|
feed = new PlacesFeed();
|
||||||
feed.store = { dispatch: sinon.spy() };
|
feed.store = { dispatch: sinon.spy() };
|
||||||
});
|
});
|
||||||
|
@ -101,6 +116,50 @@ describe("PlacesFeed", () => {
|
||||||
assert.calledOnce(feed.store.dispatch);
|
assert.calledOnce(feed.store.dispatch);
|
||||||
assert.equal(feed.store.dispatch.firstCall.args[0].type, action.type);
|
assert.equal(feed.store.dispatch.firstCall.args[0].type, action.type);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("#addToBlockedTopSitesSponsors", () => {
|
||||||
|
let spy;
|
||||||
|
beforeEach(() => {
|
||||||
|
sandbox
|
||||||
|
.stub(global.Services.prefs, "getStringPref")
|
||||||
|
.withArgs(TOP_SITES_BLOCKED_SPONSORS_PREF)
|
||||||
|
.returns(`["foo","bar"]`);
|
||||||
|
spy = sandbox.spy(global.Services.prefs, "setStringPref");
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
sandbox.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should add the blocked sponsors to the blocklist", () => {
|
||||||
|
feed.addToBlockedTopSitesSponsors([
|
||||||
|
{ url: "test.com" },
|
||||||
|
{ url: "test1.com" },
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert.calledOnce(spy);
|
||||||
|
const [, sponsors] = spy.firstCall.args;
|
||||||
|
assert.deepEqual(
|
||||||
|
new Set(["foo", "bar", "test", "test1"]),
|
||||||
|
new Set(JSON.parse(sponsors))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not add duplicate sponsors to the blocklist", () => {
|
||||||
|
feed.addToBlockedTopSitesSponsors([
|
||||||
|
{ url: "foo.com" },
|
||||||
|
{ url: "bar.com" },
|
||||||
|
{ url: "test.com" },
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert.calledOnce(spy);
|
||||||
|
const [, sponsors] = spy.firstCall.args;
|
||||||
|
assert.deepEqual(
|
||||||
|
new Set(["foo", "bar", "test"]),
|
||||||
|
new Set(JSON.parse(sponsors))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("#onAction", () => {
|
describe("#onAction", () => {
|
||||||
it("should add bookmark, history, places, blocked observers on INIT", () => {
|
it("should add bookmark, history, places, blocked observers on INIT", () => {
|
||||||
feed.onAction({ type: at.INIT });
|
feed.onAction({ type: at.INIT });
|
||||||
|
@ -161,6 +220,16 @@ describe("PlacesFeed", () => {
|
||||||
pocket_id: 1234,
|
pocket_id: 1234,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
it("should update the blocked top sites sponsors", () => {
|
||||||
|
sandbox.stub(feed, "addToBlockedTopSitesSponsors");
|
||||||
|
feed.onAction({
|
||||||
|
type: at.BLOCK_URL,
|
||||||
|
data: [{ url: "foo.com", pocket_id: 1234, isSponsoredTopSite: 1 }],
|
||||||
|
});
|
||||||
|
assert.calledWith(feed.addToBlockedTopSitesSponsors, [
|
||||||
|
{ url: "foo.com" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
it("should bookmark a url on BOOKMARK_URL", () => {
|
it("should bookmark a url on BOOKMARK_URL", () => {
|
||||||
const data = { url: "pear.com", title: "A pear" };
|
const data = { url: "pear.com", title: "A pear" };
|
||||||
const _target = { browser: { ownerGlobal() {} } };
|
const _target = { browser: { ownerGlobal() {} } };
|
||||||
|
|
|
@ -29,6 +29,7 @@ const SEARCH_SHORTCUTS_HAVE_PINNED_PREF =
|
||||||
const SHOWN_ON_NEWTAB_PREF = "feeds.topsites";
|
const SHOWN_ON_NEWTAB_PREF = "feeds.topsites";
|
||||||
const SHOW_SPONSORED_PREF = "showSponsoredTopSites";
|
const SHOW_SPONSORED_PREF = "showSponsoredTopSites";
|
||||||
const CONTILE_ENABLED_PREF = "browser.topsites.contile.enabled";
|
const CONTILE_ENABLED_PREF = "browser.topsites.contile.enabled";
|
||||||
|
const TOP_SITES_BLOCKED_SPONSORS_PREF = "browser.topsites.blockedSponsors";
|
||||||
|
|
||||||
function FakeTippyTopProvider() {}
|
function FakeTippyTopProvider() {}
|
||||||
FakeTippyTopProvider.prototype = {
|
FakeTippyTopProvider.prototype = {
|
||||||
|
@ -2026,6 +2027,10 @@ describe("Top Sites Feed", () => {
|
||||||
.stub(global.Services.prefs, "getBoolPref")
|
.stub(global.Services.prefs, "getBoolPref")
|
||||||
.withArgs(CONTILE_ENABLED_PREF)
|
.withArgs(CONTILE_ENABLED_PREF)
|
||||||
.returns(true);
|
.returns(true);
|
||||||
|
sandbox
|
||||||
|
.stub(global.Services.prefs, "getStringPref")
|
||||||
|
.withArgs(TOP_SITES_BLOCKED_SPONSORS_PREF)
|
||||||
|
.returns(`["foo","bar"]`);
|
||||||
});
|
});
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
sandbox.restore();
|
sandbox.restore();
|
||||||
|
@ -2062,6 +2067,46 @@ describe("Top Sites Feed", () => {
|
||||||
assert.equal(feed._contile.sites.length, 2);
|
assert.equal(feed._contile.sites.length, 2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should filter the blocked sponsors", async () => {
|
||||||
|
fetchStub.resolves({
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
json: () =>
|
||||||
|
Promise.resolve({
|
||||||
|
tiles: [
|
||||||
|
{
|
||||||
|
url: "https://www.test.com",
|
||||||
|
image_url: "images/test-com.png",
|
||||||
|
click_url: "https://www.test-click.com",
|
||||||
|
impression_url: "https://www.test-impression.com",
|
||||||
|
name: "test",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: "https://foo.com",
|
||||||
|
image_url: "images/foo-com.png",
|
||||||
|
click_url: "https://www.foo-click.com",
|
||||||
|
impression_url: "https://www.foo-impression.com",
|
||||||
|
name: "foo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: "https://bar.com",
|
||||||
|
image_url: "images/bar-com.png",
|
||||||
|
click_url: "https://www.bar-click.com",
|
||||||
|
impression_url: "https://www.bar-impression.com",
|
||||||
|
name: "bar",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetched = await feed._contile._fetchSites();
|
||||||
|
|
||||||
|
assert.ok(fetched);
|
||||||
|
// Both "foo" and "bar" should be filtered
|
||||||
|
assert.equal(feed._contile.sites.length, 1);
|
||||||
|
assert.equal(feed._contile.sites[0].url, "https://www.test.com");
|
||||||
|
});
|
||||||
|
|
||||||
it("should handle errors properly from Contile", async () => {
|
it("should handle errors properly from Contile", async () => {
|
||||||
fetchStub.resolves({
|
fetchStub.resolves({
|
||||||
ok: false,
|
ok: false,
|
||||||
|
|
Загрузка…
Ссылка в новой задаче