summaryrefslogtreecommitdiffstats
path: root/src/js/tab.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/js/tab.js')
-rw-r--r--src/js/tab.js1178
1 files changed, 1178 insertions, 0 deletions
diff --git a/src/js/tab.js b/src/js/tab.js
new file mode 100644
index 0000000..c505e5a
--- /dev/null
+++ b/src/js/tab.js
@@ -0,0 +1,1178 @@
+/*******************************************************************************
+
+ uBlock Origin - a comprehensive, efficient content blocker
+ Copyright (C) 2014-present Raymond Hill
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ Home: https://github.com/gorhill/uBlock
+*/
+
+'use strict';
+
+/******************************************************************************/
+
+import contextMenu from './contextmenu.js';
+import logger from './logger.js';
+import scriptletFilteringEngine from './scriptlet-filtering.js';
+import staticNetFilteringEngine from './static-net-filtering.js';
+import µb from './background.js';
+import webext from './webext.js';
+import { PageStore } from './pagestore.js';
+import { i18n$ } from './i18n.js';
+
+import {
+ sessionFirewall,
+ sessionSwitches,
+ sessionURLFiltering,
+} from './filtering-engines.js';
+
+import {
+ domainFromHostname,
+ hostnameFromURI,
+ originFromURI,
+} from './uri-utils.js';
+
+/******************************************************************************/
+/******************************************************************************/
+
+// https://github.com/gorhill/httpswitchboard/issues/303
+// Any scheme other than 'http' and 'https' is remapped into a fake
+// URL which trick the rest of µBlock into being able to process an
+// otherwise unmanageable scheme. µBlock needs web page to have a proper
+// hostname to work properly, so just like the 'chromium-behind-the-scene'
+// fake domain name, we map unknown schemes into a fake '{scheme}-scheme'
+// hostname. This way, for a specific scheme you can create scope with
+// rules which will apply only to that scheme.
+
+µb.normalizeTabURL = (( ) => {
+ const tabURLNormalizer = new URL('about:blank');
+
+ return (tabId, tabURL) => {
+ if ( tabId < 0 ) {
+ return 'http://behind-the-scene/';
+ }
+ try {
+ tabURLNormalizer.href = tabURL;
+ } catch(ex) {
+ return tabURL;
+ }
+ const protocol = tabURLNormalizer.protocol.slice(0, -1);
+ if ( protocol === 'https' || protocol === 'http' ) {
+ return tabURLNormalizer.href;
+ }
+
+ let fakeHostname = protocol + '-scheme';
+
+ if ( tabURLNormalizer.hostname !== '' ) {
+ fakeHostname = tabURLNormalizer.hostname + '.' + fakeHostname;
+ } else if ( protocol === 'about' && protocol.pathname !== '' ) {
+ fakeHostname = tabURLNormalizer.pathname + '.' + 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
+
+const onPopupUpdated = (( ) => {
+ // 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(
+ fctxt,
+ rootOpenerURL,
+ localOpenerURL,
+ targetURL,
+ popupType = 'popup'
+ ) {
+ // https://github.com/chrisaljoudi/uBlock/issues/323
+ // https://github.com/chrisaljoudi/uBlock/issues/1142
+ // https://github.com/uBlockOrigin/uBlock-issues/issues/1616
+ // Don't block if uBO is turned off in popup's context
+ if (
+ µb.getNetFilteringSwitch(targetURL) === false ||
+ µb.getNetFilteringSwitch(µb.normalizeTabURL(0, targetURL)) === false
+ ) {
+ return 0;
+ }
+
+ fctxt.setTabOriginFromURL(rootOpenerURL)
+ .setDocOriginFromURL(localOpenerURL || rootOpenerURL)
+ .setURL(targetURL)
+ .setType('popup');
+
+ // 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' &&
+ sessionSwitches.evaluateZ(
+ 'no-popups',
+ fctxt.getTabHostname()
+ )
+ ) {
+ fctxt.filter = {
+ raw: 'no-popups: ' + 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.
+ let result = sessionURLFiltering.evaluateZ(
+ fctxt.getTabHostname(),
+ targetURL,
+ popupType
+ );
+ if (
+ result === 1 && sessionURLFiltering.type === popupType ||
+ result === 2
+ ) {
+ fctxt.filter = 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 = sessionFirewall.evaluateCellZY(
+ fctxt.getTabHostname(),
+ fctxt.getHostname(),
+ popupType
+ );
+ if ( result === 2 ) {
+ fctxt.filter = sessionFirewall.toLogData();
+ return 2;
+ }
+ }
+
+ fctxt.type = popupType;
+ const result = staticNetFilteringEngine.matchRequest(fctxt, 0b0001);
+ if ( result !== 0 ) {
+ fctxt.filter = staticNetFilteringEngine.toLogData();
+ return result;
+ }
+
+ return 0;
+ };
+
+ const mapPopunderResult = function(
+ fctxt,
+ popunderURL,
+ popunderHostname,
+ result
+ ) {
+ if ( fctxt.filter === undefined || fctxt.filter !== 'static' ) {
+ return 0;
+ }
+ if ( fctxt.filter.isUntokenized() ) {
+ return 0;
+ }
+ if ( fctxt.filter.isPureHostname() ) {
+ 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(
+ fctxt,
+ rootOpenerURL,
+ localOpenerURL,
+ targetURL
+ ) {
+ let result = popupMatch(
+ fctxt,
+ targetURL,
+ undefined,
+ rootOpenerURL,
+ '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 = rootOpenerURL,
+ popunderHostname = hostnameFromURI(popunderURL);
+ if ( popunderHostname === '' ) { return 0; }
+
+ result = mapPopunderResult(
+ fctxt,
+ popunderURL,
+ popunderHostname,
+ popupMatch(fctxt, targetURL, undefined, popunderURL)
+ );
+ 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 = originFromURI(popunderURL);
+ if ( popunderURL === '' ) { return 0; }
+
+ return mapPopunderResult(
+ fctxt,
+ popunderURL,
+ popunderHostname,
+ popupMatch(fctxt, targetURL, undefined, popunderURL)
+ );
+ };
+
+ return function(targetTabId, openerDetails) {
+ // Opener details.
+ const openerTabId = openerDetails.tabId;
+ let tabContext = µb.tabContextManager.lookup(openerTabId);
+ if ( tabContext === null ) { return; }
+ const rootOpenerURL = tabContext.rawURL;
+ if ( rootOpenerURL === '' ) { return; }
+ const pageStore = µb.pageStoreFromTabId(openerTabId);
+
+ // https://github.com/uBlockOrigin/uBlock-issues/discussions/2534#discussioncomment-5264792
+ // An `about:blank` frame's context is that of the parent context
+ let localOpenerURL = openerDetails.frameId !== 0
+ ? openerDetails.frameURL
+ : undefined;
+ if ( localOpenerURL === 'about:blank' && pageStore !== null ) {
+ let openerFrameId = openerDetails.frameId;
+ do {
+ const frame = pageStore.getFrameStore(openerFrameId);
+ if ( frame === null ) { break; }
+ openerFrameId = frame.parentId;
+ const parentFrame = pageStore.getFrameStore(openerFrameId);
+ if ( parentFrame === null ) { break; }
+ localOpenerURL = parentFrame.frameURL;
+ } while ( localOpenerURL === 'about:blank' && openerFrameId !== 0 );
+ }
+
+ // 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(rootOpenerURL) === false ) { return; }
+
+ // https://github.com/gorhill/uBlock/issues/1538
+ if (
+ µb.getNetFilteringSwitch(
+ µb.normalizeTabURL(openerTabId, rootOpenerURL)
+ ) === false
+ ) {
+ return;
+ }
+
+ // If the page URL is that of our document-blocked URL, extract the URL
+ // of the page which was blocked.
+ targetURL = µb.pageURLFromMaybeDocumentBlockedURL(targetURL);
+
+ // MUST be reset before code below is called.
+ const fctxt = µb.filteringContext.duplicate();
+
+ // 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.
+ // https://github.com/uBlockOrigin/uBlock-issues/issues/1912
+ // If the target also matches the last clicked link, assume it's
+ // legit.
+ if (
+ areDifferentURLs(targetURL, openerDetails.trustedURL) &&
+ areDifferentURLs(targetURL, µb.maybeGoodPopup.url)
+ ) {
+ result = popupMatch(fctxt, rootOpenerURL, localOpenerURL, targetURL);
+ }
+
+ // Popunder test.
+ if ( result === 0 && openerDetails.popunder ) {
+ result = popunderMatch(fctxt, rootOpenerURL, localOpenerURL, 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 ( logger.enabled ) {
+ fctxt.setRealm('network').setType(popupType);
+ if ( popupType === 'popup' ) {
+ fctxt.setURL(targetURL)
+ .setTabId(openerTabId)
+ .setTabOriginFromURL(rootOpenerURL)
+ .setDocOriginFromURL(localOpenerURL || rootOpenerURL);
+ } else {
+ fctxt.setURL(rootOpenerURL)
+ .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.
+ if ( pageStore ) {
+ pageStore.journalAddRequest(fctxt, result);
+ pageStore.popupBlockedCount += 1;
+ }
+
+ // Blocked
+ if ( µb.userSettings.showIconBadge ) {
+ µb.updateToolbarIcon(openerTabId, 0b010);
+ }
+
+ // It is a popup, block and remove the tab.
+ if ( popupType === 'popup' ) {
+ µb.unbindTabFromPageStore(targetTabId);
+ vAPI.tabs.remove(targetTabId, false);
+ } else {
+ µb.unbindTabFromPageStore(openerTabId);
+ vAPI.tabs.remove(openerTabId, true);
+ }
+
+ return true;
+ };
+})();
+
+/******************************************************************************/
+/******************************************************************************
+
+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
+<https://github.com/chrisaljoudi/uBlock/issues/516>
+
+- a network request for a secondary resource is labeled with a tab id for
+which no root document was pulled for that tab.
+<https://github.com/chrisaljoudi/uBlock/issues/1001>
+
+- 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.
+<https://github.com/chrisaljoudi/uBlock/issues/1205>
+<https://github.com/chrisaljoudi/uBlock/issues/1140>
+
+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 <https://github.com/chrisaljoudi/uBlock/issues/516>.
+
+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 <https://github.com/chrisaljoudi/uBlock/issues/1205>. 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
+<https://github.com/chrisaljoudi/uBlock/issues/1001>.
+
+The TabContext manager is self-contained, and it takes care to properly
+housekeep itself.
+
+*/
+
+µb.tabContextManager = (( ) => {
+ const tabContexts = new Map();
+
+ // 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.
+ let mostRecentRootDocURL = '';
+ let mostRecentRootDocURLTimestamp = 0;
+
+ const popupCandidates = new Map();
+
+ const PopupCandidate = class {
+ constructor(createDetails, openerDetails) {
+ this.targetTabId = createDetails.tabId;
+ this.opener = {
+ tabId: createDetails.sourceTabId,
+ tabURL: openerDetails[0].url,
+ frameId: createDetails.sourceFrameId,
+ frameURL: openerDetails[1].url,
+ popunder: false,
+ trustedURL: createDetails.sourceTabId === µb.maybeGoodPopup.tabId
+ ? µb.maybeGoodPopup.url
+ : ''
+ };
+ this.selfDestructionTimer = vAPI.defer.create(( ) => {
+ this.destroy();
+ });
+ this.launchSelfDestruction();
+ }
+
+ destroy() {
+ this.selfDestructionTimer.off();
+ popupCandidates.delete(this.targetTabId);
+ }
+
+ launchSelfDestruction() {
+ this.selfDestructionTimer.offon(10000);
+ }
+ };
+
+ const popupCandidateTest = async function(targetTabId) {
+ for ( const [ tabId, candidate ] of popupCandidates ) {
+ if (
+ targetTabId !== tabId &&
+ targetTabId !== candidate.opener.tabId
+ ) {
+ continue;
+ }
+ // https://github.com/gorhill/uBlock/issues/3129
+ // If the trigger is a change in the opener's URL, mark the entry
+ // as candidate for popunder filtering.
+ if ( targetTabId === candidate.opener.tabId ) {
+ candidate.opener.popunder = true;
+ }
+ const result = onPopupUpdated(tabId, candidate.opener);
+ if ( result === true ) {
+ candidate.destroy();
+ } else {
+ candidate.launchSelfDestruction();
+ }
+ }
+ };
+
+ // https://github.com/uBlockOrigin/uBlock-issues/issues/1184
+ // Do not consider a tab opened from `about:newtab` to be a popup
+ // candidate.
+
+ const onTabCreated = async function(createDetails) {
+ const { sourceTabId, sourceFrameId, tabId } = createDetails;
+ const popup = popupCandidates.get(tabId);
+ if ( popup === undefined ) {
+ let openerDetails;
+ try {
+ openerDetails = await Promise.all([
+ webext.webNavigation.getFrame({
+ tabId: createDetails.sourceTabId,
+ frameId: 0,
+ }),
+ webext.webNavigation.getFrame({
+ tabId: sourceTabId,
+ frameId: sourceFrameId,
+ }),
+ ]);
+ }
+ catch (reason) {
+ return;
+ }
+ if (
+ Array.isArray(openerDetails) === false ||
+ openerDetails.length !== 2 ||
+ openerDetails[1] === null ||
+ openerDetails[1].url === 'about:newtab'
+ ) {
+ return;
+ }
+ popupCandidates.set(
+ tabId,
+ new PopupCandidate(createDetails, openerDetails)
+ );
+ }
+ popupCandidateTest(tabId);
+ };
+
+ const gcPeriod = 10 * 60 * 1000;
+
+ // A pushed entry is removed from the stack unless it is committed with
+ // a set time.
+ const StackEntry = function(url, commit) {
+ this.url = url;
+ this.committed = commit;
+ this.tstamp = Date.now();
+ };
+
+ const TabContext = function(tabId) {
+ this.tabId = tabId;
+ this.stack = [];
+ this.rawURL =
+ this.normalURL =
+ this.origin =
+ this.rootHostname =
+ this.rootDomain = '';
+ this.commitTimer = vAPI.defer.create(( ) => {
+ this.onCommit();
+ });
+ this.gcTimer = vAPI.defer.create(( ) => {
+ this.onGC();
+ });
+ this.onGCBarrier = false;
+ this.netFiltering = true;
+ this.netFilteringReadTime = 0;
+
+ tabContexts.set(tabId, this);
+ };
+
+ TabContext.prototype.destroy = function() {
+ if ( vAPI.isBehindTheSceneTabId(this.tabId) ) { return; }
+ this.gcTimer.off();
+ tabContexts.delete(this.tabId);
+ };
+
+ TabContext.prototype.onGC = async function() {
+ if ( vAPI.isBehindTheSceneTabId(this.tabId) ) { return; }
+ if ( this.onGCBarrier ) { return; }
+ this.onGCBarrier = true;
+ this.gcTimer.off();
+ const tab = await vAPI.tabs.get(this.tabId);
+ if ( tab instanceof Object === false || tab.discarded === true ) {
+ this.destroy();
+ } else {
+ this.gcTimer.on(gcPeriod);
+ }
+ this.onGCBarrier = false;
+ };
+
+ // https://github.com/gorhill/uBlock/issues/248
+ // Stack entries have to be committed to stick. Non-committed stack
+ // entries are removed after a set delay.
+ TabContext.prototype.onCommit = function() {
+ if ( vAPI.isBehindTheSceneTabId(this.tabId) ) { return; }
+ this.commitTimer.off();
+ // Remove uncommitted entries at the top of the stack.
+ let i = this.stack.length;
+ while ( i-- ) {
+ if ( this.stack[i].committed ) { break; }
+ }
+ // https://github.com/gorhill/uBlock/issues/300
+ // If no committed entry was found, fall back on the bottom-most one
+ // as being the committed one by default.
+ if ( i === -1 && this.stack.length !== 0 ) {
+ this.stack[0].committed = true;
+ i = 0;
+ }
+ i += 1;
+ if ( i < this.stack.length ) {
+ this.stack.length = i;
+ this.update();
+ }
+ };
+
+ // 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 flush it.
+ TabContext.prototype.autodestroy = function() {
+ if ( vAPI.isBehindTheSceneTabId(this.tabId) ) { return; }
+ this.gcTimer.on(gcPeriod);
+ };
+
+ // Update just force all properties to be updated to match the most recent
+ // root URL.
+ // https://github.com/uBlockOrigin/uBlock-issues/issues/1954
+ // In case of document-blocked page, use the blocked page URL as the
+ // context.
+ TabContext.prototype.update = function() {
+ this.netFilteringReadTime = 0;
+ if ( this.stack.length === 0 ) {
+ this.rawURL =
+ this.normalURL =
+ this.origin =
+ this.rootHostname =
+ this.rootDomain = '';
+ return;
+ }
+ const stackEntry = this.stack[this.stack.length - 1];
+ this.rawURL = µb.pageURLFromMaybeDocumentBlockedURL(stackEntry.url);
+ this.normalURL = µb.normalizeTabURL(this.tabId, this.rawURL);
+ this.origin = originFromURI(this.normalURL);
+ this.rootHostname = hostnameFromURI(this.origin);
+ this.rootDomain =
+ domainFromHostname(this.rootHostname) ||
+ this.rootHostname;
+ };
+
+ // Called whenever a candidate root URL is spotted for the tab.
+ TabContext.prototype.push = function(url) {
+ if ( vAPI.isBehindTheSceneTabId(this.tabId) ) {
+ return;
+ }
+ const count = this.stack.length;
+ if ( count !== 0 && this.stack[count - 1].url === url ) {
+ return;
+ }
+ this.stack.push(new StackEntry(url));
+ this.update();
+ popupCandidateTest(this.tabId);
+ this.commitTimer.offon(500);
+ };
+
+ // 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; }
+ if ( this.stack.length !== 0 ) {
+ const top = this.stack[this.stack.length - 1];
+ if ( top.url === url && top.committed ) { return false; }
+ }
+ this.stack = [new StackEntry(url, true)];
+ this.update();
+ return true;
+ };
+
+ TabContext.prototype.getNetFilteringSwitch = function() {
+ if ( this.netFilteringReadTime > µb.netWhitelistModifyTime ) {
+ return this.netFiltering;
+ }
+ // https://github.com/chrisaljoudi/uBlock/issues/1078
+ // Use both the raw and normalized URLs.
+ this.netFiltering = µb.getNetFilteringSwitch(this.normalURL);
+ if (
+ this.netFiltering &&
+ this.rawURL !== this.normalURL &&
+ this.rawURL !== ''
+ ) {
+ this.netFiltering = µb.getNetFilteringSwitch(this.rawURL);
+ }
+ this.netFilteringReadTime = Date.now();
+ return this.netFiltering;
+ };
+
+ // These are to be used for the API of the tab context manager.
+
+ const push = function(tabId, url) {
+ let entry = tabContexts.get(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.
+ const lookup = function(tabId) {
+ return tabContexts.get(tabId) || null;
+ };
+
+ // 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.
+ const mustLookup = function(tabId) {
+ const entry = tabContexts.get(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.get(vAPI.noTabId);
+ };
+
+ // https://github.com/gorhill/uBlock/issues/1735
+ // Filter for popups if actually committing.
+ const commit = function(tabId, url) {
+ let entry = tabContexts.get(tabId);
+ if ( entry === undefined ) {
+ entry = push(tabId, url);
+ } else if ( entry.commit(url) ) {
+ popupCandidateTest(tabId);
+ }
+ return entry;
+ };
+
+ const exists = function(tabId) {
+ return tabContexts.get(tabId) !== undefined;
+ };
+
+ // Behind-the-scene tab context
+ {
+ const entry = new TabContext(vAPI.noTabId);
+ entry.stack.push(new StackEntry('', true));
+ entry.rawURL = '';
+ entry.normalURL = µb.normalizeTabURL(entry.tabId);
+ entry.origin = originFromURI(entry.normalURL);
+ entry.rootHostname = hostnameFromURI(entry.origin);
+ entry.rootDomain = domainFromHostname(entry.rootHostname);
+ }
+
+ // Context object, typically to be used to feed filtering engines.
+ const contextJunkyard = [];
+ const Context = class {
+ constructor(tabId) {
+ this.init(tabId);
+ }
+ init(tabId) {
+ const tabContext = lookup(tabId);
+ this.rootHostname = tabContext.rootHostname;
+ this.rootDomain = tabContext.rootDomain;
+ this.pageHostname =
+ this.pageDomain =
+ this.requestURL =
+ this.origin =
+ this.requestHostname =
+ this.requestDomain = '';
+ return this;
+ }
+ dispose() {
+ contextJunkyard.push(this);
+ }
+ };
+
+ const createContext = function(tabId) {
+ if ( contextJunkyard.length ) {
+ return contextJunkyard.pop().init(tabId);
+ }
+ return new Context(tabId);
+ };
+
+ return {
+ push,
+ commit,
+ lookup,
+ mustLookup,
+ exists,
+ createContext,
+ onTabCreated,
+ };
+})();
+
+/******************************************************************************/
+/******************************************************************************/
+
+vAPI.Tabs = class extends vAPI.Tabs {
+ onActivated(details) {
+ const { tabId } = details;
+ if ( vAPI.isBehindTheSceneTabId(tabId) ) { return; }
+ // https://github.com/uBlockOrigin/uBlock-issues/issues/757
+ const pageStore = µb.pageStoreFromTabId(tabId);
+ if ( pageStore === null ) {
+ this.onNewTab(tabId);
+ return;
+ }
+ super.onActivated(details);
+ // https://github.com/uBlockOrigin/uBlock-issues/issues/680
+ µb.updateToolbarIcon(tabId);
+ contextMenu.update(tabId);
+ }
+
+ onClosed(tabId) {
+ super.onClosed(tabId);
+ if ( vAPI.isBehindTheSceneTabId(tabId) ) { return; }
+ µb.unbindTabFromPageStore(tabId);
+ contextMenu.update();
+ }
+
+ onCreated(details) {
+ super.onCreated(details);
+ µb.tabContextManager.onTabCreated(details);
+ }
+
+ // When the DOM content of root frame is loaded, this means the tab
+ // 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
+ // https://github.com/uBlockOrigin/uBlock-issues/issues/688#issuecomment-748179731
+ // For non-network URIs, defer scriptlet injection to content script. The
+ // reason for this is that we need the effective URL and this information
+ // is not available at this point.
+ //
+ // https://github.com/uBlockOrigin/uBlock-issues/issues/2343
+ // uBO's isolated world in Firefox just does not work as expected at
+ // point, so we have to wait before injecting scriptlets.
+ onNavigation(details) {
+ super.onNavigation(details);
+ const { frameId, tabId, url } = details;
+ if ( frameId === 0 ) {
+ µb.tabContextManager.commit(tabId, url);
+ const pageStore = µb.bindTabToPageStore(tabId, 'tabCommitted', details);
+ if ( pageStore !== null ) {
+ pageStore.journalAddRootFrame('committed', url);
+ }
+ }
+ const pageStore = µb.pageStoreFromTabId(tabId);
+ if ( pageStore === null ) { return; }
+ pageStore.setFrameURL(details);
+ if ( pageStore.getNetFilteringSwitch() ) {
+ scriptletFilteringEngine.injectNow(details);
+ }
+ }
+
+ async onNewTab(tabId) {
+ const tab = await vAPI.tabs.get(tabId);
+ if ( tab === null ) { return; }
+ const { id, url = '' } = tab;
+ if ( url === '' ) { return; }
+ µb.tabContextManager.commit(id, url);
+ µb.bindTabToPageStore(id, 'tabUpdated', tab);
+ contextMenu.update(id);
+ }
+
+ // 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.
+ onUpdated(tabId, changeInfo, tab) {
+ super.onUpdated(tabId, changeInfo, tab);
+ if ( !tab.url || tab.url === '' ) { return; }
+ if ( !changeInfo.url ) { return; }
+ µb.tabContextManager.commit(tabId, changeInfo.url);
+ µb.bindTabToPageStore(tabId, 'tabUpdated', tab);
+ }
+};
+
+vAPI.tabs = new vAPI.Tabs();
+
+/******************************************************************************/
+/******************************************************************************/
+
+// Create an entry for the tab if it doesn't exist.
+
+µb.bindTabToPageStore = function(tabId, context, details = undefined) {
+ this.updateToolbarIcon(tabId, 0b111);
+
+ // Do not create a page store for URLs which are of no interests
+ if ( this.tabContextManager.exists(tabId) === false ) {
+ this.unbindTabFromPageStore(tabId);
+ return null;
+ }
+
+ // Reuse page store if one exists: this allows to guess if a tab is a popup
+ let pageStore = this.pageStores.get(tabId);
+
+ // Tab is not bound
+ if ( pageStore === undefined ) {
+ pageStore = PageStore.factory(tabId, details);
+ this.pageStores.set(tabId, pageStore);
+ this.pageStoresToken = Date.now();
+ return pageStore;
+ }
+
+ // https://github.com/chrisaljoudi/uBlock/issues/516
+ // Never rebind behind-the-scene scope.
+ if ( vAPI.isBehindTheSceneTabId(tabId) ) {
+ return pageStore;
+ }
+
+ // https://github.com/chrisaljoudi/uBlock/issues/516
+ // If context is 'beforeRequest', do not rebind, wait for confirmation.
+ if ( context === 'beforeRequest' ) {
+ pageStore.netFilteringCache.empty();
+ return pageStore;
+ }
+
+ // 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(context, details);
+
+ this.pageStoresToken = Date.now();
+
+ return pageStore;
+};
+
+/******************************************************************************/
+
+µb.unbindTabFromPageStore = function(tabId) {
+ const pageStore = this.pageStores.get(tabId);
+ if ( pageStore === undefined ) { return; }
+ pageStore.dispose();
+ this.pageStores.delete(tabId);
+ this.pageStoresToken = Date.now();
+};
+
+/******************************************************************************/
+
+µb.pageStoreFromTabId = function(tabId) {
+ return this.pageStores.get(tabId) || null;
+};
+
+µb.mustPageStoreFromTabId = function(tabId) {
+ return this.pageStores.get(tabId) || this.pageStores.get(vAPI.noTabId);
+};
+
+/******************************************************************************/
+
+// Permanent page store for behind-the-scene requests. Must never be removed.
+//
+// https://github.com/uBlockOrigin/uBlock-issues/issues/651
+// The whitelist status of the tabless page store will be determined by
+// the document context (if present) of the network request.
+
+{
+ const NoPageStore = class extends PageStore {
+ getNetFilteringSwitch(fctxt) {
+ if ( fctxt ) {
+ const docOrigin = fctxt.getDocOrigin();
+ if ( docOrigin ) {
+ return µb.getNetFilteringSwitch(docOrigin);
+ }
+ }
+ return super.getNetFilteringSwitch();
+ }
+ };
+ const pageStore = new NoPageStore(vAPI.noTabId);
+ µb.pageStores.set(pageStore.tabId, pageStore);
+ pageStore.title = i18n$('logBehindTheScene');
+}
+
+/******************************************************************************/
+
+// Update visual of extension icon.
+
+{
+ const tabIdToDetails = new Map();
+
+ const computeBadgeColor = (bits) => {
+ let color = µb.blockingProfileColorCache.get(bits);
+ if ( color !== undefined ) { return color; }
+ let max = 0;
+ for ( const profile of µb.liveBlockingProfiles ) {
+ const v = bits & (profile.bits & ~1);
+ if ( v < max ) { break; }
+ color = profile.color;
+ max = v;
+ }
+ if ( color === undefined ) {
+ color = '#666';
+ }
+ µb.blockingProfileColorCache.set(bits, color);
+ return color;
+ };
+
+ const updateBadge = (tabId) => {
+ let parts = tabIdToDetails.get(tabId);
+ tabIdToDetails.delete(tabId);
+
+ let state = 0;
+ let badge = '';
+ let color = '#666';
+
+ const pageStore = µb.pageStoreFromTabId(tabId);
+ if ( pageStore !== null ) {
+ state = pageStore.getNetFilteringSwitch() ? 1 : 0;
+ if ( state === 1 ) {
+ if ( (parts & 0b0010) !== 0 ) {
+ const blockCount = pageStore.counts.blocked.any;
+ if ( blockCount !== 0 ) {
+ badge = µb.formatCount(blockCount);
+ }
+ }
+ if ( (parts & 0b0100) !== 0 ) {
+ color = computeBadgeColor(
+ µb.blockingModeFromHostname(pageStore.tabHostname)
+ );
+ }
+ }
+ }
+
+ // https://www.reddit.com/r/uBlockOrigin/comments/d33d37/
+ if ( µb.userSettings.showIconBadge === false ) {
+ parts |= 0b1000;
+ }
+
+ vAPI.setIcon(tabId, { parts, state, badge, color });
+ };
+
+ // parts: bit 0 = icon
+ // bit 1 = badge text
+ // bit 2 = badge color
+ // bit 3 = hide badge
+
+ µb.updateToolbarIcon = function(tabId, newParts = 0b0111) {
+ if ( this.readyToFilter === false ) { return; }
+ if ( typeof tabId !== 'number' ) { return; }
+ if ( vAPI.isBehindTheSceneTabId(tabId) ) { return; }
+ const currentParts = tabIdToDetails.get(tabId);
+ if ( currentParts === newParts ) { return; }
+ if ( currentParts === undefined ) {
+ self.requestIdleCallback(
+ ( ) => updateBadge(tabId),
+ { timeout: 701 }
+ );
+ } else {
+ newParts |= currentParts;
+ }
+ tabIdToDetails.set(tabId, newParts);
+ };
+}
+
+/******************************************************************************/
+
+// https://github.com/chrisaljoudi/uBlock/issues/455
+// Stale page store entries janitor
+
+{
+ let pageStoreJanitorSampleAt = 0;
+ let pageStoreJanitorSampleSize = 10;
+
+ const checkTab = async tabId => {
+ const tab = await vAPI.tabs.get(tabId);
+ if ( tab instanceof Object && tab.discarded !== true ) { return; }
+ µb.unbindTabFromPageStore(tabId);
+ };
+
+ const pageStoreJanitor = function() {
+ const tabIds = Array.from(µb.pageStores.keys()).sort();
+ if ( pageStoreJanitorSampleAt >= tabIds.length ) {
+ pageStoreJanitorSampleAt = 0;
+ }
+ const n = Math.min(
+ pageStoreJanitorSampleAt + pageStoreJanitorSampleSize,
+ tabIds.length
+ );
+ for ( let i = pageStoreJanitorSampleAt; i < n; i++ ) {
+ const tabId = tabIds[i];
+ if ( vAPI.isBehindTheSceneTabId(tabId) ) { continue; }
+ checkTab(tabId);
+ }
+ pageStoreJanitorSampleAt = n;
+
+ pageStoreJanitorTimer.on(pageStoreJanitorPeriod);
+ };
+
+ const pageStoreJanitorTimer = vAPI.defer.create(pageStoreJanitor);
+ const pageStoreJanitorPeriod = { min: 15 };
+
+ pageStoreJanitorTimer.on(pageStoreJanitorPeriod);
+}
+
+/******************************************************************************/