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 || |
| 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; |
| 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 stringifyStyle(style) |
| 206 { |
| 207 var styles = []; |
| 208 for (var i = 0; i < style.length; i++) |
| 209 { |
| 210 var property = style.item(i); |
| 211 var value = style.getPropertyValue(property); |
| 212 var priority = style.getPropertyPriority(property); |
| 213 styles.push(property + ": " + value + (priority ? " !" + priority : "") + ";
"); |
| 214 } |
| 215 styles.sort(); |
| 216 return styles.join(" "); |
| 217 } |
| 218 |
| 219 // matcher for the pseudo CSS4 class :has |
| 220 // For those browser that don't have it yet. |
| 221 function PseudoHasMatcher(selector) |
| 222 { |
| 223 this.hasSelector = selector; |
| 224 this.parsed = parsePattern({ selector: this.hasSelector }); |
| 225 if (this.parsed && this.parsed.type == "has") |
| 226 { |
| 227 console.log("unsupported :has() pattern", this.hasSelector); |
| 228 } |
| 229 } |
| 230 |
| 231 PseudoHasMatcher.prototype = { |
| 232 match: function(elem, stylesheets, firstOnly) |
| 233 { |
| 234 var matches = []; |
| 235 var selectors = []; |
| 236 |
| 237 if (this.parsed) |
| 238 { |
| 239 if (this.parsed.type == "has") |
| 240 return []; |
| 241 if (this.parsed.type == "props") |
| 242 { |
| 243 var filters = []; // don't need this |
| 244 findPropsSelectors(stylesheets[0], [this.parsed], selectors, filters); |
| 245 } |
| 246 } |
| 247 else |
| 248 { |
| 249 selectors = [this.hasSelector]; |
| 250 } |
| 251 |
| 252 // look up for all elements that match the :has(). |
| 253 var children = elem.children; |
| 254 for (var i = 0; i < children.length; i++) |
| 255 { |
| 256 for (var k = 0; k < selectors.length; k++) |
| 257 { |
| 258 try |
| 259 { |
| 260 var hasElem = elem.querySelector(selectors[k]); |
| 261 if (hasElem) |
| 262 { |
| 263 matches.push(hasElem); |
| 264 if (firstOnly) |
| 265 break; |
| 266 } |
| 267 } |
| 268 catch(e) |
| 269 { |
| 270 console.log("Exception with querySelector()", selectors[k]); |
| 271 } |
| 272 } |
| 273 } |
| 274 return matches; |
| 275 } |
| 276 }; |
| 277 |
| 278 function ElemHideEmulation(window, getFiltersFunc, addSelectorsFunc, hideElement
sFunc) |
45 { | 279 { |
46 this.window = window; | 280 this.window = window; |
47 this.getFiltersFunc = getFiltersFunc; | 281 this.getFiltersFunc = getFiltersFunc; |
48 this.addSelectorsFunc = addSelectorsFunc; | 282 this.addSelectorsFunc = addSelectorsFunc; |
| 283 this.hideElementsFunc = hideElementsFunc; |
49 } | 284 } |
50 | 285 |
51 ElemHideEmulation.prototype = { | 286 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 | 287 |
66 isSameOrigin: function(stylesheet) | 288 isSameOrigin: function(stylesheet) |
67 { | 289 { |
68 try | 290 try |
69 { | 291 { |
70 return new URL(stylesheet.href).origin == this.window.location.origin; | 292 return new URL(stylesheet.href).origin == this.window.location.origin; |
71 } | 293 } |
72 catch (e) | 294 catch (e) |
73 { | 295 { |
74 // Invalid URL, assume that it is first-party. | 296 // Invalid URL, assume that it is first-party. |
75 return true; | 297 return true; |
76 } | 298 } |
77 }, | 299 }, |
78 | 300 |
79 findSelectors: function(stylesheet, selectors, filters) | 301 findPseudoClassHasElements: function(stylesheets, elements, filters) |
80 { | 302 { |
81 // Explicitly ignore third-party stylesheets to ensure consistent behavior | 303 for (var i = 0; i < this.pseudoHasPatterns.length; i++) |
82 // between Firefox and Chrome. | 304 { |
83 if (!this.isSameOrigin(stylesheet)) | 305 var pattern = this.pseudoHasPatterns[i]; |
84 return; | 306 |
85 | 307 var haveEl = document.querySelectorAll(pattern.prefix); |
86 var rules = stylesheet.cssRules; | 308 for (var j = 0; j < haveEl.length; j++) |
87 if (!rules) | 309 { |
88 return; | 310 var matched = pattern.elementMatcher.match(haveEl[j], stylesheets, !patt
ern.suffix); |
89 | 311 if (matched.length == 0) |
90 for (var i = 0; i < rules.length; i++) | 312 continue; |
91 { | 313 |
92 var rule = rules[i]; | 314 if (pattern.suffix) |
93 if (rule.type != rule.STYLE_RULE) | 315 { |
94 continue; | 316 matched.forEach(function(e) |
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 { | 317 { |
105 var subSelector = subSelectors[k]; | 318 var subElements = selectChildren(e, pattern.suffix); |
106 selectors.push(pattern.prefix + subSelector + pattern.suffix); | 319 if (subElements) |
107 filters.push(pattern.text); | 320 { |
108 } | 321 for (var k = 0; k < subElements.length; k++) |
| 322 { |
| 323 elements.push(subElements[i]); |
| 324 filters.push(pattern.text); |
| 325 } |
| 326 } |
| 327 }); |
| 328 } |
| 329 else |
| 330 { |
| 331 elements.push(haveEl[j]); |
| 332 filters.push(pattern.text); |
109 } | 333 } |
110 } | 334 } |
111 } | 335 } |
112 }, | 336 }, |
113 | 337 |
114 addSelectors: function(stylesheets) | 338 addSelectors: function(stylesheets) |
115 { | 339 { |
116 var selectors = []; | 340 var selectors = []; |
117 var filters = []; | 341 var filters = []; |
118 for (var i = 0; i < stylesheets.length; i++) | 342 for (var i = 0; i < stylesheets.length; i++) |
119 this.findSelectors(stylesheets[i], selectors, filters); | 343 { |
| 344 // Explicitly ignore third-party stylesheets to ensure consistent behavior |
| 345 // between Firefox and Chrome. |
| 346 if (!this.isSameOrigin(stylesheets[i])) |
| 347 continue; |
| 348 findPropsSelectors(stylesheets[i], this.propSelPatterns, selectors, filter
s); |
| 349 } |
120 this.addSelectorsFunc(selectors, filters); | 350 this.addSelectorsFunc(selectors, filters); |
121 }, | 351 }, |
122 | 352 |
| 353 hideElements: function(stylesheets) |
| 354 { |
| 355 var elements = []; |
| 356 var filters = []; |
| 357 this.findPseudoClassHasElements(stylesheets, elements, filters); |
| 358 this.hideElementsFunc(elements, filters); |
| 359 }, |
| 360 |
123 onLoad: function(event) | 361 onLoad: function(event) |
124 { | 362 { |
125 var stylesheet = event.target.sheet; | 363 var stylesheet = event.target.sheet; |
126 if (stylesheet) | 364 if (stylesheet) |
127 this.addSelectors([stylesheet]); | 365 this.addSelectors([stylesheet]); |
| 366 this.hideElements([stylesheet]); |
128 }, | 367 }, |
129 | 368 |
130 apply: function() | 369 apply: function() |
131 { | 370 { |
132 this.getFiltersFunc(function(patterns) | 371 this.getFiltersFunc(function(patterns) |
133 { | 372 { |
134 this.patterns = []; | 373 this.propSelPatterns = []; |
| 374 this.pseudoHasPatterns = []; |
135 for (var i = 0; i < patterns.length; i++) | 375 for (var i = 0; i < patterns.length; i++) |
136 { | 376 { |
137 var pattern = patterns[i]; | 377 var pattern = patterns[i]; |
138 var match = propertySelectorRegExp.exec(pattern.selector); | 378 var parsed = parsePattern(pattern); |
139 if (!match) | 379 if (parsed == undefined) |
140 continue; | 380 continue; |
141 | 381 if (parsed.type == "props") |
142 var propertyExpression = match[2]; | 382 { |
143 var regexpString; | 383 this.propSelPatterns.push(parsed); |
144 if (propertyExpression.length >= 2 && propertyExpression[0] == "/" && | 384 } |
145 propertyExpression[propertyExpression.length - 1] == "/") | 385 else if (parsed.type == "has") |
146 regexpString = propertyExpression.slice(1, -1) | 386 { |
147 .replace("\\x7B ", "{").replace("\\x7D ", "}"); | 387 this.pseudoHasPatterns.push(parsed); |
148 else | 388 } |
149 regexpString = filterToRegExp(propertyExpression); | 389 } |
150 | 390 |
151 this.patterns.push({ | 391 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 { | 392 { |
161 var document = this.window.document; | 393 var document = this.window.document; |
162 this.addSelectors(document.styleSheets); | 394 this.addSelectors(document.styleSheets); |
| 395 this.hideElements(document.styleSheets); |
163 document.addEventListener("load", this.onLoad.bind(this), true); | 396 document.addEventListener("load", this.onLoad.bind(this), true); |
164 } | 397 } |
165 }.bind(this)); | 398 }.bind(this)); |
166 } | 399 } |
167 }; | 400 }; |
OLD | NEW |