diff --git a/dist/firefox/publish-signed-beta.py b/dist/firefox/publish-signed-beta.py index 627da71e5..8dbd7048c 100755 --- a/dist/firefox/publish-signed-beta.py +++ b/dist/firefox/publish-signed-beta.py @@ -291,8 +291,8 @@ if response.status_code != 204: # package is higher version than current one. # -# Be sure in sync with potentially modified files on remote -r = subprocess.run(['git', 'checkout', 'origin/master', '--', 'dist/chromium-mv3/log.txt'], stdout=subprocess.PIPE) +# Be sure we are in sync with potentially modified files on remote +r = subprocess.run(['git', 'pull', 'origin', 'master'], stdout=subprocess.PIPE) rout = bytes.decode(r.stdout).strip() print('Update GitHub to point to newly signed self-hosted xpi package...') diff --git a/platform/mv3/extension/css/popup.css b/platform/mv3/extension/css/popup.css index 62223c0ed..0bc4b1933 100644 --- a/platform/mv3/extension/css/popup.css +++ b/platform/mv3/extension/css/popup.css @@ -181,6 +181,16 @@ body.mobile.no-tooltips .toolRibbon .tool { margin-bottom: 0; } +#toggleGreatPowers { + position: relative; + } +#toggleGreatPowers .badge { + font-size: var(--font-size-xsmall); + line-height: 1; + right: 4px; + position: absolute; + bottom: 2px; + } body:not(.hasGreatPowers) [data-i18n-title="popupGrantGreatPowers"], body.hasGreatPowers [data-i18n-title="popupRevokeGreatPowers"] { display: flex; @@ -189,7 +199,7 @@ body:not(.hasGreatPowers) [data-i18n-title="popupRevokeGreatPowers"], body.hasGreatPowers [data-i18n-title="popupGrantGreatPowers"] { display: none; } -body.hasGreatPowers [data-i18n-title="popupRevokeGreatPowers"] { +body [data-i18n-title="popupRevokeGreatPowers"] { fill: var(--popup-power-ink); } diff --git a/platform/mv3/extension/js/background-css.js b/platform/mv3/extension/js/background-css.js deleted file mode 100644 index 85ecc8b11..000000000 --- a/platform/mv3/extension/js/background-css.js +++ /dev/null @@ -1,156 +0,0 @@ -/******************************************************************************* - - uBlock Origin - a browser extension to block requests. - Copyright (C) 2022-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'; - -/******************************************************************************/ - -import { browser, dnr } from './ext.js'; -import { fetchJSON } from './fetch.js'; - -/******************************************************************************/ - -const matchesFromHostnames = hostnames => { - const out = []; - for ( const hn of hostnames ) { - if ( hn === '*' ) { - out.push('*://*/*'); - } else { - out.push(`*://*.${hn}/*`); - } - } - return out; -}; - -const hostnamesFromMatches = origins => { - const out = []; - for ( const origin of origins ) { - const match = /^\*:\/\/([^\/]+)\/\*/.exec(origin); - if ( match === null ) { continue; } - out.push(match[1]); - } - return out; -}; - -/******************************************************************************/ - -const toRegisterable = entry => { - const directive = { - id: entry.css, - allFrames: true, - css: [ - `/content-css/${entry.rulesetId}/${entry.css.slice(0,1)}/${entry.css.slice(1,8)}.css` - ], - }; - if ( entry.matches ) { - directive.matches = matchesFromHostnames(entry.matches); - } else { - directive.matches = [ '*://*/*' ]; - } - if ( entry.excludeMatches ) { - directive.excludeMatches = matchesFromHostnames(entry.excludeMatches); - } - return directive; -}; - -/******************************************************************************/ - -async function registerCSS() { - - const [ - origins, - rulesetIds, - registered, - cssDetails, - ] = await Promise.all([ - browser.permissions.getAll(), - dnr.getEnabledRulesets(), - browser.scripting.getRegisteredContentScripts(), - fetchJSON('/content-css/css-specific'), - ]).then(results => { - results[0] = new Set(hostnamesFromMatches(results[0].origins)); - results[3] = new Map(results[3]); - return results; - }); - - if ( origins.has('*') && origins.size > 1 ) { - origins.clear(); - origins.add('*'); - } - - const toRegister = new Map(); - for ( const rulesetId of rulesetIds ) { - const cssEntries = cssDetails.get(rulesetId); - if ( cssEntries === undefined ) { continue; } - for ( const entry of cssEntries ) { - entry.rulesetId = rulesetId; - for ( const origin of origins ) { - if ( origin === '*' || Array.isArray(entry.matches) === false ) { - toRegister.set(entry.css, entry); - continue; - } - let hn = origin; - for (;;) { - if ( entry.matches.includes(hn) ) { - toRegister.set(entry.css, entry); - break; - } - if ( hn === '*' ) { break; } - const pos = hn.indexOf('.'); - hn = pos !== -1 - ? hn.slice(pos+1) - : '*'; - } - } - } - } - - const before = new Set(registered.map(entry => entry.id)); - const toAdd = []; - for ( const [ id, entry ] of toRegister ) { - if ( before.has(id) ) { continue; } - toAdd.push(toRegisterable(entry)); - } - const toRemove = []; - for ( const id of before ) { - if ( toRegister.has(id) ) { continue; } - toRemove.push(id); - } - - const todo = []; - if ( toRemove.length !== 0 ) { - todo.push(browser.scripting.unregisterContentScripts(toRemove)); - console.info(`Unregistered ${toRemove.length} CSS content scripts`); - } - if ( toAdd.length !== 0 ) { - todo.push(browser.scripting.registerContentScripts(toAdd)); - console.info(`Registered ${toAdd.length} CSS content scripts`); - } - if ( todo.length === 0 ) { return; } - - return Promise.all(todo); -} - -/******************************************************************************/ - -export { registerCSS }; diff --git a/platform/mv3/extension/js/background.js b/platform/mv3/extension/js/background.js index 979b333f1..07c289812 100644 --- a/platform/mv3/extension/js/background.js +++ b/platform/mv3/extension/js/background.js @@ -27,7 +27,7 @@ import { browser, dnr, i18n, runtime } from './ext.js'; import { fetchJSON } from './fetch.js'; -import { registerCSS } from './background-css.js'; +import { registerInjectable } from './scripting-manager.js'; /******************************************************************************/ @@ -37,9 +37,6 @@ const REGEXES_REALM_END = REGEXES_REALM_START + RULE_REALM_SIZE; const TRUSTED_DIRECTIVE_BASE_RULE_ID = 8000000; const CURRENT_CONFIG_BASE_RULE_ID = 9000000; -const dynamicRuleMap = new Map(); -const rulesetDetails = new Map(); - const rulesetConfig = { version: '', enabledRulesets: [], @@ -47,11 +44,48 @@ const rulesetConfig = { /******************************************************************************/ +let rulesetDetailsPromise; + +function getRulesetDetails() { + if ( rulesetDetailsPromise !== undefined ) { + return rulesetDetailsPromise; + } + rulesetDetailsPromise = fetchJSON('/rulesets/ruleset-details').then(entries => { + const map = new Map( + entries.map(entry => [ entry.id, entry ]) + ); + return map; + }); + return rulesetDetailsPromise; +} + +/******************************************************************************/ + +let dynamicRuleMapPromise; + +function getDynamicRules() { + if ( dynamicRuleMapPromise !== undefined ) { + return dynamicRuleMapPromise; + } + dynamicRuleMapPromise = dnr.getDynamicRules().then(rules => { + const map = new Map( + rules.map(rule => [ rule.id, rule ]) + ); + console.log(`Dynamic rule count: ${map.size}`); + console.log(`Available dynamic rule count: ${dnr.MAX_NUMBER_OF_DYNAMIC_AND_SESSION_RULES - map.size}`); + return map; + }); + return dynamicRuleMapPromise; +} + +/******************************************************************************/ + function getCurrentVersion() { return runtime.getManifest().version; } async function loadRulesetConfig() { + const dynamicRuleMap = await getDynamicRules(); const configRule = dynamicRuleMap.get(CURRENT_CONFIG_BASE_RULE_ID); if ( configRule === undefined ) { rulesetConfig.enabledRulesets = await defaultRulesetsFromLanguage(); @@ -71,6 +105,7 @@ async function loadRulesetConfig() { } async function saveRulesetConfig() { + const dynamicRuleMap = await getDynamicRules(); let configRule = dynamicRuleMap.get(CURRENT_CONFIG_BASE_RULE_ID); if ( configRule === undefined ) { configRule = { @@ -98,7 +133,15 @@ async function saveRulesetConfig() { /******************************************************************************/ -async function updateRegexRules(dynamicRules) { +async function updateRegexRules() { + const [ + rulesetDetails, + dynamicRules + ] = await Promise.all([ + getRulesetDetails(), + dnr.getDynamicRules(), + ]); + // Avoid testing already tested regexes const validRegexSet = new Set( dynamicRules.filter(rule => @@ -156,6 +199,7 @@ async function updateRegexRules(dynamicRules) { // Add validated regex rules to dynamic ruleset without affecting rules // outside regex rule realm. + const dynamicRuleMap = await getDynamicRules(); const newRuleMap = new Map(newRules.map(rule => [ rule.id, rule ])); const addRules = []; const removeRuleIds = []; @@ -186,6 +230,7 @@ async function updateRegexRules(dynamicRules) { async function matchesTrustedSiteDirective(details) { const url = new URL(details.origin); + const dynamicRuleMap = await getDynamicRules(); let rule = dynamicRuleMap.get(TRUSTED_DIRECTIVE_BASE_RULE_ID); if ( rule === undefined ) { return false; } const domainSet = new Set(rule.condition.requestDomains); @@ -201,6 +246,7 @@ async function matchesTrustedSiteDirective(details) { async function addTrustedSiteDirective(details) { const url = new URL(details.origin); + const dynamicRuleMap = await getDynamicRules(); let rule = dynamicRuleMap.get(TRUSTED_DIRECTIVE_BASE_RULE_ID); if ( rule !== undefined ) { rule.condition.initiatorDomains = undefined; @@ -233,6 +279,7 @@ async function addTrustedSiteDirective(details) { async function removeTrustedSiteDirective(details) { const url = new URL(details.origin); + const dynamicRuleMap = await getDynamicRules(); let rule = dynamicRuleMap.get(TRUSTED_DIRECTIVE_BASE_RULE_ID); if ( rule === undefined ) { return false; } rule.condition.initiatorDomains = undefined; @@ -291,7 +338,13 @@ async function enableRulesets(ids) { } async function getEnabledRulesetsStats() { - const ids = await dnr.getEnabledRulesets(); + const [ + rulesetDetails, + ids, + ] = await Promise.all([ + getRulesetDetails(), + dnr.getEnabledRulesets(), + ]); const out = []; for ( const id of ids ) { const ruleset = rulesetDetails.get(id); @@ -323,6 +376,7 @@ async function defaultRulesetsFromLanguage() { `\\b(${Array.from(langSet).join('|')})\\b` ); + const rulesetDetails = await getRulesetDetails(); for ( const [ id, details ] of rulesetDetails ) { if ( typeof details.lang !== 'string' ) { continue; } if ( reTargetLang.test(details.lang) === false ) { continue; } @@ -371,7 +425,11 @@ function onMessage(request, sender, callback) { } case 'getRulesetData': { - dnr.getEnabledRulesets().then(enabledRulesets => { + Promise.all([ + getRulesetDetails(), + dnr.getEnabledRulesets(), + ]).then(results => { + const [ rulesetDetails, enabledRulesets ] = results; callback({ enabledRulesets, rulesetDetails: Array.from(rulesetDetails.values()), @@ -382,6 +440,7 @@ function onMessage(request, sender, callback) { case 'grantGreatPowers': grantGreatPowers(request.hostname).then(granted => { + console.info(`Granted uBOL great powers on ${request.hostname}: ${granted}`); callback(granted); }); return true; @@ -403,6 +462,7 @@ function onMessage(request, sender, callback) { case 'revokeGreatPowers': revokeGreatPowers(request.hostname).then(removed => { + console.info(`Revoked great powers from uBOL on ${request.hostname}: ${removed}`); callback(removed); }); return true; @@ -421,37 +481,19 @@ function onMessage(request, sender, callback) { } async function onPermissionsChanged() { - await registerCSS(); + await registerInjectable(); } /******************************************************************************/ async function start() { - // Fetch enabled rulesets and dynamic rules - const dynamicRules = await dnr.getDynamicRules(); - for ( const rule of dynamicRules ) { - dynamicRuleMap.set(rule.id, rule); - } - - // Fetch ruleset details - await fetchJSON('/rulesets/ruleset-details').then(entries => { - if ( entries === undefined ) { return; } - for ( const entry of entries ) { - rulesetDetails.set(entry.id, entry); - } - }); - await loadRulesetConfig(); - - console.log(`Dynamic rule count: ${dynamicRuleMap.size}`); - console.log(`Available dynamic rule count: ${dnr.MAX_NUMBER_OF_DYNAMIC_AND_SESSION_RULES - dynamicRuleMap.size}`); - await enableRulesets(rulesetConfig.enabledRulesets); // We need to update the regex rules only when ruleset version changes. const currentVersion = getCurrentVersion(); if ( currentVersion !== rulesetConfig.version ) { - await updateRegexRules(dynamicRules); + await updateRegexRules(); console.log(`Version change: ${rulesetConfig.version} => ${currentVersion}`); rulesetConfig.version = currentVersion; } diff --git a/platform/mv3/extension/js/scripting-manager.js b/platform/mv3/extension/js/scripting-manager.js new file mode 100644 index 000000000..ea1433238 --- /dev/null +++ b/platform/mv3/extension/js/scripting-manager.js @@ -0,0 +1,254 @@ +/******************************************************************************* + + uBlock Origin - a browser extension to block requests. + Copyright (C) 2022-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'; + +/******************************************************************************/ + +import { browser, dnr } from './ext.js'; +import { fetchJSON } from './fetch.js'; + +/******************************************************************************/ + +const CSS_TYPE = 0; +const JS_TYPE = 1; + +/******************************************************************************/ + +let cssDetailsPromise; +let scriptletDetailsPromise; + +function getCSSDetails() { + if ( cssDetailsPromise !== undefined ) { + return cssDetailsPromise; + } + cssDetailsPromise = fetchJSON('/content-css/css-specific').then(rules => { + return new Map(rules); + }); + return cssDetailsPromise; +} + +function getScriptletDetails() { + if ( scriptletDetailsPromise !== undefined ) { + return scriptletDetailsPromise; + } + scriptletDetailsPromise = fetchJSON('/content-js/scriptlet-details').then(rules => { + return new Map(rules); + }); + return scriptletDetailsPromise; +} + +/******************************************************************************/ + +const matchesFromHostnames = hostnames => { + const out = []; + for ( const hn of hostnames ) { + if ( hn === '*' ) { + out.push('*://*/*'); + } else { + out.push(`*://*.${hn}/*`); + } + } + return out; +}; + +const hostnamesFromMatches = origins => { + const out = []; + for ( const origin of origins ) { + const match = /^\*:\/\/([^\/]+)\/\*/.exec(origin); + if ( match === null ) { continue; } + out.push(match[1]); + } + return out; +}; + +/******************************************************************************/ + +const toRegisterable = (fname, entry) => { + const directive = { + id: fname, + allFrames: true, + }; + if ( entry.matches ) { + directive.matches = matchesFromHostnames(entry.y); + } else { + directive.matches = [ '*://*/*' ]; + } + if ( entry.excludeMatches ) { + directive.excludeMatches = matchesFromHostnames(entry.n); + } + if ( entry.type === CSS_TYPE ) { + directive.css = [ + `/content-css/${entry.id}/${fname.slice(0,1)}/${fname.slice(1,8)}.css` + ]; + } else if ( entry.type === JS_TYPE ) { + directive.js = [ + `/content-js/${entry.id}/${fname.slice(0,1)}/${fname.slice(1,8)}.js` + ]; + directive.runAt = 'document_start'; + directive.world = 'MAIN'; + } + + return directive; +}; + +/******************************************************************************/ + +const shouldRegister = (origins, matches) => { + for ( const origin of origins ) { + if ( origin === '*' || Array.isArray(matches) === false ) { + return true; + } + let hn = origin; + for (;;) { + if ( matches.includes(hn) ) { + return true; + } + if ( hn === '*' ) { break; } + const pos = hn.indexOf('.'); + hn = pos !== -1 + ? hn.slice(pos+1) + : '*'; + } + } + return false; +}; + +/******************************************************************************/ + +async function getInjectableCount(hostname) { + + const [ + rulesetIds, + cssDetails, + scriptletDetails, + ] = await Promise.all([ + dnr.getEnabledRulesets(), + getCSSDetails(), + getScriptletDetails(), + ]); + + let total = 0; + + for ( const rulesetId of rulesetIds ) { + + if ( cssDetails.has(rulesetId) ) { + for ( const entry of cssDetails ) { + if ( shouldRegister([ hostname ], entry[1].y) === true ) { + total += 1; + } + } + } + + if ( scriptletDetails.has(rulesetId) ) { + for ( const entry of cssDetails ) { + if ( shouldRegister([ hostname ], entry[1].y) === true ) { + total += 1; + } + } + } + + } + + return total; +} + +/******************************************************************************/ + +async function registerInjectable() { + + const [ + origins, + rulesetIds, + registered, + cssDetails, + scriptletDetails, + ] = await Promise.all([ + browser.permissions.getAll(), + dnr.getEnabledRulesets(), + browser.scripting.getRegisteredContentScripts(), + getCSSDetails(), + getScriptletDetails(), + ]).then(results => { + results[0] = new Set(hostnamesFromMatches(results[0].origins)); + return results; + }); + + if ( origins.has('*') && origins.size > 1 ) { + origins.clear(); + origins.add('*'); + } + + const toRegister = new Map(); + + for ( const rulesetId of rulesetIds ) { + if ( cssDetails.has(rulesetId) ) { + for ( const [ fname, entry ] of cssDetails.get(rulesetId) ) { + entry.id = rulesetId; + entry.type = CSS_TYPE; + if ( shouldRegister(origins, entry.y) !== true ) { continue; } + toRegister.set(fname, entry); + } + } + if ( scriptletDetails.has(rulesetId) ) { + for ( const [ fname, entry ] of scriptletDetails.get(rulesetId) ) { + entry.id = rulesetId; + entry.type = JS_TYPE; + if ( shouldRegister(origins, entry.y) !== true ) { continue; } + toRegister.set(fname, entry); + } + } + } + + const before = new Set(registered.map(entry => entry.id)); + const toAdd = []; + for ( const [ fname, entry ] of toRegister ) { + if ( before.has(fname) ) { continue; } + toAdd.push(toRegisterable(fname, entry)); + } + const toRemove = []; + for ( const fname of before ) { + if ( toRegister.has(fname) ) { continue; } + toRemove.push(fname); + } + + const todo = []; + if ( toRemove.length !== 0 ) { + todo.push(browser.scripting.unregisterContentScripts(toRemove)); + console.info(`Unregistered ${toRemove.length} content (css/js)`); + } + if ( toAdd.length !== 0 ) { + todo.push(browser.scripting.registerContentScripts(toAdd)); + console.info(`Registered ${toAdd.length} content (css/js)`); + } + if ( todo.length === 0 ) { return; } + + return Promise.all(todo); +} + +/******************************************************************************/ + +export { + getInjectableCount, + registerInjectable +}; diff --git a/platform/mv3/extension/manifest.json b/platform/mv3/extension/manifest.json index fb6478536..765b5df72 100644 --- a/platform/mv3/extension/manifest.json +++ b/platform/mv3/extension/manifest.json @@ -25,7 +25,7 @@ "128": "img/icon_128.png" }, "manifest_version": 3, - "minimum_chrome_version": "101.0", + "minimum_chrome_version": "105.0", "name": "__MSG_extName__", "options_page": "dashboard.html", "optional_host_permissions": [ diff --git a/platform/mv3/extension/popup.html b/platform/mv3/extension/popup.html index 315f8676b..0de5b3e8d 100644 --- a/platform/mv3/extension/popup.html +++ b/platform/mv3/extension/popup.html @@ -44,6 +44,7 @@ sun-o sun + diff --git a/platform/mv3/make-rulesets.js b/platform/mv3/make-rulesets.js index 90f637ac6..4a1f77a33 100644 --- a/platform/mv3/make-rulesets.js +++ b/platform/mv3/make-rulesets.js @@ -51,6 +51,13 @@ const commandLineArgs = (( ) => { return args; })(); +const outputDir = commandLineArgs.get('output') || '.'; +const cacheDir = `${outputDir}/../mv3-data`; +const rulesetDir = `${outputDir}/rulesets`; +const cssDir = `${outputDir}/content-css`; +const scriptletDir = `${outputDir}/content-js`; +const env = [ 'chromium', 'ubol' ]; + /******************************************************************************/ const isUnsupported = rule => @@ -133,21 +140,344 @@ const fetchList = (url, cacheDir) => { const writeFile = async (fname, data) => { const dir = path.dirname(fname); await fs.mkdir(dir, { recursive: true }); - return fs.writeFile(fname, data); + const promise = fs.writeFile(fname, data); + writeOps.push(promise); + return promise; }; +const writeOps = []; + +/******************************************************************************/ + +const ruleResources = []; +const rulesetDetails = []; +const cssDetails = new Map(); +const scriptletDetails = new Map(); + +/******************************************************************************/ + +async function fetchAsset(assetDetails) { + // Remember fetched URLs + const fetchedURLs = new Set(); + + // Fetch list and expand `!#include` directives + let parts = assetDetails.urls.map(url => ({ url })); + while ( parts.every(v => typeof v === 'string') === false ) { + const newParts = []; + for ( const part of parts ) { + if ( typeof part === 'string' ) { + newParts.push(part); + continue; + } + if ( fetchedURLs.has(part.url) ) { + newParts.push(''); + continue; + } + fetchedURLs.add(part.url); + newParts.push( + fetchList(part.url, cacheDir).then(details => { + const { url } = details; + const content = details.content.trim(); + if ( typeof content === 'string' && content !== '' ) { + if ( + content.startsWith('<') === false || + content.endsWith('>') === false + ) { + return { url, content }; + } + } + log(`No valid content for ${details.name}`); + return { url, content: '' }; + }) + ); + } + parts = await Promise.all(newParts); + parts = StaticFilteringParser.utils.preparser.expandIncludes(parts, env); + } + const text = parts.join('\n'); + + if ( text === '' ) { + log('No filterset found'); + } + return text; +} + +/******************************************************************************/ + +async function processNetworkFilters(assetDetails, network) { + const replacer = (k, v) => { + if ( k.startsWith('__') ) { return; } + if ( Array.isArray(v) ) { + return v.sort(); + } + if ( v instanceof Object ) { + const sorted = {}; + for ( const kk of Object.keys(v).sort() ) { + sorted[kk] = v[kk]; + } + return sorted; + } + return v; + }; + + const { ruleset: rules } = network; + log(`Input filter count: ${network.filterCount}`); + log(`\tAccepted filter count: ${network.acceptedFilterCount}`); + log(`\tRejected filter count: ${network.rejectedFilterCount}`); + log(`Output rule count: ${rules.length}`); + + const good = rules.filter(rule => isGood(rule) && isRegex(rule) === false); + log(`\tGood: ${good.length}`); + + const regexes = rules.filter(rule => isGood(rule) && isRegex(rule)); + log(`\tMaybe good (regexes): ${regexes.length}`); + + const redirects = rules.filter(rule => + isUnsupported(rule) === false && + isRedirect(rule) + ); + log(`\tredirect-rule= (discarded): ${redirects.length}`); + + const headers = rules.filter(rule => + isUnsupported(rule) === false && + isCsp(rule) + ); + log(`\tcsp= (discarded): ${headers.length}`); + + const removeparams = rules.filter(rule => + isUnsupported(rule) === false && + isRemoveparam(rule) + ); + log(`\tremoveparams= (discarded): ${removeparams.length}`); + + const bad = rules.filter(rule => + isUnsupported(rule) + ); + log(`\tUnsupported: ${bad.length}`); + log( + bad.map(rule => rule._error.map(v => `\t\t${v}`)).join('\n'), + true + ); + + writeFile( + `${rulesetDir}/${assetDetails.id}.json`, + `${JSON.stringify(good, replacer)}\n` + ); + + if ( regexes.length !== 0 ) { + writeFile( + `${rulesetDir}/${assetDetails.id}.regexes.json`, + `${JSON.stringify(regexes, replacer)}\n` + ); + } + + return { + total: rules.length, + accepted: good.length, + discarded: redirects.length + headers.length + removeparams.length, + rejected: bad.length, + regexes: regexes.length, + }; +} + +/******************************************************************************/ + +function optimizeExtendedFilters(filters) { + if ( filters === undefined ) { return []; } + const merge = new Map(); + for ( const [ selector, details ] of filters ) { + const json = JSON.stringify(details); + let entries = merge.get(json); + if ( entries === undefined ) { + entries = new Set(); + merge.set(json, entries); + } + entries.add(selector); + } + const out = []; + for ( const [ json, entries ] of merge ) { + const details = JSON.parse(json); + details.payload = Array.from(entries); + out.push(details); + } + return out; +} + +/******************************************************************************/ + +const style = [ + ' display:none!important;', + ' position:absolute!important;', + ' z-index:0!important;', + ' visibility:collapse!important;', +].join('\n'); + +function processCosmeticFilters(assetDetails, mapin) { + if ( mapin === undefined ) { return 0; } + + const optimized = optimizeExtendedFilters(mapin); + const cssEntries = new Map(); + for ( const entry of optimized ) { + const selectors = entry.payload.join(',\n'); + const fname = createHash('sha256').update(selectors).digest('hex').slice(0,8); + const fpath = `${assetDetails.id}/${fname.slice(0,1)}/${fname.slice(1,8)}`; + writeFile( + `${cssDir}/${fpath}.css`, + `${selectors} {\n${style}\n}\n` + ); + cssEntries.set(fname, { + y: entry.matches, + n: entry.excludeMatches, + }); + } + + log(`CSS entries: ${cssEntries.size}`); + + if ( cssEntries.size !== 0 ) { + cssDetails.set(assetDetails.id, Array.from(cssEntries)); + } + + return cssEntries.size; +} + +/******************************************************************************/ + +async function processScriptletFilters(assetDetails, mapin) { + if ( mapin === undefined ) { return 0; } + + const originalScriptletMap = new Map(); + const dealiasingMap = new Map(); + + 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; + }; + + 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 = dealiasingMap.get(parts[0]) || ''; + if ( token !== '' && originalScriptletMap.has(token) ) { + return { + token, + args: parseArguments(parts.slice(1).join(',').trim()), + }; + } + }; + + const patchScriptlet = (filter) => { + return originalScriptletMap.get(filter.token).replace( + /^self\.\$args\$$/m, + `...${JSON.stringify(filter.args, null, 4)}` + ); + }; + + // 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 files = await fs.readdir('./scriptlets'); + const reScriptletNameOrAlias = /^\/\/\/\s+(?:name|alias)\s+(\S+)/gm; + for ( const file of files ) { + const text = await fs.readFile( + `./scriptlets/${file}`, + { encoding: 'utf8' } + ); + const aliasSet = new Set(); + for (;;) { + const match = reScriptletNameOrAlias.exec(text); + if ( match === null ) { break; } + aliasSet.add(match[1]); + } + if ( aliasSet.size === 0 ) { continue; } + const aliases = Array.from(aliasSet); + originalScriptletMap.set(aliases[0], text); + for ( let i = 0; i < aliases.length; i++ ) { + dealiasingMap.set(aliases[i], aliases[0]); + } + } + + // Merge entries after dealiasing and expanding arguments + const normalizedMap = new Map(); + for ( const [ rawFilter, toAdd ] of mapin ) { + const normalized = parseFilter(rawFilter); + if ( normalized === undefined ) { continue; } + const key = JSON.stringify(normalized); + const toMerge = normalizedMap.get(key); + if ( toMerge === undefined ) { + normalizedMap.set(key, toAdd); + continue; + } + const matches = new Set(toMerge.matches || []); + const excludeMatches = new Set(toMerge.excludeMatches || []); + if ( toAdd.matches && toAdd.matches.size !== 0 ) { + toAdd.matches.forEach(hn => { + matches.add(hn); + }); + } + if ( toAdd.excludeMatches && toAdd.excludeMatches.size !== 0 ) { + toAdd.excludeMatches.forEach(hn => { + excludeMatches.add(hn); + }); + } + if ( matches.size !== 0 ) { + toMerge.matches = matches.has('*') + ? [ '*' ] + : Array.from(matches); + } + if ( excludeMatches.size !== 0 ) { + toMerge.excludeMatches = excludeMatches.has('*') + ? [ '*' ] + : Array.from(excludeMatches); + } + } + + // Combine injected resources for same matches/excludeMatches instances + //const optimized = optimizeExtendedFilters(normalizedMap); + + // Generate distinct scriptlets according to patched scriptlets + const scriptletEntries = new Map(); + for ( const [ json, entry ] of normalizedMap ) { + const fname = createHash('sha256').update(json).digest('hex').slice(0,8); + const scriptlet = patchScriptlet(JSON.parse(json)); + const fpath = `${assetDetails.id}/${fname.slice(0,1)}/${fname.slice(1,8)}`; + writeFile(`${scriptletDir}/${fpath}.js`, scriptlet); + scriptletEntries.set(fname, { + y: entry.matches, + n: entry.excludeMatches, + }); + } + + log(`Scriptlet entries: ${scriptletEntries.size}`); + + if ( scriptletEntries.size !== 0 ) { + scriptletDetails.set(assetDetails.id, Array.from(scriptletEntries)); + } + return scriptletEntries.size; +} + /******************************************************************************/ async function main() { - const env = [ 'chromium' ]; - - const writeOps = []; - const ruleResources = []; - const rulesetDetails = []; - const cssDetails = new Map(); - const outputDir = commandLineArgs.get('output') || '.'; - // Get manifest content const manifest = await fs.readFile( `${outputDir}/manifest.json`, @@ -168,151 +498,31 @@ async function main() { } log(`Version: ${version}`); - let goodTotalCount = 0; - let maybeGoodTotalCount = 0; - - const replacer = (k, v) => { - if ( k.startsWith('__') ) { return; } - if ( Array.isArray(v) ) { - return v.sort(); - } - if ( v instanceof Object ) { - const sorted = {}; - for ( const kk of Object.keys(v).sort() ) { - sorted[kk] = v[kk]; - } - return sorted; - } - return v; - }; - - const rulesetDir = `${outputDir}/rulesets`; - const cacheDir = `${outputDir}/../mv3-data`; - const cssDir = `${outputDir}/content-css`; - const rulesetFromURLS = async function(assetDetails) { log('============================'); log(`Listset for '${assetDetails.id}':`); - // Remember fetched URLs - const fetchedURLs = new Set(); + const text = await fetchAsset(assetDetails); - // Fetch list and expand `!#include` directives - let parts = assetDetails.urls.map(url => ({ url })); - while ( parts.every(v => typeof v === 'string') === false ) { - const newParts = []; - for ( const part of parts ) { - if ( typeof part === 'string' ) { - newParts.push(part); - continue; - } - if ( fetchedURLs.has(part.url) ) { - newParts.push(''); - continue; - } - fetchedURLs.add(part.url); - newParts.push( - fetchList(part.url, cacheDir).then(details => { - const { url } = details; - const content = details.content.trim(); - if ( typeof content === 'string' && content !== '' ) { - if ( - content.startsWith('<') === false || - content.endsWith('>') === false - ) { - return { url, content }; - } - } - log(`No valid content for ${details.name}`); - return { url, content: '' }; - }) - ); - } - parts = await Promise.all(newParts); - parts = StaticFilteringParser.utils.preparser.expandIncludes(parts, env); - } - const text = parts.join('\n'); - - if ( text === '' ) { - log('No filterset found'); - return; - } - - const results = await dnrRulesetFromRawLists([ { name: assetDetails.id, text } ], { env }); - const { network } = results; - const { ruleset: rules } = network; - log(`Input filter count: ${network.filterCount}`); - log(`\tAccepted filter count: ${network.acceptedFilterCount}`); - log(`\tRejected filter count: ${network.rejectedFilterCount}`); - log(`Output rule count: ${rules.length}`); - - const good = rules.filter(rule => isGood(rule) && isRegex(rule) === false); - log(`\tGood: ${good.length}`); - - const regexes = rules.filter(rule => isGood(rule) && isRegex(rule)); - log(`\tMaybe good (regexes): ${regexes.length}`); - - const redirects = rules.filter(rule => - isUnsupported(rule) === false && - isRedirect(rule) - ); - log(`\tredirect-rule= (discarded): ${redirects.length}`); - - const headers = rules.filter(rule => - isUnsupported(rule) === false && - isCsp(rule) - ); - log(`\tcsp= (discarded): ${headers.length}`); - - const removeparams = rules.filter(rule => - isUnsupported(rule) === false && - isRemoveparam(rule) - ); - log(`\tremoveparams= (discarded): ${removeparams.length}`); - - const bad = rules.filter(rule => - isUnsupported(rule) - ); - log(`\tUnsupported: ${bad.length}`); - log( - bad.map(rule => rule._error.map(v => `\t\t${v}`)).join('\n'), - true + const results = await dnrRulesetFromRawLists( + [ { name: assetDetails.id, text } ], + { env } ); - writeOps.push( - writeFile( - `${rulesetDir}/${assetDetails.id}.json`, - `${JSON.stringify(good, replacer)}\n` - ) + const netStats = await processNetworkFilters( + assetDetails, + results.network ); - if ( regexes.length !== 0 ) { - writeOps.push( - writeFile( - `${rulesetDir}/${assetDetails.id}.regexes.json`, - `${JSON.stringify(regexes, replacer)}\n` - ) - ); - } + const cosmeticStats = await processCosmeticFilters( + assetDetails, + results.cosmetic + ); - const { cosmetic } = results; - const cssEntries = []; - for ( const entry of cosmetic ) { - const fname = createHash('sha256').update(entry.css).digest('hex').slice(0,8); - const fpath = `${assetDetails.id}/${fname.slice(0,1)}/${fname.slice(1,8)}`; - writeOps.push( - writeFile( - `${cssDir}/${fpath}.css`, - `${entry.css}\n{display:none!important;}\n` - ) - ); - entry.css = fname; - cssEntries.push(entry); - } - log(`CSS entries: ${cssEntries.length}`); - if ( cssEntries.length !== 0 ) { - cssDetails.set(assetDetails.id, cssEntries); - } + const scriptletStats = await processScriptletFilters( + assetDetails, + results.scriptlet + ); rulesetDetails.push({ id: assetDetails.id, @@ -321,19 +531,22 @@ async function main() { lang: assetDetails.lang, homeURL: assetDetails.homeURL, filters: { - total: network.filterCount, - accepted: network.acceptedFilterCount, - rejected: network.rejectedFilterCount, + total: results.network.filterCount, + accepted: results.network.acceptedFilterCount, + rejected: results.network.rejectedFilterCount, }, rules: { - total: rules.length, - accepted: good.length, - discarded: redirects.length + headers.length + removeparams.length, - rejected: bad.length, - regexes: regexes.length, + total: netStats.total, + accepted: netStats.accepted, + discarded: netStats.discarded, + rejected: netStats.rejected, + regexes: netStats.regexes, }, css: { - specific: cssEntries.length, + specific: cosmeticStats, + }, + scriptlets: { + total: scriptletStats, }, }); @@ -342,9 +555,6 @@ async function main() { enabled: assetDetails.enabled, path: `/rulesets/${assetDetails.id}.json` }); - - goodTotalCount += good.length; - maybeGoodTotalCount += regexes.length; }; // Get assets.json content @@ -419,25 +629,23 @@ async function main() { homeURL: 'https://github.com/StevenBlack/hosts#readme', }); - writeOps.push( - writeFile( - `${rulesetDir}/ruleset-details.json`, - `${JSON.stringify(rulesetDetails, null, 2)}\n` - ) + writeFile( + `${rulesetDir}/ruleset-details.json`, + `${JSON.stringify(rulesetDetails, null, 1)}\n` ); - writeOps.push( - writeFile( - `${cssDir}/css-specific.json`, - `${JSON.stringify(Array.from(cssDetails), null, 2)}\n` - ) + writeFile( + `${cssDir}/css-specific.json`, + `${JSON.stringify(Array.from(cssDetails))}\n` + ); + + writeFile( + `${scriptletDir}/scriptlet-details.json`, + `${JSON.stringify(Array.from(scriptletDetails))}\n` ); await Promise.all(writeOps); - log(`Total good rules count: ${goodTotalCount}`); - log(`Total regex rules count: ${maybeGoodTotalCount}`); - // Patch manifest manifest.declarative_net_request = { rule_resources: ruleResources }; const now = new Date(); diff --git a/platform/mv3/scriptlets/abort-current-script.js b/platform/mv3/scriptlets/abort-current-script.js new file mode 100644 index 000000000..066bd3f38 --- /dev/null +++ b/platform/mv3/scriptlets/abort-current-script.js @@ -0,0 +1,147 @@ +/******************************************************************************* + + 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 +/// alias acs +/// alias abort-current-inline-script +/// alias acis + +try { + +/******************************************************************************/ + +// Issues to mind before changing anything: +// https://github.com/uBlockOrigin/uBlock-issues/issues/2154 + +(function( + 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(); +})( +self.$args$ +); + +/******************************************************************************/ + +} catch(ex) { +} diff --git a/platform/mv3/scriptlets/json-prune.js b/platform/mv3/scriptlets/json-prune.js new file mode 100644 index 000000000..6e6f205d8 --- /dev/null +++ b/platform/mv3/scriptlets/json-prune.js @@ -0,0 +1,123 @@ +/******************************************************************************* + + 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 json-prune + +try { + +/******************************************************************************/ + +// https://github.com/uBlockOrigin/uBlock-issues/issues/1545 +// - Add support for "remove everything if needle matches" case + +(function( + rawPrunePaths = '', + rawNeedlePaths = '' +) { + const prunePaths = rawPrunePaths !== '' + ? rawPrunePaths.split(/ +/) + : []; + let needlePaths; + if ( prunePaths.length === 0 ) { return; } + needlePaths = prunePaths.length !== 0 && rawNeedlePaths !== '' + ? rawNeedlePaths.split(/ +/) + : []; + const findOwner = function(root, path, prune = false) { + let owner = root; + let chain = path; + for (;;) { + if ( typeof owner !== 'object' || owner === null ) { + return false; + } + const pos = chain.indexOf('.'); + if ( pos === -1 ) { + if ( prune === false ) { + return owner.hasOwnProperty(chain); + } + if ( chain === '*' ) { + for ( const key in owner ) { + if ( owner.hasOwnProperty(key) === false ) { continue; } + delete owner[key]; + } + } else if ( owner.hasOwnProperty(chain) ) { + delete owner[chain]; + } + return true; + } + const prop = chain.slice(0, pos); + if ( + prop === '[]' && Array.isArray(owner) || + prop === '*' && owner instanceof Object + ) { + const next = chain.slice(pos + 1); + let found = false; + for ( const key of Object.keys(owner) ) { + found = findOwner(owner[key], next, prune) || found; + } + return found; + } + if ( owner.hasOwnProperty(prop) === false ) { return false; } + owner = owner[prop]; + chain = chain.slice(pos + 1); + } + }; + const mustProcess = function(root) { + for ( const needlePath of needlePaths ) { + if ( findOwner(root, needlePath) === false ) { + return false; + } + } + return true; + }; + const pruner = function(o) { + if ( mustProcess(o) === false ) { return o; } + for ( const path of prunePaths ) { + findOwner(o, path, true); + } + return o; + }; + JSON.parse = new Proxy(JSON.parse, { + apply: function() { + return pruner(Reflect.apply(...arguments)); + }, + }); + Response.prototype.json = new Proxy(Response.prototype.json, { + apply: function() { + return Reflect.apply(...arguments).then(o => pruner(o)); + }, + }); +})( +self.$args$ +); + +/******************************************************************************/ + +} catch(ex) { +} diff --git a/platform/mv3/scriptlets/set-constant.js b/platform/mv3/scriptlets/set-constant.js new file mode 100644 index 000000000..27eb66b67 --- /dev/null +++ b/platform/mv3/scriptlets/set-constant.js @@ -0,0 +1,165 @@ +/******************************************************************************* + + 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 +/// alias set + +try { + +/******************************************************************************/ + +(function( + 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); +})( +self.$args$ +); + +/******************************************************************************/ + +} catch(ex) { +} diff --git a/src/css/themes/default.css b/src/css/themes/default.css index 1fb898686..e9441f761 100644 --- a/src/css/themes/default.css +++ b/src/css/themes/default.css @@ -128,6 +128,7 @@ :root { --font-size: 14px; --font-size-smaller: 13px; + --font-size-xsmall: 11px; --font-size-larger: 15px; --font-family: Inter, sans-serif; --monospace-size: 12px; diff --git a/src/js/static-dnr-filtering.js b/src/js/static-dnr-filtering.js index 4a441585b..91ed11819 100644 --- a/src/js/static-dnr-filtering.js +++ b/src/js/static-dnr-filtering.js @@ -37,23 +37,52 @@ import { function addExtendedToDNR(context, parser) { if ( parser.category !== parser.CATStaticExtFilter ) { return false; } - if ( (parser.flavorBits & parser.BITFlavorUnsupported) !== 0 ) { - return true; - } + if ( (parser.flavorBits & parser.BITFlavorUnsupported) !== 0 ) { return; } // Scriptlet injection if ( (parser.flavorBits & parser.BITFlavorExtScriptlet) !== 0 ) { - return true; + if ( parser.hasOptions() === false ) { return; } + if ( context.scriptletFilters === undefined ) { + context.scriptletFilters = new Map(); + } + 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); + } + if ( not ) { + if ( details.excludeMatches === undefined ) { + details.excludeMatches = []; + } + details.excludeMatches.push(hn); + continue; + } + if ( details.matches === undefined ) { + details.matches = []; + } + if ( details.matches.includes('*') ) { continue; } + if ( hn === '*' ) { + details.matches = [ '*' ]; + continue; + } + details.matches.push(hn); + } + return; } // Response header filtering if ( (parser.flavorBits & parser.BITFlavorExtResponseHeader) !== 0 ) { - return true; + return; } // HTML filtering if ( (parser.flavorBits & parser.BITFlavorExtHTML) !== 0 ) { - return true; + return; } // Cosmetic filtering @@ -66,60 +95,36 @@ function addExtendedToDNR(context, parser) { // of same filter OR globally if there is no non-negated hostnames. for ( const { hn, not, bad } of parser.extOptions() ) { if ( bad ) { continue; } + if ( hn.endsWith('.*') ) { continue; } const { compiled, exception } = parser.result; if ( compiled.startsWith('{') ) { continue; } if ( exception ) { continue; } - if ( hn.endsWith('.*') ) { continue; } - let cssdetails = context.cosmeticFilters.get(compiled); - if ( cssdetails === undefined ) { - cssdetails = { - }; - context.cosmeticFilters.set(compiled, cssdetails); + let details = context.cosmeticFilters.get(compiled); + if ( details === undefined ) { + details = {}; + context.cosmeticFilters.set(compiled, details); } if ( not ) { - if ( cssdetails.excludeMatches === undefined ) { - cssdetails.excludeMatches = []; + if ( details.excludeMatches === undefined ) { + details.excludeMatches = []; } - cssdetails.excludeMatches.push(hn); + details.excludeMatches.push(hn); continue; } - if ( cssdetails.matches === undefined ) { - cssdetails.matches = []; + if ( details.matches === undefined ) { + details.matches = []; } - if ( cssdetails.matches.includes('*') ) { continue; } + if ( details.matches.includes('*') ) { continue; } if ( hn === '*' ) { - cssdetails.matches = [ '*' ]; + details.matches = [ '*' ]; continue; } - cssdetails.matches.push(hn); + details.matches.push(hn); } } /******************************************************************************/ -function optimizeCosmeticFilters(filters) { - if ( filters === undefined ) { return []; } - const merge = new Map(); - for ( const [ selector, details ] of filters ) { - const json = JSON.stringify(details); - let entries = merge.get(json); - if ( entries === undefined ) { - entries = new Set(); - merge.set(json, entries); - } - entries.add(selector); - } - const out = []; - for ( const [ json, selectors ] of merge ) { - const details = JSON.parse(json); - details.css = Array.from(selectors).join(',\n'); - out.push(details); - } - return out; -} - -/******************************************************************************/ - function addToDNR(context, list) { const writer = new CompiledListWriter(); const lineIter = new LineIterator( @@ -175,7 +180,8 @@ function addToDNR(context, list) { /******************************************************************************/ async function dnrRulesetFromRawLists(lists, options = {}) { - const context = staticNetFilteringEngine.dnrFromCompiled('begin'); + const context = {}; + staticNetFilteringEngine.dnrFromCompiled('begin', context); context.extensionPaths = new Map(options.extensionPaths || []); context.env = options.env; const toLoad = []; @@ -191,7 +197,8 @@ async function dnrRulesetFromRawLists(lists, options = {}) { return { network: staticNetFilteringEngine.dnrFromCompiled('end', context), - cosmetic: optimizeCosmeticFilters(context.cosmeticFilters), + cosmetic: context.cosmeticFilters, + scriptlet: context.scriptletFilters, }; } diff --git a/src/js/static-filtering-parser.js b/src/js/static-filtering-parser.js index 8be871e2a..70ebc5bd8 100644 --- a/src/js/static-filtering-parser.js +++ b/src/js/static-filtering-parser.js @@ -1337,7 +1337,7 @@ Parser.prototype.SelectorCompiler = class { // context. const cssIdentifier = '[A-Za-z_][\\w-]*'; const cssClassOrId = `[.#]${cssIdentifier}`; - const cssAttribute = `\\[${cssIdentifier}[*^$]?="[^"\\]\\\\]+"\\]`; + const cssAttribute = `\\[${cssIdentifier}(?:[*^$]?="[^"\\]\\\\]+")\\]`; const cssSimple = '(?:' + `${cssIdentifier}(?:${cssClassOrId})*(?:${cssAttribute})*` + '|' + @@ -1502,7 +1502,7 @@ Parser.prototype.SelectorCompiler = class { // assign new text. sheetSelectable(s) { if ( this.reCommonSelector.test(s) ) { return true; } - if ( this.cssValidatorElement === null ) { return true; } + if ( this.cssValidatorElement === null ) { return false; } let valid = false; try { this.cssValidatorElement.childNodes[0].nodeValue = `_z + ${s}{color:red;} _z{color:red;}`; @@ -1521,7 +1521,7 @@ Parser.prototype.SelectorCompiler = class { // - opening comment `/*` querySelectable(s) { if ( this.reCommonSelector.test(s) ) { return true; } - if ( this.div === null ) { return true; } + if ( this.div === null ) { return false; } try { this.div.querySelector(`${s},${s}:not(#foo)`); if ( s.includes('/*') ) { return false; } diff --git a/src/js/static-net-filtering.js b/src/js/static-net-filtering.js index 5df4fe53f..4a1ce8006 100644 --- a/src/js/static-net-filtering.js +++ b/src/js/static-net-filtering.js @@ -3852,14 +3852,15 @@ FilterContainer.prototype.freeze = function() { FilterContainer.prototype.dnrFromCompiled = function(op, context, ...args) { if ( op === 'begin' ) { - return { + Object.assign(context, { good: new Set(), bad: new Set(), invalid: new Set(), filterCount: 0, acceptedFilterCount: 0, rejectedFilterCount: 0, - }; + }); + return; } if ( op === 'add' ) { diff --git a/tools/make-mv3.sh b/tools/make-mv3.sh index 1e116a62e..24f563a53 100755 --- a/tools/make-mv3.sh +++ b/tools/make-mv3.sh @@ -50,6 +50,7 @@ if [ "$1" != "quick" ]; then cp platform/mv3/package.json $TMPDIR/ cp platform/mv3/*.js $TMPDIR/ cp assets/assets.json $TMPDIR/ + cp -R platform/mv3/scriptlets $TMPDIR/ cd $TMPDIR node --no-warnings make-rulesets.js output=$DES cd - > /dev/null