/* 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 fetchAndParseList(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 await response.json(); } // Lazy getter for the strip-on-share strip list. ChromeUtils.defineLazyGetter(lazy, "StripOnShareList", async () => { let [stripOnShareList, stripOnShareLGPLParams] = await Promise.all([ fetchAndParseList("StripOnShare.json"), fetchAndParseList("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 for (const key in stripOnShareLGPLParams) { if (Object.hasOwn(stripOnShareList, key)) { stripOnShareList[key].queryParams.push( ...stripOnShareLGPLParams[key].queryParams ); stripOnShareList[key].topLevelSites.push( ...stripOnShareLGPLParams[key].topLevelSites ); // Removes duplicates topLevelSitres stripOnShareList[key].topLevelSites = [ ...new Set(stripOnShareList[key].topLevelSites), ]; // Removes duplicates queryParams stripOnShareList[key].queryParams = [ ...new Set(stripOnShareList[key].queryParams), ]; } else { stripOnShareList[key] = stripOnShareLGPLParams[key]; } } return stripOnShareList; }); 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 = 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; } }