From 578fc21bd9c69f58ab4d7121159ae39e2e981763 Mon Sep 17 00:00:00 2001 From: Raymond Hill Date: Tue, 9 May 2023 12:44:14 -0400 Subject: [PATCH] Report injected scriptlets in troubleshooting information This requires to rewrite portions of scriptlet filtering code. --- platform/chromium/vapi-background-ext.js | 6 +- platform/common/vapi-background.js | 2 +- platform/firefox/vapi-background-ext.js | 9 +- src/js/contentscript.js | 14 +-- src/js/messaging.js | 21 +++- src/js/scriptlet-filtering.js | 123 ++++++++++------------- src/js/scriptlets/cosmetic-report.js | 4 + 7 files changed, 94 insertions(+), 85 deletions(-) diff --git a/platform/chromium/vapi-background-ext.js b/platform/chromium/vapi-background-ext.js index b5771a711..ef372c981 100644 --- a/platform/chromium/vapi-background-ext.js +++ b/platform/chromium/vapi-background-ext.js @@ -236,13 +236,13 @@ vAPI.prefetching = (( ) => { /******************************************************************************/ -vAPI.scriptletsInjector = ((doc, scriptlets) => { +vAPI.scriptletsInjector = ((doc, details) => { let script; try { script = doc.createElement('script'); - script.appendChild(doc.createTextNode(scriptlets)); + script.appendChild(doc.createTextNode(details.scriptlets)); (doc.head || doc.documentElement).appendChild(script); - self.uBO_scriptletsInjected = true; + self.uBO_scriptletsInjected = details.filters; } catch (ex) { } if ( script ) { diff --git a/platform/common/vapi-background.js b/platform/common/vapi-background.js index 4fcf40418..1a5503953 100644 --- a/platform/common/vapi-background.js +++ b/platform/common/vapi-background.js @@ -1391,7 +1391,7 @@ vAPI.Net = class { // To be defined by platform-specific code. vAPI.scriptletsInjector = (( ) => { - self.uBO_scriptletsInjected = true; + self.uBO_scriptletsInjected = ''; }).toString(); /******************************************************************************/ diff --git a/platform/firefox/vapi-background-ext.js b/platform/firefox/vapi-background-ext.js index bcca2440a..3d61746cb 100644 --- a/platform/firefox/vapi-background-ext.js +++ b/platform/firefox/vapi-background-ext.js @@ -304,16 +304,19 @@ vAPI.Net = class extends vAPI.Net { /******************************************************************************/ -vAPI.scriptletsInjector = ((doc, scriptlets) => { +vAPI.scriptletsInjector = ((doc, details) => { let script, url; try { - const blob = new self.Blob([ scriptlets ], { type: 'text/javascript; charset=utf-8' }); + const blob = new self.Blob( + [ details.scriptlets ], + { type: 'text/javascript; charset=utf-8' } + ); url = self.URL.createObjectURL(blob); script = doc.createElement('script'); script.async = false; script.src = url; (doc.head || doc.documentElement).appendChild(script); - self.uBO_scriptletsInjected = true; + self.uBO_scriptletsInjected = details.filters; } catch (ex) { } if ( url ) { diff --git a/src/js/contentscript.js b/src/js/contentscript.js index 6244a7030..5c00cd454 100644 --- a/src/js/contentscript.js +++ b/src/js/contentscript.js @@ -1298,7 +1298,7 @@ vAPI.DOMFilterer = class { const { noSpecificCosmeticFiltering, noGenericCosmeticFiltering, - scriptlets, + scriptletDetails, } = response; vAPI.noSpecificCosmeticFiltering = noSpecificCosmeticFiltering; @@ -1322,10 +1322,12 @@ vAPI.DOMFilterer = class { // Library of resources is located at: // https://github.com/gorhill/uBlock/blob/master/assets/ublock/resources.txt - if ( scriptlets && typeof self.uBO_scriptletsInjected !== 'boolean' ) { - self.uBO_scriptletsInjected = true; - vAPI.injectScriptlet(document, scriptlets); - vAPI.injectedScripts = scriptlets; + if ( scriptletDetails && typeof self.uBO_scriptletsInjected !== 'string' ) { + self.uBO_scriptletsInjected = scriptletDetails.filters; + if ( scriptletDetails.scriptlets ) { + vAPI.injectScriptlet(document, scriptletDetails.scriptlets); + vAPI.injectedScripts = scriptletDetails.scriptlets; + } } if ( vAPI.domSurveyor ) { @@ -1346,7 +1348,7 @@ vAPI.DOMFilterer = class { vAPI.messaging.send('contentscript', { what: 'retrieveContentScriptParameters', url: vAPI.effectiveSelf.location.href, - needScriptlets: typeof self.uBO_scriptletsInjected !== 'boolean', + needScriptlets: typeof self.uBO_scriptletsInjected !== 'string', }).then(response => { onResponseReady(response); }); diff --git a/src/js/messaging.js b/src/js/messaging.js index 29339ac19..d3db84f83 100644 --- a/src/js/messaging.js +++ b/src/js/messaging.js @@ -623,8 +623,11 @@ const launchReporter = async function(request) { if ( Array.isArray(v) ) { a.push(...v); } return a; }, []); + // Remove duplicate, truncate too long filters. if ( filters.length !== 0 ) { - request.popupPanel.cosmetic = filters; + request.popupPanel.extended = Array.from( + new Set(filters.map(s => s.length <= 64 ? s : `${s.slice(0, 64)}…`)) + ); } const supportURL = new URL(vAPI.getURL('support.html')); @@ -833,8 +836,20 @@ const retrieveContentScriptParameters = async function(sender, request) { // https://github.com/uBlockOrigin/uBlock-issues/issues/688#issuecomment-748179731 // For non-network URIs, scriptlet injection is deferred to here. The // effective URL is available here in `request.url`. - if ( request.needScriptlets ) { - response.scriptlets = scriptletFilteringEngine.injectNow(request); + if ( logger.enabled || request.needScriptlets ) { + const scriptletDetails = scriptletFilteringEngine.injectNow(request); + if ( scriptletDetails !== undefined ) { + if ( logger.enabled ) { + scriptletFilteringEngine.logFilters( + tabId, + request.url, + scriptletDetails.filters + ); + } + if ( request.needScriptlets ) { + response.scriptletDetails = scriptletDetails; + } + } } // https://github.com/NanoMeow/QuickReports/issues/6#issuecomment-414516623 diff --git a/src/js/scriptlet-filtering.js b/src/js/scriptlet-filtering.js index b28166d0d..c9f023b23 100644 --- a/src/js/scriptlet-filtering.js +++ b/src/js/scriptlet-filtering.js @@ -23,7 +23,6 @@ /******************************************************************************/ -import logger from './logger.js'; import µb from './background.js'; import { redirectEngine } from './redirect-engine.js'; import { sessionFirewall } from './filtering-engines.js'; @@ -86,31 +85,32 @@ const scriptletFilteringEngine = { const contentscriptCode = (( ) => { const parts = [ '(', - function(injector, hostname, scriptlets) { + function(injector, details) { const doc = document; if ( doc.location === null || - hostname !== doc.location.hostname || - typeof self.uBO_scriptletsInjected === 'boolean' + details.hostname !== doc.location.hostname || + typeof self.uBO_scriptletsInjected === 'string' ) { return; } - injector(doc, decodeURIComponent(scriptlets)); - if ( typeof self.uBO_scriptletsInjected === 'boolean' ) { return 0; } + injector(doc, details); + if ( typeof self.uBO_scriptletsInjected === 'string' ) { return 0; } }.toString(), ')(', vAPI.scriptletsInjector, ', ', - '"', 'hostname-slot', '", ', - '"', 'scriptlets-slot', '"', + 'json-slot', ');', ]; return { - parts: parts, - hostnameSlot: parts.indexOf('hostname-slot'), - scriptletsSlot: parts.indexOf('scriptlets-slot'), - assemble: function(hostname, scriptlets) { - this.parts[this.hostnameSlot] = hostname; - this.parts[this.scriptletsSlot] = encodeURIComponent(scriptlets); + parts, + jsonSlot: parts.indexOf('json-slot'), + assemble: function(hostname, scriptlets, filters) { + this.parts[this.jsonSlot] = JSON.stringify({ + hostname, + scriptlets, + filters, + }); return this.parts.join(''); } }; @@ -221,16 +221,18 @@ const patchScriptlet = function(content, args) { ); }; -const logOne = function(tabId, url, filter) { - µb.filteringContext - .duplicate() - .fromTabId(tabId) - .setRealm('extended') - .setType('scriptlet') - .setURL(url) - .setDocOriginFromURL(url) - .setFilter({ source: 'extended', raw: filter }) - .toLogger(); +scriptletFilteringEngine.logFilters = function(tabId, url, filters) { + if ( typeof filters !== 'string' ) { return; } + const fctxt = µb.filteringContext + .duplicate() + .fromTabId(tabId) + .setRealm('extended') + .setType('scriptlet') + .setURL(url) + .setDocOriginFromURL(url); + for ( const filter of filters.split('\n') ) { + fctxt.setFilter({ source: 'extended', raw: filter }).toLogger(); + } }; scriptletFilteringEngine.reset = function() { @@ -308,7 +310,7 @@ const $exceptions = new Set(); const $scriptletMap = new Map(); const $scriptletDependencyMap = new Map(); -scriptletFilteringEngine.retrieve = function(request, options = {}) { +scriptletFilteringEngine.retrieve = function(request) { if ( scriptletDB.size === 0 ) { return; } const hostname = request.hostname; @@ -332,14 +334,13 @@ scriptletFilteringEngine.retrieve = function(request, options = {}) { return; } - const mustLog = Array.isArray(options.logEntries); - // Wholly disable scriptlet injection? if ( $exceptions.has('') ) { - if ( mustLog ) { - logOne(request.tabId, request.url, '#@#+js()'); - } - return; + return { + filters: [ + { tabId: request.tabId, url: request.url, filter: '#@#+js()' } + ] + }; } if ( scriptletCache.resetTime < redirectEngine.modifyTime ) { @@ -349,45 +350,38 @@ scriptletFilteringEngine.retrieve = function(request, options = {}) { let cacheDetails = scriptletCache.lookup(hostname); if ( cacheDetails === undefined ) { const fullCode = []; + for ( const token of $exceptions ) { + if ( $scriptlets.has(token) ) { + $scriptlets.delete(token); + } else { + $exceptions.delete(token); + } + } for ( const token of $scriptlets ) { - if ( $exceptions.has(token) ) { continue; } lookupScriptlet(token, $scriptletMap, $scriptletDependencyMap); } for ( const token of $scriptlets ) { - const isException = $exceptions.has(token); - if ( isException === false ) { - fullCode.push($scriptletMap.get(token)); - } + fullCode.push($scriptletMap.get(token)); } for ( const code of $scriptletDependencyMap.values() ) { fullCode.push(code); } cacheDetails = { code: fullCode.join('\n\n'), - tokens: Array.from($scriptlets), - exceptions: Array.from($exceptions), + filters: [ + ...Array.from($scriptlets).map(s => `##+js(${s})`), + ...Array.from($exceptions).map(s => `#@#+js(${s})`), + ].join('\n'), }; scriptletCache.add(hostname, cacheDetails); $scriptletMap.clear(); $scriptletDependencyMap.clear(); } - if ( mustLog ) { - for ( const token of cacheDetails.tokens ) { - if ( cacheDetails.exceptions.includes(token) ) { - logOne(request.tabId, request.url, `#@#+js(${token})`); - } else { - options.logEntries.push({ - token: `##+js(${token})`, - tabId: request.tabId, - url: request.url, - }); - } - } + if ( cacheDetails.code === '' ) { + return { filters: cacheDetails.filters }; } - if ( cacheDetails.code === '' ) { return; } - const scriptletGlobals = []; if ( isDevBuild === undefined ) { @@ -412,7 +406,7 @@ scriptletFilteringEngine.retrieve = function(request, options = {}) { '})();', ]; - return out.join('\n'); + return { scriptlets: out.join('\n'), filters: cacheDetails.filters }; }; scriptletFilteringEngine.injectNow = function(details) { @@ -427,30 +421,21 @@ scriptletFilteringEngine.injectNow = function(details) { }; request.domain = domainFromHostname(request.hostname); request.entity = entityFromDomain(request.domain); - const logEntries = logger.enabled ? [] : undefined; - const scriptlets = this.retrieve(request, { logEntries }); - if ( scriptlets === undefined ) { return; } - let code = contentscriptCode.assemble(request.hostname, scriptlets); + const scriptletDetails = this.retrieve(request); + if ( scriptletDetails === undefined ) { return; } + const { scriptlets = '', filters } = scriptletDetails; + if ( scriptlets === '' ) { return scriptletDetails; } + let code = contentscriptCode.assemble(request.hostname, scriptlets, filters); if ( µb.hiddenSettings.debugScriptletInjector ) { code = 'debugger;\n' + code; } - const promise = vAPI.tabs.executeScript(details.tabId, { + vAPI.tabs.executeScript(details.tabId, { code, frameId: details.frameId, matchAboutBlank: true, runAt: 'document_start', }); - if ( logEntries !== undefined ) { - promise.then(results => { - if ( Array.isArray(results) === false || results[0] !== 0 ) { - return; - } - for ( const entry of logEntries ) { - logOne(entry.tabId, entry.url, entry.token); - } - }); - } - return scriptlets; + return scriptletDetails; }; scriptletFilteringEngine.toSelfie = function() { diff --git a/src/js/scriptlets/cosmetic-report.js b/src/js/scriptlets/cosmetic-report.js index 402b13ebd..9ef16f2bf 100644 --- a/src/js/scriptlets/cosmetic-report.js +++ b/src/js/scriptlets/cosmetic-report.js @@ -127,6 +127,10 @@ if ( Array.isArray(allSelectors.exceptions) ) { } } +if ( typeof self.uBO_scriptletsInjected === 'string' ) { + matchedSelectors.push(...self.uBO_scriptletsInjected.split('\n')); +} + if ( matchedSelectors.length === 0 ) { return; } return matchedSelectors;