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)), }; };