You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

459 lines
15 KiB

/*******************************************************************************
µ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
);
})();