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.
 
 
 
 
 
 

897 lines
24 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 µMatrix, vAPI */
/* jshint boss: true */
/******************************************************************************/
/******************************************************************************/
// Default handler
(function() {
'use strict';
var µm = µMatrix;
/******************************************************************************/
// Default is for commonly used message.
function onMessage(request, sender, callback) {
// Async
switch ( request.what ) {
case 'getAssetContent':
return µm.assets.getLocal(request.url, callback);
default:
break;
}
// Sync
var response;
switch ( request.what ) {
case 'forceReloadTab':
µm.forceReload(request.tabId);
break;
case 'getUserSettings':
response = µm.userSettings;
break;
case 'gotoExtensionURL':
µm.utils.gotoExtensionURL(request.url);
break;
case 'gotoURL':
µm.utils.gotoURL(request);
break;
case 'reloadHostsFiles':
µm.reloadHostsFiles(request.switches, request.update);
break;
case 'userSettings':
response = µm.changeUserSettings(request.name, request.value);
break;
default:
return vAPI.messaging.UNHANDLED;
}
callback(response);
}
/******************************************************************************/
vAPI.messaging.setup(onMessage);
/******************************************************************************/
})();
/******************************************************************************/
/******************************************************************************/
(function() {
// popup.js
var µm = µMatrix;
/******************************************************************************/
var smartReload = function(tabs) {
var i = tabs.length;
while ( i-- ) {
µm.smartReloadTabs(µm.userSettings.smartAutoReload, tabs[i].id);
}
};
/******************************************************************************/
// Constructor is faster than object literal
var RowSnapshot = function(srcHostname, desHostname, desDomain) {
this.domain = desDomain;
this.temporary = µm.tMatrix.evaluateRowZXY(srcHostname, desHostname);
this.permanent = µm.pMatrix.evaluateRowZXY(srcHostname, desHostname);
this.counts = RowSnapshot.counts.slice();
this.totals = RowSnapshot.counts.slice();
};
RowSnapshot.counts = (function() {
var i = Object.keys(µm.Matrix.getColumnHeaders()).length;
var aa = new Array(i);
while ( i-- ) {
aa[i] = 0;
}
return aa;
})();
/******************************************************************************/
var matrixSnapshot = function(tabId, details) {
var µmuser = µm.userSettings;
var r = {
tabId: tabId,
url: '',
hostname: '',
domain: '',
blockedCount: 0,
scope: '*',
headers: µm.Matrix.getColumnHeaders(),
tSwitches: {},
pSwitches: {},
rows: {},
rowCount: 0,
diff: [],
userSettings: {
colorBlindFriendly: µmuser.colorBlindFriendly,
displayTextSize: µmuser.displayTextSize,
popupCollapseDomains: µmuser.popupCollapseDomains,
popupCollapseSpecificDomains: µmuser.popupCollapseSpecificDomains,
popupHideBlacklisted: µmuser.popupHideBlacklisted,
popupScopeLevel: µmuser.popupScopeLevel
}
};
// Allow examination of behind-the-scene requests
// TODO: Not portable
if ( details.tabURL ) {
if ( details.tabURL.lastIndexOf(vAPI.getURL(''), 0) === 0 ) {
tabId = µm.behindTheSceneTabId;
} else if ( details.tabURL === µm.behindTheSceneURL ) {
tabId = µm.behindTheSceneTabId;
}
}
var pageStore = µm.pageStatsFromTabId(tabId);
if ( !pageStore ) {
return r;
}
var headers = r.headers;
r.url = pageStore.pageUrl;
r.hostname = pageStore.pageHostname;
r.domain = pageStore.pageDomain;
r.blockedCount = pageStore.requestStats.blocked.all;
if ( µmuser.popupScopeLevel === 'site' ) {
r.scope = r.hostname;
} else if ( µmuser.popupScopeLevel === 'domain' ) {
r.scope = r.domain;
}
var switchNames = µm.Matrix.getSwitchNames();
for ( var switchName in switchNames ) {
if ( switchNames.hasOwnProperty(switchName) === false ) {
continue;
}
r.tSwitches[switchName] = µm.tMatrix.evaluateSwitchZ(switchName, r.scope);
r.pSwitches[switchName] = µm.pMatrix.evaluateSwitchZ(switchName, r.scope);
}
// These rows always exist
r.rows['*'] = new RowSnapshot(r.scope, '*', '*');
r.rows['1st-party'] = new RowSnapshot(r.scope, '1st-party', '1st-party');
r.rowCount += 1;
var µmuri = µm.URI;
var reqKey, reqType, reqHostname, reqDomain;
var desHostname;
var row, typeIndex;
var anyIndex = headers['*'];
var pageRequests = pageStore.requests;
var reqKeys = pageRequests.getRequestKeys();
var iReqKey = reqKeys.length;
var pos;
while ( iReqKey-- ) {
reqKey = reqKeys[iReqKey];
reqType = pageRequests.typeFromRequestKey(reqKey);
reqHostname = pageRequests.hostnameFromRequestKey(reqKey);
// rhill 2013-10-23: hostname can be empty if the request is a data url
// https://github.com/gorhill/httpswitchboard/issues/26
if ( reqHostname === '' ) {
reqHostname = pageStore.pageHostname;
}
reqDomain = µmuri.domainFromHostname(reqHostname) || reqHostname;
// We want rows of self and ancestors
desHostname = reqHostname;
for ( ;; ) {
// If row exists, ancestors exist
if ( r.rows.hasOwnProperty(desHostname) !== false ) {
break;
}
r.rows[desHostname] = new RowSnapshot(r.scope, desHostname, reqDomain);
r.rowCount += 1;
if ( desHostname === reqDomain ) {
break;
}
pos = desHostname.indexOf('.');
if ( pos === -1 ) {
break;
}
desHostname = desHostname.slice(pos + 1);
}
typeIndex = headers[reqType];
row = r.rows[reqHostname];
row.counts[typeIndex] += 1;
row.counts[anyIndex] += 1;
row = r.rows[reqDomain];
row.totals[typeIndex] += 1;
row.totals[anyIndex] += 1;
row = r.rows['*'];
row.totals[typeIndex] += 1;
row.totals[anyIndex] += 1;
}
r.diff = µm.tMatrix.diff(µm.pMatrix, r.hostname, Object.keys(r.rows));
return r;
};
/******************************************************************************/
var matrixSnapshotFromTabId = function(details, callback) {
if ( details.targetTabId ) {
callback(matrixSnapshot(details.targetTabId, details));
return;
}
vAPI.tabs.get(null, function(tab) {
callback(matrixSnapshot(tab.id, details));
});
};
/******************************************************************************/
var onMessage = function(request, sender, callback) {
// Async
switch ( request.what ) {
case 'matrixSnapshot':
matrixSnapshotFromTabId(request, callback);
return;
default:
break;
}
// Sync
var response;
switch ( request.what ) {
case 'disconnected':
// https://github.com/gorhill/httpswitchboard/issues/94
if ( µm.userSettings.smartAutoReload ) {
vAPI.tabs.get(null, smartReload);
}
break;
case 'toggleMatrixSwitch':
µm.tMatrix.setSwitchZ(
request.switchName,
request.srcHostname,
µm.tMatrix.evaluateSwitchZ(request.switchName, request.srcHostname) === false
);
break;
case 'blacklistMatrixCell':
µm.tMatrix.blacklistCell(
request.srcHostname,
request.desHostname,
request.type
);
break;
case 'whitelistMatrixCell':
µm.tMatrix.whitelistCell(
request.srcHostname,
request.desHostname,
request.type
);
break;
case 'graylistMatrixCell':
µm.tMatrix.graylistCell(
request.srcHostname,
request.desHostname,
request.type
);
break;
case 'applyDiffToPermanentMatrix': // aka "persist"
if ( µm.pMatrix.applyDiff(request.diff, µm.tMatrix) ) {
µm.saveMatrix();
}
break;
case 'applyDiffToTemporaryMatrix': // aka "revert"
µm.tMatrix.applyDiff(request.diff, µm.pMatrix);
break;
case 'revertTemporaryMatrix':
µm.tMatrix.assign(µm.pMatrix);
break;
default:
return vAPI.messaging.UNHANDLED;
}
callback(response);
};
vAPI.messaging.listen('popup.js', onMessage);
})();
/******************************************************************************/
/******************************************************************************/
// content scripts
(function() {
var µm = µMatrix;
/******************************************************************************/
var contentScriptSummaryHandler = function(tabId, details) {
// TODO: Investigate "Error in response to tabs.executeScript: TypeError:
// Cannot read property 'locationURL' of null" (2013-11-12). When can this
// happens?
if ( !details || !details.locationURL ) {
return;
}
var pageURL = µm.pageUrlFromTabId(tabId);
var pageStats = µm.pageStatsFromPageUrl(pageURL);
var µmuri = µm.URI.set(details.locationURL);
var frameURL = µmuri.normalizedURI();
var frameHostname = µmuri.hostname;
var urls, url, r;
// https://github.com/gorhill/httpswitchboard/issues/333
// Look-up here whether inline scripting is blocked for the frame.
var inlineScriptBlocked = µm.mustBlock(µm.scopeFromURL(pageURL), frameHostname, 'script');
// scripts
// https://github.com/gorhill/httpswitchboard/issues/25
if ( pageStats && inlineScriptBlocked ) {
urls = details.scriptSources;
for ( url in urls ) {
if ( !urls.hasOwnProperty(url) ) {
continue;
}
if ( url === '{inline_script}' ) {
url = frameURL + '{inline_script}';
}
r = µm.filterRequest(pageURL, 'script', url);
pageStats.recordRequest('script', url, r !== false, r);
}
}
// TODO: as of 2014-05-26, not sure this is needed anymore, since µMatrix
// no longer uses chrome.contentSettings API (I think that was the reason
// this code was put in).
// plugins
// https://github.com/gorhill/httpswitchboard/issues/25
if ( pageStats ) {
urls = details.pluginSources;
for ( url in urls ) {
if ( !urls.hasOwnProperty(url) ) {
continue;
}
r = µm.filterRequest(pageURL, 'plugin', url);
pageStats.recordRequest('plugin', url, r !== false, r);
}
}
// https://github.com/gorhill/httpswitchboard/issues/181
µm.onPageLoadCompleted(pageURL);
};
/******************************************************************************/
var contentScriptLocalStorageHandler = function(pageURL) {
var µmuri = µm.URI.set(pageURL);
var response = µm.mustBlock(µm.scopeFromURL(pageURL), µmuri.hostname, 'cookie');
µm.recordFromPageUrl(
pageURL,
'cookie',
µmuri.rootURL() + '/{localStorage}',
response
);
response = response && µm.userSettings.deleteLocalStorage;
if ( response ) {
µm.localStorageRemovedCounter++;
}
return response;
};
/******************************************************************************/
var onMessage = function(request, sender, callback) {
// Async
switch ( request.what ) {
default:
break;
}
var tabId = sender.tab.id;
// Sync
var response;
switch ( request.what ) {
case 'contentScriptHasLocalStorage':
response = contentScriptLocalStorageHandler(request.url);
µm.updateBadgeAsync(tabId);
break;
case 'contentScriptSummary':
contentScriptSummaryHandler(tabId, request);
µm.updateBadgeAsync(tabId);
break;
case 'checkScriptBlacklisted':
response = {
scriptBlacklisted: µm.mustBlock(
µm.scopeFromURL(request.url),
µm.hostnameFromURL(request.url),
'script'
)
};
break;
case 'getUserAgentReplaceStr':
response = µm.tMatrix.evaluateSwitchZ('ua-spoof', request.hostname) ?
µm.userAgentReplaceStr :
undefined;
break;
default:
return vAPI.messaging.UNHANDLED;
}
callback(response);
};
vAPI.messaging.listen('contentscript-start.js', onMessage);
vAPI.messaging.listen('contentscript-end.js', onMessage);
/******************************************************************************/
})();
/******************************************************************************/
/******************************************************************************/
// settings.js
(function() {
var onMessage = function(request, sender, callback) {
var µm = µMatrix;
// Async
switch ( request.what ) {
default:
break;
}
// Sync
var response;
switch ( request.what ) {
default:
return vAPI.messaging.UNHANDLED;
}
callback(response);
};
vAPI.messaging.listen('settings.js', onMessage);
})();
/******************************************************************************/
/******************************************************************************/
// privacy.js
(function() {
var onMessage = function(request, sender, callback) {
var µm = µMatrix;
// Async
switch ( request.what ) {
default:
break;
}
// Sync
var response;
switch ( request.what ) {
case 'getPrivacySettings':
response = {
userSettings: µm.userSettings,
matrixSwitches: {
'https-strict': µm.pMatrix.evaluateSwitch('https-strict', '*') === 1,
'ua-spoof': µm.pMatrix.evaluateSwitch('ua-spoof', '*') === 1,
'referrer-spoof': µm.pMatrix.evaluateSwitch('referrer-spoof', '*') === 1
}
};
break;
case 'setMatrixSwitch':
µm.tMatrix.setSwitch(request.switchName, '*', request.state);
if ( µm.pMatrix.setSwitch(request.switchName, '*', request.state) ) {
µm.saveMatrix();
}
break;
default:
return vAPI.messaging.UNHANDLED;
}
callback(response);
};
vAPI.messaging.listen('privacy.js', onMessage);
})();
/******************************************************************************/
/******************************************************************************/
// user-rules.js
(function() {
var µm = µMatrix;
/******************************************************************************/
var onMessage = function(request, sender, callback) {
// Async
switch ( request.what ) {
default:
break;
}
// Sync
var response;
switch ( request.what ) {
case 'getUserRules':
response = {
temporaryRules: µm.tMatrix.toString(),
permanentRules: µm.pMatrix.toString()
};
break;
case 'setUserRules':
if ( typeof request.temporaryRules === 'string' ) {
µm.tMatrix.fromString(request.temporaryRules);
}
if ( typeof request.permanentRules === 'string' ) {
µm.pMatrix.fromString(request.permanentRules);
µm.saveMatrix();
}
response = {
temporaryRules: µm.tMatrix.toString(),
permanentRules: µm.pMatrix.toString()
};
break;
default:
return vAPI.messaging.UNHANDLED;
}
callback(response);
};
vAPI.messaging.listen('user-rules.js', onMessage);
})();
/******************************************************************************/
/******************************************************************************/
// hosts-files.js
(function() {
var µm = µMatrix;
/******************************************************************************/
var getLists = function(callback) {
var r = {
available: null,
cache: null,
current: µm.liveHostsFiles,
blockedHostnameCount: µm.ubiquitousBlacklist.count,
autoUpdate: µm.userSettings.autoUpdate
};
var onMetadataReady = function(entries) {
r.cache = entries;
callback(r);
};
var onAvailableHostsFilesReady = function(lists) {
r.available = lists;
µm.assets.metadata(onMetadataReady);
};
µm.getAvailableHostsFiles(onAvailableHostsFilesReady);
};
/******************************************************************************/
var onMessage = function(request, sender, callback) {
var µm = µMatrix;
// Async
switch ( request.what ) {
case 'getLists':
return getLists(callback);
case 'purgeAllCaches':
return µm.assets.purgeAll(callback);
default:
break;
}
// Sync
var response;
switch ( request.what ) {
case 'purgeCache':
µm.assets.purge(request.path);
break;
default:
return vAPI.messaging.UNHANDLED;
}
callback(response);
};
vAPI.messaging.listen('hosts-files.js', onMessage);
})();
/******************************************************************************/
/******************************************************************************/
// info.js
(function() {
/******************************************************************************/
// map(pageURL) => array of request log entries
var getRequestLog = function(pageURL) {
var requestLogs = {};
var pageStores = µMatrix.pageStats;
var pageURLs = pageURL ? [pageURL] : Object.keys(pageStores);
var pageStore, pageRequestLog, logEntries, j, logEntry;
for ( var i = 0; i < pageURLs.length; i++ ) {
pageURL = pageURLs[i];
pageStore = pageStores[pageURL];
if ( !pageStore ) {
continue;
}
pageRequestLog = [];
logEntries = pageStore.requests.getLoggedRequests();
j = logEntries.length;
while ( j-- ) {
// rhill 2013-12-04: `logEntry` can be null since a ring buffer is
// now used, and it might not have been filled yet.
if ( logEntry = logEntries[j] ) {
pageRequestLog.push(logEntry);
}
}
requestLogs[pageURL] = pageRequestLog;
}
return requestLogs;
};
/******************************************************************************/
var clearRequestLog = function(pageURL) {
var pageStores = µMatrix.pageStats;
var pageURLs = pageURL ? [pageURL] : Object.keys(pageStores);
var pageStore;
for ( var i = 0; i < pageURLs.length; i++ ) {
if ( pageStore = pageStores[pageURLs[i]] ) {
pageStore.requests.clearLogBuffer();
}
}
};
/******************************************************************************/
var onMessage = function(request, sender, callback) {
var µm = µMatrix;
// Async
switch ( request.what ) {
default:
break;
}
// Sync
var response;
switch ( request.what ) {
case 'getPageURLs':
response = {
pageURLs: Object.keys(µm.pageUrlToTabId),
behindTheSceneURL: µm.behindTheSceneURL
};
break;
case 'getStats':
var pageStore = µm.pageStats[request.pageURL];
response = {
globalNetStats: µm.requestStats,
pageNetStats: pageStore ? pageStore.requestStats : null,
cookieHeaderFoiledCounter: µm.cookieHeaderFoiledCounter,
refererHeaderFoiledCounter: µm.refererHeaderFoiledCounter,
hyperlinkAuditingFoiledCounter: µm.hyperlinkAuditingFoiledCounter,
cookieRemovedCounter: µm.cookieRemovedCounter,
localStorageRemovedCounter: µm.localStorageRemovedCounter,
browserCacheClearedCounter: µm.browserCacheClearedCounter
};
break;
case 'getRequestLogs':
response = getRequestLog(request.pageURL);
break;
case 'clearRequestLogs':
clearRequestLog(request.pageURL);
break;
default:
return vAPI.messaging.UNHANDLED;
}
callback(response);
};
vAPI.messaging.listen('info.js', onMessage);
})();
/******************************************************************************/
/******************************************************************************/
// about.js
(function() {
var µm = µMatrix;
/******************************************************************************/
var restoreUserData = function(userData) {
var countdown = 3;
var onCountdown = function() {
countdown -= 1;
if ( countdown === 0 ) {
vAPI.app.restart();
}
};
var onAllRemoved = function() {
// Be sure to adjust `countdown` if adding/removing anything below
µm.XAL.keyvalSetMany(userData.settings, onCountdown);
µm.XAL.keyvalSetOne('userMatrix', userData.rules, onCountdown);
µm.XAL.keyvalSetOne('liveHostsFiles', userData.hostsFiles, onCountdown);
};
// If we are going to restore all, might as well wipe out clean local
// storage
µm.XAL.keyvalRemoveAll(onAllRemoved);
};
/******************************************************************************/
var resetUserData = function() {
var onAllRemoved = function() {
vAPI.app.restart();
};
µm.XAL.keyvalRemoveAll(onAllRemoved);
};
/******************************************************************************/
var onMessage = function(request, sender, callback) {
// Async
switch ( request.what ) {
default:
break;
}
// Sync
var response;
switch ( request.what ) {
case 'getAllUserData':
response = {
app: 'µMatrix',
version: vAPI.app.version,
when: Date.now(),
settings: µm.userSettings,
rules: µm.pMatrix.toString(),
hostsFiles: µm.liveHostsFiles
};
break;
case 'getSomeStats':
response = {
version: vAPI.app.version,
storageUsed: µm.storageUsed
};
break;
case 'restoreAllUserData':
restoreUserData(request.userData);
break;
case 'resetAllUserData':
resetUserData();
break;
default:
return vAPI.messaging.UNHANDLED;
}
callback(response);
};
vAPI.messaging.listen('about.js', onMessage);
/******************************************************************************/
/******************************************************************************/
})();
/******************************************************************************/