this fixes #1202, #1545, other improvements

This commit is contained in:
gorhill 2016-04-15 12:27:53 -04:00
parent 63d27b1d25
commit def2b7cbff

View file

@ -19,7 +19,7 @@
Home: https://github.com/gorhill/uBlock Home: https://github.com/gorhill/uBlock
*/ */
/* global CSS */ /* global CSS, CSSImportRule, CSSStyleRule */
/******************************************************************************/ /******************************************************************************/
/******************************************************************************/ /******************************************************************************/
@ -265,18 +265,59 @@ var highlightElements = function(elems, force) {
/******************************************************************************/ /******************************************************************************/
var removeElements = function(elems) { var filterElements = function(filter) {
var i = elems.length, elem; var items = elementsFromFilter(filter);
var i = items.length, item;
while ( i-- ) { while ( i-- ) {
elem = elems[i]; item = items[i];
if ( elem.parentNode ) { if (
elem.parentNode.removeChild(elem); item.type === 'cosmetic' ||
item.type === 'network' && item.src !== undefined
) {
item.elem.style.setProperty('display', 'none', 'important');
}
if ( item.type === 'network' && item.style === 'background-image' ) {
item.elem.style.setProperty('background-image', 'none', 'important');
} }
} }
}; };
/******************************************************************************/ /******************************************************************************/
var urlFromCSSPropertyValue = function(value) {
var matches = /^url\((["']?)([^"']+)\1\)$/.exec(value);
return matches !== null && matches.length === 3 ? matches[2] : '';
};
/******************************************************************************/
var backgroundImageURLFromElement = function(elem) {
var style = window.getComputedStyle(elem);
var bgImg = style.backgroundImage || '';
return bgImg !== '' ? urlFromCSSPropertyValue(bgImg) : '';
};
/******************************************************************************/
var resourceURLFromElement = function(elem) {
var tagName = elem.localName, s;
if ( (s = netFilter1stSources[tagName]) ) {
s = elem[s];
if ( typeof s === 'string' && s !== '' ) {
return s;
}
}
if ( (s = netFilter2ndSources[tagName]) ) {
s = elem[s];
if ( typeof s === 'string' && s !== '' ) {
return s;
}
}
return backgroundImageURLFromElement(elem);
};
/******************************************************************************/
var netFilterFromUnion = (function() { var netFilterFromUnion = (function() {
var reTokenizer = /[^0-9a-z%*]+|[0-9a-z%]+|\*/gi; var reTokenizer = /[^0-9a-z%*]+|[0-9a-z%]+|\*/gi;
var a = document.createElement('a'); var a = document.createElement('a');
@ -332,7 +373,10 @@ var netFilterFromUnion = (function() {
} }
from = fromTokens.join('').replace(/\*\*+/g, '*'); from = fromTokens.join('').replace(/\*\*+/g, '*');
if ( from !== '/*' && from !== to ) { if ( from !== '/*' && from !== to ) {
out.push('||' + lastNetFilterHostname + from); var filter = '||' + lastNetFilterHostname + from;
if ( out.indexOf(filter) === -1 ) {
out.push(filter);
}
} else { } else {
from = to; from = to;
} }
@ -362,17 +406,11 @@ var netFilterFromElement = function(elem, out) {
if ( elem.nodeType !== 1 ) { if ( elem.nodeType !== 1 ) {
return; return;
} }
var tagName = elem.tagName.toLowerCase(); var src = resourceURLFromElement(elem);
if ( netFilter1stSources.hasOwnProperty(tagName) === false ) { if ( src === '' ) {
return; return;
} }
var src = elem[netFilter1stSources[tagName]];
if ( typeof src !== 'string' || src.length === 0 ) {
src = elem[netFilter2ndSources[tagName]];
if ( typeof src !== 'string' || src.length === 0 ) {
return;
}
}
// Remove fragment // Remove fragment
var pos = src.indexOf('#'); var pos = src.indexOf('#');
if ( pos !== -1 ) { if ( pos !== -1 ) {
@ -395,16 +433,27 @@ var netFilterFromElement = function(elem, out) {
}; };
var netFilter1stSources = { var netFilter1stSources = {
'audio': 'src',
'embed': 'src', 'embed': 'src',
'iframe': 'src', 'iframe': 'src',
'img': 'src', 'img': 'src',
'object': 'data' 'object': 'data',
'video': 'src'
}; };
var netFilter2ndSources = { var netFilter2ndSources = {
'img': 'srcset' 'img': 'srcset'
}; };
var filterTypes = {
'audio': 'media',
'embed': 'object',
'iframe': 'subdocument',
'img': 'image',
'object': 'object',
'video': 'media',
};
/******************************************************************************/ /******************************************************************************/
// Extract the best possible cosmetic filter, i.e. as specific as possible. // Extract the best possible cosmetic filter, i.e. as specific as possible.
@ -505,12 +554,36 @@ var cosmeticFilterFromElement = function(elem, out) {
/******************************************************************************/ /******************************************************************************/
var filtersFromElement = function(elem) { var filtersFrom = function(x, y) {
netFilterCandidates.length = 0; netFilterCandidates.length = 0;
cosmeticFilterCandidates.length = 0; cosmeticFilterCandidates.length = 0;
// This is to prevent revisiting the same element more than once.
var visited = typeof Set === 'function' ?
new Set() :
{
add: function() {},
has: function() { return true; }
};
// We need at least one element.
var first = null;
if ( typeof x === 'number' ) {
first = elementFromPoint(x, y);
} else if ( x instanceof HTMLElement ) {
first = x;
x = undefined;
}
if ( first === null ) {
return 0;
}
// Extract filter candidates from self and all ancestors.
var elem = first;
while ( elem && elem !== document.body ) { while ( elem && elem !== document.body ) {
netFilterFromElement(elem, netFilterCandidates); netFilterFromElement(elem, netFilterCandidates);
cosmeticFilterFromElement(elem, cosmeticFilterCandidates); cosmeticFilterFromElement(elem, cosmeticFilterCandidates);
visited.add(elem);
elem = elem.parentNode; elem = elem.parentNode;
} }
// The body tag is needed as anchor only when the immediate child // The body tag is needed as anchor only when the immediate child
@ -519,6 +592,72 @@ var filtersFromElement = function(elem) {
if ( i !== 0 && cosmeticFilterCandidates[i-1].indexOf(':nth-of-type(') !== -1 ) { if ( i !== 0 && cosmeticFilterCandidates[i-1].indexOf(':nth-of-type(') !== -1 ) {
cosmeticFilterCandidates.push('##body'); cosmeticFilterCandidates.push('##body');
} }
// https://github.com/gorhill/uBlock/issues/1545
// Extract filter candidates from all elements found at point (x, y).
if ( typeof x === 'number' ) {
var attrName = vAPI.sessionId + '-clickblind';
var previous;
elem = first;
for (;;) {
previous = elem;
elem.setAttribute(attrName, '');
elem = elementFromPoint(x, y, true);
if ( elem === null || elem === previous ) {
break;
}
if ( visited.has(elem) === false ) {
netFilterFromElement(elem, netFilterCandidates);
visited.add(elem);
}
}
var elems = document.querySelectorAll('[' + attrName + ']');
i = elems.length;
while ( i-- ) {
elems[i].removeAttribute(attrName);
}
}
return netFilterCandidates.length + cosmeticFilterCandidates.length;
};
/******************************************************************************/
var elementsFromStylesheet = function(sheet, reURL, out) {
var rules = sheet.rules;
if ( !rules ) {
return;
}
var iRule = rules.length;
var rule, value, src, elems, iElem;
while ( iRule-- ) {
rule = rules[iRule];
if ( rule instanceof CSSImportRule ) {
elementsFromStylesheet(rule.styleSheet, reURL, out);
continue;
}
if ( rule instanceof CSSStyleRule === false ) {
continue;
}
value = rule.style.backgroundImage;
if ( value.lastIndexOf('url(', 0) !== 0 ) {
continue;
}
src = urlFromCSSPropertyValue(value);
if ( reURL.test(src) === false ) {
continue;
}
elems = document.querySelectorAll(rule.selectorText);
iElem = elems.length;
while ( iElem-- ) {
out.push({
type: 'network',
elem: elems[iElem],
style: 'background-image',
opts: 'image'
});
}
}
}; };
/******************************************************************************/ /******************************************************************************/
@ -537,11 +676,20 @@ var elementsFromFilter = function(filter) {
// One idea is to normalize all a[href] on the page, but for now I will // 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 // wait and see, as I prefer to refrain from tampering with the page
// content if I can avoid it. // content if I can avoid it.
var elems, iElem, elem;
if ( filter.lastIndexOf('##', 0) === 0 ) { if ( filter.lastIndexOf('##', 0) === 0 ) {
try { try {
out = document.querySelectorAll(filter.slice(2)); elems = document.querySelectorAll(filter.slice(2));
} }
catch (e) { catch (e) {
elems = [];
}
iElem = elems.length;
while ( iElem-- ) {
out.push({
type: 'cosmetic',
elem: elems[iElem],
});
} }
return out; return out;
} }
@ -577,37 +725,68 @@ var elementsFromFilter = function(filter) {
var reFilter = null; var reFilter = null;
try { try {
reFilter = new RegExp(reStr); reFilter = new RegExp(reStr);
} catch (e) { }
catch (e) {
return out; return out;
} }
// Lookup by tag names.
var src1stProps = netFilter1stSources; var src1stProps = netFilter1stSources;
var src2ndProps = netFilter2ndSources; var src2ndProps = netFilter2ndSources;
var elems = document.querySelectorAll(Object.keys(src1stProps).join()); var srcProp, src;
var i = elems.length; elems = document.querySelectorAll(Object.keys(src1stProps).join());
var elem, src; iElem = elems.length;
while ( i-- ) { while ( iElem-- ) {
elem = elems[i]; elem = elems[iElem];
src = elem[src1stProps[elem.localName]]; srcProp = src1stProps[elem.localName];
src = elem[srcProp];
if ( typeof src !== 'string' || src.length === 0 ) { if ( typeof src !== 'string' || src.length === 0 ) {
src = elem[src2ndProps[elem.localName]]; srcProp = src2ndProps[elem.localName];
src = elem[srcProp];
} }
if ( src && reFilter.test(src) ) { if ( src && reFilter.test(src) ) {
out.push(elem); out.push({
type: 'network',
elem: elem,
src: srcProp,
opts: filterTypes[elem.localName],
});
} }
} }
// Lookup by inline-styled background image.
elems = document.querySelectorAll('[style*="background-image"]');
iElem = elems.length;
while ( iElem-- ) {
elem = elems[iElem];
if ( reFilter.test(backgroundImageURLFromElement(elem)) ) {
out.push({
type: 'network',
elem: elem,
style: 'background-image',
opts: 'image',
});
}
}
// Lookup by stylesheet-styled background image.
var sheets = document.styleSheets;
var iSheet = sheets.length;
while ( iSheet-- ) {
elementsFromStylesheet(sheets[iSheet], reFilter, out);
}
return out; return out;
}; };
// https://www.youtube.com/watch?v=YI2XuIOW3gM // https://www.youtube.com/watch?v=nuUXJ6RfIik
/******************************************************************************/ /******************************************************************************/
var userFilterFromCandidate = function() { var userFilterFromCandidate = function() {
var v = taCandidate.value; var v = taCandidate.value;
var items = elementsFromFilter(v);
var elems = elementsFromFilter(v); if ( items.length === 0 ) {
if ( elems.length === 0 ) {
return false; return false;
} }
@ -623,19 +802,34 @@ var userFilterFromCandidate = function() {
return hostname + v; return hostname + v;
} }
// If domain included in filter, no need for domain option // Assume net filter
if ( v.lastIndexOf('||', 0) === 0 ) { var opts = [];
return v;
// If no domain included in filter, we need domain option
if ( v.lastIndexOf('||', 0) === -1 ) {
opts.push('domain=' + hostname);
} }
// Assume net filter var item = items[0];
return v + '$domain=' + hostname; if ( item.opts ) {
opts.push(item.opts);
}
if ( opts.length ) {
v += '$' + opts.join(',');
}
return v;
}; };
/******************************************************************************/ /******************************************************************************/
var onCandidateChanged = function() { var onCandidateChanged = function() {
var elems = elementsFromFilter(taCandidate.value); var elems = [];
var items = elementsFromFilter(taCandidate.value);
for ( var i = 0; i < items.length; i++ ) {
elems.push(items[i].elem);
}
dialog.querySelector('#create').disabled = elems.length === 0; dialog.querySelector('#create').disabled = elems.length === 0;
highlightElements(elems); highlightElements(elems);
}; };
@ -705,7 +899,7 @@ var onDialogClicked = function(ev) {
filters: '! ' + d.toLocaleString() + ' ' + window.location.href + '\n' + filter, filters: '! ' + d.toLocaleString() + ' ' + window.location.href + '\n' + filter,
} }
); );
removeElements(elementsFromFilter(taCandidate.value)); filterElements(taCandidate.value);
stopPicker(); stopPicker();
} }
} }
@ -789,13 +983,16 @@ var showDialog = function(options) {
/******************************************************************************/ /******************************************************************************/
var elementFromPoint = function(x, y) { var elementFromPoint = function(x, y, includeBody) {
if ( !pickerRoot ) { if ( !pickerRoot ) {
return null; return null;
} }
pickerRoot.style.pointerEvents = 'none'; pickerRoot.style.pointerEvents = 'none';
var elem = document.elementFromPoint(x, y); var elem = document.elementFromPoint(x, y);
if ( elem === document.body || elem === document.documentElement ) { if (
elem === document.body && !includeBody ||
elem === document.documentElement
) {
elem = null; elem = null;
} }
pickerRoot.style.pointerEvents = ''; pickerRoot.style.pointerEvents = '';
@ -834,12 +1031,9 @@ var onSvgClicked = function(ev) {
unpausePicker(); unpausePicker();
return; return;
} }
if ( filtersFrom(ev.clientX, ev.clientY) === 0 ) {
var elem = elementFromPoint(ev.clientX, ev.clientY);
if ( elem === null ) {
return; return;
} }
filtersFromElement(elem);
showDialog(); showDialog();
}; };
@ -962,13 +1156,9 @@ var startPicker = function(details) {
highlightElements([], true); highlightElements([], true);
var elem;
// Try using mouse position // Try using mouse position
if ( details.clientX !== -1 ) { if ( details.clientX !== -1 ) {
elem = elementFromPoint(details.clientX, details.clientY); if ( filtersFrom(details.clientX, details.clientY) !== 0 ) {
if ( elem !== null ) {
filtersFromElement(elem);
showDialog(); showDialog();
return; return;
} }
@ -982,11 +1172,11 @@ var startPicker = function(details) {
} }
var srcAttrMap = { var srcAttrMap = {
'a': 'href', 'a': 'href',
'img': 'src', 'audio': 'src',
'iframe': 'src',
'embed': 'src', 'embed': 'src',
'iframe': 'src',
'img': 'src',
'video': 'src', 'video': 'src',
'audio': 'src'
}; };
var tagName = target.slice(0, pos); var tagName = target.slice(0, pos);
var url = target.slice(pos + 1); var url = target.slice(pos + 1);
@ -996,7 +1186,7 @@ var startPicker = function(details) {
} }
var elems = document.querySelectorAll(tagName + '[' + attr + ']'); var elems = document.querySelectorAll(tagName + '[' + attr + ']');
var i = elems.length; var i = elems.length;
var src; var elem, src;
while ( i-- ) { while ( i-- ) {
elem = elems[i]; elem = elems[i];
src = elem[attr]; src = elem[attr];
@ -1010,7 +1200,7 @@ var startPicker = function(details) {
behavior: 'smooth', behavior: 'smooth',
block: 'start' block: 'start'
}); });
filtersFromElement(elem); filtersFrom(elem);
showDialog({ modifier: true }); showDialog({ modifier: true });
return; return;
} }
@ -1041,13 +1231,21 @@ pickerRoot.style.cssText = [
'outline: 0', 'outline: 0',
'z-index: 2147483647', 'z-index: 2147483647',
'' ''
].join('!important; '); ].join('!important;');
// https://github.com/gorhill/uBlock/issues/1529 // https://github.com/gorhill/uBlock/issues/1529
// In addition to inline styles, harden the element picker styles by using // In addition to inline styles, harden the element picker styles by using
// a dedicated style tag. // a dedicated style tag.
pickerStyle = document.createElement('style'); pickerStyle = document.createElement('style');
pickerStyle.textContent = '#' + pickerRoot.id + ' { ' + pickerRoot.style.cssText + ' }'; pickerStyle.textContent = [
'#' + vAPI.sessionId + ' {',
pickerRoot.style.cssText,
'}',
'[' + vAPI.sessionId + '-clickblind] {',
'pointer-events: none !important;',
'}',
''
].join('\n');
document.documentElement.appendChild(pickerStyle); document.documentElement.appendChild(pickerStyle);
pickerRoot.onload = function() { pickerRoot.onload = function() {