Ensure toolbar icon reflect updated whitelist directives

Related issue:
- https://github.com/uBlockOrigin/uBlock-issues/issues/680

Opportunistically, vAPI.tabs has been refactored toward
ES6 syntax.
This commit is contained in:
Raymond Hill 2019-07-21 10:48:44 -04:00
parent 7e1868b1c3
commit e1dd7f7043
No known key found for this signature in database
GPG key ID: 25E1490B761470C2
2 changed files with 596 additions and 596 deletions

View file

@ -275,10 +275,6 @@ vAPI.browserSettings = (function() {
/******************************************************************************/ /******************************************************************************/
/******************************************************************************/ /******************************************************************************/
vAPI.tabs = {};
/******************************************************************************/
vAPI.isBehindTheSceneTabId = function(tabId) { vAPI.isBehindTheSceneTabId = function(tabId) {
return tabId < 0; return tabId < 0;
}; };
@ -286,72 +282,37 @@ vAPI.isBehindTheSceneTabId = function(tabId) {
vAPI.unsetTabId = 0; vAPI.unsetTabId = 0;
vAPI.noTabId = -1; // definitely not any existing tab vAPI.noTabId = -1; // definitely not any existing tab
/******************************************************************************/
// To remove when tabId-as-integer has been tested enough. // To remove when tabId-as-integer has been tested enough.
var toChromiumTabId = function(tabId) { const toChromiumTabId = function(tabId) {
return typeof tabId === 'number' && !isNaN(tabId) && tabId > 0 ? return typeof tabId === 'number' && isNaN(tabId) === false
tabId : ? tabId
0; : 0;
}; };
/******************************************************************************/
vAPI.tabs.registerListeners = function() {
// https://developer.chrome.com/extensions/webNavigation // https://developer.chrome.com/extensions/webNavigation
// [onCreatedNavigationTarget ->] // https://developer.chrome.com/extensions/tabs
// onBeforeNavigate ->
// onCommitted ->
// onDOMContentLoaded ->
// onCompleted
// 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
// properly setup if network requests are fired from within the tab.
// Example: Chromium + case #6 at
// http://raymondhill.net/ublock/popup.html
const reGoodForWebRequestAPI = /^https?:\/\//;
// https://forums.lanik.us/viewtopic.php?f=62&t=32826
// Chromium-based browsers: sanitize target URL. I've seen data: URI with
// newline characters in standard fields, possibly as a way of evading
// filters. As per spec, there should be no whitespaces in a data: URI's
// standard fields.
const sanitizeURL = function(url) {
if ( url.startsWith('data:') === false ) { return url; }
const pos = url.indexOf(',');
if ( pos === -1 ) { return url; }
const s = url.slice(0, pos);
if ( s.search(/\s/) === -1 ) { return url; }
return s.replace(/\s+/, '') + url.slice(pos);
};
vAPI.Tabs = class {
constructor() {
browser.webNavigation.onCreatedNavigationTarget.addListener(details => { browser.webNavigation.onCreatedNavigationTarget.addListener(details => {
if ( typeof details.url !== 'string' ) { if ( typeof details.url !== 'string' ) {
details.url = ''; details.url = '';
} }
if ( reGoodForWebRequestAPI.test(details.url) === false ) { if ( /^https?:\/\//.test(details.url) === false ) {
details.frameId = 0; details.frameId = 0;
details.url = sanitizeURL(details.url); details.url = this.sanitizeURL(details.url);
if ( this.onNavigation ) {
this.onNavigation(details); this.onNavigation(details);
} }
} this.onCreated(
if ( vAPI.tabs.onPopupCreated ) {
vAPI.tabs.onPopupCreated(
details.tabId, details.tabId,
details.sourceTabId details.sourceTabId
); );
}
}); });
browser.webNavigation.onCommitted.addListener(details => { browser.webNavigation.onCommitted.addListener(details => {
details.url = sanitizeURL(details.url); details.url = this.sanitizeURL(details.url);
if ( this.onNavigation ) {
this.onNavigation(details); this.onNavigation(details);
}
}); });
// https://github.com/gorhill/uBlock/issues/3073 // https://github.com/gorhill/uBlock/issues/3073
@ -361,38 +322,30 @@ vAPI.tabs.registerListeners = function() {
changeInfo.url = tab && tab.url; changeInfo.url = tab && tab.url;
} }
if ( changeInfo.url ) { if ( changeInfo.url ) {
changeInfo.url = sanitizeURL(changeInfo.url); changeInfo.url = this.sanitizeURL(changeInfo.url);
} }
if ( this.onUpdated ) {
this.onUpdated(tabId, changeInfo, tab); this.onUpdated(tabId, changeInfo, tab);
}
}); });
browser.tabs.onActivated.addListener(( ) => { browser.tabs.onActivated.addListener((tabId, details) => {
if ( vAPI.contextMenu ) { this.onActivated(tabId, details);
vAPI.contextMenu.onMustUpdate();
}
}); });
browser.tabs.onRemoved.addListener((tabId, details) => { browser.tabs.onRemoved.addListener((tabId, details) => {
this.onClosed(tabId, details); this.onClosed(tabId, details);
}); });
}; }
/******************************************************************************/ get(tabId, callback) {
// Caller must be prepared to deal with nil tab argument.
// https://code.google.com/p/chromium/issues/detail?id=410868#c8
vAPI.tabs.get = function(tabId, callback) {
if ( tabId === null ) { if ( tabId === null ) {
chrome.tabs.query( chrome.tabs.query(
{ active: true, currentWindow: true }, { active: true, currentWindow: true },
tabs => { tabs => {
void chrome.runtime.lastError; void chrome.runtime.lastError;
callback( callback(
Array.isArray(tabs) && tabs.length !== 0 ? tabs[0] : null Array.isArray(tabs) && tabs.length !== 0
? tabs[0]
: null
); );
} }
); );
@ -409,43 +362,24 @@ vAPI.tabs.get = function(tabId, callback) {
void chrome.runtime.lastError; void chrome.runtime.lastError;
callback(tab); callback(tab);
}); });
};
/*******************************************************************************
Properties of the details object:
- url: 'URL', => the address that will be opened
- tabId: 1, => the tab is used if set, instead of creating a new one
- index: -1, => undefined: end of the list, -1: following tab, or
after index
- active: false, => opens the tab in background - true and undefined:
foreground
- select: true, => if a tab is already opened with that url, then select
it instead of opening a new one
- popup: true => open in a new window
*/
vAPI.tabs.open = function(details) {
let targetURL = details.url;
if ( typeof targetURL !== 'string' || targetURL === '' ) {
return null;
} }
// extension pages // Properties of the details object:
if ( /^[\w-]{2,}:/.test(targetURL) !== true ) { // - url: 'URL', => the address that will be opened
targetURL = vAPI.getURL(targetURL); // - index: -1, => undefined: end of the list, -1: following tab, or
} // after index
// - active: false, => opens the tab in background - true and undefined:
// foreground
// - popup: true => open in a new window
// dealing with Chrome's asynchronous API create(url, details) {
const wrapper = ( ) => {
if ( details.active === undefined ) { if ( details.active === undefined ) {
details.active = true; details.active = true;
} }
const subWrapper = ( ) => { const subWrapper = ( ) => {
const updateDetails = { const updateDetails = {
url: targetURL, url: url,
active: !!details.active active: !!details.active
}; };
@ -527,20 +461,42 @@ vAPI.tabs.open = function(details) {
subWrapper(); subWrapper();
}); });
}; }
// Properties of the details object:
// - url: 'URL', => the address that will be opened
// - tabId: 1, => the tab is used if set, instead of creating a new one
// - index: -1, => undefined: end of the list, -1: following tab, or
// after index
// - active: false, => opens the tab in background - true and undefined:
// foreground
// - select: true, => if a tab is already opened with that url, then select
// it instead of opening a new one
// - popup: true => open in a new window
open(details) {
let targetURL = details.url;
if ( typeof targetURL !== 'string' || targetURL === '' ) {
return null;
}
// extension pages
if ( /^[\w-]{2,}:/.test(targetURL) !== true ) {
targetURL = vAPI.getURL(targetURL);
}
if ( !details.select ) { if ( !details.select ) {
wrapper(); this.create(targetURL, details);
return; return;
} }
// https://github.com/gorhill/uBlock/issues/3053#issuecomment-332276818 // https://github.com/gorhill/uBlock/issues/3053#issuecomment-332276818
// - Do not try to lookup uBO's own pages with FF 55 or less. // Do not try to lookup uBO's own pages with FF 55 or less.
if ( if (
vAPI.webextFlavor.soup.has('firefox') && vAPI.webextFlavor.soup.has('firefox') &&
vAPI.webextFlavor.major < 56 vAPI.webextFlavor.major < 56
) { ) {
wrapper(); this.create(targetURL, details);
return; return;
} }
@ -557,7 +513,7 @@ vAPI.tabs.open = function(details) {
void browser.runtime.lastError; void browser.runtime.lastError;
const tab = Array.isArray(tabs) && tabs[0]; const tab = Array.isArray(tabs) && tabs[0];
if ( !tab ) { if ( !tab ) {
wrapper(); this.create(targetURL, details);
return; return;
} }
const updateDetails = { active: true }; const updateDetails = { active: true };
@ -570,17 +526,15 @@ vAPI.tabs.open = function(details) {
browser.windows.update(tab.windowId, { focused: true }); browser.windows.update(tab.windowId, { focused: true });
}); });
}); });
}; }
/******************************************************************************/
// Replace the URL of a tab. Noop if the tab does not exist. // Replace the URL of a tab. Noop if the tab does not exist.
vAPI.tabs.replace = function(tabId, url) { replace(tabId, url) {
tabId = toChromiumTabId(tabId); tabId = toChromiumTabId(tabId);
if ( tabId === 0 ) { return; } if ( tabId === 0 ) { return; }
var targetURL = url; let targetURL = url;
// extension pages // extension pages
if ( /^[\w-]{2,}:/.test(targetURL) !== true ) { if ( /^[\w-]{2,}:/.test(targetURL) !== true ) {
@ -588,20 +542,16 @@ vAPI.tabs.replace = function(tabId, url) {
} }
chrome.tabs.update(tabId, { url: targetURL }, vAPI.resetLastError); chrome.tabs.update(tabId, { url: targetURL }, vAPI.resetLastError);
}; }
/******************************************************************************/ remove(tabId) {
vAPI.tabs.remove = function(tabId) {
tabId = toChromiumTabId(tabId); tabId = toChromiumTabId(tabId);
if ( tabId === 0 ) { return; } if ( tabId === 0 ) { return; }
chrome.tabs.remove(tabId, vAPI.resetLastError); chrome.tabs.remove(tabId, vAPI.resetLastError);
}; }
/******************************************************************************/ reload(tabId, bypassCache = false) {
vAPI.tabs.reload = function(tabId, bypassCache = false) {
tabId = toChromiumTabId(tabId); tabId = toChromiumTabId(tabId);
if ( tabId === 0 ) { return; } if ( tabId === 0 ) { return; }
@ -610,13 +560,9 @@ vAPI.tabs.reload = function(tabId, bypassCache = false) {
{ bypassCache: bypassCache === true }, { bypassCache: bypassCache === true },
vAPI.resetLastError vAPI.resetLastError
); );
}; }
/******************************************************************************/ select(tabId) {
// Select a specific tab.
vAPI.tabs.select = function(tabId) {
tabId = toChromiumTabId(tabId); tabId = toChromiumTabId(tabId);
if ( tabId === 0 ) { return; } if ( tabId === 0 ) { return; }
@ -626,12 +572,10 @@ vAPI.tabs.select = function(tabId) {
if ( chrome.windows instanceof Object === false ) { return; } if ( chrome.windows instanceof Object === false ) { return; }
chrome.windows.update(tab.windowId, { focused: true }); chrome.windows.update(tab.windowId, { focused: true });
}); });
}; }
/******************************************************************************/ injectScript(tabId, details, callback) {
const onScriptExecuted = function() {
vAPI.tabs.injectScript = function(tabId, details, callback) {
var onScriptExecuted = function() {
// https://code.google.com/p/chromium/issues/detail?id=410868#c8 // https://code.google.com/p/chromium/issues/detail?id=410868#c8
void chrome.runtime.lastError; void chrome.runtime.lastError;
if ( typeof callback === 'function' ) { if ( typeof callback === 'function' ) {
@ -643,6 +587,37 @@ vAPI.tabs.injectScript = function(tabId, details, callback) {
} else { } else {
chrome.tabs.executeScript(details, onScriptExecuted); chrome.tabs.executeScript(details, onScriptExecuted);
} }
}
// https://forums.lanik.us/viewtopic.php?f=62&t=32826
// Chromium-based browsers: sanitize target URL. I've seen data: URI with
// newline characters in standard fields, possibly as a way of evading
// filters. As per spec, there should be no whitespaces in a data: URI's
// standard fields.
sanitizeURL(url) {
if ( url.startsWith('data:') === false ) { return url; }
const pos = url.indexOf(',');
if ( pos === -1 ) { return url; }
const s = url.slice(0, pos);
if ( s.search(/\s/) === -1 ) { return url; }
return s.replace(/\s+/, '') + url.slice(pos);
}
onActivated(/* details */) {
}
onClosed(/* tabId, details */) {
}
onCreated(/* openedTabId, openerTabId */) {
}
onNavigation(/* details */) {
}
onUpdated(/* tabId, changeInfo, tab */) {
}
}; };
/******************************************************************************/ /******************************************************************************/

View file

@ -54,6 +54,310 @@
return `http://${fakeHostname}/`; return `http://${fakeHostname}/`;
}; };
/******************************************************************************/
// https://github.com/gorhill/uBlock/issues/99
// https://github.com/gorhill/uBlock/issues/991
//
// popup:
// Test/close target URL
// popunder:
// Test/close opener URL
//
// popup filter match:
// 0 = false
// 1 = true
//
// opener: 0 0 1 1
// target: 0 1 0 1
// ---- ---- ---- ----
// result: a b c d
//
// a: do nothing
// b: close target
// c: close opener
// d: close target
µBlock.onPopupUpdated = (( ) => {
const µb = µBlock;
// The same context object will be reused everytime. This also allows to
// remember whether a popup or popunder was matched.
const fctxt = µBlock.filteringContext.setFilter(undefined);
// https://github.com/gorhill/uBlock/commit/1d448b85b2931412508aa01bf899e0b6f0033626#commitcomment-14944764
// See if two URLs are different, disregarding scheme -- because the
// scheme can be unilaterally changed by the browser.
// https://github.com/gorhill/uBlock/issues/1378
// Maybe no link element was clicked.
// https://github.com/gorhill/uBlock/issues/3287
// Do not bail out if the target URL has no hostname.
const areDifferentURLs = function(a, b) {
if ( b === '' ) { return true; }
if ( b.startsWith('about:') ) { return false; }
let pos = a.indexOf('://');
if ( pos === -1 ) { return false; }
a = a.slice(pos);
pos = b.indexOf('://');
if ( pos !== -1 ) {
b = b.slice(pos);
}
return b !== a;
};
const popupMatch = function(openerURL, targetURL, popupType) {
fctxt.setTabOriginFromURL(openerURL)
.setDocOriginFromURL(openerURL)
.setURL(targetURL)
.setType('popup');
let result;
// https://github.com/gorhill/uBlock/issues/1735
// Do not bail out on `data:` URI, they are commonly used for popups.
// https://github.com/uBlockOrigin/uAssets/issues/255
// Do not bail out on `about:blank`: an `about:blank` popup can be
// opened, with the sole purpose to serve as an intermediary in
// a sequence of chained popups.
// https://github.com/uBlockOrigin/uAssets/issues/263#issuecomment-272615772
// Do not bail out, period: the static filtering engine must be
// able to examine all sorts of URLs for popup filtering purpose.
// Dynamic filtering makes sense only when we have a valid opener
// hostname.
// https://github.com/gorhill/uBlock/commit/1d448b85b2931412508aa01bf899e0b6f0033626#commitcomment-14944764
// Ignore bad target URL. On Firefox, an `about:blank` tab may be
// opened for a new tab before it is filled in with the real target
// URL.
if ( fctxt.getTabHostname() !== '' && targetURL !== 'about:blank' ) {
// Check per-site switch first
// https://github.com/gorhill/uBlock/issues/3060
// - The no-popups switch must apply only to popups, not to
// popunders.
if (
popupType === 'popup' &&
µb.sessionSwitches.evaluateZ(
'no-popups',
fctxt.getTabHostname()
)
) {
fctxt.filter = {
raw: 'no-popups: ' + µb.sessionSwitches.z + ' true',
result: 1,
source: 'switch'
};
return 1;
}
// https://github.com/gorhill/uBlock/issues/581
// Take into account popup-specific rules in dynamic URL
// filtering, OR generic allow rules.
result = µb.sessionURLFiltering.evaluateZ(
fctxt.getTabHostname(),
targetURL,
popupType
);
if (
result === 1 && µb.sessionURLFiltering.type === popupType ||
result === 2
) {
fctxt.filter = µb.sessionURLFiltering.toLogData();
return result;
}
// https://github.com/gorhill/uBlock/issues/581
// Take into account `allow` rules in dynamic filtering: `block`
// rules are ignored, as block rules are not meant to block
// specific types like `popup` (just like with static filters).
result = µb.sessionFirewall.evaluateCellZY(
fctxt.getTabHostname(),
fctxt.getHostname(),
popupType
);
if ( result === 2 ) {
fctxt.filter = µb.sessionFirewall.toLogData();
return 2;
}
}
// https://github.com/chrisaljoudi/uBlock/issues/323
// https://github.com/chrisaljoudi/uBlock/issues/1142
// Don't block if uBlock is turned off in popup's context
if ( µb.getNetFilteringSwitch(targetURL) ) {
fctxt.type = popupType;
result = µb.staticNetFilteringEngine.matchString(fctxt, 0b0001);
if ( result !== 0 ) {
fctxt.filter = µb.staticNetFilteringEngine.toLogData();
return result;
}
}
return 0;
};
const mapPopunderResult = function(popunderURL, popunderHostname, result) {
if (
fctxt.filter === undefined ||
fctxt.filter !== 'static' ||
fctxt.filter.token === µb.staticNetFilteringEngine.noTokenHash
) {
return 0;
}
if ( fctxt.filter.token === µb.staticNetFilteringEngine.dotTokenHash ) {
return result;
}
const re = new RegExp(fctxt.filter.regex, 'i');
const matches = re.exec(popunderURL);
if ( matches === null ) { return 0; }
const beg = matches.index;
const end = beg + matches[0].length;
const pos = popunderURL.indexOf(popunderHostname);
if ( pos === -1 ) { return 0; }
// https://github.com/gorhill/uBlock/issues/1471
// We test whether the opener hostname as at least one character
// within matched portion of URL.
// https://github.com/gorhill/uBlock/issues/1903
// Ignore filters which cause a match before the start of the
// hostname in the URL.
return beg >= pos && beg < pos + popunderHostname.length && end > pos
? result
: 0;
};
const popunderMatch = function(openerURL, targetURL) {
let result = popupMatch(targetURL, openerURL, 'popunder');
if ( result === 1 ) { return result; }
// https://github.com/gorhill/uBlock/issues/1010#issuecomment-186824878
// Check the opener tab as if it were the newly opened tab: if there
// is a hit against a popup filter, and if the matching filter is not
// a broad one, we will consider the opener tab to be a popunder tab.
// For now, a "broad" filter is one which does not touch any part of
// the hostname part of the opener URL.
let popunderURL = openerURL,
popunderHostname = µb.URI.hostnameFromURI(popunderURL);
if ( popunderHostname === '' ) { return 0; }
result = mapPopunderResult(
popunderURL,
popunderHostname,
popupMatch(targetURL, popunderURL, 'popup')
);
if ( result !== 0 ) { return result; }
// https://github.com/gorhill/uBlock/issues/1598
// Try to find a match against origin part of the opener URL.
popunderURL = µb.URI.originFromURI(popunderURL);
if ( popunderURL === '' ) { return 0; }
return mapPopunderResult(
popunderURL,
popunderHostname,
popupMatch(targetURL, popunderURL, 'popup')
);
};
return function(targetTabId, openerDetails) {
// Opener details.
const openerTabId = openerDetails.tabId;
let tabContext = µb.tabContextManager.lookup(openerTabId);
if ( tabContext === null ) { return; }
const openerURL = tabContext.rawURL;
if ( openerURL === '' ) { return; }
// Popup details.
tabContext = µb.tabContextManager.lookup(targetTabId);
if ( tabContext === null ) { return; }
let targetURL = tabContext.rawURL;
if ( targetURL === '' ) { return; }
// https://github.com/gorhill/uBlock/issues/341
// Allow popups if uBlock is turned off in opener's context.
if ( µb.getNetFilteringSwitch(openerURL) === false ) { return; }
// https://github.com/gorhill/uBlock/issues/1538
if (
µb.getNetFilteringSwitch(µb.normalizePageURL(
openerTabId,
openerURL)
) === false
) {
return;
}
// If the page URL is that of our "blocked page" URL, extract the URL of
// the page which was blocked.
if ( targetURL.startsWith(vAPI.getURL('document-blocked.html')) ) {
const matches = /details=([^&]+)/.exec(targetURL);
if ( matches !== null ) {
targetURL = JSON.parse(atob(matches[1])).url;
}
}
// Popup test.
let popupType = 'popup',
result = 0;
// https://github.com/gorhill/uBlock/issues/2919
// - If the target tab matches a clicked link, assume it's legit.
if ( areDifferentURLs(targetURL, openerDetails.trustedURL) ) {
result = popupMatch(openerURL, targetURL, 'popup');
}
// Popunder test.
if ( result === 0 && openerDetails.popunder ) {
result = popunderMatch(openerURL, targetURL);
if ( result === 1 ) {
popupType = 'popunder';
}
}
// Log only for when there was a hit against an actual filter (allow or block).
// https://github.com/gorhill/uBlock/issues/2776
if ( µb.logger.enabled ) {
fctxt.setRealm('network').setType(popupType);
if ( popupType === 'popup' ) {
fctxt.setURL(targetURL)
.setTabId(openerTabId)
.setTabOriginFromURL(openerURL)
.setDocOriginFromURL(openerURL);
} else {
fctxt.setURL(openerURL)
.setTabId(targetTabId)
.setTabOriginFromURL(targetURL)
.setDocOriginFromURL(targetURL);
}
fctxt.toLogger();
}
// Not blocked
if ( result !== 1 ) {
return;
}
// Only if a popup was blocked do we report it in the dynamic
// filtering pane.
const pageStore = µb.pageStoreFromTabId(openerTabId);
if ( pageStore ) {
pageStore.journalAddRequest(fctxt.getHostname(), result);
pageStore.popupBlockedCount += 1;
}
// Blocked
if ( µb.userSettings.showIconBadge ) {
µb.updateToolbarIcon(openerTabId, 0x02);
}
// It is a popup, block and remove the tab.
if ( popupType === 'popup' ) {
µb.unbindTabFromPageStats(targetTabId);
vAPI.tabs.remove(targetTabId, false);
} else {
µb.unbindTabFromPageStats(openerTabId);
vAPI.tabs.remove(openerTabId, true);
}
return true;
};
})();
/******************************************************************************/ /******************************************************************************/
/****************************************************************************** /******************************************************************************
@ -175,7 +479,7 @@ housekeep itself.
if ( targetTabId === candidate.opener.tabId ) { if ( targetTabId === candidate.opener.tabId ) {
candidate.opener.popunder = true; candidate.opener.popunder = true;
} }
if ( vAPI.tabs.onPopupUpdated(tabId, candidate.opener) === true ) { if ( µb.onPopupUpdated(tabId, candidate.opener) === true ) {
candidate.destroy(); candidate.destroy();
} else { } else {
candidate.launchSelfDestruction(); candidate.launchSelfDestruction();
@ -183,7 +487,7 @@ housekeep itself.
} }
}; };
vAPI.tabs.onPopupCreated = function(targetTabId, openerTabId) { const onTabCreated = function(targetTabId, openerTabId) {
const popup = popupCandidates.get(targetTabId); const popup = popupCandidates.get(targetTabId);
if ( popup === undefined ) { if ( popup === undefined ) {
popupCandidates.set( popupCandidates.set(
@ -472,22 +776,52 @@ housekeep itself.
}; };
return { return {
push: push, push,
commit: commit, commit,
lookup: lookup, lookup,
mustLookup: mustLookup, mustLookup,
exists: exists, exists,
createContext: createContext createContext,
onTabCreated,
}; };
})(); })();
/******************************************************************************/ /******************************************************************************/
/******************************************************************************/ /******************************************************************************/
vAPI.Tabs = class extends vAPI.Tabs {
onActivated(details) {
super.onActivated(details);
if ( vAPI.isBehindTheSceneTabId(details.tabId) ) { return; }
// https://github.com/uBlockOrigin/uBlock-issues/issues/680
µBlock.updateToolbarIcon(details.tabId);
µBlock.contextMenu.update(details.tabId);
}
onClosed(tabId) {
super.onClosed(tabId);
if ( vAPI.isBehindTheSceneTabId(tabId) ) { return; }
µBlock.unbindTabFromPageStats(tabId);
µBlock.contextMenu.update();
}
onCreated(targetTabId, openerTabId) {
super.onCreated(targetTabId, openerTabId);
µBlock.tabContextManager.onTabCreated(targetTabId, openerTabId);
}
// When the DOM content of root frame is loaded, this means the tab // When the DOM content of root frame is loaded, this means the tab
// content has changed. // content has changed.
//
// The 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
// properly setup if network requests are fired from within the tab.
// Example: Chromium + case #6 at
// http://raymondhill.net/ublock/popup.html
vAPI.tabs.onNavigation = function(details) { onNavigation(details) {
super.onNavigation(details);
const µb = µBlock; const µb = µBlock;
if ( details.frameId === 0 ) { if ( details.frameId === 0 ) {
µb.tabContextManager.commit(details.tabId, details.url); µb.tabContextManager.commit(details.tabId, details.url);
@ -502,334 +836,22 @@ vAPI.tabs.onNavigation = function(details) {
µb.scriptletFilteringEngine.injectNow(details); µb.scriptletFilteringEngine.injectNow(details);
} }
} }
}; }
/******************************************************************************/
// It may happen the URL in the tab changes, while the page's document // It may happen the URL in the tab changes, while the page's document
// stays the same (for instance, Google Maps). Without this listener, // stays the same (for instance, Google Maps). Without this listener,
// the extension icon won't be properly refreshed. // the extension icon won't be properly refreshed.
vAPI.tabs.onUpdated = function(tabId, changeInfo, tab) { onUpdated(tabId, changeInfo, tab) {
super.onUpdated(tabId, changeInfo, tab);
if ( !tab.url || tab.url === '' ) { return; } if ( !tab.url || tab.url === '' ) { return; }
if ( !changeInfo.url ) { return; } if ( !changeInfo.url ) { return; }
µBlock.tabContextManager.commit(tabId, changeInfo.url); µBlock.tabContextManager.commit(tabId, changeInfo.url);
µBlock.bindTabToPageStats(tabId, 'tabUpdated'); µBlock.bindTabToPageStats(tabId, 'tabUpdated');
}
}; };
/******************************************************************************/ vAPI.tabs = new vAPI.Tabs();
vAPI.tabs.onClosed = function(tabId) {
if ( vAPI.isBehindTheSceneTabId(tabId) ) { return; }
µBlock.unbindTabFromPageStats(tabId);
µBlock.contextMenu.update();
};
/******************************************************************************/
// https://github.com/gorhill/uBlock/issues/99
// https://github.com/gorhill/uBlock/issues/991
//
// popup:
// Test/close target URL
// popunder:
// Test/close opener URL
//
// popup filter match:
// 0 = false
// 1 = true
//
// opener: 0 0 1 1
// target: 0 1 0 1
// ---- ---- ---- ----
// result: a b c d
//
// a: do nothing
// b: close target
// c: close opener
// d: close target
vAPI.tabs.onPopupUpdated = (( ) => {
const µb = µBlock;
// The same context object will be reused everytime. This also allows to
// remember whether a popup or popunder was matched.
const fctxt = µBlock.filteringContext.setFilter(undefined);
// https://github.com/gorhill/uBlock/commit/1d448b85b2931412508aa01bf899e0b6f0033626#commitcomment-14944764
// See if two URLs are different, disregarding scheme -- because the
// scheme can be unilaterally changed by the browser.
// https://github.com/gorhill/uBlock/issues/1378
// Maybe no link element was clicked.
// https://github.com/gorhill/uBlock/issues/3287
// Do not bail out if the target URL has no hostname.
const areDifferentURLs = function(a, b) {
if ( b === '' ) { return true; }
if ( b.startsWith('about:') ) { return false; }
let pos = a.indexOf('://');
if ( pos === -1 ) { return false; }
a = a.slice(pos);
pos = b.indexOf('://');
if ( pos !== -1 ) {
b = b.slice(pos);
}
return b !== a;
};
const popupMatch = function(openerURL, targetURL, popupType) {
fctxt.setTabOriginFromURL(openerURL)
.setDocOriginFromURL(openerURL)
.setURL(targetURL)
.setType('popup');
let result;
// https://github.com/gorhill/uBlock/issues/1735
// Do not bail out on `data:` URI, they are commonly used for popups.
// https://github.com/uBlockOrigin/uAssets/issues/255
// Do not bail out on `about:blank`: an `about:blank` popup can be
// opened, with the sole purpose to serve as an intermediary in
// a sequence of chained popups.
// https://github.com/uBlockOrigin/uAssets/issues/263#issuecomment-272615772
// Do not bail out, period: the static filtering engine must be
// able to examine all sorts of URLs for popup filtering purpose.
// Dynamic filtering makes sense only when we have a valid opener
// hostname.
// https://github.com/gorhill/uBlock/commit/1d448b85b2931412508aa01bf899e0b6f0033626#commitcomment-14944764
// Ignore bad target URL. On Firefox, an `about:blank` tab may be
// opened for a new tab before it is filled in with the real target
// URL.
if ( fctxt.getTabHostname() !== '' && targetURL !== 'about:blank' ) {
// Check per-site switch first
// https://github.com/gorhill/uBlock/issues/3060
// - The no-popups switch must apply only to popups, not to
// popunders.
if (
popupType === 'popup' &&
µb.sessionSwitches.evaluateZ(
'no-popups',
fctxt.getTabHostname()
)
) {
fctxt.filter = {
raw: 'no-popups: ' + µb.sessionSwitches.z + ' true',
result: 1,
source: 'switch'
};
return 1;
}
// https://github.com/gorhill/uBlock/issues/581
// Take into account popup-specific rules in dynamic URL
// filtering, OR generic allow rules.
result = µb.sessionURLFiltering.evaluateZ(
fctxt.getTabHostname(),
targetURL,
popupType
);
if (
result === 1 && µb.sessionURLFiltering.type === popupType ||
result === 2
) {
fctxt.filter = µb.sessionURLFiltering.toLogData();
return result;
}
// https://github.com/gorhill/uBlock/issues/581
// Take into account `allow` rules in dynamic filtering: `block`
// rules are ignored, as block rules are not meant to block
// specific types like `popup` (just like with static filters).
result = µb.sessionFirewall.evaluateCellZY(
fctxt.getTabHostname(),
fctxt.getHostname(),
popupType
);
if ( result === 2 ) {
fctxt.filter = µb.sessionFirewall.toLogData();
return 2;
}
}
// https://github.com/chrisaljoudi/uBlock/issues/323
// https://github.com/chrisaljoudi/uBlock/issues/1142
// Don't block if uBlock is turned off in popup's context
if ( µb.getNetFilteringSwitch(targetURL) ) {
fctxt.type = popupType;
result = µb.staticNetFilteringEngine.matchString(fctxt, 0b0001);
if ( result !== 0 ) {
fctxt.filter = µb.staticNetFilteringEngine.toLogData();
return result;
}
}
return 0;
};
const mapPopunderResult = function(popunderURL, popunderHostname, result) {
if (
fctxt.filter === undefined ||
fctxt.filter !== 'static' ||
fctxt.filter.token === µb.staticNetFilteringEngine.noTokenHash
) {
return 0;
}
if ( fctxt.filter.token === µb.staticNetFilteringEngine.dotTokenHash ) {
return result;
}
const re = new RegExp(fctxt.filter.regex, 'i');
const matches = re.exec(popunderURL);
if ( matches === null ) { return 0; }
const beg = matches.index;
const end = beg + matches[0].length;
const pos = popunderURL.indexOf(popunderHostname);
if ( pos === -1 ) { return 0; }
// https://github.com/gorhill/uBlock/issues/1471
// We test whether the opener hostname as at least one character
// within matched portion of URL.
// https://github.com/gorhill/uBlock/issues/1903
// Ignore filters which cause a match before the start of the
// hostname in the URL.
return beg >= pos && beg < pos + popunderHostname.length && end > pos
? result
: 0;
};
const popunderMatch = function(openerURL, targetURL) {
let result = popupMatch(targetURL, openerURL, 'popunder');
if ( result === 1 ) { return result; }
// https://github.com/gorhill/uBlock/issues/1010#issuecomment-186824878
// Check the opener tab as if it were the newly opened tab: if there
// is a hit against a popup filter, and if the matching filter is not
// a broad one, we will consider the opener tab to be a popunder tab.
// For now, a "broad" filter is one which does not touch any part of
// the hostname part of the opener URL.
let popunderURL = openerURL,
popunderHostname = µb.URI.hostnameFromURI(popunderURL);
if ( popunderHostname === '' ) { return 0; }
result = mapPopunderResult(
popunderURL,
popunderHostname,
popupMatch(targetURL, popunderURL, 'popup')
);
if ( result !== 0 ) { return result; }
// https://github.com/gorhill/uBlock/issues/1598
// Try to find a match against origin part of the opener URL.
popunderURL = µb.URI.originFromURI(popunderURL);
if ( popunderURL === '' ) { return 0; }
return mapPopunderResult(
popunderURL,
popunderHostname,
popupMatch(targetURL, popunderURL, 'popup')
);
};
return function(targetTabId, openerDetails) {
// Opener details.
const openerTabId = openerDetails.tabId;
let tabContext = µb.tabContextManager.lookup(openerTabId);
if ( tabContext === null ) { return; }
const openerURL = tabContext.rawURL;
if ( openerURL === '' ) { return; }
// Popup details.
tabContext = µb.tabContextManager.lookup(targetTabId);
if ( tabContext === null ) { return; }
let targetURL = tabContext.rawURL;
if ( targetURL === '' ) { return; }
// https://github.com/gorhill/uBlock/issues/341
// Allow popups if uBlock is turned off in opener's context.
if ( µb.getNetFilteringSwitch(openerURL) === false ) { return; }
// https://github.com/gorhill/uBlock/issues/1538
if (
µb.getNetFilteringSwitch(µb.normalizePageURL(
openerTabId,
openerURL)
) === false
) {
return;
}
// If the page URL is that of our "blocked page" URL, extract the URL of
// the page which was blocked.
if ( targetURL.startsWith(vAPI.getURL('document-blocked.html')) ) {
const matches = /details=([^&]+)/.exec(targetURL);
if ( matches !== null ) {
targetURL = JSON.parse(atob(matches[1])).url;
}
}
// Popup test.
let popupType = 'popup',
result = 0;
// https://github.com/gorhill/uBlock/issues/2919
// - If the target tab matches a clicked link, assume it's legit.
if ( areDifferentURLs(targetURL, openerDetails.trustedURL) ) {
result = popupMatch(openerURL, targetURL, 'popup');
}
// Popunder test.
if ( result === 0 && openerDetails.popunder ) {
result = popunderMatch(openerURL, targetURL);
if ( result === 1 ) {
popupType = 'popunder';
}
}
// Log only for when there was a hit against an actual filter (allow or block).
// https://github.com/gorhill/uBlock/issues/2776
if ( µb.logger.enabled ) {
fctxt.setRealm('network').setType(popupType);
if ( popupType === 'popup' ) {
fctxt.setURL(targetURL)
.setTabId(openerTabId)
.setTabOriginFromURL(openerURL)
.setDocOriginFromURL(openerURL);
} else {
fctxt.setURL(openerURL)
.setTabId(targetTabId)
.setTabOriginFromURL(targetURL)
.setDocOriginFromURL(targetURL);
}
fctxt.toLogger();
}
// Not blocked
if ( result !== 1 ) {
return;
}
// Only if a popup was blocked do we report it in the dynamic
// filtering pane.
const pageStore = µb.pageStoreFromTabId(openerTabId);
if ( pageStore ) {
pageStore.journalAddRequest(fctxt.getHostname(), result);
pageStore.popupBlockedCount += 1;
}
// Blocked
if ( µb.userSettings.showIconBadge ) {
µb.updateToolbarIcon(openerTabId, 0x02);
}
// It is a popup, block and remove the tab.
if ( popupType === 'popup' ) {
µb.unbindTabFromPageStats(targetTabId);
vAPI.tabs.remove(targetTabId, false);
} else {
µb.unbindTabFromPageStats(openerTabId);
vAPI.tabs.remove(openerTabId, true);
}
return true;
};
})();
vAPI.tabs.registerListeners();
/******************************************************************************/ /******************************************************************************/
/******************************************************************************/ /******************************************************************************/
@ -934,21 +956,22 @@ vAPI.tabs.registerListeners();
const tabIdToDetails = new Map(); const tabIdToDetails = new Map();
const updateBadge = function(tabId) { const updateBadge = function(tabId) {
const µb = µBlock;
const parts = tabIdToDetails.get(tabId); const parts = tabIdToDetails.get(tabId);
tabIdToDetails.delete(tabId); tabIdToDetails.delete(tabId);
let state = 0; let state = 0;
let badge = ''; let badge = '';
let pageStore = this.pageStoreFromTabId(tabId); let pageStore = µb.pageStoreFromTabId(tabId);
if ( pageStore !== null ) { if ( pageStore !== null ) {
state = pageStore.getNetFilteringSwitch() ? 1 : 0; state = pageStore.getNetFilteringSwitch() ? 1 : 0;
if ( if (
state === 1 && state === 1 &&
this.userSettings.showIconBadge && µb.userSettings.showIconBadge &&
pageStore.perLoadBlockedRequestCount pageStore.perLoadBlockedRequestCount
) { ) {
badge = this.formatCount(pageStore.perLoadBlockedRequestCount); badge = µb.formatCount(pageStore.perLoadBlockedRequestCount);
} }
} }
@ -958,13 +981,15 @@ vAPI.tabs.registerListeners();
// parts: bit 0 = icon // parts: bit 0 = icon
// bit 1 = badge // bit 1 = badge
return function(tabId, newParts) { return function(tabId, newParts = 0b11) {
if ( vAPI.isBehindTheSceneTabId(tabId) ) { return; } if ( vAPI.isBehindTheSceneTabId(tabId) ) { return; }
if ( newParts === undefined ) { newParts = 0x03; }
let currentParts = tabIdToDetails.get(tabId); let currentParts = tabIdToDetails.get(tabId);
if ( currentParts === newParts ) { return; } if ( currentParts === newParts ) { return; }
if ( currentParts === undefined ) { if ( currentParts === undefined ) {
vAPI.setTimeout(updateBadge.bind(this, tabId), 701); self.requestIdleCallback(
( ) => updateBadge(tabId),
{ timeout: 701 }
);
} else { } else {
newParts |= currentParts; newParts |= currentParts;
} }