diff --git a/src/_locales/en/messages.json b/src/_locales/en/messages.json
index e03bb85..a79b536 100644
--- a/src/_locales/en/messages.json
+++ b/src/_locales/en/messages.json
@@ -342,6 +342,10 @@
"message": "Collapse placeholder of blocked elements",
"description": "English: Collapse placeholder of blocked elements"
},
+ "settingsCollapseBlacklisted" : {
+ "message": "Collapse placeholder of blacklisted elements",
+ "description": "A setting in the dashboard's Settings pane: 'blacklisted' means 'for which there is a specific block rule', 'specific' means 'a rule for which the destination hostname is not `*`'"
+ },
"settingsNoscriptTagsSpoofed" : {
"message": "Spoof tags when 1st-party scripts are blocked",
"description": "This appears in the Settings pane in the dashboard"
diff --git a/src/css/dashboard-common.css b/src/css/dashboard-common.css
index d78e52e..9d465ff 100644
--- a/src/css/dashboard-common.css
+++ b/src/css/dashboard-common.css
@@ -36,7 +36,9 @@ a {
button {
padding: 0.3em 0.5em;
}
-
+input[disabled] + label {
+ color: gray;
+ }
.para {
width: 40em;
}
diff --git a/src/js/background.js b/src/js/background.js
index e178ce7..7d3b4ec 100644
--- a/src/js/background.js
+++ b/src/js/background.js
@@ -91,6 +91,7 @@ return {
clearBrowserCache: true,
clearBrowserCacheAfter: 60,
cloudStorageEnabled: false,
+ collapseBlacklisted: true,
collapseBlocked: false,
colorBlindFriendly: false,
deleteCookies: false,
diff --git a/src/js/contentscript.js b/src/js/contentscript.js
index b15957e..4717065 100644
--- a/src/js/contentscript.js
+++ b/src/js/contentscript.js
@@ -109,71 +109,86 @@ vAPI.contentscriptEndInjected = true;
// 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'
+ var resquestIdGenerator = 1,
+ processTimer,
+ toProcess = [],
+ toFilter = [],
+ toCollapse = new Map(),
+ cachedBlockedMap,
+ cachedBlockedMapHash,
+ cachedBlockedMapTimer,
+ reURLPlaceholder = /\{\{url\}\}/g;
+ var src1stProps = {
+ 'embed': 'src',
+ 'iframe': 'src',
+ 'img': 'src',
+ 'object': 'data'
};
- var reURLplaceholder = /\{\{url\}\}/g;
-
- var PendingRequest = function(target) {
- this.id = requestId++;
- this.target = target;
- pendingRequests[this.id] = this;
- pendingRequestCount += 1;
+ var src2ndProps = {
+ 'img': 'srcset'
};
-
- // 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.blocked = false;
+ var tagToTypeMap = {
+ embed: 'media',
+ iframe: 'frame',
+ img: 'image',
+ object: 'media'
+ };
+ var cachedBlockedSetClear = function() {
+ cachedBlockedMap =
+ cachedBlockedMapHash =
+ cachedBlockedMapTimer = undefined;
};
+ // https://github.com/chrisaljoudi/uBlock/issues/174
+ // Do not remove fragment from src URL
var onProcessed = function(response) {
- if ( !response ) {
+ if ( !response ) { // This happens if uBO is disabled or restarted.
+ toCollapse.clear();
return;
}
- var requests = response.requests;
- if ( requests === null || Array.isArray(requests) === false ) {
+
+ var targets = toCollapse.get(response.id);
+ if ( targets === undefined ) { return; }
+ toCollapse.delete(response.id);
+ if ( cachedBlockedMapHash !== response.hash ) {
+ cachedBlockedMap = new Map(response.blockedResources);
+ cachedBlockedMapHash = response.hash;
+ if ( cachedBlockedMapTimer !== undefined ) {
+ clearTimeout(cachedBlockedMapTimer);
+ }
+ cachedBlockedMapTimer = vAPI.setTimeout(cachedBlockedSetClear, 30000);
+ }
+ if ( cachedBlockedMap === undefined || cachedBlockedMap.size === 0 ) {
return;
}
- var collapse = response.collapse;
- var placeholders = response.placeholders;
- var i = requests.length;
- var request, entry, target, tagName, docurl, replaced;
- while ( i-- ) {
- request = requests[i];
- if ( pendingRequests.hasOwnProperty(request.id) === false ) {
- continue;
- }
- entry = pendingRequests[request.id];
- delete pendingRequests[request.id];
- pendingRequestCount -= 1;
- // Not blocked
- if ( !request.blocked ) {
- continue;
+ var placeholders = response.placeholders,
+ tag, prop, src, collapsed, docurl, replaced;
+
+ for ( var target of targets ) {
+ tag = target.localName;
+ prop = src1stProps[tag];
+ if ( prop === undefined ) { continue; }
+ src = target[prop];
+ if ( typeof src !== 'string' || src.length === 0 ) {
+ prop = src2ndProps[tag];
+ if ( prop === undefined ) { continue; }
+ src = target[prop];
+ if ( typeof src !== 'string' || src.length === 0 ) { continue; }
}
-
- target = entry.target;
-
- // No placeholders
- if ( collapse ) {
+ collapsed = cachedBlockedMap.get(tagToTypeMap[tag] + ' ' + src);
+ if ( collapsed === undefined ) { continue; }
+ if ( collapsed ) {
target.style.setProperty('display', 'none', 'important');
+ target.hidden = true;
continue;
}
-
- tagName = target.localName;
-
- // Special case: iframe
- if ( tagName === 'iframe' ) {
- docurl = 'data:text/html,' + encodeURIComponent(placeholders.iframe.replace(reURLplaceholder, request.url));
+ if ( tag === 'iframe' ) {
+ docurl =
+ 'data:text/html,' +
+ encodeURIComponent(
+ placeholders.iframe.replace(reURLPlaceholder, src)
+ );
replaced = false;
// Using contentWindow.location prevent tainting browser
// history -- i.e. breaking back button (seen on Chromium).
@@ -189,148 +204,125 @@ var collapser = (function() {
}
continue;
}
-
- // Everything else
- target.setAttribute(srcProps[tagName], placeholders[tagName]);
+ target.setAttribute(src1stProps[tag], placeholders[tag]);
target.style.setProperty('border', placeholders.border, 'important');
target.style.setProperty('background', placeholders.background, '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;
- vAPI.messaging.send('contentscript.js', {
- what: 'evaluateURLs',
- requests: newRequests
- }, onProcessed);
- newRequests = [];
+ processTimer = undefined;
+ toCollapse.set(resquestIdGenerator, toProcess);
+ var msg = {
+ what: 'lookupBlockedCollapsibles',
+ id: resquestIdGenerator,
+ toFilter: toFilter,
+ hash: cachedBlockedMapHash
+ };
+ vAPI.messaging.send('contentscript.js', msg, onProcessed);
+ toProcess = [];
+ toFilter = [];
+ resquestIdGenerator += 1;
};
var process = function(delay) {
- if ( newRequests.length === 0 ) {
- return;
- }
+ if ( toProcess.length === 0 ) { return; }
if ( delay === 0 ) {
- clearTimeout(timer);
+ if ( processTimer !== undefined ) {
+ clearTimeout(processTimer);
+ }
send();
- } else if ( timer === null ) {
- timer = vAPI.setTimeout(send, delay || 50);
+ } else if ( processTimer === undefined ) {
+ processTimer = vAPI.setTimeout(send, delay || 47);
+ }
+ };
+
+ var add = function(target) {
+ toProcess.push(target);
+ };
+
+ var addMany = function(targets) {
+ var i = targets.length;
+ while ( i-- ) {
+ toProcess.push(targets[i]);
}
};
var iframeSourceModified = function(mutations) {
var i = mutations.length;
while ( i-- ) {
- addFrameNode(mutations[i].target, true);
+ addIFrame(mutations[i].target, true);
}
process();
};
- var iframeSourceObserver = null;
+ var iframeSourceObserver;
var iframeSourceObserverOptions = {
attributes: true,
attributeFilter: [ 'src' ]
};
- var addFrameNode = function(iframe, dontObserve) {
+ var addIFrame = function(iframe, dontObserve) {
// https://github.com/gorhill/uBlock/issues/162
// Be prepared to deal with possible change of src attribute.
if ( dontObserve !== true ) {
- if ( iframeSourceObserver === null ) {
+ if ( iframeSourceObserver === undefined ) {
iframeSourceObserver = new MutationObserver(iframeSourceModified);
}
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;
- 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));
+ if ( src === '' || typeof src !== 'string' ) { return; }
+ if ( src.startsWith('http') === false ) { return; }
+ toFilter.push({ type: 'frame', url: iframe.src });
+ add(iframe);
};
- var addNodes = function(nodes) {
- var node;
- var i = nodes.length;
+ var addIFrames = function(iframes) {
+ var i = iframes.length;
while ( i-- ) {
- node = nodes[i];
- if ( node.nodeType === 1 ) {
- addNode(node);
- }
+ addIFrame(iframes[i]);
}
};
- var addBranches = function(branches) {
- var root;
- var i = branches.length;
+ var addNodeList = function(nodeList) {
+ var node,
+ i = nodeList.length;
while ( i-- ) {
- root = branches[i];
- if ( root.nodeType === 1 ) {
- addNode(root);
- // blocked images will be reported by onResourceFailed
- addNodes(root.querySelectorAll('iframe'));
+ node = nodeList[i];
+ if ( node.nodeType !== 1 ) { continue; }
+ if ( node.localName === 'iframe' ) {
+ addIFrame(node);
+ }
+ if ( node.childElementCount !== 0 ) {
+ addIFrames(node.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();
+ if ( tagToTypeMap[ev.target.localName] !== undefined ) {
+ add(ev.target);
+ process();
+ }
};
document.addEventListener('error', onResourceFailed, true);
vAPI.shutdown.add(function() {
- if ( timer !== null ) {
- clearTimeout(timer);
- timer = null;
- }
- if ( iframeSourceObserver !== null ) {
+ document.removeEventListener('error', onResourceFailed, true);
+ if ( iframeSourceObserver !== undefined ) {
iframeSourceObserver.disconnect();
- iframeSourceObserver = null;
+ iframeSourceObserver = undefined;
+ }
+ if ( processTimer !== undefined ) {
+ clearTimeout(processTimer);
+ processTimer = undefined;
}
- document.removeEventListener('error', onResourceFailed, true);
- newRequests = [];
- pendingRequests = {};
- pendingRequestCount = 0;
});
return {
- addNodes: addNodes,
- addBranches: addBranches,
+ addMany: addMany,
+ addIFrames: addIFrames,
+ addNodeList: addNodeList,
process: process
};
})();
@@ -345,10 +337,6 @@ var hasInlineScript = function(nodeList, summary) {
if ( node.nodeType !== 1 ) {
continue;
}
- if ( typeof node.localName !== 'string' ) {
- continue;
- }
-
if ( node.localName === 'script' ) {
text = node.textContent.trim();
if ( text === '' ) {
@@ -357,7 +345,6 @@ var hasInlineScript = function(nodeList, summary) {
summary.inlineScript = true;
break;
}
-
if ( node.localName === 'a' && node.href.lastIndexOf('javascript', 0) === 0 ) {
summary.inlineScript = true;
break;
@@ -368,8 +355,6 @@ var hasInlineScript = function(nodeList, summary) {
}
};
-/******************************************************************************/
-
var nodeListsAddedHandler = function(nodeLists) {
var i = nodeLists.length;
if ( i === 0 ) {
@@ -385,7 +370,7 @@ var nodeListsAddedHandler = function(nodeLists) {
if ( summary.inlineScript === false ) {
hasInlineScript(nodeLists[i], summary);
}
- collapser.addBranches(nodeLists[i]);
+ collapser.addNodeList(nodeLists[i]);
}
if ( summary.mustReport ) {
vAPI.messaging.send('contentscript.js', summary);
@@ -415,7 +400,8 @@ var nodeListsAddedHandler = function(nodeLists) {
vAPI.messaging.send('contentscript.js', summary);
- collapser.addNodes(document.querySelectorAll('iframe,img'));
+ collapser.addMany(document.querySelectorAll('img'));
+ collapser.addIFrames(document.querySelectorAll('iframe'));
collapser.process();
})();
@@ -427,6 +413,9 @@ var nodeListsAddedHandler = function(nodeLists) {
// Added node lists will be cumulated here before being processed
(function() {
+ // This fixes http://acid3.acidtests.org/
+ if ( !document.body ) { return; }
+
var addedNodeLists = [];
var addedNodeListsTimer = null;
@@ -439,28 +428,19 @@ var nodeListsAddedHandler = function(nodeLists) {
// 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;
+ var iMutation = mutations.length,
+ 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 = vAPI.setTimeout(treeMutationObservedHandler, 250);
+ addedNodeListsTimer = vAPI.setTimeout(treeMutationObservedHandler, 47);
}
};
- // This fixes http://acid3.acidtests.org/
- if ( !document.body ) {
- return;
- }
-
// https://github.com/gorhill/httpswitchboard/issues/176
var treeObserver = new MutationObserver(treeMutationObservedHandlerAsync);
treeObserver.observe(document.body, {
diff --git a/src/js/matrix.js b/src/js/matrix.js
index bcfad2e..24650ff 100644
--- a/src/js/matrix.js
+++ b/src/js/matrix.js
@@ -39,6 +39,9 @@ var uniqueIdGenerator = 1;
var Matrix = function() {
this.id = uniqueIdGenerator++;
this.reset();
+ this.sourceRegister = '';
+ this.decomposedSourceRegister = [''];
+ this.specificityRegister = 0;
};
/******************************************************************************/
@@ -141,9 +144,7 @@ var isIPAddress = function(hostname) {
/******************************************************************************/
var toBroaderHostname = function(hostname) {
- if ( hostname === '*' ) {
- return '';
- }
+ if ( hostname === '*' ) { return ''; }
if ( isIPAddress(hostname) ) {
return toBroaderIPAddress(hostname);
}
@@ -192,6 +193,20 @@ Matrix.prototype.reset = function() {
/******************************************************************************/
+Matrix.prototype.decomposeSource = function(srcHostname) {
+ if ( srcHostname === this.sourceRegister ) { return; }
+ var hn = srcHostname;
+ this.decomposedSourceRegister[0] = this.sourceRegister = hn;
+ var i = 1;
+ for (;;) {
+ hn = toBroaderHostname(hn);
+ this.decomposedSourceRegister[i++] = hn;
+ if ( hn === '' ) { break; }
+ }
+};
+
+/******************************************************************************/
+
// Copy another matrix to self. Do this incrementally to minimize impact on
// a live matrix.
@@ -331,10 +346,13 @@ Matrix.prototype.evaluateCell = function(srcHostname, desHostname, type) {
/******************************************************************************/
Matrix.prototype.evaluateCellZ = function(srcHostname, desHostname, type) {
- var bitOffset = typeBitOffsets.get(type);
- var s = srcHostname;
- var v;
+ this.decomposeSource(srcHostname);
+
+ var bitOffset = typeBitOffsets.get(type),
+ s, v, i = 0;
for (;;) {
+ s = this.decomposedSourceRegister[i++];
+ if ( s === '' ) { break; }
v = this.rules.get(s + ' ' + desHostname);
if ( v !== undefined ) {
v = v >> bitOffset & 3;
@@ -342,9 +360,6 @@ Matrix.prototype.evaluateCellZ = function(srcHostname, desHostname, type) {
return v;
}
}
- // TODO: external rules? (for presets)
- s = toBroaderHostname(s);
- if ( s === '' ) { break; }
}
// srcHostname is '*' at this point
@@ -366,6 +381,7 @@ Matrix.prototype.evaluateCellZ = function(srcHostname, desHostname, type) {
Matrix.prototype.evaluateCellZXY = function(srcHostname, desHostname, type) {
// Matrix filtering switch
+ this.specificityRegister = 0;
if ( this.evaluateSwitchZ('matrix-off', srcHostname) ) {
return Matrix.GreenIndirect;
}
@@ -377,11 +393,13 @@ Matrix.prototype.evaluateCellZXY = function(srcHostname, desHostname, type) {
// evaluating net requests.
// Specific-hostname specific-type cell
+ this.specificityRegister = 1;
var r = this.evaluateCellZ(srcHostname, desHostname, type);
if ( r === 1 ) { return Matrix.RedDirect; }
if ( r === 2 ) { return Matrix.GreenDirect; }
// Specific-hostname any-type cell
+ this.specificityRegister = 2;
var rl = this.evaluateCellZ(srcHostname, desHostname, '*');
if ( rl === 1 ) { return Matrix.RedIndirect; }
@@ -390,10 +408,9 @@ Matrix.prototype.evaluateCellZXY = function(srcHostname, desHostname, type) {
// Ancestor cells, up to 1st-party destination domain
if ( firstPartyDesDomain !== '' ) {
+ this.specificityRegister = 3;
for (;;) {
- if ( d === firstPartyDesDomain ) {
- break;
- }
+ if ( d === firstPartyDesDomain ) { break; }
d = d.slice(d.indexOf('.') + 1);
// specific-hostname specific-type cell
@@ -420,11 +437,10 @@ Matrix.prototype.evaluateCellZXY = function(srcHostname, desHostname, type) {
}
// Keep going, up to root
+ this.specificityRegister = 4;
for (;;) {
d = toBroaderHostname(d);
- if ( d === '*' ) {
- break;
- }
+ if ( d === '*' ) { break; }
// specific-hostname specific-type cell
r = this.evaluateCellZ(srcHostname, d, type);
@@ -438,6 +454,7 @@ Matrix.prototype.evaluateCellZXY = function(srcHostname, desHostname, type) {
}
// Any-hostname specific-type cells
+ this.specificityRegister = 5;
r = this.evaluateCellZ(srcHostname, '*', type);
// Line below is strict-blocking
if ( r === 1 ) { return Matrix.RedIndirect; }
@@ -446,6 +463,7 @@ Matrix.prototype.evaluateCellZXY = function(srcHostname, desHostname, type) {
if ( r === 2 ) { return Matrix.GreenIndirect; }
// Any-hostname any-type cell
+ this.specificityRegister = 6;
r = this.evaluateCellZ(srcHostname, '*', '*');
if ( r === 1 ) { return Matrix.RedIndirect; }
if ( r === 2 ) { return Matrix.GreenIndirect; }
@@ -534,12 +552,14 @@ Matrix.prototype.evaluateSwitch = function(switchName, srcHostname) {
Matrix.prototype.evaluateSwitchZ = function(switchName, srcHostname) {
var bitOffset = switchBitOffsets.get(switchName);
- if ( bitOffset === undefined ) {
- return false;
- }
- var bits;
- var s = srcHostname;
+ if ( bitOffset === undefined ) { return false; }
+
+ this.decomposeSource(srcHostname);
+
+ var s, bits, i = 0;
for (;;) {
+ s = this.decomposedSourceRegister[i++];
+ if ( s === '' ) { break; }
bits = this.switches.get(s) || 0;
if ( bits !== 0 ) {
bits = bits >> bitOffset & 3;
@@ -547,10 +567,6 @@ Matrix.prototype.evaluateSwitchZ = function(switchName, srcHostname) {
return bits === 1;
}
}
- s = toBroaderHostname(s);
- if ( s === '' ) {
- break;
- }
}
return false;
};
diff --git a/src/js/messaging.js b/src/js/messaging.js
index 797abd0..6fa1360 100644
--- a/src/js/messaging.js
+++ b/src/js/messaging.js
@@ -427,68 +427,58 @@ var contentScriptSummaryHandler = function(tabId, details) {
/******************************************************************************/
var contentScriptLocalStorageHandler = function(tabId, pageURL) {
- var µmuri = µm.URI.set(pageURL);
- var response = µm.mustBlock(µm.scopeFromURL(pageURL), µmuri.hostname, 'cookie');
- µm.recordFromTabId(
- tabId,
- 'cookie',
- µmuri.rootURL() + '/{localStorage}',
- response
+ var tabContext = µm.tabContextManager.lookup(tabId);
+ if ( tabContext === null ) { return; }
+
+ var blocked = µm.mustBlock(
+ tabContext.rootHostname,
+ µm.URI.hostnameFromURI(pageURL),
+ 'cookie'
);
- response = response && µm.userSettings.deleteLocalStorage;
- if ( response ) {
+
+ var pageStore = µm.pageStoreFromTabId(tabId);
+ if ( pageStore !== null ) {
+ var requestURL = µm.URI.originFromURI(pageURL) + '/{localStorage}';
+ pageStore.recordRequest('cookie', requestURL, blocked);
+ µm.logger.writeOne(tabId, 'net', tabContext.rootHostname, requestURL, 'cookie', blocked);
+ }
+
+ var removeStorage = blocked && µm.userSettings.deleteLocalStorage;
+ if ( removeStorage ) {
µm.localStorageRemovedCounter++;
}
- return response;
+
+ return removeStorage;
};
/******************************************************************************/
// Evaluate many URLs against the matrix.
-var evaluateURLs = function(tabId, requests) {
- var collapse = µm.userSettings.collapseBlocked;
+var lookupBlockedCollapsibles = function(tabId, requests) {
var response = {
- collapse: collapse,
- requests: requests
+ blockedResources: [],
+ hash: requests.hash,
+ id: requests.id,
+ placeholders: placeholders
};
- // Create evaluation context
var tabContext = µm.tabContextManager.lookup(tabId);
if ( tabContext === null ) {
return response;
}
- var rootHostname = tabContext.rootHostname;
-
- //console.debug('messaging.js/contentscript.js: processing %d requests', requests.length);
var pageStore = µm.pageStoreFromTabId(tabId);
- var µmuri = µm.URI;
- var typeMap = tagNameToRequestTypeMap;
- var request, type;
- var i = requests.length;
- while ( i-- ) {
- request = requests[i];
- type = typeMap[request.tagName];
- request.blocked = µm.mustBlock(
- rootHostname,
- µmuri.hostnameFromURI(request.url),
- type
- );
- // https://github.com/gorhill/uMatrix/issues/205
- // If blocked, the URL must be recorded by the page store, so as to ensure
- // they are properly reflected in the matrix.
- if ( request.blocked && pageStore ) {
- pageStore.recordRequest(type, request.url, true);
- }
+ if ( pageStore !== null ) {
+ pageStore.lookupBlockedCollapsibles(requests, response);
}
- if ( collapse ) {
- placeholders = null;
- return response;
- }
+ // TODO: evaluate whether the issue reported below still exists.
+ // https://github.com/gorhill/uMatrix/issues/205
+ // If blocked, the URL must be recorded by the page store, so as to
+ // ensure they are properly reflected in the matrix.
- if ( placeholders === null ) {
+ if ( response.placeholders === null ) {
placeholders = {
background:
vAPI.localStorage.getItem('placeholderBackground') ||
@@ -505,19 +495,12 @@ var evaluateURLs = function(tabId, requests) {
};
placeholders.iframe =
placeholders.iframe.replace('{{bg}}', placeholders.background);
+ response.placeholders = placeholders;
}
- response.placeholders = placeholders;
return response;
};
-/******************************************************************************/
-
-var tagNameToRequestTypeMap = {
- 'iframe': 'frame',
- 'img': 'image'
-};
-
var placeholders = null;
/******************************************************************************/
@@ -544,8 +527,8 @@ var onMessage = function(request, sender, callback) {
contentScriptSummaryHandler(tabId, request);
break;
- case 'evaluateURLs':
- response = evaluateURLs(tabId, request.requests);
+ case 'lookupBlockedCollapsibles':
+ response = lookupBlockedCollapsibles(tabId, request);
break;
case 'mustRenderNoscriptTags?':
diff --git a/src/js/pagestats.js b/src/js/pagestats.js
index f180449..af01c64 100644
--- a/src/js/pagestats.js
+++ b/src/js/pagestats.js
@@ -25,117 +25,245 @@
µMatrix.pageStoreFactory = (function() {
- var µm = µMatrix;
- var pageStoreJunkyard = [];
-
- // Ref: Given a URL, returns a (somewhat) unique 32-bit value
- // Based on: FNV32a
- // http://www.isthe.com/chongo/tech/comp/fnv/index.html#FNV-reference-source
- // The rest is custom, suited for µMatrix.
- var uidFromURL = function(uri) {
- var hint = 0x811c9dc5;
- var i = uri.length;
- while ( i-- ) {
- hint ^= uri.charCodeAt(i) | 0;
- hint += (hint<<1) + (hint<<4) + (hint<<7) + (hint<<8) + (hint<<24) | 0;
- hint >>>= 0;
+/******************************************************************************/
+
+var µm = µMatrix;
+
+/******************************************************************************/
+
+var BlockedCollapsibles = function() {
+ this.boundPruneAsyncCallback = this.pruneAsyncCallback.bind(this);
+ this.blocked = new Map();
+ this.hash = 0;
+ this.timer = null;
+};
+
+BlockedCollapsibles.prototype = {
+
+ shelfLife: 10 * 1000,
+
+ add: function(type, url, isSpecific) {
+ if ( this.blocked.size === 0 ) { this.pruneAsync(); }
+ var now = Date.now() / 1000 | 0;
+ // The following "trick" is to encode the specifity into the lsb of the
+ // time stamp so as to avoid to have to allocate a memory structure to
+ // store both time stamp and specificity.
+ if ( isSpecific ) {
+ now |= 0x00000001;
+ } else {
+ now &= 0xFFFFFFFE;
}
- return hint;
- };
+ this.blocked.set(type + ' ' + url, now);
+ this.hash = now;
+ },
+
+ reset: function() {
+ this.blocked.clear();
+ this.hash = 0;
+ if ( this.timer !== null ) {
+ clearTimeout(this.timer);
+ this.timer = null;
+ }
+ },
- function PageStore(tabContext) {
- this.requestStats = µm.requestStatsFactory();
- this.off = false;
- this.init(tabContext);
+ pruneAsync: function() {
+ if ( this.timer === null ) {
+ this.timer = vAPI.setTimeout(
+ this.boundPruneAsyncCallback,
+ this.shelfLife * 2
+ );
+ }
+ },
+
+ pruneAsyncCallback: function() {
+ this.timer = null;
+ var obsolete = Date.now() - this.shelfLife;
+ for ( var entry of this.blocked ) {
+ if ( entry[1] <= obsolete ) {
+ this.blocked.delete(entry[0]);
+ }
+ }
+ if ( this.blocked.size !== 0 ) { this.pruneAsync(); }
}
+};
+
+/******************************************************************************/
+
+// Ref: Given a URL, returns a (somewhat) unique 32-bit value
+// Based on: FNV32a
+// http://www.isthe.com/chongo/tech/comp/fnv/index.html#FNV-reference-source
+// The rest is custom, suited for uMatrix.
+
+var PageStore = function(tabContext) {
+ this.hostnameTypeCells = new Map();
+ this.domains = new Set();
+ this.blockedCollapsibles = new BlockedCollapsibles();
+ this.requestStats = µm.requestStatsFactory();
+ this.off = false;
+ this.init(tabContext);
+};
+
+PageStore.prototype = {
- PageStore.prototype = {
- init: function(tabContext) {
- this.tabId = tabContext.tabId;
- this.rawUrl = tabContext.rawURL;
- this.pageUrl = tabContext.normalURL;
- this.pageHostname = tabContext.rootHostname;
- this.pageDomain = tabContext.rootDomain;
- this.title = '';
- this.hostnameTypeCells = new Map();
- this.domains = new Set();
- this.allHostnamesString = ' ';
- this.requestStats.reset();
- this.distinctRequestCount = 0;
- this.perLoadAllowedRequestCount = 0;
- this.perLoadBlockedRequestCount = 0;
+ collapsibleTypes: new Set([ 'image' ]),
+ pageStoreJunkyard: [],
+
+ init: function(tabContext) {
+ this.tabId = tabContext.tabId;
+ this.rawUrl = tabContext.rawURL;
+ this.pageUrl = tabContext.normalURL;
+ this.pageHostname = tabContext.rootHostname;
+ this.pageDomain = tabContext.rootDomain;
+ this.title = '';
+ this.hostnameTypeCells.clear();
+ this.domains.clear();
+ this.allHostnamesString = ' ';
+ this.blockedCollapsibles.reset();
+ this.requestStats.reset();
+ this.distinctRequestCount = 0;
+ this.perLoadAllowedRequestCount = 0;
+ this.perLoadBlockedRequestCount = 0;
+ this.incinerationTimer = null;
+ this.mtxContentModifiedTime = 0;
+ this.mtxCountModifiedTime = 0;
+ return this;
+ },
+
+ dispose: function() {
+ this.rawUrl = '';
+ this.pageUrl = '';
+ this.pageHostname = '';
+ this.pageDomain = '';
+ this.title = '';
+ this.hostnameTypeCells.clear();
+ this.domains.clear();
+ this.allHostnamesString = ' ';
+ this.blockedCollapsibles.reset();
+ if ( this.incinerationTimer !== null ) {
+ clearTimeout(this.incinerationTimer);
this.incinerationTimer = null;
- this.mtxContentModifiedTime = 0;
- this.mtxCountModifiedTime = 0;
- return this;
- },
- dispose: function() {
- this.hostnameTypeCells.clear();
- this.rawUrl = '';
- this.pageUrl = '';
- this.pageHostname = '';
- this.pageDomain = '';
- this.title = '';
- this.domains.clear();
- this.allHostnamesString = ' ';
- if ( this.incinerationTimer !== null ) {
- clearTimeout(this.incinerationTimer);
- this.incinerationTimer = null;
- }
- if ( pageStoreJunkyard.length < 8 ) {
- pageStoreJunkyard.push(this);
- }
- },
- recordRequest: function(type, url, block) {
- var hostname = µm.URI.hostnameFromURI(url);
-
- // Store distinct network requests. This is used to:
- // - remember which hostname/type were seen
- // - count the number of distinct URLs for any given
- // hostname-type pair
- var key = hostname + ' ' + type,
- uids = this.hostnameTypeCells.get(key);
- if ( uids === undefined ) {
- this.hostnameTypeCells.set(key, (uids = new Set()));
- } else if ( uids.size > 99 ) {
- return;
- }
- var uid = uidFromURL(url);
- if ( uids.has(uid) ) { return; }
- uids.add(uid);
-
- // Count blocked/allowed requests
- this.requestStats.record(type, block);
-
- // https://github.com/gorhill/httpswitchboard/issues/306
- // If it is recorded locally, record globally
- µm.requestStats.record(type, block);
- µm.updateBadgeAsync(this.tabId);
-
- if ( block !== false ) {
- this.perLoadBlockedRequestCount++;
- } else {
- this.perLoadAllowedRequestCount++;
- }
+ }
+ if ( this.pageStoreJunkyard.length < 8 ) {
+ this.pageStoreJunkyard.push(this);
+ }
+ },
+
+ cacheBlockedCollapsible: function(type, url, specificity) {
+ if ( this.collapsibleTypes.has(type) ) {
+ this.blockedCollapsibles.add(
+ type,
+ url,
+ specificity !== 0 && specificity < 5
+ );
+ }
+ },
+
+ lookupBlockedCollapsibles: function(request, response) {
+ var tabContext = µm.tabContextManager.lookup(this.tabId);
+ if ( tabContext === null ) { return; }
- this.distinctRequestCount++;
- this.mtxCountModifiedTime = Date.now();
+ var collapseBlacklisted = µm.userSettings.collapseBlacklisted,
+ collapseBlocked = µm.userSettings.collapseBlocked,
+ entry;
- if ( this.domains.has(hostname) === false ) {
- this.domains.add(hostname);
- this.allHostnamesString += hostname + ' ';
- this.mtxContentModifiedTime = Date.now();
+ var blockedResources = response.blockedResources;
+
+ if (
+ Array.isArray(request.toFilter) &&
+ request.toFilter.length !== 0
+ ) {
+ var roothn = tabContext.rootHostname,
+ hnFromURI = µm.URI.hostnameFromURI,
+ tMatrix = µm.tMatrix;
+ for ( entry of request.toFilter ) {
+ if ( tMatrix.mustBlock(roothn, hnFromURI(entry.url), entry.type) === false ) {
+ continue;
+ }
+ blockedResources.push([
+ entry.type + ' ' + entry.url,
+ collapseBlocked ||
+ collapseBlacklisted && tMatrix.specificityRegister !== 0 &&
+ tMatrix.specificityRegister < 5
+ ]);
}
}
- };
- return function pageStoreFactory(tabContext) {
- var entry = pageStoreJunkyard.pop();
- if ( entry ) {
- return entry.init(tabContext);
+ if ( this.blockedCollapsibles.hash === response.hash ) { return; }
+ response.hash = this.blockedCollapsibles.hash;
+
+ for ( entry of this.blockedCollapsibles.blocked ) {
+ blockedResources.push([
+ entry[0],
+ collapseBlocked || collapseBlacklisted && (entry[1] & 1) !== 0
+ ]);
+ }
+ },
+
+ recordRequest: function(type, url, block) {
+ // Store distinct network requests. This is used to:
+ // - remember which hostname/type were seen
+ // - count the number of distinct URLs for any given
+ // hostname-type pair
+ var hostname = µm.URI.hostnameFromURI(url),
+ key = hostname + ' ' + type,
+ uids = this.hostnameTypeCells.get(key);
+ if ( uids === undefined ) {
+ this.hostnameTypeCells.set(key, (uids = new Set()));
+ } else if ( uids.size > 99 ) {
+ return;
+ }
+ var uid = this.uidFromURL(url);
+ if ( uids.has(uid) ) { return; }
+ uids.add(uid);
+
+ // Count blocked/allowed requests
+ this.requestStats.record(type, block);
+
+ // https://github.com/gorhill/httpswitchboard/issues/306
+ // If it is recorded locally, record globally
+ µm.requestStats.record(type, block);
+ µm.updateBadgeAsync(this.tabId);
+
+ if ( block !== false ) {
+ this.perLoadBlockedRequestCount++;
+ } else {
+ this.perLoadAllowedRequestCount++;
+ }
+
+ this.distinctRequestCount++;
+ this.mtxCountModifiedTime = Date.now();
+
+ if ( this.domains.has(hostname) === false ) {
+ this.domains.add(hostname);
+ this.allHostnamesString += hostname + ' ';
+ this.mtxContentModifiedTime = Date.now();
+ }
+ },
+
+ uidFromURL: function(uri) {
+ var hint = 0x811c9dc5,
+ i = uri.length;
+ while ( i-- ) {
+ hint ^= uri.charCodeAt(i) | 0;
+ hint += (hint<<1) + (hint<<4) + (hint<<7) + (hint<<8) + (hint<<24) | 0;
+ hint >>>= 0;
}
- return new PageStore(tabContext);
- };
+ return hint;
+ }
+};
+
+/******************************************************************************/
+
+return function pageStoreFactory(tabContext) {
+ var entry = PageStore.prototype.pageStoreJunkyard.pop();
+ if ( entry ) {
+ return entry.init(tabContext);
+ }
+ return new PageStore(tabContext);
+};
+
+/******************************************************************************/
+
})();
/******************************************************************************/
diff --git a/src/js/settings.js b/src/js/settings.js
index 9717fe3..1bf0a15 100644
--- a/src/js/settings.js
+++ b/src/js/settings.js
@@ -53,16 +53,16 @@ function changeMatrixSwitch(name, state) {
/******************************************************************************/
-function onChangeValueHandler(uelem, setting, min, max) {
+function onChangeValueHandler(elem, setting, min, max) {
var oldVal = cachedSettings.userSettings[setting];
- var newVal = Math.round(parseFloat(uelem.val()));
+ var newVal = Math.round(parseFloat(elem.value));
if ( typeof newVal !== 'number' ) {
newVal = oldVal;
} else {
newVal = Math.max(newVal, min);
newVal = Math.min(newVal, max);
}
- uelem.val(newVal);
+ elem.value = newVal;
if ( newVal !== oldVal ) {
changeUserSettings(setting, newVal);
}
@@ -71,50 +71,89 @@ function onChangeValueHandler(uelem, setting, min, max) {
/******************************************************************************/
function prepareToDie() {
- onChangeValueHandler(uDom('#delete-unused-session-cookies-after'), 'deleteUnusedSessionCookiesAfter', 15, 1440);
- onChangeValueHandler(uDom('#clear-browser-cache-after'), 'clearBrowserCacheAfter', 15, 1440);
+ onChangeValueHandler(
+ uDom.nodeFromId('deleteUnusedSessionCookiesAfter'),
+ 'deleteUnusedSessionCookiesAfter',
+ 15, 1440
+ );
+ onChangeValueHandler(
+ uDom.nodeFromId('clearBrowserCacheAfter'),
+ 'clearBrowserCacheAfter',
+ 15, 1440
+ );
}
/******************************************************************************/
-var installEventHandlers = function() {
- uDom('input[name="displayTextSize"]').on('change', function(){
- changeUserSettings('displayTextSize', this.value);
- });
-
- uDom('#popupScopeLevel').on('change', function(){
- changeUserSettings('popupScopeLevel', this.value);
- });
+function onInputChanged(ev) {
+ var target = ev.target;
+
+ switch ( target.id ) {
+ case 'displayTextSizeNormal':
+ case 'displayTextSizeLarge':
+ changeUserSettings('displayTextSize', target.value);
+ break;
+ case 'clearBrowserCache':
+ case 'cloudStorageEnabled':
+ case 'collapseBlacklisted':
+ case 'collapseBlocked':
+ case 'colorBlindFriendly':
+ case 'deleteCookies':
+ case 'deleteLocalStorage':
+ case 'deleteUnusedSessionCookies':
+ case 'iconBadgeEnabled':
+ case 'processHyperlinkAuditing':
+ changeUserSettings(target.id, target.checked);
+ break;
+ case 'noMixedContent':
+ case 'processReferer':
+ changeMatrixSwitch(
+ target.getAttribute('data-matrix-switch'),
+ target.checked
+ );
+ break;
+ case 'deleteUnusedSessionCookiesAfter':
+ onChangeValueHandler(target, 'deleteUnusedSessionCookiesAfter', 15, 1440);
+ break;
+ case 'clearBrowserCacheAfter':
+ onChangeValueHandler(target, 'clearBrowserCacheAfter', 15, 1440);
+ break;
+ case 'popupScopeLevel':
+ changeUserSettings('popupScopeLevel', target.value);
+ break;
+ default:
+ break;
+ }
- uDom('[data-setting-bool]').on('change', function(){
- var settingName = this.getAttribute('data-setting-bool');
- if ( typeof settingName === 'string' && settingName !== '' ) {
- changeUserSettings(settingName, this.checked);
- }
- });
+ switch ( target.id ) {
+ case 'collapseBlocked':
+ synchronizeWidgets();
+ break;
+ default:
+ break;
+ }
+}
- uDom('[data-matrix-switch]').on('change', function(){
- var switchName = this.getAttribute('data-matrix-switch');
- if ( typeof switchName === 'string' && switchName !== '' ) {
- changeMatrixSwitch(switchName, this.checked);
- }
- });
+/******************************************************************************/
- uDom('#delete-unused-session-cookies-after').on('change', function(){
- onChangeValueHandler(uDom(this), 'deleteUnusedSessionCookiesAfter', 15, 1440);
- });
- uDom('#clear-browser-cache-after').on('change', function(){
- onChangeValueHandler(uDom(this), 'clearBrowserCacheAfter', 15, 1440);
- });
+function synchronizeWidgets() {
+ var e1, e2;
- // https://github.com/gorhill/httpswitchboard/issues/197
- uDom(window).on('beforeunload', prepareToDie);
-};
+ e1 = uDom.nodeFromId('collapseBlocked');
+ e2 = uDom.nodeFromId('collapseBlacklisted');
+ if ( e1.checked ) {
+ e2.setAttribute('disabled', '');
+ } else {
+ e2.removeAttribute('disabled');
+ }
+}
/******************************************************************************/
-uDom.onLoad(function() {
- var onSettingsReceived = function(settings) {
+vAPI.messaging.send(
+ 'settings.js',
+ { what: 'getUserSettings' },
+ function onSettingsReceived(settings) {
// Cache copy
cachedSettings = settings;
@@ -122,10 +161,7 @@ uDom.onLoad(function() {
var matrixSwitches = settings.matrixSwitches;
uDom('[data-setting-bool]').forEach(function(elem){
- var settingName = elem.attr('data-setting-bool');
- if ( typeof settingName === 'string' && settingName !== '' ) {
- elem.prop('checked', userSettings[settingName] === true);
- }
+ elem.prop('checked', userSettings[elem.prop('id')] === true);
});
uDom('[data-matrix-switch]').forEach(function(elem){
@@ -140,18 +176,19 @@ uDom.onLoad(function() {
});
uDom.nodeFromId('popupScopeLevel').value = userSettings.popupScopeLevel;
+ uDom.nodeFromId('deleteUnusedSessionCookiesAfter').value =
+ userSettings.deleteUnusedSessionCookiesAfter;
+ uDom.nodeFromId('clearBrowserCacheAfter').value =
+ userSettings.clearBrowserCacheAfter;
- uDom('#delete-unused-session-cookies-after').val(userSettings.deleteUnusedSessionCookiesAfter);
- uDom('#clear-browser-cache-after').val(userSettings.clearBrowserCacheAfter);
+ synchronizeWidgets();
- installEventHandlers();
- };
- vAPI.messaging.send(
- 'settings.js',
- { what: 'getUserSettings' },
- onSettingsReceived
- );
-});
+ document.addEventListener('change', onInputChanged);
+
+ // https://github.com/gorhill/httpswitchboard/issues/197
+ uDom(window).on('beforeunload', prepareToDie);
+ }
+);
/******************************************************************************/
diff --git a/src/js/tab.js b/src/js/tab.js
index 7eca608..ed96b1f 100644
--- a/src/js/tab.js
+++ b/src/js/tab.js
@@ -552,19 +552,6 @@ vAPI.tabs.registerListeners();
/******************************************************************************/
-// Log a request
-
-µm.recordFromTabId = function(tabId, type, url, blocked) {
- var pageStore = this.pageStoreFromTabId(tabId);
- if ( pageStore === null ) {
- return;
- }
- pageStore.recordRequest(type, url, blocked);
- this.logger.writeOne(tabId, 'net', pageStore.pageHostname, url, type, blocked);
-};
-
-/******************************************************************************/
-
µm.forceReload = function(tabId, bypassCache) {
vAPI.tabs.reload(tabId, bypassCache);
};
diff --git a/src/js/traffic.js b/src/js/traffic.js
index 2a52c97..c25b4c1 100644
--- a/src/js/traffic.js
+++ b/src/js/traffic.js
@@ -75,26 +75,19 @@ var onBeforeRootFrameRequestHandler = function(details) {
// Intercept and filter web requests according to white and black lists.
var onBeforeRequestHandler = function(details) {
- var µm = µMatrix,
- µmuri = µm.URI;
-
- // rhill 2014-02-17: Ignore 'filesystem:': this can happen when listening
- // to 'chrome-extension://'.
- var requestScheme = µmuri.schemeFromURI(details.url);
- if ( requestScheme === 'filesystem' ) {
- return;
- }
-
var requestType = requestTypeNormalizer[details.type] || 'other';
// https://github.com/gorhill/httpswitchboard/issues/303
// Wherever the main doc comes from, create a receiver page URL: synthetize
// one if needed.
- if ( requestType === 'doc' && details.parentFrameId < 0 ) {
+ if ( requestType === 'doc' && details.parentFrameId === -1 ) {
return onBeforeRootFrameRequestHandler(details);
}
- var requestURL = details.url;
+ var µm = µMatrix,
+ µmuri = µm.URI,
+ requestURL = details.url,
+ requestScheme = µmuri.schemeFromURI(requestURL);
// Ignore non-network schemes
if ( µmuri.isNetworkScheme(requestScheme) === false ) {
@@ -109,13 +102,24 @@ var onBeforeRequestHandler = function(details) {
// to scope on unknown scheme? Etc.
// https://github.com/gorhill/httpswitchboard/issues/191
// https://github.com/gorhill/httpswitchboard/issues/91#issuecomment-37180275
- var tabContext = µm.tabContextManager.mustLookup(details.tabId);
- var tabId = tabContext.tabId;
- var rootHostname = tabContext.rootHostname;
+ var tabContext = µm.tabContextManager.mustLookup(details.tabId),
+ tabId = tabContext.tabId,
+ rootHostname = tabContext.rootHostname,
+ specificity = 0;
+
+ // Filter through matrix
+ var block = µm.tMatrix.mustBlock(
+ rootHostname,
+ µmuri.hostnameFromURI(requestURL),
+ requestType
+ );
+ if ( block ) {
+ specificity = µm.tMatrix.specificityRegister;
+ }
// Enforce strict secure connection?
- var block = false;
if (
+ block === false &&
tabContext.secure &&
µmuri.isSecureScheme(requestScheme) === false &&
µm.tMatrix.evaluateSwitchZ('https-strict', rootHostname)
@@ -123,11 +127,6 @@ var onBeforeRequestHandler = function(details) {
block = true;
}
- // Disallow request as per temporary matrix?
- if ( block === false ) {
- block = µm.mustBlock(rootHostname, µmuri.hostnameFromURI(requestURL), requestType);
- }
-
// Record request.
// https://github.com/gorhill/httpswitchboard/issues/342
// The way requests are handled now, it may happen at this point some
@@ -138,16 +137,10 @@ var onBeforeRequestHandler = function(details) {
pageStore.recordRequest(requestType, requestURL, block);
µm.logger.writeOne(tabId, 'net', rootHostname, requestURL, details.type, block);
- // Allowed?
- if ( !block ) {
- // console.debug('onBeforeRequestHandler()> ALLOW "%s": %o', details.url, details);
- return;
+ if ( block ) {
+ pageStore.cacheBlockedCollapsible(requestType, requestURL, specificity);
+ return { 'cancel': true };
}
-
- // Blocked
- // console.debug('onBeforeRequestHandler()> BLOCK "%s": %o', details.url, details);
-
- return { 'cancel': true };
};
/******************************************************************************/
diff --git a/src/settings.html b/src/settings.html
index 1c14b2e..f162596 100644
--- a/src/settings.html
+++ b/src/settings.html
@@ -33,17 +33,17 @@ ul > li.separator {