From fa3666f85d7dddfc274f6f27d20c6787d8bc43b8 Mon Sep 17 00:00:00 2001 From: Chris Date: Mon, 13 Apr 2015 18:58:50 -0600 Subject: [PATCH] Refactor context-keeping Improves reliability and thoroughness of blocking on all browsers. --- platform/chromium/vapi-background.js | 16 +- platform/firefox/vapi-background.js | 3 +- platform/safari/vapi-background.js | 23 +- platform/safari/vapi-client.js | 2 +- src/background.html | 1 - src/document-blocked.html | 81 ---- src/js/async.js | 2 +- src/js/document-blocked.js | 103 ----- src/js/hnswitches.js | 283 ------------- src/js/messaging.js | 56 +-- src/js/pagestore.js | 188 ++++----- src/js/start.js | 2 - src/js/storage.js | 6 - src/js/tab.js | 600 +++++++++++++++++++-------- src/js/traffic.js | 250 ++++------- src/js/ublock.js | 8 - 16 files changed, 647 insertions(+), 977 deletions(-) delete mode 100644 src/document-blocked.html delete mode 100644 src/js/document-blocked.js delete mode 100644 src/js/hnswitches.js diff --git a/platform/chromium/vapi-background.js b/platform/chromium/vapi-background.js index 1492ed0a8..0f93259d7 100644 --- a/platform/chromium/vapi-background.js +++ b/platform/chromium/vapi-background.js @@ -79,7 +79,7 @@ vAPI.tabs = {}; /******************************************************************************/ -vAPI.isNoTabId = function(tabId) { +vAPI.isBehindTheSceneTabId = function(tabId) { return tabId.toString() === '-1'; }; @@ -102,8 +102,8 @@ vAPI.tabs.registerListeners = function() { var popupCandidates = Object.create(null); var PopupCandidate = function(details) { - this.targetTabId = details.tabId; - this.openerTabId = details.sourceTabId; + this.targetTabId = details.tabId.toString(); + this.openerTabId = details.sourceTabId.toString(); this.targetURL = details.url; this.selfDestructionTimer = null; }; @@ -222,7 +222,12 @@ vAPI.tabs.get = function(tabId, callback) { if ( typeof tabId === 'string' ) { tabId = parseInt(tabId, 10); } - chrome.tabs.get(tabId, onTabReady); + if ( typeof tabId !== 'number' || isNaN(tabId) ) { + onTabReady(null); + } + else { + chrome.tabs.get(tabId, onTabReady); + } return; } var onTabReceived = function(tabs) { @@ -719,7 +724,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 a0d81b173..c28483c95 100644 --- a/platform/firefox/vapi-background.js +++ b/platform/firefox/vapi-background.js @@ -333,7 +333,7 @@ var tabWatcher = { /******************************************************************************/ -vAPI.isNoTabId = function(tabId) { +vAPI.isBehindTheSceneTabId = function(tabId) { return tabId.toString() === '-1'; }; @@ -1986,6 +1986,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/platform/safari/vapi-background.js b/platform/safari/vapi-background.js index 756f858f9..2c311d339 100644 --- a/platform/safari/vapi-background.js +++ b/platform/safari/vapi-background.js @@ -239,7 +239,7 @@ /******************************************************************************/ - vAPI.isNoTabId = function(tabId) { + vAPI.isBehindTheSceneTabId = function(tabId) { return tabId.toString() === this.noTabId; }; @@ -256,13 +256,13 @@ tabId = vAPI.tabs.getTabId(e.target); var details = { targetURL: url, - targetTabId: tabId, + targetTabId: tabId.toString(), openerTabId: vAPI.tabs.popupCandidate }; + vAPI.tabs.popupCandidate = false; if(vAPI.tabs.onPopup(details)) { e.preventDefault(); if(vAPI.tabs.stack[details.openerTabId]) { - vAPI.tabs.popupCandidate = false; vAPI.tabs.stack[details.openerTabId].activate(); } } @@ -731,17 +731,18 @@ } switch(e.message.type) { case "popup": - vAPI.tabs.popupCandidate = vAPI.tabs.getTabId(e.target); - if(e.message.url === "about:blank") { + var openerTabId = vAPI.tabs.getTabId(e.target).toString(); + var shouldBlock = !!vAPI.tabs.onPopup({ + targetURL: e.message.url, + targetTabId: "preempt", + openerTabId: openerTabId + }); + if(shouldBlock) { e.message = false; - return; } else { - e.message = !vAPI.tabs.onPopup({ - targetURL: e.message.url, - targetTabId: 0, - openerTabId: vAPI.tabs.getTabId(e.target) - }); + vAPI.tabs.popupCandidate = openerTabId; + e.message = true; } break; case "popstate": diff --git a/platform/safari/vapi-client.js b/platform/safari/vapi-client.js index b956ffc58..085e5ce03 100644 --- a/platform/safari/vapi-client.js +++ b/platform/safari/vapi-client.js @@ -260,7 +260,7 @@ x.setAttribute('src', block(val, 'image') ? 'data:image/gif;base64,R0lGODlhAQABA return x;\ };\ open = function(u) {\ -return block(u, 'popup') ? null : wo.apply(this, arguments);\ +if(block(u, 'popup')) return {}; else return wo.apply(this, arguments);\ };\ XMLHttpRequest.prototype.open = function(m, u) {\ if(block(u, 'xmlhttprequest')) {throw 'InvalidAccessError'; return;}\ diff --git a/src/background.html b/src/background.html index 07eb25d40..80faa1108 100644 --- a/src/background.html +++ b/src/background.html @@ -19,7 +19,6 @@ - diff --git a/src/document-blocked.html b/src/document-blocked.html deleted file mode 100644 index ee06972d9..000000000 --- a/src/document-blocked.html +++ /dev/null @@ -1,81 +0,0 @@ - - - - - - - - -
-
-

-

-
- -
-

-

-
- -
-

-

-
- -
-

-

-

-
- -
- -
- - - - - - - - diff --git a/src/js/async.js b/src/js/async.js index 7ef5cb2eb..c55a77f08 100644 --- a/src/js/async.js +++ b/src/js/async.js @@ -187,7 +187,7 @@ return asyncJobManager; }; var updateBadgeAsync = function(tabId) { - if ( vAPI.isNoTabId(tabId) ) { + if ( vAPI.isBehindTheSceneTabId(tabId) ) { return; } µb.asyncJobs.add('updateBadge-' + tabId, tabId, updateBadge, 250); diff --git a/src/js/document-blocked.js b/src/js/document-blocked.js deleted file mode 100644 index d548a7c0e..000000000 --- a/src/js/document-blocked.js +++ /dev/null @@ -1,103 +0,0 @@ -/******************************************************************************* - - uBlock - a browser extension to block requests. - Copyright (C) 2015 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/chrisaljoudi/uBlock -*/ - -/* global uDom */ - -/******************************************************************************/ - -(function() { - -'use strict'; - -/******************************************************************************/ - -var messager = vAPI.messaging.channel('document-blocked.js'); -var details = {}; - -(function() { - var matches = /details=([^&]+)/.exec(window.location.search); - if ( matches === null ) { - return; - } - details = JSON.parse(atob(matches[1])); -})(); - -/******************************************************************************/ - -var proceedToURL = function() { - window.location.replace(details.url); -}; - -/******************************************************************************/ - -var proceedTemporary = function() { - messager.send({ - what: 'temporarilyWhitelistDocument', - url: details.url - }, proceedToURL); -}; - -/******************************************************************************/ - -var proceedPermanent = function() { - messager.send({ - what: 'toggleHostnameSwitch', - name: 'dontBlockDoc', - hostname: details.hn, - state: true - }, proceedToURL); -}; - -/******************************************************************************/ - -(function() { - var matches = /^(.*)\{\{hostname\}\}(.*)$/.exec(vAPI.i18n('docblockedProceed')); - if ( matches === null ) { - return; - } - var proceed = uDom('#proceedTemplate').clone(); - proceed.descendants('span:nth-of-type(1)').text(matches[1]); - proceed.descendants('span:nth-of-type(2)').text(details.hn); - proceed.descendants('span:nth-of-type(3)').text(matches[2]); - uDom('#proceed').append(proceed); -})(); - -/******************************************************************************/ - -uDom('.what').text(details.url); -uDom('#why').text(details.why.slice(3)); - -if ( window.history.length > 1 ) { - uDom('#back').on('click', function() { window.history.back(); }); - uDom('#bye').css('display', 'none'); -} else { - uDom('#bye').on('click', function() { window.close(); }); - uDom('#back').css('display', 'none'); -} - -uDom('#proceedTemporary').attr('href', details.url).on('click', proceedTemporary); -uDom('#proceedPermanent').attr('href', details.url).on('click', proceedPermanent); - -/******************************************************************************/ - -})(); - -/******************************************************************************/ diff --git a/src/js/hnswitches.js b/src/js/hnswitches.js deleted file mode 100644 index 13f1f3465..000000000 --- a/src/js/hnswitches.js +++ /dev/null @@ -1,283 +0,0 @@ -/******************************************************************************* - - uBlock - a Chromium browser extension to black/white list requests. - Copyright (C) 2015 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/chrisaljoudi/uBlock -*/ - -/* global punycode, µBlock */ -/* jshint bitwise: false */ - -/******************************************************************************/ - -µBlock.HnSwitches = (function() { - -'use strict'; - -/******************************************************************************/ - -var HnSwitches = function() { - this.reset(); -}; - -/******************************************************************************/ - -var switchBitOffsets = { - 'dontBlockDoc': 0, - 'doBlockAllPopups': 2 -}; - -var switchStateToNameMap = { - '1': 'true', - '2': 'false' -}; - -var nameToSwitchStateMap = { - 'true': 1, - 'false': 2 -}; - -/******************************************************************************/ - -// For performance purpose, as simple tests as possible -var reHostnameVeryCoarse = /[g-z_-]/; -var reIPv4VeryCoarse = /\.\d+$/; - -// http://tools.ietf.org/html/rfc5952 -// 4.3: "MUST be represented in lowercase" -// Also: http://en.wikipedia.org/wiki/IPv6_address#Literal_IPv6_addresses_in_network_resource_identifiers - -var isIPAddress = function(hostname) { - if ( reHostnameVeryCoarse.test(hostname) ) { - return false; - } - if ( reIPv4VeryCoarse.test(hostname) ) { - return true; - } - return hostname.charAt(0) === '['; -}; - -/******************************************************************************/ - -var toBroaderHostname = function(hostname) { - if ( hostname === '*' ) { - return ''; - } - if ( isIPAddress(hostname) ) { - return '*'; - } - var pos = hostname.indexOf('.'); - if ( pos === -1 ) { - return '*'; - } - return hostname.slice(pos + 1); -}; - -HnSwitches.toBroaderHostname = toBroaderHostname; - -/******************************************************************************/ - -HnSwitches.prototype.reset = function() { - this.switches = {}; -}; - -/******************************************************************************/ - -// If value is undefined, the switch is removed - -HnSwitches.prototype.toggle = function(switchName, hostname, newVal) { - var bitOffset = switchBitOffsets[switchName]; - if ( bitOffset === undefined ) { - return false; - } - if ( newVal === this.evaluate(switchName, hostname) ) { - return false; - } - var bits = this.switches[hostname] || 0; - bits &= ~(3 << bitOffset); - bits |= newVal << bitOffset; - if ( bits === 0 ) { - delete this.switches[hostname]; - } else { - this.switches[hostname] = bits; - } - return true; -}; - -/******************************************************************************/ - -HnSwitches.prototype.toggleZ = function(switchName, hostname, newState) { - var bitOffset = switchBitOffsets[switchName]; - if ( bitOffset === undefined ) { - return false; - } - var state = this.evaluateZ(switchName, hostname); - if ( newState === state ) { - return false; - } - if ( newState === undefined ) { - newState = !state; - } - var bits = this.switches[hostname] || 0; - bits &= ~(3 << bitOffset); - if ( bits === 0 ) { - delete this.switches[hostname]; - } else { - this.switches[hostname] = bits; - } - state = this.evaluateZ(switchName, hostname); - if ( state === newState ) { - return true; - } - this.switches[hostname] = bits | ((newState ? 1 : 2) << bitOffset); - return true; -}; - -/******************************************************************************/ - -// 0 = inherit from broader scope, up to default state -// 1 = non-default state -// 2 = forced default state (to override a broader non-default state) - -HnSwitches.prototype.evaluate = function(switchName, hostname) { - var bits = this.switches[hostname] || 0; - if ( bits === 0 ) { - return 0; - } - var bitOffset = switchBitOffsets[switchName]; - if ( bitOffset === undefined ) { - return 0; - } - return (bits >> bitOffset) & 3; -}; - -/******************************************************************************/ - -HnSwitches.prototype.evaluateZ = function(switchName, hostname) { - var bitOffset = switchBitOffsets[switchName]; - if ( bitOffset === undefined ) { - return false; - } - var bits; - var s = hostname; - for (;;) { - bits = this.switches[s] || 0; - if ( bits !== 0 ) { - bits = bits >> bitOffset & 3; - if ( bits !== 0 ) { - return bits === 1; - } - } - s = toBroaderHostname(s); - if ( s === '' ) { - break; - } - } - return false; -}; - -/******************************************************************************/ - -HnSwitches.prototype.toString = function() { - var out = []; - var switchName, val; - var hostname; - for ( hostname in this.switches ) { - if ( this.switches.hasOwnProperty(hostname) === false ) { - continue; - } - for ( switchName in switchBitOffsets ) { - if ( switchBitOffsets.hasOwnProperty(switchName) === false ) { - continue; - } - val = this.evaluate(switchName, hostname); - if ( val === 0 ) { - continue; - } - out.push(switchName + ': ' + hostname + ' ' + switchStateToNameMap[val]); - } - } - return out.join('\n'); -}; - -/******************************************************************************/ - -HnSwitches.prototype.fromString = function(text) { - var textEnd = text.length; - var lineBeg = 0, lineEnd; - var line, pos; - var fields; - var switchName, hostname, state; - - while ( lineBeg < textEnd ) { - lineEnd = text.indexOf('\n', lineBeg); - if ( lineEnd < 0 ) { - lineEnd = text.indexOf('\r', lineBeg); - if ( lineEnd < 0 ) { - lineEnd = textEnd; - } - } - line = text.slice(lineBeg, lineEnd).trim(); - lineBeg = lineEnd + 1; - - pos = line.indexOf('# '); - if ( pos !== -1 ) { - line = line.slice(0, pos).trim(); - } - if ( line === '' ) { - continue; - } - - fields = line.split(/\s+/); - if ( fields.length !== 3 ) { - continue; - } - - switchName = fields[0]; - pos = switchName.indexOf(':'); - if ( pos === -1 ) { - continue; - } - switchName = switchName.slice(0, pos); - if ( switchBitOffsets.hasOwnProperty(switchName) === false ) { - continue; - } - - hostname = punycode.toASCII(fields[1]); - - state = fields[2]; - if ( nameToSwitchStateMap.hasOwnProperty(state) === false ) { - continue; - } - - this.toggle(switchName, hostname, nameToSwitchStateMap[state]); - } -}; - -/******************************************************************************/ - -return HnSwitches; - -/******************************************************************************/ - -})(); - -/******************************************************************************/ - -µBlock.hnSwitches = new µBlock.HnSwitches(); - -/******************************************************************************/ diff --git a/src/js/messaging.js b/src/js/messaging.js index d2ddcdbc4..ed8069fe5 100644 --- a/src/js/messaging.js +++ b/src/js/messaging.js @@ -76,7 +76,7 @@ var onMessage = function(request, sender, callback) { break; case 'reloadTab': - if ( vAPI.isNoTabId(request.tabId) === false ) { + if ( vAPI.isBehindTheSceneTabId(request.tabId) === false ) { vAPI.tabs.reload(request.tabId); if ( request.select && vAPI.tabs.select ) { vAPI.tabs.select(request.tabId); @@ -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,39 +214,35 @@ 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.doBlockAllPopups = µb.hnSwitches.evaluateZ('doBlockAllPopups', r.pageHostname); - r.dontBlockDoc = µb.hnSwitches.evaluateZ('dontBlockDoc', r.pageHostname); } 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; }; @@ -449,16 +446,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; while ( i-- ) { @@ -485,22 +473,18 @@ var onMessage = function(details, sender, callback) { // Sync var response; - var pageStore, frameStore = false; + var pageStore; if ( sender && sender.tab ) { pageStore = µb.pageStoreFromTabId(sender.tab.id); - var frameId = sender.frameId; - if(frameId && frameId > 0) { - frameStore = pageStore.getFrame(frameId); - } } switch ( details.what ) { case 'retrieveGenericCosmeticSelectors': response = { - shutdown: !pageStore || !pageStore.getNetFilteringSwitch() || (frameStore && !frameStore.getNetFilteringSwitch()), + shutdown: !pageStore || !pageStore.getNetFilteringSwitch(), result: null }; - if(pageStore && pageStore.getGenericCosmeticFilteringSwitch()) { + if ( !response.shutdown && pageStore.getGenericCosmeticFilteringSwitch() ) { response.result = µb.cosmeticFilteringEngine.retrieveGenericSelectors(details); } break; @@ -515,7 +499,7 @@ var onMessage = function(details, sender, callback) { shutdown: !pageStore || !pageStore.getNetFilteringSwitch(), result: null }; - if(pageStore) { + if(!response.shutdown) { response.result = filterRequests(pageStore, details); } break; @@ -998,7 +982,6 @@ var backupUserData = function(callback) { filterLists: µb.extractSelectedFilterLists(), netWhitelist: µb.stringFromWhitelist(µb.netWhitelist), dynamicFilteringString: µb.permanentFirewall.toString(), - hostnameSwitchesString: µb.hnSwitches.toString(), userFilters: details.content }; var now = new Date(); @@ -1046,7 +1029,6 @@ var restoreUserData = function(request) { var s = userData.dynamicFilteringString || userData.userSettings.dynamicFilteringString || ''; µb.XAL.keyvalSetOne('dynamicFilteringString', s, onCountdown); - µb.XAL.keyvalSetOne('hostnameSwitchesString', userData.hostnameSwitchesString || '', onCountdown); µb.assets.put('assets/user/filters.txt', userData.userFilters, onCountdown); µb.XAL.keyvalSetMany({ lastRestoreFile: request.file || '', diff --git a/src/js/pagestore.js b/src/js/pagestore.js index 3685783db..f0793c4c8 100644 --- a/src/js/pagestore.js +++ b/src/js/pagestore.js @@ -391,6 +391,9 @@ NetFilteringResultCache.prototype.lookup = function(context) { /******************************************************************************/ /******************************************************************************/ +// FrameStores are just for associating a +// frame ID with a URL. pageHostname is really +// frameHostname. // To mitigate memory churning var frameStoreJunkyard = []; var frameStoreJunkyardMax = 50; @@ -420,22 +423,13 @@ FrameStore.prototype.init = function(rootHostname, frameURL) { this.pageURL = frameURL; 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 = ''; - this.netFiltering = true; - this.netFilteringReadTime = 0; - 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); } @@ -444,55 +438,35 @@ FrameStore.prototype.dispose = function() { /******************************************************************************/ -FrameStore.prototype.getNetFilteringSwitch = function() { - if ( this.netFilteringReadTime < µb.netWhitelistModifyTime ) { - this.netFiltering = µb.getNetFilteringSwitch(this.pageURL); - this.netFilteringReadTime = Date.now(); - } - return this.netFiltering; -}; - -/******************************************************************************/ - // To mitigate memory churning var pageStoreJunkyard = []; 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 = {}; @@ -500,13 +474,13 @@ PageStore.prototype.init = function(tabId, rawURL, pageURL) { this.netFilteringReadTime = 0; this.perLoadBlockedRequestCount = 0; this.perLoadAllowedRequestCount = 0; - 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 @@ -520,7 +494,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 ) { @@ -528,8 +502,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 = ''; } @@ -539,19 +513,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; }; @@ -564,10 +535,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(); @@ -609,34 +576,74 @@ 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.userSettings.advancedUserEnabled && - µb.sessionFirewall.mustAllowCellZY(this.rootHostname, this.rootHostname, '*')) === false; + if ( this.getNetFilteringSwitch() === false ) { + return false; + } + + var tabContext = µb.tabContextManager.lookup(this.tabId); + + return µb.userSettings.advancedUserEnabled === false || + µb.sessionFirewall.mustAllowCellZY(tabContext.rootHostname, tabContext.rootHostname, '*') === false; }; /******************************************************************************/ PageStore.prototype.getGenericCosmeticFilteringSwitch = function() { - return this.getNetFilteringSwitch() && - this.skipCosmeticFiltering === false && - (µb.userSettings.advancedUserEnabled && - µb.sessionFirewall.mustAllowCellZY(this.rootHostname, this.rootHostname, '*')) === false; + if ( this.skipCosmeticFiltering ) { + return false; + } + return this.getSpecificCosmeticFilteringSwitch(); }; /******************************************************************************/ @@ -649,15 +656,8 @@ PageStore.prototype.toggleNetFilteringSwitch = function(url, scope, state) { /******************************************************************************/ PageStore.prototype.filterRequest = function(context) { - if(context.preNavigationHeader) { // sometimes we get inline-script queries before being - // informed of navigation - if(µb.getNetFilteringSwitch(context.requestURL) === false) { - return ''; - } - } - if(this.getNetFilteringSwitch() === false || // if we're turned off (whitelisted) - (typeof context.getNetFilteringSwitch === "function" && // or we're in a frame that's whitelisted - context.getNetFilteringSwitch() === false)) { + + if ( this.getNetFilteringSwitch() === false ) { if ( collapsibleRequestTypes.indexOf(context.requestType) !== -1 ) { this.netFilteringCache.add(context, ''); } @@ -678,7 +678,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.pageHostname, context.requestHostname, context.requestType); + var df = µb.sessionFirewall.evaluateCellZY( + context.rootHostname, + context.requestHostname, + context.requestType + ); if ( df.mustBlockOrAllow() ) { result = df.toFilterString(); } @@ -706,15 +710,7 @@ var collapsibleRequestTypes = 'image sub_frame object'; /******************************************************************************/ PageStore.prototype.filterRequestNoCache = function(context) { - if(context.preNavigationHeader) { // sometimes we get inline-script queries before being - // informed of navigation - if(µb.getNetFilteringSwitch(context.requestURL) === false) { - return ''; - } - } - if(this.getNetFilteringSwitch() === false || // if we're turned off (whitelisted) - (typeof context.getNetFilteringSwitch === "function" && // or we're in a frame that's whitelisted - context.getNetFilteringSwitch() === false)) { + if ( this.getNetFilteringSwitch() === false ) { return ''; } @@ -726,7 +722,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.pageHostname, context.requestHostname, context.requestType); + var df = µb.sessionFirewall.evaluateCellZY( + context.rootHostname, + context.requestHostname, + context.requestType + ); if ( df.mustBlockOrAllow() ) { result = df.toFilterString(); } @@ -748,7 +748,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 ) { @@ -771,22 +771,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/start.js b/src/js/start.js index e1cf44b55..4dfa2d619 100644 --- a/src/js/start.js +++ b/src/js/start.js @@ -137,7 +137,6 @@ var onUserSettingsReady = function(fetched) { µb.contextMenu.toggle(userSettings.contextMenuEnabled); µb.permanentFirewall.fromString(fetched.dynamicFilteringString); µb.sessionFirewall.assign(µb.permanentFirewall); - µb.hnSwitches.fromString(fetched.hostnameSwitchesString); // Remove obsolete setting delete userSettings.logRequests; @@ -216,7 +215,6 @@ return function() { var fetchableProps = { 'compiledMagic': '', 'dynamicFilteringString': '', - 'hostnameSwitchesString': '', 'lastRestoreFile': '', 'lastRestoreTime': 0, 'lastBackupFile': '', diff --git a/src/js/storage.js b/src/js/storage.js index b3188bf7d..6167e0422 100644 --- a/src/js/storage.js +++ b/src/js/storage.js @@ -75,12 +75,6 @@ /******************************************************************************/ -µBlock.saveHostnameSwitches = function() { - this.XAL.keyvalSetOne('hostnameSwitchesString', this.hnSwitches.toString()); -}; - -/******************************************************************************/ - µBlock.saveWhitelist = function() { var bin = { 'netWhitelist': this.stringFromWhitelist(this.netWhitelist) diff --git a/src/js/tab.js b/src/js/tab.js index 7c67350d5..09cf3c7ad 100644 --- a/src/js/tab.js +++ b/src/js/tab.js @@ -32,141 +32,7 @@ var µb = µBlock; -/******************************************************************************/ -/******************************************************************************/ - -// When the DOM content of root frame is loaded, this means the tab -// content has changed. - -vAPI.tabs.onNavigation = function(details) { - if ( details.frameId !== 0 ) { - return; - } - var pageStore = µb.bindTabToPageStats(details.tabId, details.url, 'afterNavigate'); - - // https://github.com/chrisaljoudi/uBlock/issues/630 - // The hostname of the bound document must always be present in the - // mini-matrix. That's the best place I could find for the fix, all other - // options had bad side-effects or complications. - // 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; - } -}; - -/******************************************************************************/ - -// It may happen the URL in the tab changes, while the page's document -// stays the same (for instance, Google Maps). Without this listener, -// the extension icon won't be properly refreshed. - -vAPI.tabs.onUpdated = function(tabId, changeInfo, tab) { - if ( !tab.url || tab.url === '' ) { - return; - } - if ( !changeInfo.url ) { - return; - } - µb.bindTabToPageStats(tabId, changeInfo.url, 'tabUpdated'); -}; - -/******************************************************************************/ - -vAPI.tabs.onClosed = function(tabId) { - if ( tabId < 0 ) { - return; - } - µb.unbindTabFromPageStats(tabId); -}; - -/******************************************************************************/ - -// https://github.com/chrisaljoudi/uBlock/issues/297 - -vAPI.tabs.onPopup = function(details) { - //console.debug('vAPI.tabs.onPopup: details = %o', details); - - var pageStore = µb.pageStoreFromTabId(details.openerTabId); - var openerURL = details.openerURL || ''; - - if ( openerURL === '' && pageStore ) { - openerURL = pageStore.pageURL; - } - - if ( openerURL === '' ) { - return; - } - - var µburi = µb.URI; - var openerHostname = µburi.hostnameFromURI(openerURL); - var openerDomain = µburi.domainFromHostname(openerHostname); - - var targetURL = details.targetURL; - - // If the page URL is that of our "blocked page" URL, extract the URL of - // the page which was blocked. - if ( targetURL.lastIndexOf(vAPI.getURL('document-blocked.html'), 0) === 0 ) { - var matches = /details=([^&]+)/.exec(targetURL); - if ( matches !== null ) { - targetURL = JSON.parse(atob(matches[1])).url; - } - } - - var context = { - pageHostname: openerHostname, - pageDomain: openerDomain, - rootHostname: openerHostname, - rootDomain: openerDomain, - requestURL: targetURL, - requestHostname: µb.URI.hostnameFromURI(targetURL), - requestType: 'popup' - }; - - var result = ''; - - // Check user switch first - if ( µb.hnSwitches.evaluateZ('doBlockAllPopups', openerHostname) ) { - result = 'ub:doBlockAllPopups true'; - } - - // https://github.com/chrisaljoudi/uBlock/issues/323 - // https://github.com/chrisaljoudi/uBlock/issues/1142 - // If popup OR opener URL is whitelisted, do not block the popup - if ( - result === '' && - µb.getNetFilteringSwitch(openerURL) && - µb.getNetFilteringSwitch(targetURL) - ) { - result = µb.staticNetFilteringEngine.matchStringExactType(context, targetURL, 'popup'); - } - - // https://github.com/chrisaljoudi/uBlock/issues/91 - if ( pageStore ) { - pageStore.logRequest(context, result); - } - - // Not blocked - if ( µb.isAllowResult(result) ) { - return; - } - - // Blocked - - // It is a popup, block and remove the tab. - µb.unbindTabFromPageStats(details.targetTabId); - vAPI.tabs.remove(details.targetTabId); - - return true; -}; - -vAPI.tabs.registerListeners(); - -/******************************************************************************/ -/******************************************************************************/ - -// https://github.com/chrisaljoudi/httpswitchboard/issues/303 +// 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 @@ -176,8 +42,11 @@ vAPI.tabs.registerListeners(); // 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.isNoTabId(tabId) ) { + if ( vAPI.isBehindTheSceneTabId(tabId) ) { return 'http://behind-the-scene/'; } var uri = this.URI.set(pageURL); @@ -195,19 +64,434 @@ vAPI.tabs.registerListeners(); 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.toString(); + 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 + }; +})(); + +/******************************************************************************/ +/******************************************************************************/ +// When the DOM content of root frame is loaded, this means the tab +// content has changed. + +vAPI.tabs.onNavigation = function(details) { + if ( details.frameId !== 0 ) { + return; + } + 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 + // mini-matrix. That's the best place I could find for the fix, all other + // options had bad side-effects or complications. + // 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 && tabContext.rawURL.lastIndexOf('http', 0) === 0 ) { + pageStore.hostnameToCountMap[tabContext.rootHostname] = 0; + } +}; + +/******************************************************************************/ + +// It may happen the URL in the tab changes, while the page's document +// stays the same (for instance, Google Maps). Without this listener, +// the extension icon won't be properly refreshed. + +vAPI.tabs.onUpdated = function(tabId, changeInfo, tab) { + if ( !tab.url || tab.url === '' ) { + return; + } + if ( !changeInfo.url ) { + return; + } + µb.tabContextManager.commit(tabId, changeInfo.url); + µb.bindTabToPageStats(tabId, 'tabUpdated'); +}; + +/******************************************************************************/ + +vAPI.tabs.onClosed = function(tabId) { + if ( tabId < 0 ) { + return; + } + µb.unbindTabFromPageStats(tabId); +}; + +/******************************************************************************/ + +// https://github.com/chrisaljoudi/uBlock/issues/297 + +vAPI.tabs.onPopup = function(details) { + // console.debug('vAPI.tabs.onPopup: details = %o', details); + + var tabContext = µb.tabContextManager.lookup(details.openerTabId); + var openerURL = ''; + if ( tabContext.tabId === details.openerTabId ) { + openerURL = tabContext.normalURL; + } + if ( openerURL === '' ) { + return; + } + + var µburi = µb.URI; + var openerHostname = µburi.hostnameFromURI(openerURL); + var openerDomain = µburi.domainFromHostname(openerHostname); + + var targetURL = details.targetURL; + + var context = { + pageHostname: openerHostname, + pageDomain: openerDomain, + rootHostname: openerHostname, + rootDomain: openerDomain, + requestURL: targetURL, + requestHostname: µb.URI.hostnameFromURI(targetURL), + requestType: 'popup' + }; + + var result = ''; + + // https://github.com/chrisaljoudi/uBlock/issues/323 + // https://github.com/chrisaljoudi/uBlock/issues/1142 + // If popup OR opener URL is whitelisted, do not block the popup + if ( + result === '' && + µb.getNetFilteringSwitch(openerURL) && + µb.getNetFilteringSwitch(targetURL) + ) { + result = µb.staticNetFilteringEngine.matchStringExactType(context, targetURL, 'popup'); + } + + // https://github.com/chrisaljoudi/uBlock/issues/91 + var pageStore = µb.pageStoreFromTabId(details.openerTabId); + if ( pageStore ) { + pageStore.logRequest(context, result); + } + + // Not blocked + if ( µb.isAllowResult(result) ) { + return; + } + + // Blocked + + // It is a popup, block and remove the tab. + if(details.targetTabId !== "preempt") { + µb.unbindTabFromPageStats(details.targetTabId); + vAPI.tabs.remove(details.targetTabId); + } + + return true; +}; + +vAPI.tabs.registerListeners(); + +/******************************************************************************/ /******************************************************************************/ // Create an entry for the tab if it doesn't exist. -µb.bindTabToPageStats = function(tabId, pageURL, context) { +µb.bindTabToPageStats = function(tabId, context) { this.updateBadgeAsync(tabId); - - // https://github.com/chrisaljoudi/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; } @@ -217,7 +501,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,11 +513,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]; @@ -243,20 +529,6 @@ vAPI.tabs.registerListeners(); } }; -/******************************************************************************/ - -µb.pageUrlFromTabId = function(tabId) { - var pageStore = this.pageStores[tabId]; - return pageStore ? pageStore.pageURL : ''; -}; - -µb.pageUrlFromPageStats = function(pageStats) { - if ( pageStats ) { - return pageStats.pageURL; - } - return ''; -}; - µb.pageStoreFromTabId = function(tabId) { return this.pageStores[tabId]; }; @@ -265,11 +537,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); /******************************************************************************/ /******************************************************************************/ @@ -300,7 +568,7 @@ var pageStoreJanitor = function() { for ( var i = pageStoreJanitorSampleAt; i < n; i++ ) { tabId = tabIds[i]; // Do not remove behind-the-scene page store - if ( vAPI.isNoTabId(tabId) ) { + if ( vAPI.isBehindTheSceneTabId(tabId) ) { continue; } checkTab(tabId); diff --git a/src/js/traffic.js b/src/js/traffic.js index 2f89bff94..e38d49bde 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. @@ -62,40 +52,22 @@ var onBeforeRequest = function(details) { // Special treatment: behind-the-scene requests var tabId = details.tabId; - if ( vAPI.isNoTabId(tabId) ) { + if ( vAPI.isBehindTheSceneTabId(tabId) ) { return onBeforeBehindTheSceneRequest(details); } // Lookup the page store associated with this tab id. var µb = µBlock; var pageStore = µb.pageStoreFromTabId(tabId); - if ( (Date.now() - mostRecentRootDocURLTimestamp) >= 500 ) { - mostRecentRootDocURL = ''; - } if ( !pageStore ) { - // 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: @@ -105,25 +77,22 @@ 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; requestContext.requestURL = requestURL; requestContext.requestHostname = details.hostname; requestContext.requestType = requestType; - if(!isFrame && mostRecentRootDocURL !== '') { - requestContext.pageHostname = µb.URI.hostnameFromURI(mostRecentRootDocURL); - } var result = pageStore.filterRequest(requestContext); // 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); @@ -138,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 ) { @@ -175,16 +129,12 @@ var onBeforeRequest = function(details) { /******************************************************************************/ var onBeforeRootFrameRequest = function(details) { + var tabId = details.tabId; var requestURL = details.url; - - mostRecentRootDocURL = requestURL; - mostRecentRootDocURLTimestamp = Date.now(); - - // 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; + + µb.tabContextManager.push(tabId, requestURL); + var requestHostname = details.hostname; var requestDomain = µb.URI.domainFromHostname(requestHostname); var context = { @@ -198,60 +148,11 @@ var onBeforeRootFrameRequest = function(details) { }; var result = ''; - - // If the site is whitelisted, disregard strict blocking - if ( µb.getNetFilteringSwitch(requestURL) === false ) { - result = 'ua:whitelisted'; - } - - // Permanently unrestricted? - if ( result === '' && µb.hnSwitches.evaluateZ('dontBlockDoc', requestHostname) ) { - result = 'ua:dontBlockDoc true'; - } - - // Temporarily whitelisted? - var obsolete = documentWhitelists[requestHostname]; - if ( obsolete !== undefined ) { - if ( obsolete > Date.now() ) { - if ( result === '' ) { - result = 'ta:*' + ' ' + requestHostname + ' doc allow'; - } - } else { - delete documentWhitelists[requestHostname]; - } - } - - // Filtering - if ( result === '' ) { - result = µb.staticNetFilteringEngine.matchString(context); - // https://github.com/chrisaljoudi/uBlock/issues/1128 - // Do not block if the match begins after the hostname. - if ( result !== '' ) { - result = toBlockDocResult(requestURL, requestHostname, result); - } - } - - // Log - var pageStore = µb.bindTabToPageStats(details.tabId, requestURL, 'beforeRequest'); + var pageStore = µb.bindTabToPageStats(tabId, 'beforeRequest'); if ( pageStore ) { pageStore.logRequest(context, result); } - - // Not blocked - if ( µb.isAllowResult(result) ) { - return; - } - - // Blocked - var query = btoa(JSON.stringify({ - url: requestURL, - hn: requestHostname, - why: result - })); - - vAPI.tabs.replace(details.tabId, vAPI.getURL('document-blocked.html?details=') + query); - - return { cancel: true }; + return; }; /******************************************************************************/ @@ -309,9 +210,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 @@ -319,10 +221,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) ) { @@ -343,58 +245,68 @@ var onBeforeBehindTheSceneRequest = function(details) { var onHeadersReceived = function(details) { // Do not interfere with behind-the-scene requests. var tabId = details.tabId; - if ( vAPI.isNoTabId(tabId) ) { + if ( vAPI.isBehindTheSceneTabId(tabId) ) { 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; + } + // 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; } - // 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') !== ''; - //} + µ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); - context = { - rootHostname: requestHostname, - rootDomain: contextDomain, - pageHostname: requestHostname, - pageDomain: contextDomain, - preNavigationHeader: true - }; - } 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'; @@ -420,6 +332,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://*/*', @@ -458,18 +382,6 @@ vAPI.net.registerListeners(); /******************************************************************************/ -exports.temporarilyWhitelistDocument = function(url) { - var µb = µBlock; - var hostname = µb.URI.hostnameFromURI(url); - if ( hostname === '' ) { - return; - } - - documentWhitelists[hostname] = Date.now() + 60 * 1000; -}; - -/******************************************************************************/ - return exports; /******************************************************************************/ diff --git a/src/js/ublock.js b/src/js/ublock.js index b1d53efa9..0fd81e495 100644 --- a/src/js/ublock.js +++ b/src/js/ublock.js @@ -318,12 +318,4 @@ var matchWhitelistDirective = function(url, hostname, directive) { /******************************************************************************/ -µBlock.toggleHostnameSwitch = function(details) { - if ( this.hnSwitches.toggleZ(details.name, details.hostname, details.state) ) { - this.saveHostnameSwitches(); - } -}; - -/******************************************************************************/ - })();