add chainable and recursive cosmetic procedural filters

This commit is contained in:
gorhill 2016-12-25 16:56:39 -05:00
parent 2f01fcda54
commit 73a69711f2
8 changed files with 654 additions and 497 deletions

View file

@ -63,7 +63,10 @@ section > div:first-child {
margin: 0;
position: relative;
}
section > div > textarea {
section.invalidFilter > div:first-child {
border-color: red;
}
section > div:first-child > textarea {
background-color: #fff;
border: none;
box-sizing: border-box;
@ -75,10 +78,7 @@ section > div > textarea {
resize: none;
width: 100%;
}
section > div > textarea.invalidFilter {
background-color: #fee;
}
section > div > textarea + div {
section > div:first-child > textarea + div {
background-color: #aaa;
bottom: 0;
color: white;
@ -86,6 +86,9 @@ section > div > textarea + div {
position: absolute;
right: 0;
}
section.invalidFilter > div:first-child > textarea + div {
background-color: red;
}
section > div:first-child + div {
direction: ltr;
margin: 2px 0;

View file

@ -137,19 +137,9 @@ vAPI.domFilterer = (function() {
/******************************************************************************/
var jobQueue = [
{ t: 'css-hide', _0: [] }, // to inject in style tag
{ t: 'css-style', _0: [] }, // to inject in style tag
{ t: 'css-ssel', _0: [] }, // to manually hide (incremental)
{ t: 'css-csel', _0: [] } // to manually hide (not incremental)
];
var reParserEx = /:(?:has|matches-css|matches-css-before|matches-css-after|style|xpath)\(.+?\)$/;
var allExceptions = createSet(),
allSelectors = createSet(),
stagedNodes = [],
matchesProp = vAPI.matchesProp;
stagedNodes = [];
// Complex selectors, due to their nature may need to be "de-committed". A
// Set() is used to implement this functionality.
@ -308,100 +298,179 @@ var platformHideNode = vAPI.hideNode,
/******************************************************************************/
var runSimpleSelectorJob = function(job, root, fn) {
if ( job._1 === undefined ) {
job._1 = job._0.join(cssNotHiddenId + ',');
}
if ( root[matchesProp](job._1) ) {
fn(root);
}
var nodes = root.querySelectorAll(job._1),
i = nodes.length;
while ( i-- ) {
fn(nodes[i], job);
}
};
// 'P' stands for 'Procedural'
var runComplexSelectorJob = function(job, fn) {
if ( job._1 === undefined ) {
job._1 = job._0.join(',');
}
var nodes = document.querySelectorAll(job._1),
i = nodes.length;
while ( i-- ) {
fn(nodes[i], job);
}
var PSelectorHasTask = function(task) {
this.selector = task[1];
};
var runHasJob = function(job, fn) {
var nodes = document.querySelectorAll(job._0),
i = nodes.length, node;
while ( i-- ) {
node = nodes[i];
if ( node.querySelector(job._1) !== null ) {
fn(node, job);
PSelectorHasTask.prototype.exec = function(input) {
var output = [];
for ( var i = 0, n = input.length; i < n; i++ ) {
if ( input[i].querySelector(this.selector) !== null ) {
output.push(input[i]);
}
}
return output;
};
// '/' = ascii 0x2F */
var parseMatchesCSSJob = function(raw) {
var prop = raw.trim();
if ( prop === '' ) { return null; }
var pos = prop.indexOf(':'),
v = pos !== -1 ? prop.slice(pos + 1).trim() : '',
vlen = v.length;
if (
vlen > 1 &&
v.charCodeAt(0) === 0x2F &&
v.charCodeAt(vlen-1) === 0x2F
) {
try { v = new RegExp(v.slice(1, -1)); } catch(ex) { return null; }
}
return { k: prop.slice(0, pos).trim(), v: v };
var PSelectorHasTextTask = function(task) {
this.needle = new RegExp(task[1]);
};
var runMatchesCSSJob = function(job, fn) {
var nodes = document.querySelectorAll(job._0),
i = nodes.length;
if ( i === 0 ) { return; }
if ( typeof job._1 === 'string' ) {
job._1 = parseMatchesCSSJob(job._1);
}
if ( job._1 === null ) { return; }
var k = job._1.k,
v = job._1.v,
node, style, match;
while ( i-- ) {
node = nodes[i];
style = window.getComputedStyle(node, job._2);
if ( style === null ) { continue; } /* FF */
if ( v instanceof RegExp ) {
match = v.test(style[k]);
} else {
match = style[k] === v;
}
if ( match ) {
fn(node, job);
PSelectorHasTextTask.prototype.exec = function(input) {
var output = [];
for ( var i = 0, n = input.length; i < n; i++ ) {
if ( this.needle.test(input[i].textContent) ) {
output.push(input[i]);
}
}
return output;
};
var runXpathJob = function(job, fn) {
if ( job._1 === undefined ) {
job._1 = document.createExpression(job._0, null);
var PSelectorIfTask = function(task) {
this.pselector = new PSelector(task[1]);
};
PSelectorIfTask.prototype.target = true;
PSelectorIfTask.prototype.exec = function(input) {
var output = [];
for ( var i = 0, n = input.length; i < n; i++ ) {
if ( this.pselector.test(input[i]) === this.target ) {
output.push(input[i]);
}
}
var xpr = job._2 = job._1.evaluate(
document,
XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
job._2 || null
);
var i = xpr.snapshotLength, node;
return output;
};
var PSelectorIfNotTask = function(task) {
PSelectorIfTask.call(this, task);
this.target = false;
};
PSelectorIfNotTask.prototype = Object.create(PSelectorIfTask.prototype);
PSelectorIfNotTask.prototype.constructor = PSelectorIfNotTask;
var PSelectorMatchesCSSTask = function(task) {
this.name = task[1].name;
this.value = new RegExp(task[1].value);
};
PSelectorMatchesCSSTask.prototype.pseudo = null;
PSelectorMatchesCSSTask.prototype.exec = function(input) {
var output = [], style;
for ( var i = 0, n = input.length; i < n; i++ ) {
style = window.getComputedStyle(input[i], this.pseudo);
if ( style === null ) { return null; } /* FF */
if ( this.value.test(style[this.name]) ) {
output.push(input[i]);
}
}
return output;
};
var PSelectorMatchesCSSAfterTask = function(task) {
PSelectorMatchesCSSTask.call(this, task);
this.pseudo = ':after';
};
PSelectorMatchesCSSAfterTask.prototype = Object.create(PSelectorMatchesCSSTask.prototype);
PSelectorMatchesCSSAfterTask.prototype.constructor = PSelectorMatchesCSSAfterTask;
var PSelectorMatchesCSSBeforeTask = function(task) {
PSelectorMatchesCSSTask.call(this, task);
this.pseudo = ':before';
};
PSelectorMatchesCSSBeforeTask.prototype = Object.create(PSelectorMatchesCSSTask.prototype);
PSelectorMatchesCSSBeforeTask.prototype.constructor = PSelectorMatchesCSSBeforeTask;
var PSelectorXpathTask = function(task) {
this.xpe = document.createExpression(task[1], null);
this.xpr = null;
};
PSelectorXpathTask.prototype.exec = function(input) {
var output = [], j, node;
for ( var i = 0, n = input.length; i < n; i++ ) {
this.xpr = this.xpe.evaluate(
input[i],
XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
this.xpr
);
j = this.xpr.snapshotLength;
while ( j-- ) {
node = this.xpr.snapshotItem(j);
if ( node.nodeType === 1 ) {
output.push(node);
}
}
}
return output;
};
var PSelector = function(o) {
if ( PSelector.prototype.operatorToTaskMap === undefined ) {
PSelector.prototype.operatorToTaskMap = new Map([
[ ':has', PSelectorHasTask ],
[ ':has-text', PSelectorHasTextTask ],
[ ':if', PSelectorIfTask ],
[ ':if-not', PSelectorIfNotTask ],
[ ':matches-css', PSelectorMatchesCSSTask ],
[ ':matches-css-after', PSelectorMatchesCSSAfterTask ],
[ ':matches-css-before', PSelectorMatchesCSSBeforeTask ],
[ ':xpath', PSelectorXpathTask ]
]);
}
this.raw = o.raw;
this.selector = o.selector;
this.tasks = [];
var tasks = o.tasks, task, ctor;
for ( var i = 0; i < tasks.length; i++ ) {
task = tasks[i];
ctor = this.operatorToTaskMap.get(task[0]);
this.tasks.push(new ctor(task));
}
};
PSelector.prototype.operatorToTaskMap = undefined;
PSelector.prototype.prime = function(input) {
var root = input || document;
if ( this.selector !== '' ) {
return root.querySelectorAll(this.selector);
}
return [ root ];
};
PSelector.prototype.exec = function(input) {
//var t0 = window.performance.now();
var tasks = this.tasks, nodes = this.prime(input);
for ( var i = 0, n = tasks.length; i < n && nodes.length !== 0; i++ ) {
nodes = tasks[i].exec(nodes);
}
//console.log('%s: %s ms', this.raw, (window.performance.now() - t0).toFixed(2));
return nodes;
};
PSelector.prototype.test = function(input) {
//var t0 = window.performance.now();
var tasks = this.tasks, nodes = this.prime(input), aa0 = [ null ], aa;
for ( var i = 0, ni = nodes.length; i < ni; i++ ) {
aa0[0] = nodes[i]; aa = aa0;
for ( var j = 0, nj = tasks.length; j < nj && aa.length !== 0; j++ ) {
aa = tasks[i].exec(aa);
}
if ( aa.length !== 0 ) { return true; }
}
//console.log('%s: %s ms', this.raw, (window.performance.now() - t0).toFixed(2));
return false;
};
var PSelectors = function() {
this.entries = [];
};
PSelectors.prototype.add = function(o) {
this.entries.push(new PSelector(o));
};
PSelectors.prototype.forEachNode = function(callback) {
var pfilters = this.entries,
i = pfilters.length,
pfilter, nodes, j;
while ( i-- ) {
node = xpr.snapshotItem(i);
if ( node.nodeType === 1 ) {
fn(node, job);
pfilter = pfilters[i];
nodes = pfilter.exec();
j = nodes.length;
while ( j-- ) {
callback(nodes[j], pfilter);
}
}
};
@ -418,14 +487,52 @@ var domFilterer = {
hiddenNodeCount: 0,
hiddenNodeEnforcer: false,
loggerEnabled: undefined,
styleTags: [],
jobQueue: jobQueue,
// Stock jobs.
job0: jobQueue[0],
job1: jobQueue[1],
job2: jobQueue[2],
job3: jobQueue[3],
newHideSelectorBuffer: [], // Hide style filter buffer
newStyleRuleBuffer: [], // Non-hide style filter buffer
simpleHideSelectors: { // Hiding filters: simple selectors
entries: [],
matchesProp: vAPI.matchesProp,
selector: undefined,
add: function(selector) {
this.entries.push(selector);
this.selector = undefined;
},
forEachNodeOfSelector: function(/*callback, root, extra*/) {
},
forEachNode: function(callback, root, extra) {
if ( this.selector === undefined ) {
this.selector = this.entries.join(extra + ',') + extra;
}
if ( root[this.matchesProp](this.selector) ) {
callback(root);
}
var nodes = root.querySelectorAll(this.selector),
i = nodes.length;
while ( i-- ) {
callback(nodes[i]);
}
}
},
complexHideSelectors: { // Hiding filters: complex selectors
entries: [],
selector: undefined,
add: function(selector) {
this.entries.push(selector);
this.selector = undefined;
},
forEachNode: function(callback) {
if ( this.selector === undefined ) {
this.selector = this.entries.join(',');
}
var nodes = document.querySelectorAll(this.selector),
i = nodes.length;
while ( i-- ) {
callback(nodes[i]);
}
}
},
proceduralSelectors: new PSelectors(), // Hiding filters: procedural
addExceptions: function(aa) {
for ( var i = 0, n = aa.length; i < n; i++ ) {
@ -433,58 +540,28 @@ var domFilterer = {
}
},
// Job:
// Stock jobs in job queue:
// 0 = css rules/css declaration to remove visibility
// 1 = css rules/any css declaration
// 2 = simple css selectors/hide
// 3 = complex css selectors/hide
// Custom jobs:
// matches-css/hide
// has/hide
// xpath/hide
addSelector: function(s) {
if ( allSelectors.has(s) || allExceptions.has(s) ) {
addSelector: function(selector) {
if ( allSelectors.has(selector) || allExceptions.has(selector) ) {
return;
}
allSelectors.add(s);
var sel0 = s, sel1 = '';
if ( s.charCodeAt(s.length - 1) === 0x29 ) {
var parts = reParserEx.exec(s);
if ( parts !== null ) {
sel1 = parts[0];
}
}
if ( sel1 === '' ) {
this.job0._0.push(sel0);
if ( sel0.indexOf(' ') === -1 ) {
this.job2._0.push(sel0);
this.job2._1 = undefined;
allSelectors.add(selector);
if ( selector.charCodeAt(0) !== 0x7B /* '{' */ ) {
this.newHideSelectorBuffer.push(selector);
if ( selector.indexOf(' ') === -1 ) {
this.simpleHideSelectors.add(selector);
} else {
this.job3._0.push(sel0);
this.job3._1 = undefined;
this.complexHideSelectors.add(selector);
}
return;
}
sel0 = sel0.slice(0, sel0.length - sel1.length);
if ( sel1.lastIndexOf(':has', 0) === 0 ) {
this.jobQueue.push({ t: 'has-hide', raw: s, _0: sel0, _1: sel1.slice(5, -1) });
} else if ( sel1.lastIndexOf(':matches-css', 0) === 0 ) {
if ( sel1.lastIndexOf(':matches-css-before', 0) === 0 ) {
this.jobQueue.push({ t: 'matches-css-hide', raw: s, _0: sel0, _1: sel1.slice(20, -1), _2: ':before' });
} else if ( sel1.lastIndexOf(':matches-css-after', 0) === 0 ) {
this.jobQueue.push({ t: 'matches-css-hide', raw: s, _0: sel0, _1: sel1.slice(19, -1), _2: ':after' });
} else {
this.jobQueue.push({ t: 'matches-css-hide', raw: s, _0: sel0, _1: sel1.slice(13, -1), _2: null });
}
} else if ( sel1.lastIndexOf(':style', 0) === 0 ) {
this.job1._0.push(sel0 + ' { ' + sel1.slice(7, -1) + ' }');
this.job1._1 = undefined;
} else if ( sel1.lastIndexOf(':xpath', 0) === 0 ) {
this.jobQueue.push({ t: 'xpath-hide', raw: s, _0: sel1.slice(7, -1) });
var o = JSON.parse(selector);
if ( o.style ) {
this.newStyleRuleBuffer.push(o.parts.join(' '));
return;
}
if ( o.procedural ) {
this.proceduralSelectors.add(o);
}
return;
},
addSelectors: function(aa) {
@ -497,27 +574,27 @@ var domFilterer = {
this.commitTimer.clear();
var beforeHiddenNodeCount = this.hiddenNodeCount,
styleText = '', i, n;
styleText = '', i;
// Stock job 0 = css rules/hide
if ( this.job0._0.length ) {
styleText = '\n:root ' + this.job0._0.join(',\n:root ') + '\n{ display: none !important; }';
this.job0._0.length = 0;
// CSS rules/hide
if ( this.newHideSelectorBuffer.length ) {
styleText = '\n:root ' + this.newHideSelectorBuffer.join(',\n:root ') + '\n{ display: none !important; }';
this.newHideSelectorBuffer.length = 0;
}
// Stock job 1 = css rules/any css declaration
if ( this.job1._0.length ) {
styleText += '\n' + this.job1._0.join('\n');
this.job1._0.length = 0;
// CSS rules/any css declaration
if ( this.newStyleRuleBuffer.length ) {
styleText += '\n' + this.newStyleRuleBuffer.join('\n');
this.newStyleRuleBuffer.length = 0;
}
// Simple selectors: incremental.
// Stock job 2 = simple css selectors/hide
if ( this.job2._0.length ) {
// Simple css selectors/hide
if ( this.simpleHideSelectors.entries.length ) {
i = stagedNodes.length;
while ( i-- ) {
runSimpleSelectorJob(this.job2, stagedNodes[i], hideNode);
this.simpleHideSelectors.forEachNode(hideNode, stagedNodes[i], cssNotHiddenId);
}
}
stagedNodes = [];
@ -526,17 +603,16 @@ var domFilterer = {
complexSelectorsOldResultSet = complexSelectorsCurrentResultSet;
complexSelectorsCurrentResultSet = createSet('object');
// Stock job 3 = complex css selectors/hide
// Complex css selectors/hide
// The handling of these can be considered optional, since they are
// also applied declaratively using a style tag.
if ( this.job3._0.length ) {
runComplexSelectorJob(this.job3, complexHideNode);
if ( this.complexHideSelectors.entries.length ) {
this.complexHideSelectors.forEachNode(complexHideNode);
}
// Custom jobs. No optional since they can't be applied in a
// declarative way.
for ( i = 4, n = this.jobQueue.length; i < n; i++ ) {
this.runJob(this.jobQueue[i], complexHideNode);
// Procedural cosmetic filters
if ( this.proceduralSelectors.entries.length ) {
this.proceduralSelectors.forEachNode(complexHideNode);
}
// https://github.com/gorhill/uBlock/issues/1912
@ -595,6 +671,10 @@ var domFilterer = {
this.commitTimer.start();
},
createProceduralFilter: function(o) {
return new PSelector(o);
},
getExcludeId: function() {
if ( this.excludeId === undefined ) {
this.excludeId = vAPI.randomToken();
@ -616,20 +696,6 @@ var domFilterer = {
this.commitTimer = new vAPI.SafeAnimationFrame(this.commit_.bind(this));
},
runJob: function(job, fn) {
switch ( job.t ) {
case 'has-hide':
runHasJob(job, fn);
break;
case 'matches-css-hide':
runMatchesCSSJob(job, fn);
break;
case 'xpath-hide':
runXpathJob(job, fn);
break;
}
},
showNode: function(node) {
node.hidden = false;
platformUnhideNode(node);
@ -1248,14 +1314,14 @@ vAPI.domSurveyor = (function() {
// Need to do this before committing DOM filterer, as needed info
// will no longer be there after commit.
if ( firstSurvey || domFilterer.job0._0.length ) {
if ( firstSurvey || domFilterer.newHideSelectorBuffer.length ) {
messaging.send(
'contentscript',
{
what: 'cosmeticFiltersInjected',
type: 'cosmetic',
hostname: window.location.hostname,
selectors: domFilterer.job0._0,
selectors: domFilterer.newHideSelectorBuffer,
first: firstSurvey,
cost: surveyCost
}
@ -1263,7 +1329,7 @@ vAPI.domSurveyor = (function() {
}
// Shutdown surveyor if too many consecutive empty resultsets.
if ( domFilterer.job0._0.length === 0 ) {
if ( domFilterer.newHideSelectorBuffer.length === 0 ) {
cosmeticSurveyingMissCount += 1;
} else {
cosmeticSurveyingMissCount = 0;

View file

@ -39,6 +39,35 @@ var µb = µBlock;
var encode = JSON.stringify;
var decode = JSON.parse;
var isValidCSSSelector = (function() {
var div = document.createElement('div'),
matchesFn;
// Keep in mind:
// https://github.com/gorhill/uBlock/issues/693
// https://github.com/gorhill/uBlock/issues/1955
if ( div.matches instanceof Function ) {
matchesFn = div.matches.bind(div);
} else if ( div.mozMatchesSelector instanceof Function ) {
matchesFn = div.mozMatchesSelector.bind(div);
} else if ( div.webkitMatchesSelector instanceof Function ) {
matchesFn = div.webkitMatchesSelector.bind(div);
} else if ( div.msMatchesSelector instanceof Function ) {
matchesFn = div.msMatchesSelector.bind(div);
} else {
matchesFn = div.querySelector.bind(div);
}
return function(s) {
try {
matchesFn(s + ', ' + s + ':not(#foo)');
} catch (ex) {
return false;
}
return true;
};
})();
var reIsRegexLiteral = /^\/.+\/$/;
var isBadRegex = function(s) {
try {
void new RegExp(s);
@ -218,7 +247,7 @@ var FilterParser = function() {
this.hostnames = [];
this.invalid = false;
this.cosmetic = true;
this.reNeedHostname = /^(?:script:contains|script:inject|.+?:has|.+?:matches-css(?:-before|-after)?|:xpath)\(.+?\)$/;
this.reNeedHostname = /^(?:script:contains|script:inject|.+?:has|.+?:has-text|.+?:if|.+?:if-not|.+?:matches-css(?:-before|-after)?|.*?:xpath)\(.+\)$/;
};
/******************************************************************************/
@ -331,7 +360,12 @@ FilterParser.prototype.parse = function(raw) {
// ##script:contains(...)
// ##script:inject(...)
// ##.foo:has(...)
// ##.foo:has-text(...)
// ##.foo:if(...)
// ##.foo:if-not(...)
// ##.foo:matches-css(...)
// ##.foo:matches-css-after(...)
// ##.foo:matches-css-before(...)
// ##:xpath(...)
if (
this.hostnames.length === 0 &&
@ -698,91 +732,178 @@ FilterContainer.prototype.freeze = function() {
// implemented (if ever). Unlikely, see:
// https://github.com/gorhill/uBlock/issues/1752
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 ( matchesProp === '' ) {
return function() {
return true;
};
}
var reHasSelector = /^(.+?):has\((.+?)\)$/,
reMatchesCSSSelector = /^(.+?):matches-css(?:-before|-after)?\((.+?)\)$/,
reXpathSelector = /^:xpath\((.+?)\)$/,
reStyleSelector = /^(.+?):style\((.+?)\)$/,
FilterContainer.prototype.compileSelector = (function() {
var reStyleSelector = /^(.+?):style\((.+?)\)$/,
reStyleBad = /url\([^)]+\)/,
reScriptSelector = /^script:(contains|inject)\((.+)\)$/;
// Keep in mind:
// https://github.com/gorhill/uBlock/issues/693
// https://github.com/gorhill/uBlock/issues/1955
var isValidCSSSelector = function(s) {
try {
div[matchesProp](s + ', ' + s + ':not(#foo)');
} catch (ex) {
return false;
return function(raw) {
if ( isValidCSSSelector(raw) && raw.indexOf('[-abp-properties=') === -1 ) {
return raw;
}
return true;
};
return function(s) {
if ( isValidCSSSelector(s) && s.indexOf('[-abp-properties=') === -1 ) {
return true;
}
// We reach this point very rarely.
// We rarely reach this point.
var matches;
// Future `:has`-based filter? If so, validate both parts of the whole
// selector.
matches = reHasSelector.exec(s);
if ( matches !== null ) {
return isValidCSSSelector(matches[1]) && isValidCSSSelector(matches[2]);
}
// Custom `:matches-css`-based filter?
matches = reMatchesCSSSelector.exec(s);
if ( matches !== null ) {
return isValidCSSSelector(matches[1]);
}
// Custom `:xpath`-based filter?
matches = reXpathSelector.exec(s);
if ( matches !== null ) {
try {
return document.createExpression(matches[1], null) instanceof XPathExpression;
} catch (e) {
}
return false;
}
// `:style` selector?
matches = reStyleSelector.exec(s);
if ( matches !== null ) {
return isValidCSSSelector(matches[1]) && reStyleBad.test(matches[2]) === false;
}
// Special `script:` filter?
matches = reScriptSelector.exec(s);
if ( matches !== null ) {
if ( matches[1] === 'inject' ) {
return true;
if ( (matches = reStyleSelector.exec(raw)) !== null ) {
if ( isValidCSSSelector(matches[1]) && reStyleBad.test(matches[2]) === false ) {
return JSON.stringify({
style: true,
raw: raw,
parts: [ matches[1], '{' + matches[2] + '}' ]
});
}
return matches[2].startsWith('/') === false ||
matches[2].endsWith('/') === false ||
isBadRegex(matches[2].slice(1, -1)) === false;
return;
}
µb.logger.writeOne('', 'error', 'Cosmetic filtering invalid filter: ' + s);
return false;
// `script:` filter?
if ( (matches = reScriptSelector.exec(raw)) !== null ) {
// :inject
if ( matches[1] === 'inject' ) {
return raw;
}
// :contains
if ( reIsRegexLiteral.test(matches[2]) === false || isBadRegex(matches[2].slice(1, -1)) === false ) {
return raw;
}
return;
}
// Procedural selector?
var compiled;
if ( (compiled = this.compileProceduralSelector(raw)) ) {
return compiled;
}
µb.logger.writeOne('', 'error', 'Cosmetic filtering invalid filter: ' + raw);
return;
};
})();
/******************************************************************************/
FilterContainer.prototype.compileProceduralSelector = (function() {
var reParserEx = /(:(?:has|has-text|if|if-not|matches-css|matches-css-after|matches-css-before|xpath))\(.+\)$/,
reFirstParentheses = /^\(*/,
reLastParentheses = /\)*$/,
reEscapeRegex = /[.*+?^${}()|[\]\\]/g;
var lastProceduralSelector = '',
lastProceduralSelectorCompiled;
var compileCSSSelector = function(s) {
if ( isValidCSSSelector(s) ) {
return s;
}
};
var compileText = function(s) {
if ( reIsRegexLiteral.test(s) ) {
s = s.slice(1, -1);
if ( isBadRegex(s) ) { return; }
} else {
s = s.replace(reEscapeRegex, '\\$&');
}
return s;
};
var compileCSSDeclaration = function(s) {
var name, value,
pos = s.indexOf(':');
if ( pos === -1 ) { return; }
name = s.slice(0, pos).trim();
value = s.slice(pos + 1).trim();
if ( reIsRegexLiteral.test(value) ) {
value = value.slice(1, -1);
if ( isBadRegex(value) ) { return; }
} else {
value = value.replace(reEscapeRegex, '\\$&');
}
return { name: name, value: value };
};
var compileConditionalSelector = function(s) {
return compile(s);
};
var compileXpathExpression = function(s) {
var dummy;
try {
dummy = document.createExpression(s, null) instanceof XPathExpression;
} catch (e) {
return;
}
return s;
};
var compileArgument = new Map([
[ ':has', compileCSSSelector ],
[ ':has-text', compileText ],
[ ':if', compileConditionalSelector ],
[ ':if-not', compileConditionalSelector ],
[ ':matches-css', compileCSSDeclaration ],
[ ':matches-css-after', compileCSSDeclaration ],
[ ':matches-css-before', compileCSSDeclaration ],
[ ':xpath', compileXpathExpression ]
]);
var compile = function(raw) {
var matches = reParserEx.exec(raw);
if ( matches === null ) { return; }
var tasks = [],
firstOperand = raw.slice(0, matches.index),
currentOperator = matches[1],
selector = raw.slice(matches.index + currentOperator.length),
currentArgument = '', nextOperand, nextOperator,
depth = 0, opening, closing;
if ( firstOperand !== '' && isValidCSSSelector(firstOperand) === false ) { return; }
for (;;) {
matches = reParserEx.exec(selector);
if ( matches !== null ) {
nextOperand = selector.slice(0, matches.index);
nextOperator = matches[1];
} else {
nextOperand = selector;
nextOperator = '';
}
opening = reFirstParentheses.exec(nextOperand)[0].length;
closing = reLastParentheses.exec(nextOperand)[0].length;
if ( opening > closing ) {
if ( depth === 0 ) { currentArgument = ''; }
depth += 1;
} else if ( closing > opening && depth > 0 ) {
depth -= 1;
if ( depth === 0 ) { nextOperand = currentArgument + nextOperand; }
}
if ( depth !== 0 ) {
currentArgument += nextOperand + nextOperator;
} else {
currentArgument = compileArgument.get(currentOperator)(nextOperand.slice(1, -1));
if ( currentArgument === undefined ) { return; }
tasks.push([ currentOperator, currentArgument ]);
currentOperator = nextOperator;
}
if ( nextOperator === '' ) { break; }
selector = selector.slice(matches.index + nextOperator.length);
}
if ( tasks.length === 0 || depth !== 0 ) { return; }
return { selector: firstOperand, tasks: tasks };
};
return function(raw) {
if ( raw === lastProceduralSelector ) {
return lastProceduralSelectorCompiled;
}
lastProceduralSelector = raw;
var compiled = compile(raw);
if ( compiled !== undefined ) {
compiled.procedural = true;
compiled.raw = raw;
compiled = JSON.stringify(compiled);
}
lastProceduralSelectorCompiled = compiled;
return compiled;
};
})();
@ -843,7 +964,7 @@ FilterContainer.prototype.compile = function(s, out) {
// still the most common, and can easily be tested using a plain regex.
if (
this.reClassOrIdSelector.test(parsed.suffix) === false &&
this.isValidSelector(parsed.suffix) === false
this.compileSelector(parsed.suffix) === undefined
) {
return true;
}
@ -895,15 +1016,15 @@ FilterContainer.prototype.compileGenericHideSelector = function(parsed, out) {
return;
}
// Composite CSS rule.
if ( this.isValidSelector(selector) ) {
if ( this.compileSelector(selector) ) {
out.push('c\vlg+\v' + key + '\v' + selector);
}
return;
}
if ( this.isValidSelector(selector) !== true ) {
return;
}
var compiled = this.compileSelector(selector);
if ( compiled === undefined ) { return; }
// TODO: Detect and error on procedural cosmetic filters.
// ["title"] and ["alt"] will go in high-low generic bin.
if ( this.reHighLow.test(selector) ) {
@ -948,10 +1069,6 @@ FilterContainer.prototype.compileGenericHideSelector = function(parsed, out) {
FilterContainer.prototype.compileGenericUnhideSelector = function(parsed, out) {
var selector = parsed.suffix;
if ( this.isValidSelector(selector) !== true ) {
return;
}
// script:contains(...)
// script:inject(...)
if ( this.reScriptSelector.test(selector) ) {
@ -959,10 +1076,14 @@ FilterContainer.prototype.compileGenericUnhideSelector = function(parsed, out) {
return;
}
// Procedural cosmetic filters are acceptable as generic exception filters.
var compiled = this.compileSelector(selector);
if ( compiled === undefined ) { return; }
// https://github.com/chrisaljoudi/uBlock/issues/497
// All generic exception filters are put in the same bucket: they are
// expected to be very rare.
out.push('c\vg1\v' + selector);
out.push('c\vg1\v' + compiled);
};
/******************************************************************************/
@ -980,20 +1101,24 @@ FilterContainer.prototype.compileHostnameSelector = function(hostname, parsed, o
hostname = this.punycode.toASCII(hostname);
}
var domain = this.µburi.domainFromHostname(hostname),
var selector = parsed.suffix,
domain = this.µburi.domainFromHostname(hostname),
hash;
// script:contains(...)
// script:inject(...)
if ( this.reScriptSelector.test(parsed.suffix) ) {
if ( this.reScriptSelector.test(selector) ) {
hash = domain !== '' ? domain : this.noDomainHash;
if ( unhide ) {
hash = '!' + hash;
}
out.push('c\vjs\v' + hash + '\v' + hostname + '\v' + parsed.suffix);
out.push('c\vjs\v' + hash + '\v' + hostname + '\v' + selector);
return;
}
var compiled = this.compileSelector(selector);
if ( compiled === undefined ) { return; }
// https://github.com/chrisaljoudi/uBlock/issues/188
// If not a real domain as per PSL, assign a synthetic one
if ( hostname.endsWith('.*') === false ) {
@ -1005,7 +1130,7 @@ FilterContainer.prototype.compileHostnameSelector = function(hostname, parsed, o
hash = '!' + hash;
}
out.push('c\vh\v' + hash + '\v' + hostname + '\v' + parsed.suffix);
out.push('c\vh\v' + hash + '\v' + hostname + '\v' + compiled);
};
/******************************************************************************/

View file

@ -100,6 +100,10 @@ var onMessage = function(request, sender, callback) {
µb.mouseURL = request.url;
break;
case 'compileCosmeticFilterSelector':
response = µb.cosmeticFilteringEngine.compileSelector(request.selector);
break;
case 'cosmeticFiltersInjected':
µb.cosmeticFilteringEngine.addToSelectorCache(request);
/* falls through */

View file

@ -134,14 +134,23 @@ var fromCosmeticFilter = function(details) {
}
candidates[details.rawFilter] = new RegExp(reStr.join('\\v') + '(?:\\n|$)');
// Procedural filters, which are pre-compiled, make thing sort of
// complicated. We are going to also search for one portion of the
// compiled form of a filter.
var filterEx = '(' +
reEscape(filter) +
'|[^\\v]+' +
reEscape(JSON.stringify({ raw: filter }).slice(1,-1)) +
'[^\\v]+)';
// Second step: find hostname-based versions.
// Reference: FilterContainer.compileHostnameSelector().
var pos;
var hostname = details.hostname;
var pos,
hostname = details.hostname;
if ( hostname !== '' ) {
for ( ;; ) {
candidates[hostname + '##' + filter] = new RegExp(
['c', 'h', '[^\\v]+', reEscape(hostname), reEscape(filter)].join('\\v') +
['c', 'h', '[^\\v]+', reEscape(hostname), filterEx].join('\\v') +
'(?:\\n|$)'
);
pos = hostname.indexOf('.');
@ -159,7 +168,7 @@ var fromCosmeticFilter = function(details) {
if ( pos !== -1 ) {
var entity = domain.slice(0, pos) + '.*';
candidates[entity + '##' + filter] = new RegExp(
['c', 'h', '[^\\v]+', reEscape(entity), reEscape(filter)].join('\\v') +
['c', 'h', '[^\\v]+', reEscape(entity), filterEx].join('\\v') +
'(?:\\n|$)'
);
}

View file

@ -31,38 +31,36 @@ if ( typeof vAPI !== 'object' || !vAPI.domFilterer ) {
return;
}
var df = vAPI.domFilterer,
loggedSelectors = vAPI.loggedSelectors || {},
matchedSelectors = [],
selectors, i, selector;
var loggedSelectors = vAPI.loggedSelectors || {},
matchedSelectors = [];
// CSS selectors.
selectors = df.jobQueue[2]._0.concat(df.jobQueue[3]._0);
i = selectors.length;
while ( i-- ) {
selector = selectors[i];
if ( loggedSelectors.hasOwnProperty(selector) ) {
continue;
var evaluateSelector = function(selector) {
if (
loggedSelectors.hasOwnProperty(selector) === false &&
document.querySelector(selector) !== null
) {
loggedSelectors[selector] = true;
matchedSelectors.push(selector);
}
if ( document.querySelector(selector) === null ) {
continue;
}
loggedSelectors[selector] = true;
matchedSelectors.push(selector);
}
// Non-CSS selectors.
var logHit = function(node, job) {
if ( !job.raw || loggedSelectors.hasOwnProperty(job.raw) ) {
return;
}
loggedSelectors[job.raw] = true;
matchedSelectors.push(job.raw);
};
for ( i = 4; i < df.jobQueue.length; i++ ) {
df.runJob(df.jobQueue[i], logHit);
}
// Simple CSS selector-based cosmetic filters.
vAPI.domFilterer.simpleHideSelectors.entries.forEach(evaluateSelector);
// Complex CSS selector-based cosmetic filters.
vAPI.domFilterer.complexHideSelectors.entries.forEach(evaluateSelector);
// Procedural cosmetic filters.
vAPI.domFilterer.proceduralSelectors.entries.forEach(function(pfilter) {
if (
loggedSelectors.hasOwnProperty(pfilter.raw) === false &&
pfilter.exec().length !== 0
) {
loggedSelectors[pfilter.raw] = true;
matchedSelectors.push(pfilter.raw);
}
});
vAPI.loggedSelectors = loggedSelectors;

View file

@ -693,7 +693,7 @@ var cosmeticFilterMapper = (function() {
i, j;
// CSS-based selectors: simple one.
selectors = vAPI.domFilterer.job2._0;
selectors = vAPI.domFilterer.simpleHideSelectors.entries;
i = selectors.length;
while ( i-- ) {
selector = selectors[i];
@ -711,7 +711,7 @@ var cosmeticFilterMapper = (function() {
}
// CSS-based selectors: complex one (must query from doc root).
selectors = vAPI.domFilterer.job3._0;
selectors = vAPI.domFilterer.complexHideSelectors.entries;
i = selectors.length;
while ( i-- ) {
selector = selectors[i];
@ -726,14 +726,12 @@ var cosmeticFilterMapper = (function() {
}
// Non-CSS selectors.
var runJobCallback = function(node, job) {
var runJobCallback = function(node, pfilter) {
if ( filterMap.has(node) === false ) {
filterMap.set(node, job.raw);
filterMap.set(node, pfilter.raw);
}
};
for ( i = 4; i < vAPI.domFilterer.jobQueue.length; i++ ) {
vAPI.domFilterer.runJob(vAPI.domFilterer.jobQueue[i], runJobCallback);
}
vAPI.domFilterer.proceduralSelectors.forEachNode(runJobCallback);
};
var incremental = function(rootNode) {

View file

@ -177,6 +177,14 @@ var safeQuerySelectorAll = function(node, selector) {
/******************************************************************************/
var rawFilterFromTextarea = function() {
var s = taCandidate.value,
pos = s.indexOf('\n');
return pos === -1 ? s.trim() : s.slice(0, pos).trim();
};
/******************************************************************************/
var getElementBoundingClientRect = function(elem) {
var rect = typeof elem.getBoundingClientRect === 'function' ?
elem.getBoundingClientRect() :
@ -635,7 +643,9 @@ var filtersFrom = function(x, y) {
filterToDOMInterface.set
@desc Look-up all the HTML elements matching the filter passed in
argument.
@param string, a cosmetic of network filter.
@param string, a cosmetic or network filter.
@param function, called once all items matching the filter have been
collected.
@return array, or undefined if the filter is invalid.
filterToDOMInterface.preview
@ -733,16 +743,15 @@ var filterToDOMInterface = (function() {
// ways to compose a valid href to the same effective URL. One idea is to
// normalize all a[href] on the page, but for now I will wait and see, as I
// prefer to refrain from tampering with the page content if I can avoid it.
var fromCosmeticFilter = function(filter) {
var fromPlainCosmeticFilter = function(filter) {
var elems;
try {
elems = document.querySelectorAll(filter);
}
catch (e) {
return fromProceduralCosmeticFilter(filter);
return;
}
var out = [],
iElem = elems.length;
var out = [], iElem = elems.length;
while ( iElem-- ) {
out.push({ type: 'cosmetic', elem: elems[iElem]});
}
@ -751,108 +760,27 @@ var filterToDOMInterface = (function() {
// https://github.com/gorhill/uBlock/issues/1772
// Handle procedural cosmetic filters.
var fromProceduralCosmeticFilter = function(filter) {
if ( filter.charCodeAt(filter.length - 1) === 0x29 /* ')' */ ) {
var parts = reProceduralCosmeticFilter.exec(filter);
if (
parts !== null &&
proceduralCosmeticFilterFunctions.hasOwnProperty(parts[2])
) {
return proceduralCosmeticFilterFunctions[parts[2]](
parts[1].trim(),
parts[3].trim()
);
}
var fromCompiledCosmeticFilter = function(raw) {
if ( typeof raw !== 'string' ) { return; }
var o;
try {
o = JSON.parse(raw);
} catch(ex) {
return;
}
};
var reProceduralCosmeticFilter = /^(.*?):(matches-css|has|style|xpath)\((.+?)\)$/;
// Collection of handlers for procedural cosmetic filters.
var proceduralCosmeticFilterFunctions = {
'has': function(selector, arg) {
if ( selector === '' ) { return; }
var elems;
try {
elems = document.querySelectorAll(selector);
document.querySelector(arg);
} catch(ex) {
return;
}
var out = [], elem;
for ( var i = 0, n = elems.length; i < n; i++ ) {
elem = elems[i];
if ( elem.querySelector(arg) ) {
out.push({ type: 'cosmetic', elem: elem });
}
}
return out;
},
'matches-css': function(selector, arg) {
if ( selector === '' ) { return; }
var elems;
try {
elems = document.querySelectorAll(selector);
} catch(ex) {
return;
}
var out = [], elem, style,
pos = arg.indexOf(':');
if ( pos === -1 ) { return; }
var prop = arg.slice(0, pos).trim(),
reText = arg.slice(pos + 1).trim();
if ( reText === '' ) { return; }
var re = reText !== '*' ?
new RegExp('^' + reText.replace(/[.+?${}()|[\]\\^]/g, '\\$&').replace(/\*+/g, '.*?') + '$') :
/./;
for ( var i = 0, n = elems.length; i < n; i++ ) {
elem = elems[i];
style = window.getComputedStyle(elem, null);
if ( re.test(style[prop]) ) {
out.push({ type: 'cosmetic', elem: elem });
}
}
return out;
},
'style': function(selector, arg) {
if ( selector === '' || arg === '' ) { return; }
var elems;
try {
elems = document.querySelectorAll(selector);
} catch(ex) {
return;
}
var out = [];
for ( var i = 0, n = elems.length; i < n; i++ ) {
out.push({ type: 'cosmetic', elem: elems[i] });
}
lastAction = selector + ' { ' + arg + ' }';
return out;
},
'xpath': function(selector, arg) {
if ( selector !== '' ) { return []; }
var result;
try {
result = document.evaluate(
arg,
document,
null,
XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
null
);
} catch(ex) {
return;
}
if ( result === undefined ) { return []; }
var out = [], elem, i = result.snapshotLength;
while ( i-- ) {
elem = result.snapshotItem(i);
if ( elem.nodeType === 1 ) {
out.push({ type: 'cosmetic', elem: elem });
}
}
return out;
var elems;
if ( o.style ) {
elems = document.querySelectorAll(o.parts[0]);
lastAction = o.parts.join(' ');
} else if ( o.procedural ) {
elems = vAPI.domFilterer.createProceduralFilter(o).exec();
}
if ( !elems ) { return; }
var out = [];
for ( var i = 0, n = elems.length; i < n; i++ ) {
out.push({ type: 'cosmetic', elem: elems[i] });
}
return out;
};
var lastFilter,
@ -862,26 +790,44 @@ var filterToDOMInterface = (function() {
applied = false,
previewing = false;
var queryAll = function(filter) {
var queryAll = function(filter, callback) {
filter = filter.trim();
if ( filter === lastFilter ) {
return lastResultset;
callback(lastResultset);
return;
}
unapply();
if ( filter === '' ) {
lastFilter = '';
lastResultset = [];
} else {
lastFilter = filter;
lastAction = undefined;
lastResultset = filter.lastIndexOf('##', 0) === 0 ?
fromCosmeticFilter(filter.slice(2)) :
fromNetworkFilter(filter);
if ( previewing ) {
apply(filter);
}
callback(lastResultset);
return;
}
return lastResultset;
lastFilter = filter;
lastAction = undefined;
if ( filter.lastIndexOf('##', 0) === -1 ) {
lastResultset = fromNetworkFilter(filter);
if ( previewing ) { apply(); }
callback(lastResultset);
return;
}
var selector = filter.slice(2);
lastResultset = fromPlainCosmeticFilter(selector);
if ( lastResultset ) {
if ( previewing ) { apply(); }
callback(lastResultset);
return;
}
// Procedural cosmetic filter
vAPI.messaging.send(
'elementPicker',
{ what: 'compileCosmeticFilterSelector', selector: selector },
function(response) {
lastResultset = fromCompiledCosmeticFilter(response);
if ( previewing ) { apply(); }
callback(lastResultset);
}
);
};
var applyHide = function() {
@ -983,9 +929,11 @@ var filterToDOMInterface = (function() {
var preview = function(filter) {
previewing = filter !== false;
if ( previewing ) {
if ( queryAll(filter) !== undefined ) {
apply();
}
queryAll(filter, function(items) {
if ( items !== undefined ) {
apply();
}
});
} else {
unapply();
}
@ -999,70 +947,75 @@ var filterToDOMInterface = (function() {
};
})();
// https://www.youtube.com/watch?v=nuUXJ6RfIik
/******************************************************************************/
var userFilterFromCandidate = function() {
var v = taCandidate.value;
var items = filterToDOMInterface.set(v);
if ( !items || items.length === 0 ) {
return false;
}
// https://github.com/gorhill/uBlock/issues/738
// Trim dots.
var hostname = window.location.hostname;
if ( hostname.slice(-1) === '.' ) {
hostname = hostname.slice(0, -1);
}
// Cosmetic filter?
if ( v.lastIndexOf('##', 0) === 0 ) {
return hostname + v;
}
// Assume net filter
var opts = [];
// If no domain included in filter, we need domain option
if ( v.lastIndexOf('||', 0) === -1 ) {
opts.push('domain=' + hostname);
}
var item = items[0];
if ( item.opts ) {
opts.push(item.opts);
}
if ( opts.length ) {
v += '$' + opts.join(',');
}
return v;
};
/******************************************************************************/
var onCandidateChanged = function() {
var elems = [],
items = filterToDOMInterface.set(taCandidate.value),
valid = items !== undefined;
if ( valid ) {
for ( var i = 0; i < items.length; i++ ) {
elems.push(items[i].elem);
var userFilterFromCandidate = function(callback) {
var v = rawFilterFromTextarea();
filterToDOMInterface.set(v, function(items) {
if ( !items || items.length === 0 ) {
callback();
return;
}
}
pickerBody.querySelector('body section textarea + div').textContent = valid ?
items.length.toLocaleString() :
'0';
taCandidate.classList.toggle('invalidFilter', !valid);
dialog.querySelector('#create').disabled = elems.length === 0;
highlightElements(elems, true);
// https://github.com/gorhill/uBlock/issues/738
// Trim dots.
var hostname = window.location.hostname;
if ( hostname.slice(-1) === '.' ) {
hostname = hostname.slice(0, -1);
}
// Cosmetic filter?
if ( v.lastIndexOf('##', 0) === 0 ) {
callback(hostname + v);
return;
}
// Assume net filter
var opts = [];
// If no domain included in filter, we need domain option
if ( v.lastIndexOf('||', 0) === -1 ) {
opts.push('domain=' + hostname);
}
var item = items[0];
if ( item.opts ) {
opts.push(item.opts);
}
if ( opts.length ) {
v += '$' + opts.join(',');
}
callback(v);
});
};
/******************************************************************************/
var onCandidateChanged = (function() {
var process = function(items) {
var elems = [], valid = items !== undefined;
if ( valid ) {
for ( var i = 0; i < items.length; i++ ) {
elems.push(items[i].elem);
}
}
pickerBody.querySelector('body section textarea + div').textContent = valid ?
items.length.toLocaleString() :
'ε';
dialog.querySelector('section').classList.toggle('invalidFilter', !valid);
dialog.querySelector('#create').disabled = elems.length === 0;
highlightElements(elems, true);
};
return function() {
filterToDOMInterface.set(rawFilterFromTextarea(), process);
};
})();
/******************************************************************************/
var candidateFromFilterChoice = function(filterChoice) {
var slot = filterChoice.slot;
var filters = filterChoice.filters;
@ -1132,8 +1085,8 @@ var onDialogClicked = function(ev) {
// We have to exit from preview mode: this guarantees matching elements
// will be found for the candidate filter.
filterToDOMInterface.preview(false);
var filter = userFilterFromCandidate();
if ( filter ) {
userFilterFromCandidate(function(filter) {
if ( !filter ) { return; }
var d = new Date();
vAPI.messaging.send(
'elementPicker',
@ -1143,9 +1096,9 @@ var onDialogClicked = function(ev) {
pageDomain: window.location.hostname
}
);
filterToDOMInterface.preview(taCandidate.value);
filterToDOMInterface.preview(rawFilterFromTextarea());
stopPicker();
}
});
}
else if ( ev.target.id === 'pick' ) {
@ -1161,7 +1114,7 @@ var onDialogClicked = function(ev) {
if ( filterToDOMInterface.previewing() ) {
filterToDOMInterface.preview(false);
} else {
filterToDOMInterface.preview(taCandidate.value);
filterToDOMInterface.preview(rawFilterFromTextarea());
}
highlightElements(targetElements, true);
}
@ -1300,6 +1253,7 @@ var onKeyPressed = function(ev) {
if ( ev.which === 27 ) {
ev.stopPropagation();
ev.preventDefault();
filterToDOMInterface.preview(false);
stopPicker();
}
};