/******************************************************************************* µBlock - a Chromium browser extension to block requests. Copyright (C) 2014 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 */ /* jshint bitwise: false */ /* global vAPI, µBlock */ /******************************************************************************* 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() { 'use strict'; /******************************************************************************/ var µb = µBlock; /******************************************************************************/ /******************************************************************************/ // To mitigate memory churning var netFilteringResultCacheEntryJunkyard = []; var netFilteringResultCacheEntryJunkyardMax = 200; /******************************************************************************/ var NetFilteringResultCacheEntry = function(result, type, flags) { this.init(result, type, flags); }; /******************************************************************************/ NetFilteringResultCacheEntry.prototype.init = function(result, type, flags) { this.result = result; this.type = type; this.flags = flags; this.time = Date.now(); }; /******************************************************************************/ NetFilteringResultCacheEntry.prototype.dispose = function() { this.result = ''; this.type = ''; if ( netFilteringResultCacheEntryJunkyard.length < netFilteringResultCacheEntryJunkyardMax ) { netFilteringResultCacheEntryJunkyard.push(this); } }; /******************************************************************************/ NetFilteringResultCacheEntry.factory = function(result, type, flags) { var entry = netFilteringResultCacheEntryJunkyard.pop(); if ( entry === undefined ) { entry = new NetFilteringResultCacheEntry(result, type, flags); } else { entry.init(result, type, flags); } return entry; }; /******************************************************************************/ /******************************************************************************/ // To mitigate memory churning var uidGenerator = 1; var netFilteringCacheJunkyard = []; var netFilteringCacheJunkyardMax = 10; /******************************************************************************/ var NetFilteringResultCache = function() { this.init(); }; /******************************************************************************/ NetFilteringResultCache.factory = function() { var entry = netFilteringCacheJunkyard.pop(); if ( entry === undefined ) { entry = new NetFilteringResultCache(); } else { entry.init(); } return entry; }; /******************************************************************************/ NetFilteringResultCache.prototype.init = function() { this.uname = 'NetFilteringResultCache:' + uidGenerator++; this.urls = {}; this.count = 0; this.shelfLife = 60 * 1000; }; /******************************************************************************/ NetFilteringResultCache.prototype.dispose = function() { for ( var key in this.urls ) { if ( this.urls.hasOwnProperty(key) === false ) { continue; } this.urls[key].dispose(); } µBlock.asyncJobs.remove(this.uname); this.uname = ''; this.urls = {}; this.count = 0; if ( netFilteringCacheJunkyard.length < netFilteringCacheJunkyardMax ) { netFilteringCacheJunkyard.push(this); } return null; }; /******************************************************************************/ NetFilteringResultCache.prototype.add = function(url, result, type, flags) { var entry = this.urls[url]; if ( entry !== undefined ) { entry.result = result; entry.type = type; entry.flags = flags; entry.time = Date.now(); return; } this.urls[url] = NetFilteringResultCacheEntry.factory(result, type, flags); if ( this.count === 0 ) { this.pruneAsync(); } this.count += 1; }; /******************************************************************************/ NetFilteringResultCache.prototype.fetchAll = function() { return this.urls; }; /******************************************************************************/ NetFilteringResultCache.prototype.compareEntries = function(a, b) { return this.urls[b].time - this.urls[a].time; }; /******************************************************************************/ NetFilteringResultCache.prototype.prune = function() { var keys = Object.keys(this.urls).sort(this.compareEntries.bind(this)); var obsolete = Date.now() - this.shelfLife; var key, entry; var i = keys.length; while ( i-- ) { key = keys[i]; entry = this.urls[key]; if ( entry.time > obsolete ) { break; } entry.dispose(); delete this.urls[key]; } this.count -= keys.length - i - 1; if ( this.count > 0 ) { this.pruneAsync(); } }; // https://www.youtube.com/watch?v=hcVpbsDyOhM /******************************************************************************/ NetFilteringResultCache.prototype.pruneAsync = function() { µBlock.asyncJobs.add( this.uname, null, this.prune.bind(this), this.shelfLife + 120000, false ); }; /******************************************************************************/ NetFilteringResultCache.prototype.lookup = function(url) { return this.urls[url]; }; /******************************************************************************/ /******************************************************************************/ // To mitigate memory churning var frameStoreJunkyard = []; var frameStoreJunkyardMax = 50; /******************************************************************************/ var FrameStore = function(rootHostname, frameURL) { this.init(rootHostname, frameURL); }; /******************************************************************************/ FrameStore.factory = function(rootHostname, frameURL) { var entry = frameStoreJunkyard.pop(); if ( entry === undefined ) { entry = new FrameStore(rootHostname, frameURL); } else { entry.init(rootHostname, frameURL); } return entry; }; /******************************************************************************/ FrameStore.prototype.init = function(rootHostname, frameURL) { var µburi = µb.URI; this.pageHostname = µburi.hostnameFromURI(frameURL); this.pageDomain = µburi.domainFromHostname(this.pageHostname) || this.pageHostname; this.rootHostname = rootHostname; this.rootDomain = µburi.domainFromHostname(rootHostname) || rootHostname; // This is part of the filtering evaluation context this.requestURL = this.requestHostname = this.requestType = ''; return this; }; /******************************************************************************/ FrameStore.prototype.dispose = function() { this.pageHostname = this.pageDomain = this.rootHostname = this.rootDomain = this.requestURL = this.requestHostname = this.requestType = ''; if ( frameStoreJunkyard.length < frameStoreJunkyardMax ) { frameStoreJunkyard.push(this); } return null; }; /******************************************************************************/ /******************************************************************************/ // To mitigate memory churning var pageStoreJunkyard = []; var pageStoreJunkyardMax = 10; /******************************************************************************/ // Cache only what is worth it if logging is disabled // http://jsperf.com/string-indexof-vs-object var collapsibleRequestTypes = 'image sub_frame object'; /******************************************************************************/ var PageStore = function(tabId, pageURL) { this.init(tabId, pageURL); }; /******************************************************************************/ PageStore.factory = function(tabId, pageURL) { var entry = pageStoreJunkyard.pop(); if ( entry === undefined ) { entry = new PageStore(tabId, pageURL); } else { entry.init(tabId, pageURL); } return entry; }; /******************************************************************************/ PageStore.prototype.bitFromRequestType = { '': 1, 'sb': 2, 'sa': 4, 'db': 8, 'da': 16 }; /******************************************************************************/ PageStore.prototype.init = function(tabId, pageURL) { this.tabId = tabId; this.previousPageURL = ''; this.pageURL = pageURL; this.pageHostname = µb.URI.hostnameFromURI(pageURL); // https://github.com/gorhill/uBlock/issues/185 // Use hostname if no domain can be extracted this.pageDomain = µb.URI.domainFromHostname(this.pageHostname) || this.pageHostname; this.rootHostname = this.pageHostname; this.rootDomain = this.pageDomain; // This is part of the filtering evaluation context this.requestURL = this.requestHostname = this.requestType = ''; this.requestHostnames = {}; this.frames = {}; this.netFiltering = true; this.netFilteringReadTime = 0; this.perLoadBlockedRequestCount = 0; this.perLoadAllowedRequestCount = 0; this.skipLocalMirroring = false; this.netFilteringCache = NetFilteringResultCache.factory(); if ( µb.userSettings.logRequests ) { this.netFilteringCache.shelfLife = 30 * 60 * 1000; } return this; }; /******************************************************************************/ PageStore.prototype.reuse = function(pageURL, 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' ) { this.previousPageURL = this.pageURL; this.pageURL = pageURL; this.pageHostname = µb.URI.hostnameFromURI(pageURL); this.pageDomain = µb.URI.domainFromHostname(this.pageHostname) || this.pageHostname; this.rootHostname = this.pageHostname; this.rootDomain = this.pageDomain; // As part of https://github.com/gorhill/uBlock/issues/405 // URL changed, force a re-evaluation of filtering switch this.netFilteringReadTime = 0; return this; } // A new page is completely reloaded from scratch, reset all. this.disposeFrameStores(); this.netFilteringCache = this.netFilteringCache.dispose(); var previousPageURL = this.pageURL; this.init(this.tabId, pageURL); this.previousPageURL = previousPageURL; return this; }; // https://www.youtube.com/watch?v=dltNSbOupgE /******************************************************************************/ PageStore.prototype.dispose = function() { // rhill 2013-11-07: Even though at init time these are reset, I still // need to release the memory taken by these, which can amount to // sizeable enough chunks (especially requests, through the request URL // used as a key). this.pageURL = this.previousPageURL = this.pageHostname = this.pageDomain = this.rootHostname = this.rootDomain = this.requestURL = this.requestHostname = this.requestType = ''; this.requestHostnames = null; this.disposeFrameStores(); this.netFilteringCache = this.netFilteringCache.dispose(); if ( pageStoreJunkyard.length < pageStoreJunkyardMax ) { pageStoreJunkyard.push(this); } return null; }; /******************************************************************************/ PageStore.prototype.disposeFrameStores = function() { var frames = this.frames; for ( var k in frames ) { if ( frames.hasOwnProperty(k) ) { frames[k].dispose(); } } this.frames = {}; }; /******************************************************************************/ PageStore.prototype.addFrame = function(frameId, frameURL) { var frameStore = this.frames[frameId]; if ( frameStore === undefined ) { this.frames[frameId] = frameStore = FrameStore.factory(this.rootHostname, frameURL); //console.debug('µBlock> PageStore.addFrame(%d, "%s")', frameId, frameURL); } return frameStore; }; /******************************************************************************/ PageStore.prototype.getFrame = function(frameId) { return this.frames[frameId]; }; /******************************************************************************/ PageStore.prototype.getNetFilteringSwitch = function() { if ( this.netFilteringReadTime < µb.netWhitelistModifyTime ) { this.netFiltering = µb.getNetFilteringSwitch(this.pageURL); this.netFilteringReadTime = Date.now(); } return this.netFiltering; }; /******************************************************************************/ PageStore.prototype.filterRequest = function(context) { var requestURL = context.requestURL; if ( this.getNetFilteringSwitch() === false ) { this.recordResult(context.requestType, requestURL, ''); return ''; } var entry = this.netFilteringCache.lookup(requestURL); if ( entry !== undefined ) { //console.debug('cache HIT: PageStore.filterRequest("%s")', requestURL); return entry.result; } var result = µb.filterRequest(context); //console.debug('cache MISS: PageStore.filterRequest("%s")', requestURL); this.recordResult(context.requestType, requestURL, result); var requestHostname = context.requestHostname; if ( this.requestHostnames.hasOwnProperty(requestHostname) ) { this.requestHostnames[requestHostname] |= this.bitFromRequestType[result.slice(0, 2)]; } else { this.requestHostnames[requestHostname] = this.bitFromRequestType[result.slice(0, 2)]; } return result; }; /******************************************************************************/ PageStore.prototype.setRequestFlags = function(requestURL, targetBits, valueBits) { var entry = this.netFilteringCache.lookup(requestURL); if ( entry !== undefined ) { entry.flags = (entry.flags & ~targetBits) | (valueBits & targetBits); } }; /******************************************************************************/ PageStore.prototype.recordResult = function(requestType, requestURL, result) { if ( collapsibleRequestTypes.indexOf(requestType) !== -1 || µb.userSettings.logRequests ) { this.netFilteringCache.add(requestURL, result, requestType, 0); } }; /******************************************************************************/ // false: not blocked // true: blocked PageStore.prototype.boolFromResult = function(result) { return typeof result === 'string' && result.charAt(1) === 'b'; }; /******************************************************************************/ PageStore.prototype.updateBadge = function() { var netFiltering = this.getNetFilteringSwitch(); var iconPaths = netFiltering ? { '19': 'img/browsericons/icon19.png', '38': 'img/browsericons/icon38.png' } : { '19': 'img/browsericons/icon19-off.png', '38': 'img/browsericons/icon38-off.png' }; var iconStr = ''; if ( µb.userSettings.showIconBadge && netFiltering && this.perLoadBlockedRequestCount ) { // Safari can't show formatted strings, only integers. if (vAPI.safari) { iconStr = this.perLoadBlockedRequestCount; } else { iconStr = µb.utils.formatCount(this.perLoadBlockedRequestCount); } } vAPI.setIcon(this.tabId, iconPaths, iconStr); }; // https://www.youtube.com/watch?v=drW8p_dTLD4 /******************************************************************************/ return { factory: PageStore.factory }; })(); /******************************************************************************/