diff options
Diffstat (limited to 'src/js/ublock.js')
-rw-r--r-- | src/js/ublock.js | 700 |
1 files changed, 700 insertions, 0 deletions
diff --git a/src/js/ublock.js b/src/js/ublock.js new file mode 100644 index 0000000..e963377 --- /dev/null +++ b/src/js/ublock.js @@ -0,0 +1,700 @@ +/******************************************************************************* + + 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 +*/ + +'use strict'; + +/******************************************************************************/ + +import io from './assets.js'; +import µb from './background.js'; +import { broadcast, filteringBehaviorChanged, onBroadcast } from './broadcast.js'; +import contextMenu from './contextmenu.js'; +import cosmeticFilteringEngine from './cosmetic-filtering.js'; +import { redirectEngine } from './redirect-engine.js'; +import { hostnameFromURI } from './uri-utils.js'; + +import { + permanentFirewall, + sessionFirewall, + permanentSwitches, + sessionSwitches, + permanentURLFiltering, + sessionURLFiltering, +} from './filtering-engines.js'; + +/******************************************************************************/ +/******************************************************************************/ + +// https://github.com/chrisaljoudi/uBlock/issues/405 +// Be more flexible with whitelist syntax + +// Any special regexp char will be escaped +const whitelistDirectiveEscape = /[-\/\\^$+?.()|[\]{}]/g; + +// All `*` will be expanded into `.*` +const whitelistDirectiveEscapeAsterisk = /\*/g; + +// Remember encountered regexps for reuse. +const directiveToRegexpMap = new Map(); + +// Probably manually entered whitelist directive +const isHandcraftedWhitelistDirective = function(directive) { + return directive.startsWith('/') && directive.endsWith('/') || + directive.indexOf('/') !== -1 && directive.indexOf('*') !== -1; +}; + +const matchDirective = function(url, hostname, directive) { + // Directive is a plain hostname. + if ( directive.indexOf('/') === -1 ) { + return hostname.endsWith(directive) && + (hostname.length === directive.length || + hostname.charAt(hostname.length - directive.length - 1) === '.'); + } + // Match URL exactly. + if ( + directive.startsWith('/') === false && + directive.indexOf('*') === -1 + ) { + return url === directive; + } + // Transpose into a regular expression. + let re = directiveToRegexpMap.get(directive); + if ( re === undefined ) { + let reStr; + if ( directive.startsWith('/') && directive.endsWith('/') ) { + reStr = directive.slice(1, -1); + } else { + reStr = directive.replace(whitelistDirectiveEscape, '\\$&') + .replace(whitelistDirectiveEscapeAsterisk, '.*'); + } + re = new RegExp(reStr); + directiveToRegexpMap.set(directive, re); + } + return re.test(url); +}; + +const matchBucket = function(url, hostname, bucket, start) { + if ( bucket ) { + for ( let i = start || 0, n = bucket.length; i < n; i++ ) { + if ( matchDirective(url, hostname, bucket[i]) ) { + return i; + } + } + } + return -1; +}; + +/******************************************************************************/ + +µb.getNetFilteringSwitch = function(url) { + const hostname = hostnameFromURI(url); + let key = hostname; + for (;;) { + if ( matchBucket(url, hostname, this.netWhitelist.get(key)) !== -1 ) { + return false; + } + const pos = key.indexOf('.'); + if ( pos === -1 ) { break; } + key = key.slice(pos + 1); + } + if ( matchBucket(url, hostname, this.netWhitelist.get('//')) !== -1 ) { + return false; + } + return true; +}; + +/******************************************************************************/ + +µb.toggleNetFilteringSwitch = function(url, scope, newState) { + const currentState = this.getNetFilteringSwitch(url); + if ( newState === undefined ) { + newState = !currentState; + } + if ( newState === currentState ) { + return currentState; + } + + const netWhitelist = this.netWhitelist; + const pos = url.indexOf('#'); + let targetURL = pos !== -1 ? url.slice(0, pos) : url; + const targetHostname = hostnameFromURI(targetURL); + let key = targetHostname; + let directive = scope === 'page' ? targetURL : targetHostname; + + // Add to directive list + if ( newState === false ) { + let bucket = netWhitelist.get(key); + if ( bucket === undefined ) { + bucket = []; + netWhitelist.set(key, bucket); + } + bucket.push(directive); + this.saveWhitelist(); + filteringBehaviorChanged({ hostname: targetHostname }); + return true; + } + + // Remove all directives which cause current URL to be whitelisted + for (;;) { + const bucket = netWhitelist.get(key); + if ( bucket !== undefined ) { + let i; + for (;;) { + i = matchBucket(targetURL, targetHostname, bucket, i); + if ( i === -1 ) { break; } + directive = bucket.splice(i, 1)[0]; + if ( isHandcraftedWhitelistDirective(directive) ) { + netWhitelist.get('#').push(`# ${directive}`); + } + } + if ( bucket.length === 0 ) { + netWhitelist.delete(key); + } + } + const pos = key.indexOf('.'); + if ( pos === -1 ) { break; } + key = key.slice(pos + 1); + } + const bucket = netWhitelist.get('//'); + if ( bucket !== undefined ) { + let i; + for (;;) { + i = matchBucket(targetURL, targetHostname, bucket, i); + if ( i === -1 ) { break; } + directive = bucket.splice(i, 1)[0]; + if ( isHandcraftedWhitelistDirective(directive) ) { + netWhitelist.get('#').push(`# ${directive}`); + } + } + if ( bucket.length === 0 ) { + netWhitelist.delete('//'); + } + } + this.saveWhitelist(); + filteringBehaviorChanged({ direction: 1 }); + return true; +}; + +/******************************************************************************/ + +µb.arrayFromWhitelist = function(whitelist) { + const out = new Set(); + for ( const bucket of whitelist.values() ) { + for ( const directive of bucket ) { + out.add(directive); + } + } + return Array.from(out).sort((a, b) => a.localeCompare(b)); +}; + +µb.stringFromWhitelist = function(whitelist) { + return this.arrayFromWhitelist(whitelist).join('\n'); +}; + +/******************************************************************************/ + +µb.whitelistFromArray = function(lines) { + const whitelist = new Map(); + + // Comment bucket must always be ready to be used. + whitelist.set('#', []); + + // New set of directives, scrap cached data. + directiveToRegexpMap.clear(); + + for ( let line of lines ) { + line = line.trim(); + + // https://github.com/gorhill/uBlock/issues/171 + // Skip empty lines + if ( line === '' ) { continue; } + + let key, directive; + + // Don't throw out commented out lines: user might want to fix them + if ( line.startsWith('#') ) { + key = '#'; + directive = line; + } + // Plain hostname + else if ( line.indexOf('/') === -1 ) { + if ( this.reWhitelistBadHostname.test(line) ) { + key = '#'; + directive = '# ' + line; + } else { + key = directive = line; + } + } + // Regex-based (ensure it is valid) + else if ( + line.length > 2 && + line.startsWith('/') && + line.endsWith('/') + ) { + key = '//'; + directive = line; + try { + const re = new RegExp(directive.slice(1, -1)); + directiveToRegexpMap.set(directive, re); + } catch(ex) { + key = '#'; + directive = '# ' + line; + } + } + // URL, possibly wildcarded: there MUST be at least one hostname + // label (or else it would be just impossible to make an efficient + // dict. + else { + const matches = this.reWhitelistHostnameExtractor.exec(line); + if ( !matches || matches.length !== 2 ) { + key = '#'; + directive = '# ' + line; + } else { + key = matches[1]; + directive = line; + } + } + + // https://github.com/gorhill/uBlock/issues/171 + // Skip empty keys + if ( key === '' ) { continue; } + + // Be sure this stays fixed: + // https://github.com/chrisaljoudi/uBlock/issues/185 + let bucket = whitelist.get(key); + if ( bucket === undefined ) { + bucket = []; + whitelist.set(key, bucket); + } + bucket.push(directive); + } + return whitelist; +}; + +µb.whitelistFromString = function(s) { + return this.whitelistFromArray(s.split('\n')); +}; + +// https://github.com/gorhill/uBlock/issues/3717 +µb.reWhitelistBadHostname = /[^a-z0-9.\-_\[\]:]/; +µb.reWhitelistHostnameExtractor = /([a-z0-9.\-_\[\]]+)(?::[\d*]+)?\/(?:[^\x00-\x20\/]|$)[^\x00-\x20]*$/; + +/******************************************************************************/ + +µb.changeUserSettings = function(name, value) { + let us = this.userSettings; + + // Return all settings if none specified. + if ( name === undefined ) { + us = JSON.parse(JSON.stringify(us)); + us.noCosmeticFiltering = sessionSwitches.evaluate('no-cosmetic-filtering', '*') === 1; + us.noLargeMedia = sessionSwitches.evaluate('no-large-media', '*') === 1; + us.noRemoteFonts = sessionSwitches.evaluate('no-remote-fonts', '*') === 1; + us.noScripting = sessionSwitches.evaluate('no-scripting', '*') === 1; + us.noCSPReports = sessionSwitches.evaluate('no-csp-reports', '*') === 1; + return us; + } + + if ( typeof name !== 'string' || name === '' ) { return; } + + if ( value === undefined ) { + return us[name]; + } + + // Pre-change + switch ( name ) { + case 'largeMediaSize': + if ( typeof value !== 'number' ) { + value = parseInt(value, 10) || 0; + } + value = Math.ceil(Math.max(value, 0)); + break; + default: + break; + } + + // Change -- but only if the user setting actually exists. + const mustSave = us.hasOwnProperty(name) && value !== us[name]; + if ( mustSave ) { + us[name] = value; + } + + // Post-change + switch ( name ) { + case 'advancedUserEnabled': + if ( value === true ) { + us.popupPanelSections |= 0b11111; + } + break; + case 'autoUpdate': + this.scheduleAssetUpdater({ updateDelay: value ? 2000 : 0 }); + break; + case 'cnameUncloakEnabled': + if ( vAPI.net.canUncloakCnames === true ) { + vAPI.net.setOptions({ cnameUncloakEnabled: value === true }); + } + break; + case 'collapseBlocked': + if ( value === false ) { + cosmeticFilteringEngine.removeFromSelectorCache('*', 'net'); + } + break; + case 'contextMenuEnabled': + contextMenu.update(null); + break; + case 'hyperlinkAuditingDisabled': + if ( this.privacySettingsSupported ) { + vAPI.browserSettings.set({ 'hyperlinkAuditing': !value }); + } + break; + case 'noCosmeticFiltering': + case 'noLargeMedia': + case 'noRemoteFonts': + case 'noScripting': + case 'noCSPReports': + let switchName; + switch ( name ) { + case 'noCosmeticFiltering': + switchName = 'no-cosmetic-filtering'; break; + case 'noLargeMedia': + switchName = 'no-large-media'; break; + case 'noRemoteFonts': + switchName = 'no-remote-fonts'; break; + case 'noScripting': + switchName = 'no-scripting'; break; + case 'noCSPReports': + switchName = 'no-csp-reports'; break; + default: + break; + } + if ( switchName === undefined ) { break; } + let switchState = value ? 1 : 0; + sessionSwitches.toggle(switchName, '*', switchState); + if ( permanentSwitches.toggle(switchName, '*', switchState) ) { + this.saveHostnameSwitches(); + } + break; + case 'prefetchingDisabled': + if ( this.privacySettingsSupported ) { + vAPI.browserSettings.set({ 'prefetching': !value }); + } + break; + case 'webrtcIPAddressHidden': + if ( this.privacySettingsSupported ) { + vAPI.browserSettings.set({ 'webrtcIPAddress': !value }); + } + break; + default: + break; + } + + if ( mustSave ) { + this.saveUserSettings(); + } +}; + +/******************************************************************************/ + +// https://www.reddit.com/r/uBlockOrigin/comments/8524cf/my_custom_scriptlets_doesnt_work_what_am_i_doing/ + +µb.changeHiddenSettings = function(hs) { + const mustReloadResources = + hs.userResourcesLocation !== this.hiddenSettings.userResourcesLocation; + this.hiddenSettings = hs; + this.saveHiddenSettings(); + if ( mustReloadResources ) { + redirectEngine.invalidateResourcesSelfie(io); + this.loadRedirectResources(); + } + broadcast({ what: 'hiddenSettingsChanged' }); +}; + +/******************************************************************************/ + +µb.elementPickerExec = async function( + tabId, + frameId, + targetElement, + zap = false, +) { + if ( vAPI.isBehindTheSceneTabId(tabId) ) { return; } + + this.epickerArgs.target = targetElement || ''; + this.epickerArgs.zap = zap; + + // https://github.com/uBlockOrigin/uBlock-issues/issues/40 + // The element picker needs this library + if ( zap !== true ) { + vAPI.tabs.executeScript(tabId, { + file: '/lib/diff/swatinem_diff.js', + runAt: 'document_end', + }); + } + + await vAPI.tabs.executeScript(tabId, { + file: '/js/scriptlets/epicker.js', + frameId, + runAt: 'document_end', + }); + + // https://github.com/uBlockOrigin/uBlock-issues/issues/168 + // Force activate the target tab once the element picker has been + // injected. + vAPI.tabs.select(tabId); +}; + +/******************************************************************************/ + +// https://github.com/gorhill/uBlock/issues/2033 +// Always set own rules, trying to be fancy to avoid setting seemingly +// (but not really) redundant rules led to this issue. + +µb.toggleFirewallRule = function(details) { + const { desHostname, requestType, action } = details; + let { srcHostname } = details; + + if ( action !== 0 ) { + sessionFirewall.setCell( + srcHostname, + desHostname, + requestType, + action + ); + } else { + sessionFirewall.unsetCell( + srcHostname, + desHostname, + requestType + ); + } + + // https://github.com/chrisaljoudi/uBlock/issues/731#issuecomment-73937469 + if ( details.persist ) { + if ( action !== 0 ) { + permanentFirewall.setCell( + srcHostname, + desHostname, + requestType, + action + ); + } else { + permanentFirewall.unsetCell( + srcHostname, + desHostname, + requestType + ); + } + this.savePermanentFirewallRules(); + } + + // https://github.com/gorhill/uBlock/issues/1662 + // Flush all cached `net` cosmetic filters if we are dealing with a + // collapsible type: any of the cached entries could be a resource on the + // target page. + if ( + (srcHostname !== '*') && + ( + requestType === '*' || + requestType === 'image' || + requestType === '3p' || + requestType === '3p-frame' + ) + ) { + srcHostname = '*'; + } + + // https://github.com/chrisaljoudi/uBlock/issues/420 + cosmeticFilteringEngine.removeFromSelectorCache(srcHostname, 'net'); + + // Flush caches + filteringBehaviorChanged({ + direction: action === 1 ? 1 : 0, + hostname: srcHostname, + }); + + if ( details.tabId === undefined ) { return; } + + if ( requestType.startsWith('3p') ) { + this.updateToolbarIcon(details.tabId, 0b100); + } + + if ( requestType === '3p' && action === 3 ) { + vAPI.tabs.executeScript(details.tabId, { + file: '/js/scriptlets/load-3p-css.js', + allFrames: true, + runAt: 'document_idle', + }); + } +}; + +/******************************************************************************/ + +µb.toggleURLFilteringRule = function(details) { + let changed = sessionURLFiltering.setRule( + details.context, + details.url, + details.type, + details.action + ); + if ( changed === false ) { return; } + + cosmeticFilteringEngine.removeFromSelectorCache(details.context, 'net'); + + if ( details.persist !== true ) { return; } + + changed = permanentURLFiltering.setRule( + details.context, + details.url, + details.type, + details.action + ); + + if ( changed ) { + this.savePermanentFirewallRules(); + } +}; + +/******************************************************************************/ + +µb.toggleHostnameSwitch = function(details) { + const newState = typeof details.state === 'boolean' + ? details.state + : sessionSwitches.evaluateZ(details.name, details.hostname) === false; + let changed = sessionSwitches.toggleZ( + details.name, + details.hostname, + !!details.deep, + newState + ); + if ( changed === false ) { return; } + + // Take per-switch action if needed + switch ( details.name ) { + case 'no-scripting': + this.updateToolbarIcon(details.tabId, 0b100); + break; + case 'no-cosmetic-filtering': { + const scriptlet = newState ? 'cosmetic-off' : 'cosmetic-on'; + vAPI.tabs.executeScript(details.tabId, { + file: `/js/scriptlets/${scriptlet}.js`, + allFrames: true, + }); + break; + } + case 'no-large-media': + const pageStore = this.pageStoreFromTabId(details.tabId); + if ( pageStore !== null ) { + pageStore.temporarilyAllowLargeMediaElements(!newState); + } + break; + default: + break; + } + + // Flush caches if needed + if ( newState ) { + switch ( details.name ) { + case 'no-scripting': + case 'no-remote-fonts': + filteringBehaviorChanged({ + direction: details.state ? 1 : 0, + hostname: details.hostname, + }); + break; + default: + break; + } + } + + if ( details.persist !== true ) { return; } + + changed = permanentSwitches.toggleZ( + details.name, + details.hostname, + !!details.deep, + newState + ); + if ( changed ) { + this.saveHostnameSwitches(); + } +}; + +/******************************************************************************/ + +µb.blockingModeFromHostname = function(hn) { + let bits = 0; + if ( sessionSwitches.evaluateZ('no-scripting', hn) ) { + bits |= 0b00000010; + } + if ( this.userSettings.advancedUserEnabled ) { + if ( sessionFirewall.evaluateCellZY(hn, '*', '3p') === 1 ) { + bits |= 0b00000100; + } + if ( sessionFirewall.evaluateCellZY(hn, '*', '3p-script') === 1 ) { + bits |= 0b00001000; + } + if ( sessionFirewall.evaluateCellZY(hn, '*', '3p-frame') === 1 ) { + bits |= 0b00010000; + } + } + return bits; +}; + +{ + const parse = function() { + const s = µb.hiddenSettings.blockingProfiles; + const profiles = []; + s.split(/\s+/).forEach(s => { + let pos = s.indexOf('/'); + if ( pos === -1 ) { + pos = s.length; + } + const bits = parseInt(s.slice(0, pos), 2); + if ( isNaN(bits) ) { return; } + const color = s.slice(pos + 1); + profiles.push({ bits, color: color !== '' ? color : '#666' }); + }); + µb.liveBlockingProfiles = profiles; + µb.blockingProfileColorCache.clear(); + }; + + parse(); + + onBroadcast(msg => { + if ( msg.what !== 'hiddenSettingsChanged' ) { return; } + parse(); + }); +} + +/******************************************************************************/ + +µb.pageURLFromMaybeDocumentBlockedURL = function(pageURL) { + if ( pageURL.startsWith(vAPI.getURL('/document-blocked.html?')) ) { + try { + const url = new URL(pageURL); + return JSON.parse(url.searchParams.get('details')).url; + } catch(ex) { + } + } + return pageURL; +}; + +/******************************************************************************/ |