 Issue 29383960:
  Issue 3143 - Filter elements with :-abp-has()  (Closed) 
  Base URL: https://hg.adblockplus.org/adblockpluscore
    
  
    Issue 29383960:
  Issue 3143 - Filter elements with :-abp-has()  (Closed) 
  Base URL: https://hg.adblockplus.org/adblockpluscore| Left: | ||
| Right: | 
| OLD | NEW | 
|---|---|
| 1 // We are currently limited to ECMAScript 5 in this file, because it is being | 1 // We are currently limited to ECMAScript 5 in this file, because it is being | 
| 2 // used in the browser tests. See https://issues.adblockplus.org/ticket/4796 | 2 // used in the browser tests. See https://issues.adblockplus.org/ticket/4796 | 
| 
Wladimir Palant
2017/04/25 10:57:52
This is really a PITA. Given that https://issues.a
 
hub
2017/04/25 19:42:47
If issue 4796 lands before this, then I'll fix it.
 
Wladimir Palant
2017/04/26 00:03:09
As I suggested on IRC, it would probably be easies
 
hub
2017/04/27 17:43:04
Newer patch is now ES6 based on top of your patch.
 | |
| 3 | 3 | 
| 4 var propertySelectorRegExp = /\[\-abp\-properties=(["'])([^"']+)\1\]/; | 4 var propertySelectorRegExp = /\[-abp-properties=(["'])([^"']+)\1\]/; | 
| 5 var abpSelectorRegExp = /\[-abp-selector=(["'])(.+)\1\]/; | |
| 6 var pseudoClassHasSelectorRegExp = /:has\((.*)\)/; | |
| 7 var pseudoClassPropsSelectorRegExp = /:-abp-properties\((["'])([^"']+)\1\)/; | |
| 
Wladimir Palant
2017/04/25 11:30:46
Having :has() but :-abp-properties() is inconsiste
 
hub
2017/04/25 19:42:47
OK.
 | |
| 5 | 8 | 
| 6 function splitSelector(selector) | 9 function splitSelector(selector) | 
| 7 { | 10 { | 
| 8 if (selector.indexOf(",") == -1) | 11 if (selector.indexOf(",") == -1) | 
| 9 return [selector]; | 12 return [selector]; | 
| 10 | 13 | 
| 11 var selectors = []; | 14 var selectors = []; | 
| 12 var start = 0; | 15 var start = 0; | 
| 13 var level = 0; | 16 var level = 0; | 
| 14 var sep = ""; | 17 var sep = ""; | 
| (...skipping 19 matching lines...) Expand all Loading... | |
| 34 selectors.push(selector.substring(start, i)); | 37 selectors.push(selector.substring(start, i)); | 
| 35 start = i + 1; | 38 start = i + 1; | 
| 36 } | 39 } | 
| 37 } | 40 } | 
| 38 } | 41 } | 
| 39 | 42 | 
| 40 selectors.push(selector.substring(start)); | 43 selectors.push(selector.substring(start)); | 
| 41 return selectors; | 44 return selectors; | 
| 42 } | 45 } | 
| 43 | 46 | 
| 44 function ElemHideEmulation(window, getFiltersFunc, addSelectorsFunc) | 47 const ABP_ATTR = "abp-0ac791f0-a03b-4f2c-a935-a285bc2e668e"; | 
| 48 const ABP_ATTR_SEL = "[" + ABP_ATTR + "] "; | |
| 49 | |
| 50 function matchChildren(e, selector) | |
| 51 { | |
| 52 var subElement; | |
| 53 var newSelector = ABP_ATTR_SEL + selector; | |
| 54 var parentNode = e.parentNode || document; | |
| 55 | |
| 56 e.setAttribute(ABP_ATTR, ""); | |
| 57 subElement = parentNode.querySelector(newSelector); | |
| 58 e.removeAttribute(ABP_ATTR); | |
| 
Wladimir Palant
2017/04/25 10:57:52
This hack is problematic. Even if we make the attr
 
hub
2017/04/25 19:42:47
I was thinking we could generate an actually rando
 
hub
2017/04/27 17:43:04
On 2017/04/25 19:42:47, hub wrote:
Newer patch re
 | |
| 59 | |
| 60 return subElement != null; | |
| 61 } | |
| 62 | |
| 63 function selectChildren(e, selector) | |
| 64 { | |
| 65 var subElements; | |
| 66 var newSelector = ABP_ATTR_SEL + selector; | |
| 67 var parentNode = e.parentNode || document; | |
| 68 | |
| 69 e.setAttribute(ABP_ATTR, ""); | |
| 70 subElements = parentNode.querySelectorAll(newSelector); | |
| 71 e.removeAttribute(ABP_ATTR); | |
| 72 | |
| 73 return subElements; | |
| 74 } | |
| 75 | |
| 76 /** Unwrap the pattern out of a [-abp-selector=''] if necessary | |
| 77 * @param {object} pattern - The pattern object to unwrap | |
| 78 * @return {object} the parsed pattern object or undefined if nothing parse. | |
| 79 */ | |
| 80 function unwrapPattern(pattern) | |
| 
Wladimir Palant
2017/04/25 10:57:53
This unwrapping belongs into lib/filterClasses.js
 
hub
2017/04/25 19:42:47
ok, moving it.
 | |
| 81 { | |
| 82 var match = abpSelectorRegExp.exec(pattern.selector); | |
| 83 if (match) | |
| 84 { | |
| 85 var prefix = pattern.selector.substr(0, match.index).trim(); | |
| 86 var suffix = pattern.selector.substr(match.index + match[0].length).trim(); | |
| 87 var selector = prefix + match[2] + suffix; | |
| 88 return parsePattern({selector: selector}, true); | |
| 89 } | |
| 90 | |
| 91 return parsePattern(pattern, false); | |
| 92 } | |
| 93 | |
| 94 function parsePattern(pattern, unwrapped) | |
| 95 { | |
| 96 var selector = pattern.selector; | |
| 97 var match; | |
| 98 | |
| 99 if (unwrapped) | |
| 100 { | |
| 101 // Seems that the pattern should be unwrapped. | |
| 102 // This is a fundamental error. | |
| 103 if (selector.indexOf("[-abp-selector=") != -1) | |
| 104 return; | |
| 105 | |
| 106 match = pseudoClassHasSelectorRegExp.exec(selector); | |
| 
Wladimir Palant
2017/04/25 10:57:52
The syntax is getting too complex, parsing CSS wit
 | |
| 107 if (match) | |
| 108 return { | |
| 109 type: "has", | |
| 110 text: pattern.text, | |
| 111 elementMatcher: new PseudoHasMatcher(match[1]), | |
| 112 prefix: selector.substr(0, match.index).trim(), | |
| 113 suffix: selector.substr(match.index + match[0].length).trim() | |
| 114 }; | |
| 115 match = pseudoClassPropsSelectorRegExp.exec(selector); | |
| 116 } | |
| 117 | |
| 118 if (!match) | |
| 119 match = propertySelectorRegExp.exec(selector); | |
| 
Wladimir Palant
2017/04/25 10:57:52
I don't think that we should have this special han
 
hub
2017/04/25 19:42:47
good idea. Like unwrap above.
 | |
| 120 if (match) | |
| 121 { | |
| 122 var regexpString; | |
| 123 var propertyExpression = match[2]; | |
| 124 if (propertyExpression.length >= 2 && propertyExpression[0] == "/" && | |
| 125 propertyExpression[propertyExpression.length - 1] == "/") | |
| 126 regexpString = propertyExpression.slice(1, -1) | |
| 127 .replace("\\x7B ", "{").replace("\\x7D ", "}"); | |
| 128 else | |
| 129 regexpString = filterToRegExp(propertyExpression); | |
| 130 return { | |
| 131 type: "props", | |
| 132 text: pattern.text, | |
| 133 regexp: new RegExp(regexpString, "i"), | |
| 134 prefix: selector.substr(0, match.index), | |
| 135 suffix: selector.substr(match.index + match[0].length) | |
| 136 }; | |
| 137 } | |
| 138 } | |
| 139 | |
| 140 function matchStyleProps(style, rule, pattern, selectors, filters) | |
| 141 { | |
| 142 if (pattern.regexp.test(style)) | |
| 143 { | |
| 144 var subSelectors = splitSelector(rule.selectorText); | |
| 145 for (var i = 0; i < subSelectors.length; i++) | |
| 146 { | |
| 147 var subSelector = subSelectors[i]; | |
| 148 selectors.push(pattern.prefix + subSelector + pattern.suffix); | |
| 149 filters.push(pattern.text); | |
| 150 } | |
| 151 } | |
| 152 } | |
| 153 | |
| 154 function findPropsSelectors(stylesheet, patterns, selectors, filters) | |
| 155 { | |
| 156 var rules = stylesheet.cssRules; | |
| 157 if (!rules) | |
| 158 return; | |
| 159 | |
| 160 for (var i = 0; i < rules.length; i++) | |
| 161 { | |
| 162 var rule = rules[i]; | |
| 163 if (rule.type != rule.STYLE_RULE) | |
| 164 continue; | |
| 165 | |
| 166 var style = stringifyStyle(rule.style); | |
| 167 for (var j = 0; j < patterns.length; j++) | |
| 168 { | |
| 169 matchStyleProps(style, rule, patterns[j], selectors, filters); | |
| 170 } | |
| 171 } | |
| 172 } | |
| 173 | |
| 174 /** | |
| 175 * Match the selector @pattern containing :has() starting from @node. | |
| 176 * @param {string} pattern - the pattern to match | |
| 177 * @param {object} node - the top level node. | |
| 178 */ | |
| 179 function pseudoClassHasMatch(pattern, node, stylesheets, elements, filters) | |
| 180 { | |
| 181 // select element for the prefix pattern. Or just use node. | |
| 182 var haveEl = pattern.prefix ? node.querySelectorAll(pattern.prefix) : [ node ] ; | |
| 
Wladimir Palant
2017/04/25 10:57:53
This will work correctly for selectors like "div >
 
hub
2017/04/25 19:42:47
I naively assumed that since these are filters, we
 
Wladimir Palant
2017/04/26 00:03:09
Feel free to suggest a meaningful subset of the sy
 | |
| 183 for (var j = 0; j < haveEl.length; j++) | |
| 184 { | |
| 185 var matched = pattern.elementMatcher.match(haveEl[j], stylesheets); | |
| 186 if (!matched) | |
| 187 continue; | |
| 188 | |
| 189 if (pattern.suffix) | |
| 190 { | |
| 191 var subElements = selectChildren(haveEl[j], pattern.suffix); | |
| 192 if (subElements) | |
| 193 { | |
| 194 for (var k = 0; k < subElements.length; k++) | |
| 195 { | |
| 196 elements.push(subElements[k]); | |
| 197 filters.push(pattern.text); | |
| 198 } | |
| 199 } | |
| 200 } | |
| 201 else | |
| 202 { | |
| 203 elements.push(haveEl[j]); | |
| 204 filters.push(pattern.text); | |
| 205 } | |
| 206 } | |
| 207 } | |
| 208 | |
| 209 function stringifyStyle(style) | |
| 210 { | |
| 211 var styles = []; | |
| 212 for (var i = 0; i < style.length; i++) | |
| 213 { | |
| 214 var property = style.item(i); | |
| 215 var value = style.getPropertyValue(property); | |
| 216 var priority = style.getPropertyPriority(property); | |
| 217 styles.push(property + ": " + value + (priority ? " !" + priority : "") + "; "); | |
| 218 } | |
| 219 styles.sort(); | |
| 220 return styles.join(" "); | |
| 221 } | |
| 222 | |
| 223 /** matcher for the pseudo CSS4 class :has() | |
| 224 * For those browser that don't have it yet. | |
| 225 */ | |
| 226 function PseudoHasMatcher(selector) | |
| 227 { | |
| 228 this.hasSelector = selector; | |
| 229 this.parsed = parsePattern({selector: this.hasSelector}, true); | |
| 230 } | |
| 231 | |
| 232 PseudoHasMatcher.prototype = { | |
| 233 match: function(elem, stylesheets) | |
| 234 { | |
| 235 var selectors = []; | |
| 236 | |
| 237 if (this.parsed) | |
| 238 { | |
| 239 var filters = []; // don't need this, we have a partial filter. | |
| 240 if (this.parsed.type == "has") | |
| 241 { | |
| 242 var child = elem.firstChild; | |
| 243 while (child) | |
| 244 { | |
| 245 if (child.nodeType === Node.ELEMENT_NODE) | |
| 246 { | |
| 247 var matches = []; | |
| 248 pseudoClassHasMatch(this.parsed, child, stylesheets, matches, filter s); | |
| 249 if (matches.length > 0) | |
| 250 return true; | |
| 251 } | |
| 252 child = child.nextSibling; | |
| 253 } | |
| 254 return false; | |
| 255 } | |
| 256 if (this.parsed.type == "props") | |
| 257 { | |
| 258 for (var i = 0; i < stylesheets.length; i++) | |
| 259 findPropsSelectors(stylesheets[i], [this.parsed], selectors, filters); | |
| 
Wladimir Palant
2017/04/25 10:57:52
We will be going through the stylesheets multiple
 | |
| 260 } | |
| 261 } | |
| 262 else | |
| 263 { | |
| 264 selectors = [this.hasSelector]; | |
| 265 } | |
| 266 | |
| 267 var matched = false; | |
| 268 // look up for all elements that match the :has(). | |
| 269 for (var k = 0; k < selectors.length; k++) | |
| 270 { | |
| 271 try | |
| 272 { | |
| 273 matched = matchChildren(elem, selectors[k]); | |
| 274 if (matched) | |
| 275 break; | |
| 276 } | |
| 277 catch (e) | |
| 278 { | |
| 279 console.error("Exception with querySelector()", selectors[k], e); | |
| 280 } | |
| 281 } | |
| 282 return matched; | |
| 283 } | |
| 284 }; | |
| 285 | |
| 286 function ElemHideEmulation(window, getFiltersFunc, addSelectorsFunc, hideElement sFunc) | |
| 45 { | 287 { | 
| 46 this.window = window; | 288 this.window = window; | 
| 47 this.getFiltersFunc = getFiltersFunc; | 289 this.getFiltersFunc = getFiltersFunc; | 
| 48 this.addSelectorsFunc = addSelectorsFunc; | 290 this.addSelectorsFunc = addSelectorsFunc; | 
| 291 this.hideElementsFunc = hideElementsFunc; | |
| 
Wladimir Palant
2017/04/25 10:57:53
I guess that hideElementsFunc should do some kind
 | |
| 49 } | 292 } | 
| 50 | 293 | 
| 51 ElemHideEmulation.prototype = { | 294 ElemHideEmulation.prototype = { | 
| 52 stringifyStyle: function(style) | |
| 53 { | |
| 54 var styles = []; | |
| 55 for (var i = 0; i < style.length; i++) | |
| 56 { | |
| 57 var property = style.item(i); | |
| 58 var value = style.getPropertyValue(property); | |
| 59 var priority = style.getPropertyPriority(property); | |
| 60 styles.push(property + ": " + value + (priority ? " !" + priority : "") + ";"); | |
| 61 } | |
| 62 styles.sort(); | |
| 63 return styles.join(" "); | |
| 64 }, | |
| 65 | 295 | 
| 66 isSameOrigin: function(stylesheet) | 296 isSameOrigin: function(stylesheet) | 
| 67 { | 297 { | 
| 68 try | 298 try | 
| 69 { | 299 { | 
| 70 return new URL(stylesheet.href).origin == this.window.location.origin; | 300 return new URL(stylesheet.href).origin == this.window.location.origin; | 
| 71 } | 301 } | 
| 72 catch (e) | 302 catch (e) | 
| 73 { | 303 { | 
| 74 // Invalid URL, assume that it is first-party. | 304 // Invalid URL, assume that it is first-party. | 
| 75 return true; | 305 return true; | 
| 76 } | 306 } | 
| 77 }, | 307 }, | 
| 78 | 308 | 
| 79 findSelectors: function(stylesheet, selectors, filters) | 309 findPseudoClassHasElements: function(node, stylesheets, elements, filters) | 
| 80 { | 310 { | 
| 81 // Explicitly ignore third-party stylesheets to ensure consistent behavior | 311 for (var i = 0; i < this.pseudoHasPatterns.length; i++) | 
| 82 // between Firefox and Chrome. | 312 { | 
| 83 if (!this.isSameOrigin(stylesheet)) | 313 pseudoClassHasMatch(this.pseudoHasPatterns[i], node, stylesheets, elements , filters); | 
| 84 return; | |
| 85 | |
| 86 var rules = stylesheet.cssRules; | |
| 87 if (!rules) | |
| 88 return; | |
| 89 | |
| 90 for (var i = 0; i < rules.length; i++) | |
| 91 { | |
| 92 var rule = rules[i]; | |
| 93 if (rule.type != rule.STYLE_RULE) | |
| 94 continue; | |
| 95 | |
| 96 var style = this.stringifyStyle(rule.style); | |
| 97 for (var j = 0; j < this.patterns.length; j++) | |
| 98 { | |
| 99 var pattern = this.patterns[j]; | |
| 100 if (pattern.regexp.test(style)) | |
| 101 { | |
| 102 var subSelectors = splitSelector(rule.selectorText); | |
| 103 for (var k = 0; k < subSelectors.length; k++) | |
| 104 { | |
| 105 var subSelector = subSelectors[k]; | |
| 106 selectors.push(pattern.prefix + subSelector + pattern.suffix); | |
| 107 filters.push(pattern.text); | |
| 108 } | |
| 109 } | |
| 110 } | |
| 111 } | 314 } | 
| 112 }, | 315 }, | 
| 113 | 316 | 
| 114 addSelectors: function(stylesheets) | 317 addSelectors: function(stylesheets) | 
| 115 { | 318 { | 
| 116 var selectors = []; | 319 var selectors = []; | 
| 117 var filters = []; | 320 var filters = []; | 
| 118 for (var i = 0; i < stylesheets.length; i++) | 321 for (var i = 0; i < stylesheets.length; i++) | 
| 119 this.findSelectors(stylesheets[i], selectors, filters); | 322 { | 
| 323 // Explicitly ignore third-party stylesheets to ensure consistent behavior | |
| 324 // between Firefox and Chrome. | |
| 325 if (!this.isSameOrigin(stylesheets[i])) | |
| 326 continue; | |
| 327 findPropsSelectors(stylesheets[i], this.propSelPatterns, selectors, filter s); | |
| 328 } | |
| 120 this.addSelectorsFunc(selectors, filters); | 329 this.addSelectorsFunc(selectors, filters); | 
| 121 }, | 330 }, | 
| 122 | 331 | 
| 332 hideElements: function(stylesheets) | |
| 333 { | |
| 334 var elements = []; | |
| 335 var filters = []; | |
| 336 this.findPseudoClassHasElements(document, stylesheets, elements, filters); | |
| 337 this.hideElementsFunc(elements, filters); | |
| 338 }, | |
| 339 | |
| 123 onLoad: function(event) | 340 onLoad: function(event) | 
| 124 { | 341 { | 
| 125 var stylesheet = event.target.sheet; | 342 var stylesheet = event.target.sheet; | 
| 126 if (stylesheet) | 343 if (stylesheet) | 
| 127 this.addSelectors([stylesheet]); | 344 this.addSelectors([stylesheet]); | 
| 345 this.hideElements([stylesheet]); | |
| 128 }, | 346 }, | 
| 129 | 347 | 
| 130 apply: function() | 348 apply: function() | 
| 131 { | 349 { | 
| 132 this.getFiltersFunc(function(patterns) | 350 this.getFiltersFunc(function(patterns) | 
| 133 { | 351 { | 
| 134 this.patterns = []; | 352 this.propSelPatterns = []; | 
| 353 this.pseudoHasPatterns = []; | |
| 135 for (var i = 0; i < patterns.length; i++) | 354 for (var i = 0; i < patterns.length; i++) | 
| 136 { | 355 { | 
| 137 var pattern = patterns[i]; | 356 var pattern = patterns[i]; | 
| 138 var match = propertySelectorRegExp.exec(pattern.selector); | 357 var parsed = unwrapPattern(pattern); | 
| 139 if (!match) | 358 if (parsed == undefined) | 
| 140 continue; | 359 continue; | 
| 141 | 360 if (parsed.type == "props") | 
| 142 var propertyExpression = match[2]; | 361 { | 
| 143 var regexpString; | 362 this.propSelPatterns.push(parsed); | 
| 144 if (propertyExpression.length >= 2 && propertyExpression[0] == "/" && | 363 } | 
| 145 propertyExpression[propertyExpression.length - 1] == "/") | 364 else if (parsed.type == "has") | 
| 146 regexpString = propertyExpression.slice(1, -1) | 365 { | 
| 147 .replace("\\x7B ", "{").replace("\\x7D ", "}"); | 366 this.pseudoHasPatterns.push(parsed); | 
| 148 else | 367 } | 
| 149 regexpString = filterToRegExp(propertyExpression); | 368 } | 
| 150 | 369 | 
| 151 this.patterns.push({ | 370 if (this.pseudoHasPatterns.length > 0 || this.propSelPatterns.length > 0) | 
| 152 text: pattern.text, | |
| 153 regexp: new RegExp(regexpString, "i"), | |
| 154 prefix: pattern.selector.substr(0, match.index), | |
| 155 suffix: pattern.selector.substr(match.index + match[0].length) | |
| 156 }); | |
| 157 } | |
| 158 | |
| 159 if (this.patterns.length > 0) | |
| 160 { | 371 { | 
| 161 var document = this.window.document; | 372 var document = this.window.document; | 
| 162 this.addSelectors(document.styleSheets); | 373 this.addSelectors(document.styleSheets); | 
| 374 this.hideElements(document.styleSheets); | |
| 163 document.addEventListener("load", this.onLoad.bind(this), true); | 375 document.addEventListener("load", this.onLoad.bind(this), true); | 
| 164 } | 376 } | 
| 165 }.bind(this)); | 377 }.bind(this)); | 
| 166 } | 378 } | 
| 167 }; | 379 }; | 
| OLD | NEW |