mirror of
https://github.com/gorhill/uBlock.git
synced 2026-03-11 09:04:36 +00:00
[mv3] Add urlskip support for strict-blocked page + extra info
Add information about which ruleset caused a page to be strict- blocked. Whenever possible, add ability to URL-skip an incoming redirect in a strict-blocked page. Add new default list: "30-day Phishing Domain List"
This commit is contained in:
parent
fb82db34f7
commit
38390bab9c
15 changed files with 418 additions and 185 deletions
|
|
@ -250,6 +250,14 @@
|
|||
"strictblockSentence1": {
|
||||
"message": "uBO Lite has prevented the following page from loading:",
|
||||
"description": "Sentence used in the strict-blocked page"
|
||||
},
|
||||
"strictblockReasonSentence1": {
|
||||
"message": "The page was blocked because of a matching filter in {{listname}}.",
|
||||
"description": "Text informing about what is causing the page to be blocked"
|
||||
},
|
||||
"strictblockRedirectSentence1": {
|
||||
"message": "The blocked page wants to redirect to another site. If you choose to proceed, you will navigate directly to: {{url}}",
|
||||
"description": "Text warning about an incoming redirect"
|
||||
},
|
||||
"strictblockNoParamsPrompt": {
|
||||
"message": "without parameters",
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ h3 {
|
|||
|
||||
label + legend {
|
||||
color: color-mix(in srgb, currentColor 69%, transparent);
|
||||
font-size: small;
|
||||
font-size: var(--font-size-smaller);
|
||||
margin-inline-start: var(--default-gap-large);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,9 @@ body {
|
|||
:root.mobile body {
|
||||
padding: var(--default-gap-small);
|
||||
}
|
||||
body.loading {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
#rootContainer {
|
||||
width: min(100%, 640px);
|
||||
|
|
@ -43,6 +46,9 @@ p {
|
|||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
q {
|
||||
font-weight: bold;
|
||||
}
|
||||
.code {
|
||||
font-size: 13px;
|
||||
word-break: break-all;
|
||||
|
|
@ -128,6 +134,12 @@ body[dir="rtl"] #toggleParse {
|
|||
font-weight: bold;
|
||||
}
|
||||
|
||||
#urlskip a {
|
||||
display: block;
|
||||
overflow-y: auto;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
#actionContainer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ import {
|
|||
excludeFromStrictBlock,
|
||||
getEnabledRulesetsDetails,
|
||||
getRulesetDetails,
|
||||
patchDefaultRulesets,
|
||||
setStrictBlockMode,
|
||||
updateDynamicRules,
|
||||
} from './ruleset-manager.js';
|
||||
|
|
@ -377,20 +378,24 @@ function onMessage(request, sender, callback) {
|
|||
async function start() {
|
||||
await loadRulesetConfig();
|
||||
|
||||
const currentVersion = getCurrentVersion();
|
||||
const isNewVersion = currentVersion !== rulesetConfig.version;
|
||||
|
||||
// The default rulesets may have changed, find out new ruleset to enable,
|
||||
// obsolete ruleset to remove.
|
||||
if ( isNewVersion ) {
|
||||
ubolLog(`Version change: ${rulesetConfig.version} => ${currentVersion}`);
|
||||
rulesetConfig.version = currentVersion;
|
||||
await patchDefaultRulesets();
|
||||
saveRulesetConfig();
|
||||
}
|
||||
|
||||
const rulesetsUpdated = process.wakeupRun === false &&
|
||||
await enableRulesets(rulesetConfig.enabledRulesets);
|
||||
|
||||
// We need to update the regex rules only when ruleset version changes.
|
||||
if ( process.wakeupRun === false ) {
|
||||
const currentVersion = getCurrentVersion();
|
||||
if ( currentVersion !== rulesetConfig.version ) {
|
||||
ubolLog(`Version change: ${rulesetConfig.version} => ${currentVersion}`);
|
||||
rulesetConfig.version = currentVersion;
|
||||
saveRulesetConfig();
|
||||
if ( rulesetsUpdated === false ) {
|
||||
updateDynamicRules();
|
||||
}
|
||||
}
|
||||
if ( isNewVersion && rulesetsUpdated === false ) {
|
||||
updateDynamicRules();
|
||||
}
|
||||
|
||||
// Permissions may have been removed while the extension was disabled
|
||||
|
|
|
|||
|
|
@ -24,13 +24,11 @@ import {
|
|||
sessionRead, sessionWrite,
|
||||
} from './ext.js';
|
||||
|
||||
import { defaultRulesetsFromLanguage } from './ruleset-manager.js';
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
export const rulesetConfig = {
|
||||
version: '',
|
||||
enabledRulesets: [ 'default' ],
|
||||
enabledRulesets: [],
|
||||
autoReload: true,
|
||||
showBlockedCount: true,
|
||||
strictBlockMode: true,
|
||||
|
|
@ -67,7 +65,6 @@ export async function loadRulesetConfig() {
|
|||
sessionWrite('rulesetConfig', rulesetConfig);
|
||||
return;
|
||||
}
|
||||
rulesetConfig.enabledRulesets = await defaultRulesetsFromLanguage();
|
||||
sessionWrite('rulesetConfig', rulesetConfig);
|
||||
localWrite('rulesetConfig', rulesetConfig);
|
||||
process.firstRun = true;
|
||||
|
|
|
|||
|
|
@ -561,8 +561,6 @@ async function filteringModesToDNR(modes) {
|
|||
/******************************************************************************/
|
||||
|
||||
async function defaultRulesetsFromLanguage() {
|
||||
const out = await dnr.getEnabledRulesets();
|
||||
|
||||
const dropCountry = lang => {
|
||||
const pos = lang.indexOf('-');
|
||||
if ( pos === -1 ) { return lang; }
|
||||
|
|
@ -581,7 +579,12 @@ async function defaultRulesetsFromLanguage() {
|
|||
);
|
||||
|
||||
const rulesetDetails = await getRulesetDetails();
|
||||
const out = [];
|
||||
for ( const [ id, details ] of rulesetDetails ) {
|
||||
if ( details.enabled ) {
|
||||
out.push(id);
|
||||
continue;
|
||||
}
|
||||
if ( typeof details.lang !== 'string' ) { continue; }
|
||||
if ( reTargetLang.test(details.lang) === false ) { continue; }
|
||||
out.push(id);
|
||||
|
|
@ -591,6 +594,42 @@ async function defaultRulesetsFromLanguage() {
|
|||
|
||||
/******************************************************************************/
|
||||
|
||||
async function patchDefaultRulesets() {
|
||||
const [
|
||||
oldDefaultIds = [],
|
||||
newDefaultIds,
|
||||
newIds,
|
||||
] = await Promise.all([
|
||||
localRead('defaultRulesetIds'),
|
||||
defaultRulesetsFromLanguage(),
|
||||
getRulesetDetails(),
|
||||
]);
|
||||
const toAdd = [];
|
||||
const toRemove = [];
|
||||
for ( const id of newDefaultIds ) {
|
||||
if ( oldDefaultIds.includes(id) ) { continue; }
|
||||
toAdd.push(id);
|
||||
}
|
||||
for ( const id of oldDefaultIds ) {
|
||||
if ( newDefaultIds.includes(id) ) { continue; }
|
||||
toRemove.push(id);
|
||||
}
|
||||
for ( const id of rulesetConfig.enabledRulesets ) {
|
||||
if ( newIds.has(id) ) { continue; }
|
||||
toRemove.push(id);
|
||||
}
|
||||
localWrite('defaultRulesetIds', newDefaultIds);
|
||||
if ( toAdd.length === 0 && toRemove.length === 0 ) { return; }
|
||||
const enabledRulesets = new Set(rulesetConfig.enabledRulesets);
|
||||
toAdd.forEach(id => enabledRulesets.add(id));
|
||||
toRemove.forEach(id => enabledRulesets.delete(id));
|
||||
const patchedRulesets = Array.from(enabledRulesets);
|
||||
ubolLog(`Patched rulesets: ${rulesetConfig.enabledRulesets} => ${patchedRulesets}`);
|
||||
rulesetConfig.enabledRulesets = patchedRulesets;
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
async function enableRulesets(ids) {
|
||||
const afterIds = new Set(ids);
|
||||
const [ beforeIds, adminIds, rulesetDetails ] = await Promise.all([
|
||||
|
|
@ -679,6 +718,7 @@ export {
|
|||
filteringModesToDNR,
|
||||
getRulesetDetails,
|
||||
getEnabledRulesetsDetails,
|
||||
patchDefaultRulesets,
|
||||
setStrictBlockMode,
|
||||
updateDynamicRules,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -20,21 +20,15 @@
|
|||
*/
|
||||
|
||||
import { dom, qs$ } from './dom.js';
|
||||
import { fetchJSON } from './fetch.js';
|
||||
import { getEnabledRulesetsDetails } from './ruleset-manager.js';
|
||||
import { i18n$ } from './i18n.js';
|
||||
import { sendMessage } from './ext.js';
|
||||
import { urlSkip } from './urlskip.js';
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const toURL = new URL('about:blank');
|
||||
|
||||
function setURL(url) {
|
||||
try {
|
||||
toURL.href = url;
|
||||
} catch(_) {
|
||||
}
|
||||
}
|
||||
|
||||
setURL(self.location.hash.slice(1));
|
||||
const rulesetDetailsPromise = getEnabledRulesetsDetails();
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
|
|
@ -66,6 +60,13 @@ async function proceed() {
|
|||
|
||||
/******************************************************************************/
|
||||
|
||||
const toURL = new URL('about:blank');
|
||||
|
||||
try {
|
||||
toURL.href = self.location.hash.slice(1);
|
||||
} catch(_) {
|
||||
}
|
||||
|
||||
dom.clear('#theURL > p > span:first-of-type');
|
||||
qs$('#theURL > p > span:first-of-type').append(urlToFragment(toURL.href));
|
||||
|
||||
|
|
@ -152,16 +153,92 @@ qs$('#theURL > p > span:first-of-type').append(urlToFragment(toURL.href));
|
|||
|
||||
/******************************************************************************/
|
||||
|
||||
// Find which list caused the blocking.
|
||||
(async ( ) => {
|
||||
const rulesetDetails = await rulesetDetailsPromise;
|
||||
let iList = -1;
|
||||
const searchInList = async i => {
|
||||
if ( iList !== -1 ) { return; }
|
||||
const hostnames = new Set(
|
||||
await fetchJSON(`/rulesets/strictblock/${rulesetDetails[i].id}`)
|
||||
);
|
||||
if ( iList !== -1 ) { return; }
|
||||
let hn = toURL.hostname;
|
||||
for (;;) {
|
||||
if ( hostnames.has(hn) ) { iList = i; break; }
|
||||
const pos = hn.indexOf('.');
|
||||
if ( pos === -1 ) { break; }
|
||||
hn = hn.slice(pos+1);
|
||||
}
|
||||
};
|
||||
const toFetch = [];
|
||||
for ( let i = 0; i < rulesetDetails.length; i++ ) {
|
||||
if ( rulesetDetails[i].rules.strictblock === 0 ) { continue; }
|
||||
toFetch.push(searchInList(i));
|
||||
}
|
||||
if ( toFetch.length === 0 ) { return; }
|
||||
await Promise.all(toFetch);
|
||||
if ( iList === -1 ) { return; }
|
||||
|
||||
const fragment = new DocumentFragment();
|
||||
const text = i18n$('strictblockReasonSentence1');
|
||||
const placeholder = '{{listname}}';
|
||||
const pos = text.indexOf(placeholder);
|
||||
if ( pos === -1 ) { return; }
|
||||
const q = document.createElement('q');
|
||||
q.append(rulesetDetails[iList].name);
|
||||
fragment.append(text.slice(0, pos), q, text.slice(pos + placeholder.length));
|
||||
qs$('#reason').append(fragment);
|
||||
dom.attr('#reason', 'hidden', null);
|
||||
})();
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
// Offer to skip redirection whenever possible
|
||||
(async ( ) => {
|
||||
const rulesetDetails = await rulesetDetailsPromise;
|
||||
const toFetch = [];
|
||||
for ( const details of rulesetDetails ) {
|
||||
if ( details.rules.urlskip === 0 ) { continue; }
|
||||
toFetch.push(fetchJSON(`/rulesets/urlskip/${details.id}`));
|
||||
}
|
||||
if ( toFetch.length === 0 ) { return; }
|
||||
const urlskipLists = await Promise.all(toFetch);
|
||||
for ( const urlskips of urlskipLists ) {
|
||||
for ( const urlskip of urlskips ) {
|
||||
const re = new RegExp(urlskip.re, urlskip.c ? undefined : 'i');
|
||||
if ( re.test(toURL.href) === false ) { continue; }
|
||||
const finalURL = urlSkip(toURL.href, false, urlskip.steps);
|
||||
if ( finalURL === undefined ) { continue; }
|
||||
const fragment = new DocumentFragment();
|
||||
const text = i18n$('strictblockRedirectSentence1');
|
||||
const linkPlaceholder = '{{url}}';
|
||||
const pos = text.indexOf(linkPlaceholder);
|
||||
if ( pos === -1 ) { return; }
|
||||
const link = document.createElement('a');
|
||||
link.href = finalURL;
|
||||
dom.cl.add(link, 'code');
|
||||
link.append(urlToFragment(finalURL));
|
||||
fragment.append(
|
||||
text.slice(0, pos),
|
||||
link,
|
||||
text.slice(pos + linkPlaceholder.length)
|
||||
);
|
||||
qs$('#urlskip').append(fragment);
|
||||
dom.attr('#urlskip', 'hidden', null);
|
||||
return;
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
// https://www.reddit.com/r/uBlockOrigin/comments/breeux/
|
||||
if ( window.history.length > 1 ) {
|
||||
dom.on('#back', 'click', ( ) => {
|
||||
window.history.back();
|
||||
});
|
||||
dom.on('#back', 'click', ( ) => { window.history.back(); });
|
||||
qs$('#bye').style.display = 'none';
|
||||
} else {
|
||||
dom.on('#bye', 'click', ( ) => {
|
||||
window.close();
|
||||
});
|
||||
dom.on('#bye', 'click', ( ) => { window.close(); });
|
||||
qs$('#back').style.display = 'none';
|
||||
}
|
||||
|
||||
|
|
@ -171,8 +248,8 @@ dom.on('#disableWarning', 'change', ev => {
|
|||
dom.cl.toggle('[data-i18n="strictblockClose"]', 'disabled', checked);
|
||||
});
|
||||
|
||||
dom.on('#proceed', 'click', ( ) => {
|
||||
proceed();
|
||||
});
|
||||
dom.on('#proceed', 'click', ( ) => { proceed(); });
|
||||
|
||||
dom.cl.remove(dom.body, 'loading');
|
||||
|
||||
/******************************************************************************/
|
||||
|
|
|
|||
|
|
@ -225,7 +225,7 @@ const isRegex = rule =>
|
|||
rule.condition.regexFilter !== undefined;
|
||||
|
||||
const isRedirect = rule => {
|
||||
if ( rule.action === undefined ) { return false; }
|
||||
if ( isUnsupported(rule) ) { return false; }
|
||||
if ( rule.action.type !== 'redirect' ) { return false; }
|
||||
if ( rule.action.redirect?.extensionPath !== undefined ) { return true; }
|
||||
if ( rule.action.redirect?.transform?.path !== undefined ) { return true; }
|
||||
|
|
@ -233,19 +233,26 @@ const isRedirect = rule => {
|
|||
};
|
||||
|
||||
const isModifyHeaders = rule =>
|
||||
rule.action !== undefined &&
|
||||
isUnsupported(rule) === false &&
|
||||
rule.action.type === 'modifyHeaders';
|
||||
|
||||
const isRemoveparam = rule =>
|
||||
rule.action !== undefined &&
|
||||
isUnsupported(rule) === false &&
|
||||
rule.action.type === 'redirect' &&
|
||||
rule.action.redirect.transform !== undefined;
|
||||
|
||||
const isGood = rule =>
|
||||
const isSafe = rule =>
|
||||
isUnsupported(rule) === false &&
|
||||
isRedirect(rule) === false &&
|
||||
isModifyHeaders(rule) === false &&
|
||||
isRemoveparam(rule) === false;
|
||||
rule.action !== undefined && (
|
||||
rule.action.type === 'block' ||
|
||||
rule.action.type === 'allow' ||
|
||||
rule.action.type === 'allowAllRequests'
|
||||
);
|
||||
|
||||
const isURLSkip = rule =>
|
||||
isUnsupported(rule) === false &&
|
||||
rule.action !== undefined &&
|
||||
rule.action.type === 'urlskip';
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
|
|
@ -357,7 +364,7 @@ async function processNetworkFilters(assetDetails, network) {
|
|||
}
|
||||
}
|
||||
|
||||
const plainGood = rules.filter(rule => isGood(rule) && isRegex(rule) === false);
|
||||
const plainGood = rules.filter(rule => isSafe(rule) && isRegex(rule) === false);
|
||||
log(`\tPlain good: ${plainGood.length}`);
|
||||
log(plainGood
|
||||
.filter(rule => Array.isArray(rule._warning))
|
||||
|
|
@ -365,7 +372,7 @@ async function processNetworkFilters(assetDetails, network) {
|
|||
.join('\n'), true
|
||||
);
|
||||
|
||||
const regexes = rules.filter(rule => isGood(rule) && isRegex(rule));
|
||||
const regexes = rules.filter(rule => isSafe(rule) && isRegex(rule));
|
||||
log(`\tMaybe good (regexes): ${regexes.length}`);
|
||||
|
||||
const redirects = rules.filter(rule =>
|
||||
|
|
@ -394,6 +401,22 @@ async function processNetworkFilters(assetDetails, network) {
|
|||
);
|
||||
log(`\tmodifyHeaders=: ${modifyHeaders.length}`);
|
||||
|
||||
const urlskips = rules.filter(rule => isURLSkip(rule)).filter(rule =>
|
||||
rule.__modifierAction === 0 &&
|
||||
rule.condition &&
|
||||
rule.condition.regexFilter &&
|
||||
rule.condition.resourceTypes &&
|
||||
rule.condition.resourceTypes.includes('main_frame')
|
||||
).map(rule => {
|
||||
const steps = rule.__modifierValue;
|
||||
return {
|
||||
re: rule.condition.regexFilter,
|
||||
c: rule.condition.isUrlFilterCaseSensitive,
|
||||
steps: steps.includes(' ') && steps.split(/ +/) || [ steps ],
|
||||
};
|
||||
});
|
||||
log(`\turlskip=: ${urlskips.length}`);
|
||||
|
||||
const bad = rules.filter(rule =>
|
||||
isUnsupported(rule)
|
||||
);
|
||||
|
|
@ -460,6 +483,13 @@ async function processNetworkFilters(assetDetails, network) {
|
|||
);
|
||||
}
|
||||
|
||||
if ( urlskips.length !== 0 ) {
|
||||
writeFile(
|
||||
`${rulesetDir}/urlskip/${assetDetails.id}.json`,
|
||||
JSON.stringify(urlskips, null, 1)
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
total: rules.length,
|
||||
plain: plainGood.length,
|
||||
|
|
@ -470,6 +500,7 @@ async function processNetworkFilters(assetDetails, network) {
|
|||
redirect: redirects.length,
|
||||
modifyHeaders: modifyHeaders.length,
|
||||
strictblock: strictBlocked.size,
|
||||
urlskip: urlskips.length,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -1114,6 +1145,7 @@ async function rulesetFromURLs(assetDetails) {
|
|||
redirect: netStats.redirect,
|
||||
modifyHeaders: netStats.modifyHeaders,
|
||||
strictblock: netStats.strictblock,
|
||||
urlskip: netStats.urlskip,
|
||||
discarded: netStats.discarded,
|
||||
rejected: netStats.rejected,
|
||||
},
|
||||
|
|
@ -1264,6 +1296,14 @@ async function main() {
|
|||
});
|
||||
|
||||
// Handpicked rulesets from abroad
|
||||
await rulesetFromURLs({
|
||||
id: 'nrd.30day.phishing',
|
||||
name: '30-day Phishing Domain List',
|
||||
enabled: true,
|
||||
urls: [ 'https://raw.githubusercontent.com/xRuffKez/NRD/refs/heads/main/lists/30-day_phishing/domains-only/nrd-phishing-30day.txt' ],
|
||||
homeURL: 'https://github.com/xRuffKez/NRD?tab=readme-ov-file',
|
||||
});
|
||||
|
||||
await rulesetFromURLs({
|
||||
id: 'stevenblack-hosts',
|
||||
name: 'Steven Black’s Unified Hosts (adware + malware)',
|
||||
|
|
|
|||
|
|
@ -10,20 +10,26 @@
|
|||
<link rel="stylesheet" href="css/strictblock.css">
|
||||
<link rel="shortcut icon" type="image/png" href="img/icon_64.png"/>
|
||||
</head>
|
||||
<body>
|
||||
<body class="loading">
|
||||
<div id="rootContainer">
|
||||
<div id="warningSign">
|
||||
<a class="fa-icon" href="https://github.com/gorhill/uBlock/wiki/Strict-blocking" target="_blank" rel="noopener noreferrer">exclamation-triangle</a>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p data-i18n="strictblockSentence1">_</p>
|
||||
<p data-i18n="strictblockSentence1"></p>
|
||||
<div id="theURL" class="collapsed">
|
||||
<p class="code"><span> </span><span id="toggleParse" class="hidden"><span class="fa-icon">zoom-in</span><span class="fa-icon">zoom-out</span></span></p>
|
||||
<ul id="parsed"></ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="reason" hidden>
|
||||
</div>
|
||||
|
||||
<div id="urlskip" hidden>
|
||||
</div>
|
||||
|
||||
<div class="li">
|
||||
<label><span class="input checkbox"><input type="checkbox" id="disableWarning"><svg viewBox="0 0 24 24"><path d="M1.73,12.91 8.1,19.28 22.79,4.59"/></svg></span><span data-i18n="strictblockDontWarn">_</span></label>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1887,7 +1887,11 @@ const onMessage = function(request, sender, callback) {
|
|||
listPromises.push(
|
||||
io.get(assetKey, { dontCache: true }).then(details => {
|
||||
listNames.push(assetKey);
|
||||
return { name: assetKey, text: details.content };
|
||||
return {
|
||||
name: assetKey,
|
||||
text: details.content,
|
||||
trustedSource: assetKey.startsWith('ublock-'),
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -284,6 +284,7 @@ function addToDNR(context, list) {
|
|||
toDNR: true,
|
||||
nativeCssHas: env.includes('native_css_has'),
|
||||
badTypes: [ sfp.NODE_TYPE_NET_OPTION_NAME_REDIRECTRULE ],
|
||||
trustedSource: list.trustedSource || undefined,
|
||||
});
|
||||
const compiler = staticNetFilteringEngine.createCompiler();
|
||||
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import BidiTrieContainer from './biditrie.js';
|
|||
import { CompiledListReader } from './static-filtering-io.js';
|
||||
import { FilteringContext } from './filtering-context.js';
|
||||
import HNTrieContainer from './hntrie.js';
|
||||
import { urlSkip } from './urlskip.js';
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
|
|
@ -4679,6 +4680,25 @@ StaticNetFilteringEngine.prototype.dnrFromCompiled = function(op, context, ...ar
|
|||
dnrAddRuleError(rule, `Incompatible with DNR: uritransform=${rule.__modifierValue}`);
|
||||
break;
|
||||
}
|
||||
case 'urlskip': {
|
||||
let urlFilter = rule.condition?.urlFilter;
|
||||
if ( urlFilter === undefined ) { break; }
|
||||
let anchor = 0b000;
|
||||
if ( urlFilter.startsWith('||') ) {
|
||||
anchor |= 0b100;
|
||||
urlFilter = urlFilter.slice(2);
|
||||
} else if ( urlFilter.startsWith('|') ) {
|
||||
anchor |= 0b10;
|
||||
urlFilter = urlFilter.slice(1);
|
||||
}
|
||||
if ( urlFilter.endsWith('|') ) {
|
||||
anchor |= 0b001;
|
||||
urlFilter = urlFilter.slice(0, -1);
|
||||
}
|
||||
rule.condition.urlFilter = undefined;
|
||||
rule.condition.regexFilter = restrFromGenericPattern(urlFilter, anchor);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
dnrAddRuleError(rule, `Unsupported modifier ${rule.__modifierType}`);
|
||||
break;
|
||||
|
|
@ -5391,57 +5411,6 @@ StaticNetFilteringEngine.prototype.transformRequest = function(fctxt, out = [])
|
|||
return out;
|
||||
};
|
||||
|
||||
/**
|
||||
* @trustedOption urlskip
|
||||
*
|
||||
* @description
|
||||
* Extract a URL from another URL according to one or more transformation steps,
|
||||
* thereby skipping over intermediate network request(s) to remote servers.
|
||||
* Requires a trusted source.
|
||||
*
|
||||
* @param steps
|
||||
* A serie of space-separated directives representing the transformation steps
|
||||
* to perform to extract the final URL to which a network request should be
|
||||
* redirected.
|
||||
*
|
||||
* Supported directives:
|
||||
*
|
||||
* `?name`: extract the value of parameter `name` as the current string.
|
||||
*
|
||||
* `&i`: extract the name of the parameter at position `i` as the current
|
||||
* string. The position is 1-based.
|
||||
*
|
||||
* `/.../`: extract the first capture group of a regex as the current string.
|
||||
*
|
||||
* `+https`: prepend the current string with `https://`.
|
||||
*
|
||||
* `-base64`: decode the current string as a base64-encoded string.
|
||||
*
|
||||
* `-safebase64`: decode the current string as a safe base64-encoded string.
|
||||
*
|
||||
* `-uricomponent`: decode the current string as a URI encoded string.
|
||||
*
|
||||
* `-blocked`: allow the redirection of blocked requests. By default, blocked
|
||||
* requests can't by urlskip'ed.
|
||||
*
|
||||
* At any given step, the currently extracted string may not necessarily be
|
||||
* a valid URL, and more transformation steps may be needed to obtain a valid
|
||||
* URL once all the steps are applied.
|
||||
*
|
||||
* An unsupported step or a failed step will abort the transformation and no
|
||||
* redirection will be performed.
|
||||
*
|
||||
* The final step is expected to yield a valid URL. If the result is not a
|
||||
* valid URL, no redirection will be performed.
|
||||
*
|
||||
* @example
|
||||
* ||example.com/path/to/tracker$urlskip=?url
|
||||
* ||example.com/path/to/tracker$urlskip=?url ?to
|
||||
* ||pixiv.net/jump.php?$urlskip=&1
|
||||
* ||podtrac.com/pts/redirect.mp3/$urlskip=/\/redirect\.mp3\/(.*?\.mp3\b)/ +https
|
||||
*
|
||||
* */
|
||||
|
||||
StaticNetFilteringEngine.prototype.urlSkip = function(
|
||||
fctxt,
|
||||
blocked,
|
||||
|
|
@ -5458,7 +5427,7 @@ StaticNetFilteringEngine.prototype.urlSkip = function(
|
|||
const urlin = fctxt.url;
|
||||
const value = directive.value;
|
||||
const steps = value.includes(' ') && value.split(/ +/) || [ value ];
|
||||
const urlout = urlSkip(directive, urlin, blocked, steps);
|
||||
const urlout = urlSkip(urlin, blocked, steps, directive);
|
||||
if ( urlout === undefined ) { continue; }
|
||||
if ( urlout === urlin ) { continue; }
|
||||
fctxt.redirectURL = urlout;
|
||||
|
|
@ -5469,91 +5438,6 @@ StaticNetFilteringEngine.prototype.urlSkip = function(
|
|||
return out;
|
||||
};
|
||||
|
||||
function urlSkip(directive, url, blocked, steps) {
|
||||
try {
|
||||
let redirectBlocked = false;
|
||||
let urlout = url;
|
||||
for ( const step of steps ) {
|
||||
const urlin = urlout;
|
||||
const c0 = step.charCodeAt(0);
|
||||
// Extract from URL parameter name at position i
|
||||
if ( c0 === 0x26 ) { // &
|
||||
const i = (parseInt(step.slice(1)) || 0) - 1;
|
||||
if ( i < 0 ) { return; }
|
||||
const url = new URL(urlin);
|
||||
if ( i >= url.searchParams.size ) { return; }
|
||||
const params = Array.from(url.searchParams.keys());
|
||||
urlout = decodeURIComponent(params[i]);
|
||||
continue;
|
||||
}
|
||||
// Enforce https
|
||||
if ( c0 === 0x2B && step === '+https' ) {
|
||||
const s = urlin.replace(/^https?:\/\//, '');
|
||||
if ( /^[\w-]:\/\//.test(s) ) { return; }
|
||||
urlout = `https://${s}`;
|
||||
continue;
|
||||
}
|
||||
// Decode
|
||||
if ( c0 === 0x2D ) {
|
||||
// Base64
|
||||
if ( step === '-base64' ) {
|
||||
urlout = self.atob(urlin);
|
||||
continue;
|
||||
}
|
||||
// Safe Base64
|
||||
if ( step === '-safebase64' ) {
|
||||
urlout = urlin.replace(/[-_]/g, safeBase64Replacer);
|
||||
urlout = self.atob(urlout);
|
||||
continue;
|
||||
}
|
||||
// URI component
|
||||
if ( step === '-uricomponent' ) {
|
||||
urlout = self.decodeURIComponent(urlin);
|
||||
continue;
|
||||
}
|
||||
// Enable skip of blocked requests
|
||||
if ( step === '-blocked' ) {
|
||||
redirectBlocked = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// Regex extraction from first capture group
|
||||
if ( c0 === 0x2F ) { // /
|
||||
if ( directive.cache === null ) {
|
||||
directive.cache = new RegExp(step.slice(1, -1));
|
||||
}
|
||||
const match = directive.cache.exec(urlin);
|
||||
if ( match === null ) { return; }
|
||||
if ( match.length <= 1 ) { return; }
|
||||
urlout = match[1];
|
||||
continue;
|
||||
}
|
||||
// Extract from URL parameter
|
||||
if ( c0 === 0x3F ) { // ?
|
||||
urlout = (new URL(urlin)).searchParams.get(step.slice(1));
|
||||
if ( urlout === null ) { return; }
|
||||
if ( urlout.includes(' ') ) {
|
||||
urlout = urlout.replace(/ /g, '%20');
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// Unknown directive
|
||||
return;
|
||||
}
|
||||
const urlfinal = new URL(urlout);
|
||||
if ( urlfinal.protocol !== 'https:' ) {
|
||||
if ( urlfinal.protocol !== 'http:' ) { return; }
|
||||
urlout = urlout.replace('http', 'https');
|
||||
}
|
||||
if ( blocked && redirectBlocked !== true ) { return; }
|
||||
return urlout;
|
||||
} catch(x) {
|
||||
}
|
||||
}
|
||||
|
||||
const safeBase64Map = { '-': '+', '_': '/' };
|
||||
const safeBase64Replacer = s => safeBase64Map[s];
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
// https://github.com/uBlockOrigin/uBlock-issues/issues/1626
|
||||
|
|
|
|||
157
src/js/urlskip.js
Normal file
157
src/js/urlskip.js
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
/*******************************************************************************
|
||||
|
||||
uBlock Origin - a comprehensive, efficient content blocker
|
||||
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
|
||||
*/
|
||||
|
||||
const safeBase64Map = { '-': '+', '_': '/' };
|
||||
const safeBase64Replacer = s => safeBase64Map[s];
|
||||
|
||||
/**
|
||||
* @trustedOption urlskip
|
||||
*
|
||||
* @description
|
||||
* Extract a URL from another URL according to one or more transformation steps,
|
||||
* thereby skipping over intermediate network request(s) to remote servers.
|
||||
* Requires a trusted source.
|
||||
*
|
||||
* @param steps
|
||||
* A serie of space-separated directives representing the transformation steps
|
||||
* to perform to extract the final URL to which a network request should be
|
||||
* redirected.
|
||||
*
|
||||
* Supported directives:
|
||||
*
|
||||
* `?name`: extract the value of parameter `name` as the current string.
|
||||
*
|
||||
* `&i`: extract the name of the parameter at position `i` as the current
|
||||
* string. The position is 1-based.
|
||||
*
|
||||
* `/.../`: extract the first capture group of a regex as the current string.
|
||||
*
|
||||
* `+https`: prepend the current string with `https://`.
|
||||
*
|
||||
* `-base64`: decode the current string as a base64-encoded string.
|
||||
*
|
||||
* `-safebase64`: decode the current string as a safe base64-encoded string.
|
||||
*
|
||||
* `-uricomponent`: decode the current string as a URI encoded string.
|
||||
*
|
||||
* `-blocked`: allow the redirection of blocked requests. By default, blocked
|
||||
* requests can't by urlskip'ed.
|
||||
*
|
||||
* At any given step, the currently extracted string may not necessarily be
|
||||
* a valid URL, and more transformation steps may be needed to obtain a valid
|
||||
* URL once all the steps are applied.
|
||||
*
|
||||
* An unsupported step or a failed step will abort the transformation and no
|
||||
* redirection will be performed.
|
||||
*
|
||||
* The final step is expected to yield a valid URL. If the result is not a
|
||||
* valid URL, no redirection will be performed.
|
||||
*
|
||||
* @example
|
||||
* ||example.com/path/to/tracker$urlskip=?url
|
||||
* ||example.com/path/to/tracker$urlskip=?url ?to
|
||||
* ||pixiv.net/jump.php?$urlskip=&1
|
||||
* ||podtrac.com/pts/redirect.mp3/$urlskip=/\/redirect\.mp3\/(.*?\.mp3\b)/ +https
|
||||
*
|
||||
* */
|
||||
|
||||
export function urlSkip(url, blocked, steps, directive = {}) {
|
||||
try {
|
||||
let redirectBlocked = false;
|
||||
let urlout = url;
|
||||
for ( const step of steps ) {
|
||||
const urlin = urlout;
|
||||
const c0 = step.charCodeAt(0);
|
||||
// Extract from URL parameter name at position i
|
||||
if ( c0 === 0x26 ) { // &
|
||||
const i = (parseInt(step.slice(1)) || 0) - 1;
|
||||
if ( i < 0 ) { return; }
|
||||
const url = new URL(urlin);
|
||||
if ( i >= url.searchParams.size ) { return; }
|
||||
const params = Array.from(url.searchParams.keys());
|
||||
urlout = decodeURIComponent(params[i]);
|
||||
continue;
|
||||
}
|
||||
// Enforce https
|
||||
if ( c0 === 0x2B && step === '+https' ) {
|
||||
const s = urlin.replace(/^https?:\/\//, '');
|
||||
if ( /^[\w-]:\/\//.test(s) ) { return; }
|
||||
urlout = `https://${s}`;
|
||||
continue;
|
||||
}
|
||||
// Decode
|
||||
if ( c0 === 0x2D ) {
|
||||
// Base64
|
||||
if ( step === '-base64' ) {
|
||||
urlout = self.atob(urlin);
|
||||
continue;
|
||||
}
|
||||
// Safe Base64
|
||||
if ( step === '-safebase64' ) {
|
||||
urlout = urlin.replace(/[-_]/g, safeBase64Replacer);
|
||||
urlout = self.atob(urlout);
|
||||
continue;
|
||||
}
|
||||
// URI component
|
||||
if ( step === '-uricomponent' ) {
|
||||
urlout = self.decodeURIComponent(urlin);
|
||||
continue;
|
||||
}
|
||||
// Enable skip of blocked requests
|
||||
if ( step === '-blocked' ) {
|
||||
redirectBlocked = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// Regex extraction from first capture group
|
||||
if ( c0 === 0x2F ) { // /
|
||||
const re = directive.cache ?? new RegExp(step.slice(1, -1));
|
||||
if ( directive.cache === null ) {
|
||||
directive.cache = re;
|
||||
}
|
||||
const match = re.exec(urlin);
|
||||
if ( match === null ) { return; }
|
||||
if ( match.length <= 1 ) { return; }
|
||||
urlout = match[1];
|
||||
continue;
|
||||
}
|
||||
// Extract from URL parameter
|
||||
if ( c0 === 0x3F ) { // ?
|
||||
urlout = (new URL(urlin)).searchParams.get(step.slice(1));
|
||||
if ( urlout === null ) { return; }
|
||||
if ( urlout.includes(' ') ) {
|
||||
urlout = urlout.replace(/ /g, '%20');
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// Unknown directive
|
||||
return;
|
||||
}
|
||||
const urlfinal = new URL(urlout);
|
||||
if ( urlfinal.protocol !== 'https:' ) {
|
||||
if ( urlfinal.protocol !== 'http:' ) { return; }
|
||||
urlout = urlout.replace('http', 'https');
|
||||
}
|
||||
if ( blocked && redirectBlocked !== true ) { return; }
|
||||
return urlout;
|
||||
} catch(x) {
|
||||
}
|
||||
}
|
||||
|
|
@ -76,6 +76,7 @@ cp "$UBO_DIR"/src/css/fa-icons.css "$DES"/css/
|
|||
cp "$UBO_DIR"/src/js/dom.js "$DES"/js/
|
||||
cp "$UBO_DIR"/src/js/fa-icons.js "$DES"/js/
|
||||
cp "$UBO_DIR"/src/js/i18n.js "$DES"/js/
|
||||
cp "$UBO_DIR"/src/js/urlskip.js "$DES"/js/
|
||||
cp "$UBO_DIR"/src/lib/punycode.js "$DES"/js/
|
||||
|
||||
cp -R "$UBO_DIR/src/img/flags-of-the-world" "$DES"/img
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ cp src/js/static-net-filtering.js $DES/js
|
|||
cp src/js/static-filtering-io.js $DES/js
|
||||
cp src/js/tasks.js $DES/js
|
||||
cp src/js/text-utils.js $DES/js
|
||||
cp src/js/urlskip.js $DES/js
|
||||
cp src/js/uri-utils.js $DES/js
|
||||
cp src/js/url-net-filtering.js $DES/js
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue