2022-09-06 17:47:52 +00:00
|
|
|
/*******************************************************************************
|
|
|
|
|
|
2023-12-04 17:10:34 +00:00
|
|
|
uBlock Origin - a comprehensive, efficient content blocker
|
2022-09-06 17:47:52 +00:00
|
|
|
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
|
|
|
|
|
*/
|
|
|
|
|
|
2024-08-22 16:36:31 +00:00
|
|
|
import * as makeScriptlet from './make-scriptlets.js';
|
|
|
|
|
import * as sfp from './js/static-filtering-parser.js';
|
2022-09-06 17:47:52 +00:00
|
|
|
|
2024-12-29 21:38:56 +00:00
|
|
|
import {
|
|
|
|
|
createHash,
|
|
|
|
|
randomBytes,
|
|
|
|
|
} from 'crypto';
|
|
|
|
|
import {
|
|
|
|
|
dnrRulesetFromRawLists,
|
|
|
|
|
mergeRules,
|
|
|
|
|
} from './js/static-dnr-filtering.js';
|
2022-09-06 17:47:52 +00:00
|
|
|
|
2025-05-20 11:34:05 +00:00
|
|
|
import { execSync } from 'node:child_process';
|
2022-09-06 17:47:52 +00:00
|
|
|
import fs from 'fs/promises';
|
2025-12-27 14:45:25 +00:00
|
|
|
import { literalStrFromRegex } from './js/regex-analyzer.js';
|
2022-09-15 17:14:08 +00:00
|
|
|
import path from 'path';
|
2022-09-06 17:47:52 +00:00
|
|
|
import process from 'process';
|
2022-10-16 16:05:24 +00:00
|
|
|
import redirectResourcesMap from './js/redirect-resources.js';
|
2023-06-06 00:17:50 +00:00
|
|
|
import { safeReplace } from './safe-replace.js';
|
2022-09-06 17:47:52 +00:00
|
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
|
|
|
|
|
const commandLineArgs = (( ) => {
|
|
|
|
|
const args = new Map();
|
|
|
|
|
let name, value;
|
|
|
|
|
for ( const arg of process.argv.slice(2) ) {
|
|
|
|
|
const pos = arg.indexOf('=');
|
|
|
|
|
if ( pos === -1 ) {
|
|
|
|
|
name = arg;
|
|
|
|
|
value = '';
|
|
|
|
|
} else {
|
|
|
|
|
name = arg.slice(0, pos);
|
|
|
|
|
value = arg.slice(pos+1);
|
|
|
|
|
}
|
|
|
|
|
args.set(name, value);
|
|
|
|
|
}
|
|
|
|
|
return args;
|
|
|
|
|
})();
|
|
|
|
|
|
2023-08-11 17:22:25 +00:00
|
|
|
const platform = commandLineArgs.get('platform') || 'chromium';
|
2022-09-16 19:56:35 +00:00
|
|
|
const outputDir = commandLineArgs.get('output') || '.';
|
|
|
|
|
const cacheDir = `${outputDir}/../mv3-data`;
|
|
|
|
|
const rulesetDir = `${outputDir}/rulesets`;
|
2022-10-15 17:05:20 +00:00
|
|
|
const scriptletDir = `${rulesetDir}/scripting`;
|
2025-09-02 16:24:59 +00:00
|
|
|
const envExtra = (( ) => {
|
|
|
|
|
const env = commandLineArgs.get('env');
|
|
|
|
|
return env ? env.split('|') : [];
|
|
|
|
|
})();
|
2022-09-30 13:18:52 +00:00
|
|
|
const env = [
|
2023-08-11 17:22:25 +00:00
|
|
|
platform,
|
2025-03-08 16:31:31 +00:00
|
|
|
'native_css_has',
|
2022-10-11 16:02:33 +00:00
|
|
|
'mv3',
|
2022-09-30 13:18:52 +00:00
|
|
|
'ublock',
|
|
|
|
|
'ubol',
|
|
|
|
|
'user_stylesheet',
|
2025-09-02 16:24:59 +00:00
|
|
|
...envExtra,
|
2022-09-30 13:18:52 +00:00
|
|
|
];
|
2022-09-16 19:56:35 +00:00
|
|
|
|
2025-05-10 16:32:09 +00:00
|
|
|
if ( platform === 'edge' ) {
|
2025-03-08 16:31:31 +00:00
|
|
|
env.push('chromium');
|
2023-08-11 17:22:25 +00:00
|
|
|
}
|
|
|
|
|
|
2022-09-06 17:47:52 +00:00
|
|
|
/******************************************************************************/
|
|
|
|
|
|
2022-09-18 21:07:02 +00:00
|
|
|
const jsonSetMapReplacer = (k, v) => {
|
|
|
|
|
if ( v instanceof Set || v instanceof Map ) {
|
|
|
|
|
if ( v.size === 0 ) { return; }
|
|
|
|
|
return Array.from(v);
|
|
|
|
|
}
|
|
|
|
|
return v;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
|
2025-02-22 12:38:12 +00:00
|
|
|
const consoleLog = console.log;
|
2022-09-13 21:44:24 +00:00
|
|
|
const stdOutput = [];
|
|
|
|
|
|
2025-02-21 13:23:27 +00:00
|
|
|
const log = (text, silent = true) => {
|
2025-12-04 17:57:25 +00:00
|
|
|
silent = silent && text.startsWith('!!!') === false;
|
2022-09-13 21:44:24 +00:00
|
|
|
stdOutput.push(text);
|
|
|
|
|
if ( silent === false ) {
|
2025-02-22 12:38:12 +00:00
|
|
|
consoleLog(text);
|
2022-09-13 21:44:24 +00:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-02-22 12:38:12 +00:00
|
|
|
console.log = log;
|
|
|
|
|
|
2025-04-13 19:37:07 +00:00
|
|
|
const logProgress = text => {
|
2025-04-13 19:45:39 +00:00
|
|
|
process?.stdout?.clearLine?.();
|
|
|
|
|
process?.stdout?.cursorTo?.(0);
|
2025-05-20 11:34:05 +00:00
|
|
|
process?.stdout?.write?.(text.length > 120 ? `${text.slice(0, 119)}… ` : `${text} `);
|
2025-04-13 19:37:07 +00:00
|
|
|
};
|
|
|
|
|
|
2025-12-25 17:34:21 +00:00
|
|
|
const isHnRegexOrPath = hn => hn.includes('/');
|
|
|
|
|
|
2022-09-13 21:44:24 +00:00
|
|
|
/******************************************************************************/
|
|
|
|
|
|
2025-05-20 11:34:05 +00:00
|
|
|
async function fetchText(url, cacheDir) {
|
2025-11-14 17:47:28 +00:00
|
|
|
logProgress(`Reading locally cached ${path.basename(url)}`);
|
2025-05-20 11:34:05 +00:00
|
|
|
const fname = url
|
2022-09-15 17:14:08 +00:00
|
|
|
.replace(/^https?:\/\//, '')
|
2025-05-20 11:34:05 +00:00
|
|
|
.replace(/\//g, '_');(url);
|
|
|
|
|
const content = await fs.readFile(
|
|
|
|
|
`${cacheDir}/${fname}`,
|
|
|
|
|
{ encoding: 'utf8' }
|
|
|
|
|
).catch(( ) => { });
|
|
|
|
|
if ( content !== undefined ) {
|
|
|
|
|
log(`\tFetched local ${url}`);
|
|
|
|
|
return { url, content };
|
|
|
|
|
}
|
2025-11-14 17:47:28 +00:00
|
|
|
logProgress(`Fetching remote ${path.basename(url)}`);
|
2025-05-20 11:34:05 +00:00
|
|
|
log(`\tFetching remote ${url}`);
|
|
|
|
|
const response = await fetch(url).catch(( ) => { });
|
|
|
|
|
if ( response === undefined ) {
|
|
|
|
|
return { url, error: `Fetching failed: ${url}` };
|
|
|
|
|
}
|
|
|
|
|
let text;
|
|
|
|
|
if ( response.ok ) {
|
|
|
|
|
text = await response.text().catch(( ) => { });
|
|
|
|
|
} else {
|
|
|
|
|
text = await fallbackFetchText(url).catch(( ) => { });
|
|
|
|
|
}
|
|
|
|
|
if ( text === undefined ) {
|
|
|
|
|
return { url, error: `Fetching text content failed: ${url}` };
|
|
|
|
|
}
|
|
|
|
|
writeFile(`${cacheDir}/${fname}`, text);
|
|
|
|
|
return { url, content: text };
|
|
|
|
|
}
|
2022-09-15 17:14:08 +00:00
|
|
|
|
2025-05-20 11:34:05 +00:00
|
|
|
async function fallbackFetchText(url) {
|
|
|
|
|
const match = /^https:\/\/raw\.githubusercontent\.com\/([^/]+)\/([^/]+)\/master\/([^?]+)/.exec(url);
|
|
|
|
|
if ( match === null ) { return; }
|
2025-11-14 17:47:28 +00:00
|
|
|
logProgress(`\tGitHub CLI-fetching remote ${path.basename(url)}`);
|
2025-05-20 11:34:05 +00:00
|
|
|
// https://docs.github.com/en/rest/repos/contents
|
|
|
|
|
const content = execSync(`gh api \
|
|
|
|
|
-H "Accept: application/vnd.github.raw+json" \
|
|
|
|
|
-H "X-GitHub-Api-Version: 2022-11-28" \
|
|
|
|
|
/repos/${match[1]}/${match[2]}/contents/${match[3]} \
|
|
|
|
|
`, { encoding: 'utf8' });
|
|
|
|
|
return content;
|
|
|
|
|
}
|
2022-09-13 21:44:24 +00:00
|
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
|
2022-09-15 17:14:08 +00:00
|
|
|
const writeFile = async (fname, data) => {
|
|
|
|
|
const dir = path.dirname(fname);
|
|
|
|
|
await fs.mkdir(dir, { recursive: true });
|
2022-09-16 19:56:35 +00:00
|
|
|
const promise = fs.writeFile(fname, data);
|
|
|
|
|
writeOps.push(promise);
|
|
|
|
|
return promise;
|
2022-09-15 17:14:08 +00:00
|
|
|
};
|
|
|
|
|
|
2022-10-16 16:05:24 +00:00
|
|
|
const copyFile = async (from, to) => {
|
|
|
|
|
const dir = path.dirname(to);
|
|
|
|
|
await fs.mkdir(dir, { recursive: true });
|
|
|
|
|
const promise = fs.copyFile(from, to);
|
|
|
|
|
writeOps.push(promise);
|
|
|
|
|
return promise;
|
|
|
|
|
};
|
|
|
|
|
|
2022-09-16 19:56:35 +00:00
|
|
|
const writeOps = [];
|
2022-09-15 17:14:08 +00:00
|
|
|
|
2022-09-16 19:56:35 +00:00
|
|
|
/******************************************************************************/
|
2022-09-06 17:47:52 +00:00
|
|
|
|
2022-09-16 19:56:35 +00:00
|
|
|
const ruleResources = [];
|
|
|
|
|
const rulesetDetails = [];
|
2022-10-15 17:05:20 +00:00
|
|
|
const scriptletStats = new Map();
|
|
|
|
|
const genericDetails = new Map();
|
2022-10-16 16:05:24 +00:00
|
|
|
const requiredRedirectResources = new Set();
|
2025-04-24 13:58:31 +00:00
|
|
|
let networkBad = new Set();
|
2022-09-13 21:44:24 +00:00
|
|
|
|
2024-12-29 21:38:56 +00:00
|
|
|
// This will be used to sign our inserted `!#trusted on` directives
|
|
|
|
|
const secret = createHash('sha256').update(randomBytes(16)).digest('hex').slice(0,16);
|
2025-02-21 13:23:27 +00:00
|
|
|
log(`Secret: ${secret}`, false);
|
2024-12-29 21:38:56 +00:00
|
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
|
|
|
|
|
const restrSeparator = '(?:[^%.0-9a-z_-]|$)';
|
|
|
|
|
|
|
|
|
|
const rePatternFromUrlFilter = s => {
|
|
|
|
|
let anchor = 0b000;
|
|
|
|
|
if ( s.startsWith('||') ) {
|
|
|
|
|
anchor = 0b100;
|
|
|
|
|
s = s.slice(2);
|
|
|
|
|
} else if ( s.startsWith('|') ) {
|
|
|
|
|
anchor = 0b010;
|
|
|
|
|
s = s.slice(1);
|
|
|
|
|
}
|
|
|
|
|
if ( s.endsWith('|') ) {
|
|
|
|
|
anchor |= 0b001;
|
|
|
|
|
s = s.slice(0, -1);
|
|
|
|
|
}
|
|
|
|
|
let reStr = s.replace(rePatternFromUrlFilter.rePlainChars, '\\$&')
|
|
|
|
|
.replace(rePatternFromUrlFilter.reSeparators, restrSeparator)
|
|
|
|
|
.replace(rePatternFromUrlFilter.reDanglingAsterisks, '')
|
|
|
|
|
.replace(rePatternFromUrlFilter.reAsterisks, '\\S*?');
|
|
|
|
|
if ( anchor & 0b100 ) {
|
|
|
|
|
reStr = (
|
|
|
|
|
reStr.startsWith('\\.') ?
|
|
|
|
|
rePatternFromUrlFilter.restrHostnameAnchor2 :
|
|
|
|
|
rePatternFromUrlFilter.restrHostnameAnchor1
|
|
|
|
|
) + reStr;
|
|
|
|
|
} else if ( anchor & 0b010 ) {
|
|
|
|
|
reStr = '^' + reStr;
|
|
|
|
|
}
|
|
|
|
|
if ( anchor & 0b001 ) {
|
|
|
|
|
reStr += '$';
|
|
|
|
|
}
|
|
|
|
|
return reStr;
|
|
|
|
|
};
|
|
|
|
|
rePatternFromUrlFilter.rePlainChars = /[.+?${}()|[\]\\]/g;
|
|
|
|
|
rePatternFromUrlFilter.reSeparators = /\^/g;
|
|
|
|
|
rePatternFromUrlFilter.reDanglingAsterisks = /^\*+|\*+$/g;
|
|
|
|
|
rePatternFromUrlFilter.reAsterisks = /\*+/g;
|
|
|
|
|
rePatternFromUrlFilter.restrHostnameAnchor1 = '^[a-z-]+://(?:[^/?#]+\\.)?';
|
|
|
|
|
rePatternFromUrlFilter.restrHostnameAnchor2 = '^[a-z-]+://(?:[^/?#]+)?';
|
|
|
|
|
|
2022-09-16 19:56:35 +00:00
|
|
|
/******************************************************************************/
|
2022-09-06 17:47:52 +00:00
|
|
|
|
2024-01-21 19:32:14 +00:00
|
|
|
async function fetchList(assetDetails) {
|
2022-09-16 19:56:35 +00:00
|
|
|
// 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);
|
2025-03-06 22:39:15 +00:00
|
|
|
if (
|
2025-03-25 16:58:26 +00:00
|
|
|
assetDetails.trusted ||
|
2025-03-06 22:39:15 +00:00
|
|
|
part.url.startsWith('https://ublockorigin.github.io/uAssets/filters/')
|
|
|
|
|
) {
|
2024-12-29 21:38:56 +00:00
|
|
|
newParts.push(`!#trusted on ${secret}`);
|
2023-06-23 12:27:07 +00:00
|
|
|
}
|
2022-09-16 19:56:35 +00:00
|
|
|
newParts.push(
|
2024-01-21 19:32:14 +00:00
|
|
|
fetchText(part.url, cacheDir).then(details => {
|
2025-05-20 11:34:05 +00:00
|
|
|
const { url, error } = details;
|
|
|
|
|
if ( error !== undefined ) { return details; }
|
2022-09-16 19:56:35 +00:00
|
|
|
const content = details.content.trim();
|
2025-07-28 14:00:25 +00:00
|
|
|
if ( /* content === '' || */ /^<.*>$/.test(content) ) {
|
2025-05-20 11:34:05 +00:00
|
|
|
return { url, error: `Bad content: ${url}` };
|
2022-09-16 19:56:35 +00:00
|
|
|
}
|
2025-05-20 11:34:05 +00:00
|
|
|
return { url, content };
|
2022-09-16 19:56:35 +00:00
|
|
|
})
|
|
|
|
|
);
|
2024-12-29 21:38:56 +00:00
|
|
|
newParts.push(`!#trusted off ${secret}`);
|
2022-09-16 19:56:35 +00:00
|
|
|
}
|
2025-05-20 11:34:05 +00:00
|
|
|
if ( parts.some(v => typeof v === 'object' && v.error) ) { return; }
|
2022-09-16 19:56:35 +00:00
|
|
|
parts = await Promise.all(newParts);
|
2023-01-23 21:53:18 +00:00
|
|
|
parts = sfp.utils.preparser.expandIncludes(parts, env);
|
2022-09-16 19:56:35 +00:00
|
|
|
}
|
|
|
|
|
const text = parts.join('\n');
|
2022-09-10 18:20:07 +00:00
|
|
|
|
2022-09-16 19:56:35 +00:00
|
|
|
if ( text === '' ) {
|
2025-02-21 13:23:27 +00:00
|
|
|
log('No filterset found', false);
|
2022-09-10 18:20:07 +00:00
|
|
|
}
|
2022-09-16 19:56:35 +00:00
|
|
|
return text;
|
|
|
|
|
}
|
2022-09-10 18:20:07 +00:00
|
|
|
|
2022-09-16 19:56:35 +00:00
|
|
|
/******************************************************************************/
|
2022-09-10 18:20:07 +00:00
|
|
|
|
2022-09-27 11:46:24 +00:00
|
|
|
const isUnsupported = rule =>
|
|
|
|
|
rule._error !== undefined;
|
|
|
|
|
|
|
|
|
|
const isRegex = rule =>
|
|
|
|
|
rule.condition !== undefined &&
|
|
|
|
|
rule.condition.regexFilter !== undefined;
|
|
|
|
|
|
2025-09-02 16:24:59 +00:00
|
|
|
const isGood = rule =>
|
2022-09-27 11:46:24 +00:00
|
|
|
isUnsupported(rule) === false &&
|
2025-09-02 16:24:59 +00:00
|
|
|
/^(allow|block|redirect|modifyHeaders|allowAllRequests)$/.test(rule.action?.type);
|
2024-12-05 17:56:25 +00:00
|
|
|
|
|
|
|
|
const isURLSkip = rule =>
|
|
|
|
|
isUnsupported(rule) === false &&
|
|
|
|
|
rule.action !== undefined &&
|
|
|
|
|
rule.action.type === 'urlskip';
|
2022-09-27 11:46:24 +00:00
|
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
|
2025-06-10 19:18:47 +00:00
|
|
|
async function patchRuleset(ruleset) {
|
|
|
|
|
return import(`./${platform}/patch-ruleset.js`).then(module => {
|
|
|
|
|
return module.patchRuleset(ruleset)
|
|
|
|
|
}).catch(( ) => {
|
|
|
|
|
return ruleset;
|
|
|
|
|
});
|
2025-04-19 17:08:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
|
2023-08-12 00:42:03 +00:00
|
|
|
// Two distinct hostnames:
|
|
|
|
|
// www.example.com
|
|
|
|
|
// example.com
|
|
|
|
|
// Can be reduced to a single one:
|
|
|
|
|
// example.com
|
|
|
|
|
// Since if example.com matches, then www.example.com (or any other subdomain
|
|
|
|
|
// of example.com) will always match.
|
|
|
|
|
|
|
|
|
|
function pruneHostnameArray(hostnames) {
|
|
|
|
|
const rootMap = new Map();
|
|
|
|
|
for ( const hostname of hostnames ) {
|
|
|
|
|
const labels = hostname.split('.');
|
|
|
|
|
let currentMap = rootMap;
|
|
|
|
|
let i = labels.length;
|
|
|
|
|
while ( i-- ) {
|
|
|
|
|
const label = labels[i];
|
|
|
|
|
let nextMap = currentMap.get(label);
|
|
|
|
|
if ( nextMap === null ) { break; }
|
|
|
|
|
if ( nextMap === undefined ) {
|
|
|
|
|
if ( i === 0 ) {
|
|
|
|
|
currentMap.set(label, (nextMap = null));
|
|
|
|
|
} else {
|
|
|
|
|
currentMap.set(label, (nextMap = new Map()));
|
|
|
|
|
}
|
|
|
|
|
} else if ( i === 0 ) {
|
|
|
|
|
currentMap.set(label, null);
|
|
|
|
|
}
|
|
|
|
|
currentMap = nextMap;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const assemble = (currentMap, currentHostname, out) => {
|
|
|
|
|
for ( const [ label, nextMap ] of currentMap ) {
|
|
|
|
|
const nextHostname = currentHostname === ''
|
|
|
|
|
? label
|
|
|
|
|
: `${label}.${currentHostname}`;
|
|
|
|
|
if ( nextMap === null ) {
|
|
|
|
|
out.push(nextHostname);
|
|
|
|
|
} else {
|
|
|
|
|
assemble(nextMap, nextHostname, out);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return out;
|
|
|
|
|
};
|
|
|
|
|
return assemble(rootMap, '', []);
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-09 16:02:20 +00:00
|
|
|
/*******************************************************************************
|
|
|
|
|
*
|
2023-12-11 14:29:59 +00:00
|
|
|
* For large rulesets, one rule per line for compromise between size and
|
|
|
|
|
* readability. This also means that the number of lines in resulting file
|
|
|
|
|
* representative of the number of rules in the ruleset.
|
2023-12-09 16:02:20 +00:00
|
|
|
*
|
|
|
|
|
* */
|
|
|
|
|
|
|
|
|
|
function toJSONRuleset(ruleset) {
|
2025-04-20 16:14:29 +00:00
|
|
|
const nodupProps = [ 'domains', 'excludedDomains', 'requestDomains', 'excludedRequestDomains', 'initiatorDomains', 'excludedInitiatorDomains' ];
|
|
|
|
|
for ( const { condition } of ruleset ) {
|
|
|
|
|
if ( condition === undefined ) { continue; }
|
|
|
|
|
for ( const prop of nodupProps ) {
|
|
|
|
|
if ( condition[prop] === undefined ) { continue; }
|
|
|
|
|
condition[prop] = Array.from(new Set(condition[prop]));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const sortProps = [ 'requestDomains', 'initiatorDomains', 'domains' ];
|
|
|
|
|
ruleset.sort((a, b) => {
|
|
|
|
|
let aLen = 0, bLen = 0;
|
|
|
|
|
for ( const prop of sortProps ) {
|
|
|
|
|
aLen += a.condition[prop]?.length ?? 0;
|
|
|
|
|
bLen += b.condition[prop]?.length ?? 0;
|
|
|
|
|
}
|
|
|
|
|
return bLen - aLen;
|
|
|
|
|
});
|
2022-09-06 17:47:52 +00:00
|
|
|
const replacer = (k, v) => {
|
2022-09-27 14:57:43 +00:00
|
|
|
if ( k.startsWith('_') ) { return; }
|
2022-09-06 17:47:52 +00:00
|
|
|
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;
|
|
|
|
|
};
|
2023-12-11 14:29:59 +00:00
|
|
|
const indent = ruleset.length > 10 ? undefined : 1;
|
2023-12-09 16:02:20 +00:00
|
|
|
const out = [];
|
2025-04-20 16:14:29 +00:00
|
|
|
let id = 1;
|
2023-12-09 16:02:20 +00:00
|
|
|
for ( const rule of ruleset ) {
|
2025-04-20 16:14:29 +00:00
|
|
|
rule.id = id++;
|
2023-12-11 14:29:59 +00:00
|
|
|
out.push(JSON.stringify(rule, replacer, indent));
|
2023-12-09 16:02:20 +00:00
|
|
|
}
|
|
|
|
|
return `[\n${out.join(',\n')}\n]\n`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/******************************************************************************/
|
2022-09-06 17:47:52 +00:00
|
|
|
|
2024-12-29 21:38:56 +00:00
|
|
|
function toStrictBlockRule(rule, out) {
|
|
|
|
|
if ( rule.action.type !== 'block' ) { return; }
|
|
|
|
|
const { condition } = rule;
|
|
|
|
|
if ( condition === undefined ) { return; }
|
|
|
|
|
if ( condition.domainType ) { return; }
|
|
|
|
|
if ( condition.excludedResourceTypes ) { return; }
|
|
|
|
|
if ( condition.requestMethods ) { return; }
|
|
|
|
|
if ( condition.excludedRequestMethods ) { return; }
|
|
|
|
|
if ( condition.responseHeaders ) { return; }
|
|
|
|
|
if ( condition.excludedResponseHeaders ) { return; }
|
|
|
|
|
if ( condition.initiatorDomains ) { return; }
|
|
|
|
|
if ( condition.excludedInitiatorDomains ) { return; }
|
|
|
|
|
const { resourceTypes } = condition;
|
|
|
|
|
if ( resourceTypes === undefined ) {
|
|
|
|
|
if ( condition.requestDomains === undefined ) { return; }
|
2025-04-03 16:09:20 +00:00
|
|
|
} else if ( resourceTypes.includes('main_frame') === false ) {
|
|
|
|
|
return;
|
2024-12-29 21:38:56 +00:00
|
|
|
}
|
|
|
|
|
let regexFilter;
|
|
|
|
|
if ( condition.urlFilter ) {
|
|
|
|
|
regexFilter = rePatternFromUrlFilter(condition.urlFilter);
|
|
|
|
|
} else if ( condition.regexFilter ) {
|
|
|
|
|
regexFilter = condition.regexFilter;
|
|
|
|
|
} else {
|
|
|
|
|
regexFilter = '^https?://.*';
|
|
|
|
|
}
|
2025-04-16 12:18:26 +00:00
|
|
|
if ( regexFilter.startsWith('^') === false ) {
|
2024-12-29 21:38:56 +00:00
|
|
|
regexFilter = `^.*${regexFilter}`;
|
|
|
|
|
}
|
|
|
|
|
if (
|
|
|
|
|
regexFilter.endsWith('$') === false &&
|
|
|
|
|
regexFilter.endsWith('.*') === false &&
|
|
|
|
|
regexFilter.endsWith('.+') === false
|
|
|
|
|
) {
|
|
|
|
|
regexFilter = `${regexFilter}.*`;
|
|
|
|
|
}
|
2025-04-16 12:18:26 +00:00
|
|
|
const strictBlockRule = out.get(regexFilter) || {
|
2024-12-29 21:38:56 +00:00
|
|
|
action: {
|
|
|
|
|
type: 'redirect',
|
|
|
|
|
redirect: {
|
|
|
|
|
regexSubstitution: `/strictblock.html#\\0`,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
condition: {
|
|
|
|
|
regexFilter,
|
|
|
|
|
resourceTypes: [ 'main_frame' ],
|
|
|
|
|
},
|
|
|
|
|
priority: 29,
|
|
|
|
|
};
|
|
|
|
|
if ( condition.requestDomains ) {
|
2025-04-16 12:18:26 +00:00
|
|
|
strictBlockRule.condition.requestDomains ??= [];
|
2025-09-11 13:29:39 +00:00
|
|
|
strictBlockRule.condition.requestDomains = Array.from(
|
|
|
|
|
new Set([
|
|
|
|
|
...strictBlockRule.condition.requestDomains,
|
|
|
|
|
...condition.requestDomains,
|
|
|
|
|
])
|
|
|
|
|
);
|
2024-12-29 21:38:56 +00:00
|
|
|
}
|
2025-04-16 12:18:26 +00:00
|
|
|
if ( condition.excludedRequestDomains ) {
|
|
|
|
|
strictBlockRule.condition.excludedRequestDomains ??= [];
|
2025-09-11 13:29:39 +00:00
|
|
|
strictBlockRule.condition.excludedRequestDomains = Array.from(
|
|
|
|
|
new Set([
|
|
|
|
|
...strictBlockRule.condition.excludedRequestDomains,
|
|
|
|
|
...condition.excludedRequestDomains,
|
|
|
|
|
])
|
|
|
|
|
);
|
2025-04-16 12:18:26 +00:00
|
|
|
}
|
|
|
|
|
out.set(regexFilter, strictBlockRule);
|
2024-12-29 21:38:56 +00:00
|
|
|
}
|
|
|
|
|
toStrictBlockRule.ruleId = 1;
|
|
|
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
|
2023-12-09 16:02:20 +00:00
|
|
|
async function processNetworkFilters(assetDetails, network) {
|
2022-09-16 19:56:35 +00:00
|
|
|
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}`);
|
2022-09-06 17:47:52 +00:00
|
|
|
|
2023-08-12 00:42:03 +00:00
|
|
|
// Minimize requestDomains arrays
|
|
|
|
|
for ( const rule of rules ) {
|
|
|
|
|
const condition = rule.condition;
|
|
|
|
|
if ( condition === undefined ) { continue; }
|
|
|
|
|
const requestDomains = condition.requestDomains;
|
|
|
|
|
if ( requestDomains === undefined ) { continue; }
|
|
|
|
|
const beforeCount = requestDomains.length;
|
|
|
|
|
condition.requestDomains = pruneHostnameArray(requestDomains);
|
|
|
|
|
const afterCount = condition.requestDomains.length;
|
|
|
|
|
if ( afterCount !== beforeCount ) {
|
|
|
|
|
log(`\tPruning requestDomains: from ${beforeCount} to ${afterCount}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-21 19:32:14 +00:00
|
|
|
// Add native DNR ruleset if present
|
|
|
|
|
if ( assetDetails.dnrURL ) {
|
|
|
|
|
const result = await fetchText(assetDetails.dnrURL, cacheDir);
|
|
|
|
|
for ( const rule of JSON.parse(result.content) ) {
|
|
|
|
|
rules.push(rule);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-02 16:24:59 +00:00
|
|
|
const staticRules = await patchRuleset(
|
|
|
|
|
rules.filter(rule => isGood(rule) && isRegex(rule) === false)
|
2025-04-19 17:08:59 +00:00
|
|
|
);
|
2025-09-02 16:24:59 +00:00
|
|
|
log(`\tStatic rules: ${staticRules.length}`);
|
|
|
|
|
log(staticRules
|
2023-05-26 17:18:20 +00:00
|
|
|
.filter(rule => Array.isArray(rule._warning))
|
|
|
|
|
.map(rule => rule._warning.map(v => `\t\t${v}`))
|
2024-08-22 16:36:31 +00:00
|
|
|
.join('\n'), true
|
2023-05-26 17:18:20 +00:00
|
|
|
);
|
2022-09-13 21:44:24 +00:00
|
|
|
|
2025-09-02 16:24:59 +00:00
|
|
|
const regexRules = await patchRuleset(
|
|
|
|
|
rules.filter(rule => isGood(rule) && isRegex(rule))
|
2025-04-19 17:08:59 +00:00
|
|
|
);
|
2025-09-02 16:24:59 +00:00
|
|
|
log(`\tMaybe good (regexes): ${regexRules.length}`);
|
2022-09-13 21:44:24 +00:00
|
|
|
|
2025-09-02 16:24:59 +00:00
|
|
|
staticRules.forEach(rule => {
|
|
|
|
|
if ( rule.action.redirect?.extensionPath === undefined ) { return; }
|
2022-10-16 16:05:24 +00:00
|
|
|
requiredRedirectResources.add(
|
|
|
|
|
rule.action.redirect.extensionPath.replace(/^\/+/, '')
|
|
|
|
|
);
|
|
|
|
|
});
|
2022-11-05 15:09:06 +00:00
|
|
|
|
2024-12-29 21:38:56 +00:00
|
|
|
const urlskips = new Map();
|
|
|
|
|
for ( const rule of rules ) {
|
|
|
|
|
if ( isURLSkip(rule) === false ) { continue; }
|
|
|
|
|
if ( rule.__modifierAction !== 0 ) { continue; }
|
|
|
|
|
const { condition } = rule;
|
|
|
|
|
if ( condition.resourceTypes ) {
|
|
|
|
|
if ( condition.resourceTypes.includes('main_frame') === false ) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const { urlFilter, regexFilter, requestDomains } = condition;
|
|
|
|
|
let re;
|
|
|
|
|
if ( urlFilter !== undefined ) {
|
|
|
|
|
re = rePatternFromUrlFilter(urlFilter);
|
|
|
|
|
} else if ( regexFilter !== undefined ) {
|
|
|
|
|
re = regexFilter;
|
|
|
|
|
} else {
|
|
|
|
|
re = '^';
|
|
|
|
|
}
|
|
|
|
|
const rawSteps = rule.__modifierValue;
|
|
|
|
|
const steps = rawSteps.includes(' ') && rawSteps.split(/ +/) || [ rawSteps ];
|
|
|
|
|
const keyEntry = {
|
|
|
|
|
re,
|
|
|
|
|
c: condition.isUrlFilterCaseSensitive,
|
|
|
|
|
steps,
|
|
|
|
|
}
|
|
|
|
|
const key = JSON.stringify(keyEntry);
|
|
|
|
|
let actualEntry = urlskips.get(key);
|
|
|
|
|
if ( actualEntry === undefined ) {
|
|
|
|
|
urlskips.set(key, keyEntry);
|
|
|
|
|
actualEntry = keyEntry;
|
|
|
|
|
}
|
|
|
|
|
if ( requestDomains !== undefined ) {
|
|
|
|
|
if ( actualEntry.hostnames === undefined ) {
|
|
|
|
|
actualEntry.hostnames = [];
|
|
|
|
|
}
|
|
|
|
|
actualEntry.hostnames.push(...requestDomains);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
log(`\turlskip=: ${urlskips.size}`);
|
2024-12-05 17:56:25 +00:00
|
|
|
|
2022-09-16 19:56:35 +00:00
|
|
|
const bad = rules.filter(rule =>
|
|
|
|
|
isUnsupported(rule)
|
|
|
|
|
);
|
|
|
|
|
log(`\tUnsupported: ${bad.length}`);
|
2022-09-29 23:51:33 +00:00
|
|
|
log(bad.map(rule => rule._error.map(v => `\t\t${v}`)).join('\n'), true);
|
2022-09-06 17:47:52 +00:00
|
|
|
|
2025-04-16 12:18:26 +00:00
|
|
|
writeFile(`${rulesetDir}/main/${assetDetails.id}.json`,
|
2025-09-02 16:24:59 +00:00
|
|
|
toJSONRuleset(staticRules)
|
2022-09-16 19:56:35 +00:00
|
|
|
);
|
2022-09-06 17:47:52 +00:00
|
|
|
|
2025-09-02 16:24:59 +00:00
|
|
|
if ( regexRules.length !== 0 ) {
|
2025-04-16 12:18:26 +00:00
|
|
|
writeFile(`${rulesetDir}/regex/${assetDetails.id}.json`,
|
2025-09-02 16:24:59 +00:00
|
|
|
toJSONRuleset(regexRules)
|
2022-11-05 15:09:06 +00:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-29 21:38:56 +00:00
|
|
|
const strictBlocked = new Map();
|
2025-09-02 16:24:59 +00:00
|
|
|
for ( const rule of staticRules ) {
|
2024-12-29 21:38:56 +00:00
|
|
|
toStrictBlockRule(rule, strictBlocked);
|
2024-12-03 21:41:34 +00:00
|
|
|
}
|
|
|
|
|
if ( strictBlocked.size !== 0 ) {
|
2024-12-29 21:38:56 +00:00
|
|
|
mergeRules(strictBlocked, 'requestDomains');
|
2025-04-16 12:18:26 +00:00
|
|
|
writeFile(`${rulesetDir}/strictblock/${assetDetails.id}.json`,
|
2024-12-29 21:38:56 +00:00
|
|
|
toJSONRuleset(Array.from(strictBlocked.values()))
|
2024-12-03 21:41:34 +00:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-29 21:38:56 +00:00
|
|
|
if ( urlskips.size !== 0 ) {
|
2025-04-16 12:18:26 +00:00
|
|
|
writeFile(`${rulesetDir}/urlskip/${assetDetails.id}.json`,
|
2024-12-29 21:38:56 +00:00
|
|
|
JSON.stringify(Array.from(urlskips.values()), null, 1)
|
2024-12-05 17:56:25 +00:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2022-09-16 19:56:35 +00:00
|
|
|
return {
|
|
|
|
|
total: rules.length,
|
2025-09-02 16:24:59 +00:00
|
|
|
plain: staticRules.length,
|
2022-09-16 19:56:35 +00:00
|
|
|
rejected: bad.length,
|
2025-09-02 16:24:59 +00:00
|
|
|
regex: regexRules.length,
|
2024-12-03 21:41:34 +00:00
|
|
|
strictblock: strictBlocked.size,
|
2024-12-29 21:38:56 +00:00
|
|
|
urlskip: urlskips.size,
|
2022-09-16 19:56:35 +00:00
|
|
|
};
|
|
|
|
|
}
|
2022-09-06 17:47:52 +00:00
|
|
|
|
2022-09-16 19:56:35 +00:00
|
|
|
/******************************************************************************/
|
|
|
|
|
|
2022-09-19 12:55:45 +00:00
|
|
|
// TODO: unify css/scriptlet processing code since now css styles are
|
|
|
|
|
// injected using scriptlet injection.
|
|
|
|
|
|
|
|
|
|
// 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.
|
|
|
|
|
|
|
|
|
|
let scriptletsMapPromise;
|
|
|
|
|
|
|
|
|
|
function loadAllSourceScriptlets() {
|
|
|
|
|
if ( scriptletsMapPromise !== undefined ) {
|
|
|
|
|
return scriptletsMapPromise;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
scriptletsMapPromise = fs.readdir('./scriptlets').then(files => {
|
2023-06-04 15:32:55 +00:00
|
|
|
const readTemplateFile = file =>
|
|
|
|
|
fs.readFile(`./scriptlets/${file}`, { encoding: 'utf8' })
|
|
|
|
|
.then(text => ({ file, text }));
|
2022-09-19 12:55:45 +00:00
|
|
|
const readPromises = [];
|
|
|
|
|
for ( const file of files ) {
|
2023-06-04 15:32:55 +00:00
|
|
|
readPromises.push(readTemplateFile(file));
|
2022-09-19 12:55:45 +00:00
|
|
|
}
|
|
|
|
|
return Promise.all(readPromises).then(results => {
|
|
|
|
|
const originalScriptletMap = new Map();
|
2023-06-04 15:32:55 +00:00
|
|
|
for ( const details of results ) {
|
|
|
|
|
originalScriptletMap.set(
|
2023-07-06 19:45:45 +00:00
|
|
|
details.file.replace('.template.js', '')
|
|
|
|
|
.replace('.template.css', ''),
|
2023-06-04 15:32:55 +00:00
|
|
|
details.text
|
|
|
|
|
);
|
2022-09-19 12:55:45 +00:00
|
|
|
}
|
|
|
|
|
return originalScriptletMap;
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return scriptletsMapPromise;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
|
2025-03-13 17:15:27 +00:00
|
|
|
// http://www.cse.yorku.ca/~oz/hash.html#djb2
|
|
|
|
|
// Must mirror content script surveyor's version
|
|
|
|
|
|
|
|
|
|
async function processGenericCosmeticFilters(
|
|
|
|
|
assetDetails,
|
|
|
|
|
selectorList,
|
|
|
|
|
exceptionList,
|
|
|
|
|
declarativeMap
|
|
|
|
|
) {
|
|
|
|
|
const exceptionSet = new Set(
|
|
|
|
|
exceptionList &&
|
|
|
|
|
exceptionList.filter(a => a.key !== undefined).map(a => a.selector)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const genericSelectorMap = new Map();
|
|
|
|
|
if ( selectorList ) {
|
|
|
|
|
for ( const { key, selector } of selectorList ) {
|
|
|
|
|
if ( key === undefined ) { continue; }
|
|
|
|
|
if ( exceptionSet.has(selector) ) { continue; }
|
|
|
|
|
const type = key.charCodeAt(0);
|
|
|
|
|
const hash = hashFromStr(type, key.slice(1));
|
|
|
|
|
const selectors = genericSelectorMap.get(hash);
|
|
|
|
|
if ( selectors === undefined ) {
|
|
|
|
|
genericSelectorMap.set(hash, selector)
|
|
|
|
|
} else {
|
2025-03-13 23:02:20 +00:00
|
|
|
genericSelectorMap.set(hash, `${selectors},\n${selector}`)
|
2023-07-11 18:38:35 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2022-10-10 16:28:24 +00:00
|
|
|
|
2025-03-13 17:15:27 +00:00
|
|
|
// Specific exceptions
|
2025-03-13 23:02:20 +00:00
|
|
|
const genericExceptionSieve = new Set();
|
2025-03-13 17:15:27 +00:00
|
|
|
const genericExceptionMap = new Map();
|
|
|
|
|
if ( declarativeMap ) {
|
2025-03-13 23:02:20 +00:00
|
|
|
for ( const [ exception, details ] of declarativeMap ) {
|
2025-03-13 17:15:27 +00:00
|
|
|
if ( details.rejected ) { continue; }
|
|
|
|
|
if ( details.key === undefined ) { continue; }
|
|
|
|
|
if ( details.matches !== undefined ) { continue; }
|
|
|
|
|
if ( details.excludeMatches === undefined ) { continue; }
|
|
|
|
|
const type = details.key.charCodeAt(0);
|
|
|
|
|
const hash = hashFromStr(type, details.key.slice(1));
|
2025-03-13 23:02:20 +00:00
|
|
|
genericExceptionSieve.add(hash);
|
|
|
|
|
for ( const hn of details.excludeMatches ) {
|
|
|
|
|
const exceptions = genericExceptionMap.get(hn);
|
|
|
|
|
if ( exceptions === undefined ) {
|
|
|
|
|
genericExceptionMap.set(hn, exception);
|
|
|
|
|
} else {
|
|
|
|
|
genericExceptionMap.set(hn, `${exceptions}\n${exception}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-03-13 17:15:27 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( genericSelectorMap.size === 0 ) {
|
|
|
|
|
if ( genericExceptionMap.size === 0 ) { return 0; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const originalScriptletMap = await loadAllSourceScriptlets();
|
2023-06-06 00:17:50 +00:00
|
|
|
let patchedScriptlet = originalScriptletMap.get('css-generic').replace(
|
|
|
|
|
'$rulesetId$',
|
|
|
|
|
assetDetails.id
|
|
|
|
|
);
|
|
|
|
|
patchedScriptlet = safeReplace(patchedScriptlet,
|
|
|
|
|
/\bself\.\$genericSelectorMap\$/,
|
2025-03-13 17:15:27 +00:00
|
|
|
`${JSON.stringify(genericSelectorMap, scriptletJsonReplacer)}`
|
|
|
|
|
);
|
2025-03-13 23:02:20 +00:00
|
|
|
patchedScriptlet = safeReplace(patchedScriptlet,
|
|
|
|
|
/\bself\.\$genericExceptionSieve\$/,
|
|
|
|
|
`${JSON.stringify(genericExceptionSieve, scriptletJsonReplacer)}`
|
|
|
|
|
);
|
2025-03-13 17:15:27 +00:00
|
|
|
patchedScriptlet = safeReplace(patchedScriptlet,
|
|
|
|
|
/\bself\.\$genericExceptionMap\$/,
|
|
|
|
|
`${JSON.stringify(genericExceptionMap, scriptletJsonReplacer)}`
|
2023-06-06 00:17:50 +00:00
|
|
|
);
|
2022-10-10 16:28:24 +00:00
|
|
|
|
2025-04-16 12:18:26 +00:00
|
|
|
writeFile(`${scriptletDir}/generic/${assetDetails.id}.js`,
|
2022-10-10 16:28:24 +00:00
|
|
|
patchedScriptlet
|
|
|
|
|
);
|
|
|
|
|
|
2025-03-13 23:02:20 +00:00
|
|
|
log(`CSS-generic: ${genericExceptionSieve.size} specific CSS exceptions`);
|
2025-03-13 17:15:27 +00:00
|
|
|
log(`CSS-generic: ${genericSelectorMap.size} plain CSS selectors`);
|
2022-10-10 16:28:24 +00:00
|
|
|
|
2025-03-13 23:02:20 +00:00
|
|
|
return genericSelectorMap.size + genericExceptionSieve.size;
|
2022-10-10 16:28:24 +00:00
|
|
|
}
|
|
|
|
|
|
2025-03-13 17:15:27 +00:00
|
|
|
const hashFromStr = (type, s) => {
|
|
|
|
|
const len = s.length;
|
|
|
|
|
const step = len + 7 >>> 3;
|
|
|
|
|
let hash = (type << 5) + type ^ len;
|
|
|
|
|
for ( let i = 0; i < len; i += step ) {
|
|
|
|
|
hash = (hash << 5) + hash ^ s.charCodeAt(i);
|
|
|
|
|
}
|
2025-10-04 17:32:16 +00:00
|
|
|
return hash & 0xFFF;
|
2025-03-13 17:15:27 +00:00
|
|
|
};
|
|
|
|
|
|
2022-10-10 16:28:24 +00:00
|
|
|
/******************************************************************************/
|
|
|
|
|
|
2025-03-13 17:15:27 +00:00
|
|
|
async function processGenericHighCosmeticFilters(
|
|
|
|
|
assetDetails,
|
|
|
|
|
genericSelectorList,
|
|
|
|
|
genericExceptionList
|
|
|
|
|
) {
|
|
|
|
|
if ( genericSelectorList === undefined ) { return 0; }
|
|
|
|
|
const genericSelectorSet = new Set(
|
|
|
|
|
genericSelectorList
|
|
|
|
|
.filter(a => a.key === undefined)
|
|
|
|
|
.map(a => a.selector)
|
|
|
|
|
);
|
2025-06-10 18:57:41 +00:00
|
|
|
// https://github.com/uBlockOrigin/uBOL-home/issues/365
|
2025-03-13 17:15:27 +00:00
|
|
|
if ( genericExceptionList ) {
|
2025-06-10 18:57:41 +00:00
|
|
|
for ( const entry of genericExceptionList ) {
|
|
|
|
|
if ( entry.key !== undefined ) { continue; }
|
|
|
|
|
globalHighlyGenericExceptionSet.add(entry.selector);
|
2023-07-11 18:38:35 +00:00
|
|
|
}
|
|
|
|
|
}
|
2025-06-10 18:57:41 +00:00
|
|
|
for ( const selector of globalHighlyGenericExceptionSet ) {
|
|
|
|
|
if ( genericSelectorSet.has(selector) === false ) { continue; }
|
|
|
|
|
genericSelectorSet.delete(selector);
|
|
|
|
|
log(`\tRemoving excepted highly generic filter ##${selector}`);
|
|
|
|
|
}
|
2025-03-13 17:15:27 +00:00
|
|
|
if ( genericSelectorSet.size === 0 ) { return 0; }
|
|
|
|
|
const selectorLists = Array.from(genericSelectorSet).sort().join(',\n');
|
2023-07-06 19:45:45 +00:00
|
|
|
const originalScriptletMap = await loadAllSourceScriptlets();
|
|
|
|
|
|
|
|
|
|
let patchedScriptlet = originalScriptletMap.get('css-generichigh').replace(
|
|
|
|
|
'$rulesetId$',
|
|
|
|
|
assetDetails.id
|
|
|
|
|
);
|
|
|
|
|
patchedScriptlet = safeReplace(patchedScriptlet,
|
|
|
|
|
/\$selectorList\$/,
|
|
|
|
|
selectorLists
|
|
|
|
|
);
|
|
|
|
|
|
2025-04-16 12:18:26 +00:00
|
|
|
writeFile(`${scriptletDir}/generichigh/${assetDetails.id}.css`,
|
2023-07-06 19:45:45 +00:00
|
|
|
patchedScriptlet
|
|
|
|
|
);
|
|
|
|
|
|
2025-03-13 17:15:27 +00:00
|
|
|
log(`CSS-generic-high: ${genericSelectorSet.size} plain CSS selectors`);
|
2023-07-06 19:45:45 +00:00
|
|
|
|
2025-03-13 17:15:27 +00:00
|
|
|
return genericSelectorSet.size;
|
2023-07-06 19:45:45 +00:00
|
|
|
}
|
|
|
|
|
|
2025-06-10 18:57:41 +00:00
|
|
|
const globalHighlyGenericExceptionSet = new Set();
|
|
|
|
|
|
2023-07-06 19:45:45 +00:00
|
|
|
/******************************************************************************/
|
|
|
|
|
|
2022-09-24 15:33:04 +00:00
|
|
|
const scriptletJsonReplacer = (k, v) => {
|
|
|
|
|
if ( k === 'n' ) {
|
|
|
|
|
if ( v === undefined || v.size === 0 ) { return; }
|
|
|
|
|
return Array.from(v);
|
|
|
|
|
}
|
|
|
|
|
if ( v instanceof Set || v instanceof Map ) {
|
|
|
|
|
if ( v.size === 0 ) { return; }
|
|
|
|
|
return Array.from(v);
|
|
|
|
|
}
|
|
|
|
|
return v;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
|
2025-12-05 13:54:34 +00:00
|
|
|
async function processCosmeticFilters(assetDetails, realm, mapin) {
|
2023-06-04 01:47:40 +00:00
|
|
|
if ( mapin === undefined ) { return 0; }
|
|
|
|
|
if ( mapin.size === 0 ) { return 0; }
|
2022-09-24 15:33:04 +00:00
|
|
|
|
2025-12-05 13:54:34 +00:00
|
|
|
// Collate all distinct selectors
|
|
|
|
|
const allSelectors = new Map();
|
|
|
|
|
const allHostnames = new Map();
|
2025-12-25 17:34:21 +00:00
|
|
|
const allRegexesOrPaths = new Map();
|
2025-03-13 17:15:27 +00:00
|
|
|
let hasEntities = false;
|
2025-12-25 17:34:21 +00:00
|
|
|
|
|
|
|
|
const storeHostnameSelectorPair = (hn, iSelector) => {
|
|
|
|
|
if ( isHnRegexOrPath(hn) ) {
|
|
|
|
|
if ( allRegexesOrPaths.has(hn) === false ) {
|
|
|
|
|
allRegexesOrPaths.set(hn, new Set());
|
|
|
|
|
}
|
|
|
|
|
allRegexesOrPaths.get(hn).add(iSelector);
|
|
|
|
|
} else {
|
|
|
|
|
if ( allHostnames.has(hn) === false ) {
|
|
|
|
|
allHostnames.set(hn, new Set());
|
|
|
|
|
}
|
|
|
|
|
allHostnames.get(hn).add(iSelector);
|
|
|
|
|
hasEntities ||= hn.endsWith('.*');
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-12-05 13:54:34 +00:00
|
|
|
for ( const [ selector, details ] of mapin ) {
|
|
|
|
|
if ( details.rejected ) { continue; }
|
|
|
|
|
if ( allSelectors.has(selector) === false ) {
|
|
|
|
|
allSelectors.set(selector, allSelectors.size);
|
2025-03-13 17:15:27 +00:00
|
|
|
}
|
2025-12-05 13:54:34 +00:00
|
|
|
const iSelector = allSelectors.get(selector);
|
|
|
|
|
if ( details.matches ) {
|
|
|
|
|
for ( const hn of details.matches ) {
|
2025-12-25 17:34:21 +00:00
|
|
|
storeHostnameSelectorPair(hn, iSelector);
|
2025-12-05 13:54:34 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if ( details.excludeMatches ) {
|
|
|
|
|
for ( const hn of details.excludeMatches ) {
|
2025-12-25 17:34:21 +00:00
|
|
|
storeHostnameSelectorPair(hn, ~iSelector);
|
2025-12-05 13:54:34 +00:00
|
|
|
}
|
[mv3] Add ability to handle entity-based CSS and scriptlet injection filters
This commit adds the ability to inject entity-based plain CSS
filters and also a set of the most commonly used entity-based
scriptlet injection filters.
Since the scripting API is not compatible with entity patterns,
the entity-related content scripts are injected in all documents
and the entity-matching is done by the content script themselves.
Given this, entity-based content scripts are enabled only when
working in the Complete filtering mode, there won't be any
entity-based filters injected in lower modes.
Also, since there is no way to reasonably have access to the
Public Suffix List in the content scripts, the entity-matching
algorithm is an approximation, though I expect false positives
to be rare (time will tell). In the event of such false
positive, simply falling back to Optimal mode will fix the
issue.
The following issues have been fixed at the same time:
Fixed the no-filtering mode related rules having lower priority
then redirect rules, i.e. redirect rules would still be applied
despite disabling all filtering on a site.
Fixed improper detection of changes to the generic-related CSS
content script, potentially causing undue delays when for example
trying to access the popup panel while working in Complete mode.
The scripting MV3 can be quite slow when registering/updating
large content scripts, so uBOL does its best to call the API only
if really needed, but there had been a regression in the recent
builds preventing uBO from properly detecting unchanged content
script parameters.
2022-10-20 19:48:06 +00:00
|
|
|
}
|
|
|
|
|
}
|
2025-12-05 13:54:34 +00:00
|
|
|
const allSelectorLists = new Map();
|
2025-12-25 17:34:21 +00:00
|
|
|
|
|
|
|
|
const ilistFromSelectorSet = selectorSet => {
|
2025-12-05 13:54:34 +00:00
|
|
|
const list = JSON.stringify(Array.from(selectorSet).sort()).slice(1, -1);
|
|
|
|
|
if ( allSelectorLists.has(list) === false ) {
|
|
|
|
|
allSelectorLists.set(list, allSelectorLists.size);
|
|
|
|
|
}
|
2025-12-25 17:34:21 +00:00
|
|
|
return allSelectorLists.get(list);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
for ( const [ hn, selectorSet ] of allHostnames ) {
|
|
|
|
|
allHostnames.set(hn, ilistFromSelectorSet(selectorSet));
|
|
|
|
|
}
|
|
|
|
|
for ( const [ regexOrPath, selectorSet ] of allRegexesOrPaths ) {
|
|
|
|
|
allRegexesOrPaths.set(regexOrPath, ilistFromSelectorSet(selectorSet));
|
2025-12-05 13:54:34 +00:00
|
|
|
}
|
[mv3] Add ability to handle entity-based CSS and scriptlet injection filters
This commit adds the ability to inject entity-based plain CSS
filters and also a set of the most commonly used entity-based
scriptlet injection filters.
Since the scripting API is not compatible with entity patterns,
the entity-related content scripts are injected in all documents
and the entity-matching is done by the content script themselves.
Given this, entity-based content scripts are enabled only when
working in the Complete filtering mode, there won't be any
entity-based filters injected in lower modes.
Also, since there is no way to reasonably have access to the
Public Suffix List in the content scripts, the entity-matching
algorithm is an approximation, though I expect false positives
to be rare (time will tell). In the event of such false
positive, simply falling back to Optimal mode will fix the
issue.
The following issues have been fixed at the same time:
Fixed the no-filtering mode related rules having lower priority
then redirect rules, i.e. redirect rules would still be applied
despite disabling all filtering on a site.
Fixed improper detection of changes to the generic-related CSS
content script, potentially causing undue delays when for example
trying to access the popup panel while working in Complete mode.
The scripting MV3 can be quite slow when registering/updating
large content scripts, so uBOL does its best to call the API only
if really needed, but there had been a regression in the recent
builds preventing uBO from properly detecting unchanged content
script parameters.
2022-10-20 19:48:06 +00:00
|
|
|
|
2025-12-05 13:54:34 +00:00
|
|
|
const sortedHostnames = Array.from(allHostnames.keys()).toSorted((a, b) => {
|
|
|
|
|
const d = a.length - b.length;
|
|
|
|
|
if ( d !== 0 ) { return d; }
|
|
|
|
|
return a < b ? -1 : 1;
|
2022-10-15 17:05:20 +00:00
|
|
|
});
|
2025-12-09 20:32:02 +00:00
|
|
|
|
2025-12-27 14:45:25 +00:00
|
|
|
const data = {
|
2025-12-09 20:32:02 +00:00
|
|
|
selectors: Array.from(allSelectors.keys()),
|
|
|
|
|
selectorLists: Array.from(allSelectorLists.keys()),
|
|
|
|
|
selectorListRefs: sortedHostnames.map(a => allHostnames.get(a)),
|
|
|
|
|
hostnames: sortedHostnames,
|
|
|
|
|
hasEntities,
|
2025-12-25 17:34:21 +00:00
|
|
|
fromRegexes: Array.from(allRegexesOrPaths)
|
|
|
|
|
.filter(a => a[0].startsWith('/') && a[0].endsWith('/'))
|
2025-12-27 14:45:25 +00:00
|
|
|
.map(a => {
|
|
|
|
|
const restr = a[0].slice(1,-1);
|
|
|
|
|
return [ literalStrFromRegex(restr).slice(0,8), restr, a[1] ]
|
|
|
|
|
}).flat(),
|
|
|
|
|
};
|
|
|
|
|
writeFile(`${scriptletDir}/${realm}/${assetDetails.id}.json`, JSON.stringify(data));
|
2025-12-09 20:32:02 +00:00
|
|
|
|
|
|
|
|
// The cosmetic filters will be injected programmatically as content
|
|
|
|
|
// script and the decisions to activate the cosmetic filters will be
|
|
|
|
|
// done at injection time according to the document's hostname.
|
|
|
|
|
const originalScriptletMap = await loadAllSourceScriptlets();
|
|
|
|
|
let patchedScriptlet = originalScriptletMap.get(`css-${realm}`).replace(
|
|
|
|
|
'self.$rulesetId$',
|
|
|
|
|
JSON.stringify(assetDetails.id)
|
2023-06-06 00:17:50 +00:00
|
|
|
);
|
2025-12-05 13:54:34 +00:00
|
|
|
writeFile(`${scriptletDir}/${realm}/${assetDetails.id}.js`, patchedScriptlet);
|
2022-10-15 17:05:20 +00:00
|
|
|
|
2025-12-05 13:54:34 +00:00
|
|
|
log(`CSS-${realm}: ${allSelectors.size} distinct filters for ${allHostnames.size} distinct hostnames`);
|
2022-09-19 15:08:34 +00:00
|
|
|
|
2025-12-25 17:34:21 +00:00
|
|
|
return sortedHostnames.length + allRegexesOrPaths.size;
|
2022-09-16 19:56:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/******************************************************************************/
|
2022-09-06 17:47:52 +00:00
|
|
|
|
2022-09-16 19:56:35 +00:00
|
|
|
async function processScriptletFilters(assetDetails, mapin) {
|
2023-06-04 01:47:40 +00:00
|
|
|
if ( mapin === undefined ) { return 0; }
|
|
|
|
|
if ( mapin.size === 0 ) { return 0; }
|
[mv3] Add ability to handle entity-based CSS and scriptlet injection filters
This commit adds the ability to inject entity-based plain CSS
filters and also a set of the most commonly used entity-based
scriptlet injection filters.
Since the scripting API is not compatible with entity patterns,
the entity-related content scripts are injected in all documents
and the entity-matching is done by the content script themselves.
Given this, entity-based content scripts are enabled only when
working in the Complete filtering mode, there won't be any
entity-based filters injected in lower modes.
Also, since there is no way to reasonably have access to the
Public Suffix List in the content scripts, the entity-matching
algorithm is an approximation, though I expect false positives
to be rare (time will tell). In the event of such false
positive, simply falling back to Optimal mode will fix the
issue.
The following issues have been fixed at the same time:
Fixed the no-filtering mode related rules having lower priority
then redirect rules, i.e. redirect rules would still be applied
despite disabling all filtering on a site.
Fixed improper detection of changes to the generic-related CSS
content script, potentially causing undue delays when for example
trying to access the popup panel while working in Complete mode.
The scripting MV3 can be quite slow when registering/updating
large content scripts, so uBOL does its best to call the API only
if really needed, but there had been a regression in the recent
builds preventing uBO from properly detecting unchanged content
script parameters.
2022-10-20 19:48:06 +00:00
|
|
|
|
2023-06-02 21:04:15 +00:00
|
|
|
makeScriptlet.init();
|
[mv3] Add ability to handle entity-based CSS and scriptlet injection filters
This commit adds the ability to inject entity-based plain CSS
filters and also a set of the most commonly used entity-based
scriptlet injection filters.
Since the scripting API is not compatible with entity patterns,
the entity-related content scripts are injected in all documents
and the entity-matching is done by the content script themselves.
Given this, entity-based content scripts are enabled only when
working in the Complete filtering mode, there won't be any
entity-based filters injected in lower modes.
Also, since there is no way to reasonably have access to the
Public Suffix List in the content scripts, the entity-matching
algorithm is an approximation, though I expect false positives
to be rare (time will tell). In the event of such false
positive, simply falling back to Optimal mode will fix the
issue.
The following issues have been fixed at the same time:
Fixed the no-filtering mode related rules having lower priority
then redirect rules, i.e. redirect rules would still be applied
despite disabling all filtering on a site.
Fixed improper detection of changes to the generic-related CSS
content script, potentially causing undue delays when for example
trying to access the popup panel while working in Complete mode.
The scripting MV3 can be quite slow when registering/updating
large content scripts, so uBOL does its best to call the API only
if really needed, but there had been a regression in the recent
builds preventing uBO from properly detecting unchanged content
script parameters.
2022-10-20 19:48:06 +00:00
|
|
|
|
2023-06-02 21:04:15 +00:00
|
|
|
for ( const details of mapin.values() ) {
|
2025-02-22 12:38:12 +00:00
|
|
|
makeScriptlet.compile(assetDetails, details);
|
[mv3] Add ability to handle entity-based CSS and scriptlet injection filters
This commit adds the ability to inject entity-based plain CSS
filters and also a set of the most commonly used entity-based
scriptlet injection filters.
Since the scripting API is not compatible with entity patterns,
the entity-related content scripts are injected in all documents
and the entity-matching is done by the content script themselves.
Given this, entity-based content scripts are enabled only when
working in the Complete filtering mode, there won't be any
entity-based filters injected in lower modes.
Also, since there is no way to reasonably have access to the
Public Suffix List in the content scripts, the entity-matching
algorithm is an approximation, though I expect false positives
to be rare (time will tell). In the event of such false
positive, simply falling back to Optimal mode will fix the
issue.
The following issues have been fixed at the same time:
Fixed the no-filtering mode related rules having lower priority
then redirect rules, i.e. redirect rules would still be applied
despite disabling all filtering on a site.
Fixed improper detection of changes to the generic-related CSS
content script, potentially causing undue delays when for example
trying to access the popup panel while working in Complete mode.
The scripting MV3 can be quite slow when registering/updating
large content scripts, so uBOL does its best to call the API only
if really needed, but there had been a regression in the recent
builds preventing uBO from properly detecting unchanged content
script parameters.
2022-10-20 19:48:06 +00:00
|
|
|
}
|
2023-06-02 21:04:15 +00:00
|
|
|
const stats = await makeScriptlet.commit(
|
|
|
|
|
assetDetails.id,
|
|
|
|
|
`${scriptletDir}/scriptlet`,
|
|
|
|
|
writeFile
|
|
|
|
|
);
|
|
|
|
|
if ( stats.length !== 0 ) {
|
|
|
|
|
scriptletStats.set(assetDetails.id, stats);
|
[mv3] Add ability to handle entity-based CSS and scriptlet injection filters
This commit adds the ability to inject entity-based plain CSS
filters and also a set of the most commonly used entity-based
scriptlet injection filters.
Since the scripting API is not compatible with entity patterns,
the entity-related content scripts are injected in all documents
and the entity-matching is done by the content script themselves.
Given this, entity-based content scripts are enabled only when
working in the Complete filtering mode, there won't be any
entity-based filters injected in lower modes.
Also, since there is no way to reasonably have access to the
Public Suffix List in the content scripts, the entity-matching
algorithm is an approximation, though I expect false positives
to be rare (time will tell). In the event of such false
positive, simply falling back to Optimal mode will fix the
issue.
The following issues have been fixed at the same time:
Fixed the no-filtering mode related rules having lower priority
then redirect rules, i.e. redirect rules would still be applied
despite disabling all filtering on a site.
Fixed improper detection of changes to the generic-related CSS
content script, potentially causing undue delays when for example
trying to access the popup panel while working in Complete mode.
The scripting MV3 can be quite slow when registering/updating
large content scripts, so uBOL does its best to call the API only
if really needed, but there had been a regression in the recent
builds preventing uBO from properly detecting unchanged content
script parameters.
2022-10-20 19:48:06 +00:00
|
|
|
}
|
2023-06-02 21:04:15 +00:00
|
|
|
makeScriptlet.reset();
|
2023-06-04 01:47:40 +00:00
|
|
|
return stats.length;
|
2022-09-16 19:56:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
|
2022-10-15 17:05:20 +00:00
|
|
|
async function rulesetFromURLs(assetDetails) {
|
2022-09-24 15:33:04 +00:00
|
|
|
log('============================');
|
|
|
|
|
log(`Listset for '${assetDetails.id}':`);
|
|
|
|
|
|
2024-12-29 21:38:56 +00:00
|
|
|
if ( assetDetails.text === undefined && assetDetails.urls.length !== 0 ) {
|
2024-01-21 19:32:14 +00:00
|
|
|
const text = await fetchList(assetDetails);
|
2025-05-20 11:34:05 +00:00
|
|
|
if ( text === undefined ) {
|
|
|
|
|
process.exit(1);
|
|
|
|
|
}
|
2022-10-15 17:05:20 +00:00
|
|
|
assetDetails.text = text;
|
2024-12-29 21:38:56 +00:00
|
|
|
} else {
|
|
|
|
|
assetDetails.text = '';
|
2022-10-15 17:05:20 +00:00
|
|
|
}
|
2022-09-24 15:33:04 +00:00
|
|
|
|
2024-12-29 21:38:56 +00:00
|
|
|
if ( Array.isArray(assetDetails.filters) && assetDetails.filters.length ) {
|
|
|
|
|
const extra = [
|
|
|
|
|
`!#trusted on ${secret}`,
|
|
|
|
|
...assetDetails.filters,
|
|
|
|
|
`!#trusted off ${secret}`,
|
|
|
|
|
assetDetails.text,
|
|
|
|
|
];
|
|
|
|
|
assetDetails.text = extra.join('\n').trim();
|
2024-02-26 01:39:21 +00:00
|
|
|
}
|
|
|
|
|
|
2024-12-29 21:38:56 +00:00
|
|
|
if ( assetDetails.text === '' ) { return; }
|
|
|
|
|
|
2022-10-16 16:05:24 +00:00
|
|
|
const extensionPaths = [];
|
|
|
|
|
for ( const [ fname, details ] of redirectResourcesMap ) {
|
|
|
|
|
const path = `/web_accessible_resources/${fname}`;
|
|
|
|
|
extensionPaths.push([ fname, path ]);
|
|
|
|
|
if ( details.alias === undefined ) { continue; }
|
|
|
|
|
if ( typeof details.alias === 'string' ) {
|
|
|
|
|
extensionPaths.push([ details.alias, path ]);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if ( Array.isArray(details.alias) === false ) { continue; }
|
|
|
|
|
for ( const alias of details.alias ) {
|
|
|
|
|
extensionPaths.push([ alias, path ]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-09-24 15:33:04 +00:00
|
|
|
const results = await dnrRulesetFromRawLists(
|
2022-10-15 17:05:20 +00:00
|
|
|
[ { name: assetDetails.id, text: assetDetails.text } ],
|
2025-04-24 13:58:31 +00:00
|
|
|
{ env, extensionPaths, secret, networkBad }
|
2022-09-24 15:33:04 +00:00
|
|
|
);
|
2025-04-24 13:58:31 +00:00
|
|
|
networkBad = results.networkBad;
|
2022-09-24 15:33:04 +00:00
|
|
|
|
2025-04-19 12:37:29 +00:00
|
|
|
// Release memory used by filter list content
|
|
|
|
|
assetDetails.text = undefined;
|
|
|
|
|
|
2022-09-24 15:33:04 +00:00
|
|
|
const netStats = await processNetworkFilters(
|
|
|
|
|
assetDetails,
|
|
|
|
|
results.network
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Split cosmetic filters into two groups: declarative and procedural
|
|
|
|
|
const declarativeCosmetic = new Map();
|
|
|
|
|
const proceduralCosmetic = new Map();
|
|
|
|
|
const rejectedCosmetic = [];
|
2022-10-10 16:28:24 +00:00
|
|
|
if ( results.specificCosmetic ) {
|
|
|
|
|
for ( const [ selector, details ] of results.specificCosmetic ) {
|
2022-09-24 15:33:04 +00:00
|
|
|
if ( details.rejected ) {
|
|
|
|
|
rejectedCosmetic.push(selector);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if ( selector.startsWith('{') === false ) {
|
|
|
|
|
declarativeCosmetic.set(selector, details);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
const parsed = JSON.parse(selector);
|
|
|
|
|
parsed.raw = undefined;
|
|
|
|
|
proceduralCosmetic.set(JSON.stringify(parsed), details);
|
|
|
|
|
}
|
|
|
|
|
}
|
2022-10-15 17:05:20 +00:00
|
|
|
if ( rejectedCosmetic.length !== 0 ) {
|
|
|
|
|
log(`Rejected cosmetic filters: ${rejectedCosmetic.length}`);
|
|
|
|
|
log(rejectedCosmetic.map(line => `\t${line}`).join('\n'), true);
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-18 21:15:35 +00:00
|
|
|
const genericDetailsForRuleset = {};
|
2023-07-06 19:45:45 +00:00
|
|
|
if (
|
|
|
|
|
Array.isArray(results.network.generichideExclusions) &&
|
|
|
|
|
results.network.generichideExclusions.length !== 0
|
|
|
|
|
) {
|
2025-03-18 21:15:35 +00:00
|
|
|
genericDetailsForRuleset.unhide = results.network.generichideExclusions
|
|
|
|
|
.filter(hn => hn.endsWith('.*') === false)
|
|
|
|
|
.sort();
|
|
|
|
|
}
|
|
|
|
|
if (
|
|
|
|
|
Array.isArray(results.network.generichideInclusions) &&
|
|
|
|
|
results.network.generichideInclusions.length !== 0
|
|
|
|
|
) {
|
|
|
|
|
genericDetailsForRuleset.hide = results.network.generichideInclusions
|
|
|
|
|
.filter(hn => hn.endsWith('.*') === false)
|
|
|
|
|
.sort();
|
|
|
|
|
}
|
|
|
|
|
if ( genericDetailsForRuleset.unhide || genericDetailsForRuleset.hide ) {
|
|
|
|
|
genericDetails.set(assetDetails.id, genericDetailsForRuleset);
|
2023-07-06 19:45:45 +00:00
|
|
|
}
|
|
|
|
|
|
2022-10-10 16:28:24 +00:00
|
|
|
const genericCosmeticStats = await processGenericCosmeticFilters(
|
|
|
|
|
assetDetails,
|
2025-03-13 17:15:27 +00:00
|
|
|
results.genericCosmeticFilters,
|
|
|
|
|
results.genericCosmeticExceptions,
|
|
|
|
|
declarativeCosmetic
|
2023-07-06 19:45:45 +00:00
|
|
|
);
|
|
|
|
|
const genericHighCosmeticStats = await processGenericHighCosmeticFilters(
|
|
|
|
|
assetDetails,
|
2025-03-13 17:15:27 +00:00
|
|
|
results.genericCosmeticFilters,
|
|
|
|
|
results.genericCosmeticExceptions,
|
2022-10-10 16:28:24 +00:00
|
|
|
);
|
|
|
|
|
const specificCosmeticStats = await processCosmeticFilters(
|
2022-09-24 15:33:04 +00:00
|
|
|
assetDetails,
|
2025-12-05 13:54:34 +00:00
|
|
|
'specific',
|
2022-09-24 15:33:04 +00:00
|
|
|
declarativeCosmetic
|
|
|
|
|
);
|
2025-12-05 13:54:34 +00:00
|
|
|
|
|
|
|
|
const proceduralStats = await processCosmeticFilters(
|
2022-09-24 15:33:04 +00:00
|
|
|
assetDetails,
|
2025-12-05 13:54:34 +00:00
|
|
|
'procedural',
|
2022-09-24 15:33:04 +00:00
|
|
|
proceduralCosmetic
|
|
|
|
|
);
|
|
|
|
|
const scriptletStats = await processScriptletFilters(
|
|
|
|
|
assetDetails,
|
|
|
|
|
results.scriptlet
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
rulesetDetails.push({
|
|
|
|
|
id: assetDetails.id,
|
|
|
|
|
name: assetDetails.name,
|
2023-06-04 01:47:40 +00:00
|
|
|
group: assetDetails.group,
|
2024-11-17 22:27:27 +00:00
|
|
|
parent: assetDetails.parent,
|
2022-09-24 15:33:04 +00:00
|
|
|
enabled: assetDetails.enabled,
|
|
|
|
|
lang: assetDetails.lang,
|
2024-11-17 22:27:27 +00:00
|
|
|
tags: assetDetails.tags,
|
2022-09-24 15:33:04 +00:00
|
|
|
homeURL: assetDetails.homeURL,
|
|
|
|
|
filters: {
|
|
|
|
|
total: results.network.filterCount,
|
|
|
|
|
accepted: results.network.acceptedFilterCount,
|
|
|
|
|
rejected: results.network.rejectedFilterCount,
|
|
|
|
|
},
|
|
|
|
|
rules: {
|
|
|
|
|
total: netStats.total,
|
2022-09-29 23:51:33 +00:00
|
|
|
plain: netStats.plain,
|
2022-10-16 16:05:24 +00:00
|
|
|
regex: netStats.regex,
|
|
|
|
|
removeparam: netStats.removeparam,
|
|
|
|
|
redirect: netStats.redirect,
|
2023-07-10 15:56:57 +00:00
|
|
|
modifyHeaders: netStats.modifyHeaders,
|
2024-12-03 21:41:34 +00:00
|
|
|
strictblock: netStats.strictblock,
|
2024-12-05 17:56:25 +00:00
|
|
|
urlskip: netStats.urlskip,
|
2022-09-24 15:33:04 +00:00
|
|
|
discarded: netStats.discarded,
|
|
|
|
|
rejected: netStats.rejected,
|
|
|
|
|
},
|
|
|
|
|
css: {
|
2022-10-10 16:28:24 +00:00
|
|
|
generic: genericCosmeticStats,
|
2023-07-06 19:45:45 +00:00
|
|
|
generichigh: genericHighCosmeticStats,
|
2022-10-10 16:28:24 +00:00
|
|
|
specific: specificCosmeticStats,
|
2022-09-24 15:33:04 +00:00
|
|
|
procedural: proceduralStats,
|
|
|
|
|
},
|
[mv3] Add ability to handle entity-based CSS and scriptlet injection filters
This commit adds the ability to inject entity-based plain CSS
filters and also a set of the most commonly used entity-based
scriptlet injection filters.
Since the scripting API is not compatible with entity patterns,
the entity-related content scripts are injected in all documents
and the entity-matching is done by the content script themselves.
Given this, entity-based content scripts are enabled only when
working in the Complete filtering mode, there won't be any
entity-based filters injected in lower modes.
Also, since there is no way to reasonably have access to the
Public Suffix List in the content scripts, the entity-matching
algorithm is an approximation, though I expect false positives
to be rare (time will tell). In the event of such false
positive, simply falling back to Optimal mode will fix the
issue.
The following issues have been fixed at the same time:
Fixed the no-filtering mode related rules having lower priority
then redirect rules, i.e. redirect rules would still be applied
despite disabling all filtering on a site.
Fixed improper detection of changes to the generic-related CSS
content script, potentially causing undue delays when for example
trying to access the popup panel while working in Complete mode.
The scripting MV3 can be quite slow when registering/updating
large content scripts, so uBOL does its best to call the API only
if really needed, but there had been a regression in the recent
builds preventing uBO from properly detecting unchanged content
script parameters.
2022-10-20 19:48:06 +00:00
|
|
|
scriptlets: scriptletStats,
|
2022-09-24 15:33:04 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
ruleResources.push({
|
|
|
|
|
id: assetDetails.id,
|
|
|
|
|
enabled: assetDetails.enabled,
|
2022-10-15 17:05:20 +00:00
|
|
|
path: `/rulesets/main/${assetDetails.id}.json`
|
2022-09-24 15:33:04 +00:00
|
|
|
});
|
2022-10-15 17:05:20 +00:00
|
|
|
}
|
2022-09-24 15:33:04 +00:00
|
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
|
2022-09-16 19:56:35 +00:00
|
|
|
async function main() {
|
|
|
|
|
|
2023-08-19 11:48:14 +00:00
|
|
|
let version = '';
|
2022-09-16 19:56:35 +00:00
|
|
|
{
|
|
|
|
|
const now = new Date();
|
2023-08-19 11:48:14 +00:00
|
|
|
const yearPart = now.getUTCFullYear();
|
|
|
|
|
const monthPart = now.getUTCMonth() + 1;
|
|
|
|
|
const dayPart = now.getUTCDate();
|
|
|
|
|
const hourPart = Math.floor(now.getUTCHours());
|
|
|
|
|
const minutePart = Math.floor(now.getUTCMinutes());
|
2025-05-08 20:53:07 +00:00
|
|
|
version = `${yearPart}.${monthPart*100+dayPart}.${hourPart*100+minutePart}`;
|
2022-09-16 19:56:35 +00:00
|
|
|
}
|
2025-02-21 13:23:27 +00:00
|
|
|
log(`Version: ${version}`, false);
|
2022-09-16 19:56:35 +00:00
|
|
|
|
2025-04-19 12:37:29 +00:00
|
|
|
// Get list of rulesets
|
|
|
|
|
const rulesets = await fs.readFile('rulesets.json', {
|
|
|
|
|
encoding: 'utf8'
|
|
|
|
|
}).then(text =>
|
2022-09-13 21:44:24 +00:00
|
|
|
JSON.parse(text)
|
|
|
|
|
);
|
|
|
|
|
|
2025-04-19 12:37:29 +00:00
|
|
|
for ( const ruleset of rulesets ) {
|
2025-06-22 16:07:06 +00:00
|
|
|
if ( ruleset.excludedPlatforms?.includes(platform) ) { continue; }
|
2025-04-19 12:37:29 +00:00
|
|
|
await rulesetFromURLs(ruleset);
|
2024-11-17 22:27:27 +00:00
|
|
|
}
|
|
|
|
|
|
2025-04-13 19:37:07 +00:00
|
|
|
logProgress('');
|
2025-04-13 13:04:13 +00:00
|
|
|
|
2025-04-16 12:18:26 +00:00
|
|
|
writeFile(`${rulesetDir}/ruleset-details.json`,
|
2022-09-16 19:56:35 +00:00
|
|
|
`${JSON.stringify(rulesetDetails, null, 1)}\n`
|
2022-09-15 17:14:08 +00:00
|
|
|
);
|
|
|
|
|
|
2025-04-16 12:18:26 +00:00
|
|
|
writeFile(`${rulesetDir}/scriptlet-details.json`,
|
2022-10-15 17:05:20 +00:00
|
|
|
`${JSON.stringify(scriptletStats, jsonSetMapReplacer, 1)}\n`
|
|
|
|
|
);
|
|
|
|
|
|
2025-04-16 12:18:26 +00:00
|
|
|
writeFile(`${rulesetDir}/generic-details.json`,
|
2022-10-15 17:05:20 +00:00
|
|
|
`${JSON.stringify(genericDetails, jsonSetMapReplacer, 1)}\n`
|
2022-09-16 19:56:35 +00:00
|
|
|
);
|
2022-09-06 17:47:52 +00:00
|
|
|
|
2022-10-16 16:05:24 +00:00
|
|
|
// Copy required redirect resources
|
|
|
|
|
for ( const path of requiredRedirectResources ) {
|
|
|
|
|
copyFile(`./${path}`, `${outputDir}/${path}`);
|
|
|
|
|
}
|
|
|
|
|
|
2022-09-16 19:56:35 +00:00
|
|
|
await Promise.all(writeOps);
|
2022-09-06 17:47:52 +00:00
|
|
|
|
|
|
|
|
// Patch manifest
|
2023-08-19 11:48:14 +00:00
|
|
|
// Get manifest content
|
|
|
|
|
const manifest = await fs.readFile(
|
|
|
|
|
`${outputDir}/manifest.json`,
|
|
|
|
|
{ encoding: 'utf8' }
|
|
|
|
|
).then(text =>
|
|
|
|
|
JSON.parse(text)
|
|
|
|
|
);
|
2022-10-16 16:05:24 +00:00
|
|
|
// Patch declarative_net_request key
|
2022-09-06 17:47:52 +00:00
|
|
|
manifest.declarative_net_request = { rule_resources: ruleResources };
|
2022-10-16 16:05:24 +00:00
|
|
|
// Patch web_accessible_resources key
|
2024-12-03 21:41:34 +00:00
|
|
|
manifest.web_accessible_resources = manifest.web_accessible_resources || [];
|
2023-04-07 14:19:43 +00:00
|
|
|
const web_accessible_resources = {
|
2025-04-19 17:08:59 +00:00
|
|
|
resources: Array.from(requiredRedirectResources).map(path => `${path}`),
|
2022-10-16 16:05:24 +00:00
|
|
|
matches: [ '<all_urls>' ],
|
2023-04-07 14:19:43 +00:00
|
|
|
};
|
2025-04-19 17:08:59 +00:00
|
|
|
if ( env.includes('chromium') && env.includes('safari') === false ) {
|
2023-04-07 14:19:43 +00:00
|
|
|
web_accessible_resources.use_dynamic_url = true;
|
|
|
|
|
}
|
2024-12-03 21:41:34 +00:00
|
|
|
manifest.web_accessible_resources.push(web_accessible_resources);
|
2023-04-07 14:19:43 +00:00
|
|
|
|
2023-08-19 11:48:14 +00:00
|
|
|
// Patch manifest version property
|
|
|
|
|
manifest.version = version;
|
2022-10-16 16:05:24 +00:00
|
|
|
// Commit changes
|
2025-04-16 12:18:26 +00:00
|
|
|
await fs.writeFile(`${outputDir}/manifest.json`,
|
2022-09-06 17:47:52 +00:00
|
|
|
JSON.stringify(manifest, null, 2) + '\n'
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Log results
|
2022-09-27 11:46:24 +00:00
|
|
|
const logContent = stdOutput.join('\n') + '\n';
|
2024-11-20 17:55:13 +00:00
|
|
|
await fs.writeFile(`${outputDir}/log.txt`, logContent);
|
2022-09-06 17:47:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
main();
|
|
|
|
|
|
|
|
|
|
/******************************************************************************/
|