diff --git a/platform/chromium/vapi-background.js b/platform/chromium/vapi-background.js index 3dbc379e1..482668922 100644 --- a/platform/chromium/vapi-background.js +++ b/platform/chromium/vapi-background.js @@ -705,7 +705,8 @@ vAPI.onLoadAllCompleted = function() { var i = tabs.length, tab; while ( i-- ) { tab = tabs[i]; - µb.bindTabToPageStats(tab.id, tab.url); + µb.tabContextManager.commit(tab.id, tab.url); + µb.bindTabToPageStats(tab.id); // https://github.com/chrisaljoudi/uBlock/issues/129 scriptStart(tab.id); } diff --git a/platform/firefox/vapi-background.js b/platform/firefox/vapi-background.js index c32e26c8f..35e4b506b 100644 --- a/platform/firefox/vapi-background.js +++ b/platform/firefox/vapi-background.js @@ -1918,6 +1918,7 @@ vAPI.onLoadAllCompleted = function() { var tabId = this.tabs.getTabId(tab); var browser = getBrowserForTab(tab); + µb.tabContextManager.commit(tabId, browser.currentURI.asciiSpec); µb.bindTabToPageStats(tabId, browser.currentURI.asciiSpec); browser.messageManager.sendAsyncMessage( location.host + '-load-completed' diff --git a/src/js/messaging.js b/src/js/messaging.js index 0bef40a23..9caf8b0fb 100644 --- a/src/js/messaging.js +++ b/src/js/messaging.js @@ -203,6 +203,7 @@ var getFirewallRules = function(srcHostname, desHostnames) { /******************************************************************************/ var getStats = function(tabId, tabTitle) { + var tabContext = µb.tabContextManager.lookup(tabId); var r = { advancedUserEnabled: µb.userSettings.advancedUserEnabled, appName: vAPI.app.name, @@ -213,40 +214,38 @@ var getStats = function(tabId, tabTitle) { globalAllowedRequestCount: µb.localSettings.allowedRequestCount, globalBlockedRequestCount: µb.localSettings.blockedRequestCount, netFilteringSwitch: false, - pageURL: '', + rawURL: tabContext.rawURL, + pageURL: tabContext.normalURL, + pageHostname: tabContext.rootHostname, + pageDomain: tabContext.rootDomain, pageAllowedRequestCount: 0, pageBlockedRequestCount: 0, tabId: tabId, tabTitle: tabTitle }; + var pageStore = µb.pageStoreFromTabId(tabId); if ( pageStore ) { - r.rawURL = pageStore.rawURL; - r.pageURL = pageStore.pageURL; - r.pageDomain = pageStore.pageDomain; - r.pageHostname = pageStore.pageHostname; r.pageBlockedRequestCount = pageStore.perLoadBlockedRequestCount; r.pageAllowedRequestCount = pageStore.perLoadAllowedRequestCount; r.netFilteringSwitch = pageStore.getNetFilteringSwitch(); r.hostnameDict = getHostnameDict(pageStore.hostnameToCountMap); r.contentLastModified = pageStore.contentLastModified; - r.firewallRules = getFirewallRules(pageStore.pageHostname, r.hostnameDict); - r.canElementPicker = r.pageHostname.indexOf('.') !== -1; + r.firewallRules = getFirewallRules(tabContext.rootHostname, r.hostnameDict); + r.canElementPicker = tabContext.rootHostname.indexOf('.') !== -1; r.canRequestLog = canRequestLog; - r.noPopups = µb.hnSwitches.evaluateZ('noPopups', r.pageHostname); - r.noStrictBlocking = µb.hnSwitches.evaluateZ('noStrictBlocking', r.pageHostname); - r.noCosmeticFiltering = µb.hnSwitches.evaluateZ('noCosmeticFiltering', r.pageHostname); + r.noPopups = µb.hnSwitches.evaluateZ('noPopups', tabContext.rootHostname); + r.noStrictBlocking = µb.hnSwitches.evaluateZ('noStrictBlocking', tabContext.rootHostname); + r.noCosmeticFiltering = µb.hnSwitches.evaluateZ('noCosmeticFiltering', tabContext.rootHostname); } else { r.hostnameDict = {}; r.firewallRules = getFirewallRules(); } - if ( r.pageHostname ) { - r.matrixIsDirty = !µb.sessionFirewall.hasSameRules( - µb.permanentFirewall, - r.pageHostname, - r.hostnameDict - ); - } + r.matrixIsDirty = !µb.sessionFirewall.hasSameRules( + µb.permanentFirewall, + tabContext.rootHostname, + r.hostnameDict + ); return r; }; @@ -473,15 +472,7 @@ var filterRequests = function(pageStore, details) { var isBlockResult = µb.isBlockResult; // Create evaluation context - var context = { - pageHostname: vAPI.punycodeHostname(details.pageHostname), - pageDomain: µburi.domainFromHostname(details.pageHostname), - rootHostname: pageStore.rootHostname, - rootDomain: pageStore.rootDomain, - requestURL: '', - requestHostname: '', - requestType: '' - }; + var context = pageStore.createContextFromFrameHostname(details.pageHostname); var request; var i = requests.length; diff --git a/src/js/pagestore.js b/src/js/pagestore.js index 635880a41..d4bb7a481 100644 --- a/src/js/pagestore.js +++ b/src/js/pagestore.js @@ -391,6 +391,11 @@ NetFilteringResultCache.prototype.lookup = function(context) { /******************************************************************************/ /******************************************************************************/ +// 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; @@ -419,20 +424,13 @@ 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 = ''; + this.pageHostname = this.pageDomain = ''; if ( frameStoreJunkyard.length < frameStoreJunkyardMax ) { frameStoreJunkyard.push(this); } @@ -448,39 +446,28 @@ var pageStoreJunkyardMax = 10; /******************************************************************************/ -var PageStore = function(tabId, rawURL, pageURL) { - this.init(tabId, rawURL, pageURL); +var PageStore = function(tabId) { + this.init(tabId); }; /******************************************************************************/ -PageStore.factory = function(tabId, rawURL, pageURL) { +PageStore.factory = function(tabId) { var entry = pageStoreJunkyard.pop(); if ( entry === undefined ) { - entry = new PageStore(tabId, rawURL, pageURL); + entry = new PageStore(tabId); } else { - entry.init(tabId, rawURL, pageURL); + entry.init(tabId); } return entry; }; /******************************************************************************/ -PageStore.prototype.init = function(tabId, rawURL, pageURL) { +PageStore.prototype.init = function(tabId) { + var tabContext = µb.tabContextManager.lookup(tabId); this.tabId = tabId; - this.rawURL = rawURL; - this.pageURL = pageURL; - this.pageHostname = µb.URI.hostnameFromURI(pageURL); - - // https://github.com/chrisaljoudi/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.tabHostname = tabContext.rootHostname; this.hostnameToCountMap = {}; this.contentLastModified = 0; this.frames = {}; @@ -489,13 +476,13 @@ PageStore.prototype.init = function(tabId, rawURL, pageURL) { this.perLoadBlockedRequestCount = 0; this.perLoadAllowedRequestCount = 0; this.hiddenElementCount = ''; // Empty string means "unknown" - this.skipLocalMirroring = false; this.netFilteringCache = NetFilteringResultCache.factory(); // Support `elemhide` filter option. Called at this point so the required // context is all setup at this point. + var context = this.createContextFromPage(); this.skipCosmeticFiltering = µb.staticNetFilteringEngine - .matchStringExactType(this, pageURL, 'cosmetic-filtering') + .matchStringExactType(context, tabContext.normalURL, 'cosmetic-filtering') .charAt(1) === 'b'; // Preserve old buffer if there is one already, it may be in use, and @@ -509,7 +496,7 @@ PageStore.prototype.init = function(tabId, rawURL, pageURL) { /******************************************************************************/ -PageStore.prototype.reuse = function(rawURL, pageURL, context) { +PageStore.prototype.reuse = function(context) { // We can't do this: when force refreshing a page, the page store data // needs to be reset //if ( pageURL === this.pageURL ) { @@ -517,8 +504,8 @@ PageStore.prototype.reuse = function(rawURL, pageURL, context) { //} // If the hostname changes, we can't merely just update the context. - var pageHostname = µb.URI.hostnameFromURI(pageURL); - if ( pageHostname !== this.pageHostname ) { + var tabContext = µb.tabContextManager.lookup(this.tabId); + if ( tabContext.rootHostname !== this.tabHostname ) { context = ''; } @@ -528,19 +515,16 @@ PageStore.prototype.reuse = function(rawURL, pageURL, context) { // 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.rawURL = rawURL; - this.pageURL = pageURL; - // As part of https://github.com/chrisaljoudi/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(); - this.init(this.tabId, rawURL, pageURL); + this.init(this.tabId); return this; }; @@ -553,10 +537,6 @@ PageStore.prototype.dispose = function() { // 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.rawURL = this.pageURL = - this.pageHostname = this.pageDomain = - this.rootHostname = this.rootDomain = - this.requestURL = this.requestHostname = this.requestType = ''; this.hostnameToCountMap = null; this.disposeFrameStores(); this.netFilteringCache = this.netFilteringCache.dispose(); @@ -598,36 +578,78 @@ PageStore.prototype.setFrame = function(frameId, frameURL) { /******************************************************************************/ +PageStore.prototype.createContextFromPage = function() { + var context = new µb.tabContextManager.createContext(this.tabId); + context.pageHostname = context.rootHostname; + context.pageDomain = context.rootDomain; + return context; +}; + +PageStore.prototype.createContextFromFrameId = function(frameId) { + var context = new µb.tabContextManager.createContext(this.tabId); + if ( this.frames.hasOwnProperty(frameId) ) { + var frameStore = this.frames[frameId]; + 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 = new µb.tabContextManager.createContext(this.tabId); + context.pageHostname = frameHostname; + context.pageDomain = µb.URI.domainFromHostname(frameHostname) || frameHostname; + return context; +}; + +/******************************************************************************/ + PageStore.prototype.getNetFilteringSwitch = function() { + var tabContext = µb.tabContextManager.lookup(this.tabId); + if ( + this.netFilteringReadTime > µb.netWhitelistModifyTime && + this.netFilteringReadTime > tabContext.modifyTime + ) { + return this.netFiltering; + } + // https://github.com/chrisaljoudi/uBlock/issues/1078 // Use both the raw and normalized URLs. - if ( this.netFilteringReadTime < µb.netWhitelistModifyTime ) { - this.netFiltering = µb.getNetFilteringSwitch(this.pageURL); - if ( this.netFiltering && this.rawURL !== this.pageURL ) { - this.netFiltering = µb.getNetFilteringSwitch(this.rawURL); - } - this.netFilteringReadTime = Date.now(); + this.netFiltering = µb.getNetFilteringSwitch(tabContext.normalURL); + if ( this.netFiltering && tabContext.rawURL !== tabContext.pageURL ) { + this.netFiltering = µb.getNetFilteringSwitch(tabContext.rawURL); } + this.netFilteringReadTime = Date.now(); return this.netFiltering; }; /******************************************************************************/ PageStore.prototype.getSpecificCosmeticFilteringSwitch = function() { - return this.getNetFilteringSwitch() && - µb.hnSwitches.evaluateZ('noCosmeticFiltering', this.rootHostname) !== true && - (µb.userSettings.advancedUserEnabled && - µb.sessionFirewall.mustAllowCellZY(this.rootHostname, this.rootHostname, '*')) === false; + if ( this.getNetFilteringSwitch() === false ) { + return false; + } + + var tabContext = µb.tabContextManager.lookup(this.tabId); + + if ( µb.hnSwitches.evaluateZ('noCosmeticFiltering', tabContext.rootHostname) ) { + return false; + } + + return µb.userSettings.advancedUserEnabled === false || + µb.sessionFirewall.mustAllowCellZY(tabContext.rootHostname, tabContext.rootHostname, '*') === false; }; /******************************************************************************/ PageStore.prototype.getGenericCosmeticFilteringSwitch = function() { - return this.getNetFilteringSwitch() && - this.skipCosmeticFiltering === false && - µb.hnSwitches.evaluateZ('noCosmeticFiltering', this.rootHostname) !== true && - (µb.userSettings.advancedUserEnabled && - µb.sessionFirewall.mustAllowCellZY(this.rootHostname, this.rootHostname, '*')) === false; + if ( this.skipCosmeticFiltering ) { + return false; + } + return this.getSpecificCosmeticFilteringSwitch(); }; /******************************************************************************/ @@ -662,7 +684,11 @@ PageStore.prototype.filterRequest = function(context) { // We evaluate dynamic filtering first, and hopefully we can skip // evaluation of static filtering. if ( µb.userSettings.advancedUserEnabled ) { - var df = µb.sessionFirewall.evaluateCellZY(context.rootHostname, context.requestHostname, context.requestType); + var df = µb.sessionFirewall.evaluateCellZY( + context.rootHostname, + context.requestHostname, + context.requestType + ); if ( df.mustBlockOrAllow() ) { result = df.toFilterString(); } @@ -690,16 +716,7 @@ var collapsibleRequestTypes = 'image sub_frame object'; /******************************************************************************/ PageStore.prototype.filterRequestNoCache = function(context) { - - // https://github.com/chrisaljoudi/uBlock/pull/1209 - // Not ideal, but until something better is figured, this solves the issue. - // Long term, I have some ideas I would like to test to take care of those - // cases for which a page store may not have been definitely bound to a tab. - if ( context.overrideStore ) { - if ( µb.getNetFilteringSwitch(context.requestURL) === false ) { - return ''; - } - } else if ( this.getNetFilteringSwitch() === false ) { + if ( this.getNetFilteringSwitch() === false ) { return ''; } @@ -711,7 +728,11 @@ PageStore.prototype.filterRequestNoCache = function(context) { // We evaluate dynamic filtering first, and hopefully we can skip // evaluation of static filtering. if ( µb.userSettings.advancedUserEnabled ) { - var df = µb.sessionFirewall.evaluateCellZY(context.rootHostname, context.requestHostname, context.requestType); + var df = µb.sessionFirewall.evaluateCellZY( + context.rootHostname, + context.requestHostname, + context.requestType + ); if ( df.mustBlockOrAllow() ) { result = df.toFilterString(); } @@ -733,7 +754,7 @@ PageStore.prototype.logRequest = function(context, result) { // be prepared to handle invalid requestHostname, I've seen this // happen: http://./ if ( requestHostname === '' ) { - requestHostname = context.pageHostname; + requestHostname = context.rootHostname; } var now = Date.now(); if ( this.hostnameToCountMap.hasOwnProperty(requestHostname) === false ) { @@ -756,22 +777,6 @@ PageStore.prototype.logRequest = function(context, result) { /******************************************************************************/ -PageStore.prototype.toMirrorURL = function(requestURL) { - // https://github.com/chrisaljoudi/uBlock/issues/351 - // Bypass experimental features when uBlock is disabled for a site - if ( µb.userSettings.experimentalEnabled === false || - this.getNetFilteringSwitch() === false || - this.skipLocalMirroring ) { - return ''; - } - - // https://code.google.com/p/chromium/issues/detail?id=387198 - // Not all redirects will succeed, until bug above is fixed. - return µb.mirrors.toURL(requestURL, true); -}; - -/******************************************************************************/ - PageStore.prototype.updateBadge = function() { var netFiltering = this.getNetFilteringSwitch(); var badge = ''; diff --git a/src/js/tab.js b/src/js/tab.js index f48ac4b75..b40de5f15 100644 --- a/src/js/tab.js +++ b/src/js/tab.js @@ -32,6 +32,337 @@ var µb = µBlock; +// https://github.com/gorhill/httpswitchboard/issues/303 +// Some kind of trick going on here: +// Any scheme other than 'http' and 'https' is remapped into a fake +// URL which trick the rest of µBlock into being able to process an +// otherwise unmanageable scheme. µBlock needs web page to have a proper +// hostname to work properly, so just like the 'chromium-behind-the-scene' +// fake domain name, we map unknown schemes into a fake '{scheme}-scheme' +// hostname. This way, for a specific scheme you can create scope with +// rules which will apply only to that scheme. + +/******************************************************************************/ +/******************************************************************************/ + +µb.normalizePageURL = function(tabId, pageURL) { + if ( vAPI.isBehindTheSceneTabId(tabId) ) { + return 'http://behind-the-scene/'; + } + var uri = this.URI.set(pageURL); + var scheme = uri.scheme; + if ( scheme === 'https' || scheme === 'http' ) { + return uri.normalizedURI(); + } + + var url = 'http://' + scheme + '-scheme/'; + + if ( uri.hostname !== '' ) { + url += uri.hostname + '/'; + } + + return url; +}; + +/******************************************************************************/ +/****************************************************************************** + +To keep track from which context *exactly* network requests are made. This is +often tricky for various reasons, and the challenge is not specific to one +browser. + +The time at which a URL is assigned to a tab and the time when a network +request for a root document is made must be assumed to be unrelated: it's all +asynchronous. There is no guaranteed order in which the two events are fired. + +Also, other "anomalies" can occur: + +- a network request for a root document is fired without the corresponding +tab being really assigned a new URL + + +- a network request for a secondary resource is labeled with a tab id for +which no root document was pulled for that tab. + + +- a network request for a secondary resource is made without the root +document to which it belongs being formally bound yet to the proper tab id, +causing a bad scope to be used for filtering purpose. + + + +So the solution here is to keep a lightweight data structure which only +purpose is to keep track as accurately as possible of which root document +belongs to which tab. That's the only purpose, and because of this, there are +no restrictions for when the URL of a root document can be associated to a tab. + +Before, the PageStore object was trying to deal with this, but it had to +enforce some restrictions so as to not descend into one of the above issues, or +other issues. The PageStore object can only be associated with a tab for which +a definitive navigation event occurred, because it collects information about +what occurred in the tab (for example, the number of requests blocked for a +page). + +The TabContext objects do not suffer this restriction, and as a result they +offer the most reliable picture of which root document URL is really associated +to which tab. Moreover, the TabObject can undo an association from a root +document, and automatically re-associate with the next most recent. This takes +care of . + +The PageStore object no longer cache the various information about which +root document it is currently bound. When it needs to find out, it will always +defer to the TabContext object, which will provide the real answer. This takes +case of . In effect, the +master switch and dynamic filtering rules can be evaluated now properly even +in the absence of a PageStore object, this was not the case before. + +Also, the TabContext object will try its best to find a good candidate root +document URL for when none exists. This takes care of +. + +The TabContext manager is self-contained, and it takes care to properly +housekeep itself. + +*/ + +µb.tabContextManager = (function() { + var tabContexts = Object.create(null); + + // https://github.com/chrisaljoudi/uBlock/issues/1001 + // This is to be used as last-resort fallback in case a tab is found to not + // be bound while network requests are fired for the tab. + var mostRecentRootDocURL = ''; + var mostRecentRootDocURLTimestamp = 0; + + var gcPeriod = 10 * 60 * 1000; + + var TabContext = function(tabId) { + this.tabId = tabId; + this.stack = []; + this.rawURL = + this.normalURL = + this.rootHostname = + this.rootDomain = ''; + this.timer = null; + this.onTabCallback = null; + this.onTimerCallback = null; + + tabContexts[tabId] = this; + }; + + TabContext.prototype.destroy = function() { + if ( vAPI.isBehindTheSceneTabId(this.tabId) ) { + return; + } + if ( this.timer !== null ) { + clearTimeout(this.timer); + this.timer = null; + } + delete tabContexts[this.tabId]; + }; + + TabContext.prototype.onTab = function(tab) { + if ( tab ) { + this.timer = setTimeout(this.onTimerCallback, gcPeriod); + } else { + this.destroy(); + } + }; + + TabContext.prototype.onTimer = function() { + this.timer = null; + if ( vAPI.isBehindTheSceneTabId(this.tabId) ) { + return; + } + vAPI.tabs.get(this.tabId, this.onTabCallback); + }; + + // This takes care of orphanized tab contexts. Can't be started for all + // contexts, as the behind-the-scene context is permanent -- so we do not + // want to slush it. + TabContext.prototype.autodestroy = function() { + if ( vAPI.isBehindTheSceneTabId(this.tabId) ) { + return; + } + this.onTabCallback = this.onTab.bind(this); + this.onTimerCallback = this.onTimer.bind(this); + this.timer = setTimeout(this.onTimerCallback, gcPeriod); + }; + + // Update just force all properties to be updated to match the most current + // root URL. + TabContext.prototype.update = function() { + if ( this.stack.length === 0 ) { + this.rawURL = this.normalURL = this.rootHostname = this.rootDomain = ''; + } else { + this.rawURL = this.stack[this.stack.length - 1]; + this.normalURL = µb.normalizePageURL(this.tabId, this.rawURL); + this.rootHostname = µb.URI.hostnameFromURI(this.normalURL); + this.rootDomain = µb.URI.domainFromHostname(this.rootHostname); + } + }; + + // Called whenever a candidate root URL is spotted for the tab. + TabContext.prototype.push = function(url) { + if ( vAPI.isBehindTheSceneTabId(this.tabId) ) { + return; + } + this.stack.push(url); + this.update(); + }; + + // Called when a former push is a false positive: + // https://github.com/chrisaljoudi/uBlock/issues/516 + TabContext.prototype.unpush = function(url) { + if ( vAPI.isBehindTheSceneTabId(this.tabId) ) { + return; + } + // We are not going to unpush if there is no other candidate, the + // point of unpush is to make space for a better candidate. + if ( this.stack.length === 1 ) { + return; + } + var pos = this.stack.indexOf(url); + if ( pos === -1 ) { + return; + } + this.stack.splice(pos, 1); + if ( this.stack.length === 0 ) { + this.destroy(); + return; + } + if ( pos !== this.stack.length ) { + return; + } + this.update(); + }; + + // This tells that the url is definitely the one to be associated with the + // tab, there is no longer any ambiguity about which root URL is really + // sitting in which tab. + TabContext.prototype.commit = function(url) { + if ( vAPI.isBehindTheSceneTabId(this.tabId) ) { + return; + } + this.stack = [url]; + this.update(); + }; + + // These are to be used for the API of the tab context manager. + + var push = function(tabId, url) { + var entry = tabContexts[tabId]; + if ( entry === undefined ) { + entry = new TabContext(tabId); + entry.autodestroy(); + } + entry.push(url); + mostRecentRootDocURL = url; + mostRecentRootDocURLTimestamp = Date.now(); + return entry; + }; + + // Find a tab context for a specific tab. If none is found, attempt to + // fix this. When all fail, the behind-the-scene context is returned. + var lookup = function(tabId, url) { + var entry; + if ( url !== undefined ) { + entry = push(tabId, url); + } else { + entry = tabContexts[tabId]; + } + if ( entry !== undefined ) { + return entry; + } + // https://github.com/chrisaljoudi/uBlock/issues/1025 + // Google Hangout popup opens without a root frame. So for now we will + // just discard that best-guess root frame if it is too far in the + // future, at which point it ceases to be a "best guess". + if ( mostRecentRootDocURL !== '' && mostRecentRootDocURLTimestamp + 500 < Date.now() ) { + mostRecentRootDocURL = ''; + } + // https://github.com/chrisaljoudi/uBlock/issues/1001 + // Not a behind-the-scene request, yet no page store found for the + // tab id: we will thus bind the last-seen root document to the + // unbound tab. It's a guess, but better than ending up filtering + // nothing at all. + if ( mostRecentRootDocURL !== '' ) { + return push(tabId, mostRecentRootDocURL); + } + // If all else fail at finding a page store, re-categorize the + // request as behind-the-scene. At least this ensures that ultimately + // the user can still inspect/filter those net requests which were + // about to fall through the cracks. + // Example: Chromium + case #12 at + // http://raymondhill.net/ublock/popup.html + return tabContexts[vAPI.noTabId]; + }; + + var commit = function(tabId, url) { + var entry = tabContexts[tabId]; + if ( entry === undefined ) { + entry = push(tabId, url); + } else { + entry.commit(url); + } + return entry; + }; + + var unpush = function(tabId, url) { + var entry = tabContexts[tabId]; + if ( entry !== undefined ) { + entry.unpush(url); + } + }; + + var destroy = function(tabId) { + var entry = tabContexts[tabId]; + if ( entry !== undefined ) { + entry.destroy(); + } + }; + + var exists = function(tabId) { + return tabContexts[tabId] !== undefined; + }; + + // Behind-the-scene tab context + (function() { + var entry = new TabContext(vAPI.noTabId); + entry.stack.push(''); + entry.rawURL = ''; + entry.normalURL = µb.normalizePageURL(entry.tabId); + entry.rootHostname = µb.URI.hostnameFromURI(entry.normalURL); + entry.rootDomain = µb.URI.domainFromHostname(entry.rootHostname); + })(); + + // Context object, typically to be used to feed filtering engines. + var Context = function(tabId) { + var tabContext = lookup(tabId); + this.rootHostname = tabContext.rootHostname; + this.rootDomain = tabContext.rootDomain; + this.pageHostname = + this.pageDomain = + this.requestURL = + this.requestHostname = + this.requestDomain = ''; + }; + + var createContext = function(tabId) { + return new Context(tabId); + }; + + return { + push: push, + unpush: unpush, + commit: commit, + lookup: lookup, + destroy: destroy, + exists: exists, + createContext: createContext + }; +})(); + /******************************************************************************/ /******************************************************************************/ @@ -42,7 +373,8 @@ vAPI.tabs.onNavigation = function(details) { if ( details.frameId !== 0 ) { return; } - var pageStore = µb.bindTabToPageStats(details.tabId, details.url, 'afterNavigate'); + var tabContext = µb.tabContextManager.commit(details.tabId, details.url); + var pageStore = µb.bindTabToPageStats(details.tabId, 'afterNavigate'); // https://github.com/chrisaljoudi/uBlock/issues/630 // The hostname of the bound document must always be present in the @@ -51,8 +383,8 @@ vAPI.tabs.onNavigation = function(details) { // TODO: Eventually, we will have to use an API to check whether a scheme // is supported as I suspect we are going to start to see `ws`, `wss` // as well soon. - if ( pageStore && details.url.lastIndexOf('http', 0) === 0 ) { - pageStore.hostnameToCountMap[pageStore.pageHostname] = 0; + if ( pageStore && tabContext.rawURL.lastIndexOf('http', 0) === 0 ) { + pageStore.hostnameToCountMap[tabContext.rootHostname] = 0; } }; @@ -69,7 +401,8 @@ vAPI.tabs.onUpdated = function(tabId, changeInfo, tab) { if ( !changeInfo.url ) { return; } - µb.bindTabToPageStats(tabId, changeInfo.url, 'tabUpdated'); + µb.tabContextManager.commit(tabId, changeInfo.url); + µb.bindTabToPageStats(tabId, 'tabUpdated'); }; /******************************************************************************/ @@ -166,50 +499,15 @@ vAPI.tabs.registerListeners(); /******************************************************************************/ /******************************************************************************/ -// https://github.com/gorhill/httpswitchboard/issues/303 -// Some kind of trick going on here: -// Any scheme other than 'http' and 'https' is remapped into a fake -// URL which trick the rest of µBlock into being able to process an -// otherwise unmanageable scheme. µBlock needs web page to have a proper -// hostname to work properly, so just like the 'chromium-behind-the-scene' -// fake domain name, we map unknown schemes into a fake '{scheme}-scheme' -// hostname. This way, for a specific scheme you can create scope with -// rules which will apply only to that scheme. - -µb.normalizePageURL = function(tabId, pageURL) { - if ( vAPI.isBehindTheSceneTabId(tabId) ) { - return 'http://behind-the-scene/'; - } - var uri = this.URI.set(pageURL); - var scheme = uri.scheme; - if ( scheme === 'https' || scheme === 'http' ) { - return uri.normalizedURI(); - } - - var url = 'http://' + scheme + '-scheme/'; - - if ( uri.hostname !== '' ) { - url += uri.hostname + '/'; - } - - return url; -}; - -/******************************************************************************/ - // Create an entry for the tab if it doesn't exist. -µb.bindTabToPageStats = function(tabId, pageURL, context) { +µb.bindTabToPageStats = function(tabId, context) { if ( vAPI.isBehindTheSceneTabId(tabId) === false ) { this.updateBadgeAsync(tabId); } - // https://github.com/gorhill/httpswitchboard/issues/303 - // Normalize page URL - var normalURL = this.normalizePageURL(tabId, pageURL); - // Do not create a page store for URLs which are of no interests - if ( normalURL === '' ) { + if ( µb.tabContextManager.exists(tabId) === false ) { this.unbindTabFromPageStats(tabId); return null; } @@ -219,7 +517,7 @@ vAPI.tabs.registerListeners(); // Tab is not bound if ( !pageStore ) { - return this.pageStores[tabId] = this.PageStore.factory(tabId, pageURL, normalURL); + return this.pageStores[tabId] = this.PageStore.factory(tabId); } // https://github.com/chrisaljoudi/uBlock/issues/516 @@ -229,7 +527,7 @@ vAPI.tabs.registerListeners(); } // https://github.com/gorhill/uBlock/issues/516 - // If context if 'beforeRequest', do not rebind + // If context if 'beforeRequest', do not rebind, wait for confirmation. if ( context === 'beforeRequest' ) { return pageStore; } @@ -237,11 +535,13 @@ vAPI.tabs.registerListeners(); // Rebind according to context. We rebind even if the URL did not change, // as maybe the tab was force-reloaded, in which case the page stats must // be all reset. - pageStore.reuse(pageURL, normalURL, context); + pageStore.reuse(context); return pageStore; }; +/******************************************************************************/ + µb.unbindTabFromPageStats = function(tabId) { //console.debug('µBlock> unbindTabFromPageStats(%d)', tabId); var pageStore = this.pageStores[tabId]; @@ -273,11 +573,7 @@ vAPI.tabs.registerListeners(); // Permanent page store for behind-the-scene requests. Must never be removed. -µb.pageStores[vAPI.noTabId] = µb.PageStore.factory( - vAPI.noTabId, - '', - µb.normalizePageURL(vAPI.noTabId) -); +µb.pageStores[vAPI.noTabId] = µb.PageStore.factory(vAPI.noTabId); /******************************************************************************/ /******************************************************************************/ diff --git a/src/js/traffic.js b/src/js/traffic.js index 5802a367a..a24fe5a65 100644 --- a/src/js/traffic.js +++ b/src/js/traffic.js @@ -33,16 +33,6 @@ var exports = {}; -// https://github.com/chrisaljoudi/uBlock/issues/1001 -// This is to be used as last-resort fallback in case a tab is found to not -// be bound while network requests are fired for the tab. - -var mostRecentRootDocURLTimestamp = 0; -var mostRecentRootDocURL = ''; - - -var documentWhitelists = Object.create(null); - /******************************************************************************/ // Intercept and filter web requests. @@ -70,36 +60,14 @@ var onBeforeRequest = function(details) { var µb = µBlock; var pageStore = µb.pageStoreFromTabId(tabId); if ( !pageStore ) { - // https://github.com/chrisaljoudi/uBlock/issues/1025 - // Google Hangout popup opens without a root frame. So for now we will - // just discard that best-guess root frame if it is too far in the - // future, at which point it ceases to be a "best guess". - if ( (Date.now() - mostRecentRootDocURLTimestamp) >= 500 ) { - mostRecentRootDocURL = ''; - } - // https://github.com/chrisaljoudi/uBlock/issues/1001 - // Not a behind-the-scene request, yet no page store found for the - // tab id: we will thus bind the last-seen root document to the - // unbound tab. It's a guess, but better than ending up filtering - // nothing at all. - if ( mostRecentRootDocURL !== '' ) { - vAPI.tabs.onNavigation({ tabId: tabId, frameId: 0, url: mostRecentRootDocURL }); - pageStore = µb.pageStoreFromTabId(tabId); - } - // If all else fail at finding a page store, re-categorize the - // request as behind-the-scene. At least this ensures that ultimately - // the user can still inspect/filter those net requests which were - // about to fall through the cracks. - // Example: Chromium + case #12 at - // http://raymondhill.net/ublock/popup.html - if ( !pageStore ) { + var tabContext = µb.tabContextManager.lookup(tabId); + if ( vAPI.isBehindTheSceneTabId(tabContext.tabId) ) { return onBeforeBehindTheSceneRequest(details); } + vAPI.tabs.onNavigation({ tabId: tabId, frameId: 0, url: tabContext.rawURL }); + pageStore = µb.pageStoreFromTabId(tabId); } - // https://github.com/chrisaljoudi/uBlock/issues/114 - var requestContext = pageStore; - var frameStore; // https://github.com/chrisaljoudi/uBlock/issues/886 // For requests of type `sub_frame`, the parent frame id must be used // to lookup the proper context: @@ -109,11 +77,9 @@ var onBeforeRequest = function(details) { // > (ref: https://developer.chrome.com/extensions/webRequest) var isFrame = requestType === 'sub_frame'; var frameId = isFrame ? details.parentFrameId : details.frameId; - if ( frameId > 0 ) { - if ( frameStore = pageStore.getFrame(frameId) ) { - requestContext = frameStore; - } - } + + // https://github.com/chrisaljoudi/uBlock/issues/114 + var requestContext = pageStore.createContextFromFrameId(frameId); // Setup context and evaluate var requestURL = details.url; @@ -125,6 +91,8 @@ var onBeforeRequest = function(details) { // Possible outcomes: blocked, allowed-passthru, allowed-mirror + pageStore.logRequest(requestContext, result); + // Not blocked if ( µb.isAllowResult(result) ) { //console.debug('traffic.js > onBeforeRequest(): ALLOW "%s" (%o) because "%s"', details.url, details, result); @@ -139,27 +107,12 @@ var onBeforeRequest = function(details) { } } - // https://code.google.com/p/chromium/issues/detail?id=387198 - // Not all redirects will succeed, until bug above is fixed. - // https://github.com/chrisaljoudi/uBlock/issues/540 - // Disabling local mirroring for the time being - //var redirectURL = pageStore.toMirrorURL(requestURL); - //if ( redirectURL !== '' ) { - // pageStore.logRequest(requestContext, 'ma:'); - //console.debug('traffic.js > "%s" redirected to "%s..."', requestURL.slice(0, 50), redirectURL.slice(0, 50)); - // return { redirectUrl: redirectURL }; - //} - - pageStore.logRequest(requestContext, result); - return; } // Blocked //console.debug('traffic.js > onBeforeRequest(): BLOCK "%s" (%o) because "%s"', details.url, details, result); - pageStore.logRequest(requestContext, result); - // https://github.com/chrisaljoudi/uBlock/issues/905#issuecomment-76543649 // No point updating the badge if it's not being displayed. if ( µb.userSettings.showIconBadge ) { @@ -176,16 +129,16 @@ var onBeforeRequest = function(details) { /******************************************************************************/ var onBeforeRootFrameRequest = function(details) { + var tabId = details.tabId; var requestURL = details.url; + var µb = µBlock; - mostRecentRootDocURL = requestURL; - mostRecentRootDocURLTimestamp = Date.now(); + µb.tabContextManager.push(tabId, requestURL); // Special handling for root document. // https://github.com/chrisaljoudi/uBlock/issues/1001 // This must be executed regardless of whether the request is // behind-the-scene - var µb = µBlock; var requestHostname = details.hostname; var requestDomain = µb.URI.domainFromHostname(requestHostname) || requestHostname; var context = { @@ -222,7 +175,7 @@ var onBeforeRootFrameRequest = function(details) { } // Log - var pageStore = µb.bindTabToPageStats(details.tabId, requestURL, 'beforeRequest'); + var pageStore = µb.bindTabToPageStats(tabId, 'beforeRequest'); if ( pageStore ) { pageStore.logRequest(context, result); } @@ -240,7 +193,7 @@ var onBeforeRootFrameRequest = function(details) { why: result })); - vAPI.tabs.replace(details.tabId, vAPI.getURL('document-blocked.html?details=') + query); + vAPI.tabs.replace(tabId, vAPI.getURL('document-blocked.html?details=') + query); return { cancel: true }; }; @@ -302,9 +255,10 @@ var onBeforeBehindTheSceneRequest = function(details) { return; } - pageStore.requestURL = details.url; - pageStore.requestHostname = details.hostname; - pageStore.requestType = details.type; + var context = pageStore.createContextFromPage(); + context.requestURL = details.url; + context.requestHostname = details.hostname; + context.requestType = details.type; // Blocking behind-the-scene requests can break a lot of stuff: prevent // browser updates, prevent extension updates, prevent extensions from @@ -312,10 +266,10 @@ var onBeforeBehindTheSceneRequest = function(details) { // So we filter if and only if the "advanced user" mode is selected var result = ''; if ( µb.userSettings.advancedUserEnabled ) { - result = pageStore.filterRequestNoCache(pageStore); + result = pageStore.filterRequestNoCache(context); } - pageStore.logRequest(pageStore, result); + pageStore.logRequest(context, result); // Not blocked if ( µb.isAllowResult(result) ) { @@ -340,56 +294,68 @@ var onHeadersReceived = function(details) { return; } - var requestURL = details.url; + // Special handling for root document. + if ( details.type === 'main_frame' ) { + return onRootFrameHeadersReceived(details); + } + + // If we reach this point, we are dealing with a sub_frame // Lookup the page store associated with this tab id. var µb = µBlock; var pageStore = µb.pageStoreFromTabId(tabId); if ( !pageStore ) { - if ( details.type === 'main_frame' ) { - pageStore = µb.bindTabToPageStats(tabId, requestURL, 'beforeRequest'); - } - if ( !pageStore ) { - return; - } + return; } - // https://github.com/chrisaljoudi/uBlock/issues/384 - // https://github.com/chrisaljoudi/uBlock/issues/540 - // Disabling local mirroring for the time being - //if ( details.parentFrameId === -1 ) { - // pageStore.skipLocalMirroring = headerStartsWith(details.responseHeaders, 'content-security-policy') !== ''; - //} + // Frame id of frame request is the their own id, while the request is made + // in the context of the parent. + var context = pageStore.createContextFromFrameId(details.parentFrameId); + context.requestURL = details.url + '{inline-script}'; + context.requestHostname = details.hostname; + context.requestType = 'inline-script'; + var result = pageStore.filterRequestNoCache(context); + + pageStore.logRequest(context, result); + + // Don't block + if ( µb.isAllowResult(result) ) { + return; + } + + µb.updateBadgeAsync(tabId); + + details.responseHeaders.push({ + 'name': 'Content-Security-Policy', + 'value': "script-src 'unsafe-eval' *" + }); + + return { 'responseHeaders': details.responseHeaders }; +}; + +/******************************************************************************/ + +var onRootFrameHeadersReceived = function(details) { + var tabId = details.tabId; + var requestURL = details.url; var requestHostname = details.hostname; + var µb = µBlock; - // https://github.com/chrisaljoudi/uBlock/issues/525 - // When we are dealing with the root frame, due to fix to issue #516, it - // is likely the root frame has not been bound yet to the tab, and thus - // we could end up using the context of the previous page for filtering. - // So when the request is that of a root frame, simply create an - // artificial context, this will ensure we are properly filtering - // inline scripts. - var context; - if ( details.parentFrameId === -1 ) { - var contextDomain = µb.URI.domainFromHostname(requestHostname); - // https://github.com/chrisaljoudi/uBlock/pull/1209 - // Using `overrideStore` solves the issue. - context = { - overrideStore: true, - rootHostname: requestHostname, - rootDomain: contextDomain, - pageHostname: requestHostname, - pageDomain: contextDomain - }; - } else { - context = pageStore; + // Check if the main_frame is a download + // ... + if ( headerValue(details.responseHeaders, 'content-disposition').lastIndexOf('attachment', 0) === 0 ) { + µb.tabContextManager.unpush(tabId, requestURL); } - // Concatenating with '{inline-script}' so that the network request cache - // can distinguish from the document itself - // The cache should do whatever it takes to not confuse same - // URLs-different type + // Lookup the page store associated with this tab id. + var pageStore = µb.pageStoreFromTabId(tabId); + if ( !pageStore ) { + pageStore = µb.bindTabToPageStats(tabId, 'beforeRequest'); + } + // I can't think of how pageStore could be null at this point. + + var context = pageStore.createContextFromPage(); context.requestURL = requestURL + '{inline-script}'; context.requestHostname = requestHostname; context.requestType = 'inline-script'; @@ -415,6 +381,18 @@ var onHeadersReceived = function(details) { /******************************************************************************/ +var headerValue = function(headers, name) { + var i = headers.length; + while ( i-- ) { + if ( headers[i].name.toLowerCase() === name ) { + return headers[i].value.trim(); + } + } + return ''; +}; + +/******************************************************************************/ + vAPI.net.onBeforeRequest = { urls: [ 'http://*/*', @@ -476,6 +454,8 @@ var isTemporarilyWhitelisted = function(result, hostname) { return result; }; +var documentWhitelists = Object.create(null); + /******************************************************************************/ exports.temporarilyWhitelistDocument = function(hostname) { diff --git a/src/js/ublock.js b/src/js/ublock.js index e1b259ed4..d8f3ff98f 100644 --- a/src/js/ublock.js +++ b/src/js/ublock.js @@ -340,6 +340,10 @@ var matchWhitelistDirective = function(url, hostname, directive) { µBlock.getHiddenElementCount = function(tabId, callback) { callback = callback || this.noopFunc; + if ( vAPI.isBehindTheSceneTabId(tabId) ) { + callback(); + return; + } vAPI.tabs.injectScript(tabId, { file: 'js/cosmetic-count.js' }, callback); };