Files
sousa-gecko/netwerk/url-classifier/UrlClassifierExceptionListService.sys.mjs
T

360 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
export function UrlClassifierExceptionListService() {}
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
RemoteSettings: "resource://services-settings/remote-settings.sys.mjs",
});
const COLLECTION_NAME = "url-classifier-exceptions";
/**
* Tracks URL classifier exception list entries for a single named feature.
*/
class Feature {
constructor(name, prefName) {
this.name = name;
this.prefName = prefName;
this.observers = new Set();
this.prefValue = null;
this.remoteEntries = null;
if (prefName) {
this.prefValue = Services.prefs.getStringPref(this.prefName, null);
Services.prefs.addObserver(prefName, this);
}
}
async addAndRunObserver(observer) {
this.observers.add(observer);
this.notifyObservers(observer);
}
removeObserver(observer) {
this.observers.delete(observer);
}
observe(subject, topic, data) {
if (topic != "nsPref:changed" || data != this.prefName) {
console.error(`Unexpected event ${topic} with ${data}`);
return;
}
this.prefValue = Services.prefs.getStringPref(this.prefName, null);
this.notifyObservers();
}
onRemoteSettingsUpdate(entries) {
this.remoteEntries = [];
for (let jsEntry of entries) {
let { classifierFeatures } = jsEntry;
if (classifierFeatures.includes(this.name)) {
let entry = Feature.rsObjectToEntry(jsEntry);
if (entry) {
this.remoteEntries.push(entry);
}
}
}
}
/**
* Convert a JS object from RemoteSettings to an nsIUrlClassifierExceptionListEntry.
*
* @param {object} rsObject - The JS object from RemoteSettings to convert.
* @returns {nsIUrlClassifierExceptionListEntry} The converted nsIUrlClassifierExceptionListEntry.
*/
static rsObjectToEntry(rsObject) {
let entry = Cc[
"@mozilla.org/url-classifier/exception-list-entry;1"
].createInstance(Ci.nsIUrlClassifierExceptionListEntry);
let {
category: categoryStr,
urlPattern,
topLevelUrlPattern = "",
isPrivateBrowsingOnly = false,
filterContentBlockingCategories = [],
classifierFeatures = [],
} = rsObject;
const CATEGORY_STR_TO_ENUM = {
"internal-pref":
Ci.nsIUrlClassifierExceptionListEntry.CATEGORY_INTERNAL_PREF,
baseline: Ci.nsIUrlClassifierExceptionListEntry.CATEGORY_BASELINE,
convenience: Ci.nsIUrlClassifierExceptionListEntry.CATEGORY_CONVENIENCE,
};
let category = CATEGORY_STR_TO_ENUM[categoryStr];
if (category == null) {
console.error(
"Invalid or unknown category",
{ rsObject },
{ categories: Object.keys(CATEGORY_STR_TO_ENUM) }
);
return null;
}
try {
entry.init(
category,
urlPattern,
topLevelUrlPattern,
isPrivateBrowsingOnly,
filterContentBlockingCategories,
classifierFeatures
);
} catch (e) {
console.error(
"Error initializing url classifier exception list entry " + e.message,
e,
{ rsObject }
);
return null;
}
return entry;
}
notifyObservers(observer = null) {
let entries = [];
if (this.prefValue) {
for (let prefEntry of this.prefValue.split(",")) {
let entry = Feature.rsObjectToEntry({
category: "internal-pref",
urlPattern: prefEntry,
classifierFeatures: [this.name],
});
if (entry) {
entries.push(entry);
}
}
}
if (this.remoteEntries) {
for (let entry of this.remoteEntries) {
entries.push(entry);
}
}
// Construct nsIUrlClassifierExceptionList with all entries that belong to
// this feature.
let list = Cc[
"@mozilla.org/url-classifier/exception-list;1"
].createInstance(Ci.nsIUrlClassifierExceptionList);
for (let entry of entries) {
try {
list.addEntry(entry);
} catch (e) {
console.error(
"Error adding url classifier exception list entry " + e.message,
e,
entry
);
}
}
if (observer) {
observer.onExceptionListUpdate(list);
} else {
for (let obs of this.observers) {
obs.onExceptionListUpdate(list);
}
}
}
}
UrlClassifierExceptionListService.prototype = {
classID: Components.ID("{b9f4fd03-9d87-4bfd-9958-85a821750ddc}"),
QueryInterface: ChromeUtils.generateQI([
"nsIUrlClassifierExceptionListService",
"nsIObserver",
]),
features: {},
_initialized: false,
ETP_PREFERENCES: [
"privacy.trackingprotection.allow_list.baseline.enabled",
"privacy.trackingprotection.allow_list.convenience.enabled",
"browser.contentblocking.category",
],
PREF_ALLOW_LIST_USER_INTERACTED:
"privacy.trackingprotection.allow_list.hasUserInteractedWithETPSettings",
observe(subject, topic, data) {
if (topic === "idle-daily") {
const baseline = Services.prefs.getBoolPref(
"privacy.trackingprotection.allow_list.baseline.enabled"
);
const convenience = Services.prefs.getBoolPref(
"privacy.trackingprotection.allow_list.convenience.enabled"
);
Glean.contentblocking.tpAllowlistBaselineEnabled.set(baseline);
// If baseline is false, having convenience as true has no effect, so we treat it as false.
Glean.contentblocking.tpAllowlistConvenienceEnabled.set(
baseline ? convenience : false
);
}
if (topic === "nsPref:changed") {
// If the user changes the baseline, convenience, or category preference, we set
// hasUserInteractedWithETP to true to indicate interaction with ETP settings.
// This lets us skip the infobar prompting users to enable allowlists if they’ve
// already made a choice.
if (this.ETP_PREFERENCES.includes(data)) {
Services.prefs.setBoolPref(this.PREF_ALLOW_LIST_USER_INTERACTED, true);
}
}
},
async lazyInit() {
if (this._initialized) {
return;
}
this.maybeMigrateCategoryPrefs();
// Add ETP preference observers AFTER migration to avoid false positives. The migration function
// above may programmatically change ETP preferences, which would incorrectly trigger our user
// interaction tracking if observers were already installed. By adding observers after
// migration, we ensure we only detect user changes to ETP settings.
this.addETPUserInteractionPrefObservers();
let rs = lazy.RemoteSettings(COLLECTION_NAME);
rs.on("sync", event => {
let {
data: { current },
} = event;
this.entries = current || [];
this.onUpdateEntries(current);
});
this._initialized = true;
// If the remote settings list hasn't been populated yet we have to make sure
// to do it before firing the first notification.
// This has to be run after _initialized is set because we'll be
// blocked while getting entries from RemoteSetting, and we don't want
// LazyInit is executed again.
try {
// The data will be initially available from the local DB (via a
// resource:// URI).
this.entries = await rs.get();
} catch (e) {}
// RemoteSettings.get() could return null, ensure passing a list to
// onUpdateEntries.
if (!this.entries) {
this.entries = [];
}
this.onUpdateEntries(this.entries);
},
/**
* Runs migration code for the allow-list category prefs.
* Users who have ETP "strict" or "custom" enabled should not automatically
* get enrolled into the new allow-list categories. Instead they should have
* the opportunity to opt in/out via the preferences UI.
*/
maybeMigrateCategoryPrefs() {
const ALLOW_LIST_CATEGORY_MIGRATION_PREF =
"privacy.trackingprotection.allow_list.hasMigratedCategoryPrefs";
if (Services.prefs.getBoolPref(ALLOW_LIST_CATEGORY_MIGRATION_PREF, false)) {
// Already migrated.
return;
}
// Set the migration pref to true so we only run the migration once.
Services.prefs.setBoolPref(ALLOW_LIST_CATEGORY_MIGRATION_PREF, true);
// This pref is set on both Desktop and Fenix (Bug 1956620).
let cbCategory = Services.prefs.getStringPref(
"browser.contentblocking.category",
"standard"
);
// Don't migrate if the user is using the default category. The default
// category pref states are already correct.
if (cbCategory == "standard") {
return;
}
// cbCategory is either "strict" or "custom". Disable both allow list
// categories.
Services.prefs.setBoolPref(
"privacy.trackingprotection.allow_list.baseline.enabled",
false
);
Services.prefs.setBoolPref(
"privacy.trackingprotection.allow_list.convenience.enabled",
false
);
},
onUpdateEntries(entries) {
for (let key of Object.keys(this.features)) {
let feature = this.features[key];
feature.onRemoteSettingsUpdate(entries);
feature.notifyObservers();
}
},
registerAndRunExceptionListObserver(feature, prefName, observer) {
// We don't await this; the caller is C++ and won't await this function,
// and because we prevent re-entering into this method, once it's been
// called once any subsequent calls will early-return anyway - so
// awaiting that would be meaningless. Instead, `Feature` implementations
// make sure not to call into observers until they have data, and we
// make sure to let feature instances know whether we have data
// immediately.
this.lazyInit();
if (!this.features[feature]) {
let featureObj = new Feature(feature, prefName);
this.features[feature] = featureObj;
// If we've previously initialized, we need to pass the entries
// we already have to the new feature.
if (this.entries) {
featureObj.onRemoteSettingsUpdate(this.entries);
}
}
this.features[feature].addAndRunObserver(observer);
},
unregisterExceptionListObserver(feature, observer) {
if (!this.features[feature]) {
return;
}
this.features[feature].removeObserver(observer);
},
/**
* Adds preference observers to track user interactions with ETP settings.
* These observers monitor changes to the baseline allow list, convenience allow list, and
* content blocking category preferences to detect when users modify ETP-related settings.
*/
addETPUserInteractionPrefObservers() {
this.ETP_PREFERENCES.forEach(pref => {
Services.prefs.addObserver(pref, this.observe.bind(this));
});
},
removeETPUserInteractionPrefObservers() {
this.ETP_PREFERENCES.forEach(pref => {
Services.prefs.removeObserver(pref, this.observe.bind(this));
});
},
clear() {
this.features = {};
this._initialized = false;
this.entries = null;
this.removeETPUserInteractionPrefObservers();
},
};