diff options
Diffstat (limited to 'src/js/pagestore.js')
-rw-r--r-- | src/js/pagestore.js | 1140 |
1 files changed, 1140 insertions, 0 deletions
diff --git a/src/js/pagestore.js b/src/js/pagestore.js new file mode 100644 index 0000000..907e747 --- /dev/null +++ b/src/js/pagestore.js @@ -0,0 +1,1140 @@ +/******************************************************************************* + + 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 contextMenu from './contextmenu.js'; +import logger from './logger.js'; +import staticNetFilteringEngine from './static-net-filtering.js'; +import µb from './background.js'; +import webext from './webext.js'; +import { orphanizeString } from './text-utils.js'; +import { redirectEngine } from './redirect-engine.js'; + +import { + sessionFirewall, + sessionSwitches, + sessionURLFiltering, +} from './filtering-engines.js'; + +import { + domainFromHostname, + hostnameFromURI, + isNetworkURI, +} from './uri-utils.js'; + +/******************************************************************************* + +A PageRequestStore object is used to store net requests in two ways: + +To record distinct net requests +To create a log of net requests + +**/ + +/******************************************************************************/ + +const NetFilteringResultCache = class { + constructor() { + this.pruneTimer = vAPI.defer.create(( ) => { + this.prune(); + }); + this.init(); + } + + init() { + this.blocked = new Map(); + this.results = new Map(); + this.hash = 0; + return this; + } + + // https://github.com/gorhill/uBlock/issues/3619 + // Don't collapse redirected resources + rememberResult(fctxt, result) { + if ( fctxt.tabId <= 0 ) { return; } + if ( this.results.size === 0 ) { + this.pruneAsync(); + } + const key = `${fctxt.getDocHostname()} ${fctxt.type} ${fctxt.url}`; + this.results.set(key, { + result, + redirectURL: fctxt.redirectURL, + logData: fctxt.filter, + tstamp: Date.now() + }); + if ( result !== 1 || fctxt.redirectURL !== undefined ) { return; } + const now = Date.now(); + this.blocked.set(key, now); + this.hash = now; + } + + rememberBlock(fctxt) { + if ( fctxt.tabId <= 0 ) { return; } + if ( this.blocked.size === 0 ) { + this.pruneAsync(); + } + if ( fctxt.redirectURL !== undefined ) { return; } + const now = Date.now(); + this.blocked.set( + `${fctxt.getDocHostname()} ${fctxt.type} ${fctxt.url}`, + now + ); + this.hash = now; + } + + forgetResult(docHostname, type, url) { + const key = `${docHostname} ${type} ${url}`; + this.results.delete(key); + this.blocked.delete(key); + } + + empty() { + this.blocked.clear(); + this.results.clear(); + this.hash = 0; + this.pruneTimer.off(); + } + + prune() { + const obsolete = Date.now() - this.shelfLife; + for ( const entry of this.blocked ) { + if ( entry[1] <= obsolete ) { + this.results.delete(entry[0]); + this.blocked.delete(entry[0]); + } + } + for ( const entry of this.results ) { + if ( entry[1].tstamp <= obsolete ) { + this.results.delete(entry[0]); + } + } + if ( this.blocked.size !== 0 || this.results.size !== 0 ) { + this.pruneAsync(); + } + } + + pruneAsync() { + this.pruneTimer.on(this.shelfLife); + } + + lookupResult(fctxt) { + const entry = this.results.get( + fctxt.getDocHostname() + ' ' + + fctxt.type + ' ' + + fctxt.url + ); + if ( entry === undefined ) { return; } + // We need to use a new WAR secret if one is present since WAR secrets + // can only be used once. + if ( + entry.redirectURL !== undefined && + entry.redirectURL.startsWith(this.extensionOriginURL) + ) { + const redirectURL = new URL(entry.redirectURL); + redirectURL.searchParams.set('secret', vAPI.warSecret.short()); + entry.redirectURL = redirectURL.href; + } + return entry; + } + + lookupAllBlocked(hostname) { + const result = []; + for ( const entry of this.blocked ) { + const pos = entry[0].indexOf(' '); + if ( entry[0].slice(0, pos) === hostname ) { + result[result.length] = entry[0].slice(pos + 1); + } + } + return result; + } + + static factory() { + return new NetFilteringResultCache(); + } +}; + +NetFilteringResultCache.prototype.shelfLife = 15000; +NetFilteringResultCache.prototype.extensionOriginURL = vAPI.getURL('/'); + +/******************************************************************************/ + +// Frame stores are used solely to associate a URL with a frame id. + +const FrameStore = class { + constructor(frameURL, parentId) { + this.init(frameURL, parentId); + } + + init(frameURL, parentId) { + this.t0 = Date.now(); + this.parentId = parentId; + this.exceptCname = undefined; + this.clickToLoad = false; + this.rawURL = frameURL; + if ( frameURL !== undefined ) { + this.hostname = hostnameFromURI(frameURL); + this.domain = domainFromHostname(this.hostname) || this.hostname; + } + // Evaluated on-demand + // - 0b01: specific cosmetic filtering + // - 0b10: generic cosmetic filtering + this._cosmeticFilteringBits = undefined; + return this; + } + + dispose() { + this.rawURL = this.hostname = this.domain = ''; + if ( FrameStore.junkyard.length < FrameStore.junkyardMax ) { + FrameStore.junkyard.push(this); + } + return null; + } + + updateURL(url) { + if ( typeof url !== 'string' ) { return; } + this.rawURL = url; + this.hostname = hostnameFromURI(url); + this.domain = domainFromHostname(this.hostname) || this.hostname; + this._cosmeticFilteringBits = undefined; + } + + getCosmeticFilteringBits(tabId) { + if ( this._cosmeticFilteringBits !== undefined ) { + return this._cosmeticFilteringBits; + } + this._cosmeticFilteringBits = 0b11; + { + const result = staticNetFilteringEngine.matchRequestReverse( + 'specifichide', + this.rawURL + ); + if ( result !== 0 && logger.enabled ) { + µb.filteringContext + .duplicate() + .fromTabId(tabId) + .setURL(this.rawURL) + .setDocOriginFromURL(this.rawURL) + .setRealm('network') + .setType('specifichide') + .setFilter(staticNetFilteringEngine.toLogData()) + .toLogger(); + } + if ( result === 2 ) { + this._cosmeticFilteringBits &= ~0b01; + } + } + { + const result = staticNetFilteringEngine.matchRequestReverse( + 'generichide', + this.rawURL + ); + if ( result !== 0 && logger.enabled ) { + µb.filteringContext + .duplicate() + .fromTabId(tabId) + .setURL(this.rawURL) + .setDocOriginFromURL(this.rawURL) + .setRealm('network') + .setType('generichide') + .setFilter(staticNetFilteringEngine.toLogData()) + .toLogger(); + } + if ( result === 2 ) { + this._cosmeticFilteringBits &= ~0b10; + } + } + return this._cosmeticFilteringBits; + } + + shouldApplySpecificCosmeticFilters(tabId) { + return (this.getCosmeticFilteringBits(tabId) & 0b01) !== 0; + } + + shouldApplyGenericCosmeticFilters(tabId) { + return (this.getCosmeticFilteringBits(tabId) & 0b10) !== 0; + } + + static factory(frameURL, parentId = -1) { + const entry = FrameStore.junkyard.pop(); + if ( entry === undefined ) { + return new FrameStore(frameURL, parentId); + } + return entry.init(frameURL, parentId); + } +}; + +// To mitigate memory churning +FrameStore.junkyard = []; +FrameStore.junkyardMax = 50; + +/******************************************************************************/ + +const CountDetails = class { + constructor() { + this.allowed = { any: 0, frame: 0, script: 0 }; + this.blocked = { any: 0, frame: 0, script: 0 }; + } + reset() { + const { allowed, blocked } = this; + blocked.any = blocked.frame = blocked.script = + allowed.any = allowed.frame = allowed.script = 0; + } + inc(blocked, type = undefined) { + const stat = blocked ? this.blocked : this.allowed; + if ( type !== undefined ) { stat[type] += 1; } + stat.any += 1; + } +}; + +const HostnameDetails = class { + constructor(hostname) { + this.counts = new CountDetails(); + this.init(hostname); + } + init(hostname) { + this.hostname = hostname; + this.counts.reset(); + } + dispose() { + this.hostname = ''; + if ( HostnameDetails.junkyard.length < HostnameDetails.junkyardMax ) { + HostnameDetails.junkyard.push(this); + } + } +}; + +HostnameDetails.junkyard = []; +HostnameDetails.junkyardMax = 100; + +const HostnameDetailsMap = class extends Map { + reset() { + this.clear(); + } + dispose() { + for ( const item of this.values() ) { + item.dispose(); + } + this.reset(); + } +}; + +/******************************************************************************/ + +const PageStore = class { + constructor(tabId, details) { + this.extraData = new Map(); + this.journal = []; + this.journalLastCommitted = this.journalLastUncommitted = -1; + this.journalLastUncommittedOrigin = undefined; + this.netFilteringCache = NetFilteringResultCache.factory(); + this.hostnameDetailsMap = new HostnameDetailsMap(); + this.counts = new CountDetails(); + this.journalTimer = vAPI.defer.create(( ) => { + this.journalProcess(); + }); + this.largeMediaTimer = vAPI.defer.create(( ) => { + this.injectLargeMediaElementScriptlet(); + }); + this.init(tabId, details); + } + + static factory(tabId, details) { + let entry = PageStore.junkyard.pop(); + if ( entry === undefined ) { + entry = new PageStore(tabId, details); + } else { + entry.init(tabId, details); + } + return entry; + } + + // https://github.com/gorhill/uBlock/issues/3201 + // The context is used to determine whether we report behavior change + // to the logger. + + init(tabId, details) { + const tabContext = µb.tabContextManager.mustLookup(tabId); + this.tabId = tabId; + + // If we are navigating from-to same site, remember whether large + // media elements were temporarily allowed. + if ( + typeof this.allowLargeMediaElementsUntil !== 'number' || + tabContext.rootHostname !== this.tabHostname + ) { + this.allowLargeMediaElementsUntil = Date.now(); + } + + this.tabHostname = tabContext.rootHostname; + this.rawURL = tabContext.rawURL; + this.hostnameDetailsMap.reset(); + this.contentLastModified = 0; + this.logData = undefined; + this.counts.reset(); + this.remoteFontCount = 0; + this.popupBlockedCount = 0; + this.largeMediaCount = 0; + this.allowLargeMediaElementsRegex = undefined; + this.extraData.clear(); + + this.frameAddCount = 0; + this.frames = new Map(); + this.setFrameURL({ url: tabContext.rawURL }); + + if ( this.titleFromDetails(details) === false ) { + this.title = tabContext.rawURL; + } + + // Evaluated on-demand + this._noCosmeticFiltering = undefined; + + // Remember if the webpage was potentially improperly filtered, for + // reporting purpose. + this.hasUnprocessedRequest = vAPI.net.hasUnprocessedRequest(tabId); + + return this; + } + + reuse(context, details) { + // When force refreshing a page, the page store data needs to be reset. + + // If the hostname changes, we can't merely just update the context. + const tabContext = µb.tabContextManager.mustLookup(this.tabId); + if ( tabContext.rootHostname !== this.tabHostname ) { + context = ''; + } + + // If URL changes without a page reload (more and more common), then + // we need to keep all that we collected for reuse. In particular, + // not doing so was causing a problem in `videos.foxnews.com`: + // clicking a video thumbnail would not work, because the frame + // hierarchy structure was flushed from memory, while not really being + // flushed on the page. + if ( context === 'tabUpdated' ) { + // As part of https://github.com/chrisaljoudi/uBlock/issues/405 + // URL changed, force a re-evaluation of filtering switch + this.rawURL = tabContext.rawURL; + this.setFrameURL({ url: this.rawURL }); + this.titleFromDetails(details); + return this; + } + + // A new page is completely reloaded from scratch, reset all. + this.largeMediaTimer.off(); + this.disposeFrameStores(); + this.init(this.tabId, details); + return this; + } + + dispose() { + this.tabHostname = ''; + this.title = ''; + this.rawURL = ''; + this.hostnameDetailsMap.dispose(); + this.netFilteringCache.empty(); + this.allowLargeMediaElementsUntil = Date.now(); + this.allowLargeMediaElementsRegex = undefined; + this.largeMediaTimer.off(); + this.disposeFrameStores(); + this.journalTimer.off(); + this.journal = []; + this.journalLastUncommittedOrigin = undefined; + this.journalLastCommitted = this.journalLastUncommitted = -1; + if ( PageStore.junkyard.length < PageStore.junkyardMax ) { + PageStore.junkyard.push(this); + } + return null; + } + + titleFromDetails(details) { + if ( + details instanceof Object === false || + details.title === undefined + ) { + return false; + } + this.title = orphanizeString(details.title.slice(0, 128)); + return true; + } + + disposeFrameStores() { + for ( const frameStore of this.frames.values() ) { + frameStore.dispose(); + } + this.frames.clear(); + } + + getFrameStore(frameId) { + return this.frames.get(frameId) || null; + } + + // https://github.com/uBlockOrigin/uBlock-issues/issues/1858 + // Mind that setFrameURL() can be called from navigation event handlers. + setFrameURL(details) { + let { frameId, url, parentFrameId } = details; + if ( frameId === undefined ) { frameId = 0; } + if ( parentFrameId === undefined ) { parentFrameId = -1; } + let frameStore = this.frames.get(frameId); + if ( frameStore !== undefined ) { + if ( url === frameStore.rawURL ) { + frameStore.parentId = parentFrameId; + } else { + frameStore.init(url, parentFrameId); + } + return frameStore; + } + frameStore = FrameStore.factory(url, parentFrameId); + this.frames.set(frameId, frameStore); + this.frameAddCount += 1; + if ( url.startsWith('about:') ) { + frameStore.updateURL(this.getEffectiveFrameURL({ frameId })); + } + if ( (this.frameAddCount & 0b111111) === 0 ) { + this.pruneFrames(); + } + return frameStore; + } + + getEffectiveFrameURL(sender) { + let { frameId } = sender; + for (;;) { + const frameStore = this.getFrameStore(frameId); + if ( frameStore === null ) { break; } + if ( frameStore.rawURL.startsWith('about:') === false ) { + return frameStore.rawURL; + } + frameId = frameStore.parentId; + if ( frameId === -1 ) { break; } + } + return sender.frameURL; + } + + // There is no event to tell us a specific subframe has been removed from + // the main document. The code below will remove subframes which are no + // longer present in the root document. Removing obsolete subframes is + // not a critical task, so this is executed just once on a while, to avoid + // bloated dictionary of subframes. + // A TTL is used to avoid race conditions when new iframes are added + // through the webRequest API but still not yet visible through the + // webNavigation API. + async pruneFrames() { + let entries; + try { + entries = await webext.webNavigation.getAllFrames({ + tabId: this.tabId + }); + } catch(ex) { + } + if ( Array.isArray(entries) === false ) { return; } + const toKeep = new Set(); + for ( const { frameId } of entries ) { + toKeep.add(frameId); + } + const obsolete = Date.now() - 60000; + for ( const [ frameId, { t0 } ] of this.frames ) { + if ( toKeep.has(frameId) || t0 >= obsolete ) { continue; } + this.frames.delete(frameId); + } + } + + getNetFilteringSwitch() { + return µb.tabContextManager + .mustLookup(this.tabId) + .getNetFilteringSwitch(); + } + + toggleNetFilteringSwitch(url, scope, state) { + µb.toggleNetFilteringSwitch(url, scope, state); + this.netFilteringCache.empty(); + } + + shouldApplyCosmeticFilters(frameId = 0) { + if ( this._noCosmeticFiltering === undefined ) { + this._noCosmeticFiltering = this.getNetFilteringSwitch() === false; + if ( this._noCosmeticFiltering === false ) { + this._noCosmeticFiltering = sessionSwitches.evaluateZ( + 'no-cosmetic-filtering', + this.tabHostname + ) === true; + if ( this._noCosmeticFiltering && logger.enabled ) { + µb.filteringContext + .duplicate() + .fromTabId(this.tabId) + .setURL(this.rawURL) + .setRealm('cosmetic') + .setType('dom') + .setFilter(sessionSwitches.toLogData()) + .toLogger(); + } + } + } + if ( this._noCosmeticFiltering ) { return false; } + if ( frameId === -1 ) { return true; } + // Cosmetic filtering can be effectively disabled when both specific + // and generic cosmetic filters are disabled. + return this.shouldApplySpecificCosmeticFilters(frameId) || + this.shouldApplyGenericCosmeticFilters(frameId); + } + + shouldApplySpecificCosmeticFilters(frameId) { + if ( this.shouldApplyCosmeticFilters(-1) === false ) { return false; } + const frameStore = this.getFrameStore(frameId); + if ( frameStore === null ) { return false; } + return frameStore.shouldApplySpecificCosmeticFilters(this.tabId); + } + + shouldApplyGenericCosmeticFilters(frameId) { + if ( this.shouldApplyCosmeticFilters(-1) === false ) { return false; } + const frameStore = this.getFrameStore(frameId); + if ( frameStore === null ) { return false; } + return frameStore.shouldApplyGenericCosmeticFilters(this.tabId); + } + + // https://github.com/gorhill/uBlock/issues/2105 + // Be sure to always include the current page's hostname -- it might not + // be present when the page itself is pulled from the browser's + // short-term memory cache. + getAllHostnameDetails() { + if ( + this.hostnameDetailsMap.has(this.tabHostname) === false && + isNetworkURI(this.rawURL) + ) { + this.hostnameDetailsMap.set( + this.tabHostname, + new HostnameDetails(this.tabHostname) + ); + } + return this.hostnameDetailsMap; + } + + injectLargeMediaElementScriptlet() { + vAPI.tabs.executeScript(this.tabId, { + file: '/js/scriptlets/load-large-media-interactive.js', + allFrames: true, + runAt: 'document_idle', + }); + contextMenu.update(this.tabId); + } + + temporarilyAllowLargeMediaElements(state) { + this.largeMediaCount = 0; + contextMenu.update(this.tabId); + if ( state ) { + this.allowLargeMediaElementsUntil = 0; + this.allowLargeMediaElementsRegex = undefined; + } else { + this.allowLargeMediaElementsUntil = Date.now(); + } + vAPI.tabs.executeScript(this.tabId, { + file: '/js/scriptlets/load-large-media-all.js', + allFrames: true, + }); + } + + // https://github.com/gorhill/uBlock/issues/2053 + // There is no way around using journaling to ensure we deal properly with + // potentially out of order navigation events vs. network request events. + journalAddRequest(fctxt, result) { + const hostname = fctxt.getHostname(); + if ( hostname === '' ) { return; } + this.journal.push(hostname, result, fctxt.itype); + this.journalTimer.on(µb.hiddenSettings.requestJournalProcessPeriod); + } + + journalAddRootFrame(type, url) { + if ( type === 'committed' ) { + this.journalLastCommitted = this.journal.length; + if ( + this.journalLastUncommitted !== -1 && + this.journalLastUncommitted < this.journalLastCommitted && + this.journalLastUncommittedOrigin === hostnameFromURI(url) + ) { + this.journalLastCommitted = this.journalLastUncommitted; + } + } else if ( type === 'uncommitted' ) { + const newOrigin = hostnameFromURI(url); + if ( + this.journalLastUncommitted === -1 || + this.journalLastUncommittedOrigin !== newOrigin + ) { + this.journalLastUncommitted = this.journal.length; + this.journalLastUncommittedOrigin = newOrigin; + } + } + this.journalTimer.offon(µb.hiddenSettings.requestJournalProcessPeriod); + } + + journalProcess() { + this.journalTimer.off(); + + const journal = this.journal; + const pivot = Math.max(0, this.journalLastCommitted); + const now = Date.now(); + const { SCRIPT, SUB_FRAME, OBJECT } = µb.FilteringContext; + let aggregateAllowed = 0; + let aggregateBlocked = 0; + + // Everything after pivot originates from current page. + for ( let i = pivot; i < journal.length; i += 3 ) { + const hostname = journal[i+0]; + let hnDetails = this.hostnameDetailsMap.get(hostname); + if ( hnDetails === undefined ) { + hnDetails = new HostnameDetails(hostname); + this.hostnameDetailsMap.set(hostname, hnDetails); + this.contentLastModified = now; + } + const blocked = journal[i+1] === 1; + const itype = journal[i+2]; + if ( itype === SCRIPT ) { + hnDetails.counts.inc(blocked, 'script'); + this.counts.inc(blocked, 'script'); + } else if ( itype === SUB_FRAME || itype === OBJECT ) { + hnDetails.counts.inc(blocked, 'frame'); + this.counts.inc(blocked, 'frame'); + } else { + hnDetails.counts.inc(blocked); + this.counts.inc(blocked); + } + if ( blocked ) { + aggregateBlocked += 1; + } else { + aggregateAllowed += 1; + } + } + this.journalLastUncommitted = this.journalLastCommitted = -1; + + // https://github.com/chrisaljoudi/uBlock/issues/905#issuecomment-76543649 + // No point updating the badge if it's not being displayed. + if ( aggregateBlocked !== 0 && µb.userSettings.showIconBadge ) { + µb.updateToolbarIcon(this.tabId, 0x02); + } + + // Everything before pivot does not originate from current page -- we + // still need to bump global blocked/allowed counts. + for ( let i = 0; i < pivot; i += 3 ) { + if ( journal[i+1] === 1 ) { + aggregateBlocked += 1; + } else { + aggregateAllowed += 1; + } + } + if ( aggregateAllowed !== 0 || aggregateBlocked !== 0 ) { + µb.localSettings.blockedRequestCount += aggregateBlocked; + µb.localSettings.allowedRequestCount += aggregateAllowed; + µb.localSettingsLastModified = now; + } + journal.length = 0; + } + + filterRequest(fctxt) { + fctxt.filter = undefined; + fctxt.redirectURL = undefined; + + if ( this.getNetFilteringSwitch(fctxt) === false ) { + return 0; + } + + if ( + fctxt.itype === fctxt.CSP_REPORT && + this.filterCSPReport(fctxt) === 1 + ) { + return 1; + } + + if ( + (fctxt.itype & fctxt.FONT_ANY) !== 0 && + this.filterFont(fctxt) === 1 ) + { + return 1; + } + + if ( + fctxt.itype === fctxt.SCRIPT && + this.filterScripting(fctxt, true) === 1 + ) { + return 1; + } + + const cacheableResult = + this.cacheableResults.has(fctxt.itype) && + fctxt.aliasURL === undefined; + + if ( cacheableResult ) { + const entry = this.netFilteringCache.lookupResult(fctxt); + if ( entry !== undefined ) { + fctxt.redirectURL = entry.redirectURL; + fctxt.filter = entry.logData; + return entry.result; + } + } + + const requestType = fctxt.type; + const loggerEnabled = logger.enabled; + + // Dynamic URL filtering. + let result = sessionURLFiltering.evaluateZ( + fctxt.getTabHostname(), + fctxt.url, + requestType + ); + if ( result !== 0 && loggerEnabled ) { + fctxt.filter = sessionURLFiltering.toLogData(); + } + + // Dynamic hostname/type filtering. + if ( result === 0 && µb.userSettings.advancedUserEnabled ) { + result = sessionFirewall.evaluateCellZY( + fctxt.getTabHostname(), + fctxt.getHostname(), + requestType + ); + if ( result !== 0 && result !== 3 && loggerEnabled ) { + fctxt.filter = sessionFirewall.toLogData(); + } + } + + // Static filtering has lowest precedence. + const snfe = staticNetFilteringEngine; + if ( result === 0 || result === 3 ) { + result = snfe.matchRequest(fctxt); + if ( result !== 0 ) { + if ( loggerEnabled ) { + fctxt.setFilter(snfe.toLogData()); + } + // https://github.com/uBlockOrigin/uBlock-issues/issues/943 + // Blanket-except blocked aliased canonical hostnames? + if ( + result === 1 && + fctxt.aliasURL !== undefined && + snfe.isBlockImportant() === false && + this.shouldExceptCname(fctxt) + ) { + return 2; + } + } + } + + // Click-to-load? + // When frameId is not -1, the resource is always sub_frame. + if ( result === 1 && fctxt.frameId !== -1 ) { + const frameStore = this.getFrameStore(fctxt.frameId); + if ( frameStore !== null && frameStore.clickToLoad ) { + result = 2; + if ( loggerEnabled ) { + fctxt.pushFilter({ + result, + source: 'network', + raw: 'click-to-load', + }); + } + } + } + + // Modifier(s)? + // A modifier is an action which transform the original network request. + // https://github.com/gorhill/uBlock/issues/949 + // Redirect blocked request? + // https://github.com/uBlockOrigin/uBlock-issues/issues/760 + // Redirect non-blocked request? + if ( (fctxt.itype & fctxt.INLINE_ANY) === 0 ) { + if ( result === 1 ) { + this.redirectBlockedRequest(fctxt); + } else { + this.redirectNonBlockedRequest(fctxt); + } + } + + if ( cacheableResult ) { + this.netFilteringCache.rememberResult(fctxt, result); + } else if ( result === 1 && this.collapsibleResources.has(fctxt.itype) ) { + this.netFilteringCache.rememberBlock(fctxt); + } + + return result; + } + + filterOnHeaders(fctxt, headers) { + fctxt.filter = undefined; + + if ( this.getNetFilteringSwitch(fctxt) === false ) { return 0; } + + let result = staticNetFilteringEngine.matchHeaders(fctxt, headers); + if ( result === 0 ) { return 0; } + + const loggerEnabled = logger.enabled; + if ( loggerEnabled ) { + fctxt.filter = staticNetFilteringEngine.toLogData(); + } + + // Dynamic filtering allow rules + // URL filtering + if ( + result === 1 && + sessionURLFiltering.evaluateZ( + fctxt.getTabHostname(), + fctxt.url, + fctxt.type + ) === 2 + ) { + result = 2; + if ( loggerEnabled ) { + fctxt.filter = sessionURLFiltering.toLogData(); + } + } + // Hostname filtering + if ( + result === 1 && + µb.userSettings.advancedUserEnabled && + sessionFirewall.evaluateCellZY( + fctxt.getTabHostname(), + fctxt.getHostname(), + fctxt.type + ) === 2 + ) { + result = 2; + if ( loggerEnabled ) { + fctxt.filter = sessionFirewall.toLogData(); + } + } + + return result; + } + + redirectBlockedRequest(fctxt) { + const directives = staticNetFilteringEngine.redirectRequest(redirectEngine, fctxt); + if ( directives === undefined ) { return; } + if ( logger.enabled !== true ) { return; } + fctxt.pushFilters(directives.map(a => a.logData())); + if ( fctxt.redirectURL === undefined ) { return; } + fctxt.pushFilter({ + source: 'redirect', + raw: directives[directives.length-1].value + }); + } + + redirectNonBlockedRequest(fctxt) { + const transformDirectives = staticNetFilteringEngine.transformRequest(fctxt); + const pruneDirectives = fctxt.redirectURL === undefined && + staticNetFilteringEngine.hasQuery(fctxt) && + staticNetFilteringEngine.filterQuery(fctxt) || + undefined; + if ( transformDirectives === undefined && pruneDirectives === undefined ) { return; } + if ( logger.enabled !== true ) { return; } + if ( transformDirectives !== undefined ) { + fctxt.pushFilters(transformDirectives.map(a => a.logData())); + } + if ( pruneDirectives !== undefined ) { + fctxt.pushFilters(pruneDirectives.map(a => a.logData())); + } + if ( fctxt.redirectURL === undefined ) { return; } + fctxt.pushFilter({ + source: 'redirect', + raw: fctxt.redirectURL + }); + } + + filterCSPReport(fctxt) { + if ( + sessionSwitches.evaluateZ( + 'no-csp-reports', + fctxt.getHostname() + ) + ) { + if ( logger.enabled ) { + fctxt.filter = sessionSwitches.toLogData(); + } + return 1; + } + return 0; + } + + filterFont(fctxt) { + if ( fctxt.itype === fctxt.FONT ) { + this.remoteFontCount += 1; + } + if ( + sessionSwitches.evaluateZ( + 'no-remote-fonts', + fctxt.getTabHostname() + ) !== false + ) { + if ( logger.enabled ) { + fctxt.filter = sessionSwitches.toLogData(); + } + return 1; + } + return 0; + } + + filterScripting(fctxt, netFiltering) { + fctxt.filter = undefined; + if ( netFiltering === undefined ) { + netFiltering = this.getNetFilteringSwitch(fctxt); + } + if ( + netFiltering === false || + sessionSwitches.evaluateZ( + 'no-scripting', + fctxt.getTabHostname() + ) === false + ) { + return 0; + } + if ( logger.enabled ) { + fctxt.filter = sessionSwitches.toLogData(); + } + return 1; + } + + // The caller is responsible to check whether filtering is enabled or not. + filterLargeMediaElement(fctxt, size) { + fctxt.filter = undefined; + + if ( this.allowLargeMediaElementsUntil === 0 ) { + return 0; + } + // Disregard large media elements previously allowed: for example, to + // seek inside a previously allowed audio/video. + if ( + this.allowLargeMediaElementsRegex instanceof RegExp && + this.allowLargeMediaElementsRegex.test(fctxt.url) + ) { + return 0; + } + if ( Date.now() < this.allowLargeMediaElementsUntil ) { + const sources = this.allowLargeMediaElementsRegex instanceof RegExp + ? [ this.allowLargeMediaElementsRegex.source ] + : []; + sources.push('^' + µb.escapeRegex(fctxt.url)); + this.allowLargeMediaElementsRegex = new RegExp(sources.join('|')); + return 0; + } + if ( + sessionSwitches.evaluateZ( + 'no-large-media', + fctxt.getTabHostname() + ) !== true + ) { + this.allowLargeMediaElementsUntil = 0; + return 0; + } + if ( (size >>> 10) < µb.userSettings.largeMediaSize ) { + return 0; + } + + this.largeMediaCount += 1; + this.largeMediaTimer.on(500); + + if ( logger.enabled ) { + fctxt.filter = sessionSwitches.toLogData(); + } + + return 1; + } + + clickToLoad(frameId, frameURL) { + let frameStore = this.getFrameStore(frameId); + if ( frameStore === null ) { + frameStore = this.setFrameURL({ frameId, url: frameURL }); + } + this.netFilteringCache.forgetResult( + this.tabHostname, + 'sub_frame', + frameURL + ); + frameStore.clickToLoad = true; + } + + shouldExceptCname(fctxt) { + let exceptCname; + let frameStore; + if ( fctxt.docId !== undefined ) { + frameStore = this.getFrameStore(fctxt.docId); + if ( frameStore instanceof Object ) { + exceptCname = frameStore.exceptCname; + } + } + if ( exceptCname === undefined ) { + const result = staticNetFilteringEngine.matchRequestReverse( + 'cname', + frameStore instanceof Object + ? frameStore.rawURL + : fctxt.getDocOrigin() + ); + exceptCname = result === 2 + ? staticNetFilteringEngine.toLogData() + : false; + if ( frameStore instanceof Object ) { + frameStore.exceptCname = exceptCname; + } + } + if ( exceptCname === false ) { return false; } + if ( exceptCname instanceof Object ) { + fctxt.setFilter(exceptCname); + } + return true; + } + + getBlockedResources(request, response) { + const normalURL = µb.normalizeTabURL(this.tabId, request.frameURL); + const resources = request.resources; + const fctxt = µb.filteringContext; + fctxt.fromTabId(this.tabId) + .setDocOriginFromURL(normalURL); + // Force some resources to go through the filtering engine in order to + // populate the blocked-resources cache. This is required because for + // some resources it's not possible to detect whether they were blocked + // content script-side (i.e. `iframes` -- unlike `img`). + if ( Array.isArray(resources) && resources.length !== 0 ) { + for ( const resource of resources ) { + this.filterRequest( + fctxt.setType(resource.type).setURL(resource.url) + ); + } + } + if ( this.netFilteringCache.hash === response.hash ) { return; } + response.hash = this.netFilteringCache.hash; + response.blockedResources = + this.netFilteringCache.lookupAllBlocked(fctxt.getDocHostname()); + } +}; + +PageStore.prototype.cacheableResults = new Set([ + µb.FilteringContext.SUB_FRAME, +]); + +PageStore.prototype.collapsibleResources = new Set([ + µb.FilteringContext.IMAGE, + µb.FilteringContext.MEDIA, + µb.FilteringContext.OBJECT, + µb.FilteringContext.SUB_FRAME, +]); + +// To mitigate memory churning +PageStore.junkyard = []; +PageStore.junkyardMax = 10; + +/******************************************************************************/ + +export { PageStore }; |