fix(cosmetic): inject rules immediately and fix selector normalization
- commit
- 27d0775c8f34eb4ac9b9bfdc59f863abb9526f57
- author
- bnema · · 2025-11-29 11:26 UTC
- parent
- 6defb29
- signature
- GPG Unverified
2B82207D370127A0The key is unknown
3 files · +83 -26
M internal/filtering/cosmetic/injector.go +23 -8
| @@ -67,7 +67,7 @@ func (ci *CosmeticInjector) GetScriptForDomain(domain string) string { | |||
| 67 | 67 | return "" | |
| 68 | 68 | } | |
| 69 | 69 | ||
| 70 | - | // Build rules for this specific domain | |
| 70 | + | // Build rules for this specific domain (including parent domains) | |
| 71 | 71 | var rules []CosmeticRule | |
| 72 | 72 | ||
| 73 | 73 | // Add generic rules | |
| @@ -77,16 +77,31 @@ func (ci *CosmeticInjector) GetScriptForDomain(domain string) string { | |||
| 77 | 77 | }) | |
| 78 | 78 | } | |
| 79 | 79 | ||
| 80 | - | // Add domain-specific rules | |
| 81 | - | if domainSelectors, exists := ci.domainRules[domain]; exists { | |
| 82 | - | for _, selector := range domainSelectors { | |
| 83 | - | rules = append(rules, CosmeticRule{ | |
| 84 | - | Domain: domain, | |
| 85 | - | Selector: selector, | |
| 86 | - | }) | |
| 80 | + | // Add domain-specific rules for the domain and its parent domains | |
| 81 | + | addDomainRules := func(domainKey string) { | |
| 82 | + | if domainKey == "" { | |
| 83 | + | return | |
| 84 | + | } | |
| 85 | + | if domainSelectors, exists := ci.domainRules[domainKey]; exists { | |
| 86 | + | for _, selector := range domainSelectors { | |
| 87 | + | rules = append(rules, CosmeticRule{ | |
| 88 | + | Domain: domainKey, | |
| 89 | + | Selector: selector, | |
| 90 | + | }) | |
| 91 | + | } | |
| 87 | 92 | } | |
| 88 | 93 | } | |
| 89 | 94 | ||
| 95 | + | addDomainRules(domain) | |
| 96 | + | ||
| 97 | + | // Walk up parent domains to include base-domain rules for subdomains | |
| 98 | + | parts := strings.Split(domain, ".") | |
| 99 | + | for len(parts) > 2 { | |
| 100 | + | parts = parts[1:] | |
| 101 | + | parentDomain := strings.Join(parts, ".") | |
| 102 | + | addDomainRules(parentDomain) | |
| 103 | + | } | |
| 104 | + | ||
| 90 | 105 | // Convert rules to JSON | |
| 91 | 106 | rulesJSON, err := json.Marshal(rules) | |
| 92 | 107 | if err != nil { | |
| @@ -67,7 +67,7 @@ func (ci *CosmeticInjector) GetScriptForDomain(domain string) string { | |||||
| 67 | return "" | 67 | return "" | ||
| 68 | } | 68 | } | ||
| 69 | 69 | ||||
| 70 | - | // Build rules for this specific domain | 70 | + | // Build rules for this specific domain (including parent domains) |
| 71 | var rules []CosmeticRule | 71 | var rules []CosmeticRule | ||
| 72 | 72 | ||||
| 73 | // Add generic rules | 73 | // Add generic rules | ||
| @@ -77,16 +77,31 @@ func (ci *CosmeticInjector) GetScriptForDomain(domain string) string { | |||||
| 77 | }) | 77 | }) | ||
| 78 | } | 78 | } | ||
| 79 | 79 | ||||
| 80 | - | // Add domain-specific rules | 80 | + | // Add domain-specific rules for the domain and its parent domains |
| 81 | - | if domainSelectors, exists := ci.domainRules[domain]; exists { | 81 | + | addDomainRules := func(domainKey string) { |
| 82 | - | for _, selector := range domainSelectors { | 82 | + | if domainKey == "" { |
| 83 | - | rules = append(rules, CosmeticRule{ | 83 | + | return |
| 84 | - | Domain: domain, | 84 | + | } |
| 85 | - | Selector: selector, | 85 | + | if domainSelectors, exists := ci.domainRules[domainKey]; exists { |
| 86 | - | }) | 86 | + | for _, selector := range domainSelectors { |
| 87 | + | rules = append(rules, CosmeticRule{ | |||
| 88 | + | Domain: domainKey, | |||
| 89 | + | Selector: selector, | |||
| 90 | + | }) | |||
| 91 | + | } | |||
| 87 | } | 92 | } | ||
| 88 | } | 93 | } | ||
| 89 | 94 | ||||
| 95 | + | addDomainRules(domain) | |||
| 96 | + | ||||
| 97 | + | // Walk up parent domains to include base-domain rules for subdomains | |||
| 98 | + | parts := strings.Split(domain, ".") | |||
| 99 | + | for len(parts) > 2 { | |||
| 100 | + | parts = parts[1:] | |||
| 101 | + | parentDomain := strings.Join(parts, ".") | |||
| 102 | + | addDomainRules(parentDomain) | |||
| 103 | + | } | |||
| 104 | + | ||||
| 90 | // Convert rules to JSON | 105 | // Convert rules to JSON | ||
| 91 | rulesJSON, err := json.Marshal(rules) | 106 | rulesJSON, err := json.Marshal(rules) | ||
| 92 | if err != nil { | 107 | if err != nil { | ||
M internal/filtering/cosmetic/injector.js +47 -16
| @@ -10,6 +10,17 @@ const CosmeticFilter = (() => { | |||
| 10 | 10 | procedural: [] // Complex selectors with :has(), etc. | |
| 11 | 11 | }; | |
| 12 | 12 | ||
| 13 | + | // Normalize rule representation into a selector string | |
| 14 | + | const normalizeSelector = (rule) => { | |
| 15 | + | if (typeof rule === 'string') { | |
| 16 | + | return rule; | |
| 17 | + | } | |
| 18 | + | if (rule && typeof rule === 'object') { | |
| 19 | + | return rule.selector || ''; | |
| 20 | + | } | |
| 21 | + | return ''; | |
| 22 | + | }; | |
| 23 | + | ||
| 13 | 24 | // Safe anti-breakage scriptlets that avoid WebKit internal interference | |
| 14 | 25 | const antiBreakage = { | |
| 15 | 26 | // Safely stub ad services without modifying global setTimeout | |
| @@ -186,16 +197,31 @@ const CosmeticFilter = (() => { | |||
| 186 | 197 | antiBreakage.neutralizeAdPromises(); | |
| 187 | 198 | antiBreakage.cleanupLoadingIndicators(); | |
| 188 | 199 | ||
| 200 | + | const hostname = window.location.hostname; | |
| 201 | + | ||
| 189 | 202 | // Parse and categorize rules (with null safety) | |
| 190 | 203 | if (rules && Array.isArray(rules)) { | |
| 191 | 204 | rules.forEach(rule => { | |
| 192 | - | if (rule.includes(':has(') || rule.includes(':not(')) { | |
| 193 | - | selectors.procedural.push(rule); | |
| 194 | - | } else if (rule.domain === window.location.hostname) { | |
| 195 | - | selectors.specific.push(rule.selector); | |
| 196 | - | } else { | |
| 197 | - | selectors.generic.push(rule.selector); | |
| 205 | + | const selector = normalizeSelector(rule); | |
| 206 | + | if (!selector) { | |
| 207 | + | return; | |
| 208 | + | } | |
| 209 | + | ||
| 210 | + | const domain = typeof rule === 'object' ? rule?.domain : null; | |
| 211 | + | const isProcedural = selector.includes(':has(') || selector.includes(':not('); | |
| 212 | + | const matchesDomain = domain ? (hostname === domain || hostname.endsWith(`.${domain}`)) : false; | |
| 213 | + | ||
| 214 | + | if (isProcedural) { | |
| 215 | + | selectors.procedural.push(selector); | |
| 216 | + | return; | |
| 198 | 217 | } | |
| 218 | + | ||
| 219 | + | if (matchesDomain) { | |
| 220 | + | selectors.specific.push(selector); | |
| 221 | + | return; | |
| 222 | + | } | |
| 223 | + | ||
| 224 | + | selectors.generic.push(selector); | |
| 199 | 225 | }); | |
| 200 | 226 | } | |
| 201 | 227 | ||
| @@ -214,15 +240,20 @@ const CosmeticFilter = (() => { | |||
| 214 | 240 | ||
| 215 | 241 | // Performance optimization: batch rule updates | |
| 216 | 242 | const updateRules = (newRules) => { | |
| 217 | - | // Diff and apply only new rules | |
| 218 | - | const newSelectors = newRules.filter(r => | |
| 219 | - | !selectors.generic.includes(r) && | |
| 220 | - | !selectors.specific.includes(r) | |
| 221 | - | ); | |
| 222 | - | ||
| 223 | - | if (newSelectors.length > 0) { | |
| 224 | - | hideElements(newSelectors); | |
| 225 | - | selectors.generic.push(...newSelectors); | |
| 243 | + | const additions = []; | |
| 244 | + | (newRules || []).forEach(rule => { | |
| 245 | + | const selector = normalizeSelector(rule); | |
| 246 | + | if (!selector) { | |
| 247 | + | return; | |
| 248 | + | } | |
| 249 | + | if (!selectors.generic.includes(selector) && !selectors.specific.includes(selector)) { | |
| 250 | + | additions.push(selector); | |
| 251 | + | } | |
| 252 | + | }); | |
| 253 | + | ||
| 254 | + | if (additions.length > 0) { | |
| 255 | + | hideElements(additions); | |
| 256 | + | selectors.generic.push(...additions); | |
| 226 | 257 | } | |
| 227 | 258 | }; | |
| 228 | 259 | ||
| @@ -242,4 +273,4 @@ window.__dumber_cosmetic_init = (rules) => CosmeticFilter.init(rules); | |||
| 242 | 273 | window.__dumber_cosmetic_update = (rules) => CosmeticFilter.updateRules(rules); | |
| 243 | 274 | window.__dumber_cosmetic_cleanup = () => CosmeticFilter.cleanup(); | |
| 244 | 275 | ||
| 245 | - | } // End guard against duplicate injection⏎̸ | |
| 276 | + | } // End guard against duplicate injection | |
| @@ -10,6 +10,17 @@ const CosmeticFilter = (() => { | |||||
| 10 | procedural: [] // Complex selectors with :has(), etc. | 10 | procedural: [] // Complex selectors with :has(), etc. | ||
| 11 | }; | 11 | }; | ||
| 12 | 12 | ||||
| 13 | + | // Normalize rule representation into a selector string | |||
| 14 | + | const normalizeSelector = (rule) => { | |||
| 15 | + | if (typeof rule === 'string') { | |||
| 16 | + | return rule; | |||
| 17 | + | } | |||
| 18 | + | if (rule && typeof rule === 'object') { | |||
| 19 | + | return rule.selector || ''; | |||
| 20 | + | } | |||
| 21 | + | return ''; | |||
| 22 | + | }; | |||
| 23 | + | ||||
| 13 | // Safe anti-breakage scriptlets that avoid WebKit internal interference | 24 | // Safe anti-breakage scriptlets that avoid WebKit internal interference | ||
| 14 | const antiBreakage = { | 25 | const antiBreakage = { | ||
| 15 | // Safely stub ad services without modifying global setTimeout | 26 | // Safely stub ad services without modifying global setTimeout | ||
| @@ -186,16 +197,31 @@ const CosmeticFilter = (() => { | |||||
| 186 | antiBreakage.neutralizeAdPromises(); | 197 | antiBreakage.neutralizeAdPromises(); | ||
| 187 | antiBreakage.cleanupLoadingIndicators(); | 198 | antiBreakage.cleanupLoadingIndicators(); | ||
| 188 | 199 | ||||
| 200 | + | const hostname = window.location.hostname; | |||
| 201 | + | ||||
| 189 | // Parse and categorize rules (with null safety) | 202 | // Parse and categorize rules (with null safety) | ||
| 190 | if (rules && Array.isArray(rules)) { | 203 | if (rules && Array.isArray(rules)) { | ||
| 191 | rules.forEach(rule => { | 204 | rules.forEach(rule => { | ||
| 192 | - | if (rule.includes(':has(') || rule.includes(':not(')) { | 205 | + | const selector = normalizeSelector(rule); |
| 193 | - | selectors.procedural.push(rule); | 206 | + | if (!selector) { |
| 194 | - | } else if (rule.domain === window.location.hostname) { | 207 | + | return; |
| 195 | - | selectors.specific.push(rule.selector); | 208 | + | } |
| 196 | - | } else { | 209 | + | |
| 197 | - | selectors.generic.push(rule.selector); | 210 | + | const domain = typeof rule === 'object' ? rule?.domain : null; |
| 211 | + | const isProcedural = selector.includes(':has(') || selector.includes(':not('); | |||
| 212 | + | const matchesDomain = domain ? (hostname === domain || hostname.endsWith(`.${domain}`)) : false; | |||
| 213 | + | ||||
| 214 | + | if (isProcedural) { | |||
| 215 | + | selectors.procedural.push(selector); | |||
| 216 | + | return; | |||
| 198 | } | 217 | } | ||
| 218 | + | ||||
| 219 | + | if (matchesDomain) { | |||
| 220 | + | selectors.specific.push(selector); | |||
| 221 | + | return; | |||
| 222 | + | } | |||
| 223 | + | ||||
| 224 | + | selectors.generic.push(selector); | |||
| 199 | }); | 225 | }); | ||
| 200 | } | 226 | } | ||
| 201 | 227 | ||||
| @@ -214,15 +240,20 @@ const CosmeticFilter = (() => { | |||||
| 214 | 240 | ||||
| 215 | // Performance optimization: batch rule updates | 241 | // Performance optimization: batch rule updates | ||
| 216 | const updateRules = (newRules) => { | 242 | const updateRules = (newRules) => { | ||
| 217 | - | // Diff and apply only new rules | 243 | + | const additions = []; |
| 218 | - | const newSelectors = newRules.filter(r => | 244 | + | (newRules || []).forEach(rule => { |
| 219 | - | !selectors.generic.includes(r) && | 245 | + | const selector = normalizeSelector(rule); |
| 220 | - | !selectors.specific.includes(r) | 246 | + | if (!selector) { |
| 221 | - | ); | 247 | + | return; |
| 222 | - | 248 | + | } | |
| 223 | - | if (newSelectors.length > 0) { | 249 | + | if (!selectors.generic.includes(selector) && !selectors.specific.includes(selector)) { |
| 224 | - | hideElements(newSelectors); | 250 | + | additions.push(selector); |
| 225 | - | selectors.generic.push(...newSelectors); | 251 | + | } |
| 252 | + | }); | |||
| 253 | + | ||||
| 254 | + | if (additions.length > 0) { | |||
| 255 | + | hideElements(additions); | |||
| 256 | + | selectors.generic.push(...additions); | |||
| 226 | } | 257 | } | ||
| 227 | }; | 258 | }; | ||
| 228 | 259 | ||||
| @@ -242,4 +273,4 @@ window.__dumber_cosmetic_init = (rules) => CosmeticFilter.init(rules); | |||||
| 242 | window.__dumber_cosmetic_update = (rules) => CosmeticFilter.updateRules(rules); | 273 | window.__dumber_cosmetic_update = (rules) => CosmeticFilter.updateRules(rules); | ||
| 243 | window.__dumber_cosmetic_cleanup = () => CosmeticFilter.cleanup(); | 274 | window.__dumber_cosmetic_cleanup = () => CosmeticFilter.cleanup(); | ||
| 244 | 275 | ||||
| 245 | - | } // End guard against duplicate injection⏎̸ | 276 | + | } // End guard against duplicate injection |
M internal/filtering/manager.go +13 -2
| @@ -380,8 +380,19 @@ func (fm *FilterManager) applyFilters(filters *CompiledFilters) { | |||
| 380 | 380 | ||
| 381 | 381 | fm.compiled = filters | |
| 382 | 382 | ||
| 383 | - | // Inject cosmetic rules | |
| 384 | - | if err := fm.cosmeticInjector.InjectRules(filters.CosmeticRules); err != nil { | |
| 383 | + | // Inject cosmetic rules (domain-specific + generic hiding under empty key) | |
| 384 | + | cosmeticRulesWithGeneric := make(map[string][]string) | |
| 385 | + | for domain, selectors := range filters.CosmeticRules { | |
| 386 | + | cosmeticRulesWithGeneric[domain] = selectors | |
| 387 | + | } | |
| 388 | + | // Add generic hiding rules under empty string key (applies to all domains) | |
| 389 | + | if len(filters.GenericHiding) > 0 { | |
| 390 | + | cosmeticRulesWithGeneric[""] = filters.GenericHiding | |
| 391 | + | logging.Info(fmt.Sprintf("[filtering] Adding %d generic hiding rules to cosmetic injector", len(filters.GenericHiding))) | |
| 392 | + | } | |
| 393 | + | logging.Info(fmt.Sprintf("[filtering] Injecting cosmetic rules: %d domains + %d generic rules", | |
| 394 | + | len(filters.CosmeticRules), len(filters.GenericHiding))) | |
| 395 | + | if err := fm.cosmeticInjector.InjectRules(cosmeticRulesWithGeneric); err != nil { | |
| 385 | 396 | logging.Error(fmt.Sprintf("Failed to inject cosmetic rules: %v", err)) | |
| 386 | 397 | } | |
| 387 | 398 | ||
| @@ -380,8 +380,19 @@ func (fm *FilterManager) applyFilters(filters *CompiledFilters) { | |||||
| 380 | 380 | ||||
| 381 | fm.compiled = filters | 381 | fm.compiled = filters | ||
| 382 | 382 | ||||
| 383 | - | // Inject cosmetic rules | 383 | + | // Inject cosmetic rules (domain-specific + generic hiding under empty key) |
| 384 | - | if err := fm.cosmeticInjector.InjectRules(filters.CosmeticRules); err != nil { | 384 | + | cosmeticRulesWithGeneric := make(map[string][]string) |
| 385 | + | for domain, selectors := range filters.CosmeticRules { | |||
| 386 | + | cosmeticRulesWithGeneric[domain] = selectors | |||
| 387 | + | } | |||
| 388 | + | // Add generic hiding rules under empty string key (applies to all domains) | |||
| 389 | + | if len(filters.GenericHiding) > 0 { | |||
| 390 | + | cosmeticRulesWithGeneric[""] = filters.GenericHiding | |||
| 391 | + | logging.Info(fmt.Sprintf("[filtering] Adding %d generic hiding rules to cosmetic injector", len(filters.GenericHiding))) | |||
| 392 | + | } | |||
| 393 | + | logging.Info(fmt.Sprintf("[filtering] Injecting cosmetic rules: %d domains + %d generic rules", | |||
| 394 | + | len(filters.CosmeticRules), len(filters.GenericHiding))) | |||
| 395 | + | if err := fm.cosmeticInjector.InjectRules(cosmeticRulesWithGeneric); err != nil { | |||
| 385 | logging.Error(fmt.Sprintf("Failed to inject cosmetic rules: %v", err)) | 396 | logging.Error(fmt.Sprintf("Failed to inject cosmetic rules: %v", err)) | ||
| 386 | } | 397 | } | ||
| 387 | 398 | ||||