1
0
mirror of https://github.com/gorhill/uBlock.git synced 2024-07-08 12:57:57 +02:00

[mv3] Add scriptlet support; improve reliability of cosmetic filtering

First iteration of adding scriptlet support. As with cosmetic
filtering, scriptlet niijection occurs only on sites for which
uBO Lite was granted extended permissions.

At the moment, only three scriptlets are supported:
- abort-current-script
- json-prune
- set-constant

More will be added in the future.
This commit is contained in:
Raymond Hill 2022-09-16 15:56:35 -04:00
parent bf4cc74d3f
commit 232c44eeb2
No known key found for this signature in database
GPG Key ID: 25E1490B761470C2
16 changed files with 1210 additions and 406 deletions

View File

@ -291,8 +291,8 @@ if response.status_code != 204:
# package is higher version than current one. # package is higher version than current one.
# #
# Be sure in sync with potentially modified files on remote # Be sure we are in sync with potentially modified files on remote
r = subprocess.run(['git', 'checkout', 'origin/master', '--', 'dist/chromium-mv3/log.txt'], stdout=subprocess.PIPE) r = subprocess.run(['git', 'pull', 'origin', 'master'], stdout=subprocess.PIPE)
rout = bytes.decode(r.stdout).strip() rout = bytes.decode(r.stdout).strip()
print('Update GitHub to point to newly signed self-hosted xpi package...') print('Update GitHub to point to newly signed self-hosted xpi package...')

View File

