123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755 |
- /*******************************************************************************
- ηMatrix - a browser extension to black/white list requests.
- Copyright (C) 2014-2019 Raymond Hill
- Copyright (C) 2019-2022 Alessio Vanni
- 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://gitlab.com/vannilla/ematrix
- uMatrix Home: https://github.com/gorhill/uMatrix
- */
- (function () {
- 'use strict';
- Cu.import('chrome://ematrix/content/lib/UriTools.jsm');
- var ηm = ηMatrix;
- // https://github.com/gorhill/httpswitchboard/issues/303
- // Some kind of trick going on here:
- // Any scheme other than 'http' and 'https' is remapped into a
- // fake URL which trick the rest of ηMatrix into being able to
- // process an otherwise unmanageable scheme. ηMatrix needs web
- // page to have a proper hostname to work properly, so just like
- // the '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.
- ηm.normalizePageURL = function (tabId, pageURL) {
- if (vAPI.isBehindTheSceneTabId(tabId)) {
- return 'http://' + this.behindTheSceneScope + '/';
- }
- // https://github.com/gorhill/uMatrix/issues/992
- if (pageURL.startsWith('wyciwyg:')) {
- // Matches strings like 'wyciwyg://101/'
- let filter = /^wyciwyg:\/\/\d+\//.exec(pageURL);
- if (filter) {
- pageURL = pageURL.slice(filter[0].length);
- }
- }
- // If the URL is that of our "blocked page" document, return
- // the URL of the blocked page.
- if (pageURL.lastIndexOf(vAPI.getURL('main-blocked.html'), 0) === 0) {
- let matches = /main-blocked\.html\?details=([^&]+)/.exec(pageURL);
- if (matches && matches.length === 2) {
- try {
- let details = JSON.parse(atob(matches[1]));
- pageURL = details.url;
- } catch (e) {
- }
- }
- }
- let uri = UriTools.set(pageURL);
- let scheme = uri.scheme;
- if (scheme === 'https' || scheme === 'http') {
- return UriTools.normalizedURI();
- }
- let fakeHostname = scheme + '-scheme';
- if (uri.hostname !== '') {
- fakeHostname = uri.hostname + '.' + fakeHostname;
- } else if (scheme === 'about') {
- fakeHostname = uri.path + '.' + fakeHostname;
- }
- return 'http://' + fakeHostname + '/';
- };
- /*
- 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.
- */
- ηm.tabContextManager = (function () {
- let tabContexts = Object.create(null);
- // https://github.com/chrisaljoudi/uBlock/issues/1001
- // This is to be used as last-resort fallback in case a tab is
- // found to not be bound while network requests are fired for
- // the tab.
- let mostRecentRootDocURL = '';
- let mostRecentRootDocURLTimestamp = 0;
- let gcPeriod = 31 * 60 * 1000; // every 31 minutes
- // A pushed entry is removed from the stack unless it is
- // committed with a set time.
- let StackEntry = function (url, commit) {
- this.url = url;
- this.committed = commit;
- this.tstamp = Date.now();
- };
- let TabContext = function (tabId) {
- this.tabId = tabId;
- this.stack = [];
- this.rawURL =
- this.normalURL =
- this.scheme =
- this.rootHostname =
- this.rootDomain = '';
- this.secure = false;
- this.commitTimer = null;
- this.gcTimer = null;
- tabContexts[tabId] = this;
- };
- TabContext.prototype.destroy = function () {
- if (vAPI.isBehindTheSceneTabId(this.tabId)) {
- return;
- }
- if (this.gcTimer !== null) {
- clearTimeout(this.gcTimer);
- this.gcTimer = null;
- }
- delete tabContexts[this.tabId];
- };
- TabContext.prototype.onTab = function (tab) {
- if (tab) {
- this.gcTimer = vAPI.setTimeout(this.onGC.bind(this), gcPeriod);
- } else {
- this.destroy();
- }
- };
- TabContext.prototype.onGC = function () {
- this.gcTimer = null;
- if (vAPI.isBehindTheSceneTabId(this.tabId)) {
- return;
- }
- vAPI.tabs.get(this.tabId, this.onTab.bind(this));
- };
- // 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 = null;
- // 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;
- if (i < this.stack.length) {
- this.stack.length = i;
- this.update();
- ηm.bindTabToPageStats(this.tabId, 'newURL');
- }
- };
- // 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 = vAPI.setTimeout(this.onGC.bind(this), gcPeriod);
- };
- // Update just force all properties to be updated to match the
- // most recent root URL.
- TabContext.prototype.update = function () {
- if (this.stack.length === 0) {
- this.rawURL =
- this.normalURL =
- this.scheme =
- this.rootHostname =
- this.rootDomain = '';
- this.secure = false;
- return;
- }
- this.rawURL = this.stack[this.stack.length - 1].url;
- this.normalURL = ηm.normalizePageURL(this.tabId, this.rawURL);
- this.scheme = UriTools.schemeFromURI(this.rawURL);
- this.rootHostname = UriTools.hostnameFromURI(this.normalURL);
- this.rootDomain = UriTools.domainFromHostname(this.rootHostname)
- || this.rootHostname;
- this.secure = UriTools.isSecureScheme(this.scheme);
- };
- // Called whenever a candidate root URL is spotted for the tab.
- TabContext.prototype.push = function (url, context) {
- if (vAPI.isBehindTheSceneTabId(this.tabId)) {
- return;
- }
- let committed = context !== undefined;
- let count = this.stack.length;
- let topEntry = this.stack[count - 1];
- if (topEntry && topEntry.url === url) {
- if (committed) {
- topEntry.committed = true;
- }
- return;
- }
- if (this.commitTimer !== null) {
- clearTimeout(this.commitTimer);
- }
- if (committed) {
- this.stack = [new StackEntry(url, true)];
- } else {
- this.stack.push(new StackEntry(url));
- this.commitTimer =
- vAPI.setTimeout(this.onCommit.bind(this), 1000);
- }
- this.update();
- ηm.bindTabToPageStats(this.tabId, context);
- };
- // These are to be used for the API of the tab context manager.
- let push = function (tabId, url, context) {
- let entry = tabContexts[tabId];
- if (entry === undefined) {
- entry = new TabContext(tabId);
- entry.autodestroy();
- }
- entry.push(url, context);
- mostRecentRootDocURL = url;
- mostRecentRootDocURLTimestamp = Date.now();
- return entry;
- };
- // Find a tab context for a specific tab. If none is found,
- // attempt to fix this. When all fail, the behind-the-scene
- // context is returned.
- let mustLookup = function (tabId, url) {
- let entry;
- if (url !== undefined) {
- entry = push(tabId, url);
- } else {
- entry = tabContexts[tabId];
- }
- if (entry !== undefined) {
- return entry;
- }
- // https://github.com/chrisaljoudi/uBlock/issues/1025
- // Google Hangout popup opens without a root frame. So for
- // now we will just discard that best-guess root frame if
- // it is too far in the future, at which point it ceases
- // to be a "best guess".
- if (mostRecentRootDocURL
- !== '' && mostRecentRootDocURLTimestamp + 500 < Date.now()) {
- mostRecentRootDocURL = '';
- }
- // https://github.com/chrisaljoudi/uBlock/issues/1001
- // Not a behind-the-scene request, yet no page store found
- // for the tab id: we will thus bind the last-seen root
- // document to the unbound tab. It's a guess, but better
- // than ending up filtering nothing at all.
- if (mostRecentRootDocURL !== '') {
- return push(tabId, mostRecentRootDocURL);
- }
- // If all else fail at finding a page store, re-categorize
- // the request as behind-the-scene. At least this ensures
- // that ultimately the user can still inspect/filter those
- // net requests which were about to fall through the
- // cracks.
- // Example: Chromium + case #12 at
- // http://raymondhill.net/ublock/popup.html
- return tabContexts[vAPI.noTabId];
- };
- let lookup = function (tabId) {
- return tabContexts[tabId] || null;
- };
- // Behind-the-scene tab context
- (function () {
- let entry = new TabContext(vAPI.noTabId);
- entry.stack.push(new StackEntry('', true));
- entry.rawURL = '';
- entry.normalURL = ηm.normalizePageURL(entry.tabId);
- entry.rootHostname = UriTools.hostnameFromURI(entry.normalURL);
- entry.rootDomain = UriTools.domainFromHostname(entry.rootHostname)
- || entry.rootHostname;
- })();
- // https://github.com/gorhill/uMatrix/issues/513
- // Force a badge update here, it could happen that all the
- // subsequent network requests are already in the page
- // store, which would cause the badge to no be updated for
- // these network requests.
- vAPI.tabs.onNavigation = function (details) {
- let tabId = details.tabId;
- if (vAPI.isBehindTheSceneTabId(tabId)) {
- return;
- }
- push(tabId, details.url, 'newURL');
- ηm.updateBadgeAsync(tabId);
- };
- // https://github.com/gorhill/uMatrix/issues/872
- // `changeInfo.url` may not always be available (Firefox).
- vAPI.tabs.onUpdated = function (tabId, changeInfo, tab) {
- if (vAPI.isBehindTheSceneTabId(tabId)) {
- return;
- }
- if (typeof tab.url !== 'string' || tab.url === '') {
- return;
- }
- let url = changeInfo.url || tab.url;
- if (url) {
- push(tabId, url, 'updateURL');
- }
- };
- vAPI.tabs.onClosed = function (tabId) {
- ηm.unbindTabFromPageStats(tabId);
- let entry = tabContexts[tabId];
- if (entry instanceof TabContext) {
- entry.destroy();
- }
- };
- return {
- push: push,
- lookup: lookup,
- mustLookup: mustLookup
- };
- })();
- vAPI.tabs.registerListeners();
- // Create an entry for the tab if it doesn't exist
- ηm.bindTabToPageStats = function (tabId, context) {
- this.updateBadgeAsync(tabId);
- // Do not create a page store for URLs which are of no
- // interests Example: dev console
- let tabContext = this.tabContextManager.lookup(tabId);
- if (tabContext === null) {
- throw new Error('Unmanaged tab id: ' + tabId);
- }
- // rhill 2013-11-24: Never ever rebind behind-the-scene
- // virtual tab.
- // https://github.com/gorhill/httpswitchboard/issues/67
- if (vAPI.isBehindTheSceneTabId(tabId)) {
- return this.pageStores[tabId];
- }
- let normalURL = tabContext.normalURL;
- let pageStore = this.pageStores[tabId] || null;
- // The previous page URL, if any, associated with the tab
- if (pageStore !== null) {
- // No change, do not rebind
- if (pageStore.pageUrl === normalURL) {
- return pageStore;
- }
- // https://github.com/gorhill/uMatrix/issues/37
- // Just rebind whenever possible: the URL changed, but the
- // document maybe is the same.
- // Example: Google Maps, Github
- // https://github.com/gorhill/uMatrix/issues/72
- // Need to double-check that the new scope is same as old scope
- if (context === 'updateURL'
- && pageStore.pageHostname === tabContext.rootHostname) {
- pageStore.rawURL = tabContext.rawURL;
- pageStore.normalURL = normalURL;
- this.updateTitle(tabId);
- this.pageStoresToken = Date.now();
- return pageStore;
- }
- // We won't be reusing this page store.
- this.unbindTabFromPageStats(tabId);
- }
- // Try to resurrect first.
- pageStore = this.resurrectPageStore(tabId, normalURL);
- if (pageStore === null) {
- pageStore = this.pageStoreFactory(tabContext);
- }
- this.pageStores[tabId] = pageStore;
- this.updateTitle(tabId);
- this.pageStoresToken = Date.now();
- return pageStore;
- };
- ηm.unbindTabFromPageStats = function (tabId) {
- if (vAPI.isBehindTheSceneTabId(tabId)) {
- return;
- }
- let pageStore = this.pageStores[tabId] || null;
- if (pageStore === null) {
- return;
- }
- delete this.pageStores[tabId];
- this.pageStoresToken = Date.now();
- if (pageStore.incinerationTimer) {
- clearTimeout(pageStore.incinerationTimer);
- pageStore.incinerationTimer = null;
- }
- if (this.pageStoreCemetery.hasOwnProperty(tabId) === false) {
- this.pageStoreCemetery[tabId] = {};
- }
- let pageStoreCrypt = this.pageStoreCemetery[tabId];
- let pageURL = pageStore.pageUrl;
- pageStoreCrypt[pageURL] = pageStore;
- pageStore.incinerationTimer =
- vAPI.setTimeout(this.incineratePageStore.bind(this, tabId, pageURL),
- 4 * 60 * 1000);
- };
- ηm.resurrectPageStore = function (tabId, pageURL) {
- if (this.pageStoreCemetery.hasOwnProperty(tabId) === false) {
- return null;
- }
- let pageStoreCrypt = this.pageStoreCemetery[tabId];
- if (pageStoreCrypt.hasOwnProperty(pageURL) === false) {
- return null;
- }
- let pageStore = pageStoreCrypt[pageURL];
- if (pageStore.incinerationTimer !== null) {
- clearTimeout(pageStore.incinerationTimer);
- pageStore.incinerationTimer = null;
- }
- delete pageStoreCrypt[pageURL];
- if (Object.keys(pageStoreCrypt).length === 0) {
- delete this.pageStoreCemetery[tabId];
- }
- return pageStore;
- };
- ηm.incineratePageStore = function (tabId, pageURL) {
- if (this.pageStoreCemetery.hasOwnProperty(tabId) === false) {
- return;
- }
- let pageStoreCrypt = this.pageStoreCemetery[tabId];
- if (pageStoreCrypt.hasOwnProperty(pageURL) === false) {
- return;
- }
- let pageStore = pageStoreCrypt[pageURL];
- if (pageStore.incinerationTimer !== null) {
- clearTimeout(pageStore.incinerationTimer);
- pageStore.incinerationTimer = null;
- }
- delete pageStoreCrypt[pageURL];
- if (Object.keys(pageStoreCrypt).length === 0) {
- delete this.pageStoreCemetery[tabId];
- }
- pageStore.dispose();
- };
- ηm.pageStoreFromTabId = function (tabId) {
- return this.pageStores[tabId] || null;
- };
- // Never return null
- ηm.mustPageStoreFromTabId = function (tabId) {
- return this.pageStores[tabId] || this.pageStores[vAPI.noTabId];
- };
- ηm.forceReload = function (tabId, bypassCache) {
- vAPI.tabs.reload(tabId, bypassCache);
- };
- // Update badge
- // rhill 2013-11-09: well this sucks, I can't update icon/badge
- // incrementally, as chromium overwrite the icon at some point
- // without notifying me, and this causes internal cached state to
- // be out of sync.
- // ηMatrix: does it matter to us?
- ηm.updateBadgeAsync = (function () {
- let tabIdToTimer = Object.create(null);
- let updateBadge = function (tabId) {
- delete tabIdToTimer[tabId];
- let iconId = null;
- let badgeStr = '';
- let pageStore = this.pageStoreFromTabId(tabId);
- if (pageStore !== null) {
- let total = pageStore.perLoadAllowedRequestCount +
- pageStore.perLoadBlockedRequestCount;
- if (total) {
- let squareSize = 19;
- let greenSize = squareSize *
- Math.sqrt(pageStore.perLoadAllowedRequestCount / total);
- iconId = greenSize < squareSize/2 ?
- Math.ceil(greenSize) :
- Math.floor(greenSize);
- }
- if (this.userSettings.iconBadgeEnabled
- && pageStore.perLoadBlockedRequestCount !== 0) {
- badgeStr =
- this.formatCount(pageStore.perLoadBlockedRequestCount);
- }
- }
- vAPI.setIcon(tabId, iconId, badgeStr);
- };
- return function (tabId) {
- if (tabIdToTimer[tabId]) {
- return;
- }
- if (vAPI.isBehindTheSceneTabId(tabId)) {
- return;
- }
- tabIdToTimer[tabId] =
- vAPI.setTimeout(updateBadge.bind(this, tabId), 750);
- };
- })();
- ηm.updateTitle = (function () {
- let tabIdToTimer = Object.create(null);
- let tabIdToTryCount = Object.create(null);
- let delay = 499;
- let tryNoMore = function (tabId) {
- delete tabIdToTryCount[tabId];
- };
- let tryAgain = function (tabId) {
- let count = tabIdToTryCount[tabId];
- if (count === undefined) {
- return false;
- }
- if (count === 1) {
- delete tabIdToTryCount[tabId];
- return false;
- }
- tabIdToTryCount[tabId] = count - 1;
- tabIdToTimer[tabId] =
- vAPI.setTimeout(updateTitle.bind(ηm, tabId), delay);
- return true;
- };
- let onTabReady = function (tabId, tab) {
- if (!tab) {
- return tryNoMore(tabId);
- }
- let pageStore = this.pageStoreFromTabId(tabId);
- if (pageStore === null) {
- return tryNoMore(tabId);
- }
- if (!tab.title && tryAgain(tabId)) {
- return;
- }
- // https://github.com/gorhill/uMatrix/issues/225
- // Sometimes title changes while page is loading.
- let settled = tab.title && tab.title === pageStore.title;
- pageStore.title = tab.title || tab.url || '';
- this.pageStoresToken = Date.now();
- if (settled || !tryAgain(tabId)) {
- tryNoMore(tabId);
- }
- };
- let updateTitle = function (tabId) {
- delete tabIdToTimer[tabId];
- vAPI.tabs.get(tabId, onTabReady.bind(this, tabId));
- };
- return function (tabId) {
- if (vAPI.isBehindTheSceneTabId(tabId)) {
- return;
- }
- if (tabIdToTimer[tabId]) {
- clearTimeout(tabIdToTimer[tabId]);
- }
- tabIdToTimer[tabId] =
- vAPI.setTimeout(updateTitle.bind(this, tabId), delay);
- tabIdToTryCount[tabId] = 5;
- };
- })();
- // Stale page store entries janitor
- // https://github.com/chrisaljoudi/uBlock/issues/455
- (function () {
- let cleanupPeriod = 7 * 60 * 1000;
- let cleanupSampleAt = 0;
- let cleanupSampleSize = 11;
- let cleanup = function () {
- let tabIds = Object.keys(ηm.pageStores).sort();
- let checkTab = function(tabId) {
- vAPI.tabs.get(tabId, function (tab) {
- if (!tab) {
- ηm.unbindTabFromPageStats(tabId);
- }
- });
- };
- if (cleanupSampleAt >= tabIds.length) {
- cleanupSampleAt = 0;
- }
- let tabId;
- let n =
- Math.min(cleanupSampleAt + cleanupSampleSize, tabIds.length);
- for (let i=cleanupSampleAt; i<n; i++) {
- tabId = tabIds[i];
- if (vAPI.isBehindTheSceneTabId(tabId)) {
- continue;
- }
- checkTab(tabId);
- }
- cleanupSampleAt = n;
- vAPI.setTimeout(cleanup, cleanupPeriod);
- };
- vAPI.setTimeout(cleanup, cleanupPeriod);
- })();
- })();
|