1
0
Fork 0
mirror of https://github.com/gorhill/uMatrix.git synced 2024-06-02 02:14:52 +12:00
uMatrix/src/js/contentscript-end.js

537 lines
17 KiB
JavaScript
Raw Normal View History

2014-10-18 08:01:09 +13:00
/*******************************************************************************
µMatrix - a Chromium browser extension to black/white list requests.
Copyright (C) 2014-2105 Raymond Hill
2014-10-18 08:01:09 +13:00
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
*/
2015-04-12 09:15:57 +12:00
/* global vAPI */
/* jshint multistr: true, boss: true */
2014-10-18 08:01:09 +13:00
/******************************************************************************/
/******************************************************************************/
2015-04-12 09:15:57 +12:00
// Injected into content pages
2014-10-18 08:01:09 +13:00
2015-04-12 09:15:57 +12:00
(function() {
2014-10-18 08:01:09 +13:00
2015-04-12 09:15:57 +12:00
'use strict';
2014-10-18 08:01:09 +13:00
2015-04-12 09:15:57 +12:00
/******************************************************************************/
2014-10-18 08:01:09 +13:00
2015-04-12 09:15:57 +12:00
// https://github.com/chrisaljoudi/uBlock/issues/464
if ( document instanceof HTMLDocument === false ) {
//console.debug('contentscript-end.js > not a HTLMDocument');
return;
}
// This can also happen (for example if script injected into a `data:` URI doc)
if ( !window.location ) {
return;
2015-04-12 09:15:57 +12:00
}
2014-10-18 08:01:09 +13:00
2015-04-12 09:15:57 +12:00
// This can happen
if ( !vAPI ) {
//console.debug('contentscript-end.js > vAPI not found');
return;
}
2014-10-18 08:01:09 +13:00
2015-04-12 09:15:57 +12:00
// https://github.com/chrisaljoudi/uBlock/issues/456
// Already injected?
if ( vAPI.contentscriptEndInjected ) {
//console.debug('contentscript-end.js > content script already injected');
return;
}
vAPI.contentscriptEndInjected = true;
2014-10-18 08:01:09 +13:00
2015-04-12 09:15:57 +12:00
/******************************************************************************/
2014-10-18 08:01:09 +13:00
2015-04-12 09:15:57 +12:00
var localMessager = vAPI.messaging.channel('contentscript-end.js');
2014-10-18 08:01:09 +13:00
vAPI.shutdown.add(function() {
localMessager.close();
});
2014-10-18 08:01:09 +13:00
/******************************************************************************/
/******************************************************************************/
// Unrendered noscript (because CSP) workaround
// Executed once.
2014-10-18 08:01:09 +13:00
(function() {
var checkScriptBlacklistedHandler = function(response) {
if ( !response.scriptBlacklisted ) {
return;
}
var scripts = document.querySelectorAll('noscript');
var i = scripts.length;
var realNoscript, fakeNoscript;
while ( i-- ) {
realNoscript = scripts[i];
fakeNoscript = document.createElement('div');
fakeNoscript.innerHTML = '<!-- uMatrix NOSCRIPT tag replacement: see <https://github.com/gorhill/httpswitchboard/issues/177> -->\n' + realNoscript.textContent;
realNoscript.parentNode.replaceChild(fakeNoscript, realNoscript);
}
};
localMessager.send({
what: 'checkScriptBlacklisted',
url: window.location.href
}, checkScriptBlacklistedHandler);
})();
2014-10-18 08:01:09 +13:00
/******************************************************************************/
2014-10-18 08:01:09 +13:00
/******************************************************************************/
// Executed only once.
2014-10-18 08:01:09 +13:00
(function() {
var localStorageHandler = function(mustRemove) {
if ( mustRemove ) {
window.localStorage.clear();
window.sessionStorage.clear();
// console.debug('HTTP Switchboard > found and removed non-empty localStorage');
}
};
// Check with extension whether local storage must be emptied
// rhill 2014-03-28: we need an exception handler in case 3rd-party access
// to site data is disabled.
// https://github.com/gorhill/httpswitchboard/issues/215
try {
var hasLocalStorage = window.localStorage && window.localStorage.length;
var hasSessionStorage = window.sessionStorage && window.sessionStorage.length;
if ( hasLocalStorage || hasSessionStorage ) {
localMessager.send({
what: 'contentScriptHasLocalStorage',
url: window.location.href
}, localStorageHandler);
}
// TODO: indexedDB
if ( window.indexedDB && !!window.indexedDB.webkitGetDatabaseNames ) {
// var db = window.indexedDB.webkitGetDatabaseNames().onsuccess = function(sender) {
// console.debug('webkitGetDatabaseNames(): result=%o', sender.target.result);
// };
}
// TODO: Web SQL
if ( window.openDatabase ) {
// Sad:
// "There is no way to enumerate or delete the databases available for an origin from this API."
// Ref.: http://www.w3.org/TR/webdatabase/#databases
}
2014-10-18 08:01:09 +13:00
}
catch (e) {
2014-10-18 08:01:09 +13:00
}
})();
2014-10-18 08:01:09 +13:00
/******************************************************************************/
2014-10-18 08:01:09 +13:00
/******************************************************************************/
// https://github.com/gorhill/uMatrix/issues/45
var collapser = (function() {
var timer = null;
var requestId = 1;
var newRequests = [];
var pendingRequests = {};
var pendingRequestCount = 0;
var srcProps = {
'img': 'src'
};
2015-05-11 09:59:58 +12:00
var reURLplaceholder = /\{\{url\}\}/g;
var PendingRequest = function(target) {
this.id = requestId++;
this.target = target;
pendingRequests[this.id] = this;
pendingRequestCount += 1;
};
// Because a while ago I have observed constructors are faster than
// literal object instanciations.
var BouncingRequest = function(id, tagName, url) {
this.id = id;
this.tagName = tagName;
this.url = url;
2015-05-03 04:07:40 +12:00
this.blocked = false;
};
2015-05-03 04:07:40 +12:00
var onProcessed = function(response) {
if ( !response ) {
return;
}
2015-05-03 04:07:40 +12:00
var requests = response.requests;
if ( requests === null || Array.isArray(requests) === false ) {
return;
}
2015-05-03 04:07:40 +12:00
var collapse = response.collapse;
2015-05-03 23:10:05 +12:00
var placeholders = response.placeholders;
var i = requests.length;
var request, entry, target, tagName;
while ( i-- ) {
request = requests[i];
if ( pendingRequests.hasOwnProperty(request.id) === false ) {
continue;
}
entry = pendingRequests[request.id];
delete pendingRequests[request.id];
pendingRequestCount -= 1;
2015-05-04 00:18:06 +12:00
// Not blocked
2015-05-03 04:07:40 +12:00
if ( !request.blocked ) {
continue;
}
2015-05-04 00:18:06 +12:00
target = entry.target;
2015-05-04 00:18:06 +12:00
// No placeholders
2015-05-03 04:07:40 +12:00
if ( collapse ) {
target.style.setProperty('display', 'none', 'important');
2015-05-03 23:33:24 +12:00
continue;
2015-05-03 04:07:40 +12:00
}
2015-05-04 00:18:06 +12:00
2015-05-03 23:33:24 +12:00
tagName = target.localName;
2015-05-04 00:18:06 +12:00
// Special case: iframe
if ( tagName === 'iframe' ) {
target.setAttribute(
'src',
2015-05-11 09:59:58 +12:00
'data:text/html,' + encodeURIComponent(
placeholders.iframe.replace(reURLplaceholder, request.url)
)
2015-05-04 00:18:06 +12:00
);
continue;
2015-05-03 23:58:10 +12:00
}
2015-05-04 00:18:06 +12:00
// Everything else
target.setAttribute(srcProps[tagName], placeholders[tagName]);
target.style.setProperty('border', placeholders.border, 'important');
target.style.setProperty('background', placeholders.background, 'important');
}
2015-05-03 04:07:40 +12:00
// Renew map: I believe that even if all properties are deleted, an
// object will still use more memory than a brand new one.
if ( pendingRequestCount === 0 ) {
pendingRequests = {};
}
};
var send = function() {
timer = null;
2015-04-12 09:15:57 +12:00
localMessager.send({
what: 'evaluateURLs',
requests: newRequests
}, onProcessed);
newRequests = [];
};
2014-10-18 08:01:09 +13:00
var process = function(delay) {
if ( newRequests.length === 0 ) {
return;
}
if ( delay === 0 ) {
clearTimeout(timer);
send();
} else if ( timer === null ) {
timer = vAPI.setTimeout(send, delay || 50);
}
};
2014-10-18 08:01:09 +13:00
2015-05-03 23:10:05 +12:00
var iframeSourceModified = function(mutations) {
var i = mutations.length;
while ( i-- ) {
addFrameNode(mutations[i].target, true);
}
process();
};
2015-05-04 01:06:35 +12:00
var iframeSourceObserver = null;
2015-05-03 23:10:05 +12:00
var iframeSourceObserverOptions = {
attributes: true,
attributeFilter: [ 'src' ]
};
var addFrameNode = function(iframe, dontObserve) {
// https://github.com/gorhill/uBlock/issues/162
// Be prepared to deal with possible change of src attribute.
if ( dontObserve !== true ) {
2015-05-04 01:06:35 +12:00
if ( iframeSourceObserver === null ) {
iframeSourceObserver = new MutationObserver(iframeSourceModified);
}
2015-05-03 23:10:05 +12:00
iframeSourceObserver.observe(iframe, iframeSourceObserverOptions);
}
// https://github.com/chrisaljoudi/uBlock/issues/174
// Do not remove fragment from src URL
var src = iframe.src;
if ( src.lastIndexOf('http', 0) !== 0 ) {
return;
}
var req = new PendingRequest(iframe);
newRequests.push(new BouncingRequest(req.id, 'iframe', src));
};
var addNode = function(target) {
var tagName = target.localName;
2015-05-03 23:10:05 +12:00
if ( tagName === 'iframe' ) {
addFrameNode(target);
return;
}
var prop = srcProps[tagName];
if ( prop === undefined ) {
return;
}
// https://github.com/chrisaljoudi/uBlock/issues/174
// Do not remove fragment from src URL
var src = target[prop];
if ( typeof src !== 'string' || src === '' ) {
return;
}
if ( src.lastIndexOf('http', 0) !== 0 ) {
return;
}
var req = new PendingRequest(target);
newRequests.push(new BouncingRequest(req.id, tagName, src));
};
2014-10-18 08:01:09 +13:00
var addNodes = function(nodes) {
var node;
var i = nodes.length;
while ( i-- ) {
node = nodes[i];
if ( node.nodeType === 1 ) {
addNode(node);
}
}
};
2014-10-18 08:01:09 +13:00
var addBranches = function(branches) {
var root;
var i = branches.length;
while ( i-- ) {
root = branches[i];
if ( root.nodeType === 1 ) {
addNode(root);
// blocked images will be reported by onResourceFailed
addNodes(root.querySelectorAll('iframe'));
}
}
};
2014-10-18 08:01:09 +13:00
// Listener to collapse blocked resources.
// - Future requests not blocked yet
// - Elements dynamically added to the page
// - Elements which resource URL changes
var onResourceFailed = function(ev) {
addNode(ev.target);
process();
};
document.addEventListener('error', onResourceFailed, true);
vAPI.shutdown.add(function() {
if ( timer !== null ) {
clearTimeout(timer);
timer = null;
}
2015-05-04 01:06:35 +12:00
if ( iframeSourceObserver !== null ) {
iframeSourceObserver.disconnect();
iframeSourceObserver = null;
}
document.removeEventListener('error', onResourceFailed, true);
newRequests = [];
pendingRequests = {};
pendingRequestCount = 0;
});
return {
addNodes: addNodes,
addBranches: addBranches,
process: process
};
})();
2014-10-18 08:01:09 +13:00
/******************************************************************************/
2014-10-18 08:01:09 +13:00
/******************************************************************************/
var hasInlineScript = function(nodeList, summary) {
2014-10-18 08:01:09 +13:00
var i = 0;
var node, text;
2014-10-18 08:01:09 +13:00
while ( node = nodeList.item(i++) ) {
if ( node.nodeType !== 1 ) {
continue;
}
if ( typeof node.localName !== 'string' ) {
2014-10-18 08:01:09 +13:00
continue;
}
if ( node.localName === 'script' ) {
2014-10-18 08:01:09 +13:00
// https://github.com/gorhill/httpswitchboard/issues/252
// Do not count uMatrix's own script tags, they are not required
2014-10-18 08:01:09 +13:00
// to "unbreak" a web page
2015-05-10 11:34:45 +12:00
if ( typeof node.id === 'string' && ownScripts[node.id] ) {
continue;
2014-10-18 08:01:09 +13:00
}
text = node.textContent.trim();
if ( text === '' ) {
continue;
2014-10-18 08:01:09 +13:00
}
summary.inlineScript = true;
2014-10-18 08:01:09 +13:00
break;
}
2014-10-18 08:01:09 +13:00
if ( node.localName === 'a' && node.href.lastIndexOf('javascript', 0) === 0 ) {
summary.inlineScript = true;
2014-10-18 08:01:09 +13:00
break;
}
}
if ( summary.inlineScript ) {
summary.mustReport = true;
}
2014-10-18 08:01:09 +13:00
};
2015-05-10 11:34:45 +12:00
var ownScripts = {
'umatrix-ua-spoofer': true
};
2014-10-18 08:01:09 +13:00
/******************************************************************************/
var nodeListsAddedHandler = function(nodeLists) {
var i = nodeLists.length;
if ( i === 0 ) {
return;
}
2014-10-18 08:01:09 +13:00
var summary = {
what: 'contentScriptSummary',
locationURL: window.location.href,
inlineScript: false,
2014-10-18 08:01:09 +13:00
mustReport: false
};
while ( i-- ) {
if ( summary.inlineScript === false ) {
hasInlineScript(nodeLists[i], summary);
}
collapser.addBranches(nodeLists[i]);
2014-10-18 08:01:09 +13:00
}
if ( summary.mustReport ) {
2015-04-12 09:15:57 +12:00
localMessager.send(summary);
2014-10-18 08:01:09 +13:00
}
collapser.process();
2014-10-18 08:01:09 +13:00
};
/******************************************************************************/
/******************************************************************************/
// Executed only once.
2014-10-18 08:01:09 +13:00
(function() {
var summary = {
what: 'contentScriptSummary',
locationURL: window.location.href,
inlineScript: false,
2014-10-18 08:01:09 +13:00
mustReport: true
};
// https://github.com/gorhill/httpswitchboard/issues/25
// &
// Looks for inline javascript also in at least one a[href] element.
// https://github.com/gorhill/httpswitchboard/issues/131
hasInlineScript(document.querySelectorAll('a[href^="javascript:"],script'), summary);
2014-10-18 08:01:09 +13:00
//console.debug('contentscript-end.js > firstObservationHandler(): found %d script tags in "%s"', Object.keys(summary.scriptSources).length, window.location.href);
2015-04-12 09:15:57 +12:00
localMessager.send(summary);
collapser.addNodes(document.querySelectorAll('iframe,img'));
collapser.process();
2014-10-18 08:01:09 +13:00
})();
/******************************************************************************/
2014-10-18 08:01:09 +13:00
/******************************************************************************/
// Observe changes in the DOM
// Added node lists will be cumulated here before being processed
(function() {
var addedNodeLists = [];
var addedNodeListsTimer = null;
var treeMutationObservedHandler = function() {
nodeListsAddedHandler(addedNodeLists);
addedNodeListsTimer = null;
addedNodeLists = [];
};
// https://github.com/gorhill/uBlock/issues/205
// Do not handle added node directly from within mutation observer.
var treeMutationObservedHandlerAsync = function(mutations) {
var iMutation = mutations.length;
var nodeList;
while ( iMutation-- ) {
nodeList = mutations[iMutation].addedNodes;
if ( nodeList.length !== 0 ) {
addedNodeLists.push(nodeList);
}
2014-10-18 08:01:09 +13:00
}
// I arbitrarily chose 250 ms for now:
// I have to compromise between the overhead of processing too few
// nodes too often and the delay of many nodes less often. There is nothing
// time critical here.
if ( addedNodeListsTimer === null ) {
addedNodeListsTimer = vAPI.setTimeout(treeMutationObservedHandler, 250);
}
};
// This fixes http://acid3.acidtests.org/
2015-05-03 23:10:05 +12:00
if ( !document.body ) {
return;
2014-10-18 08:01:09 +13:00
}
// https://github.com/gorhill/httpswitchboard/issues/176
var treeObserver = new MutationObserver(treeMutationObservedHandlerAsync);
treeObserver.observe(document.body, {
2014-10-18 08:01:09 +13:00
childList: true,
subtree: true
});
vAPI.shutdown.add(function() {
if ( addedNodeListsTimer !== null ) {
clearTimeout(addedNodeListsTimer);
addedNodeListsTimer = null;
}
if ( treeObserver !== null ) {
treeObserver.disconnect();
treeObserver = null;
}
addedNodeLists = [];
});
})();
2014-10-18 08:01:09 +13:00
/******************************************************************************/
2014-10-18 08:01:09 +13:00
/******************************************************************************/
localMessager.send({ what: 'shutdown?' }, function(response) {
if ( response === true ) {
vAPI.shutdown.exec();
}
});
2015-04-12 09:15:57 +12:00
/******************************************************************************/
/******************************************************************************/
})();