From 4fd71d4209d46f4efbec47e2d372605e177b8c17 Mon Sep 17 00:00:00 2001 From: gorhill Date: Tue, 1 Dec 2015 15:07:22 -0500 Subject: [PATCH] this adds popunder filtering support for Firefox-based browsers --- platform/chromium/vapi-background.js | 68 +---------- platform/firefox/frameModule.js | 52 ++++++--- platform/firefox/vapi-background.js | 167 ++++++++++++--------------- src/js/tab.js | 121 +++++++++++-------- 4 files changed, 187 insertions(+), 221 deletions(-) diff --git a/platform/chromium/vapi-background.js b/platform/chromium/vapi-background.js index 63b657c51..499693c8d 100644 --- a/platform/chromium/vapi-background.js +++ b/platform/chromium/vapi-background.js @@ -157,7 +157,6 @@ var toChromiumTabId = function(tabId) { vAPI.tabs.registerListeners = function() { var onNavigationClient = this.onNavigation || noopFunc; - var onPopupClient = this.onPopup || noopFunc; var onUpdatedClient = this.onUpdated || noopFunc; // https://developer.chrome.com/extensions/webNavigation @@ -167,58 +166,6 @@ vAPI.tabs.registerListeners = function() { // onDOMContentLoaded -> // onCompleted - var popupCandidates = Object.create(null); - - var PopupCandidate = function(details) { - this.targetTabId = details.tabId.toString(); - this.openerTabId = details.sourceTabId.toString(); - this.targetURL = details.url; - this.selfDestructionTimer = null; - }; - - PopupCandidate.prototype.selfDestruct = function() { - if ( this.selfDestructionTimer !== null ) { - clearTimeout(this.selfDestructionTimer); - } - delete popupCandidates[this.targetTabId]; - }; - - PopupCandidate.prototype.launchSelfDestruction = function() { - if ( this.selfDestructionTimer !== null ) { - clearTimeout(this.selfDestructionTimer); - } - this.selfDestructionTimer = setTimeout(this.selfDestruct.bind(this), 10000); - }; - - var popupCandidateCreate = function(details) { - var popup = popupCandidates[details.tabId]; - // This really should not happen... - if ( popup !== undefined ) { - return; - } - return (popupCandidates[details.tabId] = new PopupCandidate(details)); - }; - - var popupCandidateTest = function(details) { - var popup = popupCandidates[details.tabId]; - if ( popup === undefined ) { - return; - } - popup.targetURL = details.url; - if ( onPopupClient(popup) !== true ) { - return; - } - popup.selfDestruct(); - return true; - }; - - var popupCandidateDestroy = function(details) { - var popup = popupCandidates[details.tabId]; - if ( popup instanceof PopupCandidate ) { - popup.launchSelfDestruction(); - } - }; - // The chrome.webRequest.onBeforeRequest() won't be called for everything // else than `http`/`https`. Thus, in such case, we will bind the tab as // early as possible in order to increase the likelihood of a context @@ -233,22 +180,18 @@ vAPI.tabs.registerListeners = function() { details.frameId = 0; onNavigationClient(details); } - popupCandidateCreate(details); - popupCandidateTest(details); + if ( typeof vAPI.tabs.onPopupCreated === 'function' ) { + vAPI.tabs.onPopupCreated(details.tabId.toString(), details.sourceTabId.toString()); + } }; var onBeforeNavigate = function(details) { if ( details.frameId !== 0 ) { return; } - //console.debug('onBeforeNavigate: popup candidate tab id %d = "%s"', details.tabId, details.url); - popupCandidateTest(details); }; var onUpdated = function(tabId, changeInfo, tab) { - if ( changeInfo.url && popupCandidateTest({ tabId: tabId, url: changeInfo.url }) ) { - return; - } onUpdatedClient(tabId, changeInfo, tab); }; @@ -257,11 +200,6 @@ vAPI.tabs.registerListeners = function() { return; } onNavigationClient(details); - //console.debug('onCommitted: popup candidate tab id %d = "%s"', details.tabId, details.url); - if ( popupCandidateTest(details) === true ) { - return; - } - popupCandidateDestroy(details); }; chrome.webNavigation.onCreatedNavigationTarget.addListener(onCreatedNavigationTarget); diff --git a/platform/firefox/frameModule.js b/platform/firefox/frameModule.js index 8e95d023c..3e494f885 100644 --- a/platform/firefox/frameModule.js +++ b/platform/firefox/frameModule.js @@ -90,6 +90,7 @@ var contentObserver = { SUB_FRAME: Ci.nsIContentPolicy.TYPE_SUBDOCUMENT, contentBaseURI: 'chrome://' + hostName + '/content/js/', cpMessageName: hostName + ':shouldLoad', + popupMessageName: hostName + ':shouldLoadPopup', ignoredPopups: new WeakMap(), uniqueSandboxId: 1, @@ -153,6 +154,38 @@ var contentObserver = { .outerWindowID; }, + handlePopup: function(location, context) { + let openeeContext = context.contentWindow || context; + if ( + typeof openeeContext.opener !== 'object' || + openeeContext.opener === null || + openeeContext.opener === context || + this.ignoredPopups.has(openeeContext) + ) { + return; + } + // https://github.com/gorhill/uBlock/issues/452 + // Use location of top window, not that of a frame, as this + // would cause tab id lookup (necessary for popup blocking) to + // always fail. + let openerURL = openeeContext.opener.top && + openeeContext.opener.top.location.href; + if ( openerURL === null ) { + return; + } + let messageManager = getMessageManager(openeeContext); + if ( messageManager === null ) { + return; + } + if ( typeof messageManager.sendRpcMessage === 'function' ) { + // https://bugzil.la/1092216 + messageManager.sendRpcMessage(this.popupMessageName, openerURL); + } else { + // Compatibility for older versions + messageManager.sendSyncMessage(this.popupMessageName, openerURL); + } + }, + // https://bugzil.la/612921 shouldLoad: function(type, location, origin, context) { // For whatever reason, sometimes the global scope is completely @@ -170,26 +203,16 @@ var contentObserver = { return this.ACCEPT; } + if ( type === this.MAIN_FRAME ) { + this.handlePopup(location, context); + } + if ( !location.schemeIs('http') && !location.schemeIs('https') ) { return this.ACCEPT; } - let openerURL = null; - if ( type === this.MAIN_FRAME ) { context = context.contentWindow || context; - if ( - typeof context.opener === 'object' && - context.opener !== null && - context.opener !== context && - this.ignoredPopups.has(context) === false - ) { - // https://github.com/gorhill/uBlock/issues/452 - // Use location of top window, not that of a frame, as this - // would cause tab id lookup (necessary for popup blocking) to - // always fail. - openerURL = context.opener.top && context.opener.top.location.href; - } } else if ( type === this.SUB_FRAME ) { context = context.contentWindow; } else { @@ -219,7 +242,6 @@ var contentObserver = { let details = { frameId: isTopLevel ? 0 : this.getFrameId(context), - openerURL: openerURL, parentFrameId: parentFrameId, rawtype: type, tabId: '', diff --git a/platform/firefox/vapi-background.js b/platform/firefox/vapi-background.js index deacc6fba..8398c3d93 100644 --- a/platform/firefox/vapi-background.js +++ b/platform/firefox/vapi-background.js @@ -945,17 +945,24 @@ vAPI.tabs._remove = (function() { /******************************************************************************/ -vAPI.tabs.remove = function(tabId) { - var browser = tabWatcher.browserFromTabId(tabId); - if ( !browser ) { - return; - } - var tab = tabWatcher.tabFromBrowser(browser); - if ( !tab ) { - return; - } - this._remove(tab, getTabBrowser(getOwnerWindow(browser))); -}; +vAPI.tabs.remove = (function() { + var remove = function(tabId) { + var browser = tabWatcher.browserFromTabId(tabId); + if ( !browser ) { + return; + } + var tab = tabWatcher.tabFromBrowser(browser); + if ( !tab ) { + return; + } + this._remove(tab, getTabBrowser(getOwnerWindow(browser))); + }; + + // Do this asynchronously + return function(tabId) { + vAPI.setTimeout(remove.bind(this, tabId), 10); + }; +})(); /******************************************************************************/ @@ -1170,17 +1177,6 @@ var tabWatcher = (function() { onClose({ target: target }); }; - // https://developer.mozilla.org/en-US/docs/Web/Events/TabOpen - //var onOpen = function({target}) { - // var tabId = tabIdFromTarget(target); - // var browser = browserFromTabId(tabId); - // vAPI.tabs.onNavigation({ - // frameId: 0, - // tabId: tabId, - // url: browser.currentURI.asciiSpec, - // }); - //}; - // https://developer.mozilla.org/en-US/docs/Web/Events/TabShow var onShow = function({target}) { tabIdFromTarget(target); @@ -1266,7 +1262,6 @@ var tabWatcher = (function() { // not set when a tab is opened as a result of session restore -- it is // set *after* the event is fired in such case. if ( tabContainer ) { - //tabContainer.addEventListener('TabOpen', onOpen); tabContainer.addEventListener('TabShow', onShow); tabContainer.addEventListener('TabClose', onClose); // when new window is opened TabSelect doesn't run on the selected tab? @@ -1293,7 +1288,6 @@ var tabWatcher = (function() { tabContainer = tabBrowser.tabContainer; } if ( tabContainer ) { - //tabContainer.removeEventListener('TabOpen', onOpen); tabContainer.removeEventListener('TabShow', onShow); tabContainer.removeEventListener('TabClose', onClose); tabContainer.removeEventListener('TabSelect', onSelect); @@ -1847,7 +1841,6 @@ var httpObserver = { this.frameId = 0; this.parentFrameId = 0; this.rawtype = 0; - this.sourceTabId = null; this.tabId = 0; this._key = ''; // key is url, from URI.spec }, @@ -1937,9 +1930,10 @@ var httpObserver = { } aWindow = loadContext.associatedWindow; } catch (ex) { - //console.error(ex); + //console.error(ex.toString()); } } + var gBrowser; try { if ( !aWindow && channel.loadGroup && channel.loadGroup.notificationCallbacks ) { aWindow = channel @@ -1949,21 +1943,23 @@ var httpObserver = { .associatedWindow; } if ( aWindow ) { - return tabWatcher.tabIdFromTarget( - aWindow + gBrowser = aWindow .getInterface(Ci.nsIWebNavigation) .QueryInterface(Ci.nsIDocShell) .rootTreeItem .QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIDOMWindow) - .gBrowser - .getBrowserForContentWindow(aWindow) - ); + .gBrowser; } } catch (ex) { - //console.error(ex); + //console.error(ex.toString()); } - return vAPI.noTabId; + if ( !gBrowser || !gBrowser._getTabForContentWindow ) { + return vAPI.noTabId; + } + // Using `_getTabForContentWindow` ensure older versions of Firefox + // work well. + return tabWatcher.tabIdFromTarget(gBrowser._getTabForContentWindow(aWindow)); }, // https://github.com/gorhill/uBlock/issues/959 @@ -1981,24 +1977,6 @@ var httpObserver = { }; }, - handlePopup: function(URI, tabId, sourceTabId) { - if ( !sourceTabId ) { - return false; - } - - if ( !URI.schemeIs('http') && !URI.schemeIs('https') ) { - return false; - } - - var result = vAPI.tabs.onPopup({ - targetTabId: tabId, - openerTabId: sourceTabId, - targetURL: URI.asciiSpec - }); - - return result === true; - }, - handleRequest: function(channel, URI, details) { var type = this.typeMap[details.rawtype] || 'other'; if ( this.onBeforeRequestTypes && this.onBeforeRequestTypes.has(type) === false ) { @@ -2048,7 +2026,7 @@ var httpObserver = { }, handleResponseHeaders: function(channel, URI, channelData) { - var type = this.typeMap[channelData[4]] || 'other'; + var type = this.typeMap[channelData[3]] || 'other'; if ( this.onHeadersReceivedTypes && this.onHeadersReceivedTypes.has(type) === false ) { return; } @@ -2071,8 +2049,8 @@ var httpObserver = { hostname: hostname, parentFrameId: channelData[1], responseHeaders: responseHeaders, - tabId: channelData[3], - type: this.typeMap[channelData[4]] || 'other', + tabId: channelData[2], + type: this.typeMap[channelData[3]] || 'other', url: URI.asciiSpec }); @@ -2137,6 +2115,19 @@ var httpObserver = { } } + // IMPORTANT: + // If this is a main frame, ensure that the proper tab id is being + // used: it can happen that the wrong tab id was looked up at + // `shouldLoadListener` time. Without this, the popup blocker may + // not work properly, and also a tab opened from a link may end up + // being wrongly reported as an embedded element. + if ( pendingRequest !== null && pendingRequest.rawtype === 6 ) { + var tabId = this.tabIdFromChannel(channel); + if ( tabId !== vAPI.noTabId ) { + pendingRequest.tabId = tabId; + } + } + // Behind-the-scene request... Really? if ( pendingRequest === null ) { pendingRequest = this.synthesizePendingRequest(channel, rawtype); @@ -2171,7 +2162,6 @@ var httpObserver = { channel.setProperty(this.REQDATAKEY, [ pendingRequest.frameId, pendingRequest.parentFrameId, - pendingRequest.sourceTabId, pendingRequest.tabId, pendingRequest.rawtype ]); @@ -2196,16 +2186,11 @@ var httpObserver = { var channelData = oldChannel.getProperty(this.REQDATAKEY); - if ( this.handlePopup(URI, channelData[3], channelData[2]) ) { - result = this.ABORT; - return; - } - var details = { frameId: channelData[0], parentFrameId: channelData[1], - tabId: channelData[3], - rawtype: channelData[4] + tabId: channelData[2], + rawtype: channelData[3] }; if ( this.handleRequest(newChannel, URI, details) ) { @@ -2250,9 +2235,15 @@ vAPI.net.registerListeners = function() { null; } - var shouldBlockPopup = function(details) { - var sourceTabId = null; - var uri; + var shouldLoadPopupListenerMessageName = location.host + ':shouldLoadPopup'; + var shouldLoadPopupListener = function(e) { + if ( typeof vAPI.tabs.onPopupCreated !== 'function' ) { + return; + } + + var openerURL = e.data; + var popupTabId = tabWatcher.tabIdFromTarget(e.target); + var uri, openerTabId; for ( var browser of tabWatcher.browsers() ) { uri = browser.currentURI; @@ -2265,25 +2256,23 @@ vAPI.net.registerListeners = function() { // believe this may have to do with those very temporary // browser objects created when opening a new tab, i.e. related // to https://github.com/gorhill/uBlock/issues/212 - if ( !uri || uri.spec !== details.openerURL ) { + if ( !uri || uri.spec !== openerURL ) { continue; } - sourceTabId = tabWatcher.tabIdFromTarget(browser); - if ( sourceTabId === details.tabId ) { - sourceTabId = null; - continue; + openerTabId = tabWatcher.tabIdFromTarget(browser); + if ( openerTabId !== popupTabId ) { + vAPI.tabs.onPopupCreated(popupTabId, openerTabId); + break; } - - uri = Services.io.newURI(details.url, null, null); - - httpObserver.handlePopup(uri, details.tabId, sourceTabId); - break; } - - return sourceTabId; }; + vAPI.messaging.globalMessageManager.addMessageListener( + shouldLoadPopupListenerMessageName, + shouldLoadPopupListener + ); + var shouldLoadListenerMessageName = location.host + ':shouldLoad'; var shouldLoadListener = function(e) { // Non blocking: it is assumed that the http observer is fired after @@ -2291,18 +2280,6 @@ vAPI.net.registerListeners = function() { // a request would end up being categorized as a behind-the-scene // requests. var details = e.data; - var sourceTabId = null; - - details.tabId = tabWatcher.tabIdFromTarget(e.target); - - // Popup candidate: this code path is taken only for when a new top - // document loads, i.e. only once per document load. TODO: evaluate for - // popup filtering in an asynchrous manner -- it's not really required - // to evaluate on the spot. Still, there is currently no harm given - // this code path is typically taken only once per page load. - if ( details.openerURL ) { - sourceTabId = shouldBlockPopup(details); - } // We are being called synchronously from the content process, so we // must return ASAP. The code below merely record the details of the @@ -2311,8 +2288,7 @@ vAPI.net.registerListeners = function() { pendingReq.frameId = details.frameId; pendingReq.parentFrameId = details.parentFrameId; pendingReq.rawtype = details.rawtype; - pendingReq.sourceTabId = sourceTabId; - pendingReq.tabId = details.tabId; + pendingReq.tabId = tabWatcher.tabIdFromTarget(e.target); }; vAPI.messaging.globalMessageManager.addMessageListener( @@ -2378,6 +2354,11 @@ vAPI.net.registerListeners = function() { httpObserver.register(); cleanupTasks.push(function() { + vAPI.messaging.globalMessageManager.removeMessageListener( + shouldLoadPopupListenerMessageName, + shouldLoadPopupListener + ); + vAPI.messaging.globalMessageManager.removeMessageListener( shouldLoadListenerMessageName, shouldLoadListener @@ -2655,7 +2636,7 @@ vAPI.toolbarButton = { // palette might take a little longer to appear on some platforms, // give it a small delay and try again. - if ( toolbox.palette === null ) { + if ( !toolbox.palette ) { vAPI.setTimeout(onReadyStateComplete.bind(null, window, callback, tryCount), 200); return; } @@ -2707,7 +2688,7 @@ vAPI.toolbarButton = { var navbar = document.getElementById('nav-bar'); var toolbarButton = createToolbarButton(window); - if ( palette !== null && palette.querySelector('#' + tbb.id) === null ) { + if ( palette && palette.querySelector('#' + tbb.id) === null ) { palette.appendChild(toolbarButton); } diff --git a/src/js/tab.js b/src/js/tab.js index aeb3f0a00..c901e1155 100644 --- a/src/js/tab.js +++ b/src/js/tab.js @@ -136,6 +136,53 @@ housekeep itself. var mostRecentRootDocURL = ''; var mostRecentRootDocURLTimestamp = 0; + var popupCandidates = Object.create(null); + + var PopupCandidate = function(targetTabId, openerTabId) { + this.targetTabId = targetTabId; + this.openerTabId = openerTabId; + this.selfDestructionTimer = null; + this.launchSelfDestruction(); + }; + + PopupCandidate.prototype.destroy = function() { + if ( this.selfDestructionTimer !== null ) { + clearTimeout(this.selfDestructionTimer); + } + delete popupCandidates[this.targetTabId]; + }; + + PopupCandidate.prototype.launchSelfDestruction = function() { + if ( this.selfDestructionTimer !== null ) { + clearTimeout(this.selfDestructionTimer); + } + this.selfDestructionTimer = vAPI.setTimeout(this.destroy.bind(this), 10000); + }; + + var popupCandidateTest = function(targetTabId) { + var candidates = popupCandidates, entry; + for ( var tabId in candidates ) { + entry = candidates[tabId]; + if ( targetTabId !== tabId && targetTabId !== entry.openerTabId ) { + continue; + } + if ( vAPI.tabs.onPopupUpdated(tabId, entry.openerTabId) === true ) { + entry.destroy(); + } else { + entry.launchSelfDestruction(); + } + } + }; + + vAPI.tabs.onPopupCreated = function(targetTabId, openerTabId) { + var popup = popupCandidates[targetTabId]; + if ( popup !== undefined ) { + return; + } + popupCandidates[targetTabId] = new PopupCandidate(targetTabId, openerTabId); + popupCandidateTest(targetTabId); + }; + var gcPeriod = 10 * 60 * 1000; // A pushed entry is removed from the stack unless it is committed with @@ -253,34 +300,11 @@ housekeep itself. } this.stack.push(new StackEntry(url)); this.update(); + popupCandidateTest(this.tabId); if ( this.commitTimer !== null ) { clearTimeout(this.commitTimer); } - this.commitTimer = vAPI.setTimeout(this.onCommit.bind(this), 1000); - }; - - // 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. - var i = this.stack.length; - if ( i === 1 ) { - return; - } - while ( i-- ) { - if ( this.stack[i].url !== url ) { - continue; - } - this.stack.splice(i, 1); - if ( i === this.stack.length ) { - this.update(); - } - return; - } + this.commitTimer = vAPI.setTimeout(this.onCommit.bind(this), 500); }; // This tells that the url is definitely the one to be associated with the @@ -368,13 +392,6 @@ housekeep itself. return entry; }; - var unpush = function(tabId, url) { - var entry = tabContexts[tabId]; - if ( entry !== undefined ) { - entry.unpush(url); - } - }; - var exists = function(tabId) { return tabContexts[tabId] !== undefined; }; @@ -407,7 +424,6 @@ housekeep itself. return { push: push, - unpush: unpush, commit: commit, lookup: lookup, exists: exists, @@ -425,6 +441,7 @@ 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'); @@ -453,6 +470,7 @@ vAPI.tabs.onUpdated = function(tabId, changeInfo, tab) { if ( !changeInfo.url ) { return; } + µb.tabContextManager.commit(tabId, changeInfo.url); µb.bindTabToPageStats(tabId, 'tabUpdated'); }; @@ -490,14 +508,14 @@ vAPI.tabs.onClosed = function(tabId) { // c: close opener // d: close target -vAPI.tabs.onPopup = (function() { +vAPI.tabs.onPopupUpdated = (function() { //console.debug('vAPI.tabs.onPopup: details = %o', details); // The same context object will be reused everytime. This also allows to // remember whether a popup or popunder was matched. var context = {}; - var popupMatch = function(openerURL, targetURL, clickedURL) { + var popupMatch = function(openerURL, targetURL, clickedURL, popunder) { var openerHostname = µb.URI.hostnameFromURI(openerURL); var openerDomain = µb.URI.domainFromHostname(openerHostname); @@ -514,6 +532,7 @@ vAPI.tabs.onPopup = (function() { if ( openerHostname !== '' ) { // Check user switch first if ( + popunder !== true && targetURL !== clickedURL && µb.hnSwitches.evaluateZ('no-popups', openerHostname) ) { @@ -554,14 +573,25 @@ vAPI.tabs.onPopup = (function() { return ''; }; - return function(details) { - var tabContext = µb.tabContextManager.lookup(details.openerTabId); + return function(targetTabId, openerTabId) { + // Opener details. + var tabContext = µb.tabContextManager.lookup(openerTabId); var openerURL = ''; - if ( tabContext.tabId === details.openerTabId ) { - openerURL = tabContext.normalURL; + if ( tabContext.tabId === openerTabId ) { + openerURL = tabContext.rawURL; + if ( openerURL === '' ) { + return; + } } - if ( openerURL === '' ) { - return; + + // Popup details. + tabContext = µb.tabContextManager.lookup(targetTabId); + var targetURL = ''; + if ( tabContext.tabId === targetTabId ) { + targetURL = tabContext.rawURL; + if ( targetURL === '' ) { + return; + } } // https://github.com/gorhill/uBlock/issues/341 @@ -570,8 +600,6 @@ vAPI.tabs.onPopup = (function() { return; } - 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 ) { @@ -582,15 +610,12 @@ vAPI.tabs.onPopup = (function() { } // Popup test. - var openerTabId = details.openerTabId; - var targetTabId = details.targetTabId; var result = popupMatch(openerURL, targetURL, µb.mouseURL); // Popunder test. if ( result === '' ) { - openerTabId = details.targetTabId; - targetTabId = details.openerTabId; - result = popupMatch(targetURL, openerURL, µb.mouseURL); + var tmp = openerTabId; openerTabId = targetTabId; targetTabId = tmp; + result = popupMatch(targetURL, openerURL, µb.mouseURL, true); } // Log only for when there was a hit against an actual filter (allow or block).