@ -181,6 +181,16 @@ body.mobile.no-tooltips .toolRibbon .tool {
margin-bottom: 0; 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:not(.hasGreatPowers) [data-i18n-title="popupGrantGreatPowers"],
body.hasGreatPowers [data-i18n-title="popupRevokeGreatPowers"] { body.hasGreatPowers [data-i18n-title="popupRevokeGreatPowers"] {
display: flex; display: flex;
@ -189,7 +199,7 @@ body:not(.hasGreatPowers) [data-i18n-title="popupRevokeGreatPowers"],
body.hasGreatPowers [data-i18n-title="popupGrantGreatPowers"] { body.hasGreatPowers [data-i18n-title="popupGrantGreatPowers"] {
display: none; display: none;
} }
body.hasGreatPowers [data-i18n-title="popupRevokeGreatPowers"] { body [data-i18n-title="popupRevokeGreatPowers"] {
fill: var(--popup-power-ink); fill: var(--popup-power-ink);
} }

View File

@ -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 };

View File

@ -27,7 +27,7 @@
import { browser, dnr, i18n, runtime } from './ext.js'; import { browser, dnr, i18n, runtime } from './ext.js';
import { fetchJSON } from './fetch.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 TRUSTED_DIRECTIVE_BASE_RULE_ID = 8000000;
const CURRENT_CONFIG_BASE_RULE_ID = 9000000; const CURRENT_CONFIG_BASE_RULE_ID = 9000000;
const dynamicRuleMap = new Map();
const rulesetDetails = new Map();
const rulesetConfig = { const rulesetConfig = {
version: '', version: '',
enabledRulesets: [], 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() { function getCurrentVersion() {
return runtime.getManifest().version; return runtime.getManifest().version;
} }
async function loadRulesetConfig() { async function loadRulesetConfig() {
const dynamicRuleMap = await getDynamicRules();
const configRule = dynamicRuleMap.get(CURRENT_CONFIG_BASE_RULE_ID); const configRule = dynamicRuleMap.get(CURRENT_CONFIG_BASE_RULE_ID);
if ( configRule === undefined ) { if ( configRule === undefined ) {
rulesetConfig.enabledRulesets = await defaultRulesetsFromLanguage(); rulesetConfig.enabledRulesets = await defaultRulesetsFromLanguage();
@ -71,6 +105,7 @@ async function loadRulesetConfig() {
} }
async function saveRulesetConfig() { async function saveRulesetConfig() {
const dynamicRuleMap = await getDynamicRules();
let configRule = dynamicRuleMap.get(CURRENT_CONFIG_BASE_RULE_ID); let configRule = dynamicRuleMap.get(CURRENT_CONFIG_BASE_RULE_ID);
if ( configRule === undefined ) { if ( configRule === undefined ) {
configRule = { 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 // Avoid testing already tested regexes
const validRegexSet = new Set( const validRegexSet = new Set(
dynamicRules.filter(rule => dynamicRules.filter(rule =>
@ -156,6 +199,7 @@ async function updateRegexRules(dynamicRules) {
// Add validated regex rules to dynamic ruleset without affecting rules // Add validated regex rules to dynamic ruleset without affecting rules
// outside regex rule realm. // outside regex rule realm.
const dynamicRuleMap = await getDynamicRules();
const newRuleMap = new Map(newRules.map(rule => [ rule.id, rule ])); const newRuleMap = new Map(newRules.map(rule => [ rule.id, rule ]));
const addRules = []; const addRules = [];
const removeRuleIds = []; const removeRuleIds = [];
@ -186,6 +230,7 @@ async function updateRegexRules(dynamicRules) {
async function matchesTrustedSiteDirective(details) { async function matchesTrustedSiteDirective(details) {
const url = new URL(details.origin); const url = new URL(details.origin);
const dynamicRuleMap = await getDynamicRules();
let rule = dynamicRuleMap.get(TRUSTED_DIRECTIVE_BASE_RULE_ID); let rule = dynamicRuleMap.get(TRUSTED_DIRECTIVE_BASE_RULE_ID);
if ( rule === undefined ) { return false; } if ( rule === undefined ) { return false; }
const domainSet = new Set(rule.condition.requestDomains); const domainSet = new Set(rule.condition.requestDomains);
@ -201,6 +246,7 @@ async function matchesTrustedSiteDirective(details) {
async function addTrustedSiteDirective(details) { async function addTrustedSiteDirective(details) {
const url = new URL(details.origin); const url = new URL(details.origin);
const dynamicRuleMap = await getDynamicRules();
let rule = dynamicRuleMap.get(TRUSTED_DIRECTIVE_BASE_RULE_ID); let rule = dynamicRuleMap.get(TRUSTED_DIRECTIVE_BASE_RULE_ID);
if ( rule !== undefined ) { if ( rule !== undefined ) {
rule.condition.initiatorDomains = undefined; rule.condition.initiatorDomains = undefined;
@ -233,6 +279,7 @@ async function addTrustedSiteDirective(details) {
async function removeTrustedSiteDirective(details) { async function removeTrustedSiteDirective(details) {
const url = new URL(details.origin); const url = new URL(details.origin);
const dynamicRuleMap = await getDynamicRules();
let rule = dynamicRuleMap.get(TRUSTED_DIRECTIVE_BASE_RULE_ID); let rule = dynamicRuleMap.get(TRUSTED_DIRECTIVE_BASE_RULE_ID);
if ( rule === undefined ) { return false; } if ( rule === undefined ) { return false; }
rule.condition.initiatorDomains = undefined; rule.condition.initiatorDomains = undefined;
@ -291,7 +338,13 @@ async function enableRulesets(ids) {
} }
async function getEnabledRulesetsStats() { async function getEnabledRulesetsStats() {
const ids = await dnr.getEnabledRulesets(); const [
rulesetDetails,
ids,
] = await Promise.all([
getRulesetDetails(),
dnr.getEnabledRulesets(),
]);
const out = []; const out = [];
for ( const id of ids ) { for ( const id of ids ) {
const ruleset = rulesetDetails.get(id); const ruleset = rulesetDetails.get(id);
@ -323,6 +376,7 @@ async function defaultRulesetsFromLanguage() {
`\\b(${Array.from(langSet).join('|')})\\b` `\\b(${Array.from(langSet).join('|')})\\b`
); );
const rulesetDetails = await getRulesetDetails();
for ( const [ id, details ] of rulesetDetails ) { for ( const [ id, details ] of rulesetDetails ) {
if ( typeof details.lang !== 'string' ) { continue; } if ( typeof details.lang !== 'string' ) { continue; }
if ( reTargetLang.test(details.lang) === false ) { continue; } if ( reTargetLang.test(details.lang) === false ) { continue; }
@ -371,7 +425,11 @@ function onMessage(request, sender, callback) {
} }
case 'getRulesetData': { case 'getRulesetData': {
dnr.getEnabledRulesets().then(enabledRulesets => { Promise.all([
getRulesetDetails(),
dnr.getEnabledRulesets(),
]).then(results => {
const [ rulesetDetails, enabledRulesets ] = results;
callback({ callback({
enabledRulesets, enabledRulesets,
rulesetDetails: Array.from(rulesetDetails.values()), rulesetDetails: Array.from(rulesetDetails.values()),
@ -382,6 +440,7 @@ function onMessage(request, sender, callback) {
case 'grantGreatPowers': case 'grantGreatPowers':
grantGreatPowers(request.hostname).then(granted => { grantGreatPowers(request.hostname).then(granted => {
console.info(`Granted uBOL great powers on ${request.hostname}: ${granted}`);
callback(granted); callback(granted);
}); });
return true; return true;
@ -403,6 +462,7 @@ function onMessage(request, sender, callback) {
case 'revokeGreatPowers': case 'revokeGreatPowers':
revokeGreatPowers(request.hostname).then(removed => { revokeGreatPowers(request.hostname).then(removed => {
console.info(`Revoked great powers from uBOL on ${request.hostname}: ${removed}`);
callback(removed); callback(removed);
}); });
return true; return true;
@ -421,37 +481,19 @@ function onMessage(request, sender, callback) {
} }
async function onPermissionsChanged() { async function onPermissionsChanged() {
await registerCSS(); await registerInjectable();
} }
/******************************************************************************/ /******************************************************************************/
async function start() { 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(); 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); await enableRulesets(rulesetConfig.enabledRulesets);
// We need to update the regex rules only when ruleset version changes. // We need to update the regex rules only when ruleset version changes.
const currentVersion = getCurrentVersion(); const currentVersion = getCurrentVersion();
if ( currentVersion !== rulesetConfig.version ) { if ( currentVersion !== rulesetConfig.version ) {
await updateRegexRules(dynamicRules); await updateRegexRules();
console.log(`Version change: ${rulesetConfig.version} => ${currentVersion}`); console.log(`Version change: ${rulesetConfig.version} => ${currentVersion}`);
rulesetConfig.version = currentVersion; rulesetConfig.version = currentVersion;
} }

View File

@ -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
};

View File

@ -25,7 +25,7 @@
"128": "img/icon_128.png" "128": "img/icon_128.png"
}, },
"manifest_version": 3, "manifest_version": 3,
"minimum_chrome_version": "101.0", "minimum_chrome_version": "105.0",
"name": "__MSG_extName__", "name": "__MSG_extName__",
"options_page": "dashboard.html", "options_page": "dashboard.html",
"optional_host_permissions": [ "optional_host_permissions": [

View File

@ -44,6 +44,7 @@
<span id="toggleGreatPowers"> <span id="toggleGreatPowers">
<span class="fa-icon tool enabled" data-i18n-title="popupGrantGreatPowers">sun-o<span class="caption"></span></span> <span class="fa-icon tool enabled" data-i18n-title="popupGrantGreatPowers">sun-o<span class="caption"></span></span>
<span class="fa-icon tool enabled" data-i18n-title="popupRevokeGreatPowers">sun<span class="caption"></span></span> <span class="fa-icon tool enabled" data-i18n-title="popupRevokeGreatPowers">sun<span class="caption"></span></span>
<span class="badge"></span>
</span> </span>
<span></span> <span></span>
<span></span> <span></span>

View File

@ -51,6 +51,13 @@ const commandLineArgs = (( ) => {
return args; 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 => const isUnsupported = rule =>
@ -133,21 +140,344 @@ const fetchList = (url, cacheDir) => {
const writeFile = async (fname, data) => { const writeFile = async (fname, data) => {
const dir = path.dirname(fname); const dir = path.dirname(fname);
await fs.mkdir(dir, { recursive: true }); 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() { async function main() {
const env = [ 'chromium' ];
const writeOps = [];
const ruleResources = [];
const rulesetDetails = [];
const cssDetails = new Map();
const outputDir = commandLineArgs.get('output') || '.';
// Get manifest content // Get manifest content
const manifest = await fs.readFile( const manifest = await fs.readFile(
`${outputDir}/manifest.json`, `${outputDir}/manifest.json`,
@ -168,151 +498,31 @@ async function main() {
} }
log(`Version: ${version}`); 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) { const rulesetFromURLS = async function(assetDetails) {
log('============================'); log('============================');
log(`Listset for '${assetDetails.id}':`); log(`Listset for '${assetDetails.id}':`);
// Remember fetched URLs const text = await fetchAsset(assetDetails);
const fetchedURLs = new Set();
// Fetch list and expand `!#include` directives const results = await dnrRulesetFromRawLists(
let parts = assetDetails.urls.map(url => ({ url })); [ { name: assetDetails.id, text } ],
while ( parts.every(v => typeof v === 'string') === false ) { { env }
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
); );
writeOps.push( const netStats = await processNetworkFilters(
writeFile( assetDetails,
`${rulesetDir}/${assetDetails.id}.json`, results.network
`${JSON.stringify(good, replacer)}\n`
)
); );
if ( regexes.length !== 0 ) { const cosmeticStats = await processCosmeticFilters(
writeOps.push( assetDetails,
writeFile( results.cosmetic
`${rulesetDir}/${assetDetails.id}.regexes.json`, );
`${JSON.stringify(regexes, replacer)}\n`
)
);
}
const { cosmetic } = results; const scriptletStats = await processScriptletFilters(
const cssEntries = []; assetDetails,
for ( const entry of cosmetic ) { results.scriptlet
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);
}
rulesetDetails.push({ rulesetDetails.push({
id: assetDetails.id, id: assetDetails.id,
@ -321,19 +531,22 @@ async function main() {
lang: assetDetails.lang, lang: assetDetails.lang,
homeURL: assetDetails.homeURL, homeURL: assetDetails.homeURL,
filters: { filters: {
total: network.filterCount, total: results.network.filterCount,
accepted: network.acceptedFilterCount, accepted: results.network.acceptedFilterCount,
rejected: network.rejectedFilterCount, rejected: results.network.rejectedFilterCount,
}, },
rules: { rules: {
total: rules.length, total: netStats.total,
accepted: good.length, accepted: netStats.accepted,
discarded: redirects.length + headers.length + removeparams.length, discarded: netStats.discarded,
rejected: bad.length, rejected: netStats.rejected,
regexes: regexes.length, regexes: netStats.regexes,
}, },
css: { css: {
specific: cssEntries.length, specific: cosmeticStats,
},
scriptlets: {
total: scriptletStats,
}, },
}); });
@ -342,9 +555,6 @@ async function main() {
enabled: assetDetails.enabled, enabled: assetDetails.enabled,
path: `/rulesets/${assetDetails.id}.json` path: `/rulesets/${assetDetails.id}.json`
}); });
goodTotalCount += good.length;
maybeGoodTotalCount += regexes.length;
}; };
// Get assets.json content // Get assets.json content
@ -419,25 +629,23 @@ async function main() {
homeURL: 'https://github.com/StevenBlack/hosts#readme', homeURL: 'https://github.com/StevenBlack/hosts#readme',
}); });
writeOps.push( writeFile(
writeFile( `${rulesetDir}/ruleset-details.json`,
`${rulesetDir}/ruleset-details.json`, `${JSON.stringify(rulesetDetails, null, 1)}\n`
`${JSON.stringify(rulesetDetails, null, 2)}\n`
)
); );
writeOps.push( writeFile(
writeFile( `${cssDir}/css-specific.json`,
`${cssDir}/css-specific.json`, `${JSON.stringify(Array.from(cssDetails))}\n`
`${JSON.stringify(Array.from(cssDetails), null, 2)}\n` );
)
writeFile(
`${scriptletDir}/scriptlet-details.json`,
`${JSON.stringify(Array.from(scriptletDetails))}\n`
); );
await Promise.all(writeOps); await Promise.all(writeOps);
log(`Total good rules count: ${goodTotalCount}`);
log(`Total regex rules count: ${maybeGoodTotalCount}`);
// Patch manifest // Patch manifest
manifest.declarative_net_request = { rule_resources: ruleResources }; manifest.declarative_net_request = { rule_resources: ruleResources };
const now = new Date(); const now = new Date();

View File

@ -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) {
}

View File

@ -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) {
}

View File

@ -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) {
}

View File

@ -128,6 +128,7 @@
:root { :root {
--font-size: 14px; --font-size: 14px;
--font-size-smaller: 13px; --font-size-smaller: 13px;
--font-size-xsmall: 11px;
--font-size-larger: 15px; --font-size-larger: 15px;
--font-family: Inter, sans-serif; --font-family: Inter, sans-serif;
--monospace-size: 12px; --monospace-size: 12px;

View File

@ -37,23 +37,52 @@ import {
function addExtendedToDNR(context, parser) { function addExtendedToDNR(context, parser) {
if ( parser.category !== parser.CATStaticExtFilter ) { return false; } if ( parser.category !== parser.CATStaticExtFilter ) { return false; }
if ( (parser.flavorBits & parser.BITFlavorUnsupported) !== 0 ) { if ( (parser.flavorBits & parser.BITFlavorUnsupported) !== 0 ) { return; }
return true;
}
// Scriptlet injection // Scriptlet injection
if ( (parser.flavorBits & parser.BITFlavorExtScriptlet) !== 0 ) { 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 // Response header filtering
if ( (parser.flavorBits & parser.BITFlavorExtResponseHeader) !== 0 ) { if ( (parser.flavorBits & parser.BITFlavorExtResponseHeader) !== 0 ) {
return true; return;
} }
// HTML filtering // HTML filtering
if ( (parser.flavorBits & parser.BITFlavorExtHTML) !== 0 ) { if ( (parser.flavorBits & parser.BITFlavorExtHTML) !== 0 ) {
return true; return;
} }
// Cosmetic filtering // Cosmetic filtering
@ -66,60 +95,36 @@ function addExtendedToDNR(context, parser) {
// of same filter OR globally if there is no non-negated hostnames. // of same filter OR globally if there is no non-negated hostnames.
for ( const { hn, not, bad } of parser.extOptions() ) { for ( const { hn, not, bad } of parser.extOptions() ) {
if ( bad ) { continue; } if ( bad ) { continue; }
if ( hn.endsWith('.*') ) { continue; }
const { compiled, exception } = parser.result; const { compiled, exception } = parser.result;
if ( compiled.startsWith('{') ) { continue; } if ( compiled.startsWith('{') ) { continue; }
if ( exception ) { continue; } if ( exception ) { continue; }
if ( hn.endsWith('.*') ) { continue; } let details = context.cosmeticFilters.get(compiled);
let cssdetails = context.cosmeticFilters.get(compiled); if ( details === undefined ) {
if ( cssdetails === undefined ) { details = {};
cssdetails = { context.cosmeticFilters.set(compiled, details);
};
context.cosmeticFilters.set(compiled, cssdetails);
} }
if ( not ) { if ( not ) {
if ( cssdetails.excludeMatches === undefined ) { if ( details.excludeMatches === undefined ) {
cssdetails.excludeMatches = []; details.excludeMatches = [];
} }
cssdetails.excludeMatches.push(hn); details.excludeMatches.push(hn);
continue; continue;
} }
if ( cssdetails.matches === undefined ) { if ( details.matches === undefined ) {
cssdetails.matches = []; details.matches = [];
} }
if ( cssdetails.matches.includes('*') ) { continue; } if ( details.matches.includes('*') ) { continue; }
if ( hn === '*' ) { if ( hn === '*' ) {
cssdetails.matches = [ '*' ]; details.matches = [ '*' ];
continue; 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) { function addToDNR(context, list) {
const writer = new CompiledListWriter(); const writer = new CompiledListWriter();
const lineIter = new LineIterator( const lineIter = new LineIterator(
@ -175,7 +180,8 @@ function addToDNR(context, list) {
/******************************************************************************/ /******************************************************************************/
async function dnrRulesetFromRawLists(lists, options = {}) { async function dnrRulesetFromRawLists(lists, options = {}) {
const context = staticNetFilteringEngine.dnrFromCompiled('begin'); const context = {};
staticNetFilteringEngine.dnrFromCompiled('begin', context);
context.extensionPaths = new Map(options.extensionPaths || []); context.extensionPaths = new Map(options.extensionPaths || []);
context.env = options.env; context.env = options.env;
const toLoad = []; const toLoad = [];
@ -191,7 +197,8 @@ async function dnrRulesetFromRawLists(lists, options = {}) {
return { return {
network: staticNetFilteringEngine.dnrFromCompiled('end', context), network: staticNetFilteringEngine.dnrFromCompiled('end', context),
cosmetic: optimizeCosmeticFilters(context.cosmeticFilters), cosmetic: context.cosmeticFilters,
scriptlet: context.scriptletFilters,
}; };
} }

View File

@ -1337,7 +1337,7 @@ Parser.prototype.SelectorCompiler = class {
// context. // context.
const cssIdentifier = '[A-Za-z_][\\w-]*'; const cssIdentifier = '[A-Za-z_][\\w-]*';
const cssClassOrId = `[.#]${cssIdentifier}`; const cssClassOrId = `[.#]${cssIdentifier}`;
const cssAttribute = `\\[${cssIdentifier}[*^$]?="[^"\\]\\\\]+"\\]`; const cssAttribute = `\\[${cssIdentifier}(?:[*^$]?="[^"\\]\\\\]+")\\]`;
const cssSimple = const cssSimple =
'(?:' + '(?:' +
`${cssIdentifier}(?:${cssClassOrId})*(?:${cssAttribute})*` + '|' + `${cssIdentifier}(?:${cssClassOrId})*(?:${cssAttribute})*` + '|' +
@ -1502,7 +1502,7 @@ Parser.prototype.SelectorCompiler = class {
// assign new text. // assign new text.
sheetSelectable(s) { sheetSelectable(s) {
if ( this.reCommonSelector.test(s) ) { return true; } if ( this.reCommonSelector.test(s) ) { return true; }
if ( this.cssValidatorElement === null ) { return true; } if ( this.cssValidatorElement === null ) { return false; }
let valid = false; let valid = false;
try { try {
this.cssValidatorElement.childNodes[0].nodeValue = `_z + ${s}{color:red;} _z{color:red;}`; this.cssValidatorElement.childNodes[0].nodeValue = `_z + ${s}{color:red;} _z{color:red;}`;
@ -1521,7 +1521,7 @@ Parser.prototype.SelectorCompiler = class {
// - opening comment `/*` // - opening comment `/*`
querySelectable(s) { querySelectable(s) {
if ( this.reCommonSelector.test(s) ) { return true; } if ( this.reCommonSelector.test(s) ) { return true; }
if ( this.div === null ) { return true; } if ( this.div === null ) { return false; }
try { try {
this.div.querySelector(`${s},${s}:not(#foo)`); this.div.querySelector(`${s},${s}:not(#foo)`);
if ( s.includes('/*') ) { return false; } if ( s.includes('/*') ) { return false; }

View File

@ -3852,14 +3852,15 @@ FilterContainer.prototype.freeze = function() {
FilterContainer.prototype.dnrFromCompiled = function(op, context, ...args) { FilterContainer.prototype.dnrFromCompiled = function(op, context, ...args) {
if ( op === 'begin' ) { if ( op === 'begin' ) {
return { Object.assign(context, {
good: new Set(), good: new Set(),
bad: new Set(), bad: new Set(),
invalid: new Set(), invalid: new Set(),
filterCount: 0, filterCount: 0,
acceptedFilterCount: 0, acceptedFilterCount: 0,
rejectedFilterCount: 0, rejectedFilterCount: 0,
}; });
return;
} }
if ( op === 'add' ) { if ( op === 'add' ) {

View File

@ -50,6 +50,7 @@ if [ "$1" != "quick" ]; then
cp platform/mv3/package.json $TMPDIR/ cp platform/mv3/package.json $TMPDIR/
cp platform/mv3/*.js $TMPDIR/ cp platform/mv3/*.js $TMPDIR/
cp assets/assets.json $TMPDIR/ cp assets/assets.json $TMPDIR/
cp -R platform/mv3/scriptlets $TMPDIR/
cd $TMPDIR cd $TMPDIR
node --no-warnings make-rulesets.js output=$DES node --no-warnings make-rulesets.js output=$DES
cd - > /dev/null cd - > /dev/null