[mv3] Implement strict blocking

Related issue:
https://github.com/uBlockOrigin/uBOL-home/issues/214

This implements basic functionality for strict blocking, i.e. the
ability to block navigation to undesirable websites. This is a
first implementation, which converts only filters that are plain
hostnames.

Unlike with uBO, it is not possible to know from which ruleset a
blocking rule originates. Nonetheless, users will have to make a
choice as to whether navigation should proceed or not.

A setting has been added to the dashboard to wholly enable/disable
strict blocking. It is enabled by default.

Potential future improvements, pending investigation on feasability
in an MV3 framework:
- Extend coverage to explicit `document` filters
- Leverage and use `urlskip=` filters in the blocking page in order
  to proceed while bypassing unwanted redirects.
This commit is contained in:
Raymond Hill 2024-12-03 16:41:34 -05:00
parent d7df6cda4a
commit aa05cb32c6
No known key found for this signature in database
GPG key ID: 25E1490B761470C2
18 changed files with 886 additions and 372 deletions

View file

@ -25,7 +25,7 @@
"128": "img/icon_128.png"
},
"manifest_version": 3,
"minimum_chrome_version": "119.0",
"minimum_chrome_version": "122.0",
"name": "__MSG_extName__",
"options_page": "dashboard.html",
"optional_host_permissions": [
@ -41,6 +41,5 @@
"storage": {
"managed_schema": "managed_storage.json"
},
"version": "1.0",
"web_accessible_resources": []
"version": "1.0"
}

View file

@ -231,8 +231,44 @@
"message": "Show the number of blocked requests on the toolbar icon",
"description": "Label for a checkbox in the options page"
},
"enableStrictBlockLabel": {
"message": "Enable strict blocking",
"description": "Label for a checkbox in the options page"
},
"enableStrictBlockLegend": {
"message": "Navigation to potentially undesirable sites will be blocked, and you will be offered the option to proceed.",
"description": "Label for a checkbox in the options page"
},
"findListsPlaceholder": {
"message": "Find lists",
"description": "Placeholder for the input field used to find lists"
},
"strictblockTitle": {
"message": "Page blocked",
"description": "Webpage title for the strict-blocked page"
},
"strictblockSentence1": {
"message": "uBO Lite has prevented the following page from loading:",
"description": "Sentence used in the strict-blocked page"
},
"strictblockNoParamsPrompt": {
"message": "without parameters",
"description": "Label to be used for the parameter-less URL"
},
"strictblockBack": {
"message": "Go back",
"description": "A button to go back to the previous webpage"
},
"strictblockClose": {
"message": "Close this window",
"description": "A button to close the current tab"
},
"strictblockDontWarn": {
"message": "Don't warn me again about this site",
"description": "Label for checkbox in document-blocked page"
},
"strictblockProceed": {
"message": "Proceed",
"description": "A button to navigate to the blocked page"
}
}

View file

@ -12,8 +12,11 @@ body.firstRun .firstRun {
h3 {
margin: 1em 0;
}
p {
white-space: pre-line;
label + legend {
color: color-mix(in srgb, currentColor 69%, transparent);
font-size: small;
margin-inline-start: var(--default-gap-large);
}
body[data-forbid~="dashboard"] #dashboard-nav [data-pane="settings"],

View file

@ -0,0 +1,147 @@
/**
uBlock Origin - a browser extension to block requests.
Copyright (C) 2018-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
*/
body {
display: flex;
padding: var(--default-gap-xxlarge) var(--default-gap-small);
justify-content: center;
}
:root.mobile body {
padding: var(--default-gap-small);
}
#rootContainer {
width: min(100%, 640px);
}
#rootContainer > * {
margin: 0 0 var(--default-gap-xxlarge) 0;
}
:root.mobile #rootContainer > * {
margin-bottom: var(--default-gap-xlarge);
}
p {
margin: 0.5em 0;
}
a {
text-decoration: none;
}
.code {
font-size: 13px;
word-break: break-all;
}
#warningSign {
color: var(--accent-surface-1);
fill: var(--accent-surface-1);
font-size: 96px;
line-height: 1;
width: 100%;
}
:root.mobile #warningSign {
font-size: 64px;
}
#theURL {
color: var(--ink-2);
padding: 0;
}
#theURL > * {
margin: 0;
}
#theURL > p {
position: relative;
z-index: 10;
}
#theURL > p > span:first-of-type {
display: block;
max-height: 6lh;
overflow-y: auto;
}
:root.mobile #theURL > p > span:first-of-type {
max-height: 3lh;
}
#theURL #toggleParse {
background-color: transparent;
top: 100%;
box-sizing: border-box;
color: var(--ink-3);
fill: var(--ink-3);
cursor: pointer;
font-size: 1.2rem;
padding: var(--default-gap-xxsmall);
position: absolute;
transform: translate(0, -50%);
}
#theURL:not(.collapsed) #toggleParse > span:first-of-type {
display: none;
}
#theURL.collapsed #toggleParse > span:last-of-type {
display: none;
}
body[dir="ltr"] #toggleParse {
right: 0;
}
body[dir="rtl"] #toggleParse {
left: 0;
}
#theURL > p:hover #toggleParse {
transform: translate(0, -50%) scale(1.15);
}
#parsed {
background-color: var(--surface-1);
border: 4px solid var(--surface-2);
font-size: small;
overflow-x: auto;
padding: var(--default-gap-xxsmall);
text-align: initial;
text-overflow: ellipsis;
}
#theURL.collapsed > #parsed {
display: none;
}
#parsed ul, #parsed li {
list-style-type: none;
}
#parsed li {
white-space: nowrap;
}
#parsed span {
display: inline-block;
}
#parsed span:first-of-type {
font-weight: bold;
}
#actionContainer {
display: flex;
justify-content: space-between;
}
:root.mobile #actionContainer {
justify-content: center;
display: flex;
flex-direction: column;
}
#actionContainer > button {
margin-bottom: 2rem
}
/* Small-screen devices */
:root.mobile button {
width: 100%;
}

View file

@ -31,6 +31,7 @@
<p><label id="autoReload" data-i18n="autoReloadLabel"><span class="input checkbox"><input type="checkbox"><svg viewBox="0 0 24 24"><path d="M1.73,12.91 8.1,19.28 22.79,4.59"/></svg></span>_</label>
</p>
<p><label id="showBlockedCount" data-i18n="showBlockedCountLabel"><span class="input checkbox"><input type="checkbox"><svg viewBox="0 0 24 24"><path d="M1.73,12.91 8.1,19.28 22.79,4.59"/></svg></span>_</label>
<p><label id="strictBlockMode" data-i18n="enableStrictBlockLabel"><span class="input checkbox"><input type="checkbox"><svg viewBox="0 0 24 24"><path d="M1.73,12.91 8.1,19.28 22.79,4.59"/></svg></span>_</label><legend data-i18n="enableStrictBlockLegend"></legend>
<p id="developerMode" hidden><label><span class="input checkbox"><input type="checkbox"><svg viewBox="0 0 24 24"><path d="M1.73,12.91 8.1,19.28 22.79,4.59"/></svg></span>Developer mode</label>
</div>

