/******************************************************************************* uBlock Origin - a browser extension to block requests. Copyright (C) 2014-2018 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'; /******************************************************************************* A PageRequestStore object is used to store net requests in two ways: To record distinct net requests To create a log of net requests **/ /******************************************************************************/ /******************************************************************************/ µBlock.PageStore = (function() { /******************************************************************************/ var µb = µBlock; /******************************************************************************/ /******************************************************************************/ // To mitigate memory churning var netFilteringCacheJunkyard = [], netFilteringCacheJunkyardMax = 10; /******************************************************************************/ var NetFilteringResultCache = function() { this.boundPruneAsyncCallback = this.pruneAsyncCallback.bind(this); this.init(); }; /******************************************************************************/ NetFilteringResultCache.prototype.shelfLife = 15 * 1000; /******************************************************************************/ NetFilteringResultCache.factory = function() { var entry = netFilteringCacheJunkyard.pop(); if ( entry === undefined ) { entry = new NetFilteringResultCache(); } else { entry.init(); } return entry; }; /******************************************************************************/ NetFilteringResultCache.prototype.init = function() { this.blocked = new Map(); this.results = new Map(); this.hash = 0; this.timer = null; }; /******************************************************************************/ NetFilteringResultCache.prototype.dispose = function() { this.empty(); if ( netFilteringCacheJunkyard.length < netFilteringCacheJunkyardMax ) { netFilteringCacheJunkyard.push(this); } return null; }; /******************************************************************************/ NetFilteringResultCache.prototype.rememberResult = function(context, result, logData) { if ( this.results.size === 0 ) { this.pruneAsync(); } var key = context.pageHostname + ' ' + context.requestType + ' ' + context.requestURL; this.results.set(key, { result: result, logData: logData, tstamp: Date.now() }); if ( result !== 1 ) { return; } var now = Date.now(); this.blocked.set(key, now); this.hash = now; }; /******************************************************************************/ NetFilteringResultCache.prototype.rememberBlock = function(details) { if ( this.blocked.size === 0 ) { this.pruneAsync(); } var now = Date.now(); this.blocked.set( details.pageHostname + ' ' + details.requestType + ' ' + details.requestURL, now ); this.hash = now; }; /******************************************************************************/ NetFilteringResultCache.prototype.empty = function() { this.blocked.clear(); this.results.clear(); this.hash = 0; if ( this.timer !== null ) { clearTimeout(this.timer); this.timer = null; } }; /******************************************************************************/ NetFilteringResultCache.prototype.pruneAsync = function() { if ( this.timer === null ) { this.timer = vAPI.setTimeout(this.boundPruneAsyncCallback, this.shelfLife * 2); } }; NetFilteringResultCache.prototype.pruneAsyncCallback = function() { this.timer = null; var obsolete = Date.now() - this.shelfLife, entry; for ( entry of this.blocked ) { if ( entry[1] <= obsolete ) { this.results.delete(entry[0]); this.blocked.delete(entry[0]); } } for ( 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(); } }; /******************************************************************************/ NetFilteringResultCache.prototype.lookupResult = function(context) { return this.results.get( context.pageHostname + ' ' + context.requestType + ' ' + context.requestURL ); }; /******************************************************************************/ NetFilteringResultCache.prototype.lookupAllBlocked = function(hostname) { var result = [], pos; for ( var entry of this.blocked ) { pos = entry[0].indexOf(' '); if ( entry[0].slice(0, pos) === hostname ) { result[result.length] = entry[0].slice(pos + 1); } } return result; }; /******************************************************************************/ /******************************************************************************/ // Frame stores are used solely to associate a URL with a frame id. The // name `pageHostname` is used because of historical reasons. A more // appropriate name is `frameHostname` -- something to do in a future // refactoring. // To mitigate memory churning var frameStoreJunkyard = []; var frameStoreJunkyardMax = 50; /******************************************************************************/ var FrameStore = function(frameURL) { this.init(frameURL); }; /******************************************************************************/ FrameStore.factory = function(frameURL) { var entry = frameStoreJunkyard.pop(); if ( entry === undefined ) { return new FrameStore(frameURL); } return entry.init(frameURL); }; /******************************************************************************/ FrameStore.prototype.init = function(frameURL) { var µburi = µb.URI; this.pageHostname = µburi.hostnameFromURI(frameURL); this.pageDomain = µburi.domainFromHostname(this.pageHostname) || this.pageHostname; return this; }; /******************************************************************************/ FrameStore.prototype.dispose = function() { this.pageHostname = this.pageDomain = ''; if ( frameStoreJunkyard.length < frameStoreJunkyardMax ) { frameStoreJunkyard.push(this); } return null; }; /******************************************************************************/ /******************************************************************************/ // To mitigate memory churning var pageStoreJunkyard = []; var pageStoreJunkyardMax = 10; /******************************************************************************/ var PageStore = function(tabId, context) { this.init(tabId, context); this.journal = []; this.journalTimer = null; this.journalLastCommitted = this.journalLastUncommitted = undefined; this.journalLastUncommittedURL = undefined; }; /******************************************************************************/ PageStore.factory = function(tabId, context) { var entry = pageStoreJunkyard.pop(); if ( entry === undefined ) { entry = new PageStore(tabId, context); } else { entry.init(tabId, context); } return entry; }; /******************************************************************************/ // https://github.com/gorhill/uBlock/issues/3201 // The context is used to determine whether we report behavior change to the // logger. PageStore.prototype.init = function(tabId, context) { var 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 = 0; } this.tabHostname = tabContext.rootHostname; this.title = tabContext.rawURL; this.rawURL = tabContext.rawURL; this.hostnameToCountMap = new Map(); this.contentLastModified = 0; this.frames = new Map(); this.logData = undefined; this.perLoadBlockedRequestCount = 0; this.perLoadAllowedRequestCount = 0; this.hiddenElementCount = ''; // Empty string means "unknown" this.remoteFontCount = 0; this.popupBlockedCount = 0; this.largeMediaCount = 0; this.largeMediaTimer = null; this.netFilteringCache = NetFilteringResultCache.factory(); this.internalRedirectionCount = 0; this.noCosmeticFiltering = µb.hnSwitches.evaluateZ( 'no-cosmetic-filtering', tabContext.rootHostname ) === true; if ( this.noCosmeticFiltering && µb.logger.isEnabled() && context === 'tabCommitted' ) { µb.logger.writeOne( tabId, 'cosmetic', µb.hnSwitches.toLogData(), 'dom', tabContext.rawURL, this.tabHostname, this.tabHostname ); } // Support `generichide` filter option. this.noGenericCosmeticFiltering = this.noCosmeticFiltering; if ( this.noGenericCosmeticFiltering !== true ) { var result = µb.staticNetFilteringEngine.matchStringExactType( this.createContextFromPage(), tabContext.normalURL, 'generichide' ); this.noGenericCosmeticFiltering = result === 2; if ( result !== 0 && µb.logger.isEnabled() && context === 'tabCommitted' ) { µb.logger.writeOne( tabId, 'net', µb.staticNetFilteringEngine.toLogData(), 'generichide', tabContext.rawURL, this.tabHostname, this.tabHostname ); } } return this; }; /******************************************************************************/ PageStore.prototype.reuse = function(context) { // 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. var 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; return this; } // A new page is completely reloaded from scratch, reset all. if ( this.largeMediaTimer !== null ) { clearTimeout(this.largeMediaTimer); this.largeMediaTimer = null; } this.disposeFrameStores(); this.netFilteringCache = this.netFilteringCache.dispose(); this.init(this.tabId, context); return this; }; // https://www.youtube.com/watch?v=dltNSbOupgE /******************************************************************************/ PageStore.prototype.dispose = function() { this.tabHostname = ''; this.title = ''; this.rawURL = ''; this.hostnameToCountMap = null; this.allowLargeMediaElementsUntil = 0; if ( this.largeMediaTimer !== null ) { clearTimeout(this.largeMediaTimer); this.largeMediaTimer = null; } this.disposeFrameStores(); this.netFilteringCache = this.netFilteringCache.dispose(); if ( this.journalTimer !== null ) { clearTimeout(this.journalTimer); this.journalTimer = null; } this.journal = []; this.journalLastUncommittedURL = undefined; if ( pageStoreJunkyard.length < pageStoreJunkyardMax ) { pageStoreJunkyard.push(this); } return null; }; /******************************************************************************/ PageStore.prototype.disposeFrameStores = function() { for ( var frameStore of this.frames.values() ) { frameStore.dispose(); } this.frames.clear(); }; /******************************************************************************/ PageStore.prototype.getFrame = function(frameId) { return this.frames.get(frameId) || null; }; /******************************************************************************/ PageStore.prototype.setFrame = function(frameId, frameURL) { var frameStore = this.frames.get(frameId); if ( frameStore !== undefined ) { frameStore.init(frameURL); } else { this.frames.set(frameId, FrameStore.factory(frameURL)); } }; /******************************************************************************/ PageStore.prototype.createContextFromPage = function() { var context = µb.tabContextManager.createContext(this.tabId); context.pageHostname = context.rootHostname; context.pageDomain = context.rootDomain; return context; }; PageStore.prototype.createContextFromFrameId = function(frameId) { var context = µb.tabContextManager.createContext(this.tabId); var frameStore = this.frames.get(frameId); if ( frameStore !== undefined ) { context.pageHostname = frameStore.pageHostname; context.pageDomain = frameStore.pageDomain; } else { context.pageHostname = context.rootHostname; context.pageDomain = context.rootDomain; } return context; }; PageStore.prototype.createContextFromFrameHostname = function(frameHostname) { var context = µb.tabContextManager.createContext(this.tabId); context.pageHostname = frameHostname; context.pageDomain = µb.URI.domainFromHostname(frameHostname) || frameHostname; return context; }; /******************************************************************************/ PageStore.prototype.getNetFilteringSwitch = function() { return µb.tabContextManager.mustLookup(this.tabId).getNetFilteringSwitch(); }; /******************************************************************************/ PageStore.prototype.getSpecificCosmeticFilteringSwitch = function() { return this.noCosmeticFiltering !== true; }; /******************************************************************************/ PageStore.prototype.getGenericCosmeticFilteringSwitch = function() { return this.noGenericCosmeticFiltering !== true && this.noCosmeticFiltering !== true; }; /******************************************************************************/ PageStore.prototype.toggleNetFilteringSwitch = function(url, scope, state) { µb.toggleNetFilteringSwitch(url, scope, state); this.netFilteringCache.empty(); }; /******************************************************************************/ PageStore.prototype.injectLargeMediaElementScriptlet = function() { this.largeMediaTimer = null; µb.scriptlets.injectDeep( this.tabId, 'load-large-media-interactive' ); µb.contextMenu.update(this.tabId); }; PageStore.prototype.temporarilyAllowLargeMediaElements = function(state) { this.largeMediaCount = 0; µb.contextMenu.update(this.tabId); this.allowLargeMediaElementsUntil = state ? Date.now() + 86400000 : 0; µb.scriptlets.injectDeep(this.tabId, 'load-large-media-all'); }; /******************************************************************************/ // 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. PageStore.prototype.journalAddRequest = function(hostname, result) { if ( hostname === '' ) { return; } this.journal.push( hostname, result === 1 ? 0x00000001 : 0x00010000 ); if ( this.journalTimer === null ) { this.journalTimer = vAPI.setTimeout(this.journalProcess.bind(this, true), 1000); } }; PageStore.prototype.journalAddRootFrame = function(type, url) { if ( type === 'committed' ) { this.journalLastCommitted = this.journal.length; if ( this.journalLastUncommitted !== undefined && this.journalLastUncommitted < this.journalLastCommitted && this.journalLastUncommittedURL === url ) { this.journalLastCommitted = this.journalLastUncommitted; this.journalLastUncommitted = undefined; } } else if ( type === 'uncommitted' ) { this.journalLastUncommitted = this.journal.length; this.journalLastUncommittedURL = url; } if ( this.journalTimer !== null ) { clearTimeout(this.journalTimer); } this.journalTimer = vAPI.setTimeout(this.journalProcess.bind(this, true), 1000); }; PageStore.prototype.journalProcess = function(fromTimer) { if ( !fromTimer ) { clearTimeout(this.journalTimer); } this.journalTimer = null; var journal = this.journal, i, n = journal.length, hostname, count, hostnameCounts, aggregateCounts = 0, now = Date.now(), pivot = this.journalLastCommitted || 0; // Everything after pivot originates from current page. for ( i = pivot; i < n; i += 2 ) { hostname = journal[i]; hostnameCounts = this.hostnameToCountMap.get(hostname); if ( hostnameCounts === undefined ) { hostnameCounts = 0; this.contentLastModified = now; } count = journal[i+1]; this.hostnameToCountMap.set(hostname, hostnameCounts + count); aggregateCounts += count; } this.perLoadBlockedRequestCount += aggregateCounts & 0xFFFF; this.perLoadAllowedRequestCount += aggregateCounts >>> 16 & 0xFFFF; this.journalLastCommitted = undefined; // https://github.com/chrisaljoudi/uBlock/issues/905#issuecomment-76543649 // No point updating the badge if it's not being displayed. if ( (aggregateCounts & 0xFFFF) && µb.userSettings.showIconBadge ) { µb.updateBadgeAsync(this.tabId); } // Everything before pivot does not originate from current page -- we still // need to bump global blocked/allowed counts. for ( i = 0; i < pivot; i += 2 ) { aggregateCounts += journal[i+1]; } if ( aggregateCounts !== 0 ) { µb.localSettings.blockedRequestCount += aggregateCounts & 0xFFFF; µb.localSettings.allowedRequestCount += aggregateCounts >>> 16 & 0xFFFF; µb.localSettingsLastModified = now; } journal.length = 0; }; /******************************************************************************/ PageStore.prototype.filterRequest = function(context) { this.logData = undefined; if ( this.getNetFilteringSwitch() === false ) { return 0; } var requestType = context.requestType; if ( requestType === 'csp_report' && this.filterCSPReport(context) === 1 ) { return 1; } if ( requestType.endsWith('font') && this.filterFont(context) === 1 ) { return 1; } var cacheableResult = this.cacheableResults[requestType] === true; if ( cacheableResult ) { var entry = this.netFilteringCache.lookupResult(context); if ( entry !== undefined ) { this.logData = entry.logData; return entry.result; } } // Dynamic URL filtering. var result = µb.sessionURLFiltering.evaluateZ(context.rootHostname, context.requestURL, requestType); if ( result !== 0 && µb.logger.isEnabled() ) { this.logData = µb.sessionURLFiltering.toLogData(); } // Dynamic hostname/type filtering. if ( result === 0 && µb.userSettings.advancedUserEnabled ) { result = µb.sessionFirewall.evaluateCellZY(context.rootHostname, context.requestHostname, requestType); if ( result !== 0 && result !== 3 && µb.logger.isEnabled() ) { this.logData = µb.sessionFirewall.toLogData(); } } // Static filtering has lowest precedence. if ( result === 0 || result === 3 ) { result = µb.staticNetFilteringEngine.matchString(context); if ( result !== 0 && µb.logger.isEnabled() ) { this.logData = µb.staticNetFilteringEngine.toLogData(); } } if ( cacheableResult ) { this.netFilteringCache.rememberResult(context, result, this.logData); } else if ( result === 1 && this.collapsibleResources[requestType] === true ) { this.netFilteringCache.rememberBlock(context, true); } return result; }; PageStore.prototype.cacheableResults = { sub_frame: true }; PageStore.prototype.collapsibleResources = { image: true, media: true, object: true, sub_frame: true }; /******************************************************************************/ PageStore.prototype.filterCSPReport = function(context) { if ( µb.hnSwitches.evaluateZ('no-csp-reports', context.requestHostname) ) { if ( µb.logger.isEnabled() ) { this.logData = µb.hnSwitches.toLogData(); } return 1; } // https://github.com/gorhill/uBlock/issues/3140 // Special handling of CSP reports if and only if these can't be filtered // natively. if ( vAPI.net.nativeCSPReportFiltering !== true && this.internalRedirectionCount !== 0 ) { if ( µb.logger.isEnabled() ) { this.logData = { result: 1, source: 'global', raw: 'no-spurious-csp-report' }; } return 1; } return 0; }; /******************************************************************************/ PageStore.prototype.filterFont = function(context) { if ( context.requestType === 'font' ) { this.remoteFontCount += 1; } if ( µb.hnSwitches.evaluateZ('no-remote-fonts', context.rootHostname) !== false ) { if ( µb.logger.isEnabled() ) { this.logData = µb.hnSwitches.toLogData(); } return 1; } return 0; }; /******************************************************************************/ // The caller is responsible to check whether filtering is enabled or not. PageStore.prototype.filterLargeMediaElement = function(size) { this.logData = undefined; if ( Date.now() < this.allowLargeMediaElementsUntil ) { return 0; } if ( µb.hnSwitches.evaluateZ('no-large-media', this.tabHostname) !== true ) { return 0; } if ( (size >>> 10) < µb.userSettings.largeMediaSize ) { return 0; } this.largeMediaCount += 1; if ( this.largeMediaTimer === null ) { this.largeMediaTimer = vAPI.setTimeout( this.injectLargeMediaElementScriptlet.bind(this), 500 ); } if ( µb.logger.isEnabled() ) { this.logData = µb.hnSwitches.toLogData(); } return 1; }; // https://www.youtube.com/watch?v=drW8p_dTLD4 /******************************************************************************/ PageStore.prototype.getBlockedResources = function(request, response) { var µburi = µb.URI, normalURL = µb.normalizePageURL(this.tabId, request.frameURL), frameHostname = µburi.hostnameFromURI(normalURL), resources = request.resources; // 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 ) { var context = this.createContextFromFrameHostname(frameHostname); for ( var resource of resources ) { context.requestType = resource.type; context.requestHostname = µburi.hostnameFromURI(resource.url); context.requestURL = resource.url; this.filterRequest(context); } } if ( this.netFilteringCache.hash === response.hash ) { return; } response.hash = this.netFilteringCache.hash; response.blockedResources = this.netFilteringCache.lookupAllBlocked(frameHostname); }; /******************************************************************************/ return { factory: PageStore.factory }; })(); /******************************************************************************/