diff --git a/platform/chromium/manifest.json b/platform/chromium/manifest.json index bf057cbd1..ccd317d29 100644 --- a/platform/chromium/manifest.json +++ b/platform/chromium/manifest.json @@ -27,16 +27,10 @@ "content_scripts": [ { "matches": ["http://*/*", "https://*/*"], - "js": ["js/vapi-client.js", "js/contentscript-start.js"], + "js": ["js/vapi-client.js", "js/contentscript.js"], "run_at": "document_start", "all_frames": true }, - { - "matches": ["http://*/*", "https://*/*"], - "js": ["js/contentscript-end.js"], - "run_at": "document_end", - "all_frames": true - }, { "matches": ["http://*/*", "https://*/*"], "js": ["js/scriptlets/subscriber.js"], diff --git a/platform/chromium/vapi-background.js b/platform/chromium/vapi-background.js index 0ad077f0b..e9c8d42dc 100644 --- a/platform/chromium/vapi-background.js +++ b/platform/chromium/vapi-background.js @@ -1076,16 +1076,6 @@ vAPI.onLoadAllCompleted = function() { var scriptDone = function() { vAPI.lastError(); }; - var scriptEnd = function(tabId) { - if ( vAPI.lastError() ) { - return; - } - vAPI.tabs.injectScript(tabId, { - file: 'js/contentscript-end.js', - allFrames: true, - runAt: 'document_idle' - }, scriptDone); - }; var scriptStart = function(tabId) { vAPI.tabs.injectScript(tabId, { file: 'js/vapi-client.js', @@ -1093,10 +1083,10 @@ vAPI.onLoadAllCompleted = function() { runAt: 'document_idle' }, function(){ }); vAPI.tabs.injectScript(tabId, { - file: 'js/contentscript-start.js', + file: 'js/contentscript.js', allFrames: true, runAt: 'document_idle' - }, function(){ scriptEnd(tabId); }); + }, scriptDone); }; var bindToTabs = function(tabs) { var µb = µBlock; diff --git a/platform/chromium/vapi-client.js b/platform/chromium/vapi-client.js index 34865ed07..9fb54331f 100644 --- a/platform/chromium/vapi-client.js +++ b/platform/chromium/vapi-client.js @@ -21,14 +21,14 @@ /* global HTMLDocument, XMLDocument */ +'use strict'; + // For non background pages /******************************************************************************/ (function(self) { -'use strict'; - /******************************************************************************/ /******************************************************************************/ diff --git a/platform/firefox/frameModule.js b/platform/firefox/frameModule.js index 8a73cb05f..35ab7bed9 100644 --- a/platform/firefox/frameModule.js +++ b/platform/firefox/frameModule.js @@ -1,7 +1,7 @@ /******************************************************************************* - µBlock - a browser extension to block requests. - Copyright (C) 2014 The µBlock authors + uBlock Origin - a browser extension to block requests. + Copyright (C) 2014-2016 The uBlock Origin authors 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 @@ -442,7 +442,7 @@ var contentObserver = { try { lss(this.contentBaseURI + 'vapi-client.js', sandbox); - lss(this.contentBaseURI + 'contentscript-start.js', sandbox); + lss(this.contentBaseURI + 'contentscript.js', sandbox); } catch (ex) { //console.exception(ex.msg, ex.stack); return; @@ -451,7 +451,6 @@ var contentObserver = { let docReady = (e) => { let doc = e.target; doc.removeEventListener(e.type, docReady, true); - lss(this.contentBaseURI + 'contentscript-end.js', sandbox); if ( doc.querySelector('a[href^="abp:"],a[href^="https://subscribe.adblockplus.org/?"]') || diff --git a/platform/opera/manifest.json b/platform/opera/manifest.json index 74ab2a949..012eddf57 100644 --- a/platform/opera/manifest.json +++ b/platform/opera/manifest.json @@ -27,16 +27,10 @@ "content_scripts": [ { "matches": ["http://*/*", "https://*/*"], - "js": ["js/vapi-client.js", "js/contentscript-start.js"], + "js": ["js/vapi-client.js", "js/contentscript.js"], "run_at": "document_start", "all_frames": true }, - { - "matches": ["http://*/*", "https://*/*"], - "js": ["js/contentscript-end.js"], - "run_at": "document_end", - "all_frames": true - }, { "matches": ["http://*/*", "https://*/*"], "js": ["js/scriptlets/subscriber.js"], diff --git a/src/js/contentscript-start.js b/src/js/contentscript-start.js deleted file mode 100644 index 5a206448c..000000000 --- a/src/js/contentscript-start.js +++ /dev/null @@ -1,242 +0,0 @@ -/******************************************************************************* - - uBlock Origin - a browser extension to block requests. - Copyright (C) 2014-2016 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 -*/ - -/* jshint multistr: true */ - -/******************************************************************************/ - -// Injected into content pages - -/******************************************************************************/ - -(function() { - -'use strict'; - -/******************************************************************************/ - -// This can happen -if ( typeof vAPI !== 'object' ) { - return; -} - -// https://github.com/chrisaljoudi/uBlock/issues/456 -// Already injected? -if ( vAPI.contentscriptStartInjected ) { - return; -} -vAPI.contentscriptStartInjected = true; -vAPI.styles = vAPI.styles || []; - -/******************************************************************************/ -/******************************************************************************/ - -// Domain-based ABP cosmetic filters. -// These can be inserted before the DOM is loaded. - -var cosmeticFilters = function(details) { - var donthideCosmeticFilters = {}; - var hideCosmeticFilters = {}; - var donthide = details.cosmeticDonthide; - var hide = details.cosmeticHide; - var i; - if ( donthide.length !== 0 ) { - i = donthide.length; - while ( i-- ) { - donthideCosmeticFilters[donthide[i]] = true; - } - } - // https://github.com/chrisaljoudi/uBlock/issues/143 - if ( hide.length !== 0 ) { - i = hide.length; - var selector; - while ( i-- ) { - selector = hide[i]; - if ( donthideCosmeticFilters[selector] ) { - hide.splice(i, 1); - } else { - hideCosmeticFilters[selector] = true; - } - } - } - if ( hide.length !== 0 ) { - // https://github.com/gorhill/uBlock/issues/1015 - // Boost specificity of our CSS rules. - var styleText = ':root ' + hide.join(',\n:root '); - var style = document.createElement('style'); - style.setAttribute('type', 'text/css'); - // The linefeed before the style block is very important: do not remove! - style.appendChild(document.createTextNode(styleText + '\n{display:none !important;}')); - //console.debug('µBlock> "%s" cosmetic filters: injecting %d CSS rules:', details.domain, details.hide.length, hideStyleText); - var parent = document.head || document.documentElement; - if ( parent ) { - parent.appendChild(style); - vAPI.styles.push(style); - } - hideElements(styleText); - } - vAPI.donthideCosmeticFilters = donthideCosmeticFilters; - vAPI.hideCosmeticFilters = hideCosmeticFilters; -}; - -/******************************************************************************/ - -var netFilters = function(details) { - var parent = document.head || document.documentElement; - if ( !parent ) { - return; - } - var style = document.createElement('style'); - var text = details.netHide.join(',\n'); - var css = details.netCollapse ? - '\n{display:none !important;}' : - '\n{visibility:hidden !important;}'; - style.appendChild(document.createTextNode(text + css)); - parent.appendChild(style); - //console.debug('document.querySelectorAll("%s") = %o', text, document.querySelectorAll(text)); -}; - -/******************************************************************************/ - -// Create script tags and assign data URIs looked up from our library of -// redirection resources: Sometimes it is useful to use these resources as -// standalone scriptlets. These scriptlets are injected from within the -// content scripts because what must be injected, if anything, depends on the -// currently active filters, as selected by the user. -// Library of redirection resources is located at: -// https://github.com/gorhill/uBlock/blob/master/assets/ublock/resources.txt - -var injectScripts = function(scripts) { - var parent = document.head || document.documentElement; - if ( !parent ) { - return; - } - var scriptTag = document.createElement('script'); - scriptTag.appendChild(document.createTextNode(scripts)); - parent.appendChild(scriptTag); - vAPI.injectedScripts = scripts; -}; - -/******************************************************************************/ - -var filteringHandler = function(details) { - var styleTagCount = vAPI.styles.length; - - if ( details ) { - if ( - (vAPI.skipCosmeticFiltering = details.skipCosmeticFiltering) !== true && - (details.cosmeticHide.length !== 0 || details.cosmeticDonthide.length !== 0) - ) { - cosmeticFilters(details); - } - if ( details.netHide.length !== 0 ) { - netFilters(details); - } - if ( details.scripts ) { - injectScripts(details.scripts); - } - // The port will never be used again at this point, disconnecting allows - // the browser to flush this script from memory. - } - - // This is just to inform the background process that cosmetic filters were - // actually injected. - if ( vAPI.styles.length !== styleTagCount ) { - vAPI.messaging.send('contentscript', { what: 'cosmeticFiltersActivated' }); - } - - // 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. - vAPI.contentscriptStartInjected = details && details.ready; -}; - -/******************************************************************************/ - -var hideElements = function(selectors) { - if ( document.body === null ) { - return; - } - var elems = document.querySelectorAll(selectors); - var i = elems.length; - if ( i === 0 ) { - return; - } - // https://github.com/chrisaljoudi/uBlock/issues/158 - // Using CSSStyleDeclaration.setProperty is more reliable - if ( document.body.shadowRoot === undefined ) { - while ( i-- ) { - elems[i].style.setProperty('display', 'none', 'important'); - } - return; - } - // https://github.com/gorhill/uBlock/issues/435 - // Using shadow content so that we do not have to modify style - // attribute. - var sessionId = vAPI.sessionId; - var elem, shadow; - while ( i-- ) { - elem = elems[i]; - shadow = elem.shadowRoot; - // https://www.chromestatus.com/features/4668884095336448 - // "Multiple shadow roots is being deprecated." - if ( shadow !== null ) { - if ( shadow.className !== sessionId ) { - elem.style.setProperty('display', 'none', 'important'); - } - continue; - } - // https://github.com/gorhill/uBlock/pull/555 - // Not all nodes can be shadowed: - // https://github.com/w3c/webcomponents/issues/102 - // https://github.com/gorhill/uBlock/issues/762 - // Remove display style that might get in the way of the shadow - // node doing its magic. - try { - shadow = elem.createShadowRoot(); - shadow.className = sessionId; - elem.style.removeProperty('display'); - } catch (ex) { - elem.style.setProperty('display', 'none', 'important'); - } - } -}; - -/******************************************************************************/ - -var url = window.location.href; -vAPI.messaging.send( - 'contentscript', - { - what: 'retrieveDomainCosmeticSelectors', - pageURL: url, - locationURL: url - }, - filteringHandler -); - -/******************************************************************************/ -/******************************************************************************/ - -})(); - -/******************************************************************************/ diff --git a/src/js/contentscript-end.js b/src/js/contentscript.js similarity index 59% rename from src/js/contentscript-end.js rename to src/js/contentscript.js index 894dd605b..c9b6220fa 100644 --- a/src/js/contentscript-end.js +++ b/src/js/contentscript.js @@ -19,48 +19,452 @@ Home: https://github.com/gorhill/uBlock */ -/******************************************************************************/ - -// Injected into content pages - -(function() { - 'use strict'; /******************************************************************************/ -// I've seen this happens on Firefox -if ( window.location === null ) { - return; +// Injected into content pages + +/******************************************************************************/ +/******************************************************************************/ +/******************************************************************************/ + +// Abort execution by throwing if an unexpected condition arise. +// - https://github.com/chrisaljoudi/uBlock/issues/456 + +if ( typeof vAPI !== 'object' || vAPI.contentscriptInjected ) { + throw new Error('Unexpected condition: aborting.'); } -// This can happen -if ( typeof vAPI !== 'object' ) { - //console.debug('contentscript-end.js > vAPI not found'); - return; -} +vAPI.contentscriptInjected = true; -// https://github.com/chrisaljoudi/uBlock/issues/587 -// Pointless to execute without the start script having done its job. -if ( !vAPI.contentscriptStartInjected ) { - return; -} +/******************************************************************************/ +/******************************************************************************/ +/******************************************************************************/ -// https://github.com/chrisaljoudi/uBlock/issues/456 -// Already injected? -if ( vAPI.contentscriptEndInjected ) { - //console.debug('contentscript-end.js > content script already injected'); - return; -} -vAPI.contentscriptEndInjected = true; -vAPI.styles = vAPI.styles || []; +vAPI.domFilterer = { + allExceptions: Object.create(null), + allSelectors: Object.create(null), + cssNotHiddenId: '', + disabledId: String.fromCharCode(Date.now() % 26 + 97) + Math.floor(Math.random() * 982451653 + 982451653).toString(36), + enabled: true, + hiddenId: String.fromCharCode(Date.now() % 26 + 97) + Math.floor(Math.random() * 982451653 + 982451653).toString(36), + matchesProp: 'matches', + newSelectors: [], + shadowId: String.fromCharCode(Date.now() % 26 + 97) + Math.floor(Math.random() * 982451653 + 982451653).toString(36), + styleTags: [], + xpathNotHiddenId: '', + complexGroupSelector: null, + complexSelectors: [], + simpleGroupSelector: null, + simpleSelectors: [], + + complexHasSelectors: [], + simpleHasSelectors: [], + + xpathSelectors: [], + xpathExpression: null, + xpathResult: null, + + addExceptions: function(aa) { + for ( var i = 0, n = aa.length; i < n; i++ ) { + this.allExceptions[aa[i]] = true; + } + }, + + addHasSelector: function(s1, s2) { + var entry = { a: s1, b: s2.slice(5, -1) }; + if ( s1.indexOf(' ') === -1 ) { + this.simpleHasSelectors.push(entry); + } else { + this.complexHasSelectors.push(entry); + } + }, + + addSelector: function(s) { + if ( this.allSelectors[s] || this.allExceptions[s] ) { + return; + } + this.allSelectors[s] = true; + var pos = s.indexOf(':'); + if ( pos !== -1 ) { + pos = s.indexOf(':has('); + if ( pos !== -1 ) { + this.addHasSelector(s.slice(0, pos), s.slice(pos)); + return; + } + if ( s.lastIndexOf(':xpath(', 0) === 0 ) { + this.addXpathSelector('', s); + return; + } + } + if ( s.indexOf(' ') === -1 ) { + this.simpleSelectors.push(s); + this.simpleGroupSelector = null; + } else { + this.complexSelectors.push(s); + this.complexGroupSelector = null; + } + this.newSelectors.push(s); + }, + + addSelectors: function(aa) { + for ( var i = 0, n = aa.length; i < n; i++ ) { + this.addSelector(aa[i]); + } + }, + + addXpathSelector: function(s1, s2) { + this.xpathSelectors.push(s2.slice(7, -1)); + this.xpathExpression = null; + }, + + checkStyleTags: function(commitIfNeeded) { + var doc = document, + html = doc.documentElement, + head = doc.head, + newParent = head || html; + if ( newParent === null ) { + return; + } + var styles = this.styleTags, + style, oldParent, + mustCommit = false; + for ( var i = 0; i < styles.length; i++ ) { + style = styles[i]; + oldParent = style.parentNode; + // https://github.com/gorhill/uBlock/issues/1031 + // If our style tag was disabled, re-insert into the page. + if ( + style.disabled && + oldParent !== null && + style.hasAttribute(this.disabledId) === false + ) { + oldParent.removeChild(style); + oldParent = null; + } + if ( oldParent === head || oldParent === html ) { + continue; + } + style.disabled = false; + newParent.appendChild(style); + mustCommit = true; + } + if ( mustCommit && commitIfNeeded ) { + this.commit(); + } + }, + + commit: function(newNodes) { + if ( newNodes === undefined ) { + newNodes = [ document.documentElement ]; + } + + // Inject new selectors as CSS rules in a style tags. + if ( this.newSelectors.length ) { + var styleTag = document.createElement('style'); + styleTag.setAttribute('type', 'text/css'); + styleTag.textContent = + ':root ' + + this.newSelectors.join(',\n:root ') + + '\n{ display: none !important; }'; + document.head.appendChild(styleTag); + this.styleTags.push(styleTag); + } + + var nodes, node, parents, parent, i, j, k, entry; + + // Simple `:has()` selectors. + i = this.simpleHasSelectors.length; + while ( i-- ) { + entry = this.simpleHasSelectors[i]; + parents = newNodes; + j = parents.length; + while ( j-- ) { + parent = parents[j]; + if ( parent[this.matchesProp](entry.a) && parent.querySelector(entry.b) !== null ) { + this.hideNode(parent); + } + nodes = parent.querySelectorAll(entry.a + this.cssNotHiddenId); + k = nodes.length; + while ( k-- ) { + node = nodes[k]; + if ( node.querySelector(entry.b) !== null ) { + this.hideNode(node); + } + } + } + } + + // Complex `:has()` selectors. + i = this.complexHasSelectors.length; + while ( i-- ) { + entry = this.complexHasSelectors[i]; + nodes = document.querySelectorAll(entry.a + this.cssNotHiddenId); + j = nodes.length; + while ( j-- ) { + node = nodes[j]; + if ( node.querySelector(entry.b) !== null ) { + this.hideNode(node); + } + } + } + + // `:xpath()` selectors. + if ( this.xpathSelectors.length ) { + if ( this.xpathExpression === null ) { + this.xpathExpression = document.createExpression( + this.xpathSelectors.join(this.xpathNotHiddenId + '|') + this.xpathNotHiddenId, + null + ); + } + this.xpathResult = this.xpathExpression.evaluate( + document, + XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, + this.xpathResult + ); + i = this.xpathResult.snapshotLength; + while ( i-- ) { + node = this.xpathResult.snapshotItem(i); + if ( node.nodeType === 1 ) { + this.hideNode(node); + } + } + } + + // Simple selectors. + if ( this.simpleSelectors.length ) { + if ( this.simpleGroupSelector === null ) { + this.simpleGroupSelector = + this.simpleSelectors.join(this.cssNotHiddenId + ',') + + this.cssNotHiddenId; + } + parents = newNodes; + i = parents.length; + while ( i-- ) { + parent = parents[i]; + if ( parent[this.matchesProp](this.simpleGroupSelector) ) { + this.hideNode(parent); + } + nodes = parent.querySelectorAll(this.simpleGroupSelector); + j = nodes.length; + while ( j-- ) { + this.hideNode(nodes[j]); + } + } + } + + // Complex selectors. + if ( this.complexSelectors.length ) { + if ( this.complexGroupSelector === null ) { + this.complexGroupSelector = + this.complexSelectors.join(this.cssNotHiddenId + ',') + + this.cssNotHiddenId; + } + nodes = document.querySelectorAll(this.complexGroupSelector); + i = nodes.length; + while ( i-- ) { + this.hideNode(nodes[i]); + } + } + + // Reset transient state. + this.newSelectors.length = 0; + }, + + hideNode: (function() { + if ( document.documentElement.shadowRoot === undefined ) { + return function(node) { + node.setAttribute(this.hiddenId, ''); + if ( this.enabled ) { + node.style.setProperty('display', 'none', 'important'); + } + }; + } + return function(node) { + node.setAttribute(this.hiddenId, ''); + var shadow = node.shadowRoot; + // https://www.chromestatus.com/features/4668884095336448 + // "Multiple shadow roots is being deprecated." + if ( shadow !== null ) { + if ( shadow.className !== this.shadowId ) { + node.style.setProperty('display', 'none', 'important'); + } + return; + } + // https://github.com/gorhill/uBlock/pull/555 + // Not all nodes can be shadowed: + // https://github.com/w3c/webcomponents/issues/102 + // https://github.com/gorhill/uBlock/issues/762 + // Remove display style that might get in the way of the shadow + // node doing its magic. + try { + shadow = node.createShadowRoot(); + shadow.className = this.shadowId; + node.style.removeProperty('display'); + } catch (ex) { + node.style.setProperty('display', 'none', 'important'); + } + }; + })(), + + toggleOff: function() { + this.enabled = false; + }, + + toggleOn: function() { + this.enabled = true; + } +}; + + +// Not everything could be initialized at declaration time. +(function() { + var df = vAPI.domFilterer; + df.cssNotHiddenId = ':not([' + df.hiddenId + '])'; + df.xpathNotHiddenId = '[not(@' + df.hiddenId + ')]'; + var docElem = document.documentElement; + if ( typeof docElem.matches !== 'function' ) { + if ( typeof docElem.mozMatchesSelector === 'function' ) { + df.matchesProp = 'mozMatchesSelector'; + } else if ( typeof docElem.webkitMatchesSelector === 'function' ) { + df.matchesProp = 'webkitMatchesSelector'; + } + } +})(); + +/******************************************************************************/ +/******************************************************************************/ +/******************************************************************************/ + +// 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() { + +/******************************************************************************/ + +// Domain-based ABP cosmetic filters. +// These can be inserted before the DOM is loaded. + +var cosmeticFilters = function(details) { + var domFilterer = vAPI.domFilterer; + domFilterer.addExceptions(details.cosmeticDonthide); + // https://github.com/chrisaljoudi/uBlock/issues/143 + domFilterer.addSelectors(details.cosmeticHide); + domFilterer.commit(); +}; + +/******************************************************************************/ + +var netFilters = function(details) { + var parent = document.head || document.documentElement; + if ( !parent ) { + return; + } + var styleTag = document.createElement('style'); + styleTag.setAttribute('type', 'text/css'); + var text = details.netHide.join(',\n'); + var css = details.netCollapse ? + '\n{display:none !important;}' : + '\n{visibility:hidden !important;}'; + styleTag.appendChild(document.createTextNode(text + css)); + parent.appendChild(styleTag); +}; + +/******************************************************************************/ + +// Create script tags and assign data URIs looked up from our library of +// redirection resources: Sometimes it is useful to use these resources as +// standalone scriptlets. These scriptlets are injected from within the +// content scripts because what must be injected, if anything, depends on the +// currently active filters, as selected by the user. +// Library of redirection resources is located at: +// https://github.com/gorhill/uBlock/blob/master/assets/ublock/resources.txt + +var injectScripts = function(scripts) { + var parent = document.head || document.documentElement; + if ( !parent ) { + return; + } + var scriptTag = document.createElement('script'); + // Have the injected script tag remove itself when execution completes: to + // keep DOM as clean as possible. + scripts += + "\n" + + "(function() {\n" + + " var c = document.currentScript,\n" + + " p = c && c.parentNode;\n" + + " if ( p ) {\n" + + " p.removeChild(c);\n" + + " }\n" + + "})();"; + scriptTag.appendChild(document.createTextNode(scripts)); + parent.appendChild(scriptTag); + vAPI.injectedScripts = scripts; +}; + +/******************************************************************************/ + +var responseHandler = function(details) { + var domFilterer = vAPI.domFilterer; + var styleTagCount = domFilterer.styleTags.length; + + if ( details ) { + if ( + (vAPI.skipCosmeticFiltering = details.skipCosmeticFiltering) !== true && + (details.cosmeticHide.length !== 0 || details.cosmeticDonthide.length !== 0) + ) { + cosmeticFilters(details); + } + if ( details.netHide.length !== 0 ) { + netFilters(details); + } + if ( details.scripts ) { + injectScripts(details.scripts); + } + // The port will never be used again at this point, disconnecting allows + // the browser to flush this script from memory. + } + + // This is just to inform the background process that cosmetic filters were + // actually injected. + if ( domFilterer.styleTags.length !== styleTagCount ) { + vAPI.messaging.send('contentscript', { what: 'cosmeticFiltersActivated' }); + } + + // 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. + vAPI.contentscriptInjected = details && details.ready; +}; + +/******************************************************************************/ + +var url = window.location.href; +vAPI.messaging.send( + 'contentscript', + { + what: 'retrieveDomainCosmeticSelectors', + pageURL: url, + locationURL: url + }, + responseHandler +); + +/******************************************************************************/ + +})(); + +/******************************************************************************/ /******************************************************************************/ /******************************************************************************/ // https://github.com/chrisaljoudi/uBlock/issues/7 -var uBlockCollapser = (function() { +var domCollapser = (function() { var timer = null; var requestId = 1; var newRequests = []; @@ -289,6 +693,28 @@ var uBlockCollapser = (function() { })(); /******************************************************************************/ +/******************************************************************************/ +/******************************************************************************/ + +var domIsLoaded = function(ev) { + +/******************************************************************************/ + +if ( ev ) { + document.removeEventListener('DOMContentLoaded', domIsLoaded); +} + +// I've seen this happens on Firefox +if ( window.location === null ) { + return; +} + +// https://github.com/chrisaljoudi/uBlock/issues/587 +// Pointless to execute without the start script having done its job. +if ( !vAPI.contentscriptInjected ) { + return; +} + /******************************************************************************/ // Cosmetic filters @@ -304,112 +730,56 @@ var uBlockCollapser = (function() { //var timer = window.performance || Date; //var tStart = timer.now(); - var hideElements = (function() { - if ( document.body === null ) { - return function() {}; - } - if ( document.body.shadowRoot === undefined ) { - return function(selectors) { - // https://github.com/chrisaljoudi/uBlock/issues/207 - // Do not call querySelectorAll() using invalid CSS selectors - if ( selectors.length === 0 ) { return; } - var elems = document.querySelectorAll(selectors); - var i = elems.length; - if ( i === 0 ) { return; } - // https://github.com/chrisaljoudi/uBlock/issues/158 - // Using CSSStyleDeclaration.setProperty is more reliable - while ( i-- ) { - elems[i].style.setProperty('display', 'none', 'important'); - } - }; - } - return function(selectors) { - if ( selectors.length === 0 ) { return; } - var elems = document.querySelectorAll(selectors); - var i = elems.length; - if ( i === 0 ) { return; } - // https://github.com/gorhill/uBlock/issues/435 - // Using shadow content so that we do not have to modify style - // attribute. - var sessionId = vAPI.sessionId; - var elem, shadow; - while ( i-- ) { - elem = elems[i]; - shadow = elem.shadowRoot; - // https://www.chromestatus.com/features/4668884095336448 - // "Multiple shadow roots is being deprecated." - if ( shadow !== null ) { - if ( shadow.className !== sessionId ) { - elem.style.setProperty('display', 'none', 'important'); - } - continue; - } - // https://github.com/gorhill/uBlock/pull/555 - // Not all nodes can be shadowed: - // https://github.com/w3c/webcomponents/issues/102 - // https://github.com/gorhill/uBlock/issues/762 - // Remove display style that might get in the way of the shadow - // node doing its magic. - try { - shadow = elem.createShadowRoot(); - shadow.className = sessionId; - elem.style.removeProperty('display'); - } catch (ex) { - elem.style.setProperty('display', 'none', 'important'); - } - } - }; - })(); - // https://github.com/chrisaljoudi/uBlock/issues/789 // https://github.com/gorhill/uBlock/issues/873 // Be sure that our style tags used for cosmetic filtering are still applied. - var checkStyleTags = function() { - var doc = document, - html = doc.documentElement, - head = doc.head, - newParent = head || html; - if ( newParent === null ) { + var domFilterer = vAPI.domFilterer; + domFilterer.checkStyleTags(false); + domFilterer.commit(); + + var contextNodes = [ document.documentElement ]; + var messaging = vAPI.messaging; + var highGenerics = null; + var highHighGenericsInjected = false; + var lowGenericSelectors = []; + var queriedSelectors = Object.create(null); + + var responseHandler = function(response) { + // https://github.com/gorhill/uMatrix/issues/144 + if ( response && response.shutdown ) { + vAPI.shutdown.exec(); return; } - var styles = vAPI.styles || [], - style, oldParent; - for ( var i = 0; i < styles.length; i++ ) { - style = styles[i]; - oldParent = style.parentNode; - // https://github.com/gorhill/uBlock/issues/1031 - // If our style tag was disabled, force a re-insert into the page. - if ( - style.disabled && - oldParent !== null && - style[vAPI.sessionId] === undefined - ) { - oldParent.removeChild(style); - oldParent = null; - } - if ( oldParent === head || oldParent === html ) { - continue; - } - style.disabled = false; - newParent.appendChild(style); - // The page tried to get rid of us: reapply inline styles to - // blocked elements. - hideElements(style.textContent.slice(0, style.textContent.lastIndexOf('\n'))); - } - }; - checkStyleTags(); - var messaging = vAPI.messaging; - var queriedSelectors = {}; - var injectedSelectors = {}; - var lowGenericSelectors = []; - var highGenerics = null; - var contextNodes = [document]; - var nullArray = { push: function(){} }; + //var tStart = timer.now(); + var result = response && response.result; + + if ( result ) { + if ( result.hide.length ) { + processLowGenerics(result.hide); + } + if ( result.highGenerics ) { + highGenerics = result.highGenerics; + } + } + if ( highGenerics ) { + if ( highGenerics.hideLowCount ) { + processHighLowGenerics(highGenerics.hideLow); + } + if ( highGenerics.hideMediumCount ) { + processHighMediumGenerics(highGenerics.hideMedium); + } + if ( highGenerics.hideHighCount ) { + processHighHighGenericsAsync(); + } + } + domFilterer.commit(contextNodes); + contextNodes = []; + //console.debug('%f: uBlock: CSS injection time', timer.now() - tStart); + }; var retrieveGenericSelectors = function() { if ( lowGenericSelectors.length !== 0 || highGenerics === null ) { - //console.log('µBlock> ABP cosmetic filters: retrieving CSS rules using %d selectors', lowGenericSelectors.length); messaging.send( 'contentscript', { @@ -418,124 +788,18 @@ var uBlockCollapser = (function() { selectors: lowGenericSelectors, firstSurvey: highGenerics === null }, - retrieveHandler + responseHandler ); - // https://github.com/chrisaljoudi/uBlock/issues/452 - retrieveHandler = nextRetrieveHandler; } else { - nextRetrieveHandler(null); + responseHandler(null); } lowGenericSelectors = []; }; - // https://github.com/chrisaljoudi/uBlock/issues/452 - // This needs to be executed *after* the response from our query is - // received, not at `DOMContentLoaded` time, or else there is a good - // likeliness to outrun contentscript-start.js, which may still be waiting - // on a response from its own query. - var firstRetrieveHandler = function(response) { - // https://github.com/chrisaljoudi/uBlock/issues/158 - // Ensure injected styles are enforced - // rhill 2014-11-16: not sure this is needed anymore. Test case in - // above issue was fine without the line below.. - var selectors = vAPI.hideCosmeticFilters; - if ( typeof selectors === 'object' ) { - injectedSelectors = selectors; - hideElements(Object.keys(selectors)); - } - // Add exception filters into injected filters collection, in order - // to force them to be seen as "already injected". - selectors = vAPI.donthideCosmeticFilters; - if ( typeof selectors === 'object' ) { - for ( var selector in selectors ) { - if ( selectors.hasOwnProperty(selector) ) { - injectedSelectors[selector] = true; - } - } - } - // Flush dead code from memory - firstRetrieveHandler = null; - - // These are sent only once - var result = response && response.result; - if ( result ) { - if ( result.highGenerics ) { - highGenerics = result.highGenerics; - } - if ( result.donthide ) { - processLowGenerics(result.donthide, nullArray); - } - } - - nextRetrieveHandler(response); - }; - - var nextRetrieveHandler = function(response) { - // https://github.com/gorhill/uMatrix/issues/144 - if ( response && response.shutdown ) { - vAPI.shutdown.exec(); - return; - } - - //var tStart = timer.now(); - //console.debug('µBlock> contextNodes = %o', contextNodes); - var result = response && response.result; - var hideSelectors = []; - - if ( result && result.hide.length ) { - processLowGenerics(result.hide, hideSelectors); - } - if ( highGenerics ) { - if ( highGenerics.hideLowCount ) { - processHighLowGenerics(highGenerics.hideLow, hideSelectors); - } - if ( highGenerics.hideMediumCount ) { - processHighMediumGenerics(highGenerics.hideMedium, hideSelectors); - } - if ( highGenerics.hideHighCount ) { - processHighHighGenericsAsync(); - } - } - if ( hideSelectors.length !== 0 ) { - addStyleTag(hideSelectors); - } - contextNodes.length = 0; - //console.debug('%f: uBlock: CSS injection time', timer.now() - tStart); - }; - - var retrieveHandler = firstRetrieveHandler; - // Ensure elements matching a set of selectors are visually removed // from the page, by: // - Modifying the style property on the elements themselves // - Injecting a style tag - - var addStyleTag = function(selectors) { - // https://github.com/gorhill/uBlock/issues/1015 - // Boost specificity of our CSS rules. - var styleText = ':root ' + selectors.join(',\n:root '); - var style = document.createElement('style'); - style.setAttribute('type', 'text/css'); - // The linefeed before the style block is very important: do no remove! - style.appendChild(document.createTextNode(styleText + '\n{display:none !important;}')); - var parent = document.head || document.documentElement; - if ( parent ) { - parent.appendChild(style); - vAPI.styles.push(style); - } - hideElements(styleText); - messaging.send( - 'contentscript', - { - what: 'cosmeticFiltersInjected', - type: 'cosmetic', - hostname: window.location.hostname, - selectors: selectors - } - ); - //console.debug('µBlock> generic cosmetic filters: injecting %d CSS rules:', selectors.length, text); - }; - // Extract and return the staged nodes which (may) match the selectors. var selectNodes = function(selector) { @@ -562,24 +826,15 @@ var uBlockCollapser = (function() { // - [id] // - [class] - var processLowGenerics = function(generics, out) { - var i = generics.length; - var selector; - while ( i-- ) { - selector = generics[i]; - if ( injectedSelectors.hasOwnProperty(selector) ) { - continue; - } - injectedSelectors[selector] = true; - out.push(selector); - } + var processLowGenerics = function(generics) { + domFilterer.addSelectors(generics); }; // High-low generics: // - [alt="..."] // - [title="..."] - var processHighLowGenerics = function(generics, out) { + var processHighLowGenerics = function(generics) { var attrs = ['title', 'alt']; var attr, attrValue, nodeList, iNode, node; var selector; @@ -595,19 +850,15 @@ var uBlockCollapser = (function() { // form, as the generic will affect all related specific forms selector = '[' + attr + '="' + attrValue + '"]'; if ( generics.hasOwnProperty(selector) ) { - if ( injectedSelectors.hasOwnProperty(selector) === false ) { - injectedSelectors[selector] = true; - out.push(selector); - continue; - } + domFilterer.addSelector(selector); + domFilterer.hideNode(node); + continue; } // Candidate 2 = specific form selector = node.localName + selector; if ( generics.hasOwnProperty(selector) ) { - if ( injectedSelectors.hasOwnProperty(selector) === false ) { - injectedSelectors[selector] = true; - out.push(selector); - } + domFilterer.addSelector(selector); + domFilterer.hideNode(node); } } } @@ -616,7 +867,7 @@ var uBlockCollapser = (function() { // High-medium generics: // - [href^="http"] - var processHighMediumGenerics = function(generics, out) { + var processHighMediumGenerics = function(generics) { var doc = document; var i = contextNodes.length; var aa = [ null ]; @@ -625,18 +876,18 @@ var uBlockCollapser = (function() { node = contextNodes[i]; if ( node.localName === 'a' ) { aa[0] = node; - processHighMediumGenericsForNodes(aa, generics, out); + processHighMediumGenericsForNodes(aa, generics); } nodes = node.getElementsByTagName('a'); if ( nodes.length === 0 ) { continue; } - processHighMediumGenericsForNodes(nodes, generics, out); + processHighMediumGenericsForNodes(nodes, generics); if ( node === doc ) { break; } } }; - var processHighMediumGenericsForNodes = function(nodes, generics, out) { + var processHighMediumGenericsForNodes = function(nodes, generics) { var i = nodes.length; var node, href, pos, hash, selectors, j, selector; var aa = [ '' ]; @@ -658,20 +909,17 @@ var uBlockCollapser = (function() { j = selectors.length; while ( j-- ) { selector = selectors[j]; - if ( - href.lastIndexOf(selector.slice(8, -2), 0) === 0 && - injectedSelectors.hasOwnProperty(selector) === false - ) { - injectedSelectors[selector] = true; - out.push(selector); + if ( href.lastIndexOf(selector, 8) === 8 ) { + domFilterer.addSelector(selector); + domFilterer.hideNode(node); } } } }; - // High-high generics are *very costly* to process, so we will coalesce + // High-high generics are very costly to process, so we will coalesce // requests to process high-high generics into as few requests as possible. - // The gain is *significant* on bloated pages. + // The gain is significant on bloated pages. var processHighHighGenericsMisses = 8; var processHighHighGenericsTimer = null; @@ -681,7 +929,7 @@ var uBlockCollapser = (function() { if ( highGenerics.hideHigh === '' ) { return; } - if ( injectedSelectors.hasOwnProperty('{{highHighGenerics}}') ) { + if ( highHighGenericsInjected ) { return; } // When there are too many misses for these highly generic CSS rules, @@ -689,27 +937,15 @@ var uBlockCollapser = (function() { if ( document.querySelector(highGenerics.hideHigh) === null ) { processHighHighGenericsMisses -= 1; if ( processHighHighGenericsMisses === 0 ) { - injectedSelectors['{{highHighGenerics}}'] = true; + highHighGenericsInjected = true; } return; } - injectedSelectors['{{highHighGenerics}}'] = true; + highHighGenericsInjected = true; // We need to filter out possible exception cosmetic filters from // high-high generics selectors. - var selectors = highGenerics.hideHigh.split(',\n'); - var i = selectors.length; - var selector; - while ( i-- ) { - selector = selectors[i]; - if ( injectedSelectors.hasOwnProperty(selector) ) { - selectors.splice(i, 1); - } else { - injectedSelectors[selector] = true; - } - } - if ( selectors.length !== 0 ) { - addStyleTag(selectors); - } + domFilterer.addSelectors(highGenerics.hideHigh.split(',\n')); + domFilterer.commit(); }; var processHighHighGenericsAsync = function() { @@ -719,58 +955,41 @@ var uBlockCollapser = (function() { processHighHighGenericsTimer = vAPI.setTimeout(processHighHighGenerics, 300); }; - // Extract all ids: these will be passed to the cosmetic filtering - // engine, and in return we will obtain only the relevant CSS selectors. - - var idsFromNodeList = function(nodes) { - if ( !nodes || !nodes.length ) { - return; - } - var qq = queriedSelectors; - var ll = lowGenericSelectors; - var node, v; - var i = nodes.length; - while ( i-- ) { - node = nodes[i]; - if ( node.nodeType !== 1 ) { continue; } - v = node.id; - if ( typeof v !== 'string' ) { continue; } - v = v.trim(); - if ( v === '' ) { continue; } - v = '#' + v; - if ( qq.hasOwnProperty(v) ) { continue; } - ll.push(v); - qq[v] = true; - } - }; - - // Extract all classes: these will be passed to the cosmetic filtering + // 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 classesFromNodeList = function(nodes) { + var classesAndIdsFromNodeList = function(nodes) { if ( !nodes || !nodes.length ) { return; } - var qq = queriedSelectors; var ll = lowGenericSelectors; - var v, vv, len, c, beg, end; + var node, v, vv, len, c, beg, end; var i = nodes.length; - while ( i-- ) { - vv = nodes[i].className; - if ( typeof vv !== 'string' ) { continue; } + node = nodes[i]; + if ( node.nodeType !== 1 ) { continue; } + v = node.id; + if ( v !== '' && typeof v === 'string' ) { + v = '#' + v.trim(); + if ( v !== '#' && qq[v] === undefined ) { + ll.push(v); + qq[v] = true; + } + } + vv = node.className; + if ( vv === '' || typeof vv !== 'string' ) { continue; } len = vv.length; beg = 0; for (;;) { // Skip whitespaces while ( beg !== len ) { c = vv.charCodeAt(beg); - if ( c !== 0x20 && (c > 0x0D || c < 0x09) ) { break; } + if ( c > 0x20 ) { break; } beg++; } if ( beg === len ) { break; } @@ -778,11 +997,11 @@ var uBlockCollapser = (function() { // Skip non-whitespaces while ( end !== len ) { c = vv.charCodeAt(end); - if ( c === 0x20 || (c <= 0x0D && c >= 0x09) ) { break; } + if ( c <= 0x20 ) { break; } end++; } v = '.' + vv.slice(beg, end); - if ( qq.hasOwnProperty(v) === false ) { + if ( qq[v] === undefined ) { ll.push(v); qq[v] = true; } @@ -794,8 +1013,7 @@ var uBlockCollapser = (function() { // Start cosmetic filtering. - idsFromNodeList(document.querySelectorAll('[id]')); - classesFromNodeList(document.querySelectorAll('[class]')); + classesAndIdsFromNodeList(document.querySelectorAll('[class],[id]')); retrieveGenericSelectors(); //console.debug('%f: uBlock: survey time', timer.now() - tStart); @@ -824,8 +1042,9 @@ var uBlockCollapser = (function() { // Added node lists will be cumulated here before being processed var addedNodeLists = []; var addedNodeListsTimer = null; + var addedNodeListsTimerDelay = 10; var removedNodeListsTimer = null; - var collapser = uBlockCollapser; + var collapser = domCollapser; // The `cosmeticFiltersActivated` message is required: a new element could // be matching an already injected but otherwise inactive cosmetic filter. @@ -833,6 +1052,7 @@ var uBlockCollapser = (function() { // effect on the document), and thus must be logged if needed. var addedNodesHandler = function() { addedNodeListsTimer = null; + addedNodeListsTimerDelay = Math.min(addedNodeListsTimerDelay*2, 100); var iNodeList = addedNodeLists.length, nodeList, iNode, node; while ( iNodeList-- ) { @@ -852,8 +1072,7 @@ var uBlockCollapser = (function() { } addedNodeLists.length = 0; if ( contextNodes.length !== 0 ) { - idsFromNodeList(selectNodes('[id]')); - classesFromNodeList(selectNodes('[class]')); + classesAndIdsFromNodeList(selectNodes('[class],[id]')); retrieveGenericSelectors(); messaging.send('contentscript', { what: 'cosmeticFiltersActivated' }); } @@ -863,7 +1082,7 @@ var uBlockCollapser = (function() { // This will ensure our style elements will stay in the DOM. var removedNodesHandler = function() { removedNodeListsTimer = null; - checkStyleTags(); + domFilterer.checkStyleTags(true); }; // https://github.com/chrisaljoudi/uBlock/issues/205 @@ -886,7 +1105,7 @@ var uBlockCollapser = (function() { } } if ( addedNodeLists.length !== 0 && addedNodeListsTimer === null ) { - addedNodeListsTimer = vAPI.setTimeout(addedNodesHandler, 100); + addedNodeListsTimer = vAPI.setTimeout(addedNodesHandler, addedNodeListsTimerDelay); } if ( removedNodeLists && removedNodeListsTimer === null ) { removedNodeListsTimer = vAPI.setTimeout(removedNodesHandler, 100); @@ -917,7 +1136,6 @@ var uBlockCollapser = (function() { }); })(); -/******************************************************************************/ /******************************************************************************/ // Permanent @@ -930,8 +1148,8 @@ var uBlockCollapser = (function() { (function() { var onResourceFailed = function(ev) { //console.debug('onResourceFailed(%o)', ev); - uBlockCollapser.add(ev.target); - uBlockCollapser.process(); + domCollapser.add(ev.target); + domCollapser.process(); }; document.addEventListener('error', onResourceFailed, true); @@ -941,7 +1159,6 @@ var uBlockCollapser = (function() { }); })(); -/******************************************************************************/ /******************************************************************************/ // https://github.com/chrisaljoudi/uBlock/issues/7 @@ -950,7 +1167,7 @@ var uBlockCollapser = (function() { // http://jsperf.com/queryselectorall-vs-getelementsbytagname/145 (function() { - var collapser = uBlockCollapser; + var collapser = domCollapser; var elems = document.getElementsByTagName('img'), i = elems.length, elem; while ( i-- ) { @@ -965,7 +1182,6 @@ var uBlockCollapser = (function() { collapser.process(0); })(); -/******************************************************************************/ /******************************************************************************/ // To send mouse coordinates to main process, as the chrome API fails @@ -1007,9 +1223,16 @@ var uBlockCollapser = (function() { }); })(); -/******************************************************************************/ /******************************************************************************/ -})(); +}; /******************************************************************************/ +/******************************************************************************/ +/******************************************************************************/ + +if ( document.readyState !== 'loading' ) { + domIsLoaded(); +} else { + document.addEventListener('DOMContentLoaded', domIsLoaded); +} diff --git a/src/js/cosmetic-filtering.js b/src/js/cosmetic-filtering.js index ac0674892..94e2653b7 100644 --- a/src/js/cosmetic-filtering.js +++ b/src/js/cosmetic-filtering.js @@ -1,7 +1,7 @@ /******************************************************************************* - µBlock - a browser extension to block requests. - Copyright (C) 2014 Raymond Hill + uBlock Origin - a browser extension to block requests. + Copyright (C) 2014-2016 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 @@ -20,14 +20,14 @@ */ /* jshint bitwise: false */ -/* global punycode, µBlock */ +/* global punycode */ + +'use strict'; /******************************************************************************/ µBlock.cosmeticFilteringEngine = (function(){ -'use strict'; - /******************************************************************************/ var µb = µBlock; @@ -141,40 +141,6 @@ FilterPlainMore.fromSelfie = function(s) { /******************************************************************************/ -var FilterBucket = function(a, b) { - this.f = null; - this.filters = []; - if ( a !== undefined ) { - this.filters[0] = a; - if ( b !== undefined ) { - this.filters[1] = b; - } - } -}; - -FilterBucket.prototype.add = function(a) { - this.filters.push(a); -}; - -FilterBucket.prototype.retrieve = function(s, out) { - var i = this.filters.length; - while ( i-- ) { - this.filters[i].retrieve(s, out); - } -}; - -FilterBucket.prototype.fid = '[]'; - -FilterBucket.prototype.toSelfie = function() { - return this.filters.length.toString(); -}; - -FilterBucket.fromSelfie = function() { - return new FilterBucket(); -}; - -/******************************************************************************/ - // Any selector specific to a hostname // Examples: // search.snapdo.com###ABottomD @@ -235,6 +201,40 @@ FilterEntity.fromSelfie = function(s) { return new FilterEntity(decode(s.slice(0, pos)), s.slice(pos + 1)); }; +/******************************************************************************/ + +var FilterBucket = function(a, b) { + this.f = null; + this.filters = []; + if ( a !== undefined ) { + this.filters[0] = a; + if ( b !== undefined ) { + this.filters[1] = b; + } + } +}; + +FilterBucket.prototype.add = function(a) { + this.filters.push(a); +}; + +FilterBucket.prototype.retrieve = function(s, out) { + var i = this.filters.length; + while ( i-- ) { + this.filters[i].retrieve(s, out); + } +}; + +FilterBucket.prototype.fid = '[]'; + +FilterBucket.prototype.toSelfie = function() { + return this.filters.length.toString(); +}; + +FilterBucket.fromSelfie = function() { + return new FilterBucket(); +}; + /******************************************************************************/ /******************************************************************************/ @@ -245,11 +245,13 @@ var FilterParser = function() { this.invalid = false; this.cosmetic = true; this.reScriptTagFilter = /^script:(contains|inject)\((.+?)\)$/; + this.reNeedHostname = /^(?:.+?:has|:xpath)\(.+?\)$/; }; /******************************************************************************/ FilterParser.prototype.reset = function() { + this.raw = ''; this.prefix = this.suffix = this.style = ''; this.unhide = 0; this.hostnames.length = 0; @@ -264,6 +266,8 @@ FilterParser.prototype.parse = function(raw) { // important! this.reset(); + this.raw = raw; + // Find the bounds of the anchor. var lpos = raw.indexOf('#'); if ( lpos === -1 ) { @@ -349,6 +353,16 @@ FilterParser.prototype.parse = function(raw) { this.hostnames = this.prefix.split(/\s*,\s*/); } + // For some selectors, it is mandatory to have a hostname or entity. + if ( + this.hostnames.length === 0 && + this.unhide === 0 && + this.reNeedHostname.test(this.suffix) + ) { + this.invalid = true; + return this; + } + // Script tag filters: pre-process them so that can be used with minimal // overhead in the content script. // Examples: @@ -357,10 +371,16 @@ FilterParser.prototype.parse = function(raw) { // focus.de##script:inject(uabinject-defuser.js) var matches = this.reScriptTagFilter.exec(this.suffix); - if ( matches === null ) { - return this; + if ( matches !== null ) { + return this.parseScriptTagFilter(matches); } + return this; +}; + +/******************************************************************************/ + +FilterParser.prototype.parseScriptTagFilter = function(matches) { // Currently supported only as non-generic selector. Also, exception // script tag filter makes no sense, ignore. if ( this.hostnames.length === 0 || this.unhide === 1 ) { @@ -378,7 +398,7 @@ FilterParser.prototype.parse = function(raw) { } else { token = token.slice(1, -1); if ( isBadRegex(token) ) { - µb.logger.writeOne('', 'error', 'Cosmetic filtering – bad regular expression: ' + raw + ' (' + isBadRegex.message + ')'); + µb.logger.writeOne('', 'error', 'Cosmetic filtering – bad regular expression: ' + this.raw + ' (' + isBadRegex.message + ')'); this.invalid = true; } } @@ -676,19 +696,33 @@ FilterContainer.prototype.reset = function() { FilterContainer.prototype.isValidSelector = (function() { var div = document.createElement('div'); - + var matchesProp = (function() { + if ( typeof div.matches === 'function' ) { + return 'matches'; + } + if ( typeof div.mozMatchesSelector === 'function' ) { + return 'mozMatchesSelector'; + } + if ( typeof div.webkitMatchesSelector === 'function' ) { + return 'webkitMatchesSelector'; + } + return ''; + })(); // Not all browsers support `Element.matches`: // http://caniuse.com/#feat=matchesselector - if ( typeof div.matches !== 'function' ) { + if ( matchesProp === '' ) { return function() { return true; }; } + var reHasSelector = /^(.+?):has\((.+?)\)$/; + var reXpathSelector = /^:xpath\((.+?)\)$/; + return function(s) { try { // https://github.com/gorhill/uBlock/issues/693 - div.matches(s + ',\n#foo'); + div[matchesProp](s + ',\n#foo'); // Discard new ABP's `-abp-properties` directive until it is // implemented (if ever). if ( s.indexOf('[-abp-properties=') === -1 ) { @@ -697,6 +731,24 @@ FilterContainer.prototype.isValidSelector = (function() { } catch (e) { } // We reach this point very rarely. + var matches; + + // Future `:has`-based filter? If so, validate both parts of the whole + // selector. + matches = reHasSelector.exec(s); + if ( matches !== null ) { + return this.isValidSelector(matches[1]) && this.isValidSelector(matches[2]); + } + // Custom `:xpath`-based filter? + matches = reXpathSelector.exec(s); + if ( matches !== null ) { + try { + return document.createExpression(matches[1], null) instanceof XPathExpression; + } catch (e) { + } + return false; + } + // Special `script:` filter? if ( s.startsWith('script') ) { if ( s.startsWith('?', 6) || s.startsWith('+', 6) ) { return true; @@ -924,7 +976,7 @@ FilterContainer.prototype.fromCompiledContent = function(text, lineBeg, skip) { fields = line.split('\v'); - // h ir twitter.com .promoted-tweet + // h [\t] ir [\t] twitter.com [\t] .promoted-tweet if ( fields[0] === 'h' ) { // Special filter: script tags. Not a real CSS selector. if ( fields[3].startsWith('script') ) { @@ -943,8 +995,8 @@ FilterContainer.prototype.fromCompiledContent = function(text, lineBeg, skip) { continue; } - // lg 105 .largeAd - // lg+ 2jx .Mpopup + #Mad > #MadZone + // lg [\t] 105 [\t] .largeAd + // lg+ [\t] 2jx [\t] .Mpopup + #Mad > #MadZone if ( fields[0] === 'lg' || fields[0] === 'lg+' ) { filter = fields[0] === 'lg' ? filterPlain : @@ -960,7 +1012,7 @@ FilterContainer.prototype.fromCompiledContent = function(text, lineBeg, skip) { continue; } - // entity selector + // entity [\t] selector if ( fields[0] === 'e' ) { // Special filter: script tags. Not a real CSS selector. if ( fields[2].startsWith('script') ) { @@ -1392,8 +1444,6 @@ FilterContainer.prototype.retrieveGenericSelectors = function(request) { hideHigh: this.highHighGenericHide, hideHighCount: this.highHighGenericHideCount }; - // https://github.com/chrisaljoudi/uBlock/issues/497 - r.donthide = this.genericDonthide; } var hideSelectors = r.hide; @@ -1437,13 +1487,16 @@ FilterContainer.prototype.retrieveDomainSelectors = function(request) { // r.ready will tell the content script the cosmetic filtering engine is // up and ready. + // https://github.com/chrisaljoudi/uBlock/issues/497 + // Generic exception filters are to be applied on all pages. + var r = { ready: this.frozen, domain: domain, entity: pos === -1 ? domain : domain.slice(0, pos - domain.length), skipCosmeticFiltering: this.acceptedCount === 0, cosmeticHide: [], - cosmeticDonthide: [], + cosmeticDonthide: this.genericDonthide, netHide: [], netCollapse: µb.userSettings.collapseBlocked, scripts: this.retrieveScriptTags(domain, hostname) diff --git a/src/js/logger-ui-inspector.js b/src/js/logger-ui-inspector.js index 584f24a59..9df42b1ed 100644 --- a/src/js/logger-ui-inspector.js +++ b/src/js/logger-ui-inspector.js @@ -1,7 +1,7 @@ /******************************************************************************* uBlock Origin - a browser extension to block requests. - Copyright (C) 2015 Raymond Hill + Copyright (C) 2015-2016 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 @@ -19,14 +19,14 @@ Home: https://github.com/gorhill/uBlock */ -/* global vAPI, uDom */ +/* global uDom */ + +'use strict'; /******************************************************************************/ (function() { -'use strict'; - /******************************************************************************/ var showdomButton = uDom.nodeFromId('showdom'); @@ -257,21 +257,17 @@ var countFromNode = function(li) { /******************************************************************************/ -var selectorFromNode = function(node, nth) { +var selectorFromNode = function(node) { var selector = ''; var code; - if ( nth === undefined ) { - nth = 1; - } while ( node !== null ) { if ( node.localName === 'li' ) { - code = node.querySelector('code:nth-of-type(' + nth + ')'); + code = node.querySelector('code'); if ( code !== null ) { selector = code.textContent + ' > ' + selector; if ( selector.indexOf('#') !== -1 ) { break; } - nth = 1; } } node = node.parentElement; @@ -281,6 +277,21 @@ var selectorFromNode = function(node, nth) { /******************************************************************************/ +var selectorFromFilter = function(node) { + while ( node !== null ) { + if ( node.localName === 'li' ) { + var code = node.querySelector('code:nth-of-type(2)'); + if ( code !== null ) { + return code.textContent; + } + } + node = node.parentElement; + } + return ''; +}; + +/******************************************************************************/ + var nidFromNode = function(node) { var li = node; while ( li !== null ) { @@ -482,10 +493,11 @@ var onClick = function(ev) { messaging.sendTo( 'loggerUI', { - what: 'toggleNodes', + what: 'toggleFilter', original: false, target: target.classList.toggle('off'), - selector: selectorFromNode(target, 2), + selector: selectorFromNode(target), + filter: selectorFromFilter(target), nid: '' }, inspectedTabId, @@ -504,7 +516,7 @@ var onClick = function(ev) { what: 'toggleNodes', original: true, target: target.classList.toggle('off') === false, - selector: selectorFromNode(target, 1), + selector: selectorFromNode(target), nid: nidFromNode(target) }, inspectedTabId, diff --git a/src/js/scriptlets/cosmetic-logger.js b/src/js/scriptlets/cosmetic-logger.js index 722e03925..28d42fcb7 100644 --- a/src/js/scriptlets/cosmetic-logger.js +++ b/src/js/scriptlets/cosmetic-logger.js @@ -19,42 +19,27 @@ Home: https://github.com/gorhill/uBlock */ -/******************************************************************************/ - -(function() { - 'use strict'; /******************************************************************************/ -if ( typeof vAPI !== 'object' ) { - return; -} +(function() { /******************************************************************************/ -var loggedSelectors = vAPI.loggedSelectors || {}; - -var injectedSelectors = []; -var reProperties = /\s*\{[^}]+\}\s*/; -var i; -var styles = vAPI.styles || []; - -i = styles.length; -while ( i-- ) { - injectedSelectors = injectedSelectors.concat(styles[i].textContent.replace(reProperties, '').split(/\s*,\n\s*/)); -} - -if ( injectedSelectors.length === 0 ) { +if ( typeof vAPI !== 'object' || typeof vAPI.domFilterer !== 'object' ) { return; } -var matchedSelectors = []; -var selector; +var loggedSelectors = vAPI.loggedSelectors || {}, + matchedSelectors = [], + selectors, i, selector, entry, nodes, j; -i = injectedSelectors.length; +// CSS-based selectors. +selectors = vAPI.domFilterer.simpleSelectors.concat(vAPI.domFilterer.complexSelectors); +i = selectors.length; while ( i-- ) { - selector = injectedSelectors[i]; + selector = selectors[i]; if ( loggedSelectors.hasOwnProperty(selector) ) { continue; } @@ -62,27 +47,67 @@ while ( i-- ) { continue; } loggedSelectors[selector] = true; - // https://github.com/gorhill/uBlock/issues/1015 - // Discard `:root ` prefix. - matchedSelectors.push(selector.slice(6)); + matchedSelectors.push(selector); +} + +// `:has`-based selectors. +selectors = vAPI.domFilterer.simpleHasSelectors.concat(vAPI.domFilterer.complexHasSelectors); +i = selectors.length; +while ( i-- ) { + entry = selectors[i]; + selector = entry.a + ':has(' + entry.b + ')'; + if ( loggedSelectors.hasOwnProperty(selector) ) { + continue; + } + nodes = document.querySelectorAll(entry.a); + j = nodes.length; + while ( j-- ) { + if ( nodes[j].querySelector(entry.b) !== null ) { + loggedSelectors[selector] = true; + matchedSelectors.push(selector); + break; + } + } +} + +// `:xpath`-based selectors. +var xpr = null, + xpathexpr; +selectors = vAPI.domFilterer.xpathSelectors; +i = selectors.length; +while ( i-- ) { + xpathexpr = selectors[i]; + selector = ':xpath(' + xpathexpr + ')'; + if ( loggedSelectors.hasOwnProperty(selector) ) { + continue; + } + xpr = document.evaluate( + 'boolean(' + xpathexpr + ')', + document, + null, + XPathResult.BOOLEAN_TYPE, + xpr + ); + if ( xpr.booleanValue ) { + loggedSelectors[selector] = true; + matchedSelectors.push(selector); + } } vAPI.loggedSelectors = loggedSelectors; -/******************************************************************************/ - -vAPI.messaging.send( - 'scriptlets', - { - what: 'logCosmeticFilteringData', - frameURL: window.location.href, - frameHostname: window.location.hostname, - matchedSelectors: matchedSelectors - } -); +if ( matchedSelectors.length ) { + vAPI.messaging.send( + 'scriptlets', + { + what: 'logCosmeticFilteringData', + frameURL: window.location.href, + frameHostname: window.location.hostname, + matchedSelectors: matchedSelectors + } + ); +} /******************************************************************************/ })(); - -/******************************************************************************/ diff --git a/src/js/scriptlets/cosmetic-off.js b/src/js/scriptlets/cosmetic-off.js index a99ae9c45..80c5cfd9f 100644 --- a/src/js/scriptlets/cosmetic-off.js +++ b/src/js/scriptlets/cosmetic-off.js @@ -1,7 +1,7 @@ /******************************************************************************* - uBlock - a browser extension to block requests. - Copyright (C) 2015 Raymond Hill + uBlock Origin - a browser extension to block requests. + Copyright (C) 2015-2016 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 @@ -19,75 +19,51 @@ Home: https://github.com/gorhill/uBlock */ -/******************************************************************************/ - -(function() { - 'use strict'; /******************************************************************************/ -if ( typeof vAPI !== 'object' ) { - return; -} - -/******************************************************************************/ - -var styles = vAPI.styles; - -if ( Array.isArray(styles) === false ) { - return; -} - -var sessionId = vAPI.sessionId; - -/******************************************************************************/ - -// Remove all cosmetic filtering-related styles from the DOM - -var selectors = []; -var reProperties = /\s*\{[^}]+\}\s*/; -var style, i; - -i = styles.length; -while ( i-- ) { - style = styles[i]; - selectors.push(style.textContent.replace(reProperties, '')); - if ( style.sheet !== null ) { - style.sheet.disabled = true; - style[vAPI.sessionId] = true; +(function() { + if ( typeof vAPI !== 'object' || typeof vAPI.domFilterer !== 'object' ) { + return; } -} -if ( selectors.length === 0 ) { - return; -} + var styles = vAPI.domFilterer.styleTags; -var elems = []; -try { - elems = document.querySelectorAll(selectors.join(',')); -} catch (e) { -} -i = elems.length; - -var elem, shadow; -while ( i-- ) { - elem = elems[i]; - shadow = elem.shadowRoot; - if ( shadow === undefined ) { - style = elem.style; - if ( typeof style === 'object' || typeof style.removeProperty === 'function' ) { - style.removeProperty('display'); + // Disable all cosmetic filtering-related styles from the DOM + var i = styles.length, style; + while ( i-- ) { + style = styles[i]; + if ( style.sheet !== null ) { + style.sheet.disabled = true; + style[vAPI.sessionId] = true; } - continue; } - if ( shadow !== null && shadow.className === sessionId && shadow.firstElementChild === null ) { - shadow.appendChild(document.createElement('content')); + + var elems = []; + try { + elems = document.querySelectorAll('[' + vAPI.domFilterer.hiddenId + ']'); + } catch (e) { + } + i = elems.length; + while ( i-- ) { + var elem = elems[i]; + var shadow = elem.shadowRoot; + if ( shadow === undefined ) { + style = elem.style; + if ( typeof style === 'object' || typeof style.removeProperty === 'function' ) { + style.removeProperty('display'); + } + continue; + } + if ( + shadow !== null && + shadow.className === vAPI.domFilterer.shadowId && + shadow.firstElementChild === null + ) { + shadow.appendChild(document.createElement('content')); + } } -} - -/******************************************************************************/ + vAPI.domFilterer.toggleOff(); })(); - -/******************************************************************************/ diff --git a/src/js/scriptlets/cosmetic-on.js b/src/js/scriptlets/cosmetic-on.js index 78d12f34d..7f2314a00 100644 --- a/src/js/scriptlets/cosmetic-on.js +++ b/src/js/scriptlets/cosmetic-on.js @@ -1,7 +1,7 @@ /******************************************************************************* - uBlock - a browser extension to block requests. - Copyright (C) 2015 Raymond Hill + uBlock Origin - a browser extension to block requests. + Copyright (C) 2015-2016 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 @@ -19,75 +19,51 @@ Home: https://github.com/gorhill/uBlock */ -/******************************************************************************/ - -(function() { - 'use strict'; /******************************************************************************/ -if ( typeof vAPI !== 'object' ) { - return; -} - -/******************************************************************************/ - -var styles = vAPI.styles; - -if ( Array.isArray(styles) === false ) { - return; -} - -var sessionId = vAPI.sessionId; - -/******************************************************************************/ - -// Insert all cosmetic filtering-related style tags in the DOM - -var selectors = []; -var reProperties = /\s*\{[^}]+\}\s*/; -var style, i; - -i = styles.length; -while ( i-- ) { - style = styles[i]; - selectors.push(style.textContent.replace(reProperties, '')); - if ( style.sheet !== null ) { - style.sheet.disabled = false; - style[vAPI.sessionId] = undefined; +(function() { + if ( typeof vAPI !== 'object' || typeof vAPI.domFilterer !== 'object' ) { + return; } -} -if ( selectors.length === 0 ) { - return; -} + // Insert all cosmetic filtering-related style tags in the DOM -var elems = []; -try { - elems = document.querySelectorAll(selectors.join(',')); -} catch (e) { -} - -var elem, shadow; -i = elems.length; -while ( i-- ) { - elem = elems[i]; - shadow = elem.shadowRoot; - if ( shadow === undefined ) { - style = elems[i].style; - if ( typeof style === 'object' || typeof style.removeProperty === 'function' ) { - style.setProperty('display', 'none', 'important'); + var styles = vAPI.domFilterer.styleTags; + var i = styles.length, style; + while ( i-- ) { + style = styles[i]; + if ( style.sheet !== null ) { + style.sheet.disabled = false; + style[vAPI.sessionId] = undefined; } - continue; } - if ( shadow !== null && shadow.className === sessionId && shadow.firstElementChild !== null ) { - shadow.removeChild(shadow.firstElementChild); + + var elems = []; + try { + elems = document.querySelectorAll('[' + vAPI.domFilterer.hiddenId + ']'); + } catch (e) { + } + i = elems.length; + while ( i-- ) { + var elem = elems[i]; + var shadow = elem.shadowRoot; + if ( shadow === undefined ) { + style = elems[i].style; + if ( typeof style === 'object' || typeof style.removeProperty === 'function' ) { + style.setProperty('display', 'none', 'important'); + } + continue; + } + if ( + shadow !== null && + shadow.className === vAPI.domFilterer.shadowId && + shadow.firstElementChild !== null + ) { + shadow.removeChild(shadow.firstElementChild); + } } -} - -/******************************************************************************/ + vAPI.domFilterer.toggleOn(); })(); - -/******************************************************************************/ diff --git a/src/js/scriptlets/cosmetic-survey.js b/src/js/scriptlets/cosmetic-survey.js index d50c9c5b6..14f921e88 100644 --- a/src/js/scriptlets/cosmetic-survey.js +++ b/src/js/scriptlets/cosmetic-survey.js @@ -19,51 +19,29 @@ Home: https://github.com/gorhill/uBlock */ -/******************************************************************************/ - -(function() { - 'use strict'; /******************************************************************************/ -if ( typeof vAPI !== 'object' ) { - return; -} - -/******************************************************************************/ - -// Insert all cosmetic filtering-related style tags in the DOM - -var injectedSelectors = []; -var filteredElementCount = 0; - -var reProperties = /\s*\{[^}]+\}\s*/; -var i; - -var styles = vAPI.styles || []; -i = styles.length; -while ( i-- ) { - injectedSelectors = injectedSelectors.concat(styles[i].textContent.replace(reProperties, '').split(/\s*,\n\s*/)); -} - -if ( injectedSelectors.length !== 0 ) { - filteredElementCount = document.querySelectorAll(injectedSelectors.join(',')).length; -} - -/******************************************************************************/ - -vAPI.messaging.send( - 'scriptlets', - { - what: 'liveCosmeticFilteringData', - pageURL: window.location.href, - filteredElementCount: filteredElementCount +(function() { + if ( typeof vAPI !== 'object' || typeof vAPI.domFilterer !== 'object' ) { + return; } -); -/******************************************************************************/ + var xpr = document.evaluate( + 'count(//*[@' + vAPI.domFilterer.hiddenId + '])', + document, + null, + XPathResult.NUMBER_TYPE, + null + ); + vAPI.messaging.send( + 'scriptlets', + { + what: 'liveCosmeticFilteringData', + pageURL: window.location.href, + filteredElementCount: xpr && xpr.numberValue || 0 + } + ); })(); - -/******************************************************************************/ diff --git a/src/js/scriptlets/dom-inspector.js b/src/js/scriptlets/dom-inspector.js index d2ce5784b..1d680679f 100644 --- a/src/js/scriptlets/dom-inspector.js +++ b/src/js/scriptlets/dom-inspector.js @@ -28,13 +28,14 @@ /******************************************************************************/ -if ( typeof vAPI !== 'object' ) { +if ( typeof vAPI !== 'object' || typeof vAPI.domFilterer !== 'object' ) { return; } /******************************************************************************/ var sessionId = vAPI.sessionId; +var shadowId = vAPI.domFilterer.shadowId; if ( document.querySelector('iframe.dom-inspector.' + sessionId) !== null ) { return; @@ -690,19 +691,20 @@ var cosmeticFilterMapper = (function() { // Not all target nodes have necessarily been force-hidden, // do it now so that the inspector does not unhide these // nodes when disabling style tags. - var nodesFromStyleTag = function(styleTag, rootNode) { - var filterMap = nodeToCosmeticFilterMap; - var styleText = styleTag.textContent; - var selectors = styleText.slice(0, styleText.lastIndexOf('\n')).split(/,\n/); - var i = selectors.length; - var selector, nodes, j, node; + var nodesFromStyleTag = function(rootNode) { + var filterMap = nodeToCosmeticFilterMap, + selectors, selector, + nodes, node, + entries, entry, + i, j; + + // CSS-based selectors: simple one. + selectors = vAPI.domFilterer.simpleSelectors; + i = selectors.length; while ( i-- ) { - // https://github.com/gorhill/uBlock/issues/1015 - // Discard `:root ` prefix. - selector = selectors[i].slice(6); + selector = selectors[i]; if ( filterMap.has(rootNode) === false && rootNode[matchesFnName](selector) ) { filterMap.set(rootNode, selector); - hideNode(node); } nodes = rootNode.querySelectorAll(selector); j = nodes.length; @@ -710,24 +712,106 @@ var cosmeticFilterMapper = (function() { node = nodes[j]; if ( filterMap.has(node) === false ) { filterMap.set(node, selector); - hideNode(node); + } + } + } + + // CSS-based selectors: complex one (must query from doc root). + selectors = vAPI.domFilterer.complexSelectors; + i = selectors.length; + while ( i-- ) { + selector = selectors[i]; + nodes = document.querySelectorAll(selector); + j = nodes.length; + while ( j-- ) { + node = nodes[j]; + if ( filterMap.has(node) === false ) { + filterMap.set(node, selector); + } + } + } + + // `:has()`-based selectors: simple ones. + entries = vAPI.domFilterer.simpleHasSelectors; + i = entries.length; + while ( i-- ) { + entry = entries[i]; + selector = entries.a + ':has(' + entries.b + ')'; + if ( + filterMap.has(rootNode) === false && + rootNode[matchesFnName](entry.a) && + rootNode.querySelector(entry.b) !== null + ) { + filterMap.set(rootNode, selector); + } + nodes = rootNode.querySelectorAll(entries.a); + j = nodes.length; + while ( j-- ) { + node = nodes[j]; + if ( + filterMap.has(node) === false && + node.querySelector(entry.b) !== null + ) { + filterMap.set(node, selector); + } + } + } + + // `:has()`-based selectors: complex ones (must query from doc root). + entries = vAPI.domFilterer.complexHasSelectors; + i = entries.length; + while ( i-- ) { + entry = entries[i]; + selector = entries.a + ':has(' + entries.b + ')'; + nodes = document.querySelectorAll(entries.a); + j = nodes.length; + while ( j-- ) { + node = nodes[j]; + if ( + filterMap.has(node) === false && + node.querySelector(entry.b) !== null + ) { + filterMap.set(node, selector); + } + } + } + + // `:xpath()`-based selectors. + var xpr; + entries = vAPI.domFilterer.xpathSelectors; + i = entries.length; + while ( i-- ) { + entry = entries[i]; + selector = ':xpath(' + entry + ')'; + xpr = document.evaluate( + entry, + document, + null, + XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, + xpr + ); + j = xpr.snapshotLength; + while ( j-- ) { + node = xpr.snapshotItem(j); + if ( filterMap.has(node) === false ) { + filterMap.set(node, selector); } } } }; var incremental = function(rootNode) { - var styleTags = vAPI.styles || []; + var styleTags = vAPI.domFilterer.styleTags || []; var styleTag; var i = styleTags.length; while ( i-- ) { styleTag = styleTags[i]; - nodesFromStyleTag(styleTag, rootNode); if ( styleTag.sheet !== null ) { styleTag.sheet.disabled = true; styleTag[vAPI.sessionId] = true; } } + nodesFromStyleTag(rootNode); }; var reset = function() { @@ -736,7 +820,7 @@ var cosmeticFilterMapper = (function() { }; var shutdown = function() { - var styleTags = vAPI.styles || []; + var styleTags = vAPI.domFilterer.styleTags || []; var styleTag; var i = styleTags.length; while ( i-- ) { @@ -761,12 +845,51 @@ var elementsFromSelector = function(selector, context) { if ( !context ) { context = document; } - var out = []; + var out; + if ( selector.indexOf(':') !== -1 ) { + out = elementsFromSpecialSelector(selector); + if ( out !== undefined ) { + return out; + } + } + // plain CSS selector try { out = context.querySelectorAll(selector); } catch (ex) { } - return out; + return out || []; +}; + +var elementsFromSpecialSelector = function(selector) { + var out = [], i; + var matches = /^(.+?):has\((.+?)\)$/.exec(selector); + if ( matches !== null ) { + var nodes = document.querySelector(matches[1]); + i = nodes.length; + while ( i-- ) { + var node = nodes[i]; + if ( node.querySelector(matches[2]) !== null ) { + out.push(node); + } + } + return out; + } + + matches = /^:xpath\((.+?)\)$/.exec(selector); + if ( matches !== null ) { + var xpr = document.evaluate( + matches[1], + document, + null, + XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, + null + ); + i = xpr.snapshotLength; + while ( i-- ) { + out.push(xpr.snapshotItem(i)); + } + return out; + } }; /******************************************************************************/ @@ -970,7 +1093,7 @@ var showNode = function(node, v1, v2) { } else { node.style.setProperty('display', v1, v2); } - } else if ( shadow !== null && shadow.className === sessionId && shadow.firstElementChild === null ) { + } else if ( shadow !== null && shadow.className === shadowId && shadow.firstElementChild === null ) { shadow.appendChild(document.createElement('content')); } }; @@ -983,7 +1106,7 @@ var hideNode = function(node) { node.style.setProperty('display', 'none', 'important'); return; } - if ( shadow !== null && shadow.className === sessionId ) { + if ( shadow !== null && shadow.className === shadowId ) { if ( shadow.firstElementChild !== null ) { shadow.removeChild(shadow.firstElementChild); } @@ -995,7 +1118,7 @@ var hideNode = function(node) { } catch (ex) { return; } - shadow.className = sessionId; + shadow.className = shadowId; }; /******************************************************************************/ @@ -1052,6 +1175,12 @@ var onMessage = function(request) { highlightElements(); break; + case 'toggleFilter': + highlightedElementLists[0] = selectNodes(request.filter, request.nid); + toggleNodes(highlightedElementLists[0], request.original, request.target); + highlightElements(true); + break; + case 'toggleNodes': highlightedElementLists[0] = selectNodes(request.selector, request.nid); toggleNodes(highlightedElementLists[0], request.original, request.target);