/******************************************************************************* 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 } /******************************************************************************/ /******************************************************************************/