diff --git a/platform/mv3/extension/_locales/en/messages.json b/platform/mv3/extension/_locales/en/messages.json index 17a5b24cd7935..ad9d26911371e 100644 --- a/platform/mv3/extension/_locales/en/messages.json +++ b/platform/mv3/extension/_locales/en/messages.json @@ -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", diff --git a/platform/mv3/extension/css/settings.css b/platform/mv3/extension/css/settings.css index a8b07d2a1894c..02eb600370a5d 100644 --- a/platform/mv3/extension/css/settings.css +++ b/platform/mv3/extension/css/settings.css @@ -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); } diff --git a/platform/mv3/extension/css/strictblock.css b/platform/mv3/extension/css/strictblock.css index f7534078cca7b..7f1f0e9c0d162 100644 --- a/platform/mv3/extension/css/strictblock.css +++ b/platform/mv3/extension/css/strictblock.css @@ -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; diff --git a/platform/mv3/extension/js/background.js b/platform/mv3/extension/js/background.js index a58dd70639a48..e250d1025ed96 100644 --- a/platform/mv3/extension/js/background.js +++ b/platform/mv3/extension/js/background.js @@ -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 diff --git a/platform/mv3/extension/js/config.js b/platform/mv3/extension/js/config.js index e3859e63fa42f..0c79dd62b18f0 100644 --- a/platform/mv3/extension/js/config.js +++ b/platform/mv3/extension/js/config.js @@ -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; diff --git a/platform/mv3/extension/js/ruleset-manager.js b/platform/mv3/extension/js/ruleset-manager.js index c9490e6836592..04bfb3b6f241d 100644 --- a/platform/mv3/extension/js/ruleset-manager.js +++ b/platform/mv3/extension/js/ruleset-manager.js @@ -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, }; diff --git a/platform/mv3/extension/js/strictblock.js b/platform/mv3/extension/js/strictblock.js index 616ef6ddbc32d..7600bcfbb9e7f 100644 --- a/platform/mv3/extension/js/strictblock.js +++ b/platform/mv3/extension/js/strictblock.js @@ -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'); /******************************************************************************/ diff --git a/platform/mv3/make-rulesets.js b/platform/mv3/make-rulesets.js index b570067055473..4ea7ae6dba1d0 100644 --- a/platform/mv3/make-rulesets.js +++ b/platform/mv3/make-rulesets.js @@ -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 && + rule.action !== undefined && ( + rule.action.type === 'block' || + rule.action.type === 'allow' || + rule.action.type === 'allowAllRequests' + ); + +const isURLSkip = rule => isUnsupported(rule) === false && - isRedirect(rule) === false && - isModifyHeaders(rule) === false && - isRemoveparam(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)', diff --git a/platform/mv3/strictblock.html b/platform/mv3/strictblock.html index 9572eab09715b..8b6ef2d9be94b 100644 --- a/platform/mv3/strictblock.html +++ b/platform/mv3/strictblock.html @@ -10,20 +10,26 @@ -
+_
+