uBlock/platform/mv3/extension/js/background.js
Raymond Hill 61922da24b
[mv3] Extend strict-blocking coverage; improve URL-skip behavior
This extends coverage of strict-blocking to pattern-based filters
with `doc` filter option.

When proceeding with a URL-skip URL present, no temporary bypass
will be created when the "Don't warn me again about this site" is
left unchecked. The idea is to avoid the intermediate redirects
if we navigate again on the same strict-blocked site, while a
temporary bypass would prevent this.

uBO's "Badware risks" list has been spinned off as its own list.
The idea is that should a site be strict-blocked from that list,
we would want to know the strict-block is due to the
"Badware risks" list.
2024-12-29 16:38:56 -05:00

486 lines
15 KiB
JavaScript

/*******************************************************************************
uBlock Origin Lite - a comprehensive, MV3-compliant 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
*/
import {
MODE_BASIC,
MODE_OPTIMAL,
getDefaultFilteringMode,
getFilteringMode,
getTrustedSites,
setDefaultFilteringMode,
setFilteringMode,
setTrustedSites,
syncWithBrowserPermissions,
} from './mode-manager.js';
import {
adminRead,
browser,
dnr,
localRead, localRemove, localWrite,
runtime,
windows,
} from './ext.js';
import {
adminReadEx,
getAdminRulesets,
} from './admin.js';
import {
enableRulesets,
excludeFromStrictBlock,
getEnabledRulesetsDetails,
getRulesetDetails,
patchDefaultRulesets,
setStrictBlockMode,
updateDynamicRules,
updateSessionRules,
} from './ruleset-manager.js';
import {
getMatchedRules,
isSideloaded,
toggleDeveloperMode,
ubolLog,
} from './debug.js';
import {
loadRulesetConfig,
process,
rulesetConfig,
saveRulesetConfig,
} from './config.js';
import { broadcastMessage } from './utils.js';
import { registerInjectables } from './scripting-manager.js';
/******************************************************************************/
const UBOL_ORIGIN = runtime.getURL('').replace(/\/$/, '');
const canShowBlockedCount = typeof dnr.setExtensionActionOptions === 'function';
/******************************************************************************/
function getCurrentVersion() {
return runtime.getManifest().version;
}
/******************************************************************************/
async function hasGreatPowers(origin) {
if ( /^https?:\/\//.test(origin) === false ) { return false; }
return browser.permissions.contains({
origins: [ `${origin}/*` ],
});
}
async function hasOmnipotence() {
const manifest = runtime.getManifest();
const hasOmnipotence = Array.isArray(manifest.host_permissions) &&
manifest.host_permissions.includes('<all_urls>');
if ( hasOmnipotence ) { return true; }
return browser.permissions.contains({
origins: [ '<all_urls>' ],
});
}
async function onPermissionsRemoved() {
const beforeMode = await getDefaultFilteringMode();
const modified = await syncWithBrowserPermissions();
if ( modified === false ) { return false; }
const afterMode = await getDefaultFilteringMode();
if ( beforeMode > MODE_BASIC && afterMode <= MODE_BASIC ) {
updateDynamicRules();
}
registerInjectables();
return true;
}
/******************************************************************************/
async function gotoURL(url, type) {
const pageURL = new URL(url, runtime.getURL('/'));
const tabs = await browser.tabs.query({
url: pageURL.href,
windowType: type !== 'popup' ? 'normal' : 'popup'
});
if ( Array.isArray(tabs) && tabs.length !== 0 ) {
const { windowId, id } = tabs[0];
return Promise.all([
browser.windows.update(windowId, { focused: true }),
browser.tabs.update(id, { active: true }),
]);
}
if ( type === 'popup' ) {
return windows.create({
type: 'popup',
url: pageURL.href,
});
}
return browser.tabs.create({
active: true,
url: pageURL.href,
});
}
/******************************************************************************/
function onMessage(request, sender, callback) {
// Does not require trusted origin.
switch ( request.what ) {
case 'insertCSS': {
const tabId = sender?.tab?.id ?? false;
const frameId = sender?.frameId ?? false;
if ( tabId === false || frameId === false ) { return; }
browser.scripting.insertCSS({
css: request.css,
origin: 'USER',
target: { tabId, frameIds: [ frameId ] },
}).catch(reason => {
console.log(reason);
});
return false;
}
default:
break;
}
// Does require trusted origin.
// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/MessageSender
// Firefox API does not set `sender.origin`
if ( sender.origin !== undefined && sender.origin !== UBOL_ORIGIN ) { return; }
switch ( request.what ) {
case 'applyRulesets': {
enableRulesets(request.enabledRulesets).then(( ) => {
rulesetConfig.enabledRulesets = request.enabledRulesets;
return saveRulesetConfig();
}).then(( ) => {
registerInjectables();
callback();
return dnr.getEnabledRulesets();
}).then(enabledRulesets => {
broadcastMessage({ enabledRulesets });
});
return true;
}
case 'getOptionsPageData': {
Promise.all([
getDefaultFilteringMode(),
getTrustedSites(),
getRulesetDetails(),
dnr.getEnabledRulesets(),
getAdminRulesets(),
adminReadEx('disabledFeatures'),
]).then(results => {
const [
defaultFilteringMode,
trustedSites,
rulesetDetails,
enabledRulesets,
adminRulesets,
disabledFeatures,
] = results;
callback({
defaultFilteringMode,
trustedSites: Array.from(trustedSites),
enabledRulesets,
adminRulesets,
maxNumberOfEnabledRulesets: dnr.MAX_NUMBER_OF_ENABLED_STATIC_RULESETS,
rulesetDetails: Array.from(rulesetDetails.values()),
autoReload: rulesetConfig.autoReload,
showBlockedCount: rulesetConfig.showBlockedCount,
canShowBlockedCount,
strictBlockMode: rulesetConfig.strictBlockMode,
firstRun: process.firstRun,
isSideloaded,
developerMode: rulesetConfig.developerMode,
disabledFeatures,
});
process.firstRun = false;
});
return true;
}
case 'setAutoReload':
rulesetConfig.autoReload = request.state && true || false;
saveRulesetConfig().then(( ) => {
callback();
broadcastMessage({ autoReload: rulesetConfig.autoReload });
});
return true;
case 'setShowBlockedCount':
rulesetConfig.showBlockedCount = request.state && true || false;
if ( canShowBlockedCount ) {
dnr.setExtensionActionOptions({
displayActionCountAsBadgeText: rulesetConfig.showBlockedCount,
});
}
saveRulesetConfig().then(( ) => {
callback();
broadcastMessage({ showBlockedCount: rulesetConfig.showBlockedCount });
});
return true;
case 'setStrictBlockMode':
setStrictBlockMode(request.state).then(( ) => {
callback();
broadcastMessage({ strictBlockMode: rulesetConfig.strictBlockMode });
});
return true;
case 'setDeveloperMode':
rulesetConfig.developerMode = request.state;
toggleDeveloperMode(rulesetConfig.developerMode);
saveRulesetConfig().then(( ) => {
callback();
});
return true;
case 'popupPanelData': {
Promise.all([
getFilteringMode(request.hostname),
hasOmnipotence(),
hasGreatPowers(request.origin),
getEnabledRulesetsDetails(),
adminReadEx('disabledFeatures'),
]).then(results => {
callback({
level: results[0],
autoReload: rulesetConfig.autoReload,
hasOmnipotence: results[1],
hasGreatPowers: results[2],
rulesetDetails: results[3],
isSideloaded,
developerMode: rulesetConfig.developerMode,
disabledFeatures: results[4],
});
});
return true;
}
case 'getFilteringMode': {
getFilteringMode(request.hostname).then(actualLevel => {
callback(actualLevel);
});
return true;
}
case 'gotoURL':
gotoURL(request.url, request.type);
break;
case 'setFilteringMode': {
getFilteringMode(request.hostname).then(actualLevel => {
if ( request.level === actualLevel ) { return actualLevel; }
return setFilteringMode(request.hostname, request.level);
}).then(actualLevel => {
registerInjectables();
callback(actualLevel);
});
return true;
}
case 'getDefaultFilteringMode': {
getDefaultFilteringMode().then(level => {
callback(level);
});
return true;
}
case 'setDefaultFilteringMode': {
getDefaultFilteringMode().then(beforeLevel =>
setDefaultFilteringMode(request.level).then(afterLevel =>
({ beforeLevel, afterLevel })
)
).then(({ beforeLevel, afterLevel }) => {
if ( beforeLevel === 1 || afterLevel === 1 ) {
updateDynamicRules();
}
if ( afterLevel !== beforeLevel ) {
registerInjectables();
}
callback(afterLevel);
});
return true;
}
case 'setTrustedSites':
setTrustedSites(request.hostnames).then(( ) => {
registerInjectables();
return Promise.all([
getDefaultFilteringMode(),
getTrustedSites(),
]);
}).then(results => {
callback({
defaultFilteringMode: results[0],
trustedSites: Array.from(results[1]),
});
});
return true;
case 'excludeFromStrictBlock': {
excludeFromStrictBlock(request.hostname, request.permanent).then(( ) => {
callback();
});
return true;
}
case 'getMatchedRules':
getMatchedRules(request.tabId).then(entries => {
callback(entries);
});
return true;
case 'showMatchedRules':
windows.create({
type: 'popup',
url: `/matched-rules.html?tab=${request.tabId}`,
});
break;
default:
break;
}
return false;
}
/******************************************************************************/
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 ( rulesetsUpdated === false ) {
if ( isNewVersion ) {
updateDynamicRules();
} else if ( process.wakeupRun === false ) {
updateSessionRules();
}
}
// Permissions may have been removed while the extension was disabled
const permissionsChanged = await onPermissionsRemoved();
// Unsure whether the browser remembers correctly registered css/scripts
// after we quit the browser. For now uBOL will check unconditionally at
// launch time whether content css/scripts are properly registered.
if ( process.wakeupRun === false || permissionsChanged ) {
registerInjectables();
const enabledRulesets = await dnr.getEnabledRulesets();
ubolLog(`Enabled rulesets: ${enabledRulesets}`);
dnr.getAvailableStaticRuleCount().then(count => {
ubolLog(`Available static rule count: ${count}`);
});
}
// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/declarativeNetRequest
// Firefox API does not support `dnr.setExtensionActionOptions`
if ( process.wakeupRun === false && canShowBlockedCount ) {
dnr.setExtensionActionOptions({
displayActionCountAsBadgeText: rulesetConfig.showBlockedCount,
});
}
runtime.onMessage.addListener(onMessage);
browser.permissions.onRemoved.addListener(
( ) => { onPermissionsRemoved(); }
);
if ( process.firstRun ) {
const enableOptimal = await hasOmnipotence();
if ( enableOptimal ) {
const afterLevel = await setDefaultFilteringMode(MODE_OPTIMAL);
if ( afterLevel === MODE_OPTIMAL ) {
updateDynamicRules();
registerInjectables();
process.firstRun = false;
}
} else {
const disableFirstRunPage = await adminRead('disableFirstRunPage');
if ( disableFirstRunPage !== true ) {
runtime.openOptionsPage();
} else {
process.firstRun = false;
}
}
}
toggleDeveloperMode(rulesetConfig.developerMode);
// Required to ensure the up to date property is available when needed
if ( process.wakeupRun === false ) {
adminReadEx('disabledFeatures');
}
}
// https://github.com/uBlockOrigin/uBOL-home/issues/199
// Force a restart of the extension once when an "internal error" occurs
start().then(( ) => {
localRemove('goodStart');
return false;
}).catch(reason => {
console.trace(reason);
if ( process.wakeupRun ) { return; }
return localRead('goodStart').then(goodStart => {
if ( goodStart === false ) {
localRemove('goodStart');
return false;
}
return localWrite('goodStart', false).then(( ) => true);
});
}).then(restart => {
if ( restart !== true ) { return; }
runtime.reload();
});