refactor content script code + add support for new `:has` & `:xpath` filters

Aside extending cosmetic filtering abilities, I expect this will
also take care of some long standing issues (I will have to find them
and mark them as "resolved" by this commit, as time allow).
This commit is contained in:
gorhill 2016-06-27 19:09:04 -04:00
parent 72fdce64f0
commit 6c513629bf
14 changed files with 1003 additions and 896 deletions

View File

@ -27,16 +27,10 @@
"content_scripts": [ "content_scripts": [
{ {
"matches": ["http://*/*", "https://*/*"], "matches": ["http://*/*", "https://*/*"],
"js": ["js/vapi-client.js", "js/contentscript-start.js"], "js": ["js/vapi-client.js", "js/contentscript.js"],
"run_at": "document_start", "run_at": "document_start",
"all_frames": true "all_frames": true
}, },
{
"matches": ["http://*/*", "https://*/*"],
"js": ["js/contentscript-end.js"],
"run_at": "document_end",
"all_frames": true
},
{ {
"matches": ["http://*/*", "https://*/*"], "matches": ["http://*/*", "https://*/*"],
"js": ["js/scriptlets/subscriber.js"], "js": ["js/scriptlets/subscriber.js"],

View File

@ -1076,16 +1076,6 @@ vAPI.onLoadAllCompleted = function() {
var scriptDone = function() { var scriptDone = function() {
vAPI.lastError(); 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) { var scriptStart = function(tabId) {
vAPI.tabs.injectScript(tabId, { vAPI.tabs.injectScript(tabId, {
file: 'js/vapi-client.js', file: 'js/vapi-client.js',
@ -1093,10 +1083,10 @@ vAPI.onLoadAllCompleted = function() {
runAt: 'document_idle' runAt: 'document_idle'
}, function(){ }); }, function(){ });
vAPI.tabs.injectScript(tabId, { vAPI.tabs.injectScript(tabId, {
file: 'js/contentscript-start.js', file: 'js/contentscript.js',
allFrames: true, allFrames: true,
runAt: 'document_idle' runAt: 'document_idle'
}, function(){ scriptEnd(tabId); }); }, scriptDone);
}; };
var bindToTabs = function(tabs) { var bindToTabs = function(tabs) {
var µb = µBlock; var µb = µBlock;

View File

@ -21,14 +21,14 @@
/* global HTMLDocument, XMLDocument */ /* global HTMLDocument, XMLDocument */
'use strict';
// For non background pages // For non background pages
/******************************************************************************/ /******************************************************************************/
(function(self) { (function(self) {
'use strict';
/******************************************************************************/ /******************************************************************************/
/******************************************************************************/ /******************************************************************************/

View File

@ -1,7 +1,7 @@
/******************************************************************************* /*******************************************************************************
µBlock - a browser extension to block requests. uBlock Origin - a browser extension to block requests.
Copyright (C) 2014 The µBlock authors Copyright (C) 2014-2016 The uBlock Origin authors
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU General Public License as published by
@ -442,7 +442,7 @@ var contentObserver = {
try { try {
lss(this.contentBaseURI + 'vapi-client.js', sandbox); lss(this.contentBaseURI + 'vapi-client.js', sandbox);
lss(this.contentBaseURI + 'contentscript-start.js', sandbox); lss(this.contentBaseURI + 'contentscript.js', sandbox);
} catch (ex) { } catch (ex) {
//console.exception(ex.msg, ex.stack); //console.exception(ex.msg, ex.stack);
return; return;
@ -451,7 +451,6 @@ var contentObserver = {
let docReady = (e) => { let docReady = (e) => {
let doc = e.target; let doc = e.target;
doc.removeEventListener(e.type, docReady, true); doc.removeEventListener(e.type, docReady, true);
lss(this.contentBaseURI + 'contentscript-end.js', sandbox);
if ( if (
doc.querySelector('a[href^="abp:"],a[href^="https://subscribe.adblockplus.org/?"]') || doc.querySelector('a[href^="abp:"],a[href^="https://subscribe.adblockplus.org/?"]') ||

View File

@ -27,16 +27,10 @@
"content_scripts": [ "content_scripts": [
{ {
"matches": ["http://*/*", "https://*/*"], "matches": ["http://*/*", "https://*/*"],
"js": ["js/vapi-client.js", "js/contentscript-start.js"], "js": ["js/vapi-client.js", "js/contentscript.js"],
"run_at": "document_start", "run_at": "document_start",
"all_frames": true "all_frames": true
}, },
{
"matches": ["http://*/*", "https://*/*"],
"js": ["js/contentscript-end.js"],
"run_at": "document_end",
"all_frames": true
},
{ {
"matches": ["http://*/*", "https://*/*"], "matches": ["http://*/*", "https://*/*"],
"js": ["js/scriptlets/subscriber.js"], "js": ["js/scriptlets/subscriber.js"],

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
/******************************************************************************* /*******************************************************************************
µBlock - a browser extension to block requests. uBlock Origin - a browser extension to block requests.
Copyright (C) 2014 Raymond Hill Copyright (C) 2014-2016 Raymond Hill
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU General Public License as published by
@ -20,14 +20,14 @@
*/ */
/* jshint bitwise: false */ /* jshint bitwise: false */
/* global punycode, µBlock */ /* global punycode */
'use strict';
/******************************************************************************/ /******************************************************************************/
µBlock.cosmeticFilteringEngine = (function(){ µBlock.cosmeticFilteringEngine = (function(){
'use strict';
/******************************************************************************/ /******************************************************************************/
var µb = µBlock; 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 // Any selector specific to a hostname
// Examples: // Examples:
// search.snapdo.com###ABottomD // search.snapdo.com###ABottomD
@ -235,6 +201,40 @@ FilterEntity.fromSelfie = function(s) {
return new FilterEntity(decode(s.slice(0, pos)), s.slice(pos + 1)); 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.invalid = false;
this.cosmetic = true; this.cosmetic = true;
this.reScriptTagFilter = /^script:(contains|inject)\((.+?)\)$/; this.reScriptTagFilter = /^script:(contains|inject)\((.+?)\)$/;
this.reNeedHostname = /^(?:.+?:has|:xpath)\(.+?\)$/;
}; };
/******************************************************************************/ /******************************************************************************/
FilterParser.prototype.reset = function() { FilterParser.prototype.reset = function() {
this.raw = '';
this.prefix = this.suffix = this.style = ''; this.prefix = this.suffix = this.style = '';
this.unhide = 0; this.unhide = 0;
this.hostnames.length = 0; this.hostnames.length = 0;
@ -264,6 +266,8 @@ FilterParser.prototype.parse = function(raw) {
// important! // important!
this.reset(); this.reset();
this.raw = raw;
// Find the bounds of the anchor. // Find the bounds of the anchor.
var lpos = raw.indexOf('#'); var lpos = raw.indexOf('#');
if ( lpos === -1 ) { if ( lpos === -1 ) {
@ -349,6 +353,16 @@ FilterParser.prototype.parse = function(raw) {
this.hostnames = this.prefix.split(/\s*,\s*/); 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 // Script tag filters: pre-process them so that can be used with minimal
// overhead in the content script. // overhead in the content script.
// Examples: // Examples:
@ -357,10 +371,16 @@ FilterParser.prototype.parse = function(raw) {
// focus.de##script:inject(uabinject-defuser.js) // focus.de##script:inject(uabinject-defuser.js)
var matches = this.reScriptTagFilter.exec(this.suffix); var matches = this.reScriptTagFilter.exec(this.suffix);
if ( matches === null ) { if ( matches !== null ) {
return this; return this.parseScriptTagFilter(matches);
} }
return this;
};
/******************************************************************************/
FilterParser.prototype.parseScriptTagFilter = function(matches) {
// Currently supported only as non-generic selector. Also, exception // Currently supported only as non-generic selector. Also, exception
// script tag filter makes no sense, ignore. // script tag filter makes no sense, ignore.
if ( this.hostnames.length === 0 || this.unhide === 1 ) { if ( this.hostnames.length === 0 || this.unhide === 1 ) {
@ -378,7 +398,7 @@ FilterParser.prototype.parse = function(raw) {
} else { } else {
token = token.slice(1, -1); token = token.slice(1, -1);
if ( isBadRegex(token) ) { 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; this.invalid = true;
} }
} }
@ -676,19 +696,33 @@ FilterContainer.prototype.reset = function() {
FilterContainer.prototype.isValidSelector = (function() { FilterContainer.prototype.isValidSelector = (function() {
var div = document.createElement('div'); 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`: // Not all browsers support `Element.matches`:
// http://caniuse.com/#feat=matchesselector // http://caniuse.com/#feat=matchesselector
if ( typeof div.matches !== 'function' ) { if ( matchesProp === '' ) {
return function() { return function() {
return true; return true;
}; };
} }
var reHasSelector = /^(.+?):has\((.+?)\)$/;
var reXpathSelector = /^:xpath\((.+?)\)$/;
return function(s) { return function(s) {
try { try {
// https://github.com/gorhill/uBlock/issues/693 // 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 // Discard new ABP's `-abp-properties` directive until it is
// implemented (if ever). // implemented (if ever).
if ( s.indexOf('[-abp-properties=') === -1 ) { if ( s.indexOf('[-abp-properties=') === -1 ) {
@ -697,6 +731,24 @@ FilterContainer.prototype.isValidSelector = (function() {
} catch (e) { } catch (e) {
} }
// We reach this point very rarely. // 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('script') ) {
if ( s.startsWith('?', 6) || s.startsWith('+', 6) ) { if ( s.startsWith('?', 6) || s.startsWith('+', 6) ) {
return true; return true;
@ -924,7 +976,7 @@ FilterContainer.prototype.fromCompiledContent = function(text, lineBeg, skip) {
fields = line.split('\v'); fields = line.split('\v');
// h ir twitter.com .promoted-tweet // h [\t] ir [\t] twitter.com [\t] .promoted-tweet
if ( fields[0] === 'h' ) { if ( fields[0] === 'h' ) {
// Special filter: script tags. Not a real CSS selector. // Special filter: script tags. Not a real CSS selector.
if ( fields[3].startsWith('script') ) { if ( fields[3].startsWith('script') ) {
@ -943,8 +995,8 @@ FilterContainer.prototype.fromCompiledContent = function(text, lineBeg, skip) {
continue; continue;
} }
// lg 105 .largeAd // lg [\t] 105 [\t] .largeAd
// lg+ 2jx .Mpopup + #Mad > #MadZone // lg+ [\t] 2jx [\t] .Mpopup + #Mad > #MadZone
if ( fields[0] === 'lg' || fields[0] === 'lg+' ) { if ( fields[0] === 'lg' || fields[0] === 'lg+' ) {
filter = fields[0] === 'lg' ? filter = fields[0] === 'lg' ?
filterPlain : filterPlain :
@ -960,7 +1012,7 @@ FilterContainer.prototype.fromCompiledContent = function(text, lineBeg, skip) {
continue; continue;
} }
// entity selector // entity [\t] selector
if ( fields[0] === 'e' ) { if ( fields[0] === 'e' ) {
// Special filter: script tags. Not a real CSS selector. // Special filter: script tags. Not a real CSS selector.
if ( fields[2].startsWith('script') ) { if ( fields[2].startsWith('script') ) {
@ -1392,8 +1444,6 @@ FilterContainer.prototype.retrieveGenericSelectors = function(request) {
hideHigh: this.highHighGenericHide, hideHigh: this.highHighGenericHide,
hideHighCount: this.highHighGenericHideCount hideHighCount: this.highHighGenericHideCount
}; };
// https://github.com/chrisaljoudi/uBlock/issues/497
r.donthide = this.genericDonthide;
} }
var hideSelectors = r.hide; 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 // r.ready will tell the content script the cosmetic filtering engine is
// up and ready. // up and ready.
// https://github.com/chrisaljoudi/uBlock/issues/497
// Generic exception filters are to be applied on all pages.
var r = { var r = {
ready: this.frozen, ready: this.frozen,
domain: domain, domain: domain,
entity: pos === -1 ? domain : domain.slice(0, pos - domain.length), entity: pos === -1 ? domain : domain.slice(0, pos - domain.length),
skipCosmeticFiltering: this.acceptedCount === 0, skipCosmeticFiltering: this.acceptedCount === 0,
cosmeticHide: [], cosmeticHide: [],
cosmeticDonthide: [], cosmeticDonthide: this.genericDonthide,
netHide: [], netHide: [],
netCollapse: µb.userSettings.collapseBlocked, netCollapse: µb.userSettings.collapseBlocked,
scripts: this.retrieveScriptTags(domain, hostname) scripts: this.retrieveScriptTags(domain, hostname)

View File

@ -1,7 +1,7 @@
/******************************************************************************* /*******************************************************************************
uBlock Origin - a browser extension to block requests. 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 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 it under the terms of the GNU General Public License as published by
@ -19,14 +19,14 @@
Home: https://github.com/gorhill/uBlock Home: https://github.com/gorhill/uBlock
*/ */
/* global vAPI, uDom */ /* global uDom */
'use strict';
/******************************************************************************/ /******************************************************************************/
(function() { (function() {
'use strict';
/******************************************************************************/ /******************************************************************************/
var showdomButton = uDom.nodeFromId('showdom'); 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 selector = '';
var code; var code;
if ( nth === undefined ) {
nth = 1;
}
while ( node !== null ) { while ( node !== null ) {
if ( node.localName === 'li' ) { if ( node.localName === 'li' ) {
code = node.querySelector('code:nth-of-type(' + nth + ')'); code = node.querySelector('code');
if ( code !== null ) { if ( code !== null ) {
selector = code.textContent + ' > ' + selector; selector = code.textContent + ' > ' + selector;
if ( selector.indexOf('#') !== -1 ) { if ( selector.indexOf('#') !== -1 ) {
break; break;
} }
nth = 1;
} }
} }
node = node.parentElement; 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 nidFromNode = function(node) {
var li = node; var li = node;
while ( li !== null ) { while ( li !== null ) {
@ -482,10 +493,11 @@ var onClick = function(ev) {
messaging.sendTo( messaging.sendTo(
'loggerUI', 'loggerUI',
{ {
what: 'toggleNodes', what: 'toggleFilter',
original: false, original: false,
target: target.classList.toggle('off'), target: target.classList.toggle('off'),
selector: selectorFromNode(target, 2), selector: selectorFromNode(target),
filter: selectorFromFilter(target),
nid: '' nid: ''
}, },
inspectedTabId, inspectedTabId,
@ -504,7 +516,7 @@ var onClick = function(ev) {
what: 'toggleNodes', what: 'toggleNodes',
original: true, original: true,
target: target.classList.toggle('off') === false, target: target.classList.toggle('off') === false,
selector: selectorFromNode(target, 1), selector: selectorFromNode(target),
nid: nidFromNode(target) nid: nidFromNode(target)
}, },
inspectedTabId, inspectedTabId,

View File

@ -19,42 +19,27 @@
Home: https://github.com/gorhill/uBlock Home: https://github.com/gorhill/uBlock
*/ */
/******************************************************************************/
(function() {
'use strict'; 'use strict';
/******************************************************************************/ /******************************************************************************/
if ( typeof vAPI !== 'object' ) { (function() {
return;
}
/******************************************************************************/ /******************************************************************************/
var loggedSelectors = vAPI.loggedSelectors || {}; if ( typeof vAPI !== 'object' || typeof vAPI.domFilterer !== 'object' ) {
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 ) {
return; return;
} }
var matchedSelectors = []; var loggedSelectors = vAPI.loggedSelectors || {},
var selector; 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-- ) { while ( i-- ) {
selector = injectedSelectors[i]; selector = selectors[i];
if ( loggedSelectors.hasOwnProperty(selector) ) { if ( loggedSelectors.hasOwnProperty(selector) ) {
continue; continue;
} }
@ -62,15 +47,56 @@ while ( i-- ) {
continue; continue;
} }
loggedSelectors[selector] = true; loggedSelectors[selector] = true;
// https://github.com/gorhill/uBlock/issues/1015 matchedSelectors.push(selector);
// Discard `:root ` prefix. }
matchedSelectors.push(selector.slice(6));
// `: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.loggedSelectors = loggedSelectors;
/******************************************************************************/ if ( matchedSelectors.length ) {
vAPI.messaging.send( vAPI.messaging.send(
'scriptlets', 'scriptlets',
{ {
@ -80,9 +106,8 @@ vAPI.messaging.send(
matchedSelectors: matchedSelectors matchedSelectors: matchedSelectors
} }
); );
}
/******************************************************************************/ /******************************************************************************/
})(); })();
/******************************************************************************/

View File

@ -1,7 +1,7 @@
/******************************************************************************* /*******************************************************************************
uBlock - a browser extension to block requests. 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 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 it under the terms of the GNU General Public License as published by
@ -19,61 +19,36 @@
Home: https://github.com/gorhill/uBlock Home: https://github.com/gorhill/uBlock
*/ */
/******************************************************************************/
(function() {
'use strict'; 'use strict';
/******************************************************************************/ /******************************************************************************/
if ( typeof vAPI !== 'object' ) { (function() {
if ( typeof vAPI !== 'object' || typeof vAPI.domFilterer !== 'object' ) {
return; return;
} }
/******************************************************************************/ var styles = vAPI.domFilterer.styleTags;
var styles = vAPI.styles; // Disable all cosmetic filtering-related styles from the DOM
var i = styles.length, style;
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-- ) { while ( i-- ) {
style = styles[i]; style = styles[i];
selectors.push(style.textContent.replace(reProperties, ''));
if ( style.sheet !== null ) { if ( style.sheet !== null ) {
style.sheet.disabled = true; style.sheet.disabled = true;
style[vAPI.sessionId] = true; style[vAPI.sessionId] = true;
} }
} }
if ( selectors.length === 0 ) {
return;
}
var elems = []; var elems = [];
try { try {
elems = document.querySelectorAll(selectors.join(',')); elems = document.querySelectorAll('[' + vAPI.domFilterer.hiddenId + ']');
} catch (e) { } catch (e) {
} }
i = elems.length; i = elems.length;
var elem, shadow;
while ( i-- ) { while ( i-- ) {
elem = elems[i]; var elem = elems[i];
shadow = elem.shadowRoot; var shadow = elem.shadowRoot;
if ( shadow === undefined ) { if ( shadow === undefined ) {
style = elem.style; style = elem.style;
if ( typeof style === 'object' || typeof style.removeProperty === 'function' ) { if ( typeof style === 'object' || typeof style.removeProperty === 'function' ) {
@ -81,13 +56,14 @@ while ( i-- ) {
} }
continue; continue;
} }
if ( shadow !== null && shadow.className === sessionId && shadow.firstElementChild === null ) { if (
shadow !== null &&
shadow.className === vAPI.domFilterer.shadowId &&
shadow.firstElementChild === null
) {
shadow.appendChild(document.createElement('content')); shadow.appendChild(document.createElement('content'));
} }
} }
/******************************************************************************/ vAPI.domFilterer.toggleOff();
})(); })();
/******************************************************************************/

View File

@ -1,7 +1,7 @@
/******************************************************************************* /*******************************************************************************
uBlock - a browser extension to block requests. 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 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 it under the terms of the GNU General Public License as published by
@ -19,61 +19,36 @@
Home: https://github.com/gorhill/uBlock Home: https://github.com/gorhill/uBlock
*/ */
/******************************************************************************/
(function() {
'use strict'; 'use strict';
/******************************************************************************/ /******************************************************************************/
if ( typeof vAPI !== 'object' ) { (function() {
if ( typeof vAPI !== 'object' || typeof vAPI.domFilterer !== 'object' ) {
return; 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 // Insert all cosmetic filtering-related style tags in the DOM
var selectors = []; var styles = vAPI.domFilterer.styleTags;
var reProperties = /\s*\{[^}]+\}\s*/; var i = styles.length, style;
var style, i;
i = styles.length;
while ( i-- ) { while ( i-- ) {
style = styles[i]; style = styles[i];
selectors.push(style.textContent.replace(reProperties, ''));
if ( style.sheet !== null ) { if ( style.sheet !== null ) {
style.sheet.disabled = false; style.sheet.disabled = false;
style[vAPI.sessionId] = undefined; style[vAPI.sessionId] = undefined;
} }
} }
if ( selectors.length === 0 ) {
return;
}
var elems = []; var elems = [];
try { try {
elems = document.querySelectorAll(selectors.join(',')); elems = document.querySelectorAll('[' + vAPI.domFilterer.hiddenId + ']');
} catch (e) { } catch (e) {
} }
var elem, shadow;
i = elems.length; i = elems.length;
while ( i-- ) { while ( i-- ) {
elem = elems[i]; var elem = elems[i];
shadow = elem.shadowRoot; var shadow = elem.shadowRoot;
if ( shadow === undefined ) { if ( shadow === undefined ) {
style = elems[i].style; style = elems[i].style;
if ( typeof style === 'object' || typeof style.removeProperty === 'function' ) { if ( typeof style === 'object' || typeof style.removeProperty === 'function' ) {
@ -81,13 +56,14 @@ while ( i-- ) {
} }
continue; continue;
} }
if ( shadow !== null && shadow.className === sessionId && shadow.firstElementChild !== null ) { if (
shadow !== null &&
shadow.className === vAPI.domFilterer.shadowId &&
shadow.firstElementChild !== null
) {
shadow.removeChild(shadow.firstElementChild); shadow.removeChild(shadow.firstElementChild);
} }
} }
/******************************************************************************/ vAPI.domFilterer.toggleOn();
})(); })();
/******************************************************************************/

View File

@ -19,51 +19,29 @@
Home: https://github.com/gorhill/uBlock Home: https://github.com/gorhill/uBlock
*/ */
/******************************************************************************/
(function() {
'use strict'; 'use strict';
/******************************************************************************/ /******************************************************************************/
if ( typeof vAPI !== 'object' ) { (function() {
if ( typeof vAPI !== 'object' || typeof vAPI.domFilterer !== 'object' ) {
return; return;
} }
/******************************************************************************/ var xpr = document.evaluate(
'count(//*[@' + vAPI.domFilterer.hiddenId + '])',
// Insert all cosmetic filtering-related style tags in the DOM document,
null,
var injectedSelectors = []; XPathResult.NUMBER_TYPE,
var filteredElementCount = 0; null
);
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( vAPI.messaging.send(
'scriptlets', 'scriptlets',
{ {
what: 'liveCosmeticFilteringData', what: 'liveCosmeticFilteringData',
pageURL: window.location.href, pageURL: window.location.href,
filteredElementCount: filteredElementCount filteredElementCount: xpr && xpr.numberValue || 0
} }
); );
/******************************************************************************/
})(); })();
/******************************************************************************/

View File

@ -28,13 +28,14 @@
/******************************************************************************/ /******************************************************************************/
if ( typeof vAPI !== 'object' ) { if ( typeof vAPI !== 'object' || typeof vAPI.domFilterer !== 'object' ) {
return; return;
} }
/******************************************************************************/ /******************************************************************************/
var sessionId = vAPI.sessionId; var sessionId = vAPI.sessionId;
var shadowId = vAPI.domFilterer.shadowId;
if ( document.querySelector('iframe.dom-inspector.' + sessionId) !== null ) { if ( document.querySelector('iframe.dom-inspector.' + sessionId) !== null ) {
return; return;
@ -690,19 +691,20 @@ var cosmeticFilterMapper = (function() {
// Not all target nodes have necessarily been force-hidden, // Not all target nodes have necessarily been force-hidden,
// do it now so that the inspector does not unhide these // do it now so that the inspector does not unhide these
// nodes when disabling style tags. // nodes when disabling style tags.
var nodesFromStyleTag = function(styleTag, rootNode) { var nodesFromStyleTag = function(rootNode) {
var filterMap = nodeToCosmeticFilterMap; var filterMap = nodeToCosmeticFilterMap,
var styleText = styleTag.textContent; selectors, selector,
var selectors = styleText.slice(0, styleText.lastIndexOf('\n')).split(/,\n/); nodes, node,
var i = selectors.length; entries, entry,
var selector, nodes, j, node; i, j;
// CSS-based selectors: simple one.
selectors = vAPI.domFilterer.simpleSelectors;
i = selectors.length;
while ( i-- ) { while ( i-- ) {
// https://github.com/gorhill/uBlock/issues/1015 selector = selectors[i];
// Discard `:root ` prefix.
selector = selectors[i].slice(6);
if ( filterMap.has(rootNode) === false && rootNode[matchesFnName](selector) ) { if ( filterMap.has(rootNode) === false && rootNode[matchesFnName](selector) ) {
filterMap.set(rootNode, selector); filterMap.set(rootNode, selector);
hideNode(node);
} }
nodes = rootNode.querySelectorAll(selector); nodes = rootNode.querySelectorAll(selector);
j = nodes.length; j = nodes.length;
@ -710,24 +712,106 @@ var cosmeticFilterMapper = (function() {
node = nodes[j]; node = nodes[j];
if ( filterMap.has(node) === false ) { if ( filterMap.has(node) === false ) {
filterMap.set(node, selector); 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 incremental = function(rootNode) {
var styleTags = vAPI.styles || []; var styleTags = vAPI.domFilterer.styleTags || [];
var styleTag; var styleTag;
var i = styleTags.length; var i = styleTags.length;
while ( i-- ) { while ( i-- ) {
styleTag = styleTags[i]; styleTag = styleTags[i];
nodesFromStyleTag(styleTag, rootNode);
if ( styleTag.sheet !== null ) { if ( styleTag.sheet !== null ) {
styleTag.sheet.disabled = true; styleTag.sheet.disabled = true;
styleTag[vAPI.sessionId] = true; styleTag[vAPI.sessionId] = true;
} }
} }
nodesFromStyleTag(rootNode);
}; };
var reset = function() { var reset = function() {
@ -736,7 +820,7 @@ var cosmeticFilterMapper = (function() {
}; };
var shutdown = function() { var shutdown = function() {
var styleTags = vAPI.styles || []; var styleTags = vAPI.domFilterer.styleTags || [];
var styleTag; var styleTag;
var i = styleTags.length; var i = styleTags.length;
while ( i-- ) { while ( i-- ) {
@ -761,12 +845,51 @@ var elementsFromSelector = function(selector, context) {
if ( !context ) { if ( !context ) {
context = document; context = document;
} }
var out = []; var out;
if ( selector.indexOf(':') !== -1 ) {
out = elementsFromSpecialSelector(selector);
if ( out !== undefined ) {
return out;
}
}
// plain CSS selector
try { try {
out = context.querySelectorAll(selector); out = context.querySelectorAll(selector);
} catch (ex) { } catch (ex) {
} }
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; 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 { } else {
node.style.setProperty('display', v1, v2); 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')); shadow.appendChild(document.createElement('content'));
} }
}; };
@ -983,7 +1106,7 @@ var hideNode = function(node) {
node.style.setProperty('display', 'none', 'important'); node.style.setProperty('display', 'none', 'important');
return; return;
} }
if ( shadow !== null && shadow.className === sessionId ) { if ( shadow !== null && shadow.className === shadowId ) {
if ( shadow.firstElementChild !== null ) { if ( shadow.firstElementChild !== null ) {
shadow.removeChild(shadow.firstElementChild); shadow.removeChild(shadow.firstElementChild);
} }
@ -995,7 +1118,7 @@ var hideNode = function(node) {
} catch (ex) { } catch (ex) {
return; return;
} }
shadow.className = sessionId; shadow.className = shadowId;
}; };
/******************************************************************************/ /******************************************************************************/
@ -1052,6 +1175,12 @@ var onMessage = function(request) {
highlightElements(); highlightElements();
break; break;
case 'toggleFilter':
highlightedElementLists[0] = selectNodes(request.filter, request.nid);
toggleNodes(highlightedElementLists[0], request.original, request.target);
highlightElements(true);
break;
case 'toggleNodes': case 'toggleNodes':
highlightedElementLists[0] = selectNodes(request.selector, request.nid); highlightedElementLists[0] = selectNodes(request.selector, request.nid);
toggleNodes(highlightedElementLists[0], request.original, request.target); toggleNodes(highlightedElementLists[0], request.original, request.target);