summaryrefslogtreecommitdiffstats
path: root/src/js/messaging.js
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-17 05:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-17 05:47:55 +0000
commit31d6ff6f931696850c348007241195ab3b2eddc7 (patch)
tree615cb1c57ce9f6611bad93326b9105098f379609 /src/js/messaging.js
parentInitial commit. (diff)
downloadublock-origin-31d6ff6f931696850c348007241195ab3b2eddc7.tar.xz
ublock-origin-31d6ff6f931696850c348007241195ab3b2eddc7.zip
Adding upstream version 1.55.0+dfsg.upstream/1.55.0+dfsg
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/js/messaging.js')
-rw-r--r--src/js/messaging.js2195
1 files changed, 2195 insertions, 0 deletions
diff --git a/src/js/messaging.js b/src/js/messaging.js
new file mode 100644
index 0000000..52242b3
--- /dev/null
+++ b/src/js/messaging.js
@@ -0,0 +1,2195 @@
+/*******************************************************************************
+
+ uBlock Origin - a comprehensive, efficient content blocker
+ Copyright (C) 2014-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
+*/
+
+/* globals browser */
+
+'use strict';
+
+/******************************************************************************/
+
+import publicSuffixList from '../lib/publicsuffixlist/publicsuffixlist.js';
+import punycode from '../lib/punycode.js';
+
+import { filteringBehaviorChanged } from './broadcast.js';
+import cacheStorage from './cachestorage.js';
+import cosmeticFilteringEngine from './cosmetic-filtering.js';
+import htmlFilteringEngine from './html-filtering.js';
+import logger from './logger.js';
+import lz4Codec from './lz4.js';
+import io from './assets.js';
+import scriptletFilteringEngine from './scriptlet-filtering.js';
+import staticFilteringReverseLookup from './reverselookup.js';
+import staticNetFilteringEngine from './static-net-filtering.js';
+import µb from './background.js';
+import webRequest from './traffic.js';
+import { denseBase64 } from './base64-custom.js';
+import { dnrRulesetFromRawLists } from './static-dnr-filtering.js';
+import { i18n$ } from './i18n.js';
+import { redirectEngine } from './redirect-engine.js';
+import * as sfp from './static-filtering-parser.js';
+
+import {
+ permanentFirewall,
+ sessionFirewall,
+ permanentSwitches,
+ sessionSwitches,
+ permanentURLFiltering,
+ sessionURLFiltering,
+} from './filtering-engines.js';
+
+import {
+ domainFromHostname,
+ domainFromURI,
+ entityFromDomain,
+ hostnameFromURI,
+ isNetworkURI,
+} from './uri-utils.js';
+
+import './benchmarks.js';
+
+/******************************************************************************/
+
+// https://github.com/uBlockOrigin/uBlock-issues/issues/710
+// Listeners have a name and a "privileged" status.
+// The nameless default handler is always deemed "privileged".
+// Messages from privileged ports must never relayed to listeners
+// which are not privileged.
+
+/******************************************************************************/
+/******************************************************************************/
+
+// Default handler
+// privileged
+
+{
+// >>>>> start of local scope
+
+const clickToLoad = function(request, sender) {
+ const { tabId, frameId } = sender;
+ if ( tabId === undefined || frameId === undefined ) { return false; }
+ const pageStore = µb.pageStoreFromTabId(tabId);
+ if ( pageStore === null ) { return false; }
+ pageStore.clickToLoad(frameId, request.frameURL);
+ return true;
+};
+
+const getDomainNames = function(targets) {
+ return targets.map(target => {
+ if ( typeof target !== 'string' ) { return ''; }
+ return target.indexOf('/') !== -1
+ ? domainFromURI(target) || ''
+ : domainFromHostname(target) || target;
+ });
+};
+
+const onMessage = function(request, sender, callback) {
+ // Async
+ switch ( request.what ) {
+ case 'getAssetContent':
+ // https://github.com/chrisaljoudi/uBlock/issues/417
+ io.get(request.url, {
+ dontCache: true,
+ needSourceURL: true,
+ }).then(result => {
+ result.trustedSource = µb.isTrustedList(result.assetKey);
+ callback(result);
+ });
+ return;
+
+ case 'listsFromNetFilter':
+ staticFilteringReverseLookup.fromNetFilter(
+ request.rawFilter
+ ).then(response => {
+ callback(response);
+ });
+ return;
+
+ case 'listsFromCosmeticFilter':
+ staticFilteringReverseLookup.fromExtendedFilter(
+ request
+ ).then(response => {
+ callback(response);
+ });
+ return;
+
+ case 'reloadAllFilters':
+ µb.loadFilterLists().then(( ) => { callback(); });
+ return;
+
+ case 'scriptlet':
+ vAPI.tabs.executeScript(request.tabId, {
+ file: `/js/scriptlets/${request.scriptlet}.js`
+ }).then(result => {
+ callback(result);
+ });
+ return;
+
+ default:
+ break;
+ }
+
+ // Sync
+ let response;
+
+ switch ( request.what ) {
+ case 'applyFilterListSelection':
+ response = µb.applyFilterListSelection(request);
+ break;
+
+ case 'clickToLoad':
+ response = clickToLoad(request, sender);
+ break;
+
+ case 'createUserFilter':
+ µb.createUserFilters(request);
+ break;
+
+ case 'getAppData':
+ response = {
+ name: browser.runtime.getManifest().name,
+ version: vAPI.app.version,
+ canBenchmark: µb.hiddenSettings.benchmarkDatasetURL !== 'unset',
+ };
+ break;
+
+ case 'getDomainNames':
+ response = getDomainNames(request.targets);
+ break;
+
+ case 'getTrustedScriptletTokens':
+ response = redirectEngine.getTrustedScriptletTokens();
+ break;
+
+ case 'getWhitelist':
+ response = {
+ whitelist: µb.arrayFromWhitelist(µb.netWhitelist),
+ whitelistDefault: µb.netWhitelistDefault,
+ reBadHostname: µb.reWhitelistBadHostname.source,
+ reHostnameExtractor: µb.reWhitelistHostnameExtractor.source
+ };
+ break;
+
+ case 'launchElementPicker':
+ // Launched from some auxiliary pages, clear context menu coords.
+ µb.epickerArgs.mouse = false;
+ µb.elementPickerExec(request.tabId, 0, request.targetURL, request.zap);
+ break;
+
+ case 'loggerDisabled':
+ µb.clearInMemoryFilters();
+ break;
+
+ case 'gotoURL':
+ µb.openNewTab(request.details);
+ break;
+
+ case 'readyToFilter':
+ response = µb.readyToFilter;
+ break;
+
+ // https://github.com/uBlockOrigin/uBlock-issues/issues/1954
+ // In case of document-blocked page, navigate to blocked URL instead
+ // of forcing a reload.
+ case 'reloadTab': {
+ if ( vAPI.isBehindTheSceneTabId(request.tabId) ) { break; }
+ const { tabId, bypassCache, url, select } = request;
+ vAPI.tabs.get(tabId).then(tab => {
+ if ( url && tab && url !== tab.url ) {
+ vAPI.tabs.replace(tabId, url);
+ } else {
+ vAPI.tabs.reload(tabId, bypassCache === true);
+ }
+ });
+ if ( select && vAPI.tabs.select ) {
+ vAPI.tabs.select(tabId);
+ }
+ break;
+ }
+ case 'setWhitelist':
+ µb.netWhitelist = µb.whitelistFromString(request.whitelist);
+ µb.saveWhitelist();
+ filteringBehaviorChanged();
+ break;
+
+ case 'toggleHostnameSwitch':
+ µb.toggleHostnameSwitch(request);
+ break;
+
+ case 'uiAccentStylesheet':
+ µb.uiAccentStylesheet = request.stylesheet;
+ break;
+
+ case 'uiStyles':
+ response = {
+ uiAccentCustom: µb.userSettings.uiAccentCustom,
+ uiAccentCustom0: µb.userSettings.uiAccentCustom0,
+ uiAccentStylesheet: µb.uiAccentStylesheet,
+ uiStyles: µb.hiddenSettings.uiStyles,
+ uiTheme: µb.userSettings.uiTheme,
+ };
+ break;
+
+ case 'userSettings':
+ response = µb.changeUserSettings(request.name, request.value);
+ if ( response instanceof Object ) {
+ if ( vAPI.net.canUncloakCnames !== true ) {
+ response.cnameUncloakEnabled = undefined;
+ }
+ response.canLeakLocalIPAddresses =
+ vAPI.browserSettings.canLeakLocalIPAddresses === true;
+ }
+ break;
+
+ default:
+ return vAPI.messaging.UNHANDLED;
+ }
+
+ callback(response);
+};
+
+vAPI.messaging.setup(onMessage);
+
+// <<<<< end of local scope
+}
+
+/******************************************************************************/
+/******************************************************************************/
+
+// Channel:
+// popupPanel
+// privileged
+
+{
+// >>>>> start of local scope
+
+const createCounts = ( ) => {
+ return {
+ blocked: { any: 0, frame: 0, script: 0 },
+ allowed: { any: 0, frame: 0, script: 0 },
+ };
+};
+
+const getHostnameDict = function(hostnameDetailsMap, out) {
+ const hnDict = Object.create(null);
+ const cnMap = [];
+
+ const createDictEntry = (domain, hostname, details) => {
+ const cname = vAPI.net.canonicalNameFromHostname(hostname);
+ if ( cname !== undefined ) {
+ cnMap.push([ cname, hostname ]);
+ }
+ hnDict[hostname] = { domain, counts: details.counts };
+ };
+
+ for ( const hnDetails of hostnameDetailsMap.values() ) {
+ const hostname = hnDetails.hostname;
+ if ( hnDict[hostname] !== undefined ) { continue; }
+ const domain = domainFromHostname(hostname) || hostname;
+ const dnDetails =
+ hostnameDetailsMap.get(domain) || { counts: createCounts() };
+ if ( hnDict[domain] === undefined ) {
+ createDictEntry(domain, domain, dnDetails);
+ }
+ if ( hostname === domain ) { continue; }
+ createDictEntry(domain, hostname, hnDetails);
+ }
+
+ out.hostnameDict = hnDict;
+ out.cnameMap = cnMap;
+};
+
+const firewallRuleTypes = [
+ '*',
+ 'image',
+ '3p',
+ 'inline-script',
+ '1p-script',
+ '3p-script',
+ '3p-frame',
+];
+
+const getFirewallRules = function(src, out) {
+ const ruleset = out.firewallRules = {};
+ const df = sessionFirewall;
+
+ for ( const type of firewallRuleTypes ) {
+ const r = df.lookupRuleData('*', '*', type);
+ if ( r === undefined ) { continue; }
+ ruleset[`/ * ${type}`] = r;
+ }
+ if ( typeof src !== 'string' ) { return; }
+
+ for ( const type of firewallRuleTypes ) {
+ const r = df.lookupRuleData(src, '*', type);
+ if ( r === undefined ) { continue; }
+ ruleset[`. * ${type}`] = r;
+ }
+
+ const { hostnameDict } = out;
+ for ( const des in hostnameDict ) {
+ let r = df.lookupRuleData('*', des, '*');
+ if ( r !== undefined ) { ruleset[`/ ${des} *`] = r; }
+ r = df.lookupRuleData(src, des, '*');
+ if ( r !== undefined ) { ruleset[`. ${des} *`] = r; }
+ }
+};
+
+const popupDataFromTabId = function(tabId, tabTitle) {
+ const tabContext = µb.tabContextManager.mustLookup(tabId);
+ const rootHostname = tabContext.rootHostname;
+ const µbus = µb.userSettings;
+ const µbhs = µb.hiddenSettings;
+ const r = {
+ advancedUserEnabled: µbus.advancedUserEnabled,
+ appName: vAPI.app.name,
+ appVersion: vAPI.app.version,
+ colorBlindFriendly: µbus.colorBlindFriendly,
+ cosmeticFilteringSwitch: false,
+ firewallPaneMinimized: µbus.firewallPaneMinimized,
+ globalAllowedRequestCount: µb.localSettings.allowedRequestCount,
+ globalBlockedRequestCount: µb.localSettings.blockedRequestCount,
+ fontSize: µbhs.popupFontSize,
+ godMode: µbhs.filterAuthorMode,
+ netFilteringSwitch: false,
+ rawURL: tabContext.rawURL,
+ pageURL: tabContext.normalURL,
+ pageHostname: rootHostname,
+ pageDomain: tabContext.rootDomain,
+ popupBlockedCount: 0,
+ popupPanelSections: µbus.popupPanelSections,
+ popupPanelDisabledSections: µbhs.popupPanelDisabledSections,
+ popupPanelLockedSections: µbhs.popupPanelLockedSections,
+ popupPanelHeightMode: µbhs.popupPanelHeightMode,
+ tabId,
+ tabTitle,
+ tooltipsDisabled: µbus.tooltipsDisabled,
+ hasUnprocessedRequest: vAPI.net && vAPI.net.hasUnprocessedRequest(tabId),
+ };
+
+ if ( µbhs.uiPopupConfig !== 'unset' ) {
+ r.uiPopupConfig = µbhs.uiPopupConfig;
+ }
+
+ const pageStore = µb.pageStoreFromTabId(tabId);
+ if ( pageStore ) {
+ r.pageCounts = pageStore.counts;
+ r.netFilteringSwitch = pageStore.getNetFilteringSwitch();
+ getHostnameDict(pageStore.getAllHostnameDetails(), r);
+ r.contentLastModified = pageStore.contentLastModified;
+ getFirewallRules(rootHostname, r);
+ r.canElementPicker = isNetworkURI(r.rawURL);
+ r.noPopups = sessionSwitches.evaluateZ(
+ 'no-popups',
+ rootHostname
+ );
+ r.popupBlockedCount = pageStore.popupBlockedCount;
+ r.noCosmeticFiltering = sessionSwitches.evaluateZ(
+ 'no-cosmetic-filtering',
+ rootHostname
+ );
+ r.noLargeMedia = sessionSwitches.evaluateZ(
+ 'no-large-media',
+ rootHostname
+ );
+ r.largeMediaCount = pageStore.largeMediaCount;
+ r.noRemoteFonts = sessionSwitches.evaluateZ(
+ 'no-remote-fonts',
+ rootHostname
+ );
+ r.remoteFontCount = pageStore.remoteFontCount;
+ r.noScripting = sessionSwitches.evaluateZ(
+ 'no-scripting',
+ rootHostname
+ );
+ } else {
+ r.hostnameDict = {};
+ getFirewallRules(undefined, r);
+ }
+
+ r.matrixIsDirty = sessionFirewall.hasSameRules(
+ permanentFirewall,
+ rootHostname,
+ r.hostnameDict
+ ) === false;
+ if ( r.matrixIsDirty === false ) {
+ r.matrixIsDirty = sessionSwitches.hasSameRules(
+ permanentSwitches,
+ rootHostname
+ ) === false;
+ }
+ return r;
+};
+
+const popupDataFromRequest = async function(request) {
+ if ( request.tabId ) {
+ return popupDataFromTabId(request.tabId, '');
+ }
+
+ // Still no target tab id? Use currently selected tab.
+ const tab = await vAPI.tabs.getCurrent();
+ let tabId = '';
+ let tabTitle = '';
+ if ( tab instanceof Object ) {
+ tabId = tab.id;
+ tabTitle = tab.title || '';
+ }
+ return popupDataFromTabId(tabId, tabTitle);
+};
+
+const getElementCount = async function(tabId, what) {
+ const results = await vAPI.tabs.executeScript(tabId, {
+ allFrames: true,
+ file: `/js/scriptlets/dom-survey-${what}.js`,
+ runAt: 'document_end',
+ });
+
+ let total = 0;
+ for ( const count of results ) {
+ if ( typeof count !== 'number' ) { continue; }
+ if ( count === -1 ) { return -1; }
+ total += count;
+ }
+
+ return total;
+};
+
+const launchReporter = async function(request) {
+ const pageStore = µb.pageStoreFromTabId(request.tabId);
+ if ( pageStore === null ) { return; }
+ if ( pageStore.hasUnprocessedRequest ) {
+ request.popupPanel.hasUnprocessedRequest = true;
+ }
+
+ const entries = await io.getUpdateAges({
+ filters: µb.selectedFilterLists.slice()
+ });
+ const shouldUpdateLists = [];
+ for ( const entry of entries ) {
+ if ( entry.age < (2 * 60 * 60 * 1000) ) { continue; }
+ shouldUpdateLists.push(entry.assetKey);
+ }
+
+ // https://github.com/gorhill/uBlock/commit/6efd8eb#commitcomment-107523558
+ // Important: for whatever reason, not using `document_start` causes the
+ // Promise returned by `tabs.executeScript()` to resolve only when the
+ // associated tab is closed.
+ const cosmeticSurveyResults = await vAPI.tabs.executeScript(request.tabId, {
+ allFrames: true,
+ file: '/js/scriptlets/cosmetic-report.js',
+ matchAboutBlank: true,
+ runAt: 'document_start',
+ });
+
+ const filters = cosmeticSurveyResults.reduce((a, v) => {
+ if ( Array.isArray(v) ) { a.push(...v); }
+ return a;
+ }, []);
+ // Remove duplicate, truncate too long filters.
+ if ( filters.length !== 0 ) {
+ request.popupPanel.extended = Array.from(
+ new Set(filters.map(s => s.length <= 64 ? s : `${s.slice(0, 64)}…`))
+ );
+ }
+
+ const supportURL = new URL(vAPI.getURL('support.html'));
+ supportURL.searchParams.set('pageURL', request.pageURL);
+ supportURL.searchParams.set('popupPanel', JSON.stringify(request.popupPanel));
+ if ( shouldUpdateLists.length ) {
+ supportURL.searchParams.set('shouldUpdateLists', JSON.stringify(shouldUpdateLists));
+ }
+ return supportURL.href;
+};
+
+const onMessage = function(request, sender, callback) {
+ // Async
+ switch ( request.what ) {
+ case 'getHiddenElementCount':
+ getElementCount(request.tabId, 'elements').then(count => {
+ callback(count);
+ });
+ return;
+
+ case 'getScriptCount':
+ getElementCount(request.tabId, 'scripts').then(count => {
+ callback(count);
+ });
+ return;
+
+ case 'getPopupData':
+ popupDataFromRequest(request).then(popupData => {
+ callback(popupData);
+ });
+ return;
+
+ default:
+ break;
+ }
+
+ // Sync
+ let response;
+
+ switch ( request.what ) {
+ case 'dismissUnprocessedRequest':
+ vAPI.net.removeUnprocessedRequest(request.tabId);
+ µb.updateToolbarIcon(request.tabId, 0b110);
+ break;
+
+ case 'hasPopupContentChanged': {
+ const pageStore = µb.pageStoreFromTabId(request.tabId);
+ const lastModified = pageStore ? pageStore.contentLastModified : 0;
+ response = lastModified !== request.contentLastModified;
+ break;
+ }
+
+ case 'launchReporter': {
+ launchReporter(request).then(url => {
+ if ( typeof url !== 'string' ) { return; }
+ µb.openNewTab({ url, select: true, index: -1 });
+ });
+ break;
+ }
+
+ case 'revertFirewallRules':
+ // TODO: use Set() to message around sets of hostnames
+ sessionFirewall.copyRules(
+ permanentFirewall,
+ request.srcHostname,
+ Object.assign(Object.create(null), request.desHostnames)
+ );
+ sessionSwitches.copyRules(
+ permanentSwitches,
+ request.srcHostname
+ );
+ // https://github.com/gorhill/uBlock/issues/188
+ cosmeticFilteringEngine.removeFromSelectorCache(
+ request.srcHostname,
+ 'net'
+ );
+ µb.updateToolbarIcon(request.tabId, 0b100);
+ response = popupDataFromTabId(request.tabId);
+ break;
+
+ case 'saveFirewallRules':
+ // TODO: use Set() to message around sets of hostnames
+ if (
+ permanentFirewall.copyRules(
+ sessionFirewall,
+ request.srcHostname,
+ Object.assign(Object.create(null), request.desHostnames)
+ )
+ ) {
+ µb.savePermanentFirewallRules();
+ }
+ if (
+ permanentSwitches.copyRules(
+ sessionSwitches,
+ request.srcHostname
+ )
+ ) {
+ µb.saveHostnameSwitches();
+ }
+ break;
+
+ case 'toggleHostnameSwitch':
+ µb.toggleHostnameSwitch(request);
+ response = popupDataFromTabId(request.tabId);
+ break;
+
+ case 'toggleFirewallRule':
+ µb.toggleFirewallRule(request);
+ response = popupDataFromTabId(request.tabId);
+ break;
+
+ case 'toggleNetFiltering': {
+ const pageStore = µb.pageStoreFromTabId(request.tabId);
+ if ( pageStore ) {
+ pageStore.toggleNetFilteringSwitch(
+ request.url,
+ request.scope,
+ request.state
+ );
+ µb.updateToolbarIcon(request.tabId, 0b111);
+ }
+ break;
+ }
+ default:
+ return vAPI.messaging.UNHANDLED;
+ }
+
+ callback(response);
+};
+
+vAPI.messaging.listen({
+ name: 'popupPanel',
+ listener: onMessage,
+ privileged: true,
+});
+
+// <<<<< end of local scope
+}
+
+/******************************************************************************/
+/******************************************************************************/
+
+// Channel:
+// contentscript
+// unprivileged
+
+{
+// >>>>> start of local scope
+
+const retrieveContentScriptParameters = async function(sender, request) {
+ if ( µb.readyToFilter !== true ) { return; }
+ const { tabId, frameId } = sender;
+ if ( tabId === undefined || frameId === undefined ) { return; }
+
+ const pageStore = µb.pageStoreFromTabId(tabId);
+ if ( pageStore === null || pageStore.getNetFilteringSwitch() === false ) {
+ return;
+ }
+
+ // A content script may not always be able to successfully look up the
+ // effective context, hence in such case we try again to look up here
+ // using cached information about embedded frames.
+ if ( frameId !== 0 && request.url.startsWith('about:') ) {
+ request.url = pageStore.getEffectiveFrameURL(sender);
+ }
+
+ const noSpecificCosmeticFiltering =
+ pageStore.shouldApplySpecificCosmeticFilters(frameId) === false;
+ const noGenericCosmeticFiltering =
+ pageStore.shouldApplyGenericCosmeticFilters(frameId) === false;
+
+ const response = {
+ collapseBlocked: µb.userSettings.collapseBlocked,
+ noGenericCosmeticFiltering,
+ noSpecificCosmeticFiltering,
+ };
+
+ request.tabId = tabId;
+ request.frameId = frameId;
+ request.hostname = hostnameFromURI(request.url);
+ request.domain = domainFromHostname(request.hostname);
+ request.entity = entityFromDomain(request.domain);
+
+ const scf = response.specificCosmeticFilters =
+ cosmeticFilteringEngine.retrieveSpecificSelectors(request, response);
+
+ // The procedural filterer's code is loaded only when needed and must be
+ // present before returning response to caller.
+ if (
+ scf.proceduralFilters.length !== 0 || (
+ logger.enabled && (
+ scf.convertedProceduralFilters.length !== 0 ||
+ scf.exceptedFilters.length !== 0
+ )
+ )
+ ) {
+ await vAPI.tabs.executeScript(tabId, {
+ allFrames: false,
+ file: '/js/contentscript-extra.js',
+ frameId,
+ matchAboutBlank: true,
+ runAt: 'document_start',
+ });
+ }
+
+ // https://github.com/uBlockOrigin/uBlock-issues/issues/688#issuecomment-748179731
+ // For non-network URIs, scriptlet injection is deferred to here. The
+ // effective URL is available here in `request.url`.
+ if ( logger.enabled || request.needScriptlets ) {
+ const scriptletDetails = scriptletFilteringEngine.injectNow(request);
+ if ( scriptletDetails !== undefined ) {
+ scriptletFilteringEngine.toLogger(request, scriptletDetails);
+ if ( request.needScriptlets ) {
+ response.scriptletDetails = scriptletDetails;
+ }
+ }
+ }
+
+ // https://github.com/NanoMeow/QuickReports/issues/6#issuecomment-414516623
+ // Inject as early as possible to make the cosmetic logger code less
+ // sensitive to the removal of DOM nodes which may match injected
+ // cosmetic filters.
+ if ( logger.enabled ) {
+ if (
+ noSpecificCosmeticFiltering === false ||
+ noGenericCosmeticFiltering === false
+ ) {
+ vAPI.tabs.executeScript(tabId, {
+ allFrames: false,
+ file: '/js/scriptlets/cosmetic-logger.js',
+ frameId,
+ matchAboutBlank: true,
+ runAt: 'document_start',
+ });
+ }
+ }
+
+ return response;
+};
+
+const onMessage = function(request, sender, callback) {
+ // Async
+ switch ( request.what ) {
+ case 'retrieveContentScriptParameters':
+ return retrieveContentScriptParameters(
+ sender,
+ request
+ ).then(response => {
+ callback(response);
+ });
+ default:
+ break;
+ }
+
+ const pageStore = µb.pageStoreFromTabId(sender.tabId);
+
+ // Sync
+ let response;
+
+ switch ( request.what ) {
+ case 'cosmeticFiltersInjected':
+ cosmeticFilteringEngine.addToSelectorCache(request);
+ break;
+
+ case 'disableGenericCosmeticFilteringSurveyor':
+ cosmeticFilteringEngine.disableSurveyor(request);
+ break;
+
+ case 'getCollapsibleBlockedRequests':
+ response = {
+ id: request.id,
+ hash: request.hash,
+ netSelectorCacheCountMax:
+ cosmeticFilteringEngine.netSelectorCacheCountMax,
+ };
+ if (
+ µb.userSettings.collapseBlocked &&
+ pageStore && pageStore.getNetFilteringSwitch()
+ ) {
+ pageStore.getBlockedResources(request, response);
+ }
+ break;
+
+ case 'maybeGoodPopup':
+ µb.maybeGoodPopup.tabId = sender.tabId;
+ µb.maybeGoodPopup.url = request.url;
+ break;
+
+ case 'shouldRenderNoscriptTags':
+ if ( pageStore === null ) { break; }
+ const fctxt = µb.filteringContext.fromTabId(sender.tabId);
+ if ( pageStore.filterScripting(fctxt, undefined) ) {
+ vAPI.tabs.executeScript(sender.tabId, {
+ file: '/js/scriptlets/noscript-spoof.js',
+ frameId: sender.frameId,
+ runAt: 'document_end',
+ });
+ }
+ break;
+
+ case 'retrieveGenericCosmeticSelectors':
+ request.tabId = sender.tabId;
+ request.frameId = sender.frameId;
+ response = {
+ result: cosmeticFilteringEngine.retrieveGenericSelectors(request),
+ };
+ break;
+
+ default:
+ return vAPI.messaging.UNHANDLED;
+ }
+
+ callback(response);
+};
+
+vAPI.messaging.listen({
+ name: 'contentscript',
+ listener: onMessage,
+});
+
+// <<<<< end of local scope
+}
+
+/******************************************************************************/
+/******************************************************************************/
+
+// Channel:
+// elementPicker
+// unprivileged
+
+{
+// >>>>> start of local scope
+
+const onMessage = function(request, sender, callback) {
+ // Async
+ switch ( request.what ) {
+ // The procedural filterer must be present in case the user wants to
+ // type-in custom filters.
+ case 'elementPickerArguments':
+ return vAPI.tabs.executeScript(sender.tabId, {
+ allFrames: false,
+ file: '/js/contentscript-extra.js',
+ frameId: sender.frameId,
+ matchAboutBlank: true,
+ runAt: 'document_start',
+ }).then(( ) => {
+ callback({
+ target: µb.epickerArgs.target,
+ mouse: µb.epickerArgs.mouse,
+ zap: µb.epickerArgs.zap,
+ eprom: µb.epickerArgs.eprom,
+ pickerURL: vAPI.getURL(
+ `/web_accessible_resources/epicker-ui.html?secret=${vAPI.warSecret.short()}`
+ ),
+ });
+ µb.epickerArgs.target = '';
+ });
+ default:
+ break;
+ }
+
+ // Sync
+ let response;
+
+ switch ( request.what ) {
+ case 'elementPickerEprom':
+ µb.epickerArgs.eprom = request;
+ break;
+
+ default:
+ return vAPI.messaging.UNHANDLED;
+ }
+
+ callback(response);
+};
+
+vAPI.messaging.listen({
+ name: 'elementPicker',
+ listener: onMessage,
+});
+
+// <<<<< end of local scope
+}
+
+/******************************************************************************/
+/******************************************************************************/
+
+// Channel:
+// cloudWidget
+// privileged
+
+{
+// >>>>> start of local scope
+
+const fromBase64 = function(encoded) {
+ if ( typeof encoded !== 'string' ) {
+ return Promise.resolve(encoded);
+ }
+ let u8array;
+ try {
+ u8array = denseBase64.decode(encoded);
+ } catch(ex) {
+ }
+ return Promise.resolve(u8array !== undefined ? u8array : encoded);
+};
+
+const toBase64 = function(data) {
+ const value = data instanceof Uint8Array
+ ? denseBase64.encode(data)
+ : data;
+ return Promise.resolve(value);
+};
+
+const compress = function(json) {
+ return lz4Codec.encode(json, toBase64);
+};
+
+const decompress = function(encoded) {
+ return lz4Codec.decode(encoded, fromBase64);
+};
+
+const onMessage = function(request, sender, callback) {
+ // Cloud storage support is optional.
+ if ( µb.cloudStorageSupported !== true ) {
+ callback();
+ return;
+ }
+
+ // Async
+ switch ( request.what ) {
+ case 'cloudGetOptions':
+ vAPI.cloud.getOptions(function(options) {
+ options.enabled = µb.userSettings.cloudStorageEnabled === true;
+ callback(options);
+ });
+ return;
+
+ case 'cloudSetOptions':
+ vAPI.cloud.setOptions(request.options, callback);
+ return;
+
+ case 'cloudPull':
+ request.decode = decompress;
+ return vAPI.cloud.pull(request).then(result => {
+ callback(result);
+ });
+
+ case 'cloudPush':
+ if ( µb.hiddenSettings.cloudStorageCompression ) {
+ request.encode = compress;
+ }
+ return vAPI.cloud.push(request).then(result => {
+ callback(result);
+ });
+
+ case 'cloudUsed':
+ return vAPI.cloud.used(request.datakey).then(result => {
+ callback(result);
+ });
+
+ default:
+ break;
+ }
+
+ // Sync
+ let response;
+
+ switch ( request.what ) {
+ // For when cloud storage is disabled.
+ case 'cloudPull':
+ // fallthrough
+ case 'cloudPush':
+ break;
+
+ default:
+ return vAPI.messaging.UNHANDLED;
+ }
+
+ callback(response);
+};
+
+vAPI.messaging.listen({
+ name: 'cloudWidget',
+ listener: onMessage,
+ privileged: true,
+});
+
+// <<<<< end of local scope
+}
+
+/******************************************************************************/
+/******************************************************************************/
+
+// Channel:
+// dashboard
+// privileged
+
+{
+// >>>>> start of local scope
+
+// Settings
+const getLocalData = async function() {
+ const data = Object.assign({}, µb.restoreBackupSettings);
+ data.storageUsed = await µb.getBytesInUse();
+ data.cloudStorageSupported = µb.cloudStorageSupported;
+ data.privacySettingsSupported = µb.privacySettingsSupported;
+ return data;
+};
+
+const backupUserData = async function() {
+ const userFilters = await µb.loadUserFilters();
+
+ const userData = {
+ timeStamp: Date.now(),
+ version: vAPI.app.version,
+ userSettings:
+ µb.getModifiedSettings(µb.userSettings, µb.userSettingsDefault),
+ selectedFilterLists: µb.selectedFilterLists,
+ hiddenSettings:
+ µb.getModifiedSettings(µb.hiddenSettings, µb.hiddenSettingsDefault),
+ whitelist: µb.arrayFromWhitelist(µb.netWhitelist),
+ dynamicFilteringString: permanentFirewall.toString(),
+ urlFilteringString: permanentURLFiltering.toString(),
+ hostnameSwitchesString: permanentSwitches.toString(),
+ userFilters: userFilters.content,
+ };
+
+ const filename = i18n$('aboutBackupFilename')
+ .replace('{{datetime}}', µb.dateNowToSensibleString())
+ .replace(/ +/g, '_');
+ µb.restoreBackupSettings.lastBackupFile = filename;
+ µb.restoreBackupSettings.lastBackupTime = Date.now();
+ vAPI.storage.set(µb.restoreBackupSettings);
+
+ const localData = await getLocalData();
+
+ return { localData, userData };
+};
+
+const restoreUserData = async function(request) {
+ const userData = request.userData;
+
+ // https://github.com/LiCybora/NanoDefenderFirefox/issues/196
+ // Backup data could be from Chromium platform or from an older
+ // Firefox version.
+ if (
+ vAPI.webextFlavor.soup.has('firefox') &&
+ vAPI.app.intFromVersion(userData.version) <= 1031003011
+ ) {
+ userData.hostnameSwitchesString += '\nno-csp-reports: * true';
+ }
+
+ // List of external lists is meant to be a string.
+ if ( Array.isArray(userData.externalLists) ) {
+ userData.externalLists = userData.externalLists.join('\n');
+ }
+
+ // https://github.com/chrisaljoudi/uBlock/issues/1102
+ // Ensure all currently cached assets are flushed from storage AND memory.
+ io.rmrf();
+
+ // If we are going to restore all, might as well wipe out clean local
+ // storages
+ await Promise.all([
+ cacheStorage.clear(),
+ vAPI.storage.clear(),
+ ]);
+
+ // Restore block stats
+ µb.saveLocalSettings();
+
+ // Restore user data
+ vAPI.storage.set(userData.userSettings);
+
+ // Restore advanced settings.
+ let hiddenSettings = userData.hiddenSettings;
+ if ( hiddenSettings instanceof Object === false ) {
+ hiddenSettings = µb.hiddenSettingsFromString(
+ userData.hiddenSettingsString || ''
+ );
+ }
+ // Discard unknown setting or setting with default value.
+ for ( const key in hiddenSettings ) {
+ if (
+ µb.hiddenSettingsDefault.hasOwnProperty(key) === false ||
+ hiddenSettings[key] === µb.hiddenSettingsDefault[key]
+ ) {
+ delete hiddenSettings[key];
+ }
+ }
+
+ // Whitelist directives can be represented as an array or as a
+ // (eventually to be deprecated) string.
+ let whitelist = userData.whitelist;
+ if (
+ Array.isArray(whitelist) === false &&
+ typeof userData.netWhitelist === 'string' &&
+ userData.netWhitelist !== ''
+ ) {
+ whitelist = userData.netWhitelist.split('\n');
+ }
+ vAPI.storage.set({
+ hiddenSettings,
+ netWhitelist: whitelist || [],
+ dynamicFilteringString: userData.dynamicFilteringString || '',
+ urlFilteringString: userData.urlFilteringString || '',
+ hostnameSwitchesString: userData.hostnameSwitchesString || '',
+ lastRestoreFile: request.file || '',
+ lastRestoreTime: Date.now(),
+ lastBackupFile: '',
+ lastBackupTime: 0
+ });
+ µb.saveUserFilters(userData.userFilters);
+ if ( Array.isArray(userData.selectedFilterLists) ) {
+ await µb.saveSelectedFilterLists(userData.selectedFilterLists);
+ }
+
+ vAPI.app.restart();
+};
+
+// Remove all stored data but keep global counts, people can become
+// quite attached to numbers
+const resetUserData = async function() {
+ await Promise.all([
+ cacheStorage.clear(),
+ vAPI.storage.clear(),
+ ]);
+
+ await µb.saveLocalSettings();
+
+ vAPI.app.restart();
+};
+
+// Filter lists
+const prepListEntries = function(entries) {
+ for ( const k in entries ) {
+ if ( entries.hasOwnProperty(k) === false ) { continue; }
+ const entry = entries[k];
+ if ( typeof entry.supportURL === 'string' && entry.supportURL !== '' ) {
+ entry.supportName = hostnameFromURI(entry.supportURL);
+ } else if ( typeof entry.homeURL === 'string' && entry.homeURL !== '' ) {
+ const hn = hostnameFromURI(entry.homeURL);
+ entry.supportURL = `http://${hn}/`;
+ entry.supportName = domainFromHostname(hn);
+ }
+ }
+};
+
+const getLists = async function(callback) {
+ const r = {
+ autoUpdate: µb.userSettings.autoUpdate,
+ available: null,
+ cache: null,
+ cosmeticFilterCount: cosmeticFilteringEngine.getFilterCount(),
+ current: µb.availableFilterLists,
+ ignoreGenericCosmeticFilters: µb.userSettings.ignoreGenericCosmeticFilters,
+ isUpdating: io.isUpdating(),
+ netFilterCount: staticNetFilteringEngine.getFilterCount(),
+ parseCosmeticFilters: µb.userSettings.parseAllABPHideFilters,
+ suspendUntilListsAreLoaded: µb.userSettings.suspendUntilListsAreLoaded,
+ userFiltersPath: µb.userFiltersPath
+ };
+ const [ lists, metadata ] = await Promise.all([
+ µb.getAvailableLists(),
+ io.metadata(),
+ ]);
+ r.available = lists;
+ prepListEntries(r.available);
+ r.cache = metadata;
+ prepListEntries(r.cache);
+ callback(r);
+};
+
+// My filters
+
+// TODO: also return origin of embedded frames?
+const getOriginHints = function() {
+ const out = new Set();
+ for ( const tabId of µb.pageStores.keys() ) {
+ if ( tabId === -1 ) { continue; }
+ const tabContext = µb.tabContextManager.lookup(tabId);
+ if ( tabContext === null ) { continue; }
+ let { rootDomain, rootHostname } = tabContext;
+ if ( rootDomain.endsWith('-scheme') ) { continue; }
+ const isPunycode = rootHostname.includes('xn--');
+ out.add(isPunycode ? punycode.toUnicode(rootDomain) : rootDomain);
+ if ( rootHostname === rootDomain ) { continue; }
+ out.add(isPunycode ? punycode.toUnicode(rootHostname) : rootHostname);
+ }
+ return Array.from(out);
+};
+
+// My rules
+const getRules = function() {
+ return {
+ permanentRules:
+ permanentFirewall.toArray().concat(
+ permanentSwitches.toArray(),
+ permanentURLFiltering.toArray()
+ ),
+ sessionRules:
+ sessionFirewall.toArray().concat(
+ sessionSwitches.toArray(),
+ sessionURLFiltering.toArray()
+ ),
+ pslSelfie: publicSuffixList.toSelfie(),
+ };
+};
+
+const modifyRuleset = function(details) {
+ let swRuleset, hnRuleset, urlRuleset;
+ if ( details.permanent ) {
+ swRuleset = permanentSwitches;
+ hnRuleset = permanentFirewall;
+ urlRuleset = permanentURLFiltering;
+ } else {
+ swRuleset = sessionSwitches;
+ hnRuleset = sessionFirewall;
+ urlRuleset = sessionURLFiltering;
+ }
+ let toRemove = new Set(details.toRemove.trim().split(/\s*[\n\r]+\s*/));
+ for ( let rule of toRemove ) {
+ if ( rule === '' ) { continue; }
+ let parts = rule.split(/\s+/);
+ if ( hnRuleset.removeFromRuleParts(parts) === false ) {
+ if ( swRuleset.removeFromRuleParts(parts) === false ) {
+ urlRuleset.removeFromRuleParts(parts);
+ }
+ }
+ }
+ let toAdd = new Set(details.toAdd.trim().split(/\s*[\n\r]+\s*/));
+ for ( let rule of toAdd ) {
+ if ( rule === '' ) { continue; }
+ let parts = rule.split(/\s+/);
+ if ( hnRuleset.addFromRuleParts(parts) === false ) {
+ if ( swRuleset.addFromRuleParts(parts) === false ) {
+ urlRuleset.addFromRuleParts(parts);
+ }
+ }
+ }
+ if ( details.permanent ) {
+ if ( swRuleset.changed ) {
+ µb.saveHostnameSwitches();
+ swRuleset.changed = false;
+ }
+ if ( hnRuleset.changed ) {
+ µb.savePermanentFirewallRules();
+ hnRuleset.changed = false;
+ }
+ if ( urlRuleset.changed ) {
+ µb.savePermanentURLFilteringRules();
+ urlRuleset.changed = false;
+ }
+ }
+};
+
+// Support
+const getSupportData = async function() {
+ const diffArrays = function(modified, original) {
+ const modifiedSet = new Set(modified);
+ const originalSet = new Set(original);
+ let added = [];
+ let removed = [];
+ for ( const item of modifiedSet ) {
+ if ( originalSet.has(item) ) { continue; }
+ added.push(item);
+ }
+ for ( const item of originalSet ) {
+ if ( modifiedSet.has(item) ) { continue; }
+ removed.push(item);
+ }
+ if ( added.length === 0 ) {
+ added = undefined;
+ }
+ if ( removed.length === 0 ) {
+ removed = undefined;
+ }
+ if ( added !== undefined || removed !== undefined ) {
+ return { added, removed };
+ }
+ };
+
+ const modifiedUserSettings = µb.getModifiedSettings(
+ µb.userSettings,
+ µb.userSettingsDefault
+ );
+
+ const modifiedHiddenSettings = µb.getModifiedSettings(
+ µb.hiddenSettings,
+ µb.hiddenSettingsDefault
+ );
+
+ let filterset = [];
+ const userFilters = await µb.loadUserFilters();
+ for ( const line of userFilters.content.split(/\s*\n+\s*/) ) {
+ if ( /^($|![^#])/.test(line) ) { continue; }
+ filterset.push(line);
+ }
+
+ const now = Date.now();
+
+ const formatDelayFromNow = list => {
+ const time = list.writeTime;
+ if ( typeof time !== 'number' || time === 0 ) { return 'never'; }
+ if ( (time || 0) === 0 ) { return '?'; }
+ const delayInSec = (now - time) / 1000;
+ const days = (delayInSec / 86400) | 0;
+ const hours = (delayInSec % 86400) / 3600 | 0;
+ const minutes = (delayInSec % 3600) / 60 | 0;
+ const parts = [];
+ if ( days > 0 ) { parts.push(`${days}d`); }
+ if ( hours > 0 ) { parts.push(`${hours}h`); }
+ if ( minutes > 0 ) { parts.push(`${minutes}m`); }
+ if ( parts.length === 0 ) { parts.push('now'); }
+ const out = parts.join('.');
+ if ( list.diffUpdated ) { return `${out} Δ`; }
+ return out;
+ };
+
+ const lists = µb.availableFilterLists;
+ let defaultListset = {};
+ let addedListset = {};
+ let removedListset = {};
+ for ( const listKey in lists ) {
+ if ( lists.hasOwnProperty(listKey) === false ) { continue; }
+ const list = lists[listKey];
+ if ( list.content !== 'filters' ) { continue; }
+ const used = µb.selectedFilterLists.includes(listKey);
+ const listDetails = [];
+ if ( used ) {
+ if ( typeof list.entryCount === 'number' ) {
+ listDetails.push(`${list.entryCount}-${list.entryCount-list.entryUsedCount}`);
+ }
+ listDetails.push(formatDelayFromNow(list));
+ }
+ if ( list.isDefault || listKey === µb.userFiltersPath ) {
+ if ( used ) {
+ defaultListset[listKey] = listDetails.join(', ');
+ } else {
+ removedListset[listKey] = null;
+ }
+ } else if ( used ) {
+ addedListset[listKey] = listDetails.join(', ');
+ }
+ }
+ if ( Object.keys(defaultListset).length === 0 ) {
+ defaultListset = undefined;
+ }
+ if ( Object.keys(addedListset).length === 0 ) {
+ addedListset = undefined;
+ } else {
+ const added = Object.keys(addedListset);
+ const truncated = added.slice(12);
+ for ( const key of truncated ) {
+ delete addedListset[key];
+ }
+ if ( truncated.length !== 0 ) {
+ addedListset[`[${truncated.length} lists not shown]`] = '[too many]';
+ }
+ }
+ if ( Object.keys(removedListset).length === 0 ) {
+ removedListset = undefined;
+ }
+
+ let browserFamily = (( ) => {
+ if ( vAPI.webextFlavor.soup.has('firefox') ) { return 'Firefox'; }
+ if ( vAPI.webextFlavor.soup.has('chromium') ) { return 'Chromium'; }
+ return 'Unknown';
+ })();
+ if ( vAPI.webextFlavor.soup.has('mobile') ) {
+ browserFamily += ' Mobile';
+ }
+
+ return {
+ [`${vAPI.app.name}`]: `${vAPI.app.version}`,
+ [`${browserFamily}`]: `${vAPI.webextFlavor.major}`,
+ 'filterset (summary)': {
+ network: staticNetFilteringEngine.getFilterCount(),
+ cosmetic: cosmeticFilteringEngine.getFilterCount(),
+ scriptlet: scriptletFilteringEngine.getFilterCount(),
+ html: htmlFilteringEngine.getFilterCount(),
+ },
+ 'listset (total-discarded, last-updated)': {
+ removed: removedListset,
+ added: addedListset,
+ default: defaultListset,
+ },
+ 'filterset (user)': filterset,
+ trustedset: diffArrays(
+ µb.arrayFromWhitelist(µb.netWhitelist),
+ µb.netWhitelistDefault
+ ),
+ switchRuleset: diffArrays(
+ sessionSwitches.toArray(),
+ µb.hostnameSwitchesDefault
+ ),
+ hostRuleset: diffArrays(
+ sessionFirewall.toArray(),
+ µb.dynamicFilteringDefault
+ ),
+ urlRuleset: diffArrays(
+ sessionURLFiltering.toArray(),
+ []
+ ),
+ 'userSettings': modifiedUserSettings,
+ 'hiddenSettings': modifiedHiddenSettings,
+ supportStats: µb.supportStats,
+ };
+};
+
+const onMessage = function(request, sender, callback) {
+ // Async
+ switch ( request.what ) {
+ case 'backupUserData':
+ return backupUserData().then(data => {
+ callback(data);
+ });
+
+ case 'getLists':
+ return µb.isReadyPromise.then(( ) => {
+ getLists(callback);
+ });
+
+ case 'getLocalData':
+ return getLocalData().then(localData => {
+ callback(localData);
+ });
+
+ case 'getSupportData': {
+ getSupportData().then(response => {
+ callback(response);
+ });
+ return;
+ }
+
+ case 'readUserFilters':
+ return µb.loadUserFilters().then(result => {
+ result.trustedSource = µb.isTrustedList(µb.userFiltersPath);
+ callback(result);
+ });
+
+ case 'writeUserFilters':
+ return µb.saveUserFilters(request.content).then(result => {
+ callback(result);
+ });
+
+ default:
+ break;
+ }
+
+ // Sync
+ let response;
+
+ switch ( request.what ) {
+ case 'dashboardConfig':
+ response = {
+ noDashboard: µb.noDashboard,
+ };
+ break;
+
+ case 'getAutoCompleteDetails':
+ response = {};
+ if ( (request.hintUpdateToken || 0) === 0 ) {
+ response.redirectResources = redirectEngine.getResourceDetails();
+ response.preparseDirectiveEnv = vAPI.webextFlavor.env.slice();
+ response.preparseDirectiveHints = sfp.utils.preparser.getHints();
+ }
+ if ( request.hintUpdateToken !== µb.pageStoresToken ) {
+ response.originHints = getOriginHints();
+ response.hintUpdateToken = µb.pageStoresToken;
+ }
+ break;
+
+ case 'getRules':
+ response = getRules();
+ break;
+
+ case 'modifyRuleset':
+ // https://github.com/chrisaljoudi/uBlock/issues/772
+ cosmeticFilteringEngine.removeFromSelectorCache('*');
+ modifyRuleset(request);
+ response = getRules();
+ break;
+
+ case 'supportUpdateNow': {
+ const { assetKeys } = request;
+ if ( assetKeys.length === 0 ) { return; }
+ for ( const assetKey of assetKeys ) {
+ io.purge(assetKey);
+ }
+ µb.scheduleAssetUpdater({ now: true, fetchDelay: 100 });
+ break;
+ }
+
+ case 'listsUpdateNow': {
+ const { assetKeys, preferOrigin = false } = request;
+ if ( assetKeys.length === 0 ) { return; }
+ for ( const assetKey of assetKeys ) {
+ io.purge(assetKey);
+ }
+ µb.scheduleAssetUpdater({ now: true, fetchDelay: 100, auto: preferOrigin !== true });
+ break;
+ }
+
+ case 'readHiddenSettings':
+ response = {
+ 'default': µb.hiddenSettingsDefault,
+ 'admin': µb.hiddenSettingsAdmin,
+ 'current': µb.hiddenSettings,
+ };
+ break;
+
+ case 'restoreUserData':
+ restoreUserData(request);
+ break;
+
+ case 'resetUserData':
+ resetUserData();
+ break;
+
+ case 'updateNow':
+ µb.scheduleAssetUpdater({ now: true, fetchDelay: 100, auto: true });
+ break;
+
+ case 'writeHiddenSettings':
+ µb.changeHiddenSettings(µb.hiddenSettingsFromString(request.content));
+ break;
+
+ default:
+ return vAPI.messaging.UNHANDLED;
+ }
+
+ callback(response);
+};
+
+vAPI.messaging.listen({
+ name: 'dashboard',
+ listener: onMessage,
+ privileged: true,
+});
+
+// <<<<< end of local scope
+}
+
+/******************************************************************************/
+/******************************************************************************/
+
+// Channel:
+// loggerUI
+// privileged
+
+{
+// >>>>> start of local scope
+
+const extensionOriginURL = vAPI.getURL('');
+const documentBlockedURL = vAPI.getURL('document-blocked.html');
+
+const getLoggerData = async function(details, activeTabId, callback) {
+ const response = {
+ activeTabId,
+ colorBlind: µb.userSettings.colorBlindFriendly,
+ entries: logger.readAll(details.ownerId),
+ tabIdsToken: µb.pageStoresToken,
+ tooltips: µb.userSettings.tooltipsDisabled === false
+ };
+ if ( µb.pageStoresToken !== details.tabIdsToken ) {
+ response.tabIds = [];
+ for ( const [ tabId, pageStore ] of µb.pageStores ) {
+ const { rawURL, title } = pageStore;
+ if ( rawURL.startsWith(extensionOriginURL) ) {
+ if ( rawURL.startsWith(documentBlockedURL) === false ) { continue; }
+ }
+ response.tabIds.push([ tabId, title ]);
+ }
+ }
+ if ( activeTabId ) {
+ const pageStore = µb.pageStoreFromTabId(activeTabId);
+ const rawURL = pageStore && pageStore.rawURL;
+ if (
+ rawURL === null ||
+ rawURL.startsWith(extensionOriginURL) &&
+ rawURL.startsWith(documentBlockedURL) === false
+ ) {
+ response.activeTabId = undefined;
+ }
+ }
+ if ( details.popupLoggerBoxChanged && vAPI.windows instanceof Object ) {
+ const tabs = await vAPI.tabs.query({
+ url: vAPI.getURL('/logger-ui.html?popup=1')
+ });
+ if ( tabs.length !== 0 ) {
+ const win = await vAPI.windows.get(tabs[0].windowId);
+ if ( win === null ) { return; }
+ vAPI.localStorage.setItem('popupLoggerBox', JSON.stringify({
+ left: win.left,
+ top: win.top,
+ width: win.width,
+ height: win.height,
+ }));
+ }
+ }
+ callback(response);
+};
+
+const getURLFilteringData = function(details) {
+ const colors = {};
+ const response = {
+ dirty: false,
+ colors: colors
+ };
+ const suf = sessionURLFiltering;
+ const puf = permanentURLFiltering;
+ const urls = details.urls;
+ const context = details.context;
+ const type = details.type;
+ for ( const url of urls ) {
+ const colorEntry = colors[url] = { r: 0, own: false };
+ if ( suf.evaluateZ(context, url, type).r !== 0 ) {
+ colorEntry.r = suf.r;
+ colorEntry.own = suf.r !== 0 &&
+ suf.context === context &&
+ suf.url === url &&
+ suf.type === type;
+ }
+ if ( response.dirty ) { continue; }
+ puf.evaluateZ(context, url, type);
+ const pown = (
+ puf.r !== 0 &&
+ puf.context === context &&
+ puf.url === url &&
+ puf.type === type
+ );
+ response.dirty = colorEntry.own !== pown || colorEntry.r !== puf.r;
+ }
+ return response;
+};
+
+const onMessage = function(request, sender, callback) {
+ // Async
+ switch ( request.what ) {
+ case 'readAll':
+ if ( logger.ownerId !== undefined && logger.ownerId !== request.ownerId ) {
+ return callback({ unavailable: true });
+ }
+ vAPI.tabs.getCurrent().then(tab => {
+ getLoggerData(request, tab && tab.id, callback);
+ });
+ return;
+
+ case 'toggleInMemoryFilter': {
+ const promise = µb.hasInMemoryFilter(request.filter)
+ ? µb.removeInMemoryFilter(request.filter)
+ : µb.addInMemoryFilter(request.filter);
+ promise.then(status => { callback(status); });
+ return;
+ }
+ default:
+ break;
+ }
+
+ // Sync
+ let response;
+
+ switch ( request.what ) {
+ case 'hasInMemoryFilter':
+ response = µb.hasInMemoryFilter(request.filter);
+ break;
+
+ case 'releaseView':
+ if ( request.ownerId !== logger.ownerId ) { break; }
+ logger.ownerId = undefined;
+ µb.clearInMemoryFilters();
+ break;
+
+ case 'saveURLFilteringRules':
+ response = permanentURLFiltering.copyRules(
+ sessionURLFiltering,
+ request.context,
+ request.urls,
+ request.type
+ );
+ if ( response ) {
+ µb.savePermanentURLFilteringRules();
+ }
+ break;
+
+ case 'setURLFilteringRule':
+ µb.toggleURLFilteringRule(request);
+ break;
+
+ case 'getURLFilteringData':
+ response = getURLFilteringData(request);
+ break;
+
+ default:
+ return vAPI.messaging.UNHANDLED;
+ }
+
+ callback(response);
+};
+
+vAPI.messaging.listen({
+ name: 'loggerUI',
+ listener: onMessage,
+ privileged: true,
+});
+
+// <<<<< end of local scope
+}
+
+/******************************************************************************/
+/******************************************************************************/
+
+// Channel:
+// domInspectorContent
+// unprivileged
+
+{
+// >>>>> start of local scope
+
+const onMessage = (request, sender, callback) => {
+ // Async
+ switch ( request.what ) {
+ default:
+ break;
+ }
+ // Sync
+ let response;
+ switch ( request.what ) {
+ case 'getInspectorArgs':
+ const bc = new globalThis.BroadcastChannel('contentInspectorChannel');
+ bc.postMessage({
+ what: 'contentInspectorChannel',
+ tabId: sender.tabId || 0,
+ frameId: sender.frameId || 0,
+ });
+ response = {
+ inspectorURL: vAPI.getURL(
+ `/web_accessible_resources/dom-inspector.html?secret=${vAPI.warSecret.short()}`
+ ),
+ };
+ break;
+ default:
+ return vAPI.messaging.UNHANDLED;
+ }
+
+ callback(response);
+};
+
+vAPI.messaging.listen({
+ name: 'domInspectorContent',
+ listener: onMessage,
+ privileged: false,
+});
+
+// <<<<< end of local scope
+}
+
+/******************************************************************************/
+/******************************************************************************/
+
+// Channel:
+// documentBlocked
+// privileged
+
+{
+// >>>>> start of local scope
+
+const onMessage = function(request, sender, callback) {
+ const tabId = sender.tabId || 0;
+
+ // Async
+ switch ( request.what ) {
+ default:
+ break;
+ }
+
+ // Sync
+ let response;
+
+ switch ( request.what ) {
+ case 'closeThisTab':
+ vAPI.tabs.remove(tabId);
+ break;
+
+ case 'temporarilyWhitelistDocument':
+ webRequest.strictBlockBypass(request.hostname);
+ break;
+
+ default:
+ return vAPI.messaging.UNHANDLED;
+ }
+
+ callback(response);
+};
+
+vAPI.messaging.listen({
+ name: 'documentBlocked',
+ listener: onMessage,
+ privileged: true,
+});
+
+// <<<<< end of local scope
+}
+
+/******************************************************************************/
+/******************************************************************************/
+
+// Channel:
+// devTools
+// privileged
+
+{
+// >>>>> start of local scope
+
+const onMessage = function(request, sender, callback) {
+ // Async
+ switch ( request.what ) {
+ case 'purgeAllCaches':
+ µb.getBytesInUse().then(bytesInUseBefore =>
+ io.remove(/./).then(( ) =>
+ µb.getBytesInUse().then(bytesInUseAfter => {
+ callback([
+ `Storage used before: ${µb.formatCount(bytesInUseBefore)}B`,
+ `Storage used after: ${µb.formatCount(bytesInUseAfter)}B`,
+ ].join('\n'));
+ })
+ )
+ );
+ return;
+
+ case 'snfeBenchmark':
+ µb.benchmarkStaticNetFiltering({ redirectEngine }).then(result => {
+ callback(result);
+ });
+ return;
+
+ case 'snfeToDNR': {
+ const listPromises = [];
+ const listNames = [];
+ for ( const assetKey of µb.selectedFilterLists ) {
+ listPromises.push(
+ io.get(assetKey, { dontCache: true }).then(details => {
+ listNames.push(assetKey);
+ return { name: assetKey, text: details.content };
+ })
+ );
+ }
+ const options = {
+ extensionPaths: redirectEngine.getResourceDetails().filter(e =>
+ typeof e[1].extensionPath === 'string' && e[1].extensionPath !== ''
+ ).map(e =>
+ [ e[0], e[1].extensionPath ]
+ ),
+ env: vAPI.webextFlavor.env,
+ };
+ const t0 = Date.now();
+ dnrRulesetFromRawLists(listPromises, options).then(result => {
+ const { network } = result;
+ const replacer = (k, v) => {
+ if ( k.startsWith('__') ) { return; }
+ if ( Array.isArray(v) ) {
+ return v.sort();
+ }
+ if ( v instanceof Object ) {
+ const sorted = {};
+ for ( const kk of Object.keys(v).sort() ) {
+ sorted[kk] = v[kk];
+ }
+ return sorted;
+ }
+ return v;
+ };
+ const isUnsupported = rule =>
+ rule._error !== undefined;
+ const isRegex = rule =>
+ rule.condition !== undefined &&
+ rule.condition.regexFilter !== undefined;
+ const isRedirect = rule =>
+ rule.action !== undefined &&
+ rule.action.type === 'redirect' &&
+ rule.action.redirect.extensionPath !== undefined;
+ const isCsp = rule =>
+ rule.action !== undefined &&
+ rule.action.type === 'modifyHeaders';
+ const isRemoveparam = rule =>
+ rule.action !== undefined &&
+ rule.action.type === 'redirect' &&
+ rule.action.redirect.transform !== undefined;
+ const runtime = Date.now() - t0;
+ const { ruleset } = network;
+ const good = ruleset.filter(rule =>
+ isUnsupported(rule) === false &&
+ isRegex(rule) === false &&
+ isRedirect(rule) === false &&
+ isCsp(rule) === false &&
+ isRemoveparam(rule) === false
+ );
+ const unsupported = ruleset.filter(rule =>
+ isUnsupported(rule)
+ );
+ const regexes = ruleset.filter(rule =>
+ isUnsupported(rule) === false &&
+ isRegex(rule) &&
+ isRedirect(rule) === false &&
+ isCsp(rule) === false &&
+ isRemoveparam(rule) === false
+ );
+ const redirects = ruleset.filter(rule =>
+ isUnsupported(rule) === false &&
+ isRedirect(rule)
+ );
+ const headers = ruleset.filter(rule =>
+ isUnsupported(rule) === false &&
+ isCsp(rule)
+ );
+ const removeparams = ruleset.filter(rule =>
+ isUnsupported(rule) === false &&
+ isRemoveparam(rule)
+ );
+ const out = [
+ `dnrRulesetFromRawLists(${JSON.stringify(listNames, null, 2)})`,
+ `Run time: ${runtime} ms`,
+ `Filters count: ${network.filterCount}`,
+ `Accepted filter count: ${network.acceptedFilterCount}`,
+ `Rejected filter count: ${network.rejectedFilterCount}`,
+ `Un-DNR-able filter count: ${unsupported.length}`,
+ `Resulting DNR rule count: ${ruleset.length}`,
+ ];
+ out.push(`+ Good filters (${good.length}): ${JSON.stringify(good, replacer, 2)}`);
+ out.push(`+ Regex-based filters (${regexes.length}): ${JSON.stringify(regexes, replacer, 2)}`);
+ out.push(`+ 'redirect=' filters (${redirects.length}): ${JSON.stringify(redirects, replacer, 2)}`);
+ out.push(`+ 'csp=' filters (${headers.length}): ${JSON.stringify(headers, replacer, 2)}`);
+ out.push(`+ 'removeparam=' filters (${removeparams.length}): ${JSON.stringify(removeparams, replacer, 2)}`);
+ out.push(`+ Unsupported filters (${unsupported.length}): ${JSON.stringify(unsupported, replacer, 2)}`);
+ out.push(`+ generichide exclusions (${network.generichideExclusions.length}): ${JSON.stringify(network.generichideExclusions, replacer, 2)}`);
+ if ( result.specificCosmetic ) {
+ out.push(`+ Cosmetic filters: ${result.specificCosmetic.size}`);
+ for ( const details of result.specificCosmetic ) {
+ out.push(` ${JSON.stringify(details)}`);
+ }
+ } else {
+ out.push(' Cosmetic filters: 0');
+ }
+ callback(out.join('\n'));
+ });
+ return;
+ }
+ default:
+ break;
+ }
+
+ // Sync
+ let response;
+
+ switch ( request.what ) {
+ case 'snfeDump':
+ response = staticNetFilteringEngine.dump();
+ break;
+
+ case 'cfeDump':
+ response = cosmeticFilteringEngine.dump();
+ break;
+
+ default:
+ return vAPI.messaging.UNHANDLED;
+ }
+
+ callback(response);
+};
+
+vAPI.messaging.listen({
+ name: 'devTools',
+ listener: onMessage,
+ privileged: true,
+});
+
+// <<<<< end of local scope
+}
+
+/******************************************************************************/
+/******************************************************************************/
+
+// Channel:
+// scriptlets
+// unprivileged
+
+{
+// >>>>> start of local scope
+
+const logCosmeticFilters = function(tabId, details) {
+ if ( logger.enabled === false ) { return; }
+
+ const filter = { source: 'cosmetic', raw: '' };
+ const fctxt = µb.filteringContext.duplicate();
+ fctxt.fromTabId(tabId)
+ .setRealm('cosmetic')
+ .setType('dom')
+ .setURL(details.frameURL)
+ .setDocOriginFromURL(details.frameURL)
+ .setFilter(filter);
+ for ( const selector of details.matchedSelectors.sort() ) {
+ filter.raw = selector;
+ fctxt.toLogger();
+ }
+};
+
+const logCSPViolations = function(pageStore, request) {
+ if ( logger.enabled === false || pageStore === null ) {
+ return false;
+ }
+ if ( request.violations.length === 0 ) {
+ return true;
+ }
+
+ const fctxt = µb.filteringContext.duplicate();
+ fctxt.fromTabId(pageStore.tabId)
+ .setRealm('network')
+ .setDocOriginFromURL(request.docURL)
+ .setURL(request.docURL);
+
+ let cspData = pageStore.extraData.get('cspData');
+ if ( cspData === undefined ) {
+ cspData = new Map();
+
+ const staticDirectives =
+ staticNetFilteringEngine.matchAndFetchModifiers(fctxt, 'csp');
+ if ( staticDirectives !== undefined ) {
+ for ( const directive of staticDirectives ) {
+ if ( directive.result !== 1 ) { continue; }
+ cspData.set(directive.value, directive.logData());
+ }
+ }
+
+ fctxt.type = 'inline-script';
+ fctxt.filter = undefined;
+ if ( pageStore.filterRequest(fctxt) === 1 ) {
+ cspData.set(µb.cspNoInlineScript, fctxt.filter);
+ }
+
+ fctxt.type = 'script';
+ fctxt.filter = undefined;
+ if ( pageStore.filterScripting(fctxt, true) === 1 ) {
+ cspData.set(µb.cspNoScripting, fctxt.filter);
+ }
+
+ fctxt.type = 'inline-font';
+ fctxt.filter = undefined;
+ if ( pageStore.filterRequest(fctxt) === 1 ) {
+ cspData.set(µb.cspNoInlineFont, fctxt.filter);
+ }
+
+ if ( cspData.size === 0 ) { return false; }
+
+ pageStore.extraData.set('cspData', cspData);
+ }
+
+ const typeMap = logCSPViolations.policyDirectiveToTypeMap;
+ for ( const json of request.violations ) {
+ const violation = JSON.parse(json);
+ let type = typeMap.get(violation.directive);
+ if ( type === undefined ) { continue; }
+ const logData = cspData.get(violation.policy);
+ if ( logData === undefined ) { continue; }
+ if ( /^[\w.+-]+:\/\//.test(violation.url) === false ) {
+ violation.url = request.docURL;
+ if ( type === 'script' ) { type = 'inline-script'; }
+ else if ( type === 'font' ) { type = 'inline-font'; }
+ }
+ // The resource was blocked as a result of applying a CSP directive
+ // elsewhere rather than to the resource itself.
+ logData.modifier = undefined;
+ fctxt.setURL(violation.url)
+ .setType(type)
+ .setFilter(logData)
+ .toLogger();
+ }
+
+ return true;
+};
+
+logCSPViolations.policyDirectiveToTypeMap = new Map([
+ [ 'img-src', 'image' ],
+ [ 'connect-src', 'xmlhttprequest' ],
+ [ 'font-src', 'font' ],
+ [ 'frame-src', 'sub_frame' ],
+ [ 'media-src', 'media' ],
+ [ 'object-src', 'object' ],
+ [ 'script-src', 'script' ],
+ [ 'script-src-attr', 'script' ],
+ [ 'script-src-elem', 'script' ],
+ [ 'style-src', 'stylesheet' ],
+ [ 'style-src-attr', 'stylesheet' ],
+ [ 'style-src-elem', 'stylesheet' ],
+]);
+
+const onMessage = function(request, sender, callback) {
+ const tabId = sender.tabId || 0;
+ const pageStore = µb.pageStoreFromTabId(tabId);
+
+ // Async
+ switch ( request.what ) {
+ default:
+ break;
+ }
+
+ // Sync
+ let response;
+
+ switch ( request.what ) {
+ case 'inlinescriptFound':
+ if ( logger.enabled && pageStore !== null ) {
+ const fctxt = µb.filteringContext.duplicate();
+ fctxt.fromTabId(tabId)
+ .setType('inline-script')
+ .setURL(request.docURL)
+ .setDocOriginFromURL(request.docURL);
+ if ( pageStore.filterRequest(fctxt) === 0 ) {
+ fctxt.setRealm('network').toLogger();
+ }
+ }
+ break;
+
+ case 'logCosmeticFilteringData':
+ logCosmeticFilters(tabId, request);
+ break;
+
+ case 'securityPolicyViolation':
+ response = logCSPViolations(pageStore, request);
+ break;
+
+ case 'temporarilyAllowLargeMediaElement':
+ if ( pageStore !== null ) {
+ pageStore.allowLargeMediaElementsUntil = Date.now() + 5000;
+ }
+ break;
+
+ case 'subscribeTo':
+ // https://github.com/uBlockOrigin/uBlock-issues/issues/1797
+ if ( /^(file|https?):\/\//.test(request.location) === false ) { break; }
+ const url = encodeURIComponent(request.location);
+ const title = encodeURIComponent(request.title);
+ const hash = µb.selectedFilterLists.indexOf(request.location) !== -1
+ ? '#subscribed'
+ : '';
+ vAPI.tabs.open({
+ url: `/asset-viewer.html?url=${url}&title=${title}&subscribe=1${hash}`,
+ select: true,
+ });
+ break;
+
+ case 'updateLists':
+ const listkeys = request.listkeys.split(',').filter(s => s !== '');
+ if ( listkeys.length === 0 ) { return; }
+ if ( listkeys.includes('all') ) {
+ io.purge(/./, 'public_suffix_list.dat');
+ } else {
+ for ( const listkey of listkeys ) {
+ io.purge(listkey);
+ }
+ }
+ µb.openNewTab({
+ url: 'dashboard.html#3p-filters.html',
+ select: true,
+ });
+ µb.scheduleAssetUpdater({ now: true, fetchDelay: 100, auto: request.auto });
+ break;
+
+ default:
+ return vAPI.messaging.UNHANDLED;
+ }
+
+ callback(response);
+};
+
+vAPI.messaging.listen({
+ name: 'scriptlets',
+ listener: onMessage,
+});
+
+// <<<<< end of local scope
+}
+
+
+/******************************************************************************/
+/******************************************************************************/