зеркало из 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,
|
||||
// pocket_id is only for pocket stories being in highlights, and then dismissed.
|
||||
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 } : {}),
|
||||
})),
|
||||
}),
|
||||
|
|
|
@ -4521,6 +4521,8 @@ const LinkMenuOptions = {
|
|||
url: site.original_url || site.open_url || site.url,
|
||||
// pocket_id is only for pocket stories being in highlights, and then dismissed.
|
||||
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
|
||||
} : {})
|
||||
|
|
|
@ -8,6 +8,9 @@ const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
|||
const { actionCreators: ac, actionTypes: at } = ChromeUtils.import(
|
||||
"resource://activity-stream/common/Actions.jsm"
|
||||
);
|
||||
const { shortURL } = ChromeUtils.import(
|
||||
"resource://activity-stream/lib/ShortURL.jsm"
|
||||
);
|
||||
|
||||
ChromeUtils.defineModuleGetter(
|
||||
this,
|
||||
|
@ -28,6 +31,11 @@ ChromeUtils.defineModuleGetter(
|
|||
const LINK_BLOCKED_EVENT = "newtab-linkBlocked";
|
||||
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.
|
||||
*/
|
||||
|
@ -438,6 +446,24 @@ class PlacesFeed {
|
|||
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) {
|
||||
switch (action.type) {
|
||||
case at.INIT:
|
||||
|
@ -457,10 +483,17 @@ class PlacesFeed {
|
|||
}
|
||||
case at.BLOCK_URL: {
|
||||
if (action.data) {
|
||||
let sponsoredTopSites = [];
|
||||
action.data.forEach(site => {
|
||||
const { url, pocket_id } = site;
|
||||
const { url, pocket_id, isSponsoredTopSite } = site;
|
||||
NewTabUtils.activityStreamLinks.blockURL({ url, pocket_id });
|
||||
if (isSponsoredTopSite) {
|
||||
sponsoredTopSites.push({ url });
|
||||
}
|
||||
});
|
||||
if (sponsoredTopSites.length) {
|
||||
this.addToBlockedTopSitesSponsors(sponsoredTopSites);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -110,6 +110,7 @@ const DEFAULT_SITES_EXPERIMENTS_PREF_BRANCH = "browser.topsites.experiment.";
|
|||
const CONTILE_ENABLED_PREF = "browser.topsites.contile.enabled";
|
||||
const CONTILE_ENDPOINT_PREF = "browser.topsites.contile.endpoint";
|
||||
const CONTILE_UPDATE_INTERVAL = 15 * 60 * 1000; // 15 minutes
|
||||
const TOP_SITES_BLOCKED_SPONSORS_PREF = "browser.topsites.blockedSponsors";
|
||||
|
||||
function getShortURLForCurrentSearch() {
|
||||
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() {
|
||||
if (
|
||||
!Services.prefs.getBoolPref(CONTILE_ENABLED_PREF) ||
|
||||
|
@ -172,6 +186,7 @@ class ContileIntegration {
|
|||
const body = await response.json();
|
||||
if (body?.tiles && Array.isArray(body.tiles)) {
|
||||
let { tiles } = body;
|
||||
tiles = this._filterBlockedSponsors(tiles);
|
||||
if (tiles.length > MAX_NUM_SPONSORED) {
|
||||
Cu.reportError(
|
||||
`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"));
|
||||
});
|
||||
|
||||
it("should dispath telemety events on dismiss", () => {
|
||||
it("should dispatch telemety events on dismiss", () => {
|
||||
wrapper.instance().onDismissClick();
|
||||
|
||||
const firstCall = dispatchStub.getCall(0);
|
||||
|
@ -123,9 +123,9 @@ describe("<CollectionCardGrid>", () => {
|
|||
|
||||
assert.equal(firstCall.args[0].type, "BLOCK_URL");
|
||||
assert.deepEqual(firstCall.args[0].data, [
|
||||
{ url: "123", pocket_id: undefined },
|
||||
{ url: "456", pocket_id: undefined },
|
||||
{ url: "789", pocket_id: undefined },
|
||||
{ url: "123", pocket_id: undefined, isSponsoredTopSite: undefined },
|
||||
{ url: "456", pocket_id: undefined, isSponsoredTopSite: undefined },
|
||||
{ url: "789", pocket_id: undefined, isSponsoredTopSite: undefined },
|
||||
]);
|
||||
|
||||
assert.equal(secondCall.args[0].type, "TELEMETRY_USER_EVENT");
|
||||
|
|
|
@ -73,6 +73,7 @@ describe("<DSTextPromo>", () => {
|
|||
{
|
||||
url: undefined,
|
||||
pocket_id: undefined,
|
||||
isSponsoredTopSite: undefined,
|
||||
},
|
||||
]);
|
||||
|
||||
|
|
|
@ -357,6 +357,7 @@ describe("<LinkMenu>", () => {
|
|||
{
|
||||
url: FAKE_SITE.url,
|
||||
pocket_id: FAKE_SITE.pocket_id,
|
||||
isSponsoredTopSite: undefined,
|
||||
},
|
||||
],
|
||||
menu_action_webext_dismiss: {
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { actionCreators as ac, actionTypes as at } from "common/Actions.jsm";
|
||||
import { GlobalOverrider } from "test/unit/utils";
|
||||
import { PlacesFeed } from "lib/PlacesFeed.jsm";
|
||||
const { BookmarksObserver, PlacesObserver } = PlacesFeed;
|
||||
import injector from "inject!lib/PlacesFeed.jsm";
|
||||
|
||||
const FAKE_BOOKMARK = {
|
||||
bookmarkGuid: "xi31",
|
||||
|
@ -20,10 +19,16 @@ const SOURCES = {
|
|||
|
||||
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", () => {
|
||||
let PlacesFeed;
|
||||
let BookmarksObserver;
|
||||
let PlacesObserver;
|
||||
let globals;
|
||||
let sandbox;
|
||||
let feed;
|
||||
let shortURLStub;
|
||||
beforeEach(() => {
|
||||
globals = new GlobalOverrider();
|
||||
sandbox = globals.sandbox;
|
||||
|
@ -55,6 +60,11 @@ describe("PlacesFeed", () => {
|
|||
sandbox.spy(global.Services.obs, "addObserver");
|
||||
sandbox.spy(global.Services.obs, "removeObserver");
|
||||
sandbox.spy(global.Cu, "reportError");
|
||||
shortURLStub = sandbox
|
||||
.stub()
|
||||
.callsFake(site =>
|
||||
site.url.replace(/(.com|.ca)/, "").replace("https://", "")
|
||||
);
|
||||
|
||||
global.Services.io.newURI = spec => ({
|
||||
mutate: () => ({
|
||||
|
@ -77,6 +87,11 @@ describe("PlacesFeed", () => {
|
|||
};
|
||||
},
|
||||
};
|
||||
({ PlacesFeed } = injector({
|
||||
"lib/ShortURL.jsm": { shortURL: shortURLStub },
|
||||
}));
|
||||
BookmarksObserver = PlacesFeed.BookmarksObserver;
|
||||
PlacesObserver = PlacesFeed.PlacesObserver;
|
||||
feed = new PlacesFeed();
|
||||
feed.store = { dispatch: sinon.spy() };
|
||||
});
|
||||
|
@ -101,6 +116,50 @@ describe("PlacesFeed", () => {
|
|||
assert.calledOnce(feed.store.dispatch);
|
||||
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", () => {
|
||||
it("should add bookmark, history, places, blocked observers on INIT", () => {
|
||||
feed.onAction({ type: at.INIT });
|
||||
|
@ -161,6 +220,16 @@ describe("PlacesFeed", () => {
|
|||
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", () => {
|
||||
const data = { url: "pear.com", title: "A pear" };
|
||||
const _target = { browser: { ownerGlobal() {} } };
|
||||
|
|
|
@ -29,6 +29,7 @@ const SEARCH_SHORTCUTS_HAVE_PINNED_PREF =
|
|||
const SHOWN_ON_NEWTAB_PREF = "feeds.topsites";
|
||||
const SHOW_SPONSORED_PREF = "showSponsoredTopSites";
|
||||
const CONTILE_ENABLED_PREF = "browser.topsites.contile.enabled";
|
||||
const TOP_SITES_BLOCKED_SPONSORS_PREF = "browser.topsites.blockedSponsors";
|
||||
|
||||
function FakeTippyTopProvider() {}
|
||||
FakeTippyTopProvider.prototype = {
|
||||
|
@ -2026,6 +2027,10 @@ describe("Top Sites Feed", () => {
|
|||
.stub(global.Services.prefs, "getBoolPref")
|
||||
.withArgs(CONTILE_ENABLED_PREF)
|
||||
.returns(true);
|
||||
sandbox
|
||||
.stub(global.Services.prefs, "getStringPref")
|
||||
.withArgs(TOP_SITES_BLOCKED_SPONSORS_PREF)
|
||||
.returns(`["foo","bar"]`);
|
||||
});
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
|
@ -2062,6 +2067,46 @@ describe("Top Sites Feed", () => {
|
|||
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 () => {
|
||||
fetchStub.resolves({
|
||||
ok: false,
|
||||
|
|
Загрузка…
Ссылка в новой задаче