/******************************************************************************* µMatrix - a Chromium browser extension to black/white list requests. Copyright (C) 2014 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/uMatrix */ /* global chrome, µMatrix */ /******************************************************************************/ // Create a new page url stats store (if not already present) µMatrix.createPageStore = function(pageURL) { // https://github.com/gorhill/httpswitchboard/issues/303 // At this point, the URL has been page-URL-normalized // do not create stats store for urls which are of no interest if ( pageURL.search(/^https?/) !== 0 ) { return; } var pageStore = null; if ( this.pageStats.hasOwnProperty(pageURL) ) { pageStore = this.pageStats[pageURL]; } if ( pageStore === null ) { pageStore = this.PageStore.factory(pageURL); // These counters are used so that icon presents an overview of how // much allowed/blocked. pageStore.perLoadAllowedRequestCount = pageStore.perLoadBlockedRequestCount = 0; this.pageStats[pageURL] = pageStore; } // TODO: revisit code, need to account for those web pages for which the // URL changes with the content only updated if ( pageStore.pageUrl !== pageURL ) { pageStore.init(pageURL); } return pageStore; }; /******************************************************************************/ // 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 pages 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. µMatrix.normalizePageURL = function(pageURL) { var uri = this.URI.set(pageURL); if ( uri.scheme === 'https' || uri.scheme === 'http' ) { return uri.normalizedURI(); } // If it is a scheme-based page URL, it is important it is crafted as a // normalized URL just like above. if ( uri.scheme !== '' ) { return 'http://' + uri.scheme + '-scheme/'; } return ''; }; /******************************************************************************/ // Create an entry for the tab if it doesn't exist µMatrix.bindTabToPageStats = function(tabId, pageURL, context) { // https://github.com/gorhill/httpswitchboard/issues/303 // Don't rebind pages blocked by µMatrix. var blockedRootFramePrefix = this.webRequest.blockedRootFramePrefix; if ( pageURL.slice(0, blockedRootFramePrefix.length) === blockedRootFramePrefix ) { return null; } // https://github.com/gorhill/httpswitchboard/issues/303 // Normalize to a page-URL. pageURL = this.normalizePageURL(pageURL); // The page URL, if any, currently associated with the tab var previousPageURL = this.tabIdToPageUrl[tabId]; if ( previousPageURL === pageURL ) { return this.pageStats[pageURL]; } // https://github.com/gorhill/uMatrix/issues/37 // Just rebind: the URL changed, but the document itself is the same. // Example: Google Maps, Github var pageStore; if ( context === 'pageUpdated' && this.pageStats.hasOwnProperty(previousPageURL) ) { pageStore = this.pageStats[previousPageURL]; pageStore.pageUrl = pageURL; delete this.pageStats[previousPageURL]; this.pageStats[pageURL] = pageStore; delete this.pageUrlToTabId[previousPageURL]; this.pageUrlToTabId[pageURL] = tabId; this.tabIdToPageUrl[tabId] = pageURL; return pageStore; } pageStore = this.createPageStore(pageURL, context); // console.debug('tab.js > bindTabToPageStats(): dispatching traffic in tab id %d to page store "%s"', tabId, pageUrl); // rhill 2013-11-24: Never ever rebind chromium-behind-the-scene // virtual tab. // https://github.com/gorhill/httpswitchboard/issues/67 if ( tabId === this.behindTheSceneTabId ) { return pageStore; } // https://github.com/gorhill/uMatrix/issues/37 this.updateBadgeAsync(pageURL); this.unbindTabFromPageStats(tabId); // rhill 2014-02-08: Do not create an entry if no page store // exists (like when visiting about:blank) // https://github.com/gorhill/httpswitchboard/issues/186 if ( !pageStore ) { return null; } this.pageUrlToTabId[pageURL] = tabId; this.tabIdToPageUrl[tabId] = pageURL; pageStore.boundCount += 1; return pageStore; }; /******************************************************************************/ µMatrix.unbindTabFromPageStats = function(tabId) { if ( this.tabIdToPageUrl.hasOwnProperty(tabId) === false ) { return; } var pageURL = this.tabIdToPageUrl[tabId]; if ( this.pageStats.hasOwnProperty(pageURL) ) { var pageStore = this.pageStats[pageURL]; pageStore.boundCount -= 1; if ( pageStore.boundCount === 0 ) { pageStore.obsoleteAfter = Date.now() + (5 * 60 * 1000); } } delete this.tabIdToPageUrl[tabId]; delete this.pageUrlToTabId[pageURL]; }; /******************************************************************************/ // Log a request µMatrix.recordFromTabId = function(tabId, type, url, blocked) { var pageStats = this.pageStatsFromTabId(tabId); if ( pageStats ) { pageStats.recordRequest(type, url, blocked); } }; µMatrix.recordFromPageUrl = function(pageUrl, type, url, blocked, reason) { var pageStats = this.pageStatsFromPageUrl(pageUrl); if ( pageStats ) { pageStats.recordRequest(type, url, blocked, reason); } }; /******************************************************************************/ µMatrix.onPageLoadCompleted = function(pageURL) { var pageStats = this.pageStatsFromPageUrl(pageURL); if ( !pageStats ) { return; } // https://github.com/gorhill/httpswitchboard/issues/181 if ( pageStats.thirdpartyScript ) { pageStats.recordRequest('script', pageURL + '{3rd-party_scripts}', pageStats.pageScriptBlocked); } }; /******************************************************************************/ // Reload content of a tabs. µMatrix.smartReloadTabs = function(which, tabId) { if ( which === 'none' ) { return; } if ( which === 'current' && typeof tabId === 'number' ) { this.smartReloadTab(tabId); return; } // which === 'all' var reloadTabs = function(chromeTabs) { var µm = µMatrix; var tabId; var i = chromeTabs.length; while ( i-- ) { tabId = chromeTabs[i].id; if ( µm.tabExists(tabId) ) { µm.smartReloadTab(tabId); } } }; var getTabs = function() { chrome.tabs.query({ status: 'complete' }, reloadTabs); }; this.asyncJobs.add('smartReloadTabs', null, getTabs, 500); }; /******************************************************************************/ // Reload content of a tab µMatrix.smartReloadTab = function(tabId) { var pageStats = this.pageStatsFromTabId(tabId); if ( !pageStats ) { //console.error('HTTP Switchboard> µMatrix.smartReloadTab(): page stats for tab id %d not found', tabId); return; } // rhill 2013-12-23: Reload only if something previously blocked is now // unblocked. var blockRule; var oldState = pageStats.state; var newState = this.computeTabState(tabId); var mustReload = false; for ( blockRule in oldState ) { if ( !oldState.hasOwnProperty(blockRule) ) { continue; } // General rule, reload... // If something previously blocked is no longer blocked. if ( !newState[blockRule] ) { // console.debug('tab.js > µMatrix.smartReloadTab(): will reload because "%s" is no longer blocked', blockRule); mustReload = true; break; } } // Exceptions: blocking these previously unblocked types must result in a // reload: // - a script // - a frame // Related issues: // https://github.com/gorhill/httpswitchboard/issues/94 // https://github.com/gorhill/httpswitchboard/issues/141 if ( !mustReload ) { var reloadNewlyBlockedTypes = { 'doc': true, 'script' : true, 'frame': true }; var blockRuleType; for ( blockRule in newState ) { if ( !newState.hasOwnProperty(blockRule) ) { continue; } blockRuleType = blockRule.slice(0, blockRule.indexOf('|')); if ( !reloadNewlyBlockedTypes[blockRuleType] ) { continue; } if ( !oldState[blockRule] ) { // console.debug('tab.js > µMatrix.smartReloadTab(): will reload because "%s" is now blocked', blockRule); mustReload = true; break; } } } // console.log('old state: %o\nnew state: %o', oldState, newState); if ( mustReload ) { chrome.tabs.reload(tabId); } // pageStats.state = newState; }; /******************************************************************************/ // Required since not all tabs are of interests to HTTP Switchboard. // Examples: // `chrome://extensions/` // `chrome-devtools://devtools/devtools.html` // etc. µMatrix.tabExists = function(tabId) { return !!this.pageUrlFromTabId(tabId); }; /******************************************************************************/ µMatrix.computeTabState = function(tabId) { var pageStats = this.pageStatsFromTabId(tabId); if ( !pageStats ) { //console.error('tab.js > µMatrix.computeTabState(): page stats for tab id %d not found', tabId); return {}; } // Go through all recorded requests, apply filters to create state // It is a critical error for a tab to not be defined here var pageURL = pageStats.pageUrl; var srcHostname = this.scopeFromURL(pageURL); var requestDict = pageStats.requests.getRequestDict(); var computedState = {}; var desHostname, type; for ( var reqKey in requestDict ) { if ( !requestDict.hasOwnProperty(reqKey) ) { continue; } // The evaluation code here needs to reflect the evaluation code in // beforeRequestHandler() desHostname = this.PageRequestStats.hostnameFromRequestKey(reqKey); // rhill 2013-12-10: mind how stylesheets are to be evaluated: // `stylesheet` or `other`? Depends of domain of request. // https://github.com/gorhill/httpswitchboard/issues/85 type = this.PageRequestStats.typeFromRequestKey(reqKey); if ( this.mustBlock(srcHostname, desHostname, type) ) { computedState[type + '|' + desHostname] = true; } } return computedState; }; /******************************************************************************/ µMatrix.tabIdFromPageUrl = function(pageURL) { // https://github.com/gorhill/httpswitchboard/issues/303 // Normalize to a page-URL. return this.pageUrlToTabId[this.normalizePageURL(pageURL)]; }; µMatrix.tabIdFromPageStats = function(pageStats) { return this.tabIdFromPageUrl(pageStats.pageUrl); }; µMatrix.pageUrlFromTabId = function(tabId) { return this.tabIdToPageUrl[tabId]; }; µMatrix.pageUrlFromPageStats = function(pageStats) { if ( pageStats ) { return pageStats.pageUrl; } return undefined; }; µMatrix.pageStatsFromTabId = function(tabId) { var pageUrl = this.tabIdToPageUrl[tabId]; if ( pageUrl ) { return this.pageStats[pageUrl]; } return undefined; }; µMatrix.pageStatsFromPageUrl = function(pageURL) { if ( pageURL ) { return this.pageStats[this.normalizePageURL(pageURL)]; } return null; }; /******************************************************************************/ µMatrix.resizeLogBuffers = function(size) { var pageStores = this.pageStats; for ( var pageURL in pageStores ) { if ( pageStores.hasOwnProperty(pageURL) ) { pageStores[pageURL].requests.resizeLogBuffer(size); } } }; /******************************************************************************/ µMatrix.forceReload = function(pageURL) { var tabId = this.tabIdFromPageUrl(pageURL); if ( tabId ) { chrome.tabs.reload(tabId, { bypassCache: true }); } }; /******************************************************************************/ // Garbage collect stale url stats entries (function() { var µm = µMatrix; var gcPageStats = function() { var pageStore; var now = Date.now(); for ( var pageURL in µm.pageStats ) { if ( µm.pageStats.hasOwnProperty(pageURL) === false ) { continue; } pageStore = µm.pageStats[pageURL]; if ( pageStore.boundCount !== 0 ) { continue; } if ( pageStore.obsoleteAfter > now ) { continue; } µm.cookieHunter.removePageCookies(pageStore); pageStore.dispose(); delete µm.pageStats[pageURL]; } // Prune content of chromium-behind-the-scene virtual tab // When `suggest-as-you-type` is on in Chromium, this can lead to a // LOT of uninteresting behind the scene requests. pageStore = µm.pageStats[µm.behindTheSceneURL]; if ( !pageStore ) { return; } var reqKeys = pageStore.requests.getRequestKeys(); if ( reqKeys.length <= µm.behindTheSceneMaxReq ) { return; } reqKeys = reqKeys.sort(function(a,b){ return pageStore.requests[b] - pageStore.requests[a]; }).slice(µm.behindTheSceneMaxReq); var iReqKey = reqKeys.length; while ( iReqKey-- ) { pageStore.requests.disposeOne(reqKeys[iReqKey]); } }; // Time somewhat arbitrary: If a web page has not been in a tab // for some time minutes, flush its stats. µMatrix.asyncJobs.add( 'gcPageStats', null, gcPageStats, (2.5 * 60 * 1000) | 0, true ); })();