Skip to content

Commit d163df7

Browse files
committed
Optimize checking if PB is disabled for a site
1 parent 18b5a7b commit d163df7

File tree

6 files changed

+314
-52
lines changed

6 files changed

+314
-52
lines changed

src/js/background.js

Lines changed: 67 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
*/
1717

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

2021
import { log } from "./bootstrap.js";
2122
import constants from "./constants.js";
@@ -106,6 +107,8 @@ function Badger(from_qunit) {
106107
async function onStorageReady() {
107108
log("Storage is ready");
108109

110+
self.initDisabledSitesTrie();
111+
109112
self.heuristicBlocking = new HeuristicBlocking.HeuristicBlocker(self.storage);
110113

111114
self.setPrivacyOverrides();
@@ -176,6 +179,11 @@ Badger.prototype = {
176179
*/
177180
cnameDomains: {},
178181

182+
/**
183+
* Trie for looking up whether PB is disabled for a site.
184+
*/
185+
disabledSitesTrie: null,
186+
179187
// Methods
180188

181189
/**
@@ -1013,6 +1021,27 @@ Badger.prototype = {
10131021
return this.storage.getStore('private_storage');
10141022
},
10151023

1024+
/**
1025+
* (Re)builds the disabled sites trie.
1026+
*/
1027+
initDisabledSitesTrie: function () {
1028+
let self = this;
1029+
1030+
self.disabledSitesTrie = new Trie();
1031+
1032+
for (let pattern of self.getSettings().getItem("disabledSites")) {
1033+
// domains now always match subdomains
1034+
// TODO clean up user data and remove wildcard handling
1035+
if (pattern.startsWith('*')) {
1036+
pattern = pattern.slice(1);
1037+
if (pattern.startsWith('.')) {
1038+
pattern = pattern.slice(1);
1039+
}
1040+
}
1041+
self.disabledSitesTrie.insert(pattern);
1042+
}
1043+
},
1044+
10161045
/**
10171046
* Returns whether Privacy Badger is enabled on a given hostname.
10181047
*
@@ -1021,23 +1050,51 @@ Badger.prototype = {
10211050
* @returns {Boolean}
10221051
*/
10231052
isPrivacyBadgerEnabled: function (host) {
1024-
let sitePatterns = this.getSettings().getItem("disabledSites") || [];
1053+
return !this.disabledSitesTrie.globDomainMatches(host);
1054+
},
1055+
1056+
/**
1057+
* Adds a domain to the list of disabled sites.
1058+
*
1059+
* @param {String} domain The site domain to disable PB for
1060+
*/
1061+
disableOnSite: function (domain) {
1062+
let self = this,
1063+
settings = self.getSettings(),
1064+
disabledSites = settings.getItem('disabledSites');
1065+
1066+
if (!disabledSites.includes(domain)) {
1067+
disabledSites.push(domain);
1068+
settings.setItem("disabledSites", disabledSites);
10251069

1026-
for (let pattern of sitePatterns) {
10271070
// domains now always match subdomains
10281071
// TODO clean up user data and remove wildcard handling
1029-
if (pattern.startsWith('*')) {
1030-
pattern = pattern.slice(1);
1031-
if (pattern.startsWith('.')) {
1032-
pattern = pattern.slice(1);
1072+
if (domain.startsWith('*')) {
1073+
domain = domain.slice(1);
1074+
if (domain.startsWith('.')) {
1075+
domain = domain.slice(1);
10331076
}
10341077
}
1035-
if (pattern === host || host.endsWith('.' + pattern)) {
1036-
return false;
1037-
}
1078+
self.disabledSitesTrie.insert(domain);
10381079
}
1080+
},
10391081

1040-
return true;
1082+
/**
1083+
* Removes a domain from the list of disabled sites.
1084+
*
1085+
* @param {String} domain The site domain to re-enable PB on
1086+
*/
1087+
reenableOnSite: function (domain) {
1088+
let self = this,
1089+
settings = self.getSettings(),
1090+
disabledSites = settings.getItem("disabledSites"),
1091+
idx = disabledSites.indexOf(domain);
1092+
1093+
if (idx >= 0) {
1094+
disabledSites.splice(idx, 1);
1095+
settings.setItem("disabledSites", disabledSites);
1096+
self.initDisabledSitesTrie();
1097+
}
10411098
},
10421099

10431100
/**
@@ -1075,44 +1132,6 @@ Badger.prototype = {
10751132
return this.getSettings().getItem("checkForDNTPolicy");
10761133
},
10771134

1078-
/**
1079-
* Adds a domain to the list of disabled sites.
1080-
*
1081-
* @param {String} domain The site domain to disable PB for
1082-
*/
1083-
disableOnSite: function (domain) {
1084-
let settings = this.getSettings();
1085-
let disabledSites = settings.getItem('disabledSites');
1086-
if (disabledSites.indexOf(domain) < 0) {
1087-
disabledSites.push(domain);
1088-
settings.setItem("disabledSites", disabledSites);
1089-
}
1090-
},
1091-
1092-
/**
1093-
* Returns the current list of disabled sites.
1094-
*
1095-
* @returns {Array} site domains where Privacy Badger is disabled
1096-
*/
1097-
getDisabledSites: function () {
1098-
return this.getSettings().getItem("disabledSites");
1099-
},
1100-
1101-
/**
1102-
* Removes a domain from the list of disabled sites.
1103-
*
1104-
* @param {String} domain The site domain to re-enable PB on
1105-
*/
1106-
reenableOnSite: function (domain) {
1107-
let settings = this.getSettings();
1108-
let disabledSites = settings.getItem("disabledSites");
1109-
let idx = disabledSites.indexOf(domain);
1110-
if (idx >= 0) {
1111-
disabledSites.splice(idx, 1);
1112-
settings.setItem("disabledSites", disabledSites);
1113-
}
1114-
},
1115-
11161135
/**
11171136
* Checks if local storage ( in dict) has any high-entropy keys
11181137
*

src/js/webrequest.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1666,7 +1666,7 @@ function dispatcher(request, sender, sendResponse) {
16661666
sendResponse({success: false, message: chrome.runtime.lastError.message});
16671667
} else if (utils.hasOwn(store, "disabledSites")) {
16681668
let disabledSites = utils.concatUniq(
1669-
badger.getDisabledSites(),
1669+
badger.getSettings().getItem("disabledSites"),
16701670
store.disabledSites
16711671
);
16721672
badger.getSettings().setItem("disabledSites", disabledSites);
@@ -1688,7 +1688,7 @@ function dispatcher(request, sender, sendResponse) {
16881688

16891689
case "uploadCloud": {
16901690
let obj = {};
1691-
obj.disabledSites = badger.getDisabledSites();
1691+
obj.disabledSites = badger.getSettings().getItem("disabledSites");
16921692
chrome.storage.sync.set(obj, function () {
16931693
if (chrome.runtime.lastError) {
16941694
sendResponse({success: false, message: chrome.runtime.lastError.message});
@@ -1767,7 +1767,7 @@ function dispatcher(request, sender, sendResponse) {
17671767
case "disableOnSite": {
17681768
badger.disableOnSite(request.domain);
17691769
sendResponse({
1770-
disabledSites: badger.getDisabledSites()
1770+
disabledSites: badger.getSettings().getItem("disabledSites")
17711771
});
17721772
break;
17731773
}
@@ -1777,7 +1777,7 @@ function dispatcher(request, sender, sendResponse) {
17771777
badger.reenableOnSite(domain);
17781778
});
17791779
sendResponse({
1780-
disabledSites: badger.getDisabledSites()
1780+
disabledSites: badger.getSettings().getItem("disabledSites")
17811781
});
17821782
break;
17831783
}

src/lib/trie.js

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
/*
2+
* This file is part of Privacy Badger <https://privacybadger.org/>
3+
* Copyright (C) 2025 Electronic Frontier Foundation
4+
*
5+
* Privacy Badger is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU General Public License version 3 as
7+
* published by the Free Software Foundation.
8+
*
9+
* Privacy Badger is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU General Public License
15+
* along with Privacy Badger. If not, see <http://www.gnu.org/licenses/>.
16+
*/
17+
18+
import utils from "../js/utils.js";
19+
20+
/**
21+
* Trie node constructor.
22+
*
23+
* @param {String?} key
24+
*/
25+
function TrieNode(key) {
26+
this.key = key;
27+
this.parentNode = null;
28+
this.stringEndsHere = false;
29+
this.children = {};
30+
}
31+
32+
/**
33+
* Iterates through parent nodes to reconstruct the dot-separated string.
34+
*/
35+
TrieNode.prototype.getString = function () {
36+
let output = [],
37+
node = this; // eslint-disable-line consistent-this
38+
39+
for (;;) {
40+
// stop at the root node
41+
if (node.key === null) {
42+
break;
43+
}
44+
output.push(node.key);
45+
node = node.parentNode;
46+
}
47+
48+
return output.join('.');
49+
};
50+
51+
/**
52+
* Recursively populates `arr` with full strings
53+
* belonging to children of a given TrieNode.
54+
*
55+
* @param {TrieNode} node
56+
* @param {Array} arr
57+
*/
58+
function findAll(node, arr) {
59+
if (node.stringEndsHere) {
60+
arr.push(node.getString());
61+
}
62+
63+
for (let key of Object.keys(node.children)) {
64+
findAll(node.children[key], arr);
65+
}
66+
}
67+
68+
/**
69+
* Domain trie constructor.
70+
*/
71+
function Trie() {
72+
this.root = new TrieNode(null);
73+
}
74+
75+
/*
76+
* Inserts a dot-separated domain string into the trie.
77+
*
78+
* @param {String} domain
79+
*/
80+
Trie.prototype.insert = function (domain) {
81+
let node = this.root,
82+
parts = domain.split('.');
83+
84+
for (let i = parts.length-1; i >= 0; i--) {
85+
let key = parts[i];
86+
87+
if (!utils.hasOwn(node.children, key)) {
88+
node.children[key] = new TrieNode(key);
89+
node.children[key].parentNode = node;
90+
}
91+
92+
node = node.children[key];
93+
94+
if (i == 0) {
95+
node.stringEndsHere = true;
96+
}
97+
}
98+
};
99+
100+
/**
101+
* Returns any subdomains and the domain itself, if inserted directly.
102+
*
103+
* Note that the order of strings is undefined.
104+
*
105+
* @param {String} domain
106+
*
107+
* @returns {Array}
108+
*/
109+
Trie.prototype.getDomains = function (domain) {
110+
let domains = [],
111+
node = this.root,
112+
parts = domain.split('.');
113+
114+
// first navigate to the deepest TrieNode for domain
115+
for (let i = parts.length-1; i >= 0; i--) {
116+
let key = parts[i];
117+
if (!utils.hasOwn(node.children, key)) {
118+
// abort if domain isn't fully in the Trie
119+
return domains;
120+
}
121+
node = node.children[key];
122+
}
123+
124+
findAll(node, domains);
125+
126+
return domains;
127+
};
128+
129+
/**
130+
* Returns whether the domain string, or any of its
131+
* parent domain strings were inserted into the trie.
132+
*
133+
* @param {String} domain
134+
*
135+
* @returns {Boolean}
136+
*/
137+
Trie.prototype.globDomainMatches = function (domain) {
138+
let parts = domain.split('.'),
139+
node = this.root;
140+
141+
for (let i = parts.length-1; i >= 0; i--) {
142+
let key = parts[i];
143+
if (!utils.hasOwn(node.children, key)) {
144+
return false;
145+
}
146+
node = node.children[key];
147+
if (node.stringEndsHere) {
148+
return true;
149+
}
150+
}
151+
152+
return false;
153+
};
154+
155+
export {
156+
Trie
157+
};

src/tests/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
<script type="module" src="tests/options.js"></script>
4949
<script type="module" src="tests/storage.js"></script>
5050
<script type="module" src="tests/tabData.js"></script>
51+
<script type="module" src="tests/trie.js"></script>
5152
<!-- URL/host tools: --><script type="module" src="tests/baseDomain.js"></script>
5253
<script type="module" src="tests/utils.js"></script>
5354
<script type="module" src="tests/yellowlist.js"></script>

src/tests/lib/qunit_config.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@
3939
let store = badger.storage.getStore(store_name);
4040
BACKUP[store_name] = store.getItemClones();
4141
});
42+
43+
BACKUP.disabledSitesTrie = badger.disabledSitesTrie;
44+
badger.initDisabledSitesTrie();
4245
});
4346

4447
QUnit.testDone(() => {
@@ -47,6 +50,8 @@
4750
let store = badger.storage.getStore(store_name);
4851
store._store = BACKUP[store_name];
4952
});
53+
54+
badger.disabledSitesTrie = BACKUP.disabledSitesTrie;
5055
});
5156

5257
// kick off tests when we have what we need from Badger

0 commit comments

Comments
 (0)