diff --git a/platform/chromium/manifest.json b/platform/chromium/manifest.json index 67c6942..53ea428 100644 --- a/platform/chromium/manifest.json +++ b/platform/chromium/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 2, - "name": "µMatrix", - "short_name": "µMatrix", + "name": "uMatrix", + "short_name": "uMatrix", "version": "0.8.1.4", "description": "__MSG_extShortDesc__", "icons": { @@ -12,7 +12,7 @@ "default_icon": { "19": "img/browsericons/icon19-19.png" }, - "default_title": "µMatrix", + "default_title": "uMatrix", "default_popup": "popup.html" }, "author": "Raymond Hill", diff --git a/src/_locales/en/messages.json b/src/_locales/en/messages.json index fc62a15..de3686e 100644 --- a/src/_locales/en/messages.json +++ b/src/_locales/en/messages.json @@ -299,6 +299,10 @@ "message": "Opacity", "description": "English: Opacity" }, + "settingsCollapseBlocked" : { + "message": "Collapse placeholder of blocked elements", + "description": "English: Collapse placeholder of blocked elements" + }, "privacyPageTitle" : { diff --git a/src/css/popup.css b/src/css/popup.css index 90c9b51..6b3d4c3 100644 --- a/src/css/popup.css +++ b/src/css/popup.css @@ -462,22 +462,22 @@ body.powerOff .matrix .g4Meta.g4Collapsed ~ .matRow.ro { /* Cell coloring for color blind-friendly (hopefully) */ body.colorblind .t81 { color: white; - background-color: #000; + background-color: rgb(0, 19, 110); } body.colorblind .t82 { - border-color: #aaa; + border-color: rgb(255, 194, 57); color: black; - background-color: #fff; + background-color: rgb(255, 194, 57); } body.colorblind .t1 { - border-color: #333; - color: white; - background-color: #666; + border-color: rgba(0, 19, 110, 0.3); + color: black; + background-color: rgba(0, 19, 110, 0.2); } body.colorblind .t2 { - border-color: #aaa; + border-color: rgba(255, 194, 57, 0.3); color: black; - background-color: #ccc; + background-color: rgba(255, 194, 57, 0.2); } body.colorblind .matCell.p81 { background-image: url('../img/permanent-black-small-cb.png'); @@ -533,18 +533,15 @@ body.powerOff #whitelist, body.powerOff #blacklist { background-color: #080; opacity: 0.25; } -body.colorblind .rw .matCell.t1 #whitelist:hover { - background-color: #fff; +body.colorblind .rw .matCell.t1 #whitelist:hover, +body.colorblind .rw .matCell.t2 #whitelist:hover { + background-color: rgb(255, 194, 57); opacity: 0.6; } .rw .matCell.t2 #whitelist:hover { background-color: #080; opacity: 0.25; } -body.colorblind .rw .matCell.t2 #whitelist:hover { - background-color: #fff; - opacity: 0.6; - } .matCell.t81 #whitelist:hover { background-color: transparent; } @@ -555,18 +552,15 @@ body.colorblind .rw .matCell.t2 #whitelist:hover { background-color: #c00; opacity: 0.25; } -body.colorblind .rw .matCell.t1 #blacklist:hover { - background-color: #000; +body.colorblind .rw .matCell.t1 #blacklist:hover, +body.colorblind .rw .matCell.t2 #blacklist:hover { + background-color: rgb(0, 19, 110); opacity: 0.4; } .rw .matCell.t2 #blacklist:hover { background-color: #c00; opacity: 0.25; } -body.colorblind .rw .matCell.t2 #blacklist:hover { - background-color: #000; - opacity: 0.4; - } .matCell.t81 #blacklist:hover { background-color: transparent; } diff --git a/src/img/permanent-black-small-cb.png b/src/img/permanent-black-small-cb.png index 48f5361..de45e78 100644 Binary files a/src/img/permanent-black-small-cb.png and b/src/img/permanent-black-small-cb.png differ diff --git a/src/img/permanent-white-small-cb.png b/src/img/permanent-white-small-cb.png index 9f06a69..a774fd3 100644 Binary files a/src/img/permanent-white-small-cb.png and b/src/img/permanent-white-small-cb.png differ diff --git a/src/js/background.js b/src/js/background.js index 75c3971..fba362f 100644 --- a/src/js/background.js +++ b/src/js/background.js @@ -53,6 +53,7 @@ return { autoUpdate: false, clearBrowserCache: true, clearBrowserCacheAfter: 60, + collapseBlocked: false, colorBlindFriendly: false, deleteCookies: false, deleteUnusedSessionCookies: false, diff --git a/src/js/contentscript-end.js b/src/js/contentscript-end.js index 4a48567..34213c1 100644 --- a/src/js/contentscript-end.js +++ b/src/js/contentscript-end.js @@ -1,7 +1,7 @@ /******************************************************************************* µ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 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'); +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() { + 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 = '\n' + realNoscript.textContent; + realNoscript.parentNode.replaceChild(fakeNoscript, realNoscript); + } + }; -/******************************************************************************/ - -/*------------[ Unrendered Noscript (because CSP) Workaround ]----------------*/ - -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 = '\n' + realNoscript.textContent; - realNoscript.parentNode.replaceChild(fakeNoscript, realNoscript); - } -}; - -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'); - } -}; - -// 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 - } -} -catch (e) { -} - -/******************************************************************************/ - + localMessager.send({ + what: 'checkScriptBlacklisted', + url: window.location.href + }, checkScriptBlacklistedHandler); })(); /******************************************************************************/ /******************************************************************************/ -(function() { +// Executed only once. +(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 + } + } + catch (e) { + } +})(); + +/******************************************************************************/ +/******************************************************************************/ + +// 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; + } + + // 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({ + what: 'evaluateURLs', + requests: newRequests + }, onProcessed); + newRequests = []; + }; + + var process = function(delay) { + if ( newRequests.length === 0 ) { + return; + } + if ( delay === 0 ) { + clearTimeout(timer); + send(); + } else if ( timer === null ) { + timer = setTimeout(send, delay || 50); + } + }; + + 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')); + } + } + }; + + // 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) { @@ -154,17 +309,17 @@ var nodesAddedHandler = function(nodeList, summary) { if ( node.nodeType !== 1 ) { continue; } - if ( typeof node.tagName !== 'string' ) { + if ( typeof node.localName !== 'string' ) { continue; } - switch ( node.tagName.toUpperCase() ) { + switch ( node.localName ) { - case 'SCRIPT': + case 'script': // https://github.com/gorhill/httpswitchboard/issues/252 // Do not count µMatrix's own script tags, they are not required // to "unbreak" a web page - if ( node.id && node.id.indexOf('uMatrix-') === 0 ) { + if ( typeof node.id === 'string' && node.id.lastIndexOf('uMatrix-', 0) === 0 ) { break; } text = node.textContent.trim(); @@ -179,14 +334,14 @@ var nodesAddedHandler = function(nodeList, summary) { } break; - case 'A': - if ( node.href.indexOf('javascript:') === 0 ) { + case 'a': + if ( node.href.lastIndexOf('javascript', 0) === 0 ) { summary.scriptSources['{inline_script}'] = true; summary.mustReport = true; } break; - case 'OBJECT': + case 'object': src = (node.data || '').trim(); if ( src !== '' ) { summary.pluginSources[src] = true; @@ -194,7 +349,7 @@ var nodesAddedHandler = function(nodeList, summary) { } break; - case 'EMBED': + case 'embed': src = (node.src || '').trim(); if ( src !== '' ) { summary.pluginSources[src] = true; @@ -221,13 +376,18 @@ var nodeListsAddedHandler = function(nodeLists) { }; while ( i-- ) { nodesAddedHandler(nodeLists[i], summary); + collapser.addBranches(nodeLists[i]); } if ( summary.mustReport ) { localMessager.send(summary); } + collapser.process(); }; /******************************************************************************/ +/******************************************************************************/ + +// Executed only once. (function() { var summary = { @@ -241,62 +401,88 @@ var nodeListsAddedHandler = function(nodeLists) { // & // Looks for inline javascript also in at least one a[href] element. // 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); localMessager.send(summary); + + collapser.addNodes(document.querySelectorAll('iframe,img')); + collapser.process(); })(); +/******************************************************************************/ /******************************************************************************/ // Observe changes in the DOM // 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); - } -}; + // 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; + } -// This fixes http://acid3.acidtests.org/ -if ( document.body ) { // https://github.com/gorhill/httpswitchboard/issues/176 var treeObserver = new MutationObserver(treeMutationObservedHandlerAsync); treeObserver.observe(document.body, { childList: 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(); + } +}); + +/******************************************************************************/ +/******************************************************************************/ + })(); diff --git a/src/js/messaging.js b/src/js/messaging.js index 46e0209..2d80f63 100644 --- a/src/js/messaging.js +++ b/src/js/messaging.js @@ -446,6 +446,45 @@ var contentScriptLocalStorageHandler = function(tabId, pageURL) { /******************************************************************************/ +// Evaluate many URLs against the matrix. + +var evaluateURLs = function(tabId, requests) { + if ( µm.userSettings.collapseBlocked === false ) { + return requests; + } + + // Create evaluation context + var tabContext = µm.tabContextManager.lookup(tabId); + if ( tabContext === null ) { + return requests; + } + var rootHostname = tabContext.rootHostname; + + //console.debug('messaging.js/contentscript-end.js: processing %d requests', requests.length); + + var µmuri = µm.URI; + var typeMap = tagNameToRequestTypeMap; + var request; + var i = requests.length; + while ( i-- ) { + request = requests[i]; + request.collapse = µm.mustBlock( + rootHostname, + µmuri.hostnameFromURI(request.url), + typeMap[request.tagName] + ); + } + + return requests; +}; + +var tagNameToRequestTypeMap = { + 'iframe': 'sub_frame', + 'img': 'image' +}; + +/******************************************************************************/ + var onMessage = function(request, sender, callback) { // Async switch ( request.what ) { @@ -453,20 +492,12 @@ var onMessage = function(request, sender, callback) { break; } - var tabId = sender.tab.id; + var tabId = sender && sender.tab ? sender.tab.id || 0 : 0; // Sync var response; switch ( request.what ) { - case 'contentScriptHasLocalStorage': - response = contentScriptLocalStorageHandler(tabId, request.url); - break; - - case 'contentScriptSummary': - contentScriptSummaryHandler(tabId, request); - break; - case 'checkScriptBlacklisted': response = { scriptBlacklisted: µm.mustBlock( @@ -477,12 +508,31 @@ var onMessage = function(request, sender, callback) { }; break; + case 'contentScriptHasLocalStorage': + response = contentScriptLocalStorageHandler(tabId, request.url); + break; + + case 'contentScriptSummary': + contentScriptSummaryHandler(tabId, request); + break; + + case 'evaluateURLs': + response = evaluateURLs(tabId, request.requests); + break; + case 'getUserAgentReplaceStr': response = µm.tMatrix.evaluateSwitchZ('ua-spoof', request.hostname) ? µm.userAgentReplaceStr : undefined; break; + case 'shutdown?': + var tabContext = µm.tabContextManager.lookup(tabId); + if ( tabContext !== null ) { + response = µm.tMatrix.evaluateSwitchZ('matrix-off', tabContext.rootHostname); + } + break; + default: return vAPI.messaging.UNHANDLED; } diff --git a/src/settings.html b/src/settings.html index f504d25..7e079df 100644 --- a/src/settings.html +++ b/src/settings.html @@ -57,12 +57,16 @@ ul > li {