Additionally, performance improvements:
- Reduce overhead of collapsing elements
  (see https://github.com/gorhill/uBlock/issues/2839)
- Cache decomposition of source hostname when matrix-filtering

Also, various code review.
pull/2/head
gorhill 7 years ago
parent 8615f3b804
commit 73c8da05b7
No known key found for this signature in database
GPG Key ID: 25E1490B761470C2

@ -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 <code><noscript></code> tags when 1st-party scripts are blocked",
"description": "This appears in the Settings pane in the dashboard"

@ -36,7 +36,9 @@ a {
button {
padding: 0.3em 0.5em;
}
input[disabled] + label {
color: gray;
}
.para {
width: 40em;
}

@ -91,6 +91,7 @@ return {
clearBrowserCache: true,
clearBrowserCacheAfter: 60,
cloudStorageEnabled: false,
collapseBlacklisted: true,
collapseBlocked: false,
colorBlindFriendly: false,
deleteCookies: false,

@ -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, {

@ -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;
};

@ -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?':

@ -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);
};
/******************************************************************************/
})();
/******************************************************************************/

@ -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);
}
);
/******************************************************************************/

@ -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);
};

@ -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 };
};
/******************************************************************************/

@ -33,17 +33,17 @@ ul > li.separator {
<h2 data-i18n="settingsMatrixConvenienceHeader"></h2>
<ul>
<li>
<input id="iconBadgeEnabled" type="checkbox" data-setting-bool="iconBadgeEnabled">
<li><input id="iconBadgeEnabled" type="checkbox" data-setting-bool>
<label data-i18n="settingsIconBadgeEnabled" for="iconBadgeEnabled"></label>
<li>
<input id="collapseBlocked" type="checkbox" data-setting-bool="collapseBlocked">
<li><input id="collapseBlocked" type="checkbox" data-setting-bool>
<label data-i18n="settingsCollapseBlocked" for="collapseBlocked"></label>
<li>
<input id="noscriptTagsSpoofed" type="checkbox" data-matrix-switch="noscript-spoof">
<ul>
<li><input id="collapseBlacklisted" type="checkbox" data-setting-bool>
<label data-i18n="settingsCollapseBlacklisted" for="collapseBlacklisted"></label>
</ul>
<li><input id="noscriptTagsSpoofed" type="checkbox" data-matrix-switch="noscript-spoof">
<label data-i18n="settingsNoscriptTagsSpoofed" for="noscriptTagsSpoofed"></label>
<li>
<input id="cloudStorageEnabled" type="checkbox" data-setting-bool="cloudStorageEnabled">
<li><input id="cloudStorageEnabled" type="checkbox" data-setting-bool>
<label data-i18n="settingsCloudStorageEnabled" for="cloudStorageEnabled"></label>
</ul>
<h2 data-i18n="settingsMatrixDisplayHeader"></h2>
@ -56,18 +56,18 @@ ul > li.separator {
<label data-i18n="settingsDefaultScopeLevel"></label> <select id="popupScopeLevel"><option data-i18n="settingsDefaultScopeLevel2" value="site"><option data-i18n="settingsDefaultScopeLevel1" value="domain"><option data-i18n="settingsDefaultScopeLevel0" value="*"></select>
<li class="separator">
<li>
<input id="colorBlindFriendly" type="checkbox" data-setting-bool="colorBlindFriendly">
<input id="colorBlindFriendly" type="checkbox" data-setting-bool>
<label data-i18n="settingsMatrixDisplayColorBlind" for="colorBlindFriendly"></label>
</ul>
<h2 data-i18n="privacyPageName"></h2>
<ul>
<li>
<input id="delete-blacklisted-cookies" type="checkbox" data-setting-bool="deleteCookies"><label data-i18n="privacyDeleteBlockedCookiesPrompt" for="delete-blacklisted-cookies"></label>
<input id="deleteCookies" type="checkbox" data-setting-bool><label data-i18n="privacyDeleteBlockedCookiesPrompt" for="deleteCookies"></label>
<span class="whatisthis"></span>
<div class="whatisthis-expandable para" data-i18n="privacyDeleteBlockedCookiesHelp"></div>
<li>
<input id="delete-unused-session-cookies" type="checkbox" data-setting-bool="deleteUnusedSessionCookies"><label data-i18n="privacyDeleteNonBlockedSessionCookiesPrompt1" for="delete-unused-session-cookies"></label>
<input id="delete-unused-session-cookies-after" type="text" value="60" size="3"><span data-i18n="privacyDeleteNonBlockedSessionCookiesPrompt2"></span>
<input id="deleteUnusedSessionCookies" type="checkbox" data-setting-bool><label data-i18n="privacyDeleteNonBlockedSessionCookiesPrompt1" for="deleteUnusedSessionCookies"></label>
<input id="deleteUnusedSessionCookiesAfter" type="text" value="60" size="3"><span data-i18n="privacyDeleteNonBlockedSessionCookiesPrompt2"></span>
<span class="whatisthis"></span>
<div class="whatisthis-expandable para" data-i18n="privacyDeleteNonBlockedSessionCookiesHelp"></div>
<!--
@ -89,22 +89,22 @@ ul > li.separator {
of these cookies so that they cannot be used to track you.
-->
<li>
<input id="delete-blacklisted-localstorage" type="checkbox" data-setting-bool="deleteLocalStorage"><label data-i18n="privacyDeleteBlockedLocalStoragePrompt" for="delete-blacklisted-localstorage"></label>
<input id="deleteLocalStorage" type="checkbox" data-setting-bool><label data-i18n="privacyDeleteBlockedLocalStoragePrompt" for="deleteLocalStorage"></label>
<li>
<input id="clear-browser-cache" type="checkbox" data-setting-bool="clearBrowserCache"><label data-i18n="privacyClearCachePrompt1" for="clear-browser-cache"></label>
<input id="clear-browser-cache-after" type="text" value="60" size="3"> <label data-i18n="privacyClearCachePrompt2" for="clear-browser-cache-after"></label>
<input id="clearBrowserCache" type="checkbox" data-setting-bool><label data-i18n="privacyClearCachePrompt1" for="clearBrowserCache"></label>
<input id="clearBrowserCacheAfter" type="text" value="60" size="3"> <label data-i18n="privacyClearCachePrompt2" for="clearBrowserCacheAfter"></label>
<span class="whatisthis"></span>
<div class="whatisthis-expandable para" data-i18n="privacyClearCacheHelp"></div>
<li>
<input id="process-referer" type="checkbox" data-matrix-switch="referrer-spoof"><label data-i18n="privacyProcessRefererPrompt" for="process-referer"></label>
<input id="processReferer" type="checkbox" data-matrix-switch="referrer-spoof"><label data-i18n="privacyProcessRefererPrompt" for="processReferer"></label>
<span class="whatisthis"></span>
<div class="whatisthis-expandable para" data-i18n="privacyProcessRefererHelp"></div>
<li>
<input id="no-mixed-content" type="checkbox" data-matrix-switch="https-strict"><label data-i18n="privacyNoMixedContentPrompt" for="no-mixed-content"></label>
<input id="noMixedContent" type="checkbox" data-matrix-switch="https-strict"><label data-i18n="privacyNoMixedContentPrompt" for="noMixedContent"></label>
<span class="whatisthis"></span>
<div class="whatisthis-expandable para" data-i18n="privacyNoMixedContentHelp"></div>
<li>
<input id="process-hyperlink-auditing" type="checkbox" data-setting-bool="processHyperlinkAuditing"><label data-i18n="privacyProcessHyperlinkAuditingPrompt" for="process-hyperlink-auditing"></label>
<input id="processHyperlinkAuditing" type="checkbox" data-setting-bool><label data-i18n="privacyProcessHyperlinkAuditingPrompt" for="processHyperlinkAuditing"></label>
<span class="whatisthis"></span>
<div class="whatisthis-expandable para" data-i18n="privacyProcessHyperlinkAuditingHelp"></div>
</ul>

Loading…
Cancel
Save