468 lines
13 KiB
JavaScript
468 lines
13 KiB
JavaScript
/* 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/. */
|
|
|
|
const lazy = {};
|
|
|
|
ChromeUtils.defineESModuleGetters(lazy, {
|
|
RemoteSettings: "resource://services-settings/remote-settings.sys.mjs",
|
|
});
|
|
|
|
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
|
|
|
|
const COLLECTION_NAME = "query-stripping";
|
|
const SHARED_DATA_KEY = "URLQueryStripping";
|
|
const PREF_STRIP_LIST_NAME = "privacy.query_stripping.strip_list";
|
|
const PREF_ALLOW_LIST_NAME = "privacy.query_stripping.allow_list";
|
|
const PREF_TESTING_ENABLED = "privacy.query_stripping.testing";
|
|
const PREF_STRIP_IS_TEST =
|
|
"privacy.query_stripping.strip_on_share.enableTestMode";
|
|
|
|
ChromeUtils.defineLazyGetter(lazy, "logger", () => {
|
|
return console.createInstance({
|
|
prefix: "URLQueryStrippingListService",
|
|
maxLogLevelPref: "privacy.query_stripping.listService.logLevel",
|
|
});
|
|
});
|
|
|
|
XPCOMUtils.defineLazyPreferenceGetter(
|
|
lazy,
|
|
"testStripOnShare",
|
|
PREF_STRIP_IS_TEST
|
|
);
|
|
|
|
async function fetchList(fileName) {
|
|
let response = await fetch(
|
|
"chrome://global/content/antitracking/" + fileName
|
|
);
|
|
if (!response.ok) {
|
|
lazy.logger.error(
|
|
"Error fetching strip-on-share strip list" + response.status
|
|
);
|
|
throw new Error(
|
|
"Error fetching strip-on-share strip list" + response.status
|
|
);
|
|
}
|
|
return response.json();
|
|
}
|
|
|
|
// Lazy getter for the strip-on-share strip list.
|
|
ChromeUtils.defineLazyGetter(lazy, "StripOnShareList", async () => {
|
|
let [stripOnShareList, stripOnShareLGPLParams] = await Promise.all([
|
|
fetchList("StripOnShare.json"),
|
|
fetchList("StripOnShareLGPL.json"),
|
|
]);
|
|
|
|
if (!stripOnShareList || !stripOnShareLGPLParams) {
|
|
lazy.logger.error("Error strip-on-share strip list were not loaded");
|
|
throw new Error("Error fetching strip-on-share strip list were not loaded");
|
|
}
|
|
|
|
// Combines the mozilla licensed strip on share param
|
|
// list and the LGPL licensed strip on share param list
|
|
return combineAndParseLists(stripOnShareList, [stripOnShareLGPLParams]);
|
|
});
|
|
|
|
function combineAndParseLists(mainList, arrOfLists) {
|
|
arrOfLists.forEach(additionalList => {
|
|
for (let key in additionalList) {
|
|
if (Object.hasOwn(mainList, key)) {
|
|
mainList[key].queryParams.push(...additionalList[key].queryParams);
|
|
|
|
mainList[key].topLevelSites.push(...additionalList[key].topLevelSites);
|
|
} else {
|
|
mainList[key] = additionalList[key];
|
|
}
|
|
}
|
|
});
|
|
|
|
for (let key in mainList) {
|
|
mainList[key].queryParams = mainList[key].queryParams.map(param =>
|
|
param.toLowerCase()
|
|
);
|
|
|
|
mainList[key].topLevelSites = mainList[key].topLevelSites.map(param =>
|
|
param.toLowerCase()
|
|
);
|
|
|
|
// Removes duplicates topLevelSites
|
|
mainList[key].topLevelSites = [...new Set(mainList[key].topLevelSites)];
|
|
|
|
// Removes duplicates queryParams
|
|
mainList[key].queryParams = [...new Set(mainList[key].queryParams)];
|
|
}
|
|
|
|
return mainList;
|
|
}
|
|
|
|
export class URLQueryStrippingListService {
|
|
classId = Components.ID("{afff16f0-3fd2-4153-9ccd-c6d9abd879e4}");
|
|
QueryInterface = ChromeUtils.generateQI(["nsIURLQueryStrippingListService"]);
|
|
|
|
#isInitialized = false;
|
|
#pendingInit = null;
|
|
#initResolver;
|
|
#stripOnShareTestList = null;
|
|
|
|
#rs;
|
|
#onSyncCallback;
|
|
|
|
constructor() {
|
|
lazy.logger.debug("constructor");
|
|
this.observers = new Set();
|
|
this.stripOnShareObservers = new Set();
|
|
this.stripOnShareParams = null;
|
|
this.prefStripList = new Set();
|
|
this.prefAllowList = new Set();
|
|
this.remoteStripList = new Set();
|
|
this.remoteAllowList = new Set();
|
|
this.isParentProcess =
|
|
Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_DEFAULT;
|
|
}
|
|
|
|
#onSync(event) {
|
|
lazy.logger.debug("onSync", event);
|
|
let {
|
|
data: { current },
|
|
} = event;
|
|
this._onRemoteSettingsUpdate(current);
|
|
}
|
|
|
|
async testSetList(testList) {
|
|
this.#stripOnShareTestList = combineAndParseLists(testList, []);
|
|
await this._notifyStripOnShareObservers();
|
|
}
|
|
|
|
testHasStripOnShareObservers() {
|
|
return !!this.stripOnShareObservers.size;
|
|
}
|
|
|
|
testHasQPSObservers() {
|
|
return !!this.observers.size;
|
|
}
|
|
|
|
async #init() {
|
|
// If there is already an init pending wait for it to complete.
|
|
if (this.#pendingInit) {
|
|
lazy.logger.debug("#init: Waiting for pending init");
|
|
await this.#pendingInit;
|
|
return;
|
|
}
|
|
|
|
if (this.#isInitialized) {
|
|
lazy.logger.debug("#init: Skip, already initialized");
|
|
return;
|
|
}
|
|
// Create a promise that resolves when init is complete. This allows us to
|
|
// handle incoming init calls while we're still initializing.
|
|
this.#pendingInit = new Promise(initResolve => {
|
|
this.#initResolver = initResolve;
|
|
});
|
|
this.#isInitialized = true;
|
|
|
|
lazy.logger.debug("#init: Run");
|
|
|
|
// We can only access the remote settings in the parent process. For content
|
|
// processes, we will use sharedData to sync the list to content processes.
|
|
if (this.isParentProcess) {
|
|
this.#rs = lazy.RemoteSettings(COLLECTION_NAME);
|
|
|
|
if (!this.#onSyncCallback) {
|
|
this.#onSyncCallback = this.#onSync.bind(this);
|
|
this.#rs.on("sync", this.#onSyncCallback);
|
|
}
|
|
|
|
// Get the initially available entries for remote settings.
|
|
let entries;
|
|
try {
|
|
entries = await this.#rs.get();
|
|
} catch (e) {}
|
|
this._onRemoteSettingsUpdate(entries || []);
|
|
} else {
|
|
// Register the message listener for the remote settings update from the
|
|
// sharedData.
|
|
Services.cpmm.sharedData.addEventListener("change", this);
|
|
|
|
// Get the remote settings data from the shared data.
|
|
let data = this._getListFromSharedData();
|
|
|
|
this._onRemoteSettingsUpdate(data);
|
|
}
|
|
|
|
// Get the list from pref.
|
|
await this._onPrefUpdate(
|
|
PREF_STRIP_LIST_NAME,
|
|
Services.prefs.getStringPref(PREF_STRIP_LIST_NAME, "")
|
|
);
|
|
await this._onPrefUpdate(
|
|
PREF_ALLOW_LIST_NAME,
|
|
Services.prefs.getStringPref(PREF_ALLOW_LIST_NAME, "")
|
|
);
|
|
|
|
Services.prefs.addObserver(PREF_STRIP_LIST_NAME, this);
|
|
Services.prefs.addObserver(PREF_ALLOW_LIST_NAME, this);
|
|
|
|
Services.obs.addObserver(this, "xpcom-shutdown");
|
|
|
|
this.#initResolver();
|
|
this.#pendingInit = null;
|
|
}
|
|
|
|
async #shutdown() {
|
|
// Ensure any pending init is done before shutdown.
|
|
if (this.#pendingInit) {
|
|
await this.#pendingInit;
|
|
}
|
|
|
|
// Already shut down.
|
|
if (!this.#isInitialized) {
|
|
return;
|
|
}
|
|
this.#isInitialized = false;
|
|
|
|
lazy.logger.debug("#shutdown");
|
|
|
|
// Unregister RemoteSettings listener (if it was registered).
|
|
if (this.#onSyncCallback) {
|
|
this.#rs.off("sync", this.#onSyncCallback);
|
|
this.#onSyncCallback = null;
|
|
}
|
|
|
|
Services.obs.removeObserver(this, "xpcom-shutdown");
|
|
Services.prefs.removeObserver(PREF_STRIP_LIST_NAME, this);
|
|
Services.prefs.removeObserver(PREF_ALLOW_LIST_NAME, this);
|
|
}
|
|
|
|
get hasObservers() {
|
|
return !this.observers.size && !this.stripOnShareObservers.size;
|
|
}
|
|
_onRemoteSettingsUpdate(entries) {
|
|
this.remoteStripList.clear();
|
|
this.remoteAllowList.clear();
|
|
|
|
for (let entry of entries) {
|
|
for (let item of entry.stripList) {
|
|
this.remoteStripList.add(item);
|
|
}
|
|
|
|
for (let item of entry.allowList) {
|
|
this.remoteAllowList.add(item);
|
|
}
|
|
}
|
|
|
|
// Because only the parent process will get the remote settings update, so
|
|
// we will sync the list to the shared data so that content processes can
|
|
// get the list.
|
|
if (this.isParentProcess) {
|
|
Services.ppmm.sharedData.set(SHARED_DATA_KEY, {
|
|
stripList: this.remoteStripList,
|
|
allowList: this.remoteAllowList,
|
|
});
|
|
|
|
if (Services.prefs.getBoolPref(PREF_TESTING_ENABLED, false)) {
|
|
Services.ppmm.sharedData.flush();
|
|
}
|
|
}
|
|
|
|
this._notifyObservers();
|
|
}
|
|
|
|
async _onPrefUpdate(pref, value) {
|
|
switch (pref) {
|
|
case PREF_STRIP_LIST_NAME:
|
|
this.prefStripList = new Set(value ? value.split(" ") : []);
|
|
break;
|
|
|
|
case PREF_ALLOW_LIST_NAME:
|
|
this.prefAllowList = new Set(value ? value.split(",") : []);
|
|
break;
|
|
|
|
default:
|
|
console.error(`Unexpected pref name ${pref}`);
|
|
return;
|
|
}
|
|
|
|
this._notifyObservers();
|
|
await this._notifyStripOnShareObservers();
|
|
}
|
|
|
|
_getListFromSharedData() {
|
|
let data = Services.cpmm.sharedData.get(SHARED_DATA_KEY);
|
|
|
|
return data ? [data] : [];
|
|
}
|
|
|
|
_notifyObservers(observer) {
|
|
let stripEntries = new Set([
|
|
...this.prefStripList,
|
|
...this.remoteStripList,
|
|
]);
|
|
let allowEntries = new Set([
|
|
...this.prefAllowList,
|
|
...this.remoteAllowList,
|
|
]);
|
|
let stripEntriesAsString = Array.from(stripEntries).join(" ").toLowerCase();
|
|
let allowEntriesAsString = Array.from(allowEntries).join(",").toLowerCase();
|
|
|
|
let observers = observer ? [observer] : this.observers;
|
|
|
|
if (observer || this.observers.size) {
|
|
lazy.logger.debug("_notifyObservers", {
|
|
observerCount: observers.length,
|
|
runObserverAfterRegister: observer != null,
|
|
stripEntriesAsString,
|
|
allowEntriesAsString,
|
|
});
|
|
}
|
|
|
|
for (let obs of observers) {
|
|
obs.onQueryStrippingListUpdate(
|
|
stripEntriesAsString,
|
|
allowEntriesAsString
|
|
);
|
|
}
|
|
}
|
|
|
|
async _notifyStripOnShareObservers(observer) {
|
|
this.stripOnShareParams = await lazy.StripOnShareList;
|
|
|
|
// Changing to different test list allows us to test
|
|
// site specific params as the websites that current have
|
|
// site specific params cannot be opened in a test env
|
|
if (lazy.testStripOnShare) {
|
|
this.stripOnShareParams = this.#stripOnShareTestList;
|
|
}
|
|
|
|
if (!this.stripOnShareParams) {
|
|
lazy.logger.error("StripOnShare list is undefined");
|
|
return;
|
|
}
|
|
|
|
// Add the qps params to the global rules of the strip-on-share list.
|
|
let qpsParams = [...this.prefStripList, ...this.remoteStripList].map(
|
|
param => param.toLowerCase()
|
|
);
|
|
|
|
this.stripOnShareParams.global.queryParams.push(...qpsParams);
|
|
// Getting rid of duplicates.
|
|
this.stripOnShareParams.global.queryParams = [
|
|
...new Set(this.stripOnShareParams.global.queryParams),
|
|
];
|
|
|
|
// Build an array of StripOnShareRules.
|
|
let rules = Object.values(this.stripOnShareParams);
|
|
let stringifiedRules = [];
|
|
// We need to stringify the rules so later we can initialise WebIDL dictionaries from them.
|
|
// The dictionaries init call needs stringified json.
|
|
rules.forEach(rule => {
|
|
stringifiedRules.push(JSON.stringify(rule));
|
|
});
|
|
|
|
let observers = observer ? new Set([observer]) : this.stripOnShareObservers;
|
|
|
|
if (observers.size) {
|
|
lazy.logger.debug("_notifyStripOnShareObservers", {
|
|
observerCount: observers.size,
|
|
runObserverAfterRegister: observer != null,
|
|
stringifiedRules,
|
|
});
|
|
}
|
|
for (let obs of observers) {
|
|
obs.onStripOnShareUpdate(stringifiedRules);
|
|
}
|
|
}
|
|
|
|
async registerAndRunObserver(observer) {
|
|
lazy.logger.debug("registerAndRunObserver", {
|
|
isInitialized: this.#isInitialized,
|
|
pendingInit: this.#pendingInit,
|
|
});
|
|
|
|
await this.#init();
|
|
this.observers.add(observer);
|
|
this._notifyObservers(observer);
|
|
}
|
|
|
|
async registerAndRunObserverStripOnShare(observer) {
|
|
lazy.logger.debug("registerAndRunObserverStripOnShare", {
|
|
isInitialized: this.#isInitialized,
|
|
pendingInit: this.#pendingInit,
|
|
});
|
|
|
|
await this.#init();
|
|
this.stripOnShareObservers.add(observer);
|
|
await this._notifyStripOnShareObservers(observer);
|
|
}
|
|
|
|
async unregisterObserver(observer) {
|
|
this.observers.delete(observer);
|
|
|
|
if (this.hasObservers) {
|
|
lazy.logger.debug("Last observer unregistered, shutting down...");
|
|
await this.#shutdown();
|
|
}
|
|
}
|
|
|
|
async unregisterStripOnShareObserver(observer) {
|
|
this.stripOnShareObservers.delete(observer);
|
|
|
|
if (this.hasObservers) {
|
|
lazy.logger.debug("Last observer unregistered, shutting down...");
|
|
await this.#shutdown();
|
|
}
|
|
}
|
|
|
|
async clearLists() {
|
|
if (!this.isParentProcess) {
|
|
return;
|
|
}
|
|
|
|
// Ensure init.
|
|
await this.#init();
|
|
|
|
// Clear the lists of remote settings.
|
|
this._onRemoteSettingsUpdate([]);
|
|
|
|
// Clear the user pref for the strip list. The pref change observer will
|
|
// handle the rest of the work.
|
|
Services.prefs.clearUserPref(PREF_STRIP_LIST_NAME);
|
|
Services.prefs.clearUserPref(PREF_ALLOW_LIST_NAME);
|
|
}
|
|
|
|
observe(subject, topic, data) {
|
|
lazy.logger.debug("observe", { topic, data });
|
|
switch (topic) {
|
|
case "xpcom-shutdown":
|
|
this.#shutdown();
|
|
break;
|
|
case "nsPref:changed":
|
|
let prefValue = Services.prefs.getStringPref(data, "");
|
|
this._onPrefUpdate(data, prefValue);
|
|
break;
|
|
default:
|
|
console.error(`Unexpected event ${topic}`);
|
|
}
|
|
}
|
|
|
|
handleEvent(event) {
|
|
if (event.type != "change") {
|
|
return;
|
|
}
|
|
|
|
if (!event.changedKeys.includes(SHARED_DATA_KEY)) {
|
|
return;
|
|
}
|
|
|
|
let data = this._getListFromSharedData();
|
|
this._onRemoteSettingsUpdate(data);
|
|
this._notifyObservers();
|
|
}
|
|
|
|
async testWaitForInit() {
|
|
if (this.#pendingInit) {
|
|
await this.#pendingInit;
|
|
}
|
|
|
|
return this.#isInitialized;
|
|
}
|
|
}
|