333 lines
9.7 KiB
JavaScript
333 lines
9.7 KiB
JavaScript
/* Any copyright is dedicated to the Public Domain.
|
|
https://creativecommons.org/publicdomain/zero/1.0/ */
|
|
|
|
"use strict";
|
|
|
|
const { RemoteSettings } = ChromeUtils.importESModule(
|
|
"resource://services-settings/remote-settings.sys.mjs"
|
|
);
|
|
|
|
// Name of the RemoteSettings collection containing exceptions.
|
|
const COLLECTION_NAME = "url-classifier-exceptions";
|
|
|
|
// RemoteSettings collection db.
|
|
let db;
|
|
|
|
/**
|
|
* Wait for the exception list service to initialize.
|
|
* @param {string} [urlPattern] - The URL pattern to wait for to be present.
|
|
* Pass null to wait for all entries to be removed.
|
|
*/
|
|
async function waitForExceptionListServiceSynced(urlPattern) {
|
|
info(
|
|
`Waiting for the exception list service to initialize for ${urlPattern}`
|
|
);
|
|
let classifier = Cc["@mozilla.org/url-classifier/dbservice;1"].getService(
|
|
Ci.nsIURIClassifier
|
|
);
|
|
let feature = classifier.getFeatureByName("tracking-protection");
|
|
await TestUtils.waitForCondition(() => {
|
|
if (urlPattern == null) {
|
|
return feature.exceptionList.testGetEntries().length === 0;
|
|
}
|
|
return feature.exceptionList
|
|
.testGetEntries()
|
|
.some(entry => entry.urlPattern === urlPattern);
|
|
}, "Exception list service initialized");
|
|
}
|
|
|
|
/**
|
|
* Dispatch a RemoteSettings "sync" event.
|
|
* @param {Object} data - The event's data payload.
|
|
* @param {Object} [data.created] - Records that were created.
|
|
* @param {Object} [data.updated] - Records that were updated.
|
|
* @param {Object} [data.deleted] - Records that were removed.
|
|
* @param {Object} [data.current] - The current list of records.
|
|
*/
|
|
async function remoteSettingsSync({ created, updated, deleted, current }) {
|
|
await RemoteSettings(COLLECTION_NAME).emit("sync", {
|
|
data: {
|
|
created,
|
|
updated,
|
|
deleted,
|
|
// The list service seems to require this field to be set.
|
|
current,
|
|
},
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Wait for a content blocking event to occur.
|
|
* @param {Window} win - The window to listen for the event on.
|
|
* @returns {Promise} A promise that resolves when the event occurs.
|
|
*/
|
|
async function waitForContentBlockingEvent(win) {
|
|
return new Promise(resolve => {
|
|
let listener = {
|
|
onContentBlockingEvent(webProgress, request, event) {
|
|
if (event & Ci.nsIWebProgressListener.STATE_BLOCKED_TRACKING_CONTENT) {
|
|
win.gBrowser.removeProgressListener(listener);
|
|
resolve();
|
|
}
|
|
},
|
|
};
|
|
win.gBrowser.addProgressListener(listener);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Load a tracker in third-party context.
|
|
* @param {string} topLevelURL - The URL of the top level page to load.
|
|
* @param {boolean} usePrivateWindow - Whether to use a private window.
|
|
* @returns {Promise} A promise that resolves when the tracker is loaded or
|
|
* blocked. The promise resolves to "loaded" if the tracker is loaded, or
|
|
* "blocked" if it is blocked.
|
|
*/
|
|
async function loadTracker({ topLevelURL, usePrivateWindow }) {
|
|
let win = window;
|
|
|
|
if (usePrivateWindow) {
|
|
win = await BrowserTestUtils.openNewBrowserWindow({ private: true });
|
|
}
|
|
|
|
let tab = win.gBrowser.addTab(topLevelURL, {
|
|
triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
|
|
});
|
|
win.gBrowser.selectedTab = tab;
|
|
let browser = tab.linkedBrowser;
|
|
|
|
await BrowserTestUtils.browserLoaded(browser, false, topLevelURL);
|
|
|
|
info("Create an iframe loading tracking.example.org.");
|
|
|
|
let blockedPromise = waitForContentBlockingEvent(win).then(() => "blocked");
|
|
|
|
let loadPromise = SpecialPowers.spawn(
|
|
browser,
|
|
["https://tracking.example.org"],
|
|
async function (url) {
|
|
let iframe = content.document.createElement("iframe");
|
|
let loadPromise = ContentTaskUtils.waitForEvent(iframe, "load").then(
|
|
() => "loaded"
|
|
);
|
|
iframe.src = url;
|
|
content.document.body.appendChild(iframe);
|
|
return loadPromise;
|
|
}
|
|
);
|
|
|
|
let result = await Promise.race([loadPromise, blockedPromise]);
|
|
|
|
// Clean up.
|
|
if (usePrivateWindow) {
|
|
await BrowserTestUtils.closeWindow(win);
|
|
} else {
|
|
await BrowserTestUtils.removeTab(tab);
|
|
}
|
|
|
|
return result == "loaded";
|
|
}
|
|
|
|
add_setup(async function () {
|
|
await SpecialPowers.pushPrefEnv({
|
|
set: [
|
|
// Add test tracker.
|
|
["urlclassifier.trackingTable.testEntries", "tracking.example.org"],
|
|
[
|
|
"urlclassifier.trackingAnnotationTable.testEntries",
|
|
"tracking.example.org",
|
|
],
|
|
// Clear predefined exceptions.
|
|
["urlclassifier.trackingAnnotationSkipURLs", ""],
|
|
["urlclassifier.trackingSkipURLs", ""],
|
|
// Make sure we're in "standard" mode.
|
|
["browser.contentblocking.category", "standard"],
|
|
],
|
|
});
|
|
|
|
// Start with an empty RS collection.
|
|
info(`Initializing RemoteSettings collection "${COLLECTION_NAME}".`);
|
|
db = RemoteSettings(COLLECTION_NAME).db;
|
|
await db.importChanges({}, Date.now(), [], { clear: true });
|
|
});
|
|
|
|
/**
|
|
* Set exceptions via RemoteSettings.
|
|
* @param {Object[]} entries - The entries to set. If empty, the exceptions will be cleared.
|
|
*/
|
|
async function setExceptions(entries) {
|
|
info("Set exceptions via RemoteSettings");
|
|
if (!entries.length) {
|
|
await db.clear();
|
|
await db.importChanges({}, Date.now());
|
|
await remoteSettingsSync({ current: [] });
|
|
await waitForExceptionListServiceSynced();
|
|
return;
|
|
}
|
|
|
|
let entriesPromises = entries.map(e =>
|
|
db.create({
|
|
urlPattern: e.urlPattern,
|
|
classifierFeatures: e.classifierFeatures,
|
|
// Only apply to private browsing in ETP "standard" mode.
|
|
isPrivateBrowsingOnly: e.isPrivateBrowsingOnly,
|
|
filterContentBlockingCategories: e.filterContentBlockingCategories,
|
|
})
|
|
);
|
|
|
|
let rsEntries = await Promise.all(entriesPromises);
|
|
|
|
await db.importChanges({}, Date.now());
|
|
await remoteSettingsSync({ current: rsEntries });
|
|
await waitForExceptionListServiceSynced(rsEntries[0].urlPattern);
|
|
}
|
|
|
|
// Tests that exception list entries can be scoped to only private browsing.
|
|
add_task(async function test_private_browsing_exception() {
|
|
info("Load tracker in normal browsing.");
|
|
let success = await loadTracker({
|
|
topLevelURL: "https://example.com/",
|
|
usePrivateWindow: false,
|
|
});
|
|
is(
|
|
success,
|
|
true,
|
|
"Tracker access should be allowed in normal browsing, because TP is disabled."
|
|
);
|
|
|
|
info("Load tracker in private browsing.");
|
|
success = await loadTracker({
|
|
topLevelURL: "https://example.com/",
|
|
usePrivateWindow: true,
|
|
});
|
|
is(
|
|
success,
|
|
false,
|
|
"Tracker access should be blocked in private browsing, because TP is enabled and there is no exception set."
|
|
);
|
|
|
|
info("Set exception for private browsing.");
|
|
await setExceptions([
|
|
{
|
|
urlPattern: "*://tracking.example.org/*",
|
|
topLevelUrlPattern: "*://example.com/*",
|
|
classifierFeatures: ["tracking-protection"],
|
|
isPrivateBrowsingOnly: true,
|
|
filterContentBlockingCategories: ["standard"],
|
|
},
|
|
]);
|
|
|
|
info("Load tracker in normal browsing.");
|
|
success = await loadTracker({
|
|
topLevelURL: "https://example.com/",
|
|
usePrivateWindow: false,
|
|
});
|
|
is(
|
|
success,
|
|
true,
|
|
"Tracker access should be allowed in normal browsing, because TP is disabled."
|
|
);
|
|
|
|
info("Load tracker in private browsing.");
|
|
success = await loadTracker({
|
|
topLevelURL: "https://example.com/",
|
|
usePrivateWindow: true,
|
|
});
|
|
is(
|
|
success,
|
|
true,
|
|
"Tracker access should be allowed in private browsing even though TP is enabled, because it's allow-listed."
|
|
);
|
|
|
|
info("Switch to ETP 'strict' mode.");
|
|
await SpecialPowers.pushPrefEnv({
|
|
set: [["browser.contentblocking.category", "strict"]],
|
|
});
|
|
|
|
info("Load tracker in normal browsing.");
|
|
success = await loadTracker({
|
|
topLevelURL: "https://example.com/",
|
|
usePrivateWindow: false,
|
|
});
|
|
is(
|
|
success,
|
|
false,
|
|
"Tracker access should be blocked in normal browsing in ETP 'strict' mode."
|
|
);
|
|
|
|
info("Load tracker in private browsing.");
|
|
success = await loadTracker({
|
|
topLevelURL: "https://example.com/",
|
|
usePrivateWindow: true,
|
|
});
|
|
is(
|
|
success,
|
|
false,
|
|
"Tracker access should be blocked in private browsing in ETP 'strict' mode."
|
|
);
|
|
|
|
info("Switch back to ETP 'standard' mode.");
|
|
await SpecialPowers.popPrefEnv();
|
|
|
|
info("Test with different top level site.");
|
|
success = await loadTracker({
|
|
topLevelURL: "https://example.org/",
|
|
usePrivateWindow: false,
|
|
});
|
|
is(
|
|
success,
|
|
true,
|
|
"Tracker access should be allowed in normal browsing, because TP is disabled."
|
|
);
|
|
|
|
info("Load tracker in private browsing.");
|
|
success = await loadTracker({
|
|
topLevelURL: "https://example.org/",
|
|
usePrivateWindow: true,
|
|
});
|
|
is(
|
|
success,
|
|
true,
|
|
"Tracker access should be blocked in private browsing, because TP is enabled and there is no exception set for top the top level URL https://example.org."
|
|
);
|
|
|
|
info("Update exception, removing the additional filtering criteria.");
|
|
await setExceptions([
|
|
{
|
|
urlPattern: "*://tracking.example.org/*",
|
|
classifierFeatures: ["tracking-protection"],
|
|
filterContentBlockingCategories: ["standard", "strict"],
|
|
},
|
|
]);
|
|
|
|
info("Load tracker in private browsing.");
|
|
success = await loadTracker({
|
|
topLevelURL: "https://example.org/",
|
|
usePrivateWindow: true,
|
|
});
|
|
is(
|
|
success,
|
|
true,
|
|
"Tracker access should be allowed in private browsing, because TP is enabled and there is an exception set."
|
|
);
|
|
|
|
info("Switch to ETP 'strict' mode.");
|
|
await SpecialPowers.pushPrefEnv({
|
|
set: [["browser.contentblocking.category", "strict"]],
|
|
});
|
|
|
|
info("Load tracker in normal browsing.");
|
|
success = await loadTracker({
|
|
topLevelURL: "https://example.com/",
|
|
usePrivateWindow: false,
|
|
});
|
|
is(
|
|
success,
|
|
true,
|
|
"Tracker access should be allowed in normal browsing in ETP 'strict' mode, because there is an exception set."
|
|
);
|
|
|
|
info("Cleanup");
|
|
await SpecialPowers.popPrefEnv();
|
|
await setExceptions([]);
|
|
});
|