| 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 |
| 3 | 3 |
| 4 var propertySelectorRegExp = /\[\-abp\-properties=(["'])([^"']+)\1\]/; | 4 var propertySelectorRegExp = /\[\-abp\-properties=(["'])([^"']+)\1\]/; |
| 5 var pseudoClassHasSelectorRegExp = /:has\((.*)\)/; | |
| 6 | |
| 7 // polyfill. We should deal with this better, but PhantomJS doesn't | |
| 8 // have matches. At least not in the version we use. | |
| 9 // Chrome 34, Firefox 34, Opera 21 and Safari 7.1 do have it. | |
| 10 if (!Element.prototype.matches) { | |
| 11 Element.prototype.matches = | |
| 12 Element.prototype.matchesSelector || | |
| 13 Element.prototype.mozMatchesSelector || | |
|
Sebastian Noack
2017/03/29 07:46:19
These fallbacks seem unnecessary. We only support
hub
2017/03/29 14:00:00
I need at least the webkit one for PhantomJS, othe
| |
| 14 Element.prototype.msMatchesSelector || | |
| 15 Element.prototype.oMatchesSelector || | |
| 16 Element.prototype.webkitMatchesSelector | |
| 17 } | |
| 18 | |
| 19 // return the index were the simple-selector ends | |
| 20 function findFirstSelector(selector) | |
| 21 { | |
| 22 var sepIndex = selector.indexOf(' '); | |
| 23 var nextIndex = selector.indexOf('>'); | |
| 24 if (nextIndex != -1) | |
| 25 sepIndex = sepIndex == -1 ? nextIndex : Math.min(sepIndex, nextIndex); | |
| 26 nextIndex = selector.indexOf('+'); | |
| 27 if (nextIndex != -1) | |
| 28 sepIndex = sepIndex == -1 ? nextIndex : Math.min(sepIndex, nextIndex); | |
| 29 nextIndex = selector.indexOf('~'); | |
| 30 if (nextIndex != -1) | |
| 31 sepIndex = sepIndex == -1 ? nextIndex : Math.min(sepIndex, nextIndex); | |
| 32 return sepIndex; | |
|
Sebastian Noack
2017/03/29 07:46:19
There is a lot of duplication above. How about usi
hub
2017/03/29 14:00:00
Done
| |
| 33 } | |
| 34 | |
| 35 function extractFirstSelector(selector) | |
| 36 { | |
| 37 var sepIndex = findFirstSelector(selector); | |
| 38 | |
| 39 if (sepIndex == -1) | |
| 40 return selector; | |
| 41 | |
| 42 return selector.substr(0, sepIndex); | |
| 43 } | |
| 5 | 44 |
| 6 function splitSelector(selector) | 45 function splitSelector(selector) |
| 7 { | 46 { |
| 8 if (selector.indexOf(",") == -1) | 47 if (selector.indexOf(",") == -1) |
| 9 return [selector]; | 48 return [selector]; |
| 10 | 49 |
| 11 var selectors = []; | 50 var selectors = []; |
| 12 var start = 0; | 51 var start = 0; |
| 13 var level = 0; | 52 var level = 0; |
| 14 var sep = ""; | 53 var sep = ""; |
| (...skipping 19 matching lines...) Expand all Loading... | |
| 34 selectors.push(selector.substring(start, i)); | 73 selectors.push(selector.substring(start, i)); |
| 35 start = i + 1; | 74 start = i + 1; |
| 36 } | 75 } |
| 37 } | 76 } |
| 38 } | 77 } |
| 39 | 78 |
| 40 selectors.push(selector.substring(start)); | 79 selectors.push(selector.substring(start)); |
| 41 return selectors; | 80 return selectors; |
| 42 } | 81 } |
| 43 | 82 |
| 44 function ElemHideEmulation(window, getFiltersFunc, addSelectorsFunc) | 83 function selectChildren(e, selector) |
| 84 { | |
| 85 var sel = selector; | |
| 86 // XXX we should have a more elegant way | |
| 87 // also startsWith isn't available in PhantomJS. | |
| 88 var combinator = sel.substr(0, 1); | |
| 89 var subElements; | |
| 90 var nextEl = e; | |
| 91 sel = sel.substr(1).trim(); | |
| 92 switch (combinator) | |
| 93 { | |
| 94 case ">": | |
| 95 subElements = e.querySelectorAll(sel); | |
| 96 break; | |
| 97 | |
| 98 case "+": | |
| 99 do | |
| 100 { | |
| 101 nextEl = nextEl.nextSibling; | |
| 102 } | |
| 103 while (nextEl && nextEl.nodeType != 1); | |
| 104 | |
| 105 var siblingSel = extractFirstSelector(sel); | |
| 106 var idx = findFirstSelector(sel); | |
| 107 var childSel = idx != -1 ? sel.substr(idx + 1).trim() : ""; | |
| 108 | |
| 109 if (nextEl && nextEl.matches(siblingSel)) | |
| 110 { | |
| 111 if (childSel != "") | |
| 112 subElements = selectChildren(nextEl, childSel); | |
| 113 else | |
| 114 subElements = [ nextEl ]; | |
| 115 } | |
| 116 break; | |
| 117 | |
| 118 case "~": | |
| 119 do | |
| 120 { | |
| 121 nextEl = nextEl.nextSibling; | |
| 122 if (nextEl && nextEl.nodeType == 1 && nextEl.matches(sel)) | |
| 123 { | |
| 124 subElements = nextEl.querySelectorAll(sel); | |
| 125 break; | |
| 126 } | |
| 127 } | |
| 128 while (nextEl); | |
| 129 | |
| 130 break; | |
| 131 } | |
| 132 return subElements; | |
| 133 } | |
| 134 | |
| 135 function parsePattern(pattern) | |
| 136 { | |
| 137 // we should catch the :has() pseudo class first. | |
| 138 var match = pseudoClassHasSelectorRegExp.exec(pattern.selector); | |
| 139 if (match) | |
| 140 { | |
| 141 return { | |
| 142 type: "has", | |
| 143 text: pattern.text, | |
| 144 elementMatcher: new PseudoHasMatcher(match[1]), | |
| 145 prefix: pattern.selector.substr(0, match.index).trim(), | |
| 146 suffix: pattern.selector.substr(match.index + match[0].length).trim() | |
| 147 }; | |
| 148 } | |
| 149 | |
| 150 match = propertySelectorRegExp.exec(pattern.selector); | |
| 151 if (match) | |
| 152 { | |
| 153 var regexpString; | |
| 154 var propertyExpression = match[2]; | |
| 155 if (propertyExpression.length >= 2 && propertyExpression[0] == "/" && | |
| 156 propertyExpression[propertyExpression.length - 1] == "/") | |
| 157 regexpString = propertyExpression.slice(1, -1) | |
| 158 .replace("\\x7B ", "{").replace("\\x7D ", "}"); | |
| 159 else | |
| 160 regexpString = filterToRegExp(propertyExpression); | |
| 161 return { | |
| 162 type: "props", | |
| 163 text: pattern.text, | |
| 164 regexp: new RegExp(regexpString, "i"), | |
| 165 prefix: pattern.selector.substr(0, match.index), | |
| 166 suffix: pattern.selector.substr(match.index + match[0].length) | |
| 167 }; | |
| 168 } | |
| 169 } | |
| 170 | |
| 171 function matchStyleProps(style, rule, pattern, selectors, filters) | |
| 172 { | |
| 173 if (pattern.regexp.test(style)) | |
| 174 { | |
| 175 var subSelectors = splitSelector(rule.selectorText); | |
| 176 for (var i = 0; i < subSelectors.length; i++) | |
| 177 { | |
| 178 var subSelector = subSelectors[i]; | |
| 179 selectors.push(pattern.prefix + subSelector + pattern.suffix); | |
| 180 filters.push(pattern.text); | |
| 181 } | |
| 182 } | |
| 183 } | |
| 184 | |
| 185 function findPropsSelectors(stylesheet, patterns, selectors, filters) | |
| 186 { | |
| 187 var rules = stylesheet.cssRules; | |
| 188 if (!rules) | |
| 189 return; | |
| 190 | |
| 191 for (var i = 0; i < rules.length; i++) | |
| 192 { | |
| 193 var rule = rules[i]; | |
| 194 if (rule.type != rule.STYLE_RULE) | |
| 195 continue; | |
| 196 | |
| 197 var style = stringifyStyle(rule.style); | |
| 198 for (var j = 0; j < patterns.length; j++) | |
| 199 { | |
| 200 matchStyleProps(style, rule, patterns[j], selectors, filters); | |
| 201 } | |
| 202 } | |
| 203 } | |
| 204 | |
| 205 function pseudoClassHasMatch(pattern, node, stylesheets, elements, filters) | |
| 206 { | |
| 207 var haveEl = pattern.prefix ? node.querySelectorAll(pattern.prefix) : [ node ] ; | |
| 208 for (var j = 0; j < haveEl.length; j++) | |
| 209 { | |
| 210 var matched = pattern.elementMatcher.match(haveEl[j], stylesheets, !pattern. suffix); | |
| 211 if (matched.length == 0) | |
| 212 continue; | |
| 213 | |
| 214 if (pattern.suffix) | |
| 215 { | |
| 216 matched.forEach(function(e) | |
| 217 { | |
| 218 var subElements = selectChildren(e, pattern.suffix); | |
| 219 if (subElements) | |
| 220 { | |
| 221 for (var k = 0; k < subElements.length; k++) | |
| 222 { | |
| 223 elements.push(subElements[k]); | |
| 224 filters.push(pattern.text); | |
| 225 } | |
| 226 } | |
| 227 }); | |
| 228 } | |
| 229 else | |
| 230 { | |
| 231 elements.push(haveEl[j]); | |
| 232 filters.push(pattern.text); | |
| 233 } | |
| 234 } | |
| 235 } | |
| 236 | |
| 237 function stringifyStyle(style) | |
| 238 { | |
| 239 var styles = []; | |
| 240 for (var i = 0; i < style.length; i++) | |
| 241 { | |
| 242 var property = style.item(i); | |
| 243 var value = style.getPropertyValue(property); | |
| 244 var priority = style.getPropertyPriority(property); | |
| 245 styles.push(property + ": " + value + (priority ? " !" + priority : "") + "; "); | |
| 246 } | |
| 247 styles.sort(); | |
| 248 return styles.join(" "); | |
| 249 } | |
| 250 | |
| 251 // matcher for the pseudo CSS4 class :has | |
| 252 // For those browser that don't have it yet. | |
| 253 function PseudoHasMatcher(selector) | |
| 254 { | |
| 255 this.hasSelector = selector; | |
| 256 this.parsed = parsePattern({ selector: this.hasSelector }); | |
| 257 } | |
| 258 | |
| 259 PseudoHasMatcher.prototype = { | |
| 260 match: function(elem, stylesheets, firstOnly) | |
| 261 { | |
| 262 var matches = []; | |
| 263 var selectors = []; | |
| 264 | |
| 265 if (this.parsed) | |
| 266 { | |
| 267 var filters = []; // don't need this | |
|
Sebastian Noack
2017/03/29 07:46:19
These are actually needed for the developer tools
hub
2017/03/29 14:00:00
At that level we might be processing fragments of
| |
| 268 if (this.parsed.type == "has") | |
| 269 { | |
| 270 pseudoClassHasMatch(this.parsed, elem, stylesheets, matches, filters) | |
| 271 return matches; | |
| 272 } | |
| 273 if (this.parsed.type == "props") | |
| 274 { | |
| 275 for (var i = 0; i < stylesheets.length; i++) | |
| 276 findPropsSelectors(stylesheets[i], [this.parsed], selectors, filters); | |
| 277 } | |
| 278 } | |
| 279 else | |
| 280 { | |
| 281 selectors = [this.hasSelector]; | |
| 282 } | |
| 283 | |
| 284 // look up for all elements that match the :has(). | |
| 285 for (var k = 0; k < selectors.length; k++) | |
| 286 { | |
| 287 try | |
| 288 { | |
| 289 var hasElem = elem.querySelector(selectors[k]); | |
| 290 if (hasElem) | |
| 291 { | |
| 292 matches.push(hasElem); | |
| 293 if (firstOnly) | |
| 294 break; | |
| 295 } | |
| 296 } | |
| 297 catch(e) | |
| 298 { | |
| 299 console.log("Exception with querySelector()", selectors[k]); | |
| 300 } | |
| 301 } | |
| 302 return matches; | |
| 303 } | |
| 304 }; | |
| 305 | |
| 306 function ElemHideEmulation(window, getFiltersFunc, addSelectorsFunc, hideElement sFunc) | |
| 45 { | 307 { |
| 46 this.window = window; | 308 this.window = window; |
| 47 this.getFiltersFunc = getFiltersFunc; | 309 this.getFiltersFunc = getFiltersFunc; |
| 48 this.addSelectorsFunc = addSelectorsFunc; | 310 this.addSelectorsFunc = addSelectorsFunc; |
| 311 this.hideElementsFunc = hideElementsFunc; | |
| 49 } | 312 } |
| 50 | 313 |
| 51 ElemHideEmulation.prototype = { | 314 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 | 315 |
| 66 isSameOrigin: function(stylesheet) | 316 isSameOrigin: function(stylesheet) |
| 67 { | 317 { |
| 68 try | 318 try |
| 69 { | 319 { |
| 70 return new URL(stylesheet.href).origin == this.window.location.origin; | 320 return new URL(stylesheet.href).origin == this.window.location.origin; |
| 71 } | 321 } |
| 72 catch (e) | 322 catch (e) |
| 73 { | 323 { |
| 74 // Invalid URL, assume that it is first-party. | 324 // Invalid URL, assume that it is first-party. |
| 75 return true; | 325 return true; |
| 76 } | 326 } |
| 77 }, | 327 }, |
| 78 | 328 |
| 79 findSelectors: function(stylesheet, selectors, filters) | 329 findPseudoClassHasElements: function(node, stylesheets, elements, filters) |
| 80 { | 330 { |
| 81 // Explicitly ignore third-party stylesheets to ensure consistent behavior | 331 for (var i = 0; i < this.pseudoHasPatterns.length; i++) |
| 82 // between Firefox and Chrome. | 332 { |
| 83 if (!this.isSameOrigin(stylesheet)) | 333 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 } | 334 } |
| 112 }, | 335 }, |
| 113 | 336 |
| 114 addSelectors: function(stylesheets) | 337 addSelectors: function(stylesheets) |
| 115 { | 338 { |
| 116 var selectors = []; | 339 var selectors = []; |
| 117 var filters = []; | 340 var filters = []; |
| 118 for (var i = 0; i < stylesheets.length; i++) | 341 for (var i = 0; i < stylesheets.length; i++) |
| 119 this.findSelectors(stylesheets[i], selectors, filters); | 342 { |
| 343 // Explicitly ignore third-party stylesheets to ensure consistent behavior | |
| 344 // between Firefox and Chrome. | |
| 345 if (!this.isSameOrigin(stylesheets[i])) | |
| 346 continue; | |
| 347 findPropsSelectors(stylesheets[i], this.propSelPatterns, selectors, filter s); | |
| 348 } | |
| 120 this.addSelectorsFunc(selectors, filters); | 349 this.addSelectorsFunc(selectors, filters); |
| 121 }, | 350 }, |
| 122 | 351 |
| 352 hideElements: function(stylesheets) | |
| 353 { | |
| 354 var elements = []; | |
| 355 var filters = []; | |
| 356 this.findPseudoClassHasElements(document, stylesheets, elements, filters); | |
| 357 this.hideElementsFunc(elements, filters); | |
| 358 }, | |
| 359 | |
| 123 onLoad: function(event) | 360 onLoad: function(event) |
| 124 { | 361 { |
| 125 var stylesheet = event.target.sheet; | 362 var stylesheet = event.target.sheet; |
| 126 if (stylesheet) | 363 if (stylesheet) |
| 127 this.addSelectors([stylesheet]); | 364 this.addSelectors([stylesheet]); |
| 365 this.hideElements([stylesheet]); | |
| 128 }, | 366 }, |
| 129 | 367 |
| 130 apply: function() | 368 apply: function() |
| 131 { | 369 { |
| 132 this.getFiltersFunc(function(patterns) | 370 this.getFiltersFunc(function(patterns) |
| 133 { | 371 { |
| 134 this.patterns = []; | 372 this.propSelPatterns = []; |
| 373 this.pseudoHasPatterns = []; | |
| 135 for (var i = 0; i < patterns.length; i++) | 374 for (var i = 0; i < patterns.length; i++) |
| 136 { | 375 { |
| 137 var pattern = patterns[i]; | 376 var pattern = patterns[i]; |
| 138 var match = propertySelectorRegExp.exec(pattern.selector); | 377 var parsed = parsePattern(pattern); |
| 139 if (!match) | 378 if (parsed == undefined) |
| 140 continue; | 379 continue; |
| 141 | 380 if (parsed.type == "props") |
| 142 var propertyExpression = match[2]; | 381 { |
| 143 var regexpString; | 382 this.propSelPatterns.push(parsed); |
| 144 if (propertyExpression.length >= 2 && propertyExpression[0] == "/" && | 383 } |
| 145 propertyExpression[propertyExpression.length - 1] == "/") | 384 else if (parsed.type == "has") |
| 146 regexpString = propertyExpression.slice(1, -1) | 385 { |
| 147 .replace("\\x7B ", "{").replace("\\x7D ", "}"); | 386 this.pseudoHasPatterns.push(parsed); |
| 148 else | 387 } |
| 149 regexpString = filterToRegExp(propertyExpression); | 388 } |
| 150 | 389 |
| 151 this.patterns.push({ | 390 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 { | 391 { |
| 161 var document = this.window.document; | 392 var document = this.window.document; |
| 162 this.addSelectors(document.styleSheets); | 393 this.addSelectors(document.styleSheets); |
| 394 this.hideElements(document.styleSheets); | |
| 163 document.addEventListener("load", this.onLoad.bind(this), true); | 395 document.addEventListener("load", this.onLoad.bind(this), true); |
| 164 } | 396 } |
| 165 }.bind(this)); | 397 }.bind(this)); |
| 166 } | 398 } |
| 167 }; | 399 }; |
| OLD | NEW |