View file

@ -47,8 +47,10 @@ import {
import {
enableRulesets,
excludeFromStrictBlock,
getEnabledRulesetsDetails,
getRulesetDetails,
setStrictBlockMode,
updateDynamicRules,
} from './ruleset-manager.js';
@ -213,6 +215,7 @@ function onMessage(request, sender, callback) {
autoReload: rulesetConfig.autoReload,
showBlockedCount: rulesetConfig.showBlockedCount,
canShowBlockedCount,
strictBlockMode: rulesetConfig.strictBlockMode,
firstRun: process.firstRun,
isSideloaded,
developerMode: rulesetConfig.developerMode,
@ -244,6 +247,13 @@ function onMessage(request, sender, callback) {
});
return true;
case 'setStrictBlockMode':
setStrictBlockMode(request.state).then(( ) => {
callback();
broadcastMessage({ strictBlockMode: rulesetConfig.strictBlockMode });
});
return true;
case 'setDeveloperMode':
rulesetConfig.developerMode = request.state;
toggleDeveloperMode(rulesetConfig.developerMode);
@ -335,6 +345,13 @@ function onMessage(request, sender, callback) {
});
return true;
case 'excludeFromStrictBlock': {
excludeFromStrictBlock(request.hostname, request.permanent).then(( ) => {
callback();
});
return true;
}
case 'getMatchedRules':
getMatchedRules(request.tabId).then(entries => {
callback(entries);
@ -360,19 +377,19 @@ function onMessage(request, sender, callback) {
async function start() {
await loadRulesetConfig();
if ( process.wakeupRun === false ) {
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}`);
updateDynamicRules().then(( ) => {
rulesetConfig.version = currentVersion;
saveRulesetConfig();
});
rulesetConfig.version = currentVersion;
saveRulesetConfig();
if ( rulesetsUpdated === false ) {
updateDynamicRules();
}
}
}

View file

@ -33,6 +33,7 @@ export const rulesetConfig = {
enabledRulesets: [ 'default' ],
autoReload: true,
showBlockedCount: true,
strictBlockMode: true,
developerMode: false,
};
@ -50,6 +51,7 @@ export async function loadRulesetConfig() {
rulesetConfig.enabledRulesets = sessionData.enabledRulesets;
rulesetConfig.autoReload = sessionData.autoReload ?? true;
rulesetConfig.showBlockedCount = sessionData.showBlockedCount ?? true;
rulesetConfig.strictBlockMode = sessionData.strictBlockMode ?? true;
rulesetConfig.developerMode = sessionData.developerMode ?? false;
process.wakeupRun = true;
return;
@ -60,6 +62,7 @@ export async function loadRulesetConfig() {
rulesetConfig.enabledRulesets = localData.enabledRulesets;
rulesetConfig.autoReload = localData.autoReload ?? true;
rulesetConfig.showBlockedCount = localData.showBlockedCount ?? true;
rulesetConfig.strictBlockMode = localData.strictBlockMode ?? true;
rulesetConfig.developerMode = localData.developerMode ?? false;
sessionWrite('rulesetConfig', rulesetConfig);
return;

View file

@ -98,6 +98,12 @@ export async function sessionWrite(key, value) {
return browser.storage.session.set({ [key]: value });
}
export async function sessionRemove(key) {
if ( browser.storage instanceof Object === false ) { return; }
if ( browser.storage.session instanceof Object === false ) { return; }
return browser.storage.session.remove(key);
}
/******************************************************************************/
export async function adminRead(key) {

View file

@ -19,11 +19,6 @@
Home: https://github.com/gorhill/uBlock
*/
import {
TRUSTED_DIRECTIVE_BASE_RULE_ID,
getDynamicRules,
} from './ruleset-manager.js';
import {
broadcastMessage,
hostnamesFromMatches,
@ -33,12 +28,12 @@ import {
import {
browser,
dnr,
localRead, localWrite,
sessionRead, sessionWrite,
} from './ext.js';
import { adminReadEx } from './admin.js';
import { filteringModesToDNR } from './ruleset-manager.js';
/******************************************************************************/
@ -74,19 +69,6 @@ const pruneHostnameFromSet = (hostname, hnSet) => {
/******************************************************************************/
const eqSets = (setBefore, setAfter) => {
if ( setBefore.size !== setAfter.size ) { return false; }
for ( const hn of setAfter ) {
if ( setBefore.has(hn) === false ) { return false; }
}
for ( const hn of setBefore ) {
if ( setAfter.has(hn) === false ) { return false; }
}
return true;
};
/******************************************************************************/
const serializeModeDetails = details => {
return {
none: Array.from(details.none),
@ -284,93 +266,6 @@ async function writeFilteringModeDetails(afterDetails) {
/******************************************************************************/
async function filteringModesToDNR(modes) {
const dynamicRuleMap = await getDynamicRules();
const trustedRule = dynamicRuleMap.get(TRUSTED_DIRECTIVE_BASE_RULE_ID+0);
const beforeRequestDomainSet = new Set(trustedRule?.condition.requestDomains);
const beforeExcludedRrequestDomainSet = new Set(trustedRule?.condition.excludedRequestDomains);
if ( trustedRule !== undefined && beforeRequestDomainSet.size === 0 ) {
beforeRequestDomainSet.add('all-urls');
} else {
beforeExcludedRrequestDomainSet.add('all-urls');
}
const noneHostnames = new Set([ ...modes.none ]);
const notNoneHostnames = new Set([ ...modes.basic, ...modes.optimal, ...modes.complete ]);
let afterRequestDomainSet = new Set();
let afterExcludedRequestDomainSet = new Set();
if ( noneHostnames.has('all-urls') ) {
afterRequestDomainSet = new Set([ 'all-urls' ]);
afterExcludedRequestDomainSet = notNoneHostnames;
} else {
afterRequestDomainSet = noneHostnames;
afterExcludedRequestDomainSet = new Set([ 'all-urls' ]);
}
if ( eqSets(beforeRequestDomainSet, afterRequestDomainSet) ) {
if ( eqSets(beforeExcludedRrequestDomainSet, afterExcludedRequestDomainSet) ) {
return;
}
}
const removeRuleIds = [
TRUSTED_DIRECTIVE_BASE_RULE_ID+0,
TRUSTED_DIRECTIVE_BASE_RULE_ID+1,
];
dynamicRuleMap.delete(TRUSTED_DIRECTIVE_BASE_RULE_ID+0);
dynamicRuleMap.delete(TRUSTED_DIRECTIVE_BASE_RULE_ID+1);
const allowEverywhere = afterRequestDomainSet.delete('all-urls');
afterExcludedRequestDomainSet.delete('all-urls');
const addRules = [];
if (
allowEverywhere ||
afterRequestDomainSet.size !== 0 ||
afterExcludedRequestDomainSet.size !== 0
) {
const rule0 = {
id: TRUSTED_DIRECTIVE_BASE_RULE_ID+0,
action: { type: 'allowAllRequests' },
condition: {
resourceTypes: [ 'main_frame' ],
},
priority: 100,
};
if ( afterRequestDomainSet.size !== 0 ) {
rule0.condition.requestDomains = Array.from(afterRequestDomainSet);
} else if ( afterExcludedRequestDomainSet.size !== 0 ) {
rule0.condition.excludedRequestDomains = Array.from(afterExcludedRequestDomainSet);
}
addRules.push(rule0);
dynamicRuleMap.set(TRUSTED_DIRECTIVE_BASE_RULE_ID+0, rule0);
// https://github.com/uBlockOrigin/uBOL-home/issues/114
const rule1 = {
id: TRUSTED_DIRECTIVE_BASE_RULE_ID+1,
action: { type: 'allow' },
condition: {
resourceTypes: [ 'script' ],
},
priority: 100,
};
if ( rule0.condition.requestDomains ) {
rule1.condition.initiatorDomains = rule0.condition.requestDomains.slice();
} else if ( rule0.condition.excludedRequestDomains ) {
rule1.condition.excludedInitiatorDomains = rule0.condition.excludedRequestDomains.slice();
}
addRules.push(rule1);
dynamicRuleMap.set(TRUSTED_DIRECTIVE_BASE_RULE_ID+1, rule1);
}
const updateOptions = { removeRuleIds };
if ( addRules.length ) {
updateOptions.addRules = addRules;
}
await dnr.updateDynamicRules(updateOptions);
}
/******************************************************************************/
export async function getFilteringModeDetails() {
const actualDetails = await readFilteringModeDetails();
return {

View file

@ -34,7 +34,7 @@ import punycode from './punycode.js';
const popupPanelData = {};
const currentTab = {};
let tabHostname = '';
const tabURL = new URL(runtime.getURL('/'));
/******************************************************************************/
@ -68,8 +68,8 @@ function setFilteringMode(level, commit = false) {
}
async function commitFilteringMode() {
if ( tabHostname === '' ) { return; }
const targetHostname = normalizedHostname(tabHostname);
if ( tabURL.hostname === '' ) { return; }
const targetHostname = normalizedHostname(tabURL.hostname);
const modeSlider = qs$('.filteringModeSlider');
const afterLevel = parseInt(modeSlider.dataset.level, 10);
const beforeLevel = parseInt(modeSlider.dataset.levelBefore, 10);
@ -100,7 +100,9 @@ async function commitFilteringMode() {
}
if ( actualLevel !== beforeLevel && popupPanelData.autoReload ) {
self.setTimeout(( ) => {
browser.tabs.reload(currentTab.id);
browser.tabs.update(currentTab.id, {
url: tabURL.href,
});
}, 437);
}
}
@ -317,8 +319,12 @@ async function init() {
let url;
try {
const strictBlockURL = runtime.getURL('/strictblock.');
url = new URL(currentTab.url);
tabHostname = url.hostname || '';
if ( url.href.startsWith(strictBlockURL) ) {
url = new URL(url.hash.slice(1));
}
tabURL.href = url.href || '';
} catch(ex) {
}
@ -326,7 +332,7 @@ async function init() {
const response = await sendMessage({
what: 'popupPanelData',
origin: url.origin,
hostname: normalizedHostname(tabHostname),
hostname: normalizedHostname(tabURL.hostname),
});
if ( response instanceof Object ) {
Object.assign(popupPanelData, response);
@ -337,7 +343,7 @@ async function init() {
setFilteringMode(popupPanelData.level);
dom.text('#hostname', punycode.toUnicode(tabHostname));
dom.text('#hostname', punycode.toUnicode(tabURL.hostname));
dom.cl.toggle('#showMatchedRules', 'enabled',
popupPanelData.isSideloaded === true &&

View file

@ -23,25 +23,44 @@ import {
browser,
dnr,
i18n,
runtime,
} from './ext.js';
import {
localRead, localRemove, localWrite,
sessionRead, sessionRemove, sessionWrite,
} from './ext.js';
import {
rulesetConfig,
saveRulesetConfig,
} from './config.js';
import { fetchJSON } from './fetch.js';
import { getAdminRulesets } from './admin.js';
import { ubolLog } from './debug.js';
/******************************************************************************/
const RULE_REALM_SIZE = 1000000;
const REGEXES_REALM_START = 1000000;
const REGEXES_REALM_END = REGEXES_REALM_START + RULE_REALM_SIZE;
const REMOVEPARAMS_REALM_START = REGEXES_REALM_END;
const REMOVEPARAMS_REALM_END = REMOVEPARAMS_REALM_START + RULE_REALM_SIZE;
const REDIRECT_REALM_START = REMOVEPARAMS_REALM_END;
const REDIRECT_REALM_END = REDIRECT_REALM_START + RULE_REALM_SIZE;
const MODIFYHEADERS_REALM_START = REDIRECT_REALM_END;
const MODIFYHEADERS_REALM_END = MODIFYHEADERS_REALM_START + RULE_REALM_SIZE;
const STRICTBLOCK_BASE_RULE_ID = 7000000;
const TRUSTED_DIRECTIVE_BASE_RULE_ID = 8000000;
let dynamicRuleId = 1;
/******************************************************************************/
const eqSets = (setBefore, setAfter) => {
if ( setBefore.size !== setAfter.size ) { return false; }
for ( const hn of setAfter ) {
if ( setBefore.has(hn) === false ) { return false; }
}
for ( const hn of setBefore ) {
if ( setAfter.has(hn) === false ) { return false; }
}
return true;
};
/******************************************************************************/
function getRulesetDetails() {
@ -59,79 +78,50 @@ function getRulesetDetails() {
/******************************************************************************/
function getDynamicRules() {
if ( getDynamicRules.dynamicRuleMapPromise !== undefined ) {
return getDynamicRules.dynamicRuleMapPromise;
}
getDynamicRules.dynamicRuleMapPromise = dnr.getDynamicRules().then(rules => {
const rulesMap = new Map(rules.map(rule => [ rule.id, rule ]));
ubolLog(`Dynamic rule count: ${rulesMap.size}`);
ubolLog(`Available dynamic rule count: ${dnr.MAX_NUMBER_OF_DYNAMIC_AND_SESSION_RULES - rulesMap.size}`);
return rulesMap;
});
return getDynamicRules.dynamicRuleMapPromise;
}
/******************************************************************************/
async function pruneInvalidRegexRules(realm, rulesIn) {
// Avoid testing already tested regexes
const dynamicRules = await dnr.getDynamicRules();
const validRegexSet = new Set(
dynamicRules.filter(rule =>
rule.condition?.regexFilter && true || false
).map(rule =>
rule.condition.regexFilter
)
);
const rejectedRegexRules = [];
const validateRegex = regex => {
return dnr.isRegexSupported({ regex, isCaseSensitive: false }).then(result => {
const isSupported = result?.isSupported || false;
pruneInvalidRegexRules.validated.set(regex, isSupported);
if ( isSupported ) { return true; }
rejectedRegexRules.push(`\t${regex} ${result?.reason}`);
return false;
});
};
// Validate regex-based rules
const toCheck = [];
const rejectedRegexRules = [];
for ( const rule of rulesIn ) {
if ( rule.condition?.regexFilter === undefined ) {
toCheck.push(true);
continue;
}
const {
regexFilter: regex,
isUrlFilterCaseSensitive: isCaseSensitive
} = rule.condition;
if ( validRegexSet.has(regex) ) {
toCheck.push(true);
const { regexFilter } = rule.condition;
if ( pruneInvalidRegexRules.validated.has(regexFilter) ) {
toCheck.push(pruneInvalidRegexRules.validated.get(regexFilter));
continue;
}
if ( pruneInvalidRegexRules.invalidRegexes.has(regex) ) {
toCheck.push(false);
continue;
}
toCheck.push(
dnr.isRegexSupported({ regex, isCaseSensitive }).then(result => {
if ( result.isSupported ) { return true; }
pruneInvalidRegexRules.invalidRegexes.add(regex);
rejectedRegexRules.push(`\t${regex} ${result.reason}`);
return false;
})
);
toCheck.push(validateRegex(regexFilter));
}
// Collate results
const isValid = await Promise.all(toCheck);
if ( rejectedRegexRules.length !== 0 ) {
ubolLog(
`${realm} realm: rejected regexes:\n`,
ubolLog(`${realm} realm: rejected regexes:\n`,
rejectedRegexRules.join('\n')
);
}
return rulesIn.filter((v, i) => isValid[i]);
}
pruneInvalidRegexRules.invalidRegexes = new Set();
pruneInvalidRegexRules.validated = new Map();
/******************************************************************************/
async function updateRegexRules() {
async function updateRegexRules(toAdd) {
const rulesetDetails = await getEnabledRulesetsDetails();
// Fetch regexes for all enabled rulesets
@ -144,69 +134,31 @@ async function updateRegexRules() {
// Collate all regexes rules
const allRules = [];
let regexRuleId = REGEXES_REALM_START;
for ( const rules of regexRulesets ) {
if ( Array.isArray(rules) === false ) { continue; }
for ( const rule of rules ) {
rule.id = regexRuleId++;
rule.id = dynamicRuleId++;
allRules.push(rule);
}
}
if ( allRules.length === 0 ) { return; }
const validatedRules = await pruneInvalidRegexRules('regexes', allRules);
const validRules = await pruneInvalidRegexRules('regexes', allRules);
if ( validRules.length === 0 ) { return; }
// Add validated regex rules to dynamic ruleset without affecting rules
// outside regex rules realm.
const dynamicRuleMap = await getDynamicRules();
const newRuleMap = new Map(validatedRules.map(rule => [ rule.id, rule ]));
const addRules = [];
const removeRuleIds = [];
for ( const oldRule of dynamicRuleMap.values() ) {
if ( oldRule.id < REGEXES_REALM_START ) { continue; }
if ( oldRule.id >= REGEXES_REALM_END ) { continue; }
const newRule = newRuleMap.get(oldRule.id);
if ( newRule === undefined ) {
removeRuleIds.push(oldRule.id);
dynamicRuleMap.delete(oldRule.id);
} else if ( JSON.stringify(oldRule) !== JSON.stringify(newRule) ) {
removeRuleIds.push(oldRule.id);
addRules.push(newRule);
dynamicRuleMap.set(oldRule.id, newRule);
}
}
for ( const newRule of newRuleMap.values() ) {
if ( dynamicRuleMap.has(newRule.id) ) { continue; }
addRules.push(newRule);
dynamicRuleMap.set(newRule.id, newRule);
}
if ( addRules.length === 0 && removeRuleIds.length === 0 ) { return; }
if ( removeRuleIds.length !== 0 ) {
ubolLog(`Remove ${removeRuleIds.length} DNR regex rules`);
}
if ( addRules.length !== 0 ) {
ubolLog(`Add ${addRules.length} DNR regex rules`);
}
return dnr.updateDynamicRules({ addRules, removeRuleIds }).catch(reason => {
console.error(`updateRegexRules() / ${reason}`);
});
ubolLog(`Add ${validRules.length} DNR regex rules`);
toAdd.push(...validRules);
}
/******************************************************************************/
async function updateRemoveparamRules() {
async function updateRemoveparamRules(toAdd) {
const [
hasOmnipotence,
rulesetDetails,
dynamicRuleMap,
] = await Promise.all([
browser.permissions.contains({ origins: [ '<all_urls>' ] }),
getEnabledRulesetsDetails(),
getDynamicRules(),
]);
// Fetch removeparam rules for all enabled rulesets
@ -220,69 +172,32 @@ async function updateRemoveparamRules() {
// Removeparam rules can only be enforced with omnipotence
const allRules = [];
if ( hasOmnipotence ) {
let removeparamRuleId = REMOVEPARAMS_REALM_START;
for ( const rules of removeparamRulesets ) {
if ( Array.isArray(rules) === false ) { continue; }
for ( const rule of rules ) {
rule.id = removeparamRuleId++;
rule.id = dynamicRuleId++;
allRules.push(rule);
}
}
}
if ( allRules.length === 0 ) { return; }
const validatedRules = await pruneInvalidRegexRules('removeparam', allRules);
const validRules = await pruneInvalidRegexRules('removeparam', allRules);
if ( validRules.length === 0 ) { return; }
// Add removeparam rules to dynamic ruleset without affecting rules
// outside removeparam rules realm.
const newRuleMap = new Map(validatedRules.map(rule => [ rule.id, rule ]));
const addRules = [];
const removeRuleIds = [];
for ( const oldRule of dynamicRuleMap.values() ) {
if ( oldRule.id < REMOVEPARAMS_REALM_START ) { continue; }
if ( oldRule.id >= REMOVEPARAMS_REALM_END ) { continue; }
const newRule = newRuleMap.get(oldRule.id);
if ( newRule === undefined ) {
removeRuleIds.push(oldRule.id);
dynamicRuleMap.delete(oldRule.id);
} else if ( JSON.stringify(oldRule) !== JSON.stringify(newRule) ) {
removeRuleIds.push(oldRule.id);
addRules.push(newRule);
dynamicRuleMap.set(oldRule.id, newRule);
}
}
for ( const newRule of newRuleMap.values() ) {
if ( dynamicRuleMap.has(newRule.id) ) { continue; }
addRules.push(newRule);
dynamicRuleMap.set(newRule.id, newRule);
}
if ( addRules.length === 0 && removeRuleIds.length === 0 ) { return; }
if ( removeRuleIds.length !== 0 ) {
ubolLog(`Remove ${removeRuleIds.length} DNR removeparam rules`);
}
if ( addRules.length !== 0 ) {
ubolLog(`Add ${addRules.length} DNR removeparam rules`);
}
return dnr.updateDynamicRules({ addRules, removeRuleIds }).catch(reason => {
console.error(`updateRemoveparamRules() / ${reason}`);
});
ubolLog(`Add ${validRules.length} DNR removeparam rules`);
toAdd.push(...validRules);
}
/******************************************************************************/
async function updateRedirectRules() {
async function updateRedirectRules(toAdd) {
const [
hasOmnipotence,
rulesetDetails,
dynamicRuleMap,
] = await Promise.all([
browser.permissions.contains({ origins: [ '<all_urls>' ] }),
getEnabledRulesetsDetails(),
getDynamicRules(),
]);
// Fetch redirect rules for all enabled rulesets
@ -296,69 +211,32 @@ async function updateRedirectRules() {
// Redirect rules can only be enforced with omnipotence
const allRules = [];
if ( hasOmnipotence ) {
let redirectRuleId = REDIRECT_REALM_START;
for ( const rules of redirectRulesets ) {
if ( Array.isArray(rules) === false ) { continue; }
for ( const rule of rules ) {
rule.id = redirectRuleId++;
rule.id = dynamicRuleId++;
allRules.push(rule);
}
}
}
if ( allRules.length === 0 ) { return; }
const validatedRules = await pruneInvalidRegexRules('redirect', allRules);
const validRules = await pruneInvalidRegexRules('redirect', allRules);
if ( validRules.length === 0 ) { return; }
// Add redirect rules to dynamic ruleset without affecting rules
// outside redirect rules realm.
const newRuleMap = new Map(validatedRules.map(rule => [ rule.id, rule ]));
const addRules = [];
const removeRuleIds = [];
for ( const oldRule of dynamicRuleMap.values() ) {
if ( oldRule.id < REDIRECT_REALM_START ) { continue; }
if ( oldRule.id >= REDIRECT_REALM_END ) { continue; }
const newRule = newRuleMap.get(oldRule.id);
if ( newRule === undefined ) {
removeRuleIds.push(oldRule.id);
dynamicRuleMap.delete(oldRule.id);
} else if ( JSON.stringify(oldRule) !== JSON.stringify(newRule) ) {
removeRuleIds.push(oldRule.id);
addRules.push(newRule);
dynamicRuleMap.set(oldRule.id, newRule);
}
}
for ( const newRule of newRuleMap.values() ) {
if ( dynamicRuleMap.has(newRule.id) ) { continue; }
addRules.push(newRule);
dynamicRuleMap.set(newRule.id, newRule);
}
if ( addRules.length === 0 && removeRuleIds.length === 0 ) { return; }
if ( removeRuleIds.length !== 0 ) {
ubolLog(`Remove ${removeRuleIds.length} DNR redirect rules`);
}
if ( addRules.length !== 0 ) {
ubolLog(`Add ${addRules.length} DNR redirect rules`);
}
return dnr.updateDynamicRules({ addRules, removeRuleIds }).catch(reason => {
console.error(`updateRedirectRules() / ${reason}`);
});
ubolLog(`Add ${validRules.length} DNR redirect rules`);
toAdd.push(...validRules);
}
/******************************************************************************/
async function updateModifyHeadersRules() {
async function updateModifyHeadersRules(toAdd) {
const [
hasOmnipotence,
rulesetDetails,
dynamicRuleMap,
] = await Promise.all([
browser.permissions.contains({ origins: [ '<all_urls>' ] }),
getEnabledRulesetsDetails(),
getDynamicRules(),
]);
// Fetch modifyHeaders rules for all enabled rulesets
@ -372,69 +250,312 @@ async function updateModifyHeadersRules() {
// Redirect rules can only be enforced with omnipotence
const allRules = [];
if ( hasOmnipotence ) {
let ruleId = MODIFYHEADERS_REALM_START;
for ( const rules of rulesets ) {
if ( Array.isArray(rules) === false ) { continue; }
for ( const rule of rules ) {
rule.id = ruleId++;
rule.id = dynamicRuleId++;
allRules.push(rule);
}
}
}
if ( allRules.length === 0 ) { return; }
const validatedRules = await pruneInvalidRegexRules('modify-headers', allRules);
const validRules = await pruneInvalidRegexRules('modify-headers', allRules);
if ( validRules.length === 0 ) { return; }
// Add modifyHeaders rules to dynamic ruleset without affecting rules
// outside modifyHeaders realm.
const newRuleMap = new Map(validatedRules.map(rule => [ rule.id, rule ]));
const addRules = [];
const removeRuleIds = [];
for ( const oldRule of dynamicRuleMap.values() ) {
if ( oldRule.id < MODIFYHEADERS_REALM_START ) { continue; }
if ( oldRule.id >= MODIFYHEADERS_REALM_END ) { continue; }
const newRule = newRuleMap.get(oldRule.id);
if ( newRule === undefined ) {
removeRuleIds.push(oldRule.id);
dynamicRuleMap.delete(oldRule.id);
} else if ( JSON.stringify(oldRule) !== JSON.stringify(newRule) ) {
removeRuleIds.push(oldRule.id);
addRules.push(newRule);
dynamicRuleMap.set(oldRule.id, newRule);
}
}
for ( const newRule of newRuleMap.values() ) {
if ( dynamicRuleMap.has(newRule.id) ) { continue; }
addRules.push(newRule);
dynamicRuleMap.set(newRule.id, newRule);
}
if ( addRules.length === 0 && removeRuleIds.length === 0 ) { return; }
if ( removeRuleIds.length !== 0 ) {
ubolLog(`Remove ${removeRuleIds.length} DNR modifyHeaders rules`);
}
if ( addRules.length !== 0 ) {
ubolLog(`Add ${addRules.length} DNR modifyHeaders rules`);
}
return dnr.updateDynamicRules({ addRules, removeRuleIds }).catch(reason => {
console.error(`updateModifyHeadersRules() / ${reason}`);
});
ubolLog(`Add ${validRules.length} DNR modify-headers rules`);
toAdd.push(...validRules);
}
/******************************************************************************/
// TODO: group all omnipotence-related rules into one realm.
async function updateStrictBlockRules(dynamicRules, sessionRules) {
if ( rulesetConfig.strictBlockMode === false ) { return; }
const [
hasOmnipotence,
rulesetDetails,
permanentlyExcluded = [],
temporarilyExcluded = [],
] = await Promise.all([
browser.permissions.contains({ origins: [ '<all_urls>' ] }),
getEnabledRulesetsDetails(),
localRead('excludedStrictBlockHostnames'),
sessionRead('excludedStrictBlockHostnames'),
]);
// Fetch strick-block hostnames
const toFetch = [];
for ( const details of rulesetDetails ) {
if ( details.rules.strictblock === 0 ) { continue; }
toFetch.push(fetchJSON(`/rulesets/strictblock/${details.id}`));
}
const strictblockRulesets = await Promise.all(toFetch);
// Strict-block rules can only be enforced with omnipotence
let toStrictBlock = new Set();
if ( hasOmnipotence ) {
for ( const hostnames of strictblockRulesets ) {
if ( Array.isArray(hostnames) === false ) { continue; }
toStrictBlock = toStrictBlock.union(new Set(hostnames));
}
} else {
if ( permanentlyExcluded.length !== 0 ) {
localRemove('excludedStrictBlockHostnames');
permanentlyExcluded.length = 0;
}
if ( temporarilyExcluded.length !== 0 ) {
sessionRemove('excludedStrictBlockHostnames');
temporarilyExcluded.length = 0;
}
}
for ( const hn of permanentlyExcluded ) {
toStrictBlock.delete(hn);
}
if ( toStrictBlock.size === 0 ) { return; }
const manifest = runtime.getManifest();
let strictblockPath = '';
for ( const war of manifest.web_accessible_resources ) {
if ( war.resources.length !== 1 ) { continue; }
if ( war.resources[0].startsWith('/strictblock.') === false ) { continue; }
strictblockPath = runtime.getURL(war.resources[0]);
break;
}
if ( strictblockPath === '' ) { return; }
const dynamicRule = {
id: STRICTBLOCK_BASE_RULE_ID,
action: {
type: 'redirect',
redirect: {
regexSubstitution: `${strictblockPath}#\\0`,
},
},
condition: {
regexFilter: '^https?://.+',
requestDomains: Array.from(toStrictBlock),
resourceTypes: [ 'main_frame' ],
},
priority: 29,
};
if ( permanentlyExcluded.length !== 0 ) {
dynamicRule.condition.excludedRequestDomains = permanentlyExcluded;
}
dynamicRules.push(dynamicRule);
ubolLog(`Add 1 DNR dynamic rule with ${toStrictBlock.size} strictblock domains`);
if ( temporarilyExcluded.length === 0 ) { return; }
sessionRules.push({
id: STRICTBLOCK_BASE_RULE_ID,
action: {
type: 'allow',
},
condition: {
requestDomains: temporarilyExcluded,
resourceTypes: [ 'main_frame' ],
},
priority: 29,
});
ubolLog(`Add 1 DNR session rule with ${temporarilyExcluded.length} excluded strictblock domains`);
}
async function commitStrictBlockRules() {
const [
beforePermanentRules,
beforeTemporaryRules,
] = await Promise.all([
dnr.getDynamicRules({ ruleIds: [ STRICTBLOCK_BASE_RULE_ID ] }),
dnr.getSessionRules({ ruleIds: [ STRICTBLOCK_BASE_RULE_ID ] }),
]);
if ( beforePermanentRules?.length ) {
ubolLog(`Remove 1 DNR dynamic strictblock rule`);
}
if ( beforeTemporaryRules?.length ) {
ubolLog(`Remove 1 DNR session strictblock rule`);
}
const afterPermanentRules = [];
const afterTemporaryRules = [];
await updateStrictBlockRules(afterPermanentRules, afterTemporaryRules)
return Promise.all([
dnr.updateDynamicRules({
addRules: afterPermanentRules,
removeRuleIds: beforePermanentRules.map(rule => rule.id),
}),
dnr.updateSessionRules({
addRules: afterTemporaryRules,
removeRuleIds: beforeTemporaryRules.map(rule => rule.id),
}),
]);
}
async function excludeFromStrictBlock(hostname, permanent) {
if ( typeof hostname !== 'string' || hostname === '' ) { return; }
const readFn = permanent ? localRead : sessionRead;
const hostnames = new Set(await readFn('excludedStrictBlockHostnames'));
hostnames.add(hostname);
const writeFn = permanent ? localWrite : sessionWrite;
await writeFn('excludedStrictBlockHostnames', Array.from(hostnames));
return commitStrictBlockRules();
}
async function setStrictBlockMode(state) {
const newState = Boolean(state);
if ( newState === rulesetConfig.strictBlockMode ) { return; }
rulesetConfig.strictBlockMode = newState;
const promises = [ saveRulesetConfig() ];
if ( newState === false ) {
promises.push(
localRemove('excludedStrictBlockHostnames'),
sessionRemove('excludedStrictBlockHostnames')
);
}
await Promise.all(promises);
return commitStrictBlockRules();
}
/******************************************************************************/
async function updateDynamicRules() {
return Promise.all([
updateRegexRules(),
updateRemoveparamRules(),
updateRedirectRules(),
updateModifyHeadersRules(),
dynamicRuleId = 1;
const dynamicRules = [];
const sessionRules = [];
const [
dynamicRuleIds,
sessionRuleIds,
] = await Promise.all([
dnr.getDynamicRules().then(rules =>
rules.map(rule => rule.id)
.filter(id => id < TRUSTED_DIRECTIVE_BASE_RULE_ID)
),
dnr.getSessionRules().then(rules => rules.map(rule => rule.id)),
updateRegexRules(dynamicRules),
updateRemoveparamRules(dynamicRules),
updateRedirectRules(dynamicRules),
updateModifyHeadersRules(dynamicRules),
updateStrictBlockRules(dynamicRules, sessionRules),
]);
if ( dynamicRules.length === 0 && dynamicRuleIds.length === 0 ) { return; }
const promises = [];
if ( dynamicRules.length !== 0 || dynamicRuleIds.length !== 0 ) {
promises.push(
dnr.updateDynamicRules({
addRules: dynamicRules,
removeRuleIds: dynamicRuleIds,
}).then(( ) => {
if ( dynamicRuleIds.length !== 0 ) {
ubolLog(`Remove ${dynamicRuleIds.length} dynamic DNR rules`);
}
if ( dynamicRules.length !== 0 ) {
ubolLog(`Add ${dynamicRules.length} dynamic DNR rules`);
}
}).catch(reason => {
console.error(`updateDynamicRules() / ${reason}`);
})
);
}
if ( sessionRules.length !== 0 || sessionRuleIds.length !== 0 ) {
promises.push(
dnr.updateSessionRules({
addRules: sessionRules,
removeRuleIds: sessionRuleIds,
}).then(( ) => {
if ( sessionRuleIds.length !== 0 ) {
ubolLog(`Remove ${sessionRuleIds.length} session DNR rules`);
}
if ( sessionRules.length !== 0 ) {
ubolLog(`Add ${sessionRules.length} session DNR rules`);
}
}).catch(reason => {
console.error(`updateSessionRules() / ${reason}`);
})
);
}
return Promise.all(promises);
}
/******************************************************************************/
async function filteringModesToDNR(modes) {
const trustedRules = await dnr.getDynamicRules({
ruleIds: [ TRUSTED_DIRECTIVE_BASE_RULE_ID+0 ],
});
const trustedRule = trustedRules.length !== 0 && trustedRules[0] || undefined;
const beforeRequestDomainSet = new Set(trustedRule?.condition.requestDomains);
const beforeExcludedRrequestDomainSet = new Set(trustedRule?.condition.excludedRequestDomains);
if ( trustedRule !== undefined && beforeRequestDomainSet.size === 0 ) {
beforeRequestDomainSet.add('all-urls');
} else {
beforeExcludedRrequestDomainSet.add('all-urls');
}
const noneHostnames = new Set([ ...modes.none ]);
const notNoneHostnames = new Set([ ...modes.basic, ...modes.optimal, ...modes.complete ]);
let afterRequestDomainSet = new Set();
let afterExcludedRequestDomainSet = new Set();
if ( noneHostnames.has('all-urls') ) {
afterRequestDomainSet = new Set([ 'all-urls' ]);
afterExcludedRequestDomainSet = notNoneHostnames;
} else {
afterRequestDomainSet = noneHostnames;
afterExcludedRequestDomainSet = new Set([ 'all-urls' ]);
}
if ( eqSets(beforeRequestDomainSet, afterRequestDomainSet) ) {
if ( eqSets(beforeExcludedRrequestDomainSet, afterExcludedRequestDomainSet) ) {
return;
}
}
const removeRuleIds = [];
if ( trustedRule ) {
removeRuleIds.push(
TRUSTED_DIRECTIVE_BASE_RULE_ID+0,
TRUSTED_DIRECTIVE_BASE_RULE_ID+1
);
}
const allowEverywhere = afterRequestDomainSet.delete('all-urls');
afterExcludedRequestDomainSet.delete('all-urls');
const addRules = [];
if (
allowEverywhere ||
afterRequestDomainSet.size !== 0 ||
afterExcludedRequestDomainSet.size !== 0
) {
const rule0 = {
id: TRUSTED_DIRECTIVE_BASE_RULE_ID+0,
action: { type: 'allowAllRequests' },
condition: {
resourceTypes: [ 'main_frame' ],
},
priority: 100,
};
if ( afterRequestDomainSet.size !== 0 ) {
rule0.condition.requestDomains = Array.from(afterRequestDomainSet);
} else if ( afterExcludedRequestDomainSet.size !== 0 ) {
rule0.condition.excludedRequestDomains = Array.from(afterExcludedRequestDomainSet);
}
addRules.push(rule0);
// https://github.com/uBlockOrigin/uBOL-home/issues/114
const rule1 = {
id: TRUSTED_DIRECTIVE_BASE_RULE_ID+1,
action: { type: 'allow' },
condition: {
resourceTypes: [ 'script' ],
},
priority: 100,
};
if ( rule0.condition.requestDomains ) {
rule1.condition.initiatorDomains = rule0.condition.requestDomains.slice();
} else if ( rule0.condition.excludedRequestDomains ) {
rule1.condition.excludedInitiatorDomains = rule0.condition.excludedRequestDomains.slice();
}
addRules.push(rule1);
}
if ( addRules.length === 0 && removeRuleIds.length === 0 ) { return; }
return dnr.updateDynamicRules({ addRules, removeRuleIds });
}
/******************************************************************************/
@ -511,7 +632,7 @@ async function enableRulesets(ids) {
}
if ( enableRulesetSet.size === 0 && disableRulesetSet.size === 0 ) {
return;
return false;
}
const enableRulesetIds = Array.from(enableRulesetSet);
@ -525,7 +646,9 @@ async function enableRulesets(ids) {
}
await dnr.updateEnabledRulesets({ enableRulesetIds, disableRulesetIds });
return updateDynamicRules();
await updateDynamicRules();
return true;
}
/******************************************************************************/
@ -550,11 +673,12 @@ async function getEnabledRulesetsDetails() {
/******************************************************************************/
export {
TRUSTED_DIRECTIVE_BASE_RULE_ID,
getRulesetDetails,
getDynamicRules,
enableRulesets,
defaultRulesetsFromLanguage,
enableRulesets,
excludeFromStrictBlock,
filteringModesToDNR,
getRulesetDetails,
getEnabledRulesetsDetails,
setStrictBlockMode,
updateDynamicRules,
};

View file

@ -67,6 +67,8 @@ function renderWidgets() {
}
}
qs$('#strictBlockMode input[type="checkbox"]').checked = cachedRulesetData.strictBlockMode;
{
dom.prop('#developerMode input[type="checkbox"]', 'checked',
Boolean(cachedRulesetData.developerMode)
@ -146,6 +148,13 @@ dom.on('#showBlockedCount input[type="checkbox"]', 'change', ev => {
});
});
dom.on('#strictBlockMode input[type="checkbox"]', 'change', ev => {
sendMessage({
what: 'setStrictBlockMode',
state: ev.target.checked,
});
});
dom.on('#developerMode input[type="checkbox"]', 'change', ev => {
sendMessage({
what: 'setDeveloperMode',
@ -240,6 +249,13 @@ listen.onmessage = ev => {
}
}
if ( message.strictBlockMode !== undefined ) {
if ( message.strictBlockMode !== local.strictBlockMode ) {
local.strictBlockMode = message.strictBlockMode;
render = true;
}
}
if ( message.adminRulesets !== undefined ) {
if ( hashFromIterable(message.adminRulesets) !== hashFromIterable(local.adminRulesets) ) {
local.adminRulesets = message.adminRulesets;

View file

@ -0,0 +1,178 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2024-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
*/
import { dom, qs$ } from './dom.js';
import { i18n$ } from './i18n.js';
import { sendMessage } from './ext.js';
/******************************************************************************/
const toURL = new URL('about:blank');
function setURL(url) {
try {
toURL.href = url;
} catch(_) {
}
}
setURL(self.location.hash.slice(1));
/******************************************************************************/
function urlToFragment(raw) {
try {
const fragment = new DocumentFragment();
const url = new URL(raw);
const hn = url.hostname;
const i = raw.indexOf(hn);
const b = document.createElement('b');
b.append(hn);
fragment.append(raw.slice(0,i), b, raw.slice(i+hn.length));
return fragment;
} catch(_) {
}
return raw;
}
/******************************************************************************/
async function proceed() {
await sendMessage({
what: 'excludeFromStrictBlock',
hostname: toURL.hostname,
permanent: qs$('#disableWarning').checked,
});
window.location.replace(toURL.href);
}
/******************************************************************************/
dom.clear('#theURL > p > span:first-of-type');
qs$('#theURL > p > span:first-of-type').append(urlToFragment(toURL.href));
/******************************************************************************/
// https://github.com/gorhill/uBlock/issues/691
// Parse URL to extract as much useful information as possible. This is
// useful to assist the user in deciding whether to navigate to the web page.
(( ) => {
const reURL = /^https?:\/\//;
const liFromParam = function(name, value) {
if ( value === '' ) {
value = name;
name = '';
}
const li = dom.create('li');
let span = dom.create('span');
dom.text(span, name);
li.appendChild(span);
if ( name !== '' && value !== '' ) {
li.appendChild(document.createTextNode(' = '));
}
span = dom.create('span');
if ( reURL.test(value) ) {
const a = dom.create('a');
dom.attr(a, 'href', value);
dom.text(a, value);
span.appendChild(a);
} else {
dom.text(span, value);
}
li.appendChild(span);
return li;
};
// https://github.com/uBlockOrigin/uBlock-issues/issues/1649
// Limit recursion.
const renderParams = function(parentNode, rawURL, depth = 0) {
let url;
try {
url = new URL(rawURL);
} catch(ex) {
return false;
}
const search = url.search.slice(1);
if ( search === '' ) { return false; }
url.search = '';
const li = liFromParam(i18n$('strictblockNoParamsPrompt'), url.href);
parentNode.appendChild(li);
const params = new self.URLSearchParams(search);
for ( const [ name, value ] of params ) {
const li = liFromParam(name, value);
if ( depth < 2 && reURL.test(value) ) {
const ul = dom.create('ul');
renderParams(ul, value, depth + 1);
li.appendChild(ul);
}
parentNode.appendChild(li);
}
return true;
};
if ( renderParams(qs$('#parsed'), toURL.href) === false ) { return; }
dom.cl.remove('#toggleParse', 'hidden');
dom.on('#toggleParse', 'click', ( ) => {
dom.cl.toggle('#theURL', 'collapsed');
//vAPI.localStorage.setItem(
// 'document-blocked-expand-url',
// (dom.cl.has('#theURL', 'collapsed') === false).toString()
//);
});
//vAPI.localStorage.getItemAsync('document-blocked-expand-url').then(value => {
// dom.cl.toggle('#theURL', 'collapsed', value !== 'true' && value !== true);
//});
})();
/******************************************************************************/
// https://www.reddit.com/r/uBlockOrigin/comments/breeux/
if ( window.history.length > 1 ) {
dom.on('#back', 'click', ( ) => {
window.history.back();
});
qs$('#bye').style.display = 'none';
} else {
dom.on('#bye', 'click', ( ) => {
window.close();
});
qs$('#back').style.display = 'none';
}
dom.on('#disableWarning', 'change', ev => {
const checked = ev.target.checked;
dom.cl.toggle('[data-i18n="strictblockBack"]', 'disabled', checked);
dom.cl.toggle('[data-i18n="strictblockClose"]', 'disabled', checked);
});
dom.on('#proceed', 'click', ( ) => {
proceed();
});
/******************************************************************************/

View file

@ -16,10 +16,10 @@
"browser_specific_settings": {
"gecko": {
"id": "uBOLite@raymondhill.net",
"strict_min_version": "114.0"
"strict_min_version": "127.0"
},
"gecko_android": {
"strict_min_version": "114.0"
"strict_min_version": "127.0"
}
},
"declarative_net_request": {
@ -50,6 +50,5 @@
"storage"
],
"short_name": "uBO Lite",
"version": "1.0",
"web_accessible_resources": []
"version": "1.0"
}

View file

@ -433,6 +433,33 @@ async function processNetworkFilters(assetDetails, network) {
);
}
const strictBlocked = new Set();
for ( const rule of plainGood ) {
if ( rule.action.type !== 'block' ) { continue; }
if ( rule.condition.domainType ) { continue; }
if ( rule.condition.regexFilter ) { continue; }
if ( rule.condition.urlFilter ) { continue; }
if ( rule.condition.requestMethods ) { continue; }
if ( rule.condition.excludedRequestMethods ) { continue; }
if ( rule.condition.resourceTypes ) { continue; }
if ( rule.condition.excludedResourceTypes ) { continue; }
if ( rule.condition.responseHeaders ) { continue; }
if ( rule.condition.excludedResponseHeaders ) { continue; }
if ( rule.condition.initiatorDomains ) { continue; }
if ( rule.condition.excludedInitiatorDomains ) { continue; }
if ( rule.condition.requestDomains === undefined ) { continue; }
if ( rule.condition.excludedRequestDomains ) { continue; }
for ( const hn of rule.condition.requestDomains ) {
strictBlocked.add(hn);
}
}
if ( strictBlocked.size !== 0 ) {
writeFile(
`${rulesetDir}/strictblock/${assetDetails.id}.json`,
toJSONRuleset(Array.from(strictBlocked))
);
}
return {
total: rules.length,
plain: plainGood.length,
@ -442,6 +469,7 @@ async function processNetworkFilters(assetDetails, network) {
removeparam: removeparamsGood.length,
redirect: redirects.length,
modifyHeaders: modifyHeaders.length,
strictblock: strictBlocked.size,
};
}
@ -1085,6 +1113,7 @@ async function rulesetFromURLs(assetDetails) {
removeparam: netStats.removeparam,
redirect: netStats.redirect,
modifyHeaders: netStats.modifyHeaders,
strictblock: netStats.strictblock,
discarded: netStats.discarded,
rejected: netStats.rejected,
},
@ -1332,6 +1361,15 @@ async function main() {
// Patch declarative_net_request key
manifest.declarative_net_request = { rule_resources: ruleResources };
// Patch web_accessible_resources key
manifest.web_accessible_resources = manifest.web_accessible_resources || [];
// Strict-block-related resource
const strictblockDocument = `strictblock.${secret}.html`;
copyFile('./strictblock.html', `${outputDir}/${strictblockDocument}`);
manifest.web_accessible_resources.push({
resources: [ `/${strictblockDocument}` ],
matches: [ '<all_urls>' ],
});
// Secondary resources
const web_accessible_resources = {
resources: Array.from(requiredRedirectResources).map(path => `/${path}`),
matches: [ '<all_urls>' ],
@ -1339,7 +1377,7 @@ async function main() {
if ( platform === 'chromium' ) {
web_accessible_resources.use_dynamic_url = true;
}
manifest.web_accessible_resources = [ web_accessible_resources ];
manifest.web_accessible_resources.push(web_accessible_resources);
// Patch manifest version property
manifest.version = version;

View file

@ -0,0 +1,43 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1 user-scalable=yes">
<title data-i18n="strictblockTitle"></title>
<link rel="stylesheet" href="css/default.css">
<link rel="stylesheet" href="css/common.css">
<link rel="stylesheet" href="css/fa-icons.css">
<link rel="stylesheet" href="css/strictblock.css">
<link rel="shortcut icon" type="image/png" href="img/icon_64.png"/>
</head>
<body>
<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>
<div id="theURL" class="collapsed">
<p class="code"><span>&nbsp;</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 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>
<div id="actionContainer">
<button id="back" data-i18n="strictblockBack" type="button">_<span class="hover"></span></button>
<button id="bye" data-i18n="strictblockClose" type="button">_<span class="hover"></span></button>
<button id="proceed" class="preferred" data-i18n="strictblockProceed" type="button"><span class="hover"></span></button>
</div>
</div>
<script src="js/theme.js" type="module"></script>
<script src="js/fa-icons.js" type="module"></script>
<script src="js/i18n.js" type="module"></script>
<script src="js/strictblock.js" type="module"></script>
</body>
</html>

View file

@ -47,10 +47,12 @@
</div>
</div>
<div id="templates" style="display: none;">
<ul>
<li class="filterList">
<a class="filterListSource" href="asset-viewer.html?url=" target="_blank"></a>&nbsp;<!--
--><a class="fa-icon filterListSupport hidden" href="#" target="_blank" rel="noopener noreferrer">home</a>
</span>
</li>
</ul>
</div>
<script src="lib/hsluv/hsluv-0.1.0.min.js"></script>

View file

@ -107,6 +107,7 @@ if [ "$QUICK" != "yes" ]; then
cp platform/mv3/package.json "$TMPDIR"/
cp platform/mv3/*.js "$TMPDIR"/
cp platform/mv3/*.mjs "$TMPDIR"/
cp platform/mv3/*.html "$TMPDIR"/
cp platform/mv3/extension/js/utils.js "$TMPDIR"/js/
cp -R "$UBO_DIR"/src/js/resources "$TMPDIR"/js/
cp "$UBO_DIR"/assets/assets.dev.json "$TMPDIR"/