@ -1,7 +1,7 @@
/ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
/ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
µMatrix - a Chromium browser extension to black / white list requests .
µMatrix - a Chromium browser extension to black / white list requests .
Copyright ( C ) 2014 Raymond Hill
Copyright ( C ) 2014 - 2105 Raymond Hill
This program is free software : you can redistribute it and / or modify
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
it under the terms of the GNU General Public License as published by
@ -63,88 +63,243 @@ vAPI.contentscriptEndInjected = true;
var localMessager = vAPI . messaging . channel ( 'contentscript-end.js' ) ;
var localMessager = vAPI . messaging . channel ( 'contentscript-end.js' ) ;
vAPI . shutdown . add ( function ( ) {
localMessager . close ( ) ;
} ) ;
/******************************************************************************/
/******************************************************************************/
/******************************************************************************/
/******************************************************************************/
// This is to be executed only once: putting this code in its own closure
// means the code will be flushed from memory once executed.
// Unrendered noscript (because CSP) workaround
// Executed once.
( function ( ) {
( 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 ) ;
} ) ( ) ;
/******************************************************************************/
/******************************************************************************/
/******************************************************************************/
/*------------[ Unrendered Noscript (because CSP) Workaround ]----------------*/
// Executed only once.
var checkScriptBlacklistedHandler = function ( response ) {
if ( ! response . scriptBlacklisted ) {
return ;
( 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
}
}
}
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 ) ;
catch ( e ) {
}
}
} ;
localMessager . send ( {
what : 'checkScriptBlacklisted' ,
url : window . location . href
} , checkScriptBlacklistedHandler ) ;
} ) ( ) ;
/******************************************************************************/
/******************************************************************************/
/******************************************************************************/
var localStorageHandler = function ( mustRemove ) {
if ( mustRemove ) {
window . localStorage . clear ( ) ;
window . sessionStorage . clear ( ) ;
// console.debug('HTTP Switchboard > found and removed non-empty localStorage');
}
} ;
// 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 = {
'iframe' : 'src' ,
'img' : 'src'
} ;
var PendingRequest = function ( target , tagName , attr ) {
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 ;
this . collapse = false ;
} ;
var onProcessed = function ( requests ) {
if ( requests === null || Array . isArray ( requests ) === false ) {
return ;
}
var i = requests . length ;
var request , entry ;
while ( i -- ) {
request = requests [ i ] ;
if ( pendingRequests . hasOwnProperty ( request . id ) === false ) {
continue ;
}
entry = pendingRequests [ request . id ] ;
delete pendingRequests [ request . id ] ;
pendingRequestCount -= 1 ;
// https://github.com/chrisaljoudi/uBlock/issues/869
if ( ! request . collapse ) {
continue ;
}
// 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 ) {
// https://github.com/chrisaljoudi/uBlock/issues/399
// Never remove elements from the DOM, just hide them
entry . target . style . setProperty ( 'display' , 'none' , 'important' ) ;
}
// 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 ;
localMessager . send ( {
localMessager . send ( {
what : 'contentScriptHasLocalStorage' ,
url : window . location . href
} , localStorageHandler ) ;
}
what : 'evaluateURLs' ,
requests : newRequests
} , onProcessed ) ;
newRequests = [ ] ;
} ;
// TODO: indexedDB
if ( window . indexedDB && ! ! window . indexedDB . webkitGetDatabaseNames ) {
// var db = window.indexedDB.webkitGetDatabaseNames().onsuccess = function(sender) {
// console.debug('webkitGetDatabaseNames(): result=%o', sender.target.result);
// };
}
var process = function ( delay ) {
if ( newRequests . length === 0 ) {
return ;
}
if ( delay === 0 ) {
clearTimeout ( timer ) ;
send ( ) ;
} else if ( timer === null ) {
timer = setTimeout ( send , delay || 50 ) ;
}
} ;
// 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
}
}
catch ( e ) {
}
var addNode = function ( target ) {
var tagName = target . localName ;
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 , tagName , prop ) ;
newRequests . push ( new BouncingRequest ( req . id , tagName , src ) ) ;
} ;
} ) ( ) ;
var addNodes = function ( nodes ) {
var node ;
var i = nodes . length ;
while ( i -- ) {
node = nodes [ i ] ;
if ( node . nodeType === 1 ) {
addNode ( node ) ;
}
}
} ;
/******************************************************************************/
/******************************************************************************/
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' ) ) ;
}
}
} ;
( function ( ) {
// 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 ;
}
document . removeEventListener ( 'error' , onResourceFailed , true ) ;
newRequests = [ ] ;
pendingRequests = { } ;
pendingRequestCount = 0 ;
} ) ;
return {
addNodes : addNodes ,
addBranches : addBranches ,
process : process
} ;
} ) ( ) ;
/******************************************************************************/
/******************************************************************************/
/******************************************************************************/
var nodesAddedHandler = function ( nodeList , summary ) {
var nodesAddedHandler = function ( nodeList , summary ) {
@ -154,17 +309,17 @@ var nodesAddedHandler = function(nodeList, summary) {
if ( node . nodeType !== 1 ) {
if ( node . nodeType !== 1 ) {
continue ;
continue ;
}
}
if ( typeof node . tag Name !== 'string' ) {
if ( typeof node . local Name !== 'string' ) {
continue ;
continue ;
}
}
switch ( node . tagName . toUpperCase ( ) ) {
switch ( node . localName ) {
case 'SCRIPT ' :
case 'script ' :
// https://github.com/gorhill/httpswitchboard/issues/252
// https://github.com/gorhill/httpswitchboard/issues/252
// Do not count µMatrix's own script tags, they are not required
// Do not count µMatrix's own script tags, they are not required
// to "unbreak" a web page
// to "unbreak" a web page
if ( node . id && node . id . i ndexOf( 'uMatrix-' ) === 0 ) {
if ( typeof node . id === 'string' && node . id . lastI ndexOf( 'uMatrix-' , 0 ) === 0 ) {
break ;
break ;
}
}
text = node . textContent . trim ( ) ;
text = node . textContent . trim ( ) ;
@ -179,14 +334,14 @@ var nodesAddedHandler = function(nodeList, summary) {
}
}
break ;
break ;
case 'A ' :
if ( node . href . i ndexOf( 'javascript: ' ) === 0 ) {
case 'a ' :
if ( node . href . lastI ndexOf( 'javascript' , 0 ) === 0 ) {
summary . scriptSources [ '{inline_script}' ] = true ;
summary . scriptSources [ '{inline_script}' ] = true ;
summary . mustReport = true ;
summary . mustReport = true ;
}
}
break ;
break ;
case 'OBJECT ' :
case 'object ' :
src = ( node . data || '' ) . trim ( ) ;
src = ( node . data || '' ) . trim ( ) ;
if ( src !== '' ) {
if ( src !== '' ) {
summary . pluginSources [ src ] = true ;
summary . pluginSources [ src ] = true ;
@ -194,7 +349,7 @@ var nodesAddedHandler = function(nodeList, summary) {
}
}
break ;
break ;
case 'EMBED ' :
case 'embed ' :
src = ( node . src || '' ) . trim ( ) ;
src = ( node . src || '' ) . trim ( ) ;
if ( src !== '' ) {
if ( src !== '' ) {
summary . pluginSources [ src ] = true ;
summary . pluginSources [ src ] = true ;
@ -221,13 +376,18 @@ var nodeListsAddedHandler = function(nodeLists) {
} ;
} ;
while ( i -- ) {
while ( i -- ) {
nodesAddedHandler ( nodeLists [ i ] , summary ) ;
nodesAddedHandler ( nodeLists [ i ] , summary ) ;
collapser . addBranches ( nodeLists [ i ] ) ;
}
}
if ( summary . mustReport ) {
if ( summary . mustReport ) {
localMessager . send ( summary ) ;
localMessager . send ( summary ) ;
}
}
collapser . process ( ) ;
} ;
} ;
/******************************************************************************/
/******************************************************************************/
/******************************************************************************/
// Executed only once.
( function ( ) {
( function ( ) {
var summary = {
var summary = {
@ -241,60 +401,86 @@ var nodeListsAddedHandler = function(nodeLists) {
// &
// &
// Looks for inline javascript also in at least one a[href] element.
// Looks for inline javascript also in at least one a[href] element.
// https://github.com/gorhill/httpswitchboard/issues/131
// https://github.com/gorhill/httpswitchboard/issues/131
nodesAddedHandler ( document . querySelectorAll ( 'script, a[href^="javascript:"], object, embed ' ) , summary ) ;
nodesAddedHandler ( document . querySelectorAll ( 'a[href^="javascript:"],embed,object,script ' ) , summary ) ;
//console.debug('contentscript-end.js > firstObservationHandler(): found %d script tags in "%s"', Object.keys(summary.scriptSources).length, window.location.href);
//console.debug('contentscript-end.js > firstObservationHandler(): found %d script tags in "%s"', Object.keys(summary.scriptSources).length, window.location.href);
localMessager . send ( summary ) ;
localMessager . send ( summary ) ;
collapser . addNodes ( document . querySelectorAll ( 'iframe,img' ) ) ;
collapser . process ( ) ;
} ) ( ) ;
} ) ( ) ;
/******************************************************************************/
/******************************************************************************/
/******************************************************************************/
// Observe changes in the DOM
// Observe changes in the DOM
// Added node lists will be cumulated here before being processed
// Added node lists will be cumulated here before being processed
var addedNodeLists = [ ] ;
var addedNodeListsTimer = null ;
var treeMutationObservedHandler = function ( ) {
nodeListsAddedHandler ( addedNodeLists ) ;
addedNodeListsTimer = null ;
addedNodeLists = [ ] ;
} ;
( function ( ) {
var addedNodeLists = [ ] ;
var addedNodeListsTimer = null ;
// 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 ) ;
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 ) ;
}
}
}
// 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 = setTimeout ( treeMutationObservedHandler , 250 ) ;
}
} ;
// This fixes http://acid3.acidtests.org/
if ( document . body ) {
return ;
}
}
// 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 = setTimeout ( treeMutationObservedHandler , 250 ) ;
}
} ;
// This fixes http://acid3.acidtests.org/
if ( document . body ) {
// https://github.com/gorhill/httpswitchboard/issues/176
// https://github.com/gorhill/httpswitchboard/issues/176
var treeObserver = new MutationObserver ( treeMutationObservedHandlerAsync ) ;
var treeObserver = new MutationObserver ( treeMutationObservedHandlerAsync ) ;
treeObserver . observe ( document . body , {
treeObserver . observe ( document . body , {
childList : true ,
childList : true ,
subtree : true
subtree : true
} ) ;
} ) ;
}
vAPI . shutdown . add ( function ( ) {
if ( addedNodeListsTimer !== null ) {
clearTimeout ( addedNodeListsTimer ) ;
addedNodeListsTimer = null ;
}
if ( treeObserver !== null ) {
treeObserver . disconnect ( ) ;
treeObserver = null ;
}
addedNodeLists = [ ] ;
} ) ;
} ) ( ) ;
/******************************************************************************/
/******************************************************************************/
/******************************************************************************/
} ) ( ) ;
localMessager . send ( { what : 'shutdown?' } , function ( response ) {
if ( response === true ) {
vAPI . shutdown . exec ( ) ;
}
} ) ;
/******************************************************************************/
/******************************************************************************/
/******************************************************************************/
/******************************************************************************/