| 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 |
| 3 | 3 |
| 4 var propertySelectorRegExp = /\[\-abp\-properties=(["'])([^"']+)\1\]/; | 4 var propertySelectorRegExp = /\[\-abp\-properties=(["'])([^"']+)\1\]/; |
| 5 var pseudoClassHasSelectorRegExp = /:has\((.*)\)/; |
| 5 | 6 |
| 6 function splitSelector(selector) | 7 function splitSelector(selector) |
| 7 { | 8 { |
| 8 if (selector.indexOf(",") == -1) | 9 if (selector.indexOf(",") == -1) |
| 9 return [selector]; | 10 return [selector]; |
| 10 | 11 |
| 11 var selectors = []; | 12 var selectors = []; |
| 12 var start = 0; | 13 var start = 0; |
| 13 var level = 0; | 14 var level = 0; |
| 14 var sep = ""; | 15 var sep = ""; |
| (...skipping 19 matching lines...) Expand all Loading... |
| 34 selectors.push(selector.substring(start, i)); | 35 selectors.push(selector.substring(start, i)); |
| 35 start = i + 1; | 36 start = i + 1; |
| 36 } | 37 } |
| 37 } | 38 } |
| 38 } | 39 } |
| 39 | 40 |
| 40 selectors.push(selector.substring(start)); | 41 selectors.push(selector.substring(start)); |
| 41 return selectors; | 42 return selectors; |
| 42 } | 43 } |
| 43 | 44 |
| 44 function ElemHideEmulation(window, getFiltersFunc, addSelectorsFunc) | 45 const ABP_ATTR = "abp-0ac791f0-a03b-4f2c-a935-a285bc2e668e"; |
| 46 const ABP_ATTR_SEL = "[" + ABP_ATTR + "] "; |
| 47 |
| 48 function matchChildren(e, selector) |
| 49 { |
| 50 var subElement; |
| 51 var newSelector = ABP_ATTR_SEL + selector; |
| 52 var parentNode = e.parentNode || document; |
| 53 |
| 54 e.setAttribute(ABP_ATTR, ""); |
| 55 subElement = parentNode.querySelector(newSelector); |
| 56 e.removeAttribute(ABP_ATTR); |
| 57 |
| 58 return subElement != null; |
| 59 } |
| 60 |
| 61 function selectChildren(e, selector) |
| 62 { |
| 63 var subElements; |
| 64 var newSelector = ABP_ATTR_SEL + selector; |
| 65 var parentNode = e.parentNode || document; |
| 66 |
| 67 e.setAttribute(ABP_ATTR, ""); |
| 68 subElements = parentNode.querySelectorAll(newSelector); |
| 69 e.removeAttribute(ABP_ATTR); |
| 70 |
| 71 return subElements; |
| 72 } |
| 73 |
| 74 function parsePattern(pattern) |
| 75 { |
| 76 // we should catch the :has() pseudo class first. |
| 77 var match = pseudoClassHasSelectorRegExp.exec(pattern.selector); |
| 78 if (match) |
| 79 { |
| 80 return { |
| 81 type: "has", |
| 82 text: pattern.text, |
| 83 elementMatcher: new PseudoHasMatcher(match[1]), |
| 84 prefix: pattern.selector.substr(0, match.index).trim(), |
| 85 suffix: pattern.selector.substr(match.index + match[0].length).trim() |
| 86 }; |
| 87 } |
| 88 |
| 89 match = propertySelectorRegExp.exec(pattern.selector); |
| 90 if (match) |
| 91 { |
| 92 var regexpString; |
| 93 var propertyExpression = match[2]; |
| 94 if (propertyExpression.length >= 2 && propertyExpression[0] == "/" && |
| 95 propertyExpression[propertyExpression.length - 1] == "/") |
| 96 regexpString = propertyExpression.slice(1, -1) |
| 97 .replace("\\x7B ", "{").replace("\\x7D ", "}"); |
| 98 else |
| 99 regexpString = filterToRegExp(propertyExpression); |
| 100 return { |
| 101 type: "props", |
| 102 text: pattern.text, |
| 103 regexp: new RegExp(regexpString, "i"), |
| 104 prefix: pattern.selector.substr(0, match.index), |
| 105 suffix: pattern.selector.substr(match.index + match[0].length) |
| 106 }; |
| 107 } |
| 108 } |
| 109 |
| 110 function matchStyleProps(style, rule, pattern, selectors, filters) |
| 111 { |
| 112 if (pattern.regexp.test(style)) |
| 113 { |
| 114 var subSelectors = splitSelector(rule.selectorText); |
| 115 for (var i = 0; i < subSelectors.length; i++) |
| 116 { |
| 117 var subSelector = subSelectors[i]; |
| 118 selectors.push(pattern.prefix + subSelector + pattern.suffix); |
| 119 filters.push(pattern.text); |
| 120 } |
| 121 } |
| 122 } |
| 123 |
| 124 function findPropsSelectors(stylesheet, patterns, selectors, filters) |
| 125 { |
| 126 var rules = stylesheet.cssRules; |
| 127 if (!rules) |
| 128 return; |
| 129 |
| 130 for (var i = 0; i < rules.length; i++) |
| 131 { |
| 132 var rule = rules[i]; |
| 133 if (rule.type != rule.STYLE_RULE) |
| 134 continue; |
| 135 |
| 136 var style = stringifyStyle(rule.style); |
| 137 for (var j = 0; j < patterns.length; j++) |
| 138 { |
| 139 matchStyleProps(style, rule, patterns[j], selectors, filters); |
| 140 } |
| 141 } |
| 142 } |
| 143 |
| 144 /** |
| 145 * Match the selector @pattern containing :has() starting from @node. |
| 146 */ |
| 147 function pseudoClassHasMatch(pattern, node, stylesheets, elements, filters) |
| 148 { |
| 149 // select element for the prefix pattern. Or just use node. |
| 150 var haveEl = pattern.prefix ? node.querySelectorAll(pattern.prefix) : [ node ]
; |
| 151 for (var j = 0; j < haveEl.length; j++) |
| 152 { |
| 153 var matched = pattern.elementMatcher.match(haveEl[j], stylesheets); |
| 154 if (!matched) |
| 155 continue; |
| 156 |
| 157 if (pattern.suffix) |
| 158 { |
| 159 var subElements = selectChildren(haveEl[j], pattern.suffix); |
| 160 if (subElements) |
| 161 { |
| 162 for (var k = 0; k < subElements.length; k++) |
| 163 { |
| 164 elements.push(subElements[k]); |
| 165 filters.push(pattern.text); |
| 166 } |
| 167 } |
| 168 } |
| 169 else |
| 170 { |
| 171 elements.push(haveEl[j]); |
| 172 filters.push(pattern.text); |
| 173 } |
| 174 } |
| 175 } |
| 176 |
| 177 function stringifyStyle(style) |
| 178 { |
| 179 var styles = []; |
| 180 for (var i = 0; i < style.length; i++) |
| 181 { |
| 182 var property = style.item(i); |
| 183 var value = style.getPropertyValue(property); |
| 184 var priority = style.getPropertyPriority(property); |
| 185 styles.push(property + ": " + value + (priority ? " !" + priority : "") + ";
"); |
| 186 } |
| 187 styles.sort(); |
| 188 return styles.join(" "); |
| 189 } |
| 190 |
| 191 // matcher for the pseudo CSS4 class :has |
| 192 // For those browser that don't have it yet. |
| 193 function PseudoHasMatcher(selector) |
| 194 { |
| 195 this.hasSelector = selector; |
| 196 this.parsed = parsePattern({ selector: this.hasSelector }); |
| 197 } |
| 198 |
| 199 PseudoHasMatcher.prototype = { |
| 200 match: function(elem, stylesheets) |
| 201 { |
| 202 var selectors = []; |
| 203 |
| 204 if (this.parsed) |
| 205 { |
| 206 var filters = []; // don't need this, we have a partial filter. |
| 207 if (this.parsed.type == "has") |
| 208 { |
| 209 var child = elem.firstChild; |
| 210 while (child) |
| 211 { |
| 212 if (child.nodeType === Node.ELEMENT_NODE) |
| 213 { |
| 214 var matches = []; |
| 215 pseudoClassHasMatch(this.parsed, child, stylesheets, matches, filter
s) |
| 216 if (matches.length > 0) |
| 217 return true; |
| 218 } |
| 219 child = child.nextSibling; |
| 220 } |
| 221 return false; |
| 222 } |
| 223 if (this.parsed.type == "props") |
| 224 { |
| 225 for (var i = 0; i < stylesheets.length; i++) |
| 226 findPropsSelectors(stylesheets[i], [this.parsed], selectors, filters); |
| 227 } |
| 228 } |
| 229 else |
| 230 { |
| 231 selectors = [this.hasSelector]; |
| 232 } |
| 233 |
| 234 var matched = false; |
| 235 // look up for all elements that match the :has(). |
| 236 for (var k = 0; k < selectors.length; k++) |
| 237 { |
| 238 try |
| 239 { |
| 240 matched = matchChildren(elem, selectors[k]); |
| 241 if (matched) |
| 242 break; |
| 243 } |
| 244 catch(e) |
| 245 { |
| 246 console.error("Exception with querySelector()", selectors[k], e); |
| 247 } |
| 248 } |
| 249 return matched; |
| 250 } |
| 251 }; |
| 252 |
| 253 function ElemHideEmulation(window, getFiltersFunc, addSelectorsFunc, hideElement
sFunc) |
| 45 { | 254 { |
| 46 this.window = window; | 255 this.window = window; |
| 47 this.getFiltersFunc = getFiltersFunc; | 256 this.getFiltersFunc = getFiltersFunc; |
| 48 this.addSelectorsFunc = addSelectorsFunc; | 257 this.addSelectorsFunc = addSelectorsFunc; |
| 258 this.hideElementsFunc = hideElementsFunc; |
| 49 } | 259 } |
| 50 | 260 |
| 51 ElemHideEmulation.prototype = { | 261 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 | 262 |
| 66 isSameOrigin: function(stylesheet) | 263 isSameOrigin: function(stylesheet) |
| 67 { | 264 { |
| 68 try | 265 try |
| 69 { | 266 { |
| 70 return new URL(stylesheet.href).origin == this.window.location.origin; | 267 return new URL(stylesheet.href).origin == this.window.location.origin; |
| 71 } | 268 } |
| 72 catch (e) | 269 catch (e) |
| 73 { | 270 { |
| 74 // Invalid URL, assume that it is first-party. | 271 // Invalid URL, assume that it is first-party. |
| 75 return true; | 272 return true; |
| 76 } | 273 } |
| 77 }, | 274 }, |
| 78 | 275 |
| 79 findSelectors: function(stylesheet, selectors, filters) | 276 findPseudoClassHasElements: function(node, stylesheets, elements, filters) |
| 80 { | 277 { |
| 81 // Explicitly ignore third-party stylesheets to ensure consistent behavior | 278 for (var i = 0; i < this.pseudoHasPatterns.length; i++) |
| 82 // between Firefox and Chrome. | 279 { |
| 83 if (!this.isSameOrigin(stylesheet)) | 280 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 } | 281 } |
| 112 }, | 282 }, |
| 113 | 283 |
| 114 addSelectors: function(stylesheets) | 284 addSelectors: function(stylesheets) |
| 115 { | 285 { |
| 116 var selectors = []; | 286 var selectors = []; |
| 117 var filters = []; | 287 var filters = []; |
| 118 for (var i = 0; i < stylesheets.length; i++) | 288 for (var i = 0; i < stylesheets.length; i++) |
| 119 this.findSelectors(stylesheets[i], selectors, filters); | 289 { |
| 290 // Explicitly ignore third-party stylesheets to ensure consistent behavior |
| 291 // between Firefox and Chrome. |
| 292 if (!this.isSameOrigin(stylesheets[i])) |
| 293 continue; |
| 294 findPropsSelectors(stylesheets[i], this.propSelPatterns, selectors, filter
s); |
| 295 } |
| 120 this.addSelectorsFunc(selectors, filters); | 296 this.addSelectorsFunc(selectors, filters); |
| 121 }, | 297 }, |
| 122 | 298 |
| 299 hideElements: function(stylesheets) |
| 300 { |
| 301 var elements = []; |
| 302 var filters = []; |
| 303 this.findPseudoClassHasElements(document, stylesheets, elements, filters); |
| 304 this.hideElementsFunc(elements, filters); |
| 305 }, |
| 306 |
| 123 onLoad: function(event) | 307 onLoad: function(event) |
| 124 { | 308 { |
| 125 var stylesheet = event.target.sheet; | 309 var stylesheet = event.target.sheet; |
| 126 if (stylesheet) | 310 if (stylesheet) |
| 127 this.addSelectors([stylesheet]); | 311 this.addSelectors([stylesheet]); |
| 312 this.hideElements([stylesheet]); |
| 128 }, | 313 }, |
| 129 | 314 |
| 130 apply: function() | 315 apply: function() |
| 131 { | 316 { |
| 132 this.getFiltersFunc(function(patterns) | 317 this.getFiltersFunc(function(patterns) |
| 133 { | 318 { |
| 134 this.patterns = []; | 319 this.propSelPatterns = []; |
| 320 this.pseudoHasPatterns = []; |
| 135 for (var i = 0; i < patterns.length; i++) | 321 for (var i = 0; i < patterns.length; i++) |
| 136 { | 322 { |
| 137 var pattern = patterns[i]; | 323 var pattern = patterns[i]; |
| 138 var match = propertySelectorRegExp.exec(pattern.selector); | 324 var parsed = parsePattern(pattern); |
| 139 if (!match) | 325 if (parsed == undefined) |
| 140 continue; | 326 continue; |
| 141 | 327 if (parsed.type == "props") |
| 142 var propertyExpression = match[2]; | 328 { |
| 143 var regexpString; | 329 this.propSelPatterns.push(parsed); |
| 144 if (propertyExpression.length >= 2 && propertyExpression[0] == "/" && | 330 } |
| 145 propertyExpression[propertyExpression.length - 1] == "/") | 331 else if (parsed.type == "has") |
| 146 regexpString = propertyExpression.slice(1, -1) | 332 { |
| 147 .replace("\\x7B ", "{").replace("\\x7D ", "}"); | 333 this.pseudoHasPatterns.push(parsed); |
| 148 else | 334 } |
| 149 regexpString = filterToRegExp(propertyExpression); | 335 } |
| 150 | 336 |
| 151 this.patterns.push({ | 337 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 { | 338 { |
| 161 var document = this.window.document; | 339 var document = this.window.document; |
| 162 this.addSelectors(document.styleSheets); | 340 this.addSelectors(document.styleSheets); |
| 341 this.hideElements(document.styleSheets); |
| 163 document.addEventListener("load", this.onLoad.bind(this), true); | 342 document.addEventListener("load", this.onLoad.bind(this), true); |
| 164 } | 343 } |
| 165 }.bind(this)); | 344 }.bind(this)); |
| 166 } | 345 } |
| 167 }; | 346 }; |
| OLD | NEW |