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