Source code
Revision control
Copy as Markdown
Other Tools
Test Info:
"use strict";
// privacy.trackingprotection.content.protection.engines
// privacy.trackingprotection.content.protection.engines.pbmode
// privacy.trackingprotection.content.annotation.engines
// privacy.trackingprotection.content.annotation.engines.pbmode
//
// These prefs take comma-separated feature names from the static feature
// table in ContentClassifierService.cpp. The matching engine is built from
// the union of rules in the feature's mListIds, and used at classify time
// based on the channel's PBM-ness and the protection-vs-annotation phase.
// "trackers" feature reads rules from RS list "disconnect-tracker-base"
// (see kFeatures table in ContentClassifierService.cpp). With protection.
// engines = "trackers" set, a matching third-party image should be blocked.
add_task(async function test_engines_pref_blocks_matching_feature() {
let client = getRSClient();
let records = await populateMultipleRS(client.db, [
{
id: "trackers",
name: "disconnect-tracker-base",
rules: ["||example.org^"],
},
]);
await pushEnginePrefs({ protection: "trackers" });
let tab = await openTestTab();
let browser = tab.linkedBrowser;
await syncAndWaitForLists(client, records);
await assertImageBlocked(
browser,
TEST_BLOCKED_3RD_PARTY_DOMAIN,
"example.org blocked by trackers feature via engines pref"
);
});
// A feature that is referenced only by .engines.pbmode (and NOT by .engines)
// must NOT block in a non-PBM channel.
add_task(async function test_pbm_pref_does_not_affect_non_pbm() {
let client = getRSClient();
let records = await populateMultipleRS(client.db, [
{
id: "trackers",
name: "disconnect-tracker-base",
rules: ["||example.org^"],
},
]);
await pushEnginePrefs({ pbmProtection: "trackers" });
let tab = await openTestTab();
let browser = tab.linkedBrowser;
await syncAndWaitForLists(client, records);
await assertImageLoaded(
browser,
TEST_BLOCKED_3RD_PARTY_DOMAIN,
"Non-PBM channel not affected by pbmode engines pref"
);
await assertLacksBlockingState(
browser,
TEST_BLOCKED_3RD_PARTY_DOMAIN,
Ci.nsIWebProgressListener.STATE_BLOCKED_TRACKING_CONTENT,
"Non-PBM channel: no STATE_BLOCKED_TRACKING_CONTENT from pbmode-only pref"
);
});
// A feature that is referenced only by .engines.pbmode SHOULD block on a
// PBM channel even when .engines (non-PBM) is empty.
add_task(async function test_pbm_pref_blocks_in_private_window() {
let client = getRSClient();
let records = await populateMultipleRS(client.db, [
{
id: "trackers",
name: "disconnect-tracker-base",
rules: ["||example.org^"],
},
]);
await pushEnginePrefs({ pbmProtection: "trackers" });
// Sync data while a tab exists so the RS client initializes and stores
// the filter list data — necessary because GetInstance() only runs on
// a real channel classification.
let tab = await openTestTab();
await syncAndWaitForLists(client, records);
BrowserTestUtils.removeTab(tab);
let privateTab = await openPrivateTab();
let pbmBrowser = privateTab.linkedBrowser;
await assertImageBlocked(
pbmBrowser,
TEST_BLOCKED_3RD_PARTY_DOMAIN,
"example.org blocked in PBM via engines.pbmode pref"
);
await assertHasBlockingState(
pbmBrowser,
TEST_BLOCKED_3RD_PARTY_DOMAIN,
Ci.nsIWebProgressListener.STATE_BLOCKED_TRACKING_CONTENT,
"PBM log carries STATE_BLOCKED_TRACKING_CONTENT"
);
});
// Independence of the two protection engines prefs: setting different
// features in .engines vs .engines.pbmode must result in non-PBM channels
// applying only the non-PBM set and PBM channels applying only the PBM
// set. This catches a regression that wired both prefs to the same value.
add_task(async function test_pbm_pref_uses_different_feature_than_non_pbm() {
let client = getRSClient();
let records = await populateMultipleRS(client.db, [
{
id: "trackers",
name: "disconnect-tracker-base",
rules: ["||example.org^"],
},
{
id: "fingerprinters",
name: "disconnect-fingerprinters-base",
rules: ["||example.com^"],
},
]);
await pushEnginePrefs({
protection: "fingerprinters",
pbmProtection: "trackers",
});
let tab = await openTestTab();
let browser = tab.linkedBrowser;
await syncAndWaitForLists(client, records);
await assertImageLoaded(
browser,
TEST_BLOCKED_3RD_PARTY_DOMAIN,
"Non-PBM: example.org loads (trackers is PBM-only)"
);
await assertImageBlocked(
browser,
TEST_ANNOTATED_3RD_PARTY_DOMAIN,
"Non-PBM: example.com blocked by fingerprinters"
);
BrowserTestUtils.removeTab(tab);
let privateTab = await openPrivateTab();
let pbmBrowser = privateTab.linkedBrowser;
await assertImageBlocked(
pbmBrowser,
TEST_BLOCKED_3RD_PARTY_DOMAIN,
"PBM: example.org blocked by trackers (pbmode-only)"
);
await assertImageLoaded(
pbmBrowser,
TEST_ANNOTATED_3RD_PARTY_DOMAIN,
"PBM: example.com loads (fingerprinters is non-PBM-only)"
);
});
// engines pref controls blocking phase; annotation phase should not pick
// up features from the protection.engines pref.
add_task(async function test_engines_pref_phase_separation() {
let client = getRSClient();
let records = await populateMultipleRS(client.db, [
{
id: "trackers",
name: "disconnect-tracker-base",
rules: ["||example.org^"],
},
{
id: "fingerprinters",
name: "disconnect-fingerprinters-base",
rules: ["||example.com^"],
},
]);
await pushEnginePrefs({
protection: "trackers",
annotation: "fingerprinters",
});
let tab = await openTestTab();
let browser = tab.linkedBrowser;
await syncAndWaitForLists(client, records);
await assertImageBlocked(
browser,
TEST_BLOCKED_3RD_PARTY_DOMAIN,
"example.org blocked (trackers in protection.engines)"
);
await assertImageLoaded(
browser,
TEST_ANNOTATED_3RD_PARTY_DOMAIN,
"example.com not blocked (fingerprinters only in annotation)"
);
});
// Unknown feature names should be ignored (logged and skipped), not
// crash; known feature alongside unknown should still block.
add_task(async function test_engines_pref_unknown_feature_ignored() {
let client = getRSClient();
let records = await populateMultipleRS(client.db, [
{
id: "trackers",
name: "disconnect-tracker-base",
rules: ["||example.org^"],
},
]);
await pushEnginePrefs({ protection: "not-a-real-feature, trackers" });
let tab = await openTestTab();
let browser = tab.linkedBrowser;
await syncAndWaitForLists(client, records);
await assertImageBlocked(
browser,
TEST_BLOCKED_3RD_PARTY_DOMAIN,
"Unknown feature ignored, known feature still blocks"
);
});
// Multiple feature names in a single engines pref should each block their
// respective domains.
add_task(async function test_multiple_features_in_engines_pref() {
let client = getRSClient();
let records = await populateMultipleRS(client.db, [
{
id: "trackers",
name: "disconnect-tracker-base",
rules: ["||example.org^"],
},
{
id: "fingerprinters",
name: "disconnect-fingerprinters-base",
rules: ["||example.com^"],
},
]);
await pushEnginePrefs({ protection: "trackers,fingerprinters" });
let tab = await openTestTab();
let browser = tab.linkedBrowser;
await syncAndWaitForLists(client, records);
await assertImageBlocked(
browser,
TEST_BLOCKED_3RD_PARTY_DOMAIN,
"example.org blocked by trackers feature"
);
await assertImageBlocked(
browser,
TEST_ANNOTATED_3RD_PARTY_DOMAIN,
"example.com blocked by fingerprinters feature"
);
});
// Per-feature attribution: each feature in kFeatures advertises its own
// mClassificationFlag / mLoadedState / mReplacedState / mAllowedState /
// mBlockingErrorCode. This block drives one feature at a time in either
// annotation or protection phase and asserts that the corresponding
// state value reaches the content blocking log via MaybeAnnotateChannel /
// MaybeCancelChannel.
async function runAttribution({ listName, feature, expectedState, phase }) {
let client = getRSClient();
let records = await populateMultipleRS(client.db, [
{ id: "attr", name: listName, rules: ["||example.com^"] },
]);
await pushEnginePrefs(
phase === "annotate" ? { annotation: feature } : { protection: feature }
);
let tab = await openTestTab();
let browser = tab.linkedBrowser;
await syncAndWaitForLists(client, records);
if (phase === "annotate") {
await assertImageLoaded(
browser,
TEST_ANNOTATED_3RD_PARTY_DOMAIN,
`example.com not blocked via annotation-only feature ${feature}`
);
} else {
await assertImageBlocked(
browser,
TEST_ANNOTATED_3RD_PARTY_DOMAIN,
`example.com blocked via feature ${feature}`
);
}
await assertHasBlockingState(
browser,
TEST_ANNOTATED_3RD_PARTY_DOMAIN,
expectedState,
`log carries expected state ${expectedState} for ${feature}`
);
}
// email-trackers is intentionally omitted from the annotate set:
// ChannelClassifierUtils::AnnotateChannel only writes a content-blocking-log
// entry when the classification flag is in CLASSIFIED_ANY_BASIC_TRACKING (or
// is a cryptomining flag). CLASSIFIED_EMAILTRACKING is in neither, so the
// log path is not testable via getContentBlockingLog() for this feature.
// SetClassificationFlagsHelper still tags the channel; that's a separate
// observable that this suite doesn't currently cover.
const ATTRIBUTION_CASES = [
{
phase: "annotate",
feature: "trackers-content",
listName: "disconnect-tracker-content",
expectedState:
Ci.nsIWebProgressListener.STATE_LOADED_LEVEL_2_TRACKING_CONTENT,
},
{
phase: "annotate",
feature: "social-trackers",
listName: "mozilla-social",
expectedState:
Ci.nsIWebProgressListener.STATE_LOADED_SOCIALTRACKING_CONTENT,
},
{
phase: "annotate",
feature: "fingerprinters",
listName: "disconnect-fingerprinters-base",
expectedState:
Ci.nsIWebProgressListener.STATE_LOADED_FINGERPRINTING_CONTENT,
},
{
phase: "annotate",
feature: "cryptominers",
listName: "disconnect-cryptominer-base",
expectedState: Ci.nsIWebProgressListener.STATE_LOADED_CRYPTOMINING_CONTENT,
},
{
phase: "block",
feature: "fingerprinters",
listName: "disconnect-fingerprinters-base",
expectedState:
Ci.nsIWebProgressListener.STATE_BLOCKED_FINGERPRINTING_CONTENT,
},
{
phase: "block",
feature: "cryptominers",
listName: "disconnect-cryptominer-base",
expectedState: Ci.nsIWebProgressListener.STATE_BLOCKED_CRYPTOMINING_CONTENT,
},
{
phase: "block",
feature: "social-trackers",
listName: "mozilla-social",
expectedState:
Ci.nsIWebProgressListener.STATE_BLOCKED_SOCIALTRACKING_CONTENT,
},
{
phase: "block",
feature: "trackers",
listName: "disconnect-tracker-base",
expectedState: Ci.nsIWebProgressListener.STATE_BLOCKED_TRACKING_CONTENT,
},
];
for (let c of ATTRIBUTION_CASES) {
add_task({ name: `test_attr_${c.feature}_${c.phase}` }, () =>
runAttribution(c)
);
}
// Replace/allow attribution: a feature's mReplacedState / mAllowedState
// reach the log when the channel-cancel intercept rewrites the outcome.
async function runAttributionReplaceOrAllow({
feature,
listName,
action,
expectedState,
expectedMarkedBlocked,
}) {
let client = getRSClient();
let records = await populateMultipleRS(client.db, [
{ id: "attr", name: listName, rules: ["||example.com^"] },
]);
await pushEnginePrefs({ protection: feature });
let tab = await openTestTab();
let browser = tab.linkedBrowser;
await syncAndWaitForLists(client, records);
let interceptPromise = UrlClassifierTestUtils.handleBeforeBlockChannel({
filterOrigin: TEST_ANNOTATED_3RD_PARTY_DOMAIN.replace(/\/$/, ""),
action,
});
await assertImageLoaded(
browser,
TEST_ANNOTATED_3RD_PARTY_DOMAIN,
`example.com should load (intercept=${action}) via ${feature}`
);
await interceptPromise;
let entry = await assertHasBlockingState(
browser,
TEST_ANNOTATED_3RD_PARTY_DOMAIN,
expectedState,
`log carries expected state ${expectedState} for ${feature}`
);
is(
entry[1],
expectedMarkedBlocked,
`entry blocked-flag matches intercept action ${action}`
);
}
add_task(async function test_attr_fingerprinters_replace() {
await runAttributionReplaceOrAllow({
feature: "fingerprinters",
listName: "disconnect-fingerprinters-base",
action: "replace",
expectedState:
Ci.nsIWebProgressListener.STATE_REPLACED_FINGERPRINTING_CONTENT,
expectedMarkedBlocked: true,
});
});
add_task(async function test_attr_fingerprinters_allow() {
await runAttributionReplaceOrAllow({
feature: "fingerprinters",
listName: "disconnect-fingerprinters-base",
action: "allow",
expectedState:
Ci.nsIWebProgressListener.STATE_ALLOWED_FINGERPRINTING_CONTENT,
expectedMarkedBlocked: false,
});
});
// Multi-engine aggregation in the block phase: two features match the same
// URL. Only one wins the cancel (the first in kFeatures iteration order
// whose mBlockingErrorCode != NS_OK), so we deliberately do not assert
// STATE_BLOCKED_FINGERPRINTING_CONTENT here.
add_task(async function test_multi_engine_aggregation_block() {
let client = getRSClient();
let records = await populateMultipleRS(client.db, [
{
id: "trackers",
name: "disconnect-tracker-base",
rules: ["||example.org^"],
},
{
id: "fingerprinters",
name: "disconnect-fingerprinters-base",
rules: ["||example.org^"],
},
]);
await pushEnginePrefs({ protection: "trackers,fingerprinters" });
let tab = await openTestTab();
let browser = tab.linkedBrowser;
await syncAndWaitForLists(client, records);
await assertImageBlocked(
browser,
TEST_BLOCKED_3RD_PARTY_DOMAIN,
"example.org blocked when both features match"
);
await assertHasBlockingState(
browser,
TEST_BLOCKED_3RD_PARTY_DOMAIN,
Ci.nsIWebProgressListener.STATE_BLOCKED_TRACKING_CONTENT,
"trackers wins the cancel (earlier in kFeatures iteration order)"
);
});
// Multi-engine aggregation in the annotate phase: MaybeAnnotateChannel
// iterates every matched feature, so the log should carry both flags.
add_task(async function test_multi_engine_aggregation_annotate() {
let client = getRSClient();
let records = await populateMultipleRS(client.db, [
{
id: "trackers",
name: "disconnect-tracker-base",
rules: ["||example.com^"],
},
{
id: "fingerprinters",
name: "disconnect-fingerprinters-base",
rules: ["||example.com^"],
},
]);
await pushEnginePrefs({ annotation: "trackers,fingerprinters" });
let tab = await openTestTab();
let browser = tab.linkedBrowser;
await syncAndWaitForLists(client, records);
await assertImageLoaded(
browser,
TEST_ANNOTATED_3RD_PARTY_DOMAIN,
"example.com loads (annotation phase only)"
);
await assertHasBlockingState(
browser,
TEST_ANNOTATED_3RD_PARTY_DOMAIN,
Ci.nsIWebProgressListener.STATE_LOADED_LEVEL_1_TRACKING_CONTENT,
"annotate log carries STATE_LOADED_LEVEL_1_TRACKING_CONTENT"
);
await assertHasBlockingState(
browser,
TEST_ANNOTATED_3RD_PARTY_DOMAIN,
Ci.nsIWebProgressListener.STATE_LOADED_FINGERPRINTING_CONTENT,
"annotate log carries STATE_LOADED_FINGERPRINTING_CONTENT"
);
});
// The content blocking allow-list (trackingprotection permission on the
// top-level origin) should suppress cancellation regardless of how many
// features match the third-party resource.
add_task(async function test_allowlist_skips_multifeature_blocking() {
let client = getRSClient();
let records = await populateMultipleRS(client.db, [
{
id: "trackers",
name: "disconnect-tracker-base",
rules: ["||example.org^"],
},
{
id: "fingerprinters",
name: "disconnect-fingerprinters-base",
rules: ["||example.org^"],
},
]);
await pushEnginePrefs({ protection: "trackers,fingerprinters" });
let topLevelOrigin = TEST_DOMAIN.replace(/\/$/, "");
await SpecialPowers.addPermission(
"trackingprotection",
Services.perms.ALLOW_ACTION,
{ url: topLevelOrigin }
);
registerCleanupFunction(() =>
SpecialPowers.removePermission("trackingprotection", {
url: topLevelOrigin,
})
);
let tab = await openTestTab();
let browser = tab.linkedBrowser;
await syncAndWaitForLists(client, records);
await assertImageLoaded(
browser,
TEST_BLOCKED_3RD_PARTY_DOMAIN,
"allowlisted top-level page should not cancel example.org"
);
await assertLacksBlockingState(
browser,
TEST_BLOCKED_3RD_PARTY_DOMAIN,
Ci.nsIWebProgressListener.STATE_BLOCKED_TRACKING_CONTENT,
"no STATE_BLOCKED_TRACKING_CONTENT for allowlisted page"
);
await assertLacksBlockingState(
browser,
TEST_BLOCKED_3RD_PARTY_DOMAIN,
Ci.nsIWebProgressListener.STATE_BLOCKED_FINGERPRINTING_CONTENT,
"no STATE_BLOCKED_FINGERPRINTING_CONTENT for allowlisted page"
);
});
// Exception semantics: when an exception feature carries an
// `@@||example.com^` allowlist rule and a separate blocking feature
// carries `||example.com^`, the aggregated ContentClassifierResult's
// status is promoted to Exception (which ranks above Hit), so
// MaybeCancelChannel's `aResult.Hit()` returns false and the channel
// is never cancelled. The exception feature is listed first in the
// engines pref to also confirm there's no early-out that would skip
// the trailing blocker.
async function runExceptionAllowsBlocker({
exceptionFeature,
exceptionListName,
}) {
let client = getRSClient();
let records = await populateMultipleRS(client.db, [
{ id: "exception", name: exceptionListName, rules: ["@@||example.com^"] },
{
id: "trackers",
name: "disconnect-tracker-base",
rules: ["||example.com^"],
},
]);
await pushEnginePrefs({ protection: `trackers,${exceptionFeature}` });
let tab = await openTestTab();
let browser = tab.linkedBrowser;
await syncAndWaitForLists(client, records);
await assertImageLoaded(
browser,
TEST_ANNOTATED_3RD_PARTY_DOMAIN,
`${exceptionFeature} exception rule allows example.com against trackers`
);
await assertLacksBlockingState(
browser,
TEST_ANNOTATED_3RD_PARTY_DOMAIN,
Ci.nsIWebProgressListener.STATE_BLOCKED_TRACKING_CONTENT,
`${exceptionFeature} allowlist suppresses STATE_BLOCKED_TRACKING_CONTENT`
);
}
add_task(async function test_minor_exceptions_allows_blocker() {
await runExceptionAllowsBlocker({
exceptionFeature: "minor-exceptions",
exceptionListName: "mozilla-minor-exceptions",
});
});
add_task(async function test_major_exceptions_allows_blocker() {
await runExceptionAllowsBlocker({
exceptionFeature: "major-exceptions",
exceptionListName: "mozilla-major-exceptions",
});
});