Skip to content

Commit e2df53d

Browse files
committed
[mv3] Add support for highly generic cosmetic filter exceptions
Related issue: uBlockOrigin/uBOL-home#406
1 parent cae53be commit e2df53d

5 files changed

Lines changed: 253 additions & 358 deletions

File tree

platform/mv3/extension/js/scripting-manager.js

Lines changed: 0 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -83,70 +83,6 @@ async function resetCSSCache() {
8383

8484
/******************************************************************************/
8585

86-
function registerHighGeneric(context, genericDetails) {
87-
const { filteringModeDetails, rulesetsDetails } = context;
88-
89-
const excludeHostnames = [];
90-
const includeHostnames = [];
91-
const css = [];
92-
for ( const details of rulesetsDetails ) {
93-
const hostnames = genericDetails.get(details.id);
94-
if ( hostnames ) {
95-
if ( hostnames.unhide ) {
96-
excludeHostnames.push(...hostnames.unhide);
97-
}
98-
if ( hostnames.hide ) {
99-
includeHostnames.push(...hostnames.hide);
100-
}
101-
}
102-
const count = details.css?.generichigh || 0;
103-
if ( count === 0 ) { continue; }
104-
css.push(`/rulesets/scripting/generichigh/${details.id}.css`);
105-
}
106-
107-
if ( css.length === 0 ) { return; }
108-
109-
const { none, basic, optimal, complete } = filteringModeDetails;
110-
const matches = [];
111-
const excludeMatches = [];
112-
if ( complete.has('all-urls') ) {
113-
excludeMatches.push(...ut.matchesFromHostnames(none));
114-
excludeMatches.push(...ut.matchesFromHostnames(basic));
115-
excludeMatches.push(...ut.matchesFromHostnames(optimal));
116-
excludeMatches.push(...ut.matchesFromHostnames(excludeHostnames));
117-
matches.push('<all_urls>');
118-
} else {
119-
matches.push(
120-
...ut.matchesFromHostnames(
121-
ut.subtractHostnameIters(
122-
Array.from(complete),
123-
excludeHostnames
124-
)
125-
)
126-
);
127-
}
128-
if ( matches.length === 0 ) { return; }
129-
130-
// https://github.com/w3c/webextensions/issues/414#issuecomment-1623992885
131-
// Once supported, add:
132-
// cssOrigin: 'USER',
133-
const directive = {
134-
id: 'css-generichigh',
135-
css,
136-
matches,
137-
allFrames: true,
138-
runAt: 'document_end',
139-
};
140-
if ( excludeMatches.length !== 0 ) {
141-
directive.excludeMatches = excludeMatches;
142-
}
143-
144-
// register
145-
context.toAdd.push(directive);
146-
}
147-
148-
/******************************************************************************/
149-
15086
function registerGeneric(context, genericDetails) {
15187
const { filteringModeDetails, rulesetsDetails } = context;
15288

@@ -401,7 +337,6 @@ registerInjectables.register = async function register() {
401337
registerCosmetic('specific', context),
402338
registerCosmetic('procedural', context),
403339
registerGeneric(context, genericDetails),
404-
registerHighGeneric(context, genericDetails),
405340
registerCustomFilters(context),
406341
registerCustomScriptlets(context),
407342
registerPreventPopup(context),

platform/mv3/extension/js/scripting/css-generic.js

Lines changed: 118 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -25,21 +25,22 @@
2525
// Isolate from global scope
2626
(function uBOL_cssGeneric() {
2727

28-
const genericSelectorMap = self.genericSelectorMap || new Map();
29-
self.genericSelectorMap = undefined;
30-
if ( genericSelectorMap.size === 0 ) { return; }
28+
const genericSelectorMaps = self.genericSelectorMaps ?? [];
29+
self.genericSelectorMaps = undefined;
3130

32-
const genericExceptionSieve = self.genericExceptionSieve || new Set();
33-
self.genericExceptionSieve = undefined;
31+
const genericDetails = self.genericDetails ?? [];
32+
self.genericDetails = undefined;
3433

35-
const genericExceptionMap = self.genericExceptionMap || new Map();
36-
self.genericExceptionMap = undefined;
34+
if ( genericDetails.length === 0 ) { return; }
35+
if ( document.documentElement === null ) { return; }
3736

3837
/******************************************************************************/
3938

4039
const maxSurveyTimeSlice = 4;
4140
const maxSurveyNodeSlice = 64;
42-
const styleSheetSelectors = [];
41+
const seenHashes = new Set();
42+
const pendingHashes = new Set();
43+
const pendingSelectors = [];
4344
const stopAllRatio = 0.95; // To be investigated
4445

4546
let surveyCount = 0;
@@ -51,6 +52,35 @@ let lastDomChange = Date.now();
5152

5253
/******************************************************************************/
5354

55+
const pendingNodes = {
56+
addedNodes: [],
57+
nodeSet: new Set(),
58+
add(node) {
59+
this.addedNodes.push(node);
60+
},
61+
next(out) {
62+
for ( const added of this.addedNodes ) {
63+
if ( this.nodeSet.has(added) ) { continue; }
64+
this.nodeSet.add(added);
65+
if ( added.firstElementChild === null ) { continue; }
66+
for ( const descendant of added.querySelectorAll('[id],[class]') ) {
67+
this.nodeSet.add(descendant);
68+
}
69+
}
70+
this.addedNodes.length = 0;
71+
for ( const node of this.nodeSet ) {
72+
this.nodeSet.delete(node);
73+
out.push(node);
74+
if ( out.length === maxSurveyNodeSlice ) { break; }
75+
}
76+
},
77+
hasNodes() {
78+
return this.addedNodes.length !== 0 || this.nodeSet.size !== 0;
79+
},
80+
};
81+
82+
/******************************************************************************/
83+
5484
// http://www.cse.yorku.ca/~oz/hash.html#djb2
5585
// Must mirror dnrRulesetFromRawLists's version
5686

@@ -61,7 +91,7 @@ const hashFromStr = (type, s) => {
6191
for ( let i = 0; i < len; i += step ) {
6292
hash = (hash << 5) + hash ^ s.charCodeAt(i);
6393
}
64-
return hash & 0xFFF;
94+
return hash & 0xFFFF;
6595
};
6696

6797
/******************************************************************************/
@@ -78,14 +108,9 @@ const uBOL_idFromNode = node => {
78108
const raw = node.id;
79109
if ( typeof raw !== 'string' || raw.length === 0 ) { return; }
80110
const hash = hashFromStr(0x23 /* '#' */, raw.trim());
81-
const selectorList = genericSelectorMap.get(hash);
82-
if ( selectorList === undefined ) { return; }
83-
genericSelectorMap.delete(hash);
84-
if ( genericExceptionSieve.has(hash) ) {
85-
applyExceptions(selectorList);
86-
} else {
87-
styleSheetSelectors.push(selectorList);
88-
}
111+
if ( seenHashes.has(hash) ) { return; }
112+
seenHashes.add(hash);
113+
pendingHashes.add(hash);
89114
};
90115

91116
// https://github.com/uBlockOrigin/uBlock-issues/discussions/2076
@@ -102,58 +127,31 @@ const uBOL_classesFromNode = node => {
102127
beg = end;
103128
if ( token.length === 0 ) { continue; }
104129
const hash = hashFromStr(0x2E /* '.' */, token);
105-
const selectorList = genericSelectorMap.get(hash);
106-
if ( selectorList === undefined ) { continue; }
107-
genericSelectorMap.delete(hash);
108-
if ( genericExceptionSieve.has(hash) ) {
109-
applyExceptions(selectorList);
110-
} else {
111-
styleSheetSelectors.push(selectorList);
112-
}
130+
if ( seenHashes.has(hash) ) { continue; }
131+
seenHashes.add(hash);
132+
pendingHashes.add(hash);
113133
}
114134
};
115135

116-
const applyExceptions = selectorList => {
117-
const selectors = new Set(selectorList.split(',\n'));
118-
self.isolatedAPI.forEachHostname(hostname => {
119-
const exceptions = genericExceptionMap.get(hostname);
120-
if ( exceptions === undefined ) { return; }
121-
for ( const exception of exceptions.split('\n') ) {
122-
selectors.delete(exception);
136+
/******************************************************************************/
137+
138+
const processPendingHashes = ( ) => {
139+
for ( const hash of pendingHashes ) {
140+
for ( const selectorMap of genericSelectorMaps ) {
141+
const selectors = selectorMap.get(hash);
142+
if ( selectors === undefined ) { continue; }
143+
selectorMap.delete(hash);
144+
pendingSelectors.push(selectors);
123145
}
124-
if ( selectors.size === 0 ) { return true; }
125-
}, { hasEntities: true });
126-
if ( selectors.size === 0 ) { return; }
127-
styleSheetSelectors.push(Array.from(selectors).join(',\n'));
128-
}
146+
}
147+
};
129148

130149
/******************************************************************************/
131150

132-
const pendingNodes = {
133-
addedNodes: [],
134-
nodeSet: new Set(),
135-
add(node) {
136-
this.addedNodes.push(node);
137-
},
138-
next(out) {
139-
for ( const added of this.addedNodes ) {
140-
if ( this.nodeSet.has(added) ) { continue; }
141-
this.nodeSet.add(added);
142-
if ( added.firstElementChild === null ) { continue; }
143-
for ( const descendant of added.querySelectorAll('[id],[class]') ) {
144-
this.nodeSet.add(descendant);
145-
}
146-
}
147-
this.addedNodes.length = 0;
148-
for ( const node of this.nodeSet ) {
149-
this.nodeSet.delete(node);
150-
out.push(node);
151-
if ( out.length === maxSurveyNodeSlice ) { break; }
152-
}
153-
},
154-
hasNodes() {
155-
return this.addedNodes.length !== 0 || this.nodeSet.size !== 0;
156-
},
151+
const exceptPendingSelectors = ( ) => {
152+
if ( exceptionSet.size === 0 ) { return pendingSelectors.join(',\n'); }
153+
const selectorSet = new Set(pendingSelectors.map(a => a.split(',\n')).flat());
154+
return Array.from(selectorSet.difference(exceptionSet)).join(',\n');
157155
};
158156

159157
/******************************************************************************/
@@ -173,21 +171,24 @@ const uBOL_processNodes = ( ) => {
173171
if ( performance.now() >= deadline ) { break; }
174172
}
175173
surveyCount += 1;
176-
if ( styleSheetSelectors.length === 0 ) {
174+
processPendingHashes();
175+
const styleSheetSelectors = exceptPendingSelectors();
176+
pendingHashes.clear();
177+
pendingSelectors.length = 0;
178+
if ( styleSheetSelectors === '' ) {
177179
surveyMissCount += 1;
178-
if (
179-
surveyCount >= 100 &&
180-
(surveyMissCount / surveyCount) >= stopAllRatio
181-
) {
182-
stopAll(`too many misses in surveyor (${surveyMissCount}/${surveyCount})`);
180+
if ( surveyCount >= 64 ) {
181+
if ( (surveyMissCount / surveyCount) >= stopAllRatio ) {
182+
stopAll(`too many misses in surveyor (${surveyMissCount}/${surveyCount})`);
183+
}
183184
}
184185
return;
185186
}
186187
if ( styleSheetTimer !== undefined ) { return; }
188+
surveyMissCount = 0;
187189
styleSheetTimer = self.requestAnimationFrame(( ) => {
188190
styleSheetTimer = undefined;
189-
self.cssAPI.insert(`${styleSheetSelectors.join(',')}{display:none!important;}`);
190-
styleSheetSelectors.length = 0;
191+
self.cssAPI.insert(`${styleSheetSelectors}{display:none!important;}`);
191192
});
192193
};
193194

@@ -208,7 +209,7 @@ const uBOL_processChanges = mutations => {
208209
}
209210
}
210211
if ( pendingNodes.hasNodes() === false ) {
211-
if ( styleSheetSelectors.length === 0 ) { return; }
212+
if ( pendingHashes.size === 0 ) { return; }
212213
}
213214
lastDomChange = Date.now();
214215
if ( processTimer !== undefined ) { return; }
@@ -225,15 +226,54 @@ const stopAll = ( ) => {
225226
self.clearTimeout(domChangeTimer);
226227
domChangeTimer = undefined;
227228
}
228-
domMutationObserver.disconnect();
229-
domMutationObserver.takeRecords();
230-
domMutationObserver = undefined;
231-
genericSelectorMap.clear();
229+
if ( domMutationObserver ) {
230+
domMutationObserver.disconnect();
231+
domMutationObserver.takeRecords();
232+
domMutationObserver = undefined;
233+
}
234+
genericSelectorMaps.length = 0;
232235
};
233236

234237
/******************************************************************************/
235238

236-
if ( document.documentElement === null ) { return; }
239+
// Perform once:
240+
// - Inject highly generics
241+
// - Collate exceptions matching current context
242+
243+
const exceptionSet = new Set();
244+
for ( const entry of genericDetails ) {
245+
const { highlyGeneric, exceptions, hostnames } = entry;
246+
if ( highlyGeneric ) {
247+
pendingSelectors.push(highlyGeneric);
248+
}
249+
if ( hostnames.length === 0 ) { continue; }
250+
let i = -1;
251+
for ( const hostname of self.isolatedAPI.contexts.hostnames ) {
252+
i = self.isolatedAPI.binarySearch(hostnames, hostname, i);
253+
if ( i >= 0 ) {
254+
exceptions[i].split('\n').forEach(a => exceptionSet.add(a));
255+
} else {
256+
i = ~i;
257+
}
258+
}
259+
if ( entry.hasEntities ) {
260+
i = -1;
261+
for ( const entity of self.isolatedAPI.contexts.entities ) {
262+
i = self.isolatedAPI.binarySearch(hostnames, entity, i);
263+
if ( i >= 0 ) {
264+
exceptions[i].split('\n').forEach(a => exceptionSet.add(a));
265+
} else {
266+
i = ~i;
267+
}
268+
}
269+
}
270+
}
271+
genericDetails.length = 0;
272+
273+
/******************************************************************************/
274+
275+
// Start applying generic cosmetic filters
276+
237277
pendingNodes.add(document.documentElement);
238278
uBOL_processNodes();
239279

@@ -248,10 +288,10 @@ domMutationObserver.observe(document, {
248288
const needDomChangeObserver = ( ) => {
249289
domChangeTimer = undefined;
250290
if ( domMutationObserver === undefined ) { return; }
251-
if ( (Date.now() - lastDomChange) > 20000 ) {
291+
if ( (Date.now() - lastDomChange) > 30000 ) {
252292
return stopAll('no more DOM changes');
253293
}
254-
domChangeTimer = self.setTimeout(needDomChangeObserver, 20000);
294+
domChangeTimer = self.setTimeout(needDomChangeObserver, 30000);
255295
};
256296

257297
needDomChangeObserver();

0 commit comments

Comments
 (0)