Source code
Revision control
Copy as Markdown
Other Tools
/* vim: set ts=2 et sw=2 tw=80: */
/* 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
/* import-globals-from head.js */
"use strict";
var gFeatures = undefined;
var gTestTrackersCleanedUp = false;
var gTestTrackersCleanupRegistered = false;
/**
* Force garbage collection.
*/
function forceGC() {
SpecialPowers.gc();
SpecialPowers.forceShrinkingGC();
SpecialPowers.forceCC();
SpecialPowers.gc();
SpecialPowers.forceShrinkingGC();
SpecialPowers.forceCC();
}
this.AntiTracking = {
runTestInNormalAndPrivateMode(
name,
callbackTracking,
callbackNonTracking,
cleanupFunction,
extraPrefs,
windowOpenTest = true,
userInteractionTest = true,
expectedBlockingNotifications = Ci.nsIWebProgressListener
.STATE_COOKIES_BLOCKED_TRACKER,
iframeSandbox = null,
accessRemoval = null,
callbackAfterRemoval = null,
iframeAllow = null
) {
// Normal mode
this.runTest(
name,
callbackTracking,
callbackNonTracking,
cleanupFunction,
extraPrefs,
windowOpenTest,
userInteractionTest,
expectedBlockingNotifications,
false,
iframeSandbox,
accessRemoval,
callbackAfterRemoval,
iframeAllow
);
// Private mode
this.runTest(
name,
callbackTracking,
callbackNonTracking,
cleanupFunction,
extraPrefs,
windowOpenTest,
userInteractionTest,
expectedBlockingNotifications,
true,
iframeSandbox,
accessRemoval,
callbackAfterRemoval,
iframeAllow
);
},
runTest(
name,
callbackTracking,
callbackNonTracking,
cleanupFunction,
extraPrefs,
windowOpenTest = true,
userInteractionTest = true,
expectedBlockingNotifications = Ci.nsIWebProgressListener
.STATE_COOKIES_BLOCKED_TRACKER,
runInPrivateWindow = false,
iframeSandbox = null,
accessRemoval = null,
callbackAfterRemoval = null,
iframeAllow = null
) {
let runExtraTests = true;
let options = {};
if (typeof callbackNonTracking == "object" && !!callbackNonTracking) {
options.callback = callbackNonTracking.callback;
runExtraTests = callbackNonTracking.runExtraTests;
if ("cookieBehavior" in callbackNonTracking) {
options.cookieBehavior = callbackNonTracking.cookieBehavior;
} else {
options.cookieBehavior = BEHAVIOR_ACCEPT;
}
if ("expectedBlockingNotifications" in callbackNonTracking) {
options.expectedBlockingNotifications =
callbackNonTracking.expectedBlockingNotifications;
} else {
options.expectedBlockingNotifications = 0;
}
if ("blockingByAllowList" in callbackNonTracking) {
options.blockingByAllowList = callbackNonTracking.blockingByAllowList;
if (options.blockingByAllowList) {
// If we're on the allow list, there won't be any blocking!
options.expectedBlockingNotifications = 0;
}
} else {
options.blockingByAllowList = false;
}
callbackNonTracking = options.callback;
options.accessRemoval = null;
options.callbackAfterRemoval = null;
}
// Here we want to test that a 3rd party context is simply blocked.
this._createTask({
name,
cookieBehavior: BEHAVIOR_REJECT_TRACKER,
allowList: false,
callback: callbackTracking,
extraPrefs,
expectedBlockingNotifications,
runInPrivateWindow,
iframeSandbox,
accessRemoval,
callbackAfterRemoval,
iframeAllow,
});
this._createCleanupTask(cleanupFunction);
if (callbackNonTracking) {
// Phase 1: Here we want to test that a 3rd party context is not blocked if pref is off.
if (runExtraTests) {
// There are five ways in which the third-party context may not be blocked:
// * If the cookieBehavior pref causes it to not be blocked.
// * If the contentBlocking pref causes it to not be blocked.
// * If both of these prefs cause it to not be blocked.
// * If the top-level page is on the content blocking allow list.
// * If the contentBlocking third-party cookies UI pref is off, the allow list will be ignored.
// All of these cases are tested here.
this._createTask({
name,
cookieBehavior: BEHAVIOR_ACCEPT,
allowList: false,
callback: callbackNonTracking,
extraPrefs,
expectedBlockingNotifications: 0,
runInPrivateWindow,
iframeSandbox,
accessRemoval: null, // only passed with non-blocking callback
callbackAfterRemoval: null,
iframeAllow,
});
this._createCleanupTask(cleanupFunction);
this._createTask({
name,
cookieBehavior: BEHAVIOR_ACCEPT,
allowList: true,
callback: callbackNonTracking,
extraPrefs,
expectedBlockingNotifications: 0,
runInPrivateWindow,
iframeSandbox,
accessRemoval: null, // only passed with non-blocking callback
callbackAfterRemoval: null,
iframeAllow,
});
this._createCleanupTask(cleanupFunction);
this._createTask({
name,
cookieBehavior: BEHAVIOR_REJECT,
allowList: false,
callback: callbackTracking,
extraPrefs,
expectedBlockingNotifications: expectedBlockingNotifications
? Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_ALL
: 0,
runInPrivateWindow,
iframeSandbox,
accessRemoval: null, // only passed with non-blocking callback
callbackAfterRemoval: null,
iframeAllow,
});
this._createCleanupTask(cleanupFunction);
this._createTask({
name,
cookieBehavior: BEHAVIOR_LIMIT_FOREIGN,
allowList: true,
callback: callbackNonTracking,
extraPrefs,
expectedBlockingNotifications: 0,
runInPrivateWindow,
iframeSandbox,
accessRemoval: null, // only passed with non-blocking callback
callbackAfterRemoval: null,
iframeAllow,
});
this._createCleanupTask(cleanupFunction);
this._createTask({
name: name + " reject foreign with exception",
cookieBehavior: BEHAVIOR_REJECT_FOREIGN,
allowList: true,
callback: callbackNonTracking,
extraPrefs: [...(extraPrefs || [])],
expectedBlockingNotifications: 0,
runInPrivateWindow,
iframeSandbox,
accessRemoval,
callbackAfterRemoval,
iframeAllow,
});
this._createCleanupTask(cleanupFunction);
this._createTask({
name,
cookieBehavior: BEHAVIOR_REJECT_FOREIGN,
allowList: false,
callback: callbackTracking,
extraPrefs: [...(extraPrefs || [])],
expectedBlockingNotifications: expectedBlockingNotifications
? Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_FOREIGN
: 0,
runInPrivateWindow,
iframeSandbox,
accessRemoval,
callbackAfterRemoval,
iframeAllow,
});
this._createCleanupTask(cleanupFunction);
this._createTask({
name,
cookieBehavior: BEHAVIOR_REJECT_TRACKER,
allowList: true,
callback: callbackNonTracking,
extraPrefs,
expectedBlockingNotifications: 0,
runInPrivateWindow,
iframeSandbox,
accessRemoval,
callbackAfterRemoval,
iframeAllow,
});
this._createCleanupTask(cleanupFunction);
this._createTask({
name,
cookieBehavior: BEHAVIOR_REJECT_TRACKER,
allowList: false,
callback: callbackNonTracking,
extraPrefs,
expectedBlockingNotifications: false,
runInPrivateWindow,
iframeSandbox,
accessRemoval: null, // only passed with non-blocking callback
callbackAfterRemoval: null,
thirdPartyPage: TEST_ANOTHER_3RD_PARTY_PAGE,
iframeAllow,
});
this._createCleanupTask(cleanupFunction);
} else {
// This is only used for imageCacheWorker.js tests
this._createTask({
name,
cookieBehavior: options.cookieBehavior,
allowList: options.blockingByAllowList,
callback: options.callback,
extraPrefs,
expectedBlockingNotifications: options.expectedBlockingNotifications,
runInPrivateWindow,
iframeSandbox,
accessRemoval: options.accessRemoval,
callbackAfterRemoval: options.callbackAfterRemoval,
iframeAllow,
});
this._createCleanupTask(cleanupFunction);
}
// Phase 2: Here we want to test that a third-party context doesn't
// get blocked with when the same origin is opened through window.open().
if (windowOpenTest) {
this._createWindowOpenTask(
name,
BEHAVIOR_REJECT_TRACKER,
callbackTracking,
callbackNonTracking,
runInPrivateWindow,
iframeSandbox,
false,
extraPrefs,
iframeAllow
);
this._createCleanupTask(cleanupFunction);
// Now, check if it works for nested iframes.
this._createWindowOpenTask(
name,
BEHAVIOR_REJECT_TRACKER,
callbackTracking,
callbackNonTracking,
runInPrivateWindow,
iframeSandbox,
true,
extraPrefs,
iframeAllow
);
this._createCleanupTask(cleanupFunction);
}
// Phase 3: Here we want to test that a third-party context doesn't
// get blocked with user interaction present
if (userInteractionTest) {
this._createUserInteractionTask(
name,
BEHAVIOR_REJECT_TRACKER,
callbackTracking,
callbackNonTracking,
runInPrivateWindow,
iframeSandbox,
false,
extraPrefs,
iframeAllow
);
this._createCleanupTask(cleanupFunction);
// Now, check if it works for nested iframes.
this._createUserInteractionTask(
name,
BEHAVIOR_REJECT_TRACKER,
callbackTracking,
callbackNonTracking,
runInPrivateWindow,
iframeSandbox,
true,
extraPrefs,
iframeAllow
);
this._createCleanupTask(cleanupFunction);
}
}
},
_waitObserver(targetTopic, expectedCount) {
let cnt = 0;
return new Promise(resolve => {
Services.obs.addObserver(function observer(subject, topic) {
if (topic != targetTopic) {
return;
}
cnt++;
if (cnt != expectedCount) {
return;
}
Services.obs.removeObserver(observer, targetTopic);
resolve();
}, targetTopic);
});
},
_waitUserInteractionPerm() {
return this._waitObserver(
"antitracking-test-user-interaction-perm-added",
1
);
},
_waitStorageAccessPerm(expectedCount) {
return this._waitObserver(
"antitracking-test-storage-access-perm-added",
expectedCount
);
},
async interactWithTracker() {
let win = await BrowserTestUtils.openNewBrowserWindow();
await BrowserTestUtils.withNewTab(
{ gBrowser: win.gBrowser, url: TEST_3RD_PARTY_PAGE },
async function (browser) {
info("Let's interact with the tracker");
await SpecialPowers.spawn(browser, [], async function () {
SpecialPowers.wrap(content.document).userInteractionForTesting();
});
}
);
await BrowserTestUtils.closeWindow(win);
},
async _setupTest(win, cookieBehavior, runInPrivateWindow, extraPrefs) {
await SpecialPowers.flushPrefEnv();
await setCookieBehaviorPref(cookieBehavior, runInPrivateWindow);
await SpecialPowers.pushPrefEnv({
set: [
["dom.storage_access.enabled", true],
["privacy.trackingprotection.enabled", false],
["privacy.trackingprotection.pbmode.enabled", false],
["dom.security.https_first_pbm", false],
[
"privacy.trackingprotection.annotate_channels",
cookieBehavior != BEHAVIOR_ACCEPT,
],
["privacy.restrict3rdpartystorage.console.lazy", false],
[
"privacy.restrict3rdpartystorage.userInteractionRequiredForHosts",
"tracking.example.com,tracking.example.org",
],
["privacy.antitracking.testing", true],
],
});
if (extraPrefs && Array.isArray(extraPrefs) && extraPrefs.length) {
await SpecialPowers.pushPrefEnv({ set: extraPrefs });
let enableWebcompat = Services.prefs.getBoolPref(
"privacy.antitracking.enableWebcompat"
);
// If the skip list is disabled by pref, it will always return an empty
// list.
if (enableWebcompat) {
for (let item of extraPrefs) {
// When setting up exception URLs, we need to wait to ensure our prefs
// actually take effect. In order to do this, we set up a exception
// list observer and wait until it calls us back.
if (item[0] == "urlclassifier.trackingAnnotationSkipURLs") {
info("Waiting for the exception list service to initialize...");
let classifier = Cc[
"@mozilla.org/url-classifier/dbservice;1"
].getService(Ci.nsIURIClassifier);
let feature = classifier.getFeatureByName("tracking-annotation");
await TestUtils.waitForCondition(() => {
for (let x of item[1].toLowerCase().split(",")) {
if (feature.exceptionHostList.split(",").includes(x)) {
return true;
}
}
return false;
}, "Exception list service initialized");
break;
}
}
}
}
await UrlClassifierTestUtils.addTestTrackers();
if (!gTestTrackersCleanupRegistered) {
registerCleanupFunction(_ => {
if (gTestTrackersCleanedUp) {
return;
}
UrlClassifierTestUtils.cleanupTestTrackers();
gTestTrackersCleanedUp = true;
});
gTestTrackersCleanupRegistered = true;
}
},
_createTask(options) {
add_task(async function () {
info(
"Starting " +
(options.cookieBehavior != BEHAVIOR_ACCEPT
? "blocking"
: "non-blocking") +
" cookieBehavior (" +
options.cookieBehavior +
") with" +
(options.allowList ? "" : "out") +
" allow list test " +
options.name +
" running in a " +
(options.runInPrivateWindow ? "private" : "normal") +
" window " +
" with iframe sandbox set to " +
options.iframeSandbox +
" and iframe allow set to " +
options.iframeAllow +
" and access removal set to " +
options.accessRemoval +
(typeof options.thirdPartyPage == "string"
? " and third party page set to " + options.thirdPartyPage
: "") +
(typeof options.topPage == "string"
? " and top page set to " + options.topPage
: "")
);
is(
!!options.callbackAfterRemoval,
!!options.accessRemoval,
"callbackAfterRemoval must be passed when accessRemoval is non-null"
);
let win = window;
if (options.runInPrivateWindow) {
win = OpenBrowserWindow({ private: true });
await TestUtils.topicObserved("browser-delayed-startup-finished");
}
await AntiTracking._setupTest(
win,
options.cookieBehavior,
options.runInPrivateWindow,
options.extraPrefs
);
let topPage;
if (typeof options.topPage == "string") {
topPage = options.topPage;
} else {
topPage = TEST_TOP_PAGE;
}
let thirdPartyPage, thirdPartyDomainURI;
if (typeof options.thirdPartyPage == "string") {
thirdPartyPage = options.thirdPartyPage;
let url = new URL(thirdPartyPage);
thirdPartyDomainURI = Services.io.newURI(url.origin);
} else {
thirdPartyPage = TEST_3RD_PARTY_PAGE;
thirdPartyDomainURI = Services.io.newURI(TEST_3RD_PARTY_DOMAIN);
}
// It's possible that the third-party domain has been exceptionlisted
// through extraPrefs, so let's try annotating it here and adjust our
// blocking expectations as necessary.
if (
options.expectedBlockingNotifications ==
Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER
) {
if (
!(await AntiTracking._isThirdPartyPageClassifiedAsTracker(
topPage,
thirdPartyDomainURI
))
) {
options.expectedBlockingNotifications = 0;
}
}
let cookieBlocked = 0;
let { expectedBlockingNotifications } = options;
if (!Array.isArray(expectedBlockingNotifications)) {
expectedBlockingNotifications = [expectedBlockingNotifications];
}
let listener = {
onContentBlockingEvent(webProgress, request, event) {
for (const notification of expectedBlockingNotifications) {
if (event & notification) {
++cookieBlocked;
}
}
},
};
function prepareTestEnvironmentOnPage() {
win.gBrowser.addProgressListener(listener);
Services.console.reset();
}
if (!options.allowList) {
prepareTestEnvironmentOnPage();
}
let consoleWarningPromise;
if (options.expectedBlockingNotifications) {
consoleWarningPromise = new Promise(resolve => {
let consoleListener = {
observe(msg) {
if (
msg
.QueryInterface(Ci.nsIScriptError)
.category.startsWith("cookieBlocked")
) {
Services.console.unregisterListener(consoleListener);
resolve();
}
},
};
Services.console.registerListener(consoleListener);
});
} else {
consoleWarningPromise = Promise.resolve();
}
let { expectedPartitioningNotifications } = options;
if (!Array.isArray(expectedPartitioningNotifications)) {
expectedPartitioningNotifications = [expectedPartitioningNotifications];
}
let consolePartitioningWarningPromise;
if (options.expectedPartitioningNotifications) {
consolePartitioningWarningPromise = new Promise(resolve => {
let consoleListener = {
observe(msg) {
if (
msg
.QueryInterface(Ci.nsIScriptError)
.category.startsWith("cookiePartitioned")
) {
Services.console.unregisterListener(consoleListener);
resolve();
}
},
};
Services.console.registerListener(consoleListener);
});
} else {
consolePartitioningWarningPromise = Promise.resolve();
}
info("Creating a new tab");
let tab = BrowserTestUtils.addTab(win.gBrowser, topPage);
win.gBrowser.selectedTab = tab;
let browser = win.gBrowser.getBrowserForTab(tab);
await BrowserTestUtils.browserLoaded(browser);
info("Check the cookieJarSettings of the browser object");
ok(
browser.cookieJarSettings,
"The browser object has the cookieJarSettings."
);
is(
browser.cookieJarSettings.cookieBehavior,
options.cookieBehavior,
"The cookieJarSettings has the correct cookieBehavior"
);
if (options.allowList) {
info("Disabling content blocking for this page");
win.gProtectionsHandler.disableForCurrentPage();
prepareTestEnvironmentOnPage();
// The previous function reloads the browser, so wait for it to load again!
await BrowserTestUtils.browserLoaded(browser);
}
info("Creating a 3rd party content");
let doAccessRemovalChecks =
typeof options.accessRemoval == "string" &&
options.cookieBehavior == BEHAVIOR_REJECT_TRACKER &&
!options.allowList;
await SpecialPowers.spawn(
browser,
[
{
page: thirdPartyPage,
nextPage: TEST_4TH_PARTY_PAGE,
callback: options.callback.toString(),
callbackAfterRemoval: options.callbackAfterRemoval
? options.callbackAfterRemoval.toString()
: null,
accessRemoval: options.accessRemoval,
iframeSandbox: options.iframeSandbox,
iframeAllow: options.iframeAllow,
allowList: options.allowList,
doAccessRemovalChecks,
},
],
async function (obj) {
let id = "id" + Math.random();
await new content.Promise(resolve => {
let ifr = content.document.createElement("iframe");
ifr.id = id;
ifr.onload = function () {
info("Sending code to the 3rd party content");
let callback = obj.allowList + "!!!" + obj.callback;
ifr.contentWindow.postMessage(callback, "*");
};
if (typeof obj.iframeSandbox == "string") {
ifr.setAttribute("sandbox", obj.iframeSandbox);
}
if (typeof obj.iframeAllow == "string") {
ifr.setAttribute("allow", obj.iframeAllow);
}
content.addEventListener("message", function msg(event) {
if (event.data.type == "finish") {
content.removeEventListener("message", msg);
resolve();
return;
}
if (event.data.type == "ok") {
ok(event.data.what, event.data.msg);
return;
}
if (event.data.type == "info") {
info(event.data.msg);
return;
}
ok(false, "Unknown message");
});
content.document.body.appendChild(ifr);
ifr.src = obj.page;
});
if (obj.doAccessRemovalChecks) {
info(`Running after removal checks (${obj.accessRemoval})`);
switch (obj.accessRemoval) {
case "navigate-subframe":
await new content.Promise(resolve => {
let ifr = content.document.getElementById(id);
let oldWindow = ifr.contentWindow;
ifr.onload = function () {
info("Sending code to the old 3rd party content");
oldWindow.postMessage(obj.callbackAfterRemoval, "*");
};
if (typeof obj.iframeSandbox == "string") {
ifr.setAttribute("sandbox", obj.iframeSandbox);
}
if (typeof obj.iframeAllow == "string") {
ifr.setAttribute("allow", obj.iframeAllow);
}
content.addEventListener("message", function msg(event) {
if (event.data.type == "finish") {
content.removeEventListener("message", msg);
resolve();
return;
}
if (event.data.type == "ok") {
ok(event.data.what, event.data.msg);
return;
}
if (event.data.type == "info") {
info(event.data.msg);
return;
}
ok(false, "Unknown message");
});
ifr.src = obj.nextPage;
});
break;
case "navigate-topframe":
// pass-through
break;
default:
ok(
false,
"Unexpected accessRemoval code passed: " + obj.accessRemoval
);
break;
}
}
}
);
if (
doAccessRemovalChecks &&
options.accessRemoval == "navigate-topframe"
) {
BrowserTestUtils.startLoadingURIString(browser, TEST_4TH_PARTY_PAGE);
await BrowserTestUtils.browserLoaded(browser);
let pageshow = BrowserTestUtils.waitForContentEvent(
tab.linkedBrowser,
"pageshow"
);
gBrowser.goBack();
await pageshow;
await SpecialPowers.spawn(
browser,
[
{
page: thirdPartyPage,
callbackAfterRemoval: options.callbackAfterRemoval
? options.callbackAfterRemoval.toString()
: null,
iframeSandbox: options.iframeSandbox,
iframeAllow: options.iframeAllow,
},
],
async function (obj) {
let ifr = content.document.createElement("iframe");
ifr.onload = function () {
info(
"Sending code to the 3rd party content to verify accessRemoval"
);
ifr.contentWindow.postMessage(obj.callbackAfterRemoval, "*");
};
if (typeof obj.iframeSandbox == "string") {
ifr.setAttribute("sandbox", obj.iframeSandbox);
}
if (typeof obj.iframeAllow == "string") {
ifr.setAttribute("allow", obj.iframeAllow);
}
content.addEventListener("message", function msg(event) {
if (event.data.type == "finish") {
content.removeEventListener("message", msg);
return;
}
if (event.data.type == "ok") {
ok(event.data.what, event.data.msg);
return;
}
if (event.data.type == "info") {
info(event.data.msg);
return;
}
ok(false, "Unknown message");
});
content.document.body.appendChild(ifr);
ifr.src = obj.page;
}
);
}
// Wait until the message appears on the console.
await consoleWarningPromise;
await consolePartitioningWarningPromise;
let allMessages = Services.console.getMessageArray().filter(msg => {
try {
// Select all messages that the anti-tracking backend could generate.
let category = msg.QueryInterface(Ci.nsIScriptError).category;
let isBlocking = category.startsWith("cookieBlocked");
// Only look for partitioning notifications if we expect them.
let isPartitioning =
options.expectedPartitioningNotifications &&
category.startsWith("cookiePartitioned");
return isBlocking || isPartitioning;
} catch (e) {
return false;
}
});
// When changing this list, please make sure to update the corresponding
// code in ReportBlockingToConsole().
let expectedCategories = [];
let rawExpectedCategories =
options.expectedBlockingNotifications ??
options.expectedPartitioningNotifications;
if (!Array.isArray(rawExpectedCategories)) {
// if given a single value to match, expect each message to match it
rawExpectedCategories = Array(allMessages.length).fill(
rawExpectedCategories
);
}
for (let category of rawExpectedCategories) {
switch (category) {
case Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_BY_PERMISSION:
expectedCategories.push("cookieBlockedPermission");
break;
case Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER:
expectedCategories.push("cookieBlockedTracker");
break;
case Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_ALL:
expectedCategories.push("cookieBlockedAll");
break;
case Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_FOREIGN:
expectedCategories.push("cookieBlockedForeign");
break;
case Ci.nsIWebProgressListener.STATE_COOKIES_PARTITIONED_TRACKER:
expectedCategories.push("cookiePartitionedForeign");
break;
}
}
if (!expectedCategories.length) {
is(allMessages.length, 0, "No console messages should be generated");
} else {
ok(!!allMessages.length, "Some console message should be generated");
if (options.errorMessageDomains) {
is(
allMessages.length,
options.errorMessageDomains.length,
"Enough items provided in errorMessageDomains"
);
}
}
let index = 0;
for (let msg of allMessages) {
is(
msg.category,
expectedCategories[index],
`Message ${index} should be of expected category`
);
if (options.errorMessageDomains) {
ok(
msg.errorMessage.includes(options.errorMessageDomains[index]),
`Error message domain ${options.errorMessageDomains[index]} (${index}) found in "${msg.errorMessage}"`
);
index++;
}
}
if (options.allowList) {
info("Enabling content blocking for this page");
win.gProtectionsHandler.enableForCurrentPage();
// The previous function reloads the browser, so wait for it to load again!
await BrowserTestUtils.browserLoaded(browser);
}
win.gBrowser.removeProgressListener(listener);
if (!!cookieBlocked != !!options.expectedBlockingNotifications) {
ok(false, JSON.stringify(cookieBlocked));
ok(false, JSON.stringify(options.expectedBlockingNotifications));
}
is(
!!cookieBlocked,
!!options.expectedBlockingNotifications,
"Checking cookie blocking notifications"
);
info("Removing the tab");
BrowserTestUtils.removeTab(tab);
if (options.runInPrivateWindow) {
win.close();
}
});
},
_createCleanupTask(cleanupFunction) {
add_task(async function () {
info("Cleaning up.");
if (cleanupFunction) {
await cleanupFunction();
}
// While running these tests we typically do not have enough idle time to do
// GC reliably, so force it here.
forceGC();
});
},
_createWindowOpenTask(
name,
cookieBehavior,
blockingCallback,
nonBlockingCallback,
runInPrivateWindow,
iframeSandbox,
testInSubIFrame,
extraPrefs,
iframeAllow
) {
add_task(async function () {
info(
`Starting window-open${
testInSubIFrame ? " sub iframe" : ""
} test ${name}`
);
let win = window;
if (runInPrivateWindow) {
win = OpenBrowserWindow({ private: true });
await TestUtils.topicObserved("browser-delayed-startup-finished");
}
await AntiTracking._setupTest(
win,
cookieBehavior,
runInPrivateWindow,
extraPrefs
);
info("Creating a new tab");
let tab = BrowserTestUtils.addTab(win.gBrowser, TEST_TOP_PAGE);
win.gBrowser.selectedTab = tab;
let browser = win.gBrowser.getBrowserForTab(tab);
await BrowserTestUtils.browserLoaded(browser);
info("Create a first-level iframe to test sub iframes.");
if (testInSubIFrame) {
let iframeBrowsingContext = await SpecialPowers.spawn(
browser,
[{ page: TEST_IFRAME_PAGE }],
async function (obj) {
// Add an iframe.
let ifr = content.document.createElement("iframe");
let loading = new content.Promise(resolve => {
ifr.onload = resolve;
});
content.document.body.appendChild(ifr);
ifr.src = obj.page;
await loading;
return ifr.browsingContext;
}
);
browser = iframeBrowsingContext;
}
let pageURL = TEST_3RD_PARTY_PAGE_WO;
if (gFeatures == "noopener") {
pageURL += "?noopener";
}
info("Creating a 3rd party content");
await SpecialPowers.spawn(
browser,
[
{
page: pageURL,
blockingCallback: blockingCallback.toString(),
nonBlockingCallback: nonBlockingCallback.toString(),
iframeSandbox,
iframeAllow,
},
],
async function (obj) {
await new content.Promise(resolve => {
let ifr = content.document.createElement("iframe");
ifr.onload = function () {
info("Sending code to the 3rd party content");
ifr.contentWindow.postMessage(obj, "*");
};
if (typeof obj.iframeSandbox == "string") {
ifr.setAttribute("sandbox", obj.iframeSandbox);
}
if (typeof obj.iframeAllow == "string") {
ifr.setAttribute("allow", obj.iframeAllow);
}
content.addEventListener("message", function msg(event) {
if (event.data.type == "finish") {
content.removeEventListener("message", msg);
resolve();
return;
}
if (event.data.type == "ok") {
ok(event.data.what, event.data.msg);
return;
}
if (event.data.type == "info") {
info(event.data.msg);
return;
}
ok(false, "Unknown message");
});
content.document.body.appendChild(ifr);
ifr.src = obj.page;
});
}
);
info("Removing the tab");
BrowserTestUtils.removeTab(tab);
if (runInPrivateWindow) {
win.close();
}
});
},
_createUserInteractionTask(
name,
cookieBehavior,
blockingCallback,
nonBlockingCallback,
runInPrivateWindow,
iframeSandbox,
testInSubIFrame,
extraPrefs,
iframeAllow
) {
add_task(async function () {
info(
`Starting user-interaction${
testInSubIFrame ? " sub iframe" : ""
} test ${name}`
);
let win = window;
if (runInPrivateWindow) {
win = OpenBrowserWindow({ private: true });
await TestUtils.topicObserved("browser-delayed-startup-finished");
}
await AntiTracking._setupTest(
win,
cookieBehavior,
runInPrivateWindow,
extraPrefs
);
info("Creating a new tab");
let tab = BrowserTestUtils.addTab(win.gBrowser, TEST_TOP_PAGE);
win.gBrowser.selectedTab = tab;
let browser = win.gBrowser.getBrowserForTab(tab);
await BrowserTestUtils.browserLoaded(browser);
if (testInSubIFrame) {
info("Create a first-level iframe to test sub iframes.");
let iframeBrowsingContext = await SpecialPowers.spawn(
browser,
[{ page: TEST_IFRAME_PAGE }],
async function (obj) {
// Add an iframe.
let ifr = content.document.createElement("iframe");
let loading = new content.Promise(resolve => {
ifr.onload = resolve;
});
content.document.body.appendChild(ifr);
ifr.src = obj.page;
await loading;
return ifr.browsingContext;
}
);
browser = iframeBrowsingContext;
}
// The following test will open an popup which interacts with the tracker
// page. So there will be an user-interaction permission added. We wait
// it explicitly.
let promiseUIPerm = AntiTracking._waitUserInteractionPerm();
info("Creating a 3rd party content");
await SpecialPowers.spawn(
browser,
[
{
page: TEST_3RD_PARTY_PAGE_UI,
popup: TEST_POPUP_PAGE,
blockingCallback: blockingCallback.toString(),
iframeSandbox,
iframeAllow,
},
],
async function (obj) {
let ifr = content.document.createElement("iframe");
let loading = new content.Promise(resolve => {
ifr.onload = resolve;
});
if (typeof obj.iframeSandbox == "string") {
ifr.setAttribute("sandbox", obj.iframeSandbox);
}
if (typeof obj.iframeAllow == "string") {
ifr.setAttribute("allow", obj.iframeAllow);
}
content.document.body.appendChild(ifr);
ifr.src = obj.page;
await loading;
info(
"The 3rd party content should not have access to first party storage."
);
await new content.Promise(resolve => {
content.addEventListener("message", function msg(event) {
if (event.data.type == "finish") {
content.removeEventListener("message", msg);
resolve();
return;
}
if (event.data.type == "ok") {
ok(event.data.what, event.data.msg);
return;
}
if (event.data.type == "info") {
info(event.data.msg);
return;
}
ok(false, "Unknown message");
});
ifr.contentWindow.postMessage(
{ callback: obj.blockingCallback },
"*"
);
});
let windowClosed = new content.Promise(resolve => {
Services.ww.registerNotification(
function notification(aSubject, aTopic) {
if (aTopic == "domwindowclosed") {
Services.ww.unregisterNotification(notification);
resolve();
}
}
);
});
info("Opening a window from the iframe.");
SpecialPowers.spawn(
ifr,
[{ popup: obj.popup }],
async function (obj) {
content.open(obj.popup);
}
);
info("Let's wait for the window to be closed");
await windowClosed;
info(
"First time, the 3rd party content should not have access to first party storage " +
"because the tracker did not have user interaction"
);
await new content.Promise(resolve => {
content.addEventListener("message", function msg(event) {
if (event.data.type == "finish") {
content.removeEventListener("message", msg);
resolve();
return;
}
if (event.data.type == "ok") {
ok(event.data.what, event.data.msg);
return;
}
if (event.data.type == "info") {
info(event.data.msg);
return;
}
ok(false, "Unknown message");
});
ifr.contentWindow.postMessage(
{ callback: obj.blockingCallback },
"*"
);
});
}
);
// We wait until the user-interaction permission is added.
await promiseUIPerm;
// We also need to wait the user-interaction permission here.
promiseUIPerm = AntiTracking._waitUserInteractionPerm();
await AntiTracking.interactWithTracker();
await promiseUIPerm;
// Following test will also open an popup to interact with the page. We
// need to explicitly wait it. Without waiting it, it could be added after
// we clear up the test and interfere the next test.
promiseUIPerm = AntiTracking._waitUserInteractionPerm();
// We have to wait until the storage access permission is added. This has
// the same reason as above user-interaction permission. Note that there
// will be two storage access permission added due to the way how we
// trigger the heuristic. The first permission is added due to 'Opener'
// heuristic and the second one is due to 'Opener after user interaction'.
// The page we use to trigger the heuristic will trigger both heuristic,
// so we have to wait 2 permissions.
let promiseStorageAccessPerm = AntiTracking._waitStorageAccessPerm(2);
await SpecialPowers.spawn(
browser,
[
{
page: TEST_3RD_PARTY_PAGE_UI,
popup: TEST_POPUP_PAGE,
nonBlockingCallback: nonBlockingCallback.toString(),
iframeSandbox,
iframeAllow,
},
],
async function (obj) {
let ifr = content.document.createElement("iframe");
let loading = new content.Promise(resolve => {
ifr.onload = resolve;
});
if (typeof obj.iframeSandbox == "string") {
ifr.setAttribute("sandbox", obj.iframeSandbox);
}
if (typeof obj.iframeAllow == "string") {
ifr.setAttribute("allow", obj.iframeAllow);
}
content.document.body.appendChild(ifr);
ifr.src = obj.page;
await loading;
let windowClosed = new content.Promise(resolve => {
Services.ww.registerNotification(
function notification(aSubject, aTopic) {
if (aTopic == "domwindowclosed") {
Services.ww.unregisterNotification(notification);
resolve();
}
}
);
});
info("Opening a window from the iframe.");
SpecialPowers.spawn(
ifr,
[{ popup: obj.popup }],
async function (obj) {
content.open(obj.popup);
}
);
info("Let's wait for the window to be closed");
await windowClosed;
info(
"The 3rd party content should now have access to first party storage."
);
await new content.Promise(resolve => {
content.addEventListener("message", function msg(event) {
if (event.data.type == "finish") {
content.removeEventListener("message", msg);
resolve();
return;
}
if (event.data.type == "ok") {
ok(event.data.what, event.data.msg);
return;
}
if (event.data.type == "info") {
info(event.data.msg);
return;
}
ok(false, "Unknown message");
});
ifr.contentWindow.postMessage(
{ callback: obj.nonBlockingCallback },
"*"
);
});
}
);
// Explicitly wait the user-interaction and storage access permission
// before we do the cleanup.
await promiseUIPerm;
await promiseStorageAccessPerm;
info("Removing the tab");
BrowserTestUtils.removeTab(tab);
if (runInPrivateWindow) {
win.close();
}
});
},
async _isThirdPartyPageClassifiedAsTracker(topPage, thirdPartyDomainURI) {
let channel;
await new Promise((resolve, reject) => {
channel = NetUtil.newChannel({
uri: thirdPartyDomainURI,
loadingPrincipal: Services.scriptSecurityManager.createContentPrincipal(
thirdPartyDomainURI,
{}
),
securityFlags:
Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER,
});
channel
.QueryInterface(Ci.nsIHttpChannelInternal)
.setTopWindowURIIfUnknown(Services.io.newURI(topPage));
function Listener() {}
Listener.prototype = {
onStartRequest() {},
onDataAvailable(request, stream, off, cnt) {
// Consume the data to prevent hitting the assertion.
NetUtil.readInputStreamToString(stream, cnt);
},
onStopRequest(request) {
let status = request.QueryInterface(Ci.nsIHttpChannel).responseStatus;
if (status == 200) {
resolve();
} else {
reject();
}
},
};
let listener = new Listener();
channel.asyncOpen(listener);
});
return !!(
channel.QueryInterface(Ci.nsIClassifiedChannel).classificationFlags &
Ci.nsIClassifiedChannel.CLASSIFIED_ANY_BASIC_TRACKING
);
},
};