diff --git a/src/js/contentscript-end.js b/src/js/contentscript-end.js index 4729c4a0f..74d6f829c 100644 --- a/src/js/contentscript-end.js +++ b/src/js/contentscript-end.js @@ -83,6 +83,169 @@ var messager = vAPI.messaging.channel('contentscript-end.js'); } })(); +/******************************************************************************/ +/******************************************************************************/ + +// https://github.com/gorhill/uBlock/issues/7 + +var uBlockCollapser = (function() { + var timer = null; + var requestId = 1; + var newRequests = []; + var pendingRequests = {}; + var pendingRequestCount = 0; + var srcProps = { + 'embed': 'src', + 'iframe': 'src', + 'img': 'src', + 'object': 'data' + }; + + var PendingRequest = function(target, tagName, attr) { + this.id = requestId++; + this.target = target; + this.tagName = tagName; + this.attr = attr; + 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 selectors = []; + var i = requests.length; + var request, entry, target, value; + 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/gorhill/uBlock/issues/869 + if ( !request.collapse ) { + continue; + } + + target = entry.target; + + // https://github.com/gorhill/uBlock/issues/399 + // Never remove elements from the DOM, just hide them + target.style.setProperty('display', 'none', 'important'); + + // https://github.com/gorhill/uBlock/issues/1048 + // Use attribute to construct CSS rule + if ( value = target.getAttribute(entry.attr) ) { + selectors.push(entry.tagName + '[' + entry.attr + '="' + value + '"]'); + } + } + if ( selectors.length !== 0 ) { + messager.send({ + what: 'injectedSelectors', + type: 'net', + hostname: window.location.hostname, + selectors: selectors + }); + } + // 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; + messager.send({ + what: 'filterRequests', + pageURL: window.location.href, + pageHostname: window.location.hostname, + 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 || 20); + } + }; + + // If needed eventually, we could listen to `src` attribute changes + // for iframes. + + var add = function(target) { + var tagName = target.localName; + var prop = srcProps[tagName]; + if ( prop === undefined ) { + return; + } + // https://github.com/gorhill/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 addIFrame = function(iframe) { + var src = iframe.src; + // TODO: niject content script in `about:blank` as well. + if ( src === '' || typeof src !== 'string' ) { + return; + } + if ( src.lastIndexOf('http', 0) !== 0 ) { + return; + } + var req = new PendingRequest(iframe, 'iframe', 'src'); + newRequests.push(new BouncingRequest(req.id, 'iframe', src)); + }; + + var iframesFromNode = function(node) { + if ( node.localName === 'iframe' ) { + add(node); + } + var iframes = node.querySelectorAll('iframe'); + var i = iframes.length; + while ( i-- ) { + addIFrame(iframes[i]); + } + process(); + }; + + return { + add: add, + addIFrame: addIFrame, + iframesFromNode: iframesFromNode, + process: process + }; +})(); + +/******************************************************************************/ /******************************************************************************/ // Cosmetic filters @@ -298,7 +461,7 @@ var messager = vAPI.messaging.channel('contentscript-end.js'); } } // Candidate 2 = specific form - selector = node.tagName.toLowerCase() + selector; + selector = node.localName + selector; if ( generics.hasOwnProperty(selector) ) { if ( injectedSelectors.hasOwnProperty(selector) === false ) { injectedSelectors[selector] = true; @@ -461,20 +624,22 @@ var messager = vAPI.messaging.channel('contentscript-end.js'); return; } + // https://github.com/gorhill/uBlock/issues/618 + // Following is to observe dynamically added iframes: + // - On Firefox, the iframes fails to fire a `load` event + var ignoreTags = { 'link': true, - 'LINK': true, 'script': true, - 'SCRIPT': true, - 'style': true, - 'STYLE': true + 'style': true }; // Added node lists will be cumulated here before being processed var addedNodeLists = []; var addedNodeListsTimer = null; + var collapser = uBlockCollapser; - var mutationObservedHandler = function() { + var treeMutationObservedHandler = function() { var nodeList, iNode, node; while ( nodeList = addedNodeLists.pop() ) { iNode = nodeList.length; @@ -483,10 +648,11 @@ var messager = vAPI.messaging.channel('contentscript-end.js'); if ( node.nodeType !== 1 ) { continue; } - if ( ignoreTags.hasOwnProperty(node.tagName) ) { + if ( ignoreTags.hasOwnProperty(node.localName) ) { continue; } contextNodes.push(node); + collapser.iframesFromNode(node); } } addedNodeListsTimer = null; @@ -512,7 +678,7 @@ var messager = vAPI.messaging.channel('contentscript-end.js'); // I arbitrarily chose 100 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. - addedNodeListsTimer = setTimeout(mutationObservedHandler, 100); + addedNodeListsTimer = setTimeout(treeMutationObservedHandler, 100); } }; @@ -529,134 +695,17 @@ var messager = vAPI.messaging.channel('contentscript-end.js'); // Permanent -(function() { - // https://github.com/gorhill/uBlock/issues/683 - // Instead of a closure we use a map to remember the element to collapse - var filterRequestId = 1; - var filterRequests = {}; +// Listener to collapse blocked resources. +// - Future requests not blocked yet +// - Elements dynamically added to the page +// - Elements which resource URL changes - var FilterRequest = function(target, tagName, attr) { - this.id = filterRequestId++; - this.target = target; - this.tagName = tagName; - this.attr = attr; - }; - - FilterRequest.send = function(target, tagName, prop, src) { - var req = new FilterRequest(target, tagName, prop); - filterRequests[req.id] = req; - messager.send( - { - what: 'filterRequest', - id: req.id, - tagName: tagName, - requestURL: src, - pageHostname: window.location.hostname, - pageURL: window.location.href - }, - onAnswerReceived - ); - }; - - // Process answer: collapse, or do nothing. - - var onAnswerReceived = function(details) { - // This should not happen under normal circumstances. It probably can - // happen if the extension is disabled though. - if ( typeof details !== 'object' || details === null ) { - return; - } - - // This should definitely not happen - if ( filterRequests.hasOwnProperty(details.id) === false ) { - return; - } - - var req = filterRequests[details.id]; - delete filterRequests[details.id]; - - // https://github.com/gorhill/uBlock/issues/869 - if ( details.collapse !== true ) { - return; - } - - //console.log('contentscript-end.js > onAnswerReceived(%o)', req); - - // If `!important` is not there, going back using history will - // likely cause the hidden element to re-appear. - // https://github.com/gorhill/uBlock/issues/399 - - // Never remove elements from the DOM, just hide them - req.target.style.setProperty('display', 'none', 'important'); - - // https://github.com/gorhill/uBlock/issues/1048 - // We need to use the atrtibute value for the CSS rule - var value = req.target.getAttribute(req.attr); - if ( !value ) { - return; - } - - messager.send({ - what: 'injectedSelectors', - type: 'net', - hostname: window.location.hostname, - selectors: req.tagName + '[' + req.attr + '="' + value + '"]' - }); - }; - - // https://github.com/gorhill/uBlock/issues/174 - // Do not remove fragment from src URL - - // TODO: Find out whether trying to send more than one filter request per - // message is worth it. - - var onResource = function(target, dict) { - if ( !target ) { - return; - } - var tagName = target.tagName.toLowerCase(); - var prop = dict[tagName]; - if ( prop === undefined ) { - return; - } - var src = target[prop]; - if ( typeof src !== 'string' || src === '' ) { - return; - } - if ( src.lastIndexOf('http', 0) !== 0 ) { - return; - } - FilterRequest.send(target, tagName, prop, src); - }; - - // Listeners to mop up whatever is otherwise missed: - // - Future requests not blocked yet - // - Elements dynamically added to the page - // - Elements which resource URL changes - - var loadedElements = { - 'iframe': 'src' - }; - - var failedElements = { - 'img': 'src', - 'input': 'src', - 'object': 'data' - }; - - var onResourceLoaded = function(ev) { - //console.debug('onResourceLoaded(%o)', ev); - onResource(ev.target, loadedElements); - }; - - var onResourceFailed = function(ev) { - //console.debug('onResourceFailed(%o)', ev); - onResource(ev.target, failedElements); - }; - - document.addEventListener('load', onResourceLoaded, true); - document.addEventListener('error', onResourceFailed, true); -})(); +var onResourceFailed = function(ev) { + //console.debug('onResourceFailed(%o)', ev); + uBlockCollapser.add(ev.target); + uBlockCollapser.process(); +}; +document.addEventListener('error', onResourceFailed, true); /******************************************************************************/ /******************************************************************************/ @@ -666,88 +715,30 @@ var messager = vAPI.messaging.channel('contentscript-end.js'); // Executed only once (function() { - var srcProps = { - 'embed': 'src', - 'iframe': 'src', - 'img': 'src', - 'object': 'data' - }; - var elements = []; + var collapser = uBlockCollapser; + var elems, i, elem; - var onAnswerReceived = function(details) { - if ( typeof details !== 'object' || details === null ) { - return; - } - var collapse = details.collapse; + elems = document.querySelectorAll('embed, object'); + i = elems.length; + while ( i-- ) { + collapser.add(elems[i]); + } - // https://github.com/gorhill/uBlock/issues/869 - if ( collapse !== true ) { - return; - } - - var requests = details.requests; - var selectors = []; - var i = requests.length; - var request, elem, attr, value; - while ( i-- ) { - request = requests[i]; - elem = elements[request.index]; - // https://github.com/gorhill/uBlock/issues/399 - // Never remove elements from the DOM, just hide them - elem.style.setProperty('display', 'none', 'important'); - // https://github.com/gorhill/uBlock/issues/1048 - // Use attribute to construct CSS rule - attr = srcProps[request.tagName]; - if ( value = elem.getAttribute(attr) ) { - selectors.push(request.tagName + '[' + attr + '="' + value + '"]'); - } - } - if ( selectors.length !== 0 ) { - messager.send({ - what: 'injectedSelectors', - type: 'net', - hostname: window.location.hostname, - selectors: selectors - }); - } - }; - - var requests = []; - var tagNames = ['embed','iframe','img','object']; - var elementIndex = 0; - var tagName, elems, i, elem, prop, src; - while ( tagName = tagNames.pop() ) { - elems = document.getElementsByTagName(tagName); - i = elems.length; - while ( i-- ) { - elem = elems[i]; - prop = srcProps[tagName]; - if ( prop === undefined ) { - continue; - } - src = elem[prop]; - if ( typeof src !== 'string' || src === '' ) { - continue; - } - if ( src.lastIndexOf('http', 0) !== 0 ) { - continue; - } - requests.push({ - index: elementIndex, - tagName: tagName, - url: src - }); - elements[elementIndex] = elem; - elementIndex += 1; + elems = document.querySelectorAll('img'); + i = elems.length; + while ( i-- ) { + elem = elems[i]; + if ( elem.complete ) { + collapser.add(elem); } } - var details = { - what: 'filterRequests', - pageURL: window.location.href, - pageHostname: window.location.hostname, - requests: requests - }; - messager.send(details, onAnswerReceived); + + elems = document.querySelectorAll('iframe'); + i = elems.length; + while ( i-- ) { + collapser.addIFrame(elems[i]); + } + collapser.process(0); })(); /******************************************************************************/ diff --git a/src/js/messaging.js b/src/js/messaging.js index 82f8e7e8a..05b9ce9ea 100644 --- a/src/js/messaging.js +++ b/src/js/messaging.js @@ -435,60 +435,42 @@ var tagNameToRequestTypeMap = { // Evaluate many requests var filterRequests = function(pageStore, details) { + var requests = details.requests; + if ( !pageStore || !pageStore.getNetFilteringSwitch() ) { + return requests; + } + if ( µb.userSettings.collapseBlocked === false ) { + return requests; + } + + //console.debug('messaging.js/contentscript-end.js: processing %d requests', requests.length); + var µburi = µb.URI; var isBlockResult = µb.isBlockResult; // Create evaluation context - details.pageHostname = vAPI.punycodeHostname(details.pageHostname); - details.pageDomain = µburi.domainFromHostname(details.pageHostname); - details.rootHostname = pageStore.rootHostname; - details.rootDomain = pageStore.rootDomain; - details.requestHostname = ''; - - var inRequests = details.requests; - var outRequests = []; - var request; - var i = inRequests.length; - while ( i-- ) { - request = inRequests[i]; - if ( tagNameToRequestTypeMap.hasOwnProperty(request.tagName) === false ) { - continue; - } - details.requestURL = vAPI.punycodeURL(request.url); - details.requestHostname = µburi.hostnameFromURI(details.requestURL); - details.requestType = tagNameToRequestTypeMap[request.tagName]; - if ( isBlockResult(pageStore.filterRequest(details)) ) { - outRequests.push(request); - } - } - return { - collapse: µb.userSettings.collapseBlocked, - requests: outRequests + var context = { + pageHostname: vAPI.punycodeHostname(details.pageHostname), + pageDomain: µburi.domainFromHostname(details.pageHostname), + rootHostname: pageStore.rootHostname, + rootDomain: pageStore.rootDomain, + requestURL: '', + requestHostname: '', + requestType: '' }; -}; -/******************************************************************************/ - -// Evaluate a single request - -var filterRequest = function(pageStore, details) { - if ( tagNameToRequestTypeMap.hasOwnProperty(details.tagName) === false ) { - return; - } - var µburi = µb.URI; - details.pageHostname = vAPI.punycodeHostname(details.pageHostname); - details.pageDomain = µburi.domainFromHostname(details.pageHostname); - details.rootHostname = pageStore.rootHostname; - details.rootDomain = pageStore.rootDomain; - details.requestURL = vAPI.punycodeURL(details.requestURL); - details.requestHostname = µburi.hostnameFromURI(details.requestURL); - details.requestType = tagNameToRequestTypeMap[details.tagName]; - if ( µb.isBlockResult(pageStore.filterRequest(details)) ) { - return { - collapse: µb.userSettings.collapseBlocked, - id: details.id - }; + var request; + var i = requests.length; + while ( i-- ) { + request = requests[i]; + context.requestURL = vAPI.punycodeURL(request.url); + context.requestHostname = µburi.hostnameFromURI(request.url); + context.requestType = tagNameToRequestTypeMap[request.tagName]; + if ( isBlockResult(pageStore.filterRequest(context)) ) { + request.collapse = true; + } } + return requests; }; /******************************************************************************/ @@ -521,20 +503,7 @@ var onMessage = function(details, sender, callback) { // Evaluate many requests case 'filterRequests': - if ( pageStore && pageStore.getNetFilteringSwitch() ) { - response = filterRequests(pageStore, details); - } - break; - - // Evaluate a single request - case 'filterRequest': - if ( pageStore && pageStore.getNetFilteringSwitch() ) { - // console.log('contentscript-end.js > filterRequest(%o)', details); - response = filterRequest(pageStore, details); - } - if ( response === undefined ) { - response = { id: details.id }; - } + response = filterRequests(pageStore, details); break; default: