From 35aefed92616cbfb75f12f37c7ea7fb3a3cc3369 Mon Sep 17 00:00:00 2001 From: Raymond Hill Date: Mon, 7 Sep 2020 08:28:01 -0400 Subject: [PATCH] Add support to chain `:style()` to procedural operators Related issue: - https://github.com/uBlockOrigin/uBlock-issues/issues/382 Additionally, remnant code for pseudo-user stylesheets has been removed. Related commit: - https://github.com/gorhill/uBlock/commit/5c68867b92735931a791dfedf4ef9608cc364862 --- platform/chromium/vapi-background.js | 23 +- platform/chromium/vapi-client.js | 8 +- src/js/background.js | 4 +- src/js/contentscript.js | 280 +++++++++-------------- src/js/cosmetic-filtering.js | 167 ++++---------- src/js/epicker-ui.js | 17 +- src/js/scriptlets/cosmetic-logger.js | 6 +- src/js/scriptlets/dom-inspector.js | 23 +- src/js/scriptlets/dom-survey-elements.js | 4 +- src/js/scriptlets/epicker.js | 182 +++++---------- src/js/static-filtering-parser.js | 43 ++-- 11 files changed, 283 insertions(+), 474 deletions(-) diff --git a/platform/chromium/vapi-background.js b/platform/chromium/vapi-background.js index d5bbfd7a8..03b19f137 100644 --- a/platform/chromium/vapi-background.js +++ b/platform/chromium/vapi-background.js @@ -56,6 +56,16 @@ window.addEventListener('webextFlavor', function() { /******************************************************************************/ +vAPI.randomToken = function() { + const n = Math.random(); + return String.fromCharCode(n * 26 + 97) + + Math.floor( + (0.25 + n * 0.75) * Number.MAX_SAFE_INTEGER + ).toString(36).slice(-8); +}; + +/******************************************************************************/ + vAPI.app = { name: manifest.name.replace(/ dev\w+ build/, ''), version: (( ) => { @@ -339,7 +349,10 @@ vAPI.Tabs = class { return tabs.length !== 0 ? tabs[0] : null; } - async insertCSS() { + async insertCSS(tabId, details) { + if ( vAPI.supportsUserStylesheets ) { + details.cssOrigin = 'user'; + } try { await webext.tabs.insertCSS(...arguments); } @@ -357,7 +370,10 @@ vAPI.Tabs = class { return Array.isArray(tabs) ? tabs : []; } - async removeCSS() { + async removeCSS(tabId, details) { + if ( vAPI.supportsUserStylesheets ) { + details.cssOrigin = 'user'; + } try { await webext.tabs.removeCSS(...arguments); } @@ -1003,9 +1019,6 @@ vAPI.messaging = { frameId: sender.frameId, matchAboutBlank: true }; - if ( vAPI.supportsUserStylesheets ) { - details.cssOrigin = 'user'; - } if ( msg.add ) { details.runAt = 'document_start'; } diff --git a/platform/chromium/vapi-client.js b/platform/chromium/vapi-client.js index 491e503ab..1dc42277d 100644 --- a/platform/chromium/vapi-client.js +++ b/platform/chromium/vapi-client.js @@ -39,9 +39,11 @@ if ( /******************************************************************************/ vAPI.randomToken = function() { - const now = Date.now(); - return String.fromCharCode(now % 26 + 97) + - Math.floor((1 + Math.random()) * now).toString(36); + const n = Math.random(); + return String.fromCharCode(n * 26 + 97) + + Math.floor( + (0.25 + n * 0.75) * Number.MAX_SAFE_INTEGER + ).toString(36).slice(-8); }; vAPI.sessionId = vAPI.randomToken(); diff --git a/src/js/background.js b/src/js/background.js index ec32e2222..2ff449f7a 100644 --- a/src/js/background.js +++ b/src/js/background.js @@ -138,8 +138,8 @@ const µBlock = (( ) => { // jshint ignore:line // Read-only systemSettings: { - compiledMagic: 28, // Increase when compiled format changes - selfieMagic: 28, // Increase when selfie format changes + compiledMagic: 29, // Increase when compiled format changes + selfieMagic: 29, // Increase when selfie format changes }, // https://github.com/uBlockOrigin/uBlock-issues/issues/759#issuecomment-546654501 diff --git a/src/js/contentscript.js b/src/js/contentscript.js index 05422e690..d625c8828 100644 --- a/src/js/contentscript.js +++ b/src/js/contentscript.js @@ -88,16 +88,8 @@ The domFilterer makes use of platform-dependent user stylesheets[1]. - At time of writing, only modern Firefox provides a custom implementation, - which makes for solid, reliable and low overhead cosmetic filtering on - Firefox. - - The generic implementation[2] performs as best as can be, but won't ever be - as reliable and accurate as real user stylesheets. - [1] "user stylesheets" refer to local CSS rules which have priority over, and can't be overriden by a web page's own CSS rules. - [2] below, see platformUserCSS / platformHideNode / platformUnhideNode */ @@ -492,6 +484,11 @@ vAPI.injectScriptlet = function(doc, text) { */ { + vAPI.hideStyle = 'display:none!important;'; + + // TODO: Experiment/evaluate loading procedural operator code using an + // on demand approach. + // 'P' stands for 'Procedural' const PSelectorHasTextTask = class { @@ -562,14 +559,6 @@ vAPI.injectScriptlet = function(doc, text) { } }; - const PSelectorPassthru = class { - constructor() { - } - transpose(node, output) { - output.push(node); - } - }; - const PSelectorSpathTask = class { constructor(task) { this.spath = task[1]; @@ -701,17 +690,13 @@ vAPI.injectScriptlet = function(doc, text) { [ ':min-text-length', PSelectorMinTextLengthTask ], [ ':not', PSelectorIfNotTask ], [ ':nth-ancestor', PSelectorUpwardTask ], - [ ':remove', PSelectorPassthru ], [ ':spath', PSelectorSpathTask ], [ ':upward', PSelectorUpwardTask ], [ ':watch-attr', PSelectorWatchAttrs ], [ ':xpath', PSelectorXpathTask ], ]); } - this.budget = 200; // I arbitrary picked a 1/5 second this.raw = o.raw; - this.cost = 0; - this.lastAllowanceTime = 0; this.selector = o.selector; this.tasks = []; const tasks = o.tasks; @@ -722,9 +707,6 @@ vAPI.injectScriptlet = function(doc, text) { ); } } - if ( o.action !== undefined ) { - this.action = o.action; - } } prime(input) { const root = input || document; @@ -760,10 +742,20 @@ vAPI.injectScriptlet = function(doc, text) { return false; } }; - PSelector.prototype.action = undefined; - PSelector.prototype.hit = false; PSelector.prototype.operatorToTaskMap = undefined; + const PSelectorRoot = class extends PSelector { + constructor(o, styleToken) { + super(o); + this.budget = 200; // I arbitrary picked a 1/5 second + this.raw = o.raw; + this.cost = 0; + this.lastAllowanceTime = 0; + this.styleToken = styleToken; + } + }; + PSelectorRoot.prototype.hit = false; + const DOMProceduralFilterer = class { constructor(domFilterer) { this.domFilterer = domFilterer; @@ -771,40 +763,48 @@ vAPI.injectScriptlet = function(doc, text) { this.domIsWatched = false; this.mustApplySelectors = false; this.selectors = new Map(); - this.hiddenNodes = new Set(); + this.masterToken = vAPI.randomToken(); + this.styleTokenMap = new Map(); + this.styledNodes = new Set(); if ( vAPI.domWatcher instanceof Object ) { vAPI.domWatcher.addListener(this); } } - addProceduralSelectors(aa) { + addProceduralSelectors(selectors) { const addedSelectors = []; let mustCommit = this.domIsWatched; - for ( let i = 0, n = aa.length; i < n; i++ ) { - const raw = aa[i]; + for ( const raw of selectors ) { + if ( this.selectors.has(raw) ) { continue; } const o = JSON.parse(raw); - if ( o.action === 'style' ) { - this.domFilterer.addCSSRule(o.selector, o.tasks[0][1]); - mustCommit = true; - continue; - } if ( o.pseudo !== undefined ) { - this.domFilterer.addCSSRule( - o.selector, - 'display:none!important;' - ); + this.domFilterer.addCSSRule(o.selector, vAPI.hideStyle); mustCommit = true; continue; } - if ( o.tasks !== undefined ) { - if ( this.selectors.has(raw) === false ) { - const pselector = new PSelector(o); - this.selectors.set(raw, pselector); - addedSelectors.push(pselector); - mustCommit = true; - } + // CSS selector-based styles. + if ( + o.action !== undefined && + o.action[0] === ':style' && + o.tasks === undefined + ) { + this.domFilterer.addCSSRule(o.selector, o.action[1]); + mustCommit = true; continue; } + let style, styleToken; + if ( o.action === undefined ) { + style = vAPI.hideStyle; + } else if ( o.action[0] === ':style' ) { + style = o.action[1]; + } + if ( style !== undefined ) { + styleToken = this.styleTokenFromStyle(style); + } + const pselector = new PSelectorRoot(o, styleToken); + this.selectors.set(raw, pselector); + addedSelectors.push(pselector); + mustCommit = true; } if ( mustCommit === false ) { return; } this.mustApplySelectors = this.selectors.size !== 0; @@ -828,8 +828,8 @@ vAPI.injectScriptlet = function(doc, text) { // https://github.com/uBlockOrigin/uBlock-issues/issues/341 // Be ready to unhide nodes which no longer matches any of // the procedural selectors. - const toRemove = this.hiddenNodes; - this.hiddenNodes = new Set(); + const toUnstyle = this.styledNodes; + this.styledNodes = new Set(); let t0 = Date.now(); @@ -851,37 +851,54 @@ vAPI.injectScriptlet = function(doc, text) { t0 = t1; if ( nodes.length === 0 ) { continue; } pselector.hit = true; - if ( pselector.action === 'remove' ) { - this.removeNodes(nodes); - } else { - this.hideNodes(nodes); - } + this.styleNodes(nodes, pselector.styleToken); } - for ( const node of toRemove ) { - if ( this.hiddenNodes.has(node) ) { continue; } - this.domFilterer.unhideNode(node); - } + this.unstyleNodes(toUnstyle); //console.timeEnd('procedural selectors/dom layout changed'); } - hideNodes(nodes) { + styleTokenFromStyle(style) { + if ( style === undefined ) { return; } + let styleToken = this.styleTokenMap.get(style); + if ( styleToken !== undefined ) { return styleToken; } + styleToken = vAPI.randomToken(); + this.styleTokenMap.set(style, styleToken); + this.domFilterer.addCSSRule( + `[${this.masterToken}][${styleToken}]`, + style, + { silent: true } + ); + return styleToken; + } + + styleNodes(nodes, styleToken) { + if ( styleToken === undefined ) { + for ( const node of nodes ) { + node.textContent = ''; + node.remove(); + } + return; + } for ( const node of nodes ) { if ( node.parentElement === null ) { continue; } - this.domFilterer.hideNode(node); - this.hiddenNodes.add(node); + node.setAttribute(this.masterToken, ''); + node.setAttribute(styleToken, ''); } } - removeNodes(nodes) { + // TODO: Current assumption is one style per hit element. Could be an + // issue if an element has multiple styling and one styling is + // brough back. Possibly too rare to care about this for now. + unstyleNodes(nodes) { for ( const node of nodes ) { - node.textContent = ''; - node.remove(); + if ( this.styledNodes.has(node) ) { continue; } + node.removeAttribute(this.masterToken); } } createProceduralFilter(o) { - return new PSelector(o); + return new PSelectorRoot(o); } onDOMCreated() { @@ -908,14 +925,10 @@ vAPI.injectScriptlet = function(doc, text) { this.disabled = false; this.listeners = []; this.filterset = new Set(); - this.excludedNodeSet = new WeakSet(); this.addedCSSRules = new Set(); this.exceptedCSSRules = []; - this.reOnlySelectors = /\n\{[^\n]+/g; this.exceptions = []; this.proceduralFilterer = null; - this.hideNodeAttr = undefined; - this.hideNodeStyleSheetInjected = false; // https://github.com/uBlockOrigin/uBlock-issues/issues/167 // By the time the DOMContentLoaded is fired, the content script might // have been disconnected from the background page. Unclear why this @@ -988,33 +1001,6 @@ vAPI.injectScriptlet = function(doc, text) { } } - excludeNode(node) { - this.excludedNodeSet.add(node); - this.unhideNode(node); - } - - unexcludeNode(node) { - this.excludedNodeSet.delete(node); - } - - hideNode(node) { - if ( this.excludedNodeSet.has(node) ) { return; } - if ( this.hideNodeAttr === undefined ) { return; } - node.setAttribute(this.hideNodeAttr, ''); - if ( this.hideNodeStyleSheetInjected ) { return; } - this.hideNodeStyleSheetInjected = true; - this.addCSSRule( - `[${this.hideNodeAttr}]`, - 'display:none!important;', - { silent: true } - ); - } - - unhideNode(node) { - if ( this.hideNodeAttr === undefined ) { return; } - node.removeAttribute(this.hideNodeAttr); - } - toggle(state, callback) { if ( state === undefined ) { state = this.disabled; } if ( state !== this.disabled ) { return; } @@ -1031,24 +1017,6 @@ vAPI.injectScriptlet = function(doc, text) { userStylesheet.apply(callback); } - getAllSelectors_(all) { - const out = { - declarative: [], - exceptions: this.exceptedCSSRules, - }; - for ( const entry of this.filterset ) { - let selectors = entry.selectors; - if ( all !== true && this.hideNodeAttr !== undefined ) { - selectors = selectors - .replace(`[${this.hideNodeAttr}]`, '') - .replace(/^,\n|,\n$/gm, ''); - if ( selectors === '' ) { continue; } - } - out.declarative.push([ selectors, entry.declarations ]); - } - return out; - } - // Here we will deal with: // - Injecting low priority user styles; // - Notifying listeners about changed filterset. @@ -1097,7 +1065,7 @@ vAPI.injectScriptlet = function(doc, text) { } addProceduralSelectors(aa) { - if ( aa.length === 0 ) { return; } + if ( Array.isArray(aa) === false || aa.length === 0 ) { return; } this.proceduralFiltererInstance().addProceduralSelectors(aa); } @@ -1105,25 +1073,39 @@ vAPI.injectScriptlet = function(doc, text) { return this.proceduralFiltererInstance().createProceduralFilter(o); } - getAllSelectors() { - const out = this.getAllSelectors_(false); - out.procedural = this.proceduralFilterer instanceof Object - ? Array.from(this.proceduralFilterer.selectors.values()) - : []; + getAllSelectors(bits = 0) { + const out = { + declarative: [], + exceptions: this.exceptedCSSRules, + }; + const hasProcedural = this.proceduralFilterer instanceof Object; + const includePrivateSelectors = (bits & 0b01) !== 0; + const masterToken = hasProcedural + ? `[${this.proceduralFilterer.masterToken}]` + : undefined; + for ( const entry of this.filterset ) { + const selectors = entry.selectors; + if ( + includePrivateSelectors === false && + masterToken !== undefined && + selectors.startsWith(masterToken) + ) { + continue; + } + out.declarative.push([ selectors, entry.declarations ]); + } + const excludeProcedurals = (bits & 0b10) !== 0; + if ( excludeProcedurals !== true ) { + out.procedural = hasProcedural + ? Array.from(this.proceduralFilterer.selectors.values()) + : []; + } return out; } getAllExceptionSelectors() { return this.exceptions.join(',\n'); } - - getFilteredElementCount() { - const details = this.getAllSelectors_(true); - if ( Array.isArray(details.declarative) === false ) { return 0; } - const selectors = details.declarative.map(entry => entry[0]); - if ( selectors.length === 0 ) { return 0; } - return document.querySelectorAll(selectors.join(',\n')).length; - } }; } @@ -1548,29 +1530,11 @@ vAPI.injectScriptlet = function(doc, text) { let mustCommit = false; if ( result ) { - let selectors = result.simple; - if ( Array.isArray(selectors) && selectors.length !== 0 ) { - domFilterer.addCSSRule( - selectors, - 'display:none!important;', - { type: 'simple' } - ); - mustCommit = true; - } - selectors = result.complex; - if ( Array.isArray(selectors) && selectors.length !== 0 ) { - domFilterer.addCSSRule( - selectors, - 'display:none!important;', - { type: 'complex' } - ); - mustCommit = true; - } - selectors = result.injected; + let selectors = result.injected; if ( typeof selectors === 'string' && selectors.length !== 0 ) { domFilterer.addCSSRule( selectors, - 'display:none!important;', + vAPI.hideStyle, { injected: true } ); mustCommit = true; @@ -1740,37 +1704,15 @@ vAPI.injectScriptlet = function(doc, text) { vAPI.domSurveyor = null; } domFilterer.exceptions = cfeDetails.exceptionFilters; - domFilterer.hideNodeAttr = cfeDetails.hideNodeAttr; - domFilterer.hideNodeStyleSheetInjected = - cfeDetails.hideNodeStyleSheetInjected === true; - domFilterer.addCSSRule( - cfeDetails.declarativeFilters, - 'display:none!important;' - ); - domFilterer.addCSSRule( - cfeDetails.highGenericHideSimple, - 'display:none!important;', - { type: 'simple', lazy: true } - ); - domFilterer.addCSSRule( - cfeDetails.highGenericHideComplex, - 'display:none!important;', - { type: 'complex', lazy: true } - ); domFilterer.addCSSRule( cfeDetails.injectedHideFilters, - 'display:none!important;', + vAPI.hideStyle, { injected: true } ); domFilterer.addProceduralSelectors(cfeDetails.proceduralFilters); domFilterer.exceptCSSRules(cfeDetails.exceptedFilters); } - if ( cfeDetails.networkFilters.length !== 0 ) { - vAPI.userStylesheet.add( - cfeDetails.networkFilters + '\n{display:none!important;}'); - } - vAPI.userStylesheet.apply(); // Library of resources is located at: diff --git a/src/js/cosmetic-filtering.js b/src/js/cosmetic-filtering.js index a1ce10b34..392b4cfe2 100644 --- a/src/js/cosmetic-filtering.js +++ b/src/js/cosmetic-filtering.js @@ -32,12 +32,6 @@ const cosmeticSurveyingMissCountMax = parseInt(vAPI.localStorage.getItem('cosmeticSurveyingMissCountMax'), 10) || 15; -let supportsUserStylesheets = vAPI.webextFlavor.soup.has('user_stylesheet'); -// https://www.reddit.com/r/uBlockOrigin/comments/8dkvqn/116_broken_loading_custom_filters_from_my_filters/ -window.addEventListener('webextFlavor', function() { - supportsUserStylesheets = vAPI.webextFlavor.soup.has('user_stylesheet'); -}, { once: true }); - /******************************************************************************/ /******************************************************************************/ @@ -754,9 +748,9 @@ FilterContainer.prototype.triggerSelectorCachePruner = function() { /******************************************************************************/ FilterContainer.prototype.addToSelectorCache = function(details) { - let hostname = details.hostname; + const hostname = details.hostname; if ( typeof hostname !== 'string' || hostname === '' ) { return; } - let selectors = details.selectors; + const selectors = details.selectors; if ( Array.isArray(selectors) === false ) { return; } let entry = this.selectorCache.get(hostname); if ( entry === undefined ) { @@ -838,14 +832,6 @@ FilterContainer.prototype.pruneSelectorCacheAsync = function() { /******************************************************************************/ -FilterContainer.prototype.randomAlphaToken = function() { - const now = Date.now(); - return String.fromCharCode(now % 26 + 97) + - Math.floor((1 + Math.random()) * now).toString(36); -}; - -/******************************************************************************/ - FilterContainer.prototype.getSession = function() { return this.sessionFilterDB; }; @@ -912,57 +898,38 @@ FilterContainer.prototype.retrieveGenericSelectors = function(request) { return; } - const out = { - simple: Array.from(simpleSelectors), - complex: Array.from(complexSelectors), - injected: '', - excepted, - }; + const out = { injected: '', excepted, }; - // Important: always clear used registers before leaving. - simpleSelectors.clear(); - complexSelectors.clear(); + const injected = []; + if ( simpleSelectors.size !== 0 ) { + injected.push(Array.from(simpleSelectors).join(',\n')); + simpleSelectors.clear(); + } + if ( complexSelectors.size !== 0 ) { + injected.push(Array.from(complexSelectors).join(',\n')); + complexSelectors.clear(); + } - // Cache and inject (if user stylesheets supported) looked-up low generic - // cosmetic filters. - if ( - (typeof request.hostname === 'string' && request.hostname !== '') && - (out.simple.length !== 0 || out.complex.length !== 0) - ) { + // Cache and inject looked-up low generic cosmetic filters. + if ( injected.length === 0 ) { return out; } + + if ( typeof request.hostname === 'string' && request.hostname !== '' ) { this.addToSelectorCache({ cost: request.surveyCost || 0, hostname: request.hostname, injectedHideFilters: '', - selectors: out.simple.concat(out.complex), - type: 'cosmetic' + selectors: injected, + type: 'cosmetic', }); } - // If user stylesheets are supported in the current process, inject the - // cosmetic filters now. - if ( - supportsUserStylesheets && - request.tabId !== undefined && - request.frameId !== undefined - ) { - const injected = []; - if ( out.simple.length !== 0 ) { - injected.push(out.simple.join(',\n')); - out.simple = []; - } - if ( out.complex.length !== 0 ) { - injected.push(out.complex.join(',\n')); - out.complex = []; - } - out.injected = injected.join(',\n'); - vAPI.tabs.insertCSS(request.tabId, { - code: out.injected + '\n{display:none!important;}', - cssOrigin: 'user', - frameId: request.frameId, - matchAboutBlank: true, - runAt: 'document_start', - }); - } + out.injected = injected.join(',\n'); + vAPI.tabs.insertCSS(request.tabId, { + code: out.injected + '\n{display:none!important;}', + frameId: request.frameId, + matchAboutBlank: true, + runAt: 'document_start', + }); //console.timeEnd('cosmeticFilteringEngine.retrieveGenericSelectors'); @@ -989,18 +956,11 @@ FilterContainer.prototype.retrieveSpecificSelectors = function( ready: this.frozen, hostname: hostname, domain: request.domain, - declarativeFilters: [], exceptionFilters: [], exceptedFilters: [], - hideNodeAttr: this.randomAlphaToken(), - hideNodeStyleSheetInjected: false, - highGenericHideSimple: '', - highGenericHideComplex: '', - injectedHideFilters: '', - networkFilters: '', noDOMSurveying: this.needDOMSurveyor === false, - proceduralFilters: [] }; + const injectedHideFilters = []; if ( options.noCosmeticFiltering !== true ) { const specificSet = this.$specificSet; @@ -1063,7 +1023,7 @@ FilterContainer.prototype.retrieveSpecificSelectors = function( } if ( specificSet.size !== 0 ) { - out.declarativeFilters = Array.from(specificSet); + injectedHideFilters.push(Array.from(specificSet).join(',\n')); } if ( proceduralSet.size !== 0 ) { out.proceduralFilters = Array.from(proceduralSet); @@ -1078,10 +1038,10 @@ FilterContainer.prototype.retrieveSpecificSelectors = function( // string in memory, which I have observed occurs when the string is // stored directly as a value in a Map. if ( options.noGenericCosmeticFiltering !== true ) { - const exceptionHash = out.exceptionFilters.join(); - for ( const type in this.highlyGeneric ) { - const entry = this.highlyGeneric[type]; - let str = entry.mru.lookup(exceptionHash); + const exceptionSetHash = out.exceptionFilters.join(); + for ( const key in this.highlyGeneric ) { + const entry = this.highlyGeneric[key]; + let str = entry.mru.lookup(exceptionSetHash); if ( str === undefined ) { str = { s: entry.str, excepted: [] }; let genericSet = entry.dict; @@ -1098,13 +1058,14 @@ FilterContainer.prototype.retrieveSpecificSelectors = function( } str.s = Array.from(genericSet).join(',\n'); } - entry.mru.add(exceptionHash, str); + entry.mru.add(exceptionSetHash, str); } - out[entry.canonical] = str.s; if ( str.excepted.length !== 0 ) { out.exceptedFilters.push(...str.excepted); } - + if ( str.s.length !== 0 ) { + injectedHideFilters.push(str.s); + } } } @@ -1115,55 +1076,27 @@ FilterContainer.prototype.retrieveSpecificSelectors = function( dummySet.clear(); } + const details = { + code: '', + frameId: request.frameId, + matchAboutBlank: true, + runAt: 'document_start', + }; + + if ( injectedHideFilters.length !== 0 ) { + out.injectedHideFilters = injectedHideFilters.join(',\n'); + details.code = out.injectedHideFilters + '\n{display:none!important;}'; + vAPI.tabs.insertCSS(request.tabId, details); + } + // CSS selectors for collapsible blocked elements if ( cacheEntry ) { const networkFilters = []; cacheEntry.retrieve('net', networkFilters); - out.networkFilters = networkFilters.join(',\n'); - } - - // https://github.com/gorhill/uBlock/issues/3160 - // If user stylesheets are supported in the current process, inject the - // cosmetic filters now. - if ( - supportsUserStylesheets && - request.tabId !== undefined && - request.frameId !== undefined - ) { - const injectedHideFilters = []; - if ( out.declarativeFilters.length !== 0 ) { - injectedHideFilters.push(out.declarativeFilters.join(',\n')); - out.declarativeFilters = []; - } - if ( out.proceduralFilters.length !== 0 ) { - injectedHideFilters.push('[' + out.hideNodeAttr + ']'); - out.hideNodeStyleSheetInjected = true; - } - if ( out.highGenericHideSimple.length !== 0 ) { - injectedHideFilters.push(out.highGenericHideSimple); - out.highGenericHideSimple = ''; - } - if ( out.highGenericHideComplex.length !== 0 ) { - injectedHideFilters.push(out.highGenericHideComplex); - out.highGenericHideComplex = ''; - } - out.injectedHideFilters = injectedHideFilters.join(',\n'); - const details = { - code: '', - cssOrigin: 'user', - frameId: request.frameId, - matchAboutBlank: true, - runAt: 'document_start', - }; - if ( out.injectedHideFilters.length !== 0 ) { - details.code = out.injectedHideFilters + '\n{display:none!important;}'; + if ( networkFilters.length !== 0 ) { + details.code = networkFilters.join('\n') + '\n{display:none!important;}'; vAPI.tabs.insertCSS(request.tabId, details); } - if ( out.networkFilters.length !== 0 ) { - details.code = out.networkFilters + '\n{display:none!important;}'; - vAPI.tabs.insertCSS(request.tabId, details); - out.networkFilters = ''; - } } return out; diff --git a/src/js/epicker-ui.js b/src/js/epicker-ui.js index 2e0014ea0..d7fd9e3eb 100644 --- a/src/js/epicker-ui.js +++ b/src/js/epicker-ui.js @@ -56,7 +56,7 @@ if ( epickerId === null ) { return; } let epickerConnectionId; let filterHostname = ''; let filterOrigin = ''; -let filterResultset = []; +let resultsetOpt; /******************************************************************************/ @@ -88,11 +88,8 @@ const userFilterFromCandidate = function(filter) { opts.push(`domain=${filterHostname}`); } - if ( filterResultset.length !== 0 ) { - const item = filterResultset[0]; - if ( item.opts ) { - opts.push(item.opts); - } + if ( resultsetOpt !== undefined ) { + opts.push(resultsetOpt); } if ( opts.length ) { @@ -632,10 +629,10 @@ const onPickerMessage = function(msg) { case 'showDialog': showDialog(msg); break; - case 'filterResultset': { - filterResultset = msg.resultset; - $id('resultsetCount').textContent = filterResultset.length; - if ( filterResultset.length !== 0 ) { + case 'resultsetDetails': { + resultsetOpt = msg.opt; + $id('resultsetCount').textContent = msg.count; + if ( msg.count !== 0 ) { $id('create').removeAttribute('disabled'); } else { $id('create').setAttribute('disabled', ''); diff --git a/src/js/scriptlets/cosmetic-logger.js b/src/js/scriptlets/cosmetic-logger.js index ca17d5321..53a626eed 100644 --- a/src/js/scriptlets/cosmetic-logger.js +++ b/src/js/scriptlets/cosmetic-logger.js @@ -278,7 +278,11 @@ const handlers = { continue; } const details = JSON.parse(selector); - if ( details.action === 'style' ) { + if ( + details.action !== undefined && + details.tasks === undefined && + details.action[0] === ':style' + ) { exceptionDict.set(details.selector, details.raw); continue; } diff --git a/src/js/scriptlets/dom-inspector.js b/src/js/scriptlets/dom-inspector.js index c4cfc8368..3cd8740b4 100644 --- a/src/js/scriptlets/dom-inspector.js +++ b/src/js/scriptlets/dom-inspector.js @@ -501,16 +501,15 @@ const cosmeticFilterMapper = (function() { } const nodesFromStyleTag = function(rootNode) { - var filterMap = roRedNodes, - entry, selector, canonical, nodes, node; - - var details = vAPI.domFilterer.getAllSelectors(); + const filterMap = roRedNodes; + const details = vAPI.domFilterer.getAllSelectors(); // Declarative selectors. - for ( entry of (details.declarative || []) ) { - for ( selector of entry[0].split(',\n') ) { - canonical = selector; - if ( entry[1] !== 'display:none!important;' ) { + for ( const entry of (details.declarative || []) ) { + for ( const selector of entry[0].split(',\n') ) { + let canonical = selector; + let nodes; + if ( entry[1] !== vAPI.hideStyle ) { canonical += ':style(' + entry[1] + ')'; } if ( reHasCSSCombinators.test(selector) ) { @@ -524,7 +523,7 @@ const cosmeticFilterMapper = (function() { } nodes = rootNode.querySelectorAll(selector); } - for ( node of nodes ) { + for ( const node of nodes ) { if ( filterMap.has(node) === false ) { filterMap.set(node, canonical); } @@ -533,9 +532,9 @@ const cosmeticFilterMapper = (function() { } // Procedural selectors. - for ( entry of (details.procedural || []) ) { - nodes = entry.exec(); - for ( node of nodes ) { + for ( const entry of (details.procedural || []) ) { + const nodes = entry.exec(); + for ( const node of nodes ) { // Upgrade declarative selector to procedural one filterMap.set(node, entry.raw); } diff --git a/src/js/scriptlets/dom-survey-elements.js b/src/js/scriptlets/dom-survey-elements.js index 51b109d60..0386f3894 100644 --- a/src/js/scriptlets/dom-survey-elements.js +++ b/src/js/scriptlets/dom-survey-elements.js @@ -51,7 +51,7 @@ if ( isNaN(surveyResults.hiddenElementCount) ) { surveyResults.hiddenElementCount = (( ) => { if ( vAPI.domFilterer instanceof Object === false ) { return 0; } - const details = vAPI.domFilterer.getAllSelectors_(true); + const details = vAPI.domFilterer.getAllSelectors(0b11); if ( Array.isArray(details.declarative) === false || details.declarative.length === 0 @@ -59,7 +59,7 @@ return 0; } return document.querySelectorAll( - details.declarative.map(entry => entry[0]).join(',') + details.declarative.map(entry => entry[0]).join(',') ).length; })(); } diff --git a/src/js/scriptlets/epicker.js b/src/js/scriptlets/epicker.js index 7cb13a7e6..10a9ff920 100644 --- a/src/js/scriptlets/epicker.js +++ b/src/js/scriptlets/epicker.js @@ -49,6 +49,8 @@ const lastNetFilterSession = window.location.host + window.location.pathname; let lastNetFilterHostname = ''; let lastNetFilterUnion = ''; +const hideBackgroundStyle = 'background-image:none!important;'; + /******************************************************************************/ const safeQuerySelectorAll = function(node, selector) { @@ -645,10 +647,10 @@ const filterToDOMInterface = (( ) => { reFilter.test(elem.currentSrc) ) { out.push({ - type: 'network', - elem: elem, + elem, src: srcProp, - opts: filterTypes[elem.localName], + opt: filterTypes[elem.localName], + style: vAPI.hideStyle, }); } } @@ -657,10 +659,10 @@ const filterToDOMInterface = (( ) => { for ( const elem of candidateElements ) { if ( reFilter.test(backgroundImageURLFromElement(elem)) ) { out.push({ - type: 'network', - elem: elem, - style: 'background-image', - opts: 'image', + elem, + bg: true, + opt: 'image', + style: hideBackgroundStyle, }); } } @@ -690,7 +692,7 @@ const filterToDOMInterface = (( ) => { const out = []; for ( const elem of elems ) { if ( elem === pickerRoot ) { continue; } - out.push({ type: 'cosmetic', elem, raw }); + out.push({ elem, raw, style: vAPI.hideStyle }); } return out; }; @@ -702,33 +704,28 @@ const filterToDOMInterface = (( ) => { // Remove trailing pseudo-element when querying. const fromCompiledCosmeticFilter = function(raw) { if ( typeof raw !== 'string' ) { return; } - let elems; + let elems, style; try { const o = JSON.parse(raw); - if ( o.action === 'style' ) { - elems = document.querySelectorAll( - o.selector.replace(rePseudoElements, '') - ); - lastAction = o.selector + ' {' + o.tasks[0][1] + '}'; - } else if ( o.tasks ) { - elems = vAPI.domFilterer.createProceduralFilter(o).exec(); - } + elems = vAPI.domFilterer.createProceduralFilter(o).exec(); + style = o.action === undefined || o.action[0] !== ':style' + ? vAPI.hideStyle + : o.action[1]; } catch(ex) { return; } if ( !elems ) { return; } const out = []; for ( const elem of elems ) { - out.push({ type: 'cosmetic', elem, raw }); + out.push({ elem, raw, style }); } return out; }; + vAPI.epickerStyleProxies = vAPI.epickerStyleProxies || new Map(); + let lastFilter; let lastResultset; - let lastAction; - let appliedStyleTag; - let applied = false; let previewing = false; const queryAll = function(details) { @@ -738,11 +735,10 @@ const filterToDOMInterface = (( ) => { unapply(); if ( filter === '' || filter === '!' ) { lastFilter = ''; - lastResultset = []; - return lastResultset; + lastResultset = undefined; + return; } lastFilter = filter; - lastAction = undefined; if ( filter.startsWith('##') === false ) { lastResultset = fromNetworkFilter(filter); if ( previewing ) { apply(); } @@ -759,86 +755,29 @@ const filterToDOMInterface = (( ) => { return lastResultset; }; - // https://github.com/gorhill/uBlock/issues/1629 - // Avoid hiding the element picker's related elements. - const applyHide = function() { - const htmlElem = document.documentElement; - for ( const item of lastResultset ) { - const elem = item.elem; - if ( elem === pickerRoot ) { continue; } - if ( - (elem !== htmlElem) && - (item.type === 'cosmetic' || item.type === 'network' && item.src !== undefined) - ) { - vAPI.domFilterer.hideNode(elem); - item.hidden = true; - } - if ( item.type === 'network' && item.style === 'background-image' ) { - const style = elem.style; - item.backgroundImage = style.getPropertyValue('background-image'); - item.backgroundImagePriority = style.getPropertyPriority('background-image'); - style.setProperty('background-image', 'none', 'important'); - } - } - }; - - const unapplyHide = function() { - if ( lastResultset === undefined ) { return; } - for ( const item of lastResultset ) { - if ( item.hidden === true ) { - vAPI.domFilterer.unhideNode(item.elem); - item.hidden = false; - } - if ( item.hasOwnProperty('backgroundImage') ) { - item.elem.style.setProperty( - 'background-image', - item.backgroundImage, - item.backgroundImagePriority - ); - delete item.backgroundImage; - } - } - }; - - const unapplyStyle = function() { - if ( !appliedStyleTag || appliedStyleTag.parentNode === null ) { - return; - } - appliedStyleTag.parentNode.removeChild(appliedStyleTag); - }; - - const applyStyle = function() { - if ( !appliedStyleTag ) { - appliedStyleTag = document.createElement('style'); - appliedStyleTag.setAttribute('type', 'text/css'); - } - appliedStyleTag.textContent = lastAction; - if ( appliedStyleTag.parentNode === null ) { - document.head.appendChild(appliedStyleTag); - } - }; - const apply = function() { - if ( applied ) { - unapply(); + unapply(); + if ( Array.isArray(lastResultset) === false ) { return; } + const rootElem = document.documentElement; + for ( const { elem, style } of lastResultset ) { + if ( elem === pickerRoot ) { continue; } + if ( elem === rootElem && style === vAPI.hideStyle ) { continue; } + let styleToken = vAPI.epickerStyleProxies.get(style); + if ( styleToken === undefined ) { + styleToken = vAPI.randomToken(); + vAPI.epickerStyleProxies.set(style, styleToken); + vAPI.userStylesheet.add(`[${styleToken}]\n{${style}}`, true); + } + elem.setAttribute(styleToken, ''); } - if ( lastResultset === undefined ) { return; } - if ( typeof lastAction === 'string' ) { - applyStyle(); - } else { - applyHide(); - } - applied = true; }; const unapply = function() { - if ( !applied ) { return; } - if ( typeof lastAction === 'string' ) { - unapplyStyle(); - } else { - unapplyHide(); + for ( const styleToken of vAPI.epickerStyleProxies.values() ) { + for ( const elem of document.querySelectorAll(`[${styleToken}]`) ) { + elem.removeAttribute(styleToken); + } } - applied = false; }; // https://www.reddit.com/r/uBlockOrigin/comments/c62irc/ @@ -849,24 +788,24 @@ const filterToDOMInterface = (( ) => { if ( previewing === false ) { return unapply(); } - if ( lastResultset === undefined ) { return; } - apply(); - if ( permanent === false ) { return; } + if ( Array.isArray(lastResultset) === false ) { return; } + if ( permanent === false || lastFilter.startsWith('##') === false ) { + return apply(); + } if ( vAPI.domFilterer instanceof Object === false ) { return; } const cssSelectors = new Set(); const proceduralSelectors = new Set(); - for ( const item of lastResultset ) { - if ( item.type !== 'cosmetic' ) { continue; } - if ( item.raw.startsWith('{') ) { - proceduralSelectors.add(item.raw); + for ( const { raw } of lastResultset ) { + if ( raw.startsWith('{') ) { + proceduralSelectors.add(raw); } else { - cssSelectors.add(item.raw); + cssSelectors.add(raw); } } if ( cssSelectors.size !== 0 ) { vAPI.domFilterer.addCSSRule( Array.from(cssSelectors), - 'display:none!important;' + vAPI.hideStyle ); } if ( proceduralSelectors.size !== 0 ) { @@ -876,11 +815,7 @@ const filterToDOMInterface = (( ) => { } }; - return { - get previewing() { return previewing; }, - preview, - queryAll, - }; + return { preview, queryAll }; })(); /******************************************************************************/ @@ -1091,11 +1026,6 @@ const quitPicker = function() { if ( pickerRoot === null ) { return; } - // https://github.com/gorhill/uBlock/issues/2060 - if ( vAPI.domFilterer instanceof Object ) { - vAPI.domFilterer.unexcludeNode(pickerRoot); - } - pickerRoot.remove(); pickerRoot = null; @@ -1118,16 +1048,13 @@ const onDialogMessage = function(msg) { quitPicker(); break; case 'dialogSetFilter': { - const resultset = filterToDOMInterface.queryAll(msg); + const resultset = filterToDOMInterface.queryAll(msg) || []; highlightElements(resultset.map(a => a.elem), true); if ( msg.filter === '!' ) { break; } vAPI.MessagingConnection.sendTo(epickerConnectionId, { - what: 'filterResultset', - resultset: resultset.map(a => { - const o = Object.assign({}, a); - o.elem = undefined; - return o; - }), + what: 'resultsetDetails', + count: resultset.length, + opt: resultset.length !== 0 ? resultset[0].opt : undefined, }); break; } @@ -1250,7 +1177,7 @@ const pickerCSSStyle = [ ].join(' !important;'); const pickerCSS = ` -:root [${vAPI.sessionId}] { +:root > [${vAPI.sessionId}] { ${pickerCSSStyle} } :root [${vAPI.sessionId}-clickblind] { @@ -1265,11 +1192,6 @@ pickerRoot = document.createElement('iframe'); pickerRoot.setAttribute(vAPI.sessionId, ''); document.documentElement.append(pickerRoot); -// https://github.com/gorhill/uBlock/issues/2060 -if ( vAPI.domFilterer instanceof Object ) { - vAPI.domFilterer.excludeNode(pickerRoot); -} - vAPI.shutdown.add(quitPicker); vAPI.MessagingConnection.addListener(onConnectionMessage); diff --git a/src/js/static-filtering-parser.js b/src/js/static-filtering-parser.js index 1a7a6ddf1..143c06aa1 100644 --- a/src/js/static-filtering-parser.js +++ b/src/js/static-filtering-parser.js @@ -1177,7 +1177,7 @@ Parser.prototype.SelectorCompiler = class { ]); this.reSimpleSelector = /^[#.][A-Za-z_][\w-]*$/; this.div = document.createElement('div'); - this.rePseudoClass = /:(?::?after|:?before|:-?[a-z][a-z-]*[a-z])$/; + this.rePseudoElement = /:(?::?after|:?before|:-?[a-z][a-z-]*[a-z])$/; this.reProceduralOperator = new RegExp([ '^(?:', Array.from(parser.proceduralOperatorTokens.keys()).join('|'), @@ -1296,7 +1296,7 @@ Parser.prototype.SelectorCompiler = class { // is fixed. cssSelectorType(s) { if ( this.reSimpleSelector.test(s) ) { return 1; } - const pos = this.cssPseudoSelector(s); + const pos = this.cssPseudoElement(s); if ( pos !== -1 ) { return this.cssSelectorType(s.slice(0, pos)) === 1 ? 3 : 0; } @@ -1308,9 +1308,9 @@ Parser.prototype.SelectorCompiler = class { return 1; } - cssPseudoSelector(s) { + cssPseudoElement(s) { if ( s.lastIndexOf(':') === -1 ) { return -1; } - const match = this.rePseudoClass.exec(s); + const match = this.rePseudoElement.exec(s); return match !== null ? match.index : -1; } @@ -1450,13 +1450,10 @@ Parser.prototype.SelectorCompiler = class { // The normalized string version is what is reported in the logger, // by design. decompileProcedural(compiled) { - const tasks = compiled.tasks; - if ( Array.isArray(tasks) === false ) { - return compiled.selector; - } + const tasks = compiled.tasks || []; const raw = [ compiled.selector ]; - let value; for ( const task of tasks ) { + let value; switch ( task[0] ) { case ':has': case ':if': @@ -1494,8 +1491,6 @@ Parser.prototype.SelectorCompiler = class { raw.push(task[1]); break; case ':min-text-length': - case ':remove': - case ':style': case ':upward': case ':watch-attr': case ':xpath': @@ -1503,6 +1498,10 @@ Parser.prototype.SelectorCompiler = class { break; } } + if ( Array.isArray(compiled.action) ) { + const [ op, arg ] = compiled.action; + raw.push(`${op}(${arg})`); + } return raw.join(''); } @@ -1578,10 +1577,12 @@ Parser.prototype.SelectorCompiler = class { tasks.push([ ':spath', spath ]); } if ( action !== undefined ) { return; } - tasks.push([ operator, args ]); + const task = [ operator, args ]; if ( this.actionOperators.has(operator) ) { if ( root === false ) { return; } - action = operator.slice(1); + action = task; + } else { + tasks.push(task); } opPrefixBeg = i; if ( i === n ) { break; } @@ -1589,7 +1590,7 @@ Parser.prototype.SelectorCompiler = class { // No task found: then we have a CSS selector. // At least one task found: nothing should be left to parse. - if ( tasks.length === 0 ) { + if ( tasks.length === 0 && action === undefined ) { prefix = raw; } else if ( opPrefixBeg < n ) { if ( action !== undefined ) { return; } @@ -1626,21 +1627,17 @@ Parser.prototype.SelectorCompiler = class { } // Expose action to take in root descriptor. - // - // https://github.com/uBlockOrigin/uBlock-issues/issues/961 - // https://github.com/uBlockOrigin/uBlock-issues/issues/382 - // For the time being, `style` action can't be used in a - // procedural selector. if ( action !== undefined ) { - if ( tasks.length > 1 && action === 'style' ) { return; } out.action = action; } - // Pseudo-selectors are valid only when used in a root task list. + // Pseudo elements are valid only when used in a root task list AND + // only when there are no procedural operators: pseudo elements can't + // be querySelectorAll-ed. if ( prefix !== '' ) { - const pos = this.cssPseudoSelector(prefix); + const pos = this.cssPseudoElement(prefix); if ( pos !== -1 ) { - if ( root === false ) { return; } + if ( root === false || tasks.length !== 0 ) { return; } out.pseudo = pos; } }