From 433adac6ff40a1093aecda26ac29fde2017dfa28 Mon Sep 17 00:00:00 2001 From: Raymond Hill Date: Thu, 20 Oct 2022 15:48:06 -0400 Subject: [PATCH] [mv3] Add ability to handle entity-based CSS and scriptlet injection filters This commit adds the ability to inject entity-based plain CSS filters and also a set of the most commonly used entity-based scriptlet injection filters. Since the scripting API is not compatible with entity patterns, the entity-related content scripts are injected in all documents and the entity-matching is done by the content script themselves. Given this, entity-based content scripts are enabled only when working in the Complete filtering mode, there won't be any entity-based filters injected in lower modes. Also, since there is no way to reasonably have access to the Public Suffix List in the content scripts, the entity-matching algorithm is an approximation, though I expect false positives to be rare (time will tell). In the event of such false positive, simply falling back to Optimal mode will fix the issue. The following issues have been fixed at the same time: Fixed the no-filtering mode related rules having lower priority then redirect rules, i.e. redirect rules would still be applied despite disabling all filtering on a site. Fixed improper detection of changes to the generic-related CSS content script, potentially causing undue delays when for example trying to access the popup panel while working in Complete mode. The scripting MV3 can be quite slow when registering/updating large content scripts, so uBOL does its best to call the API only if really needed, but there had been a regression in the recent builds preventing uBO from properly detecting unchanged content script parameters. --- platform/mv3/extension/js/mode-manager.js | 1 + platform/mv3/extension/js/popup.js | 7 +- .../mv3/extension/js/scripting-manager.js | 136 +++++++- .../js/scripting/css-specific.entity.js | 86 +++++ platform/mv3/make-rulesets.js | 308 +++++++++++++++--- .../scriptlets/abort-current-script.entity.js | 181 ++++++++++ .../abort-on-property-read.entity.js | 139 ++++++++ .../abort-on-property-write.entity.js | 113 +++++++ .../mv3/scriptlets/css-specific.entity.js | 51 +++ .../no-addeventlistener-if.entity.js | 113 +++++++ .../mv3/scriptlets/no-settimeout-if.entity.js | 117 +++++++ .../mv3/scriptlets/no-windowopen-if.entity.js | 154 +++++++++ .../mv3/scriptlets/set-constant.entity.js | 199 +++++++++++ src/js/static-dnr-filtering.js | 6 +- src/js/static-net-filtering.js | 2 +- 15 files changed, 1549 insertions(+), 64 deletions(-) create mode 100644 platform/mv3/extension/js/scripting/css-specific.entity.js create mode 100644 platform/mv3/scriptlets/abort-current-script.entity.js create mode 100644 platform/mv3/scriptlets/abort-on-property-read.entity.js create mode 100644 platform/mv3/scriptlets/abort-on-property-write.entity.js create mode 100644 platform/mv3/scriptlets/css-specific.entity.js create mode 100644 platform/mv3/scriptlets/no-addeventlistener-if.entity.js create mode 100644 platform/mv3/scriptlets/no-settimeout-if.entity.js create mode 100644 platform/mv3/scriptlets/no-windowopen-if.entity.js create mode 100644 platform/mv3/scriptlets/set-constant.entity.js diff --git a/platform/mv3/extension/js/mode-manager.js b/platform/mv3/extension/js/mode-manager.js index e2a94a973..1b52f6cb9 100644 --- a/platform/mv3/extension/js/mode-manager.js +++ b/platform/mv3/extension/js/mode-manager.js @@ -134,6 +134,7 @@ async function setFilteringModeDetails(afterDetails) { requestDomains: [], resourceTypes: [ 'main_frame' ], }, + priority: 100, }; if ( actualDetails.none.size ) { rule.condition.requestDomains = Array.from(actualDetails.none); diff --git a/platform/mv3/extension/js/popup.js b/platform/mv3/extension/js/popup.js index f1aad0f6e..bd7ea7078 100644 --- a/platform/mv3/extension/js/popup.js +++ b/platform/mv3/extension/js/popup.js @@ -295,12 +295,17 @@ async function init() { if ( popupPanelData.hasOmnipotence ) { ruleCount += rules.removeparam + rules.redirect; } + let specificCount = 0; + if ( css.specific instanceof Object ) { + specificCount += css.specific.domainBased; + specificCount += css.specific.entityBased; + } dom.text( qs$('p', div), i18n$('perRulesetStats') .replace('{{ruleCount}}', ruleCount.toLocaleString()) .replace('{{filterCount}}', filters.accepted.toLocaleString()) - .replace('{{cssSpecificCount}}', css.specific.toLocaleString()) + .replace('{{cssSpecificCount}}', specificCount.toLocaleString()) ); parent.append(div); } diff --git a/platform/mv3/extension/js/scripting-manager.js b/platform/mv3/extension/js/scripting-manager.js index c23e16a60..b9fbfdd74 100644 --- a/platform/mv3/extension/js/scripting-manager.js +++ b/platform/mv3/extension/js/scripting-manager.js @@ -133,8 +133,9 @@ function registerGeneric(context, genericDetails) { if ( hostnames !== undefined ) { excludeHostnames.push(...hostnames); } + if ( details.css.generic instanceof Object === false ) { continue; } if ( details.css.generic.count === 0 ) { continue; } - js.push(`/rulesets/scripting/generic/${details.id}.generic.js`); + js.push(`/rulesets/scripting/generic/${details.id}.js`); } if ( js.length === 0 ) { return; } @@ -202,7 +203,7 @@ function registerProcedural(context, proceduralDetails) { const hostnameMatches = []; for ( const details of rulesetsDetails ) { if ( details.css.procedural === 0 ) { continue; } - js.push(`/rulesets/scripting/procedural/${details.id}.procedural.js`); + js.push(`/rulesets/scripting/procedural/${details.id}.js`); if ( proceduralDetails.has(details.id) ) { hostnameMatches.push(...proceduralDetails.get(details.id)); } @@ -278,7 +279,7 @@ function registerDeclarative(context, declarativeDetails) { const hostnameMatches = []; for ( const details of rulesetsDetails ) { if ( details.css.declarative === 0 ) { continue; } - js.push(`/rulesets/scripting/declarative/${details.id}.declarative.js`); + js.push(`/rulesets/scripting/declarative/${details.id}.js`); if ( declarativeDetails.has(details.id) ) { hostnameMatches.push(...declarativeDetails.get(details.id)); } @@ -420,6 +421,71 @@ function registerScriptlet(context, scriptletDetails) { /******************************************************************************/ +function registerScriptletEntity(context) { + const { before, filteringModeDetails, rulesetsDetails } = context; + + const js = []; + for ( const details of rulesetsDetails ) { + const { scriptlets } = details; + if ( scriptlets instanceof Object === false ) { continue; } + if ( Array.isArray(scriptlets.entityBasedTokens) === false ) { continue; } + if ( scriptlets.entityBasedTokens.length === 0 ) { continue; } + for ( const token of scriptlets.entityBasedTokens ) { + js.push(`/rulesets/scripting/scriptlet-entity/${details.id}.${token}.js`); + } + } + + if ( js.length === 0 ) { return; } + + const matches = []; + const excludeMatches = []; + if ( filteringModeDetails.extendedGeneric.has('all-urls') ) { + excludeMatches.push(...ut.matchesFromHostnames(filteringModeDetails.none)); + excludeMatches.push(...ut.matchesFromHostnames(filteringModeDetails.network)); + excludeMatches.push(...ut.matchesFromHostnames(filteringModeDetails.extendedSpecific)); + matches.push(''); + } else { + matches.push( + ...ut.matchesFromHostnames(filteringModeDetails.extendedGeneric) + ); + } + + if ( matches.length === 0 ) { return; } + + const registered = before.get('scriptlet.entity'); + before.delete('scriptlet.entity'); // Important! + + // register + if ( registered === undefined ) { + context.toAdd.push({ + id: 'scriptlet.entity', + js, + matches, + excludeMatches, + runAt: 'document_start', + world: 'MAIN', + }); + return; + } + + // update + const directive = { id: 'scriptlet.entity' }; + if ( arrayEq(registered.js, js, false) === false ) { + directive.js = js; + } + if ( arrayEq(registered.matches, matches) === false ) { + directive.matches = matches; + } + if ( arrayEq(registered.excludeMatches, excludeMatches) === false ) { + directive.excludeMatches = excludeMatches; + } + if ( directive.js || directive.matches || directive.excludeMatches ) { + context.toUpdate.push(directive); + } +} + +/******************************************************************************/ + function registerSpecific(context, specificDetails) { const { filteringModeDetails } = context; @@ -561,6 +627,68 @@ const toUpdatableScript = (context, fname, hostnames) => { /******************************************************************************/ +function registerSpecificEntity(context) { + const { before, filteringModeDetails, rulesetsDetails } = context; + + const js = []; + for ( const details of rulesetsDetails ) { + if ( details.css.specific instanceof Object === false ) { continue; } + if ( details.css.specific.entityBased === 0 ) { continue; } + js.push(`/rulesets/scripting/specific-entity/${details.id}.js`); + } + + if ( js.length === 0 ) { return; } + + const matches = []; + const excludeMatches = []; + if ( filteringModeDetails.extendedGeneric.has('all-urls') ) { + excludeMatches.push(...ut.matchesFromHostnames(filteringModeDetails.none)); + excludeMatches.push(...ut.matchesFromHostnames(filteringModeDetails.network)); + excludeMatches.push(...ut.matchesFromHostnames(filteringModeDetails.extendedSpecific)); + matches.push(''); + } else { + matches.push( + ...ut.matchesFromHostnames(filteringModeDetails.extendedGeneric) + ); + } + + if ( matches.length === 0 ) { return; } + + js.push('/js/scripting/css-specific.entity.js'); + + const registered = before.get('css-specific.entity'); + before.delete('css-specific.entity'); // Important! + + // register + if ( registered === undefined ) { + context.toAdd.push({ + id: 'css-specific.entity', + js, + matches, + excludeMatches, + runAt: 'document_start', + }); + return; + } + + // update + const directive = { id: 'css-specific.entity' }; + if ( arrayEq(registered.js, js, false) === false ) { + directive.js = js; + } + if ( arrayEq(registered.matches, matches) === false ) { + directive.matches = matches; + } + if ( arrayEq(registered.excludeMatches, excludeMatches) === false ) { + directive.excludeMatches = excludeMatches; + } + if ( directive.js || directive.matches || directive.excludeMatches ) { + context.toUpdate.push(directive); + } +} + +/******************************************************************************/ + async function registerInjectables(origins) { void origins; @@ -604,7 +732,9 @@ async function registerInjectables(origins) { registerDeclarative(context, declarativeDetails); registerProcedural(context, proceduralDetails); registerScriptlet(context, scriptletDetails); + registerScriptletEntity(context); registerSpecific(context, specificDetails); + registerSpecificEntity(context); registerGeneric(context, genericDetails); toRemove.push(...Array.from(before.keys())); diff --git a/platform/mv3/extension/js/scripting/css-specific.entity.js b/platform/mv3/extension/js/scripting/css-specific.entity.js new file mode 100644 index 000000000..9f4379533 --- /dev/null +++ b/platform/mv3/extension/js/scripting/css-specific.entity.js @@ -0,0 +1,86 @@ +/******************************************************************************* + + uBlock Origin - a browser extension to block requests. + Copyright (C) 2019-present Raymond Hill + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see {http://www.gnu.org/licenses/}. + + Home: https://github.com/gorhill/uBlock +*/ + +/* jshint esversion:11 */ + +'use strict'; + +/******************************************************************************/ + +// Important! +// Isolate from global scope +(function uBOL_cssSpecificEntity() { + +/******************************************************************************/ + +// $rulesetId$ + +const specificEntityImports = self.specificEntityImports || []; + +/******************************************************************************/ + +const lookupSelectors = (hn, entity, out) => { + for ( const { argsList, entitiesMap } of specificEntityImports ) { + let argsIndices = entitiesMap.get(entity); + if ( argsIndices === undefined ) { continue; } + if ( typeof argsIndices === 'number' ) { argsIndices = [ argsIndices ]; } + for ( const argsIndex of argsIndices ) { + const details = argsList[argsIndex]; + if ( details.n && details.n.includes(hn) ) { continue; } + out.push(details.a); + } + } +}; + +let hn = ''; +try { hn = document.location.hostname; } catch(ex) { } +const selectors = []; +const hnparts = hn.split('.'); +const hnpartslen = hnparts.length - 1; +for ( let i = 0; i < hnpartslen; i++ ) { + for ( let j = hnpartslen; j > i; j-- ) { + lookupSelectors( + hnparts.slice(i).join('.'), + hnparts.slice(i,j).join('.'), + selectors + ); + } +} + +self.specificEntityImports = undefined; + +if ( selectors.length === 0 ) { return; } + +try { + const sheet = new CSSStyleSheet(); + sheet.replace(`@layer{${selectors.join(',')}{display:none!important;}}`); + document.adoptedStyleSheets = [ + ...document.adoptedStyleSheets, + sheet + ]; +} catch(ex) { +} + +/******************************************************************************/ + +})(); + +/******************************************************************************/ diff --git a/platform/mv3/make-rulesets.js b/platform/mv3/make-rulesets.js index 6b446072c..7d7be63ce 100644 --- a/platform/mv3/make-rulesets.js +++ b/platform/mv3/make-rulesets.js @@ -441,7 +441,7 @@ async function processGenericCosmeticFilters(assetDetails, bucketsMap, exclusion ); writeFile( - `${scriptletDir}/generic/${assetDetails.id}.generic.js`, + `${scriptletDir}/generic/${assetDetails.id}.js`, patchedScriptlet ); @@ -573,11 +573,50 @@ function argsMap2List(argsMap, hostnamesMap) { /******************************************************************************/ -async function processCosmeticFilters(assetDetails, mapin) { - if ( mapin === undefined ) { return 0; } +function splitDomainAndEntity(mapin) { + const domainBased = new Map(); + const entityBased = new Map(); + for ( const [ selector, domainDetails ] of mapin ) { + domainBased.set(selector, domainDetails); + if ( domainDetails.rejected ) { continue; } + if ( Array.isArray(domainDetails.matches) === false ) { continue; } + const domainMatches = []; + const entityMatches = []; + for ( const hn of domainDetails.matches ) { + if ( hn.endsWith('.*') ) { + entityMatches.push(hn.slice(0, -2)); + } else { + domainMatches.push(hn); + } + } + if ( entityMatches.length === 0 ) { continue; } + if ( domainMatches.length !== 0 ) { + domainDetails.matches = domainMatches; + } else { + domainBased.delete(selector); + } + const entityDetails = { + matches: entityMatches, + }; + if ( Array.isArray(domainDetails.excludeMatches) ) { + entityDetails.excludeMatches = domainDetails.excludeMatches.slice(); + } + entityBased.set(selector, entityDetails); + } + return { domainBased, entityBased }; +} - const contentArray = groupHostnamesBySelectors( - groupSelectorsByHostnames(mapin) +/******************************************************************************/ + +async function processCosmeticFilters(assetDetails, mapin) { + if ( mapin === undefined ) { return; } + + const { domainBased, entityBased } = splitDomainAndEntity(mapin); + const entityBasedEntries = groupHostnamesBySelectors( + groupSelectorsByHostnames(entityBased) + ); + const domainBasedEntries = groupHostnamesBySelectors( + groupSelectorsByHostnames(domainBased) ); // We do not want more than n CSS files per subscription, so we will @@ -591,8 +630,8 @@ async function processCosmeticFilters(assetDetails, mapin) { const originalScriptletMap = await loadAllSourceScriptlets(); const generatedFiles = []; - for ( let i = 0; i < contentArray.length; i += MAX_COSMETIC_FILTERS_PER_FILE ) { - const slice = contentArray.slice(i, i + MAX_COSMETIC_FILTERS_PER_FILE); + for ( let i = 0; i < domainBasedEntries.length; i += MAX_COSMETIC_FILTERS_PER_FILE ) { + const slice = domainBasedEntries.slice(i, i + MAX_COSMETIC_FILTERS_PER_FILE); const argsMap = slice.map(entry => [ entry[0], { @@ -629,13 +668,51 @@ async function processCosmeticFilters(assetDetails, mapin) { } } + // For entity-based entries, we generate a single scriptlet which will be + // injected only in Complete mode. + if ( entityBasedEntries.length !== 0 ) { + const argsMap = entityBasedEntries.map(entry => [ + entry[0], + { + a: entry[1].a ? entry[1].a.join(',') : undefined, + n: entry[1].n, + } + ]); + const entitiesMap = new Map(); + for ( const [ id, details ] of entityBasedEntries ) { + if ( details.y === undefined ) { continue; } + scriptletHostnameToIdMap(details.y, id, entitiesMap); + } + const argsList = argsMap2List(argsMap, entitiesMap); + const patchedScriptlet = originalScriptletMap.get('css-specific.entity') + .replace( + '$rulesetId$', + assetDetails.id + ).replace( + /\bself\.\$argsList\$/m, + `${JSON.stringify(argsList, scriptletJsonReplacer)}` + ).replace( + /\bself\.\$entitiesMap\$/m, + `${JSON.stringify(entitiesMap, scriptletJsonReplacer)}` + ); + const fname = `${assetDetails.id}`; + writeFile(`${scriptletDir}/specific-entity/${fname}.js`, patchedScriptlet); + generatedFiles.push(fname); + } + if ( generatedFiles.length !== 0 ) { - log(`CSS-specific distinct filters: ${contentArray.length} distinct combined selectors`); + log(`CSS-specific domain-based: ${domainBased.size} distinct filters`); + log(`\tCombined into ${domainBasedEntries.length} distinct entries`); + log(`CSS-specific entity-based: ${entityBased.size} distinct filters`); + log(`\tCombined into ${entityBasedEntries.length} distinct entries`); log(`CSS-specific injectable files: ${generatedFiles.length}`); log(`\t${generatedFiles.join(', ')}`); } - return contentArray.length; + return { + domainBased: domainBasedEntries.length, + entityBased: entityBasedEntries.length, + }; } /******************************************************************************/ @@ -683,7 +760,7 @@ async function processDeclarativeCosmeticFilters(assetDetails, mapin) { /\bself\.\$hostnamesMap\$/m, `${JSON.stringify(hostnamesMap, scriptletJsonReplacer)}` ); - writeFile(`${scriptletDir}/declarative/${assetDetails.id}.declarative.js`, patchedScriptlet); + writeFile(`${scriptletDir}/declarative/${assetDetails.id}.js`, patchedScriptlet); { const hostnames = new Set(); @@ -752,7 +829,7 @@ async function processProceduralCosmeticFilters(assetDetails, mapin) { /\bself\.\$hostnamesMap\$/m, `${JSON.stringify(hostnamesMap, scriptletJsonReplacer)}` ); - writeFile(`${scriptletDir}/procedural/${assetDetails.id}.procedural.js`, patchedScriptlet); + writeFile(`${scriptletDir}/procedural/${assetDetails.id}.js`, patchedScriptlet); { const hostnames = new Set(); @@ -779,49 +856,68 @@ async function processProceduralCosmeticFilters(assetDetails, mapin) { /******************************************************************************/ async function processScriptletFilters(assetDetails, mapin) { - if ( mapin === undefined ) { return 0; } + if ( mapin === undefined ) { return; } + + const { domainBased, entityBased } = splitDomainAndEntity(mapin); // Load all available scriptlets into a key-val map, where the key is the // scriptlet token, and val is the whole content of the file. const originalScriptletMap = await loadAllSourceScriptlets(); - const parseArguments = (raw) => { - const out = []; - let s = raw; - let len = s.length; - let beg = 0, pos = 0; - let i = 1; - while ( beg < len ) { - pos = s.indexOf(',', pos); - // Escaped comma? If so, skip. - if ( pos > 0 && s.charCodeAt(pos - 1) === 0x5C /* '\\' */ ) { - s = s.slice(0, pos - 1) + s.slice(pos); - len -= 1; - continue; - } - if ( pos === -1 ) { pos = len; } - out.push(s.slice(beg, pos).trim()); - beg = pos = pos + 1; - i++; - } - return out; - }; + let domainBasedTokens; + if ( domainBased.size !== 0 ) { + domainBasedTokens = await processDomainScriptletFilters(assetDetails, domainBased, originalScriptletMap); + } + let entityBasedTokens; + if ( entityBased.size !== 0 ) { + entityBasedTokens = await processEntityScriptletFilters(assetDetails, entityBased, originalScriptletMap); + } - const parseFilter = (raw) => { - const filter = raw.slice(4, -1); - const end = filter.length; - let pos = filter.indexOf(','); - if ( pos === -1 ) { pos = end; } - const parts = filter.trim().split(',').map(s => s.trim()); - const token = scriptletDealiasingMap.get(parts[0]) || ''; - if ( token !== '' && originalScriptletMap.has(token) ) { - return { - token, - args: parseArguments(parts.slice(1).join(',').trim()), - }; - } - }; + return { domainBasedTokens, entityBasedTokens }; +} +/******************************************************************************/ + +const parseScriptletArguments = raw => { + const out = []; + let s = raw; + let len = s.length; + let beg = 0, pos = 0; + let i = 1; + while ( beg < len ) { + pos = s.indexOf(',', pos); + // Escaped comma? If so, skip. + if ( pos > 0 && s.charCodeAt(pos - 1) === 0x5C /* '\\' */ ) { + s = s.slice(0, pos - 1) + s.slice(pos); + len -= 1; + continue; + } + if ( pos === -1 ) { pos = len; } + out.push(s.slice(beg, pos).trim()); + beg = pos = pos + 1; + i++; + } + return out; +}; + +const parseScriptletFilter = (raw, scriptletMap, tokenSuffix = '') => { + const filter = raw.slice(4, -1); + const end = filter.length; + let pos = filter.indexOf(','); + if ( pos === -1 ) { pos = end; } + const parts = filter.trim().split(',').map(s => s.trim()); + const token = scriptletDealiasingMap.get(parts[0]) || ''; + if ( token === '' ) { return; } + if ( scriptletMap.has(`${token}${tokenSuffix}`) === false ) { return; } + return { + token, + args: parseScriptletArguments(parts.slice(1).join(',').trim()), + }; +}; + +/******************************************************************************/ + +async function processDomainScriptletFilters(assetDetails, domainBased, originalScriptletMap) { // For each instance of distinct scriptlet, we will collect distinct // instances of arguments, and for each distinct set of argument, we // will collect the set of hostnames for which the scriptlet/args is meant @@ -831,8 +927,13 @@ async function processScriptletFilters(assetDetails, mapin) { // should have no more generated content script per subscription than the // number of distinct source scriptlets. const scriptletDetails = new Map(); - for ( const [ rawFilter, entry ] of mapin ) { - const normalized = parseFilter(rawFilter); + const rejectedFilters = []; + for ( const [ rawFilter, entry ] of domainBased ) { + if ( entry.rejected ) { + rejectedFilters.push(rawFilter); + continue; + } + const normalized = parseScriptletFilter(rawFilter, originalScriptletMap); if ( normalized === undefined ) { continue; } let argsDetails = scriptletDetails.get(normalized.token); if ( argsDetails === undefined ) { @@ -861,7 +962,11 @@ async function processScriptletFilters(assetDetails, mapin) { } } + log(`Rejected scriptlet filters: ${rejectedFilters.length}`); + log(rejectedFilters.map(line => `\t${line}`).join('\n'), true); + const generatedFiles = []; + const tokens = []; for ( const [ token, argsDetails ] of scriptletDetails ) { const argsMap = Array.from(argsDetails).map(entry => [ @@ -889,6 +994,7 @@ async function processScriptletFilters(assetDetails, mapin) { const fpath = `${scriptletDir}/scriptlet/${fname}`; writeFile(fpath, patchedScriptlet); generatedFiles.push(fname); + tokens.push(token); const hostnameMatches = new Set(hostnamesMap.keys()); if ( hostnameMatches.has('*') ) { @@ -910,7 +1016,97 @@ async function processScriptletFilters(assetDetails, mapin) { log(`\t${generatedFiles.join(', ')}`); } - return generatedFiles.length; + return tokens; +} + +/******************************************************************************/ + +async function processEntityScriptletFilters(assetDetails, entityBased, originalScriptletMap) { + // For each instance of distinct scriptlet, we will collect distinct + // instances of arguments, and for each distinct set of argument, we + // will collect the set of hostnames for which the scriptlet/args is meant + // to execute. This will allow us a single content script file and the + // scriptlets execution will depend on hostname testing against the + // URL of the document at scriptlet execution time. In the end, we + // should have no more generated content script per subscription than the + // number of distinct source scriptlets. + const scriptletMap = new Map(); + const rejectedFilters = []; + for ( const [ rawFilter, entry ] of entityBased ) { + if ( entry.rejected ) { + rejectedFilters.push(rawFilter); + continue; + } + const normalized = parseScriptletFilter(rawFilter, originalScriptletMap, '.entity'); + if ( normalized === undefined ) { continue; } + let argsDetails = scriptletMap.get(normalized.token); + if ( argsDetails === undefined ) { + argsDetails = new Map(); + scriptletMap.set(normalized.token, argsDetails); + } + const argsHash = JSON.stringify(normalized.args); + let scriptletDetails = argsDetails.get(argsHash); + if ( scriptletDetails === undefined ) { + scriptletDetails = { + a: normalized.args, + y: new Set(), + n: new Set(), + }; + argsDetails.set(argsHash, scriptletDetails); + } + if ( entry.matches ) { + for ( const entity of entry.matches ) { + scriptletDetails.y.add(entity); + } + } + if ( entry.excludeMatches ) { + for ( const hn of entry.excludeMatches ) { + scriptletDetails.n.add(hn); + } + } + } + + log(`Rejected scriptlet filters: ${rejectedFilters.length}`); + log(rejectedFilters.map(line => `\t${line}`).join('\n'), true); + + const generatedFiles = []; + const tokens = []; + + for ( const [ token, argsDetails ] of scriptletMap ) { + const argsMap = Array.from(argsDetails).map(entry => [ + uidint32(entry[0]), + { a: entry[1].a, n: entry[1].n } + ]); + const entitiesMap = new Map(); + for ( const [ argsHash, details ] of argsDetails ) { + scriptletHostnameToIdMap(details.y, uidint32(argsHash), entitiesMap); + } + + const argsList = argsMap2List(argsMap, entitiesMap); + const patchedScriptlet = originalScriptletMap.get(`${token}.entity`) + .replace( + '$rulesetId$', + assetDetails.id + ).replace( + /\bself\.\$argsList\$/m, + `${JSON.stringify(argsList, scriptletJsonReplacer)}` + ).replace( + /\bself\.\$entitiesMap\$/m, + `${JSON.stringify(entitiesMap, scriptletJsonReplacer)}` + ); + const fname = `${assetDetails.id}.${token}.js`; + const fpath = `${scriptletDir}/scriptlet-entity/${fname}`; + writeFile(fpath, patchedScriptlet); + generatedFiles.push(fname); + tokens.push(token); + } + + if ( generatedFiles.length !== 0 ) { + log(`Scriptlet-related entity-based injectable files: ${generatedFiles.length}`); + log(`\t${generatedFiles.join(', ')}`); + } + + return tokens; } /******************************************************************************/ @@ -925,7 +1121,6 @@ async function rulesetFromURLs(assetDetails) { assetDetails.text = text; } - const extensionPaths = []; for ( const [ fname, details ] of redirectResourcesMap ) { const path = `/web_accessible_resources/${fname}`; @@ -966,6 +1161,13 @@ async function rulesetFromURLs(assetDetails) { continue; } const parsed = JSON.parse(selector); + const matches = + details.matches.filter(hn => hn.endsWith('.*') === false); + if ( matches.length === 0 ) { + rejectedCosmetic.push(`Entity-based filter not supported: ${parsed.raw}`); + continue; + } + details.matches = matches; parsed.raw = undefined; proceduralCosmetic.set(JSON.stringify(parsed), details); } @@ -1023,9 +1225,7 @@ async function rulesetFromURLs(assetDetails) { declarative: declarativeStats, procedural: proceduralStats, }, - scriptlets: { - total: scriptletStats, - }, + scriptlets: scriptletStats, }); ruleResources.push({ diff --git a/platform/mv3/scriptlets/abort-current-script.entity.js b/platform/mv3/scriptlets/abort-current-script.entity.js new file mode 100644 index 000000000..689cf851f --- /dev/null +++ b/platform/mv3/scriptlets/abort-current-script.entity.js @@ -0,0 +1,181 @@ +/******************************************************************************* + + uBlock Origin - a browser extension to block requests. + Copyright (C) 2019-present Raymond Hill + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see {http://www.gnu.org/licenses/}. + + Home: https://github.com/gorhill/uBlock + + The scriptlets below are meant to be injected only into a + web page context. +*/ + +/* jshint esversion:11 */ + +'use strict'; + +/******************************************************************************/ + +/// name abort-current-script.entity +/// alias acs.entity +/// alias abort-current-inline-script.entity +/// alias acis.entity + +/******************************************************************************/ + +// Important! +// Isolate from global scope +(function uBOL_abortCurrentScriptEntity() { + +/******************************************************************************/ + +// $rulesetId$ + +const argsList = self.$argsList$; + +const entitiesMap = new Map(self.$entitiesMap$); + +/******************************************************************************/ + +// Issues to mind before changing anything: +// https://github.com/uBlockOrigin/uBlock-issues/issues/2154 + +const scriptlet = ( + target = '', + needle = '', + context = '' +) => { + if ( target === '' ) { return; } + const reRegexEscape = /[.*+?^${}()|[\]\\]/g; + const reNeedle = (( ) => { + if ( needle === '' ) { return /^/; } + if ( /^\/.+\/$/.test(needle) ) { + return new RegExp(needle.slice(1,-1)); + } + return new RegExp(needle.replace(reRegexEscape, '\\$&')); + })(); + const reContext = (( ) => { + if ( context === '' ) { return; } + if ( /^\/.+\/$/.test(context) ) { + return new RegExp(context.slice(1,-1)); + } + return new RegExp(context.replace(reRegexEscape, '\\$&')); + })(); + const chain = target.split('.'); + let owner = window; + let prop; + for (;;) { + prop = chain.shift(); + if ( chain.length === 0 ) { break; } + owner = owner[prop]; + if ( owner instanceof Object === false ) { return; } + } + let value; + let desc = Object.getOwnPropertyDescriptor(owner, prop); + if ( + desc instanceof Object === false || + desc.get instanceof Function === false + ) { + value = owner[prop]; + desc = undefined; + } + const magic = String.fromCharCode(Date.now() % 26 + 97) + + Math.floor(Math.random() * 982451653 + 982451653).toString(36); + const scriptTexts = new WeakMap(); + const getScriptText = elem => { + let text = elem.textContent; + if ( text.trim() !== '' ) { return text; } + if ( scriptTexts.has(elem) ) { return scriptTexts.get(elem); } + const [ , mime, content ] = + /^data:([^,]*),(.+)$/.exec(elem.src.trim()) || + [ '', '', '' ]; + try { + switch ( true ) { + case mime.endsWith(';base64'): + text = self.atob(content); + break; + default: + text = self.decodeURIComponent(content); + break; + } + } catch(ex) { + } + scriptTexts.set(elem, text); + return text; + }; + const validate = ( ) => { + const e = document.currentScript; + if ( e instanceof HTMLScriptElement === false ) { return; } + if ( reContext !== undefined && reContext.test(e.src) === false ) { + return; + } + if ( reNeedle.test(getScriptText(e)) === false ) { return; } + throw new ReferenceError(magic); + }; + Object.defineProperty(owner, prop, { + get: function() { + validate(); + return desc instanceof Object + ? desc.get.call(owner) + : value; + }, + set: function(a) { + validate(); + if ( desc instanceof Object ) { + desc.set.call(owner, a); + } else { + value = a; + } + } + }); + const oe = window.onerror; + window.onerror = function(msg) { + if ( typeof msg === 'string' && msg.includes(magic) ) { + return true; + } + if ( oe instanceof Function ) { + return oe.apply(this, arguments); + } + }.bind(); +}; + +/******************************************************************************/ + +const hnparts = []; +try { hnparts.push(...document.location.hostname.split('.')); } catch(ex) { } +const hnpartslen = hnparts.length - 1; +for ( let i = 0; i < hnpartslen; i++ ) { + for ( let j = hnpartslen; j > i; j-- ) { + const hn = hnparts.slice(i).join('.'); + const en = hnparts.slice(i,j).join('.'); + let argsIndices = entitiesMap.get(en); + if ( argsIndices === undefined ) { continue; } + if ( typeof argsIndices === 'number' ) { argsIndices = [ argsIndices ]; } + for ( const argsIndex of argsIndices ) { + const details = argsList[argsIndex]; + if ( details.n && details.n.includes(hn) ) { continue; } + try { scriptlet(...details.a); } catch(ex) {} + } + } +} + +argsList.length = 0; +entitiesMap.clear(); + +/******************************************************************************/ + +})(); + +/******************************************************************************/ diff --git a/platform/mv3/scriptlets/abort-on-property-read.entity.js b/platform/mv3/scriptlets/abort-on-property-read.entity.js new file mode 100644 index 000000000..499c2281c --- /dev/null +++ b/platform/mv3/scriptlets/abort-on-property-read.entity.js @@ -0,0 +1,139 @@ +/******************************************************************************* + + uBlock Origin - a browser extension to block requests. + Copyright (C) 2019-present Raymond Hill + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see {http://www.gnu.org/licenses/}. + + Home: https://github.com/gorhill/uBlock + + The scriptlets below are meant to be injected only into a + web page context. +*/ + +/* jshint esversion:11 */ + +'use strict'; + +/******************************************************************************/ + +/// name abort-on-property-read.entity +/// alias aopr.entity + +/******************************************************************************/ + +// Important! +// Isolate from global scope +(function uBOL_abortOnPropertyReadEntity() { + +/******************************************************************************/ + +// $rulesetId$ + +const argsList = self.$argsList$; + +const entitiesMap = new Map(self.$entitiesMap$); + +/******************************************************************************/ + +const ObjGetOwnPropertyDescriptor = Object.getOwnPropertyDescriptor; +const ObjDefineProperty = Object.defineProperty; + +const magic = + String.fromCharCode(Date.now() % 26 + 97) + + Math.floor(Math.random() * 982451653 + 982451653).toString(36); + +const abort = function() { + throw new ReferenceError(magic); +}; + +const makeProxy = function(owner, chain) { + const pos = chain.indexOf('.'); + if ( pos === -1 ) { + const desc = ObjGetOwnPropertyDescriptor(owner, chain); + if ( !desc || desc.get !== abort ) { + ObjDefineProperty(owner, chain, { + get: abort, + set: function(){} + }); + } + return; + } + + const prop = chain.slice(0, pos); + let v = owner[prop]; + chain = chain.slice(pos + 1); + if ( v ) { + makeProxy(v, chain); + return; + } + + const desc = ObjGetOwnPropertyDescriptor(owner, prop); + if ( desc && desc.set !== undefined ) { return; } + + ObjDefineProperty(owner, prop, { + get: function() { return v; }, + set: function(a) { + v = a; + if ( a instanceof Object ) { + makeProxy(a, chain); + } + } + }); +}; + +const scriptlet = ( + chain = '' +) => { + const owner = window; + makeProxy(owner, chain); + const oe = window.onerror; + window.onerror = function(msg, src, line, col, error) { + if ( typeof msg === 'string' && msg.includes(magic) ) { + return true; + } + if ( oe instanceof Function ) { + return oe(msg, src, line, col, error); + } + }.bind(); +}; + +/******************************************************************************/ + +const hnparts = []; +try { hnparts.push(...document.location.hostname.split('.')); } catch(ex) { } +const hnpartslen = hnparts.length - 1; +for ( let i = 0; i < hnpartslen; i++ ) { + for ( let j = hnpartslen; j > i; j-- ) { + const hn = hnparts.slice(i).join('.'); + const en = hnparts.slice(i,j).join('.'); + let argsIndices = entitiesMap.get(en); + if ( argsIndices === undefined ) { continue; } + if ( typeof argsIndices === 'number' ) { argsIndices = [ argsIndices ]; } + for ( const argsIndex of argsIndices ) { + const details = argsList[argsIndex]; + if ( details.n && details.n.includes(hn) ) { continue; } + try { scriptlet(...details.a); } catch(ex) {} + } + } +} + +argsList.length = 0; +entitiesMap.clear(); + +/******************************************************************************/ + +})(); + +/******************************************************************************/ diff --git a/platform/mv3/scriptlets/abort-on-property-write.entity.js b/platform/mv3/scriptlets/abort-on-property-write.entity.js new file mode 100644 index 000000000..fa0ebe260 --- /dev/null +++ b/platform/mv3/scriptlets/abort-on-property-write.entity.js @@ -0,0 +1,113 @@ +/******************************************************************************* + + uBlock Origin - a browser extension to block requests. + Copyright (C) 2019-present Raymond Hill + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see {http://www.gnu.org/licenses/}. + + Home: https://github.com/gorhill/uBlock + + The scriptlets below are meant to be injected only into a + web page context. +*/ + +/* jshint esversion:11 */ + +'use strict'; + +/******************************************************************************/ + +/// name abort-on-property-write.entity +/// alias aopw.entity + +/******************************************************************************/ + +// Important! +// Isolate from global scope +(function uBOL_abortOnPropertyWriteEntity() { + +/******************************************************************************/ + +// $rulesetId$ + +const argsList = self.$argsList$; + +const entitiesMap = new Map(self.$entitiesMap$); + +/******************************************************************************/ + +const magic = + String.fromCharCode(Date.now() % 26 + 97) + + Math.floor(Math.random() * 982451653 + 982451653).toString(36); + +const abort = function() { + throw new ReferenceError(magic); +}; + +const scriptlet = ( + prop = '' +) => { + let owner = window; + for (;;) { + const pos = prop.indexOf('.'); + if ( pos === -1 ) { break; } + owner = owner[prop.slice(0, pos)]; + if ( owner instanceof Object === false ) { return; } + prop = prop.slice(pos + 1); + } + delete owner[prop]; + Object.defineProperty(owner, prop, { + set: function() { + abort(); + } + }); + const oe = window.onerror; + window.onerror = function(msg, src, line, col, error) { + if ( typeof msg === 'string' && msg.includes(magic) ) { + return true; + } + if ( oe instanceof Function ) { + return oe(msg, src, line, col, error); + } + }.bind(); +}; + +/******************************************************************************/ + +const hnparts = []; +try { hnparts.push(...document.location.hostname.split('.')); } catch(ex) { } +const hnpartslen = hnparts.length - 1; +for ( let i = 0; i < hnpartslen; i++ ) { + for ( let j = hnpartslen; j > i; j-- ) { + const hn = hnparts.slice(i).join('.'); + const en = hnparts.slice(i,j).join('.'); + let argsIndices = entitiesMap.get(en); + if ( argsIndices === undefined ) { continue; } + if ( typeof argsIndices === 'number' ) { argsIndices = [ argsIndices ]; } + for ( const argsIndex of argsIndices ) { + const details = argsList[argsIndex]; + if ( details.n && details.n.includes(hn) ) { continue; } + try { scriptlet(...details.a); } catch(ex) {} + } + } +} + +argsList.length = 0; +entitiesMap.clear(); + +/******************************************************************************/ + +})(); + +/******************************************************************************/ diff --git a/platform/mv3/scriptlets/css-specific.entity.js b/platform/mv3/scriptlets/css-specific.entity.js new file mode 100644 index 000000000..e6f494f66 --- /dev/null +++ b/platform/mv3/scriptlets/css-specific.entity.js @@ -0,0 +1,51 @@ +/******************************************************************************* + + uBlock Origin - a browser extension to block requests. + Copyright (C) 2019-present Raymond Hill + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see {http://www.gnu.org/licenses/}. + + Home: https://github.com/gorhill/uBlock +*/ + +/* jshint esversion:11 */ + +'use strict'; + +/******************************************************************************/ + +/// name css-specific.entity + +/******************************************************************************/ + +// Important! +// Isolate from global scope +(function uBOL_cssSpecificEntityImport() { + +/******************************************************************************/ + +// $rulesetId$ + +const argsList = self.$argsList$; + +const entitiesMap = new Map(self.$entitiesMap$); + +self.specificEntityImports = self.specificEntityImports || []; +self.specificEntityImports.push({ argsList, entitiesMap }); + +/******************************************************************************/ + +})(); + +/******************************************************************************/ diff --git a/platform/mv3/scriptlets/no-addeventlistener-if.entity.js b/platform/mv3/scriptlets/no-addeventlistener-if.entity.js new file mode 100644 index 000000000..e9f0f2e6e --- /dev/null +++ b/platform/mv3/scriptlets/no-addeventlistener-if.entity.js @@ -0,0 +1,113 @@ +/******************************************************************************* + + uBlock Origin - a browser extension to block requests. + Copyright (C) 2019-present Raymond Hill + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see {http://www.gnu.org/licenses/}. + + Home: https://github.com/gorhill/uBlock + + The scriptlets below are meant to be injected only into a + web page context. +*/ + +/* jshint esversion:11 */ + +'use strict'; + +/******************************************************************************/ + +/// name no-addeventlistener-if.entity +/// alias noaelif.entity +/// alias aeld.entity + +/******************************************************************************/ + +// Important! +// Isolate from global scope +(function uBOL_noAddEventListenerIfEntity() { + +/******************************************************************************/ + +// $rulesetId$ + +const argsList = self.$argsList$; + +const entitiesMap = new Map(self.$entitiesMap$); + +/******************************************************************************/ + +const regexpFromArg = arg => { + if ( arg === '' ) { return /^/; } + if ( /^\/.+\/$/.test(arg) ) { return new RegExp(arg.slice(1,-1)); } + return new RegExp(arg.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')); +}; + +/******************************************************************************/ + +const scriptlet = ( + needle1 = '', + needle2 = '' +) => { + const reNeedle1 = regexpFromArg(needle1); + const reNeedle2 = regexpFromArg(needle2); + self.EventTarget.prototype.addEventListener = new Proxy( + self.EventTarget.prototype.addEventListener, + { + apply: function(target, thisArg, args) { + let type, handler; + try { + type = String(args[0]); + handler = String(args[1]); + } catch(ex) { + } + if ( + reNeedle1.test(type) === false || + reNeedle2.test(handler) === false + ) { + return target.apply(thisArg, args); + } + } + } + ); +}; + +/******************************************************************************/ + +const hnparts = []; +try { hnparts.push(...document.location.hostname.split('.')); } catch(ex) { } +const hnpartslen = hnparts.length - 1; +for ( let i = 0; i < hnpartslen; i++ ) { + for ( let j = hnpartslen; j > i; j-- ) { + const hn = hnparts.slice(i).join('.'); + const en = hnparts.slice(i,j).join('.'); + let argsIndices = entitiesMap.get(en); + if ( argsIndices === undefined ) { continue; } + if ( typeof argsIndices === 'number' ) { argsIndices = [ argsIndices ]; } + for ( const argsIndex of argsIndices ) { + const details = argsList[argsIndex]; + if ( details.n && details.n.includes(hn) ) { continue; } + try { scriptlet(...details.a); } catch(ex) {} + } + } +} + +argsList.length = 0; +entitiesMap.clear(); + +/******************************************************************************/ + +})(); + +/******************************************************************************/ diff --git a/platform/mv3/scriptlets/no-settimeout-if.entity.js b/platform/mv3/scriptlets/no-settimeout-if.entity.js new file mode 100644 index 000000000..536591111 --- /dev/null +++ b/platform/mv3/scriptlets/no-settimeout-if.entity.js @@ -0,0 +1,117 @@ +/******************************************************************************* + + uBlock Origin - a browser extension to block requests. + Copyright (C) 2019-present Raymond Hill + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see {http://www.gnu.org/licenses/}. + + Home: https://github.com/gorhill/uBlock + + The scriptlets below are meant to be injected only into a + web page context. +*/ + +/* jshint esversion:11 */ + +'use strict'; + +/******************************************************************************/ + +/// name no-settimeout-if.entity +/// alias no-setTimeout-if.entity +/// alias nostif.entity + +/******************************************************************************/ + +// Important! +// Isolate from global scope +(function uBOL_noSetTimeoutIfEntity() { + +/******************************************************************************/ + +// $rulesetId$ + +const argsList = self.$argsList$; + +const entitiesMap = new Map(self.$entitiesMap$); + +/******************************************************************************/ + +const scriptlet = ( + needle = '', + delay = '' +) => { + const needleNot = needle.charAt(0) === '!'; + if ( needleNot ) { needle = needle.slice(1); } + if ( delay === '' ) { delay = undefined; } + let delayNot = false; + if ( delay !== undefined ) { + delayNot = delay.charAt(0) === '!'; + if ( delayNot ) { delay = delay.slice(1); } + delay = parseInt(delay, 10); + } + if ( needle.startsWith('/') && needle.endsWith('/') ) { + needle = needle.slice(1,-1); + } else if ( needle !== '' ) { + needle = needle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + } + const reNeedle = new RegExp(needle); + const regexpTest = RegExp.prototype.test; + self.setTimeout = new Proxy(self.setTimeout, { + apply: function(target, thisArg, args) { + const a = String(args[0]); + const b = args[1]; + let defuse; + if ( needle !== '' ) { + defuse = regexpTest.call(reNeedle, a) !== needleNot; + } + if ( defuse !== false && delay !== undefined ) { + defuse = (b === delay || isNaN(b) && isNaN(delay) ) !== delayNot; + } + if ( defuse ) { + args[0] = function(){}; + } + return target.apply(thisArg, args); + } + }); +}; + +/******************************************************************************/ + +const hnparts = []; +try { hnparts.push(...document.location.hostname.split('.')); } catch(ex) { } +const hnpartslen = hnparts.length - 1; +for ( let i = 0; i < hnpartslen; i++ ) { + for ( let j = hnpartslen; j > i; j-- ) { + const hn = hnparts.slice(i).join('.'); + const en = hnparts.slice(i,j).join('.'); + let argsIndices = entitiesMap.get(en); + if ( argsIndices === undefined ) { continue; } + if ( typeof argsIndices === 'number' ) { argsIndices = [ argsIndices ]; } + for ( const argsIndex of argsIndices ) { + const details = argsList[argsIndex]; + if ( details.n && details.n.includes(hn) ) { continue; } + try { scriptlet(...details.a); } catch(ex) {} + } + } +} + +argsList.length = 0; +entitiesMap.clear(); + +/******************************************************************************/ + +})(); + +/******************************************************************************/ diff --git a/platform/mv3/scriptlets/no-windowopen-if.entity.js b/platform/mv3/scriptlets/no-windowopen-if.entity.js new file mode 100644 index 000000000..8ada06c16 --- /dev/null +++ b/platform/mv3/scriptlets/no-windowopen-if.entity.js @@ -0,0 +1,154 @@ +/******************************************************************************* + + uBlock Origin - a browser extension to block requests. + Copyright (C) 2019-present Raymond Hill + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see {http://www.gnu.org/licenses/}. + + Home: https://github.com/gorhill/uBlock + + The scriptlets below are meant to be injected only into a + web page context. +*/ + +/* jshint esversion:11 */ + +'use strict'; + +/******************************************************************************/ + +/// name no-windowopen-if.entity +/// alias no-windowOpen-if.entity +/// alias nowoif.entity + +/******************************************************************************/ + +// Important! +// Isolate from global scope +(function uBOL_noWindowOpenIfEntity() { + +/******************************************************************************/ + +// $rulesetId$ + +const argsList = self.$argsList$; + +const entitiesMap = new Map(self.$entitiesMap$); + +/******************************************************************************/ + +const scriptlet = ( + needle = '', + delay = '', + options = '' +) => { + const newSyntax = /^[01]?$/.test(needle) === false; + let pattern = ''; + let targetResult = true; + let autoRemoveAfter = -1; + if ( newSyntax ) { + pattern = needle; + if ( pattern.startsWith('!') ) { + targetResult = false; + pattern = pattern.slice(1); + } + autoRemoveAfter = parseInt(delay); + if ( isNaN(autoRemoveAfter) ) { + autoRemoveAfter = -1; + } + } else { + pattern = delay; + if ( needle === '0' ) { + targetResult = false; + } + } + if ( pattern === '' ) { + pattern = '.?'; + } else if ( /^\/.+\/$/.test(pattern) ) { + pattern = pattern.slice(1,-1); + } else { + pattern = pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + } + const rePattern = new RegExp(pattern); + const createDecoy = function(tag, urlProp, url) { + const decoy = document.createElement(tag); + decoy[urlProp] = url; + decoy.style.setProperty('height','1px', 'important'); + decoy.style.setProperty('position','fixed', 'important'); + decoy.style.setProperty('top','-1px', 'important'); + decoy.style.setProperty('width','1px', 'important'); + document.body.appendChild(decoy); + setTimeout(( ) => decoy.remove(), autoRemoveAfter * 1000); + return decoy; + }; + window.open = new Proxy(window.open, { + apply: function(target, thisArg, args) { + const url = args[0]; + if ( rePattern.test(url) !== targetResult ) { + return target.apply(thisArg, args); + } + if ( autoRemoveAfter < 0 ) { return null; } + const decoy = /\bobj\b/.test(options) + ? createDecoy('object', 'data', url) + : createDecoy('iframe', 'src', url); + let popup = decoy.contentWindow; + if ( typeof popup === 'object' && popup !== null ) { + Object.defineProperty(popup, 'closed', { value: false }); + } else { + const noopFunc = (function(){}).bind(self); + popup = new Proxy(self, { + get: function(target, prop) { + if ( prop === 'closed' ) { return false; } + const r = Reflect.get(...arguments); + if ( typeof r === 'function' ) { return noopFunc; } + return target[prop]; + }, + set: function() { + return Reflect.set(...arguments); + }, + }); + } + return popup; + } + }); +}; + +/******************************************************************************/ + +const hnparts = []; +try { hnparts.push(...document.location.hostname.split('.')); } catch(ex) { } +const hnpartslen = hnparts.length - 1; +for ( let i = 0; i < hnpartslen; i++ ) { + for ( let j = hnpartslen; j > i; j-- ) { + const hn = hnparts.slice(i).join('.'); + const en = hnparts.slice(i,j).join('.'); + let argsIndices = entitiesMap.get(en); + if ( argsIndices === undefined ) { continue; } + if ( typeof argsIndices === 'number' ) { argsIndices = [ argsIndices ]; } + for ( const argsIndex of argsIndices ) { + const details = argsList[argsIndex]; + if ( details.n && details.n.includes(hn) ) { continue; } + try { scriptlet(...details.a); } catch(ex) {} + } + } +} + +argsList.length = 0; +entitiesMap.clear(); + +/******************************************************************************/ + +})(); + +/******************************************************************************/ diff --git a/platform/mv3/scriptlets/set-constant.entity.js b/platform/mv3/scriptlets/set-constant.entity.js new file mode 100644 index 000000000..8aa41c44c --- /dev/null +++ b/platform/mv3/scriptlets/set-constant.entity.js @@ -0,0 +1,199 @@ +/******************************************************************************* + + uBlock Origin - a browser extension to block requests. + Copyright (C) 2019-present Raymond Hill + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see {http://www.gnu.org/licenses/}. + + Home: https://github.com/gorhill/uBlock + + The scriptlets below are meant to be injected only into a + web page context. +*/ + +/* jshint esversion:11 */ + +'use strict'; + +/******************************************************************************/ + +/// name set-constant.entity +/// alias set.entity + +/******************************************************************************/ + +// Important! +// Isolate from global scope +(function uBOL_setConstantEntity() { + +/******************************************************************************/ + +// $rulesetId$ + +const argsList = self.$argsList$; + +const entitiesMap = new Map(self.$entitiesMap$); + +/******************************************************************************/ + +const scriptlet = ( + chain = '', + cValue = '' +) => { + if ( chain === '' ) { return; } + if ( cValue === 'undefined' ) { + cValue = undefined; + } else if ( cValue === 'false' ) { + cValue = false; + } else if ( cValue === 'true' ) { + cValue = true; + } else if ( cValue === 'null' ) { + cValue = null; + } else if ( cValue === "''" ) { + cValue = ''; + } else if ( cValue === '[]' ) { + cValue = []; + } else if ( cValue === '{}' ) { + cValue = {}; + } else if ( cValue === 'noopFunc' ) { + cValue = function(){}; + } else if ( cValue === 'trueFunc' ) { + cValue = function(){ return true; }; + } else if ( cValue === 'falseFunc' ) { + cValue = function(){ return false; }; + } else if ( /^\d+$/.test(cValue) ) { + cValue = parseFloat(cValue); + if ( isNaN(cValue) ) { return; } + if ( Math.abs(cValue) > 0x7FFF ) { return; } + } else { + return; + } + let aborted = false; + const mustAbort = function(v) { + if ( aborted ) { return true; } + aborted = + (v !== undefined && v !== null) && + (cValue !== undefined && cValue !== null) && + (typeof v !== typeof cValue); + return aborted; + }; + // https://github.com/uBlockOrigin/uBlock-issues/issues/156 + // Support multiple trappers for the same property. + const trapProp = function(owner, prop, configurable, handler) { + if ( handler.init(owner[prop]) === false ) { return; } + const odesc = Object.getOwnPropertyDescriptor(owner, prop); + let prevGetter, prevSetter; + if ( odesc instanceof Object ) { + owner[prop] = cValue; + if ( odesc.get instanceof Function ) { + prevGetter = odesc.get; + } + if ( odesc.set instanceof Function ) { + prevSetter = odesc.set; + } + } + try { + Object.defineProperty(owner, prop, { + configurable, + get() { + if ( prevGetter !== undefined ) { + prevGetter(); + } + return handler.getter(); // cValue + }, + set(a) { + if ( prevSetter !== undefined ) { + prevSetter(a); + } + handler.setter(a); + } + }); + } catch(ex) { + } + }; + const trapChain = function(owner, chain) { + const pos = chain.indexOf('.'); + if ( pos === -1 ) { + trapProp(owner, chain, false, { + v: undefined, + init: function(v) { + if ( mustAbort(v) ) { return false; } + this.v = v; + return true; + }, + getter: function() { + return cValue; + }, + setter: function(a) { + if ( mustAbort(a) === false ) { return; } + cValue = a; + } + }); + return; + } + const prop = chain.slice(0, pos); + const v = owner[prop]; + chain = chain.slice(pos + 1); + if ( v instanceof Object || typeof v === 'object' && v !== null ) { + trapChain(v, chain); + return; + } + trapProp(owner, prop, true, { + v: undefined, + init: function(v) { + this.v = v; + return true; + }, + getter: function() { + return this.v; + }, + setter: function(a) { + this.v = a; + if ( a instanceof Object ) { + trapChain(a, chain); + } + } + }); + }; + trapChain(window, chain); +}; + +/******************************************************************************/ + +const hnparts = []; +try { hnparts.push(...document.location.hostname.split('.')); } catch(ex) { } +const hnpartslen = hnparts.length - 1; +for ( let i = 0; i < hnpartslen; i++ ) { + for ( let j = hnpartslen; j > i; j-- ) { + const hn = hnparts.slice(i).join('.'); + const en = hnparts.slice(i,j).join('.'); + let argsIndices = entitiesMap.get(en); + if ( argsIndices === undefined ) { continue; } + if ( typeof argsIndices === 'number' ) { argsIndices = [ argsIndices ]; } + for ( const argsIndex of argsIndices ) { + const details = argsList[argsIndex]; + if ( details.n && details.n.includes(hn) ) { continue; } + try { scriptlet(...details.a); } catch(ex) {} + } + } +} + +argsList.length = 0; +entitiesMap.clear(); + +/******************************************************************************/ + +})(); + +/******************************************************************************/ diff --git a/src/js/static-dnr-filtering.js b/src/js/static-dnr-filtering.js index a453ecc81..19aa36901 100644 --- a/src/js/static-dnr-filtering.js +++ b/src/js/static-dnr-filtering.js @@ -100,12 +100,10 @@ function addExtendedToDNR(context, parser) { const { raw, exception } = parser.result; for ( const { hn, not, bad } of parser.extOptions() ) { if ( bad ) { continue; } - if ( hn.endsWith('.*') ) { continue; } if ( exception ) { continue; } let details = context.scriptletFilters.get(raw); if ( details === undefined ) { - details = {}; - context.scriptletFilters.set(raw, details); + context.scriptletFilters.set(raw, details = {}); } if ( not ) { if ( details.excludeMatches === undefined ) { @@ -174,8 +172,6 @@ function addExtendedToDNR(context, parser) { let rejected; if ( compiled === undefined ) { rejected = `Invalid filter: ${hn}##${raw}`; - } else if ( hn.endsWith('.*') ) { - rejected = `Entity not supported: ${hn}##${raw}`; } if ( rejected ) { compiled = rejected; diff --git a/src/js/static-net-filtering.js b/src/js/static-net-filtering.js index 4e7979611..e36e9ab97 100644 --- a/src/js/static-net-filtering.js +++ b/src/js/static-net-filtering.js @@ -4298,7 +4298,7 @@ FilterContainer.prototype.dnrFromCompiled = function(op, context, ...args) { filterCount: context.filterCount, acceptedFilterCount: context.acceptedFilterCount, rejectedFilterCount: context.rejectedFilterCount, - generichideExclusions, + generichideExclusions: Array.from(new Set(generichideExclusions)), }; };