/******************************************************************************* uBlock Origin - a browser extension to block requests. Copyright (C) 2014-2017 Raymond Hill This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see {http://www.gnu.org/licenses/}. Home: https://github.com/gorhill/uBlock */ 'use strict'; /******************************************************************************* +--> [[domSurveyor] --> domFilterer] domWatcher--| +--> [domCollapser] domWatcher: Watches for changes in the DOM, and notify the other components about these changes. domCollapser: Enforces the collapsing of DOM elements for which a corresponding resource was blocked through network filtering. domFilterer: Enforces the filtering of DOM elements, by feeding it cosmetic filters. domSurveyor: Surveys the DOM to find new cosmetic filters to apply to the current page. If page is whitelisted: - domWatcher: off - domCollapser: off - domFilterer: off - domSurveyor: off I verified that the code in this file is completely flushed out of memory when a page is whitelisted. If cosmetic filtering is disabled: - domWatcher: on - domCollapser: on - domFilterer: off - domSurveyor: off If generic cosmetic filtering is disabled: - domWatcher: on - domCollapser: on - domFilterer: on - domSurveyor: off Additionally, the domSurveyor can turn itself off once it decides that it has become pointless (repeatedly not finding new cosmetic filters). The domFilterer makes use of platform-dependent user styles[1] code, or provide a default generic implementation if none is present. At time of writing, only modern Firefox provides a custom implementation, which makes for solid, reliable and low overhead cosmetic filtering on Firefox. The generic implementation[2] performs as best as can be, but won't ever be as reliable as real user styles. [1] "user styles" refer to local CSS rules which have priority over, and can't be overriden by a web page's own CSS rules. [2] below, see platformUserCSS / platformHideNode / platformUnhideNode */ // Abort execution if our global vAPI object does not exist. // https://github.com/chrisaljoudi/uBlock/issues/456 // https://github.com/gorhill/uBlock/issues/2029 if ( typeof vAPI !== undefined ) { // >>>>>>>> start of HUGE-IF-BLOCK /******************************************************************************/ /******************************************************************************/ /******************************************************************************/ vAPI.matchesProp = (function() { var docElem = document.documentElement; if ( typeof docElem.matches !== 'function' ) { if ( typeof docElem.mozMatchesSelector === 'function' ) { return 'mozMatchesSelector'; } else if ( typeof docElem.webkitMatchesSelector === 'function' ) { return 'webkitMatchesSelector'; } else if ( typeof docElem.msMatchesSelector === 'function' ) { return 'msMatchesSelector'; } } return 'matches'; })(); /******************************************************************************/ /******************************************************************************/ /******************************************************************************/ // https://github.com/gorhill/uBlock/issues/2147 vAPI.SafeAnimationFrame = function(callback) { this.fid = this.tid = null; this.callback = callback; }; vAPI.SafeAnimationFrame.prototype.start = function() { if ( this.fid !== null ) { return; } this.fid = requestAnimationFrame(this.callback); this.tid = vAPI.setTimeout(this.callback, 1200000); }; vAPI.SafeAnimationFrame.prototype.clear = function() { if ( this.fid === null ) { return; } cancelAnimationFrame(this.fid); clearTimeout(this.tid); this.fid = this.tid = null; }; /******************************************************************************/ /******************************************************************************/ /******************************************************************************/ vAPI.injectScriptlet = function(doc, text) { if ( !doc ) { return; } try { var script = doc.createElement('script'); script.appendChild(doc.createTextNode(text)); (doc.head || doc.documentElement).appendChild(script); } catch (ex) { } }; /******************************************************************************/ /******************************************************************************/ /******************************************************************************/ // The DOM filterer is the heart of uBO's cosmetic filtering. vAPI.domFilterer = (function() { /******************************************************************************/ var allExceptions = new Set(), allSelectors = new Set(), stagedNodes = []; // Complex selectors, due to their nature may need to be "de-committed". A // Set() is used to implement this functionality. var complexSelectorsOldResultSet, complexSelectorsCurrentResultSet = new Set(); /******************************************************************************/ var cosmeticFiltersActivatedTimer = null; var cosmeticFiltersActivated = function() { cosmeticFiltersActivatedTimer = null; vAPI.messaging.send( 'contentscript', { what: 'cosmeticFiltersActivated' } ); }; /******************************************************************************/ // If a platform does not support its own vAPI.userCSS (user styles), we // provide a default (imperfect) implementation. // Probably no longer need to watch for style tags removal/tampering with fix // to https://github.com/gorhill/uBlock/issues/963 // https://github.com/gorhill/uBlock/issues/2810 // With Firefox Nightly, it may happens style tags are injected before the // head element is present. var platformUserCSS = (function() { if ( vAPI.userCSS instanceof Object ) { return vAPI.userCSS; } return { enabled: true, styles: [], add: function(css) { var style = document.createElement('style'); style.setAttribute('type', 'text/css'); style.textContent = css; var parent = document.head || document.documentElement; if ( parent !== null ) { parent.appendChild(style); } this.styles.push(style); if ( style.sheet ) { style.sheet.disabled = !this.enabled; } }, remove: function(css) { var i = this.styles.length, style, parent; while ( i-- ) { style = this.styles[i]; if ( style.textContent !== css ) { continue; } parent = style.parentNode; if ( parent !== null ) { parent.removeChild(style); } this.styles.splice(i, 1); } }, toggle: function(state) { if ( this.styles.length === '' ) { return; } if ( state === undefined ) { state = !this.enabled; } var i = this.styles.length, style; while ( i-- ) { style = this.styles[i]; if ( style.sheet !== null ) { style.sheet.disabled = !state; } } this.enabled = state; } }; })(); // If a platform does not provide its own (improved) vAPI.hideNode, we assign // a default one to try to override author styles as best as can be. var platformHideNode = vAPI.hideNode, platformUnhideNode = vAPI.unhideNode; (function() { if ( platformHideNode instanceof Function ) { return; } var uid, timer, observer, changedNodes = new Set(), observerOptions = { attributes: true, attributeFilter: [ 'style' ] }; // https://jsperf.com/clientheight-and-clientwidth-vs-getcomputedstyle // Avoid getComputedStyle(), detecting whether a node is visible can be // achieved with clientWidth/clientHeight. // https://gist.github.com/paulirish/5d52fb081b3570c81e3a // Do not interleave read-from/write-to the DOM. Write-to DOM // operations would cause the first read-from to be expensive, and // interleaving means that potentially all single read-from operation // would be expensive rather than just the 1st one. // Benchmarking toggling off/on cosmetic filtering confirms quite an // improvement when: // - batching as much as possible handling of all nodes; // - avoiding to interleave read-from/write-to operations. // However, toggling off/on cosmetic filtering repeatedly is not // a real use case, but this shows this will help performance // on sites which try to use inline styles to bypass blockers. var batchProcess = function() { timer.clear(); var uid_ = uid; for ( var node of changedNodes ) { if ( node[uid_] === undefined || node.clientHeight === 0 || node.clientWidth === 0 ) { continue; } var attr = node.getAttribute('style'); if ( attr === null ) { attr = ''; } else if ( attr.length !== 0 && attr.charCodeAt(attr.length - 1) !== 0x3B /* ';' */ ) { attr += '; '; } node.setAttribute('style', attr + 'display: none !important;'); } changedNodes.clear(); }; var observerHandler = function(mutations) { var i = mutations.length, changedNodes_ = changedNodes; while ( i-- ) { changedNodes_.add(mutations[i].target); } timer.start(); }; platformHideNode = function(node) { if ( uid === undefined ) { uid = vAPI.randomToken(); timer = new vAPI.SafeAnimationFrame(batchProcess); } if ( node[uid] === undefined ) { node[uid] = node.hasAttribute('style') && (node.getAttribute('style') || ''); } // Performance: batch-process nodes to hide. changedNodes.add(node); timer.start(); if ( observer === undefined ) { observer = new MutationObserver(observerHandler); } observer.observe(node, observerOptions); }; platformUnhideNode = function(node) { if ( uid === undefined ) { return; } var attr = node[uid]; if ( attr === false ) { node.removeAttribute('style'); } else if ( typeof attr === 'string' ) { node.setAttribute('style', attr); } delete node[uid]; }; })(); /******************************************************************************/ // 'P' stands for 'Procedural' var PSelectorHasTask = function(task) { this.selector = task[1]; }; PSelectorHasTask.prototype.exec = function(input) { var output = []; for ( var i = 0, n = input.length; i < n; i++ ) { if ( input[i].querySelector(this.selector) !== null ) { output.push(input[i]); } } return output; }; var PSelectorHasTextTask = function(task) { this.needle = new RegExp(task[1]); }; PSelectorHasTextTask.prototype.exec = function(input) { var output = []; for ( var i = 0, n = input.length; i < n; i++ ) { if ( this.needle.test(input[i].textContent) ) { output.push(input[i]); } } return output; }; var PSelectorIfTask = function(task) { this.pselector = new PSelector(task[1]); }; PSelectorIfTask.prototype.target = true; PSelectorIfTask.prototype.exec = function(input) { var output = []; for ( var i = 0, n = input.length; i < n; i++ ) { if ( this.pselector.test(input[i]) === this.target ) { output.push(input[i]); } } return output; }; var PSelectorIfNotTask = function(task) { PSelectorIfTask.call(this, task); this.target = false; }; PSelectorIfNotTask.prototype = Object.create(PSelectorIfTask.prototype); PSelectorIfNotTask.prototype.constructor = PSelectorIfNotTask; var PSelectorMatchesCSSTask = function(task) { this.name = task[1].name; this.value = new RegExp(task[1].value); }; PSelectorMatchesCSSTask.prototype.pseudo = null; PSelectorMatchesCSSTask.prototype.exec = function(input) { var output = [], style; for ( var i = 0, n = input.length; i < n; i++ ) { style = window.getComputedStyle(input[i], this.pseudo); if ( style === null ) { return null; } /* FF */ if ( this.value.test(style[this.name]) ) { output.push(input[i]); } } return output; }; var PSelectorMatchesCSSAfterTask = function(task) { PSelectorMatchesCSSTask.call(this, task); this.pseudo = ':after'; }; PSelectorMatchesCSSAfterTask.prototype = Object.create(PSelectorMatchesCSSTask.prototype); PSelectorMatchesCSSAfterTask.prototype.constructor = PSelectorMatchesCSSAfterTask; var PSelectorMatchesCSSBeforeTask = function(task) { PSelectorMatchesCSSTask.call(this, task); this.pseudo = ':before'; }; PSelectorMatchesCSSBeforeTask.prototype = Object.create(PSelectorMatchesCSSTask.prototype); PSelectorMatchesCSSBeforeTask.prototype.constructor = PSelectorMatchesCSSBeforeTask; var PSelectorXpathTask = function(task) { this.xpe = document.createExpression(task[1], null); this.xpr = null; }; PSelectorXpathTask.prototype.exec = function(input) { var output = [], j, node; for ( var i = 0, n = input.length; i < n; i++ ) { this.xpr = this.xpe.evaluate( input[i], XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, this.xpr ); j = this.xpr.snapshotLength; while ( j-- ) { node = this.xpr.snapshotItem(j); if ( node.nodeType === 1 ) { output.push(node); } } } return output; }; var PSelector = function(o) { if ( PSelector.prototype.operatorToTaskMap === undefined ) { PSelector.prototype.operatorToTaskMap = new Map([ [ ':has', PSelectorHasTask ], [ ':has-text', PSelectorHasTextTask ], [ ':if', PSelectorIfTask ], [ ':if-not', PSelectorIfNotTask ], [ ':matches-css', PSelectorMatchesCSSTask ], [ ':matches-css-after', PSelectorMatchesCSSAfterTask ], [ ':matches-css-before', PSelectorMatchesCSSBeforeTask ], [ ':xpath', PSelectorXpathTask ] ]); } this.raw = o.raw; this.selector = o.selector; this.tasks = []; var tasks = o.tasks; if ( !tasks ) { return; } for ( var i = 0, task, ctor; i < tasks.length; i++ ) { task = tasks[i]; ctor = this.operatorToTaskMap.get(task[0]); this.tasks.push(new ctor(task)); } }; PSelector.prototype.operatorToTaskMap = undefined; PSelector.prototype.prime = function(input) { var root = input || document; if ( this.selector !== '' ) { return root.querySelectorAll(this.selector); } return [ root ]; }; PSelector.prototype.exec = function(input) { //var t0 = window.performance.now(); var tasks = this.tasks, nodes = this.prime(input); for ( var i = 0, n = tasks.length; i < n && nodes.length !== 0; i++ ) { nodes = tasks[i].exec(nodes); } //console.log('%s: %s ms', this.raw, (window.performance.now() - t0).toFixed(2)); return nodes; }; PSelector.prototype.test = function(input) { //var t0 = window.performance.now(); var tasks = this.tasks, nodes = this.prime(input), AA = [ null ], aa; for ( var i = 0, ni = nodes.length; i < ni; i++ ) { AA[0] = nodes[i]; aa = AA; for ( var j = 0, nj = tasks.length; j < nj && aa.length !== 0; j++ ) { aa = tasks[j].exec(aa); } if ( aa.length !== 0 ) { return true; } } //console.log('%s: %s ms', this.raw, (window.performance.now() - t0).toFixed(2)); return false; }; /******************************************************************************/ var domFilterer = { addedNodesHandlerMissCount: 0, commitTimer: null, disabledId: vAPI.randomToken(), enabled: true, excludeId: undefined, hiddenId: vAPI.randomToken(), hiddenNodeCount: 0, hiddenNodeEnforcer: false, loggerEnabled: undefined, newHideSelectorBuffer: [], // Hide style filter buffer newStyleRuleBuffer: [], // Non-hide style filter buffer simpleHideSelectors: { // Hiding filters: simple selectors entries: [], matchesProp: vAPI.matchesProp, selector: undefined, add: function(selector) { this.entries.push(selector); this.selector = undefined; }, forEachNode: function(callback, root, extra) { if ( this.selector === undefined ) { this.selector = this.entries.join(extra + ',') + extra; } if ( root[this.matchesProp](this.selector) ) { callback(root); } var nodes = root.querySelectorAll(this.selector), i = nodes.length; while ( i-- ) { callback(nodes[i]); } } }, complexHideSelectors: { // Hiding filters: complex selectors entries: [], selector: undefined, add: function(selector) { this.entries.push(selector); this.selector = undefined; }, forEachNode: function(callback) { if ( this.selector === undefined ) { this.selector = this.entries.join(','); } var nodes = document.querySelectorAll(this.selector), i = nodes.length; while ( i-- ) { callback(nodes[i]); } } }, nqsSelectors: [], // Non-querySelector-able filters proceduralSelectors: { // Hiding filters: procedural entries: [], add: function(o) { this.entries.push(new PSelector(o)); }, forEachNode: function(callback) { var pfilters = this.entries, i = pfilters.length, pfilter, nodes, j; while ( i-- ) { pfilter = pfilters[i]; nodes = pfilter.exec(); j = nodes.length; while ( j-- ) { callback(nodes[j], pfilter); } } } }, addExceptions: function(aa) { for ( var i = 0, n = aa.length; i < n; i++ ) { allExceptions.add(aa[i]); } }, addSelector: function(selector) { if ( allSelectors.has(selector) || allExceptions.has(selector) ) { return; } allSelectors.add(selector); if ( selector.charCodeAt(0) !== 0x7B /* '{' */ ) { this.newHideSelectorBuffer.push(selector); if ( selector.indexOf(' ') === -1 ) { this.simpleHideSelectors.add(selector); } else { this.complexHideSelectors.add(selector); } return; } var o = JSON.parse(selector); if ( o.style ) { this.newStyleRuleBuffer.push(o.style.join(' ')); this.nqsSelectors.push(o.raw); return; } if ( o.pseudoclass ) { this.newHideSelectorBuffer.push(o.raw); this.nqsSelectors.push(o.raw); return; } if ( o.tasks ) { this.proceduralSelectors.add(o); return; } }, addSelectors: function(aa) { for ( var i = 0, n = aa.length; i < n; i++ ) { this.addSelector(aa[i]); } }, commit_: function() { this.commitTimer.clear(); var beforeHiddenNodeCount = this.hiddenNodeCount, styleText = ''; // CSS rules/hide if ( this.newHideSelectorBuffer.length ) { styleText = '\n:root ' + this.newHideSelectorBuffer.join(',\n:root ') + '\n{ display: none !important; }'; this.newHideSelectorBuffer.length = 0; } // CSS rules/any css declaration if ( this.newStyleRuleBuffer.length ) { styleText += '\n' + this.newStyleRuleBuffer.join('\n'); this.newStyleRuleBuffer.length = 0; } // Simple selectors: incremental. // Simple css selectors/hide if ( this.simpleHideSelectors.entries.length ) { var i = stagedNodes.length; while ( i-- ) { this.simpleHideSelectors.forEachNode(hideNode, stagedNodes[i], cssNotHiddenId); } } stagedNodes = []; // Complex selectors: non-incremental. complexSelectorsOldResultSet = complexSelectorsCurrentResultSet; complexSelectorsCurrentResultSet = new Set(); // Complex css selectors/hide // The handling of these can be considered optional, since they are // also applied declaratively using a style tag. if ( this.complexHideSelectors.entries.length ) { this.complexHideSelectors.forEachNode(complexHideNode); } // Procedural cosmetic filters if ( this.proceduralSelectors.entries.length ) { this.proceduralSelectors.forEachNode(complexHideNode); } // https://github.com/gorhill/uBlock/issues/1912 // If one or more nodes have been manually hidden, insert a style tag // targeting these manually hidden nodes. For browsers supporting // user styles, this allows uBO to win. var commitHit = this.hiddenNodeCount !== beforeHiddenNodeCount; if ( commitHit ) { if ( this.hiddenNodeEnforcer === false ) { styleText += '\n:root *[' + this.hiddenId + '][hidden] { display: none !important; }'; this.hiddenNodeEnforcer = true; } this.addedNodesHandlerMissCount = 0; } else { this.addedNodesHandlerMissCount += 1; } if ( styleText !== '' ) { platformUserCSS.add(styleText); } // Un-hide nodes previously hidden. for ( var node of complexSelectorsOldResultSet ) { this.unhideNode(node); } complexSelectorsOldResultSet.clear(); // If DOM nodes have been affected, lazily notify core process. if ( this.loggerEnabled !== false && commitHit && cosmeticFiltersActivatedTimer === null ) { cosmeticFiltersActivatedTimer = vAPI.setTimeout( cosmeticFiltersActivated, 503 ); } }, commit: function(nodes, commitNow) { if ( nodes === 'all' ) { stagedNodes = [ document.documentElement ]; } else if ( stagedNodes[0] !== document.documentElement ) { stagedNodes = stagedNodes.concat(nodes); } if ( commitNow ) { this.commitTimer.clear(); this.commit_(); return; } this.commitTimer.start(); }, createProceduralFilter: function(o) { return new PSelector(o); }, getExcludeId: function() { if ( this.excludeId === undefined ) { this.excludeId = vAPI.randomToken(); } return this.excludeId; }, hideNode: function(node) { if ( node[this.hiddenId] !== undefined ) { return; } if ( this.excludeId !== undefined && node[this.excludeId] ) { return; } node.setAttribute(this.hiddenId, ''); this.hiddenNodeCount += 1; node.hidden = true; node[this.hiddenId] = null; platformHideNode(node); }, init: function() { this.commitTimer = new vAPI.SafeAnimationFrame(this.commit_.bind(this)); }, showNode: function(node) { node.hidden = false; platformUnhideNode(node); }, toggleLogging: function(state) { this.loggerEnabled = state; }, toggleOff: function() { platformUserCSS.toggle(false); this.enabled = false; }, toggleOn: function() { platformUserCSS.toggle(true); this.enabled = true; }, userCSS: platformUserCSS, unhideNode: function(node) { if ( node[this.hiddenId] !== undefined ) { this.hiddenNodeCount--; } node.removeAttribute(this.hiddenId); node[this.hiddenId] = undefined; node.hidden = false; platformUnhideNode(node); }, unshowNode: function(node) { node.hidden = true; platformHideNode(node); }, domChangedHandler: function(addedNodes) { this.commit(addedNodes); }, start: function() { var domChangedHandler = this.domChangedHandler.bind(this); vAPI.domWatcher.addListener(domChangedHandler); vAPI.shutdown.add(function() { vAPI.domWatcher.removeListener(domChangedHandler); }); } }; /******************************************************************************/ var hideNode = domFilterer.hideNode.bind(domFilterer); var complexHideNode = function(node) { complexSelectorsCurrentResultSet.add(node); if ( !complexSelectorsOldResultSet.delete(node) ) { hideNode(node); } }; var cssNotHiddenId = ':not([' + domFilterer.hiddenId + '])'; domFilterer.init(); /******************************************************************************/ return domFilterer; /******************************************************************************/ })(); /******************************************************************************/ /******************************************************************************/ /******************************************************************************/ // This is executed once, and since no hooks are left behind once the response // is received, I expect this code to be garbage collected by the browser. (function domIsLoading() { var responseHandler = function(response) { // cosmetic filtering engine aka 'cfe' var cfeDetails = response && response.specificCosmeticFilters; if ( !cfeDetails || !cfeDetails.ready ) { vAPI.domWatcher = vAPI.domCollapser = vAPI.domFilterer = vAPI.domSurveyor = vAPI.domIsLoaded = null; return; } if ( response.noCosmeticFiltering ) { vAPI.domFilterer = null; vAPI.domSurveyor = null; } else { var domFilterer = vAPI.domFilterer; domFilterer.toggleLogging(response.loggerEnabled); if ( response.noGenericCosmeticFiltering || cfeDetails.noDOMSurveying ) { vAPI.domSurveyor = null; } if ( cfeDetails.cosmeticHide.length !== 0 || cfeDetails.cosmeticDonthide.length !== 0 ) { domFilterer.addExceptions(cfeDetails.cosmeticDonthide); domFilterer.addSelectors(cfeDetails.cosmeticHide); domFilterer.commit('all', true); } } var parent = document.head || document.documentElement; if ( parent ) { var elem, text; if ( cfeDetails.netHide.length !== 0 ) { elem = document.createElement('style'); elem.setAttribute('type', 'text/css'); text = cfeDetails.netHide.join(',\n'); text += response.collapseBlocked ? '\n{display:none !important;}' : '\n{visibility:hidden !important;}'; elem.appendChild(document.createTextNode(text)); parent.appendChild(elem); } // Library of resources is located at: // https://github.com/gorhill/uBlock/blob/master/assets/ublock/resources.txt if ( cfeDetails.scripts ) { // Have the injected script tag remove itself when execution completes: // to keep DOM as clean as possible. text = cfeDetails.scripts + "\n" + "(function() {\n" + " var c = document.currentScript,\n" + " p = c && c.parentNode;\n" + " if ( p ) {\n" + " p.removeChild(c);\n" + " }\n" + "})();"; vAPI.injectScriptlet(document, text); vAPI.injectedScripts = text; } } // https://github.com/chrisaljoudi/uBlock/issues/587 // If no filters were found, maybe the script was injected before // uBlock's process was fully initialized. When this happens, pages // won't be cleaned right after browser launch. if ( document.readyState !== 'loading' ) { (new vAPI.SafeAnimationFrame(vAPI.domIsLoaded)).start(); } else { document.addEventListener('DOMContentLoaded', vAPI.domIsLoaded); } }; var url = window.location.href; vAPI.messaging.send( 'contentscript', { what: 'retrieveContentScriptParameters', pageURL: url, locationURL: url }, responseHandler ); })(); /******************************************************************************/ /******************************************************************************/ /******************************************************************************/ vAPI.domWatcher = (function() { var domLayoutObserver = null, ignoreTags = new Set([ 'head', 'link', 'meta', 'script', 'style' ]), addedNodeLists = [], addedNodes = [], removedNodes = false, listeners = []; var safeObserverHandler = function() { safeObserverHandlerTimer.clear(); var i = addedNodeLists.length, j = addedNodes.length, nodeList, iNode, node; while ( i-- ) { nodeList = addedNodeLists[i]; iNode = nodeList.length; while ( iNode-- ) { node = nodeList[iNode]; if ( node.nodeType === 1 && ignoreTags.has(node.localName) === false && node.parentElement !== null ) { addedNodes[j++] = node; } } } addedNodeLists.length = 0; if ( j === 0 && removedNodes === false ) { return; } listeners[0](addedNodes); if ( listeners[1] ) { listeners[1](addedNodes); } addedNodes.length = 0; removedNodes = false; }; var safeObserverHandlerTimer = new vAPI.SafeAnimationFrame(safeObserverHandler); // https://github.com/chrisaljoudi/uBlock/issues/205 // Do not handle added node directly from within mutation observer. var observerHandler = function(mutations) { var nodeList, mutation, i = mutations.length; while ( i-- ) { mutation = mutations[i]; nodeList = mutation.addedNodes; if ( nodeList.length !== 0 ) { addedNodeLists.push(nodeList); } if ( mutation.removedNodes.length !== 0 ) { removedNodes = true; } } if ( addedNodeLists.length !== 0 || removedNodes ) { safeObserverHandlerTimer.start(); } }; var addListener = function(listener) { if ( listeners.indexOf(listener) !== -1 ) { return; } listeners.push(listener); if ( domLayoutObserver !== null ) { return; } domLayoutObserver = new MutationObserver(observerHandler); domLayoutObserver.observe(document.documentElement, { //attributeFilter: [ 'class', 'id' ], //attributes: true, childList: true, subtree: true }); }; var removeListener = function(listener) { var pos = listeners.indexOf(listener); if ( pos === -1 ) { return; } listeners.splice(pos, 1); if ( listeners.length !== 0 || domLayoutObserver === null ) { return; } domLayoutObserver.disconnect(); domLayoutObserver = null; }; var start = function() { vAPI.shutdown.add(function() { if ( domLayoutObserver !== null ) { domLayoutObserver.disconnect(); domLayoutObserver = null; } safeObserverHandlerTimer.clear(); }); }; return { addListener: addListener, removeListener: removeListener, start: start }; })(); /******************************************************************************/ /******************************************************************************/ /******************************************************************************/ vAPI.domCollapser = (function() { var resquestIdGenerator = 1, processTimer, toProcess = [], toFilter = [], toCollapse = new Map(), cachedBlockedSet, cachedBlockedSetHash, cachedBlockedSetTimer; var src1stProps = { 'embed': 'src', 'iframe': 'src', 'img': 'src', 'object': 'data' }; var src2ndProps = { 'img': 'srcset' }; var tagToTypeMap = { embed: 'object', iframe: 'sub_frame', img: 'image', object: 'object' }; var netSelectorCacheCount = 0, messaging = vAPI.messaging; var cachedBlockedSetClear = function() { cachedBlockedSet = cachedBlockedSetHash = cachedBlockedSetTimer = undefined; }; // https://github.com/chrisaljoudi/uBlock/issues/174 // Do not remove fragment from src URL var onProcessed = function(response) { if ( !response ) { // This happens if uBO is disabled or restarted. toCollapse.clear(); return; } var targets = toCollapse.get(response.id); if ( targets === undefined ) { return; } toCollapse.delete(response.id); if ( cachedBlockedSetHash !== response.hash ) { cachedBlockedSet = new Set(response.blockedResources); cachedBlockedSetHash = response.hash; if ( cachedBlockedSetTimer !== undefined ) { clearTimeout(cachedBlockedSetTimer); } cachedBlockedSetTimer = vAPI.setTimeout(cachedBlockedSetClear, 30000); } if ( cachedBlockedSet === undefined || cachedBlockedSet.size === 0 ) { return; } var selectors = [], iframeLoadEventPatch = vAPI.iframeLoadEventPatch, netSelectorCacheCountMax = response.netSelectorCacheCountMax, tag, prop, src, value; 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; } } if ( cachedBlockedSet.has(tagToTypeMap[tag] + ' ' + src) === false ) { continue; } // https://github.com/chrisaljoudi/uBlock/issues/399 // Never remove elements from the DOM, just hide them target.style.setProperty('display', 'none', 'important'); target.hidden = true; // https://github.com/chrisaljoudi/uBlock/issues/1048 // Use attribute to construct CSS rule if ( netSelectorCacheCount <= netSelectorCacheCountMax && (value = target.getAttribute(prop)) ) { selectors.push(tag + '[' + prop + '="' + value + '"]'); netSelectorCacheCount += 1; } if ( iframeLoadEventPatch !== undefined ) { iframeLoadEventPatch(target); } } if ( selectors.length !== 0 ) { messaging.send( 'contentscript', { what: 'cosmeticFiltersInjected', type: 'net', hostname: window.location.hostname, selectors: selectors } ); } }; var send = function() { processTimer = undefined; toCollapse.set(resquestIdGenerator, toProcess); // https://github.com/gorhill/uBlock/issues/1927 // Normalize hostname to avoid trailing dot of FQHN. var pageHostname = window.location.hostname || ''; if ( pageHostname.length && pageHostname.charCodeAt(pageHostname.length - 1) === 0x2e ) { pageHostname = pageHostname.slice(0, -1); } var msg = { what: 'getCollapsibleBlockedRequests', id: resquestIdGenerator, pageURL: window.location.href, pageHostname: pageHostname, resources: toFilter, hash: cachedBlockedSetHash }; messaging.send('contentscript', msg, onProcessed); toProcess = []; toFilter = []; resquestIdGenerator += 1; }; var process = function(delay) { if ( toProcess.length === 0 ) { return; } if ( delay === 0 ) { if ( processTimer !== undefined ) { clearTimeout(processTimer); } send(); } else if ( processTimer === undefined ) { processTimer = vAPI.setTimeout(send, delay || 20); } }; var add = function(target) { toProcess[toProcess.length] = target; }; var addMany = function(targets) { var i = targets.length; while ( i-- ) { add(targets[i]); } }; var iframeSourceModified = function(mutations) { var i = mutations.length; while ( i-- ) { addIFrame(mutations[i].target, true); } process(); }; var iframeSourceObserver = new MutationObserver(iframeSourceModified); var iframeSourceObserverOptions = { attributes: true, attributeFilter: [ 'src' ] }; var primeLocalIFrame = function(iframe) { // Should probably also copy injected styles. // The injected scripts are those which were injected in the current // document, from within the `contentscript-start.js / injectScripts`, // and which scripts are selectively looked-up from: // https://github.com/gorhill/uBlock/blob/master/assets/ublock/resources.txt if ( vAPI.injectedScripts ) { vAPI.injectScriptlet(iframe.contentDocument, vAPI.injectedScripts); } }; 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 ) { iframeSourceObserver.observe(iframe, iframeSourceObserverOptions); } var src = iframe.src; if ( src === '' || typeof src !== 'string' ) { primeLocalIFrame(iframe); return; } if ( src.lastIndexOf('http', 0) !== 0 ) { return; } toFilter[toFilter.length] = { type: 'sub_frame', url: iframe.src }; add(iframe); }; var addIFrames = function(iframes) { var i = iframes.length; while ( i-- ) { addIFrame(iframes[i]); } }; var onResourceFailed = function(ev) { if ( tagToTypeMap[ev.target.localName] !== undefined ) { vAPI.domCollapser.add(ev.target); vAPI.domCollapser.process(); } }; var domChangedHandler = function(nodes) { var node; for ( var i = 0, ni = nodes.length; i < ni; i++ ) { node = nodes[i]; if ( node.localName === 'iframe' ) { addIFrame(node); } if ( node.childElementCount !== 0 ) { var iframes = node.getElementsByTagName('iframe'); if ( iframes.length !== 0 ) { addIFrames(iframes); } } } process(); }; var start = function() { // Listener to collapse blocked resources. // - Future requests not blocked yet // - Elements dynamically added to the page // - Elements which resource URL changes // https://github.com/chrisaljoudi/uBlock/issues/7 // Preferring getElementsByTagName over querySelectorAll: // http://jsperf.com/queryselectorall-vs-getelementsbytagname/145 var elems = document.images || document.getElementsByTagName('img'), i = elems.length, elem; while ( i-- ) { elem = elems[i]; if ( elem.complete ) { add(elem); } } addMany(document.embeds || document.getElementsByTagName('embed')); addMany(document.getElementsByTagName('object')); addIFrames(document.getElementsByTagName('iframe')); process(0); document.addEventListener('error', onResourceFailed, true); vAPI.domWatcher.addListener(domChangedHandler); vAPI.shutdown.add(function() { document.removeEventListener('error', onResourceFailed, true); vAPI.domWatcher.removeListener(domChangedHandler); if ( processTimer !== undefined ) { clearTimeout(processTimer); } }); }; return { add: add, addMany: addMany, addIFrame: addIFrame, addIFrames: addIFrames, process: process, start: start }; })(); /******************************************************************************/ /******************************************************************************/ /******************************************************************************/ vAPI.domSurveyor = (function() { var domFilterer = null, messaging = vAPI.messaging, surveyPhase3Nodes = [], cosmeticSurveyingMissCount = 0, highGenerics = null, lowGenericSelectors = [], queriedSelectors = new Set(), surveyCost = 0; // Handle main process' response. var surveyPhase3 = function(response) { var result = response && response.result, firstSurvey = highGenerics === null; if ( result ) { if ( result.hide.length ) { processLowGenerics(result.hide); } if ( result.highGenerics ) { highGenerics = result.highGenerics; } } if ( highGenerics ) { var t0 = window.performance.now(); if ( highGenerics.hideLowCount ) { processHighLowGenerics(highGenerics.hideLow); } if ( highGenerics.hideMediumCount ) { processHighMediumGenerics(highGenerics.hideMedium); } if ( highGenerics.hideHighSimpleCount || highGenerics.hideHighComplexCount ) { processHighHighGenerics(); } surveyCost += window.performance.now() - t0; } // Need to do this before committing DOM filterer, as needed info // will no longer be there after commit. if ( firstSurvey || domFilterer.newHideSelectorBuffer.length ) { messaging.send( 'contentscript', { what: 'cosmeticFiltersInjected', type: 'cosmetic', hostname: window.location.hostname, selectors: domFilterer.newHideSelectorBuffer, first: firstSurvey, cost: surveyCost } ); } // Shutdown surveyor if too many consecutive empty resultsets. if ( domFilterer.newHideSelectorBuffer.length === 0 ) { cosmeticSurveyingMissCount += 1; } else { cosmeticSurveyingMissCount = 0; } domFilterer.commit(surveyPhase3Nodes); surveyPhase3Nodes = []; }; // Query main process. var surveyPhase2 = function(addedNodes) { surveyPhase3Nodes = surveyPhase3Nodes.concat(addedNodes); if ( lowGenericSelectors.length !== 0 || highGenerics === null ) { messaging.send( 'contentscript', { what: 'retrieveGenericCosmeticSelectors', pageURL: window.location.href, selectors: lowGenericSelectors, firstSurvey: highGenerics === null }, surveyPhase3 ); lowGenericSelectors = []; } else { surveyPhase3(null); } }; // Low generics: // - [id] // - [class] var processLowGenerics = function(generics) { domFilterer.addSelectors(generics); }; // High-low generics: // - [alt="..."] // - [title="..."] var processHighLowGenerics = function(generics) { var attrs = ['title', 'alt']; var attr, attrValue, nodeList, iNode, node; var selector; while ( (attr = attrs.pop()) ) { nodeList = selectNodes('[' + attr + ']', surveyPhase3Nodes); iNode = nodeList.length; while ( iNode-- ) { node = nodeList[iNode]; attrValue = node.getAttribute(attr); if ( !attrValue ) { continue; } // Candidate 1 = generic form // If generic form is injected, no need to process the // specific form, as the generic will affect all related // specific forms. selector = '[' + attr + '="' + attrValue + '"]'; if ( generics.hasOwnProperty(selector) ) { domFilterer.addSelector(selector); continue; } // Candidate 2 = specific form selector = node.localName + selector; if ( generics.hasOwnProperty(selector) ) { domFilterer.addSelector(selector); } } } }; // High-medium generics: // - [href^="http"] var processHighMediumGenerics = function(generics) { var stagedNodes = surveyPhase3Nodes, i = stagedNodes.length; if ( i === 1 && stagedNodes[0] === document.documentElement ) { processHighMediumGenericsForNodes(document.links, generics); return; } var aa = [ null ], node, nodes; while ( i-- ) { node = stagedNodes[i]; if ( node.localName === 'a' ) { aa[0] = node; processHighMediumGenericsForNodes(aa, generics); } nodes = node.getElementsByTagName('a'); if ( nodes.length !== 0 ) { processHighMediumGenericsForNodes(nodes, generics); } } }; var processHighMediumGenericsForNodes = function(nodes, generics) { var i = nodes.length, node, href, pos, entry, j, selector; while ( i-- ) { node = nodes[i]; href = node.getAttribute('href'); if ( !href ) { continue; } pos = href.indexOf('://'); if ( pos === -1 ) { continue; } entry = generics[href.slice(pos + 3, pos + 11)]; if ( entry === undefined ) { continue; } if ( typeof entry === 'string' ) { if ( href.lastIndexOf(entry.slice(8, -2), 0) === 0 ) { domFilterer.addSelector(entry); } continue; } j = entry.length; while ( j-- ) { selector = entry[j]; if ( href.lastIndexOf(selector.slice(8, -2), 0) === 0 ) { domFilterer.addSelector(selector); } } } }; var highHighSimpleGenericsCost = 0, highHighSimpleGenericsInjected = false, highHighComplexGenericsCost = 0, highHighComplexGenericsInjected = false; var processHighHighGenerics = function() { var tstart; // Simple selectors. if ( highHighSimpleGenericsInjected === false && highHighSimpleGenericsCost < 50 && highGenerics.hideHighSimpleCount !== 0 ) { tstart = window.performance.now(); var matchesProp = vAPI.matchesProp, nodes = surveyPhase3Nodes, i = nodes.length, node; while ( i-- ) { node = nodes[i]; if ( node[matchesProp](highGenerics.hideHighSimple) || node.querySelector(highGenerics.hideHighSimple) !== null ) { highHighSimpleGenericsInjected = true; domFilterer.addSelectors(highGenerics.hideHighSimple.split(',\n')); break; } } highHighSimpleGenericsCost += window.performance.now() - tstart; } // Complex selectors. if ( highHighComplexGenericsInjected === false && highHighComplexGenericsCost < 50 && highGenerics.hideHighComplexCount !== 0 ) { tstart = window.performance.now(); if ( document.querySelector(highGenerics.hideHighComplex) !== null ) { highHighComplexGenericsInjected = true; domFilterer.addSelectors(highGenerics.hideHighComplex.split(',\n')); } highHighComplexGenericsCost += window.performance.now() - tstart; } }; // Extract and return the staged nodes which (may) match the selectors. var selectNodes = function(selector, nodes) { var stagedNodes = nodes, i = stagedNodes.length; if ( i === 1 && stagedNodes[0] === document.documentElement ) { return document.querySelectorAll(selector); } var targetNodes = [], node, nodeList, j; while ( i-- ) { node = stagedNodes[i]; targetNodes.push(node); nodeList = node.querySelectorAll(selector); j = nodeList.length; while ( j-- ) { targetNodes.push(nodeList[j]); } } return targetNodes; }; // Extract all classes/ids: these will be passed to the cosmetic // filtering engine, and in return we will obtain only the relevant // CSS selectors. // https://github.com/gorhill/uBlock/issues/672 // http://www.w3.org/TR/2014/REC-html5-20141028/infrastructure.html#space-separated-tokens // http://jsperf.com/enumerate-classes/6 var surveyPhase1 = function(addedNodes) { var t0 = window.performance.now(), rews = reWhitespace, qq = queriedSelectors, ll = lowGenericSelectors, lli = ll.length, nodes, i, node, v, vv, j; nodes = selectNodes('[id]', addedNodes); i = nodes.length; while ( i-- ) { node = nodes[i]; v = node.id; if ( typeof v !== 'string' ) { continue; } v = '#' + v.trim(); if ( qq.has(v) === false && v.length !== 1 ) { ll[lli] = v; lli++; qq.add(v); } } nodes = selectNodes('[class]', addedNodes); i = nodes.length; while ( i-- ) { node = nodes[i]; vv = node.className; if ( typeof vv !== 'string' ) { continue; } if ( rews.test(vv) === false ) { v = '.' + vv; if ( qq.has(v) === false && v.length !== 1 ) { ll[lli] = v; lli++; qq.add(v); } } else { vv = node.classList; j = vv.length; while ( j-- ) { v = '.' + vv[j]; if ( qq.has(v) === false ) { ll[lli] = v; lli++; qq.add(v); } } } } surveyCost += window.performance.now() - t0; surveyPhase2(addedNodes); }; var reWhitespace = /\s/; var domChangedHandler = function(addedNodes) { if ( cosmeticSurveyingMissCount > 255 ) { vAPI.domWatcher.removeListener(domChangedHandler); vAPI.domSurveyor = null; domFilterer.domChangedHandler(addedNodes); domFilterer.start(); return; } surveyPhase1(addedNodes); }; var start = function() { domFilterer = vAPI.domFilterer; domChangedHandler([ document.documentElement ]); vAPI.domWatcher.addListener(domChangedHandler); vAPI.shutdown.add(function() { vAPI.domWatcher.removeListener(domChangedHandler); }); }; return { start: start }; })(); /******************************************************************************/ /******************************************************************************/ /******************************************************************************/ vAPI.domIsLoaded = function(ev) { // This can happen on Firefox. For instance: // https://github.com/gorhill/uBlock/issues/1893 if ( window.location === null ) { return; } var slowLoad = ev instanceof Event; if ( slowLoad ) { document.removeEventListener('DOMContentLoaded', vAPI.domIsLoaded); } vAPI.domIsLoaded = null; vAPI.domWatcher.start(); vAPI.domCollapser.start(); if ( vAPI.domFilterer ) { // To avoid neddless CPU overhead, we commit existing cosmetic filters // only if the page loaded "slowly", i.e. if the code here had to wait // for a DOMContentLoaded event -- in which case the DOM may have // changed a lot since last time the domFilterer acted on it. if ( slowLoad ) { vAPI.domFilterer.commit('all'); } if ( vAPI.domSurveyor ) { vAPI.domSurveyor.start(); } else { vAPI.domFilterer.start(); } } // To send mouse coordinates to main process, as the chrome API fails // to provide the mouse position to context menu listeners. // https://github.com/chrisaljoudi/uBlock/issues/1143 // Also, find a link under the mouse, to try to avoid confusing new tabs // as nuisance popups. // Ref.: https://developer.mozilla.org/en-US/docs/Web/Events/contextmenu var onMouseClick = function(ev) { var elem = ev.target; while ( elem !== null && elem.localName !== 'a' ) { elem = elem.parentElement; } vAPI.messaging.send( 'contentscript', { what: 'mouseClick', x: ev.clientX, y: ev.clientY, url: elem !== null ? elem.href : '' } ); }; (function() { if ( window !== window.top || !vAPI.domFilterer ) { return; } document.addEventListener('mousedown', onMouseClick, true); // https://github.com/gorhill/uMatrix/issues/144 vAPI.shutdown.add(function() { document.removeEventListener('mousedown', onMouseClick, true); }); })(); }; /******************************************************************************/ /******************************************************************************/ /******************************************************************************/ } // <<<<<<<< end of HUGE-IF-BLOCK