Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 67 additions & 48 deletions src/js/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
*/

import { extractHostFromURL, getBaseDomain } from "../lib/basedomain.js";
import { Trie } from "../lib/trie.js";

import { log } from "./bootstrap.js";
import constants from "./constants.js";
Expand Down Expand Up @@ -106,6 +107,8 @@ function Badger(from_qunit) {
async function onStorageReady() {
log("Storage is ready");

self.initDisabledSitesTrie();

self.heuristicBlocking = new HeuristicBlocking.HeuristicBlocker(self.storage);

self.setPrivacyOverrides();
Expand Down Expand Up @@ -176,6 +179,11 @@ Badger.prototype = {
*/
cnameDomains: {},

/**
* Trie for looking up whether PB is disabled for a site.
*/
disabledSitesTrie: null,

// Methods

/**
Expand Down Expand Up @@ -1013,6 +1021,27 @@ Badger.prototype = {
return this.storage.getStore('private_storage');
},

/**
* (Re)builds the disabled sites trie.
*/
initDisabledSitesTrie: function () {
let self = this;

self.disabledSitesTrie = new Trie();

for (let pattern of self.getSettings().getItem("disabledSites")) {
// domains now always match subdomains
// TODO clean up user data and remove wildcard handling
if (pattern.startsWith('*')) {
pattern = pattern.slice(1);
if (pattern.startsWith('.')) {
pattern = pattern.slice(1);
}
}
self.disabledSitesTrie.insert(pattern);
}
},

/**
* Returns whether Privacy Badger is enabled on a given hostname.
*
Expand All @@ -1021,23 +1050,51 @@ Badger.prototype = {
* @returns {Boolean}
*/
isPrivacyBadgerEnabled: function (host) {
let sitePatterns = this.getSettings().getItem("disabledSites") || [];
return !this.disabledSitesTrie.globDomainMatches(host);
},

/**
* Adds a domain to the list of disabled sites.
*
* @param {String} domain The site domain to disable PB for
*/
disableOnSite: function (domain) {
let self = this,
settings = self.getSettings(),
disabledSites = settings.getItem('disabledSites');

if (!disabledSites.includes(domain)) {
disabledSites.push(domain);
settings.setItem("disabledSites", disabledSites);

for (let pattern of sitePatterns) {
// domains now always match subdomains
// TODO clean up user data and remove wildcard handling
if (pattern.startsWith('*')) {
pattern = pattern.slice(1);
if (pattern.startsWith('.')) {
pattern = pattern.slice(1);
if (domain.startsWith('*')) {
domain = domain.slice(1);
if (domain.startsWith('.')) {
domain = domain.slice(1);
}
}
if (pattern === host || host.endsWith('.' + pattern)) {
return false;
}
self.disabledSitesTrie.insert(domain);
}
},

return true;
/**
* Removes a domain from the list of disabled sites.
*
* @param {String} domain The site domain to re-enable PB on
*/
reenableOnSite: function (domain) {
let self = this,
settings = self.getSettings(),
disabledSites = settings.getItem("disabledSites"),
idx = disabledSites.indexOf(domain);

if (idx >= 0) {
disabledSites.splice(idx, 1);
settings.setItem("disabledSites", disabledSites);
self.initDisabledSitesTrie();
}
},

/**
Expand Down Expand Up @@ -1075,44 +1132,6 @@ Badger.prototype = {
return this.getSettings().getItem("checkForDNTPolicy");
},

/**
* Adds a domain to the list of disabled sites.
*
* @param {String} domain The site domain to disable PB for
*/
disableOnSite: function (domain) {
let settings = this.getSettings();
let disabledSites = settings.getItem('disabledSites');
if (disabledSites.indexOf(domain) < 0) {
disabledSites.push(domain);
settings.setItem("disabledSites", disabledSites);
}
},

/**
* Returns the current list of disabled sites.
*
* @returns {Array} site domains where Privacy Badger is disabled
*/
getDisabledSites: function () {
return this.getSettings().getItem("disabledSites");
},

/**
* Removes a domain from the list of disabled sites.
*
* @param {String} domain The site domain to re-enable PB on
*/
reenableOnSite: function (domain) {
let settings = this.getSettings();
let disabledSites = settings.getItem("disabledSites");
let idx = disabledSites.indexOf(domain);
if (idx >= 0) {
disabledSites.splice(idx, 1);
settings.setItem("disabledSites", disabledSites);
}
},

/**
* Checks if local storage ( in dict) has any high-entropy keys
*
Expand Down
9 changes: 6 additions & 3 deletions src/js/storage.js
Original file line number Diff line number Diff line change
Expand Up @@ -788,6 +788,9 @@ BadgerStorage.prototype = {
// combine array settings via intersection/union
if (prop == "disabledSites" || prop == "widgetReplacementExceptions") {
self.setItem(prop, utils.concatUniq(self.getItem(prop), mapData[prop]));
if (prop == "disabledSites") {
badger.initDisabledSitesTrie();
}

// string/array map
} else if (prop == "widgetSiteAllowlist") {
Expand Down Expand Up @@ -971,9 +974,9 @@ let _syncStorage = (function () {
if (!callback) {
callback = function () {};
}
let obj = {};
obj[badgerStore.name] = badgerStore._store;
chrome.storage.local.set(obj, function () {
let data = {};
data[badgerStore.name] = badgerStore._store;
chrome.storage.local.set(data, function () {
if (!chrome.runtime.lastError) {
callback(null);
return;
Expand Down
22 changes: 11 additions & 11 deletions src/js/webrequest.js
Original file line number Diff line number Diff line change
Expand Up @@ -1663,13 +1663,13 @@ function dispatcher(request, sender, sendResponse) {
case "downloadCloud": {
chrome.storage.sync.get("disabledSites", function (store) {
if (chrome.runtime.lastError) {
sendResponse({success: false, message: chrome.runtime.lastError.message});
sendResponse({ success: false, message: chrome.runtime.lastError.message });
} else if (utils.hasOwn(store, "disabledSites")) {
let disabledSites = utils.concatUniq(
badger.getDisabledSites(),
store.disabledSites
);
badger.getSettings().getItem("disabledSites"),
store.disabledSites);
badger.getSettings().setItem("disabledSites", disabledSites);
badger.initDisabledSitesTrie();
sendResponse({
success: true,
disabledSites
Expand All @@ -1687,13 +1687,13 @@ function dispatcher(request, sender, sendResponse) {
}

case "uploadCloud": {
let obj = {};
obj.disabledSites = badger.getDisabledSites();
chrome.storage.sync.set(obj, function () {
let data = {};
data.disabledSites = badger.getSettings().getItem("disabledSites");
chrome.storage.sync.set(data, function () {
if (chrome.runtime.lastError) {
sendResponse({success: false, message: chrome.runtime.lastError.message});
sendResponse({ success: false, message: chrome.runtime.lastError.message });
} else {
sendResponse({success: true});
sendResponse({ success: true });
}
});
// indicate this is an async response to chrome.runtime.onMessage
Expand Down Expand Up @@ -1767,7 +1767,7 @@ function dispatcher(request, sender, sendResponse) {
case "disableOnSite": {
badger.disableOnSite(request.domain);
sendResponse({
disabledSites: badger.getDisabledSites()
disabledSites: badger.getSettings().getItem("disabledSites")
});
break;
}
Expand All @@ -1777,7 +1777,7 @@ function dispatcher(request, sender, sendResponse) {
badger.reenableOnSite(domain);
});
sendResponse({
disabledSites: badger.getDisabledSites()
disabledSites: badger.getSettings().getItem("disabledSites")
});
break;
}
Expand Down
154 changes: 154 additions & 0 deletions src/lib/trie.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
/*
* This file is part of Privacy Badger <https://privacybadger.org/>
* Copyright (C) 2025 Electronic Frontier Foundation
*
* Privacy Badger is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* Privacy Badger is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Privacy Badger. If not, see <http://www.gnu.org/licenses/>.
*/

import utils from "../js/utils.js";

/**
* Trie node constructor.
*
* @param {String?} key
*/
function TrieNode(key) {
this.key = key;
this.parentNode = null;
this.stringEndsHere = false;
this.children = {};
}

/**
* Iterates through parent nodes to reconstruct the dot-separated string.
*/
TrieNode.prototype.getString = function () {
let output = [],
node = this; // eslint-disable-line consistent-this

// stop at the root node
while (node.key !== null) {
output.push(node.key);
node = node.parentNode;
}

return output.join('.');
};

/**
* Recursively populates `arr` with full strings
* belonging to children of a given TrieNode.
*
* @param {TrieNode} node
* @param {Array} arr
*/
function findAll(node, arr) {
if (node.stringEndsHere) {
arr.push(node.getString());
}

for (let key of Object.keys(node.children)) {
findAll(node.children[key], arr);
}
}

/**
* Domain trie constructor.
*/
function Trie() {
this.root = new TrieNode(null);
}

/*
* Inserts a dot-separated domain string into the trie.
*
* @param {String} domain
*/
Trie.prototype.insert = function (domain) {
let node = this.root,
parts = domain.split('.');

for (let i = parts.length-1; i >= 0; i--) {
let key = parts[i];

if (!utils.hasOwn(node.children, key)) {
node.children[key] = new TrieNode(key);
node.children[key].parentNode = node;
}

node = node.children[key];

if (i == 0) {
node.stringEndsHere = true;
}
}
};

/**
* Returns any subdomains and the domain itself, if inserted directly.
*
* Note that the order of strings is undefined.
*
* @param {String} domain
*
* @returns {Array}
*/
Trie.prototype.getDomains = function (domain) {
let domains = [],
node = this.root,
parts = domain.split('.');

// first navigate to the deepest TrieNode for domain
for (let i = parts.length-1; i >= 0; i--) {
let key = parts[i];
if (!utils.hasOwn(node.children, key)) {
// abort if domain isn't fully in the Trie
return domains;
}
node = node.children[key];
}

findAll(node, domains);

return domains;
};

/**
* Returns whether the domain string, or any of its
* parent domain strings were inserted into the trie.
*
* @param {String} domain
*
* @returns {Boolean}
*/
Trie.prototype.globDomainMatches = function (domain) {
let parts = domain.split('.'),
node = this.root;

for (let i = parts.length-1; i >= 0; i--) {
let key = parts[i];
if (!utils.hasOwn(node.children, key)) {
return false;
}
node = node.children[key];
if (node.stringEndsHere) {
return true;
}
}

return false;
};

export {
Trie
};
Loading